@spooky-sync/client-solid2 0.0.1-canary.236 → 0.0.1-canary.238
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 +4 -1
- package/QUICK_START.md +5 -2
- package/README.md +1 -1
- package/dist/index.cjs +23 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +26 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -10
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/skills/sp00ky-solid2/SKILL.md +5 -2
- package/src/lib/__tests__/create-query.test.ts +117 -4
- package/src/lib/create-query.ts +73 -18
package/AGENTS.md
CHANGED
|
@@ -39,7 +39,10 @@ Provider props are identical to client-solid: `config`, `fallback`, `preload` (a
|
|
|
39
39
|
- `data()` — non-suspending. `[]` / `null` before the first result, keyed-reconciled live rows after.
|
|
40
40
|
- `ready()` — suspending read for `<Loading fallback={...}>` (Solid 2's renamed `<Suspense>`).
|
|
41
41
|
- `error()` — registration/sync failure (e.g. SSP 503 NOT_READY). Never thrown into the render tree; the sync scheduler retries underneath.
|
|
42
|
-
- `isLoading()`
|
|
42
|
+
- `isLoading()` — cold first load only (nothing painted for this identity, server not heard, no error). A cached paint ends it; an authoritative empty answer ends it. Never true again for the same identity.
|
|
43
|
+
- `isFetching()` — a fetch cycle is in flight (registration, a sync round).
|
|
44
|
+
- `isAuthoritative()` — server membership is known for this identity (latched until it changes). `hasData()` — `data()` holds something. `isEmpty()` — authoritative AND no data: the server said "no rows"; render empty states on this, not on `!hasData()`.
|
|
45
|
+
- `isSettled()` — authoritative AND idle; gate windowed-list end detection on it. Not monotonic; latch it (a `createMemo(prev => prev || q.isSettled())`) for a verdict that must stick.
|
|
43
46
|
- Options: `enabled?: () => boolean`, `deregisterOnCleanup?: boolean`.
|
|
44
47
|
- **`createSubmission(fn)`** — button pending/error state around a mutation: `submit()`, `pending()`, `error()`, `result()`, `clearError()`. There is deliberately no `action()` / `createOptimisticStore` layer (see `src/lib/create-submission.ts`).
|
|
45
48
|
- **`createPreload`**, **`usePendingMutations`**, **`useSyncStatus`**, **`useStorageStatus`**, **`useFeatureFlag`**, **`useAppRelease`**, **`useCrdtField`**, **`useFileUpload`**, **`useDownloadFile`** — same shapes as client-solid.
|
package/QUICK_START.md
CHANGED
|
@@ -81,9 +81,12 @@ import { Loading } from 'solid-js';
|
|
|
81
81
|
| `data()` | Rows (or row/`null` for `.one()`). Never suspends, never throws. Keyed-reconciled in place: unchanged rows keep identity, `<For>` is notified on add/remove/reorder. |
|
|
82
82
|
| `ready()` | Same data, suspends into the nearest `<Loading>` until first result or error. |
|
|
83
83
|
| `error()` | Registration/sync error (e.g. SSP 503 during bootstrap). Never thrown into the render tree; the sync scheduler retries underneath. |
|
|
84
|
-
| `isLoading()` |
|
|
84
|
+
| `isLoading()` | Cold first load only: nothing painted for this identity yet, server not heard, no error. A cached paint ends it; so does the server answering with zero rows. |
|
|
85
85
|
| `isFetching()` | Sync engine is pulling records for this query. |
|
|
86
|
-
| `
|
|
86
|
+
| `isAuthoritative()` | Server membership is known for this identity (registration/poll landed, or a previous session's membership was read on boot). Latched until the identity changes. |
|
|
87
|
+
| `hasData()` | `data()` holds rows (or the `.one()` row). |
|
|
88
|
+
| `isEmpty()` | Authoritative AND no data — the server said "no rows". Render empty states on this. |
|
|
89
|
+
| `isSettled()` | Authoritative AND idle — windowed lists may trust a short result as the true end of the list. Re-enters on later sync rounds; latch it for a sticky verdict. |
|
|
87
90
|
|
|
88
91
|
Options: `{ enabled?: () => boolean, deregisterOnCleanup?: boolean }` — both as in client-solid.
|
|
89
92
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Counterpart of `@spooky-sync/client-solid` (Solid 1.x); the two coexist until So
|
|
|
7
7
|
What "native" means here:
|
|
8
8
|
|
|
9
9
|
- Query results are a plain store written from the engine's live subscription and merged keyed by `id`: row identity preserved, coarse `<For>` readers notified, only changed fields written. Not an async-generator projection - see `create-query.ts` for why a never-returning generator breaks navigation transitions.
|
|
10
|
-
- `createQuery` exposes both worlds: non-suspending accessors (`data`, `isLoading`, `isFetching`, `isSettled`, `error`) and a suspending `ready()` for `<Loading>` boundaries. Born committed, so local-first cache paints never suspend.
|
|
10
|
+
- `createQuery` exposes both worlds: non-suspending accessors (`data`, `isLoading`, `isFetching`, `isAuthoritative`, `hasData`, `isEmpty`, `isSettled`, `error`) and a suspending `ready()` for `<Loading>` boundaries. Born committed, so local-first cache paints never suspend. `isLoading()` is true only on a cold first load (nothing cached, server not heard yet); `isEmpty()` is the server saying "no rows".
|
|
11
11
|
- Status hooks (`useSyncStatus`, `useStorageStatus`, `usePendingMutations`, feature flags, app release) are async-iterable-backed memos with `loadingValue`.
|
|
12
12
|
- Mutations stay plain async calls — the engine is already optimistic local-first end to end (local commit → live re-emit → outbox sync). `createSubmission` adds button pending/error state.
|
|
13
13
|
|
package/dist/index.cjs
CHANGED
|
@@ -197,7 +197,8 @@ function createQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
197
197
|
}
|
|
198
198
|
const sp00ky = db.getSp00ky();
|
|
199
199
|
const [error, setError] = (0, solid_js.createSignal)(void 0, { ownedWrite: true });
|
|
200
|
-
const [
|
|
200
|
+
const [isAuthoritative, setIsAuthoritative] = (0, solid_js.createSignal)(false, { ownedWrite: true });
|
|
201
|
+
const [hasPainted, setHasPainted] = (0, solid_js.createSignal)(false, { ownedWrite: true });
|
|
201
202
|
const [isFetching, setIsFetching] = (0, solid_js.createSignal)(false, { ownedWrite: true });
|
|
202
203
|
let activeHash;
|
|
203
204
|
const [store, setStore] = (0, solid_js.createStore)({ value: null });
|
|
@@ -215,7 +216,8 @@ function createQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
215
216
|
};
|
|
216
217
|
}, ({ enabled, query }) => {
|
|
217
218
|
const myRun = ++runId;
|
|
218
|
-
|
|
219
|
+
setIsAuthoritative(false);
|
|
220
|
+
setHasPainted(false);
|
|
219
221
|
setError(void 0);
|
|
220
222
|
if (!enabled || !query) return;
|
|
221
223
|
const cleanups = [];
|
|
@@ -244,16 +246,19 @@ function createQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
244
246
|
if (disposed || myRun !== runId) return;
|
|
245
247
|
activeHash = hash;
|
|
246
248
|
addCleanup(sp00ky.subscribeQueryStatus(hash, (status) => setIsFetching(status === "fetching"), { immediate: true }));
|
|
247
|
-
|
|
249
|
+
addCleanup(sp00ky.subscribeQueryAuthority(hash, (known) => {
|
|
250
|
+
if (disposed || myRun !== runId) return;
|
|
251
|
+
setIsAuthoritative(known);
|
|
252
|
+
if (known) wakeReady();
|
|
253
|
+
else setHasPainted(false);
|
|
254
|
+
}, { immediate: true }));
|
|
248
255
|
addCleanup(sp00ky.subscribe(hash, (rows) => {
|
|
249
256
|
if (disposed || myRun !== runId) return;
|
|
250
257
|
const queryData = query.isOne ? rows[0] ?? null : rows;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
setIsFetched(true);
|
|
258
|
+
if (query.isOne ? queryData !== null && queryData !== void 0 : rows.length > 0) {
|
|
259
|
+
setHasPainted(true);
|
|
254
260
|
wakeReady();
|
|
255
261
|
}
|
|
256
|
-
isFirstCall = false;
|
|
257
262
|
const t0 = performance.now();
|
|
258
263
|
setStore((s) => {
|
|
259
264
|
if (query.isOne || queryData === null || !Array.isArray(s.value)) s.value = queryData;
|
|
@@ -281,7 +286,7 @@ function createQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
281
286
|
return v;
|
|
282
287
|
};
|
|
283
288
|
const readyGate = (0, solid_js.createMemo)(async () => {
|
|
284
|
-
if (
|
|
289
|
+
if (isAuthoritative() || hasPainted() || error()) return true;
|
|
285
290
|
await new Promise((resolve) => readyWaiters.push(resolve));
|
|
286
291
|
return true;
|
|
287
292
|
}, { lazy: true });
|
|
@@ -292,14 +297,22 @@ function createQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
|
292
297
|
(0, solid_js.onCleanup)(() => {
|
|
293
298
|
if (options?.deregisterOnCleanup && activeHash) sp00ky.deregisterQuery(activeHash);
|
|
294
299
|
});
|
|
295
|
-
const
|
|
296
|
-
|
|
300
|
+
const hasData = () => {
|
|
301
|
+
const v = store.value;
|
|
302
|
+
return Array.isArray(v) ? v.length > 0 : v !== null && v !== void 0;
|
|
303
|
+
};
|
|
304
|
+
const isLoading = () => !isAuthoritative() && !hasPainted() && error() === void 0;
|
|
305
|
+
const isEmpty = () => isAuthoritative() && !hasData();
|
|
306
|
+
const isSettled = () => isAuthoritative() && !isFetching();
|
|
297
307
|
return {
|
|
298
308
|
data,
|
|
299
309
|
ready,
|
|
300
310
|
error,
|
|
301
311
|
isLoading,
|
|
302
312
|
isFetching,
|
|
313
|
+
isAuthoritative,
|
|
314
|
+
hasData,
|
|
315
|
+
isEmpty,
|
|
303
316
|
isSettled
|
|
304
317
|
};
|
|
305
318
|
}
|