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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useMemo, useRef, useCallback, useSyncExternalStore, useState, useContext } from 'react';
1
+ import { createContext, useRef, useEffect, useMemo, useCallback, useSyncExternalStore, useState, useContext } from 'react';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
 
4
4
  // src/useSteddy.ts
@@ -6,6 +6,9 @@ import { jsx } from 'react/jsx-runtime';
6
6
  // src/coordinator.ts
7
7
  var DEDUP_WINDOW_MS = 2e3;
8
8
  var UNSUBSCRIBE_GRACE_MS = 0;
9
+ function now() {
10
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
11
+ }
9
12
  function isAbortError(error) {
10
13
  return typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
11
14
  }
@@ -13,7 +16,16 @@ function createCoordinator(store) {
13
16
  const registered = /* @__PURE__ */ new Map();
14
17
  const inflight = /* @__PURE__ */ new Map();
15
18
  const releaseTimers = /* @__PURE__ */ new Map();
19
+ const listeners = /* @__PURE__ */ new Set();
16
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
+ }
17
29
  function cancelRelease(serializedKey) {
18
30
  const timer = releaseTimers.get(serializedKey);
19
31
  if (timer == null) {
@@ -27,6 +39,11 @@ function createCoordinator(store) {
27
39
  if (!current) {
28
40
  return;
29
41
  }
42
+ emit({
43
+ type: "abort",
44
+ key: serializedKey,
45
+ ms: now() - current.startedAt
46
+ });
30
47
  current.controller.abort();
31
48
  current.settle();
32
49
  inflight.delete(serializedKey);
@@ -38,9 +55,13 @@ function createCoordinator(store) {
38
55
  }
39
56
  }
40
57
  const coordinator = {
41
- register(serializedKey, key, fetcher) {
58
+ register(serializedKey, key, fetcher, options) {
42
59
  cancelRelease(serializedKey);
43
- registered.set(serializedKey, { key, fetcher });
60
+ registered.set(serializedKey, {
61
+ key,
62
+ fetcher,
63
+ staleTime: options?.staleTime ?? DEDUP_WINDOW_MS
64
+ });
44
65
  },
45
66
  unregister(serializedKey) {
46
67
  cancelRelease(serializedKey);
@@ -61,7 +82,9 @@ function createCoordinator(store) {
61
82
  async revalidate(serializedKey, fetcher, options) {
62
83
  if (!options?.force) {
63
84
  const entry = store.get(serializedKey);
64
- if (entry && !entry.isValidating && entry.error == null && entry.data !== void 0 && Date.now() - entry.timestamp < DEDUP_WINDOW_MS) {
85
+ const staleTime = registered.get(serializedKey)?.staleTime ?? DEDUP_WINDOW_MS;
86
+ if (entry && !entry.isValidating && entry.error == null && entry.hasData && Date.now() - entry.timestamp < staleTime) {
87
+ emit({ type: "dedup", key: serializedKey });
65
88
  return;
66
89
  }
67
90
  }
@@ -85,15 +108,19 @@ function createCoordinator(store) {
85
108
  settleWaiter();
86
109
  }
87
110
  };
111
+ const startedAt = now();
88
112
  inflight.set(serializedKey, {
89
113
  controller,
90
114
  generation: currentGeneration,
91
115
  waiter,
92
- settle
116
+ settle,
117
+ startedAt
93
118
  });
119
+ emit({ type: "start", key: serializedKey });
94
120
  const previous = store.get(serializedKey);
95
121
  store.set(serializedKey, {
96
122
  data: previous?.data,
123
+ hasData: previous?.hasData ?? false,
97
124
  error: previous?.error,
98
125
  timestamp: previous?.timestamp ?? 0,
99
126
  isValidating: true
@@ -107,10 +134,16 @@ function createCoordinator(store) {
107
134
  inflight.delete(serializedKey);
108
135
  store.set(serializedKey, {
109
136
  data,
137
+ hasData: true,
110
138
  error: void 0,
111
139
  timestamp: Date.now(),
112
140
  isValidating: false
113
141
  });
142
+ emit({
143
+ type: "write",
144
+ key: serializedKey,
145
+ ms: now() - startedAt
146
+ });
114
147
  } catch (error) {
115
148
  const stillCurrent = inflight.get(serializedKey)?.generation === currentGeneration;
116
149
  if (!stillCurrent || controller.signal.aborted || isAbortError(error)) {
@@ -120,10 +153,16 @@ function createCoordinator(store) {
120
153
  const current = store.get(serializedKey);
121
154
  store.set(serializedKey, {
122
155
  data: current?.data,
156
+ hasData: current?.hasData ?? false,
123
157
  error,
124
158
  timestamp: current?.timestamp ?? 0,
125
159
  isValidating: false
126
160
  });
161
+ emit({
162
+ type: "error",
163
+ key: serializedKey,
164
+ ms: now() - startedAt
165
+ });
127
166
  throw error;
128
167
  } finally {
129
168
  settle();
@@ -143,7 +182,7 @@ function createCoordinator(store) {
143
182
  abortInternal(serializedKey, true);
144
183
  },
145
184
  evict(options) {
146
- const now = Date.now();
185
+ const now2 = Date.now();
147
186
  const removable = [];
148
187
  for (const key of store.keys()) {
149
188
  if (inflight.has(key) || store.subscriberCount(key) > 0) {
@@ -158,7 +197,7 @@ function createCoordinator(store) {
158
197
  const victims = /* @__PURE__ */ new Set();
159
198
  if (options.maxAge != null) {
160
199
  for (const item of removable) {
161
- if (now - item.timestamp >= options.maxAge) {
200
+ if (now2 - item.timestamp >= options.maxAge) {
162
201
  victims.add(item.key);
163
202
  }
164
203
  }
@@ -193,6 +232,12 @@ function createCoordinator(store) {
193
232
  abortInternal(key, false);
194
233
  }
195
234
  registered.clear();
235
+ },
236
+ subscribe(listener) {
237
+ listeners.add(listener);
238
+ return () => {
239
+ listeners.delete(listener);
240
+ };
196
241
  }
197
242
  };
198
243
  return coordinator;
@@ -201,6 +246,7 @@ function createCoordinator(store) {
201
246
  // src/store.ts
202
247
  var EMPTY_SNAPSHOT = Object.freeze({
203
248
  data: void 0,
249
+ hasData: false,
204
250
  error: void 0,
205
251
  timestamp: 0,
206
252
  isValidating: false
@@ -222,6 +268,10 @@ function createStore() {
222
268
  return entries.get(key);
223
269
  },
224
270
  set(key, entry) {
271
+ const existing = entries.get(key);
272
+ if (existing && Object.is(existing.data, entry.data) && existing.hasData === entry.hasData && existing.error === entry.error && existing.timestamp === entry.timestamp && existing.isValidating === entry.isValidating) {
273
+ return;
274
+ }
225
275
  entries.set(key, entry);
226
276
  emit(key);
227
277
  },
@@ -323,6 +373,7 @@ function createMutate(store, coordinator) {
323
373
  if (isThenable(next)) {
324
374
  store.set(serialized, {
325
375
  data: currentData,
376
+ hasData: previous?.hasData ?? false,
326
377
  error: previous?.error,
327
378
  timestamp: previous?.timestamp ?? 0,
328
379
  isValidating: true
@@ -330,6 +381,7 @@ function createMutate(store, coordinator) {
330
381
  const resolved = await next;
331
382
  store.set(serialized, {
332
383
  data: resolved,
384
+ hasData: true,
333
385
  error: void 0,
334
386
  timestamp: Date.now(),
335
387
  isValidating: revalidate
@@ -337,6 +389,7 @@ function createMutate(store, coordinator) {
337
389
  } else {
338
390
  store.set(serialized, {
339
391
  data: next,
392
+ hasData: true,
340
393
  error: void 0,
341
394
  timestamp: Date.now(),
342
395
  isValidating: revalidate
@@ -353,6 +406,7 @@ function createMutate(store, coordinator) {
353
406
  const current = store.get(serialized);
354
407
  store.set(serialized, {
355
408
  data: current?.data,
409
+ hasData: current?.hasData ?? false,
356
410
  error,
357
411
  timestamp: current?.timestamp ?? Date.now(),
358
412
  isValidating: false
@@ -384,24 +438,44 @@ function SteddyProvider({
384
438
  cache,
385
439
  children
386
440
  }) {
387
- const value = useMemo(() => {
388
- if (cache) {
389
- hydrateAll(cache, store);
441
+ const hydratedFingerprint = useRef(null);
442
+ useEffect(() => {
443
+ if (!cache) {
444
+ hydratedFingerprint.current = null;
445
+ return;
446
+ }
447
+ const fingerprint = JSON.stringify(cache);
448
+ if (hydratedFingerprint.current === fingerprint) {
449
+ return;
390
450
  }
391
- return {
451
+ hydratedFingerprint.current = fingerprint;
452
+ hydrateAll(cache, store);
453
+ }, [cache, store]);
454
+ const value = useMemo(
455
+ () => ({
392
456
  store,
393
457
  coordinator,
394
458
  mutate: createMutate(store, coordinator)
395
- };
396
- }, [store, coordinator, cache]);
459
+ }),
460
+ [store, coordinator]
461
+ );
397
462
  return /* @__PURE__ */ jsx(SteddyContext.Provider, { value, children });
398
463
  }
464
+ var warnedDefaultOnServer = false;
399
465
  function useSteddyRuntime() {
400
- return useContext(SteddyContext);
466
+ const runtime = useContext(SteddyContext);
467
+ if (process.env.NODE_ENV !== "production" && typeof window === "undefined" && runtime === defaultRuntime && !warnedDefaultOnServer) {
468
+ warnedDefaultOnServer = true;
469
+ console.warn(
470
+ "[steddy] useSteddy on the server without SteddyProvider shares one cache across requests. Use createRuntime() per request."
471
+ );
472
+ }
473
+ return runtime;
401
474
  }
402
475
  function hydrate(key, data, store = defaultStore) {
403
476
  store.set(serializeKey(key), {
404
477
  data,
478
+ hasData: true,
405
479
  error: void 0,
406
480
  timestamp: 0,
407
481
  isValidating: false
@@ -411,7 +485,7 @@ function dump(store = defaultStore) {
411
485
  const snapshot = {};
412
486
  for (const key of store.keys()) {
413
487
  const entry = store.get(key);
414
- if (!entry || entry.data === void 0) {
488
+ if (!entry?.hasData) {
415
489
  continue;
416
490
  }
417
491
  snapshot[key] = { data: entry.data, timestamp: entry.timestamp };
@@ -422,12 +496,23 @@ function hydrateAll(snapshot, store = defaultStore) {
422
496
  for (const [key, payload] of Object.entries(snapshot)) {
423
497
  store.set(key, {
424
498
  data: payload.data,
499
+ hasData: true,
425
500
  error: void 0,
426
- timestamp: 0,
501
+ timestamp: payload.timestamp,
427
502
  isValidating: false
428
503
  });
429
504
  }
430
505
  }
506
+ async function prefetch(key, fetcher, runtime = defaultRuntime) {
507
+ const serialized = serializeKey(key);
508
+ runtime.coordinator.register(serialized, key, fetcher);
509
+ try {
510
+ await runtime.coordinator.revalidate(serialized);
511
+ } catch {
512
+ } finally {
513
+ runtime.coordinator.unregister(serialized);
514
+ }
515
+ }
431
516
  function clear(key, runtime = defaultRuntime) {
432
517
  if (key === void 0) {
433
518
  for (const active of runtime.coordinator.getRegisteredKeys()) {
@@ -466,11 +551,15 @@ function useSteddy(key, fetcher, options) {
466
551
  fetcherRef.current = fetcher;
467
552
  const keyRef = useRef(key);
468
553
  keyRef.current = key;
554
+ const staleTime = options?.staleTime ?? DEDUP_WINDOW_MS;
555
+ const staleTimeRef = useRef(staleTime);
556
+ staleTimeRef.current = staleTime;
469
557
  if (serialized != null && key != null) {
470
558
  coordinator.register(
471
559
  serialized,
472
560
  key,
473
- (k, ctx) => fetcherRef.current(k, ctx)
561
+ (k, ctx) => fetcherRef.current(k, ctx),
562
+ { staleTime: staleTimeRef.current }
474
563
  );
475
564
  }
476
565
  const subscribe = useCallback(
@@ -483,7 +572,8 @@ function useSteddy(key, fetcher, options) {
483
572
  coordinator.register(
484
573
  serialized,
485
574
  originalKey,
486
- (k, ctx) => fetcherRef.current(k, ctx)
575
+ (k, ctx) => fetcherRef.current(k, ctx),
576
+ { staleTime: staleTimeRef.current }
487
577
  );
488
578
  const unsubscribe = store.subscribe(serialized, onStoreChange);
489
579
  if (!coordinator.isInFlight(serialized)) {
@@ -519,15 +609,15 @@ function useSteddy(key, fetcher, options) {
519
609
  const previousRef = useRef(
520
610
  void 0
521
611
  );
522
- if (serialized != null && snapshot.data !== void 0) {
612
+ if (serialized != null && snapshot.hasData) {
523
613
  previousRef.current = { serialized, data: snapshot.data };
524
614
  }
525
- const data = keepPreviousData && serialized != null && snapshot.data === void 0 && snapshot.error == null && previousRef.current != null && previousRef.current.serialized !== serialized ? previousRef.current.data : snapshot.data;
615
+ const data = keepPreviousData && serialized != null && !snapshot.hasData && snapshot.error == null && previousRef.current != null && previousRef.current.serialized !== serialized ? previousRef.current.data : snapshot.data;
526
616
  if (options?.suspense && serialized != null) {
527
617
  if (snapshot.error != null) {
528
618
  throw snapshot.error;
529
619
  }
530
- if (snapshot.data === void 0 && data === void 0) {
620
+ if (!snapshot.hasData && data === void 0) {
531
621
  const waiter = coordinator.getInFlightPromise(serialized) ?? coordinator.revalidate(serialized).catch(() => {
532
622
  });
533
623
  throw waiter;
@@ -536,7 +626,7 @@ function useSteddy(key, fetcher, options) {
536
626
  return {
537
627
  data,
538
628
  error: snapshot.error,
539
- isLoading: serialized != null && data === void 0 && snapshot.error == null,
629
+ isLoading: serialized != null && !snapshot.hasData && snapshot.error == null && data === void 0,
540
630
  isValidating: snapshot.isValidating,
541
631
  mutate: boundMutate
542
632
  };
@@ -627,7 +717,7 @@ function useSteddyInfinite(getKey, fetcher, options) {
627
717
  );
628
718
  const fingerprint = currentPages.map((page) => {
629
719
  const entry = store.getSnapshot(page.serialized);
630
- return `${page.serialized}:${entry.timestamp}:${entry.isValidating ? 1 : 0}:${entry.error == null ? 0 : 1}:${entry.data === void 0 ? 0 : 1}`;
720
+ return `${page.serialized}:${entry.timestamp}:${entry.isValidating ? 1 : 0}:${entry.error == null ? 0 : 1}:${entry.hasData ? 1 : 0}`;
631
721
  }).join("|");
632
722
  if (snapshotRef.current.fingerprint === fingerprint) {
633
723
  return snapshotRef.current;
@@ -644,7 +734,7 @@ function useSteddyInfinite(getKey, fetcher, options) {
644
734
  if (entry.error != null && error === void 0) {
645
735
  error = entry.error;
646
736
  }
647
- if (entry.data === void 0) {
737
+ if (!entry.hasData) {
648
738
  missing = true;
649
739
  break;
650
740
  }
@@ -711,6 +801,7 @@ function useSteddyInfinite(getKey, fetcher, options) {
711
801
  }
712
802
  store.set(page.serialized, {
713
803
  data: pagesValue[index],
804
+ hasData: true,
714
805
  error: void 0,
715
806
  timestamp: Date.now(),
716
807
  isValidating: revalidate
@@ -740,7 +831,7 @@ function useSteddyInfinite(getKey, fetcher, options) {
740
831
  return {
741
832
  data: snapshot.data,
742
833
  error: snapshot.error,
743
- isLoading: first != null && firstEntry?.data === void 0 && firstEntry?.error == null,
834
+ isLoading: first != null && !firstEntry?.hasData && firstEntry?.error == null,
744
835
  isValidating: snapshot.isValidating,
745
836
  size,
746
837
  setSize,
@@ -748,23 +839,37 @@ function useSteddyInfinite(getKey, fetcher, options) {
748
839
  };
749
840
  }
750
841
 
842
+ // src/plugins/subscribedKeys.ts
843
+ function keysWithSubscribers(keys, store) {
844
+ return keys.filter((key) => store.subscriberCount(key) > 0);
845
+ }
846
+
751
847
  // src/plugins/focus.ts
752
- function focusRevalidate(coordinator) {
753
- const onFocus = () => {
754
- for (const key of coordinator.getRegisteredKeys()) {
848
+ function focusRevalidate(coordinator, store) {
849
+ const run = () => {
850
+ if (typeof document !== "undefined" && document.visibilityState !== "visible") {
851
+ return;
852
+ }
853
+ const keys = store ? keysWithSubscribers(coordinator.getRegisteredKeys(), store) : coordinator.getRegisteredKeys();
854
+ for (const key of keys) {
755
855
  void coordinator.revalidate(key);
756
856
  }
757
857
  };
758
- window.addEventListener("focus", onFocus);
858
+ if (typeof document === "undefined") {
859
+ return () => {
860
+ };
861
+ }
862
+ document.addEventListener("visibilitychange", run);
759
863
  return () => {
760
- window.removeEventListener("focus", onFocus);
864
+ document.removeEventListener("visibilitychange", run);
761
865
  };
762
866
  }
763
867
 
764
868
  // src/plugins/reconnect.ts
765
- function reconnectRevalidate(coordinator) {
869
+ function reconnectRevalidate(coordinator, store) {
766
870
  const onOnline = () => {
767
- for (const key of coordinator.getRegisteredKeys()) {
871
+ const keys = store ? keysWithSubscribers(coordinator.getRegisteredKeys(), store) : coordinator.getRegisteredKeys();
872
+ for (const key of keys) {
768
873
  void coordinator.revalidate(key);
769
874
  }
770
875
  };
@@ -841,6 +946,67 @@ function ttlEvict(coordinator, options) {
841
946
  };
842
947
  }
843
948
 
844
- export { DEDUP_WINDOW_MS, SteddyProvider, UNSUBSCRIBE_GRACE_MS, clear, createCoordinator, createMutate, createRuntime, createStore, defaultCoordinator, defaultStore, dump, focusRevalidate, hydrate, hydrateAll, mutate, pollingRevalidate, reconnectRevalidate, retryOnError, serializeKey, ttlEvict, useSteddy, useSteddyInfinite };
949
+ // src/plugins/measure.ts
950
+ function measurePerf(coordinator, onEvent) {
951
+ const snap = {
952
+ starts: 0,
953
+ aborts: 0,
954
+ dedups: 0,
955
+ writes: 0,
956
+ errors: 0,
957
+ totalMs: 0
958
+ };
959
+ const stop = coordinator.subscribe((event) => {
960
+ switch (event.type) {
961
+ case "start":
962
+ snap.starts += 1;
963
+ break;
964
+ case "abort":
965
+ snap.aborts += 1;
966
+ snap.totalMs += event.ms;
967
+ break;
968
+ case "dedup":
969
+ snap.dedups += 1;
970
+ break;
971
+ case "write":
972
+ snap.writes += 1;
973
+ snap.totalMs += event.ms;
974
+ break;
975
+ case "error":
976
+ snap.errors += 1;
977
+ snap.totalMs += event.ms;
978
+ break;
979
+ }
980
+ onEvent?.(event);
981
+ });
982
+ return {
983
+ snapshot: () => ({ ...snap }),
984
+ reset() {
985
+ snap.starts = 0;
986
+ snap.aborts = 0;
987
+ snap.dedups = 0;
988
+ snap.writes = 0;
989
+ snap.errors = 0;
990
+ snap.totalMs = 0;
991
+ },
992
+ stop
993
+ };
994
+ }
995
+
996
+ // src/plugins/attachDefaults.ts
997
+ function attachDefaults(coordinator, store, options) {
998
+ const stops = [
999
+ focusRevalidate(coordinator, store),
1000
+ reconnectRevalidate(coordinator, store),
1001
+ ttlEvict(coordinator, options?.ttl ?? { maxAge: 3e5 })
1002
+ ];
1003
+ return () => {
1004
+ for (const stop of stops) {
1005
+ stop();
1006
+ }
1007
+ };
1008
+ }
1009
+
1010
+ export { DEDUP_WINDOW_MS, SteddyProvider, UNSUBSCRIBE_GRACE_MS, attachDefaults, clear, createCoordinator, createMutate, createRuntime, createStore, defaultCoordinator, defaultStore, dump, focusRevalidate, hydrate, hydrateAll, measurePerf, mutate, pollingRevalidate, prefetch, reconnectRevalidate, retryOnError, serializeKey, ttlEvict, useSteddy, useSteddyInfinite };
845
1011
  //# sourceMappingURL=index.js.map
846
1012
  //# sourceMappingURL=index.js.map