@spooky-sync/client-solid 0.0.1-canary.11 → 0.0.1-canary.111

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/AGENTS.md ADDED
@@ -0,0 +1,66 @@
1
+ # `@spooky-sync/client-solid` — agent guide
2
+
3
+ ## What this package is
4
+
5
+ The SolidJS binding for sp00ky. Exposes a `Sp00kyProvider` that initializes a `Sp00kyClient` and a set of reactive hooks (`useDb`, `useQuery`, `useCrdtField`, `useFileUpload`, `useDownloadFile`). All hooks expect to be called inside a `<Sp00kyProvider>` boundary.
6
+
7
+ ## Setup pattern
8
+
9
+ ```ts
10
+ // db.ts
11
+ import type { SyncedDbConfig } from '@spooky-sync/client-solid';
12
+ import { schema, SURQL_SCHEMA } from './schema.gen'; // generated by `spky generate`
13
+
14
+ export const dbConfig: SyncedDbConfig<typeof schema> = {
15
+ schema,
16
+ schemaSurql: SURQL_SCHEMA,
17
+ database: {
18
+ namespace: 'main',
19
+ database: 'app',
20
+ endpoint: 'ws://localhost:8666/rpc',
21
+ store: 'memory', // or 'indexeddb' for persistence
22
+ persistenceClient: 'localstorage',
23
+ },
24
+ };
25
+ ```
26
+
27
+ ```tsx
28
+ // App.tsx
29
+ <Sp00kyProvider config={dbConfig}>{/* app */}</Sp00kyProvider>
30
+ ```
31
+
32
+ ## Key hooks
33
+
34
+ - **`useDb<typeof schema>()`** — returns the `SyncedDb<S>` instance. Methods:
35
+ - `db.create(id, payload)` — `id` is a full record ID like `'thread:abc'`.
36
+ - `db.update(table, id, payload, options?)` — `options.debounced` coalesces updates.
37
+ - `db.delete(table, idOrSelector)`.
38
+ - `db.query(table)` — returns a `QueryBuilder`. Chain `.related()`, `.orderBy()`, `.limit()`, etc., end with `.build()`.
39
+ - `db.run(backend, route, payload)` — call a backend RPC route.
40
+ - `db.bucket(name)` — get a `BucketHandle` for file storage.
41
+ - `db.useRemote(fn)` — escape hatch to the raw `Surreal` client (skips cache).
42
+ - `db.authenticate(token)`, `db.signOut()`, `db.auth`.
43
+ - `db.pendingMutationCount`, `db.subscribeToPendingMutations(cb)`.
44
+ - **`useQuery(() => db.query(...).build())`** — reactive query. Returns `{ data, status, error, ... }` accessors. The factory function is tracked, so passing reactive params (signals) re-runs the query.
45
+ - **`useCrdtField(table, () => recordId, field, () => valueAccessor)`** — wires a CRDT text field to a Loro doc. Pair with `db.update(table, id, { [field]: newValue }, { debounced: true })` so rapid keystrokes don't flood the queue. *All four arguments take accessor functions where reactive — that's deliberate, for SolidJS tracking.*
46
+ - **`useFileUpload()`** / **`useDownloadFile()`** — bucket helpers; the upload result includes the storage path you write into a record column.
47
+
48
+ ## Re-exports for convenience
49
+
50
+ - `RecordId`, `Uuid` from `surrealdb`.
51
+ - Query-builder types: `TableModel`, `TableNames`, `GetTable`, `QueryResult`, etc. (see `@spooky-sync/query-builder/AGENTS.md`).
52
+ - `Model<S, T>`, `GenericModel`, `ModelPayload` — typed row shapes.
53
+
54
+ ## Common gotchas
55
+
56
+ - **`useDb()` requires `<typeof schema>`.** Without the generic, all calls fall back to `unknown` and you lose type safety.
57
+ - **CRDT fields are not regular columns.** Don't read or write them via `useQuery` — read with `useCrdtField`, write via `db.update` with `{ debounced: true }`.
58
+ - **Generate IDs explicitly.** `const id = new Uuid().toString()`, then `db.create(\`thread:\${id}\`, ...)`. SurrealDB's auto-id only fires on direct DB writes, not through the sync queue.
59
+ - **`useQuery` factories must call `.build()` (or `.all()`, `.first()`, etc.).** A bare `db.query('thread')` is a builder, not a query — `useQuery` will throw or return forever-loading.
60
+ - **Provider is mandatory.** Calling any hook outside `<Sp00kyProvider>` throws.
61
+
62
+ ## Pointers
63
+
64
+ - Sync engine: `node_modules/@spooky-sync/core/AGENTS.md`
65
+ - Query builder DSL: `node_modules/@spooky-sync/query-builder/AGENTS.md`
66
+ - Schema authoring + codegen: `node_modules/@spooky-sync/cli/AGENTS.md`
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,125 @@ 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/use-sync-status.ts
112
+ /**
113
+ * Observe sync health for a "can't reach the server" banner / indicator.
114
+ *
115
+ * Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
116
+ * remote 500 on query registration, a dropped socket) are absorbed by the
117
+ * retry and never flip this; `isDegraded()` only goes true once failures
118
+ * persist for the configured number of consecutive rounds (sp00ky core config
119
+ * `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
120
+ * the next successful round. Must be used within a `<Sp00kyProvider>`.
121
+ */
122
+ function useSyncStatus() {
123
+ const db = useDb();
124
+ const [health, setHealth] = (0, solid_js.createSignal)(db.syncHealth);
125
+ (0, solid_js.onCleanup)(db.subscribeToSyncHealth(setHealth));
126
+ return {
127
+ health,
128
+ status: () => health().status,
129
+ isHealthy: () => health().status === "healthy",
130
+ isDegraded: () => health().status === "degraded",
131
+ everConnected: () => health().everConnected,
132
+ isOffline: () => health().status === "degraded" && health().everConnected
133
+ };
134
+ }
135
+
136
+ //#endregion
137
+ //#region src/lib/use-crdt-field.ts
138
+ function useCrdtField(table, recordId, field, fallbackText) {
139
+ const db = (0, solid_js.useContext)(Sp00kyContext);
140
+ if (!db) throw new Error("useCrdtField must be used within a <Sp00kyProvider>");
141
+ const [crdtField, setCrdtField] = (0, solid_js.createSignal)(null);
142
+ let currentId;
143
+ let initialized = false;
144
+ (0, solid_js.createEffect)(() => {
145
+ const id = recordId();
146
+ if (initialized && id === currentId) return;
147
+ if (currentId && crdtField()) {
148
+ db.getSp00ky().closeCrdtField(table, currentId, field);
149
+ setCrdtField(null);
150
+ }
151
+ currentId = id;
152
+ initialized = true;
153
+ if (!id) return;
154
+ const sp00ky = db.getSp00ky();
155
+ const text = fallbackText?.();
156
+ sp00ky.openCrdtField(table, id, field, text).then((cf) => {
157
+ if (currentId === id) setCrdtField(cf);
158
+ }).catch((err) => {
159
+ console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);
160
+ });
161
+ });
162
+ (0, solid_js.onCleanup)(() => {
163
+ if (currentId && crdtField()) {
164
+ db.getSp00ky().closeCrdtField(table, currentId, field);
165
+ setCrdtField(null);
166
+ }
167
+ });
168
+ return crdtField;
169
+ }
170
+
171
+ //#endregion
172
+ //#region src/lib/use-feature-flag.ts
173
+ /**
174
+ * Subscribe to a feature flag for the currently authenticated user.
175
+ *
176
+ * Returns three Solid accessors that update reactively whenever the
177
+ * server-materialized assignment in `_00_user_feature` changes. Backed by
178
+ * the same SSP + sync pipeline that powers `useQuery`, so toggling a flag
179
+ * via `spky flag enable <key>` propagates to the UI without a refresh.
180
+ *
181
+ * `enabled()` is `true` when the resolved variant exists and is not 'off'.
182
+ * For multi-variant flags, prefer `variant()` directly.
183
+ */
184
+ function useFeatureFlag(key, options) {
185
+ const handle = useDb().getSp00ky().feature(key, options);
186
+ const [variant, setVariant] = (0, solid_js.createSignal)(handle.variant());
187
+ const [payload, setPayload] = (0, solid_js.createSignal)(handle.payload());
188
+ const unsub = handle.subscribe((s) => {
189
+ setVariant(s.variant ?? options?.fallback);
190
+ setPayload(s.payload);
191
+ });
192
+ (0, solid_js.onCleanup)(() => {
193
+ unsub();
194
+ handle.close();
195
+ });
196
+ return {
197
+ variant,
198
+ payload,
199
+ enabled: () => {
200
+ const v = variant();
201
+ return v !== void 0 && v !== "off";
202
+ }
73
203
  };
74
204
  }
