@spooky-sync/client-solid2 0.0.1-canary.200
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/LICENSE +21 -0
- package/QUICK_START.md +126 -0
- package/README.md +19 -0
- package/dist/index.cjs +903 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +498 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +498 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +884 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/skills/sp00ky-solid2/SKILL.md +68 -0
- package/src/index.ts +365 -0
- package/src/lib/Sp00kyProvider.ts +104 -0
- package/src/lib/__tests__/conflate.test.ts +120 -0
- package/src/lib/__tests__/create-query.test.ts +284 -0
- package/src/lib/__tests__/rc-semantics.test.ts +389 -0
- package/src/lib/conflate.ts +74 -0
- package/src/lib/context.ts +28 -0
- package/src/lib/create-preload.ts +115 -0
- package/src/lib/create-query.ts +285 -0
- package/src/lib/create-submission.ts +57 -0
- package/src/lib/from-subscription.ts +32 -0
- package/src/lib/models.ts +8 -0
- package/src/lib/use-app-release.ts +89 -0
- package/src/lib/use-crdt-field.ts +57 -0
- package/src/lib/use-download-file.ts +181 -0
- package/src/lib/use-feature-flag.ts +43 -0
- package/src/lib/use-file-upload.ts +146 -0
- package/src/lib/use-storage-status.ts +44 -0
- package/src/lib/use-sync-status.ts +63 -0
- package/src/types/index.ts +83 -0
- package/tsconfig.json +27 -0
- package/tsdown.config.ts +18 -0
- package/vitest.config.ts +14 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
import { Sp00kyClient, fileToUint8Array, semverGt } from "@spooky-sync/core";
|
|
2
|
+
import { RecordId, Uuid } from "surrealdb";
|
|
3
|
+
import { createComponent, createContext, createEffect, createMemo, createProjection, createSignal, merge, onCleanup, onSettled, useContext } from "solid-js";
|
|
4
|
+
|
|
5
|
+
//#region src/lib/conflate.ts
|
|
6
|
+
/**
|
|
7
|
+
* Latest-wins async iterable over a subscribe-callback source.
|
|
8
|
+
*
|
|
9
|
+
* Bridges spooky's push-callback subscriptions into the AsyncIterable shape
|
|
10
|
+
* Solid 2 computations consume natively. Each spooky emission is a full result
|
|
11
|
+
* set, so intermediate values are droppable: only the newest unconsumed value
|
|
12
|
+
* is buffered, and a pending pull resolves with it immediately.
|
|
13
|
+
*
|
|
14
|
+
* Teardown contract (probed in rc-semantics.test.ts): Solid 2 does NOT
|
|
15
|
+
* terminate a superseded/disposed computation's async generator — no
|
|
16
|
+
* `return()`, no `finally`. Consumers MUST call `it.return()` themselves from
|
|
17
|
+
* an `onCleanup` registered synchronously in the compute scope. `return()`
|
|
18
|
+
* unsubscribes (awaiting the unsubscribe if the subscribe returned a promise,
|
|
19
|
+
* as `sp00ky.subscribe` does) and resolves any parked pull as done.
|
|
20
|
+
*/
|
|
21
|
+
function conflate(subscribe) {
|
|
22
|
+
return { [Symbol.asyncIterator]() {
|
|
23
|
+
let buffered;
|
|
24
|
+
let resolveNext;
|
|
25
|
+
let done = false;
|
|
26
|
+
const unsubMaybe = subscribe((v) => {
|
|
27
|
+
if (done) return;
|
|
28
|
+
if (resolveNext) {
|
|
29
|
+
const r = resolveNext;
|
|
30
|
+
resolveNext = void 0;
|
|
31
|
+
r({
|
|
32
|
+
value: v,
|
|
33
|
+
done: false
|
|
34
|
+
});
|
|
35
|
+
} else buffered = { v };
|
|
36
|
+
});
|
|
37
|
+
const finish = () => {
|
|
38
|
+
if (done) return;
|
|
39
|
+
done = true;
|
|
40
|
+
buffered = void 0;
|
|
41
|
+
Promise.resolve(unsubMaybe).then((unsub) => unsub()).catch(() => {});
|
|
42
|
+
if (resolveNext) {
|
|
43
|
+
const r = resolveNext;
|
|
44
|
+
resolveNext = void 0;
|
|
45
|
+
r({
|
|
46
|
+
value: void 0,
|
|
47
|
+
done: true
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
next() {
|
|
53
|
+
if (done) return Promise.resolve({
|
|
54
|
+
value: void 0,
|
|
55
|
+
done: true
|
|
56
|
+
});
|
|
57
|
+
if (buffered) {
|
|
58
|
+
const v = buffered.v;
|
|
59
|
+
buffered = void 0;
|
|
60
|
+
return Promise.resolve({
|
|
61
|
+
value: v,
|
|
62
|
+
done: false
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return new Promise((r) => resolveNext = r);
|
|
66
|
+
},
|
|
67
|
+
return() {
|
|
68
|
+
finish();
|
|
69
|
+
return Promise.resolve({
|
|
70
|
+
value: void 0,
|
|
71
|
+
done: true
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
throw(e) {
|
|
75
|
+
finish();
|
|
76
|
+
return Promise.reject(e);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
} };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/lib/from-subscription.ts
|
|
84
|
+
/**
|
|
85
|
+
* Reactive view over a spooky subscribe-callback API.
|
|
86
|
+
*
|
|
87
|
+
* The memo's async generator pulls from a conflated (latest-wins) iterator;
|
|
88
|
+
* `initial` is committed as the memo's `loadingValue`, so the accessor is
|
|
89
|
+
* readable synchronously from birth and never suspends. Spooky's subscribe
|
|
90
|
+
* APIs fire immediately with the current value, so the real value lands within
|
|
91
|
+
* a tick of the first read.
|
|
92
|
+
*
|
|
93
|
+
* Teardown is manual by contract (see conflate.ts): onCleanup terminates the
|
|
94
|
+
* iterator, which unsubscribes.
|
|
95
|
+
*/
|
|
96
|
+
function fromSubscription(subscribe, initial) {
|
|
97
|
+
return createMemo(async function* () {
|
|
98
|
+
const it = conflate(subscribe)[Symbol.asyncIterator]();
|
|
99
|
+
onCleanup(() => void it.return?.());
|
|
100
|
+
while (true) {
|
|
101
|
+
const r = await it.next();
|
|
102
|
+
if (r.done) break;
|
|
103
|
+
yield r.value;
|
|
104
|
+
}
|
|
105
|
+
}, { loadingValue: initial });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/lib/context.ts
|
|
110
|
+
const Sp00kyContext = createContext();
|
|
111
|
+
function useDb() {
|
|
112
|
+
try {
|
|
113
|
+
return useContext(Sp00kyContext);
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error("useDb must be used within a <Sp00kyProvider>. Wrap your app in <Sp00kyProvider config={...}>.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Count of locally-committed mutations not yet acknowledged by the server.
|
|
120
|
+
* Drive an "unsaved changes" indicator off this.
|
|
121
|
+
*/
|
|
122
|
+
function usePendingMutations() {
|
|
123
|
+
const db = useDb();
|
|
124
|
+
return fromSubscription((cb) => db.subscribeToPendingMutations(cb), db.pendingMutationCount);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/lib/create-query.ts
|
|
129
|
+
function createQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
130
|
+
let db;
|
|
131
|
+
let finalQuery;
|
|
132
|
+
let options;
|
|
133
|
+
if (dbOrQuery instanceof SyncedDb) {
|
|
134
|
+
db = dbOrQuery;
|
|
135
|
+
finalQuery = queryOrOptions;
|
|
136
|
+
options = maybeOptions;
|
|
137
|
+
} else {
|
|
138
|
+
db = useDb();
|
|
139
|
+
finalQuery = dbOrQuery;
|
|
140
|
+
options = queryOrOptions;
|
|
141
|
+
}
|
|
142
|
+
const sp00ky = db.getSp00ky();
|
|
143
|
+
const [error, setError] = createSignal(void 0, { ownedWrite: true });
|
|
144
|
+
const [isFetched, setIsFetched] = createSignal(false, { ownedWrite: true });
|
|
145
|
+
const [isFetching, setIsFetching] = createSignal(false, { ownedWrite: true });
|
|
146
|
+
let activeHash;
|
|
147
|
+
const store = createProjection(async function* () {
|
|
148
|
+
const enabled = options?.enabled?.() ?? true;
|
|
149
|
+
const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
|
|
150
|
+
if (!enabled || !query) {
|
|
151
|
+
setIsFetched(false);
|
|
152
|
+
setError(void 0);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
setIsFetched(false);
|
|
156
|
+
setError(void 0);
|
|
157
|
+
const iterators = [];
|
|
158
|
+
const cleanups = [];
|
|
159
|
+
onCleanup(() => {
|
|
160
|
+
for (const it of iterators) it.return?.();
|
|
161
|
+
for (const c of cleanups) c();
|
|
162
|
+
});
|
|
163
|
+
try {
|
|
164
|
+
/**
|
|
165
|
+
* Registration can fail — the canonical case is the SSP answering 503
|
|
166
|
+
* NOT_READY while it bootstraps. Surface it as `error()` instead of
|
|
167
|
+
* throwing into the graph: the sync scheduler retries the
|
|
168
|
+
* registration underneath, so a transient failure still recovers, and
|
|
169
|
+
* a spinner driven by `isLoading()` resolves via `error()`.
|
|
170
|
+
*/
|
|
171
|
+
const { hash } = await query.run();
|
|
172
|
+
activeHash = hash;
|
|
173
|
+
cleanups.push(sp00ky.subscribeQueryStatus(hash, (status) => setIsFetching(status === "fetching"), { immediate: true }));
|
|
174
|
+
const it = conflate((cb) => sp00ky.subscribe(hash, cb, { immediate: true }))[Symbol.asyncIterator]();
|
|
175
|
+
iterators.push(it);
|
|
176
|
+
let isFirstCall = true;
|
|
177
|
+
while (true) {
|
|
178
|
+
const r = await it.next();
|
|
179
|
+
if (r.done) break;
|
|
180
|
+
const e = r.value;
|
|
181
|
+
const queryData = query.isOne ? e[0] ?? null : e;
|
|
182
|
+
const hasData = query.isOne ? queryData !== null && queryData !== void 0 : e.length > 0;
|
|
183
|
+
if (!isFirstCall || hasData) setIsFetched(true);
|
|
184
|
+
isFirstCall = false;
|
|
185
|
+
const t0 = performance.now();
|
|
186
|
+
yield { value: queryData };
|
|
187
|
+
sp00ky.reportFrontendTiming(hash, performance.now() - t0);
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
191
|
+
}
|
|
192
|
+
}, { value: null }, {
|
|
193
|
+
key: "id",
|
|
194
|
+
seedLoadingValue: true
|
|
195
|
+
});
|
|
196
|
+
const emptyList = [];
|
|
197
|
+
const data = () => {
|
|
198
|
+
const v = store.value;
|
|
199
|
+
if (v === null || v === void 0) {
|
|
200
|
+
const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
|
|
201
|
+
if (query && !query.isOne) return emptyList;
|
|
202
|
+
}
|
|
203
|
+
return v;
|
|
204
|
+
};
|
|
205
|
+
const readyGate = createMemo(async () => {
|
|
206
|
+
if (isFetched() || error()) return true;
|
|
207
|
+
await new Promise(() => {});
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
210
|
+
const ready = () => {
|
|
211
|
+
readyGate();
|
|
212
|
+
return data();
|
|
213
|
+
};
|
|
214
|
+
onCleanup(() => {
|
|
215
|
+
if (options?.deregisterOnCleanup && activeHash) sp00ky.deregisterQuery(activeHash);
|
|
216
|
+
});
|
|
217
|
+
const isLoading = () => !isFetched() && error() === void 0;
|
|
218
|
+
const isSettled = () => isFetched() && !isFetching();
|
|
219
|
+
return {
|
|
220
|
+
data,
|
|
221
|
+
ready,
|
|
222
|
+
error,
|
|
223
|
+
isLoading,
|
|
224
|
+
isFetching,
|
|
225
|
+
isSettled
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** @deprecated Renamed `createQuery` in the Solid 2 binding. */
|
|
229
|
+
const useQuery = createQuery;
|
|
230
|
+
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/lib/create-preload.ts
|
|
233
|
+
/**
|
|
234
|
+
* Reactive, fire-and-forget prewarm. Resolves the query (calling it if it's a
|
|
235
|
+
* function so it tracks reactive deps), dedupes on the query's stable identity
|
|
236
|
+
* hash, and warms it into the local cache via `db.preload`. No subscription and
|
|
237
|
+
* no cleanup: preload registers nothing that needs tearing down.
|
|
238
|
+
*
|
|
239
|
+
* Typical use: inside a list row, preload the detail query the user is likely
|
|
240
|
+
* to open next, so navigation paints from cache instead of the network.
|
|
241
|
+
*/
|
|
242
|
+
function createPreload(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
243
|
+
let db;
|
|
244
|
+
let finalQuery;
|
|
245
|
+
let options;
|
|
246
|
+
if (dbOrQuery instanceof SyncedDb) {
|
|
247
|
+
db = dbOrQuery;
|
|
248
|
+
finalQuery = queryOrOptions;
|
|
249
|
+
options = maybeOptions;
|
|
250
|
+
} else {
|
|
251
|
+
db = useDb();
|
|
252
|
+
finalQuery = dbOrQuery;
|
|
253
|
+
options = queryOrOptions;
|
|
254
|
+
}
|
|
255
|
+
let prevHash;
|
|
256
|
+
createEffect(() => {
|
|
257
|
+
if (!(options?.enabled?.() ?? true)) return void 0;
|
|
258
|
+
const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
|
|
259
|
+
if (!query) return void 0;
|
|
260
|
+
if (query.hash === prevHash) return void 0;
|
|
261
|
+
prevHash = query.hash;
|
|
262
|
+
return query;
|
|
263
|
+
}, (query) => {
|
|
264
|
+
if (!query) return;
|
|
265
|
+
db.getSp00ky().preload(query, {
|
|
266
|
+
refresh: options?.refresh,
|
|
267
|
+
staleTime: options?.staleTime
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/lib/use-sync-status.ts
|
|
274
|
+
/**
|
|
275
|
+
* Observe sync health for a "can't reach the server" banner / indicator.
|
|
276
|
+
*
|
|
277
|
+
* Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
|
|
278
|
+
* remote 500 on query registration, a dropped socket) are absorbed by the
|
|
279
|
+
* retry and never flip this; `isDegraded()` only goes true once failures
|
|
280
|
+
* persist for the configured number of consecutive rounds (sp00ky core config
|
|
281
|
+
* `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
|
|
282
|
+
* the next successful round. Must be used within a `<Sp00kyProvider>`.
|
|
283
|
+
*/
|
|
284
|
+
function useSyncStatus() {
|
|
285
|
+
const db = useDb();
|
|
286
|
+
const health = fromSubscription((cb) => db.subscribeToSyncHealth(cb), db.syncHealth);
|
|
287
|
+
return {
|
|
288
|
+
health,
|
|
289
|
+
status: () => health().status,
|
|
290
|
+
isHealthy: () => health().status === "healthy",
|
|
291
|
+
isDegraded: () => health().status === "degraded",
|
|
292
|
+
everConnected: () => health().everConnected,
|
|
293
|
+
isOffline: () => health().status === "degraded" && health().everConnected,
|
|
294
|
+
connection: () => health().connection,
|
|
295
|
+
isReconnecting: () => health().connection === "reconnecting"
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/lib/use-storage-status.ts
|
|
301
|
+
/**
|
|
302
|
+
* Observe how durable the LOCAL cache is, for a "no local storage" warning.
|
|
303
|
+
*
|
|
304
|
+
* Under `localEngine: 'sqlite'` with `store: 'indexeddb'` the durable store is
|
|
305
|
+
* the OPFS SAHPool VFS, and only ONE client per bucket can hold it open: a
|
|
306
|
+
* second tab of the same app cannot get it and runs in memory instead (the
|
|
307
|
+
* engine retries first, so a closing tab's lock is usually waited out). Must be
|
|
308
|
+
* used within a `<Sp00kyProvider>`.
|
|
309
|
+
*/
|
|
310
|
+
function useStorageStatus() {
|
|
311
|
+
const db = useDb();
|
|
312
|
+
const health = fromSubscription((cb) => db.subscribeToStorageHealth(cb), db.storageHealth);
|
|
313
|
+
return {
|
|
314
|
+
health,
|
|
315
|
+
status: () => health().status,
|
|
316
|
+
isPersistent: () => health().status === "persistent",
|
|
317
|
+
isMemoryFallback: () => health().fallback
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/lib/use-crdt-field.ts
|
|
323
|
+
function useCrdtField(table, recordId, field, fallbackText) {
|
|
324
|
+
const db = useDb();
|
|
325
|
+
const [crdtField, setCrdtField] = createSignal(null, { ownedWrite: true });
|
|
326
|
+
createEffect(() => recordId(), (id) => {
|
|
327
|
+
if (!id) {
|
|
328
|
+
setCrdtField(null);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const sp00ky = db.getSp00ky();
|
|
332
|
+
let superseded = false;
|
|
333
|
+
const text = fallbackText?.();
|
|
334
|
+
sp00ky.openCrdtField(table, id, field, text).then((cf) => {
|
|
335
|
+
if (!superseded) setCrdtField(cf);
|
|
336
|
+
else sp00ky.closeCrdtField(table, id, field);
|
|
337
|
+
}).catch((err) => {
|
|
338
|
+
console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);
|
|
339
|
+
});
|
|
340
|
+
return () => {
|
|
341
|
+
superseded = true;
|
|
342
|
+
if (crdtField()) {
|
|
343
|
+
sp00ky.closeCrdtField(table, id, field);
|
|
344
|
+
setCrdtField(null);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
return crdtField;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/lib/use-feature-flag.ts
|
|
353
|
+
/**
|
|
354
|
+
* Subscribe to a feature flag for the currently authenticated user.
|
|
355
|
+
*
|
|
356
|
+
* Returns three Solid accessors that update reactively whenever the
|
|
357
|
+
* server-materialized assignment in `_00_user_feature` changes. Backed by
|
|
358
|
+
* the same SSP + sync pipeline that powers `createQuery`, so toggling a flag
|
|
359
|
+
* via `spky flag enable <key>` propagates to the UI without a refresh.
|
|
360
|
+
*
|
|
361
|
+
* `enabled()` is `true` when the resolved variant exists and is not 'off'.
|
|
362
|
+
* For multi-variant flags, prefer `variant()` directly.
|
|
363
|
+
*/
|
|
364
|
+
function useFeatureFlag(key, options) {
|
|
365
|
+
const handle = useDb().getSp00ky().feature(key, options);
|
|
366
|
+
onCleanup(() => handle.close());
|
|
367
|
+
const state = fromSubscription((cb) => handle.subscribe((s) => cb({
|
|
368
|
+
variant: s.variant ?? options?.fallback,
|
|
369
|
+
payload: s.payload
|
|
370
|
+
})), {
|
|
371
|
+
variant: handle.variant(),
|
|
372
|
+
payload: handle.payload()
|
|
373
|
+
});
|
|
374
|
+
return {
|
|
375
|
+
variant: () => state().variant,
|
|
376
|
+
payload: () => state().payload,
|
|
377
|
+
enabled: () => {
|
|
378
|
+
const v = state().variant;
|
|
379
|
+
return v !== void 0 && v !== "off";
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/lib/use-app-release.ts
|
|
386
|
+
async function reloadForSnapshot(snapshot) {
|
|
387
|
+
if (typeof window === "undefined") return;
|
|
388
|
+
if (snapshot.cacheBust) try {
|
|
389
|
+
if (window.caches) {
|
|
390
|
+
const keys = await window.caches.keys();
|
|
391
|
+
await Promise.all(keys.map((k) => window.caches.delete(k)));
|
|
392
|
+
}
|
|
393
|
+
if (navigator.serviceWorker) {
|
|
394
|
+
const regs = await navigator.serviceWorker.getRegistrations();
|
|
395
|
+
for (const r of regs) r.update().catch(() => {});
|
|
396
|
+
}
|
|
397
|
+
window.location.href = window.location.pathname + "?cb=" + Date.now();
|
|
398
|
+
return;
|
|
399
|
+
} catch {}
|
|
400
|
+
window.location.reload();
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Observe the app's announced release (`_00_app_release:<app>`, written by
|
|
404
|
+
* `spky deploy` / `spky release`) and compare it against the running build.
|
|
405
|
+
*
|
|
406
|
+
* Typical use: mount a small "new version available — Reload" notification
|
|
407
|
+
* gated on `updateAvailable()`, auto-invoking `reload()` when `mandatory()`
|
|
408
|
+
* (guard the auto path against reload loops with a per-version marker, since
|
|
409
|
+
* a client can reload while the deploy is still rolling out and land on the
|
|
410
|
+
* old bundle again).
|
|
411
|
+
*/
|
|
412
|
+
function useAppRelease(options) {
|
|
413
|
+
const handle = useDb().getSp00ky().appRelease(options.app, { ttl: options.ttl });
|
|
414
|
+
onCleanup(() => handle.close());
|
|
415
|
+
const snapshot = fromSubscription((cb) => handle.subscribe(cb), handle.snapshot());
|
|
416
|
+
const updateAvailable = () => semverGt(snapshot().version, options.currentVersion);
|
|
417
|
+
return {
|
|
418
|
+
latestVersion: () => snapshot().version,
|
|
419
|
+
updateAvailable,
|
|
420
|
+
mandatory: () => updateAvailable() && snapshot().mandatory,
|
|
421
|
+
cacheBust: () => snapshot().cacheBust,
|
|
422
|
+
reload: () => reloadForSnapshot(snapshot())
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
//#endregion
|
|
427
|
+
//#region src/lib/use-file-upload.ts
|
|
428
|
+
function useFileUpload(dbOrBucketName, maybeBucketName) {
|
|
429
|
+
let db;
|
|
430
|
+
let bucketName;
|
|
431
|
+
if (typeof dbOrBucketName === "string") {
|
|
432
|
+
db = useDb();
|
|
433
|
+
bucketName = dbOrBucketName;
|
|
434
|
+
} else {
|
|
435
|
+
db = dbOrBucketName;
|
|
436
|
+
bucketName = maybeBucketName;
|
|
437
|
+
}
|
|
438
|
+
const [isUploading, setIsUploading] = createSignal(false, { ownedWrite: true });
|
|
439
|
+
const [error, setError] = createSignal(null, { ownedWrite: true });
|
|
440
|
+
const objectUrls = [];
|
|
441
|
+
onCleanup(() => {
|
|
442
|
+
for (const url of objectUrls) URL.revokeObjectURL(url);
|
|
443
|
+
});
|
|
444
|
+
const clearError = () => setError(null);
|
|
445
|
+
const validate = (file) => {
|
|
446
|
+
const config = db.getBucketConfig(bucketName);
|
|
447
|
+
if (!config) return;
|
|
448
|
+
if (config.maxSize !== null && config.maxSize !== void 0 && file.size > config.maxSize) {
|
|
449
|
+
const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
|
|
450
|
+
throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
|
|
451
|
+
}
|
|
452
|
+
if (config.allowedExtensions && config.allowedExtensions.length > 0) {
|
|
453
|
+
const fileName = file.name;
|
|
454
|
+
if (fileName) {
|
|
455
|
+
const ext = fileName.split(".").pop()?.toLowerCase();
|
|
456
|
+
if (!ext || !config.allowedExtensions.includes(ext)) throw new Error(`File type not allowed. Accepted: ${config.allowedExtensions.join(", ")}.`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
const upload = async (path, file, options) => {
|
|
461
|
+
setError(null);
|
|
462
|
+
try {
|
|
463
|
+
validate(file);
|
|
464
|
+
} catch (e) {
|
|
465
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
setIsUploading(true);
|
|
469
|
+
try {
|
|
470
|
+
const bytes = await fileToUint8Array(file);
|
|
471
|
+
return await db.bucket(bucketName).put(path, bytes, options);
|
|
472
|
+
} catch (e) {
|
|
473
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
474
|
+
} finally {
|
|
475
|
+
setIsUploading(false);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const download = async (path) => {
|
|
479
|
+
setError(null);
|
|
480
|
+
try {
|
|
481
|
+
const content = await db.bucket(bucketName).get(path);
|
|
482
|
+
if (!content) return null;
|
|
483
|
+
const objectUrl = URL.createObjectURL(new Blob([content]));
|
|
484
|
+
objectUrls.push(objectUrl);
|
|
485
|
+
return objectUrl;
|
|
486
|
+
} catch (e) {
|
|
487
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
const remove = async (path) => {
|
|
492
|
+
setError(null);
|
|
493
|
+
try {
|
|
494
|
+
await db.bucket(bucketName).delete(path);
|
|
495
|
+
} catch (e) {
|
|
496
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const exists = async (path) => {
|
|
500
|
+
setError(null);
|
|
501
|
+
try {
|
|
502
|
+
return await db.bucket(bucketName).exists(path);
|
|
503
|
+
} catch (e) {
|
|
504
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
return {
|
|
509
|
+
isUploading,
|
|
510
|
+
error,
|
|
511
|
+
clearError,
|
|
512
|
+
upload,
|
|
513
|
+
download,
|
|
514
|
+
remove,
|
|
515
|
+
exists
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/lib/use-download-file.ts
|
|
521
|
+
function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeOptions) {
|
|
522
|
+
let db;
|
|
523
|
+
let bucketName;
|
|
524
|
+
let path;
|
|
525
|
+
let options;
|
|
526
|
+
if (typeof dbOrBucketName === "string") {
|
|
527
|
+
db = useDb();
|
|
528
|
+
bucketName = dbOrBucketName;
|
|
529
|
+
path = bucketNameOrPath;
|
|
530
|
+
options = pathOrOptions ?? {};
|
|
531
|
+
} else {
|
|
532
|
+
db = dbOrBucketName;
|
|
533
|
+
bucketName = bucketNameOrPath;
|
|
534
|
+
path = pathOrOptions;
|
|
535
|
+
options = maybeOptions ?? {};
|
|
536
|
+
}
|
|
537
|
+
const useCache = options.cache !== false;
|
|
538
|
+
const [url, setUrl] = createSignal(null, { ownedWrite: true });
|
|
539
|
+
const [isLoading, setIsLoading] = createSignal(false, { ownedWrite: true });
|
|
540
|
+
const [error, setError] = createSignal(null, { ownedWrite: true });
|
|
541
|
+
let lease = null;
|
|
542
|
+
let privateUrl = null;
|
|
543
|
+
const [refetchSignal, setRefetchSignal] = createSignal(0);
|
|
544
|
+
/** Consumed by the next effect run, so `refetch()` bypasses every layer once. */
|
|
545
|
+
let reloadOnce = false;
|
|
546
|
+
function releaseCurrent() {
|
|
547
|
+
lease?.release();
|
|
548
|
+
lease = null;
|
|
549
|
+
if (privateUrl) {
|
|
550
|
+
URL.revokeObjectURL(privateUrl);
|
|
551
|
+
privateUrl = null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
createEffect(() => {
|
|
555
|
+
refetchSignal();
|
|
556
|
+
return path();
|
|
557
|
+
}, (filePath) => {
|
|
558
|
+
releaseCurrent();
|
|
559
|
+
if (!filePath) {
|
|
560
|
+
setUrl(null);
|
|
561
|
+
setIsLoading(false);
|
|
562
|
+
setError(null);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const reload = reloadOnce;
|
|
566
|
+
reloadOnce = false;
|
|
567
|
+
let cancelled = false;
|
|
568
|
+
setIsLoading(true);
|
|
569
|
+
setError(null);
|
|
570
|
+
const bucket = db.bucket(bucketName);
|
|
571
|
+
(useCache ? bucket.url(filePath, {
|
|
572
|
+
persist: options.persist !== false,
|
|
573
|
+
pin: options.pin,
|
|
574
|
+
revalidate: options.revalidate,
|
|
575
|
+
reload
|
|
576
|
+
}).then((acquired) => {
|
|
577
|
+
if (!acquired) return null;
|
|
578
|
+
if (cancelled) {
|
|
579
|
+
acquired.release();
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
lease = acquired;
|
|
583
|
+
return acquired.url;
|
|
584
|
+
}) : bucket.read(filePath, {
|
|
585
|
+
persist: false,
|
|
586
|
+
reload: true
|
|
587
|
+
}).then((blob) => {
|
|
588
|
+
if (!blob || cancelled) return null;
|
|
589
|
+
privateUrl = URL.createObjectURL(blob);
|
|
590
|
+
return privateUrl;
|
|
591
|
+
})).then((result) => {
|
|
592
|
+
if (!cancelled) {
|
|
593
|
+
setUrl(result);
|
|
594
|
+
setIsLoading(false);
|
|
595
|
+
}
|
|
596
|
+
}, (err) => {
|
|
597
|
+
if (!cancelled) {
|
|
598
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
599
|
+
setIsLoading(false);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
return () => {
|
|
603
|
+
cancelled = true;
|
|
604
|
+
};
|
|
605
|
+
});
|
|
606
|
+
onCleanup(() => {
|
|
607
|
+
releaseCurrent();
|
|
608
|
+
});
|
|
609
|
+
const refetch = () => {
|
|
610
|
+
reloadOnce = true;
|
|
611
|
+
setRefetchSignal((n) => n + 1);
|
|
612
|
+
};
|
|
613
|
+
return {
|
|
614
|
+
url,
|
|
615
|
+
isLoading,
|
|
616
|
+
error,
|
|
617
|
+
refetch
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
//#endregion
|
|
622
|
+
//#region src/lib/Sp00kyProvider.ts
|
|
623
|
+
function Sp00kyProvider(props) {
|
|
624
|
+
const merged = merge({ fallback: void 0 }, props);
|
|
625
|
+
const [db, setDb] = createSignal(void 0, { ownedWrite: true });
|
|
626
|
+
let disposed = false;
|
|
627
|
+
onCleanup(() => {
|
|
628
|
+
disposed = true;
|
|
629
|
+
});
|
|
630
|
+
onSettled(() => {
|
|
631
|
+
(async () => {
|
|
632
|
+
try {
|
|
633
|
+
const instance = new SyncedDb(merged.config);
|
|
634
|
+
await instance.init();
|
|
635
|
+
if (disposed) {
|
|
636
|
+
await instance.close();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (merged.preload) try {
|
|
640
|
+
await merged.preload(instance);
|
|
641
|
+
} catch (e) {
|
|
642
|
+
console.error("Sp00kyProvider: preload failed; revealing UI anyway", e);
|
|
643
|
+
}
|
|
644
|
+
setDb(() => instance);
|
|
645
|
+
merged.onReady?.(instance);
|
|
646
|
+
} catch (e) {
|
|
647
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
648
|
+
if (merged.onError) merged.onError(error);
|
|
649
|
+
else console.error("Sp00kyProvider: Failed to initialize database", error);
|
|
650
|
+
}
|
|
651
|
+
})();
|
|
652
|
+
});
|
|
653
|
+
return createMemo(() => {
|
|
654
|
+
const instance = db();
|
|
655
|
+
if (!instance) return merged.fallback;
|
|
656
|
+
return createComponent(Sp00kyContext, {
|
|
657
|
+
value: instance,
|
|
658
|
+
get children() {
|
|
659
|
+
return merged.children;
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/lib/create-submission.ts
|
|
667
|
+
/**
|
|
668
|
+
* Thin submission-state wrapper for mutations — button spinner/disable state
|
|
669
|
+
* around `db.create/update/delete/run` calls.
|
|
670
|
+
*
|
|
671
|
+
* Deliberately NOT built on Solid 2's `action()`/`createOptimisticStore`: the
|
|
672
|
+
* spooky engine is already optimistic local-first (writes commit to the local
|
|
673
|
+
* DB and re-render through live queries before sync; `run()` is an outbox
|
|
674
|
+
* CREATE), so a transaction/revert layer on top buys nothing and `action()`'s
|
|
675
|
+
* await-vs-yield transaction escape is a real footgun. Errors here mean the
|
|
676
|
+
* LOCAL commit failed — sync/push failures surface through `useSyncStatus`
|
|
677
|
+
* and `usePendingMutations` instead.
|
|
678
|
+
*/
|
|
679
|
+
function createSubmission(fn) {
|
|
680
|
+
const [inFlight, setInFlight] = createSignal(0, { ownedWrite: true });
|
|
681
|
+
const [error, setError] = createSignal(void 0, { ownedWrite: true });
|
|
682
|
+
const [result, setResult] = createSignal(void 0, { ownedWrite: true });
|
|
683
|
+
const submit = async (...args) => {
|
|
684
|
+
setError(void 0);
|
|
685
|
+
setInFlight((n) => n + 1);
|
|
686
|
+
try {
|
|
687
|
+
const r = await fn(...args);
|
|
688
|
+
setResult(() => r);
|
|
689
|
+
return r;
|
|
690
|
+
} catch (e) {
|
|
691
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
692
|
+
return;
|
|
693
|
+
} finally {
|
|
694
|
+
setInFlight((n) => n - 1);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
return {
|
|
698
|
+
submit,
|
|
699
|
+
pending: () => inFlight() > 0,
|
|
700
|
+
error,
|
|
701
|
+
result,
|
|
702
|
+
clearError: () => setError(void 0)
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/index.ts
|
|
708
|
+
/**
|
|
709
|
+
* SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration.
|
|
710
|
+
* Delegates all logic to the underlying sp00ky-ts instance.
|
|
711
|
+
*
|
|
712
|
+
* NOTE: keep in sync with packages/client-solid/src/index.ts (SyncedDb).
|
|
713
|
+
* Copied rather than shared so this package's dependency graph never pulls
|
|
714
|
+
* in solid-js 1.x; fold the two together once client-solid moves to Solid 2.
|
|
715
|
+
*/
|
|
716
|
+
var SyncedDb = class {
|
|
717
|
+
constructor(config) {
|
|
718
|
+
this.sp00ky = null;
|
|
719
|
+
this._initialized = false;
|
|
720
|
+
this.config = config;
|
|
721
|
+
}
|
|
722
|
+
getSp00ky() {
|
|
723
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
724
|
+
return this.sp00ky;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Initialize the sp00ky-ts instance
|
|
728
|
+
*/
|
|
729
|
+
async init() {
|
|
730
|
+
if (this._initialized) return;
|
|
731
|
+
this.sp00ky = new Sp00kyClient(this.config);
|
|
732
|
+
await this.sp00ky.init();
|
|
733
|
+
this._initialized = true;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Tear down the client: leaves the tabs broker, closes the local store and
|
|
737
|
+
* remote socket, and frees the wasm circuit. Without this a remounted provider
|
|
738
|
+
* (or an HMR reload) strands a whole client, and the abandoned wasm heaps stay
|
|
739
|
+
* resident because V8 cannot see how much wasm memory a dropped wrapper holds.
|
|
740
|
+
*/
|
|
741
|
+
async close() {
|
|
742
|
+
const instance = this.sp00ky;
|
|
743
|
+
this.sp00ky = null;
|
|
744
|
+
this._initialized = false;
|
|
745
|
+
if (instance) await instance.close();
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Create a new record in the database
|
|
749
|
+
*/
|
|
750
|
+
async create(id, payload) {
|
|
751
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
752
|
+
await this.sp00ky.create(id, payload);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Update an existing record in the database
|
|
756
|
+
*/
|
|
757
|
+
async update(tableName, recordId, payload, options) {
|
|
758
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
759
|
+
await this.sp00ky.update(tableName, recordId, payload, options);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Delete an existing record in the database
|
|
763
|
+
*/
|
|
764
|
+
async delete(tableName, selector) {
|
|
765
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
766
|
+
const ctorName = selector?.constructor?.name;
|
|
767
|
+
const isRecordId = selector instanceof RecordId || ctorName === "RecordId" || ctorName === "bound RecordId";
|
|
768
|
+
let id;
|
|
769
|
+
if (typeof selector === "string") id = selector;
|
|
770
|
+
else if (isRecordId) id = `${tableName}:${selector.id}`;
|
|
771
|
+
else throw new Error("Only string ID or RecordId selectors are supported currently with core");
|
|
772
|
+
await this.sp00ky.delete(tableName, id);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Preload/prewarm a built query into the local cache without registering a
|
|
776
|
+
* live view. Fetches once and stores the rows (+ embedded related children)
|
|
777
|
+
* locally so a later `createQuery` for the same data paints instantly. Best-effort.
|
|
778
|
+
*/
|
|
779
|
+
async preload(finalQuery, options) {
|
|
780
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
781
|
+
await this.sp00ky.preload(finalQuery, options);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Query data from the database
|
|
785
|
+
*/
|
|
786
|
+
query(table) {
|
|
787
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
788
|
+
return this.sp00ky.query(table, {});
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Run a backend operation
|
|
792
|
+
*/
|
|
793
|
+
async run(backend, path, payload, options) {
|
|
794
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
795
|
+
await this.sp00ky.run(backend, path, payload, options);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Sign out, clear session and local storage
|
|
799
|
+
*/
|
|
800
|
+
async signOut() {
|
|
801
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
802
|
+
await this.sp00ky.auth.signOut();
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Execute a function with direct access to the remote database connection
|
|
806
|
+
*/
|
|
807
|
+
async useRemote(fn) {
|
|
808
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
809
|
+
return await this.sp00ky.useRemote(fn);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Access the remote database service directly
|
|
813
|
+
*/
|
|
814
|
+
get remote() {
|
|
815
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
816
|
+
return this.sp00ky.remoteClient;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Access the local database service directly
|
|
820
|
+
*/
|
|
821
|
+
get local() {
|
|
822
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
823
|
+
return this.sp00ky.localClient;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Access the auth service
|
|
827
|
+
*/
|
|
828
|
+
get auth() {
|
|
829
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
830
|
+
return this.sp00ky.auth;
|
|
831
|
+
}
|
|
832
|
+
get pendingMutationCount() {
|
|
833
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
834
|
+
return this.sp00ky.pendingMutationCount;
|
|
835
|
+
}
|
|
836
|
+
/** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
|
|
837
|
+
get liveRetryCount() {
|
|
838
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
839
|
+
return this.sp00ky.liveRetryCount;
|
|
840
|
+
}
|
|
841
|
+
subscribeToPendingMutations(cb) {
|
|
842
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
843
|
+
return this.sp00ky.subscribeToPendingMutations(cb);
|
|
844
|
+
}
|
|
845
|
+
/** Current sync-health snapshot. See {@link useSyncStatus}. */
|
|
846
|
+
get syncHealth() {
|
|
847
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
848
|
+
return this.sp00ky.syncHealth;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Observe sync health. Fires immediately with the current status and again
|
|
852
|
+
* on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
|
|
853
|
+
* components; this is the imperative escape hatch.
|
|
854
|
+
*/
|
|
855
|
+
subscribeToSyncHealth(cb) {
|
|
856
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
857
|
+
return this.sp00ky.subscribeToSyncHealth(cb);
|
|
858
|
+
}
|
|
859
|
+
/** Current local-store durability snapshot. See {@link useStorageStatus}. */
|
|
860
|
+
get storageHealth() {
|
|
861
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
862
|
+
return this.sp00ky.storageHealth;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Observe local-store durability. Fires immediately with the current snapshot
|
|
866
|
+
* and again on change. Prefer the `useStorageStatus` hook in components; this
|
|
867
|
+
* is the imperative escape hatch.
|
|
868
|
+
*/
|
|
869
|
+
subscribeToStorageHealth(cb) {
|
|
870
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
871
|
+
return this.sp00ky.subscribeToStorageHealth(cb);
|
|
872
|
+
}
|
|
873
|
+
bucket(name) {
|
|
874
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
875
|
+
return this.sp00ky.bucket(name);
|
|
876
|
+
}
|
|
877
|
+
getBucketConfig(name) {
|
|
878
|
+
return this.config.schema.buckets?.find((b) => b.name === name);
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
//#endregion
|
|
883
|
+
export { RecordId, Sp00kyProvider, SyncedDb, Uuid, conflate, createPreload, createQuery, createSubmission, fromSubscription, useAppRelease, useCrdtField, useDb, useDownloadFile, useFeatureFlag, useFileUpload, usePendingMutations, useQuery, useStorageStatus, useSyncStatus };
|
|
884
|
+
//# sourceMappingURL=index.js.map
|