@spooky-sync/client-solid 0.0.1-canary.1 → 0.0.1-canary.100

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,27 +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
- const unsub = await spooky.subscribe(hash, (e) => {
41
- const data = query.isOne ? e[0] : e;
42
- setData(() => data);
43
- setIsFetched(true);
55
+ let isFirstCall = true;
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;
63
+ if (!isFirstCall || hasData) setIsFetched(true);
64
+ isFirstCall = false;
44
65
  }, { immediate: true });
45
- 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;
46
76
  };
47
77
  (0, solid_js.createEffect)(() => {
48
78
  if (!(options?.enabled?.() ?? true)) {
@@ -51,14 +81,18 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
51
81
  }
52
82
  const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
53
83
  if (!query) return;
54
- const queryString = JSON.stringify(query);
84
+ const queryString = String(query.hash);
55
85
  if (queryString === prevQueryString) return;
56
86
  prevQueryString = queryString;
87
+ const myRun = ++runId;
88
+ teardownActive();
57
89
  setIsFetched(false);
58
- initQuery(query);
59
- (0, solid_js.onCleanup)(() => {
60
- unsubscribe?.();
61
- });
90
+ initQuery(query, myRun);
91
+ });
92
+ (0, solid_js.onCleanup)(() => {
93
+ runId++;
94
+ teardownActive();
95
+ if (options?.deregisterOnCleanup && activeHash) sp00ky.deregisterQuery(activeHash);
62
96
  });
