@spooky-sync/client-solid2 0.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/AGENTS.md +81 -0
  2. package/QUICK_START.md +129 -0
  3. package/README.md +19 -0
  4. package/dist/index.cjs +1290 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +670 -0
  7. package/dist/index.d.cts.map +1 -0
  8. package/dist/index.d.ts +670 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +1266 -0
  11. package/dist/index.js.map +1 -0
  12. package/package.json +63 -0
  13. package/skills/sp00ky-solid2/SKILL.md +71 -0
  14. package/src/index.ts +410 -0
  15. package/src/lib/Blurhash.ts +64 -0
  16. package/src/lib/BucketImage.ts +170 -0
  17. package/src/lib/Sp00kyProvider.ts +104 -0
  18. package/src/lib/__tests__/conflate.test.ts +120 -0
  19. package/src/lib/__tests__/create-query.test.ts +397 -0
  20. package/src/lib/__tests__/rc-semantics.test.ts +393 -0
  21. package/src/lib/__tests__/use-sync-activity.test.ts +104 -0
  22. package/src/lib/conflate.ts +74 -0
  23. package/src/lib/context.ts +28 -0
  24. package/src/lib/create-preload.ts +116 -0
  25. package/src/lib/create-query.ts +388 -0
  26. package/src/lib/create-submission.ts +57 -0
  27. package/src/lib/from-subscription.ts +32 -0
  28. package/src/lib/merge-rows.test.ts +92 -0
  29. package/src/lib/merge-rows.ts +72 -0
  30. package/src/lib/models.ts +8 -0
  31. package/src/lib/use-app-release.ts +89 -0
  32. package/src/lib/use-blurhash.ts +82 -0
  33. package/src/lib/use-bucket-image.ts +108 -0
  34. package/src/lib/use-crdt-field.ts +57 -0
  35. package/src/lib/use-download-file.ts +181 -0
  36. package/src/lib/use-feature-flag.ts +43 -0
  37. package/src/lib/use-file-upload.ts +146 -0
  38. package/src/lib/use-storage-status.ts +44 -0
  39. package/src/lib/use-sync-activity.ts +99 -0
  40. package/src/lib/use-sync-status.ts +63 -0
  41. package/src/types/index.ts +83 -0
  42. package/tsconfig.json +27 -0
  43. package/tsdown.config.ts +20 -0
  44. package/vitest.config.ts +14 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,1290 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let _spooky_sync_core = require("@spooky-sync/core");
