steddy 0.1.0 → 0.1.2
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 +2 -2
- package/dist/index.cjs +195 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +194 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -5,18 +5,45 @@ import { jsx } from 'react/jsx-runtime';
|
|
|
5
5
|
|
|
6
6
|
// src/coordinator.ts
|
|
7
7
|
var DEDUP_WINDOW_MS = 2e3;
|
|
8
|
+
var UNSUBSCRIBE_GRACE_MS = 0;
|
|
9
|
+
function now() {
|
|
10
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
11
|
+
}
|
|
8
12
|
function isAbortError(error) {
|
|
9
13
|
return typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
|
|
10
14
|
}
|
|
11
15
|
function createCoordinator(store) {
|
|
12
16
|
const registered = /* @__PURE__ */ new Map();
|
|
13
17
|
const inflight = /* @__PURE__ */ new Map();
|
|
18
|
+
const releaseTimers = /* @__PURE__ */ new Map();
|
|
19
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
14
20
|
let generation = 0;
|
|
21
|
+
function emit(event) {
|
|
22
|
+
if (listeners.size === 0) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
for (const listener of listeners) {
|
|
26
|
+
listener(event);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function cancelRelease(serializedKey) {
|
|
30
|
+
const timer = releaseTimers.get(serializedKey);
|
|
31
|
+
if (timer == null) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
releaseTimers.delete(serializedKey);
|
|
36
|
+
}
|
|
15
37
|
function abortInternal(serializedKey, markIdle) {
|
|
16
38
|
const current = inflight.get(serializedKey);
|
|
17
39
|
if (!current) {
|
|
18
40
|
return;
|
|
19
41
|
}
|
|
42
|
+
emit({
|
|
43
|
+
type: "abort",
|
|
44
|
+
key: serializedKey,
|
|
45
|
+
ms: now() - current.startedAt
|
|
46
|
+
});
|
|
20
47
|
current.controller.abort();
|
|
21
48
|
current.settle();
|
|
22
49
|
inflight.delete(serializedKey);
|
|
@@ -29,15 +56,30 @@ function createCoordinator(store) {
|
|
|
29
56
|
}
|
|
30
57
|
const coordinator = {
|
|
31
58
|
register(serializedKey, key, fetcher) {
|
|
59
|
+
cancelRelease(serializedKey);
|
|
32
60
|
registered.set(serializedKey, { key, fetcher });
|
|
33
61
|
},
|
|
34
62
|
unregister(serializedKey) {
|
|
63
|
+
cancelRelease(serializedKey);
|
|
35
64
|
registered.delete(serializedKey);
|
|
36
65
|
},
|
|
66
|
+
scheduleRelease(serializedKey) {
|
|
67
|
+
cancelRelease(serializedKey);
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
releaseTimers.delete(serializedKey);
|
|
70
|
+
if (store.subscriberCount(serializedKey) > 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
abortInternal(serializedKey, true);
|
|
74
|
+
registered.delete(serializedKey);
|
|
75
|
+
}, UNSUBSCRIBE_GRACE_MS);
|
|
76
|
+
releaseTimers.set(serializedKey, timer);
|
|
77
|
+
},
|
|
37
78
|
async revalidate(serializedKey, fetcher, options) {
|
|
38
79
|
if (!options?.force) {
|
|
39
80
|
const entry = store.get(serializedKey);
|
|
40
81
|
if (entry && !entry.isValidating && entry.error == null && entry.data !== void 0 && Date.now() - entry.timestamp < DEDUP_WINDOW_MS) {
|
|
82
|
+
emit({ type: "dedup", key: serializedKey });
|
|
41
83
|
return;
|
|
42
84
|
}
|
|
43
85
|
}
|
|
@@ -61,12 +103,15 @@ function createCoordinator(store) {
|
|
|
61
103
|
settleWaiter();
|
|
62
104
|
}
|
|
63
105
|
};
|
|
106
|
+
const startedAt = now();
|
|
64
107
|
inflight.set(serializedKey, {
|
|
65
108
|
controller,
|
|
66
109
|
generation: currentGeneration,
|
|
67
110
|
waiter,
|
|
68
|
-
settle
|
|
111
|
+
settle,
|
|
112
|
+
startedAt
|
|
69
113
|
});
|
|
114
|
+
emit({ type: "start", key: serializedKey });
|
|
70
115
|
const previous = store.get(serializedKey);
|
|
71
116
|
store.set(serializedKey, {
|
|
72
117
|
data: previous?.data,
|
|
@@ -87,6 +132,11 @@ function createCoordinator(store) {
|
|
|
87
132
|
timestamp: Date.now(),
|
|
88
133
|
isValidating: false
|
|
89
134
|
});
|
|
135
|
+
emit({
|
|
136
|
+
type: "write",
|
|
137
|
+
key: serializedKey,
|
|
138
|
+
ms: now() - startedAt
|
|
139
|
+
});
|
|
90
140
|
} catch (error) {
|
|
91
141
|
const stillCurrent = inflight.get(serializedKey)?.generation === currentGeneration;
|
|
92
142
|
if (!stillCurrent || controller.signal.aborted || isAbortError(error)) {
|
|
@@ -100,6 +150,11 @@ function createCoordinator(store) {
|
|
|
100
150
|
timestamp: current?.timestamp ?? 0,
|
|
101
151
|
isValidating: false
|
|
102
152
|
});
|
|
153
|
+
emit({
|
|
154
|
+
type: "error",
|
|
155
|
+
key: serializedKey,
|
|
156
|
+
ms: now() - startedAt
|
|
157
|
+
});
|
|
103
158
|
throw error;
|
|
104
159
|
} finally {
|
|
105
160
|
settle();
|
|
@@ -115,10 +170,11 @@ function createCoordinator(store) {
|
|
|
115
170
|
return [...registered.keys()];
|
|
116
171
|
},
|
|
117
172
|
abort(serializedKey) {
|
|
173
|
+
cancelRelease(serializedKey);
|
|
118
174
|
abortInternal(serializedKey, true);
|
|
119
175
|
},
|
|
120
176
|
evict(options) {
|
|
121
|
-
const
|
|
177
|
+
const now2 = Date.now();
|
|
122
178
|
const removable = [];
|
|
123
179
|
for (const key of store.keys()) {
|
|
124
180
|
if (inflight.has(key) || store.subscriberCount(key) > 0) {
|
|
@@ -133,7 +189,7 @@ function createCoordinator(store) {
|
|
|
133
189
|
const victims = /* @__PURE__ */ new Set();
|
|
134
190
|
if (options.maxAge != null) {
|
|
135
191
|
for (const item of removable) {
|
|
136
|
-
if (
|
|
192
|
+
if (now2 - item.timestamp >= options.maxAge) {
|
|
137
193
|
victims.add(item.key);
|
|
138
194
|
}
|
|
139
195
|
}
|
|
@@ -153,6 +209,7 @@ function createCoordinator(store) {
|
|
|
153
209
|
}
|
|
154
210
|
}
|
|
155
211
|
for (const key of victims) {
|
|
212
|
+
cancelRelease(key);
|
|
156
213
|
abortInternal(key, false);
|
|
157
214
|
registered.delete(key);
|
|
158
215
|
store.delete(key);
|
|
@@ -160,10 +217,19 @@ function createCoordinator(store) {
|
|
|
160
217
|
return [...victims];
|
|
161
218
|
},
|
|
162
219
|
reset() {
|
|
220
|
+
for (const key of [...releaseTimers.keys()]) {
|
|
221
|
+
cancelRelease(key);
|
|
222
|
+
}
|
|
163
223
|
for (const key of [...inflight.keys()]) {
|
|
164
224
|
abortInternal(key, false);
|
|
165
225
|
}
|
|
166
226
|
registered.clear();
|
|
227
|
+
},
|
|
228
|
+
subscribe(listener) {
|
|
229
|
+
listeners.add(listener);
|
|
230
|
+
return () => {
|
|
231
|
+
listeners.delete(listener);
|
|
232
|
+
};
|
|
167
233
|
}
|
|
168
234
|
};
|
|
169
235
|
return coordinator;
|
|
@@ -464,8 +530,7 @@ function useSteddy(key, fetcher, options) {
|
|
|
464
530
|
return () => {
|
|
465
531
|
unsubscribe();
|
|
466
532
|
if (store.subscriberCount(serialized) === 0) {
|
|
467
|
-
coordinator.
|
|
468
|
-
coordinator.unregister(serialized);
|
|
533
|
+
coordinator.scheduleRelease(serialized);
|
|
469
534
|
}
|
|
470
535
|
};
|
|
471
536
|
},
|
|
@@ -487,26 +552,38 @@ function useSteddy(key, fetcher, options) {
|
|
|
487
552
|
},
|
|
488
553
|
[runtimeMutate]
|
|
489
554
|
);
|
|
555
|
+
const keepPreviousData = options?.keepPreviousData === true;
|
|
556
|
+
const previousRef = useRef(
|
|
557
|
+
void 0
|
|
558
|
+
);
|
|
559
|
+
if (serialized != null && snapshot.data !== void 0) {
|
|
560
|
+
previousRef.current = { serialized, data: snapshot.data };
|
|
561
|
+
}
|
|
562
|
+
const data = keepPreviousData && serialized != null && snapshot.data === void 0 && snapshot.error == null && previousRef.current != null && previousRef.current.serialized !== serialized ? previousRef.current.data : snapshot.data;
|
|
490
563
|
if (options?.suspense && serialized != null) {
|
|
491
564
|
if (snapshot.error != null) {
|
|
492
565
|
throw snapshot.error;
|
|
493
566
|
}
|
|
494
|
-
if (snapshot.data === void 0) {
|
|
567
|
+
if (snapshot.data === void 0 && data === void 0) {
|
|
495
568
|
const waiter = coordinator.getInFlightPromise(serialized) ?? coordinator.revalidate(serialized).catch(() => {
|
|
496
569
|
});
|
|
497
570
|
throw waiter;
|
|
498
571
|
}
|
|
499
572
|
}
|
|
500
573
|
return {
|
|
501
|
-
data
|
|
574
|
+
data,
|
|
502
575
|
error: snapshot.error,
|
|
503
|
-
isLoading: serialized != null &&
|
|
576
|
+
isLoading: serialized != null && data === void 0 && snapshot.error == null,
|
|
504
577
|
isValidating: snapshot.isValidating,
|
|
505
578
|
mutate: boundMutate
|
|
506
579
|
};
|
|
507
580
|
}
|
|
581
|
+
function isThenable2(value) {
|
|
582
|
+
return typeof value === "object" && value !== null && typeof value.then === "function";
|
|
583
|
+
}
|
|
508
584
|
function collectPages(getKey, getData, size) {
|
|
509
585
|
const pages = [];
|
|
586
|
+
const seen = /* @__PURE__ */ new Set();
|
|
510
587
|
let previous;
|
|
511
588
|
for (let index = 0; index < size; index++) {
|
|
512
589
|
const key = getKey(index, previous);
|
|
@@ -514,13 +591,17 @@ function collectPages(getKey, getData, size) {
|
|
|
514
591
|
break;
|
|
515
592
|
}
|
|
516
593
|
const serialized = serializeKey(key);
|
|
594
|
+
if (seen.has(serialized)) {
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
seen.add(serialized);
|
|
517
598
|
pages.push({ key, serialized });
|
|
518
599
|
previous = getData(serialized);
|
|
519
600
|
}
|
|
520
601
|
return pages;
|
|
521
602
|
}
|
|
522
603
|
function useSteddyInfinite(getKey, fetcher, options) {
|
|
523
|
-
const { store, coordinator
|
|
604
|
+
const { store, coordinator } = useSteddyRuntime();
|
|
524
605
|
const [size, setSizeState] = useState(options?.initialSize ?? 1);
|
|
525
606
|
const fetcherRef = useRef(fetcher);
|
|
526
607
|
fetcherRef.current = fetcher;
|
|
@@ -562,8 +643,7 @@ function useSteddyInfinite(getKey, fetcher, options) {
|
|
|
562
643
|
for (const [index, page] of currentPages.entries()) {
|
|
563
644
|
unsubscribes[index]?.();
|
|
564
645
|
if (store.subscriberCount(page.serialized) === 0) {
|
|
565
|
-
coordinator.
|
|
566
|
-
coordinator.unregister(page.serialized);
|
|
646
|
+
coordinator.scheduleRelease(page.serialized);
|
|
567
647
|
}
|
|
568
648
|
}
|
|
569
649
|
};
|
|
@@ -626,32 +706,71 @@ function useSteddyInfinite(getKey, fetcher, options) {
|
|
|
626
706
|
);
|
|
627
707
|
const boundMutate = useCallback(
|
|
628
708
|
async (updater, mutateOptions) => {
|
|
709
|
+
const revalidate = mutateOptions?.revalidate ?? true;
|
|
710
|
+
const rollbackOnError = mutateOptions?.rollbackOnError ?? true;
|
|
629
711
|
const currentPages = collectPages(
|
|
630
712
|
getKeyRef.current,
|
|
631
713
|
(serialized) => store.get(serialized)?.data,
|
|
632
714
|
size
|
|
633
715
|
);
|
|
634
|
-
|
|
635
|
-
if (!first2) {
|
|
716
|
+
if (currentPages.length === 0) {
|
|
636
717
|
return void 0;
|
|
637
718
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
719
|
+
const previous = currentPages.map((page) => ({
|
|
720
|
+
serialized: page.serialized,
|
|
721
|
+
entry: store.get(page.serialized)
|
|
722
|
+
}));
|
|
723
|
+
const restore = () => {
|
|
724
|
+
for (const item of previous) {
|
|
725
|
+
if (item.entry === void 0) {
|
|
726
|
+
store.delete(item.serialized);
|
|
727
|
+
} else {
|
|
728
|
+
store.set(item.serialized, item.entry);
|
|
729
|
+
}
|
|
643
730
|
}
|
|
644
|
-
|
|
731
|
+
};
|
|
732
|
+
try {
|
|
733
|
+
const loaded = [];
|
|
734
|
+
for (const page of currentPages) {
|
|
735
|
+
const pageData = store.get(page.serialized)?.data;
|
|
736
|
+
if (pageData === void 0) {
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
loaded.push(pageData);
|
|
740
|
+
}
|
|
741
|
+
const current = loaded.length === 0 ? void 0 : loaded;
|
|
742
|
+
const next = typeof updater === "function" ? updater(current) : updater;
|
|
743
|
+
const resolved = isThenable2(next) ? await next : next;
|
|
744
|
+
const pagesValue = resolved;
|
|
745
|
+
for (const [index, page] of currentPages.entries()) {
|
|
746
|
+
if (index >= pagesValue.length) {
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
store.set(page.serialized, {
|
|
750
|
+
data: pagesValue[index],
|
|
751
|
+
error: void 0,
|
|
752
|
+
timestamp: Date.now(),
|
|
753
|
+
isValidating: revalidate
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
if (revalidate) {
|
|
757
|
+
await Promise.all(
|
|
758
|
+
currentPages.map(
|
|
759
|
+
(page) => coordinator.revalidate(page.serialized, void 0, {
|
|
760
|
+
force: true
|
|
761
|
+
})
|
|
762
|
+
)
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
return currentPages.map((page) => store.get(page.serialized)?.data).filter((page) => page !== void 0);
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (rollbackOnError) {
|
|
768
|
+
restore();
|
|
769
|
+
}
|
|
770
|
+
throw error;
|
|
645
771
|
}
|
|
646
|
-
await Promise.all(
|
|
647
|
-
currentPages.map(
|
|
648
|
-
(page) => coordinator.revalidate(page.serialized, void 0, { force: true }).catch(() => {
|
|
649
|
-
})
|
|
650
|
-
)
|
|
651
|
-
);
|
|
652
|
-
return currentPages.map((page) => store.get(page.serialized)?.data).filter((page) => page !== void 0);
|
|
653
772
|
},
|
|
654
|
-
[coordinator,
|
|
773
|
+
[coordinator, size, store]
|
|
655
774
|
);
|
|
656
775
|
const first = pages[0];
|
|
657
776
|
const firstEntry = first ? store.getSnapshot(first.serialized) : void 0;
|
|
@@ -759,6 +878,53 @@ function ttlEvict(coordinator, options) {
|
|
|
759
878
|
};
|
|
760
879
|
}
|
|
761
880
|
|
|
762
|
-
|
|
881
|
+
// src/plugins/measure.ts
|
|
882
|
+
function measurePerf(coordinator, onEvent) {
|
|
883
|
+
const snap = {
|
|
884
|
+
starts: 0,
|
|
885
|
+
aborts: 0,
|
|
886
|
+
dedups: 0,
|
|
887
|
+
writes: 0,
|
|
888
|
+
errors: 0,
|
|
889
|
+
totalMs: 0
|
|
890
|
+
};
|
|
891
|
+
const stop = coordinator.subscribe((event) => {
|
|
892
|
+
switch (event.type) {
|
|
893
|
+
case "start":
|
|
894
|
+
snap.starts += 1;
|
|
895
|
+
break;
|
|
896
|
+
case "abort":
|
|
897
|
+
snap.aborts += 1;
|
|
898
|
+
snap.totalMs += event.ms;
|
|
899
|
+
break;
|
|
900
|
+
case "dedup":
|
|
901
|
+
snap.dedups += 1;
|
|
902
|
+
break;
|
|
903
|
+
case "write":
|
|
904
|
+
snap.writes += 1;
|
|
905
|
+
snap.totalMs += event.ms;
|
|
906
|
+
break;
|
|
907
|
+
case "error":
|
|
908
|
+
snap.errors += 1;
|
|
909
|
+
snap.totalMs += event.ms;
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
onEvent?.(event);
|
|
913
|
+
});
|
|
914
|
+
return {
|
|
915
|
+
snapshot: () => ({ ...snap }),
|
|
916
|
+
reset() {
|
|
917
|
+
snap.starts = 0;
|
|
918
|
+
snap.aborts = 0;
|
|
919
|
+
snap.dedups = 0;
|
|
920
|
+
snap.writes = 0;
|
|
921
|
+
snap.errors = 0;
|
|
922
|
+
snap.totalMs = 0;
|
|
923
|
+
},
|
|
924
|
+
stop
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export { DEDUP_WINDOW_MS, SteddyProvider, UNSUBSCRIBE_GRACE_MS, clear, createCoordinator, createMutate, createRuntime, createStore, defaultCoordinator, defaultStore, dump, focusRevalidate, hydrate, hydrateAll, measurePerf, mutate, pollingRevalidate, reconnectRevalidate, retryOnError, serializeKey, ttlEvict, useSteddy, useSteddyInfinite };
|
|
763
929
|
//# sourceMappingURL=index.js.map
|
|
764
930
|
//# sourceMappingURL=index.js.map
|