@spooky-sync/client-solid2 0.0.1-canary.240 → 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.
@@ -85,15 +85,15 @@ describe('useSyncActivity', () => {
85
85
  });
86
86
  });
87
87
 
88
- it('isUploading needs more than the threshold of queued writes', async () => {
88
+ it('turns isUploading on after the delay for any queued write, and off at once', async () => {
89
89
  const m = mockDb(0, 1);
90
90
  await createRoot(async (dispose) => {
91
- const a = useSyncActivity(m.db);
91
+ const a = useSyncActivity(m.db, { uploadDelayMs: 150 });
92
92
  await settle();
93
93
  expect(a.pendingMutations()).toBe(1);
94
94
  expect(a.isUploading()).toBe(false);
95
- m.setPending(2);
96
- await settle();
95
+ await vi.advanceTimersByTimeAsync(200);
96
+ flush();
97
97
  expect(a.isUploading()).toBe(true);
98
98
  m.setPending(0);
99
99
  await settle();
@@ -101,4 +101,34 @@ describe('useSyncActivity', () => {
101
101
  dispose();
102
102
  });
103
103
  });
104
+
105
+ it('a write acknowledged inside the delay never shows', async () => {
106
+ const m = mockDb(0, 0);
107
+ await createRoot(async (dispose) => {
108
+ const a = useSyncActivity(m.db, { uploadDelayMs: 150 });
109
+ await settle();
110
+ m.setPending(1);
111
+ await settle();
112
+ await vi.advanceTimersByTimeAsync(80);
113
+ m.setPending(0);
114
+ await settle();
115
+ await vi.advanceTimersByTimeAsync(300);
116
+ flush();
117
+ expect(a.isUploading()).toBe(false);
118
+ dispose();
119
+ });
120
+ });
121
+
122
+ it('uploadThreshold holds the mark back until a backlog builds', async () => {
123
+ const m = mockDb(0, 1);
124
+ await createRoot(async (dispose) => {
125
+ const a = useSyncActivity(m.db, { uploadThreshold: 1, uploadDelayMs: 0 });
126
+ await settle();
127
+ expect(a.isUploading()).toBe(false);
128
+ m.setPending(2);
129
+ await settle();
130
+ expect(a.isUploading()).toBe(true);
131
+ dispose();
132
+ });
133
+ });
104
134
  });
@@ -58,8 +58,9 @@ export function createPreload<
58
58
  /**
59
59
  * Reactive, fire-and-forget prewarm. Resolves the query (calling it if it's a
60
60
  * function so it tracks reactive deps), dedupes on the query's stable identity
61
- * hash, and warms it into the local cache via `db.preload`. No subscription and
62
- * no cleanup: preload registers nothing that needs tearing down.
61
+ * hash, and registers it via `db.preload`: a live query nobody subscribes to,
62
+ * evicted a ttl after it was registered unless a view mounts it first. Nothing
63
+ * to clean up here.
63
64
  *
64
65
  * Typical use: inside a list row, preload the detail query the user is likely
65
66
  * to open next, so navigation paints from cache instead of the network.
@@ -109,7 +110,7 @@ export function createPreload<
109
110
  if (!query) return;
110
111
  void db
111
112
  .getSp00ky()
112
- .preload(query, { refresh: options?.refresh, staleTime: options?.staleTime });
113
+ .preload(query, { signal: options?.signal });
113
114
  }
114
115
  );
115
116
  }
@@ -13,10 +13,16 @@ export interface UseSyncActivityOptions {
13
13
  downloadDelayMs?: number;
14
14
  /**
15
15
  * `isUploading()` turns on once MORE than this many mutations are waiting in
16
- * the outbox. Default 1: a single write acknowledged within a round trip is
17
- * not worth an animation; a backlog is.
16
+ * the outbox. Default 0: any unacknowledged write is upsync worth showing.
18
17
  */
19
18
  uploadThreshold?: number;
19
+ /**
20
+ * How long the outbox must stay above `uploadThreshold` before
21
+ * `isUploading()` turns on. The mirror of `downloadDelayMs`: a write the
22
+ * server acknowledges within a round trip never lights the indicator, a
23
+ * write that is actually waiting does. Default 150 ms.
24
+ */
25
+ uploadDelayMs?: number;
20
26
  }
21
27
 
22
28
  export interface UseSyncActivity {
@@ -26,7 +32,10 @@ export interface UseSyncActivity {
26
32
  pendingMutations: Accessor<number>;
27
33
  /** Fetching for longer than `downloadDelayMs`. Drives a "downloading" mark. */
28
34
  isDownloading: Accessor<boolean>;
29
- /** More than `uploadThreshold` writes queued. Drives an "uploading" mark. */
35
+ /**
36
+ * More than `uploadThreshold` writes have been queued for longer than
37
+ * `uploadDelayMs`. Drives an "uploading" mark.
38
+ */
30
39
  isUploading: Accessor<boolean>;
31
40
  }
32
41
 
@@ -34,10 +43,10 @@ export interface UseSyncActivity {
34
43
  * The two directions of sync traffic, for an indicator in the app chrome.
35
44
  *
36
45
  * `fetchingQueries` is one subscription on the engine's aggregate fetch count,
37
- * not one per query; `pendingMutations` is the outbox depth. `isDownloading`
38
- * is the fetch count debounced ON by `downloadDelayMs` (and off at once), so a
39
- * local-first page that answers from cache and confirms with the server in a
40
- * few milliseconds never flickers. Must be used within a `<Sp00kyProvider>`, or
46
+ * not one per query; `pendingMutations` is the outbox depth. Both
47
+ * `isDownloading` and `isUploading` are debounced ON by their delay (and off at
48
+ * once), so a local-first page that answers from cache and confirms with the
49
+ * server in a few milliseconds never flickers. Must be used within a `<Sp00kyProvider>`, or
41
50
  * pass the `SyncedDb` explicitly.
42
51
  */
43
52
  export function useSyncActivity<S extends SchemaStructure = any>(
@@ -51,7 +60,8 @@ export function useSyncActivity<S extends SchemaStructure = any>(
51
60
  const options = (explicitDb ? maybeOptions : (dbOrOptions as UseSyncActivityOptions)) ?? {};
52
61
  const db = explicitDb ?? useDb<S>();
53
62
  const downloadDelayMs = options.downloadDelayMs ?? 200;
54
- const uploadThreshold = options.uploadThreshold ?? 1;
63
+ const uploadThreshold = options.uploadThreshold ?? 0;
64
+ const uploadDelayMs = options.uploadDelayMs ?? 150;
55
65
 
56
66
  const fetchingQueries = fromSubscription<number>(
57
67
  (cb) => db.subscribeToFetchActivity(cb),
@@ -62,38 +72,41 @@ export function useSyncActivity<S extends SchemaStructure = any>(
62
72
  db.pendingMutationCount
63
73
  );
64
74
 
65
- // ON after the delay, OFF immediately. Written from a timer, hence ownedWrite.
66
- const [isDownloading, setIsDownloading] = createSignal(false, { ownedWrite: true });
75
+ const isDownloading = delayedOn(() => fetchingQueries() > 0, downloadDelayMs);
76
+ const isUploading = delayedOn(() => pendingMutations() > uploadThreshold, uploadDelayMs);
77
+
78
+ return { fetchingQueries, pendingMutations, isDownloading, isUploading };
79
+ }
80
+
81
+ /**
82
+ * `busy` held for `delayMs`, dropped the instant it clears. Both directions of
83
+ * traffic use it, so a burst too short to see never flickers the indicator and
84
+ * neither one can latch on after the work is done.
85
+ */
86
+ function delayedOn(busy: Accessor<boolean>, delayMs: number): Accessor<boolean> {
87
+ // Written from a timer, hence ownedWrite.
88
+ const [on, setOn] = createSignal(false, { ownedWrite: true });
67
89
  let timer: ReturnType<typeof setTimeout> | undefined;
68
- createEffect(
69
- () => fetchingQueries() > 0,
70
- (busy) => {
71
- if (timer !== undefined) {
72
- clearTimeout(timer);
73
- timer = undefined;
74
- }
75
- if (!busy) {
76
- setIsDownloading(false);
77
- return;
78
- }
79
- if (downloadDelayMs <= 0) {
80
- setIsDownloading(true);
81
- return;
82
- }
83
- timer = setTimeout(() => {
84
- timer = undefined;
85
- setIsDownloading(true);
86
- }, downloadDelayMs);
90
+ createEffect(busy, (isBusy) => {
91
+ if (timer !== undefined) {
92
+ clearTimeout(timer);
93
+ timer = undefined;
87
94
  }
88
- );
95
+ if (!isBusy) {
96
+ setOn(false);
97
+ return;
98
+ }
99
+ if (delayMs <= 0) {
100
+ setOn(true);
101
+ return;
102
+ }
103
+ timer = setTimeout(() => {
104
+ timer = undefined;
105
+ setOn(true);
106
+ }, delayMs);
107
+ });
89
108
  onCleanup(() => {
90
109
  if (timer !== undefined) clearTimeout(timer);
91
110
  });
92
-
93
- return {
94
- fetchingQueries,
95
- pendingMutations,
96
- isDownloading,
97
- isUploading: () => pendingMutations() > uploadThreshold,
98
- };
111
+ return on;
99
112
  }