// SPDX-License-Identifier: AGPL-3.0-or-later import {createRequire} from 'node:module'; import type { AppStoreAccount, AppStoreAccountCompareAndSwapRequest, AppStoreAccountImport, AppStoreAccountImportReport, AppStoreBinding, AppStoreClearStoreExceptRequest, AppStoreEntry, AppStoreEntryImport, AppStoreEntryImportReport, AppStoreInitialization, AppStoreKnownInstance, AppStoreOptions, AppStorePruneReport, AppStorePruneRequest, } from '@fluxer/app-store'; export const APP_STORE_ADDON_PACKAGE = '@fluxer/app-store'; type AppStoreBindingConstructor = new (options: AppStoreOptions) => AppStoreBinding; interface AppStoreEntryAddress { store: string; scope: string; key: string; } export interface AppStoreBoundary { readonly initialization: AppStoreInitialization; close: () => void; getMetadata: (request: {key: string}) => Promise; setMetadata: (request: {key: string; value: string}) => Promise; getAllAccounts: () => Promise>; getAccount: (request: {storageKey: string}) => Promise; upsertAccount: (request: AppStoreAccount) => Promise; compareAndSwapAccount: (request: AppStoreAccountCompareAndSwapRequest) => Promise; deleteAccount: (request: {storageKey: string}) => Promise; importAccounts: (request: AppStoreAccountImport) => Promise; getAllKnownInstances: () => Promise>; upsertKnownInstance: (request: AppStoreKnownInstance) => Promise; deleteKnownInstance: (request: {key: string}) => Promise; getEntries: (request: {store: string; scope: string}) => Promise>; getEntry: (request: AppStoreEntryAddress) => Promise; setEntry: (request: AppStoreEntry) => Promise; deleteEntry: (request: AppStoreEntryAddress) => Promise; clearScope: (request: {scope: string}) => Promise; clearStoreExcept: (request: AppStoreClearStoreExceptRequest) => Promise; importEntries: (request: AppStoreEntryImport) => Promise; prune: (request: AppStorePruneRequest) => Promise; } export type AppStoreBoundaryFactory = (options: AppStoreOptions) => AppStoreBoundary | null; type AppStoreNativeModule = { AppStore: AppStoreBindingConstructor | null; loadError: Error | null; }; const requireModule = createRequire(import.meta.url); const ADDON_LOAD_FAILURE_MAX_LENGTH = 300; let bindingCache: AppStoreBindingConstructor | null | undefined; let bindingLoadFailure: string | null = null; function describeAddonLoadFailure(error: unknown): string { if (error == null) { return 'the app store addon did not export a binding'; } const message = error instanceof Error ? error.message : String(error); const reason = /^reason=(.+)$/mu.exec(message)?.[1]?.trim(); const summary = (reason ?? message.split('\n')[0] ?? '').trim(); if (summary.length === 0) { return 'the app store addon failed to load'; } return summary.length > ADDON_LOAD_FAILURE_MAX_LENGTH ? `${summary.slice(0, ADDON_LOAD_FAILURE_MAX_LENGTH - 1)}\u2026` : summary; } export function loadAppStoreBinding(): AppStoreBindingConstructor | null { if (bindingCache !== undefined) return bindingCache; try { const native = requireModule(APP_STORE_ADDON_PACKAGE) as AppStoreNativeModule; bindingCache = native.AppStore ?? null; bindingLoadFailure = bindingCache == null ? describeAddonLoadFailure(native.loadError) : null; } catch (error) { bindingCache = null; bindingLoadFailure = describeAddonLoadFailure(error); } return bindingCache; } export function getAppStoreLoadFailure(): string | null { loadAppStoreBinding(); return bindingLoadFailure; } async function decode(result: Promise): Promise { return JSON.parse(await result) as T; } export function createAppStoreBoundary( options: AppStoreOptions, binding: AppStoreBindingConstructor | null = loadAppStoreBinding(), ): AppStoreBoundary | null { if (!binding) return null; const store = new binding(options); return { initialization: JSON.parse(store.initialization) as AppStoreInitialization, close: () => { store.close(); }, getMetadata: ({key}) => decode(store.getMetadata(key)), setMetadata: ({key, value}) => store.setMetadata(key, value), getAllAccounts: () => decode>(store.getAllAccounts()), getAccount: ({storageKey}) => decode(store.getAccount(storageKey)), upsertAccount: (request) => store.upsertAccount(JSON.stringify(request)), compareAndSwapAccount: (request) => store.compareAndSwapAccount(JSON.stringify(request)), deleteAccount: ({storageKey}) => store.deleteAccount(storageKey), importAccounts: (request) => decode(store.importAccounts(JSON.stringify(request))), getAllKnownInstances: () => decode>(store.getAllKnownInstances()), upsertKnownInstance: (request) => store.upsertKnownInstance(JSON.stringify(request)), deleteKnownInstance: ({key}) => store.deleteKnownInstance(key), getEntries: ({store: entryStore, scope}) => decode>(store.getEntries(entryStore, scope)), getEntry: ({store: entryStore, scope, key}) => decode(store.getEntry(entryStore, scope, key)), setEntry: (request) => store.setEntry(JSON.stringify(request)), deleteEntry: ({store: entryStore, scope, key}) => store.deleteEntry(entryStore, scope, key), clearScope: ({scope}) => store.clearScope(scope), clearStoreExcept: (request) => store.clearStoreExcept(JSON.stringify(request)), importEntries: (request) => decode(store.importEntries(JSON.stringify(request))), prune: (request) => decode(store.prune(JSON.stringify(request))), }; }