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/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 now = Date.now();
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 (now - item.timestamp >= options.maxAge) {
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.abort(serialized);
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: snapshot.data,
574
+ data,
502
575
  error: snapshot.error,
503
- isLoading: serialized != null && snapshot.data === void 0 && snapshot.error == 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, mutate: runtimeMutate } = useSteddyRuntime();
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.abort(page.serialized);
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
- const first2 = currentPages[0];
635
- if (!first2) {
716
+ if (currentPages.length === 0) {
636
717
  return void 0;
637
718
  }
638
- if (updater !== void 0 && typeof updater !== "function") {
639
- const pagesValue = updater;
640
- const firstPage = pagesValue[0];
641
- if (firstPage !== void 0) {
642
- await runtimeMutate(first2.key, firstPage, mutateOptions);
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
- return pagesValue;
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, runtimeMutate, size, store]
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
- export { DEDUP_WINDOW_MS, SteddyProvider, clear, createCoordinator, createMutate, createRuntime, createStore, defaultCoordinator, defaultStore, dump, focusRevalidate, hydrate, hydrateAll, mutate, pollingRevalidate, reconnectRevalidate, retryOnError, serializeKey, ttlEvict, useSteddy, useSteddyInfinite };
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