@spooky-sync/client-solid 0.0.1-canary.15 → 0.0.1-canary.150

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,68 @@
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.preload(query, options?)` — cache-aware, awaitable prewarm. Cold (nothing cached in the bucket) fetches + persists and the promise awaits it (blocks); warm returns instantly. `options.refresh` (`'onUse'` default / `'background'` / `'stale'`) + `options.staleTime` control warm-refresh. One-shot snapshot, no live view — freshens on use.
40
+ - `db.run(backend, route, payload)` — call a backend RPC route.
41
+ - `db.bucket(name)` — get a `BucketHandle` for file storage.
42
+ - `db.useRemote(fn)` — escape hatch to the raw `Surreal` client (skips cache).
43
+ - `db.authenticate(token)`, `db.signOut()`, `db.auth`.
44
+ - `db.pendingMutationCount`, `db.subscribeToPendingMutations(cb)`.
45
+ - **`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.
46
+ - **`createPreload(() => db.query(...).build(), options?)`** — reactive, fire-and-forget prewarm (same overloads/`enabled` as `useQuery`, plus `refresh`/`staleTime`). Warms a query the user is likely to open next (e.g. a list row's detail) into the local cache. For blocking first-load, use the `Sp00kyProvider` `preload` prop instead.
47
+ - **`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.*
48
+ - **`useFileUpload()`** / **`useDownloadFile()`** — bucket helpers; the upload result includes the storage path you write into a record column.
49
+
50
+ ## Re-exports for convenience
51
+
52
+ - `RecordId`, `Uuid` from `surrealdb`.
53
+ - Query-builder types: `TableModel`, `TableNames`, `GetTable`, `QueryResult`, etc. (see `@spooky-sync/query-builder/AGENTS.md`).
54
+ - `Model<S, T>`, `GenericModel`, `ModelPayload` — typed row shapes.
55
+
56
+ ## Common gotchas
57
+
58
+ - **`useDb()` requires `<typeof schema>`.** Without the generic, all calls fall back to `unknown` and you lose type safety.
59
+ - **CRDT fields are not regular columns.** Don't read or write them via `useQuery` — read with `useCrdtField`, write via `db.update` with `{ debounced: true }`.
60
+ - **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.
61
+ - **`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.
62
+ - **Provider is mandatory.** Calling any hook outside `<Sp00kyProvider>` throws.
63
+
64
+ ## Pointers
65
+
66
+ - Sync engine: `node_modules/@spooky-sync/core/AGENTS.md`
67
+ - Query builder DSL: `node_modules/@spooky-sync/query-builder/AGENTS.md`
68
+ - Schema authoring + codegen: `node_modules/@spooky-sync/cli/AGENTS.md`
package/README.md CHANGED
@@ -11,6 +11,7 @@ A SurrealDB client for Solid.js with automatic cache synchronization and live qu
11
11
  - **Type-Safe**: Full TypeScript support with generated schema types
12
12
  - **Reactive**: Seamless integration with Solid.js reactivity
13
13
  - **Offline Support**: Local cache works even when remote is temporarily unavailable
14
+ - **Preload / Prewarm**: Warm data into the local cache ahead of time so screens open instantly — awaitable to gate first load, reactive to prefetch what's next
14
15
 
15
16
  ## Quick Start
16
17
 
@@ -215,6 +216,25 @@ const liveQuery = await db.query.thread
215
216
  .orderBy('created_at', 'desc'); // ✅ Field names autocompleted
216
217
  ```
217
218
 
219
+ ### Preload / Prewarm
220
+
221
+ Warm a query's results into the local cache before they're needed, so a later `useQuery` paints
222
+ instantly instead of waiting on the network. Preload is a one-shot snapshot — it does not register
223
+ a live view; the data freshens on use when the real `useQuery` mounts.
224
+
225
+ ```typescript
226
+ // Awaitable + cache-aware: blocks on the FIRST load (nothing cached), returns
227
+ // instantly on later loads (already cached). Great for gating the UI on config.
228
+ await db.preload(db.query('config').build());
229
+
230
+ // Opt into a one-time background refresh when the cache is stale:
231
+ await db.preload(db.query('config').build(), { refresh: 'stale', staleTime: '1d' });
232
+ ```
233
+
234
+ Block first-load UI on essential data via the `Sp00kyProvider` `preload` prop, or reactively
235
+ prefetch what the user is likely to open next with `createPreload(() => …build())`. Full details
236
+ in the [SKILL guide](./skills/sp00ky-solid/SKILL.md#preload).
237
+
218
238
  ## Documentation
219
239
 
220
240
  - **[Quick Start Guide](./QUICK_START.md)**: Get up and running quickly
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,211 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
54
81
  }
55
82
  const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
56
83
  if (!query) return;
57
- const queryString = JSON.stringify(query);
84
+ const queryString = String(query.hash);
58
85
  if (queryString === prevQueryString) return;
59
86
  prevQueryString = queryString;
87
+ const myRun = ++runId;
88
+ teardownActive();
60
89
  setIsFetched(false);
61
- initQuery(query);
62
- (0, solid_js.onCleanup)(() => {
63
- unsubscribe()?.();
64
- });
90
+ initQuery(query, myRun);
91
+ });
92
+ (0, solid_js.onCleanup)(() => {
93
+ runId++;
94
+ teardownActive();
95
+ if (options?.deregisterOnCleanup && activeHash) sp00ky.deregisterQuery(activeHash);
65
96
  });
66
97
  const isLoading = () => {
67
98
  return !isFetched() && error() === void 0;
68
99
  };
100
+ const isSettled = () => isFetched() && !isFetching();
69
101
  return {
70
102
  data,
71
103
  error,
72
- isLoading
104
+ isLoading,
105
+ isFetching,
106
+ isSettled
107
+ };
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/lib/create-preload.ts
112
+ /**
113
+ * Reactive, fire-and-forget prewarm. Resolves the query (calling it if it's a
114
+ * function so it tracks reactive deps), dedupes on the query's stable identity
115
+ * hash, and warms it into the local cache via `db.preload`. No subscription and
116
+ * no cleanup: preload registers nothing that needs tearing down.
117
+ *
118
+ * Typical use: inside a list row, preload the detail query the user is likely
119
+ * to open next, so navigation paints from cache instead of the network.
120
+ */
121
+ function createPreload(dbOrQuery, queryOrOptions, maybeOptions) {
122
+ let db;
123
+ let finalQuery;
124
+ let options;
125
+ if (dbOrQuery instanceof SyncedDb) {
126
+ db = dbOrQuery;
127
+ finalQuery = queryOrOptions;
128
+ options = maybeOptions;
129
+ } else {
130
+ const contextDb = (0, solid_js.useContext)(Sp00kyContext);
131
+ if (!contextDb) throw new Error("createPreload: No db argument provided and no Sp00kyContext found. Either pass a SyncedDb instance or wrap your app in <Sp00kyProvider>.");
132
+ db = contextDb;
133
+ finalQuery = dbOrQuery;
134
+ options = queryOrOptions;
135
+ }
136
+ let prevHash;
137
+ (0, solid_js.createEffect)(() => {
138
+ if (!(options?.enabled?.() ?? true)) return;
139
+ const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
140
+ if (!query) return;
141
+ if (query.hash === prevHash) return;
142
+ prevHash = query.hash;
143
+ db.getSp00ky().preload(query, {
144
+ refresh: options?.refresh,
145
+ staleTime: options?.staleTime
146
+ });
147
+ });
148
+ }
149
+
150
+ //#endregion
151
+ //#region src/lib/use-sync-status.ts
152
+ /**
153
+ * Observe sync health for a "can't reach the server" banner / indicator.
154
+ *
155
+ * Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
156
+ * remote 500 on query registration, a dropped socket) are absorbed by the
157
+ * retry and never flip this; `isDegraded()` only goes true once failures
158
+ * persist for the configured number of consecutive rounds (sp00ky core config
159
+ * `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
160
+ * the next successful round. Must be used within a `<Sp00kyProvider>`.
161
+ */
162
+ function useSyncStatus() {
163
+ const db = useDb();
164
+ const [health, setHealth] = (0, solid_js.createSignal)(db.syncHealth);
165
+ (0, solid_js.onCleanup)(db.subscribeToSyncHealth(setHealth));
166
+ return {
167
+ health,
168
+ status: () => health().status,
169
+ isHealthy: () => health().status === "healthy",
170
+ isDegraded: () => health().status === "degraded",
171
+ everConnected: () => health().everConnected,
172
+ isOffline: () => health().status === "degraded" && health().everConnected
173
+ };
174
+ }
175
+
176
+ //#endregion
177
+ //#region src/lib/use-crdt-field.ts
178
+ function useCrdtField(table, recordId, field, fallbackText) {
179
+ const db = (0, solid_js.useContext)(Sp00kyContext);
180
+ if (!db) throw new Error("useCrdtField must be used within a <Sp00kyProvider>");
181
+ const [crdtField, setCrdtField] = (0, solid_js.createSignal)(null);
182
+ let currentId;
183
+ let initialized = false;
184
+ (0, solid_js.createEffect)(() => {
185
+ const id = recordId();
186
+ if (initialized && id === currentId) return;
187
+ if (currentId && crdtField()) {
188
+ db.getSp00ky().closeCrdtField(table, currentId, field);
189
+ setCrdtField(null);
190
+ }
191
+ currentId = id;
192
+ initialized = true;
193
+ if (!id) return;
194
+ const sp00ky = db.getSp00ky();
195
+ const text = fallbackText?.();
196
+ sp00ky.openCrdtField(table, id, field, text).then((cf) => {
197
+ if (currentId === id) setCrdtField(cf);
198
+ }).catch((err) => {
199
+ console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);
200
+ });
201
+ });
202
+ (0, solid_js.onCleanup)(() => {
203
+ if (currentId && crdtField()) {
204
+ db.getSp00ky().closeCrdtField(table, currentId, field);
205
+ setCrdtField(null);
206
+ }
207
+ });
208
+ return crdtField;
209
+ }
210
+
211
+ //#endregion
212
+ //#region src/lib/use-feature-flag.ts
213
+ /**
214
+ * Subscribe to a feature flag for the currently authenticated user.
215
+ *
216
+ * Returns three Solid accessors that update reactively whenever the
217
+ * server-materialized assignment in `_00_user_feature` changes. Backed by
218
+ * the same SSP + sync pipeline that powers `useQuery`, so toggling a flag
219
+ * via `spky flag enable <key>` propagates to the UI without a refresh.
220
+ *
221
+ * `enabled()` is `true` when the resolved variant exists and is not 'off'.
222
+ * For multi-variant flags, prefer `variant()` directly.
223
+ */
224
+ function useFeatureFlag(key, options) {
225
+ const handle = useDb().getSp00ky().feature(key, options);
226
+ const [variant, setVariant] = (0, solid_js.createSignal)(handle.variant());
227
+ const [payload, setPayload] = (0, solid_js.createSignal)(handle.payload());
228
+ const unsub = handle.subscribe((s) => {
229
+ setVariant(s.variant ?? options?.fallback);
230
+ setPayload(s.payload);
231
+ });
232
+ (0, solid_js.onCleanup)(() => {
233
+ unsub();
234
+ handle.close();
235
+ });
236
+ return {
237
+ variant,
238
+ payload,
239
+ enabled: () => {
240
+ const v = variant();
241
+ return v !== void 0 && v !== "off";
242
+ }
243
+ };
244
+ }
245
+
246
+ //#endregion
247
+ //#region src/lib/use-app-release.ts
248
+ async function reloadForSnapshot(snapshot) {
249
+ if (typeof window === "undefined") return;
250
+ if (snapshot.cacheBust) try {
251
+ if (window.caches) {
252
+ const keys = await window.caches.keys();
253
+ await Promise.all(keys.map((k) => window.caches.delete(k)));
254
+ }
255
+ if (navigator.serviceWorker) {
256
+ const regs = await navigator.serviceWorker.getRegistrations();
257
+ for (const r of regs) r.update().catch(() => {});
258
+ }
259
+ window.location.href = window.location.pathname + "?cb=" + Date.now();
260
+ return;
261
+ } catch {}
262
+ window.location.reload();
263
+ }
264
+ /**
265
+ * Observe the app's announced release (`_00_app_release:<app>`, written by
266
+ * `spky deploy` / `spky release`) and compare it against the running build.
267
+ *
268
+ * Typical use: mount a small "new version available — Reload" notification
269
+ * gated on `updateAvailable()`, auto-invoking `reload()` when `mandatory()`
270
+ * (guard the auto path against reload loops with a per-version marker, since
271
+ * a client can reload while the deploy is still rolling out and land on the
272
+ * old bundle again).
273
+ */
274
+ function useAppRelease(options) {
275
+ const handle = useDb().getSp00ky().appRelease(options.app, { ttl: options.ttl });
276
+ const [snapshot, setSnapshot] = (0, solid_js.createSignal)(handle.snapshot());
277
+ const unsub = handle.subscribe(setSnapshot);
278
+ (0, solid_js.onCleanup)(() => {
279
+ unsub();
280
+ handle.close();
281
+ });
282
+ const updateAvailable = () => (0, _spooky_sync_core.semverGt)(snapshot().version, options.currentVersion);
283
+ return {
284
+ latestVersion: () => snapshot().version,
285
+ updateAvailable,
286
+ mandatory: () => updateAvailable() && snapshot().mandatory,
287
+ cacheBust: () => snapshot().cacheBust,
288
+ reload: () => reloadForSnapshot(snapshot())
73
289
  };
74
290
  }
