@drakkar.software/starfish-client 3.0.0-alpha.5 → 3.0.0-alpha.50

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.
Files changed (47) hide show
  1. package/README.md +59 -0
  2. package/dist/append-log.d.ts +228 -0
  3. package/dist/append-log.js +267 -0
  4. package/dist/background-sync.js +29 -0
  5. package/dist/bindings/legend.d.ts +23 -0
  6. package/dist/bindings/legend.js +32 -0
  7. package/dist/bindings/legend.js.map +2 -2
  8. package/dist/bindings/suspense.js +49 -0
  9. package/dist/bindings/zustand.d.ts +167 -2
  10. package/dist/bindings/zustand.js +941 -82
  11. package/dist/bindings/zustand.js.map +4 -4
  12. package/dist/blob-seal.d.ts +123 -0
  13. package/dist/client.d.ts +270 -5
  14. package/dist/client.js +391 -0
  15. package/dist/config.d.ts +9 -0
  16. package/dist/config.js +18 -0
  17. package/dist/debounced-sync.js +120 -0
  18. package/dist/dedup.js +35 -0
  19. package/dist/events.d.ts +150 -0
  20. package/dist/events.js +116 -0
  21. package/dist/events.js.map +7 -0
  22. package/dist/export.js +115 -0
  23. package/dist/fetch.d.ts +40 -0
  24. package/dist/fetch.js +51 -14
  25. package/dist/fetch.js.map +2 -2
  26. package/dist/history.js +61 -0
  27. package/dist/index.d.ts +16 -7
  28. package/dist/index.js +1029 -94
  29. package/dist/index.js.map +4 -4
  30. package/dist/kv-cache.d.ts +63 -0
  31. package/dist/logger.d.ts +3 -0
  32. package/dist/logger.js +80 -0
  33. package/dist/migrate.js +38 -0
  34. package/dist/mobile-lifecycle.d.ts +28 -1
  35. package/dist/mobile-lifecycle.js +94 -0
  36. package/dist/multi-store.js +92 -0
  37. package/dist/mutate.d.ts +39 -0
  38. package/dist/polling.js +52 -0
  39. package/dist/resolvers.js +223 -0
  40. package/dist/service-worker.js +55 -0
  41. package/dist/storage/indexeddb.js +59 -0
  42. package/dist/sync.d.ts +83 -0
  43. package/dist/sync.js +181 -0
  44. package/dist/types.d.ts +106 -11
  45. package/dist/types.js +18 -0
  46. package/dist/validate.js +28 -0
  47. package/package.json +12 -3
@@ -57,7 +57,39 @@ function createStarfishObservable(options) {
57
57
  };
58
58
  return { state, pull, set, flush, setOnline };
59
59
  }
60
+ function createStarfishLogObservable(options) {
61
+ const { cursor } = options;
62
+ const state = observable({
63
+ // Seed from the cursor so a warm-started cursor's items show immediately.
64
+ items: cursor.getItems(),
65
+ loading: false,
66
+ online: true,
67
+ error: null,
68
+ checkpoint: cursor.getCheckpoint()
69
+ });
70
+ const pull = async () => {
71
+ if (state.loading.get()) return [];
72
+ state.loading.set(true);
73
+ state.error.set(null);
74
+ try {
75
+ const batch = await cursor.pull();
76
+ state.items.set(cursor.getItems());
77
+ state.checkpoint.set(cursor.getCheckpoint());
78
+ return batch;
79
+ } catch (err) {
80
+ state.error.set(err instanceof Error ? err.message : String(err));
81
+ return [];
82
+ } finally {
83
+ state.loading.set(false);
84
+ }
85
+ };
86
+ const setOnline = (online) => {
87
+ state.online.set(online);
88
+ };
89
+ return { state, pull, setOnline };
90
+ }
60
91
  export {
92
+ createStarfishLogObservable,
61
93
  createStarfishObservable
62
94
  };
63
95
  //# sourceMappingURL=legend.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/bindings/legend.ts"],