63
97
  const isLoading = () => {
64
98
  return !isFetched() && error() === void 0;
@@ -66,13 +100,359 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
66
100
  return {
67
101
  data,
68
102
  error,
69
- isLoading
103
+ isLoading,
104
+ isFetching
105
+ };
106
+ }
107
+
108
+ //#endregion
109
+ //#region src/lib/use-sync-status.ts
110
+ /**
111
+ * Observe sync health for a "can't reach the server" banner / indicator.
112
+ *
113
+ * Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
114
+ * remote 500 on query registration, a dropped socket) are absorbed by the
115
+ * retry and never flip this; `isDegraded()` only goes true once failures
116
+ * persist for the configured number of consecutive rounds (sp00ky core config
117
+ * `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
118
+ * the next successful round. Must be used within a `<Sp00kyProvider>`.
119
+ */
120
+ function useSyncStatus() {
121
+ const db = useDb();
122
+ const [health, setHealth] = (0, solid_js.createSignal)(db.syncHealth);
123
+ (0, solid_js.onCleanup)(db.subscribeToSyncHealth(setHealth));
124
+ return {
125
+ health,
126
+ status: () => health().status,
127
+ isHealthy: () => health().status === "healthy",
128
+ isDegraded: () => health().status === "degraded"
129
+ };
130
+ }
131
+
132
+ //#endregion
133
+ //#region src/lib/use-crdt-field.ts
134
+ function useCrdtField(table, recordId, field, fallbackText) {
135
+ const db = (0, solid_js.useContext)(Sp00kyContext);
136
+ if (!db) throw new Error("useCrdtField must be used within a <Sp00kyProvider>");
137
+ const [crdtField, setCrdtField] = (0, solid_js.createSignal)(null);
138
+ let currentId;
139
+ let initialized = false;
140
+ (0, solid_js.createEffect)(() => {
141
+ const id = recordId();
142
+ if (initialized && id === currentId) return;
143
+ if (currentId && crdtField()) {
144
+ db.getSp00ky().closeCrdtField(table, currentId, field);
145
+ setCrdtField(null);
146
+ }
147
+ currentId = id;
148
+ initialized = true;
149
+ if (!id) return;
150
+ const sp00ky = db.getSp00ky();
151
+ const text = fallbackText?.();
152
+ sp00ky.openCrdtField(table, id, field, text).then((cf) => {
153
+ if (currentId === id) setCrdtField(cf);
154
+ }).catch((err) => {
155
+ console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);
156
+ });
157
+ });
158
+ (0, solid_js.onCleanup)(() => {
159
+ if (currentId && crdtField()) {
160
+ db.getSp00ky().closeCrdtField(table, currentId, field);
161
+ setCrdtField(null);
162
+ }
163
+ });
164
+ return crdtField;
165
+ }
166
+
167
+ //#endregion
168
+ //#region src/lib/use-feature-flag.ts
169
+ /**
170
+ * Subscribe to a feature flag for the currently authenticated user.
171
+ *
172
+ * Returns three Solid accessors that update reactively whenever the
173
+ * server-materialized assignment in `_00_user_feature` changes. Backed by
174
+ * the same SSP + sync pipeline that powers `useQuery`, so toggling a flag
175
+ * via `spky flag enable <key>` propagates to the UI without a refresh.
176
+ *
177
+ * `enabled()` is `true` when the resolved variant exists and is not 'off'.
178
+ * For multi-variant flags, prefer `variant()` directly.
179
+ */
180
+ function useFeatureFlag(key, options) {
181
+ const handle = useDb().getSp00ky().feature(key, options);
182
+ const [variant, setVariant] = (0, solid_js.createSignal)(handle.variant());
183
+ const [payload, setPayload] = (0, solid_js.createSignal)(handle.payload());
184
+ const unsub = handle.subscribe((s) => {
185
+ setVariant(s.variant ?? options?.fallback);
186
+ setPayload(s.payload);
187
+ });
188
+ (0, solid_js.onCleanup)(() => {
189
+ unsub();
190
+ handle.close();
191
+ });
192
+ return {
193
+ variant,
194
+ payload,
195
+ enabled: () => {
196
+ const v = variant();
197
+ return v !== void 0 && v !== "off";
198
+ }
199
+ };
200
+ }
201
+
202
+ //#endregion
203
+ //#region src/lib/use-file-upload.ts
204
+ function useFileUpload(dbOrBucketName, maybeBucketName) {
205
+ let db;
206
+ let bucketName;
207
+ if (typeof dbOrBucketName === "string") {
208
+ db = useDb();
209
+ bucketName = dbOrBucketName;
210
+ } else {
211
+ db = dbOrBucketName;
212
+ bucketName = maybeBucketName;
213
+ }
214
+ const [isUploading, setIsUploading] = (0, solid_js.createSignal)(false);
215
+ const [error, setError] = (0, solid_js.createSignal)(null);
216
+ const objectUrls = [];
217
+ (0, solid_js.onCleanup)(() => {
218
+ for (const url of objectUrls) URL.revokeObjectURL(url);
219
+ });
220
+ const clearError = () => setError(null);
221
+ const validate = (file) => {
222
+ const config = db.getBucketConfig(bucketName);
223
+ if (!config) return;
224
+ if (config.maxSize !== null && config.maxSize !== void 0 && file.size > config.maxSize) {
225
+ const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
226
+ throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
227
+ }
228
+ if (config.allowedExtensions && config.allowedExtensions.length > 0) {
229
+ const fileName = file.name;
230
+ if (fileName) {
231
+ const ext = fileName.split(".").pop()?.toLowerCase();
232
+ if (!ext || !config.allowedExtensions.includes(ext)) throw new Error(`File type not allowed. Accepted: ${config.allowedExtensions.join(", ")}.`);
233
+ }
234
+ }
235
+ };
236
+ const upload = async (path, file) => {
237
+ setError(null);
238
+ try {
239
+ validate(file);
240
+ } catch (e) {
241
+ setError(e instanceof Error ? e : new Error(String(e)));
242
+ return;
243
+ }
244
+ setIsUploading(true);
245
+ try {
246
+ const bytes = await (0, _spooky_sync_core.fileToUint8Array)(file);
247
+ await db.bucket(bucketName).put(path, bytes);
248
+ } catch (e) {
249
+ setError(e instanceof Error ? e : new Error(String(e)));
250
+ } finally {
251
+ setIsUploading(false);
252
+ }
253
+ };
254
+ const download = async (path) => {
255
+ setError(null);
256
+ try {
257
+ const content = await db.bucket(bucketName).get(path);
258
+ if (!content) return null;
259
+ const objectUrl = URL.createObjectURL(new Blob([content]));
260
+ objectUrls.push(objectUrl);
261
+ return objectUrl;
262
+ } catch (e) {
263
+ setError(e instanceof Error ? e : new Error(String(e)));
264
+ return null;
265
+ }
266
+ };
267
+ const remove = async (path) => {
268
+ setError(null);
269
+ try {
270
+ await db.bucket(bucketName).delete(path);
271
+ } catch (e) {
272
+ setError(e instanceof Error ? e : new Error(String(e)));
273
+ }
274
+ };
275
+ const exists = async (path) => {
276
+ setError(null);
277
+ try {
278
+ return await db.bucket(bucketName).exists(path);
279
+ } catch (e) {
280
+ setError(e instanceof Error ? e : new Error(String(e)));
281
+ return false;
282
+ }
283
+ };
284
+ return {
285
+ isUploading,
286
+ error,
287
+ clearError,
288
+ upload,
289
+ download,
290
+ remove,
291
+ exists
70
292
  };
71
293
  }