75
291
 
@@ -95,7 +311,7 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
95
311
  const validate = (file) => {
96
312
  const config = db.getBucketConfig(bucketName);
97
313
  if (!config) return;
98
- if (config.maxSize != null && file.size > config.maxSize) {
314
+ if (config.maxSize !== null && config.maxSize !== void 0 && file.size > config.maxSize) {
99
315
  const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
100
316
  throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
101
317
  }
@@ -204,9 +420,8 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
204
420
  const [error, setError] = (0, solid_js.createSignal)(null);
205
421
  let currentKey = null;
206
422
  let privateUrl = null;
207
- let refetchTrigger;
208
423
  const [refetchSignal, setRefetchSignal] = (0, solid_js.createSignal)(0);
209
- refetchTrigger = () => setRefetchSignal((n) => n + 1);
424
+ const refetchTrigger = () => setRefetchSignal((n) => n + 1);
210
425
  async function doDownload(key, filePath) {
211
426
  if (useCache) {
212
427
  const cached = downloadCache.get(key);
@@ -326,26 +541,31 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
326
541
  }
327
542
 
328
543
  //#endregion
329
- //#region src/lib/SpookyProvider.ts
330
- function SpookyProvider(props) {
544
+ //#region src/lib/Sp00kyProvider.ts
545
+ function Sp00kyProvider(props) {
331
546
  const merged = (0, solid_js.mergeProps)({ fallback: void 0 }, props);
332
547
  const [db, setDb] = (0, solid_js.createSignal)(void 0);
333
548
  (0, solid_js.onMount)(async () => {
334
549
  try {
335
550
  const instance = new SyncedDb(merged.config);
336
551
  await instance.init();
552
+ if (merged.preload) try {
553
+ await merged.preload(instance);
554
+ } catch (e) {
555
+ console.error("Sp00kyProvider: preload failed; revealing UI anyway", e);
556
+ }
337
557
  setDb(() => instance);
338
558
  merged.onReady?.(instance);
339
559
  } catch (e) {
340
560
  const error = e instanceof Error ? e : new Error(String(e));
341
561
  if (merged.onError) merged.onError(error);
342
- else console.error("SpookyProvider: Failed to initialize database", error);
562
+ else console.error("Sp00kyProvider: Failed to initialize database", error);
343
563
  }
344
564
  });
345
565
  return (0, solid_js.createMemo)(() => {
346
566
  const instance = db();
347
567
  if (!instance) return merged.fallback;
348
- return (0, solid_js.createComponent)(SpookyContext.Provider, {
568
+ return (0, solid_js.createComponent)(Sp00kyContext.Provider, {
349
569
  value: instance,
350
570
  get children() {
351
571
  return merged.children;
@@ -357,69 +577,82 @@ function SpookyProvider(props) {
357
577
  //#endregion
358
578
  //#region src/index.ts
359
579
  /**
360
- * SyncedDb - A thin wrapper around spooky-ts for Solid.js integration
361
- * Delegates all logic to the underlying spooky-ts instance
580
+ * SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
581
+ * Delegates all logic to the underlying sp00ky-ts instance
362
582
  */
363
583
  var SyncedDb = class {
364
584
  constructor(config) {
365
- this.spooky = null;
585
+ this.sp00ky = null;
366
586
  this._initialized = false;
367
587
  this.config = config;
368
588
  }
369
- getSpooky() {
370
- if (!this.spooky) throw new Error("SyncedDb not initialized");
371
- return this.spooky;
589
+ getSp00ky() {
590
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
591
+ return this.sp00ky;
372
592
  }
373
593
  /**
374
- * Initialize the spooky-ts instance
594
+ * Initialize the sp00ky-ts instance
375
595
  */
376
596
  async init() {
377
597
  if (this._initialized) return;
378
- this.spooky = new _spooky_sync_core.SpookyClient(this.config);
379
- await this.spooky.init();
598
+ this.sp00ky = new _spooky_sync_core.Sp00kyClient(this.config);
599
+ await this.sp00ky.init();
380
600
  this._initialized = true;
381
601
  }
382
602
  /**
383
603
  * Create a new record in the database
384
604
  */
385
605
  async create(id, payload) {
386
- if (!this.spooky) throw new Error("SyncedDb not initialized");
387
- await this.spooky.create(id, payload);
606
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
607
+ await this.sp00ky.create(id, payload);
388
608
  }
389
609
  /**
390
610
  * Update an existing record in the database
391
611
  */
392
612
  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);
613
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
614
+ await this.sp00ky.update(tableName, recordId, payload, options);
395
615
  }
396
616
  /**
397
617
  * Delete an existing record in the database
398
618
  */
399
619
  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);
620
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
621
+ const isRecordId = selector instanceof surrealdb.RecordId || selector?.constructor?.name === "RecordId";
622
+ let id;
623
+ if (typeof selector === "string") id = selector;
624
+ else if (isRecordId) id = `${tableName}:${selector.id}`;
625
+ else throw new Error("Only string ID or RecordId selectors are supported currently with core");
626
+ await this.sp00ky.delete(tableName, id);
627
+ }
628
+ /**
629
+ * Preload/prewarm a built query into the local cache without registering a
630
+ * live view. Fetches once and stores the rows (+ embedded related children)
631
+ * locally so a later `useQuery` for the same data paints instantly. Best-effort.
632
+ */
633
+ async preload(finalQuery, options) {
634
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
635
+ await this.sp00ky.preload(finalQuery, options);
403
636
  }
404
637
  /**
405
638
  * Query data from the database
406
639
  */
407
640
  query(table) {
408
- if (!this.spooky) throw new Error("SyncedDb not initialized");
409
- return this.spooky.query(table, {});
641
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
642
+ return this.sp00ky.query(table, {});
410
643
  }
411
644
  /**
412
645
  * Run a backend operation
413
646
  */
414
647
  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);
648
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
649
+ await this.sp00ky.run(backend, path, payload, options);
417
650
  }
418
651
  /**
419
652
  * Authenticate with the database
420
653
  */
421
654
  async authenticate(token) {
422
- await this.spooky?.authenticate(token);
655
+ await this.sp00ky?.authenticate(token);
423
656
  return new surrealdb.RecordId("user", "me");
424
657
  }
425
658
  /**
@@ -433,48 +666,67 @@ var SyncedDb = class {
433
666
  * Sign out, clear session and local storage
434
667
  */
435
668
  async signOut() {
436
- if (!this.spooky) throw new Error("SyncedDb not initialized");
437
- await this.spooky.auth.signOut();
669
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
670
+ await this.sp00ky.auth.signOut();
438
671
  }
439
672
  /**
440
673
  * Execute a function with direct access to the remote database connection
441
674
  */
442
675
  async useRemote(fn) {
443
- if (!this.spooky) throw new Error("SyncedDb not initialized");
444
- return await this.spooky.useRemote(fn);
676
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
677
+ return await this.sp00ky.useRemote(fn);
445
678
  }
446
679
  /**
447
680
  * Access the remote database service directly
448
681
  */
449
682
  get remote() {
450
- if (!this.spooky) throw new Error("SyncedDb not initialized");
451
- return this.spooky.remoteClient;
683
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
684
+ return this.sp00ky.remoteClient;
452
685
  }
453
686
  /**
454
687
  * Access the local database service directly
455
688
  */
456
689
  get local() {
457
- if (!this.spooky) throw new Error("SyncedDb not initialized");
458
- return this.spooky.localClient;
690
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
691
+ return this.sp00ky.localClient;
459
692
  }
460
693
  /**
461
694
  * Access the auth service
462
695
  */
463
696
  get auth() {
464
- if (!this.spooky) throw new Error("SyncedDb not initialized");
465
- return this.spooky.auth;
697
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
698
+ return this.sp00ky.auth;
466
699
  }
467
700
  get pendingMutationCount() {
468
- if (!this.spooky) throw new Error("SyncedDb not initialized");
469
- return this.spooky.pendingMutationCount;
701
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
702
+ return this.sp00ky.pendingMutationCount;
703
+ }
704
+ /** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
705
+ get liveRetryCount() {
706
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
707
+ return this.sp00ky.liveRetryCount;
470
708
  }
471
709
  subscribeToPendingMutations(cb) {
472
- if (!this.spooky) throw new Error("SyncedDb not initialized");
473
- return this.spooky.subscribeToPendingMutations(cb);
710
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
711
+ return this.sp00ky.subscribeToPendingMutations(cb);
712
+ }
713
+ /** Current sync-health snapshot. See {@link useSyncStatus}. */
714
+ get syncHealth() {
715
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
716
+ return this.sp00ky.syncHealth;
717
+ }
718
+ /**
719
+ * Observe sync health. Fires immediately with the current status and again
720
+ * on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
721
+ * components; this is the imperative escape hatch.
722
+ */
723
+ subscribeToSyncHealth(cb) {
724
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
725
+ return this.sp00ky.subscribeToSyncHealth(cb);
474
726
  }
475
727
  bucket(name) {
476
- if (!this.spooky) throw new Error("SyncedDb not initialized");
477
- return this.spooky.bucket(name);
728
+ if (!this.sp00ky) throw new Error("SyncedDb not initialized");
729
+ return this.sp00ky.bucket(name);
478
730
  }
479
731
  getBucketConfig(name) {
480
732
  return this.config.schema.buckets?.find((b) => b.name === name);
@@ -483,11 +735,16 @@ var SyncedDb = class {
483
735
 
484
736
  //#endregion
485
737
  exports.RecordId = surrealdb.RecordId;
486
- exports.SpookyProvider = SpookyProvider;
738
+ exports.Sp00kyProvider = Sp00kyProvider;
487
739
  exports.SyncedDb = SyncedDb;
488
740
  exports.Uuid = surrealdb.Uuid;
741
+ exports.createPreload = createPreload;
742
+ exports.useAppRelease = useAppRelease;
743
+ exports.useCrdtField = useCrdtField;
489
744
  exports.useDb = useDb;
490
745
  exports.useDownloadFile = useDownloadFile;
746
+ exports.useFeatureFlag = useFeatureFlag;
491
747
  exports.useFileUpload = useFileUpload;
492
748
  exports.useQuery = useQuery;
749
+ exports.useSyncStatus = useSyncStatus;
493
750
  //# sourceMappingURL=index.cjs.map