@drakkar.software/starfish-client 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bindings/zustand.js +25 -1
- package/dist/bindings/zustand.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +25 -0
- package/dist/index.js.map +2 -2
- package/dist/sync.d.ts +6 -0
- package/package.json +2 -2
package/dist/bindings/zustand.js
CHANGED
|
@@ -417,6 +417,12 @@ var ValidationError = class extends Error {
|
|
|
417
417
|
};
|
|
418
418
|
|
|
419
419
|
// src/sync.ts
|
|
420
|
+
var AbortError = class extends Error {
|
|
421
|
+
constructor() {
|
|
422
|
+
super("SyncManager was aborted");
|
|
423
|
+
this.name = "AbortError";
|
|
424
|
+
}
|
|
425
|
+
};
|
|
420
426
|
var SyncManager = class {
|
|
421
427
|
client;
|
|
422
428
|
pullPath;
|
|
@@ -431,6 +437,7 @@ var SyncManager = class {
|
|
|
431
437
|
lastHash = null;
|
|
432
438
|
lastCheckpoint = 0;
|
|
433
439
|
localData = {};
|
|
440
|
+
aborted = false;
|
|
434
441
|
constructor(options) {
|
|
435
442
|
this.client = options.client;
|
|
436
443
|
this.pullPath = options.pullPath;
|
|
@@ -443,6 +450,12 @@ var SyncManager = class {
|
|
|
443
450
|
this.validate = options.validate;
|
|
444
451
|
this.encryptor = options.encryptor ?? (options.encryptionSecret && options.encryptionSalt ? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo) : null);
|
|
445
452
|
}
|
|
453
|
+
abort() {
|
|
454
|
+
this.aborted = true;
|
|
455
|
+
}
|
|
456
|
+
get isAborted() {
|
|
457
|
+
return this.aborted;
|
|
458
|
+
}
|
|
446
459
|
getData() {
|
|
447
460
|
return { ...this.localData };
|
|
448
461
|
}
|
|
@@ -457,12 +470,15 @@ var SyncManager = class {
|
|
|
457
470
|
return this.lastCheckpoint;
|
|
458
471
|
}
|
|
459
472
|
async pull() {
|
|
473
|
+
if (this.aborted) throw new AbortError();
|
|
460
474
|
this.logger?.pullStart(this.loggerName);
|
|
461
475
|
const start = performance.now();
|
|
462
476
|
try {
|
|
463
477
|
const result = await this.client.pull(this.pullPath, this.lastCheckpoint);
|
|
478
|
+
if (this.aborted) throw new AbortError();
|
|
464
479
|
if (this.encryptor) {
|
|
465
480
|
const decrypted = await this.encryptor.decrypt(result.data);
|
|
481
|
+
if (this.aborted) throw new AbortError();
|
|
466
482
|
this.localData = decrypted;
|
|
467
483
|
result.data = decrypted;
|
|
468
484
|
} else if (this.lastCheckpoint > 0) {
|
|
@@ -481,6 +497,7 @@ var SyncManager = class {
|
|
|
481
497
|
}
|
|
482
498
|
}
|
|
483
499
|
async push(data) {
|
|
500
|
+
if (this.aborted) throw new AbortError();
|
|
484
501
|
if (this.validate) {
|
|
485
502
|
const result = this.validate(data);
|
|
486
503
|
if (result !== true) throw new ValidationError(result);
|
|
@@ -492,19 +509,23 @@ var SyncManager = class {
|
|
|
492
509
|
while (attempt <= this.maxRetries) {
|
|
493
510
|
try {
|
|
494
511
|
const payload = this.encryptor ? await this.encryptor.encrypt(pendingData) : pendingData;
|
|
512
|
+
if (this.aborted) throw new AbortError();
|
|
495
513
|
const sig = this.signData ? await this.signData(stableStringify(payload)) : void 0;
|
|
514
|
+
if (this.aborted) throw new AbortError();
|
|
496
515
|
const result = await this.client.push(
|
|
497
516
|
this.pushPath,
|
|
498
517
|
payload,
|
|
499
518
|
this.lastHash,
|
|
500
519
|
sig
|
|
501
520
|
);
|
|
521
|
+
if (this.aborted) throw new AbortError();
|
|
502
522
|
this.lastHash = result.hash;
|
|
503
523
|
this.lastCheckpoint = result.timestamp;
|
|
504
524
|
this.localData = pendingData;
|
|
505
525
|
this.logger?.pushSuccess(this.loggerName, Math.round(performance.now() - start));
|
|
506
526
|
return result;
|
|
507
527
|
} catch (err) {
|
|
528
|
+
if (err instanceof AbortError) throw err;
|
|
508
529
|
if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {
|
|
509
530
|
this.logger?.pushError(this.loggerName, err instanceof Error ? err.message : String(err));
|
|
510
531
|
throw err;
|
|
@@ -512,11 +533,14 @@ var SyncManager = class {
|
|
|
512
533
|
this.logger?.conflict(this.loggerName, attempt + 1);
|
|
513
534
|
try {
|
|
514
535
|
const remote = await this.client.pull(this.pullPath);
|
|
536
|
+
if (this.aborted) throw new AbortError();
|
|
515
537
|
const remoteData = this.encryptor ? await this.encryptor.decrypt(remote.data) : remote.data;
|
|
538
|
+
if (this.aborted) throw new AbortError();
|
|
516
539
|
this.lastHash = remote.hash;
|
|
517
540
|
this.lastCheckpoint = remote.timestamp;
|
|
518
541
|
pendingData = this.onConflict(pendingData, remoteData);
|
|
519
542
|
} catch (resolveErr) {
|
|
543
|
+
if (resolveErr instanceof AbortError) throw resolveErr;
|
|
520
544
|
const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr);
|
|
521
545
|
this.logger?.pushError(this.loggerName, `Conflict resolution failed (attempt ${attempt + 1}): ${msg}`);
|
|
522
546
|
throw resolveErr;
|
|
@@ -664,7 +688,7 @@ function createStarfishStore(options) {
|
|
|
664
688
|
hash: state.hash
|
|
665
689
|
}),
|
|
666
690
|
onRehydrateStorage: () => (state) => {
|
|
667
|
-
if (state?.hash) syncManager.setHash(state.hash);
|
|
691
|
+
if (state?.hash && syncManager.getHash() === null) syncManager.setHash(state.hash);
|
|
668
692
|
}
|
|
669
693
|
});
|
|
670
694
|
const withSelector = subscribeWithSelector(withPersist);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/bindings/zustand.ts", "../../../../../node_modules/.pnpm/zustand@5.0.11_@types+react@19.2.14_immer@11.1.4_react@19.2.4_use-sync-external-store@1.6.0_react@19.2.4_/node_modules/zustand/esm/middleware.mjs", "../../src/types.ts", "../../src/client.ts", "../../src/sync.ts", "../../src/crypto.ts", "../../src/validate.ts", "../../src/broadcast.ts"],
|
|
4
|
-
"sourcesContent": ["import { createStore, type StoreApi } from \"zustand/vanilla\"\nimport { useStore } from \"zustand\"\nimport {\n persist,\n subscribeWithSelector,\n createJSONStorage,\n type StateStorage,\n} from \"zustand/middleware\"\nimport type { DevtoolsOptions } from \"zustand/middleware\"\nimport { useEffect, useRef, useState, useCallback } from \"react\"\nimport { StarfishClient } from \"../client.js\"\nimport { SyncManager } from \"../sync.js\"\nimport { setupCrossTabSync, type BroadcastableStore } from \"../broadcast.js\"\nimport type { AuthProvider, ConflictResolver } from \"../types.js\"\nimport type { SyncLogger } from \"../logger.js\"\nimport type { Validator } from \"../validate.js\"\n\nexport interface StarfishState {\n data: Record<string, unknown>\n syncing: boolean\n online: boolean\n dirty: boolean\n error: string | null\n /** Last-known server hash, persisted alongside `data`/`dirty`. Restored into the bound SyncManager on hydration. */\n hash: string | null\n}\n\nexport interface StarfishActions {\n pull: () => Promise<void>\n set: (modifier: (current: Record<string, unknown>) => Record<string, unknown>) => void\n /** Update data without marking dirty or triggering flush. Use for restoring pulled data into the store. */\n restore: (data: Record<string, unknown>) => void\n flush: () => Promise<void>\n setOnline: (online: boolean) => void\n}\n\nexport type StarfishStore = StarfishState & StarfishActions\n\nexport interface CreateStarfishStoreOptions {\n /** Unique name used as the persistence key (prefixed with `starfish-`) */\n name: string\n syncManager: SyncManager\n /** Pass `false` to disable persistence. Defaults to `localStorage` in browsers. */\n storage?: StateStorage | false\n /**\n * Wrap the store with Redux DevTools. Import `devtools` from `'zustand/middleware'`\n * and pass it directly \u2014 this keeps the import in your code, preventing\n * `import.meta.env` from being bundled in Metro/Hermes environments.\n *\n * @example\n * import { devtools } from 'zustand/middleware'\n * createStarfishStore({ devtools: (fn) => devtools(fn, { name: 'my-app' }) })\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n devtools?: (storeCreator: any) => any\n /** Pass `produce` from `immer` to enable draft-based mutations in `set()`. */\n produce?: <T>(base: T, recipe: (draft: T) => T | void) => T\n /**\n * Called when remote data arrives via `pull()` \u2014 **not** called for local `set()` writes.\n *\n * Use this to restore domain stores after a pull without worrying about feedback loops.\n * The callback fires **after** the Starfish store state is updated, so the store already\n * reflects the new data when this runs.\n *\n * Replaces the manual `isRestoring` flag pattern:\n * ```ts\n * createStarfishStore({\n * name: \"app\",\n * syncManager,\n * onRemoteUpdate: (data) => {\n * taskStore.setState({ tasks: data.tasks as Task[] })\n * settingsStore.setState({ settings: data.settings as Settings })\n * },\n * })\n * ```\n */\n onRemoteUpdate?: (data: Record<string, unknown>) => void\n}\n\n// Re-export DevtoolsOptions for convenience\nexport type { DevtoolsOptions }\n\nexport function createStarfishStore(\n options: CreateStarfishStoreOptions,\n): StoreApi<StarfishStore> {\n const { name, syncManager, storage } = options\n\n type NamedSet = (partial: Partial<StarfishStore>, replace?: boolean, action?: string) => void\n\n const storeCreator = (\n rawSet: StoreApi<StarfishStore>[\"setState\"],\n get: StoreApi<StarfishStore>[\"getState\"],\n ): StarfishStore => {\n const set = rawSet as NamedSet\n return {\n data: {},\n syncing: false,\n online: true,\n dirty: false,\n error: null,\n hash: null,\n\n pull: async () => {\n set({ syncing: true, error: null }, false, \"pull/start\")\n try {\n await syncManager.pull()\n const newData = syncManager.getData()\n set({ data: newData, syncing: false, hash: syncManager.getHash() }, false, \"pull/success\")\n // Fire after state update so domain stores can read the updated Starfish state if needed.\n // Calling set() inside onRemoteUpdate does NOT re-enter pull(), so no feedback loop.\n options.onRemoteUpdate?.(newData)\n } catch (err) {\n set({ syncing: false, error: err instanceof Error ? err.message : String(err) }, false, \"pull/error\")\n }\n },\n\n set: (modifier) => {\n try {\n const next = options.produce\n ? options.produce(get().data, modifier as (draft: Record<string, unknown>) => Record<string, unknown> | void)\n : modifier(get().data)\n set({ data: next, dirty: true, error: null }, false, \"set\")\n if (get().online) get().flush().catch(() => {})\n } catch (err) {\n set({ error: err instanceof Error ? err.message : String(err) }, false, \"set/error\")\n }\n },\n\n restore: (data) => {\n set({ data }, false, \"restore\")\n },\n\n flush: async () => {\n if (get().syncing || !get().dirty) return\n set({ syncing: true, error: null }, false, \"flush/start\")\n try {\n await syncManager.push(get().data)\n set({ data: syncManager.getData(), syncing: false, dirty: false, hash: syncManager.getHash() }, false, \"flush/success\")\n } catch (err) {\n set({ syncing: false, error: err instanceof Error ? err.message : String(err) }, false, \"flush/error\")\n }\n },\n\n setOnline: (online) => {\n set({ online }, false, \"setOnline\")\n if (online && get().dirty) get().flush().catch(() => {})\n },\n }}\n\n const withPersist = storage === false\n ? storeCreator\n : persist(storeCreator, {\n name: `starfish-${name}`,\n storage: storage ? createJSONStorage(() => storage) : undefined,\n partialize: (state) => ({\n data: state.data,\n dirty: state.dirty,\n hash: state.hash,\n }),\n onRehydrateStorage: () => (state) => {\n if (state?.hash) syncManager.setHash(state.hash)\n },\n })\n\n const withSelector = subscribeWithSelector(withPersist)\n\n return createStore<StarfishStore>()(\n options.devtools ? options.devtools(withSelector) : withSelector,\n )\n}\n\n// \u2500\u2500 React hooks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Derived sync status for UI display. */\nexport type SyncStatus = \"synced\" | \"syncing\" | \"pending\" | \"error\" | \"offline\"\n\n/** Derive a single sync status from store state. */\nexport function deriveSyncStatus(state: StarfishState): SyncStatus {\n if (!state.online) return \"offline\"\n if (state.error) return \"error\"\n if (state.syncing) return \"syncing\"\n if (state.dirty) return \"pending\"\n return \"synced\"\n}\n\n/**\n * Aggregate multiple sync statuses into a single worst-case status.\n * Priority (worst first): error > syncing > pending > offline > synced.\n */\nexport function aggregateSyncStatus(statuses: SyncStatus[]): SyncStatus {\n if (statuses.includes(\"error\")) return \"error\"\n if (statuses.includes(\"syncing\")) return \"syncing\"\n if (statuses.includes(\"pending\")) return \"pending\"\n if (statuses.includes(\"offline\")) return \"offline\"\n return \"synced\"\n}\n\n/** Use the full Starfish store state and actions. */\nexport function useStarfish(store: StoreApi<StarfishStore>): StarfishStore {\n return useStore(store)\n}\n\n/** Use only the synced data, with an optional selector for fine-grained subscriptions. */\nexport function useStarfishData<T = Record<string, unknown>>(\n store: StoreApi<StarfishStore>,\n selector?: (data: Record<string, unknown>) => T,\n): T {\n return useStore(store, (state) =>\n selector ? selector(state.data) : (state.data as unknown as T),\n )\n}\n\n/** Use the derived sync status (synced | syncing | pending | error | offline). */\nexport function useSyncStatus(store: StoreApi<StarfishStore>): SyncStatus {\n return useStore(store, deriveSyncStatus)\n}\n\n/**\n * Subscribe to sync status changes outside of React.\n *\n * Framework-agnostic \u2014 works in React Native, Node.js, or anywhere hooks are unavailable.\n * The callback is invoked immediately with the current status and then on every change.\n *\n * ```ts\n * const unsub = subscribeSyncStatus(store, (status) => {\n * updateStatusBar(status)\n * })\n *\n * // Later, to stop listening:\n * unsub()\n * ```\n */\nexport function subscribeSyncStatus(\n store: StoreApi<StarfishStore>,\n callback: (status: SyncStatus) => void,\n): () => void {\n let prev = deriveSyncStatus(store.getState())\n callback(prev)\n return store.subscribe((state) => {\n const next = deriveSyncStatus(state)\n if (next !== prev) {\n prev = next\n callback(next)\n }\n })\n}\n\n/** Sets up cross-tab sync for a Starfish store. Cleans up on unmount. */\nexport function useCrossTabSync(\n store: StoreApi<StarfishStore>,\n name: string,\n): void {\n useEffect(() => {\n return setupCrossTabSync(store as unknown as BroadcastableStore, name)\n }, [store, name])\n}\n\n/** Binds browser online/offline events to the store's setOnline action. Cleans up on unmount. */\nexport function useConnectivity(store: StoreApi<StarfishStore>): void {\n useEffect(() => {\n const handleOnline = () => store.getState().setOnline(true)\n const handleOffline = () => store.getState().setOnline(false)\n\n window.addEventListener(\"online\", handleOnline)\n window.addEventListener(\"offline\", handleOffline)\n\n return () => {\n window.removeEventListener(\"online\", handleOnline)\n window.removeEventListener(\"offline\", handleOffline)\n }\n }, [store])\n}\n\n/** Returns a human-readable \"last synced\" label that updates every 5 seconds. */\nexport function useLastSynced(store: StoreApi<StarfishStore>): string {\n const lastSyncedAt = useRef<number | null>(null)\n const [label, setLabel] = useState(\"Never synced\")\n\n const computeLabel = useCallback(() => {\n if (lastSyncedAt.current === null) return \"Never synced\"\n const seconds = Math.floor((Date.now() - lastSyncedAt.current) / 1000)\n if (seconds < 10) return \"Just now\"\n if (seconds < 60) return `${seconds}s ago`\n return `${Math.floor(seconds / 60)}m ago`\n }, [])\n\n // Track sync completion\n useEffect(() => {\n let prevSyncing = store.getState().syncing\n const unsub = store.subscribe((state) => {\n if (prevSyncing && !state.syncing && !state.error) {\n lastSyncedAt.current = Date.now()\n setLabel(computeLabel())\n }\n prevSyncing = state.syncing\n })\n return unsub\n }, [store, computeLabel])\n\n // Update label periodically\n useEffect(() => {\n const timer = setInterval(() => {\n setLabel(computeLabel())\n }, 5000)\n return () => clearInterval(timer)\n }, [computeLabel])\n\n return label\n}\n\n// \u2500\u2500 SyncInitializer hook \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SyncInitConfig {\n serverUrl: string\n auth?: AuthProvider\n pullPath: string\n pushPath: string\n encryptionSecret?: string\n encryptionSalt?: string\n onConflict?: ConflictResolver\n /** Called when pulled data arrives. Use to restore domain stores. */\n onData?: (data: Record<string, unknown>) => void\n storeName?: string\n storage?: StateStorage | false\n fetch?: typeof globalThis.fetch\n logger?: SyncLogger\n validate?: Validator\n}\n\n/**\n * React hook that manages the full Starfish sync lifecycle.\n *\n * Creates StarfishClient \u2192 SyncManager \u2192 Zustand store, pulls on mount,\n * calls `onData` when remote data arrives, and tears down on unmount or\n * config change.\n *\n * Pass `null` to disable sync (returns `null`).\n */\nexport function useSyncInit(config: SyncInitConfig | null): StoreApi<StarfishStore> | null {\n const [store, setStore] = useState<StoreApi<StarfishStore> | null>(null)\n const onDataRef = useRef(config?.onData)\n onDataRef.current = config?.onData\n\n useEffect(() => {\n if (!config) {\n setStore(null)\n return\n }\n\n const client = new StarfishClient({\n baseUrl: config.serverUrl,\n auth: config.auth,\n fetch: config.fetch,\n })\n\n const syncManager = new SyncManager({\n client,\n pullPath: config.pullPath,\n pushPath: config.pushPath,\n encryptionSecret: config.encryptionSecret,\n encryptionSalt: config.encryptionSalt,\n onConflict: config.onConflict,\n logger: config.logger,\n validate: config.validate,\n })\n\n const newStore = createStarfishStore({\n name: config.storeName ?? \"sync\",\n syncManager,\n storage: config.storage,\n // onRemoteUpdate fires only for pull() results, never for local set() writes \u2014\n // so no isRestoring flag is needed.\n onRemoteUpdate: (data) => {\n try {\n onDataRef.current?.(data)\n } catch (err) {\n newStore.setState({\n error: `onData failed: ${err instanceof Error ? err.message : String(err)}`,\n })\n }\n },\n })\n\n setStore(newStore)\n\n // Initial pull \u2014 errors are stored in state.error by the pull() action\n newStore.getState().pull().catch(() => {})\n\n return () => {\n setStore(null)\n }\n // Intentionally depend on serializable config values, not the object reference\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n config?.serverUrl,\n config?.pullPath,\n config?.pushPath,\n config?.encryptionSecret,\n config?.encryptionSalt,\n config?.storeName,\n ])\n\n return store\n}\n", "const reduxImpl = (reducer, initial) => (set, _get, api) => {\n api.dispatch = (action) => {\n set((state) => reducer(state, action), false, action);\n return action;\n };\n api.dispatchFromDevtools = true;\n return { dispatch: (...args) => api.dispatch(...args), ...initial };\n};\nconst redux = reduxImpl;\n\nconst shouldDispatchFromDevtools = (api) => !!api.dispatchFromDevtools && typeof api.dispatch === \"function\";\nconst trackedConnections = /* @__PURE__ */ new Map();\nconst getTrackedConnectionState = (name) => {\n const api = trackedConnections.get(name);\n if (!api) return {};\n return Object.fromEntries(\n Object.entries(api.stores).map(([key, api2]) => [key, api2.getState()])\n );\n};\nconst extractConnectionInformation = (store, extensionConnector, options) => {\n if (store === void 0) {\n return {\n type: \"untracked\",\n connection: extensionConnector.connect(options)\n };\n }\n const existingConnection = trackedConnections.get(options.name);\n if (existingConnection) {\n return { type: \"tracked\", store, ...existingConnection };\n }\n const newConnection = {\n connection: extensionConnector.connect(options),\n stores: {}\n };\n trackedConnections.set(options.name, newConnection);\n return { type: \"tracked\", store, ...newConnection };\n};\nconst removeStoreFromTrackedConnections = (name, store) => {\n if (store === void 0) return;\n const connectionInfo = trackedConnections.get(name);\n if (!connectionInfo) return;\n delete connectionInfo.stores[store];\n if (Object.keys(connectionInfo.stores).length === 0) {\n trackedConnections.delete(name);\n }\n};\nconst findCallerName = (stack) => {\n var _a, _b;\n if (!stack) return void 0;\n const traceLines = stack.split(\"\\n\");\n const apiSetStateLineIndex = traceLines.findIndex(\n (traceLine) => traceLine.includes(\"api.setState\")\n );\n if (apiSetStateLineIndex < 0) return void 0;\n const callerLine = ((_a = traceLines[apiSetStateLineIndex + 1]) == null ? void 0 : _a.trim()) || \"\";\n return (_b = /.+ (.+) .+/.exec(callerLine)) == null ? void 0 : _b[1];\n};\nconst devtoolsImpl = (fn, devtoolsOptions = {}) => (set, get, api) => {\n const { enabled, anonymousActionType, store, ...options } = devtoolsOptions;\n let extensionConnector;\n try {\n extensionConnector = (enabled != null ? enabled : (import.meta.env ? import.meta.env.MODE : void 0) !== \"production\") && window.__REDUX_DEVTOOLS_EXTENSION__;\n } catch (e) {\n }\n if (!extensionConnector) {\n return fn(set, get, api);\n }\n const { connection, ...connectionInformation } = extractConnectionInformation(store, extensionConnector, options);\n let isRecording = true;\n api.setState = ((state, replace, nameOrAction) => {\n const r = set(state, replace);\n if (!isRecording) return r;\n const action = nameOrAction === void 0 ? {\n type: anonymousActionType || findCallerName(new Error().stack) || \"anonymous\"\n } : typeof nameOrAction === \"string\" ? { type: nameOrAction } : nameOrAction;\n if (store === void 0) {\n connection == null ? void 0 : connection.send(action, get());\n return r;\n }\n connection == null ? void 0 : connection.send(\n {\n ...action,\n type: `${store}/${action.type}`\n },\n {\n ...getTrackedConnectionState(options.name),\n [store]: api.getState()\n }\n );\n return r;\n });\n api.devtools = {\n cleanup: () => {\n if (connection && typeof connection.unsubscribe === \"function\") {\n connection.unsubscribe();\n }\n removeStoreFromTrackedConnections(options.name, store);\n }\n };\n const setStateFromDevtools = (...a) => {\n const originalIsRecording = isRecording;\n isRecording = false;\n set(...a);\n isRecording = originalIsRecording;\n };\n const initialState = fn(api.setState, get, api);\n if (connectionInformation.type === \"untracked\") {\n connection == null ? void 0 : connection.init(initialState);\n } else {\n connectionInformation.stores[connectionInformation.store] = api;\n connection == null ? void 0 : connection.init(\n Object.fromEntries(\n Object.entries(connectionInformation.stores).map(([key, store2]) => [\n key,\n key === connectionInformation.store ? initialState : store2.getState()\n ])\n )\n );\n }\n if (shouldDispatchFromDevtools(api)) {\n let didWarnAboutReservedActionType = false;\n const originalDispatch = api.dispatch;\n api.dispatch = (...args) => {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\" && args[0].type === \"__setState\" && !didWarnAboutReservedActionType) {\n console.warn(\n '[zustand devtools middleware] \"__setState\" action type is reserved to set state from the devtools. Avoid using it.'\n );\n didWarnAboutReservedActionType = true;\n }\n originalDispatch(...args);\n };\n }\n connection.subscribe((message) => {\n var _a;\n switch (message.type) {\n case \"ACTION\":\n if (typeof message.payload !== \"string\") {\n console.error(\n \"[zustand devtools middleware] Unsupported action format\"\n );\n return;\n }\n return parseJsonThen(\n message.payload,\n (action) => {\n if (action.type === \"__setState\") {\n if (store === void 0) {\n setStateFromDevtools(action.state);\n return;\n }\n if (Object.keys(action.state).length !== 1) {\n console.error(\n `\n [zustand devtools middleware] Unsupported __setState action format.\n When using 'store' option in devtools(), the 'state' should have only one key, which is a value of 'store' that was passed in devtools(),\n and value of this only key should be a state object. Example: { \"type\": \"__setState\", \"state\": { \"abc123Store\": { \"foo\": \"bar\" } } }\n `\n );\n }\n const stateFromDevtools = action.state[store];\n if (stateFromDevtools === void 0 || stateFromDevtools === null) {\n return;\n }\n if (JSON.stringify(api.getState()) !== JSON.stringify(stateFromDevtools)) {\n setStateFromDevtools(stateFromDevtools);\n }\n return;\n }\n if (shouldDispatchFromDevtools(api)) {\n api.dispatch(action);\n }\n }\n );\n case \"DISPATCH\":\n switch (message.payload.type) {\n case \"RESET\":\n setStateFromDevtools(initialState);\n if (store === void 0) {\n return connection == null ? void 0 : connection.init(api.getState());\n }\n return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n case \"COMMIT\":\n if (store === void 0) {\n connection == null ? void 0 : connection.init(api.getState());\n return;\n }\n return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n case \"ROLLBACK\":\n return parseJsonThen(message.state, (state) => {\n if (store === void 0) {\n setStateFromDevtools(state);\n connection == null ? void 0 : connection.init(api.getState());\n return;\n }\n setStateFromDevtools(state[store]);\n connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n });\n case \"JUMP_TO_STATE\":\n case \"JUMP_TO_ACTION\":\n return parseJsonThen(message.state, (state) => {\n if (store === void 0) {\n setStateFromDevtools(state);\n return;\n }\n if (JSON.stringify(api.getState()) !== JSON.stringify(state[store])) {\n setStateFromDevtools(state[store]);\n }\n });\n case \"IMPORT_STATE\": {\n const { nextLiftedState } = message.payload;\n const lastComputedState = (_a = nextLiftedState.computedStates.slice(-1)[0]) == null ? void 0 : _a.state;\n if (!lastComputedState) return;\n if (store === void 0) {\n setStateFromDevtools(lastComputedState);\n } else {\n setStateFromDevtools(lastComputedState[store]);\n }\n connection == null ? void 0 : connection.send(\n null,\n // FIXME no-any\n nextLiftedState\n );\n return;\n }\n case \"PAUSE_RECORDING\":\n return isRecording = !isRecording;\n }\n return;\n }\n });\n return initialState;\n};\nconst devtools = devtoolsImpl;\nconst parseJsonThen = (stringified, fn) => {\n let parsed;\n try {\n parsed = JSON.parse(stringified);\n } catch (e) {\n console.error(\n \"[zustand devtools middleware] Could not parse the received json\",\n e\n );\n }\n if (parsed !== void 0) fn(parsed);\n};\n\nconst subscribeWithSelectorImpl = (fn) => (set, get, api) => {\n const origSubscribe = api.subscribe;\n api.subscribe = ((selector, optListener, options) => {\n let listener = selector;\n if (optListener) {\n const equalityFn = (options == null ? void 0 : options.equalityFn) || Object.is;\n let currentSlice = selector(api.getState());\n listener = (state) => {\n const nextSlice = selector(state);\n if (!equalityFn(currentSlice, nextSlice)) {\n const previousSlice = currentSlice;\n optListener(currentSlice = nextSlice, previousSlice);\n }\n };\n if (options == null ? void 0 : options.fireImmediately) {\n optListener(currentSlice, currentSlice);\n }\n }\n return origSubscribe(listener);\n });\n const initialState = fn(set, get, api);\n return initialState;\n};\nconst subscribeWithSelector = subscribeWithSelectorImpl;\n\nfunction combine(initialState, create) {\n return (...args) => Object.assign({}, initialState, create(...args));\n}\n\nfunction createJSONStorage(getStorage, options) {\n let storage;\n try {\n storage = getStorage();\n } catch (e) {\n return;\n }\n const persistStorage = {\n getItem: (name) => {\n var _a;\n const parse = (str2) => {\n if (str2 === null) {\n return null;\n }\n return JSON.parse(str2, options == null ? void 0 : options.reviver);\n };\n const str = (_a = storage.getItem(name)) != null ? _a : null;\n if (str instanceof Promise) {\n return str.then(parse);\n }\n return parse(str);\n },\n setItem: (name, newValue) => storage.setItem(name, JSON.stringify(newValue, options == null ? void 0 : options.replacer)),\n removeItem: (name) => storage.removeItem(name)\n };\n return persistStorage;\n}\nconst toThenable = (fn) => (input) => {\n try {\n const result = fn(input);\n if (result instanceof Promise) {\n return result;\n }\n return {\n then(onFulfilled) {\n return toThenable(onFulfilled)(result);\n },\n catch(_onRejected) {\n return this;\n }\n };\n } catch (e) {\n return {\n then(_onFulfilled) {\n return this;\n },\n catch(onRejected) {\n return toThenable(onRejected)(e);\n }\n };\n }\n};\nconst persistImpl = (config, baseOptions) => (set, get, api) => {\n let options = {\n storage: createJSONStorage(() => window.localStorage),\n partialize: (state) => state,\n version: 0,\n merge: (persistedState, currentState) => ({\n ...currentState,\n ...persistedState\n }),\n ...baseOptions\n };\n let hasHydrated = false;\n let hydrationVersion = 0;\n const hydrationListeners = /* @__PURE__ */ new Set();\n const finishHydrationListeners = /* @__PURE__ */ new Set();\n let storage = options.storage;\n if (!storage) {\n return config(\n (...args) => {\n console.warn(\n `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`\n );\n set(...args);\n },\n get,\n api\n );\n }\n const setItem = () => {\n const state = options.partialize({ ...get() });\n return storage.setItem(options.name, {\n state,\n version: options.version\n });\n };\n const savedSetState = api.setState;\n api.setState = (state, replace) => {\n savedSetState(state, replace);\n return setItem();\n };\n const configResult = config(\n (...args) => {\n set(...args);\n return setItem();\n },\n get,\n api\n );\n api.getInitialState = () => configResult;\n let stateFromStorage;\n const hydrate = () => {\n var _a, _b;\n if (!storage) return;\n const currentVersion = ++hydrationVersion;\n hasHydrated = false;\n hydrationListeners.forEach((cb) => {\n var _a2;\n return cb((_a2 = get()) != null ? _a2 : configResult);\n });\n const postRehydrationCallback = ((_b = options.onRehydrateStorage) == null ? void 0 : _b.call(options, (_a = get()) != null ? _a : configResult)) || void 0;\n return toThenable(storage.getItem.bind(storage))(options.name).then((deserializedStorageValue) => {\n if (deserializedStorageValue) {\n if (typeof deserializedStorageValue.version === \"number\" && deserializedStorageValue.version !== options.version) {\n if (options.migrate) {\n const migration = options.migrate(\n deserializedStorageValue.state,\n deserializedStorageValue.version\n );\n if (migration instanceof Promise) {\n return migration.then((result) => [true, result]);\n }\n return [true, migration];\n }\n console.error(\n `State loaded from storage couldn't be migrated since no migrate function was provided`\n );\n } else {\n return [false, deserializedStorageValue.state];\n }\n }\n return [false, void 0];\n }).then((migrationResult) => {\n var _a2;\n if (currentVersion !== hydrationVersion) {\n return;\n }\n const [migrated, migratedState] = migrationResult;\n stateFromStorage = options.merge(\n migratedState,\n (_a2 = get()) != null ? _a2 : configResult\n );\n set(stateFromStorage, true);\n if (migrated) {\n return setItem();\n }\n }).then(() => {\n if (currentVersion !== hydrationVersion) {\n return;\n }\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0);\n stateFromStorage = get();\n hasHydrated = true;\n finishHydrationListeners.forEach((cb) => cb(stateFromStorage));\n }).catch((e) => {\n if (currentVersion !== hydrationVersion) {\n return;\n }\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e);\n });\n };\n api.persist = {\n setOptions: (newOptions) => {\n options = {\n ...options,\n ...newOptions\n };\n if (newOptions.storage) {\n storage = newOptions.storage;\n }\n },\n clearStorage: () => {\n storage == null ? void 0 : storage.removeItem(options.name);\n },\n getOptions: () => options,\n rehydrate: () => hydrate(),\n hasHydrated: () => hasHydrated,\n onHydrate: (cb) => {\n hydrationListeners.add(cb);\n return () => {\n hydrationListeners.delete(cb);\n };\n },\n onFinishHydration: (cb) => {\n finishHydrationListeners.add(cb);\n return () => {\n finishHydrationListeners.delete(cb);\n };\n }\n };\n if (!options.skipHydration) {\n hydrate();\n }\n return stateFromStorage || configResult;\n};\nconst persist = persistImpl;\n\nfunction ssrSafe(config, isSSR = typeof window === \"undefined\") {\n return (set, get, api) => {\n if (!isSSR) {\n return config(set, get, api);\n }\n const ssrSet = () => {\n throw new Error(\"Cannot set state of Zustand store in SSR\");\n };\n api.setState = ssrSet;\n return config(ssrSet, get, api);\n };\n}\n\nexport { combine, createJSONStorage, devtools, persist, redux, subscribeWithSelector, ssrSafe as unstable_ssrSafe };\n", "/** Push conflict error (HTTP 409). */\nexport class ConflictError extends Error {\n constructor() {\n super(\"hash_mismatch\")\n this.name = \"ConflictError\"\n }\n}\n\n/** HTTP error from the Starfish server. */\nexport class StarfishHttpError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string\n ) {\n super(`HTTP ${status}: ${body}`)\n this.name = \"StarfishHttpError\"\n }\n}\n\n/**\n * Auth provider: returns headers to include in requests.\n * Called for every authenticated request (pull and push).\n */\nexport type AuthProvider = (req: {\n method: string\n path: string\n body: string | null\n}) => Record<string, string> | Promise<Record<string, string>>\n\n/** Options for creating a StarfishClient. */\nexport interface StarfishClientOptions {\n /** Base URL of the Starfish server (e.g. \"https://api.example.com/v1\"). */\n baseUrl: string\n /** Auth provider that returns headers for authenticated requests. Optional for public-read collections. */\n auth?: AuthProvider\n /** Optional fetch implementation (defaults to global fetch). */\n fetch?: typeof fetch\n}\n\n/** Conflict resolver: given local and remote data, return merged result. */\nexport type ConflictResolver = (\n local: Record<string, unknown>,\n remote: Record<string, unknown>\n) => Record<string, unknown>\n", "import type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\nimport type {\n StarfishClientOptions,\n AuthProvider,\n} from \"./types.js\"\nimport { ConflictError, StarfishHttpError } from \"./types.js\"\n\nconst APPEND_DEFAULT_FIELD = \"items\"\n\n/** Result of pulling a binary blob from the server. */\nexport interface BlobPullResult {\n data: ArrayBuffer\n /** Content hash from the ETag header. Null if the server didn't include an ETag. */\n hash: string | null\n contentType: string\n}\n\n/** Result of pushing a binary blob to the server. */\nexport interface BlobPushResult {\n hash: string\n}\n\n/** Options for append-only pull \u2014 extracts a single array field from the response. */\nexport interface AppendPullOptions {\n /** Array field name in `data`. Defaults to `\"items\"`. */\n appendField?: string\n /** Only return items appended after this timestamp (ms). Sent as `?checkpoint=`. */\n since?: number\n /** Return only the last K items (applied after `since` filter). Sent as `?last=`. */\n last?: number\n}\n\n/**\n * Low-level HTTP client for the Starfish sync protocol.\n * Handles auth headers and response parsing.\n */\nexport class StarfishClient {\n private readonly baseUrl: string\n private readonly auth?: AuthProvider\n private readonly fetch: typeof globalThis.fetch\n\n constructor(options: StarfishClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\")\n this.auth = options.auth\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n }\n\n /** Pull synced data from the server. Returns the raw `PullResult`. */\n async pull(path: string, checkpoint?: number): Promise<PullResult>\n /** Pull an append-only collection. Extracts and returns `data[appendField]` as `T[]`. */\n async pull<T = unknown>(path: string, options: AppendPullOptions): Promise<T[]>\n async pull<T = unknown>(\n path: string,\n checkpointOrOptions?: number | AppendPullOptions,\n ): Promise<PullResult | T[]> {\n let url = `${this.baseUrl}${path}`\n let appendField: string | undefined\n\n if (typeof checkpointOrOptions === \"number\") {\n if (checkpointOrOptions) url += `?checkpoint=${checkpointOrOptions}`\n } else if (checkpointOrOptions != null) {\n appendField = checkpointOrOptions.appendField ?? APPEND_DEFAULT_FIELD\n const params = new URLSearchParams()\n if (checkpointOrOptions.since != null) {\n if (checkpointOrOptions.since < 0) throw new Error(\"since must be non-negative\")\n params.set(\"checkpoint\", String(checkpointOrOptions.since))\n }\n if (checkpointOrOptions.last != null) {\n if (checkpointOrOptions.last < 0) throw new Error(\"last must be non-negative\")\n params.set(\"last\", String(checkpointOrOptions.last))\n }\n if (params.size > 0) url += `?${params.toString()}`\n }\n\n const authHeaders = this.auth\n ? await this.auth({ method: \"GET\", path, body: null })\n : {}\n\n const res = await this.fetch(url, {\n method: \"GET\",\n headers: { Accept: \"application/json\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n\n const result = await res.json() as PullResult\n if (appendField !== undefined) {\n const list = (result.data as Record<string, unknown> | null)?.[appendField]\n return (Array.isArray(list) ? list : []) as T[]\n }\n return result\n }\n\n /**\n * Push synced data to the server.\n * @param path - The push endpoint path (e.g. \"/push/users/abc/settings\")\n * @param data - The full document data to push\n * @param baseHash - Hash of the document this push is based on (null for first push)\n * @param authorSignature - Optional author signature for provenance\n * @throws {ConflictError} if the server detects a hash mismatch (409)\n */\n async push(\n path: string,\n data: Record<string, unknown>,\n baseHash: string | null,\n authorSignature?: string\n ): Promise<PushSuccess> {\n const body = JSON.stringify({\n data,\n baseHash,\n ...(authorSignature && { authorSignature }),\n })\n\n const authHeaders = this.auth\n ? await this.auth({ method: \"POST\", path, body })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...authHeaders,\n },\n body,\n })\n\n if (res.status === 409) {\n throw new ConflictError()\n }\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<PushSuccess>\n }\n\n /**\n * Pull binary data from a blob collection.\n * Returns raw bytes with the content hash from the ETag header.\n */\n async pullBlob(path: string): Promise<BlobPullResult> {\n const authHeaders = this.auth\n ? await this.auth({ method: \"GET\", path, body: null })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"GET\",\n headers: { Accept: \"*/*\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n\n const etag = res.headers.get(\"ETag\")?.replace(/\"/g, \"\") ?? null\n const contentType = res.headers.get(\"Content-Type\") ?? \"application/octet-stream\"\n const data = await res.arrayBuffer()\n\n return { data, hash: etag, contentType }\n }\n\n /**\n * Push binary data to a blob collection.\n * Binary collections use last-write-wins (no conflict detection).\n */\n async pushBlob(\n path: string,\n data: ArrayBuffer | Uint8Array | Blob,\n contentType: string,\n ): Promise<BlobPushResult> {\n const authHeaders = this.auth\n ? await this.auth({ method: \"POST\", path, body: null })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": contentType,\n Accept: \"application/json\",\n ...authHeaders,\n },\n body: data as BodyInit,\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<BlobPushResult>\n }\n}\n", "import type { PullResult } from \"@drakkar.software/starfish-protocol\"\nimport { deepMerge, stableStringify } from \"@drakkar.software/starfish-protocol\"\nimport type { ConflictResolver } from \"./types.js\"\nimport { ConflictError } from \"./types.js\"\nimport { StarfishClient } from \"./client.js\"\nimport type { Encryptor } from \"./crypto.js\"\nimport { createEncryptor } from \"./crypto.js\"\nimport type { SyncLogger } from \"./logger.js\"\nimport type { Validator } from \"./validate.js\"\nimport { ValidationError } from \"./validate.js\"\n\n\nexport interface SyncManagerOptions {\n client: StarfishClient\n pullPath: string\n pushPath: string\n /** Custom conflict resolver. Defaults to remote-wins deep merge. Arrays are atomic. */\n onConflict?: ConflictResolver\n /** Max conflict retry attempts (default: 3). */\n maxRetries?: number\n encryptionSecret?: string\n encryptionSalt?: string\n encryptionInfo?: string\n /**\n * Pre-created Encryptor. Use this with `createGroupEncryptor` for group encryption.\n * Takes precedence over `encryptionSecret` / `encryptionSalt` if both are provided.\n */\n encryptor?: Encryptor\n signData?: (data: string) => Promise<string>\n /** Structured logger for sync events. */\n logger?: SyncLogger\n /** Name passed to logger methods (default: derived from pullPath). */\n loggerName?: string\n /** Validate data before push. Throws ValidationError on failure. */\n validate?: Validator\n}\n\nexport class SyncManager {\n private readonly client: StarfishClient\n private readonly pullPath: string\n private readonly pushPath: string\n private readonly onConflict: ConflictResolver\n private readonly maxRetries: number\n private readonly encryptor: Encryptor | null\n private readonly signData?: (data: string) => Promise<string>\n private readonly logger?: SyncLogger\n private readonly loggerName: string\n private readonly validate?: Validator\n\n private lastHash: string | null = null\n private lastCheckpoint: number = 0\n private localData: Record<string, unknown> = {}\n\n constructor(options: SyncManagerOptions) {\n this.client = options.client\n this.pullPath = options.pullPath\n this.pushPath = options.pushPath\n this.onConflict = options.onConflict ?? deepMerge\n this.maxRetries = options.maxRetries ?? 3\n this.signData = options.signData\n this.logger = options.logger\n this.loggerName = options.loggerName ?? options.pullPath.split(\"/\").filter(Boolean).pop() ?? options.pullPath\n this.validate = options.validate\n this.encryptor =\n options.encryptor ??\n (options.encryptionSecret && options.encryptionSalt\n ? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo)\n : null)\n }\n\n getData(): Record<string, unknown> {\n return { ...this.localData }\n }\n\n getHash(): string | null {\n return this.lastHash\n }\n\n /** Set the last-known server hash. Used by persistence layers to restore state across restarts. */\n setHash(hash: string | null): void {\n this.lastHash = hash\n }\n\n getCheckpoint(): number {\n return this.lastCheckpoint\n }\n\n async pull(): Promise<PullResult> {\n this.logger?.pullStart(this.loggerName)\n const start = performance.now()\n try {\n const result = await this.client.pull(this.pullPath, this.lastCheckpoint)\n\n if (this.encryptor) {\n const decrypted = await this.encryptor.decrypt(result.data)\n this.localData = decrypted\n result.data = decrypted\n } else if (this.lastCheckpoint > 0) {\n this.localData = deepMerge(this.localData, result.data)\n result.data = this.localData\n } else {\n this.localData = result.data\n }\n\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n }\n\n async push(data: Record<string, unknown>): Promise<{ hash: string; timestamp: number }> {\n if (this.validate) {\n const result = this.validate(data)\n if (result !== true) throw new ValidationError(result)\n }\n this.logger?.pushStart(this.loggerName)\n const start = performance.now()\n let attempt = 0\n let pendingData = data\n\n while (attempt <= this.maxRetries) {\n try {\n const payload = this.encryptor\n ? await this.encryptor.encrypt(pendingData)\n : pendingData\n\n const sig = this.signData\n ? await this.signData(stableStringify(payload))\n : undefined\n\n const result = await this.client.push(\n this.pushPath,\n payload,\n this.lastHash,\n sig\n )\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.localData = pendingData\n this.logger?.pushSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {\n this.logger?.pushError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n this.logger?.conflict(this.loggerName, attempt + 1)\n try {\n const remote = await this.client.pull(this.pullPath)\n const remoteData = this.encryptor\n ? await this.encryptor.decrypt(remote.data)\n : remote.data\n this.lastHash = remote.hash\n this.lastCheckpoint = remote.timestamp\n pendingData = this.onConflict(pendingData, remoteData)\n } catch (resolveErr) {\n const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr)\n this.logger?.pushError(this.loggerName, `Conflict resolution failed (attempt ${attempt + 1}): ${msg}`)\n throw resolveErr\n }\n await new Promise<void>(resolve => setTimeout(resolve, Math.min(100 * Math.pow(2, attempt), 2000) + Math.random() * 100))\n attempt++\n }\n }\n throw new ConflictError()\n }\n\n async update(\n modifier: (current: Record<string, unknown>) => Record<string, unknown>\n ): Promise<{ hash: string; timestamp: number }> {\n await this.pull()\n const updated = modifier(this.localData)\n return this.push(updated)\n }\n}\n", "import { getCrypto, getBase64, IV_BYTES, ENCRYPTED_KEY, deriveKey } from \"@drakkar.software/starfish-protocol\"\n\nconst ALGO = \"AES-GCM\"\n\nexport { ENCRYPTED_KEY }\n\n/** Encrypt/decrypt interface for client-side E2E encryption. */\nexport interface Encryptor {\n encrypt(data: Record<string, unknown>): Promise<Record<string, unknown>>\n decrypt(wrapper: Record<string, unknown>): Promise<Record<string, unknown>>\n}\n\n/**\n * Creates an Encryptor that uses AES-256-GCM with HKDF-derived keys.\n */\nexport function createEncryptor(secret: string, salt: string, info: string = \"starfish-e2e\"): Encryptor {\n if (!secret) throw new Error(\"encryptionSecret must not be empty\")\n if (!salt) throw new Error(\"encryptionSalt must not be empty\")\n const keyPromise = deriveKey(secret, salt, info)\n\n return {\n async encrypt(data: Record<string, unknown>): Promise<Record<string, unknown>> {\n const key = await keyPromise\n const c = getCrypto()\n const b64 = getBase64()\n const plaintext = new TextEncoder().encode(JSON.stringify(data))\n const iv = c.getRandomValues(new Uint8Array(IV_BYTES))\n const ciphertext = await c.subtle.encrypt({ name: ALGO, iv }, key, plaintext)\n\n const combined = new Uint8Array(iv.length + ciphertext.byteLength)\n combined.set(iv)\n combined.set(new Uint8Array(ciphertext), iv.length)\n\n return { [ENCRYPTED_KEY]: b64.encode(combined) }\n },\n\n async decrypt(wrapper: Record<string, unknown>): Promise<Record<string, unknown>> {\n const encoded = wrapper[ENCRYPTED_KEY]\n if (typeof encoded !== \"string\") {\n throw new Error(\"Expected encrypted data but received unencrypted document\")\n }\n\n const key = await keyPromise\n const c = getCrypto()\n const b64 = getBase64()\n const combined = b64.decode(encoded)\n if (combined.length < IV_BYTES) {\n throw new Error(\"Encrypted data is too short\")\n }\n const iv = combined.slice(0, IV_BYTES)\n const ciphertext = combined.slice(IV_BYTES)\n try {\n const plaintext = await c.subtle.decrypt({ name: ALGO, iv }, key, ciphertext)\n return JSON.parse(new TextDecoder().decode(plaintext))\n } catch (err) {\n throw new Error(\"Decryption failed: data may be tampered or key is incorrect\", { cause: err })\n }\n },\n }\n}\n", "/** Validation result: true if valid, or an array of error messages. */\nexport type ValidationResult = true | string[]\n\n/** A function that validates data before push. */\nexport type Validator = (data: Record<string, unknown>) => ValidationResult\n\n/** Error thrown when pre-push validation fails. */\nexport class ValidationError extends Error {\n constructor(public readonly errors: string[]) {\n super(`Validation failed: ${errors.join(\"; \")}`)\n this.name = \"ValidationError\"\n }\n}\n\n/**\n * Creates a validator from a JSON Schema object.\n * Requires an Ajv-compatible validate function.\n *\n * @example\n * ```ts\n * import Ajv from \"ajv\"\n * const ajv = new Ajv()\n * const validator = createSchemaValidator(ajv, mySchema)\n * ```\n */\nexport function createSchemaValidator(\n ajv: { compile: (schema: object) => { (data: unknown): boolean; errors?: unknown }; errorsText: (errors?: unknown) => string },\n schema: object,\n): Validator {\n const validate = ajv.compile(schema)\n return (data) => {\n if (validate(data)) return true\n return [ajv.errorsText(validate.errors)]\n }\n}\n", "/** Minimal store interface for cross-tab sync. Works with both Zustand and Legend bindings. */\nexport interface BroadcastableStore {\n getState(): { data: Record<string, unknown>; dirty: boolean }\n setState(partial: { data: Record<string, unknown>; dirty: boolean }): void\n subscribe(listener: (state: { data: Record<string, unknown>; dirty: boolean }, prev: { data: Record<string, unknown>; dirty: boolean }) => void): () => void\n}\n\ninterface BroadcastPayload {\n data: Record<string, unknown>\n dirty: boolean\n}\n\n/**\n * Syncs a Starfish store across browser tabs using BroadcastChannel.\n * Works with any store that has getState/setState/subscribe (Zustand, Legend adapters, etc.).\n * Returns a cleanup function that closes the channel.\n */\nexport function setupBroadcastSync(\n store: BroadcastableStore,\n name: string,\n): () => void {\n const channel = new BroadcastChannel(`starfish-${name}`)\n let lastReceivedData: Record<string, unknown> | null = null\n\n channel.onmessage = (event: MessageEvent<unknown>) => {\n const payload = event.data as BroadcastPayload | undefined\n if (!payload || typeof payload !== \"object\" || !payload.data || typeof payload.data !== \"object\") return\n lastReceivedData = payload.data\n store.setState({ data: payload.data, dirty: !!payload.dirty })\n }\n\n const unsub = store.subscribe((state, prev) => {\n if (state.data === lastReceivedData) return\n if (state.data !== prev.data || state.dirty !== prev.dirty) {\n try {\n channel.postMessage({ data: state.data, dirty: state.dirty } satisfies BroadcastPayload)\n } catch { /* non-serializable data \u2014 skip broadcast */ }\n }\n })\n\n return () => {\n unsub()\n channel.close()\n }\n}\n\n/**\n * Syncs a Starfish store across browser tabs using storage events.\n * Fallback for environments without BroadcastChannel.\n * Returns a cleanup function.\n */\nexport function setupStorageFallback(\n store: BroadcastableStore,\n name: string,\n): () => void {\n const storageKey = `starfish-broadcast-${name}`\n let lastReceivedData: Record<string, unknown> | null = null\n\n const onStorage = (e: StorageEvent) => {\n if (e.key !== storageKey || !e.newValue) return\n let payload: BroadcastPayload\n try {\n payload = JSON.parse(e.newValue)\n } catch {\n return\n }\n if (!payload || typeof payload !== \"object\" || !payload.data || typeof payload.data !== \"object\") return\n lastReceivedData = payload.data\n store.setState({ data: payload.data, dirty: !!payload.dirty })\n }\n\n globalThis.addEventListener(\"storage\", onStorage)\n\n const unsub = store.subscribe((state, prev) => {\n if (state.data === lastReceivedData) return\n if (state.data !== prev.data || state.dirty !== prev.dirty) {\n try {\n localStorage.setItem(\n storageKey,\n JSON.stringify({ data: state.data, dirty: state.dirty } satisfies BroadcastPayload),\n )\n } catch { /* quota exceeded or non-serializable \u2014 skip */ }\n }\n })\n\n return () => {\n unsub()\n globalThis.removeEventListener(\"storage\", onStorage)\n }\n}\n\n/**\n * Auto-detects the best cross-tab sync mechanism and sets it up.\n * Uses BroadcastChannel when available, falls back to storage events.\n * Returns a cleanup function.\n */\nexport function setupCrossTabSync(\n store: BroadcastableStore,\n name: string,\n): () => void {\n if (typeof BroadcastChannel !== \"undefined\") {\n return setupBroadcastSync(store, name)\n }\n if (typeof globalThis.addEventListener === \"function\" && typeof localStorage !== \"undefined\") {\n return setupStorageFallback(store, name)\n }\n return () => {}\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { createStore, type StoreApi } from \"zustand/vanilla\"\nimport { useStore } from \"zustand\"\nimport {\n persist,\n subscribeWithSelector,\n createJSONStorage,\n type StateStorage,\n} from \"zustand/middleware\"\nimport type { DevtoolsOptions } from \"zustand/middleware\"\nimport { useEffect, useRef, useState, useCallback } from \"react\"\nimport { StarfishClient } from \"../client.js\"\nimport { SyncManager } from \"../sync.js\"\nimport { setupCrossTabSync, type BroadcastableStore } from \"../broadcast.js\"\nimport type { AuthProvider, ConflictResolver } from \"../types.js\"\nimport type { SyncLogger } from \"../logger.js\"\nimport type { Validator } from \"../validate.js\"\n\nexport interface StarfishState {\n data: Record<string, unknown>\n syncing: boolean\n online: boolean\n dirty: boolean\n error: string | null\n /** Last-known server hash, persisted alongside `data`/`dirty`. Restored into the bound SyncManager on hydration. */\n hash: string | null\n}\n\nexport interface StarfishActions {\n pull: () => Promise<void>\n set: (modifier: (current: Record<string, unknown>) => Record<string, unknown>) => void\n /** Update data without marking dirty or triggering flush. Use for restoring pulled data into the store. */\n restore: (data: Record<string, unknown>) => void\n flush: () => Promise<void>\n setOnline: (online: boolean) => void\n}\n\nexport type StarfishStore = StarfishState & StarfishActions\n\nexport interface CreateStarfishStoreOptions {\n /** Unique name used as the persistence key (prefixed with `starfish-`) */\n name: string\n syncManager: SyncManager\n /** Pass `false` to disable persistence. Defaults to `localStorage` in browsers. */\n storage?: StateStorage | false\n /**\n * Wrap the store with Redux DevTools. Import `devtools` from `'zustand/middleware'`\n * and pass it directly \u2014 this keeps the import in your code, preventing\n * `import.meta.env` from being bundled in Metro/Hermes environments.\n *\n * @example\n * import { devtools } from 'zustand/middleware'\n * createStarfishStore({ devtools: (fn) => devtools(fn, { name: 'my-app' }) })\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n devtools?: (storeCreator: any) => any\n /** Pass `produce` from `immer` to enable draft-based mutations in `set()`. */\n produce?: <T>(base: T, recipe: (draft: T) => T | void) => T\n /**\n * Called when remote data arrives via `pull()` \u2014 **not** called for local `set()` writes.\n *\n * Use this to restore domain stores after a pull without worrying about feedback loops.\n * The callback fires **after** the Starfish store state is updated, so the store already\n * reflects the new data when this runs.\n *\n * Replaces the manual `isRestoring` flag pattern:\n * ```ts\n * createStarfishStore({\n * name: \"app\",\n * syncManager,\n * onRemoteUpdate: (data) => {\n * taskStore.setState({ tasks: data.tasks as Task[] })\n * settingsStore.setState({ settings: data.settings as Settings })\n * },\n * })\n * ```\n */\n onRemoteUpdate?: (data: Record<string, unknown>) => void\n}\n\n// Re-export DevtoolsOptions for convenience\nexport type { DevtoolsOptions }\n\nexport function createStarfishStore(\n options: CreateStarfishStoreOptions,\n): StoreApi<StarfishStore> {\n const { name, syncManager, storage } = options\n\n type NamedSet = (partial: Partial<StarfishStore>, replace?: boolean, action?: string) => void\n\n const storeCreator = (\n rawSet: StoreApi<StarfishStore>[\"setState\"],\n get: StoreApi<StarfishStore>[\"getState\"],\n ): StarfishStore => {\n const set = rawSet as NamedSet\n return {\n data: {},\n syncing: false,\n online: true,\n dirty: false,\n error: null,\n hash: null,\n\n pull: async () => {\n set({ syncing: true, error: null }, false, \"pull/start\")\n try {\n await syncManager.pull()\n const newData = syncManager.getData()\n set({ data: newData, syncing: false, hash: syncManager.getHash() }, false, \"pull/success\")\n // Fire after state update so domain stores can read the updated Starfish state if needed.\n // Calling set() inside onRemoteUpdate does NOT re-enter pull(), so no feedback loop.\n options.onRemoteUpdate?.(newData)\n } catch (err) {\n set({ syncing: false, error: err instanceof Error ? err.message : String(err) }, false, \"pull/error\")\n }\n },\n\n set: (modifier) => {\n try {\n const next = options.produce\n ? options.produce(get().data, modifier as (draft: Record<string, unknown>) => Record<string, unknown> | void)\n : modifier(get().data)\n set({ data: next, dirty: true, error: null }, false, \"set\")\n if (get().online) get().flush().catch(() => {})\n } catch (err) {\n set({ error: err instanceof Error ? err.message : String(err) }, false, \"set/error\")\n }\n },\n\n restore: (data) => {\n set({ data }, false, \"restore\")\n },\n\n flush: async () => {\n if (get().syncing || !get().dirty) return\n set({ syncing: true, error: null }, false, \"flush/start\")\n try {\n await syncManager.push(get().data)\n set({ data: syncManager.getData(), syncing: false, dirty: false, hash: syncManager.getHash() }, false, \"flush/success\")\n } catch (err) {\n set({ syncing: false, error: err instanceof Error ? err.message : String(err) }, false, \"flush/error\")\n }\n },\n\n setOnline: (online) => {\n set({ online }, false, \"setOnline\")\n if (online && get().dirty) get().flush().catch(() => {})\n },\n }}\n\n const withPersist = storage === false\n ? storeCreator\n : persist(storeCreator, {\n name: `starfish-${name}`,\n storage: storage ? createJSONStorage(() => storage) : undefined,\n partialize: (state) => ({\n data: state.data,\n dirty: state.dirty,\n hash: state.hash,\n }),\n onRehydrateStorage: () => (state) => {\n // Only restore if the manager hasn't already received a hash from a live pull/push.\n // With async storage, pull() may resolve before hydration completes \u2014 the server's\n // hash always wins over the persisted one.\n if (state?.hash && syncManager.getHash() === null) syncManager.setHash(state.hash)\n },\n })\n\n const withSelector = subscribeWithSelector(withPersist)\n\n return createStore<StarfishStore>()(\n options.devtools ? options.devtools(withSelector) : withSelector,\n )\n}\n\n// \u2500\u2500 React hooks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Derived sync status for UI display. */\nexport type SyncStatus = \"synced\" | \"syncing\" | \"pending\" | \"error\" | \"offline\"\n\n/** Derive a single sync status from store state. */\nexport function deriveSyncStatus(state: StarfishState): SyncStatus {\n if (!state.online) return \"offline\"\n if (state.error) return \"error\"\n if (state.syncing) return \"syncing\"\n if (state.dirty) return \"pending\"\n return \"synced\"\n}\n\n/**\n * Aggregate multiple sync statuses into a single worst-case status.\n * Priority (worst first): error > syncing > pending > offline > synced.\n */\nexport function aggregateSyncStatus(statuses: SyncStatus[]): SyncStatus {\n if (statuses.includes(\"error\")) return \"error\"\n if (statuses.includes(\"syncing\")) return \"syncing\"\n if (statuses.includes(\"pending\")) return \"pending\"\n if (statuses.includes(\"offline\")) return \"offline\"\n return \"synced\"\n}\n\n/** Use the full Starfish store state and actions. */\nexport function useStarfish(store: StoreApi<StarfishStore>): StarfishStore {\n return useStore(store)\n}\n\n/** Use only the synced data, with an optional selector for fine-grained subscriptions. */\nexport function useStarfishData<T = Record<string, unknown>>(\n store: StoreApi<StarfishStore>,\n selector?: (data: Record<string, unknown>) => T,\n): T {\n return useStore(store, (state) =>\n selector ? selector(state.data) : (state.data as unknown as T),\n )\n}\n\n/** Use the derived sync status (synced | syncing | pending | error | offline). */\nexport function useSyncStatus(store: StoreApi<StarfishStore>): SyncStatus {\n return useStore(store, deriveSyncStatus)\n}\n\n/**\n * Subscribe to sync status changes outside of React.\n *\n * Framework-agnostic \u2014 works in React Native, Node.js, or anywhere hooks are unavailable.\n * The callback is invoked immediately with the current status and then on every change.\n *\n * ```ts\n * const unsub = subscribeSyncStatus(store, (status) => {\n * updateStatusBar(status)\n * })\n *\n * // Later, to stop listening:\n * unsub()\n * ```\n */\nexport function subscribeSyncStatus(\n store: StoreApi<StarfishStore>,\n callback: (status: SyncStatus) => void,\n): () => void {\n let prev = deriveSyncStatus(store.getState())\n callback(prev)\n return store.subscribe((state) => {\n const next = deriveSyncStatus(state)\n if (next !== prev) {\n prev = next\n callback(next)\n }\n })\n}\n\n/** Sets up cross-tab sync for a Starfish store. Cleans up on unmount. */\nexport function useCrossTabSync(\n store: StoreApi<StarfishStore>,\n name: string,\n): void {\n useEffect(() => {\n return setupCrossTabSync(store as unknown as BroadcastableStore, name)\n }, [store, name])\n}\n\n/** Binds browser online/offline events to the store's setOnline action. Cleans up on unmount. */\nexport function useConnectivity(store: StoreApi<StarfishStore>): void {\n useEffect(() => {\n const handleOnline = () => store.getState().setOnline(true)\n const handleOffline = () => store.getState().setOnline(false)\n\n window.addEventListener(\"online\", handleOnline)\n window.addEventListener(\"offline\", handleOffline)\n\n return () => {\n window.removeEventListener(\"online\", handleOnline)\n window.removeEventListener(\"offline\", handleOffline)\n }\n }, [store])\n}\n\n/** Returns a human-readable \"last synced\" label that updates every 5 seconds. */\nexport function useLastSynced(store: StoreApi<StarfishStore>): string {\n const lastSyncedAt = useRef<number | null>(null)\n const [label, setLabel] = useState(\"Never synced\")\n\n const computeLabel = useCallback(() => {\n if (lastSyncedAt.current === null) return \"Never synced\"\n const seconds = Math.floor((Date.now() - lastSyncedAt.current) / 1000)\n if (seconds < 10) return \"Just now\"\n if (seconds < 60) return `${seconds}s ago`\n return `${Math.floor(seconds / 60)}m ago`\n }, [])\n\n // Track sync completion\n useEffect(() => {\n let prevSyncing = store.getState().syncing\n const unsub = store.subscribe((state) => {\n if (prevSyncing && !state.syncing && !state.error) {\n lastSyncedAt.current = Date.now()\n setLabel(computeLabel())\n }\n prevSyncing = state.syncing\n })\n return unsub\n }, [store, computeLabel])\n\n // Update label periodically\n useEffect(() => {\n const timer = setInterval(() => {\n setLabel(computeLabel())\n }, 5000)\n return () => clearInterval(timer)\n }, [computeLabel])\n\n return label\n}\n\n// \u2500\u2500 SyncInitializer hook \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SyncInitConfig {\n serverUrl: string\n auth?: AuthProvider\n pullPath: string\n pushPath: string\n encryptionSecret?: string\n encryptionSalt?: string\n onConflict?: ConflictResolver\n /** Called when pulled data arrives. Use to restore domain stores. */\n onData?: (data: Record<string, unknown>) => void\n storeName?: string\n storage?: StateStorage | false\n fetch?: typeof globalThis.fetch\n logger?: SyncLogger\n validate?: Validator\n}\n\n/**\n * React hook that manages the full Starfish sync lifecycle.\n *\n * Creates StarfishClient \u2192 SyncManager \u2192 Zustand store, pulls on mount,\n * calls `onData` when remote data arrives, and tears down on unmount or\n * config change.\n *\n * Pass `null` to disable sync (returns `null`).\n */\nexport function useSyncInit(config: SyncInitConfig | null): StoreApi<StarfishStore> | null {\n const [store, setStore] = useState<StoreApi<StarfishStore> | null>(null)\n const onDataRef = useRef(config?.onData)\n onDataRef.current = config?.onData\n\n useEffect(() => {\n if (!config) {\n setStore(null)\n return\n }\n\n const client = new StarfishClient({\n baseUrl: config.serverUrl,\n auth: config.auth,\n fetch: config.fetch,\n })\n\n const syncManager = new SyncManager({\n client,\n pullPath: config.pullPath,\n pushPath: config.pushPath,\n encryptionSecret: config.encryptionSecret,\n encryptionSalt: config.encryptionSalt,\n onConflict: config.onConflict,\n logger: config.logger,\n validate: config.validate,\n })\n\n const newStore = createStarfishStore({\n name: config.storeName ?? \"sync\",\n syncManager,\n storage: config.storage,\n // onRemoteUpdate fires only for pull() results, never for local set() writes \u2014\n // so no isRestoring flag is needed.\n onRemoteUpdate: (data) => {\n try {\n onDataRef.current?.(data)\n } catch (err) {\n newStore.setState({\n error: `onData failed: ${err instanceof Error ? err.message : String(err)}`,\n })\n }\n },\n })\n\n setStore(newStore)\n\n // Initial pull \u2014 errors are stored in state.error by the pull() action\n newStore.getState().pull().catch(() => {})\n\n return () => {\n setStore(null)\n }\n // Intentionally depend on serializable config values, not the object reference\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n config?.serverUrl,\n config?.pullPath,\n config?.pushPath,\n config?.encryptionSecret,\n config?.encryptionSalt,\n config?.storeName,\n ])\n\n return store\n}\n", "const reduxImpl = (reducer, initial) => (set, _get, api) => {\n api.dispatch = (action) => {\n set((state) => reducer(state, action), false, action);\n return action;\n };\n api.dispatchFromDevtools = true;\n return { dispatch: (...args) => api.dispatch(...args), ...initial };\n};\nconst redux = reduxImpl;\n\nconst shouldDispatchFromDevtools = (api) => !!api.dispatchFromDevtools && typeof api.dispatch === \"function\";\nconst trackedConnections = /* @__PURE__ */ new Map();\nconst getTrackedConnectionState = (name) => {\n const api = trackedConnections.get(name);\n if (!api) return {};\n return Object.fromEntries(\n Object.entries(api.stores).map(([key, api2]) => [key, api2.getState()])\n );\n};\nconst extractConnectionInformation = (store, extensionConnector, options) => {\n if (store === void 0) {\n return {\n type: \"untracked\",\n connection: extensionConnector.connect(options)\n };\n }\n const existingConnection = trackedConnections.get(options.name);\n if (existingConnection) {\n return { type: \"tracked\", store, ...existingConnection };\n }\n const newConnection = {\n connection: extensionConnector.connect(options),\n stores: {}\n };\n trackedConnections.set(options.name, newConnection);\n return { type: \"tracked\", store, ...newConnection };\n};\nconst removeStoreFromTrackedConnections = (name, store) => {\n if (store === void 0) return;\n const connectionInfo = trackedConnections.get(name);\n if (!connectionInfo) return;\n delete connectionInfo.stores[store];\n if (Object.keys(connectionInfo.stores).length === 0) {\n trackedConnections.delete(name);\n }\n};\nconst findCallerName = (stack) => {\n var _a, _b;\n if (!stack) return void 0;\n const traceLines = stack.split(\"\\n\");\n const apiSetStateLineIndex = traceLines.findIndex(\n (traceLine) => traceLine.includes(\"api.setState\")\n );\n if (apiSetStateLineIndex < 0) return void 0;\n const callerLine = ((_a = traceLines[apiSetStateLineIndex + 1]) == null ? void 0 : _a.trim()) || \"\";\n return (_b = /.+ (.+) .+/.exec(callerLine)) == null ? void 0 : _b[1];\n};\nconst devtoolsImpl = (fn, devtoolsOptions = {}) => (set, get, api) => {\n const { enabled, anonymousActionType, store, ...options } = devtoolsOptions;\n let extensionConnector;\n try {\n extensionConnector = (enabled != null ? enabled : (import.meta.env ? import.meta.env.MODE : void 0) !== \"production\") && window.__REDUX_DEVTOOLS_EXTENSION__;\n } catch (e) {\n }\n if (!extensionConnector) {\n return fn(set, get, api);\n }\n const { connection, ...connectionInformation } = extractConnectionInformation(store, extensionConnector, options);\n let isRecording = true;\n api.setState = ((state, replace, nameOrAction) => {\n const r = set(state, replace);\n if (!isRecording) return r;\n const action = nameOrAction === void 0 ? {\n type: anonymousActionType || findCallerName(new Error().stack) || \"anonymous\"\n } : typeof nameOrAction === \"string\" ? { type: nameOrAction } : nameOrAction;\n if (store === void 0) {\n connection == null ? void 0 : connection.send(action, get());\n return r;\n }\n connection == null ? void 0 : connection.send(\n {\n ...action,\n type: `${store}/${action.type}`\n },\n {\n ...getTrackedConnectionState(options.name),\n [store]: api.getState()\n }\n );\n return r;\n });\n api.devtools = {\n cleanup: () => {\n if (connection && typeof connection.unsubscribe === \"function\") {\n connection.unsubscribe();\n }\n removeStoreFromTrackedConnections(options.name, store);\n }\n };\n const setStateFromDevtools = (...a) => {\n const originalIsRecording = isRecording;\n isRecording = false;\n set(...a);\n isRecording = originalIsRecording;\n };\n const initialState = fn(api.setState, get, api);\n if (connectionInformation.type === \"untracked\") {\n connection == null ? void 0 : connection.init(initialState);\n } else {\n connectionInformation.stores[connectionInformation.store] = api;\n connection == null ? void 0 : connection.init(\n Object.fromEntries(\n Object.entries(connectionInformation.stores).map(([key, store2]) => [\n key,\n key === connectionInformation.store ? initialState : store2.getState()\n ])\n )\n );\n }\n if (shouldDispatchFromDevtools(api)) {\n let didWarnAboutReservedActionType = false;\n const originalDispatch = api.dispatch;\n api.dispatch = (...args) => {\n if ((import.meta.env ? import.meta.env.MODE : void 0) !== \"production\" && args[0].type === \"__setState\" && !didWarnAboutReservedActionType) {\n console.warn(\n '[zustand devtools middleware] \"__setState\" action type is reserved to set state from the devtools. Avoid using it.'\n );\n didWarnAboutReservedActionType = true;\n }\n originalDispatch(...args);\n };\n }\n connection.subscribe((message) => {\n var _a;\n switch (message.type) {\n case \"ACTION\":\n if (typeof message.payload !== \"string\") {\n console.error(\n \"[zustand devtools middleware] Unsupported action format\"\n );\n return;\n }\n return parseJsonThen(\n message.payload,\n (action) => {\n if (action.type === \"__setState\") {\n if (store === void 0) {\n setStateFromDevtools(action.state);\n return;\n }\n if (Object.keys(action.state).length !== 1) {\n console.error(\n `\n [zustand devtools middleware] Unsupported __setState action format.\n When using 'store' option in devtools(), the 'state' should have only one key, which is a value of 'store' that was passed in devtools(),\n and value of this only key should be a state object. Example: { \"type\": \"__setState\", \"state\": { \"abc123Store\": { \"foo\": \"bar\" } } }\n `\n );\n }\n const stateFromDevtools = action.state[store];\n if (stateFromDevtools === void 0 || stateFromDevtools === null) {\n return;\n }\n if (JSON.stringify(api.getState()) !== JSON.stringify(stateFromDevtools)) {\n setStateFromDevtools(stateFromDevtools);\n }\n return;\n }\n if (shouldDispatchFromDevtools(api)) {\n api.dispatch(action);\n }\n }\n );\n case \"DISPATCH\":\n switch (message.payload.type) {\n case \"RESET\":\n setStateFromDevtools(initialState);\n if (store === void 0) {\n return connection == null ? void 0 : connection.init(api.getState());\n }\n return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n case \"COMMIT\":\n if (store === void 0) {\n connection == null ? void 0 : connection.init(api.getState());\n return;\n }\n return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n case \"ROLLBACK\":\n return parseJsonThen(message.state, (state) => {\n if (store === void 0) {\n setStateFromDevtools(state);\n connection == null ? void 0 : connection.init(api.getState());\n return;\n }\n setStateFromDevtools(state[store]);\n connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));\n });\n case \"JUMP_TO_STATE\":\n case \"JUMP_TO_ACTION\":\n return parseJsonThen(message.state, (state) => {\n if (store === void 0) {\n setStateFromDevtools(state);\n return;\n }\n if (JSON.stringify(api.getState()) !== JSON.stringify(state[store])) {\n setStateFromDevtools(state[store]);\n }\n });\n case \"IMPORT_STATE\": {\n const { nextLiftedState } = message.payload;\n const lastComputedState = (_a = nextLiftedState.computedStates.slice(-1)[0]) == null ? void 0 : _a.state;\n if (!lastComputedState) return;\n if (store === void 0) {\n setStateFromDevtools(lastComputedState);\n } else {\n setStateFromDevtools(lastComputedState[store]);\n }\n connection == null ? void 0 : connection.send(\n null,\n // FIXME no-any\n nextLiftedState\n );\n return;\n }\n case \"PAUSE_RECORDING\":\n return isRecording = !isRecording;\n }\n return;\n }\n });\n return initialState;\n};\nconst devtools = devtoolsImpl;\nconst parseJsonThen = (stringified, fn) => {\n let parsed;\n try {\n parsed = JSON.parse(stringified);\n } catch (e) {\n console.error(\n \"[zustand devtools middleware] Could not parse the received json\",\n e\n );\n }\n if (parsed !== void 0) fn(parsed);\n};\n\nconst subscribeWithSelectorImpl = (fn) => (set, get, api) => {\n const origSubscribe = api.subscribe;\n api.subscribe = ((selector, optListener, options) => {\n let listener = selector;\n if (optListener) {\n const equalityFn = (options == null ? void 0 : options.equalityFn) || Object.is;\n let currentSlice = selector(api.getState());\n listener = (state) => {\n const nextSlice = selector(state);\n if (!equalityFn(currentSlice, nextSlice)) {\n const previousSlice = currentSlice;\n optListener(currentSlice = nextSlice, previousSlice);\n }\n };\n if (options == null ? void 0 : options.fireImmediately) {\n optListener(currentSlice, currentSlice);\n }\n }\n return origSubscribe(listener);\n });\n const initialState = fn(set, get, api);\n return initialState;\n};\nconst subscribeWithSelector = subscribeWithSelectorImpl;\n\nfunction combine(initialState, create) {\n return (...args) => Object.assign({}, initialState, create(...args));\n}\n\nfunction createJSONStorage(getStorage, options) {\n let storage;\n try {\n storage = getStorage();\n } catch (e) {\n return;\n }\n const persistStorage = {\n getItem: (name) => {\n var _a;\n const parse = (str2) => {\n if (str2 === null) {\n return null;\n }\n return JSON.parse(str2, options == null ? void 0 : options.reviver);\n };\n const str = (_a = storage.getItem(name)) != null ? _a : null;\n if (str instanceof Promise) {\n return str.then(parse);\n }\n return parse(str);\n },\n setItem: (name, newValue) => storage.setItem(name, JSON.stringify(newValue, options == null ? void 0 : options.replacer)),\n removeItem: (name) => storage.removeItem(name)\n };\n return persistStorage;\n}\nconst toThenable = (fn) => (input) => {\n try {\n const result = fn(input);\n if (result instanceof Promise) {\n return result;\n }\n return {\n then(onFulfilled) {\n return toThenable(onFulfilled)(result);\n },\n catch(_onRejected) {\n return this;\n }\n };\n } catch (e) {\n return {\n then(_onFulfilled) {\n return this;\n },\n catch(onRejected) {\n return toThenable(onRejected)(e);\n }\n };\n }\n};\nconst persistImpl = (config, baseOptions) => (set, get, api) => {\n let options = {\n storage: createJSONStorage(() => window.localStorage),\n partialize: (state) => state,\n version: 0,\n merge: (persistedState, currentState) => ({\n ...currentState,\n ...persistedState\n }),\n ...baseOptions\n };\n let hasHydrated = false;\n let hydrationVersion = 0;\n const hydrationListeners = /* @__PURE__ */ new Set();\n const finishHydrationListeners = /* @__PURE__ */ new Set();\n let storage = options.storage;\n if (!storage) {\n return config(\n (...args) => {\n console.warn(\n `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`\n );\n set(...args);\n },\n get,\n api\n );\n }\n const setItem = () => {\n const state = options.partialize({ ...get() });\n return storage.setItem(options.name, {\n state,\n version: options.version\n });\n };\n const savedSetState = api.setState;\n api.setState = (state, replace) => {\n savedSetState(state, replace);\n return setItem();\n };\n const configResult = config(\n (...args) => {\n set(...args);\n return setItem();\n },\n get,\n api\n );\n api.getInitialState = () => configResult;\n let stateFromStorage;\n const hydrate = () => {\n var _a, _b;\n if (!storage) return;\n const currentVersion = ++hydrationVersion;\n hasHydrated = false;\n hydrationListeners.forEach((cb) => {\n var _a2;\n return cb((_a2 = get()) != null ? _a2 : configResult);\n });\n const postRehydrationCallback = ((_b = options.onRehydrateStorage) == null ? void 0 : _b.call(options, (_a = get()) != null ? _a : configResult)) || void 0;\n return toThenable(storage.getItem.bind(storage))(options.name).then((deserializedStorageValue) => {\n if (deserializedStorageValue) {\n if (typeof deserializedStorageValue.version === \"number\" && deserializedStorageValue.version !== options.version) {\n if (options.migrate) {\n const migration = options.migrate(\n deserializedStorageValue.state,\n deserializedStorageValue.version\n );\n if (migration instanceof Promise) {\n return migration.then((result) => [true, result]);\n }\n return [true, migration];\n }\n console.error(\n `State loaded from storage couldn't be migrated since no migrate function was provided`\n );\n } else {\n return [false, deserializedStorageValue.state];\n }\n }\n return [false, void 0];\n }).then((migrationResult) => {\n var _a2;\n if (currentVersion !== hydrationVersion) {\n return;\n }\n const [migrated, migratedState] = migrationResult;\n stateFromStorage = options.merge(\n migratedState,\n (_a2 = get()) != null ? _a2 : configResult\n );\n set(stateFromStorage, true);\n if (migrated) {\n return setItem();\n }\n }).then(() => {\n if (currentVersion !== hydrationVersion) {\n return;\n }\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0);\n stateFromStorage = get();\n hasHydrated = true;\n finishHydrationListeners.forEach((cb) => cb(stateFromStorage));\n }).catch((e) => {\n if (currentVersion !== hydrationVersion) {\n return;\n }\n postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e);\n });\n };\n api.persist = {\n setOptions: (newOptions) => {\n options = {\n ...options,\n ...newOptions\n };\n if (newOptions.storage) {\n storage = newOptions.storage;\n }\n },\n clearStorage: () => {\n storage == null ? void 0 : storage.removeItem(options.name);\n },\n getOptions: () => options,\n rehydrate: () => hydrate(),\n hasHydrated: () => hasHydrated,\n onHydrate: (cb) => {\n hydrationListeners.add(cb);\n return () => {\n hydrationListeners.delete(cb);\n };\n },\n onFinishHydration: (cb) => {\n finishHydrationListeners.add(cb);\n return () => {\n finishHydrationListeners.delete(cb);\n };\n }\n };\n if (!options.skipHydration) {\n hydrate();\n }\n return stateFromStorage || configResult;\n};\nconst persist = persistImpl;\n\nfunction ssrSafe(config, isSSR = typeof window === \"undefined\") {\n return (set, get, api) => {\n if (!isSSR) {\n return config(set, get, api);\n }\n const ssrSet = () => {\n throw new Error(\"Cannot set state of Zustand store in SSR\");\n };\n api.setState = ssrSet;\n return config(ssrSet, get, api);\n };\n}\n\nexport { combine, createJSONStorage, devtools, persist, redux, subscribeWithSelector, ssrSafe as unstable_ssrSafe };\n", "/** Push conflict error (HTTP 409). */\nexport class ConflictError extends Error {\n constructor() {\n super(\"hash_mismatch\")\n this.name = \"ConflictError\"\n }\n}\n\n/** HTTP error from the Starfish server. */\nexport class StarfishHttpError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string\n ) {\n super(`HTTP ${status}: ${body}`)\n this.name = \"StarfishHttpError\"\n }\n}\n\n/**\n * Auth provider: returns headers to include in requests.\n * Called for every authenticated request (pull and push).\n */\nexport type AuthProvider = (req: {\n method: string\n path: string\n body: string | null\n}) => Record<string, string> | Promise<Record<string, string>>\n\n/** Options for creating a StarfishClient. */\nexport interface StarfishClientOptions {\n /** Base URL of the Starfish server (e.g. \"https://api.example.com/v1\"). */\n baseUrl: string\n /** Auth provider that returns headers for authenticated requests. Optional for public-read collections. */\n auth?: AuthProvider\n /** Optional fetch implementation (defaults to global fetch). */\n fetch?: typeof fetch\n}\n\n/** Conflict resolver: given local and remote data, return merged result. */\nexport type ConflictResolver = (\n local: Record<string, unknown>,\n remote: Record<string, unknown>\n) => Record<string, unknown>\n", "import type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\nimport type {\n StarfishClientOptions,\n AuthProvider,\n} from \"./types.js\"\nimport { ConflictError, StarfishHttpError } from \"./types.js\"\n\nconst APPEND_DEFAULT_FIELD = \"items\"\n\n/** Result of pulling a binary blob from the server. */\nexport interface BlobPullResult {\n data: ArrayBuffer\n /** Content hash from the ETag header. Null if the server didn't include an ETag. */\n hash: string | null\n contentType: string\n}\n\n/** Result of pushing a binary blob to the server. */\nexport interface BlobPushResult {\n hash: string\n}\n\n/** Options for append-only pull \u2014 extracts a single array field from the response. */\nexport interface AppendPullOptions {\n /** Array field name in `data`. Defaults to `\"items\"`. */\n appendField?: string\n /** Only return items appended after this timestamp (ms). Sent as `?checkpoint=`. */\n since?: number\n /** Return only the last K items (applied after `since` filter). Sent as `?last=`. */\n last?: number\n}\n\n/**\n * Low-level HTTP client for the Starfish sync protocol.\n * Handles auth headers and response parsing.\n */\nexport class StarfishClient {\n private readonly baseUrl: string\n private readonly auth?: AuthProvider\n private readonly fetch: typeof globalThis.fetch\n\n constructor(options: StarfishClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\")\n this.auth = options.auth\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n }\n\n /** Pull synced data from the server. Returns the raw `PullResult`. */\n async pull(path: string, checkpoint?: number): Promise<PullResult>\n /** Pull an append-only collection. Extracts and returns `data[appendField]` as `T[]`. */\n async pull<T = unknown>(path: string, options: AppendPullOptions): Promise<T[]>\n async pull<T = unknown>(\n path: string,\n checkpointOrOptions?: number | AppendPullOptions,\n ): Promise<PullResult | T[]> {\n let url = `${this.baseUrl}${path}`\n let appendField: string | undefined\n\n if (typeof checkpointOrOptions === \"number\") {\n if (checkpointOrOptions) url += `?checkpoint=${checkpointOrOptions}`\n } else if (checkpointOrOptions != null) {\n appendField = checkpointOrOptions.appendField ?? APPEND_DEFAULT_FIELD\n const params = new URLSearchParams()\n if (checkpointOrOptions.since != null) {\n if (checkpointOrOptions.since < 0) throw new Error(\"since must be non-negative\")\n params.set(\"checkpoint\", String(checkpointOrOptions.since))\n }\n if (checkpointOrOptions.last != null) {\n if (checkpointOrOptions.last < 0) throw new Error(\"last must be non-negative\")\n params.set(\"last\", String(checkpointOrOptions.last))\n }\n if (params.size > 0) url += `?${params.toString()}`\n }\n\n const authHeaders = this.auth\n ? await this.auth({ method: \"GET\", path, body: null })\n : {}\n\n const res = await this.fetch(url, {\n method: \"GET\",\n headers: { Accept: \"application/json\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n\n const result = await res.json() as PullResult\n if (appendField !== undefined) {\n const list = (result.data as Record<string, unknown> | null)?.[appendField]\n return (Array.isArray(list) ? list : []) as T[]\n }\n return result\n }\n\n /**\n * Push synced data to the server.\n * @param path - The push endpoint path (e.g. \"/push/users/abc/settings\")\n * @param data - The full document data to push\n * @param baseHash - Hash of the document this push is based on (null for first push)\n * @param authorSignature - Optional author signature for provenance\n * @throws {ConflictError} if the server detects a hash mismatch (409)\n */\n async push(\n path: string,\n data: Record<string, unknown>,\n baseHash: string | null,\n authorSignature?: string\n ): Promise<PushSuccess> {\n const body = JSON.stringify({\n data,\n baseHash,\n ...(authorSignature && { authorSignature }),\n })\n\n const authHeaders = this.auth\n ? await this.auth({ method: \"POST\", path, body })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...authHeaders,\n },\n body,\n })\n\n if (res.status === 409) {\n throw new ConflictError()\n }\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<PushSuccess>\n }\n\n /**\n * Pull binary data from a blob collection.\n * Returns raw bytes with the content hash from the ETag header.\n */\n async pullBlob(path: string): Promise<BlobPullResult> {\n const authHeaders = this.auth\n ? await this.auth({ method: \"GET\", path, body: null })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"GET\",\n headers: { Accept: \"*/*\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n\n const etag = res.headers.get(\"ETag\")?.replace(/\"/g, \"\") ?? null\n const contentType = res.headers.get(\"Content-Type\") ?? \"application/octet-stream\"\n const data = await res.arrayBuffer()\n\n return { data, hash: etag, contentType }\n }\n\n /**\n * Push binary data to a blob collection.\n * Binary collections use last-write-wins (no conflict detection).\n */\n async pushBlob(\n path: string,\n data: ArrayBuffer | Uint8Array | Blob,\n contentType: string,\n ): Promise<BlobPushResult> {\n const authHeaders = this.auth\n ? await this.auth({ method: \"POST\", path, body: null })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": contentType,\n Accept: \"application/json\",\n ...authHeaders,\n },\n body: data as BodyInit,\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<BlobPushResult>\n }\n}\n", "import type { PullResult } from \"@drakkar.software/starfish-protocol\"\nimport { deepMerge, stableStringify } from \"@drakkar.software/starfish-protocol\"\nimport type { ConflictResolver } from \"./types.js\"\nimport { ConflictError } from \"./types.js\"\nimport { StarfishClient } from \"./client.js\"\nimport type { Encryptor } from \"./crypto.js\"\nimport { createEncryptor } from \"./crypto.js\"\nimport type { SyncLogger } from \"./logger.js\"\nimport type { Validator } from \"./validate.js\"\nimport { ValidationError } from \"./validate.js\"\n\nexport class AbortError extends Error {\n constructor() {\n super(\"SyncManager was aborted\")\n this.name = \"AbortError\"\n }\n}\n\n\nexport interface SyncManagerOptions {\n client: StarfishClient\n pullPath: string\n pushPath: string\n /** Custom conflict resolver. Defaults to remote-wins deep merge. Arrays are atomic. */\n onConflict?: ConflictResolver\n /** Max conflict retry attempts (default: 3). */\n maxRetries?: number\n encryptionSecret?: string\n encryptionSalt?: string\n encryptionInfo?: string\n /**\n * Pre-created Encryptor. Use this with `createGroupEncryptor` for group encryption.\n * Takes precedence over `encryptionSecret` / `encryptionSalt` if both are provided.\n */\n encryptor?: Encryptor\n signData?: (data: string) => Promise<string>\n /** Structured logger for sync events. */\n logger?: SyncLogger\n /** Name passed to logger methods (default: derived from pullPath). */\n loggerName?: string\n /** Validate data before push. Throws ValidationError on failure. */\n validate?: Validator\n}\n\nexport class SyncManager {\n private readonly client: StarfishClient\n private readonly pullPath: string\n private readonly pushPath: string\n private readonly onConflict: ConflictResolver\n private readonly maxRetries: number\n private readonly encryptor: Encryptor | null\n private readonly signData?: (data: string) => Promise<string>\n private readonly logger?: SyncLogger\n private readonly loggerName: string\n private readonly validate?: Validator\n\n private lastHash: string | null = null\n private lastCheckpoint: number = 0\n private localData: Record<string, unknown> = {}\n private aborted: boolean = false\n\n constructor(options: SyncManagerOptions) {\n this.client = options.client\n this.pullPath = options.pullPath\n this.pushPath = options.pushPath\n this.onConflict = options.onConflict ?? deepMerge\n this.maxRetries = options.maxRetries ?? 3\n this.signData = options.signData\n this.logger = options.logger\n this.loggerName = options.loggerName ?? options.pullPath.split(\"/\").filter(Boolean).pop() ?? options.pullPath\n this.validate = options.validate\n this.encryptor =\n options.encryptor ??\n (options.encryptionSecret && options.encryptionSalt\n ? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo)\n : null)\n }\n\n abort(): void {\n this.aborted = true\n }\n\n get isAborted(): boolean {\n return this.aborted\n }\n\n getData(): Record<string, unknown> {\n return { ...this.localData }\n }\n\n getHash(): string | null {\n return this.lastHash\n }\n\n /** Set the last-known server hash. Used by persistence layers to restore state across restarts. */\n setHash(hash: string | null): void {\n this.lastHash = hash\n }\n\n getCheckpoint(): number {\n return this.lastCheckpoint\n }\n\n async pull(): Promise<PullResult> {\n if (this.aborted) throw new AbortError()\n this.logger?.pullStart(this.loggerName)\n const start = performance.now()\n try {\n const result = await this.client.pull(this.pullPath, this.lastCheckpoint)\n if (this.aborted) throw new AbortError()\n\n if (this.encryptor) {\n const decrypted = await this.encryptor.decrypt(result.data)\n if (this.aborted) throw new AbortError()\n this.localData = decrypted\n result.data = decrypted\n } else if (this.lastCheckpoint > 0) {\n this.localData = deepMerge(this.localData, result.data)\n result.data = this.localData\n } else {\n this.localData = result.data\n }\n\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n }\n\n async push(data: Record<string, unknown>): Promise<{ hash: string; timestamp: number }> {\n if (this.aborted) throw new AbortError()\n if (this.validate) {\n const result = this.validate(data)\n if (result !== true) throw new ValidationError(result)\n }\n this.logger?.pushStart(this.loggerName)\n const start = performance.now()\n let attempt = 0\n let pendingData = data\n\n while (attempt <= this.maxRetries) {\n try {\n const payload = this.encryptor\n ? await this.encryptor.encrypt(pendingData)\n : pendingData\n if (this.aborted) throw new AbortError()\n\n const sig = this.signData\n ? await this.signData(stableStringify(payload))\n : undefined\n if (this.aborted) throw new AbortError()\n\n const result = await this.client.push(\n this.pushPath,\n payload,\n this.lastHash,\n sig\n )\n if (this.aborted) throw new AbortError()\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.localData = pendingData\n this.logger?.pushSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n if (err instanceof AbortError) throw err\n if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {\n this.logger?.pushError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n this.logger?.conflict(this.loggerName, attempt + 1)\n try {\n const remote = await this.client.pull(this.pullPath)\n if (this.aborted) throw new AbortError()\n const remoteData = this.encryptor\n ? await this.encryptor.decrypt(remote.data)\n : remote.data\n if (this.aborted) throw new AbortError()\n this.lastHash = remote.hash\n this.lastCheckpoint = remote.timestamp\n pendingData = this.onConflict(pendingData, remoteData)\n } catch (resolveErr) {\n if (resolveErr instanceof AbortError) throw resolveErr\n const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr)\n this.logger?.pushError(this.loggerName, `Conflict resolution failed (attempt ${attempt + 1}): ${msg}`)\n throw resolveErr\n }\n await new Promise<void>(resolve => setTimeout(resolve, Math.min(100 * Math.pow(2, attempt), 2000) + Math.random() * 100))\n attempt++\n }\n }\n throw new ConflictError()\n }\n\n async update(\n modifier: (current: Record<string, unknown>) => Record<string, unknown>\n ): Promise<{ hash: string; timestamp: number }> {\n await this.pull()\n const updated = modifier(this.localData)\n return this.push(updated)\n }\n}\n", "import { getCrypto, getBase64, IV_BYTES, ENCRYPTED_KEY, deriveKey } from \"@drakkar.software/starfish-protocol\"\n\nconst ALGO = \"AES-GCM\"\n\nexport { ENCRYPTED_KEY }\n\n/** Encrypt/decrypt interface for client-side E2E encryption. */\nexport interface Encryptor {\n encrypt(data: Record<string, unknown>): Promise<Record<string, unknown>>\n decrypt(wrapper: Record<string, unknown>): Promise<Record<string, unknown>>\n}\n\n/**\n * Creates an Encryptor that uses AES-256-GCM with HKDF-derived keys.\n */\nexport function createEncryptor(secret: string, salt: string, info: string = \"starfish-e2e\"): Encryptor {\n if (!secret) throw new Error(\"encryptionSecret must not be empty\")\n if (!salt) throw new Error(\"encryptionSalt must not be empty\")\n const keyPromise = deriveKey(secret, salt, info)\n\n return {\n async encrypt(data: Record<string, unknown>): Promise<Record<string, unknown>> {\n const key = await keyPromise\n const c = getCrypto()\n const b64 = getBase64()\n const plaintext = new TextEncoder().encode(JSON.stringify(data))\n const iv = c.getRandomValues(new Uint8Array(IV_BYTES))\n const ciphertext = await c.subtle.encrypt({ name: ALGO, iv }, key, plaintext)\n\n const combined = new Uint8Array(iv.length + ciphertext.byteLength)\n combined.set(iv)\n combined.set(new Uint8Array(ciphertext), iv.length)\n\n return { [ENCRYPTED_KEY]: b64.encode(combined) }\n },\n\n async decrypt(wrapper: Record<string, unknown>): Promise<Record<string, unknown>> {\n const encoded = wrapper[ENCRYPTED_KEY]\n if (typeof encoded !== \"string\") {\n throw new Error(\"Expected encrypted data but received unencrypted document\")\n }\n\n const key = await keyPromise\n const c = getCrypto()\n const b64 = getBase64()\n const combined = b64.decode(encoded)\n if (combined.length < IV_BYTES) {\n throw new Error(\"Encrypted data is too short\")\n }\n const iv = combined.slice(0, IV_BYTES)\n const ciphertext = combined.slice(IV_BYTES)\n try {\n const plaintext = await c.subtle.decrypt({ name: ALGO, iv }, key, ciphertext)\n return JSON.parse(new TextDecoder().decode(plaintext))\n } catch (err) {\n throw new Error(\"Decryption failed: data may be tampered or key is incorrect\", { cause: err })\n }\n },\n }\n}\n", "/** Validation result: true if valid, or an array of error messages. */\nexport type ValidationResult = true | string[]\n\n/** A function that validates data before push. */\nexport type Validator = (data: Record<string, unknown>) => ValidationResult\n\n/** Error thrown when pre-push validation fails. */\nexport class ValidationError extends Error {\n constructor(public readonly errors: string[]) {\n super(`Validation failed: ${errors.join(\"; \")}`)\n this.name = \"ValidationError\"\n }\n}\n\n/**\n * Creates a validator from a JSON Schema object.\n * Requires an Ajv-compatible validate function.\n *\n * @example\n * ```ts\n * import Ajv from \"ajv\"\n * const ajv = new Ajv()\n * const validator = createSchemaValidator(ajv, mySchema)\n * ```\n */\nexport function createSchemaValidator(\n ajv: { compile: (schema: object) => { (data: unknown): boolean; errors?: unknown }; errorsText: (errors?: unknown) => string },\n schema: object,\n): Validator {\n const validate = ajv.compile(schema)\n return (data) => {\n if (validate(data)) return true\n return [ajv.errorsText(validate.errors)]\n }\n}\n", "/** Minimal store interface for cross-tab sync. Works with both Zustand and Legend bindings. */\nexport interface BroadcastableStore {\n getState(): { data: Record<string, unknown>; dirty: boolean }\n setState(partial: { data: Record<string, unknown>; dirty: boolean }): void\n subscribe(listener: (state: { data: Record<string, unknown>; dirty: boolean }, prev: { data: Record<string, unknown>; dirty: boolean }) => void): () => void\n}\n\ninterface BroadcastPayload {\n data: Record<string, unknown>\n dirty: boolean\n}\n\n/**\n * Syncs a Starfish store across browser tabs using BroadcastChannel.\n * Works with any store that has getState/setState/subscribe (Zustand, Legend adapters, etc.).\n * Returns a cleanup function that closes the channel.\n */\nexport function setupBroadcastSync(\n store: BroadcastableStore,\n name: string,\n): () => void {\n const channel = new BroadcastChannel(`starfish-${name}`)\n let lastReceivedData: Record<string, unknown> | null = null\n\n channel.onmessage = (event: MessageEvent<unknown>) => {\n const payload = event.data as BroadcastPayload | undefined\n if (!payload || typeof payload !== \"object\" || !payload.data || typeof payload.data !== \"object\") return\n lastReceivedData = payload.data\n store.setState({ data: payload.data, dirty: !!payload.dirty })\n }\n\n const unsub = store.subscribe((state, prev) => {\n if (state.data === lastReceivedData) return\n if (state.data !== prev.data || state.dirty !== prev.dirty) {\n try {\n channel.postMessage({ data: state.data, dirty: state.dirty } satisfies BroadcastPayload)\n } catch { /* non-serializable data \u2014 skip broadcast */ }\n }\n })\n\n return () => {\n unsub()\n channel.close()\n }\n}\n\n/**\n * Syncs a Starfish store across browser tabs using storage events.\n * Fallback for environments without BroadcastChannel.\n * Returns a cleanup function.\n */\nexport function setupStorageFallback(\n store: BroadcastableStore,\n name: string,\n): () => void {\n const storageKey = `starfish-broadcast-${name}`\n let lastReceivedData: Record<string, unknown> | null = null\n\n const onStorage = (e: StorageEvent) => {\n if (e.key !== storageKey || !e.newValue) return\n let payload: BroadcastPayload\n try {\n payload = JSON.parse(e.newValue)\n } catch {\n return\n }\n if (!payload || typeof payload !== \"object\" || !payload.data || typeof payload.data !== \"object\") return\n lastReceivedData = payload.data\n store.setState({ data: payload.data, dirty: !!payload.dirty })\n }\n\n globalThis.addEventListener(\"storage\", onStorage)\n\n const unsub = store.subscribe((state, prev) => {\n if (state.data === lastReceivedData) return\n if (state.data !== prev.data || state.dirty !== prev.dirty) {\n try {\n localStorage.setItem(\n storageKey,\n JSON.stringify({ data: state.data, dirty: state.dirty } satisfies BroadcastPayload),\n )\n } catch { /* quota exceeded or non-serializable \u2014 skip */ }\n }\n })\n\n return () => {\n unsub()\n globalThis.removeEventListener(\"storage\", onStorage)\n }\n}\n\n/**\n * Auto-detects the best cross-tab sync mechanism and sets it up.\n * Uses BroadcastChannel when available, falls back to storage events.\n * Returns a cleanup function.\n */\nexport function setupCrossTabSync(\n store: BroadcastableStore,\n name: string,\n): () => void {\n if (typeof BroadcastChannel !== \"undefined\") {\n return setupBroadcastSync(store, name)\n }\n if (typeof globalThis.addEventListener === \"function\" && typeof localStorage !== \"undefined\") {\n return setupStorageFallback(store, name)\n }\n return () => {}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { stableStringify, computeHash } from "@drakkar.software/starfish-protoco
|
|
|
4
4
|
export type { PullResult, PushSuccess } from "@drakkar.software/starfish-protocol";
|
|
5
5
|
export { StarfishClient } from "./client.js";
|
|
6
6
|
export type { BlobPullResult, BlobPushResult, AppendPullOptions } from "./client.js";
|
|
7
|
-
export { SyncManager } from "./sync.js";
|
|
7
|
+
export { SyncManager, AbortError } from "./sync.js";
|
|
8
8
|
export type { SyncManagerOptions } from "./sync.js";
|
|
9
9
|
export { createEncryptor, ENCRYPTED_KEY } from "./crypto.js";
|
|
10
10
|
export type { Encryptor } from "./crypto.js";
|
package/dist/index.js
CHANGED
|
@@ -198,6 +198,12 @@ function createSchemaValidator(ajv, schema) {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
// src/sync.ts
|
|
201
|
+
var AbortError = class extends Error {
|
|
202
|
+
constructor() {
|
|
203
|
+
super("SyncManager was aborted");
|
|
204
|
+
this.name = "AbortError";
|
|
205
|
+
}
|
|
206
|
+
};
|
|
201
207
|
var SyncManager = class {
|
|
202
208
|
client;
|
|
203
209
|
pullPath;
|
|
@@ -212,6 +218,7 @@ var SyncManager = class {
|
|
|
212
218
|
lastHash = null;
|
|
213
219
|
lastCheckpoint = 0;
|
|
214
220
|
localData = {};
|
|
221
|
+
aborted = false;
|
|
215
222
|
constructor(options) {
|
|
216
223
|
this.client = options.client;
|
|
217
224
|
this.pullPath = options.pullPath;
|
|
@@ -224,6 +231,12 @@ var SyncManager = class {
|
|
|
224
231
|
this.validate = options.validate;
|
|
225
232
|
this.encryptor = options.encryptor ?? (options.encryptionSecret && options.encryptionSalt ? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo) : null);
|
|
226
233
|
}
|
|
234
|
+
abort() {
|
|
235
|
+
this.aborted = true;
|
|
236
|
+
}
|
|
237
|
+
get isAborted() {
|
|
238
|
+
return this.aborted;
|
|
239
|
+
}
|
|
227
240
|
getData() {
|
|
228
241
|
return { ...this.localData };
|
|
229
242
|
}
|
|
@@ -238,12 +251,15 @@ var SyncManager = class {
|
|
|
238
251
|
return this.lastCheckpoint;
|
|
239
252
|
}
|
|
240
253
|
async pull() {
|
|
254
|
+
if (this.aborted) throw new AbortError();
|
|
241
255
|
this.logger?.pullStart(this.loggerName);
|
|
242
256
|
const start = performance.now();
|
|
243
257
|
try {
|
|
244
258
|
const result = await this.client.pull(this.pullPath, this.lastCheckpoint);
|
|
259
|
+
if (this.aborted) throw new AbortError();
|
|
245
260
|
if (this.encryptor) {
|
|
246
261
|
const decrypted = await this.encryptor.decrypt(result.data);
|
|
262
|
+
if (this.aborted) throw new AbortError();
|
|
247
263
|
this.localData = decrypted;
|
|
248
264
|
result.data = decrypted;
|
|
249
265
|
} else if (this.lastCheckpoint > 0) {
|
|
@@ -262,6 +278,7 @@ var SyncManager = class {
|
|
|
262
278
|
}
|
|
263
279
|
}
|
|
264
280
|
async push(data) {
|
|
281
|
+
if (this.aborted) throw new AbortError();
|
|
265
282
|
if (this.validate) {
|
|
266
283
|
const result = this.validate(data);
|
|
267
284
|
if (result !== true) throw new ValidationError(result);
|
|
@@ -273,19 +290,23 @@ var SyncManager = class {
|
|
|
273
290
|
while (attempt <= this.maxRetries) {
|
|
274
291
|
try {
|
|
275
292
|
const payload = this.encryptor ? await this.encryptor.encrypt(pendingData) : pendingData;
|
|
293
|
+
if (this.aborted) throw new AbortError();
|
|
276
294
|
const sig = this.signData ? await this.signData(stableStringify(payload)) : void 0;
|
|
295
|
+
if (this.aborted) throw new AbortError();
|
|
277
296
|
const result = await this.client.push(
|
|
278
297
|
this.pushPath,
|
|
279
298
|
payload,
|
|
280
299
|
this.lastHash,
|
|
281
300
|
sig
|
|
282
301
|
);
|
|
302
|
+
if (this.aborted) throw new AbortError();
|
|
283
303
|
this.lastHash = result.hash;
|
|
284
304
|
this.lastCheckpoint = result.timestamp;
|
|
285
305
|
this.localData = pendingData;
|
|
286
306
|
this.logger?.pushSuccess(this.loggerName, Math.round(performance.now() - start));
|
|
287
307
|
return result;
|
|
288
308
|
} catch (err) {
|
|
309
|
+
if (err instanceof AbortError) throw err;
|
|
289
310
|
if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {
|
|
290
311
|
this.logger?.pushError(this.loggerName, err instanceof Error ? err.message : String(err));
|
|
291
312
|
throw err;
|
|
@@ -293,11 +314,14 @@ var SyncManager = class {
|
|
|
293
314
|
this.logger?.conflict(this.loggerName, attempt + 1);
|
|
294
315
|
try {
|
|
295
316
|
const remote = await this.client.pull(this.pullPath);
|
|
317
|
+
if (this.aborted) throw new AbortError();
|
|
296
318
|
const remoteData = this.encryptor ? await this.encryptor.decrypt(remote.data) : remote.data;
|
|
319
|
+
if (this.aborted) throw new AbortError();
|
|
297
320
|
this.lastHash = remote.hash;
|
|
298
321
|
this.lastCheckpoint = remote.timestamp;
|
|
299
322
|
pendingData = this.onConflict(pendingData, remoteData);
|
|
300
323
|
} catch (resolveErr) {
|
|
324
|
+
if (resolveErr instanceof AbortError) throw resolveErr;
|
|
301
325
|
const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr);
|
|
302
326
|
this.logger?.pushError(this.loggerName, `Conflict resolution failed (attempt ${attempt + 1}): ${msg}`);
|
|
303
327
|
throw resolveErr;
|
|
@@ -1312,6 +1336,7 @@ async function pullEntitlements(client, userId, opts) {
|
|
|
1312
1336
|
}
|
|
1313
1337
|
}
|
|
1314
1338
|
export {
|
|
1339
|
+
AbortError,
|
|
1315
1340
|
ConflictError,
|
|
1316
1341
|
ENCRYPTED_KEY,
|
|
1317
1342
|
SnapshotHistory,
|