3
+ let surrealdb = require("surrealdb");
4
+ let solid_js = require("solid-js");
5
+
6
+ //#region src/lib/conflate.ts
7
+ /**
8
+ * Latest-wins async iterable over a subscribe-callback source.
9
+ *
10
+ * Bridges spooky's push-callback subscriptions into the AsyncIterable shape
11
+ * Solid 2 computations consume natively. Each spooky emission is a full result
12
+ * set, so intermediate values are droppable: only the newest unconsumed value
13
+ * is buffered, and a pending pull resolves with it immediately.
14
+ *
15
+ * Teardown contract (probed in rc-semantics.test.ts): Solid 2 does NOT
16
+ * terminate a superseded/disposed computation's async generator — no
17
+ * `return()`, no `finally`. Consumers MUST call `it.return()` themselves from
18
+ * an `onCleanup` registered synchronously in the compute scope. `return()`
19
+ * unsubscribes (awaiting the unsubscribe if the subscribe returned a promise,
20
+ * as `sp00ky.subscribe` does) and resolves any parked pull as done.
21
+ */
22
+ function conflate(subscribe) {
23
+ return { [Symbol.asyncIterator]() {
24
+ let buffered;
25
+ let resolveNext;
26
+ let done = false;
27
+ const unsubMaybe = subscribe((v) => {
28
+ if (done) return;
29
+ if (resolveNext) {
30
+ const r = resolveNext;
31
+ resolveNext = void 0;
32
+ r({
33
+ value: v,
34
+ done: false
35
+ });
36
+ } else buffered = { v };
37
+ });
38
+ const finish = () => {
39
+ if (done) return;
40
+ done = true;
41
+ buffered = void 0;
42
+ Promise.resolve(unsubMaybe).then((unsub) => unsub()).catch(() => {});
43
+ if (resolveNext) {
44
+ const r = resolveNext;
45
+ resolveNext = void 0;
46
+ r({
47
+ value: void 0,
48
+ done: true
49
+ });
50
+ }
51
+ };
52
+ return {
53
+ next() {
54
+ if (done) return Promise.resolve({
55
+ value: void 0,
56
+ done: true
57
+ });
58
+ if (buffered) {
59
+ const v = buffered.v;
60
+ buffered = void 0;
61
+ return Promise.resolve({
62
+ value: v,
63
+ done: false
64
+ });
65
+ }
66
+ return new Promise((r) => resolveNext = r);
67
+ },
68
+ return() {
69
+ finish();
70
+ return Promise.resolve({
71
+ value: void 0,
72
+ done: true
73
+ });
74
+ },
75
+ throw(e) {
76
+ finish();
77
+ return Promise.reject(e);
78
+ }
79
+ };
80
+ } };
81
+ }
82
+
83
+ //#endregion
84
+ //#region src/lib/from-subscription.ts
85
+ /**
86
+ * Reactive view over a spooky subscribe-callback API.
87
+ *
88
+ * The memo's async generator pulls from a conflated (latest-wins) iterator;
89
+ * `initial` is committed as the memo's `loadingValue`, so the accessor is
90
+ * readable synchronously from birth and never suspends. Spooky's subscribe
91
+ * APIs fire immediately with the current value, so the real value lands within
92
+ * a tick of the first read.
93
+ *
94
+ * Teardown is manual by contract (see conflate.ts): onCleanup terminates the
95
+ * iterator, which unsubscribes.
96
+ */
97
+ function fromSubscription(subscribe, initial) {
98
+ return (0, solid_js.createMemo)(async function* () {
99
+ const it = conflate(subscribe)[Symbol.asyncIterator]();
100
+ (0, solid_js.onCleanup)(() => void it.return?.());
101
+ while (true) {
102
+ const r = await it.next();
103
+ if (r.done) break;
104
+ yield r.value;
105
+ }
106
+ }, { loadingValue: initial });
107
+ }
108
+
109
+ //#endregion
110
+ //#region src/lib/context.ts
111
+ const Sp00kyContext = (0, solid_js.createContext)();
112
+ function useDb() {
113
+ try {
114
+ return (0, solid_js.useContext)(Sp00kyContext);
115
+ } catch {
116
+ throw new Error("useDb must be used within a <Sp00kyProvider>. Wrap your app in <Sp00kyProvider config={...}>.");
117
+ }
118
+ }
119
+ /**
120
+ * Count of locally-committed mutations not yet acknowledged by the server.
121
+ * Drive an "unsaved changes" indicator off this.
122
+ */
123
+ function usePendingMutations() {
124
+ const db = useDb();
125
+ return fromSubscription((cb) => db.subscribeToPendingMutations(cb), db.pendingMutationCount);
126
+ }
127
+
128
+ //#endregion
129
+ //#region src/lib/merge-rows.ts
130
+ /**
131
+ * Keyed in-place merge of a fresh row list into a store draft array.
132
+ *
133
+ * Why not `reconcile(rows, 'id')`: in Solid 2 rc.1 a reconcile REPLACES the row
134
+ * objects, so a component that captured `data()[0]` sees a different object
135
+ * after the next emission, and anything keyed on row identity (`<For>` without
136
+ * an explicit key, a memo comparing rows) re-creates its subtree on every live
137
+ * update. Mutating the draft in place keeps the identity AND keeps updates
138
+ * fine-grained: only the fields that actually changed are written, so a row
139
+ * whose data is unchanged notifies nobody.
140
+ *
141
+ * Rows are matched by `key` (default `id`); unmatched incoming rows are
142
+ * inserted as-is and trailing rows are dropped, so add / remove / reorder all
143
+ * reach coarse readers.
144
+ */
145
+ /**
146
+ * Field-level equality for the merge. `===` is not enough: the decoder hands
147
+ * out a fresh RecordId / Date instance for every record-link and datetime
148
+ * column on every emission, so by identity every one of those fields "changed"
149
+ * on every update - and a store write on an unchanged `id` re-ran everything a
150
+ * page keyed on that row (a thread's anchor, its composer, its whole subtree).
151
+ * Compare those wrappers by value; everything else stays identity (nested
152
+ * objects and arrays are reconciled by their own writes).
153
+ */
154
+ function sameValue(a, b) {
155
+ if (a === b) return true;
156
+ if (a == null || b == null) return false;
157
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
158
+ if (typeof a === "object" && typeof b === "object") {
159
+ const ctor = a.constructor;
160
+ if (ctor && ctor === b.constructor && ctor !== Object && ctor !== Array) return String(a) === String(b);
161
+ }
162
+ return false;
163
+ }
164
+ function mergeRows(draft, next, key = "id") {
165
+ const byKey = /* @__PURE__ */ new Map();
166
+ for (const row of draft) {
167
+ const k = row?.[key];
168
+ if (k !== void 0) byKey.set(String(k), row);
169
+ }
170
+ for (let i = 0; i < next.length; i++) {
171
+ const incoming = next[i];
172
+ const k = incoming?.[key];
173
+ const reuse = k !== void 0 ? byKey.get(String(k)) : void 0;
174
+ if (reuse) {
175
+ for (const field of Object.keys(incoming)) if (!sameValue(reuse[field], incoming[field])) reuse[field] = incoming[field];
176
+ for (const field of Object.keys((0, solid_js.snapshot)(reuse))) if (!(field in incoming)) delete reuse[field];
177
+ if (draft[i] !== reuse) draft[i] = reuse;
178
+ } else if (draft[i] !== incoming) draft[i] = incoming;
179
+ }
180
+ if (draft.length > next.length) draft.splice(next.length);
181
+ }
182
+
183
+ //#endregion
184
+ //#region src/lib/create-query.ts
185
+ function createQuery(dbOrQuery, queryOrOptions, maybeOptions) {
186
+ let db;
187
+ let finalQuery;
188
+ let options;
189
+ if (dbOrQuery instanceof SyncedDb) {
190
+ db = dbOrQuery;
191
+ finalQuery = queryOrOptions;
192
+ options = maybeOptions;
193
+ } else {
194
+ db = useDb();
195
+ finalQuery = dbOrQuery;
196
+ options = queryOrOptions;
197
+ }
198
+ const sp00ky = db.getSp00ky();
199
+ const [error, setError] = (0, solid_js.createSignal)(void 0, { ownedWrite: true });
200
+ const [isAuthoritative, setIsAuthoritative] = (0, solid_js.createSignal)(false, { ownedWrite: true });
201
+ const [hasPainted, setHasPainted] = (0, solid_js.createSignal)(false, { ownedWrite: true });
202
+ const [isFetching, setIsFetching] = (0, solid_js.createSignal)(false, { ownedWrite: true });
203
+ let activeHash;
204
+ const [store, setStore] = (0, solid_js.createStore)({ value: null });
205
+ let runId = 0;
206
+ let readyWaiters = [];
207
+ const wakeReady = () => {
208
+ const waiters = readyWaiters;
209
+ readyWaiters = [];
210
+ for (const w of waiters) w();
211
+ };
212
+ (0, solid_js.createEffect)(() => {
213
+ return {
214
+ enabled: options?.enabled?.() ?? true,
215
+ query: typeof finalQuery === "function" ? finalQuery() : finalQuery
216
+ };
217
+ }, ({ enabled, query }) => {
218
+ const myRun = ++runId;
219
+ setIsAuthoritative(false);
220
+ setHasPainted(false);
221
+ setError(void 0);
222
+ if (!enabled || !query) return;
223
+ const cleanups = [];
224
+ let disposed = false;
225
+ const addCleanup = (c) => {
226
+ if (!c) return;
227
+ if (typeof c === "function") {
228
+ if (disposed) c();
229
+ else cleanups.push(c);
230
+ return;
231
+ }
232
+ Promise.resolve(c).then((fn) => {
233
+ if (typeof fn !== "function") return;
234
+ if (disposed) fn();
235
+ else cleanups.push(fn);
236
+ });
237
+ };
238
+ /**
239
+ * Registration can fail — the canonical case is the SSP answering 503
240
+ * NOT_READY while it bootstraps. Surface it as `error()` instead of
241
+ * throwing into the graph: the sync scheduler retries the registration
242
+ * underneath, so a transient failure still recovers, and a spinner
243
+ * driven by `isLoading()` resolves via `error()`.
244
+ */
245
+ query.run().then(({ hash }) => {
246
+ if (disposed || myRun !== runId) return;
247
+ activeHash = hash;
248
+ addCleanup(sp00ky.subscribeQueryStatus(hash, (status) => setIsFetching(status === "fetching"), { immediate: true }));
249
+ addCleanup(sp00ky.subscribeQueryAuthority(hash, (known) => {
250
+ if (disposed || myRun !== runId) return;
251
+ setIsAuthoritative(known);
252
+ if (known) wakeReady();
253
+ else setHasPainted(false);
254
+ }, { immediate: true }));
255
+ addCleanup(sp00ky.subscribe(hash, (rows) => {
256
+ if (disposed || myRun !== runId) return;
257
+ const queryData = query.isOne ? rows[0] ?? null : rows;
258
+ if (query.isOne ? queryData !== null && queryData !== void 0 : rows.length > 0) {
259
+ setHasPainted(true);
260
+ wakeReady();
261
+ }
262
+ const t0 = performance.now();
263
+ setStore((s) => {
264
+ if (query.isOne || queryData === null || !Array.isArray(s.value)) s.value = queryData;
265
+ else mergeRows(s.value, queryData);
266
+ });
267
+ sp00ky.reportFrontendTiming(hash, performance.now() - t0);
268
+ }, { immediate: true }));
269
+ }).catch((err) => {
270
+ if (disposed || myRun !== runId) return;
271
+ setError(err instanceof Error ? err : new Error(String(err)));
272
+ wakeReady();
273
+ });
274
+ return () => {
275
+ disposed = true;
276
+ for (const c of cleanups) c();
277
+ };
278
+ });
279
+ const emptyList = [];
280
+ const data = () => {
281
+ const v = store.value;
282
+ if (v === null || v === void 0) {
283
+ const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
284
+ if (query && !query.isOne) return emptyList;
285
+ }
286
+ return v;
287
+ };
288
+ const readyGate = (0, solid_js.createMemo)(async () => {
289
+ if (isAuthoritative() || hasPainted() || error()) return true;
290
+ await new Promise((resolve) => readyWaiters.push(resolve));
291
+ return true;
292
+ }, { lazy: true });
293
+ const ready = () => {
294
+ readyGate();
295
+ return data();
296
+ };
297
+ (0, solid_js.onCleanup)(() => {
298
+ if (options?.deregisterOnCleanup && activeHash) sp00ky.deregisterQuery(activeHash);
299
+ });
300
+ const hasData = () => {
301
+ const v = store.value;
302
+ return Array.isArray(v) ? v.length > 0 : v !== null && v !== void 0;
303
+ };
304
+ const isLoading = () => !isAuthoritative() && !hasPainted() && error() === void 0;
305
+ const isEmpty = () => isAuthoritative() && !hasData();
306
+ const isSettled = () => isAuthoritative() && !isFetching();
307
+ return {
308
+ data,
309
+ ready,
310
+ error,
311
+ isLoading,
312
+ isFetching,
313
+ isAuthoritative,
314
+ hasData,
315
+ isEmpty,
316
+ isSettled
317
+ };
318
+ }
319
+ /** @deprecated Renamed `createQuery` in the Solid 2 binding. */
320
+ const useQuery = createQuery;
321
+
322
+ //#endregion
323
+ //#region src/lib/create-preload.ts
324
+ /**
325
+ * Reactive, fire-and-forget prewarm. Resolves the query (calling it if it's a
326
+ * function so it tracks reactive deps), dedupes on the query's stable identity
327
+ * hash, and registers it via `db.preload`: a live query nobody subscribes to,
328
+ * evicted a ttl after it was registered unless a view mounts it first. Nothing
329
+ * to clean up here.
330
+ *
331
+ * Typical use: inside a list row, preload the detail query the user is likely
332
+ * to open next, so navigation paints from cache instead of the network.
333
+ */
334
+ function createPreload(dbOrQuery, queryOrOptions, maybeOptions) {
335
+ let db;
336
+ let finalQuery;
337
+ let options;
338
+ if (dbOrQuery instanceof SyncedDb) {
339
+ db = dbOrQuery;
340
+ finalQuery = queryOrOptions;
341
+ options = maybeOptions;
342
+ } else {
343
+ db = useDb();
344
+ finalQuery = dbOrQuery;
345
+ options = queryOrOptions;
346
+ }
347
+ let prevHash;
348
+ (0, solid_js.createEffect)(() => {
349
+ if (!(options?.enabled?.() ?? true)) return void 0;
350
+ const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
351
+ if (!query) return void 0;
352
+ if (query.hash === prevHash) return void 0;
353
+ prevHash = query.hash;
354
+ return query;
355
+ }, (query) => {
356
+ if (!query) return;
357
+ db.getSp00ky().preload(query, { signal: options?.signal });
358
+ });
359
+ }
360
+
361
+ //#endregion
362
+ //#region src/lib/use-sync-status.ts
363
+ /**
364
+ * Observe sync health for a "can't reach the server" banner / indicator.
365
+ *
366
+ * Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
367
+ * remote 500 on query registration, a dropped socket) are absorbed by the
368
+ * retry and never flip this; `isDegraded()` only goes true once failures
369
+ * persist for the configured number of consecutive rounds (sp00ky core config
370
+ * `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
371
+ * the next successful round. Must be used within a `<Sp00kyProvider>`.
372
+ */
373
+ function useSyncStatus() {
374
+ const db = useDb();
375
+ const health = fromSubscription((cb) => db.subscribeToSyncHealth(cb), db.syncHealth);
376
+ return {
377
+ health,
378
+ status: () => health().status,
379
+ isHealthy: () => health().status === "healthy",
380
+ isDegraded: () => health().status === "degraded",
381
+ everConnected: () => health().everConnected,
382
+ isOffline: () => health().status === "degraded" && health().everConnected,
383
+ connection: () => health().connection,
384
+ isReconnecting: () => health().connection === "reconnecting"
385
+ };
386
+ }
387
+
388
+ //#endregion
389
+ //#region src/lib/use-sync-activity.ts
390
+ /**
391
+ * The two directions of sync traffic, for an indicator in the app chrome.
392
+ *
393
+ * `fetchingQueries` is one subscription on the engine's aggregate fetch count,
394
+ * not one per query; `pendingMutations` is the outbox depth. `isDownloading`
395
+ * is the fetch count debounced ON by `downloadDelayMs` (and off at once), so a
396
+ * local-first page that answers from cache and confirms with the server in a
397
+ * few milliseconds never flickers. Must be used within a `<Sp00kyProvider>`, or
398
+ * pass the `SyncedDb` explicitly.
399
+ */
400
+ function useSyncActivity(dbOrOptions, maybeOptions) {
401
+ const explicitDb = dbOrOptions && typeof dbOrOptions.subscribeToFetchActivity === "function" ? dbOrOptions : void 0;
402
+ const options = (explicitDb ? maybeOptions : dbOrOptions) ?? {};
403
+ const db = explicitDb ?? useDb();
404
+ const downloadDelayMs = options.downloadDelayMs ?? 200;
405
+ const uploadThreshold = options.uploadThreshold ?? 1;
406
+ const fetchingQueries = fromSubscription((cb) => db.subscribeToFetchActivity(cb), db.fetchingQueryCount);
407
+ const pendingMutations = fromSubscription((cb) => db.subscribeToPendingMutations(cb), db.pendingMutationCount);
408
+ const [isDownloading, setIsDownloading] = (0, solid_js.createSignal)(false, { ownedWrite: true });
409
+ let timer;
410
+ (0, solid_js.createEffect)(() => fetchingQueries() > 0, (busy) => {
411
+ if (timer !== void 0) {
412
+ clearTimeout(timer);
413
+ timer = void 0;
414
+ }
415
+ if (!busy) {
416
+ setIsDownloading(false);
417
+ return;
418
+ }
419
+ if (downloadDelayMs <= 0) {
420
+ setIsDownloading(true);
421
+ return;
422
+ }
423
+ timer = setTimeout(() => {
424
+ timer = void 0;
425
+ setIsDownloading(true);
426
+ }, downloadDelayMs);
427
+ });
428
+ (0, solid_js.onCleanup)(() => {
429
+ if (timer !== void 0) clearTimeout(timer);
430
+ });
431
+ return {
432
+ fetchingQueries,
433
+ pendingMutations,
434
+ isDownloading,
435
+ isUploading: () => pendingMutations() > uploadThreshold
436
+ };
437
+ }
438
+
439
+ //#endregion
440
+ //#region src/lib/use-storage-status.ts
441
+ /**
442
+ * Observe how durable the LOCAL cache is, for a "no local storage" warning.
443
+ *
444
+ * Under `localEngine: 'sqlite'` with `store: 'indexeddb'` the durable store is
445
+ * the OPFS SAHPool VFS, and only ONE client per bucket can hold it open: a
446
+ * second tab of the same app cannot get it and runs in memory instead (the
447
+ * engine retries first, so a closing tab's lock is usually waited out). Must be
448
+ * used within a `<Sp00kyProvider>`.
449
+ */
450
+ function useStorageStatus() {
451
+ const db = useDb();
452
+ const health = fromSubscription((cb) => db.subscribeToStorageHealth(cb), db.storageHealth);
453
+ return {
454
+ health,
455
+ status: () => health().status,
456
+ isPersistent: () => health().status === "persistent",
457
+ isMemoryFallback: () => health().fallback
458
+ };
459
+ }
460
+
461
+ //#endregion
462
+ //#region src/lib/use-crdt-field.ts
463
+ function useCrdtField(table, recordId, field, fallbackText) {
464
+ const db = useDb();
465
+ const [crdtField, setCrdtField] = (0, solid_js.createSignal)(null, { ownedWrite: true });
466
+ (0, solid_js.createEffect)(() => recordId(), (id) => {
467
+ if (!id) {
468
+ setCrdtField(null);
469
+ return;
470
+ }
471
+ const sp00ky = db.getSp00ky();
472
+ let superseded = false;
473
+ const text = fallbackText?.();
474
+ sp00ky.openCrdtField(table, id, field, text).then((cf) => {
475
+ if (!superseded) setCrdtField(cf);
476
+ else sp00ky.closeCrdtField(table, id, field);
477
+ }).catch((err) => {
478
+ console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);
479
+ });
480
+ return () => {
481
+ superseded = true;
482
+ if (crdtField()) {
483
+ sp00ky.closeCrdtField(table, id, field);
484
+ setCrdtField(null);
485
+ }
486
+ };
487
+ });
488
+ return crdtField;
489
+ }
490
+
491
+ //#endregion
492
+ //#region src/lib/use-feature-flag.ts
493
+ /**
494
+ * Subscribe to a feature flag for the currently authenticated user.
495
+ *
496
+ * Returns three Solid accessors that update reactively whenever the
497
+ * server-materialized assignment in `_00_user_feature` changes. Backed by
498
+ * the same SSP + sync pipeline that powers `createQuery`, so toggling a flag
499
+ * via `spky flag enable <key>` propagates to the UI without a refresh.
500
+ *
501
+ * `enabled()` is `true` when the resolved variant exists and is not 'off'.
502
+ * For multi-variant flags, prefer `variant()` directly.
503
+ */
504
+ function useFeatureFlag(key, options) {
505
+ const handle = useDb().getSp00ky().feature(key, options);
506
+ (0, solid_js.onCleanup)(() => handle.close());
507
+ const state = fromSubscription((cb) => handle.subscribe((s) => cb({
508
+ variant: s.variant ?? options?.fallback,
509
+ payload: s.payload
510
+ })), {
511
+ variant: handle.variant(),
512
+ payload: handle.payload()
513
+ });
514
+ return {
515
+ variant: () => state().variant,
516
+ payload: () => state().payload,
517
+ enabled: () => {
518
+ const v = state().variant;
519
+ return v !== void 0 && v !== "off";
520
+ }
521
+ };
522
+ }
523
+
524
+ //#endregion
525
+ //#region src/lib/use-app-release.ts
526
+ async function reloadForSnapshot(snapshot) {
527
+ if (typeof window === "undefined") return;
528
+ if (snapshot.cacheBust) try {
529
+ if (window.caches) {
530
+ const keys = await window.caches.keys();
531
+ await Promise.all(keys.map((k) => window.caches.delete(k)));
532
+ }
533
+ if (navigator.serviceWorker) {
534
+ const regs = await navigator.serviceWorker.getRegistrations();
535
+ for (const r of regs) r.update().catch(() => {});
536
+ }
537
+ window.location.href = window.location.pathname + "?cb=" + Date.now();
538
+ return;
539
+ } catch {}
540
+ window.location.reload();
541
+ }
542
+ /**
543
+ * Observe the app's announced release (`_00_app_release:<app>`, written by
544
+ * `spky deploy` / `spky release`) and compare it against the running build.
545
+ *
546
+ * Typical use: mount a small "new version available — Reload" notification
547
+ * gated on `updateAvailable()`, auto-invoking `reload()` when `mandatory()`
548
+ * (guard the auto path against reload loops with a per-version marker, since
549
+ * a client can reload while the deploy is still rolling out and land on the
550
+ * old bundle again).
551
+ */
552
+ function useAppRelease(options) {
553
+ const handle = useDb().getSp00ky().appRelease(options.app, { ttl: options.ttl });
554
+ (0, solid_js.onCleanup)(() => handle.close());
555
+ const snapshot = fromSubscription((cb) => handle.subscribe(cb), handle.snapshot());
556
+ const updateAvailable = () => (0, _spooky_sync_core.semverGt)(snapshot().version, options.currentVersion);
557
+ return {
558
+ latestVersion: () => snapshot().version,
559
+ updateAvailable,
560
+ mandatory: () => updateAvailable() && snapshot().mandatory,
561
+ cacheBust: () => snapshot().cacheBust,
562
+ reload: () => reloadForSnapshot(snapshot())
563
+ };
564
+ }
565
+
566
+ //#endregion
567
+ //#region src/lib/use-file-upload.ts
568
+ function useFileUpload(dbOrBucketName, maybeBucketName) {
569
+ let db;
570
+ let bucketName;
571
+ if (typeof dbOrBucketName === "string") {
572
+ db = useDb();
573
+ bucketName = dbOrBucketName;
574
+ } else {
575
+ db = dbOrBucketName;
576
+ bucketName = maybeBucketName;
577
+ }
578
+ const [isUploading, setIsUploading] = (0, solid_js.createSignal)(false, { ownedWrite: true });
579
+ const [error, setError] = (0, solid_js.createSignal)(null, { ownedWrite: true });
580
+ const objectUrls = [];
581
+ (0, solid_js.onCleanup)(() => {
582
+ for (const url of objectUrls) URL.revokeObjectURL(url);
583
+ });
584
+ const clearError = () => setError(null);
585
+ const validate = (file) => {
586
+ const config = db.getBucketConfig(bucketName);
587
+ if (!config) return;
588
+ if (config.maxSize !== null && config.maxSize !== void 0 && file.size > config.maxSize) {
589
+ const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
590
+ throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
591
+ }
592
+ if (config.allowedExtensions && config.allowedExtensions.length > 0) {
593
+ const fileName = file.name;
594
+ if (fileName) {
595
+ const ext = fileName.split(".").pop()?.toLowerCase();
596
+ if (!ext || !config.allowedExtensions.includes(ext)) throw new Error(`File type not allowed. Accepted: ${config.allowedExtensions.join(", ")}.`);
597
+ }
598
+ }
599
+ };
600
+ const upload = async (path, file, options) => {
601
+ setError(null);
602
+ try {
603
+ validate(file);
604
+ } catch (e) {
605
+ setError(e instanceof Error ? e : new Error(String(e)));
606
+ return;
607
+ }
608
+ setIsUploading(true);
609
+ try {
610
+ const bytes = await (0, _spooky_sync_core.fileToUint8Array)(file);
611
+ return await db.bucket(bucketName).put(path, bytes, options);
612
+ } catch (e) {
613
+ setError(e instanceof Error ? e : new Error(String(e)));
614
+ } finally {
615
+ setIsUploading(false);
616
+ }
617
+ };
618
+ const download = async (path) => {
619
+ setError(null);
620
+ try {
621
+ const content = await db.bucket(bucketName).get(path);
622
+ if (!content) return null;
623
+ const objectUrl = URL.createObjectURL(new Blob([content]));
624
+ objectUrls.push(objectUrl);
625
+ return objectUrl;
626
+ } catch (e) {
627
+ setError(e instanceof Error ? e : new Error(String(e)));
628
+ return null;
629
+ }
630
+ };
631
+ const remove = async (path) => {
632
+ setError(null);
633
+ try {
634
+ await db.bucket(bucketName).delete(path);
635
+ } catch (e) {
636
+ setError(e instanceof Error ? e : new Error(String(e)));
637
+ }
638
+ };
639
+ const exists = async (path) => {
640
+ setError(null);
641
+ try {
642
+ return await db.bucket(bucketName).exists(path);
643
+ } catch (e) {
644
+ setError(e instanceof Error ? e : new Error(String(e)));
645
+ return false;
646
+ }
647
+ };
648
+ return {
649
+ isUploading,
650
+ error,
651
+ clearError,
652
+ upload,
653
+ download,
654
+ remove,
655
+ exists
656
+ };
657
+ }
658
+
659
+ //#endregion
660
+ //#region src/lib/use-download-file.ts
661
+ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeOptions) {
662
+ let db;
663
+ let bucketName;
664
+ let path;
665
+ let options;
666
+ if (typeof dbOrBucketName === "string") {
667
+ db = useDb();
668
+ bucketName = dbOrBucketName;
669
+ path = bucketNameOrPath;
670
+ options = pathOrOptions ?? {};
671
+ } else {
672
+ db = dbOrBucketName;
673
+ bucketName = bucketNameOrPath;
674
+ path = pathOrOptions;
675
+ options = maybeOptions ?? {};
676
+ }
677
+ const useCache = options.cache !== false;
678
+ const [url, setUrl] = (0, solid_js.createSignal)(null, { ownedWrite: true });
679
+ const [isLoading, setIsLoading] = (0, solid_js.createSignal)(false, { ownedWrite: true });
680
+ const [error, setError] = (0, solid_js.createSignal)(null, { ownedWrite: true });
681
+ let lease = null;
682
+ let privateUrl = null;
683
+ const [refetchSignal, setRefetchSignal] = (0, solid_js.createSignal)(0);
684
+ /** Consumed by the next effect run, so `refetch()` bypasses every layer once. */
685
+ let reloadOnce = false;
686
+ function releaseCurrent() {
687
+ lease?.release();
688
+ lease = null;
689
+ if (privateUrl) {
690
+ URL.revokeObjectURL(privateUrl);
691
+ privateUrl = null;
692
+ }
693
+ }
694
+ (0, solid_js.createEffect)(() => {
695
+ refetchSignal();
696
+ return path();
697
+ }, (filePath) => {
698
+ releaseCurrent();
699
+ if (!filePath) {
700
+ setUrl(null);
701
+ setIsLoading(false);
702
+ setError(null);
703
+ return;
704
+ }
705
+ const reload = reloadOnce;
706
+ reloadOnce = false;
707
+ let cancelled = false;
708
+ setIsLoading(true);
709
+ setError(null);
710
+ const bucket = db.bucket(bucketName);
711
+ (useCache ? bucket.url(filePath, {
712
+ persist: options.persist !== false,
713
+ pin: options.pin,
714
+ revalidate: options.revalidate,
715
+ reload
716
+ }).then((acquired) => {
717
+ if (!acquired) return null;
718
+ if (cancelled) {
719
+ acquired.release();
720
+ return null;
721
+ }
722
+ lease = acquired;
723
+ return acquired.url;
724
+ }) : bucket.read(filePath, {
725
+ persist: false,
726
+ reload: true
727
+ }).then((blob) => {
728
+ if (!blob || cancelled) return null;
729
+ privateUrl = URL.createObjectURL(blob);
730
+ return privateUrl;
731
+ })).then((result) => {
732
+ if (!cancelled) {
733
+ setUrl(result);
734
+ setIsLoading(false);
735
+ }
736
+ }, (err) => {
737
+ if (!cancelled) {
738
+ setError(err instanceof Error ? err : new Error(String(err)));
739
+ setIsLoading(false);
740
+ }
741
+ });
742
+ return () => {
743
+ cancelled = true;
744
+ };
745
+ });
746
+ (0, solid_js.onCleanup)(() => {
747
+ releaseCurrent();
748
+ });
749
+ const refetch = () => {
750
+ reloadOnce = true;
751
+ setRefetchSignal((n) => n + 1);
752
+ };
753
+ return {
754
+ url,
755
+ isLoading,
756
+ error,
757
+ refetch
758
+ };
759
+ }
760
+
761
+ //#endregion
762
+ //#region src/lib/use-blurhash.ts
763
+ function useBlurhash(dbOrBucketName, bucketNameOrPath, maybePath) {
764
+ let db;
765
+ let bucketName;
766
+ let path;
767
+ if (typeof dbOrBucketName === "string") {
768
+ db = useDb();
769
+ bucketName = dbOrBucketName;
770
+ path = bucketNameOrPath;
771
+ } else {
772
+ db = dbOrBucketName;
773
+ bucketName = bucketNameOrPath;
774
+ path = maybePath;
775
+ }
776
+ const [hash, setHash] = (0, solid_js.createSignal)(null, { ownedWrite: true });
777
+ const [isLoading, setIsLoading] = (0, solid_js.createSignal)(false, { ownedWrite: true });
778
+ (0, solid_js.createEffect)(() => path(), (filePath) => {
779
+ if (!filePath) {
780
+ setHash(null);
781
+ setIsLoading(false);
782
+ return;
783
+ }
784
+ let cancelled = false;
785
+ setIsLoading(true);
786
+ db.bucket(bucketName).blurhash(filePath).then((result) => {
787
+ if (cancelled) return;
788
+ setHash(result);
789
+ setIsLoading(false);
790
+ }).catch(() => {
791
+ if (cancelled) return;
792
+ setHash(null);
793
+ setIsLoading(false);
794
+ });
795
+ return () => {
796
+ cancelled = true;
797
+ };
798
+ });
799
+ return {
800
+ hash,
801
+ isLoading
802
+ };
803
+ }
804
+
805
+ //#endregion
806
+ //#region src/lib/use-bucket-image.ts
807
+ function useBucketImage(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeOptions) {
808
+ let db;
809
+ let bucketName;
810
+ let path;
811
+ let options;
812
+ if (typeof dbOrBucketName === "string") {
813
+ db = useDb();
814
+ bucketName = dbOrBucketName;
815
+ path = bucketNameOrPath;
816
+ options = pathOrOptions ?? {};
817
+ } else {
818
+ db = dbOrBucketName;
819
+ bucketName = bucketNameOrPath;
820
+ path = pathOrOptions;
821
+ options = maybeOptions ?? {};
822
+ }
823
+ const wantHash = options.blurhash !== false;
824
+ const { hash } = useBlurhash(db, bucketName, () => wantHash ? path() : null);
825
+ const file = useDownloadFile(db, bucketName, path, options);
826
+ const [ready, setReady] = (0, solid_js.createSignal)(false, { ownedWrite: true });
827
+ (0, solid_js.createEffect)(file.url, () => {
828
+ setReady(false);
829
+ }, { defer: true });
830
+ const gate = (img) => {
831
+ const done = () => setReady(true);
832
+ if (typeof img.decode === "function") img.decode().then(done, done);
833
+ else if (img.complete) done();
834
+ else {
835
+ img.onload = done;
836
+ img.onerror = done;
837
+ }
838
+ };
839
+ return {
840
+ ...file,
841
+ blurhash: hash,
842
+ ready,
843
+ gate
844
+ };
845
+ }
846
+
847
+ //#endregion
848
+ //#region src/lib/Blurhash.ts
849
+ /**
850
+ * A blurhash painted onto a canvas, once per hash change. Size the canvas via
851
+ * `class`/`style` (e.g. `absolute inset-0 w-full h-full`); the internal decode
852
+ * resolution stays tiny regardless of the displayed size.
853
+ */
854
+ function Blurhash(props) {
855
+ if (typeof document === "undefined") return null;
856
+ const canvas = document.createElement("canvas");
857
+ (0, solid_js.createEffect)(() => props.class ?? "", (className) => {
858
+ canvas.className = className;
859
+ });
860
+ (0, solid_js.createEffect)(() => props.style ?? "", (style) => {
861
+ canvas.style.cssText = style;
862
+ });
863
+ (0, solid_js.createEffect)(() => ({
864
+ width: props.width ?? 32,
865
+ height: props.height ?? 32,
866
+ hash: props.hash,
867
+ punch: props.punch ?? 1
868
+ }), ({ width, height, hash, punch }) => {
869
+ canvas.width = width;
870
+ canvas.height = height;
871
+ if (!hash) return;
872
+ try {
873
+ const pixels = (0, _spooky_sync_core.decodeBlurhash)(hash, width, height, punch);
874
+ const ctx = canvas.getContext("2d");
875
+ if (!ctx) return;
876
+ const imageData = ctx.createImageData(width, height);
877
+ imageData.data.set(pixels);
878
+ ctx.putImageData(imageData, 0, 0);
879
+ } catch {}
880
+ });
881
+ return canvas;
882
+ }
883
+
884
+ //#endregion
885
+ //#region src/lib/BucketImage.ts
886
+ const LAYER_STYLE = "position:absolute;inset:0;width:100%;height:100%;";
887
+ /**
888
+ * A bucket image that never pops in: it layers (bottom to top) your `fallback`
889
+ * plate, the automatically stored blurhash, and the real image, which stays
890
+ * transparent until the bitmap is DECODED and then crossfades over the
891
+ * placeholders. Placeholder layers unmount once the fade settles. Respects
892
+ * prefers-reduced-motion (instant swap). The container is made
893
+ * `position: relative` unless your `class` positions it already.
894
+ *
895
+ * ```tsx
896
+ * <BucketImage bucket="covers" path={row.cover_key} class="absolute inset-0"
897
+ * fallback={<MyPlate />} alt="" />
898
+ * ```
899
+ */
900
+ function BucketImage(props) {
901
+ if (typeof document === "undefined") return null;
902
+ const image = useBucketImage(props.bucket, () => props.path, {
903
+ ...props.options,
904
+ blurhash: props.blurhash !== false
905
+ });
906
+ const reducedMotion = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
907
+ const root = document.createElement("div");
908
+ (0, solid_js.createEffect)(() => props.class ?? "", (className) => {
909
+ root.className = className;
910
+ });
911
+ (0, solid_js.onSettled)(() => {
912
+ if (getComputedStyle(root).position === "static") root.style.position = "relative";
913
+ });
914
+ const placeholder = document.createElement("div");
915
+ placeholder.style.cssText = LAYER_STYLE;
916
+ const fallbackHolder = document.createElement("div");
917
+ fallbackHolder.style.cssText = LAYER_STYLE;
918
+ placeholder.append(fallbackHolder);
919
+ const resolvedFallback = (0, solid_js.children)(() => props.fallback);
920
+ (0, solid_js.createEffect)(() => resolvedFallback.toArray().filter((node) => node instanceof Node), (nodes) => {
921
+ fallbackHolder.replaceChildren(...nodes);
922
+ });
923
+ const hashCanvas = Blurhash({
924
+ get hash() {
925
+ return image.blurhash();
926
+ },
927
+ style: LAYER_STYLE
928
+ });
929
+ if (hashCanvas instanceof Node) placeholder.append(hashCanvas);
930
+ const img = document.createElement("img");
931
+ img.decoding = "async";
932
+ img.style.cssText = `${LAYER_STYLE}opacity:0;`;
933
+ (0, solid_js.createEffect)(() => props.imgClass ?? "", (className) => {
934
+ img.className = className;
935
+ });
936
+ (0, solid_js.createEffect)(() => props.fit ?? "cover", (fit) => {
937
+ img.style.objectFit = fit;
938
+ });
939
+ (0, solid_js.createEffect)(() => props.alt ?? "", (alt) => {
940
+ img.alt = alt;
941
+ });
942
+ (0, solid_js.createEffect)(() => reducedMotion ? "none" : `opacity ${props.transition ?? 300}ms ${props.easing ?? "cubic-bezier(0.16, 1, 0.3, 1)"}`, (transition) => {
943
+ img.style.transition = transition;
944
+ });
945
+ (0, solid_js.createEffect)(() => image.url(), (url) => {
946
+ if (!url) {
947
+ img.removeAttribute("src");
948
+ return;
949
+ }
950
+ img.src = url;
951
+ image.gate(img);
952
+ });
953
+ (0, solid_js.createEffect)(() => image.ready(), (ready) => {
954
+ img.style.opacity = ready ? "1" : "0";
955
+ });
956
+ const [settled, setSettled] = (0, solid_js.createSignal)(false, { ownedWrite: true });
957
+ (0, solid_js.createEffect)(() => ({
958
+ ready: image.ready(),
959
+ transition: props.transition ?? 300
960
+ }), ({ ready, transition }) => {
961
+ if (!ready) {
962
+ setSettled(false);
963
+ return;
964
+ }
965
+ const wait = (reducedMotion ? 0 : transition) + 120;
966
+ const timer = setTimeout(() => setSettled(true), wait);
967
+ return () => clearTimeout(timer);
968
+ });
969
+ (0, solid_js.createEffect)(() => settled(), (isSettled) => {
970
+ if (isSettled) placeholder.remove();
971
+ else if (!placeholder.isConnected) root.insertBefore(placeholder, img);
972
+ });
973
+ root.append(placeholder, img);
974
+ return root;
975
+ }
976
+
977
+ //#endregion
978
+ //#region src/lib/Sp00kyProvider.ts
979
+ function Sp00kyProvider(props) {
980
+ const merged = (0, solid_js.merge)({ fallback: void 0 }, props);
981
+ const [db, setDb] = (0, solid_js.createSignal)(void 0, { ownedWrite: true });
982
+ let disposed = false;
983
+ (0, solid_js.onCleanup)(() => {
984
+ disposed = true;
985
+ });
986
+ (0, solid_js.onSettled)(() => {
987
+ (async () => {
988
+ try {
989
+ const instance = new SyncedDb(merged.config);
990
+ await instance.init();
991
+ if (disposed) {
992
+ await instance.close();
993
+ return;
994
+ }
995
+ if (merged.preload) try {
996
+ await merged.preload(instance);
997
+ } catch (e) {
998
+ console.error("Sp00kyProvider: preload failed; revealing UI anyway", e);
999
+ }
1000
+ setDb(() => instance);
1001
+ merged.onReady?.(instance);
1002
+ } catch (e) {
1003
+ const error = e instanceof Error ? e : new Error(String(e));
1004
+ if (merged.onError) merged.onError(error);
1005
+ else console.error("Sp00kyProvider: Failed to initialize database", error);
1006
+ }
1007
+ })();
1008
+ });
1009
+ return (0, solid_js.createMemo)(() => {
1010
+ const instance = db();
1011
+ if (!instance) return merged.fallback;
1012
+ return (0, solid_js.createComponent)(Sp00kyContext, {
1013
+ value: instance,
1014
+ get children() {
1015
+ return merged.children;
1016
+ }
1017
+ });
1018
+ });
1019
+ }
1020
+
1021
+ //#endregion
1022
+ //#region src/lib/create-submission.ts
1023
+ /**
1024
+ * Thin submission-state wrapper for mutations — button spinner/disable state
1025
+ * around `db.create/update/delete/run` calls.
1026
+ *
1027
+ * Deliberately NOT built on Solid 2's `action()`/`createOptimisticStore`: the
1028
+ * spooky engine is already optimistic local-first (writes commit to the local
1029
+ * DB and re-render through live queries before sync; `run()` is an outbox
1030
+ * CREATE), so a transaction/revert layer on top buys nothing and `action()`'s
1031
+ * await-vs-yield transaction escape is a real footgun. Errors here mean the
1032
+ * LOCAL commit failed — sync/push failures surface through `useSyncStatus`
1033
+ * and `usePendingMutations` instead.
1034
+ */
1035
+ function createSubmission(fn) {
1036
+ const [inFlight, setInFlight] = (0, solid_js.createSignal)(0, { ownedWrite: true });
1037
+ const [error, setError] = (0, solid_js.createSignal)(void 0, { ownedWrite: true });
1038
+ const [result, setResult] = (0, solid_js.createSignal)(void 0, { ownedWrite: true });
1039
+ const submit = async (...args) => {
1040
+ setError(void 0);
1041
+ setInFlight((n) => n + 1);
1042
+ try {
1043
+ const r = await fn(...args);
1044
+ setResult(() => r);
1045
+ return r;
1046
+ } catch (e) {
1047
+ setError(e instanceof Error ? e : new Error(String(e)));
1048
+ return;
1049
+ } finally {
1050
+ setInFlight((n) => n - 1);
1051
+ }
1052
+ };
1053
+ return {
1054
+ submit,
1055
+ pending: () => inFlight() > 0,
1056
+ error,
1057
+ result,
1058
+ clearError: () => setError(void 0)
1059
+ };
1060
+ }
1061
+
1062
+ //#endregion
1063
+ //#region src/index.ts
1064
+ /**
1065
+ * SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration.
1066
+ * Delegates all logic to the underlying sp00ky-ts instance.
1067
+ *
1068
+ * NOTE: keep in sync with packages/client-solid/src/index.ts (SyncedDb).
1069
+ * Copied rather than shared so this package's dependency graph never pulls
1070
+ * in solid-js 1.x; fold the two together once client-solid moves to Solid 2.
1071
+ * Solid-2-only differences: `delete` accepts 'bound RecordId', and write
1072
+ * payloads go through `snapshot()` (see `unproxy` below).
1073
+ */
1074
+ /**
1075
+ * Solid 2 stores wrap every object read out of them (query rows, their nested
1076
+ * arrays, `createStore` docs) in a Proxy. Those proxies cannot cross
1077
+ * `postMessage` (the sqlite and shared-tabs workers): structuredClone throws
1078
+ * DataCloneError. `snapshot()` returns the underlying plain value for store
1079
+ * proxies and passes anything else through untouched.
1080
+ */
1081
+ function unproxy(value) {
1082
+ if (value === null || typeof value !== "object") return value;
1083
+ return (0, solid_js.snapshot)(value);
1084
+ }
1085
+ var SyncedDb = class {
1086
+ constructor(config) {
1087
+ this.sp00ky = null;
1088
+ this._initialized = false;
1089
+ this.config = config;
1090
+ }
1091
+ getSp00ky() {
1092
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1093
+ return this.sp00ky;
1094
+ }
1095
+ /**
1096
+ * Initialize the sp00ky-ts instance
1097
+ */
1098
+ async init() {
1099
+ if (this._initialized) return;
1100
+ this.sp00ky = new _spooky_sync_core.Sp00kyClient(this.config);
1101
+ await this.sp00ky.init();
1102
+ this._initialized = true;
1103
+ }
1104
+ /**
1105
+ * Tear down the client: leaves the tabs broker, closes the local store and
1106
+ * remote socket, and frees the wasm circuit. Without this a remounted provider
1107
+ * (or an HMR reload) strands a whole client, and the abandoned wasm heaps stay
1108
+ * resident because V8 cannot see how much wasm memory a dropped wrapper holds.
1109
+ */
1110
+ async close() {
1111
+ const instance = this.sp00ky;
1112
+ this.sp00ky = null;
1113
+ this._initialized = false;
1114
+ if (instance) await instance.close();
1115
+ }
1116
+ /**
1117
+ * Create a new record in the database
1118
+ */
1119
+ async create(id, payload) {
1120
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1121
+ await this.sp00ky.create(id, unproxy(payload));
1122
+ }
1123
+ /**
1124
+ * Update an existing record in the database
1125
+ */
1126
+ async update(tableName, recordId, payload, options) {
1127
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1128
+ await this.sp00ky.update(tableName, recordId, unproxy(payload), options);
1129
+ }
1130
+ /**
1131
+ * Delete an existing record in the database
1132
+ */
1133
+ async delete(tableName, selector) {
1134
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1135
+ const raw = unproxy(selector);
1136
+ const ctorName = raw?.constructor?.name;
1137
+ const isRecordId = raw instanceof surrealdb.RecordId || ctorName === "RecordId" || ctorName === "bound RecordId";
1138
+ let id;
1139
+ if (typeof raw === "string") id = raw;
1140
+ else if (isRecordId) id = `${tableName}:${raw.id}`;
1141
+ else throw new Error("Only string ID or RecordId selectors are supported currently with core");
1142
+ await this.sp00ky.delete(tableName, id);
1143
+ }
1144
+ /**
1145
+ * Preload/prewarm a built query into the local cache without registering a
1146
+ * live view. Fetches once and stores the rows (+ embedded related children)
1147
+ * locally so a later `createQuery` for the same data paints instantly. Best-effort.
1148
+ */
1149
+ async preload(finalQuery, options) {
1150
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1151
+ await this.sp00ky.preload(finalQuery, options);
1152
+ }
1153
+ /**
1154
+ * Query data from the database
1155
+ */
1156
+ query(table) {
1157
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1158
+ return this.sp00ky.query(table, {});
1159
+ }
1160
+ /**
1161
+ * Run a backend operation
1162
+ */
1163
+ async run(backend, path, payload, options) {
1164
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1165
+ await this.sp00ky.run(backend, path, unproxy(payload), options);
1166
+ }
1167
+ /**
1168
+ * Sign out, clear session and local storage
1169
+ */
1170
+ async signOut() {
1171
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1172
+ await this.sp00ky.auth.signOut();
1173
+ }
1174
+ /**
1175
+ * Execute a function with direct access to the remote database connection
1176
+ */
1177
+ async useRemote(fn) {
1178
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1179
+ return await this.sp00ky.useRemote(fn);
1180
+ }
1181
+ /**
1182
+ * Access the remote database service directly
1183
+ */
1184
+ get remote() {
1185
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1186
+ return this.sp00ky.remoteClient;
1187
+ }
1188
+ /**
1189
+ * Access the local database service directly
1190
+ */
1191
+ get local() {
1192
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1193
+ return this.sp00ky.localClient;
1194
+ }
1195
+ /**
1196
+ * Access the auth service
1197
+ */
1198
+ get auth() {
1199
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1200
+ return this.sp00ky.auth;
1201
+ }
1202
+ get pendingMutationCount() {
1203
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1204
+ return this.sp00ky.pendingMutationCount;
1205
+ }
1206
+ /** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
1207
+ get liveRetryCount() {
1208
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1209
+ return this.sp00ky.liveRetryCount;
1210
+ }
1211
+ subscribeToPendingMutations(cb) {
1212
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1213
+ return this.sp00ky.subscribeToPendingMutations(cb);
1214
+ }
1215
+ /** Queries mid-fetch right now. See {@link useSyncActivity}. */
1216
+ get fetchingQueryCount() {
1217
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1218
+ return this.sp00ky.fetchingQueryCount;
1219
+ }
1220
+ /**
1221
+ * Observe how many queries are fetching from the server. Fires immediately
1222
+ * and on every change. Prefer the `useSyncActivity` hook in components.
1223
+ */
1224
+ subscribeToFetchActivity(cb) {
1225
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1226
+ return this.sp00ky.subscribeToFetchActivity(cb);
1227
+ }
1228
+ /** Current sync-health snapshot. See {@link useSyncStatus}. */
1229
+ get syncHealth() {
1230
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1231
+ return this.sp00ky.syncHealth;
1232
+ }
1233
+ /**
1234
+ * Observe sync health. Fires immediately with the current status and again
1235
+ * on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
1236
+ * components; this is the imperative escape hatch.
1237
+ */
1238
+ subscribeToSyncHealth(cb) {
1239
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1240
+ return this.sp00ky.subscribeToSyncHealth(cb);
1241
+ }
1242
+ /** Current local-store durability snapshot. See {@link useStorageStatus}. */
1243
+ get storageHealth() {
1244
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1245
+ return this.sp00ky.storageHealth;
1246
+ }
1247
+ /**
1248
+ * Observe local-store durability. Fires immediately with the current snapshot
1249
+ * and again on change. Prefer the `useStorageStatus` hook in components; this
1250
+ * is the imperative escape hatch.
1251
+ */
1252
+ subscribeToStorageHealth(cb) {
1253
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1254
+ return this.sp00ky.subscribeToStorageHealth(cb);
1255
+ }
1256
+ bucket(name) {
1257
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
1258
+ return this.sp00ky.bucket(name);
1259
+ }
1260
+ getBucketConfig(name) {
1261
+ return this.config.schema.buckets?.find((b) => b.name === name);
1262
+ }
1263
+ };
1264
+
1265
+ //#endregion
1266
+ exports.Blurhash = Blurhash;
1267
+ exports.BucketImage = BucketImage;
1268
+ exports.RecordId = surrealdb.RecordId;
1269
+ exports.Sp00kyProvider = Sp00kyProvider;
1270
+ exports.SyncedDb = SyncedDb;
1271
+ exports.Uuid = surrealdb.Uuid;
1272
+ exports.conflate = conflate;
1273
+ exports.createPreload = createPreload;
1274
+ exports.createQuery = createQuery;
1275
+ exports.createSubmission = createSubmission;
1276
+ exports.fromSubscription = fromSubscription;
1277
+ exports.useAppRelease = useAppRelease;
1278
+ exports.useBlurhash = useBlurhash;
1279
+ exports.useBucketImage = useBucketImage;
1280
+ exports.useCrdtField = useCrdtField;
1281
+ exports.useDb = useDb;
1282
+ exports.useDownloadFile = useDownloadFile;
1283
+ exports.useFeatureFlag = useFeatureFlag;
1284
+ exports.useFileUpload = useFileUpload;
1285
+ exports.usePendingMutations = usePendingMutations;
1286
+ exports.useQuery = useQuery;
1287
+ exports.useStorageStatus = useStorageStatus;
1288
+ exports.useSyncActivity = useSyncActivity;
1289
+ exports.useSyncStatus = useSyncStatus;
1290
+ //# sourceMappingURL=index.cjs.map