@spooky-sync/client-solid 0.0.1-canary.20 → 0.0.1-canary.201
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 +652 -158
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +362 -22
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +362 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +644 -160
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/skills/{spooky-solid → sp00ky-solid}/SKILL.md +85 -14
- package/skills/{spooky-solid → sp00ky-solid}/references/file-hooks.md +34 -5
- package/src/cache/index.ts +1 -1
- package/src/cache/surrealdb-wasm-factory.ts +4 -1
- package/src/index.ts +178 -61
- package/src/lib/Blurhash.ts +54 -0
- package/src/lib/BucketImage.ts +141 -0
- package/src/lib/Sp00kyProvider.ts +104 -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-blurhash.ts +77 -0
- package/src/lib/use-bucket-image.ts +100 -0
- package/src/lib/use-crdt-field.ts +68 -0
- package/src/lib/use-download-file.ts +66 -110
- package/src/lib/use-feature-flag.ts +50 -0
- package/src/lib/use-file-upload.ts +10 -4
- package/src/lib/use-query.ts +168 -28
- package/src/lib/use-storage-status.ts +44 -0
- package/src/lib/use-sync-status.ts +64 -0
- package/src/types/index.ts +3 -4
- package/src/lib/SpookyProvider.ts +0 -55
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
|
|
8
|
+
const Sp00kyContext = (0, solid_js.createContext)();
|
|
8
9
|
function useDb() {
|
|
9
|
-
const db = (0, solid_js.useContext)(
|
|
10
|
-
if (!db) throw new Error("useDb must be used within a <
|
|
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,73 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
22
23
|
finalQuery = queryOrOptions;
|
|
23
24
|
options = maybeOptions;
|
|
24
25
|
} else {
|
|
25
|
-
const contextDb = (0, solid_js.useContext)(
|
|
26
|
-
if (!contextDb) throw new Error("useQuery: No db argument provided and no
|
|
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 [
|
|
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
|
-
|
|
37
|
-
|
|
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
|
+
/**
|
|
51
|
+
* Registration can fail — the canonical case is the SSP answering 503
|
|
52
|
+
* NOT_READY while it bootstraps. Nothing here used to catch that: the
|
|
53
|
+
* rejection escaped as an unhandled promise, `isFetched` stayed false, and
|
|
54
|
+
* `isLoading()` therefore stayed true FOREVER, which is what a spinner that
|
|
55
|
+
* never resolves actually was. Surface it as `error()` instead; the sync
|
|
56
|
+
* scheduler retries the registration underneath, so a transient failure
|
|
57
|
+
* still recovers on its own.
|
|
58
|
+
*/
|
|
59
|
+
const initQuery = async (query, myRun) => {
|
|
60
|
+
try {
|
|
61
|
+
await subscribeQuery(query, myRun);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (myRun !== runId) return;
|
|
64
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const subscribeQuery = async (query, myRun) => {
|
|
38
68
|
const { hash } = await query.run();
|
|
69
|
+
if (myRun !== runId) return;
|
|
70
|
+
activeHash = hash;
|
|
39
71
|
setError(void 0);
|
|
40
72
|
let isFirstCall = true;
|
|
41
|
-
const unsub = await
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
const unsub = await sp00ky.subscribe(hash, (e) => {
|
|
74
|
+
const queryData = query.isOne ? e[0] : e;
|
|
75
|
+
const reconcileStart = performance.now();
|
|
76
|
+
setState("value", (0, solid_js_store.reconcile)(queryData, { key: "id" }));
|
|
77
|
+
setVersion((v) => v + 1);
|
|
78
|
+
sp00ky.reportFrontendTiming(hash, performance.now() - reconcileStart);
|
|
79
|
+
const hasData = query.isOne ? queryData !== null && queryData !== void 0 : e.length > 0;
|
|
45
80
|
if (!isFirstCall || hasData) setIsFetched(true);
|
|
46
81
|
isFirstCall = false;
|
|
47
82
|
}, { immediate: true });
|
|
48
|
-
|
|
83
|
+
const unsubStatus = sp00ky.subscribeQueryStatus(hash, (status) => setIsFetching(status === "fetching"), { immediate: true });
|
|
84
|
+
const teardown = () => {
|
|
85
|
+
unsub();
|
|
86
|
+
unsubStatus();
|
|
87
|
+
};
|
|
88
|
+
if (myRun !== runId) {
|
|
89
|
+
teardown();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
activeUnsub = teardown;
|
|
49
93
|
};
|
|
50
94
|
(0, solid_js.createEffect)(() => {
|
|
51
95
|
if (!(options?.enabled?.() ?? true)) {
|
|
@@ -54,22 +98,237 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
54
98
|
}
|
|
55
99
|
const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
|
|
56
100
|
if (!query) return;
|
|
57
|
-
const queryString =
|
|
101
|
+
const queryString = String(query.hash);
|
|
58
102
|
if (queryString === prevQueryString) return;
|
|
59
103
|
prevQueryString = queryString;
|
|
104
|
+
const myRun = ++runId;
|
|
105
|
+
teardownActive();
|
|
60
106
|
setIsFetched(false);
|
|
61
|
-
|
|
62
|
-
(
|
|
63
|
-
|
|
64
|
-
|
|
107
|
+
setError(void 0);
|
|
108
|
+
initQuery(query, myRun);
|
|
109
|
+
});
|
|
110
|
+
(0, solid_js.onCleanup)(() => {
|
|
111
|
+
runId++;
|
|
112
|
+
teardownActive();
|
|
113
|
+
if (options?.deregisterOnCleanup && activeHash) sp00ky.deregisterQuery(activeHash);
|
|
65
114
|
});
|
|
66
115
|
const isLoading = () => {
|
|
67
116
|
return !isFetched() && error() === void 0;
|
|
68
117
|
};
|
|
118
|
+
const isSettled = () => isFetched() && !isFetching();
|
|
69
119
|
return {
|
|
70
120
|
data,
|
|
71
121
|
error,
|
|
72
|
-
isLoading
|
|
122
|
+
isLoading,
|
|
123
|
+
isFetching,
|
|
124
|
+
isSettled
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/lib/create-preload.ts
|
|
130
|
+
/**
|
|
131
|
+
* Reactive, fire-and-forget prewarm. Resolves the query (calling it if it's a
|
|
132
|
+
* function so it tracks reactive deps), dedupes on the query's stable identity
|
|
133
|
+
* hash, and warms it into the local cache via `db.preload`. No subscription and
|
|
134
|
+
* no cleanup: preload registers nothing that needs tearing down.
|
|
135
|
+
*
|
|
136
|
+
* Typical use: inside a list row, preload the detail query the user is likely
|
|
137
|
+
* to open next, so navigation paints from cache instead of the network.
|
|
138
|
+
*/
|
|
139
|
+
function createPreload(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
140
|
+
let db;
|
|
141
|
+
let finalQuery;
|
|
142
|
+
let options;
|
|
143
|
+
if (dbOrQuery instanceof SyncedDb) {
|
|
144
|
+
db = dbOrQuery;
|
|
145
|
+
finalQuery = queryOrOptions;
|
|
146
|
+
options = maybeOptions;
|
|
147
|
+
} else {
|
|
148
|
+
const contextDb = (0, solid_js.useContext)(Sp00kyContext);
|
|
149
|
+
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>.");
|
|
150
|
+
db = contextDb;
|
|
151
|
+
finalQuery = dbOrQuery;
|
|
152
|
+
options = queryOrOptions;
|
|
153
|
+
}
|
|
154
|
+
let prevHash;
|
|
155
|
+
(0, solid_js.createEffect)(() => {
|
|
156
|
+
if (!(options?.enabled?.() ?? true)) return;
|
|
157
|
+
const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
|
|
158
|
+
if (!query) return;
|
|
159
|
+
if (query.hash === prevHash) return;
|
|
160
|
+
prevHash = query.hash;
|
|
161
|
+
db.getSp00ky().preload(query, {
|
|
162
|
+
refresh: options?.refresh,
|
|
163
|
+
staleTime: options?.staleTime
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/lib/use-sync-status.ts
|
|
170
|
+
/**
|
|
171
|
+
* Observe sync health for a "can't reach the server" banner / indicator.
|
|
172
|
+
*
|
|
173
|
+
* Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
|
|
174
|
+
* remote 500 on query registration, a dropped socket) are absorbed by the
|
|
175
|
+
* retry and never flip this; `isDegraded()` only goes true once failures
|
|
176
|
+
* persist for the configured number of consecutive rounds (sp00ky core config
|
|
177
|
+
* `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
|
|
178
|
+
* the next successful round. Must be used within a `<Sp00kyProvider>`.
|
|
179
|
+
*/
|
|
180
|
+
function useSyncStatus() {
|
|
181
|
+
const db = useDb();
|
|
182
|
+
const [health, setHealth] = (0, solid_js.createSignal)(db.syncHealth);
|
|
183
|
+
(0, solid_js.onCleanup)(db.subscribeToSyncHealth(setHealth));
|
|
184
|
+
return {
|
|
185
|
+
health,
|
|
186
|
+
status: () => health().status,
|
|
187
|
+
isHealthy: () => health().status === "healthy",
|
|
188
|
+
isDegraded: () => health().status === "degraded",
|
|
189
|
+
everConnected: () => health().everConnected,
|
|
190
|
+
isOffline: () => health().status === "degraded" && health().everConnected,
|
|
191
|
+
connection: () => health().connection,
|
|
192
|
+
isReconnecting: () => health().connection === "reconnecting"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/lib/use-storage-status.ts
|
|
198
|
+
/**
|
|
199
|
+
* Observe how durable the LOCAL cache is, for a "no local storage" warning.
|
|
200
|
+
*
|
|
201
|
+
* Under `localEngine: 'sqlite'` with `store: 'indexeddb'` the durable store is
|
|
202
|
+
* the OPFS SAHPool VFS, and only ONE client per bucket can hold it open: a
|
|
203
|
+
* second tab of the same app cannot get it and runs in memory instead (the
|
|
204
|
+
* engine retries first, so a closing tab's lock is usually waited out). Must be
|
|
205
|
+
* used within a `<Sp00kyProvider>`.
|
|
206
|
+
*/
|
|
207
|
+
function useStorageStatus() {
|
|
208
|
+
const db = useDb();
|
|
209
|
+
const [health, setHealth] = (0, solid_js.createSignal)(db.storageHealth);
|
|
210
|
+
(0, solid_js.onCleanup)(db.subscribeToStorageHealth(setHealth));
|
|
211
|
+
return {
|
|
212
|
+
health,
|
|
213
|
+
status: () => health().status,
|
|
214
|
+
isPersistent: () => health().status === "persistent",
|
|
215
|
+
isMemoryFallback: () => health().fallback
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/lib/use-crdt-field.ts
|
|
221
|
+
function useCrdtField(table, recordId, field, fallbackText) {
|
|
222
|
+
const db = (0, solid_js.useContext)(Sp00kyContext);
|
|
223
|
+
if (!db) throw new Error("useCrdtField must be used within a <Sp00kyProvider>");
|
|
224
|
+
const [crdtField, setCrdtField] = (0, solid_js.createSignal)(null);
|
|
225
|
+
let currentId;
|
|
226
|
+
let initialized = false;
|
|
227
|
+
(0, solid_js.createEffect)(() => {
|
|
228
|
+
const id = recordId();
|
|
229
|
+
if (initialized && id === currentId) return;
|
|
230
|
+
if (currentId && crdtField()) {
|
|
231
|
+
db.getSp00ky().closeCrdtField(table, currentId, field);
|
|
232
|
+
setCrdtField(null);
|
|
233
|
+
}
|
|
234
|
+
currentId = id;
|
|
235
|
+
initialized = true;
|
|
236
|
+
if (!id) return;
|
|
237
|
+
const sp00ky = db.getSp00ky();
|
|
238
|
+
const text = fallbackText?.();
|
|
239
|
+
sp00ky.openCrdtField(table, id, field, text).then((cf) => {
|
|
240
|
+
if (currentId === id) setCrdtField(cf);
|
|
241
|
+
}).catch((err) => {
|
|
242
|
+
console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
(0, solid_js.onCleanup)(() => {
|
|
246
|
+
if (currentId && crdtField()) {
|
|
247
|
+
db.getSp00ky().closeCrdtField(table, currentId, field);
|
|
248
|
+
setCrdtField(null);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
return crdtField;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/lib/use-feature-flag.ts
|
|
256
|
+
/**
|
|
257
|
+
* Subscribe to a feature flag for the currently authenticated user.
|
|
258
|
+
*
|
|
259
|
+
* Returns three Solid accessors that update reactively whenever the
|
|
260
|
+
* server-materialized assignment in `_00_user_feature` changes. Backed by
|
|
261
|
+
* the same SSP + sync pipeline that powers `useQuery`, so toggling a flag
|
|
262
|
+
* via `spky flag enable <key>` propagates to the UI without a refresh.
|
|
263
|
+
*
|
|
264
|
+
* `enabled()` is `true` when the resolved variant exists and is not 'off'.
|
|
265
|
+
* For multi-variant flags, prefer `variant()` directly.
|
|
266
|
+
*/
|
|
267
|
+
function useFeatureFlag(key, options) {
|
|
268
|
+
const handle = useDb().getSp00ky().feature(key, options);
|
|
269
|
+
const [variant, setVariant] = (0, solid_js.createSignal)(handle.variant());
|
|
270
|
+
const [payload, setPayload] = (0, solid_js.createSignal)(handle.payload());
|
|
271
|
+
const unsub = handle.subscribe((s) => {
|
|
272
|
+
setVariant(s.variant ?? options?.fallback);
|
|
273
|
+
setPayload(s.payload);
|
|
274
|
+
});
|
|
275
|
+
(0, solid_js.onCleanup)(() => {
|
|
276
|
+
unsub();
|
|
277
|
+
handle.close();
|
|
278
|
+
});
|
|
279
|
+
return {
|
|
280
|
+
variant,
|
|
281
|
+
payload,
|
|
282
|
+
enabled: () => {
|
|
283
|
+
const v = variant();
|
|
284
|
+
return v !== void 0 && v !== "off";
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/lib/use-app-release.ts
|
|
291
|
+
async function reloadForSnapshot(snapshot) {
|
|
292
|
+
if (typeof window === "undefined") return;
|
|
293
|
+
if (snapshot.cacheBust) try {
|
|
294
|
+
if (window.caches) {
|
|
295
|
+
const keys = await window.caches.keys();
|
|
296
|
+
await Promise.all(keys.map((k) => window.caches.delete(k)));
|
|
297
|
+
}
|
|
298
|
+
if (navigator.serviceWorker) {
|
|
299
|
+
const regs = await navigator.serviceWorker.getRegistrations();
|
|
300
|
+
for (const r of regs) r.update().catch(() => {});
|
|
301
|
+
}
|
|
302
|
+
window.location.href = window.location.pathname + "?cb=" + Date.now();
|
|
303
|
+
return;
|
|
304
|
+
} catch {}
|
|
305
|
+
window.location.reload();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Observe the app's announced release (`_00_app_release:<app>`, written by
|
|
309
|
+
* `spky deploy` / `spky release`) and compare it against the running build.
|
|
310
|
+
*
|
|
311
|
+
* Typical use: mount a small "new version available — Reload" notification
|
|
312
|
+
* gated on `updateAvailable()`, auto-invoking `reload()` when `mandatory()`
|
|
313
|
+
* (guard the auto path against reload loops with a per-version marker, since
|
|
314
|
+
* a client can reload while the deploy is still rolling out and land on the
|
|
315
|
+
* old bundle again).
|
|
316
|
+
*/
|
|
317
|
+
function useAppRelease(options) {
|
|
318
|
+
const handle = useDb().getSp00ky().appRelease(options.app, { ttl: options.ttl });
|
|
319
|
+
const [snapshot, setSnapshot] = (0, solid_js.createSignal)(handle.snapshot());
|
|
320
|
+
const unsub = handle.subscribe(setSnapshot);
|
|
321
|
+
(0, solid_js.onCleanup)(() => {
|
|
322
|
+
unsub();
|
|
323
|
+
handle.close();
|
|
324
|
+
});
|
|
325
|
+
const updateAvailable = () => (0, _spooky_sync_core.semverGt)(snapshot().version, options.currentVersion);
|
|
326
|
+
return {
|
|
327
|
+
latestVersion: () => snapshot().version,
|
|
328
|
+
updateAvailable,
|
|
329
|
+
mandatory: () => updateAvailable() && snapshot().mandatory,
|
|
330
|
+
cacheBust: () => snapshot().cacheBust,
|
|
331
|
+
reload: () => reloadForSnapshot(snapshot())
|
|
73
332
|
};
|
|
74
333
|
}
|
|
75
334
|
|
|
@@ -95,7 +354,7 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
|
|
|
95
354
|
const validate = (file) => {
|
|
96
355
|
const config = db.getBucketConfig(bucketName);
|
|
97
356
|
if (!config) return;
|
|
98
|
-
if (config.maxSize
|
|
357
|
+
if (config.maxSize !== null && config.maxSize !== void 0 && file.size > config.maxSize) {
|
|
99
358
|
const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
|
|
100
359
|
throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
|
|
101
360
|
}
|
|
@@ -107,7 +366,7 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
|
|
|
107
366
|
}
|
|
108
367
|
}
|
|
109
368
|
};
|
|
110
|
-
const upload = async (path, file) => {
|
|
369
|
+
const upload = async (path, file, options) => {
|
|
111
370
|
setError(null);
|
|
112
371
|
try {
|
|
113
372
|
validate(file);
|
|
@@ -118,7 +377,7 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
|
|
|
118
377
|
setIsUploading(true);
|
|
119
378
|
try {
|
|
120
379
|
const bytes = await (0, _spooky_sync_core.fileToUint8Array)(file);
|
|
121
|
-
await db.bucket(bucketName).put(path, bytes);
|
|
380
|
+
return await db.bucket(bucketName).put(path, bytes, options);
|
|
122
381
|
} catch (e) {
|
|
123
382
|
setError(e instanceof Error ? e : new Error(String(e)));
|
|
124
383
|
} finally {
|
|
@@ -168,20 +427,6 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
|
|
|
168
427
|
|
|
169
428
|
//#endregion
|
|
170
429
|
//#region src/lib/use-download-file.ts
|
|
171
|
-
const downloadCache = /* @__PURE__ */ new Map();
|
|
172
|
-
const inflightRequests = /* @__PURE__ */ new Map();
|
|
173
|
-
function cacheKey(bucket, path) {
|
|
174
|
-
return `${bucket}:${path}`;
|
|
175
|
-
}
|
|
176
|
-
function releaseEntry(key) {
|
|
177
|
-
const entry = downloadCache.get(key);
|
|
178
|
-
if (!entry) return;
|
|
179
|
-
entry.refCount--;
|
|
180
|
-
if (entry.refCount <= 0) {
|
|
181
|
-
URL.revokeObjectURL(entry.url);
|
|
182
|
-
downloadCache.delete(key);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
430
|
function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeOptions) {
|
|
186
431
|
let db;
|
|
187
432
|
let bucketName;
|
|
@@ -202,63 +447,15 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
|
|
|
202
447
|
const [url, setUrl] = (0, solid_js.createSignal)(null);
|
|
203
448
|
const [isLoading, setIsLoading] = (0, solid_js.createSignal)(false);
|
|
204
449
|
const [error, setError] = (0, solid_js.createSignal)(null);
|
|
205
|
-
let
|
|
450
|
+
let lease = null;
|
|
206
451
|
let privateUrl = null;
|
|
207
|
-
let refetchTrigger;
|
|
208
452
|
const [refetchSignal, setRefetchSignal] = (0, solid_js.createSignal)(0);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
currentKey = key;
|
|
216
|
-
return cached.url;
|
|
217
|
-
}
|
|
218
|
-
const inflight = inflightRequests.get(key);
|
|
219
|
-
if (inflight) {
|
|
220
|
-
const result = await inflight;
|
|
221
|
-
if (result) {
|
|
222
|
-
const entry = downloadCache.get(key);
|
|
223
|
-
if (entry) {
|
|
224
|
-
entry.refCount++;
|
|
225
|
-
currentKey = key;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return result;
|
|
229
|
-
}
|
|
230
|
-
const promise = (async () => {
|
|
231
|
-
const content = await db.bucket(bucketName).get(filePath);
|
|
232
|
-
if (!content) return null;
|
|
233
|
-
const objectUrl = URL.createObjectURL(new Blob([content]));
|
|
234
|
-
downloadCache.set(key, {
|
|
235
|
-
url: objectUrl,
|
|
236
|
-
refCount: 1
|
|
237
|
-
});
|
|
238
|
-
return objectUrl;
|
|
239
|
-
})();
|
|
240
|
-
inflightRequests.set(key, promise);
|
|
241
|
-
try {
|
|
242
|
-
const result = await promise;
|
|
243
|
-
currentKey = key;
|
|
244
|
-
return result;
|
|
245
|
-
} finally {
|
|
246
|
-
inflightRequests.delete(key);
|
|
247
|
-
}
|
|
248
|
-
} else {
|
|
249
|
-
const content = await db.bucket(bucketName).get(filePath);
|
|
250
|
-
if (!content) return null;
|
|
251
|
-
const objectUrl = URL.createObjectURL(new Blob([content]));
|
|
252
|
-
privateUrl = objectUrl;
|
|
253
|
-
return objectUrl;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
function releaseCurrentEntry() {
|
|
257
|
-
if (useCache && currentKey) {
|
|
258
|
-
releaseEntry(currentKey);
|
|
259
|
-
currentKey = null;
|
|
260
|
-
}
|
|
261
|
-
if (!useCache && privateUrl) {
|
|
453
|
+
/** Consumed by the next effect run, so `refetch()` bypasses every layer once. */
|
|
454
|
+
let reloadOnce = false;
|
|
455
|
+
function releaseCurrent() {
|
|
456
|
+
lease?.release();
|
|
457
|
+
lease = null;
|
|
458
|
+
if (privateUrl) {
|
|
262
459
|
URL.revokeObjectURL(privateUrl);
|
|
263
460
|
privateUrl = null;
|
|
264
461
|
}
|
|
@@ -266,29 +463,40 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
|
|
|
266
463
|
(0, solid_js.createEffect)(() => {
|
|
267
464
|
const filePath = path();
|
|
268
465
|
refetchSignal();
|
|
269
|
-
|
|
466
|
+
releaseCurrent();
|
|
270
467
|
if (!filePath) {
|
|
271
468
|
setUrl(null);
|
|
272
469
|
setIsLoading(false);
|
|
273
470
|
setError(null);
|
|
274
471
|
return;
|
|
275
472
|
}
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
const cached = downloadCache.get(key);
|
|
279
|
-
if (cached) {
|
|
280
|
-
cached.refCount++;
|
|
281
|
-
currentKey = key;
|
|
282
|
-
setUrl(cached.url);
|
|
283
|
-
setIsLoading(false);
|
|
284
|
-
setError(null);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
473
|
+
const reload = reloadOnce;
|
|
474
|
+
reloadOnce = false;
|
|
288
475
|
let cancelled = false;
|
|
289
476
|
setIsLoading(true);
|
|
290
477
|
setError(null);
|
|
291
|
-
|
|
478
|
+
const bucket = db.bucket(bucketName);
|
|
479
|
+
(useCache ? bucket.url(filePath, {
|
|
480
|
+
persist: options.persist !== false,
|
|
481
|
+
pin: options.pin,
|
|
482
|
+
revalidate: options.revalidate,
|
|
483
|
+
reload
|
|
484
|
+
}).then((acquired) => {
|
|
485
|
+
if (!acquired) return null;
|
|
486
|
+
if (cancelled) {
|
|
487
|
+
acquired.release();
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
lease = acquired;
|
|
491
|
+
return acquired.url;
|
|
492
|
+
}) : bucket.read(filePath, {
|
|
493
|
+
persist: false,
|
|
494
|
+
reload: true
|
|
495
|
+
}).then((blob) => {
|
|
496
|
+
if (!blob || cancelled) return null;
|
|
497
|
+
privateUrl = URL.createObjectURL(blob);
|
|
498
|
+
return privateUrl;
|
|
499
|
+
})).then((result) => {
|
|
292
500
|
if (!cancelled) {
|
|
293
501
|
setUrl(result);
|
|
294
502
|
setIsLoading(false);
|
|
@@ -304,18 +512,11 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
|
|
|
304
512
|
});
|
|
305
513
|
});
|
|
306
514
|
(0, solid_js.onCleanup)(() => {
|
|
307
|
-
|
|
515
|
+
releaseCurrent();
|
|
308
516
|
});
|
|
309
517
|
const refetch = () => {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (entry) {
|
|
313
|
-
URL.revokeObjectURL(entry.url);
|
|
314
|
-
downloadCache.delete(currentKey);
|
|
315
|
-
}
|
|
316
|
-
currentKey = null;
|
|
317
|
-
}
|
|
318
|
-
refetchTrigger();
|
|
518
|
+
reloadOnce = true;
|
|
519
|
+
setRefetchSignal((n) => n + 1);
|
|
319
520
|
};
|
|
320
521
|
return {
|
|
321
522
|
url,
|
|
@@ -326,26 +527,251 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
|
|
|
326
527
|
}
|
|
327
528
|
|
|
328
529
|
//#endregion
|
|
329
|
-
//#region src/lib/
|
|
330
|
-
function
|
|
530
|
+
//#region src/lib/use-blurhash.ts
|
|
531
|
+
function useBlurhash(dbOrBucketName, bucketNameOrPath, maybePath) {
|
|
532
|
+
let db;
|
|
533
|
+
let bucketName;
|
|
534
|
+
let path;
|
|
535
|
+
if (typeof dbOrBucketName === "string") {
|
|
536
|
+
db = useDb();
|
|
537
|
+
bucketName = dbOrBucketName;
|
|
538
|
+
path = bucketNameOrPath;
|
|
539
|
+
} else {
|
|
540
|
+
db = dbOrBucketName;
|
|
541
|
+
bucketName = bucketNameOrPath;
|
|
542
|
+
path = maybePath;
|
|
543
|
+
}
|
|
544
|
+
const [hash, setHash] = (0, solid_js.createSignal)(null);
|
|
545
|
+
const [isLoading, setIsLoading] = (0, solid_js.createSignal)(false);
|
|
546
|
+
(0, solid_js.createEffect)(() => {
|
|
547
|
+
const filePath = path();
|
|
548
|
+
if (!filePath) {
|
|
549
|
+
setHash(null);
|
|
550
|
+
setIsLoading(false);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
let cancelled = false;
|
|
554
|
+
setIsLoading(true);
|
|
555
|
+
db.bucket(bucketName).blurhash(filePath).then((result) => {
|
|
556
|
+
if (cancelled) return;
|
|
557
|
+
setHash(result);
|
|
558
|
+
setIsLoading(false);
|
|
559
|
+
}).catch(() => {
|
|
560
|
+
if (cancelled) return;
|
|
561
|
+
setHash(null);
|
|
562
|
+
setIsLoading(false);
|
|
563
|
+
});
|
|
564
|
+
(0, solid_js.onCleanup)(() => {
|
|
565
|
+
cancelled = true;
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
return {
|
|
569
|
+
hash,
|
|
570
|
+
isLoading
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/lib/use-bucket-image.ts
|
|
576
|
+
function useBucketImage(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeOptions) {
|
|
577
|
+
let db;
|
|
578
|
+
let bucketName;
|
|
579
|
+
let path;
|
|
580
|
+
let options;
|
|
581
|
+
if (typeof dbOrBucketName === "string") {
|
|
582
|
+
db = useDb();
|
|
583
|
+
bucketName = dbOrBucketName;
|
|
584
|
+
path = bucketNameOrPath;
|
|
585
|
+
options = pathOrOptions ?? {};
|
|
586
|
+
} else {
|
|
587
|
+
db = dbOrBucketName;
|
|
588
|
+
bucketName = bucketNameOrPath;
|
|
589
|
+
path = pathOrOptions;
|
|
590
|
+
options = maybeOptions ?? {};
|
|
591
|
+
}
|
|
592
|
+
const wantHash = options.blurhash !== false;
|
|
593
|
+
const { hash } = useBlurhash(db, bucketName, () => wantHash ? path() : null);
|
|
594
|
+
const file = useDownloadFile(db, bucketName, path, options);
|
|
595
|
+
const [ready, setReady] = (0, solid_js.createSignal)(false);
|
|
596
|
+
(0, solid_js.createEffect)((0, solid_js.on)(file.url, () => setReady(false), { defer: true }));
|
|
597
|
+
const gate = (img) => {
|
|
598
|
+
const done = () => setReady(true);
|
|
599
|
+
if (typeof img.decode === "function") img.decode().then(done, done);
|
|
600
|
+
else if (img.complete) done();
|
|
601
|
+
else {
|
|
602
|
+
img.onload = done;
|
|
603
|
+
img.onerror = done;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
return {
|
|
607
|
+
...file,
|
|
608
|
+
blurhash: hash,
|
|
609
|
+
ready,
|
|
610
|
+
gate
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
//#endregion
|
|
615
|
+
//#region src/lib/Blurhash.ts
|
|
616
|
+
/**
|
|
617
|
+
* A blurhash painted onto a canvas, once per hash change. Size the canvas via
|
|
618
|
+
* `class`/`style` (e.g. `absolute inset-0 w-full h-full`); the internal decode
|
|
619
|
+
* resolution stays tiny regardless of the displayed size.
|
|
620
|
+
*/
|
|
621
|
+
function Blurhash(props) {
|
|
622
|
+
if (typeof document === "undefined") return null;
|
|
623
|
+
const canvas = document.createElement("canvas");
|
|
624
|
+
(0, solid_js.createEffect)(() => {
|
|
625
|
+
canvas.className = props.class ?? "";
|
|
626
|
+
});
|
|
627
|
+
(0, solid_js.createEffect)(() => {
|
|
628
|
+
canvas.style.cssText = props.style ?? "";
|
|
629
|
+
});
|
|
630
|
+
(0, solid_js.createEffect)(() => {
|
|
631
|
+
const width = props.width ?? 32;
|
|
632
|
+
const height = props.height ?? 32;
|
|
633
|
+
canvas.width = width;
|
|
634
|
+
canvas.height = height;
|
|
635
|
+
const hash = props.hash;
|
|
636
|
+
if (!hash) return;
|
|
637
|
+
try {
|
|
638
|
+
const pixels = (0, _spooky_sync_core.decodeBlurhash)(hash, width, height, props.punch ?? 1);
|
|
639
|
+
const ctx = canvas.getContext("2d");
|
|
640
|
+
if (!ctx) return;
|
|
641
|
+
const imageData = ctx.createImageData(width, height);
|
|
642
|
+
imageData.data.set(pixels);
|
|
643
|
+
ctx.putImageData(imageData, 0, 0);
|
|
644
|
+
} catch {}
|
|
645
|
+
});
|
|
646
|
+
return canvas;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region src/lib/BucketImage.ts
|
|
651
|
+
const LAYER_STYLE = "position:absolute;inset:0;width:100%;height:100%;";
|
|
652
|
+
/**
|
|
653
|
+
* A bucket image that never pops in: it layers (bottom to top) your `fallback`
|
|
654
|
+
* plate, the automatically stored blurhash, and the real image, which stays
|
|
655
|
+
* transparent until the bitmap is DECODED and then crossfades over the
|
|
656
|
+
* placeholders. Placeholder layers unmount once the fade settles. Respects
|
|
657
|
+
* prefers-reduced-motion (instant swap). The container is made
|
|
658
|
+
* `position: relative` unless your `class` positions it already.
|
|
659
|
+
*
|
|
660
|
+
* ```tsx
|
|
661
|
+
* <BucketImage bucket="covers" path={row.cover_key} class="absolute inset-0"
|
|
662
|
+
* fallback={<MyPlate />} alt="" />
|
|
663
|
+
* ```
|
|
664
|
+
*/
|
|
665
|
+
function BucketImage(props) {
|
|
666
|
+
if (typeof document === "undefined") return null;
|
|
667
|
+
const image = useBucketImage(props.bucket, () => props.path, {
|
|
668
|
+
...props.options,
|
|
669
|
+
blurhash: props.blurhash !== false
|
|
670
|
+
});
|
|
671
|
+
const reducedMotion = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
672
|
+
const root = document.createElement("div");
|
|
673
|
+
(0, solid_js.createEffect)(() => {
|
|
674
|
+
root.className = props.class ?? "";
|
|
675
|
+
});
|
|
676
|
+
(0, solid_js.onMount)(() => {
|
|
677
|
+
if (getComputedStyle(root).position === "static") root.style.position = "relative";
|
|
678
|
+
});
|
|
679
|
+
const placeholder = document.createElement("div");
|
|
680
|
+
placeholder.style.cssText = LAYER_STYLE;
|
|
681
|
+
const fallbackHolder = document.createElement("div");
|
|
682
|
+
fallbackHolder.style.cssText = LAYER_STYLE;
|
|
683
|
+
placeholder.append(fallbackHolder);
|
|
684
|
+
const resolvedFallback = (0, solid_js.children)(() => props.fallback);
|
|
685
|
+
(0, solid_js.createEffect)(() => {
|
|
686
|
+
const nodes = resolvedFallback.toArray().filter((node) => node instanceof Node);
|
|
687
|
+
fallbackHolder.replaceChildren(...nodes);
|
|
688
|
+
});
|
|
689
|
+
const hashCanvas = Blurhash({
|
|
690
|
+
get hash() {
|
|
691
|
+
return image.blurhash();
|
|
692
|
+
},
|
|
693
|
+
style: LAYER_STYLE
|
|
694
|
+
});
|
|
695
|
+
if (hashCanvas instanceof Node) placeholder.append(hashCanvas);
|
|
696
|
+
const img = document.createElement("img");
|
|
697
|
+
img.decoding = "async";
|
|
698
|
+
img.style.cssText = `${LAYER_STYLE}opacity:0;`;
|
|
699
|
+
(0, solid_js.createEffect)(() => {
|
|
700
|
+
img.className = props.imgClass ?? "";
|
|
701
|
+
});
|
|
702
|
+
(0, solid_js.createEffect)(() => {
|
|
703
|
+
img.style.objectFit = props.fit ?? "cover";
|
|
704
|
+
});
|
|
705
|
+
(0, solid_js.createEffect)(() => {
|
|
706
|
+
img.alt = props.alt ?? "";
|
|
707
|
+
});
|
|
708
|
+
(0, solid_js.createEffect)(() => {
|
|
709
|
+
img.style.transition = reducedMotion ? "none" : `opacity ${props.transition ?? 300}ms ${props.easing ?? "cubic-bezier(0.16, 1, 0.3, 1)"}`;
|
|
710
|
+
});
|
|
711
|
+
(0, solid_js.createEffect)(() => {
|
|
712
|
+
const url = image.url();
|
|
713
|
+
if (!url) {
|
|
714
|
+
img.removeAttribute("src");
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
img.src = url;
|
|
718
|
+
image.gate(img);
|
|
719
|
+
});
|
|
720
|
+
(0, solid_js.createEffect)(() => {
|
|
721
|
+
img.style.opacity = image.ready() ? "1" : "0";
|
|
722
|
+
});
|
|
723
|
+
const [settled, setSettled] = (0, solid_js.createSignal)(false);
|
|
724
|
+
(0, solid_js.createEffect)(() => {
|
|
725
|
+
if (!image.ready()) {
|
|
726
|
+
setSettled(false);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const wait = (reducedMotion ? 0 : props.transition ?? 300) + 120;
|
|
730
|
+
const timer = setTimeout(() => setSettled(true), wait);
|
|
731
|
+
(0, solid_js.onCleanup)(() => clearTimeout(timer));
|
|
732
|
+
});
|
|
733
|
+
(0, solid_js.createEffect)(() => {
|
|
734
|
+
if (settled()) placeholder.remove();
|
|
735
|
+
else if (!placeholder.isConnected) root.insertBefore(placeholder, img);
|
|
736
|
+
});
|
|
737
|
+
root.append(placeholder, img);
|
|
738
|
+
return root;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
//#endregion
|
|
742
|
+
//#region src/lib/Sp00kyProvider.ts
|
|
743
|
+
function Sp00kyProvider(props) {
|
|
331
744
|
const merged = (0, solid_js.mergeProps)({ fallback: void 0 }, props);
|
|
332
745
|
const [db, setDb] = (0, solid_js.createSignal)(void 0);
|
|
746
|
+
let disposed = false;
|
|
747
|
+
(0, solid_js.onCleanup)(() => {
|
|
748
|
+
disposed = true;
|
|
749
|
+
});
|
|
333
750
|
(0, solid_js.onMount)(async () => {
|
|
334
751
|
try {
|
|
335
752
|
const instance = new SyncedDb(merged.config);
|
|
336
753
|
await instance.init();
|
|
754
|
+
if (disposed) {
|
|
755
|
+
await instance.close();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (merged.preload) try {
|
|
759
|
+
await merged.preload(instance);
|
|
760
|
+
} catch (e) {
|
|
761
|
+
console.error("Sp00kyProvider: preload failed; revealing UI anyway", e);
|
|
762
|
+
}
|
|
337
763
|
setDb(() => instance);
|
|
338
764
|
merged.onReady?.(instance);
|
|
339
765
|
} catch (e) {
|
|
340
766
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
341
767
|
if (merged.onError) merged.onError(error);
|
|
342
|
-
else console.error("
|
|
768
|
+
else console.error("Sp00kyProvider: Failed to initialize database", error);
|
|
343
769
|
}
|
|
344
770
|
});
|
|
345
771
|
return (0, solid_js.createMemo)(() => {
|
|
346
772
|
const instance = db();
|
|
347
773
|
if (!instance) return merged.fallback;
|
|
348
|
-
return (0, solid_js.createComponent)(
|
|
774
|
+
return (0, solid_js.createComponent)(Sp00kyContext.Provider, {
|
|
349
775
|
value: instance,
|
|
350
776
|
get children() {
|
|
351
777
|
return merged.children;
|
|
@@ -357,69 +783,94 @@ function SpookyProvider(props) {
|
|
|
357
783
|
//#endregion
|
|
358
784
|
//#region src/index.ts
|
|
359
785
|
/**
|
|
360
|
-
* SyncedDb - A thin wrapper around
|
|
361
|
-
* Delegates all logic to the underlying
|
|
786
|
+
* SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
|
|
787
|
+
* Delegates all logic to the underlying sp00ky-ts instance
|
|
362
788
|
*/
|
|
363
789
|
var SyncedDb = class {
|
|
364
790
|
constructor(config) {
|
|
365
|
-
this.
|
|
791
|
+
this.sp00ky = null;
|
|
366
792
|
this._initialized = false;
|
|
367
793
|
this.config = config;
|
|
368
794
|
}
|
|
369
|
-
|
|
370
|
-
if (!this.
|
|
371
|
-
return this.
|
|
795
|
+
getSp00ky() {
|
|
796
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
797
|
+
return this.sp00ky;
|
|
372
798
|
}
|
|
373
799
|
/**
|
|
374
|
-
* Initialize the
|
|
800
|
+
* Initialize the sp00ky-ts instance
|
|
375
801
|
*/
|
|
376
802
|
async init() {
|
|
377
803
|
if (this._initialized) return;
|
|
378
|
-
this.
|
|
379
|
-
await this.
|
|
804
|
+
this.sp00ky = new _spooky_sync_core.Sp00kyClient(this.config);
|
|
805
|
+
await this.sp00ky.init();
|
|
380
806
|
this._initialized = true;
|
|
381
807
|
}
|
|
382
808
|
/**
|
|
809
|
+
* Tear down the client: leaves the tabs broker, closes the local store and
|
|
810
|
+
* remote socket, and frees the wasm circuit. Without this a remounted provider
|
|
811
|
+
* (or an HMR reload) strands a whole client, and the abandoned wasm heaps stay
|
|
812
|
+
* resident because V8 cannot see how much wasm memory a dropped wrapper holds.
|
|
813
|
+
*/
|
|
814
|
+
async close() {
|
|
815
|
+
const instance = this.sp00ky;
|
|
816
|
+
this.sp00ky = null;
|
|
817
|
+
this._initialized = false;
|
|
818
|
+
if (instance) await instance.close();
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
383
821
|
* Create a new record in the database
|
|
384
822
|
*/
|
|
385
823
|
async create(id, payload) {
|
|
386
|
-
if (!this.
|
|
387
|
-
await this.
|
|
824
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
825
|
+
await this.sp00ky.create(id, payload);
|
|
388
826
|
}
|
|
389
827
|
/**
|
|
390
828
|
* Update an existing record in the database
|
|
391
829
|
*/
|
|
392
830
|
async update(tableName, recordId, payload, options) {
|
|
393
|
-
if (!this.
|
|
394
|
-
await this.
|
|
831
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
832
|
+
await this.sp00ky.update(tableName, recordId, payload, options);
|
|
395
833
|
}
|
|
396
834
|
/**
|
|
397
835
|
* Delete an existing record in the database
|
|
398
836
|
*/
|
|
399
837
|
async delete(tableName, selector) {
|
|
400
|
-
if (!this.
|
|
401
|
-
|
|
402
|
-
|
|
838
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
839
|
+
const isRecordId = selector instanceof surrealdb.RecordId || selector?.constructor?.name === "RecordId";
|
|
840
|
+
let id;
|
|
841
|
+
if (typeof selector === "string") id = selector;
|
|
842
|
+
else if (isRecordId) id = `${tableName}:${selector.id}`;
|
|
843
|
+
else throw new Error("Only string ID or RecordId selectors are supported currently with core");
|
|
844
|
+
await this.sp00ky.delete(tableName, id);
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Preload/prewarm a built query into the local cache without registering a
|
|
848
|
+
* live view. Fetches once and stores the rows (+ embedded related children)
|
|
849
|
+
* locally so a later `useQuery` for the same data paints instantly. Best-effort.
|
|
850
|
+
*/
|
|
851
|
+
async preload(finalQuery, options) {
|
|
852
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
853
|
+
await this.sp00ky.preload(finalQuery, options);
|
|
403
854
|
}
|
|
404
855
|
/**
|
|
405
856
|
* Query data from the database
|
|
406
857
|
*/
|
|
407
858
|
query(table) {
|
|
408
|
-
if (!this.
|
|
409
|
-
return this.
|
|
859
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
860
|
+
return this.sp00ky.query(table, {});
|
|
410
861
|
}
|
|
411
862
|
/**
|
|
412
863
|
* Run a backend operation
|
|
413
864
|
*/
|
|
414
865
|
async run(backend, path, payload, options) {
|
|
415
|
-
if (!this.
|
|
416
|
-
await this.
|
|
866
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
867
|
+
await this.sp00ky.run(backend, path, payload, options);
|
|
417
868
|
}
|
|
418
869
|
/**
|
|
419
870
|
* Authenticate with the database
|
|
420
871
|
*/
|
|
421
872
|
async authenticate(token) {
|
|
422
|
-
await this.
|
|
873
|
+
await this.sp00ky?.authenticate(token);
|
|
423
874
|
return new surrealdb.RecordId("user", "me");
|
|
424
875
|
}
|
|
425
876
|
/**
|
|
@@ -433,48 +884,81 @@ var SyncedDb = class {
|
|
|
433
884
|
* Sign out, clear session and local storage
|
|
434
885
|
*/
|
|
435
886
|
async signOut() {
|
|
436
|
-
if (!this.
|
|
437
|
-
await this.
|
|
887
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
888
|
+
await this.sp00ky.auth.signOut();
|
|
438
889
|
}
|
|
439
890
|
/**
|
|
440
891
|
* Execute a function with direct access to the remote database connection
|
|
441
892
|
*/
|
|
442
893
|
async useRemote(fn) {
|
|
443
|
-
if (!this.
|
|
444
|
-
return await this.
|
|
894
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
895
|
+
return await this.sp00ky.useRemote(fn);
|
|
445
896
|
}
|
|
446
897
|
/**
|
|
447
898
|
* Access the remote database service directly
|
|
448
899
|
*/
|
|
449
900
|
get remote() {
|
|
450
|
-
if (!this.
|
|
451
|
-
return this.
|
|
901
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
902
|
+
return this.sp00ky.remoteClient;
|
|
452
903
|
}
|
|
453
904
|
/**
|
|
454
905
|
* Access the local database service directly
|
|
455
906
|
*/
|
|
456
907
|
get local() {
|
|
457
|
-
if (!this.
|
|
458
|
-
return this.
|
|
908
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
909
|
+
return this.sp00ky.localClient;
|
|
459
910
|
}
|
|
460
911
|
/**
|
|
461
912
|
* Access the auth service
|
|
462
913
|
*/
|
|
463
914
|
get auth() {
|
|
464
|
-
if (!this.
|
|
465
|
-
return this.
|
|
915
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
916
|
+
return this.sp00ky.auth;
|
|
466
917
|
}
|
|
467
918
|
get pendingMutationCount() {
|
|
468
|
-
if (!this.
|
|
469
|
-
return this.
|
|
919
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
920
|
+
return this.sp00ky.pendingMutationCount;
|
|
921
|
+
}
|
|
922
|
+
/** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
|
|
923
|
+
get liveRetryCount() {
|
|
924
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
925
|
+
return this.sp00ky.liveRetryCount;
|
|
470
926
|
}
|
|
471
927
|
subscribeToPendingMutations(cb) {
|
|
472
|
-
if (!this.
|
|
473
|
-
return this.
|
|
928
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
929
|
+
return this.sp00ky.subscribeToPendingMutations(cb);
|
|
930
|
+
}
|
|
931
|
+
/** Current sync-health snapshot. See {@link useSyncStatus}. */
|
|
932
|
+
get syncHealth() {
|
|
933
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
934
|
+
return this.sp00ky.syncHealth;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Observe sync health. Fires immediately with the current status and again
|
|
938
|
+
* on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
|
|
939
|
+
* components; this is the imperative escape hatch.
|
|
940
|
+
*/
|
|
941
|
+
subscribeToSyncHealth(cb) {
|
|
942
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
943
|
+
return this.sp00ky.subscribeToSyncHealth(cb);
|
|
944
|
+
}
|
|
945
|
+
/** Current local-store durability snapshot. See {@link useStorageStatus}. */
|
|
946
|
+
get storageHealth() {
|
|
947
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
948
|
+
return this.sp00ky.storageHealth;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Observe local-store durability. Fires immediately with the current snapshot
|
|
952
|
+
* and again on change. Prefer the `useStorageStatus` hook in components; this
|
|
953
|
+
* is the imperative escape hatch.
|
|
954
|
+
*/
|
|
955
|
+
subscribeToStorageHealth(cb) {
|
|
956
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
957
|
+
return this.sp00ky.subscribeToStorageHealth(cb);
|
|
474
958
|
}
|
|
475
959
|
bucket(name) {
|
|
476
|
-
if (!this.
|
|
477
|
-
return this.
|
|
960
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
961
|
+
return this.sp00ky.bucket(name);
|
|
478
962
|
}
|
|
479
963
|
getBucketConfig(name) {
|
|
480
964
|
return this.config.schema.buckets?.find((b) => b.name === name);
|
|
@@ -482,12 +966,22 @@ var SyncedDb = class {
|
|
|
482
966
|
};
|
|
483
967
|
|
|
484
968
|
//#endregion
|
|
969
|
+
exports.Blurhash = Blurhash;
|
|
970
|
+
exports.BucketImage = BucketImage;
|
|
485
971
|
exports.RecordId = surrealdb.RecordId;
|
|
486
|
-
exports.
|
|
972
|
+
exports.Sp00kyProvider = Sp00kyProvider;
|
|
487
973
|
exports.SyncedDb = SyncedDb;
|
|
488
974
|
exports.Uuid = surrealdb.Uuid;
|
|
975
|
+
exports.createPreload = createPreload;
|
|
976
|
+
exports.useAppRelease = useAppRelease;
|
|
977
|
+
exports.useBlurhash = useBlurhash;
|
|
978
|
+
exports.useBucketImage = useBucketImage;
|
|
979
|
+
exports.useCrdtField = useCrdtField;
|
|
489
980
|
exports.useDb = useDb;
|
|
490
981
|
exports.useDownloadFile = useDownloadFile;
|
|
982
|
+
exports.useFeatureFlag = useFeatureFlag;
|
|
491
983
|
exports.useFileUpload = useFileUpload;
|
|
492
984
|
exports.useQuery = useQuery;
|
|
985
|
+
exports.useStorageStatus = useStorageStatus;
|
|
986
|
+
exports.useSyncStatus = useSyncStatus;
|
|
493
987
|
//# sourceMappingURL=index.cjs.map
|