@spooky-sync/client-solid 0.0.1-canary.16 → 0.0.1-canary.160

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/index.cjs CHANGED
@@ -2,12 +2,13 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  let _spooky_sync_core = require("@spooky-sync/core");
3
3
  let surrealdb = require("surrealdb");
4
4
  let solid_js = require("solid-js");
5
+ let solid_js_store = require("solid-js/store");
5
6
 
6
7
  //#region src/lib/context.ts
7
- const SpookyContext = (0, solid_js.createContext)();
8
+ const Sp00kyContext = (0, solid_js.createContext)();
8
9
  function useDb() {
9
- const db = (0, solid_js.useContext)(SpookyContext);
10
- if (!db) throw new Error("useDb must be used within a <SpookyProvider>. Wrap your app in <SpookyProvider config={...}>.");
10
+ const db = (0, solid_js.useContext)(Sp00kyContext);
11
+ if (!db) throw new Error("useDb must be used within a <Sp00kyProvider>. Wrap your app in <Sp00kyProvider config={...}>.");
11
12
  return db;
12
13
  }
13
14
 
@@ -22,30 +23,56 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
22
23
  finalQuery = queryOrOptions;
23
24
  options = maybeOptions;
24
25
  } else {
25
- const contextDb = (0, solid_js.useContext)(SpookyContext);
26
- if (!contextDb) throw new Error("useQuery: No db argument provided and no SpookyContext found. Either pass a SyncedDb instance or wrap your app in <SpookyProvider>.");
26
+ const contextDb = (0, solid_js.useContext)(Sp00kyContext);
27
+ if (!contextDb) throw new Error("useQuery: No db argument provided and no Sp00kyContext found. Either pass a SyncedDb instance or wrap your app in <Sp00kyProvider>.");
27
28
  db = contextDb;
28
29
  finalQuery = dbOrQuery;
29
30
  options = queryOrOptions;
30
31
  }
31
- const [data, setData] = (0, solid_js.createSignal)(void 0);
32
32
  const [error, setError] = (0, solid_js.createSignal)(void 0);
33
33
  const [isFetched, setIsFetched] = (0, solid_js.createSignal)(false);
34
- const [unsubscribe, setUnsubscribe] = (0, solid_js.createSignal)(void 0);
34
+ const [isFetching, setIsFetching] = (0, solid_js.createSignal)(false);
35
+ const [state, setState] = (0, solid_js_store.createStore)({ value: void 0 });
36
+ const [version, setVersion] = (0, solid_js.createSignal)(0);
37
+ const data = () => {
38
+ version();
39
+ return state.value;
40
+ };
35
41
  let prevQueryString;
