@spooky-sync/client-solid2 0.0.1-canary.239 → 0.0.1-canary.241

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
@@ -45,7 +45,7 @@ Provider props are identical to client-solid: `config`, `fallback`, `preload` (a
45
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.
46
46
  - Options: `enabled?: () => boolean`, `deregisterOnCleanup?: boolean`.
47
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`).
48
- - **`useSyncActivity(options?)`** — `{ fetchingQueries, pendingMutations, isDownloading, isUploading }`. One subscription on the engine's aggregate fetch count plus the outbox depth; `isDownloading()` is debounced ON by `downloadDelayMs` (200) and off at once, `isUploading()` is `pendingMutations() > uploadThreshold` (1). Drive a logo/traffic indicator off it; pair with `useSyncStatus().isOffline()` to show the unsaved count while disconnected.
48
+ - **`useSyncActivity(options?)`** — `{ fetchingQueries, pendingMutations, isDownloading, isUploading }`. One subscription on the engine's aggregate fetch count plus the outbox depth; both are debounced ON by their delay (`downloadDelayMs` 200, `uploadDelayMs` 150) and off at once, and `isUploading()` additionally needs `pendingMutations() > uploadThreshold` (0, so any unacknowledged write counts). Drive a logo/traffic indicator off it; pair with `useSyncStatus().connection()` / `.isOffline()` to switch the same mark to a reconnecting or offline state and show the unsaved count.
49
49
  - **`createPreload`**, **`usePendingMutations`**, **`useSyncStatus`**, **`useStorageStatus`**, **`useFeatureFlag`**, **`useAppRelease`**, **`useCrdtField`**, **`useFileUpload`**, **`useDownloadFile`** — same shapes as client-solid.
50
50
  - **`conflate`**, **`fromSubscription`** — the async-iterable helpers the hooks are built on; useful for wrapping other engine subscriptions.
51
51
 
package/QUICK_START.md CHANGED
@@ -116,7 +116,7 @@ Track a backend job's progress by querying its outbox row: `createQuery(() => db
116
116
 
117
117
  ## Status hooks
118
118
 
119
- Same shapes as client-solid, rebuilt on async-iterable-backed memos: `useSyncStatus`, `useStorageStatus`, `usePendingMutations`, `useSyncActivity` (new: `isDownloading()` after 200 ms of fetching, `isUploading()` above one queued write, for a traffic indicator), `useFeatureFlag`, `useAppRelease`, `useCrdtField`, `useFileUpload`, `useDownloadFile`, `createPreload`.
119
+ Same shapes as client-solid, rebuilt on async-iterable-backed memos: `useSyncStatus`, `useStorageStatus`, `usePendingMutations`, `useSyncActivity` (new: `isDownloading()` after 200 ms of fetching, `isUploading()` after 150 ms of a queued write, for a traffic indicator), `useFeatureFlag`, `useAppRelease`, `useCrdtField`, `useFileUpload`, `useDownloadFile`, `createPreload`.
120
120
 
121
121
  ## Migrating from client-solid
122
122
 
package/README.md CHANGED
@@ -8,7 +8,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
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
- - Status hooks (`useSyncStatus`, `useSyncActivity`, `useStorageStatus`, `usePendingMutations`, feature flags, app release) are async-iterable-backed memos with `loadingValue`. `useSyncActivity` is the traffic indicator pair: `isDownloading()` (queries fetching for longer than 200 ms) and `isUploading()` (more than one write queued), plus the raw `fetchingQueries()` / `pendingMutations()` counts.
11
+ - Status hooks (`useSyncStatus`, `useSyncActivity`, `useStorageStatus`, `usePendingMutations`, feature flags, app release) are async-iterable-backed memos with `loadingValue`. `useSyncActivity` is the traffic indicator pair: `isDownloading()` (queries fetching for longer than 200 ms) and `isUploading()` (writes queued for longer than 150 ms), plus the raw `fetchingQueries()` / `pendingMutations()` counts.
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
 
14
14
  See `QUICK_START.md` for usage and the migration table from client-solid, and `src/lib/__tests__/rc-semantics.test.ts` for the probed Solid 2 rc contracts this package depends on (run them after every Solid version bump).
package/dist/index.cjs CHANGED
@@ -324,8 +324,9 @@ const useQuery = createQuery;
324
324
  /**
325
325
  * Reactive, fire-and-forget prewarm. Resolves the query (calling it if it's a
326
326
  * function so it tracks reactive deps), dedupes on the query's stable identity
327
- * hash, and warms it into the local cache via `db.preload`. No subscription and
328
- * no cleanup: preload registers nothing that needs tearing down.
327
+ * hash, and registers it via `db.preload`: a live query nobody subscribes to,
328
+ * evicted a ttl after it was registered unless a view mounts it first. Nothing
329
+ * to clean up here.
329
330
  *
330
331
  * Typical use: inside a list row, preload the detail query the user is likely
331
332
  * to open next, so navigation paints from cache instead of the network.
@@ -353,10 +354,7 @@ function createPreload(dbOrQuery, queryOrOptions, maybeOptions) {
353
354
  return query;
354
355
  }, (query) => {
355
356
  if (!query) return;
356
- db.getSp00ky().preload(query, {
357
- refresh: options?.refresh,
358
- staleTime: options?.staleTime
359
- });
357
+ db.getSp00ky().preload(query, { signal: options?.signal });
360
358
  });
361
359
  }
362
360
 
@@ -393,10 +391,10 @@ function useSyncStatus() {
393
391
  * The two directions of sync traffic, for an indicator in the app chrome.
394
392
  *
395
393
  * `fetchingQueries` is one subscription on the engine's aggregate fetch count,
396
- * not one per query; `pendingMutations` is the outbox depth. `isDownloading`
397
- * is the fetch count debounced ON by `downloadDelayMs` (and off at once), so a
398
- * local-first page that answers from cache and confirms with the server in a
399
- * few milliseconds never flickers. Must be used within a `<Sp00kyProvider>`, or
394
+ * not one per query; `pendingMutations` is the outbox depth. Both
395
+ * `isDownloading` and `isUploading` are debounced ON by their delay (and off at
396
+ * once), so a local-first page that answers from cache and confirms with the
397
+ * server in a few milliseconds never flickers. Must be used within a `<Sp00kyProvider>`, or
400
398
  * pass the `SyncedDb` explicitly.
401
399
  */
402
400
  function useSyncActivity(dbOrOptions, maybeOptions) {
@@ -404,38 +402,47 @@ function useSyncActivity(dbOrOptions, maybeOptions) {
404
402
  const options = (explicitDb ? maybeOptions : dbOrOptions) ?? {};
405
403
  const db = explicitDb ?? useDb();
406
404
  const downloadDelayMs = options.downloadDelayMs ?? 200;
407
- const uploadThreshold = options.uploadThreshold ?? 1;
405
+ const uploadThreshold = options.uploadThreshold ?? 0;
406
+ const uploadDelayMs = options.uploadDelayMs ?? 150;
408
407
  const fetchingQueries = fromSubscription((cb) => db.subscribeToFetchActivity(cb), db.fetchingQueryCount);
409
408
  const pendingMutations = fromSubscription((cb) => db.subscribeToPendingMutations(cb), db.pendingMutationCount);
410
- const [isDownloading, setIsDownloading] = (0, solid_js.createSignal)(false, { ownedWrite: true });
409
+ return {
410
+ fetchingQueries,
411
+ pendingMutations,
412
+ isDownloading: delayedOn(() => fetchingQueries() > 0, downloadDelayMs),
413
+ isUploading: delayedOn(() => pendingMutations() > uploadThreshold, uploadDelayMs)
414
+ };
415
+ }
416
+ /**
417
+ * `busy` held for `delayMs`, dropped the instant it clears. Both directions of
418
+ * traffic use it, so a burst too short to see never flickers the indicator and
419
+ * neither one can latch on after the work is done.
420
+ */
421
+ function delayedOn(busy, delayMs) {
422
+ const [on, setOn] = (0, solid_js.createSignal)(false, { ownedWrite: true });
411
423
  let timer;
412
- (0, solid_js.createEffect)(() => fetchingQueries() > 0, (busy) => {
424
+ (0, solid_js.createEffect)(busy, (isBusy) => {
413
425
  if (timer !== void 0) {
414
426
  clearTimeout(timer);
415
427
  timer = void 0;
416
428
  }
417
- if (!busy) {
418
- setIsDownloading(false);
429
+ if (!isBusy) {
430
+ setOn(false);
419
431
  return;
420
432
  }
421
- if (downloadDelayMs <= 0) {
422
- setIsDownloading(true);
433
+ if (delayMs <= 0) {
434
+ setOn(true);
423
435
  return;
424
436
  }
425
437
  timer = setTimeout(() => {
426
438
  timer = void 0;
427
- setIsDownloading(true);
428
- }, downloadDelayMs);
439
+ setOn(true);
440
+ }, delayMs);
429
441
  });
430
442
  (0, solid_js.onCleanup)(() => {
431
443
  if (timer !== void 0) clearTimeout(timer);
432
444
  });
433
- return {
434
- fetchingQueries,
435
- pendingMutations,
436
- isDownloading,
437
- isUploading: () => pendingMutations() > uploadThreshold
438
- };
445
+ return on;
439
446
  }
440
447
 
441
448
  //#endregion