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