@spooky-sync/client-solid2 0.0.1-canary.237 → 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 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()` / `isFetching()` / `isSettled()` — `isSettled()` means delivered AND idle; gate windowed-list end detection on it.
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()` | No result yet and no error. |
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
- | `isSettled()` | Delivered AND idle — windowed lists may trust a short result as the true end of the list. |
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 [isFetched, setIsFetched] = (0, solid_js.createSignal)(false, { ownedWrite: true });
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
- setIsFetched(false);
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
- let isFirstCall = true;
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
- const hasData = query.isOne ? queryData !== null && queryData !== void 0 : rows.length > 0;
252
- if (!isFirstCall || hasData) {
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 (isFetched() || error()) return true;
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 isLoading = () => !isFetched() && error() === void 0;
296
- const isSettled = () => isFetched() && !isFetching();
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
  }