yuustate 0.0.1 → 3.10.0

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/react.js ADDED
@@ -0,0 +1,1040 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var vanilla = require('yuustate/vanilla');
5
+
6
+ const useIsomorphicLayoutEffect = vanilla.isClient ? react.useLayoutEffect : react.useEffect;
7
+
8
+ const getValueByPath = (obj, path) => path.reduce((acc, key) => acc == null ? void 0 : acc[key], obj);
9
+ const isPrefixPath = (candidatePrefix, targetPath) => {
10
+ if (candidatePrefix.length >= targetPath.length) return false;
11
+ for (let i = 0; i < candidatePrefix.length; i++) {
12
+ if (candidatePrefix[i] !== targetPath[i]) return false;
13
+ }
14
+ return true;
15
+ };
16
+ const compressPaths = (paths) => {
17
+ const result = [];
18
+ let prev = null;
19
+ for (let i = paths.length - 1; i >= 0; i--) {
20
+ const current = paths[i];
21
+ if (!prev || !isPrefixPath(current, prev)) result.push(current);
22
+ prev = current;
23
+ }
24
+ return result;
25
+ };
26
+ const useStoreStateProxy = (storeState) => {
27
+ const usedPathsRef = react.useRef([]);
28
+ usedPathsRef.current = [];
29
+ const trackedState = react.useMemo(() => {
30
+ const track = (path) => usedPathsRef.current.push(path);
31
+ const proxyCache = /* @__PURE__ */ new WeakMap();
32
+ const createDeepProxy = (target, path = []) => {
33
+ if (typeof target !== "object" || target === null) {
34
+ return target;
35
+ }
36
+ if (proxyCache.has(target)) {
37
+ return proxyCache.get(target);
38
+ }
39
+ const proxy = new Proxy(target, {
40
+ get(obj, key) {
41
+ const newPath = [...path, key];
42
+ track(newPath);
43
+ const value = obj[key];
44
+ return createDeepProxy(value, newPath);
45
+ }
46
+ });
47
+ proxyCache.set(target, proxy);
48
+ return proxy;
49
+ };
50
+ return createDeepProxy(storeState);
51
+ }, [storeState]);
52
+ return [trackedState, usedPathsRef];
53
+ };
54
+ const NO_INITIAL_VALUE = {};
55
+ const useStoreStateWithInitializer = (store, initialState = NO_INITIAL_VALUE) => {
56
+ const initiatedAt = react.useRef(new WeakMap([[store, 0]]));
57
+ react.useEffect(() => {
58
+ if (initialState === NO_INITIAL_VALUE || initiatedAt.current.get(store)) return;
59
+ store.setState(initialState);
60
+ initiatedAt.current.set(store, Date.now());
61
+ }, [store, initialState]);
62
+ const storeState = store.getState();
63
+ const finalState = initialState === NO_INITIAL_VALUE || initiatedAt.current.get(store) ? storeState : { ...storeState, ...initialState };
64
+ return [finalState, initiatedAt];
65
+ };
66
+ const useStoreState = (store, options = {}) => {
67
+ const [state] = useStoreStateWithInitializer(store, options.initialState);
68
+ const [trackedState, usedPathsRef] = useStoreStateProxy(state);
69
+ const snapshotRef = react.useRef(store.getState());
70
+ const getSnapshot = () => snapshotRef.current;
71
+ react.useSyncExternalStore(
72
+ react.useCallback(
73
+ (onStoreChange) => {
74
+ return store.subscribe((nextState, prevState, changedKeys) => {
75
+ const paths = compressPaths(usedPathsRef.current);
76
+ for (const path of paths) {
77
+ const rootKey = path[0];
78
+ if (!changedKeys.includes(rootKey)) continue;
79
+ const prevVal = getValueByPath(prevState, path);
80
+ const nextVal = getValueByPath(nextState, path);
81
+ if (!Object.is(prevVal, nextVal)) {
82
+ snapshotRef.current = nextState;
83
+ return onStoreChange();
84
+ }
85
+ }
86
+ });
87
+ },
88
+ // False positive (missing dependency: 'usedPathsRef'): usedPathsRef is a ref, no need to add to deps
89
+ // eslint-disable-next-line react-hooks/exhaustive-deps
90
+ [store]
91
+ ),
92
+ getSnapshot,
93
+ getSnapshot
94
+ );
95
+ return trackedState;
96
+ };
97
+
98
+ const createStore = (initialState, options) => {
99
+ const store = vanilla.initStore(initialState, options);
100
+ const useStore = (options2) => useStoreState(store, options2);
101
+ return Object.assign(useStore, store);
102
+ };
103
+
104
+ const createStores = (initialState, options) => {
105
+ const stores = /* @__PURE__ */ new Map();
106
+ const getStore = (key = {}) => {
107
+ const keyHash = vanilla.getHash(key);
108
+ let store;
109
+ if (stores.has(keyHash)) {
110
+ store = stores.get(keyHash);
111
+ } else {
112
+ store = vanilla.initStore(
113
+ initialState,
114
+ options
115
+ // Intentionally using as any: don't want to add generic on `initStore`
116
+ );
117
+ store.key = key;
118
+ store.keyHash = keyHash;
119
+ stores.set(keyHash, store);
120
+ store.delete = () => {
121
+ if (store.getSubscriberCount() > 0) {
122
+ console.warn(
123
+ "Cannot delete store while it still has active subscribers. Unsubscribe all listeners before deleting the store."
124
+ );
125
+ return false;
126
+ }
127
+ store.setState(initialState);
128
+ return stores.delete(keyHash);
129
+ };
130
+ }
131
+ const useStore = (options2) => useStoreState(store, options2);
132
+ return Object.assign(useStore, store);
133
+ };
134
+ return getStore;
135
+ };
136
+
137
+ const INITIAL_STATE$2 = {
138
+ isPending: false,
139
+ isRevalidating: false,
140
+ willRetryAt: void 0,
141
+ isRetrying: false,
142
+ retryCount: 0,
143
+ state: "INITIAL",
144
+ isSuccess: false,
145
+ isError: false,
146
+ data: void 0,
147
+ dataUpdatedAt: void 0,
148
+ dataStaleAt: void 0,
149
+ error: void 0,
150
+ errorUpdatedAt: void 0
151
+ };
152
+ const createQuery = (queryFn, options = {}) => {
153
+ const {
154
+ staleTime = 2500,
155
+ // 2.5 seconds,
156
+ gcTime = 5 * 60 * 1e3,
157
+ // 5 minutes
158
+ revalidateOnFocus = true,
159
+ revalidateOnReconnect = true,
160
+ onSuccess = vanilla.noop,
161
+ onError,
162
+ onSettled = vanilla.noop,
163
+ shouldRetry: shouldRetryFn = (_, s) => s.retryCount === 0 ? [true, 1500] : [false]
164
+ } = options;
165
+ const initialState = { ...INITIAL_STATE$2 };
166
+ const stores = /* @__PURE__ */ new Map();
167
+ const configureStoreEvents = (variableHash) => ({
168
+ ...options,
169
+ onFirstSubscribe: (state, store) => {
170
+ var _a;
171
+ (_a = options.onFirstSubscribe) == null ? void 0 : _a.call(options, state, store);
172
+ const { internal, revalidate: revalidate2 } = store;
173
+ clearTimeout(internal.garbageCollectionTimeoutId);
174
+ if (vanilla.isClient) {
175
+ if (revalidateOnFocus) {
176
+ focusListeners.add(revalidate2);
177
+ if (!focusListenersAdded) {
178
+ window.addEventListener("focus", onWindowFocus);
179
+ focusListenersAdded = true;
180
+ }
181
+ }
182
+ if (revalidateOnReconnect) {
183
+ onlineListeners$1.add(revalidate2);
184
+ if (!onlineListenersAdded) {
185
+ window.addEventListener("online", onWindowOnline$1);
186
+ onlineListenersAdded = true;
187
+ }
188
+ }
189
+ }
190
+ },
191
+ onLastUnsubscribe: (state, store) => {
192
+ var _a;
193
+ (_a = options.onLastUnsubscribe) == null ? void 0 : _a.call(options, state, store);
194
+ const { internal, revalidate: revalidate2 } = store;
195
+ clearTimeout(internal.retryTimeoutId);
196
+ if (internal.retryResolver) {
197
+ store.setState({ willRetryAt: void 0 });
198
+ internal.retryResolver(store.getState());
199
+ internal.retryResolver = void 0;
200
+ }
201
+ internal.garbageCollectionTimeoutId = setTimeout(() => {
202
+ if (internal.promiseResolver || internal.retryResolver) {
203
+ store.setState(initialState);
204
+ } else {
205
+ stores.delete(variableHash);
206
+ }
207
+ }, gcTime);
208
+ if (vanilla.isClient) {
209
+ if (revalidateOnFocus) {
210
+ focusListeners.delete(revalidate2);
211
+ if (focusListeners.size === 0) {
212
+ window.removeEventListener("focus", onWindowFocus);
213
+ focusListenersAdded = false;
214
+ }
215
+ }
216
+ if (revalidateOnReconnect) {
217
+ onlineListeners$1.delete(revalidate2);
218
+ if (onlineListeners$1.size === 0) {
219
+ window.removeEventListener("online", onWindowOnline$1);
220
+ onlineListenersAdded = false;
221
+ }
222
+ }
223
+ }
224
+ }
225
+ });
226
+ const getApis = (store, variable) => ({
227
+ internal: {},
228
+ setInitialData: (data, revalidate2 = false) => {
229
+ const state = store.getState();
230
+ if (state.state === "INITIAL" && state.data === void 0) {
231
+ const { internal } = store;
232
+ if (revalidate2) internal.isInvalidated = true;
233
+ store.setState({
234
+ state: "SUCCESS",
235
+ isSuccess: true,
236
+ data,
237
+ dataUpdatedAt: Date.now()
238
+ });
239
+ return true;
240
+ }
241
+ return false;
242
+ },
243
+ execute: ({ overwriteOngoingExecution = true } = {}) => {
244
+ return execute(store, variable, overwriteOngoingExecution);
245
+ },
246
+ revalidate: ({ overwriteOngoingExecution = true } = {}) => {
247
+ return revalidate(store, variable, overwriteOngoingExecution);
248
+ },
249
+ invalidate: (options2) => {
250
+ const { internal } = store;
251
+ internal.isInvalidated = true;
252
+ if (store.getSubscriberCount() > 0) {
253
+ store.execute(options2);
254
+ return true;
255
+ }
256
+ return false;
257
+ },
258
+ reset: () => {
259
+ var _a, _b;
260
+ const { internal } = store;
261
+ clearTimeout(internal.retryTimeoutId);
262
+ if (internal.retryResolver || internal.promiseResolver) {
263
+ console.debug(
264
+ "Ongoing query execution was ignored due to reset(). The result will not update the store state."
265
+ );
266
+ (_a = internal.promiseResolver) == null ? void 0 : _a.call(internal, initialState);
267
+ (_b = internal.retryResolver) == null ? void 0 : _b.call(internal, initialState);
268
+ internal.promiseResolver = void 0;
269
+ internal.retryResolver = void 0;
270
+ }
271
+ internal.promise = void 0;
272
+ store.setState(initialState);
273
+ },
274
+ delete: () => {
275
+ if (store.getSubscriberCount() > 0) {
276
+ console.warn(
277
+ "Cannot delete query store while it still has active subscribers. Unsubscribe all listeners before deleting the store."
278
+ );
279
+ return false;
280
+ }
281
+ store.reset();
282
+ return stores.delete(store.variableHash);
283
+ },
284
+ optimisticUpdate: (optimisticData) => {
285
+ const { internal, revalidate: revalidate2, rollbackOptimisticUpdate } = store;
286
+ internal.rollbackData = store.getState().data;
287
+ store.setState({ data: optimisticData });
288
+ return { revalidate: revalidate2, rollback: rollbackOptimisticUpdate };
289
+ },
290
+ rollbackOptimisticUpdate: () => {
291
+ const { internal } = store;
292
+ store.setState({ data: internal.rollbackData });
293
+ return internal.rollbackData;
294
+ }
295
+ });
296
+ const execute = async (store, variable, overwriteOngoingExecution = false) => {
297
+ const { internal: _internal } = store;
298
+ const internal = _internal;
299
+ if (!overwriteOngoingExecution && internal.promise) return internal.promise;
300
+ clearTimeout(internal.retryTimeoutId);
301
+ const createPromise = () => {
302
+ const promise = new Promise((resolve) => {
303
+ internal.promiseResolver = resolve;
304
+ const stateBeforeExecute = store.getState();
305
+ store.setState({
306
+ isPending: true,
307
+ isRevalidating: stateBeforeExecute.state === "SUCCESS",
308
+ willRetryAt: void 0,
309
+ isRetrying: !!internal.retryResolver,
310
+ retryCount: internal.retryResolver ? stateBeforeExecute.retryCount + 1 : 0
311
+ });
312
+ queryFn(variable, stateBeforeExecute, store.variableHash).then((data) => {
313
+ var _a;
314
+ if (data === void 0) {
315
+ console.error(
316
+ "Query function returned undefined. Successful responses must not be undefined."
317
+ );
318
+ }
319
+ if (!internal.promiseResolver) return;
320
+ if (promise !== internal.promise) return resolve(internal.promise);
321
+ const now = Date.now();
322
+ store.setState({
323
+ isPending: false,
324
+ isRevalidating: false,
325
+ isRetrying: false,
326
+ retryCount: 0,
327
+ state: "SUCCESS",
328
+ isSuccess: true,
329
+ isError: false,
330
+ data,
331
+ dataUpdatedAt: now,
332
+ dataStaleAt: now + staleTime,
333
+ error: void 0,
334
+ errorUpdatedAt: void 0
335
+ });
336
+ internal.isInvalidated = false;
337
+ internal.rollbackData = data;
338
+ resolve(store.getState());
339
+ (_a = internal.retryResolver) == null ? void 0 : _a.call(internal, store.getState());
340
+ internal.retryResolver = void 0;
341
+ onSuccess(data, variable, stateBeforeExecute);
342
+ onSettled(variable, stateBeforeExecute);
343
+ }).catch((error) => {
344
+ var _a;
345
+ if (!internal.promiseResolver && !internal.retryResolver) return;
346
+ if (promise !== internal.promise) return resolve(internal.promise);
347
+ const nextState = {
348
+ ...store.getState(),
349
+ isPending: false,
350
+ isRevalidating: false,
351
+ isRetrying: false
352
+ };
353
+ const [shouldRetry, retryDelay] = shouldRetryFn(error, nextState);
354
+ const hasSubscriber = store.getSubscriberCount() > 0;
355
+ if (shouldRetry && hasSubscriber) {
356
+ internal.retryResolver = resolve;
357
+ internal.retryTimeoutId = setTimeout(createPromise, retryDelay);
358
+ store.setState({
359
+ isPending: false,
360
+ isRevalidating: false,
361
+ isRetrying: false,
362
+ willRetryAt: Date.now() + retryDelay
363
+ });
364
+ } else {
365
+ store.setState({
366
+ isPending: false,
367
+ isRevalidating: false,
368
+ isRetrying: false,
369
+ retryCount: 0,
370
+ state: store.getState().state === "SUCCESS" ? "SUCCESS_BUT_REVALIDATION_ERROR" : "ERROR",
371
+ isError: true,
372
+ error,
373
+ errorUpdatedAt: Date.now()
374
+ });
375
+ const state = store.getState();
376
+ resolve(state);
377
+ (_a = internal.retryResolver) == null ? void 0 : _a.call(internal, state);
378
+ internal.retryResolver = void 0;
379
+ if (onError) onError(error, variable, stateBeforeExecute);
380
+ else console.error(state);
381
+ onSettled(variable, stateBeforeExecute);
382
+ }
383
+ }).finally(() => {
384
+ if (internal.promise === promise) {
385
+ internal.promise = void 0;
386
+ internal.promiseResolver = void 0;
387
+ }
388
+ });
389
+ });
390
+ internal.promise = promise;
391
+ return promise;
392
+ };
393
+ return createPromise();
394
+ };
395
+ const revalidate = async (store, variable, overwriteOngoingExecution) => {
396
+ const { internal } = store;
397
+ if (!overwriteOngoingExecution && internal.promise) return internal.promise;
398
+ const state = store.getState();
399
+ if (state.dataUpdatedAt) {
400
+ const isFresh = state.dataUpdatedAt + staleTime > Date.now();
401
+ if (isFresh && !internal.isInvalidated) return state;
402
+ }
403
+ return execute(store, variable, overwriteOngoingExecution);
404
+ };
405
+ const getStore = (variable = {}) => {
406
+ const variableHash = vanilla.getHash(variable);
407
+ let store;
408
+ if (stores.has(variableHash)) {
409
+ store = stores.get(variableHash);
410
+ } else {
411
+ store = vanilla.initStore(
412
+ initialState,
413
+ configureStoreEvents(variableHash)
414
+ // Intentionally using as any: don't want to add generic on `initStore`
415
+ );
416
+ store.variableHash = variableHash;
417
+ stores.set(variableHash, store);
418
+ const apis = getApis(store, variable);
419
+ store.setInitialData = apis.setInitialData;
420
+ store.execute = apis.execute;
421
+ store.revalidate = apis.revalidate;
422
+ store.invalidate = apis.invalidate;
423
+ store.reset = apis.reset;
424
+ store.delete = apis.delete;
425
+ store.optimisticUpdate = apis.optimisticUpdate;
426
+ store.rollbackOptimisticUpdate = apis.rollbackOptimisticUpdate;
427
+ store.internal = apis.internal;
428
+ }
429
+ const useStore = (options2 = {}) => {
430
+ const {
431
+ initialData = NO_INITIAL_VALUE,
432
+ initialDataIsStale = false,
433
+ revalidateOnMount = true,
434
+ keepPreviousData
435
+ } = options2;
436
+ const [storeState, initialDataInitiatedAt] = useStoreStateWithInitializer(
437
+ store,
438
+ initialData === NO_INITIAL_VALUE ? void 0 : {
439
+ state: "SUCCESS",
440
+ isSuccess: true,
441
+ data: initialData
442
+ }
443
+ );
444
+ const prevState = react.useRef({});
445
+ let storeStateToBeUsed = storeState;
446
+ if (storeState.state !== "INITIAL") {
447
+ prevState.current = {
448
+ data: storeState.data,
449
+ dataUpdatedAt: storeState.dataUpdatedAt
450
+ };
451
+ } else if (keepPreviousData) {
452
+ storeStateToBeUsed = { ...storeState, ...prevState.current };
453
+ }
454
+ const [trackedState, usedPathsRef] = useStoreStateProxy(
455
+ revalidateOnMount && storeState.state === "INITIAL" ? (
456
+ // Optimize rendering on initial state
457
+ // Do { isPending: true } → result
458
+ // instead of { isPending: false } → { isPending: true } → result
459
+ { ...storeStateToBeUsed, isPending: true }
460
+ ) : storeStateToBeUsed
461
+ );
462
+ const snapshotRef = react.useRef(store.getState());
463
+ const getSnapshot = () => snapshotRef.current;
464
+ react.useSyncExternalStore(
465
+ react.useCallback(
466
+ (onStoreChange) => {
467
+ return store.subscribe((nextState, prevState2, changedKeys) => {
468
+ if (prevState2.state === "INITIAL" && !prevState2.isPending && nextState.isPending && !nextState.isRetrying) {
469
+ return;
470
+ }
471
+ const paths = compressPaths(usedPathsRef.current);
472
+ for (const path of paths) {
473
+ const rootKey = path[0];
474
+ if (!changedKeys.includes(rootKey)) continue;
475
+ const prevVal = getValueByPath(prevState2, path);
476
+ const nextVal = getValueByPath(nextState, path);
477
+ if (!Object.is(prevVal, nextVal)) {
478
+ snapshotRef.current = nextState;
479
+ return onStoreChange();
480
+ }
481
+ }
482
+ });
483
+ },
484
+ // False positive (missing dependency: 'usedPathsRef'): usedPathsRef is a ref, no need to add to deps
485
+ // eslint-disable-next-line react-hooks/exhaustive-deps
486
+ [store]
487
+ ),
488
+ getSnapshot,
489
+ getSnapshot
490
+ );
491
+ react.useEffect(() => {
492
+ if (revalidateOnMount !== false) {
493
+ if (!initialDataIsStale) {
494
+ const dataInitiatedAt = initialDataInitiatedAt.current.get(store);
495
+ if (dataInitiatedAt && dataInitiatedAt + staleTime > Date.now()) {
496
+ return;
497
+ }
498
+ }
499
+ revalidate(store, variable, false);
500
+ }
501
+ }, [store, revalidateOnMount, initialDataIsStale]);
502
+ if (keepPreviousData) {
503
+ !!trackedState.error;
504
+ }
505
+ return trackedState;
506
+ };
507
+ return Object.assign(useStore, {
508
+ ...store,
509
+ setState: (value) => {
510
+ console.debug("Manual setState (not via provided actions) on query store");
511
+ store.setState(value);
512
+ }
513
+ });
514
+ };
515
+ return Object.assign(getStore, {
516
+ /**
517
+ * Executes all query instances.
518
+ *
519
+ * @remarks
520
+ * - Useful for bulk refetching.
521
+ */
522
+ executeAll: (options2) => {
523
+ stores.forEach((store) => store.execute(options2));
524
+ },
525
+ /**
526
+ * Revalidates all query instances.
527
+ *
528
+ * @remarks
529
+ * - Only re-fetches stale queries.
530
+ */
531
+ revalidateAll: (options2) => {
532
+ stores.forEach((store) => store.revalidate(options2));
533
+ },
534
+ /**
535
+ * Invalidates all query instances.
536
+ *
537
+ * @remarks
538
+ * - Marks all queries as invalidated and triggers revalidation if active.
539
+ * - Invalidated queries bypass `staleTime` until successfully executed again.
540
+ */
541
+ invalidateAll: (options2) => {
542
+ stores.forEach((store) => store.invalidate(options2));
543
+ },
544
+ /**
545
+ * Resets all query instances.
546
+ */
547
+ resetAll: () => {
548
+ stores.forEach((store) => store.reset());
549
+ }
550
+ });
551
+ };
552
+ let focusListenersAdded = false;
553
+ const focusListeners = /* @__PURE__ */ new Set();
554
+ const onWindowFocus = () => [...focusListeners].forEach((fn) => fn());
555
+ let onlineListenersAdded = false;
556
+ const onlineListeners$1 = /* @__PURE__ */ new Set();
557
+ const onWindowOnline$1 = () => [...onlineListeners$1].forEach((fn) => fn());
558
+
559
+ const INITIAL_STATE$1 = {
560
+ state: "INITIAL",
561
+ isPending: false,
562
+ isSuccess: false,
563
+ isError: false,
564
+ variable: void 0,
565
+ data: void 0,
566
+ dataUpdatedAt: void 0,
567
+ error: void 0,
568
+ errorUpdatedAt: void 0
569
+ };
570
+ const createMutation = (mutationFn, options = {}) => {
571
+ const { onSuccess = vanilla.noop, onError, onSettled = vanilla.noop } = options;
572
+ const initialState = { ...INITIAL_STATE$1 };
573
+ let ongoingPromise;
574
+ const resolveFns = /* @__PURE__ */ new Set([]);
575
+ const store = vanilla.initStore(initialState, options);
576
+ const useStore = () => useStoreState(store);
577
+ const execute = (variable) => {
578
+ let currentResolveFn;
579
+ const stateBeforeExecute = store.getState();
580
+ if (stateBeforeExecute.isPending) {
581
+ console.warn(
582
+ "A mutation was executed while a previous execution is still pending. The previous execution will be ignored (latest execution wins)."
583
+ );
584
+ }
585
+ store.setState({ isPending: true });
586
+ const promise = new Promise((resolve) => {
587
+ currentResolveFn = resolve;
588
+ mutationFn(variable, stateBeforeExecute).then((data) => {
589
+ if (promise !== ongoingPromise) {
590
+ return resolve({ data, variable });
591
+ }
592
+ store.setState({
593
+ state: "SUCCESS",
594
+ isPending: false,
595
+ isSuccess: true,
596
+ isError: false,
597
+ variable,
598
+ data,
599
+ dataUpdatedAt: Date.now(),
600
+ error: void 0,
601
+ errorUpdatedAt: void 0
602
+ });
603
+ resolve({ data, variable });
604
+ resolveFns.clear();
605
+ onSuccess(data, variable, stateBeforeExecute);
606
+ }).catch((error) => {
607
+ if (promise !== ongoingPromise) {
608
+ return resolve({ error, variable });
609
+ }
610
+ store.setState({
611
+ state: "ERROR",
612
+ isPending: false,
613
+ isSuccess: false,
614
+ isError: true,
615
+ variable,
616
+ data: void 0,
617
+ dataUpdatedAt: void 0,
618
+ error,
619
+ errorUpdatedAt: Date.now()
620
+ });
621
+ resolve({ error, variable });
622
+ resolveFns.clear();
623
+ if (onError) onError(error, variable, stateBeforeExecute);
624
+ else console.error(store.getState());
625
+ }).finally(() => {
626
+ if (promise !== ongoingPromise) return;
627
+ onSettled(variable, stateBeforeExecute);
628
+ ongoingPromise = void 0;
629
+ });
630
+ });
631
+ if (ongoingPromise) resolveFns.forEach((resolveFn) => resolveFn(promise));
632
+ resolveFns.add(currentResolveFn);
633
+ ongoingPromise = promise;
634
+ return promise;
635
+ };
636
+ return Object.assign(useStore, {
637
+ subscribe: store.subscribe,
638
+ getSubscriberCount: store.getSubscriberCount,
639
+ getState: store.getState,
640
+ /**
641
+ * Manually updates the mutation state.
642
+ *
643
+ * @remarks
644
+ * - Intended for advanced use cases.
645
+ * - Prefer using provided mutation actions (`execute`, `reset`) instead.
646
+ */
647
+ setState: (value) => {
648
+ console.debug("Manual setState (not via provided actions) on mutation store");
649
+ store.setState(value);
650
+ },
651
+ /**
652
+ * Executes the mutation.
653
+ *
654
+ * @param variable - Input passed to the mutation function
655
+ *
656
+ * @returns A promise that always resolves with:
657
+ * - `{ data, variable }` on success
658
+ * - `{ error, variable }` on failure
659
+ *
660
+ * @remarks
661
+ * - The promise never rejects to simplify async handling.
662
+ * - If a mutation is already in progress, a warning is logged.
663
+ * - When a new execution starts, all previous pending executions will resolve with the result of the latest execution.
664
+ */
665
+ execute,
666
+ /**
667
+ * Resets the mutation state back to its initial state.
668
+ *
669
+ * @remarks
670
+ * - Does not cancel any ongoing execution.
671
+ * - If an execution is still pending, its result may override the reset state.
672
+ */
673
+ reset: () => {
674
+ if (store.getState().isPending) {
675
+ console.warn(
676
+ "Mutation state was reset while a request is still pending. The request will continue, but its result may override the reset state."
677
+ );
678
+ }
679
+ store.setState(initialState);
680
+ }
681
+ });
682
+ };
683
+
684
+ const useMutation = (mutationFn, options = {}) => {
685
+ const { onSuccess = vanilla.noop, onError, onSettled = vanilla.noop } = options;
686
+ const callbackRef = react.useRef({ onSuccess, onError, onSettled });
687
+ callbackRef.current.onSuccess = onSuccess;
688
+ callbackRef.current.onError = onError;
689
+ callbackRef.current.onSettled = onSettled;
690
+ const stateRef = react.useRef({ ...INITIAL_STATE$1 });
691
+ const [, reRender] = react.useState({});
692
+ const refs = react.useRef({
693
+ mutationFn,
694
+ ongoingPromise: void 0,
695
+ resolveFns: /* @__PURE__ */ new Set()
696
+ });
697
+ refs.current.mutationFn = mutationFn;
698
+ const execute = react.useCallback((variable) => {
699
+ let currentResolveFn;
700
+ const stateBeforeExecute = stateRef.current;
701
+ if (stateBeforeExecute.isPending) {
702
+ console.warn(
703
+ "A mutation was executed while a previous execution is still pending. The previous execution will be ignored (latest execution wins)."
704
+ );
705
+ }
706
+ stateRef.current.isPending = true;
707
+ reRender({});
708
+ const promise = new Promise(
709
+ (resolve) => {
710
+ currentResolveFn = resolve;
711
+ refs.current.mutationFn(variable, stateBeforeExecute).then((data) => {
712
+ if (promise !== refs.current.ongoingPromise) {
713
+ return resolve({ data, variable });
714
+ }
715
+ stateRef.current = {
716
+ state: "SUCCESS",
717
+ isPending: false,
718
+ isSuccess: true,
719
+ isError: false,
720
+ variable,
721
+ data,
722
+ dataUpdatedAt: Date.now(),
723
+ error: void 0,
724
+ errorUpdatedAt: void 0
725
+ };
726
+ reRender({});
727
+ resolve({ data, variable });
728
+ refs.current.resolveFns.clear();
729
+ callbackRef.current.onSuccess(data, variable, stateBeforeExecute);
730
+ }).catch((error) => {
731
+ if (promise !== refs.current.ongoingPromise) {
732
+ return resolve({ error, variable });
733
+ }
734
+ stateRef.current = {
735
+ state: "ERROR",
736
+ isPending: false,
737
+ isSuccess: false,
738
+ isError: true,
739
+ variable,
740
+ data: void 0,
741
+ dataUpdatedAt: void 0,
742
+ error,
743
+ errorUpdatedAt: Date.now()
744
+ };
745
+ reRender({});
746
+ resolve({ error, variable });
747
+ refs.current.resolveFns.clear();
748
+ if (callbackRef.current.onError) {
749
+ callbackRef.current.onError(error, variable, stateBeforeExecute);
750
+ } else {
751
+ console.error(stateRef.current);
752
+ }
753
+ }).finally(() => {
754
+ if (promise !== refs.current.ongoingPromise) return;
755
+ callbackRef.current.onSettled(variable, stateBeforeExecute);
756
+ refs.current.ongoingPromise = void 0;
757
+ });
758
+ }
759
+ );
760
+ if (refs.current.ongoingPromise) {
761
+ refs.current.resolveFns.forEach((resolveFn) => resolveFn(promise));
762
+ }
763
+ refs.current.resolveFns.add(currentResolveFn);
764
+ refs.current.ongoingPromise = promise;
765
+ return promise;
766
+ }, []);
767
+ const reset = react.useCallback(() => {
768
+ if (stateRef.current.isPending) {
769
+ console.warn(
770
+ "Mutation state was reset while a request is still pending. The request will continue, but its result may override the reset state."
771
+ );
772
+ }
773
+ stateRef.current = { ...INITIAL_STATE$1 };
774
+ reRender({});
775
+ }, []);
776
+ const r = [
777
+ stateRef.current,
778
+ {
779
+ execute,
780
+ reset,
781
+ getLatestState: () => stateRef.current
782
+ }
783
+ ];
784
+ return r;
785
+ };
786
+
787
+ const INITIAL_STATE = {
788
+ connectionState: "INITIAL",
789
+ connectingAt: void 0,
790
+ connectedAt: void 0,
791
+ disconnectedAt: void 0,
792
+ state: "INITIAL",
793
+ isSuccess: false,
794
+ isError: false,
795
+ data: void 0,
796
+ dataUpdatedAt: void 0,
797
+ error: void 0,
798
+ errorUpdatedAt: void 0
799
+ };
800
+ const createStream = (connect, disconnect, options = {}) => {
801
+ const {
802
+ disconnectOn = () => 5e3,
803
+ // 5 seconds after any `DisconnectTrigger`
804
+ reconnectOn = () => false
805
+ // no need reconnect if still connected
806
+ } = options.connection || {};
807
+ const {
808
+ gcTime = 5 * 60 * 1e3
809
+ // 5 minutes
810
+ } = options.data || {};
811
+ const initialState = { ...INITIAL_STATE };
812
+ const stores = /* @__PURE__ */ new Map();
813
+ const connections = /* @__PURE__ */ new WeakMap();
814
+ const disconnectFns = /* @__PURE__ */ new WeakMap();
815
+ const disconnectTimeoutIds = /* @__PURE__ */ new WeakMap();
816
+ const clearAllTimeouts = (store) => {
817
+ const gcTimeoutId = gcTimeoutIds.get(store);
818
+ if (gcTimeoutId) {
819
+ clearTimeout(gcTimeoutId);
820
+ gcTimeoutIds.delete(store);
821
+ }
822
+ const disconnectTimeoutIds_ = disconnectTimeoutIds.get(store);
823
+ if (disconnectTimeoutIds_) {
824
+ clearTimeout(disconnectTimeoutIds_["last-unsubscribe"]);
825
+ clearTimeout(disconnectTimeoutIds_["document-hidden"]);
826
+ clearTimeout(disconnectTimeoutIds_.offline);
827
+ }
828
+ };
829
+ const gcTimeoutIds = /* @__PURE__ */ new WeakMap();
830
+ const configureStoreEvents = () => ({
831
+ ...options,
832
+ onFirstSubscribe: (state, store) => {
833
+ var _a, _b;
834
+ clearTimeout((_a = disconnectTimeoutIds.get(store)) == null ? void 0 : _a["last-unsubscribe"]);
835
+ (_b = options.onFirstSubscribe) == null ? void 0 : _b.call(options, state, store);
836
+ triggerReconnect(store, "first-subscribe");
837
+ if (vanilla.isClient) {
838
+ visibilityChangeListeners.add(triggers.visibilityChange);
839
+ onlineListeners.add(triggers.online);
840
+ offlineListeners.add(triggers.offline);
841
+ if (!listenersAdded) {
842
+ document.addEventListener("visibilitychange", onVisibilityChange);
843
+ window.addEventListener("online", onWindowOnline);
844
+ window.addEventListener("offline", onWindowOffline);
845
+ listenersAdded = true;
846
+ }
847
+ }
848
+ },
849
+ onLastUnsubscribe: (state, store) => {
850
+ var _a;
851
+ (_a = options.onLastUnsubscribe) == null ? void 0 : _a.call(options, state, store);
852
+ triggerDisconnect(store, "last-unsubscribe");
853
+ if (vanilla.isClient) {
854
+ visibilityChangeListeners.delete(triggers.visibilityChange);
855
+ onlineListeners.delete(triggers.online);
856
+ offlineListeners.delete(triggers.offline);
857
+ if (visibilityChangeListeners.size === 0) {
858
+ document.removeEventListener("visibilitychange", onVisibilityChange);
859
+ window.removeEventListener("online", onWindowOnline);
860
+ window.removeEventListener("offline", onWindowOffline);
861
+ listenersAdded = false;
862
+ }
863
+ }
864
+ }
865
+ });
866
+ const disconnectAndSetState = (store, showLog = false) => {
867
+ var _a;
868
+ if (store.getSubscriberCount() && showLog) {
869
+ console.log("Stream disconnected while there is subscriber");
870
+ }
871
+ clearAllTimeouts(store);
872
+ (_a = disconnectFns.get(store)) == null ? void 0 : _a();
873
+ if (store.getState().connectionState !== "INITIAL") {
874
+ store.setState({
875
+ connectionState: "DISCONNECTED",
876
+ disconnectedAt: Date.now()
877
+ });
878
+ }
879
+ connections.delete(store);
880
+ disconnectFns.delete(store);
881
+ gcTimeoutIds.set(
882
+ store,
883
+ setTimeout(() => {
884
+ if (store.getSubscriberCount() === 0) store.delete();
885
+ }, gcTime)
886
+ );
887
+ };
888
+ const getStore = (variable = {}) => {
889
+ const variableHash = vanilla.getHash(variable);
890
+ let store;
891
+ if (stores.has(variableHash)) {
892
+ store = stores.get(variableHash);
893
+ } else {
894
+ store = vanilla.initStore(
895
+ initialState,
896
+ configureStoreEvents()
897
+ // Intentionally using as any: don't want to add generic on `initStore`
898
+ );
899
+ store.variableHash = variableHash;
900
+ stores.set(variableHash, store);
901
+ store.connection = {};
902
+ store.connection.get = () => connections.get(store);
903
+ store.connection.reconnect = () => {
904
+ clearAllTimeouts(store);
905
+ const { connectionState } = store.getState();
906
+ if (connectionState === "CONNECTING") return;
907
+ const prevDisconnect = disconnectFns.get(store);
908
+ if (prevDisconnect) {
909
+ prevDisconnect();
910
+ disconnectFns.delete(store);
911
+ }
912
+ store.setState({
913
+ connectionState: "CONNECTING",
914
+ connectingAt: Date.now(),
915
+ connectedAt: void 0,
916
+ disconnectedAt: void 0
917
+ });
918
+ const connection = connect(variable, {
919
+ connected: () => {
920
+ store.setState({
921
+ connectionState: "CONNECTED",
922
+ connectedAt: Date.now(),
923
+ disconnectedAt: void 0
924
+ });
925
+ },
926
+ data: (reducer) => {
927
+ store.setState((prev) => {
928
+ var _a;
929
+ return {
930
+ connectionState: "CONNECTED",
931
+ connectedAt: (_a = prev.connectedAt) != null ? _a : Date.now(),
932
+ state: "SUCCESS",
933
+ isSuccess: true,
934
+ isError: false,
935
+ data: reducer(prev.data),
936
+ dataUpdatedAt: Date.now(),
937
+ error: void 0,
938
+ errorUpdatedAt: void 0
939
+ };
940
+ });
941
+ },
942
+ error: (error) => {
943
+ store.setState((prev) => ({
944
+ state: prev.state === "SUCCESS" ? "SUCCESS_BUT_THEN_ERROR" : "ERROR",
945
+ isError: true,
946
+ error,
947
+ errorUpdatedAt: Date.now()
948
+ }));
949
+ }
950
+ });
951
+ connections.set(store, connection);
952
+ disconnectFns.set(store, () => disconnect(connection));
953
+ };
954
+ store.connection.disconnect = () => disconnectAndSetState(store, true);
955
+ store.data = {};
956
+ store.data.reset = () => {
957
+ store.setState({
958
+ state: "INITIAL",
959
+ isSuccess: false,
960
+ isError: false,
961
+ data: void 0,
962
+ dataUpdatedAt: void 0,
963
+ error: void 0,
964
+ errorUpdatedAt: void 0
965
+ });
966
+ };
967
+ store.delete = () => {
968
+ if (store.getSubscriberCount() > 0) {
969
+ console.warn(
970
+ "Cannot delete store while it still has active subscribers. Unsubscribe all listeners before deleting the store."
971
+ );
972
+ return false;
973
+ }
974
+ clearAllTimeouts(store);
975
+ store.setState(initialState);
976
+ return stores.delete(variableHash);
977
+ };
978
+ }
979
+ const useStore = (options2) => useStoreState(store, {
980
+ initialState: { data: options2 == null ? void 0 : options2.initialData }
981
+ });
982
+ return Object.assign(useStore, {
983
+ ...store,
984
+ setState: (value) => {
985
+ console.debug("Manual setState (not via provided actions) on stream store");
986
+ store.setState(value);
987
+ }
988
+ });
989
+ };
990
+ const triggerReconnect = (store, trigger) => {
991
+ clearAllTimeouts(store);
992
+ const { connectionState } = store.getState();
993
+ if (connectionState === "INITIAL" || connectionState === "DISCONNECTED") {
994
+ queueMicrotask(() => {
995
+ store.connection.reconnect();
996
+ });
997
+ return;
998
+ }
999
+ const shouldReconnect = reconnectOn(trigger, store.getState());
1000
+ if (shouldReconnect) store.connection.reconnect();
1001
+ };
1002
+ const triggerDisconnect = (store, trigger) => {
1003
+ const disconnectDelay = disconnectOn(trigger, store.getState());
1004
+ if (typeof disconnectDelay !== "number") return;
1005
+ if (!disconnectTimeoutIds.has(store)) disconnectTimeoutIds.set(store, {});
1006
+ const disconnectTimeoutIds_ = disconnectTimeoutIds.get(store);
1007
+ disconnectTimeoutIds_[trigger] = setTimeout(() => {
1008
+ disconnectAndSetState(store);
1009
+ }, disconnectDelay);
1010
+ };
1011
+ const triggers = {
1012
+ visibilityChange: () => {
1013
+ console.info("visibilityChange", document.visibilityState);
1014
+ if (document.visibilityState === "visible") {
1015
+ stores.forEach((store) => triggerReconnect(store, "document-visible"));
1016
+ } else {
1017
+ stores.forEach((store) => triggerDisconnect(store, "document-hidden"));
1018
+ }
1019
+ },
1020
+ online: () => stores.forEach((store) => triggerReconnect(store, "online")),
1021
+ offline: () => stores.forEach((store) => triggerDisconnect(store, "offline"))
1022
+ };
1023
+ return getStore;
1024
+ };
1025
+ const visibilityChangeListeners = /* @__PURE__ */ new Set();
1026
+ const onVisibilityChange = () => [...visibilityChangeListeners].forEach((fn) => fn());
1027
+ const onlineListeners = /* @__PURE__ */ new Set();
1028
+ const onWindowOnline = () => [...onlineListeners].forEach((fn) => fn());
1029
+ const offlineListeners = /* @__PURE__ */ new Set();
1030
+ const onWindowOffline = () => [...offlineListeners].forEach((fn) => fn());
1031
+ let listenersAdded = false;
1032
+
1033
+ exports.createMutation = createMutation;
1034
+ exports.createQuery = createQuery;
1035
+ exports.createStore = createStore;
1036
+ exports.createStores = createStores;
1037
+ exports.createStream = createStream;
1038
+ exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
1039
+ exports.useMutation = useMutation;
1040
+ exports.useStoreState = useStoreState;