CI/CD / preconditions (pull_request) Successful in 1s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m2s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m38s
CI/CD / test_and_check_app (pull_request) Successful in 6m47s
CI/CD / Publish immutable image (pull_request) Skipped
137 lines
3.6 KiB
TypeScript
137 lines
3.6 KiB
TypeScript
import { computed, readonly, ref } from 'vue';
|
|
import { isNativeAppState, updateIsBusy } from '@/lib/native-app';
|
|
import type { NativeAction, NativeAppState } from '@/types/native-app';
|
|
|
|
const state = ref<NativeAppState | null>(null);
|
|
const pending = ref(false);
|
|
const error = ref<string | null>(null);
|
|
let initializing = false;
|
|
let refreshing: Promise<void> | null = null;
|
|
|
|
async function request(
|
|
action: NativeAction,
|
|
payload: Record<string, unknown> = {},
|
|
) {
|
|
if (typeof window === 'undefined' || window.kanbanNative?.version !== 1) {
|
|
throw new Error('bridgeUnavailable');
|
|
}
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
try {
|
|
return await Promise.race([
|
|
window.kanbanNative.request(action, payload),
|
|
new Promise<never>((_, reject) => {
|
|
timer = setTimeout(
|
|
() => reject(new Error('bridgeTimeout')),
|
|
11000,
|
|
);
|
|
}),
|
|
]);
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
async function refresh() {
|
|
if (refreshing) {
|
|
return refreshing;
|
|
}
|
|
refreshing = (async () => {
|
|
const result = await request('getState');
|
|
if (!isNativeAppState(result)) {
|
|
throw new Error('incompatibleVersion');
|
|
}
|
|
state.value = result;
|
|
})();
|
|
try {
|
|
await refreshing;
|
|
} finally {
|
|
refreshing = null;
|
|
}
|
|
}
|
|
|
|
export async function initializeNativeApp() {
|
|
if (
|
|
initializing ||
|
|
state.value ||
|
|
typeof window === 'undefined' ||
|
|
window.kanbanNative?.version !== 1
|
|
) {
|
|
return;
|
|
}
|
|
initializing = true;
|
|
try {
|
|
await refresh();
|
|
await request('settingsReady');
|
|
} catch {
|
|
state.value = null;
|
|
} finally {
|
|
initializing = false;
|
|
}
|
|
}
|
|
|
|
export function useNativeApp() {
|
|
const busy = computed(
|
|
() => pending.value || updateIsBusy(state.value?.updates),
|
|
);
|
|
async function act(
|
|
action: NativeAction,
|
|
payload: Record<string, unknown> = {},
|
|
) {
|
|
if (
|
|
pending.value ||
|
|
(updateIsBusy(state.value?.updates) && action !== 'openSettings')
|
|
) {
|
|
return false;
|
|
}
|
|
pending.value = true;
|
|
error.value = null;
|
|
try {
|
|
await request(action, payload);
|
|
if (action !== 'setWorkspace') {
|
|
await refresh();
|
|
}
|
|
return true;
|
|
} catch (cause) {
|
|
error.value =
|
|
cause instanceof Error ? cause.message : 'bridgeUnavailable';
|
|
return false;
|
|
} finally {
|
|
pending.value = false;
|
|
}
|
|
}
|
|
async function poll() {
|
|
try {
|
|
await refresh();
|
|
} catch (cause) {
|
|
error.value =
|
|
cause instanceof Error ? cause.message : 'bridgeUnavailable';
|
|
}
|
|
}
|
|
return { state: readonly(state), busy, error: readonly(error), act, poll };
|
|
}
|
|
|
|
export async function recoverNativeWorkspace() {
|
|
if (!state.value) {
|
|
return;
|
|
}
|
|
try {
|
|
await request('openSettings');
|
|
} catch {
|
|
/* Native recovery remains available from the app menu or toolbar. */
|
|
}
|
|
}
|
|
|
|
export function registerNativeRecovery() {
|
|
window.addEventListener('offline', () => {
|
|
void recoverNativeWorkspace();
|
|
});
|
|
document.addEventListener('inertia:exception', () => {
|
|
void recoverNativeWorkspace();
|
|
});
|
|
document.addEventListener('inertia:invalid', (event) => {
|
|
if (event.detail.response.status >= 400) {
|
|
void recoverNativeWorkspace();
|
|
}
|
|
});
|
|
}
|