72
294
 
73
295
  //#endregion
74
- //#region src/lib/SpookyProvider.ts
75
- function SpookyProvider(props) {
296
+ //#region src/lib/use-download-file.ts
297
+ const downloadCache = /* @__PURE__ */ new Map();
298
+ const inflightRequests = /* @__PURE__ */ new Map();
299
+ function cacheKey(bucket, path) {
300
+ return `${bucket}:${path}`;
301
+ }
302
+ function releaseEntry(key) {
303
+ const entry = downloadCache.get(key);
304
+ if (!entry) return;
305
+ entry.refCount--;
306
+ if (entry.refCount <= 0) {
307
+ URL.revokeObjectURL(entry.url);
308
+ downloadCache.delete(key);
309
+ }
310
+ }
311
+ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeOptions) {
312
+ let db;
313
+ let bucketName;
314
+ let path;
315
+ let options;
316
+ if (typeof dbOrBucketName === "string") {
317
+ db = useDb();
318
+ bucketName = dbOrBucketName;
319
+ path = bucketNameOrPath;
320
+ options = pathOrOptions ?? {};
321
+ } else {
322
+ db = dbOrBucketName;
323
+ bucketName = bucketNameOrPath;
324
+ path = pathOrOptions;
325
+ options = maybeOptions ?? {};
326
+ }
327
+ const useCache = options.cache !== false;
328
+ const [url, setUrl] = (0, solid_js.createSignal)(null);
329
+ const [isLoading, setIsLoading] = (0, solid_js.createSignal)(false);
330
+ const [error, setError] = (0, solid_js.createSignal)(null);
331
+ let currentKey = null;
332
+ let privateUrl = null;
333
+ const [refetchSignal, setRefetchSignal] = (0, solid_js.createSignal)(0);
334
+ const refetchTrigger = () => setRefetchSignal((n) => n + 1);
335
+ async function doDownload(key, filePath) {
336
+ if (useCache) {
337
+ const cached = downloadCache.get(key);
338
+ if (cached) {
339
+ cached.refCount++;
340
+ currentKey = key;
341
+ return cached.url;
342
+ }
343
+ const inflight = inflightRequests.get(key);
344
+ if (inflight) {
345
+ const result = await inflight;
346
+ if (result) {
347
+ const entry = downloadCache.get(key);
348
+ if (entry) {
349
+ entry.refCount++;
350
+ currentKey = key;
351
+ }
352
+ }
353
+ return result;
354
+ }
355
+ const promise = (async () => {
356
+ const content = await db.bucket(bucketName).get(filePath);
357
+ if (!content) return null;
358
+ const objectUrl = URL.createObjectURL(new Blob([content]));
359
+ downloadCache.set(key, {
360
+ url: objectUrl,
361
+ refCount: 1
362
+ });
363
+ return objectUrl;
364
+ })();
365
+ inflightRequests.set(key, promise);
366
+ try {
367
+ const result = await promise;
368
+ currentKey = key;
369
+ return result;
370
+ } finally {
371
+ inflightRequests.delete(key);
372
+ }
373
+ } else {
374
+ const content = await db.bucket(bucketName).get(filePath);
375
+ if (!content) return null;
376
+ const objectUrl = URL.createObjectURL(new Blob([content]));
377
+ privateUrl = objectUrl;
378
+ return objectUrl;
379
+ }
380
+ }
381
+ function releaseCurrentEntry() {
382
+ if (useCache && currentKey) {
383
+ releaseEntry(currentKey);
384
+ currentKey = null;
385
+ }
386
+ if (!useCache && privateUrl) {
387
+ URL.revokeObjectURL(privateUrl);
388
+ privateUrl = null;
389
+ }
390
+ }
391
+ (0, solid_js.createEffect)(() => {
392
+ const filePath = path();
393
+ refetchSignal();
394
+ releaseCurrentEntry();
395
+ if (!filePath) {
396
+ setUrl(null);
397
+ setIsLoading(false);
398
+ setError(null);
399
+ return;
400
+ }
401
+ const key = cacheKey(bucketName, filePath);
402
+ if (useCache) {
403
+ const cached = downloadCache.get(key);
404
+ if (cached) {
405
+ cached.refCount++;
406
+ currentKey = key;
407
+ setUrl(cached.url);
408
+ setIsLoading(false);
409
+ setError(null);
410
+ return;
411
+ }
412
+ }
413
+ let cancelled = false;
414
+ setIsLoading(true);
415
+ setError(null);
416
+ doDownload(key, filePath).then((result) => {
417
+ if (!cancelled) {
418
+ setUrl(result);
419
+ setIsLoading(false);
420
+ }
421
+ }, (err) => {
422
+ if (!cancelled) {
423
+ setError(err instanceof Error ? err : new Error(String(err)));
424
+ setIsLoading(false);
425
+ }
426
+ });
427
+ (0, solid_js.onCleanup)(() => {
428
+ cancelled = true;
429
+ });
430
+ });
431
+ (0, solid_js.onCleanup)(() => {
432
+ releaseCurrentEntry();
433
+ });
434
+ const refetch = () => {
435
+ if (useCache && currentKey) {
436
+ const entry = downloadCache.get(currentKey);
437
+ if (entry) {
438
+ URL.revokeObjectURL(entry.url);
439
+ downloadCache.delete(currentKey);
440
+ }
441
+ currentKey = null;
442
+ }
443
+ refetchTrigger();
444
+ };
445
+ return {
446
+ url,
447
+ isLoading,
448
+ error,
449
+ refetch
450
+ };
451
+ }
452
+
453
+ //#endregion
454
+ //#region src/lib/Sp00kyProvider.ts
455
+ function Sp00kyProvider(props) {
76
456
  const merged = (0, solid_js.mergeProps)({ fallback: void 0 }, props);
77
457
  const [db, setDb] = (0, solid_js.createSignal)(void 0);
78
458
  (0, solid_js.onMount)(async () => {
@@ -84,13 +464,13 @@ function SpookyProvider(props) {
84
464
  } catch (e) {
85
465
  const error = e instanceof Error ? e : new Error(String(e));
86
466
  if (merged.onError) merged.onError(error);
87
- else console.error("SpookyProvider: Failed to initialize database", error);
467
+ else console.error("Sp00kyProvider: Failed to initialize database", error);
88
468
  }
89
469
  });
90
470
  return (0, solid_js.createMemo)(() => {
91
471
  const instance = db();
92
472
  if (!instance) return merged.fallback;
93
- return (0, solid_js.createComponent)(SpookyContext.Provider, {
473
+ return (0, solid_js.createComponent)(Sp00kyContext.Provider, {
94
474
  value: instance,
95
475
  get children() {
96
476
  return merged.children;
@@ -102,69 +482,73 @@ function SpookyProvider(props) {
102
482
  //#endregion
103
483
  //#region src/index.ts
104
484
  /**
105
- * SyncedDb - A thin wrapper around spooky-ts for Solid.js integration
106
- * Delegates all logic to the underlying spooky-ts instance
485
+ * SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
486
+ * Delegates all logic to the underlying sp00ky-ts instance
107
487
  */
108
488
  var SyncedDb = class {
109
489
  constructor(config) {
110
- this.spooky = null;
490
+ this.sp00ky = null;
111
491
  this._initialized = false;
112
492
  this.config = config;
113
493
  }
114
- getSpooky() {
115
- if (!this.spooky) throw new Error("SyncedDb not initialized");
116
- return this.spooky;
494
+ getSp00ky() {
495
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
496
+ return this.sp00ky;
117
497
  }
118
498
  /**
119
- * Initialize the spooky-ts instance
499
+ * Initialize the sp00ky-ts instance
120
500
  */
121
501
  async init() {
122
502
  if (this._initialized) return;
123
- this.spooky = new _spooky_sync_core.SpookyClient(this.config);
124
- await this.spooky.init();
503
+ this.sp00ky = new _spooky_sync_core.Sp00kyClient(this.config);
504
+ await this.sp00ky.init();
125
505
  this._initialized = true;
126
506
  }
127
507
  /**
128
508
  * Create a new record in the database
129
509
  */
130
510
  async create(id, payload) {
131
- if (!this.spooky) throw new Error("SyncedDb not initialized");
132
- await this.spooky.create(id, payload);
511
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
512
+ await this.sp00ky.create(id, payload);
133
513
  }
134
514
  /**
135
515
  * Update an existing record in the database
136
516
  */
137
517
  async update(tableName, recordId, payload, options) {
138
- if (!this.spooky) throw new Error("SyncedDb not initialized");
139
- await this.spooky.update(tableName, recordId, payload, options);
518
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
519
+ await this.sp00ky.update(tableName, recordId, payload, options);
140
520
  }
141
521
  /**
142
522
  * Delete an existing record in the database
143
523
  */
144
524
  async delete(tableName, selector) {
145
- if (!this.spooky) throw new Error("SyncedDb not initialized");
146
- if (typeof selector !== "string") throw new Error("Only string ID selectors are supported currently with core");
147
- await this.spooky.delete(tableName, selector);
525
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
526
+ const isRecordId = selector instanceof surrealdb.RecordId || selector?.constructor?.name === "RecordId";
527
+ let id;
528
+ if (typeof selector === "string") id = selector;
529
+ else if (isRecordId) id = `${tableName}:${selector.id}`;
530
+ else throw new Error("Only string ID or RecordId selectors are supported currently with core");
531
+ await this.sp00ky.delete(tableName, id);
148
532
  }
149
533
  /**
150
534
  * Query data from the database
151
535
  */
152
536
  query(table) {
153
- if (!this.spooky) throw new Error("SyncedDb not initialized");
154
- return this.spooky.query(table, {});
537
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
538
+ return this.sp00ky.query(table, {});
155
539
  }
156
540
  /**
157
541
  * Run a backend operation
158
542
  */
159
543
  async run(backend, path, payload, options) {
160
- if (!this.spooky) throw new Error("SyncedDb not initialized");
161
- await this.spooky.run(backend, path, payload, options);
544
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
545
+ await this.sp00ky.run(backend, path, payload, options);
162
546
  }
163
547
  /**
164
548
  * Authenticate with the database
165
549
  */
166
550
  async authenticate(token) {
167
- await this.spooky?.authenticate(token);
551
+ await this.sp00ky?.authenticate(token);
168
552
  return new surrealdb.RecordId("user", "me");
169
553
  }
170
554
  /**
@@ -178,52 +562,83 @@ var SyncedDb = class {
178
562
  * Sign out, clear session and local storage
179
563
  */
180
564
  async signOut() {
181
- if (!this.spooky) throw new Error("SyncedDb not initialized");
182
- await this.spooky.auth.signOut();
565
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
566
+ await this.sp00ky.auth.signOut();
183
567
  }
184
568
  /**
185
569
  * Execute a function with direct access to the remote database connection
186
570
  */
187
571
  async useRemote(fn) {
188
- if (!this.spooky) throw new Error("SyncedDb not initialized");
189
- return await this.spooky.useRemote(fn);
572
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
573
+ return await this.sp00ky.useRemote(fn);
190
574
  }
191
575
  /**
192
576
  * Access the remote database service directly
193
577
  */
194
578
  get remote() {
195
- if (!this.spooky) throw new Error("SyncedDb not initialized");
196
- return this.spooky.remoteClient;
579
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
580
+ return this.sp00ky.remoteClient;
197
581
  }
198
582
  /**
199
583
  * Access the local database service directly
200
584
  */
201
585
  get local() {
202
- if (!this.spooky) throw new Error("SyncedDb not initialized");
203
- return this.spooky.localClient;
586
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
587
+ return this.sp00ky.localClient;
204
588
  }
205
589
  /**
206
590
  * Access the auth service
207
591
  */
208
592
  get auth() {
209
- if (!this.spooky) throw new Error("SyncedDb not initialized");
210
- return this.spooky.auth;
593
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
594
+ return this.sp00ky.auth;
211
595
  }
212
596
  get pendingMutationCount() {
213
- if (!this.spooky) throw new Error("SyncedDb not initialized");
214
- return this.spooky.pendingMutationCount;
597
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
598
+ return this.sp00ky.pendingMutationCount;
599
+ }
600
+ /** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
601
+ get liveRetryCount() {
602
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
603
+ return this.sp00ky.liveRetryCount;
215
604
  }
216
605
  subscribeToPendingMutations(cb) {
217
- if (!this.spooky) throw new Error("SyncedDb not initialized");
218
- return this.spooky.subscribeToPendingMutations(cb);
606
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
607
+ return this.sp00ky.subscribeToPendingMutations(cb);
608
+ }
609
+ /** Current sync-health snapshot. See {@link useSyncStatus}. */
610
+ get syncHealth() {
611
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
612
+ return this.sp00ky.syncHealth;
613
+ }
614
+ /**
615
+ * Observe sync health. Fires immediately with the current status and again
616
+ * on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
617
+ * components; this is the imperative escape hatch.
618
+ */
619
+ subscribeToSyncHealth(cb) {
620
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
621
+ return this.sp00ky.subscribeToSyncHealth(cb);
622
+ }
623
+ bucket(name) {
624
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
625
+ return this.sp00ky.bucket(name);
626
+ }
627
+ getBucketConfig(name) {
628
+ return this.config.schema.buckets?.find((b) => b.name === name);
219
629
  }
220
630
  };
221
631
 
222
632
  //#endregion
223
633
  exports.RecordId = surrealdb.RecordId;
224
- exports.SpookyProvider = SpookyProvider;
634
+ exports.Sp00kyProvider = Sp00kyProvider;
225
635
  exports.SyncedDb = SyncedDb;
226
636
  exports.Uuid = surrealdb.Uuid;
637
+ exports.useCrdtField = useCrdtField;
227
638
  exports.useDb = useDb;
639
+ exports.useDownloadFile = useDownloadFile;
640
+ exports.useFeatureFlag = useFeatureFlag;
641
+ exports.useFileUpload = useFileUpload;
228
642
  exports.useQuery = useQuery;
643
+ exports.useSyncStatus = useSyncStatus;
229
644
  //# sourceMappingURL=index.cjs.map