steddy 0.1.1 → 0.1.3
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/README.md +57 -6
- package/dist/index.cjs +200 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -5
- package/dist/index.d.ts +63 -5
- package/dist/index.js +199 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -10,6 +10,34 @@ Explainer: [https://cincinnatus101010.github.io/steddyweb/](https://cincinnatus1
|
|
|
10
10
|
npm install steddy
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## App root
|
|
14
|
+
|
|
15
|
+
Wire an isolated runtime once. On the server, create a **new** runtime per request — do not rely on the module singleton.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { useEffect } from "react";
|
|
19
|
+
import {
|
|
20
|
+
attachDefaults,
|
|
21
|
+
createRuntime,
|
|
22
|
+
SteddyProvider,
|
|
23
|
+
} from "steddy";
|
|
24
|
+
|
|
25
|
+
const runtime = createRuntime();
|
|
26
|
+
|
|
27
|
+
export function AppProviders({ children }: { children: React.ReactNode }) {
|
|
28
|
+
useEffect(() => attachDefaults(runtime.coordinator, runtime.store), []);
|
|
29
|
+
return (
|
|
30
|
+
<SteddyProvider store={runtime.store} coordinator={runtime.coordinator}>
|
|
31
|
+
{children}
|
|
32
|
+
</SteddyProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For Next.js / RSC: fetch on the server, `dump(runtime.store)`, pass `cache={payload}` into `SteddyProvider` on the client. `hydrateAll` preserves timestamps from `dump` so dedup can skip an immediate refetch.
|
|
38
|
+
|
|
39
|
+
## Hook
|
|
40
|
+
|
|
13
41
|
```ts
|
|
14
42
|
import { useSteddy } from "steddy";
|
|
15
43
|
|
|
@@ -21,6 +49,7 @@ function Profile({ id }: { id: string }) {
|
|
|
21
49
|
if (!response.ok) throw new Error("failed");
|
|
22
50
|
return response.json();
|
|
23
51
|
},
|
|
52
|
+
{ keepPreviousData: true, staleTime: 60_000 },
|
|
24
53
|
);
|
|
25
54
|
|
|
26
55
|
if (error) return <p>Failed to load</p>;
|
|
@@ -35,17 +64,39 @@ function Profile({ id }: { id: string }) {
|
|
|
35
64
|
}
|
|
36
65
|
```
|
|
37
66
|
|
|
38
|
-
`key === null` skips fetching.
|
|
67
|
+
`key === null` skips fetching. Options: `{ suspense: true }`, `{ keepPreviousData: true }`, `{ staleTime }` (default 2000ms dedup window).
|
|
68
|
+
|
|
69
|
+
## Helpers
|
|
39
70
|
|
|
40
71
|
```ts
|
|
41
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
createRuntime,
|
|
74
|
+
dump,
|
|
75
|
+
hydrateAll,
|
|
76
|
+
prefetch,
|
|
77
|
+
clear,
|
|
78
|
+
} from "steddy";
|
|
79
|
+
|
|
80
|
+
// Route loader / link hover — no mounted hook required
|
|
81
|
+
await prefetch(["user", id], fetchUser, runtime);
|
|
82
|
+
|
|
83
|
+
clear("user", runtime);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Plugins (opt-in)
|
|
42
87
|
|
|
43
|
-
|
|
44
|
-
focusRevalidate
|
|
45
|
-
|
|
88
|
+
```ts
|
|
89
|
+
import { attachDefaults, focusRevalidate, measurePerf } from "steddy";
|
|
90
|
+
|
|
91
|
+
// Or wire individually — pass store so focus/reconnect only hit subscribed keys
|
|
92
|
+
focusRevalidate(coordinator, store);
|
|
93
|
+
|
|
94
|
+
const perf = measurePerf(coordinator);
|
|
46
95
|
```
|
|
47
96
|
|
|
48
|
-
|
|
97
|
+
Also: `reconnectRevalidate`, `pollingRevalidate`, `retryOnError` (fetcher wrapper), `ttlEvict`, `useSteddyInfinite`.
|
|
98
|
+
|
|
99
|
+
`measurePerf(coordinator)` returns `{ starts, aborts, dedups, writes, errors, totalMs }`. Run `npm run bench` for coordinator microbenchmarks.
|
|
49
100
|
|
|
50
101
|
## Architecture
|
|
51
102
|
|
package/dist/index.cjs
CHANGED
|
@@ -8,6 +8,9 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
8
8
|
// src/coordinator.ts
|
|
9
9
|
var DEDUP_WINDOW_MS = 2e3;
|
|
10
10
|
var UNSUBSCRIBE_GRACE_MS = 0;
|
|
11
|
+
function now() {
|
|
12
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
13
|
+
}
|
|
11
14
|
function isAbortError(error) {
|
|
12
15
|
return typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
|
|
13
16
|
}
|
|
@@ -15,7 +18,16 @@ function createCoordinator(store) {
|
|
|
15
18
|
const registered = /* @__PURE__ */ new Map();
|
|
16
19
|
const inflight = /* @__PURE__ */ new Map();
|
|
17
20
|
const releaseTimers = /* @__PURE__ */ new Map();
|
|
21
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
18
22
|
let generation = 0;
|
|
23
|
+
function emit(event) {
|
|
24
|
+
if (listeners.size === 0) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const listener of listeners) {
|
|
28
|
+
listener(event);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
19
31
|
function cancelRelease(serializedKey) {
|
|
20
32
|
const timer = releaseTimers.get(serializedKey);
|
|
21
33
|
if (timer == null) {
|
|
@@ -29,6 +41,11 @@ function createCoordinator(store) {
|
|
|
29
41
|
if (!current) {
|
|
30
42
|
return;
|
|
31
43
|
}
|
|
44
|
+
emit({
|
|
45
|
+
type: "abort",
|
|
46
|
+
key: serializedKey,
|
|
47
|
+
ms: now() - current.startedAt
|
|
48
|
+
});
|
|
32
49
|
current.controller.abort();
|
|
33
50
|
current.settle();
|
|
34
51
|
inflight.delete(serializedKey);
|
|
@@ -40,9 +57,13 @@ function createCoordinator(store) {
|
|
|
40
57
|
}
|
|
41
58
|
}
|
|
42
59
|
const coordinator = {
|
|
43
|
-
register(serializedKey, key, fetcher) {
|
|
60
|
+
register(serializedKey, key, fetcher, options) {
|
|
44
61
|
cancelRelease(serializedKey);
|
|
45
|
-
registered.set(serializedKey, {
|
|
62
|
+
registered.set(serializedKey, {
|
|
63
|
+
key,
|
|
64
|
+
fetcher,
|
|
65
|
+
staleTime: options?.staleTime ?? DEDUP_WINDOW_MS
|
|
66
|
+
});
|
|
46
67
|
},
|
|
47
68
|
unregister(serializedKey) {
|
|
48
69
|
cancelRelease(serializedKey);
|
|
@@ -63,7 +84,9 @@ function createCoordinator(store) {
|
|
|
63
84
|
async revalidate(serializedKey, fetcher, options) {
|
|
64
85
|
if (!options?.force) {
|
|
65
86
|
const entry = store.get(serializedKey);
|
|
66
|
-
|
|
87
|
+
const staleTime = registered.get(serializedKey)?.staleTime ?? DEDUP_WINDOW_MS;
|
|
88
|
+
if (entry && !entry.isValidating && entry.error == null && entry.hasData && Date.now() - entry.timestamp < staleTime) {
|
|
89
|
+
emit({ type: "dedup", key: serializedKey });
|
|
67
90
|
return;
|
|
68
91
|
}
|
|
69
92
|
}
|
|
@@ -87,15 +110,19 @@ function createCoordinator(store) {
|
|
|
87
110
|
settleWaiter();
|
|
88
111
|
}
|
|
89
112
|
};
|
|
113
|
+
const startedAt = now();
|
|
90
114
|
inflight.set(serializedKey, {
|
|
91
115
|
controller,
|
|
92
116
|
generation: currentGeneration,
|
|
93
117
|
waiter,
|
|
94
|
-
settle
|
|
118
|
+
settle,
|
|
119
|
+
startedAt
|
|
95
120
|
});
|
|
121
|
+
emit({ type: "start", key: serializedKey });
|
|
96
122
|
const previous = store.get(serializedKey);
|
|
97
123
|
store.set(serializedKey, {
|
|
98
124
|
data: previous?.data,
|
|
125
|
+
hasData: previous?.hasData ?? false,
|
|
99
126
|
error: previous?.error,
|
|
100
127
|
timestamp: previous?.timestamp ?? 0,
|
|
101
128
|
isValidating: true
|
|
@@ -109,10 +136,16 @@ function createCoordinator(store) {
|
|
|
109
136
|
inflight.delete(serializedKey);
|
|
110
137
|
store.set(serializedKey, {
|
|
111
138
|
data,
|
|
139
|
+
hasData: true,
|
|
112
140
|
error: void 0,
|
|
113
141
|
timestamp: Date.now(),
|
|
114
142
|
isValidating: false
|
|
115
143
|
});
|
|
144
|
+
emit({
|
|
145
|
+
type: "write",
|
|
146
|
+
key: serializedKey,
|
|
147
|
+
ms: now() - startedAt
|
|
148
|
+
});
|
|
116
149
|
} catch (error) {
|
|
117
150
|
const stillCurrent = inflight.get(serializedKey)?.generation === currentGeneration;
|
|
118
151
|
if (!stillCurrent || controller.signal.aborted || isAbortError(error)) {
|
|
@@ -122,10 +155,16 @@ function createCoordinator(store) {
|
|
|
122
155
|
const current = store.get(serializedKey);
|
|
123
156
|
store.set(serializedKey, {
|
|
124
157
|
data: current?.data,
|
|
158
|
+
hasData: current?.hasData ?? false,
|
|
125
159
|
error,
|
|
126
160
|
timestamp: current?.timestamp ?? 0,
|
|
127
161
|
isValidating: false
|
|
128
162
|
});
|
|
163
|
+
emit({
|
|
164
|
+
type: "error",
|
|
165
|
+
key: serializedKey,
|
|
166
|
+
ms: now() - startedAt
|
|
167
|
+
});
|
|
129
168
|
throw error;
|
|
130
169
|
} finally {
|
|
131
170
|
settle();
|
|
@@ -145,7 +184,7 @@ function createCoordinator(store) {
|
|
|
145
184
|
abortInternal(serializedKey, true);
|
|
146
185
|
},
|
|
147
186
|
evict(options) {
|
|
148
|
-
const
|
|
187
|
+
const now2 = Date.now();
|
|
149
188
|
const removable = [];
|
|
150
189
|
for (const key of store.keys()) {
|
|
151
190
|
if (inflight.has(key) || store.subscriberCount(key) > 0) {
|
|
@@ -160,7 +199,7 @@ function createCoordinator(store) {
|
|
|
160
199
|
const victims = /* @__PURE__ */ new Set();
|
|
161
200
|
if (options.maxAge != null) {
|
|
162
201
|
for (const item of removable) {
|
|
163
|
-
if (
|
|
202
|
+
if (now2 - item.timestamp >= options.maxAge) {
|
|
164
203
|
victims.add(item.key);
|
|
165
204
|
}
|
|
166
205
|
}
|
|
@@ -195,6 +234,12 @@ function createCoordinator(store) {
|
|
|
195
234
|
abortInternal(key, false);
|
|
196
235
|
}
|
|
197
236
|
registered.clear();
|
|
237
|
+
},
|
|
238
|
+
subscribe(listener) {
|
|
239
|
+
listeners.add(listener);
|
|
240
|
+
return () => {
|
|
241
|
+
listeners.delete(listener);
|
|
242
|
+
};
|
|
198
243
|
}
|
|
199
244
|
};
|
|
200
245
|
return coordinator;
|
|
@@ -203,6 +248,7 @@ function createCoordinator(store) {
|
|
|
203
248
|
// src/store.ts
|
|
204
249
|
var EMPTY_SNAPSHOT = Object.freeze({
|
|
205
250
|
data: void 0,
|
|
251
|
+
hasData: false,
|
|
206
252
|
error: void 0,
|
|
207
253
|
timestamp: 0,
|
|
208
254
|
isValidating: false
|
|
@@ -224,6 +270,10 @@ function createStore() {
|
|
|
224
270
|
return entries.get(key);
|
|
225
271
|
},
|
|
226
272
|
set(key, entry) {
|
|
273
|
+
const existing = entries.get(key);
|
|
274
|
+
if (existing && Object.is(existing.data, entry.data) && existing.hasData === entry.hasData && existing.error === entry.error && existing.timestamp === entry.timestamp && existing.isValidating === entry.isValidating) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
227
277
|
entries.set(key, entry);
|
|
228
278
|
emit(key);
|
|
229
279
|
},
|
|
@@ -325,6 +375,7 @@ function createMutate(store, coordinator) {
|
|
|
325
375
|
if (isThenable(next)) {
|
|
326
376
|
store.set(serialized, {
|
|
327
377
|
data: currentData,
|
|
378
|
+
hasData: previous?.hasData ?? false,
|
|
328
379
|
error: previous?.error,
|
|
329
380
|
timestamp: previous?.timestamp ?? 0,
|
|
330
381
|
isValidating: true
|
|
@@ -332,6 +383,7 @@ function createMutate(store, coordinator) {
|
|
|
332
383
|
const resolved = await next;
|
|
333
384
|
store.set(serialized, {
|
|
334
385
|
data: resolved,
|
|
386
|
+
hasData: true,
|
|
335
387
|
error: void 0,
|
|
336
388
|
timestamp: Date.now(),
|
|
337
389
|
isValidating: revalidate
|
|
@@ -339,6 +391,7 @@ function createMutate(store, coordinator) {
|
|
|
339
391
|
} else {
|
|
340
392
|
store.set(serialized, {
|
|
341
393
|
data: next,
|
|
394
|
+
hasData: true,
|
|
342
395
|
error: void 0,
|
|
343
396
|
timestamp: Date.now(),
|
|
344
397
|
isValidating: revalidate
|
|
@@ -355,6 +408,7 @@ function createMutate(store, coordinator) {
|
|
|
355
408
|
const current = store.get(serialized);
|
|
356
409
|
store.set(serialized, {
|
|
357
410
|
data: current?.data,
|
|
411
|
+
hasData: current?.hasData ?? false,
|
|
358
412
|
error,
|
|
359
413
|
timestamp: current?.timestamp ?? Date.now(),
|
|
360
414
|
isValidating: false
|
|
@@ -386,24 +440,44 @@ function SteddyProvider({
|
|
|
386
440
|
cache,
|
|
387
441
|
children
|
|
388
442
|
}) {
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
443
|
+
const hydratedFingerprint = react.useRef(null);
|
|
444
|
+
react.useEffect(() => {
|
|
445
|
+
if (!cache) {
|
|
446
|
+
hydratedFingerprint.current = null;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const fingerprint = JSON.stringify(cache);
|
|
450
|
+
if (hydratedFingerprint.current === fingerprint) {
|
|
451
|
+
return;
|
|
392
452
|
}
|
|
393
|
-
|
|
453
|
+
hydratedFingerprint.current = fingerprint;
|
|
454
|
+
hydrateAll(cache, store);
|
|
455
|
+
}, [cache, store]);
|
|
456
|
+
const value = react.useMemo(
|
|
457
|
+
() => ({
|
|
394
458
|
store,
|
|
395
459
|
coordinator,
|
|
396
460
|
mutate: createMutate(store, coordinator)
|
|
397
|
-
}
|
|
398
|
-
|
|
461
|
+
}),
|
|
462
|
+
[store, coordinator]
|
|
463
|
+
);
|
|
399
464
|
return /* @__PURE__ */ jsxRuntime.jsx(SteddyContext.Provider, { value, children });
|
|
400
465
|
}
|
|
466
|
+
var warnedDefaultOnServer = false;
|
|
401
467
|
function useSteddyRuntime() {
|
|
402
|
-
|
|
468
|
+
const runtime = react.useContext(SteddyContext);
|
|
469
|
+
if (process.env.NODE_ENV !== "production" && typeof window === "undefined" && runtime === defaultRuntime && !warnedDefaultOnServer) {
|
|
470
|
+
warnedDefaultOnServer = true;
|
|
471
|
+
console.warn(
|
|
472
|
+
"[steddy] useSteddy on the server without SteddyProvider shares one cache across requests. Use createRuntime() per request."
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return runtime;
|
|
403
476
|
}
|
|
404
477
|
function hydrate(key, data, store = defaultStore) {
|
|
405
478
|
store.set(serializeKey(key), {
|
|
406
479
|
data,
|
|
480
|
+
hasData: true,
|
|
407
481
|
error: void 0,
|
|
408
482
|
timestamp: 0,
|
|
409
483
|
isValidating: false
|
|
@@ -413,7 +487,7 @@ function dump(store = defaultStore) {
|
|
|
413
487
|
const snapshot = {};
|
|
414
488
|
for (const key of store.keys()) {
|
|
415
489
|
const entry = store.get(key);
|
|
416
|
-
if (!entry
|
|
490
|
+
if (!entry?.hasData) {
|
|
417
491
|
continue;
|
|
418
492
|
}
|
|
419
493
|
snapshot[key] = { data: entry.data, timestamp: entry.timestamp };
|
|
@@ -424,12 +498,23 @@ function hydrateAll(snapshot, store = defaultStore) {
|
|
|
424
498
|
for (const [key, payload] of Object.entries(snapshot)) {
|
|
425
499
|
store.set(key, {
|
|
426
500
|
data: payload.data,
|
|
501
|
+
hasData: true,
|
|
427
502
|
error: void 0,
|
|
428
|
-
timestamp:
|
|
503
|
+
timestamp: payload.timestamp,
|
|
429
504
|
isValidating: false
|
|
430
505
|
});
|
|
431
506
|
}
|
|
432
507
|
}
|
|
508
|
+
async function prefetch(key, fetcher, runtime = defaultRuntime) {
|
|
509
|
+
const serialized = serializeKey(key);
|
|
510
|
+
runtime.coordinator.register(serialized, key, fetcher);
|
|
511
|
+
try {
|
|
512
|
+
await runtime.coordinator.revalidate(serialized);
|
|
513
|
+
} catch {
|
|
514
|
+
} finally {
|
|
515
|
+
runtime.coordinator.unregister(serialized);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
433
518
|
function clear(key, runtime = defaultRuntime) {
|
|
434
519
|
if (key === void 0) {
|
|
435
520
|
for (const active of runtime.coordinator.getRegisteredKeys()) {
|
|
@@ -468,11 +553,15 @@ function useSteddy(key, fetcher, options) {
|
|
|
468
553
|
fetcherRef.current = fetcher;
|
|
469
554
|
const keyRef = react.useRef(key);
|
|
470
555
|
keyRef.current = key;
|
|
556
|
+
const staleTime = options?.staleTime ?? DEDUP_WINDOW_MS;
|
|
557
|
+
const staleTimeRef = react.useRef(staleTime);
|
|
558
|
+
staleTimeRef.current = staleTime;
|
|
471
559
|
if (serialized != null && key != null) {
|
|
472
560
|
coordinator.register(
|
|
473
561
|
serialized,
|
|
474
562
|
key,
|
|
475
|
-
(k, ctx) => fetcherRef.current(k, ctx)
|
|
563
|
+
(k, ctx) => fetcherRef.current(k, ctx),
|
|
564
|
+
{ staleTime: staleTimeRef.current }
|
|
476
565
|
);
|
|
477
566
|
}
|
|
478
567
|
const subscribe = react.useCallback(
|
|
@@ -485,7 +574,8 @@ function useSteddy(key, fetcher, options) {
|
|
|
485
574
|
coordinator.register(
|
|
486
575
|
serialized,
|
|
487
576
|
originalKey,
|
|
488
|
-
(k, ctx) => fetcherRef.current(k, ctx)
|
|
577
|
+
(k, ctx) => fetcherRef.current(k, ctx),
|
|
578
|
+
{ staleTime: staleTimeRef.current }
|
|
489
579
|
);
|
|
490
580
|
const unsubscribe = store.subscribe(serialized, onStoreChange);
|
|
491
581
|
if (!coordinator.isInFlight(serialized)) {
|
|
@@ -521,15 +611,15 @@ function useSteddy(key, fetcher, options) {
|
|
|
521
611
|
const previousRef = react.useRef(
|
|
522
612
|
void 0
|
|
523
613
|
);
|
|
524
|
-
if (serialized != null && snapshot.
|
|
614
|
+
if (serialized != null && snapshot.hasData) {
|
|
525
615
|
previousRef.current = { serialized, data: snapshot.data };
|
|
526
616
|
}
|
|
527
|
-
const data = keepPreviousData && serialized != null && snapshot.
|
|
617
|
+
const data = keepPreviousData && serialized != null && !snapshot.hasData && snapshot.error == null && previousRef.current != null && previousRef.current.serialized !== serialized ? previousRef.current.data : snapshot.data;
|
|
528
618
|
if (options?.suspense && serialized != null) {
|
|
529
619
|
if (snapshot.error != null) {
|
|
530
620
|
throw snapshot.error;
|
|
531
621
|
}
|
|
532
|
-
if (snapshot.
|
|
622
|
+
if (!snapshot.hasData && data === void 0) {
|
|
533
623
|
const waiter = coordinator.getInFlightPromise(serialized) ?? coordinator.revalidate(serialized).catch(() => {
|
|
534
624
|
});
|
|
535
625
|
throw waiter;
|
|
@@ -538,7 +628,7 @@ function useSteddy(key, fetcher, options) {
|
|
|
538
628
|
return {
|
|
539
629
|
data,
|
|
540
630
|
error: snapshot.error,
|
|
541
|
-
isLoading: serialized != null &&
|
|
631
|
+
isLoading: serialized != null && !snapshot.hasData && snapshot.error == null && data === void 0,
|
|
542
632
|
isValidating: snapshot.isValidating,
|
|
543
633
|
mutate: boundMutate
|
|
544
634
|
};
|
|
@@ -629,7 +719,7 @@ function useSteddyInfinite(getKey, fetcher, options) {
|
|
|
629
719
|
);
|
|
630
720
|
const fingerprint = currentPages.map((page) => {
|
|
631
721
|
const entry = store.getSnapshot(page.serialized);
|
|
632
|
-
return `${page.serialized}:${entry.timestamp}:${entry.isValidating ? 1 : 0}:${entry.error == null ? 0 : 1}:${entry.
|
|
722
|
+
return `${page.serialized}:${entry.timestamp}:${entry.isValidating ? 1 : 0}:${entry.error == null ? 0 : 1}:${entry.hasData ? 1 : 0}`;
|
|
633
723
|
}).join("|");
|
|
634
724
|
if (snapshotRef.current.fingerprint === fingerprint) {
|
|
635
725
|
return snapshotRef.current;
|
|
@@ -646,7 +736,7 @@ function useSteddyInfinite(getKey, fetcher, options) {
|
|
|
646
736
|
if (entry.error != null && error === void 0) {
|
|
647
737
|
error = entry.error;
|
|
648
738
|
}
|
|
649
|
-
if (entry.
|
|
739
|
+
if (!entry.hasData) {
|
|
650
740
|
missing = true;
|
|
651
741
|
break;
|
|
652
742
|
}
|
|
@@ -713,6 +803,7 @@ function useSteddyInfinite(getKey, fetcher, options) {
|
|
|
713
803
|
}
|
|
714
804
|
store.set(page.serialized, {
|
|
715
805
|
data: pagesValue[index],
|
|
806
|
+
hasData: true,
|
|
716
807
|
error: void 0,
|
|
717
808
|
timestamp: Date.now(),
|
|
718
809
|
isValidating: revalidate
|
|
@@ -742,7 +833,7 @@ function useSteddyInfinite(getKey, fetcher, options) {
|
|
|
742
833
|
return {
|
|
743
834
|
data: snapshot.data,
|
|
744
835
|
error: snapshot.error,
|
|
745
|
-
isLoading: first != null && firstEntry?.
|
|
836
|
+
isLoading: first != null && !firstEntry?.hasData && firstEntry?.error == null,
|
|
746
837
|
isValidating: snapshot.isValidating,
|
|
747
838
|
size,
|
|
748
839
|
setSize,
|
|
@@ -750,23 +841,37 @@ function useSteddyInfinite(getKey, fetcher, options) {
|
|
|
750
841
|
};
|
|
751
842
|
}
|
|
752
843
|
|
|
844
|
+
// src/plugins/subscribedKeys.ts
|
|
845
|
+
function keysWithSubscribers(keys, store) {
|
|
846
|
+
return keys.filter((key) => store.subscriberCount(key) > 0);
|
|
847
|
+
}
|
|
848
|
+
|
|
753
849
|
// src/plugins/focus.ts
|
|
754
|
-
function focusRevalidate(coordinator) {
|
|
755
|
-
const
|
|
756
|
-
|
|
850
|
+
function focusRevalidate(coordinator, store) {
|
|
851
|
+
const run = () => {
|
|
852
|
+
if (typeof document !== "undefined" && document.visibilityState !== "visible") {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const keys = store ? keysWithSubscribers(coordinator.getRegisteredKeys(), store) : coordinator.getRegisteredKeys();
|
|
856
|
+
for (const key of keys) {
|
|
757
857
|
void coordinator.revalidate(key);
|
|
758
858
|
}
|
|
759
859
|
};
|
|
760
|
-
|
|
860
|
+
if (typeof document === "undefined") {
|
|
861
|
+
return () => {
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
document.addEventListener("visibilitychange", run);
|
|
761
865
|
return () => {
|
|
762
|
-
|
|
866
|
+
document.removeEventListener("visibilitychange", run);
|
|
763
867
|
};
|
|
764
868
|
}
|
|
765
869
|
|
|
766
870
|
// src/plugins/reconnect.ts
|
|
767
|
-
function reconnectRevalidate(coordinator) {
|
|
871
|
+
function reconnectRevalidate(coordinator, store) {
|
|
768
872
|
const onOnline = () => {
|
|
769
|
-
|
|
873
|
+
const keys = store ? keysWithSubscribers(coordinator.getRegisteredKeys(), store) : coordinator.getRegisteredKeys();
|
|
874
|
+
for (const key of keys) {
|
|
770
875
|
void coordinator.revalidate(key);
|
|
771
876
|
}
|
|
772
877
|
};
|
|
@@ -843,9 +948,71 @@ function ttlEvict(coordinator, options) {
|
|
|
843
948
|
};
|
|
844
949
|
}
|
|
845
950
|
|
|
951
|
+
// src/plugins/measure.ts
|
|
952
|
+
function measurePerf(coordinator, onEvent) {
|
|
953
|
+
const snap = {
|
|
954
|
+
starts: 0,
|
|
955
|
+
aborts: 0,
|
|
956
|
+
dedups: 0,
|
|
957
|
+
writes: 0,
|
|
958
|
+
errors: 0,
|
|
959
|
+
totalMs: 0
|
|
960
|
+
};
|
|
961
|
+
const stop = coordinator.subscribe((event) => {
|
|
962
|
+
switch (event.type) {
|
|
963
|
+
case "start":
|
|
964
|
+
snap.starts += 1;
|
|
965
|
+
break;
|
|
966
|
+
case "abort":
|
|
967
|
+
snap.aborts += 1;
|
|
968
|
+
snap.totalMs += event.ms;
|
|
969
|
+
break;
|
|
970
|
+
case "dedup":
|
|
971
|
+
snap.dedups += 1;
|
|
972
|
+
break;
|
|
973
|
+
case "write":
|
|
974
|
+
snap.writes += 1;
|
|
975
|
+
snap.totalMs += event.ms;
|
|
976
|
+
break;
|
|
977
|
+
case "error":
|
|
978
|
+
snap.errors += 1;
|
|
979
|
+
snap.totalMs += event.ms;
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
onEvent?.(event);
|
|
983
|
+
});
|
|
984
|
+
return {
|
|
985
|
+
snapshot: () => ({ ...snap }),
|
|
986
|
+
reset() {
|
|
987
|
+
snap.starts = 0;
|
|
988
|
+
snap.aborts = 0;
|
|
989
|
+
snap.dedups = 0;
|
|
990
|
+
snap.writes = 0;
|
|
991
|
+
snap.errors = 0;
|
|
992
|
+
snap.totalMs = 0;
|
|
993
|
+
},
|
|
994
|
+
stop
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/plugins/attachDefaults.ts
|
|
999
|
+
function attachDefaults(coordinator, store, options) {
|
|
1000
|
+
const stops = [
|
|
1001
|
+
focusRevalidate(coordinator, store),
|
|
1002
|
+
reconnectRevalidate(coordinator, store),
|
|
1003
|
+
ttlEvict(coordinator, options?.ttl ?? { maxAge: 3e5 })
|
|
1004
|
+
];
|
|
1005
|
+
return () => {
|
|
1006
|
+
for (const stop of stops) {
|
|
1007
|
+
stop();
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
846
1012
|
exports.DEDUP_WINDOW_MS = DEDUP_WINDOW_MS;
|
|
847
1013
|
exports.SteddyProvider = SteddyProvider;
|
|
848
1014
|
exports.UNSUBSCRIBE_GRACE_MS = UNSUBSCRIBE_GRACE_MS;
|
|
1015
|
+
exports.attachDefaults = attachDefaults;
|
|
849
1016
|
exports.clear = clear;
|
|
850
1017
|
exports.createCoordinator = createCoordinator;
|
|
851
1018
|
exports.createMutate = createMutate;
|
|
@@ -857,8 +1024,10 @@ exports.dump = dump;
|
|
|
857
1024
|
exports.focusRevalidate = focusRevalidate;
|
|
858
1025
|
exports.hydrate = hydrate;
|
|
859
1026
|
exports.hydrateAll = hydrateAll;
|
|
1027
|
+
exports.measurePerf = measurePerf;
|
|
860
1028
|
exports.mutate = mutate;
|
|
861
1029
|
exports.pollingRevalidate = pollingRevalidate;
|
|
1030
|
+
exports.prefetch = prefetch;
|
|
862
1031
|
exports.reconnectRevalidate = reconnectRevalidate;
|
|
863
1032
|
exports.retryOnError = retryOnError;
|
|
864
1033
|
exports.serializeKey = serializeKey;
|