@spooky-sync/client-solid 0.0.1-canary.1 → 0.0.1-canary.100
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 +66 -0
- package/dist/index.cjs +475 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -23
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +120 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +471 -61
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/skills/sp00ky-solid/SKILL.md +264 -0
- package/skills/sp00ky-solid/references/file-hooks.md +112 -0
- package/src/cache/index.ts +1 -1
- package/src/cache/surrealdb-wasm-factory.ts +4 -1
- package/src/index.ts +114 -52
- package/src/lib/{SpookyProvider.ts → Sp00kyProvider.ts} +9 -7
- package/src/lib/context.ts +3 -3
- package/src/lib/models.ts +1 -1
- package/src/lib/use-crdt-field.ts +68 -0
- package/src/lib/use-download-file.ts +219 -0
- package/src/lib/use-feature-flag.ts +50 -0
- package/src/lib/use-file-upload.ts +136 -0
- package/src/lib/use-query.ts +137 -28
- package/src/lib/use-sync-status.ts +39 -0
- package/src/types/index.ts +3 -4
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Sp00kyClient, fileToUint8Array } from "@spooky-sync/core";
|
|
2
2
|
import { RecordId, Uuid } from "surrealdb";
|
|
3
3
|
import { createComponent, createContext, createEffect, createMemo, createSignal, mergeProps, onCleanup, onMount, useContext } from "solid-js";
|
|
4
|
+
import { createStore, reconcile } from "solid-js/store";
|
|
4
5
|
|
|
5
6
|
//#region src/lib/context.ts
|
|
6
|
-
const
|
|
7
|
+
const Sp00kyContext = createContext();
|
|
7
8
|
function useDb() {
|
|
8
|
-
const db = useContext(
|
|
9
|
-
if (!db) throw new Error("useDb must be used within a <
|
|
9
|
+
const db = useContext(Sp00kyContext);
|
|
10
|
+
if (!db) throw new Error("useDb must be used within a <Sp00kyProvider>. Wrap your app in <Sp00kyProvider config={...}>.");
|
|
10
11
|
return db;
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -21,27 +22,56 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
21
22
|
finalQuery = queryOrOptions;
|
|
22
23
|
options = maybeOptions;
|
|
23
24
|
} else {
|
|
24
|
-
const contextDb = useContext(
|
|
25
|
-
if (!contextDb) throw new Error("useQuery: No db argument provided and no
|
|
25
|
+
const contextDb = useContext(Sp00kyContext);
|
|
26
|
+
if (!contextDb) throw new Error("useQuery: No db argument provided and no Sp00kyContext found. Either pass a SyncedDb instance or wrap your app in <Sp00kyProvider>.");
|
|
26
27
|
db = contextDb;
|
|
27
28
|
finalQuery = dbOrQuery;
|
|
28
29
|
options = queryOrOptions;
|
|
29
30
|
}
|
|
30
|
-
const [data, setData] = createSignal(void 0);
|
|
31
31
|
const [error, setError] = createSignal(void 0);
|
|
32
32
|
const [isFetched, setIsFetched] = createSignal(false);
|
|
33
|
-
const [
|
|
33
|
+
const [isFetching, setIsFetching] = createSignal(false);
|
|
34
|
+
const [state, setState] = createStore({ value: void 0 });
|
|
35
|
+
const [version, setVersion] = createSignal(0);
|
|
36
|
+
const data = () => {
|
|
37
|
+
version();
|
|
38
|
+
return state.value;
|
|
39
|
+
};
|
|
34
40
|
let prevQueryString;
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
let runId = 0;
|
|
42
|
+
let activeUnsub;
|
|
43
|
+
let activeHash;
|
|
44
|
+
const teardownActive = () => {
|
|
45
|
+
activeUnsub?.();
|
|
46
|
+
activeUnsub = void 0;
|
|
47
|
+
};
|
|
48
|
+
const sp00ky = db.getSp00ky();
|
|
49
|
+
const initQuery = async (query, myRun) => {
|
|
37
50
|
const { hash } = await query.run();
|
|
51
|
+
if (myRun !== runId) return;
|
|
52
|
+
activeHash = hash;
|
|
38
53
|
setError(void 0);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
let isFirstCall = true;
|
|
55
|
+
const unsub = await sp00ky.subscribe(hash, (e) => {
|
|
56
|
+
const queryData = query.isOne ? e[0] : e;
|
|
57
|
+
const reconcileStart = performance.now();
|
|
58
|
+
setState("value", reconcile(queryData, { key: "id" }));
|
|
59
|
+
setVersion((v) => v + 1);
|
|
60
|
+
sp00ky.reportFrontendTiming(hash, performance.now() - reconcileStart);
|
|
61
|
+
const hasData = query.isOne ? queryData !== null && queryData !== void 0 : e.length > 0;
|
|
62
|
+
if (!isFirstCall || hasData) setIsFetched(true);
|
|
63
|
+
isFirstCall = false;
|
|
43
64
|
}, { immediate: true });
|
|
44
|
-
|
|
65
|
+
const unsubStatus = sp00ky.subscribeQueryStatus(hash, (status) => setIsFetching(status === "fetching"), { immediate: true });
|
|
66
|
+
const teardown = () => {
|
|
67
|
+
unsub();
|
|
68
|
+
unsubStatus();
|
|
69
|
+
};
|
|
70
|
+
if (myRun !== runId) {
|
|
71
|
+
teardown();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
activeUnsub = teardown;
|
|
45
75
|
};
|
|
46
76
|
createEffect(() => {
|
|
47
77
|
if (!(options?.enabled?.() ?? true)) {
|
|
@@ -50,14 +80,18 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
50
80
|
}
|
|
51
81
|
const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
|
|
52
82
|
if (!query) return;
|
|
53
|
-
const queryString =
|
|
83
|
+
const queryString = String(query.hash);
|
|
54
84
|
if (queryString === prevQueryString) return;
|
|
55
85
|
prevQueryString = queryString;
|
|
86
|
+
const myRun = ++runId;
|
|
87
|
+
teardownActive();
|
|
56
88
|
setIsFetched(false);
|
|
57
|
-
initQuery(query);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
89
|
+
initQuery(query, myRun);
|
|
90
|
+
});
|
|
91
|
+
onCleanup(() => {
|
|
92
|
+
runId++;
|
|
93
|
+
teardownActive();
|
|
94
|
+
if (options?.deregisterOnCleanup && activeHash) sp00ky.deregisterQuery(activeHash);
|
|
61
95
|
});
|
|
62
96
|
const isLoading = () => {
|
|
63
97
|
return !isFetched() && error() === void 0;
|
|
@@ -65,13 +99,359 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
65
99
|
return {
|
|
66
100
|
data,
|
|
67
101
|
error,
|
|
68
|
-
isLoading
|
|
102
|
+
isLoading,
|
|
103
|
+
isFetching
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/lib/use-sync-status.ts
|
|
109
|
+
/**
|
|
110
|
+
* Observe sync health for a "can't reach the server" banner / indicator.
|
|
111
|
+
*
|
|
112
|
+
* Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
|
|
113
|
+
* remote 500 on query registration, a dropped socket) are absorbed by the
|
|
114
|
+
* retry and never flip this; `isDegraded()` only goes true once failures
|
|
115
|
+
* persist for the configured number of consecutive rounds (sp00ky core config
|
|
116
|
+
* `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
|
|
117
|
+
* the next successful round. Must be used within a `<Sp00kyProvider>`.
|
|
118
|
+
*/
|
|
119
|
+
function useSyncStatus() {
|
|
120
|
+
const db = useDb();
|
|
121
|
+
const [health, setHealth] = createSignal(db.syncHealth);
|
|
122
|
+
onCleanup(db.subscribeToSyncHealth(setHealth));
|
|
123
|
+
return {
|
|
124
|
+
health,
|
|
125
|
+
status: () => health().status,
|
|
126
|
+
isHealthy: () => health().status === "healthy",
|
|
127
|
+
isDegraded: () => health().status === "degraded"
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/lib/use-crdt-field.ts
|
|
133
|
+
function useCrdtField(table, recordId, field, fallbackText) {
|
|
134
|
+
const db = useContext(Sp00kyContext);
|
|
135
|
+
if (!db) throw new Error("useCrdtField must be used within a <Sp00kyProvider>");
|
|
136
|
+
const [crdtField, setCrdtField] = createSignal(null);
|
|
137
|
+
let currentId;
|
|
138
|
+
let initialized = false;
|
|
139
|
+
createEffect(() => {
|
|
140
|
+
const id = recordId();
|
|
141
|
+
if (initialized && id === currentId) return;
|
|
142
|
+
if (currentId && crdtField()) {
|
|
143
|
+
db.getSp00ky().closeCrdtField(table, currentId, field);
|
|
144
|
+
setCrdtField(null);
|
|
145
|
+
}
|
|
146
|
+
currentId = id;
|
|
147
|
+
initialized = true;
|
|
148
|
+
if (!id) return;
|
|
149
|
+
const sp00ky = db.getSp00ky();
|
|
150
|
+
const text = fallbackText?.();
|
|
151
|
+
sp00ky.openCrdtField(table, id, field, text).then((cf) => {
|
|
152
|
+
if (currentId === id) setCrdtField(cf);
|
|
153
|
+
}).catch((err) => {
|
|
154
|
+
console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
onCleanup(() => {
|
|
158
|
+
if (currentId && crdtField()) {
|
|
159
|
+
db.getSp00ky().closeCrdtField(table, currentId, field);
|
|
160
|
+
setCrdtField(null);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return crdtField;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/lib/use-feature-flag.ts
|
|
168
|
+
/**
|
|
169
|
+
* Subscribe to a feature flag for the currently authenticated user.
|
|
170
|
+
*
|
|
171
|
+
* Returns three Solid accessors that update reactively whenever the
|
|
172
|
+
* server-materialized assignment in `_00_user_feature` changes. Backed by
|
|
173
|
+
* the same SSP + sync pipeline that powers `useQuery`, so toggling a flag
|
|
174
|
+
* via `spky flag enable <key>` propagates to the UI without a refresh.
|
|
175
|
+
*
|
|
176
|
+
* `enabled()` is `true` when the resolved variant exists and is not 'off'.
|
|
177
|
+
* For multi-variant flags, prefer `variant()` directly.
|
|
178
|
+
*/
|
|
179
|
+
function useFeatureFlag(key, options) {
|
|
180
|
+
const handle = useDb().getSp00ky().feature(key, options);
|
|
181
|
+
const [variant, setVariant] = createSignal(handle.variant());
|
|
182
|
+
const [payload, setPayload] = createSignal(handle.payload());
|
|
183
|
+
const unsub = handle.subscribe((s) => {
|
|
184
|
+
setVariant(s.variant ?? options?.fallback);
|
|
185
|
+
setPayload(s.payload);
|
|
186
|
+
});
|
|
187
|
+
onCleanup(() => {
|
|
188
|
+
unsub();
|
|
189
|
+
handle.close();
|
|
190
|
+
});
|
|
191
|
+
return {
|
|
192
|
+
variant,
|
|
193
|
+
payload,
|
|
194
|
+
enabled: () => {
|
|
195
|
+
const v = variant();
|
|
196
|
+
return v !== void 0 && v !== "off";
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/lib/use-file-upload.ts
|
|
203
|
+
function useFileUpload(dbOrBucketName, maybeBucketName) {
|
|
204
|
+
let db;
|
|
205
|
+
let bucketName;
|
|
206
|
+
if (typeof dbOrBucketName === "string") {
|
|
207
|
+
db = useDb();
|
|
208
|
+
bucketName = dbOrBucketName;
|
|
209
|
+
} else {
|
|
210
|
+
db = dbOrBucketName;
|
|
211
|
+
bucketName = maybeBucketName;
|
|
212
|
+
}
|
|
213
|
+
const [isUploading, setIsUploading] = createSignal(false);
|
|
214
|
+
const [error, setError] = createSignal(null);
|
|
215
|
+
const objectUrls = [];
|
|
216
|
+
onCleanup(() => {
|
|
217
|
+
for (const url of objectUrls) URL.revokeObjectURL(url);
|
|
218
|
+
});
|
|
219
|
+
const clearError = () => setError(null);
|
|
220
|
+
const validate = (file) => {
|
|
221
|
+
const config = db.getBucketConfig(bucketName);
|
|
222
|
+
if (!config) return;
|
|
223
|
+
if (config.maxSize !== null && config.maxSize !== void 0 && file.size > config.maxSize) {
|
|
224
|
+
const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
|
|
225
|
+
throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
|
|
226
|
+
}
|
|
227
|
+
if (config.allowedExtensions && config.allowedExtensions.length > 0) {
|
|
228
|
+
const fileName = file.name;
|
|
229
|
+
if (fileName) {
|
|
230
|
+
const ext = fileName.split(".").pop()?.toLowerCase();
|
|
231
|
+
if (!ext || !config.allowedExtensions.includes(ext)) throw new Error(`File type not allowed. Accepted: ${config.allowedExtensions.join(", ")}.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const upload = async (path, file) => {
|
|
236
|
+
setError(null);
|
|
237
|
+
try {
|
|
238
|
+
validate(file);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
setIsUploading(true);
|
|
244
|
+
try {
|
|
245
|
+
const bytes = await fileToUint8Array(file);
|
|
246
|
+
await db.bucket(bucketName).put(path, bytes);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
249
|
+
} finally {
|
|
250
|
+
setIsUploading(false);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const download = async (path) => {
|
|
254
|
+
setError(null);
|
|
255
|
+
try {
|
|
256
|
+
const content = await db.bucket(bucketName).get(path);
|
|
257
|
+
if (!content) return null;
|
|
258
|
+
const objectUrl = URL.createObjectURL(new Blob([content]));
|
|
259
|
+
objectUrls.push(objectUrl);
|
|
260
|
+
return objectUrl;
|
|
261
|
+
} catch (e) {
|
|
262
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
const remove = async (path) => {
|
|
267
|
+
setError(null);
|
|
268
|
+
try {
|
|
269
|
+
await db.bucket(bucketName).delete(path);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const exists = async (path) => {
|
|
275
|
+
setError(null);
|
|
276
|
+
try {
|
|
277
|
+
return await db.bucket(bucketName).exists(path);
|
|
278
|
+
} catch (e) {
|
|
279
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
return {
|
|
284
|
+
isUploading,
|
|
285
|
+
error,
|
|
286
|
+
clearError,
|
|
287
|
+
upload,
|
|
288
|
+
download,
|
|
289
|
+
remove,
|
|
290
|
+
exists
|
|
69
291
|
};
|
|
70
292
|
}
|
|
71
293
|
|
|
72
294
|
//#endregion
|
|
73
|
-
//#region src/lib/
|
|
74
|
-
|
|
295
|
+
//#region src/lib/use-download-file.ts
|
|
296
|
+
const downloadCache = /* @__PURE__ */ new Map();
|
|
297
|
+
const inflightRequests = /* @__PURE__ */ new Map();
|
|
298
|
+
function cacheKey(bucket, path) {
|
|
299
|
+
return `${bucket}:${path}`;
|
|
300
|
+
}
|
|
301
|
+
function releaseEntry(key) {
|
|
302
|
+
const entry = downloadCache.get(key);
|
|
303
|
+
if (!entry) return;
|
|
304
|
+
entry.refCount--;
|
|
305
|
+
if (entry.refCount <= 0) {
|
|
306
|
+
URL.revokeObjectURL(entry.url);
|
|
307
|
+
downloadCache.delete(key);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeOptions) {
|
|
311
|
+
let db;
|
|
312
|
+
let bucketName;
|
|
313
|
+
let path;
|
|
314
|
+
let options;
|
|
315
|
+
if (typeof dbOrBucketName === "string") {
|
|
316
|
+
db = useDb();
|
|
317
|
+
bucketName = dbOrBucketName;
|
|
318
|
+
path = bucketNameOrPath;
|
|
319
|
+
options = pathOrOptions ?? {};
|
|
320
|
+
} else {
|
|
321
|
+
db = dbOrBucketName;
|
|
322
|
+
bucketName = bucketNameOrPath;
|
|
323
|
+
path = pathOrOptions;
|
|
324
|
+
options = maybeOptions ?? {};
|
|
325
|
+
}
|
|
326
|
+
const useCache = options.cache !== false;
|
|
327
|
+
const [url, setUrl] = createSignal(null);
|
|
328
|
+
const [isLoading, setIsLoading] = createSignal(false);
|
|
329
|
+
const [error, setError] = createSignal(null);
|
|
330
|
+
let currentKey = null;
|
|
331
|
+
let privateUrl = null;
|
|
332
|
+
const [refetchSignal, setRefetchSignal] = createSignal(0);
|
|
333
|
+
const refetchTrigger = () => setRefetchSignal((n) => n + 1);
|
|
334
|
+
async function doDownload(key, filePath) {
|
|
335
|
+
if (useCache) {
|
|
336
|
+
const cached = downloadCache.get(key);
|
|
337
|
+
if (cached) {
|
|
338
|
+
cached.refCount++;
|
|
339
|
+
currentKey = key;
|
|
340
|
+
return cached.url;
|
|
341
|
+
}
|
|
342
|
+
const inflight = inflightRequests.get(key);
|
|
343
|
+
if (inflight) {
|
|
344
|
+
const result = await inflight;
|
|
345
|
+
if (result) {
|
|
346
|
+
const entry = downloadCache.get(key);
|
|
347
|
+
if (entry) {
|
|
348
|
+
entry.refCount++;
|
|
349
|
+
currentKey = key;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
const promise = (async () => {
|
|
355
|
+
const content = await db.bucket(bucketName).get(filePath);
|
|
356
|
+
if (!content) return null;
|
|
357
|
+
const objectUrl = URL.createObjectURL(new Blob([content]));
|
|
358
|
+
downloadCache.set(key, {
|
|
359
|
+
url: objectUrl,
|
|
360
|
+
refCount: 1
|
|
361
|
+
});
|
|
362
|
+
return objectUrl;
|
|
363
|
+
})();
|
|
364
|
+
inflightRequests.set(key, promise);
|
|
365
|
+
try {
|
|
366
|
+
const result = await promise;
|
|
367
|
+
currentKey = key;
|
|
368
|
+
return result;
|
|
369
|
+
} finally {
|
|
370
|
+
inflightRequests.delete(key);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
const content = await db.bucket(bucketName).get(filePath);
|
|
374
|
+
if (!content) return null;
|
|
375
|
+
const objectUrl = URL.createObjectURL(new Blob([content]));
|
|
376
|
+
privateUrl = objectUrl;
|
|
377
|
+
return objectUrl;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function releaseCurrentEntry() {
|
|
381
|
+
if (useCache && currentKey) {
|
|
382
|
+
releaseEntry(currentKey);
|
|
383
|
+
currentKey = null;
|
|
384
|
+
}
|
|
385
|
+
if (!useCache && privateUrl) {
|
|
386
|
+
URL.revokeObjectURL(privateUrl);
|
|
387
|
+
privateUrl = null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
createEffect(() => {
|
|
391
|
+
const filePath = path();
|
|
392
|
+
refetchSignal();
|
|
393
|
+
releaseCurrentEntry();
|
|
394
|
+
if (!filePath) {
|
|
395
|
+
setUrl(null);
|
|
396
|
+
setIsLoading(false);
|
|
397
|
+
setError(null);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const key = cacheKey(bucketName, filePath);
|
|
401
|
+
if (useCache) {
|
|
402
|
+
const cached = downloadCache.get(key);
|
|
403
|
+
if (cached) {
|
|
404
|
+
cached.refCount++;
|
|
405
|
+
currentKey = key;
|
|
406
|
+
setUrl(cached.url);
|
|
407
|
+
setIsLoading(false);
|
|
408
|
+
setError(null);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
let cancelled = false;
|
|
413
|
+
setIsLoading(true);
|
|
414
|
+
setError(null);
|
|
415
|
+
doDownload(key, filePath).then((result) => {
|
|
416
|
+
if (!cancelled) {
|
|
417
|
+
setUrl(result);
|
|
418
|
+
setIsLoading(false);
|
|
419
|
+
}
|
|
420
|
+
}, (err) => {
|
|
421
|
+
if (!cancelled) {
|
|
422
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
423
|
+
setIsLoading(false);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
onCleanup(() => {
|
|
427
|
+
cancelled = true;
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
onCleanup(() => {
|
|
431
|
+
releaseCurrentEntry();
|
|
432
|
+
});
|
|
433
|
+
const refetch = () => {
|
|
434
|
+
if (useCache && currentKey) {
|
|
435
|
+
const entry = downloadCache.get(currentKey);
|
|
436
|
+
if (entry) {
|
|
437
|
+
URL.revokeObjectURL(entry.url);
|
|
438
|
+
downloadCache.delete(currentKey);
|
|
439
|
+
}
|
|
440
|
+
currentKey = null;
|
|
441
|
+
}
|
|
442
|
+
refetchTrigger();
|
|
443
|
+
};
|
|
444
|
+
return {
|
|
445
|
+
url,
|
|
446
|
+
isLoading,
|
|
447
|
+
error,
|
|
448
|
+
refetch
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/lib/Sp00kyProvider.ts
|
|
454
|
+
function Sp00kyProvider(props) {
|
|
75
455
|
const merged = mergeProps({ fallback: void 0 }, props);
|
|
76
456
|
const [db, setDb] = createSignal(void 0);
|
|
77
457
|
onMount(async () => {
|
|
@@ -83,13 +463,13 @@ function SpookyProvider(props) {
|
|
|
83
463
|
} catch (e) {
|
|
84
464
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
85
465
|
if (merged.onError) merged.onError(error);
|
|
86
|
-
else console.error("
|
|
466
|
+
else console.error("Sp00kyProvider: Failed to initialize database", error);
|
|
87
467
|
}
|
|
88
468
|
});
|
|
89
469
|
return createMemo(() => {
|
|
90
470
|
const instance = db();
|
|
91
471
|
if (!instance) return merged.fallback;
|
|
92
|
-
return createComponent(
|
|
472
|
+
return createComponent(Sp00kyContext.Provider, {
|
|
93
473
|
value: instance,
|
|
94
474
|
get children() {
|
|
95
475
|
return merged.children;
|
|
@@ -101,69 +481,73 @@ function SpookyProvider(props) {
|
|
|
101
481
|
//#endregion
|
|
102
482
|
//#region src/index.ts
|
|
103
483
|
/**
|
|
104
|
-
* SyncedDb - A thin wrapper around
|
|
105
|
-
* Delegates all logic to the underlying
|
|
484
|
+
* SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
|
|
485
|
+
* Delegates all logic to the underlying sp00ky-ts instance
|
|
106
486
|
*/
|
|
107
487
|
var SyncedDb = class {
|
|
108
488
|
constructor(config) {
|
|
109
|
-
this.
|
|
489
|
+
this.sp00ky = null;
|
|
110
490
|
this._initialized = false;
|
|
111
491
|
this.config = config;
|
|
112
492
|
}
|
|
113
|
-
|
|
114
|
-
if (!this.
|
|
115
|
-
return this.
|
|
493
|
+
getSp00ky() {
|
|
494
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
495
|
+
return this.sp00ky;
|
|
116
496
|
}
|
|
117
497
|
/**
|
|
118
|
-
* Initialize the
|
|
498
|
+
* Initialize the sp00ky-ts instance
|
|
119
499
|
*/
|
|
120
500
|
async init() {
|
|
121
501
|
if (this._initialized) return;
|
|
122
|
-
this.
|
|
123
|
-
await this.
|
|
502
|
+
this.sp00ky = new Sp00kyClient(this.config);
|
|
503
|
+
await this.sp00ky.init();
|
|
124
504
|
this._initialized = true;
|
|
125
505
|
}
|
|
126
506
|
/**
|
|
127
507
|
* Create a new record in the database
|
|
128
508
|
*/
|
|
129
509
|
async create(id, payload) {
|
|
130
|
-
if (!this.
|
|
131
|
-
await this.
|
|
510
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
511
|
+
await this.sp00ky.create(id, payload);
|
|
132
512
|
}
|
|
133
513
|
/**
|
|
134
514
|
* Update an existing record in the database
|
|
135
515
|
*/
|
|
136
516
|
async update(tableName, recordId, payload, options) {
|
|
137
|
-
if (!this.
|
|
138
|
-
await this.
|
|
517
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
518
|
+
await this.sp00ky.update(tableName, recordId, payload, options);
|
|
139
519
|
}
|
|
140
520
|
/**
|
|
141
521
|
* Delete an existing record in the database
|
|
142
522
|
*/
|
|
143
523
|
async delete(tableName, selector) {
|
|
144
|
-
if (!this.
|
|
145
|
-
|
|
146
|
-
|
|
524
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
525
|
+
const isRecordId = selector instanceof RecordId || selector?.constructor?.name === "RecordId";
|
|
526
|
+
let id;
|
|
527
|
+
if (typeof selector === "string") id = selector;
|
|
528
|
+
else if (isRecordId) id = `${tableName}:${selector.id}`;
|
|
529
|
+
else throw new Error("Only string ID or RecordId selectors are supported currently with core");
|
|
530
|
+
await this.sp00ky.delete(tableName, id);
|
|
147
531
|
}
|
|
148
532
|
/**
|
|
149
533
|
* Query data from the database
|
|
150
534
|
*/
|
|
151
535
|
query(table) {
|
|
152
|
-
if (!this.
|
|
153
|
-
return this.
|
|
536
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
537
|
+
return this.sp00ky.query(table, {});
|
|
154
538
|
}
|
|
155
539
|
/**
|
|
156
540
|
* Run a backend operation
|
|
157
541
|
*/
|
|
158
542
|
async run(backend, path, payload, options) {
|
|
159
|
-
if (!this.
|
|
160
|
-
await this.
|
|
543
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
544
|
+
await this.sp00ky.run(backend, path, payload, options);
|
|
161
545
|
}
|
|
162
546
|
/**
|
|
163
547
|
* Authenticate with the database
|
|
164
548
|
*/
|
|
165
549
|
async authenticate(token) {
|
|
166
|
-
await this.
|
|
550
|
+
await this.sp00ky?.authenticate(token);
|
|
167
551
|
return new RecordId("user", "me");
|
|
168
552
|
}
|
|
169
553
|
/**
|
|
@@ -177,47 +561,73 @@ var SyncedDb = class {
|
|
|
177
561
|
* Sign out, clear session and local storage
|
|
178
562
|
*/
|
|
179
563
|
async signOut() {
|
|
180
|
-
if (!this.
|
|
181
|
-
await this.
|
|
564
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
565
|
+
await this.sp00ky.auth.signOut();
|
|
182
566
|
}
|
|
183
567
|
/**
|
|
184
568
|
* Execute a function with direct access to the remote database connection
|
|
185
569
|
*/
|
|
186
570
|
async useRemote(fn) {
|
|
187
|
-
if (!this.
|
|
188
|
-
return await this.
|
|
571
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
572
|
+
return await this.sp00ky.useRemote(fn);
|
|
189
573
|
}
|
|
190
574
|
/**
|
|
191
575
|
* Access the remote database service directly
|
|
192
576
|
*/
|
|
193
577
|
get remote() {
|
|
194
|
-
if (!this.
|
|
195
|
-
return this.
|
|
578
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
579
|
+
return this.sp00ky.remoteClient;
|
|
196
580
|
}
|
|
197
581
|
/**
|
|
198
582
|
* Access the local database service directly
|
|
199
583
|
*/
|
|
200
584
|
get local() {
|
|
201
|
-
if (!this.
|
|
202
|
-
return this.
|
|
585
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
586
|
+
return this.sp00ky.localClient;
|
|
203
587
|
}
|
|
204
588
|
/**
|
|
205
589
|
* Access the auth service
|
|
206
590
|
*/
|
|
207
591
|
get auth() {
|
|
208
|
-
if (!this.
|
|
209
|
-
return this.
|
|
592
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
593
|
+
return this.sp00ky.auth;
|
|
210
594
|
}
|
|
211
595
|
get pendingMutationCount() {
|
|
212
|
-
if (!this.
|
|
213
|
-
return this.
|
|
596
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
597
|
+
return this.sp00ky.pendingMutationCount;
|
|
598
|
+
}
|
|
599
|
+
/** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
|
|
600
|
+
get liveRetryCount() {
|
|
601
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
602
|
+
return this.sp00ky.liveRetryCount;
|
|
214
603
|
}
|
|
215
604
|
subscribeToPendingMutations(cb) {
|
|
216
|
-
if (!this.
|
|
217
|
-
return this.
|
|
605
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
606
|
+
return this.sp00ky.subscribeToPendingMutations(cb);
|
|
607
|
+
}
|
|
608
|
+
/** Current sync-health snapshot. See {@link useSyncStatus}. */
|
|
609
|
+
get syncHealth() {
|
|
610
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
611
|
+
return this.sp00ky.syncHealth;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Observe sync health. Fires immediately with the current status and again
|
|
615
|
+
* on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
|
|
616
|
+
* components; this is the imperative escape hatch.
|
|
617
|
+
*/
|
|
618
|
+
subscribeToSyncHealth(cb) {
|
|
619
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
620
|
+
return this.sp00ky.subscribeToSyncHealth(cb);
|
|
621
|
+
}
|
|
622
|
+
bucket(name) {
|
|
623
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
624
|
+
return this.sp00ky.bucket(name);
|
|
625
|
+
}
|
|
626
|
+
getBucketConfig(name) {
|
|
627
|
+
return this.config.schema.buckets?.find((b) => b.name === name);
|
|
218
628
|
}
|
|
219
629
|
};
|
|
220
630
|
|
|
221
631
|
//#endregion
|
|
222
|
-
export { RecordId,
|
|
632
|
+
export { RecordId, Sp00kyProvider, SyncedDb, Uuid, useCrdtField, useDb, useDownloadFile, useFeatureFlag, useFileUpload, useQuery, useSyncStatus };
|
|
223
633
|
//# sourceMappingURL=index.js.map
|