@spooky-sync/client-solid2 0.0.1-canary.240 → 0.0.1-canary.242
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 +1 -1
- package/QUICK_START.md +1 -1
- package/README.md +1 -1
- package/dist/index.cjs +32 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +16 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -25
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/lib/__tests__/use-sync-activity.test.ts +34 -4
- package/src/lib/create-preload.ts +4 -3
- package/src/lib/use-sync-activity.ts +50 -37
|
@@ -85,15 +85,15 @@ describe('useSyncActivity', () => {
|
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
it('isUploading
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
62
|
-
*
|
|
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, {
|
|
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
|
|
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
|
-
/**
|
|
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.
|
|
38
|
-
*
|
|
39
|
-
* local-first page that answers from cache and confirms with the
|
|
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 ??
|
|
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
|
-
|
|
66
|
-
const
|
|
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
|
-
(
|
|
70
|
-
|
|
71
|
-
|
|
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
|
}
|