36
- const spooky = db.getSpooky();
37
- const initQuery = async (query) => {
42
+ let runId = 0;
43
+ let activeUnsub;
44
+ let activeHash;
45
+ const teardownActive = () => {
46
+ activeUnsub?.();
47
+ activeUnsub = void 0;
48
+ };
49
+ const sp00ky = db.getSp00ky();
50
+ const initQuery = async (query, myRun) => {
38
51
  const { hash } = await query.run();
52
+ if (myRun !== runId) return;
53
+ activeHash = hash;
39
54
  setError(void 0);
40
55
  let isFirstCall = true;
41
- const unsub = await spooky.subscribe(hash, (e) => {
42
- const data = query.isOne ? e[0] : e;
43
- setData(() => data);
44
- const hasData = query.isOne ? data != null : e.length > 0;
56
+ const unsub = await sp00ky.subscribe(hash, (e) => {
57
+ const queryData = query.isOne ? e[0] : e;
58
+ const reconcileStart = performance.now();
59
+ setState("value", (0, solid_js_store.reconcile)(queryData, { key: "id" }));
60
+ setVersion((v) => v + 1);
61
+ sp00ky.reportFrontendTiming(hash, performance.now() - reconcileStart);
62
+ const hasData = query.isOne ? queryData !== null && queryData !== void 0 : e.length > 0;
45
63
  if (!isFirstCall || hasData) setIsFetched(true);
46
64
  isFirstCall = false;
47
65
  }, { immediate: true });
48
- setUnsubscribe(() => unsub);
66
+ const unsubStatus = sp00ky.subscribeQueryStatus(hash, (status) => setIsFetching(status === "fetching"), { immediate: true });
67
+ const teardown = () => {
68
+ unsub();
69
+ unsubStatus();
70
+ };
71
+ if (myRun !== runId) {
72
+ teardown();
73
+ return;
74
+ }
75
+ activeUnsub = teardown;
49
76
  };
50
77
  (0, solid_js.createEffect)(() => {
51
78
  if (!(options?.enabled?.() ?? true)) {
@@ -54,22 +81,234 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
54
81
  }
55
82
  const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
56
83
  if (!query) return;
57
- const queryString = JSON.stringify(query);
84
+ const queryString = String(query.hash);
58
85
  if (queryString === prevQueryString) return;
59
86
  prevQueryString = queryString;
87
+ const myRun = ++runId;
88
+ teardownActive();
60
89
  setIsFetched(false);
61
- initQuery(query);
62
- (0, solid_js.onCleanup)(() => {
63
- unsubscribe()?.();
64
- });
90
+ initQuery(query, myRun);
91
+ });
92
+ (0, solid_js.onCleanup)(() => {
93
+ runId++;
94
+ teardownActive();
95
+ if (options?.deregisterOnCleanup && activeHash) sp00ky.deregisterQuery(activeHash);
65
96
  });
66
97
  const isLoading = () => {
67
98
  return !isFetched() && error() === void 0;
68
99
  };
100
+ const isSettled = () => isFetched() && !isFetching();
69
101
  return {
70
102
  data,
71
103
  error,
72
- isLoading
104
+ isLoading,
105
+ isFetching,
106
+ isSettled
107
+ };
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/lib/create-preload.ts
112
+ /**
113
+ * Reactive, fire-and-forget prewarm. Resolves the query (calling it if it's a
114
+ * function so it tracks reactive deps), dedupes on the query's stable identity
115
+ * hash, and warms it into the local cache via `db.preload`. No subscription and
116
+ * no cleanup: preload registers nothing that needs tearing down.
117
+ *
118
+ * Typical use: inside a list row, preload the detail query the user is likely
119
+ * to open next, so navigation paints from cache instead of the network.
120
+ */
121
+ function createPreload(dbOrQuery, queryOrOptions, maybeOptions) {
122
+ let db;
123
+ let finalQuery;
124
+ let options;
125
+ if (dbOrQuery instanceof SyncedDb) {
126
+ db = dbOrQuery;
127
+ finalQuery = queryOrOptions;
128
+ options = maybeOptions;
129
+ } else {
130
+ const contextDb = (0, solid_js.useContext)(Sp00kyContext);
131
+ if (!contextDb) throw new Error("createPreload: No db argument provided and no Sp00kyContext found. Either pass a SyncedDb instance or wrap your app in <Sp00kyProvider>.");
132
+ db = contextDb;
133
+ finalQuery = dbOrQuery;
134
+ options = queryOrOptions;
135
+ }
136
+ let prevHash;
137
+ (0, solid_js.createEffect)(() => {
138
+ if (!(options?.enabled?.() ?? true)) return;
139
+ const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
140
+ if (!query) return;
141
+ if (query.hash === prevHash) return;
142
+ prevHash = query.hash;
143
+ db.getSp00ky().preload(query, {
144
+ refresh: options?.refresh,
145
+ staleTime: options?.staleTime
146
+ });
147
+ });
148
+ }
149
+
150
+ //#endregion
151
+ //#region src/lib/use-sync-status.ts
152
+ /**
153
+ * Observe sync health for a "can't reach the server" banner / indicator.
154
+ *
155
+ * Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
156
+ * remote 500 on query registration, a dropped socket) are absorbed by the
157
+ * retry and never flip this; `isDegraded()` only goes true once failures
158
+ * persist for the configured number of consecutive rounds (sp00ky core config
159
+ * `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
160
+ * the next successful round. Must be used within a `<Sp00kyProvider>`.
161
+ */
162
+ function useSyncStatus() {
163
+ const db = useDb();
164
+ const [health, setHealth] = (0, solid_js.createSignal)(db.syncHealth);
165
+ (0, solid_js.onCleanup)(db.subscribeToSyncHealth(setHealth));
166
+ return {
167
+ health,
168
+ status: () => health().status,
169
+ isHealthy: () => health().status === "healthy",
170
+ isDegraded: () => health().status === "degraded",
171
+ everConnected: () => health().everConnected,
172
+ isOffline: () => health().status === "degraded" && health().everConnected
173
+ };
174
+ }
175
+
176
+ //#endregion
177
+ //#region src/lib/use-storage-status.ts
178
+ /**
179
+ * Observe how durable the LOCAL cache is, for a "no local storage" warning.
180
+ *
181
+ * Under `localEngine: 'sqlite'` with `store: 'indexeddb'` the durable store is
182
+ * the OPFS SAHPool VFS, and only ONE client per bucket can hold it open: a
183
+ * second tab of the same app cannot get it and runs in memory instead (the
184
+ * engine retries first, so a closing tab's lock is usually waited out). Must be
185
+ * used within a `<Sp00kyProvider>`.
186
+ */
187
+ function useStorageStatus() {
188
+ const db = useDb();
189
+ const [health, setHealth] = (0, solid_js.createSignal)(db.storageHealth);
190
+ (0, solid_js.onCleanup)(db.subscribeToStorageHealth(setHealth));
191
+ return {
192
+ health,
193
+ status: () => health().status,
194
+ isPersistent: () => health().status === "persistent",
195
+ isMemoryFallback: () => health().fallback
196
+ };
197
+ }
198
+
199
+ //#endregion
200
+ //#region src/lib/use-crdt-field.ts
201
+ function useCrdtField(table, recordId, field, fallbackText) {
202
+ const db = (0, solid_js.useContext)(Sp00kyContext);
203
+ if (!db) throw new Error("useCrdtField must be used within a <Sp00kyProvider>");
204
+ const [crdtField, setCrdtField] = (0, solid_js.createSignal)(null);
205
+ let currentId;
206
+ let initialized = false;
207
+ (0, solid_js.createEffect)(() => {
208
+ const id = recordId();
209
+ if (initialized && id === currentId) return;
210
+ if (currentId && crdtField()) {
211
+ db.getSp00ky().closeCrdtField(table, currentId, field);
212
+ setCrdtField(null);
213
+ }
214
+ currentId = id;
215
+ initialized = true;
216
+ if (!id) return;
217
+ const sp00ky = db.getSp00ky();
218
+ const text = fallbackText?.();
219
+ sp00ky.openCrdtField(table, id, field, text).then((cf) => {
220
+ if (currentId === id) setCrdtField(cf);
221
+ }).catch((err) => {
222
+ console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);
223
+ });
224
+ });
225
+ (0, solid_js.onCleanup)(() => {
226
+ if (currentId && crdtField()) {
227
+ db.getSp00ky().closeCrdtField(table, currentId, field);
228
+ setCrdtField(null);
229
+ }
230
+ });
231
+ return crdtField;
232
+ }
233
+
234
+ //#endregion
235
+ //#region src/lib/use-feature-flag.ts
236
+ /**
237
+ * Subscribe to a feature flag for the currently authenticated user.
238
+ *
239
+ * Returns three Solid accessors that update reactively whenever the
240
+ * server-materialized assignment in `_00_user_feature` changes. Backed by
241
+ * the same SSP + sync pipeline that powers `useQuery`, so toggling a flag
242
+ * via `spky flag enable <key>` propagates to the UI without a refresh.
243
+ *
244
+ * `enabled()` is `true` when the resolved variant exists and is not 'off'.
245
+ * For multi-variant flags, prefer `variant()` directly.
246
+ */
247
+ function useFeatureFlag(key, options) {
248
+ const handle = useDb().getSp00ky().feature(key, options);
249
+ const [variant, setVariant] = (0, solid_js.createSignal)(handle.variant());
250
+ const [payload, setPayload] = (0, solid_js.createSignal)(handle.payload());
251
+ const unsub = handle.subscribe((s) => {
252
+ setVariant(s.variant ?? options?.fallback);
253
+ setPayload(s.payload);
254
+ });
255
+ (0, solid_js.onCleanup)(() => {
256
+ unsub();
257
+ handle.close();
258
+ });
259
+ return {
260
+ variant,
261
+ payload,
262
+ enabled: () => {
263
+ const v = variant();
264
+ return v !== void 0 && v !== "off";
265
+ }
266
+ };
267
+ }
268
+
269
+ //#endregion
270
+ //#region src/lib/use-app-release.ts
271
+ async function reloadForSnapshot(snapshot) {
272
+ if (typeof window === "undefined") return;
273
+ if (snapshot.cacheBust) try {
274
+ if (window.caches) {
275
+ const keys = await window.caches.keys();
276
+ await Promise.all(keys.map((k) => window.caches.delete(k)));
277
+ }
278
+ if (navigator.serviceWorker) {
279
+ const regs = await navigator.serviceWorker.getRegistrations();
280
+ for (const r of regs) r.update().catch(() => {});
281
+ }
282
+ window.location.href = window.location.pathname + "?cb=" + Date.now();
283
+ return;
284
+ } catch {}
285
+ window.location.reload();
286
+ }
287
+ /**
288
+ * Observe the app's announced release (`_00_app_release:<app>`, written by
289
+ * `spky deploy` / `spky release`) and compare it against the running build.
290
+ *
291
+ * Typical use: mount a small "new version available — Reload" notification
292
+ * gated on `updateAvailable()`, auto-invoking `reload()` when `mandatory()`
293
+ * (guard the auto path against reload loops with a per-version marker, since
294
+ * a client can reload while the deploy is still rolling out and land on the
295
+ * old bundle again).
296
+ */
297
+ function useAppRelease(options) {
298
+ const handle = useDb().getSp00ky().appRelease(options.app, { ttl: options.ttl });
299
+ const [snapshot, setSnapshot] = (0, solid_js.createSignal)(handle.snapshot());
300
+ const unsub = handle.subscribe(setSnapshot);
301
+ (0, solid_js.onCleanup)(() => {
302
+ unsub();
303
+ handle.close();
304
+ });
305
+ const updateAvailable = () => (0, _spooky_sync_core.semverGt)(snapshot().version, options.currentVersion);
306
+ return {
307
+ latestVersion: () => snapshot().version,
308
+ updateAvailable,
309
+ mandatory: () => updateAvailable() && snapshot().mandatory,
310
+ cacheBust: () => snapshot().cacheBust,
311
+ reload: () => reloadForSnapshot(snapshot())
73
312
  };
74
313
  }
75
314
 
@@ -95,7 +334,7 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
95
334
  const validate = (file) => {
96
335
  const config = db.getBucketConfig(bucketName);
97
336
  if (!config) return;
98
- if (config.maxSize != null && file.size > config.maxSize) {
337
+ if (config.maxSize !== null && config.maxSize !== void 0 && file.size > config.maxSize) {
99
338
  const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
100
339
  throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
101
340
  }
@@ -204,9 +443,8 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
204
443
  const [error, setError] = (0, solid_js.createSignal)(null);
205
444
  let currentKey = null;
206
445
  let privateUrl = null;
207
- let refetchTrigger;
208
446
  const [refetchSignal, setRefetchSignal] = (0, solid_js.createSignal)(0);
209
- refetchTrigger = () => setRefetchSignal((n) => n + 1);
447
+ const refetchTrigger = () => setRefetchSignal((n) => n + 1);
210
448
  async function doDownload(key, filePath) {
211
449
  if (useCache) {
212
450
  const cached = downloadCache.get(key);
@@ -326,26 +564,44 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
326
564
  }
327
565
 
328
566
  //#endregion
329
- //#region src/lib/SpookyProvider.ts
330
- function SpookyProvider(props) {
567
+ //#region src/lib/Sp00kyProvider.ts
568
+ function Sp00kyProvider(props) {
331
569
  const merged = (0, solid_js.mergeProps)({ fallback: void 0 }, props);
332
570
  const [db, setDb] = (0, solid_js.createSignal)(void 0);
571
+ let disposed = false;
572
+ let live;
573
+ (0, solid_js.onCleanup)(() => {
574
+ disposed = true;
575
+ const instance = live;
576
+ live = void 0;
577
+ instance?.close();
578
+ });
333
579
  (0, solid_js.onMount)(async () => {
334
580
  try {
335
581
  const instance = new SyncedDb(merged.config);
582
+ live = instance;
336
583
  await instance.init();
584
+ if (disposed) {
585
+ await instance.close();
586
+ return;
587
+ }
588
+ if (merged.preload) try {
589
+ await merged.preload(instance);
590
+ } catch (e) {
591
+ console.error("Sp00kyProvider: preload failed; revealing UI anyway", e);
592
+ }
337
593
  setDb(() => instance);
338
594
  merged.onReady?.(instance);
339
595
  } catch (e) {
340
596
  const error = e instanceof Error ? e : new Error(String(e));
341
597
  if (merged.onError) merged.onError(error);
342
- else console.error("SpookyProvider: Failed to initialize database", error);
598
+ else console.error("Sp00kyProvider: Failed to initialize database", error);
343
599
  }
344
600
  });
345
601
  return (0, solid_js.createMemo)(() => {
346
602
  const instance = db();
347
603
  if (!instance) return merged.fallback;
348
- return (0, solid_js.createComponent)(SpookyContext.Provider, {
604
+ return (0, solid_js.createComponent)(Sp00kyContext.Provider, {
349
605
  value: instance,
350
606
  get children() {
351
607
  return merged.children;
@@ -357,69 +613,94 @@ function SpookyProvider(props) {
357
613
  //#endregion
358
614
  //#region src/index.ts
359
615
  /**
360
- * SyncedDb - A thin wrapper around spooky-ts for Solid.js integration
361
- * Delegates all logic to the underlying spooky-ts instance
616
+ * SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
617
+ * Delegates all logic to the underlying sp00ky-ts instance
362
618
  */
363
619
  var SyncedDb = class {
364
620
  constructor(config) {
365
- this.spooky = null;
621
+ this.sp00ky = null;
366
622
  this._initialized = false;
367
623
  this.config = config;
368
624
  }
369
- getSpooky() {
370
- if (!this.spooky) throw new Error("SyncedDb not initialized");
371
- return this.spooky;
625
+ getSp00ky() {
626
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
627
+ return this.sp00ky;
372
628
  }
373
629
  /**
374
- * Initialize the spooky-ts instance
630
+ * Initialize the sp00ky-ts instance
375
631
  */
376
632
  async init() {
377
633
  if (this._initialized) return;
378
- this.spooky = new _spooky_sync_core.SpookyClient(this.config);
379
- await this.spooky.init();
634
+ this.sp00ky = new _spooky_sync_core.Sp00kyClient(this.config);
635
+ await this.sp00ky.init();
380
636
  this._initialized = true;
381
637
  }
382
638
  /**
639
+ * Tear down the client: leaves the tabs broker, closes the local store and
640
+ * remote socket, and frees the wasm circuit. Without this a remounted provider
641
+ * (or an HMR reload) strands a whole client, and the abandoned wasm heaps stay
642
+ * resident because V8 cannot see how much wasm memory a dropped wrapper holds.
643
+ */
644
+ async close() {
645
+ const instance = this.sp00ky;
646
+ this.sp00ky = null;
647
+ this._initialized = false;
648
+ if (instance) await instance.close();
649
+ }
650
+ /**
383
651
  * Create a new record in the database
384
652
  */
385
653
  async create(id, payload) {
386
- if (!this.spooky) throw new Error("SyncedDb not initialized");
387
- await this.spooky.create(id, payload);
654
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
655
+ await this.sp00ky.create(id, payload);
388
656
  }
389
657
  /**
390
658
  * Update an existing record in the database
391
659
  */
392
660
  async update(tableName, recordId, payload, options) {
393
- if (!this.spooky) throw new Error("SyncedDb not initialized");
394
- await this.spooky.update(tableName, recordId, payload, options);
661
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
662
+ await this.sp00ky.update(tableName, recordId, payload, options);
395
663
  }
396
664
  /**
397
665
  * Delete an existing record in the database
398
666
  */
399
667
  async delete(tableName, selector) {
400
- if (!this.spooky) throw new Error("SyncedDb not initialized");
401
- if (typeof selector !== "string") throw new Error("Only string ID selectors are supported currently with core");
402
- await this.spooky.delete(tableName, selector);
668
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
669
+ const isRecordId = selector instanceof surrealdb.RecordId || selector?.constructor?.name === "RecordId";
670
+ let id;
671
+ if (typeof selector === "string") id = selector;
672
+ else if (isRecordId) id = `${tableName}:${selector.id}`;
673
+ else throw new Error("Only string ID or RecordId selectors are supported currently with core");
674
+ await this.sp00ky.delete(tableName, id);
675
+ }
676
+ /**
677
+ * Preload/prewarm a built query into the local cache without registering a
678
+ * live view. Fetches once and stores the rows (+ embedded related children)
679
+ * locally so a later `useQuery` for the same data paints instantly. Best-effort.
680
+ */
681
+ async preload(finalQuery, options) {
682
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
683
+ await this.sp00ky.preload(finalQuery, options);
403
684
  }
404
685
  /**
405
686
  * Query data from the database
406
687
  */
407
688
  query(table) {
408
- if (!this.spooky) throw new Error("SyncedDb not initialized");
409
- return this.spooky.query(table, {});
689
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
690
+ return this.sp00ky.query(table, {});
410
691
  }
411
692
  /**
412
693
  * Run a backend operation
413
694
  */
414
695
  async run(backend, path, payload, options) {
415
- if (!this.spooky) throw new Error("SyncedDb not initialized");
416
- await this.spooky.run(backend, path, payload, options);
696
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
697
+ await this.sp00ky.run(backend, path, payload, options);
417
698
  }
418
699
  /**
419
700
  * Authenticate with the database
420
701
  */
421
702
  async authenticate(token) {
422
- await this.spooky?.authenticate(token);
703
+ await this.sp00ky?.authenticate(token);
423
704
  return new surrealdb.RecordId("user", "me");
424
705
  }
425
706
  /**
@@ -433,48 +714,81 @@ var SyncedDb = class {
433
714
  * Sign out, clear session and local storage
434
715
  */
435
716
  async signOut() {
436
- if (!this.spooky) throw new Error("SyncedDb not initialized");
437
- await this.spooky.auth.signOut();
717
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
718
+ await this.sp00ky.auth.signOut();
438
719
  }
439
720
  /**
440
721
  * Execute a function with direct access to the remote database connection
441
722
  */
442
723
  async useRemote(fn) {
443
- if (!this.spooky) throw new Error("SyncedDb not initialized");
444
- return await this.spooky.useRemote(fn);
724
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
725
+ return await this.sp00ky.useRemote(fn);
445
726
  }
446
727
  /**
447
728
  * Access the remote database service directly
448
729
  */
449
730
  get remote() {
450
- if (!this.spooky) throw new Error("SyncedDb not initialized");
451
- return this.spooky.remoteClient;
731
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
732
+ return this.sp00ky.remoteClient;
452
733
  }
453
734
  /**
454
735
  * Access the local database service directly
455
736
  */
456
737
  get local() {
457
- if (!this.spooky) throw new Error("SyncedDb not initialized");
458
- return this.spooky.localClient;
738
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
739
+ return this.sp00ky.localClient;
459
740
  }
460
741
  /**
461
742
  * Access the auth service
462
743
  */
463
744
  get auth() {
464
- if (!this.spooky) throw new Error("SyncedDb not initialized");
465
- return this.spooky.auth;
745
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
746
+ return this.sp00ky.auth;
466
747
  }
467
748
  get pendingMutationCount() {
468
- if (!this.spooky) throw new Error("SyncedDb not initialized");
469
- return this.spooky.pendingMutationCount;
749
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
750
+ return this.sp00ky.pendingMutationCount;
751
+ }
752
+ /** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
753
+ get liveRetryCount() {
754
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
755
+ return this.sp00ky.liveRetryCount;
470
756
  }
471
757
  subscribeToPendingMutations(cb) {
472
- if (!this.spooky) throw new Error("SyncedDb not initialized");
473
- return this.spooky.subscribeToPendingMutations(cb);
758
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
759
+ return this.sp00ky.subscribeToPendingMutations(cb);
760
+ }
761
+ /** Current sync-health snapshot. See {@link useSyncStatus}. */
762
+ get syncHealth() {
763
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
764
+ return this.sp00ky.syncHealth;
765
+ }
766
+ /**
767
+ * Observe sync health. Fires immediately with the current status and again
768
+ * on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
769
+ * components; this is the imperative escape hatch.
770
+ */
771
+ subscribeToSyncHealth(cb) {
772
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
773
+ return this.sp00ky.subscribeToSyncHealth(cb);
774
+ }
775
+ /** Current local-store durability snapshot. See {@link useStorageStatus}. */
776
+ get storageHealth() {
777
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
778
+ return this.sp00ky.storageHealth;
779
+ }
780
+ /**
781
+ * Observe local-store durability. Fires immediately with the current snapshot
782
+ * and again on change. Prefer the `useStorageStatus` hook in components; this
783
+ * is the imperative escape hatch.
784
+ */
785
+ subscribeToStorageHealth(cb) {
786
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
787
+ return this.sp00ky.subscribeToStorageHealth(cb);
474
788
  }
475
789
  bucket(name) {
476
- if (!this.spooky) throw new Error("SyncedDb not initialized");
477
- return this.spooky.bucket(name);
790
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
791
+ return this.sp00ky.bucket(name);
478
792
  }
479
793
  getBucketConfig(name) {
480
794
  return this.config.schema.buckets?.find((b) => b.name === name);
@@ -483,11 +797,17 @@ var SyncedDb = class {
483
797
 
484
798
  //#endregion
485
799
  exports.RecordId = surrealdb.RecordId;
486
- exports.SpookyProvider = SpookyProvider;
800
+ exports.Sp00kyProvider = Sp00kyProvider;
487
801
  exports.SyncedDb = SyncedDb;
488
802
  exports.Uuid = surrealdb.Uuid;
803
+ exports.createPreload = createPreload;
804
+ exports.useAppRelease = useAppRelease;
805
+ exports.useCrdtField = useCrdtField;
489
806
  exports.useDb = useDb;
490
807
  exports.useDownloadFile = useDownloadFile;
808
+ exports.useFeatureFlag = useFeatureFlag;
491
809
  exports.useFileUpload = useFileUpload;
492
810
  exports.useQuery = useQuery;
811
+ exports.useStorageStatus = useStorageStatus;
812
+ exports.useSyncStatus = useSyncStatus;
493
813
  //# sourceMappingURL=index.cjs.map