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