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