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