@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.
- package/README.md +59 -0
- package/dist/append-log.d.ts +228 -0
- package/dist/append-log.js +267 -0
- package/dist/background-sync.js +29 -0
- package/dist/bindings/legend.d.ts +23 -0
- package/dist/bindings/legend.js +32 -0
- package/dist/bindings/legend.js.map +2 -2
- package/dist/bindings/suspense.js +49 -0
- package/dist/bindings/zustand.d.ts +167 -2
- package/dist/bindings/zustand.js +941 -82
- package/dist/bindings/zustand.js.map +4 -4
- package/dist/blob-seal.d.ts +123 -0
- package/dist/client.d.ts +270 -5
- package/dist/client.js +391 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +18 -0
- package/dist/debounced-sync.js +120 -0
- package/dist/dedup.js +35 -0
- package/dist/events.d.ts +150 -0
- package/dist/events.js +116 -0
- package/dist/events.js.map +7 -0
- package/dist/export.js +115 -0
- package/dist/fetch.d.ts +40 -0
- package/dist/fetch.js +51 -14
- package/dist/fetch.js.map +2 -2
- package/dist/history.js +61 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +1029 -94
- package/dist/index.js.map +4 -4
- package/dist/kv-cache.d.ts +63 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +80 -0
- package/dist/migrate.js +38 -0
- package/dist/mobile-lifecycle.d.ts +28 -1
- package/dist/mobile-lifecycle.js +94 -0
- package/dist/multi-store.js +92 -0
- package/dist/mutate.d.ts +39 -0
- package/dist/polling.js +52 -0
- package/dist/resolvers.js +223 -0
- package/dist/service-worker.js +55 -0
- package/dist/storage/indexeddb.js +59 -0
- package/dist/sync.d.ts +83 -0
- package/dist/sync.js +181 -0
- package/dist/types.d.ts +106 -11
- package/dist/types.js +18 -0
- package/dist/validate.js +28 -0
- package/package.json +12 -3
package/dist/bindings/legend.js
CHANGED
|
@@ -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;
|
|
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
|
|
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;
|