4
- "sourcesContent": ["import { observable } from \"@legendapp/state\"\nimport type { Observable } from \"@legendapp/state\"\nimport type { SyncManager } from \"../sync.js\"\n\nexport interface StarfishLegendState {\n data: Record<string, unknown>\n syncing: boolean\n online: boolean\n dirty: boolean\n error: string | null\n}\n\nexport interface StarfishLegendStore {\n /** The observable state tree \u2014 read fields with `.get()` inside `observer` components. */\n state: Observable<StarfishLegendState>\n pull: () => Promise<void>\n set: (modifier: (current: Record<string, unknown>) => Record<string, unknown>) => void\n flush: () => Promise<void>\n setOnline: (online: boolean) => void\n}\n\nexport interface CreateStarfishObservableOptions {\n /** Unique name for this collection (used for persistence keys when applicable). */\n name: string\n syncManager: SyncManager\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\nexport function createStarfishObservable(\n options: CreateStarfishObservableOptions,\n): StarfishLegendStore {\n const state = observable<StarfishLegendState>({\n data: {},\n syncing: false,\n online: true,\n dirty: false,\n error: null,\n })\n\n const flush = async (): Promise<void> => {\n if (state.syncing.get() || !state.dirty.get()) return\n state.syncing.set(true)\n state.error.set(null)\n try {\n await options.syncManager.push(state.data.get())\n state.data.set(options.syncManager.getData())\n state.dirty.set(false)\n } catch (err) {\n state.error.set(err instanceof Error ? err.message : String(err))\n } finally {\n state.syncing.set(false)\n }\n }\n\n const pull = async (): Promise<void> => {\n state.syncing.set(true)\n state.error.set(null)\n try {\n await options.syncManager.pull()\n state.data.set(options.syncManager.getData())\n } catch (err) {\n state.error.set(err instanceof Error ? err.message : String(err))\n } finally {\n state.syncing.set(false)\n }\n }\n\n const set = (\n modifier: (current: Record<string, unknown>) => Record<string, unknown>,\n ): void => {\n try {\n const current = state.data.get()\n const next = options.produce\n ? options.produce(\n current,\n modifier as (draft: Record<string, unknown>) => Record<string, unknown> | void,\n )\n : modifier(current)\n state.data.set(next)\n state.dirty.set(true)\n state.error.set(null)\n if (state.online.get()) flush().catch(() => {})\n } catch (err) {\n state.error.set(err instanceof Error ? err.message : String(err))\n }\n }\n\n const setOnline = (online: boolean): void => {\n state.online.set(online)\n if (online && state.dirty.get()) flush().catch(() => {})\n }\n\n return { state, pull, set, flush, setOnline }\n}\n"],
5
- "mappings": ";AAAA,SAAS,kBAAkB;AA6BpB,SAAS,yBACd,SACqB;AACrB,QAAM,QAAQ,WAAgC;AAAA,IAC5C,MAAM,CAAC;AAAA,IACP,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,OAAO;AAAA,EACT,CAAC;AAED,QAAM,QAAQ,YAA2B;AACvC,QAAI,MAAM,QAAQ,IAAI,KAAK,CAAC,MAAM,MAAM,IAAI,EAAG;AAC/C,UAAM,QAAQ,IAAI,IAAI;AACtB,UAAM,MAAM,IAAI,IAAI;AACpB,QAAI;AACF,YAAM,QAAQ,YAAY,KAAK,MAAM,KAAK,IAAI,CAAC;AAC/C,YAAM,KAAK,IAAI,QAAQ,YAAY,QAAQ,CAAC;AAC5C,YAAM,MAAM,IAAI,KAAK;AAAA,IACvB,SAAS,KAAK;AACZ,YAAM,MAAM,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IAClE,UAAE;AACA,YAAM,QAAQ,IAAI,KAAK;AAAA,IACzB;AAAA,EACF;AAEA,QAAM,OAAO,YAA2B;AACtC,UAAM,QAAQ,IAAI,IAAI;AACtB,UAAM,MAAM,IAAI,IAAI;AACpB,QAAI;AACF,YAAM,QAAQ,YAAY,KAAK;AAC/B,YAAM,KAAK,IAAI,QAAQ,YAAY,QAAQ,CAAC;AAAA,IAC9C,SAAS,KAAK;AACZ,YAAM,MAAM,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IAClE,UAAE;AACA,YAAM,QAAQ,IAAI,KAAK;AAAA,IACzB;AAAA,EACF;AAEA,QAAM,MAAM,CACV,aACS;AACT,QAAI;AACF,YAAM,UAAU,MAAM,KAAK,IAAI;AAC/B,YAAM,OAAO,QAAQ,UACjB,QAAQ;AAAA,QACN;AAAA,QACA;AAAA,MACF,IACA,SAAS,OAAO;AACpB,YAAM,KAAK,IAAI,IAAI;AACnB,YAAM,MAAM,IAAI,IAAI;AACpB,YAAM,MAAM,IAAI,IAAI;AACpB,UAAI,MAAM,OAAO,IAAI,EAAG,OAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAChD,SAAS,KAAK;AACZ,YAAM,MAAM,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IAClE;AAAA,EACF;AAEA,QAAM,YAAY,CAAC,WAA0B;AAC3C,UAAM,OAAO,IAAI,MAAM;AACvB,QAAI,UAAU,MAAM,MAAM,IAAI,EAAG,OAAM,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACzD;AAEA,SAAO,EAAE,OAAO,MAAM,KAAK,OAAO,UAAU;AAC9C;",
4
+ "sourcesContent": ["import { observable } from \"@legendapp/state\"\nimport type { Observable } from \"@legendapp/state\"\nimport type { SyncManager } from \"../sync.js\"\nimport type { AppendLogCursor, AppendElement } from \"../append-log.js\"\n\nexport interface StarfishLegendState {\n data: Record<string, unknown>\n syncing: boolean\n online: boolean\n dirty: boolean\n error: string | null\n}\n\nexport interface StarfishLegendStore {\n /** The observable state tree \u2014 read fields with `.get()` inside `observer` components. */\n state: Observable<StarfishLegendState>\n pull: () => Promise<void>\n set: (modifier: (current: Record<string, unknown>) => Record<string, unknown>) => void\n flush: () => Promise<void>\n setOnline: (online: boolean) => void\n}\n\nexport interface CreateStarfishObservableOptions {\n /** Unique name for this collection (used for persistence keys when applicable). */\n name: string\n syncManager: SyncManager\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\nexport function createStarfishObservable(\n options: CreateStarfishObservableOptions,\n): StarfishLegendStore {\n const state = observable<StarfishLegendState>({\n data: {},\n syncing: false,\n online: true,\n dirty: false,\n error: null,\n })\n\n const flush = async (): Promise<void> => {\n if (state.syncing.get() || !state.dirty.get()) return\n state.syncing.set(true)\n state.error.set(null)\n try {\n await options.syncManager.push(state.data.get())\n state.data.set(options.syncManager.getData())\n state.dirty.set(false)\n } catch (err) {\n state.error.set(err instanceof Error ? err.message : String(err))\n } finally {\n state.syncing.set(false)\n }\n }\n\n const pull = async (): Promise<void> => {\n state.syncing.set(true)\n state.error.set(null)\n try {\n await options.syncManager.pull()\n state.data.set(options.syncManager.getData())\n } catch (err) {\n state.error.set(err instanceof Error ? err.message : String(err))\n } finally {\n state.syncing.set(false)\n }\n }\n\n const set = (\n modifier: (current: Record<string, unknown>) => Record<string, unknown>,\n ): void => {\n try {\n const current = state.data.get()\n const next = options.produce\n ? options.produce(\n current,\n modifier as (draft: Record<string, unknown>) => Record<string, unknown> | void,\n )\n : modifier(current)\n state.data.set(next)\n state.dirty.set(true)\n state.error.set(null)\n if (state.online.get()) flush().catch(() => {})\n } catch (err) {\n state.error.set(err instanceof Error ? err.message : String(err))\n }\n }\n\n const setOnline = (online: boolean): void => {\n state.online.set(online)\n if (online && state.dirty.get()) flush().catch(() => {})\n }\n\n return { state, pull, set, flush, setOnline }\n}\n\n// \u2500\u2500 Append-only log binding \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// The reactive counterpart for an append-only collection, backed by an\n// `AppendLogCursor`. Read-only (a log only grows): no `set`/`flush`/`dirty`.\n// The cursor owns the items + checkpoint; persist via `getItems()` and\n// rehydrate by constructing the cursor with `initialItems`.\n//\n// The store assumes it is the SOLE driver of its cursor (it seeds from\n// `cursor.getItems()` at construction and updates only via its own `pull()`);\n// don't also call `cursor.pull()` directly, or the observable will go stale.\n\nexport interface StarfishLogObservableState {\n /** The full accumulated log, newest appended last. */\n items: AppendElement[]\n /** A `pull()` is in flight. */\n loading: boolean\n online: boolean\n error: string | null\n /** The cursor's checkpoint (max `ts` held). */\n checkpoint: number\n}\n\nexport interface StarfishLogObservableStore {\n /** The observable state tree \u2014 read fields with `.get()` inside `observer` components. */\n state: Observable<StarfishLogObservableState>\n /** Pull elements newer than the checkpoint, append them, return the new batch.\n * Errors are captured into `state.error`. */\n pull: () => Promise<AppendElement[]>\n setOnline: (online: boolean) => void\n}\n\nexport interface CreateStarfishLogObservableOptions {\n cursor: AppendLogCursor\n}\n\nexport function createStarfishLogObservable(\n options: CreateStarfishLogObservableOptions,\n): StarfishLogObservableStore {\n const { cursor } = options\n const state = observable<StarfishLogObservableState>({\n // Seed from the cursor so a warm-started cursor's items show immediately.\n items: cursor.getItems(),\n loading: false,\n online: true,\n error: null,\n checkpoint: cursor.getCheckpoint(),\n })\n\n const pull = async (): Promise<AppendElement[]> => {\n if (state.loading.get()) return []\n state.loading.set(true)\n state.error.set(null)\n try {\n const batch = await cursor.pull()\n state.items.set(cursor.getItems())\n state.checkpoint.set(cursor.getCheckpoint())\n return batch\n } catch (err) {\n state.error.set(err instanceof Error ? err.message : String(err))\n return []\n } finally {\n state.loading.set(false)\n }\n }\n\n const setOnline = (online: boolean): void => {\n state.online.set(online)\n }\n\n return { state, pull, setOnline }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * React Suspense integration for Starfish sync data.
3
+ * Creates resources that throw Promises while loading (Suspense protocol).
4
+ */
5
+ /**
6
+ * Create a Suspense-compatible resource from an async fetcher.
7
+ * The first call to `read()` triggers the fetch. While loading, `read()` throws
8
+ * a Promise (which React Suspense catches to show a fallback). Once resolved,
9
+ * `read()` returns the value synchronously.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const resource = createSuspenseResource(() => syncManager.pull())
14
+ * function MyComponent() {
15
+ * const data = resource.read() // throws while loading, returns data when ready
16
+ * return <div>{JSON.stringify(data)}</div>
17
+ * }
18
+ * ```
19
+ */
20
+ export function createSuspenseResource(fetcher) {
21
+ let status = "pending";
22
+ let result;
23
+ let error;
24
+ let promise = null;
25
+ function init() {
26
+ if (promise)
27
+ return promise;
28
+ promise = fetcher().then((value) => {
29
+ status = "resolved";
30
+ result = value;
31
+ }, (err) => {
32
+ status = "rejected";
33
+ error = err;
34
+ });
35
+ return promise;
36
+ }
37
+ return {
38
+ read() {
39
+ switch (status) {
40
+ case "pending":
41
+ throw init();
42
+ case "resolved":
43
+ return result;
44
+ case "rejected":
45
+ throw error;
46
+ }
47
+ },
48
+ };
49
+ }
@@ -1,9 +1,10 @@
1
1
  import { type StoreApi } from "zustand/vanilla";
2
2
  import { type StateStorage } from "zustand/middleware";
3
3
  import type { DevtoolsOptions } from "zustand/middleware";
4
- import type { Encryptor } from "@drakkar.software/starfish-protocol";
4
+ import type { Encryptor, PullResult } from "@drakkar.software/starfish-protocol";
5
5
  import { SyncManager } from "../sync.js";
6
- import type { StarfishCapProvider, ConflictResolver } from "../types.js";
6
+ import { AppendLogCursor, type AppendElement } from "../append-log.js";
7
+ import type { StarfishClientOptions, StarfishCapProvider, ConflictResolver, PullCache } from "../types.js";
7
8
  import type { SyncLogger } from "../logger.js";
8
9
  import type { Validator } from "../validate.js";
9
10
  export interface StarfishState {
@@ -14,6 +15,14 @@ export interface StarfishState {
14
15
  error: string | null;
15
16
  /** Last-known server hash, persisted alongside `data`/`dirty`. Restored into the bound SyncManager on hydration. */
16
17
  hash: string | null;
18
+ /**
19
+ * True when the currently-shown `data` came from the offline read-through
20
+ * cache (a cache-first {@link StarfishActions.seed} or a {@link StarfishActions.pull}
21
+ * the client served from cache because the transport was unreachable) rather
22
+ * than a live server response. A successful live pull/flush clears it. Use it
23
+ * to drive an "offline / showing last-synced data" indicator.
24
+ */
25
+ stale: boolean;
17
26
  }
18
27
  export interface StarfishActions {
19
28
  pull: () => Promise<void>;
@@ -22,6 +31,26 @@ export interface StarfishActions {
22
31
  restore: (data: Record<string, unknown>) => void;
23
32
  flush: () => Promise<void>;
24
33
  setOnline: (online: boolean) => void;
34
+ /**
35
+ * Cache-first paint: populate `data` from the client's offline read-through
36
+ * cache (decrypting in memory for E2E collections) without touching the
37
+ * network. A no-op when the client has no cache configured or there's no
38
+ * (unexpired) entry. {@link useSyncInit} calls this once before the initial
39
+ * pull; the live pull then supersedes the seeded snapshot.
40
+ */
41
+ seed: () => Promise<void>;
42
+ /**
43
+ * Apply a freshly-fetched `PullResult` to the store WITHOUT firing a network
44
+ * request. Decrypts in memory for E2E collections, conflict-merges against
45
+ * any local optimistic writes (same logic as a live pull), and clears `stale`.
46
+ *
47
+ * Primarily called automatically by the binding when
48
+ * {@link StarfishClientOptions.onRevalidated} fires (background revalidation
49
+ * delivered a fresh snapshot after a 429/5xx hit or an SWR-on-read). Also
50
+ * available for manual use when a caller holds a fresh `PullResult` it wants
51
+ * to push into the store without a second network round-trip.
52
+ */
53
+ mergeResult: (result: PullResult) => Promise<void>;
25
54
  }
26
55
  export type StarfishStore = StarfishState & StarfishActions;
27
56
  export interface CreateStarfishStoreOptions {
@@ -62,6 +91,20 @@ export interface CreateStarfishStoreOptions {
62
91
  * ```
63
92
  */
64
93
  onRemoteUpdate?: (data: Record<string, unknown>) => void;
94
+ /**
95
+ * Auto re-attempt a failed flush with exponential backoff while the store
96
+ * stays dirty + online. Omit to keep the current no-retry behavior.
97
+ *
98
+ * Defaults when the option is present: `maxRetries: 5`, `initialDelayMs: 500`,
99
+ * `maxDelayMs: 30_000`. Backoff is `min(initial * 2^attempt, max) + jitter(100ms)`.
100
+ * A successful flush resets the counter. Going offline cancels any pending retry.
101
+ * `AbortError`s are never retried.
102
+ */
103
+ flushRetry?: {
104
+ maxRetries?: number;
105
+ initialDelayMs?: number;
106
+ maxDelayMs?: number;
107
+ };
65
108
  }
66
109
  export type { DevtoolsOptions };
67
110
  export declare function createStarfishStore(options: CreateStarfishStoreOptions): StoreApi<StarfishStore>;
@@ -76,6 +119,8 @@ export declare function deriveSyncStatus(state: StarfishState): SyncStatus;
76
119
  export declare function aggregateSyncStatus(statuses: SyncStatus[]): SyncStatus;
77
120
  /** Use the full Starfish store state and actions. */
78
121
  export declare function useStarfish(store: StoreApi<StarfishStore>): StarfishStore;
122
+ /** Subscribe to a fine-grained slice of Starfish store state. Avoids re-renders on unrelated field changes. */
123
+ export declare function useStarfishState<T>(store: StoreApi<StarfishStore>, selector: (state: StarfishState) => T): T;
79
124
  /** Use only the synced data, with an optional selector for fine-grained subscriptions. */
80
125
  export declare function useStarfishData<T = Record<string, unknown>>(store: StoreApi<StarfishStore>, selector?: (data: Record<string, unknown>) => T): T;
81
126
  /** Use the derived sync status (synced | syncing | pending | error | offline). */
@@ -104,6 +149,13 @@ export declare function useConnectivity(store: StoreApi<StarfishStore>): void;
104
149
  export declare function useLastSynced(store: StoreApi<StarfishStore>): string;
105
150
  export interface SyncInitConfig {
106
151
  serverUrl: string;
152
+ /**
153
+ * Optional server namespace, forwarded to the underlying {@link StarfishClient}
154
+ * so `pullPath`/`pushPath` are rewritten to `/v1/<namespace>/…` (signed AND sent).
155
+ * Leave unset for a root-mounted server. Pass the bare name (e.g. `"octochat"`),
156
+ * not `/v1/octochat` — the `/v1/` is added by the client.
157
+ */
158
+ namespace?: string;
107
159
  capProvider?: StarfishCapProvider;
108
160
  pullPath: string;
109
161
  pushPath: string;
@@ -115,6 +167,29 @@ export interface SyncInitConfig {
115
167
  storeName?: string;
116
168
  storage?: StateStorage | false;
117
169
  fetch?: typeof globalThis.fetch;
170
+ /**
171
+ * Offline-first read-through cache for the underlying {@link StarfishClient}
172
+ * (see {@link StarfishClientOptions.cache}). When set, the store seeds from the
173
+ * last-synced ciphertext on creation (cache-first paint, decrypted in memory)
174
+ * and the live pull falls back to it when the transport is unreachable; the
175
+ * store's `stale` flag reflects whether the shown data is from cache.
176
+ */
177
+ cache?: PullCache;
178
+ /** Max age (ms) for {@link cache} entries; see {@link StarfishClientOptions.cacheMaxAgeMs}. */
179
+ cacheMaxAgeMs?: number;
180
+ /**
181
+ * HTTP status codes for which pulls fall back to the last-cached snapshot rather than
182
+ * throwing — stale-while-revalidate for transient server failures.
183
+ * See {@link StarfishClientOptions.cacheFallbackStatuses}.
184
+ * Recommended set for offline-first apps: `[429, 500, 502, 503, 504]`.
185
+ */
186
+ cacheFallbackStatuses?: number[];
187
+ /**
188
+ * Called after a background revalidation following a {@link cacheFallbackStatuses} hit:
189
+ * the server returned a live response and the fresh snapshot has been written through.
190
+ * See {@link StarfishClientOptions.onRevalidated}.
191
+ */
192
+ onRevalidated?: StarfishClientOptions["onRevalidated"];
118
193
  logger?: SyncLogger;
119
194
  validate?: Validator;
120
195
  }
@@ -128,3 +203,93 @@ export interface SyncInitConfig {
128
203
  * Pass `null` to disable sync (returns `null`).
129
204
  */
130
205
  export declare function useSyncInit(config: SyncInitConfig | null): StoreApi<StarfishStore> | null;
206
+ /**
207
+ * Config for a shared sync store — identical to {@link SyncInitConfig} EXCEPT:
208
+ * - `onData` is omitted: it is not safe to fan out a single `onRemoteUpdate`
209
+ * callback to multiple independent subscribers. Consumers should instead
210
+ * subscribe to the returned store via `store.subscribe(...)`.
211
+ * - `storeName` is REQUIRED: it is the registry key, not an optional label.
212
+ */
213
+ export type SharedSyncConfig = Omit<SyncInitConfig, "onData" | "storeName"> & {
214
+ /** Registry key AND store persistence label. Required; there is no default. */
215
+ storeName: string;
216
+ };
217
+ /**
218
+ * Return (or create) the shared zustand store for `config.storeName`.
219
+ *
220
+ * On the **first** acquire, constructs `StarfishClient` → `SyncManager` → store
221
+ * (forwarding all config fields, including `cacheFallbackStatuses` and `onRevalidated`
222
+ * for native stale-while-revalidate), then fires `seed().finally(pull())`. On every
223
+ * subsequent acquire of the same `storeName`, the existing store is returned — **no**
224
+ * new pull fires.
225
+ *
226
+ * Always pair with {@link releaseSyncStore}. Call {@link clearSyncStoreRegistry}
227
+ * on account switch / sign-out.
228
+ */
229
+ export declare function acquireSyncStore(config: SharedSyncConfig): StoreApi<StarfishStore>;
230
+ /**
231
+ * Release a previously acquired store. Decrements the refCount; on 0 the entry is
232
+ * evicted — the store, client, and sync manager are dropped and GC'd (mirrors
233
+ * `useSyncInit`'s own teardown, which simply drops the local store reference).
234
+ */
235
+ export declare function releaseSyncStore(storeName: string): void;
236
+ /**
237
+ * Clear all registry entries.
238
+ *
239
+ * Call on account switch or sign-out alongside any other per-session cache clears.
240
+ * An identity guard inside {@link acquireSyncStore} prevents any in-flight pull from
241
+ * firing against the old session's cap after this is called.
242
+ */
243
+ export declare function clearSyncStoreRegistry(): void;
244
+ /**
245
+ * React hook that returns (or creates) the shared zustand store for
246
+ * `config.storeName` — a drop-in replacement for {@link useSyncInit} when the
247
+ * same logical document is consumed from multiple components.
248
+ *
249
+ * **Key design decision — effect deps include only `storeName`:** config identity
250
+ * churn (fresh `capProvider`/`encryptor` refs per render) is intentionally ignored.
251
+ * For a given `(user, space)` the cap and keyring are functionally equivalent across
252
+ * refs, and no `onData` fan-out is needed, so the shared store never needs to rebuild
253
+ * on churn. The `configRef` pattern ensures the latest config values are captured at
254
+ * acquire-time without re-running the effect.
255
+ *
256
+ * Pass `null` to disable sync (returns `null`).
257
+ */
258
+ export declare function useSharedSyncStore(config: SharedSyncConfig | null): StoreApi<StarfishStore> | null;
259
+ export interface StarfishLogState {
260
+ /** The full accumulated log, newest appended last. */
261
+ items: AppendElement[];
262
+ /** A `pull()` is in flight. */
263
+ loading: boolean;
264
+ online: boolean;
265
+ error: string | null;
266
+ /** The cursor's checkpoint (max `ts` held). */
267
+ checkpoint: number;
268
+ }
269
+ export interface StarfishLogActions {
270
+ /** Pull elements newer than the checkpoint, append them, and return the new
271
+ * batch. Errors are captured into `error` (mirroring the SyncManager store). */
272
+ pull: () => Promise<AppendElement[]>;
273
+ setOnline: (online: boolean) => void;
274
+ }
275
+ export type StarfishLogStore = StarfishLogState & StarfishLogActions;
276
+ export interface CreateStarfishLogOptions {
277
+ cursor: AppendLogCursor;
278
+ devtools?: (storeCreator: any) => any;
279
+ }
280
+ export declare function createStarfishLog(options: CreateStarfishLogOptions): StoreApi<StarfishLogStore>;
281
+ /** Derived status for an append-log store. */
282
+ export type LogStatus = "idle" | "loading" | "error" | "offline";
283
+ /** Derive a single status from log store state. */
284
+ export declare function deriveLogStatus(state: StarfishLogState): LogStatus;
285
+ /** Use the full append-log store state and actions. */
286
+ export declare function useStarfishLog(store: StoreApi<StarfishLogStore>): StarfishLogStore;
287
+ /** Use only the accumulated items, with an optional selector for fine-grained subscriptions. */
288
+ export declare function useStarfishLogItems<T = AppendElement[]>(store: StoreApi<StarfishLogStore>, selector?: (items: AppendElement[]) => T): T;
289
+ /** Use the derived log status (idle | loading | error | offline). */
290
+ export declare function useLogStatus(store: StoreApi<StarfishLogStore>): LogStatus;
291
+ /** Subscribe to log status changes outside of React. Invoked immediately with the
292
+ * current status, then on every change. Returns an unsubscribe function. */
293
+ export declare function subscribeLogStatus(store: StoreApi<StarfishLogStore>, callback: (status: LogStatus) => void): () => void;
294
+ /** Binds browser online/offline events to the log store's setOnline action. Cleans up on unmount. */
295
+ export declare function useLogConnectivity(store: StoreApi<StarfishLogStore>): void;