@spooky-sync/client-solid 0.0.1-canary.13 → 0.0.1-canary.130
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 +68 -0
- package/README.md +20 -0
- package/dist/index.cjs +275 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -21
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +129 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +272 -66
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/skills/sp00ky-solid/SKILL.md +335 -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 +121 -55
- package/src/lib/Sp00kyProvider.ts +76 -0
- package/src/lib/context.ts +3 -3
- package/src/lib/create-preload.ts +111 -0
- package/src/lib/models.ts +1 -1
- package/src/lib/use-crdt-field.ts +68 -0
- package/src/lib/use-download-file.ts +2 -2
- package/src/lib/use-feature-flag.ts +50 -0
- package/src/lib/use-file-upload.ts +2 -1
- package/src/lib/use-query.ts +143 -28
- package/src/lib/use-sync-status.ts +50 -0
- package/src/types/index.ts +3 -4
- package/src/lib/SpookyProvider.ts +0 -55
package/AGENTS.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# `@spooky-sync/client-solid` — agent guide
|
|
2
|
+
|
|
3
|
+
## What this package is
|
|
4
|
+
|
|
5
|
+
The SolidJS binding for sp00ky. Exposes a `Sp00kyProvider` that initializes a `Sp00kyClient` and a set of reactive hooks (`useDb`, `useQuery`, `useCrdtField`, `useFileUpload`, `useDownloadFile`). All hooks expect to be called inside a `<Sp00kyProvider>` boundary.
|
|
6
|
+
|
|
7
|
+
## Setup pattern
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// db.ts
|
|
11
|
+
import type { SyncedDbConfig } from '@spooky-sync/client-solid';
|
|
12
|
+
import { schema, SURQL_SCHEMA } from './schema.gen'; // generated by `spky generate`
|
|
13
|
+
|
|
14
|
+
export const dbConfig: SyncedDbConfig<typeof schema> = {
|
|
15
|
+
schema,
|
|
16
|
+
schemaSurql: SURQL_SCHEMA,
|
|
17
|
+
database: {
|
|
18
|
+
namespace: 'main',
|
|
19
|
+
database: 'app',
|
|
20
|
+
endpoint: 'ws://localhost:8666/rpc',
|
|
21
|
+
store: 'memory', // or 'indexeddb' for persistence
|
|
22
|
+
persistenceClient: 'localstorage',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// App.tsx
|
|
29
|
+
<Sp00kyProvider config={dbConfig}>{/* app */}</Sp00kyProvider>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Key hooks
|
|
33
|
+
|
|
34
|
+
- **`useDb<typeof schema>()`** — returns the `SyncedDb<S>` instance. Methods:
|
|
35
|
+
- `db.create(id, payload)` — `id` is a full record ID like `'thread:abc'`.
|
|
36
|
+
- `db.update(table, id, payload, options?)` — `options.debounced` coalesces updates.
|
|
37
|
+
- `db.delete(table, idOrSelector)`.
|
|
38
|
+
- `db.query(table)` — returns a `QueryBuilder`. Chain `.related()`, `.orderBy()`, `.limit()`, etc., end with `.build()`.
|
|
39
|
+
- `db.preload(query, options?)` — cache-aware, awaitable prewarm. Cold (nothing cached in the bucket) fetches + persists and the promise awaits it (blocks); warm returns instantly. `options.refresh` (`'onUse'` default / `'background'` / `'stale'`) + `options.staleTime` control warm-refresh. One-shot snapshot, no live view — freshens on use.
|
|
40
|
+
- `db.run(backend, route, payload)` — call a backend RPC route.
|
|
41
|
+
- `db.bucket(name)` — get a `BucketHandle` for file storage.
|
|
42
|
+
- `db.useRemote(fn)` — escape hatch to the raw `Surreal` client (skips cache).
|
|
43
|
+
- `db.authenticate(token)`, `db.signOut()`, `db.auth`.
|
|
44
|
+
- `db.pendingMutationCount`, `db.subscribeToPendingMutations(cb)`.
|
|
45
|
+
- **`useQuery(() => db.query(...).build())`** — reactive query. Returns `{ data, status, error, ... }` accessors. The factory function is tracked, so passing reactive params (signals) re-runs the query.
|
|
46
|
+
- **`createPreload(() => db.query(...).build(), options?)`** — reactive, fire-and-forget prewarm (same overloads/`enabled` as `useQuery`, plus `refresh`/`staleTime`). Warms a query the user is likely to open next (e.g. a list row's detail) into the local cache. For blocking first-load, use the `Sp00kyProvider` `preload` prop instead.
|
|
47
|
+
- **`useCrdtField(table, () => recordId, field, () => valueAccessor)`** — wires a CRDT text field to a Loro doc. Pair with `db.update(table, id, { [field]: newValue }, { debounced: true })` so rapid keystrokes don't flood the queue. *All four arguments take accessor functions where reactive — that's deliberate, for SolidJS tracking.*
|
|
48
|
+
- **`useFileUpload()`** / **`useDownloadFile()`** — bucket helpers; the upload result includes the storage path you write into a record column.
|
|
49
|
+
|
|
50
|
+
## Re-exports for convenience
|
|
51
|
+
|
|
52
|
+
- `RecordId`, `Uuid` from `surrealdb`.
|
|
53
|
+
- Query-builder types: `TableModel`, `TableNames`, `GetTable`, `QueryResult`, etc. (see `@spooky-sync/query-builder/AGENTS.md`).
|
|
54
|
+
- `Model<S, T>`, `GenericModel`, `ModelPayload` — typed row shapes.
|
|
55
|
+
|
|
56
|
+
## Common gotchas
|
|
57
|
+
|
|
58
|
+
- **`useDb()` requires `<typeof schema>`.** Without the generic, all calls fall back to `unknown` and you lose type safety.
|
|
59
|
+
- **CRDT fields are not regular columns.** Don't read or write them via `useQuery` — read with `useCrdtField`, write via `db.update` with `{ debounced: true }`.
|
|
60
|
+
- **Generate IDs explicitly.** `const id = new Uuid().toString()`, then `db.create(\`thread:\${id}\`, ...)`. SurrealDB's auto-id only fires on direct DB writes, not through the sync queue.
|
|
61
|
+
- **`useQuery` factories must call `.build()` (or `.all()`, `.first()`, etc.).** A bare `db.query('thread')` is a builder, not a query — `useQuery` will throw or return forever-loading.
|
|
62
|
+
- **Provider is mandatory.** Calling any hook outside `<Sp00kyProvider>` throws.
|
|
63
|
+
|
|
64
|
+
## Pointers
|
|
65
|
+
|
|
66
|
+
- Sync engine: `node_modules/@spooky-sync/core/AGENTS.md`
|
|
67
|
+
- Query builder DSL: `node_modules/@spooky-sync/query-builder/AGENTS.md`
|
|
68
|
+
- Schema authoring + codegen: `node_modules/@spooky-sync/cli/AGENTS.md`
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A SurrealDB client for Solid.js with automatic cache synchronization and live qu
|
|
|
11
11
|
- **Type-Safe**: Full TypeScript support with generated schema types
|
|
12
12
|
- **Reactive**: Seamless integration with Solid.js reactivity
|
|
13
13
|
- **Offline Support**: Local cache works even when remote is temporarily unavailable
|
|
14
|
+
- **Preload / Prewarm**: Warm data into the local cache ahead of time so screens open instantly — awaitable to gate first load, reactive to prefetch what's next
|
|
14
15
|
|
|
15
16
|
## Quick Start
|
|
16
17
|
|
|
@@ -215,6 +216,25 @@ const liveQuery = await db.query.thread
|
|
|
215
216
|
.orderBy('created_at', 'desc'); // ✅ Field names autocompleted
|
|
216
217
|
```
|
|
217
218
|
|
|
219
|
+
### Preload / Prewarm
|
|
220
|
+
|
|
221
|
+
Warm a query's results into the local cache before they're needed, so a later `useQuery` paints
|
|
222
|
+
instantly instead of waiting on the network. Preload is a one-shot snapshot — it does not register
|
|
223
|
+
a live view; the data freshens on use when the real `useQuery` mounts.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// Awaitable + cache-aware: blocks on the FIRST load (nothing cached), returns
|
|
227
|
+
// instantly on later loads (already cached). Great for gating the UI on config.
|
|
228
|
+
await db.preload(db.query('config').build());
|
|
229
|
+
|
|
230
|
+
// Opt into a one-time background refresh when the cache is stale:
|
|
231
|
+
await db.preload(db.query('config').build(), { refresh: 'stale', staleTime: '1d' });
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Block first-load UI on essential data via the `Sp00kyProvider` `preload` prop, or reactively
|
|
235
|
+
prefetch what the user is likely to open next with `createPreload(() => …build())`. Full details
|
|
236
|
+
in the [SKILL guide](./skills/sp00ky-solid/SKILL.md#preload).
|
|
237
|
+
|
|
218
238
|
## Documentation
|
|
219
239
|
|
|
220
240
|
- **[Quick Start Guide](./QUICK_START.md)**: Get up and running quickly
|
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,30 +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
55
|
let isFirstCall = true;
|
|
41
|
-
const unsub = await
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
|
45
63
|
if (!isFirstCall || hasData) setIsFetched(true);
|
|
46
64
|
isFirstCall = false;
|
|
47
65
|
}, { immediate: true });
|
|
48
|
-
|
|
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;
|
|
49
76
|
};
|
|
50
77
|
(0, solid_js.createEffect)(() => {
|
|
51
78
|
if (!(options?.enabled?.() ?? true)) {
|
|
@@ -54,22 +81,165 @@ function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
54
81
|
}
|
|
55
82
|
const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
|
|
56
83
|
if (!query) return;
|
|
57
|
-
const queryString =
|
|
84
|
+
const queryString = String(query.hash);
|
|
58
85
|
if (queryString === prevQueryString) return;
|
|
59
86
|
prevQueryString = queryString;
|
|
87
|
+
const myRun = ++runId;
|
|
88
|
+
teardownActive();
|
|
60
89
|
setIsFetched(false);
|
|
61
|
-
initQuery(query);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
initQuery(query, myRun);
|
|
91
|
+
});
|
|
92
|
+
(0, solid_js.onCleanup)(() => {
|
|
93
|
+
runId++;
|
|
94
|
+
teardownActive();
|
|
95
|
+
if (options?.deregisterOnCleanup && activeHash) sp00ky.deregisterQuery(activeHash);
|
|
65
96
|
});
|
|
66
97
|
const isLoading = () => {
|
|
67
98
|
return !isFetched() && error() === void 0;
|
|
68
99
|
};
|
|
100
|
+
const isSettled = () => isFetched() && !isFetching();
|
|
69
101
|
return {
|
|
70
102
|
data,
|
|
71
103
|
error,
|
|
72
|
-
isLoading
|
|
104
|
+
isLoading,
|
|
105
|
+
isFetching,
|
|
106
|
+
isSettled
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/lib/create-preload.ts
|
|
112
|
+
/**
|
|
113
|
+
* Reactive, fire-and-forget prewarm. Resolves the query (calling it if it's a
|
|
114
|
+
* function so it tracks reactive deps), dedupes on the query's stable identity
|
|
115
|
+
* hash, and warms it into the local cache via `db.preload`. No subscription and
|
|
116
|
+
* no cleanup: preload registers nothing that needs tearing down.
|
|
117
|
+
*
|
|
118
|
+
* Typical use: inside a list row, preload the detail query the user is likely
|
|
119
|
+
* to open next, so navigation paints from cache instead of the network.
|
|
120
|
+
*/
|
|
121
|
+
function createPreload(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
122
|
+
let db;
|
|
123
|
+
let finalQuery;
|
|
124
|
+
let options;
|
|
125
|
+
if (dbOrQuery instanceof SyncedDb) {
|
|
126
|
+
db = dbOrQuery;
|
|
127
|
+
finalQuery = queryOrOptions;
|
|
128
|
+
options = maybeOptions;
|
|
129
|
+
} else {
|
|
130
|
+
const contextDb = (0, solid_js.useContext)(Sp00kyContext);
|
|
131
|
+
if (!contextDb) throw new Error("createPreload: No db argument provided and no Sp00kyContext found. Either pass a SyncedDb instance or wrap your app in <Sp00kyProvider>.");
|
|
132
|
+
db = contextDb;
|
|
133
|
+
finalQuery = dbOrQuery;
|
|
134
|
+
options = queryOrOptions;
|
|
135
|
+
}
|
|
136
|
+
let prevHash;
|
|
137
|
+
(0, solid_js.createEffect)(() => {
|
|
138
|
+
if (!(options?.enabled?.() ?? true)) return;
|
|
139
|
+
const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
|
|
140
|
+
if (!query) return;
|
|
141
|
+
if (query.hash === prevHash) return;
|
|
142
|
+
prevHash = query.hash;
|
|
143
|
+
db.getSp00ky().preload(query, {
|
|
144
|
+
refresh: options?.refresh,
|
|
145
|
+
staleTime: options?.staleTime
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/lib/use-sync-status.ts
|
|
152
|
+
/**
|
|
153
|
+
* Observe sync health for a "can't reach the server" banner / indicator.
|
|
154
|
+
*
|
|
155
|
+
* Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
|
|
156
|
+
* remote 500 on query registration, a dropped socket) are absorbed by the
|
|
157
|
+
* retry and never flip this; `isDegraded()` only goes true once failures
|
|
158
|
+
* persist for the configured number of consecutive rounds (sp00ky core config
|
|
159
|
+
* `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
|
|
160
|
+
* the next successful round. Must be used within a `<Sp00kyProvider>`.
|
|
161
|
+
*/
|
|
162
|
+
function useSyncStatus() {
|
|
163
|
+
const db = useDb();
|
|
164
|
+
const [health, setHealth] = (0, solid_js.createSignal)(db.syncHealth);
|
|
165
|
+
(0, solid_js.onCleanup)(db.subscribeToSyncHealth(setHealth));
|
|
166
|
+
return {
|
|
167
|
+
health,
|
|
168
|
+
status: () => health().status,
|
|
169
|
+
isHealthy: () => health().status === "healthy",
|
|
170
|
+
isDegraded: () => health().status === "degraded",
|
|
171
|
+
everConnected: () => health().everConnected,
|
|
172
|
+
isOffline: () => health().status === "degraded" && health().everConnected
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/lib/use-crdt-field.ts
|
|
178
|
+
function useCrdtField(table, recordId, field, fallbackText) {
|
|
179
|
+
const db = (0, solid_js.useContext)(Sp00kyContext);
|
|
180
|
+
if (!db) throw new Error("useCrdtField must be used within a <Sp00kyProvider>");
|
|
181
|
+
const [crdtField, setCrdtField] = (0, solid_js.createSignal)(null);
|
|
182
|
+
let currentId;
|
|
183
|
+
let initialized = false;
|
|
184
|
+
(0, solid_js.createEffect)(() => {
|
|
185
|
+
const id = recordId();
|
|
186
|
+
if (initialized && id === currentId) return;
|
|
187
|
+
if (currentId && crdtField()) {
|
|
188
|
+
db.getSp00ky().closeCrdtField(table, currentId, field);
|
|
189
|
+
setCrdtField(null);
|
|
190
|
+
}
|
|
191
|
+
currentId = id;
|
|
192
|
+
initialized = true;
|
|
193
|
+
if (!id) return;
|
|
194
|
+
const sp00ky = db.getSp00ky();
|
|
195
|
+
const text = fallbackText?.();
|
|
196
|
+
sp00ky.openCrdtField(table, id, field, text).then((cf) => {
|
|
197
|
+
if (currentId === id) setCrdtField(cf);
|
|
198
|
+
}).catch((err) => {
|
|
199
|
+
console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
(0, solid_js.onCleanup)(() => {
|
|
203
|
+
if (currentId && crdtField()) {
|
|
204
|
+
db.getSp00ky().closeCrdtField(table, currentId, field);
|
|
205
|
+
setCrdtField(null);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
return crdtField;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/lib/use-feature-flag.ts
|
|
213
|
+
/**
|
|
214
|
+
* Subscribe to a feature flag for the currently authenticated user.
|
|
215
|
+
*
|
|
216
|
+
* Returns three Solid accessors that update reactively whenever the
|
|
217
|
+
* server-materialized assignment in `_00_user_feature` changes. Backed by
|
|
218
|
+
* the same SSP + sync pipeline that powers `useQuery`, so toggling a flag
|
|
219
|
+
* via `spky flag enable <key>` propagates to the UI without a refresh.
|
|
220
|
+
*
|
|
221
|
+
* `enabled()` is `true` when the resolved variant exists and is not 'off'.
|
|
222
|
+
* For multi-variant flags, prefer `variant()` directly.
|
|
223
|
+
*/
|
|
224
|
+
function useFeatureFlag(key, options) {
|
|
225
|
+
const handle = useDb().getSp00ky().feature(key, options);
|
|
226
|
+
const [variant, setVariant] = (0, solid_js.createSignal)(handle.variant());
|
|
227
|
+
const [payload, setPayload] = (0, solid_js.createSignal)(handle.payload());
|
|
228
|
+
const unsub = handle.subscribe((s) => {
|
|
229
|
+
setVariant(s.variant ?? options?.fallback);
|
|
230
|
+
setPayload(s.payload);
|
|
231
|
+
});
|
|
232
|
+
(0, solid_js.onCleanup)(() => {
|
|
233
|
+
unsub();
|
|
234
|
+
handle.close();
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
variant,
|
|
238
|
+
payload,
|
|
239
|
+
enabled: () => {
|
|
240
|
+
const v = variant();
|
|
241
|
+
return v !== void 0 && v !== "off";
|
|
242
|
+
}
|
|
73
243
|
};
|
|
74
244
|
}
|
|
75
245
|
|
|
@@ -95,7 +265,7 @@ function useFileUpload(dbOrBucketName, maybeBucketName) {
|
|
|
95
265
|
const validate = (file) => {
|
|
96
266
|
const config = db.getBucketConfig(bucketName);
|
|
97
267
|
if (!config) return;
|
|
98
|
-
if (config.maxSize
|
|
268
|
+
if (config.maxSize !== null && config.maxSize !== void 0 && file.size > config.maxSize) {
|
|
99
269
|
const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
|
|
100
270
|
throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
|
|
101
271
|
}
|
|
@@ -204,9 +374,8 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
|
|
|
204
374
|
const [error, setError] = (0, solid_js.createSignal)(null);
|
|
205
375
|
let currentKey = null;
|
|
206
376
|
let privateUrl = null;
|
|
207
|
-
let refetchTrigger;
|
|
208
377
|
const [refetchSignal, setRefetchSignal] = (0, solid_js.createSignal)(0);
|
|
209
|
-
refetchTrigger = () => setRefetchSignal((n) => n + 1);
|
|
378
|
+
const refetchTrigger = () => setRefetchSignal((n) => n + 1);
|
|
210
379
|
async function doDownload(key, filePath) {
|
|
211
380
|
if (useCache) {
|
|
212
381
|
const cached = downloadCache.get(key);
|
|
@@ -326,26 +495,31 @@ function useDownloadFile(dbOrBucketName, bucketNameOrPath, pathOrOptions, maybeO
|
|
|
326
495
|
}
|
|
327
496
|
|
|
328
497
|
//#endregion
|
|
329
|
-
//#region src/lib/
|
|
330
|
-
function
|
|
498
|
+
//#region src/lib/Sp00kyProvider.ts
|
|
499
|
+
function Sp00kyProvider(props) {
|
|
331
500
|
const merged = (0, solid_js.mergeProps)({ fallback: void 0 }, props);
|
|
332
501
|
const [db, setDb] = (0, solid_js.createSignal)(void 0);
|
|
333
502
|
(0, solid_js.onMount)(async () => {
|
|
334
503
|
try {
|
|
335
504
|
const instance = new SyncedDb(merged.config);
|
|
336
505
|
await instance.init();
|
|
506
|
+
if (merged.preload) try {
|
|
507
|
+
await merged.preload(instance);
|
|
508
|
+
} catch (e) {
|
|
509
|
+
console.error("Sp00kyProvider: preload failed; revealing UI anyway", e);
|
|
510
|
+
}
|
|
337
511
|
setDb(() => instance);
|
|
338
512
|
merged.onReady?.(instance);
|
|
339
513
|
} catch (e) {
|
|
340
514
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
341
515
|
if (merged.onError) merged.onError(error);
|
|
342
|
-
else console.error("
|
|
516
|
+
else console.error("Sp00kyProvider: Failed to initialize database", error);
|
|
343
517
|
}
|
|
344
518
|
});
|
|
345
519
|
return (0, solid_js.createMemo)(() => {
|
|
346
520
|
const instance = db();
|
|
347
521
|
if (!instance) return merged.fallback;
|
|
348
|
-
return (0, solid_js.createComponent)(
|
|
522
|
+
return (0, solid_js.createComponent)(Sp00kyContext.Provider, {
|
|
349
523
|
value: instance,
|
|
350
524
|
get children() {
|
|
351
525
|
return merged.children;
|
|
@@ -357,69 +531,82 @@ function SpookyProvider(props) {
|
|
|
357
531
|
//#endregion
|
|
358
532
|
//#region src/index.ts
|
|
359
533
|
/**
|
|
360
|
-
* SyncedDb - A thin wrapper around
|
|
361
|
-
* Delegates all logic to the underlying
|
|
534
|
+
* SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
|
|
535
|
+
* Delegates all logic to the underlying sp00ky-ts instance
|
|
362
536
|
*/
|
|
363
537
|
var SyncedDb = class {
|
|
364
538
|
constructor(config) {
|
|
365
|
-
this.
|
|
539
|
+
this.sp00ky = null;
|
|
366
540
|
this._initialized = false;
|
|
367
541
|
this.config = config;
|
|
368
542
|
}
|
|
369
|
-
|
|
370
|
-
if (!this.
|
|
371
|
-
return this.
|
|
543
|
+
getSp00ky() {
|
|
544
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
545
|
+
return this.sp00ky;
|
|
372
546
|
}
|
|
373
547
|
/**
|
|
374
|
-
* Initialize the
|
|
548
|
+
* Initialize the sp00ky-ts instance
|
|
375
549
|
*/
|
|
376
550
|
async init() {
|
|
377
551
|
if (this._initialized) return;
|
|
378
|
-
this.
|
|
379
|
-
await this.
|
|
552
|
+
this.sp00ky = new _spooky_sync_core.Sp00kyClient(this.config);
|
|
553
|
+
await this.sp00ky.init();
|
|
380
554
|
this._initialized = true;
|
|
381
555
|
}
|
|
382
556
|
/**
|
|
383
557
|
* Create a new record in the database
|
|
384
558
|
*/
|
|
385
559
|
async create(id, payload) {
|
|
386
|
-
if (!this.
|
|
387
|
-
await this.
|
|
560
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
561
|
+
await this.sp00ky.create(id, payload);
|
|
388
562
|
}
|
|
389
563
|
/**
|
|
390
564
|
* Update an existing record in the database
|
|
391
565
|
*/
|
|
392
566
|
async update(tableName, recordId, payload, options) {
|
|
393
|
-
if (!this.
|
|
394
|
-
await this.
|
|
567
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
568
|
+
await this.sp00ky.update(tableName, recordId, payload, options);
|
|
395
569
|
}
|
|
396
570
|
/**
|
|
397
571
|
* Delete an existing record in the database
|
|
398
572
|
*/
|
|
399
573
|
async delete(tableName, selector) {
|
|
400
|
-
if (!this.
|
|
401
|
-
|
|
402
|
-
|
|
574
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
575
|
+
const isRecordId = selector instanceof surrealdb.RecordId || selector?.constructor?.name === "RecordId";
|
|
576
|
+
let id;
|
|
577
|
+
if (typeof selector === "string") id = selector;
|
|
578
|
+
else if (isRecordId) id = `${tableName}:${selector.id}`;
|
|
579
|
+
else throw new Error("Only string ID or RecordId selectors are supported currently with core");
|
|
580
|
+
await this.sp00ky.delete(tableName, id);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Preload/prewarm a built query into the local cache without registering a
|
|
584
|
+
* live view. Fetches once and stores the rows (+ embedded related children)
|
|
585
|
+
* locally so a later `useQuery` for the same data paints instantly. Best-effort.
|
|
586
|
+
*/
|
|
587
|
+
async preload(finalQuery, options) {
|
|
588
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
589
|
+
await this.sp00ky.preload(finalQuery, options);
|
|
403
590
|
}
|
|
404
591
|
/**
|
|
405
592
|
* Query data from the database
|
|
406
593
|
*/
|
|
407
594
|
query(table) {
|
|
408
|
-
if (!this.
|
|
409
|
-
return this.
|
|
595
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
596
|
+
return this.sp00ky.query(table, {});
|
|
410
597
|
}
|
|
411
598
|
/**
|
|
412
599
|
* Run a backend operation
|
|
413
600
|
*/
|
|
414
601
|
async run(backend, path, payload, options) {
|
|
415
|
-
if (!this.
|
|
416
|
-
await this.
|
|
602
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
603
|
+
await this.sp00ky.run(backend, path, payload, options);
|
|
417
604
|
}
|
|
418
605
|
/**
|
|
419
606
|
* Authenticate with the database
|
|
420
607
|
*/
|
|
421
608
|
async authenticate(token) {
|
|
422
|
-
await this.
|
|
609
|
+
await this.sp00ky?.authenticate(token);
|
|
423
610
|
return new surrealdb.RecordId("user", "me");
|
|
424
611
|
}
|
|
425
612
|
/**
|
|
@@ -433,48 +620,67 @@ var SyncedDb = class {
|
|
|
433
620
|
* Sign out, clear session and local storage
|
|
434
621
|
*/
|
|
435
622
|
async signOut() {
|
|
436
|
-
if (!this.
|
|
437
|
-
await this.
|
|
623
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
624
|
+
await this.sp00ky.auth.signOut();
|
|
438
625
|
}
|
|
439
626
|
/**
|
|
440
627
|
* Execute a function with direct access to the remote database connection
|
|
441
628
|
*/
|
|
442
629
|
async useRemote(fn) {
|
|
443
|
-
if (!this.
|
|
444
|
-
return await this.
|
|
630
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
631
|
+
return await this.sp00ky.useRemote(fn);
|
|
445
632
|
}
|
|
446
633
|
/**
|
|
447
634
|
* Access the remote database service directly
|
|
448
635
|
*/
|
|
449
636
|
get remote() {
|
|
450
|
-
if (!this.
|
|
451
|
-
return this.
|
|
637
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
638
|
+
return this.sp00ky.remoteClient;
|
|
452
639
|
}
|
|
453
640
|
/**
|
|
454
641
|
* Access the local database service directly
|
|
455
642
|
*/
|
|
456
643
|
get local() {
|
|
457
|
-
if (!this.
|
|
458
|
-
return this.
|
|
644
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
645
|
+
return this.sp00ky.localClient;
|
|
459
646
|
}
|
|
460
647
|
/**
|
|
461
648
|
* Access the auth service
|
|
462
649
|
*/
|
|
463
650
|
get auth() {
|
|
464
|
-
if (!this.
|
|
465
|
-
return this.
|
|
651
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
652
|
+
return this.sp00ky.auth;
|
|
466
653
|
}
|
|
467
654
|
get pendingMutationCount() {
|
|
468
|
-
if (!this.
|
|
469
|
-
return this.
|
|
655
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
656
|
+
return this.sp00ky.pendingMutationCount;
|
|
657
|
+
}
|
|
658
|
+
/** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
|
|
659
|
+
get liveRetryCount() {
|
|
660
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
661
|
+
return this.sp00ky.liveRetryCount;
|
|
470
662
|
}
|
|
471
663
|
subscribeToPendingMutations(cb) {
|
|
472
|
-
if (!this.
|
|
473
|
-
return this.
|
|
664
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
665
|
+
return this.sp00ky.subscribeToPendingMutations(cb);
|
|
666
|
+
}
|
|
667
|
+
/** Current sync-health snapshot. See {@link useSyncStatus}. */
|
|
668
|
+
get syncHealth() {
|
|
669
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
670
|
+
return this.sp00ky.syncHealth;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Observe sync health. Fires immediately with the current status and again
|
|
674
|
+
* on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
|
|
675
|
+
* components; this is the imperative escape hatch.
|
|
676
|
+
*/
|
|
677
|
+
subscribeToSyncHealth(cb) {
|
|
678
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
679
|
+
return this.sp00ky.subscribeToSyncHealth(cb);
|
|
474
680
|
}
|
|
475
681
|
bucket(name) {
|
|
476
|
-
if (!this.
|
|
477
|
-
return this.
|
|
682
|
+
if (!this.sp00ky) throw new Error("SyncedDb not initialized");
|
|
683
|
+
return this.sp00ky.bucket(name);
|
|
478
684
|
}
|
|
479
685
|
getBucketConfig(name) {
|
|
480
686
|
return this.config.schema.buckets?.find((b) => b.name === name);
|
|
@@ -483,11 +689,15 @@ var SyncedDb = class {
|
|
|
483
689
|
|
|
484
690
|
//#endregion
|
|
485
691
|
exports.RecordId = surrealdb.RecordId;
|
|
486
|
-
exports.
|
|
692
|
+
exports.Sp00kyProvider = Sp00kyProvider;
|
|
487
693
|
exports.SyncedDb = SyncedDb;
|
|
488
694
|
exports.Uuid = surrealdb.Uuid;
|
|
695
|
+
exports.createPreload = createPreload;
|
|
696
|
+
exports.useCrdtField = useCrdtField;
|
|
489
697
|
exports.useDb = useDb;
|
|
490
698
|
exports.useDownloadFile = useDownloadFile;
|
|
699
|
+
exports.useFeatureFlag = useFeatureFlag;
|
|
491
700
|
exports.useFileUpload = useFileUpload;
|
|
492
701
|
exports.useQuery = useQuery;
|
|
702
|
+
exports.useSyncStatus = useSyncStatus;
|
|
493
703
|
//# sourceMappingURL=index.cjs.map
|