@spooky-sync/client-solid 0.0.1-canary.33 → 0.0.1-canary.331
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 +70 -0
- package/README.md +20 -0
- package/dist/index.cjs +685 -111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +405 -9
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +405 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +671 -114
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/skills/sp00ky-solid/SKILL.md +72 -1
- package/skills/sp00ky-solid/references/file-hooks.md +33 -4
- package/src/cache/index.ts +1 -1
- package/src/cache/surrealdb-wasm-factory.ts +4 -1
- package/src/index.ts +200 -19
- package/src/lib/Blurhash.ts +54 -0
- package/src/lib/BucketImage.ts +141 -0
- package/src/lib/Sp00kyProvider.ts +50 -1
- package/src/lib/create-preload.ts +112 -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 +65 -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-impersonation.ts +44 -0
- package/src/lib/use-query.ts +184 -20
- package/src/lib/use-storage-status.ts +44 -0
- package/src/lib/use-sync-status.ts +64 -0
- package/src/types/index.ts +1 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Sp00kyClient, fileToUint8Array } from "@spooky-sync/core";
|
|
1
|
+
import { Sp00kyClient, WebPushError, decodeBlurhash, fileToUint8Array, semverGt } from "@spooky-sync/core";
|
|
2
2
|
import { RecordId, Uuid } from "surrealdb";
|
|
3
|
-
import { createComponent, createContext, createEffect, createMemo, createSignal, mergeProps, onCleanup, onMount, useContext } from "solid-js";
|
|
3
|
+
import { children, createComponent, createContext, createEffect, createMemo, createSignal, mergeProps, on, onCleanup, onMount, useContext } from "solid-js";
|
|
4
|
+
import { createStore, reconcile } from "solid-js/store";
|
|
4
5
|
|
|
5
6
|
//#region src/lib/context.ts
|
|
6
7
|
const Sp00kyContext = createContext();
|
|
@@ -27,24 +28,81 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
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;
|
|
41
|
+
let runId = 0;
|
|
42
|
+
let activeUnsub;
|
|
43
|
+
let activeHash;
|
|
44
|
+
const teardownActive = () => {
|
|
45
|
+
activeUnsub?.();
|
|
46
|
+
activeUnsub = void 0;
|
|
47
|
+
};
|
|
35
48
|
const sp00ky = db.getSp00ky();
|
|
36
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Registration can fail — the canonical case is the SSP answering 503
|
|
51
|
+
* NOT_READY while it bootstraps. Nothing here used to catch that: the
|
|
52
|
+
* rejection escaped as an unhandled promise, `isFetched` stayed false, and
|
|
53
|
+
* `isLoading()` therefore stayed true FOREVER, which is what a spinner that
|
|
54
|
+
* never resolves actually was. Surface it as `error()` instead; the sync
|
|
55
|
+
* scheduler retries the registration underneath, so a transient failure
|
|
56
|
+
* still recovers on its own.
|
|
57
|
+
*/
|
|
58
|
+
const initQuery = async (query, myRun) => {
|
|
59
|
+
try {
|
|
60
|
+
await subscribeQuery(query, myRun);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (myRun !== runId) return;
|
|
63
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const subscribeQuery = async (query, myRun) => {
|
|
37
67
|
const { hash } = await query.run();
|
|
68
|
+
if (myRun !== runId) return;
|
|
69
|
+
activeHash = hash;
|
|
38
70
|
setError(void 0);
|
|
39
71
|
let isFirstCall = true;
|
|
40
72
|
const unsub = await sp00ky.subscribe(hash, (e) => {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
const queryData = query.isOne ? e[0] : e;
|
|
74
|
+
const reconcileStart = performance.now();
|
|
75
|
+
setState("value", reconcile(queryData, { key: "id" }));
|
|
76
|
+
setVersion((v) => v + 1);
|
|
77
|
+
sp00ky.reportFrontendTiming(hash, performance.now() - reconcileStart);
|
|
78
|
+
const hasData = query.isOne ? queryData !== null && queryData !== void 0 : e.length > 0;
|
|
44
79
|
if (!isFirstCall || hasData) setIsFetched(true);
|
|
45
80
|
isFirstCall = false;
|
|
46
81
|
}, { immediate: true });
|
|
47
|
-
|
|
82
|
+
let known = false;
|
|
83
|
+
let idle = false;
|
|
84
|
+
const settleFromServer = () => {
|
|
85
|
+
if (known && idle && myRun === runId) setIsFetched(true);
|
|
86
|
+
};
|
|
87
|
+
const unsubStatus = sp00ky.subscribeQueryStatus(hash, (status) => {
|
|
88
|
+
setIsFetching(status === "fetching");
|
|
89
|
+
idle = status === "idle";
|
|
90
|
+
settleFromServer();
|
|
91
|
+
}, { immediate: true });
|
|
92
|
+
const unsubAuthority = sp00ky.subscribeQueryAuthority(hash, (k) => {
|
|
93
|
+
known = k;
|
|
94
|
+
settleFromServer();
|
|
95
|
+
}, { immediate: true });
|
|
96
|
+
const teardown = () => {
|
|
97
|
+
unsub();
|
|
98
|
+
unsubStatus();
|
|
99
|
+
unsubAuthority();
|
|
100
|
+
};
|
|
101
|
+
if (myRun !== runId) {
|
|
102
|
+
teardown();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
activeUnsub = teardown;
|
|
48
106
|
};
|
|
49
107
|
createEffect(() => {
|
|
50
108
|
if (!(options?.enabled?.() ?? true)) {
|
|
@@ -53,22 +111,265 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
53
111
|
}
|
|
54
112
|
const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
|
|
55
113
|
if (!query) return;
|
|
56
|
-
const queryString =
|
|
114
|
+
const queryString = String(query.hash);
|
|
57
115
|
if (queryString === prevQueryString) return;
|
|
58
116
|
prevQueryString = queryString;
|
|
117
|
+
const myRun = ++runId;
|
|
118
|
+
teardownActive();
|
|
59
119
|
setIsFetched(false);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
120
|
+
setError(void 0);
|
|
121
|
+
initQuery(query, myRun);
|
|
122
|
+
});
|
|
123
|
+
onCleanup(() => {
|
|
124
|
+
runId++;
|
|
125
|
+
teardownActive();
|
|
126
|
+
if (options?.deregisterOnCleanup && activeHash) sp00ky.deregisterQuery(activeHash);
|
|
64
127
|
});
|
|
65
128
|
const isLoading = () => {
|
|
66
129
|
return !isFetched() && error() === void 0;
|
|
67
130
|
};
|
|
131
|
+
const isSettled = () => isFetched() && !isFetching();
|
|
68
132
|
return {
|
|
69
133
|
data,
|
|
70
134
|
error,
|
|
71
|
-
isLoading
|
|
135
|
+
isLoading,
|
|
136
|
+
isFetching,
|
|
137
|
+
isSettled
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/lib/create-preload.ts
|
|
143
|
+
/**
|
|
144
|
+
* Reactive, fire-and-forget prewarm. Resolves the query (calling it if it's a
|
|
145
|
+
* function so it tracks reactive deps), dedupes on the query's stable identity
|
|
146
|
+
* hash, and registers it via `db.preload`: a live query nobody subscribes to,
|
|
147
|
+
* evicted a ttl after it was registered unless a view mounts it first. Nothing
|
|
148
|
+
* to clean up here.
|
|
149
|
+
*
|
|
150
|
+
* Typical use: inside a list row, preload the detail query the user is likely
|
|
151
|
+
* to open next, so navigation paints from cache instead of the network.
|
|
152
|
+
*/
|
|
153
|
+
function createPreload(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
154
|
+
let db;
|
|
155
|
+
let finalQuery;
|
|
156
|
+
let options;
|
|
157
|
+
if (dbOrQuery instanceof SyncedDb) {
|
|
158
|
+
db = dbOrQuery;
|
|
159
|
+
finalQuery = queryOrOptions;
|
|
160
|
+
options = maybeOptions;
|
|
161
|
+
} else {
|
|
162
|
+
const contextDb = useContext(Sp00kyContext);
|
|
163
|
+
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>.");
|
|
164
|
+
db = contextDb;
|
|
165
|
+
finalQuery = dbOrQuery;
|
|
166
|
+
options = queryOrOptions;
|
|
167
|
+
}
|
|
168
|
+
let prevHash;
|
|
169
|
+
createEffect(() => {
|
|
170
|
+
if (!(options?.enabled?.() ?? true)) return;
|
|
171
|
+
const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
|
|
172
|
+
if (!query) return;
|
|
173
|
+
if (query.hash === prevHash) return;
|
|
174
|
+
prevHash = query.hash;
|
|
175
|
+
db.getSp00ky().preload(query, { signal: options?.signal });
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/lib/use-sync-status.ts
|
|
181
|
+
/**
|
|
182
|
+
* Observe sync health for a "can't reach the server" banner / indicator.
|
|
183
|
+
*
|
|
184
|
+
* Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
|
|
185
|
+
* remote 500 on query registration, a dropped socket) are absorbed by the
|
|
186
|
+
* retry and never flip this; `isDegraded()` only goes true once failures
|
|
187
|
+
* persist for the configured number of consecutive rounds (sp00ky core config
|
|
188
|
+
* `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
|
|
189
|
+
* the next successful round. Must be used within a `<Sp00kyProvider>`.
|
|
190
|
+
*/
|
|
191
|
+
function useSyncStatus() {
|
|
192
|
+
const db = useDb();
|
|
193
|
+
const [health, setHealth] = createSignal(db.syncHealth);
|
|
194
|
+
onCleanup(db.subscribeToSyncHealth(setHealth));
|
|
195
|
+
return {
|
|
196
|
+
health,
|
|
197
|
+
status: () => health().status,
|
|
198
|
+
isHealthy: () => health().status === "healthy",
|
|
199
|
+
isDegraded: () => health().status === "degraded",
|
|
200
|
+
everConnected: () => health().everConnected,
|
|
201
|
+
isOffline: () => health().status === "degraded" && health().everConnected,
|
|
202
|
+
connection: () => health().connection,
|
|
203
|
+
isReconnecting: () => health().connection === "reconnecting"
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/lib/use-impersonation.ts
|
|
209
|
+
/**
|
|
210
|
+
* Observe admin impersonation (started from the DevTools). The client already
|
|
211
|
+
* shows its own warning bar with a Stop button; use this for anything extra,
|
|
212
|
+
* such as hiding actions an admin should not take on someone's behalf.
|
|
213
|
+
* Must be used within a `<Sp00kyProvider>`.
|
|
214
|
+
*/
|
|
215
|
+
function useImpersonation(options = {}) {
|
|
216
|
+
const db = useDb();
|
|
217
|
+
const [impersonation, setImpersonation] = createSignal(db.auth.impersonation);
|
|
218
|
+
onCleanup(db.auth.subscribeImpersonation(setImpersonation));
|
|
219
|
+
createEffect(() => {
|
|
220
|
+
if (options.rendersBanner && impersonation()) db.acknowledgeImpersonationBanner();
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
impersonation,
|
|
224
|
+
isImpersonating: () => impersonation() !== null,
|
|
225
|
+
stop: () => db.auth.stopImpersonating()
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/lib/use-storage-status.ts
|
|
231
|
+
/**
|
|
232
|
+
* Observe how durable the LOCAL cache is, for a "no local storage" warning.
|
|
233
|
+
*
|
|
234
|
+
* Under `localEngine: 'sqlite'` with `store: 'indexeddb'` the durable store is
|
|
235
|
+
* the OPFS SAHPool VFS, and only ONE client per bucket can hold it open: a
|
|
236
|
+
* second tab of the same app cannot get it and runs in memory instead (the
|
|
237
|
+
* engine retries first, so a closing tab's lock is usually waited out). Must be
|
|
238
|
+
* used within a `<Sp00kyProvider>`.
|
|
239
|
+
*/
|
|
240
|
+
function useStorageStatus() {
|
|
241
|
+
const db = useDb();
|
|
242
|
+
const [health, setHealth] = createSignal(db.storageHealth);
|
|
243
|
+
onCleanup(db.subscribeToStorageHealth(setHealth));
|
|
244
|
+
return {
|
|
245
|
+
health,
|
|
246
|
+
status: () => health().status,
|
|
247
|
+
isPersistent: () => health().status === "persistent",
|
|
248
|
+
isMemoryFallback: () => health().fallback
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/lib/use-crdt-field.ts
|
|
254
|
+
/**
|
|
255
|
+
* Open a `@crdt` field of the record `recordId()` names, for an editor to bind
|
|
256
|
+
* to (`field()?.getDoc()`). Re-opens when the id changes; closes on cleanup.
|
|
257
|
+
*
|
|
258
|
+
* The accessor stays `null` until the field is loaded (`CrdtField.isLoaded`):
|
|
259
|
+
* this device had the document, or the server answered. Until then the
|
|
260
|
+
* document may be empty only because nothing has arrived yet, so show the
|
|
261
|
+
* column's text instead of binding an editor to it.
|
|
262
|
+
*/
|
|
263
|
+
function useCrdtField(table, recordId, field) {
|
|
264
|
+
const db = useContext(Sp00kyContext);
|
|
265
|
+
if (!db) throw new Error("useCrdtField must be used within a <Sp00kyProvider>");
|
|
266
|
+
const [crdtField, setCrdtField] = createSignal(null);
|
|
267
|
+
createEffect(on(createMemo(recordId), (current) => {
|
|
268
|
+
if (!current) return;
|
|
269
|
+
const sp00ky = db.getSp00ky();
|
|
270
|
+
let active = true;
|
|
271
|
+
let opened = false;
|
|
272
|
+
let offLoaded = () => {};
|
|
273
|
+
sp00ky.openCrdtField(table, current, field).then((cf) => {
|
|
274
|
+
opened = true;
|
|
275
|
+
if (!active) {
|
|
276
|
+
sp00ky.closeCrdtField(table, current, field);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
offLoaded = cf.onLoaded(() => {
|
|
280
|
+
if (active) setCrdtField(cf);
|
|
281
|
+
});
|
|
282
|
+
}).catch((err) => {
|
|
283
|
+
console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${current}:`, err);
|
|
284
|
+
});
|
|
285
|
+
onCleanup(() => {
|
|
286
|
+
active = false;
|
|
287
|
+
offLoaded();
|
|
288
|
+
setCrdtField(null);
|
|
289
|
+
if (opened) sp00ky.closeCrdtField(table, current, field);
|
|
290
|
+
});
|
|
291
|
+
}));
|
|
292
|
+
return crdtField;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/lib/use-feature-flag.ts
|
|
297
|
+
/**
|
|
298
|
+
* Subscribe to a feature flag for the currently authenticated user.
|
|
299
|
+
*
|
|
300
|
+
* Returns three Solid accessors that update reactively whenever the
|
|
301
|
+
* server-materialized assignment in `_00_user_feature` changes. Backed by
|
|
302
|
+
* the same SSP + sync pipeline that powers `useQuery`, so toggling a flag
|
|
303
|
+
* via `spky flag enable <key>` propagates to the UI without a refresh.
|
|
304
|
+
*
|
|
305
|
+
* `enabled()` is `true` when the resolved variant exists and is not 'off'.
|
|
306
|
+
* For multi-variant flags, prefer `variant()` directly.
|
|
307
|
+
*/
|
|
308
|
+
function useFeatureFlag(key, options) {
|
|
309
|
+
const handle = useDb().getSp00ky().feature(key, options);
|
|
310
|
+
const [variant, setVariant] = createSignal(handle.variant());
|
|
311
|
+
const [payload, setPayload] = createSignal(handle.payload());
|
|
312
|
+
const unsub = handle.subscribe((s) => {
|
|
313
|
+
setVariant(s.variant ?? options?.fallback);
|
|
314
|
+
setPayload(s.payload);
|
|
315
|
+
});
|
|
316
|
+
onCleanup(() => {
|
|
317
|
+
unsub();
|
|
318
|
+
handle.close();
|
|
319
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
variant,
|
|
322
|
+
payload,
|
|
323
|
+
enabled: () => {
|
|
324
|
+
const v = variant();
|
|
325
|
+
return v !== void 0 && v !== "off";
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/lib/use-app-release.ts
|
|
332
|
+
async function reloadForSnapshot(snapshot) {
|
|
333
|
+
if (typeof window === "undefined") return;
|
|
334
|
+
if (snapshot.cacheBust) try {
|
|
335
|
+
if (window.caches) {
|
|
336
|
+
const keys = await window.caches.keys();
|
|
337
|
+
await Promise.all(keys.map((k) => window.caches.delete(k)));
|
|
338
|
+
}
|
|
339
|
+
if (navigator.serviceWorker) {
|
|
340
|
+
const regs = await navigator.serviceWorker.getRegistrations();
|
|
341
|
+
for (const r of regs) r.update().catch(() => {});
|
|
342
|
+
}
|
|
343
|
+
window.location.href = window.location.pathname + "?cb=" + Date.now();
|
|
344
|
+
return;
|
|
345
|
+
} catch {}
|
|
346
|
+
window.location.reload();
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Observe the app's announced release (`_00_app_release:<app>`, written by
|
|
350
|
+
* `spky deploy` / `spky release`) and compare it against the running build.
|
|
351
|
+
*
|
|
352
|
+
* Typical use: mount a small "new version available — Reload" notification
|
|
353
|
+
* gated on `updateAvailable()`, auto-invoking `reload()` when `mandatory()`
|
|
354
|
+
* (guard the auto path against reload loops with a per-version marker, since
|
|
355
|
+
* a client can reload while the deploy is still rolling out and land on the
|
|
356
|
+
* old bundle again).
|
|
357
|
+
*/
|
|
358
|
+
function useAppRelease(options) {
|
|
359
|
+
const handle = useDb().getSp00ky().appRelease(options.app, { ttl: options.ttl });
|
|
360
|
+
const [snapshot, setSnapshot] = createSignal(handle.snapshot());
|
|
361
|
+
const unsub = handle.subscribe(setSnapshot);
|
|
362
|
+
onCleanup(() => {
|
|
363
|
+
unsub();
|
|
364
|
+
handle.close();
|
|
365
|
+
});
|
|
366
|
+
const updateAvailable = () => semverGt(snapshot().version, options.currentVersion);
|
|
367
|
+
return {
|
|
368
|
+
latestVersion: () => snapshot().version,
|
|
369
|
+
updateAvailable,
|
|
370
|
+
mandatory: () => updateAvailable() && snapshot().mandatory,
|
|
371
|
+
cacheBust: () => snapshot().cacheBust,
|
|
372
|
+
reload: () => reloadForSnapshot(snapshot())
|
|
72
373
|
};
|
|
73
374
|
}
|
|
74
375
|
|
|
@@ -94,7 +395,7 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
|
|
|
94
395
|
const validate = (file) => {
|
|
95
396
|
const config = db.getBucketConfig(bucketName);
|
|
96
397
|
if (!config) return;
|
|
97
|
-
if (config.maxSize
|
|
398
|
+
if (config.maxSize !== null && config.maxSize !== void 0 && file.size > config.maxSize) {
|
|
98
399
|
const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
|
|
99
400
|
throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
|
|
100
401
|
}
|
|
@@ -106,7 +407,7 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
|
|
|
106
407
|
}
|
|
107
408
|
}
|
|
108
409
|
};
|
|
109
|
-
const upload = async (path, file) => {
|
|
410
|
+
const upload = async (path, file, options) => {
|
|
110
411
|
setError(null);
|
|
111
412
|
try {
|
|
112
413
|
validate(file);
|
|
@@ -117,7 +418,7 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
|
|
|
117
418
|
setIsUploading(true);
|
|
118
419
|
try {
|
|
119
420
|
const bytes = await fileToUint8Array(file);
|
|
120
|
-
await db.bucket(bucketName).put(path, bytes);
|
|
421
|
+
return await db.bucket(bucketName).put(path, bytes, options);
|
|
121
422
|
} catch (e) {
|
|
122
423
|
setError(e instanceof Error ? e : new Error(String(e)));
|
|
123
424
|
} finally {
|
|
@@ -167,20 +468,6 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
|
|
|
167
468
|
|
|
168
469
|
//#endregion
|
|
169
470
|
//#region src/lib/use-download-file.ts
|
|
170
|
-
const downloadCache = /* @__PURE__ */ new Map();
|
|
171
|
-
const inflightRequests = /* @__PURE__ */ new Map();
|
|
172
|
-
function cacheKey(bucket, path) {
|
|
173
|
-
return `${bucket}:${path}`;
|
|
174
|
-
}
|
|
175
|
-
function releaseEntry(key) {
|
|
176
|
-
const entry = downloadCache.get(key);
|
|
177
|
-
if (!entry) return;
|
|
178
|
-
entry.refCount--;
|
|
179
|
-
if (entry.refCount <= 0) {
|
|
180
|
-
URL.revokeObjectURL(entry.url);
|
|
181
|
-
downloadCache.delete(key);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
471
|
function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeOptions) {
|
|
185
472
|
let db;
|
|
186
473
|
let bucketName;
|
|
@@ -201,63 +488,15 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
|
|
|
201
488
|
const [url, setUrl] = createSignal(null);
|
|
202
489
|
const [isLoading, setIsLoading] = createSignal(false);
|
|
203
490
|
const [error, setError] = createSignal(null);
|
|
204
|
-
let
|
|
491
|
+
let lease = null;
|
|
205
492
|
let privateUrl = null;
|
|
206
|
-
let refetchTrigger;
|
|
207
493
|
const [refetchSignal, setRefetchSignal] = createSignal(0);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
currentKey = key;
|
|
215
|
-
return cached.url;
|
|
216
|
-
}
|
|
217
|
-
const inflight = inflightRequests.get(key);
|
|
218
|
-
if (inflight) {
|
|
219
|
-
const result = await inflight;
|
|
220
|
-
if (result) {
|
|
221
|
-
const entry = downloadCache.get(key);
|
|
222
|
-
if (entry) {
|
|
223
|
-
entry.refCount++;
|
|
224
|
-
currentKey = key;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return result;
|
|
228
|
-
}
|
|
229
|
-
const promise = (async () => {
|
|
230
|
-
const content = await db.bucket(bucketName).get(filePath);
|
|
231
|
-
if (!content) return null;
|
|
232
|
-
const objectUrl = URL.createObjectURL(new Blob([content]));
|
|
233
|
-
downloadCache.set(key, {
|
|
234
|
-
url: objectUrl,
|
|
235
|
-
refCount: 1
|
|
236
|
-
});
|
|
237
|
-
return objectUrl;
|
|
238
|
-
})();
|
|
239
|
-
inflightRequests.set(key, promise);
|
|
240
|
-
try {
|
|
241
|
-
const result = await promise;
|
|
242
|
-
currentKey = key;
|
|
243
|
-
return result;
|
|
244
|
-
} finally {
|
|
245
|
-
inflightRequests.delete(key);
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
const content = await db.bucket(bucketName).get(filePath);
|
|
249
|
-
if (!content) return null;
|
|
250
|
-
const objectUrl = URL.createObjectURL(new Blob([content]));
|
|
251
|
-
privateUrl = objectUrl;
|
|
252
|
-
return objectUrl;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
function releaseCurrentEntry() {
|
|
256
|
-
if (useCache && currentKey) {
|
|
257
|
-
releaseEntry(currentKey);
|
|
258
|
-
currentKey = null;
|
|
259
|
-
}
|
|
260
|
-
if (!useCache && privateUrl) {
|
|
494
|
+
/** Consumed by the next effect run, so `refetch()` bypasses every layer once. */
|
|
495
|
+
let reloadOnce = false;
|
|
496
|
+
function releaseCurrent() {
|
|
497
|
+
lease?.release();
|
|
498
|
+
lease = null;
|
|
499
|
+
if (privateUrl) {
|
|
261
500
|
URL.revokeObjectURL(privateUrl);
|
|
262
501
|
privateUrl = null;
|
|
263
502
|
}
|
|
@@ -265,29 +504,40 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
|
|
|
265
504
|
createEffect(() => {
|
|
266
505
|
const filePath = path();
|
|
267
506
|
refetchSignal();
|
|
268
|
-
|
|
507
|
+
releaseCurrent();
|
|
269
508
|
if (!filePath) {
|
|
270
509
|
setUrl(null);
|
|
271
510
|
setIsLoading(false);
|
|
272
511
|
setError(null);
|
|
273
512
|
return;
|
|
274
513
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
const cached = downloadCache.get(key);
|
|
278
|
-
if (cached) {
|
|
279
|
-
cached.refCount++;
|
|
280
|
-
currentKey = key;
|
|
281
|
-
setUrl(cached.url);
|
|
282
|
-
setIsLoading(false);
|
|
283
|
-
setError(null);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
514
|
+
const reload = reloadOnce;
|
|
515
|
+
reloadOnce = false;
|
|
287
516
|
let cancelled = false;
|
|
288
517
|
setIsLoading(true);
|
|
289
518
|
setError(null);
|
|
290
|
-
|
|
519
|
+
const bucket = db.bucket(bucketName);
|
|
520
|
+
(useCache ? bucket.url(filePath, {
|
|
521
|
+
persist: options.persist !== false,
|
|
522
|
+
pin: options.pin,
|
|
523
|
+
revalidate: options.revalidate,
|
|
524
|
+
reload
|
|
525
|
+
}).then((acquired) => {
|
|
526
|
+
if (!acquired) return null;
|
|
527
|
+
if (cancelled) {
|
|
528
|
+
acquired.release();
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
lease = acquired;
|
|
532
|
+
return acquired.url;
|
|
533
|
+
}) : bucket.read(filePath, {
|
|
534
|
+
persist: false,
|
|
535
|
+
reload: true
|
|
536
|
+
}).then((blob) => {
|
|
537
|
+
if (!blob || cancelled) return null;
|
|
538
|
+
privateUrl = URL.createObjectURL(blob);
|
|
539
|
+
return privateUrl;
|
|
540
|
+
})).then((result) => {
|
|
291
541
|
if (!cancelled) {
|
|
292
542
|
setUrl(result);
|
|
293
543
|
setIsLoading(false);
|
|
@@ -303,18 +553,11 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
|
|
|
303
553
|
});
|
|
304
554
|
});
|
|
305
555
|
onCleanup(() => {
|
|
306
|
-
|
|
556
|
+
releaseCurrent();
|
|
307
557
|
});
|
|
308
558
|
const refetch = () => {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (entry) {
|
|
312
|
-
URL.revokeObjectURL(entry.url);
|
|
313
|
-
downloadCache.delete(currentKey);
|
|
314
|
-
}
|
|
315
|
-
currentKey = null;
|
|
316
|
-
}
|
|
317
|
-
refetchTrigger();
|
|
559
|
+
reloadOnce = true;
|
|
560
|
+
setRefetchSignal((n) => n + 1);
|
|
318
561
|
};
|
|
319
562
|
return {
|
|
320
563
|
url,
|
|
@@ -324,15 +567,240 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
|
|
|
324
567
|
};
|
|
325
568
|
}
|
|
326
569
|
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/lib/use-blurhash.ts
|
|
572
|
+
function useBlurhash(dbOrBucketName, bucketNameOrPath, maybePath) {
|
|
573
|
+
let db;
|
|
574
|
+
let bucketName;
|
|
575
|
+
let path;
|
|
576
|
+
if (typeof dbOrBucketName === "string") {
|
|
577
|
+
db = useDb();
|
|
578
|
+
bucketName = dbOrBucketName;
|
|
579
|
+
path = bucketNameOrPath;
|
|
580
|
+
} else {
|
|
581
|
+
db = dbOrBucketName;
|
|
582
|
+
bucketName = bucketNameOrPath;
|
|
583
|
+
path = maybePath;
|
|
584
|
+
}
|
|
585
|
+
const [hash, setHash] = createSignal(null);
|
|
586
|
+
const [isLoading, setIsLoading] = createSignal(false);
|
|
587
|
+
createEffect(() => {
|
|
588
|
+
const filePath = path();
|
|
589
|
+
if (!filePath) {
|
|
590
|
+
setHash(null);
|
|
591
|
+
setIsLoading(false);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
let cancelled = false;
|
|
595
|
+
setIsLoading(true);
|
|
596
|
+
db.bucket(bucketName).blurhash(filePath).then((result) => {
|
|
597
|
+
if (cancelled) return;
|
|
598
|
+
setHash(result);
|
|
599
|
+
setIsLoading(false);
|
|
600
|
+
}).catch(() => {
|
|
601
|
+
if (cancelled) return;
|
|
602
|
+
setHash(null);
|
|
603
|
+
setIsLoading(false);
|
|
604
|
+
});
|
|
605
|
+
onCleanup(() => {
|
|
606
|
+
cancelled = true;
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
return {
|
|
610
|
+
hash,
|
|
611
|
+
isLoading
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region src/lib/use-bucket-image.ts
|
|
617
|
+
function useBucketImage(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeOptions) {
|
|
618
|
+
let db;
|
|
619
|
+
let bucketName;
|
|
620
|
+
let path;
|
|
621
|
+
let options;
|
|
622
|
+
if (typeof dbOrBucketName === "string") {
|
|
623
|
+
db = useDb();
|
|
624
|
+
bucketName = dbOrBucketName;
|
|
625
|
+
path = bucketNameOrPath;
|
|
626
|
+
options = pathOrOptions ?? {};
|
|
627
|
+
} else {
|
|
628
|
+
db = dbOrBucketName;
|
|
629
|
+
bucketName = bucketNameOrPath;
|
|
630
|
+
path = pathOrOptions;
|
|
631
|
+
options = maybeOptions ?? {};
|
|
632
|
+
}
|
|
633
|
+
const wantHash = options.blurhash !== false;
|
|
634
|
+
const { hash } = useBlurhash(db, bucketName, () => wantHash ? path() : null);
|
|
635
|
+
const file = useDownloadFile(db, bucketName, path, options);
|
|
636
|
+
const [ready, setReady] = createSignal(false);
|
|
637
|
+
createEffect(on(file.url, () => setReady(false), { defer: true }));
|
|
638
|
+
const gate = (img) => {
|
|
639
|
+
const done = () => setReady(true);
|
|
640
|
+
if (typeof img.decode === "function") img.decode().then(done, done);
|
|
641
|
+
else if (img.complete) done();
|
|
642
|
+
else {
|
|
643
|
+
img.onload = done;
|
|
644
|
+
img.onerror = done;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
return {
|
|
648
|
+
...file,
|
|
649
|
+
blurhash: hash,
|
|
650
|
+
ready,
|
|
651
|
+
gate
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/lib/Blurhash.ts
|
|
657
|
+
/**
|
|
658
|
+
* A blurhash painted onto a canvas, once per hash change. Size the canvas via
|
|
659
|
+
* `class`/`style` (e.g. `absolute inset-0 w-full h-full`); the internal decode
|
|
660
|
+
* resolution stays tiny regardless of the displayed size.
|
|
661
|
+
*/
|
|
662
|
+
function Blurhash(props) {
|
|
663
|
+
if (typeof document === "undefined") return null;
|
|
664
|
+
const canvas = document.createElement("canvas");
|
|
665
|
+
createEffect(() => {
|
|
666
|
+
canvas.className = props.class ?? "";
|
|
667
|
+
});
|
|
668
|
+
createEffect(() => {
|
|
669
|
+
canvas.style.cssText = props.style ?? "";
|
|
670
|
+
});
|
|
671
|
+
createEffect(() => {
|
|
672
|
+
const width = props.width ?? 32;
|
|
673
|
+
const height = props.height ?? 32;
|
|
674
|
+
canvas.width = width;
|
|
675
|
+
canvas.height = height;
|
|
676
|
+
const hash = props.hash;
|
|
677
|
+
if (!hash) return;
|
|
678
|
+
try {
|
|
679
|
+
const pixels = decodeBlurhash(hash, width, height, props.punch ?? 1);
|
|
680
|
+
const ctx = canvas.getContext("2d");
|
|
681
|
+
if (!ctx) return;
|
|
682
|
+
const imageData = ctx.createImageData(width, height);
|
|
683
|
+
imageData.data.set(pixels);
|
|
684
|
+
ctx.putImageData(imageData, 0, 0);
|
|
685
|
+
} catch {}
|
|
686
|
+
});
|
|
687
|
+
return canvas;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/lib/BucketImage.ts
|
|
692
|
+
const LAYER_STYLE = "position:absolute;inset:0;width:100%;height:100%;";
|
|
693
|
+
/**
|
|
694
|
+
* A bucket image that never pops in: it layers (bottom to top) your `fallback`
|
|
695
|
+
* plate, the automatically stored blurhash, and the real image, which stays
|
|
696
|
+
* transparent until the bitmap is DECODED and then crossfades over the
|
|
697
|
+
* placeholders. Placeholder layers unmount once the fade settles. Respects
|
|
698
|
+
* prefers-reduced-motion (instant swap). The container is made
|
|
699
|
+
* `position: relative` unless your `class` positions it already.
|
|
700
|
+
*
|
|
701
|
+
* ```tsx
|
|
702
|
+
* <BucketImage bucket="covers" path={row.cover_key} class="absolute inset-0"
|
|
703
|
+
* fallback={<MyPlate />} alt="" />
|
|
704
|
+
* ```
|
|
705
|
+
*/
|
|
706
|
+
function BucketImage(props) {
|
|
707
|
+
if (typeof document === "undefined") return null;
|
|
708
|
+
const image = useBucketImage(props.bucket, () => props.path, {
|
|
709
|
+
...props.options,
|
|
710
|
+
blurhash: props.blurhash !== false
|
|
711
|
+
});
|
|
712
|
+
const reducedMotion = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
713
|
+
const root = document.createElement("div");
|
|
714
|
+
createEffect(() => {
|
|
715
|
+
root.className = props.class ?? "";
|
|
716
|
+
});
|
|
717
|
+
onMount(() => {
|
|
718
|
+
if (getComputedStyle(root).position === "static") root.style.position = "relative";
|
|
719
|
+
});
|
|
720
|
+
const placeholder = document.createElement("div");
|
|
721
|
+
placeholder.style.cssText = LAYER_STYLE;
|
|
722
|
+
const fallbackHolder = document.createElement("div");
|
|
723
|
+
fallbackHolder.style.cssText = LAYER_STYLE;
|
|
724
|
+
placeholder.append(fallbackHolder);
|
|
725
|
+
const resolvedFallback = children(() => props.fallback);
|
|
726
|
+
createEffect(() => {
|
|
727
|
+
const nodes = resolvedFallback.toArray().filter((node) => node instanceof Node);
|
|
728
|
+
fallbackHolder.replaceChildren(...nodes);
|
|
729
|
+
});
|
|
730
|
+
const hashCanvas = Blurhash({
|
|
731
|
+
get hash() {
|
|
732
|
+
return image.blurhash();
|
|
733
|
+
},
|
|
734
|
+
style: LAYER_STYLE
|
|
735
|
+
});
|
|
736
|
+
if (hashCanvas instanceof Node) placeholder.append(hashCanvas);
|
|
737
|
+
const img = document.createElement("img");
|
|
738
|
+
img.decoding = "async";
|
|
739
|
+
img.style.cssText = `${LAYER_STYLE}opacity:0;`;
|
|
740
|
+
createEffect(() => {
|
|
741
|
+
img.className = props.imgClass ?? "";
|
|
742
|
+
});
|
|
743
|
+
createEffect(() => {
|
|
744
|
+
img.style.objectFit = props.fit ?? "cover";
|
|
745
|
+
});
|
|
746
|
+
createEffect(() => {
|
|
747
|
+
img.alt = props.alt ?? "";
|
|
748
|
+
});
|
|
749
|
+
createEffect(() => {
|
|
750
|
+
img.style.transition = reducedMotion ? "none" : `opacity ${props.transition ?? 300}ms ${props.easing ?? "cubic-bezier(0.16, 1, 0.3, 1)"}`;
|
|
751
|
+
});
|
|
752
|
+
createEffect(() => {
|
|
753
|
+
const url = image.url();
|
|
754
|
+
if (!url) {
|
|
755
|
+
img.removeAttribute("src");
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
img.src = url;
|
|
759
|
+
image.gate(img);
|
|
760
|
+
});
|
|
761
|
+
createEffect(() => {
|
|
762
|
+
img.style.opacity = image.ready() ? "1" : "0";
|
|
763
|
+
});
|
|
764
|
+
const [settled, setSettled] = createSignal(false);
|
|
765
|
+
createEffect(() => {
|
|
766
|
+
if (!image.ready()) {
|
|
767
|
+
setSettled(false);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const wait = (reducedMotion ? 0 : props.transition ?? 300) + 120;
|
|
771
|
+
const timer = setTimeout(() => setSettled(true), wait);
|
|
772
|
+
onCleanup(() => clearTimeout(timer));
|
|
773
|
+
});
|
|
774
|
+
createEffect(() => {
|
|
775
|
+
if (settled()) placeholder.remove();
|
|
776
|
+
else if (!placeholder.isConnected) root.insertBefore(placeholder, img);
|
|
777
|
+
});
|
|
778
|
+
root.append(placeholder, img);
|
|
779
|
+
return root;
|
|
780
|
+
}
|
|
781
|
+
|
|
327
782
|
//#endregion
|
|
328
783
|
//#region src/lib/Sp00kyProvider.ts
|
|
329
784
|
function Sp00kyProvider(props) {
|
|
330
785
|
const merged = mergeProps({ fallback: void 0 }, props);
|
|
331
786
|
const [db, setDb] = createSignal(void 0);
|
|
787
|
+
let disposed = false;
|
|
788
|
+
onCleanup(() => {
|
|
789
|
+
disposed = true;
|
|
790
|
+
});
|
|
332
791
|
onMount(async () => {
|
|
333
792
|
try {
|
|
334
793
|
const instance = new SyncedDb(merged.config);
|
|
335
794
|
await instance.init();
|
|
795
|
+
if (disposed) {
|
|
796
|
+
await instance.close();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (merged.preload) try {
|
|
800
|
+
await merged.preload(instance);
|
|
801
|
+
} catch (e) {
|
|
802
|
+
console.error("Sp00kyProvider: preload failed; revealing UI anyway", e);
|
|
803
|
+
}
|
|
336
804
|
setDb(() => instance);
|
|
337
805
|
merged.onReady?.(instance);
|
|
338
806
|
} catch (e) {
|
|
@@ -379,6 +847,18 @@ var SyncedDb = class {
|
|
|
379
847
|
this._initialized = true;
|
|
380
848
|
}
|
|
381
849
|
/**
|
|
850
|
+
* Tear down the client: leaves the tabs broker, closes the local store and
|
|
851
|
+
* remote socket, and frees the wasm circuit. Without this a remounted provider
|
|
852
|
+
* (or an HMR reload) strands a whole client, and the abandoned wasm heaps stay
|
|
853
|
+
* resident because V8 cannot see how much wasm memory a dropped wrapper holds.
|
|
854
|
+
*/
|
|
855
|
+
async close() {
|
|
856
|
+
const instance = this.sp00ky;
|
|
857
|
+
this.sp00ky = null;
|
|
858
|
+
this._initialized = false;
|
|
859
|
+
if (instance) await instance.close();
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
382
862
|
* Create a new record in the database
|
|
383
863
|
*/
|
|
384
864
|
async create(id, payload) {
|
|
@@ -397,8 +877,21 @@ var SyncedDb = class {
|
|
|
397
877
|
*/
|
|
398
878
|
async delete(tableName, selector) {
|
|
399
879
|
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
400
|
-
|
|
401
|
-
|
|
880
|
+
const isRecordId = selector instanceof RecordId || selector?.constructor?.name === "RecordId";
|
|
881
|
+
let id;
|
|
882
|
+
if (typeof selector === "string") id = selector;
|
|
883
|
+
else if (isRecordId) id = `${tableName}:${selector.id}`;
|
|
884
|
+
else throw new Error("Only string ID or RecordId selectors are supported currently with core");
|
|
885
|
+
await this.sp00ky.delete(tableName, id);
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Preload/prewarm a built query into the local cache without registering a
|
|
889
|
+
* live view. Fetches once and stores the rows (+ embedded related children)
|
|
890
|
+
* locally so a later `useQuery` for the same data paints instantly. Best-effort.
|
|
891
|
+
*/
|
|
892
|
+
async preload(finalQuery, options) {
|
|
893
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
894
|
+
await this.sp00ky.preload(finalQuery, options);
|
|
402
895
|
}
|
|
403
896
|
/**
|
|
404
897
|
* Query data from the database
|
|
@@ -429,6 +922,15 @@ var SyncedDb = class {
|
|
|
429
922
|
await this.signOut();
|
|
430
923
|
}
|
|
431
924
|
/**
|
|
925
|
+
* Tell the client that this app's own impersonation banner is on screen.
|
|
926
|
+
* Only needed with `impersonationBanner: { mode: 'custom' }`; the
|
|
927
|
+
* `useImpersonation({ rendersBanner: true })` hook calls it for you.
|
|
928
|
+
*/
|
|
929
|
+
acknowledgeImpersonationBanner() {
|
|
930
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
931
|
+
this.sp00ky.acknowledgeImpersonationBanner();
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
432
934
|
* Sign out, clear session and local storage
|
|
433
935
|
*/
|
|
434
936
|
async signOut() {
|
|
@@ -443,6 +945,19 @@ var SyncedDb = class {
|
|
|
443
945
|
return await this.sp00ky.useRemote(fn);
|
|
444
946
|
}
|
|
445
947
|
/**
|
|
948
|
+
* Run one SurrealQL statement on the server and return its result array.
|
|
949
|
+
* Nothing is cached locally. Prefer this over `useRemote(s => s.query(...))`
|
|
950
|
+
* for a plain read or write: the bare SDK client that callback receives
|
|
951
|
+
* skips the connect gate, so a statement issued while the socket is being
|
|
952
|
+
* rebuilt fails with "Specify a namespace to use" instead of waiting for
|
|
953
|
+
* the session. This one is also bounded by the remote service's
|
|
954
|
+
* concurrency limit and query timeout.
|
|
955
|
+
*/
|
|
956
|
+
async remoteQuery(sql, vars) {
|
|
957
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
958
|
+
return await this.sp00ky.remoteQuery(sql, vars);
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
446
961
|
* Access the remote database service directly
|
|
447
962
|
*/
|
|
448
963
|
get remote() {
|
|
@@ -467,10 +982,52 @@ var SyncedDb = class {
|
|
|
467
982
|
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
468
983
|
return this.sp00ky.pendingMutationCount;
|
|
469
984
|
}
|
|
985
|
+
/** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
|
|
986
|
+
get liveRetryCount() {
|
|
987
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
988
|
+
return this.sp00ky.liveRetryCount;
|
|
989
|
+
}
|
|
470
990
|
subscribeToPendingMutations(cb) {
|
|
471
991
|
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
472
992
|
return this.sp00ky.subscribeToPendingMutations(cb);
|
|
473
993
|
}
|
|
994
|
+
/** Current sync-health snapshot. See {@link useSyncStatus}. */
|
|
995
|
+
get syncHealth() {
|
|
996
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
997
|
+
return this.sp00ky.syncHealth;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Observe sync health. Fires immediately with the current status and again
|
|
1001
|
+
* on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
|
|
1002
|
+
* components; this is the imperative escape hatch.
|
|
1003
|
+
*/
|
|
1004
|
+
subscribeToSyncHealth(cb) {
|
|
1005
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
1006
|
+
return this.sp00ky.subscribeToSyncHealth(cb);
|
|
1007
|
+
}
|
|
1008
|
+
/** Current local-store durability snapshot. See {@link useStorageStatus}. */
|
|
1009
|
+
get storageHealth() {
|
|
1010
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
1011
|
+
return this.sp00ky.storageHealth;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Observe local-store durability. Fires immediately with the current snapshot
|
|
1015
|
+
* and again on change. Prefer the `useStorageStatus` hook in components; this
|
|
1016
|
+
* is the imperative escape hatch.
|
|
1017
|
+
*/
|
|
1018
|
+
subscribeToStorageHealth(cb) {
|
|
1019
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
1020
|
+
return this.sp00ky.subscribeToStorageHealth(cb);
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Web Push for this browser: `support()`, `subscribe()` (from a click),
|
|
1024
|
+
* `devices()`, `notify()` / `test()` (push to yourself), `bridge()` for the
|
|
1025
|
+
* service worker. See the push-notifications docs page.
|
|
1026
|
+
*/
|
|
1027
|
+
get webPush() {
|
|
1028
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
1029
|
+
return this.sp00ky.webPush;
|
|
1030
|
+
}
|
|
474
1031
|
bucket(name) {
|
|
475
1032
|
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
476
1033
|
return this.sp00ky.bucket(name);
|
|
@@ -481,5 +1038,5 @@ var SyncedDb = class {
|
|
|
481
1038
|
};
|
|
482
1039
|
|
|
483
1040
|
//#endregion
|
|
484
|
-
export { RecordId, Sp00kyProvider, SyncedDb, Uuid, useDb, useDownloadFile, useFileUpload, useQuery };
|
|
1041
|
+
export { Blurhash, BucketImage, RecordId, Sp00kyProvider, SyncedDb, Uuid, WebPushError, createPreload, useAppRelease, useBlurhash, useBucketImage, useCrdtField, useDb, useDownloadFile, useFeatureFlag, useFileUpload, useImpersonation, useQuery, useStorageStatus, useSyncStatus };
|
|
485
1042
|
//# sourceMappingURL=index.js.map
|