75
205
 
@@ -95,7 +225,7 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
95
225
  const validate = (file) => {
96
226
  const config = db.getBucketConfig(bucketName);
97
227
  if (!config) return;
98
- if (config.maxSize != null && file.size > config.maxSize) {
228
+ if (config.maxSize !== null && config.maxSize !== void 0 && file.size > config.maxSize) {
99
229
  const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
100
230
  throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
101
231
  }
@@ -204,9 +334,8 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
204
334
  const [error, setError] = (0, solid_js.createSignal)(null);
205
335
  let currentKey = null;
206
336
  let privateUrl = null;
207
- let refetchTrigger;
208
337
  const [refetchSignal, setRefetchSignal] = (0, solid_js.createSignal)(0);
209
- refetchTrigger = () => setRefetchSignal((n) => n + 1);
338
+ const refetchTrigger = () => setRefetchSignal((n) => n + 1);
210
339
  async function doDownload(key, filePath) {
211
340
  if (useCache) {
212
341
  const cached = downloadCache.get(key);
@@ -326,8 +455,8 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
326
455
  }
327
456
 
328
457
  //#endregion
329
- //#region src/lib/SpookyProvider.ts
330
- function SpookyProvider(props) {
458
+ //#region src/lib/Sp00kyProvider.ts
459
+ function Sp00kyProvider(props) {
331
460
  const merged = (0, solid_js.mergeProps)({ fallback: void 0 }, props);
332
461
  const [db, setDb] = (0, solid_js.createSignal)(void 0);
333
462
  (0, solid_js.onMount)(async () => {
@@ -339,13 +468,13 @@ function SpookyProvider(props) {
339
468
  } catch (e) {
340
469
  const error = e instanceof Error ? e : new Error(String(e));
341
470
  if (merged.onError) merged.onError(error);
342
- else console.error("SpookyProvider: Failed to initialize database", error);
471
+ else console.error("Sp00kyProvider: Failed to initialize database", error);
343
472
  }
344
473
  });
345
474
  return (0, solid_js.createMemo)(() => {
346
475
  const instance = db();
347
476
  if (!instance) return merged.fallback;
348
- return (0, solid_js.createComponent)(SpookyContext.Provider, {
477
+ return (0, solid_js.createComponent)(Sp00kyContext.Provider, {
349
478
  value: instance,
350
479
  get children() {
351
480
  return merged.children;
@@ -357,69 +486,73 @@ function SpookyProvider(props) {
357
486
  //#endregion
358
487
  //#region src/index.ts
359
488
  /**
360
- * SyncedDb - A thin wrapper around spooky-ts for Solid.js integration
361
- * Delegates all logic to the underlying spooky-ts instance
489
+ * SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
490
+ * Delegates all logic to the underlying sp00ky-ts instance
362
491
  */
363
492
  var SyncedDb = class {
364
493
  constructor(config) {
365
- this.spooky = null;
494
+ this.sp00ky = null;
366
495
  this._initialized = false;
367
496
  this.config = config;
368
497
  }
369
- getSpooky() {
370
- if (!this.spooky) throw new Error("SyncedDb not initialized");
371
- return this.spooky;
498
+ getSp00ky() {
499
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
500
+ return this.sp00ky;
372
501
  }
373
502
  /**
374
- * Initialize the spooky-ts instance
503
+ * Initialize the sp00ky-ts instance
375
504
  */
376
505
  async init() {
377
506
  if (this._initialized) return;
378
- this.spooky = new _spooky_sync_core.SpookyClient(this.config);
379
- await this.spooky.init();
507
+ this.sp00ky = new _spooky_sync_core.Sp00kyClient(this.config);
508
+ await this.sp00ky.init();
380
509
  this._initialized = true;
381
510
  }
382
511
  /**
383
512
  * Create a new record in the database
384
513
  */
385
514
  async create(id, payload) {
386
- if (!this.spooky) throw new Error("SyncedDb not initialized");
387
- await this.spooky.create(id, payload);
515
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
516
+ await this.sp00ky.create(id, payload);
388
517
  }
389
518
  /**
390
519
  * Update an existing record in the database
391
520
  */
392
521
  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);
522
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
523
+ await this.sp00ky.update(tableName, recordId, payload, options);
395
524
  }
396
525
  /**
397
526
  * Delete an existing record in the database
398
527
  */
399
528
  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);
529
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
530
+ const isRecordId = selector instanceof surrealdb.RecordId || selector?.constructor?.name === "RecordId";
531
+ let id;
532
+ if (typeof selector === "string") id = selector;
533
+ else if (isRecordId) id = `${tableName}:${selector.id}`;
534
+ else throw new Error("Only string ID or RecordId selectors are supported currently with core");
535
+ await this.sp00ky.delete(tableName, id);
403
536
  }
404
537
  /**
405
538
  * Query data from the database
406
539
  */
407
540
  query(table) {
408
- if (!this.spooky) throw new Error("SyncedDb not initialized");
409
- return this.spooky.query(table, {});
541
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
542
+ return this.sp00ky.query(table, {});
410
543
  }
411
544
  /**
412
545
  * Run a backend operation
413
546
  */
414
547
  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);
548
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
549
+ await this.sp00ky.run(backend, path, payload, options);
417
550
  }
418
551
  /**
419
552
  * Authenticate with the database
420
553
  */
421
554
  async authenticate(token) {
422
- await this.spooky?.authenticate(token);
555
+ await this.sp00ky?.authenticate(token);
423
556
  return new surrealdb.RecordId("user", "me");
424
557
  }
425
558
  /**
@@ -433,48 +566,67 @@ var SyncedDb = class {
433
566
  * Sign out, clear session and local storage
434
567
  */
435
568
  async signOut() {
436
- if (!this.spooky) throw new Error("SyncedDb not initialized");
437
- await this.spooky.auth.signOut();
569
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
570
+ await this.sp00ky.auth.signOut();
438
571
  }
439
572
  /**
440
573
  * Execute a function with direct access to the remote database connection
441
574
  */
442
575
  async useRemote(fn) {
443
- if (!this.spooky) throw new Error("SyncedDb not initialized");
444
- return await this.spooky.useRemote(fn);
576
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
577
+ return await this.sp00ky.useRemote(fn);
445
578
  }
446
579
  /**
447
580
  * Access the remote database service directly
448
581
  */
449
582
  get remote() {
450
- if (!this.spooky) throw new Error("SyncedDb not initialized");
451
- return this.spooky.remoteClient;
583
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
584
+ return this.sp00ky.remoteClient;
452
585
  }
453
586
  /**
454
587
  * Access the local database service directly
455
588
  */
456
589
  get local() {
457
- if (!this.spooky) throw new Error("SyncedDb not initialized");
458
- return this.spooky.localClient;
590
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
591
+ return this.sp00ky.localClient;
459
592
  }
460
593
  /**
461
594
  * Access the auth service
462
595
  */
463
596
  get auth() {
464
- if (!this.spooky) throw new Error("SyncedDb not initialized");
465
- return this.spooky.auth;
597
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
598
+ return this.sp00ky.auth;
466
599
  }
467
600
  get pendingMutationCount() {
468
- if (!this.spooky) throw new Error("SyncedDb not initialized");
469
- return this.spooky.pendingMutationCount;
601
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
602
+ return this.sp00ky.pendingMutationCount;
603
+ }
604
+ /** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
605
+ get liveRetryCount() {
606
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
607
+ return this.sp00ky.liveRetryCount;
470
608
  }
471
609
  subscribeToPendingMutations(cb) {
472
- if (!this.spooky) throw new Error("SyncedDb not initialized");
473
- return this.spooky.subscribeToPendingMutations(cb);
610
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
611
+ return this.sp00ky.subscribeToPendingMutations(cb);
612
+ }
613
+ /** Current sync-health snapshot. See {@link useSyncStatus}. */
614
+ get syncHealth() {
615
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
616
+ return this.sp00ky.syncHealth;
617
+ }
618
+ /**
619
+ * Observe sync health. Fires immediately with the current status and again
620
+ * on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
621
+ * components; this is the imperative escape hatch.
622
+ */
623
+ subscribeToSyncHealth(cb) {
624
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
625
+ return this.sp00ky.subscribeToSyncHealth(cb);
474
626
  }
475
627
  bucket(name) {
476
- if (!this.spooky) throw new Error("SyncedDb not initialized");
477
- return this.spooky.bucket(name);
628
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
629
+ return this.sp00ky.bucket(name);
478
630
  }
479
631
  getBucketConfig(name) {
480
632
  return this.config.schema.buckets?.find((b) => b.name === name);
@@ -483,11 +635,14 @@ var SyncedDb = class {
483
635
 
484
636
  //#endregion
485
637
  exports.RecordId = surrealdb.RecordId;
486
- exports.SpookyProvider = SpookyProvider;
638
+ exports.Sp00kyProvider = Sp00kyProvider;
487
639
  exports.SyncedDb = SyncedDb;
488
640
  exports.Uuid = surrealdb.Uuid;
641
+ exports.useCrdtField = useCrdtField;
489
642
  exports.useDb = useDb;
490
643
  exports.useDownloadFile = useDownloadFile;
644
+ exports.useFeatureFlag = useFeatureFlag;
491
645
  exports.useFileUpload = useFileUpload;
492
646
  exports.useQuery = useQuery;
647
+ exports.useSyncStatus = useSyncStatus;
493
648
  //# sourceMappingURL=index.cjs.map