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 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. Plugins (`focusRevalidate`, `reconnectRevalidate`, `pollingRevalidate`, `retryOnError`) are opt-in named exports.
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 { defaultCoordinator, focusRevalidate, hydrate, clear } from "steddy";
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
- hydrate("user", { name: "Ada" });
44
- focusRevalidate(defaultCoordinator);
45
- clear("user");
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
- Need an isolated cache (tests, multiple trees, SSR): wrap with `SteddyProvider` and pass `createStore()` + `createCoordinator(store)`, or `createRuntime()`. Pass `cache={dump(store)}` across an RSC boundary. `{ suspense: true }` throws the in-flight waiter. `{ keepPreviousData: true }` keeps the last value on screen while a new key loads. `useSteddyInfinite` keeps one cache entry per page; `mutate` writes every page, and `getKey` stops when a later page would reuse an earlier key. `ttlEvict` drops unused keys.
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, { key, fetcher });
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
- if (entry && !entry.isValidating && entry.error == null && entry.data !== void 0 && Date.now() - entry.timestamp < DEDUP_WINDOW_MS) {
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 now = Date.now();
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 (now - item.timestamp >= options.maxAge) {
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 value = react.useMemo(() => {
390
- if (cache) {
391
- hydrateAll(cache, store);
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
- return {
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
- }, [store, coordinator, cache]);
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
- return react.useContext(SteddyContext);
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 || entry.data === void 0) {
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: 0,
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.data !== void 0) {
614
+ if (serialized != null && snapshot.hasData) {
525
615
  previousRef.current = { serialized, data: snapshot.data };
526
616
  }
527
- const data = keepPreviousData && serialized != null && snapshot.data === void 0 && snapshot.error == null && previousRef.current != null && previousRef.current.serialized !== serialized ? previousRef.current.data : snapshot.data;
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.data === void 0 && data === void 0) {
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 && data === void 0 && snapshot.error == 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.data === void 0 ? 0 : 1}`;
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.data === void 0) {
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?.data === void 0 && firstEntry?.error == null,
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 onFocus = () => {
756
- for (const key of coordinator.getRegisteredKeys()) {
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
- window.addEventListener("focus", onFocus);
860
+ if (typeof document === "undefined") {
861
+ return () => {
862
+ };
863
+ }
864
+ document.addEventListener("visibilitychange", run);
761
865
  return () => {
762
- window.removeEventListener("focus", onFocus);
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
- for (const key of coordinator.getRegisteredKeys()) {
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;