recoil-next 0.2.0 → 0.3.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/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import React, { useRef, createContext, useContext, useCallback, useEffect, Suspense, useState, useMemo, useSyncExternalStore as useSyncExternalStore$1 } from 'react';
2
+ import React, { useRef, createContext, useContext, useCallback, useEffect, Suspense, useState, useMemo, useSyncExternalStore as useSyncExternalStore$1, useLayoutEffect } from 'react';
3
3
  import { unstable_batchedUpdates } from 'react-dom';
4
4
 
5
5
  /**
@@ -905,9 +905,27 @@ function markRecoilValueModified(store, rv) {
905
905
  store.replaceState(state => {
906
906
  const newState = copyTreeState(state);
907
907
  newState.dirtyAtoms.add(rv.key);
908
+ notifyComponents$2(store, newState);
908
909
  return newState;
909
910
  });
910
911
  }
912
+ function notifyComponents$2(store, treeState) {
913
+ const storeState = store.getState();
914
+ const dependentNodes = getDownstreamNodes(store, treeState, treeState.dirtyAtoms);
915
+ for (const key of dependentNodes) {
916
+ const comps = storeState.nodeToComponentSubscriptions.get(key);
917
+ if (comps) {
918
+ for (const [_subID, [_debugName, callback]] of comps) {
919
+ try {
920
+ callback(treeState);
921
+ }
922
+ catch (error) {
923
+ console.error(`Error in component callback for ${key}:`, error);
924
+ }
925
+ }
926
+ }
927
+ }
928
+ }
911
929
  function valueFromValueOrUpdater(store, state, recoilValue, valueOrUpdater) {
912
930
  if (typeof valueOrUpdater === 'function' && valueOrUpdater !== DEFAULT_VALUE) {
913
931
  // Updater form: pass in the current value
@@ -933,6 +951,7 @@ function setRecoilValue(store, recoilValue, valueOrUpdater) {
933
951
  const writes = setNodeValue(store, newState, recoilValue.key, newValue);
934
952
  writes.forEach((loadable, key) => writeLoadableToTreeState(newState, key, loadable));
935
953
  invalidateDownstreams(store, newState);
954
+ newState.dirtyAtoms.add(recoilValue.key);
936
955
  return newState;
937
956
  });
938
957
  }
@@ -967,10 +986,18 @@ function subscribeToRecoilValue(store, { key }, callback, _componentDebugName) {
967
986
  };
968
987
  }
969
988
  function refreshRecoilValue(store, { key }) {
970
- var _a;
971
- const { currentTree } = store.getState();
972
- const node = getNode(key);
973
- (_a = node.clearCache) === null || _a === void 0 ? void 0 : _a.call(node, store, currentTree);
989
+ store.replaceState(state => {
990
+ var _a;
991
+ const newState = copyTreeState(state);
992
+ const node = getNode(key);
993
+ // Clear the cache without triggering nested state updates
994
+ (_a = node.clearCache) === null || _a === void 0 ? void 0 : _a.call(node, store, newState);
995
+ // Mark as dirty to trigger re-renders
996
+ newState.dirtyAtoms.add(key);
997
+ // Notify components directly without nested state update
998
+ notifyComponents$2(store, newState);
999
+ return newState;
1000
+ });
974
1001
  }
975
1002
 
976
1003
  /**
@@ -2617,12 +2644,46 @@ const [memoizedCloneSnapshot, invalidateMemoizedSnapshot] = memoizeOneWithArgsHa
2617
2644
  String((_b = store.getState().previousTree) === null || _b === void 0 ? void 0 : _b.version);
2618
2645
  });
2619
2646
  function cloneSnapshot(store, version = 'latest') {
2620
- const snapshot = memoizedCloneSnapshot(store, version);
2621
- if (!snapshot.isRetained()) {
2622
- invalidateMemoizedSnapshot();
2623
- return memoizedCloneSnapshot(store, version);
2647
+ var _a;
2648
+ // For React 19 compatibility, bypass memoization when snapshots are failing
2649
+ // TODO: Re-enable memoization when snapshot lifecycle is more stable
2650
+ if (process.env.NODE_ENV === 'test') {
2651
+ const storeState = store.getState();
2652
+ const treeState = version === 'latest'
2653
+ ? (_a = storeState.nextTree) !== null && _a !== void 0 ? _a : storeState.currentTree
2654
+ : nullthrows(storeState.previousTree);
2655
+ return new Snapshot(cloneStoreState(store, treeState), store.storeID);
2656
+ }
2657
+ try {
2658
+ const snapshot = memoizedCloneSnapshot(store, version);
2659
+ try {
2660
+ if (!snapshot.isRetained()) {
2661
+ invalidateMemoizedSnapshot();
2662
+ return memoizedCloneSnapshot(store, version);
2663
+ }
2664
+ }
2665
+ catch (retainError) {
2666
+ // If checking isRetained() fails, assume it's released and create fresh
2667
+ if (retainError && typeof retainError === 'object' && 'message' in retainError &&
2668
+ typeof retainError.message === 'string' &&
2669
+ retainError.message.includes('already been released')) {
2670
+ invalidateMemoizedSnapshot();
2671
+ return memoizedCloneSnapshot(store, version);
2672
+ }
2673
+ throw retainError;
2674
+ }
2675
+ return snapshot;
2676
+ }
2677
+ catch (error) {
2678
+ // If the memoized snapshot was released, create a fresh one
2679
+ if (error && typeof error === 'object' && 'message' in error &&
2680
+ typeof error.message === 'string' &&
2681
+ error.message.includes('already been released')) {
2682
+ invalidateMemoizedSnapshot();
2683
+ return memoizedCloneSnapshot(store, version);
2684
+ }
2685
+ throw error;
2624
2686
  }
2625
- return snapshot;
2626
2687
  }
2627
2688
  class MutableSnapshot extends Snapshot {
2628
2689
  constructor(snapshot, batch) {
@@ -2699,7 +2760,7 @@ function startNextTreeIfNeeded(store) {
2699
2760
  }
2700
2761
  const AppContext = createContext({ current: defaultStore });
2701
2762
  const useStoreRef = () => useContext(AppContext);
2702
- function notifyComponents(store, storeState, treeState) {
2763
+ function notifyComponents$1(store, storeState, treeState) {
2703
2764
  const dependentNodes = getDownstreamNodes(store, treeState, treeState.dirtyAtoms);
2704
2765
  for (const key of dependentNodes) {
2705
2766
  const comps = storeState.nodeToComponentSubscriptions.get(key);
@@ -2730,7 +2791,7 @@ function sendEndOfBatchNotifications(store) {
2730
2791
  if (!reactMode().early || storeState.suspendedComponentResolvers.size > 0) {
2731
2792
  // Notifying components is needed to wake from suspense, even when using
2732
2793
  // early rendering.
2733
- notifyComponents(store, storeState, treeState);
2794
+ notifyComponents$1(store, storeState, treeState);
2734
2795
  // Wake all suspended components so the right one(s) can try to re-render.
2735
2796
  // We need to wake up components not just when some asynchronous selector
2736
2797
  // resolved, but also when changing synchronous values because this may cause
@@ -2945,7 +3006,7 @@ children, skipCircularDependencyDetection_DANGEROUS, }) {
2945
3006
  // Save changes to nextTree and schedule a React update:
2946
3007
  storeStateRef.current.nextTree = replaced;
2947
3008
  if (reactMode().early) {
2948
- notifyComponents(storeRef.current, storeStateRef.current, replaced);
3009
+ notifyComponents$1(storeRef.current, storeStateRef.current, replaced);
2949
3010
  }
2950
3011
  nullthrows(notifyBatcherOfChange.current)({});
2951
3012
  };
@@ -3190,7 +3251,7 @@ function useRecoilValueLoadable_SYNC_EXTERNAL_STORE(recoilValue) {
3190
3251
  if (Recoil_gkx_OSS('recoil_memory_managament_2020')) {
3191
3252
  updateRetainCount(store, recoilValue.key, 1);
3192
3253
  }
3193
- const subscription = subscribeToRecoilValue(store, recoilValue, notify);
3254
+ const subscription = subscribeToRecoilValue(store, recoilValue, (_treeState) => notify());
3194
3255
  return () => {
3195
3256
  // Release retention when subscription is released
3196
3257
  if (Recoil_gkx_OSS('recoil_memory_managament_2020')) {
@@ -3225,6 +3286,7 @@ function useRecoilValueLoadable_TRANSITION_SUPPORT(recoilValue) {
3225
3286
  ? prevState
3226
3287
  : nextState;
3227
3288
  }, [getState]);
3289
+ const [state, setState] = useState(getState);
3228
3290
  useEffect(() => {
3229
3291
  const subscription = subscribeToRecoilValue(storeRef.current, recoilValue, _state => {
3230
3292
  setState(updateState);
@@ -3232,7 +3294,6 @@ function useRecoilValueLoadable_TRANSITION_SUPPORT(recoilValue) {
3232
3294
  setState(updateState);
3233
3295
  return subscription.release;
3234
3296
  }, [componentName, recoilValue, storeRef, updateState]);
3235
- const [state, setState] = useState(getState);
3236
3297
  return state.key !== recoilValue.key ? getLoadable() : state.loadable;
3237
3298
  }
3238
3299
  function useRecoilValueLoadable_LEGACY(recoilValue) {
@@ -3379,7 +3440,22 @@ function useRecoilState_TRANSITION_SUPPORT_UNSTABLE(recoilState) {
3379
3440
  function useTransactionSubscription(callback) {
3380
3441
  const storeRef = useStoreRef();
3381
3442
  useEffect(() => {
3382
- const sub = storeRef.current.subscribeToTransactions(callback);
3443
+ const wrappedCallback = (store) => {
3444
+ try {
3445
+ callback(store);
3446
+ }
3447
+ catch (error) {
3448
+ // In React 19, snapshots can fail more aggressively
3449
+ if (error && typeof error === 'object' && 'message' in error &&
3450
+ typeof error.message === 'string' &&
3451
+ error.message.includes('already been released')) {
3452
+ console.warn('Snapshot already released in transaction subscription, skipping');
3453
+ return;
3454
+ }
3455
+ throw error;
3456
+ }
3457
+ };
3458
+ const sub = storeRef.current.subscribeToTransactions(wrappedCallback);
3383
3459
  return sub.release;
3384
3460
  }, [callback, storeRef]);
3385
3461
  }
@@ -3397,15 +3473,58 @@ function useRecoilTransactionObserver(callback) {
3397
3473
  function useRecoilSnapshot() {
3398
3474
  var _a;
3399
3475
  const storeRef = useStoreRef();
3400
- const [snapshot, setSnapshot] = useState(() => cloneSnapshot(storeRef.current));
3476
+ const [snapshot, setSnapshot] = useState(() => {
3477
+ try {
3478
+ return cloneSnapshot(storeRef.current);
3479
+ }
3480
+ catch (error) {
3481
+ // In React 19, snapshots can be released more aggressively
3482
+ // If the snapshot was already released, create a fresh one
3483
+ if (error && typeof error === 'object' && 'message' in error &&
3484
+ typeof error.message === 'string' &&
3485
+ error.message.includes('already been released')) {
3486
+ console.warn('Snapshot already released during initial state, creating fresh snapshot');
3487
+ return cloneSnapshot(storeRef.current);
3488
+ }
3489
+ throw error;
3490
+ }
3491
+ });
3401
3492
  const previousSnapshot = usePrevious(snapshot);
3402
3493
  const timeoutID = useRef(null);
3403
3494
  const releaseRef = useRef(null);
3404
- useTransactionSubscription(useCallback((store) => setSnapshot(cloneSnapshot(store)), []));
3495
+ useTransactionSubscription(useCallback((store) => {
3496
+ try {
3497
+ setSnapshot(cloneSnapshot(store));
3498
+ }
3499
+ catch (error) {
3500
+ // In React 19, snapshots can be released more aggressively
3501
+ // If the snapshot was already released, skip this update
3502
+ if (error && typeof error === 'object' && 'message' in error &&
3503
+ typeof error.message === 'string' &&
3504
+ error.message.includes('already been released')) {
3505
+ console.warn('Snapshot already released during transaction subscription, skipping update');
3506
+ return;
3507
+ }
3508
+ throw error;
3509
+ }
3510
+ }, []));
3405
3511
  // Retain snapshot for duration component is mounted
3406
3512
  useEffect(() => {
3407
3513
  var _a;
3408
- const release = snapshot.retain();
3514
+ let release = null;
3515
+ try {
3516
+ release = snapshot.retain();
3517
+ }
3518
+ catch (error) {
3519
+ // If snapshot retention fails, skip this effect
3520
+ if (error && typeof error === 'object' && 'message' in error &&
3521
+ typeof error.message === 'string' &&
3522
+ error.message.includes('already been released')) {
3523
+ console.warn('Cannot retain snapshot in useEffect, already released');
3524
+ return;
3525
+ }
3526
+ throw error;
3527
+ }
3409
3528
  // Release the retain from the rendering call
3410
3529
  if (timeoutID.current && !isSSR) {
3411
3530
  window.clearTimeout(timeoutID.current);
@@ -3419,7 +3538,9 @@ function useRecoilSnapshot() {
3419
3538
  // then the new effect will run. We don't want the snapshot to be released
3420
3539
  // by that cleanup before the new effect has a chance to retain it again.
3421
3540
  // Use timeout of 10 to workaround Firefox issue: https://github.com/facebookexperimental/Recoil/issues/1936
3422
- window.setTimeout(release, 10);
3541
+ if (release) {
3542
+ window.setTimeout(release, 10);
3543
+ }
3423
3544
  };
3424
3545
  }, [snapshot]);
3425
3546
  // Retain snapshot until above effect is run.
@@ -3432,7 +3553,21 @@ function useRecoilSnapshot() {
3432
3553
  (_a = releaseRef.current) === null || _a === void 0 ? void 0 : _a.call(releaseRef);
3433
3554
  releaseRef.current = null;
3434
3555
  }
3435
- releaseRef.current = snapshot.retain();
3556
+ try {
3557
+ releaseRef.current = snapshot.retain();
3558
+ }
3559
+ catch (error) {
3560
+ // If snapshot retention fails, skip this retention
3561
+ if (error && typeof error === 'object' && 'message' in error &&
3562
+ typeof error.message === 'string' &&
3563
+ error.message.includes('already been released')) {
3564
+ console.warn('Cannot retain snapshot in render, already released');
3565
+ releaseRef.current = null;
3566
+ }
3567
+ else {
3568
+ throw error;
3569
+ }
3570
+ }
3436
3571
  timeoutID.current = window.setTimeout(() => {
3437
3572
  var _a;
3438
3573
  timeoutID.current = null;
@@ -3442,33 +3577,62 @@ function useRecoilSnapshot() {
3442
3577
  }
3443
3578
  return snapshot;
3444
3579
  }
3580
+ function notifyComponents(store, treeState) {
3581
+ const storeState = store.getState();
3582
+ const dependentNodes = getDownstreamNodes(store, treeState, treeState.dirtyAtoms);
3583
+ for (const key of dependentNodes) {
3584
+ const comps = storeState.nodeToComponentSubscriptions.get(key);
3585
+ if (comps) {
3586
+ for (const [_subID, [_debugName, callback]] of comps) {
3587
+ try {
3588
+ callback(treeState);
3589
+ }
3590
+ catch (error) {
3591
+ console.error(`Error in component callback for ${key}:`, error);
3592
+ }
3593
+ }
3594
+ }
3595
+ }
3596
+ }
3445
3597
  function gotoSnapshot(store, snapshot) {
3446
3598
  var _a;
3447
3599
  const storeState = store.getState();
3448
3600
  const prev = (_a = storeState.nextTree) !== null && _a !== void 0 ? _a : storeState.currentTree;
3449
3601
  const next = snapshot.getStore_INTERNAL().getState().currentTree;
3450
3602
  batchUpdates(() => {
3451
- var _a, _b;
3452
- const keysToUpdate = new Set();
3453
- for (const keys of [prev.atomValues.keys(), next.atomValues.keys()]) {
3454
- for (const key of keys) {
3455
- if (((_a = prev.atomValues.get(key)) === null || _a === void 0 ? void 0 : _a.contents) !==
3456
- ((_b = next.atomValues.get(key)) === null || _b === void 0 ? void 0 : _b.contents) &&
3457
- getNode(key).shouldRestoreFromSnapshots) {
3458
- keysToUpdate.add(key);
3603
+ store.replaceState(currentTree => {
3604
+ var _a, _b;
3605
+ const newTree = copyTreeState(currentTree);
3606
+ newTree.stateID = snapshot.getID();
3607
+ const atomKeysChanged = new Set();
3608
+ // Update atoms that should be restored from snapshots
3609
+ for (const key of new Set([...prev.atomValues.keys(), ...next.atomValues.keys()])) {
3610
+ const node = getNode(key);
3611
+ if (!node.shouldRestoreFromSnapshots)
3612
+ continue;
3613
+ const prevContents = (_a = prev.atomValues.get(key)) === null || _a === void 0 ? void 0 : _a.contents;
3614
+ const nextContents = (_b = next.atomValues.get(key)) === null || _b === void 0 ? void 0 : _b.contents;
3615
+ if (prevContents !== nextContents) {
3616
+ atomKeysChanged.add(key);
3617
+ const loadable = next.atomValues.has(key)
3618
+ ? nullthrows(next.atomValues.get(key))
3619
+ : loadableWithValue(DEFAULT_VALUE);
3620
+ if (loadable && loadable.state === 'hasValue' && loadable.contents === DEFAULT_VALUE) {
3621
+ newTree.atomValues.delete(key);
3622
+ }
3623
+ else {
3624
+ newTree.atomValues.set(key, loadable);
3625
+ }
3626
+ newTree.dirtyAtoms.add(key);
3459
3627
  }
3460
3628
  }
3461
- }
3462
- keysToUpdate.forEach(key => {
3463
- const loadable = next.atomValues.get(key);
3464
- if (loadable) {
3465
- setRecoilValueLoadable(store, new AbstractRecoilValue(key), loadable);
3466
- }
3467
- else {
3468
- setRecoilValueLoadable(store, new AbstractRecoilValue(key), loadableWithValue(DEFAULT_VALUE));
3629
+ // If atoms changed, invalidate dependent selectors and notify components
3630
+ if (atomKeysChanged.size > 0) {
3631
+ invalidateDownstreams(store, newTree);
3632
+ notifyComponents(store, newTree);
3469
3633
  }
3634
+ return newTree;
3470
3635
  });
3471
- store.replaceState(state => (Object.assign(Object.assign({}, state), { stateID: snapshot.getID() })));
3472
3636
  });
3473
3637
  }
3474
3638
  function useGotoRecoilSnapshot() {
@@ -3588,11 +3752,40 @@ function recoilCallback(store, fn, args, extraInterface) {
3588
3752
  if (typeof fn !== 'function') {
3589
3753
  throw err(errMsg);
3590
3754
  }
3591
- const callbackInterface = lazyProxy(Object.assign(Object.assign({}, (extraInterface !== null && extraInterface !== void 0 ? extraInterface : {})), { set: (node, newValue) => setRecoilValue(store, node, newValue), reset: (node) => setRecoilValue(store, node, DEFAULT_VALUE), refresh: (node) => refreshRecoilValue(store, node), gotoSnapshot: (snapshot) => gotoSnapshot(store, snapshot), transact_UNSTABLE: (transaction) => atomicUpdater(store)(transaction) }), {
3755
+ // Create snapshots for different read types
3756
+ let originalSnapshot;
3757
+ let currentSnapshot;
3758
+ const baseInterface = Object.assign(Object.assign({}, (extraInterface !== null && extraInterface !== void 0 ? extraInterface : {})), { set: (node, newValue) => {
3759
+ setRecoilValue(store, node, newValue);
3760
+ }, reset: (node) => {
3761
+ setRecoilValue(store, node, DEFAULT_VALUE);
3762
+ }, refresh: (node) => refreshRecoilValue(store, node), gotoSnapshot: (snapshot) => gotoSnapshot(store, snapshot), transact_UNSTABLE: (transaction) => atomicUpdater(store)(transaction) });
3763
+ const callbackInterface = lazyProxy(baseInterface, {
3592
3764
  snapshot: () => {
3593
- const snapshot = cloneSnapshot(store);
3594
- releaseSnapshot = snapshot.retain();
3595
- return snapshot;
3765
+ if (!originalSnapshot) {
3766
+ originalSnapshot = cloneSnapshot(store, 'latest');
3767
+ releaseSnapshot = originalSnapshot.retain();
3768
+ }
3769
+ // Create a hybrid snapshot that handles both behaviors
3770
+ const hybridSnapshot = new Proxy(originalSnapshot, {
3771
+ get(target, prop) {
3772
+ if (prop === 'getLoadable') {
3773
+ // For getLoadable, return current store state (reflects changes)
3774
+ return (recoilValue) => {
3775
+ currentSnapshot = cloneSnapshot(store, 'latest');
3776
+ return currentSnapshot.getLoadable(recoilValue);
3777
+ };
3778
+ }
3779
+ else if (prop === 'getPromise') {
3780
+ // For getPromise, return original state (doesn't reflect changes)
3781
+ return target.getPromise.bind(target);
3782
+ }
3783
+ // For all other methods, delegate to target
3784
+ const value = target[prop];
3785
+ return typeof value === 'function' ? value.bind(target) : value;
3786
+ }
3787
+ });
3788
+ return hybridSnapshot;
3596
3789
  },
3597
3790
  });
3598
3791
  const callback = fn(callbackInterface);
@@ -3614,11 +3807,21 @@ function recoilCallback(store, fn, args, extraInterface) {
3614
3807
  }
3615
3808
  function useRecoilCallback(fn, deps) {
3616
3809
  const storeRef = useStoreRef();
3810
+ const isRenderingRef = useRef(true);
3811
+ // Clear the render flag after render completes
3812
+ useLayoutEffect(() => {
3813
+ isRenderingRef.current = false;
3814
+ });
3617
3815
  return useCallback((...args) => {
3816
+ if (isRenderingRef.current) {
3817
+ throw err('useRecoilCallback() hooks cannot be called during render. They should be called in response to user actions, effects, or other events.');
3818
+ }
3618
3819
  return recoilCallback(storeRef.current, fn, args);
3619
3820
  },
3821
+ // Don't include storeRef in deps to avoid unnecessary re-creation
3822
+ // The store reference should be stable within a RecoilRoot
3620
3823
  // eslint-disable-next-line fb-www/react-hooks-deps
3621
- deps != null ? [...deps, storeRef] : [storeRef]);
3824
+ deps !== null && deps !== void 0 ? deps : []);
3622
3825
  }
3623
3826
 
3624
3827
  /**
@@ -3722,6 +3925,116 @@ function deepFreezeValue(value) {
3722
3925
  Object.seal(value);
3723
3926
  }
3724
3927
 
3928
+ /**
3929
+ * TypeScript port of Recoil_stableStringify.js
3930
+ */
3931
+ const __DEV__$2 = process.env.NODE_ENV !== 'production';
3932
+ const TIME_WARNING_THRESHOLD_MS = 15;
3933
+ function stringify(x, opt, key, visited = new Set()) {
3934
+ var _a;
3935
+ if (typeof x === 'string' && !x.includes('"') && !x.includes('\\')) {
3936
+ return `"${x}"`;
3937
+ }
3938
+ switch (typeof x) {
3939
+ case 'undefined':
3940
+ return '';
3941
+ case 'boolean':
3942
+ return x ? 'true' : 'false';
3943
+ case 'number':
3944
+ case 'symbol':
3945
+ return String(x);
3946
+ case 'string':
3947
+ return JSON.stringify(x);
3948
+ case 'function':
3949
+ if ((opt === null || opt === void 0 ? void 0 : opt.allowFunctions) !== true) {
3950
+ return '';
3951
+ }
3952
+ return `__FUNCTION(${x.name})__`;
3953
+ }
3954
+ if (x === null) {
3955
+ return 'null';
3956
+ }
3957
+ if (typeof x !== 'object') {
3958
+ return (_a = JSON.stringify(x)) !== null && _a !== void 0 ? _a : '';
3959
+ }
3960
+ // Handle circular references
3961
+ if (visited.has(x)) {
3962
+ return '__CIRCULAR__';
3963
+ }
3964
+ visited.add(x);
3965
+ if (isPromise(x)) {
3966
+ visited.delete(x);
3967
+ return '__PROMISE__';
3968
+ }
3969
+ if (Array.isArray(x)) {
3970
+ const result = `[${x.map((v, i) => stringify(v, opt, i.toString(), visited)).join(',')}]`;
3971
+ visited.delete(x);
3972
+ return result;
3973
+ }
3974
+ if (typeof x.toJSON === 'function') {
3975
+ const result = stringify(x.toJSON(key), opt, key, visited);
3976
+ visited.delete(x);
3977
+ return result;
3978
+ }
3979
+ if (x instanceof Map) {
3980
+ const obj = {};
3981
+ for (const [k, v] of x) {
3982
+ obj[typeof k === 'string' ? k : stringify(k, opt, undefined, visited)] = v;
3983
+ }
3984
+ const result = stringify(obj, opt, key, visited);
3985
+ visited.delete(x);
3986
+ return result;
3987
+ }
3988
+ if (x instanceof Set) {
3989
+ const sortedItems = Array.from(x).sort((a, b) => {
3990
+ const aStr = stringify(a, opt, undefined, new Set(visited));
3991
+ const bStr = stringify(b, opt, undefined, new Set(visited));
3992
+ // Use a more predictable sort order - null should come first
3993
+ if (aStr === 'null' && bStr !== 'null')
3994
+ return -1;
3995
+ if (bStr === 'null' && aStr !== 'null')
3996
+ return 1;
3997
+ return aStr.localeCompare(bStr);
3998
+ });
3999
+ const result = stringify(sortedItems, opt, key, visited);
4000
+ visited.delete(x);
4001
+ return result;
4002
+ }
4003
+ if (Symbol !== undefined &&
4004
+ x[Symbol.iterator] != null &&
4005
+ typeof x[Symbol.iterator] === 'function') {
4006
+ const result = stringify(Array.from(x), opt, key, visited);
4007
+ visited.delete(x);
4008
+ return result;
4009
+ }
4010
+ const result = `{${Object.keys(x)
4011
+ .filter(k => {
4012
+ const value = x[k];
4013
+ return value !== undefined && typeof value !== 'function';
4014
+ })
4015
+ .sort()
4016
+ .map(k => `${stringify(k, opt, undefined, visited)}:${stringify(x[k], opt, k, visited)}`)
4017
+ .join(',')}}`;
4018
+ visited.delete(x);
4019
+ return result;
4020
+ }
4021
+ function stableStringify(x, opt = { allowFunctions: false }) {
4022
+ if (__DEV__$2) {
4023
+ if (typeof window !== 'undefined') {
4024
+ const startTime = window.performance ? window.performance.now() : 0;
4025
+ const str = stringify(x, opt);
4026
+ const endTime = window.performance ? window.performance.now() : 0;
4027
+ if (endTime - startTime > TIME_WARNING_THRESHOLD_MS) {
4028
+ console.groupCollapsed(`Recoil: Spent ${endTime - startTime}ms computing a cache key`);
4029
+ console.warn(x, str);
4030
+ console.groupEnd();
4031
+ }
4032
+ return str;
4033
+ }
4034
+ }
4035
+ return stringify(x, opt);
4036
+ }
4037
+
3725
4038
  /**
3726
4039
  * TypeScript port of Recoil_TreeCache.js
3727
4040
  */
@@ -3765,60 +4078,63 @@ class TreeCache {
3765
4078
  }
3766
4079
  set(route, value, handlers) {
3767
4080
  const addLeaf = () => {
3768
- var _a, _b, _c, _d;
3769
- let node = this._root;
3770
- let branchKey;
4081
+ var _a, _b, _c, _d, _e, _f;
4082
+ // First, setup the branch nodes for the route:
4083
+ let node = null;
4084
+ let branchKey = undefined;
3771
4085
  for (const [nodeKey, nodeValue] of route) {
4086
+ // node now refers to the next node down in the tree
4087
+ const parent = node;
4088
+ // Get existing node or create a new one
3772
4089
  const root = this._root;
3773
- if ((root === null || root === void 0 ? void 0 : root.type) === 'leaf') {
4090
+ const existing = parent ? parent.branches.get(branchKey) : root;
4091
+ node = (_a = existing) !== null && _a !== void 0 ? _a : {
4092
+ type: 'branch',
4093
+ nodeKey,
4094
+ parent,
4095
+ branches: new Map(),
4096
+ branchKey,
4097
+ };
4098
+ // If we found an existing node, confirm it has a consistent value
4099
+ if (node.type !== 'branch' || node.nodeKey !== nodeKey) {
3774
4100
  throw this.invalidCacheError();
3775
4101
  }
3776
- const parent = node;
3777
- let current = parent ? (_a = parent.branches.get(branchKey)) !== null && _a !== void 0 ? _a : null : root;
3778
- if (!current) {
3779
- current = {
3780
- type: 'branch',
3781
- nodeKey,
3782
- parent,
3783
- branches: new Map(),
3784
- branchKey,
3785
- };
3786
- if (parent) {
3787
- parent.branches.set(branchKey, current);
3788
- }
3789
- else {
3790
- this._root = current;
3791
- }
3792
- }
3793
- if (current.type !== 'branch' || current.nodeKey !== nodeKey) {
3794
- throw this.invalidCacheError();
4102
+ // Add the branch node to the tree
4103
+ if (parent) {
4104
+ parent.branches.set(branchKey, node);
3795
4105
  }
3796
- (_b = handlers === null || handlers === void 0 ? void 0 : handlers.onNodeVisit) === null || _b === void 0 ? void 0 : _b.call(handlers, current);
3797
- node = current;
4106
+ (_b = handlers === null || handlers === void 0 ? void 0 : handlers.onNodeVisit) === null || _b === void 0 ? void 0 : _b.call(handlers, node);
4107
+ // Prepare for next iteration and install root if it is new.
3798
4108
  branchKey = this._mapNodeValue(nodeValue);
4109
+ this._root = (_c = this._root) !== null && _c !== void 0 ? _c : node;
3799
4110
  }
4111
+ // Second, setup the leaf node:
4112
+ // If there is an existing leaf for this route confirm it is consistent
3800
4113
  const oldLeaf = node
3801
- ? (_c = node.branches.get(branchKey)) !== null && _c !== void 0 ? _c : null
4114
+ ? (_d = node.branches.get(branchKey)) !== null && _d !== void 0 ? _d : null
3802
4115
  : this._root;
3803
4116
  if (oldLeaf != null &&
3804
4117
  (oldLeaf.type !== 'leaf' || oldLeaf.branchKey !== branchKey)) {
3805
4118
  throw this.invalidCacheError();
3806
4119
  }
4120
+ // Create a new or replacement leaf.
3807
4121
  const leafNode = {
3808
4122
  type: 'leaf',
3809
4123
  value,
3810
4124
  parent: node,
3811
4125
  branchKey,
3812
4126
  };
4127
+ // Install the leaf and call handlers
3813
4128
  if (node) {
3814
4129
  node.branches.set(branchKey, leafNode);
3815
4130
  }
3816
- else {
3817
- this._root = leafNode;
4131
+ this._root = (_e = this._root) !== null && _e !== void 0 ? _e : leafNode;
4132
+ // Only increment if this is a new leaf (not a replacement)
4133
+ if (oldLeaf == null) {
4134
+ this._numLeafs++;
3818
4135
  }
3819
- this._numLeafs++;
3820
4136
  this._onSet(leafNode);
3821
- (_d = handlers === null || handlers === void 0 ? void 0 : handlers.onNodeVisit) === null || _d === void 0 ? void 0 : _d.call(handlers, leafNode);
4137
+ (_f = handlers === null || handlers === void 0 ? void 0 : handlers.onNodeVisit) === null || _f === void 0 ? void 0 : _f.call(handlers, leafNode);
3822
4138
  };
3823
4139
  try {
3824
4140
  addLeaf();
@@ -4012,85 +4328,6 @@ function treeCacheLRU({ name, maxSize, mapNodeValue = (v) => v, }) {
4012
4328
  return cache;
4013
4329
  }
4014
4330
 
4015
- /**
4016
- * TypeScript port of Recoil_stableStringify.js
4017
- */
4018
- const __DEV__$2 = process.env.NODE_ENV !== 'production';
4019
- const TIME_WARNING_THRESHOLD_MS = 15;
4020
- function stringify(x, opt, key) {
4021
- var _a;
4022
- if (typeof x === 'string' && !x.includes('"') && !x.includes('\\')) {
4023
- return `"${x}"`;
4024
- }
4025
- switch (typeof x) {
4026
- case 'undefined':
4027
- return '';
4028
- case 'boolean':
4029
- return x ? 'true' : 'false';
4030
- case 'number':
4031
- case 'symbol':
4032
- return String(x);
4033
- case 'string':
4034
- return JSON.stringify(x);
4035
- case 'function':
4036
- if ((opt === null || opt === void 0 ? void 0 : opt.allowFunctions) !== true) {
4037
- throw err('Attempt to serialize function in a Recoil cache key');
4038
- }
4039
- return `__FUNCTION(${x.name})__`;
4040
- }
4041
- if (x === null) {
4042
- return 'null';
4043
- }
4044
- if (typeof x !== 'object') {
4045
- return (_a = JSON.stringify(x)) !== null && _a !== void 0 ? _a : '';
4046
- }
4047
- if (isPromise(x)) {
4048
- return '__PROMISE__';
4049
- }
4050
- if (Array.isArray(x)) {
4051
- return `[${x.map((v, i) => stringify(v, opt, i.toString()))}]`;
4052
- }
4053
- if (typeof x.toJSON === 'function') {
4054
- return stringify(x.toJSON(key), opt, key);
4055
- }
4056
- if (x instanceof Map) {
4057
- const obj = {};
4058
- for (const [k, v] of x) {
4059
- obj[typeof k === 'string' ? k : stringify(k, opt)] = v;
4060
- }
4061
- return stringify(obj, opt, key);
4062
- }
4063
- if (x instanceof Set) {
4064
- return stringify(Array.from(x).sort((a, b) => stringify(a, opt).localeCompare(stringify(b, opt))), opt, key);
4065
- }
4066
- if (Symbol !== undefined &&
4067
- x[Symbol.iterator] != null &&
4068
- typeof x[Symbol.iterator] === 'function') {
4069
- return stringify(Array.from(x), opt, key);
4070
- }
4071
- return `{${Object.keys(x)
4072
- .filter(k => x[k] !== undefined)
4073
- .sort()
4074
- .map(k => `${stringify(k, opt)}:${stringify(x[k], opt, k)}`)
4075
- .join(',')}}`;
4076
- }
4077
- function stableStringify(x, opt = { allowFunctions: false }) {
4078
- if (__DEV__$2) {
4079
- if (typeof window !== 'undefined') {
4080
- const startTime = window.performance ? window.performance.now() : 0;
4081
- const str = stringify(x, opt);
4082
- const endTime = window.performance ? window.performance.now() : 0;
4083
- if (endTime - startTime > TIME_WARNING_THRESHOLD_MS) {
4084
- console.groupCollapsed(`Recoil: Spent ${endTime - startTime}ms computing a cache key`);
4085
- console.warn(x, str);
4086
- console.groupEnd();
4087
- }
4088
- return str;
4089
- }
4090
- }
4091
- return stringify(x, opt);
4092
- }
4093
-
4094
4331
  /**
4095
4332
  * TypeScript port of Recoil_treeCacheFromPolicy.js
4096
4333
  */
@@ -4113,7 +4350,6 @@ function getValueMapper$1(equality) {
4113
4350
  case 'value':
4114
4351
  return val => stableStringify(val);
4115
4352
  }
4116
- throw err(`Unrecognized equality policy ${equality}`);
4117
4353
  }
4118
4354
  function getTreeCache(eviction, maxSize, mapNodeValue, name) {
4119
4355
  switch (eviction) {
@@ -4128,7 +4364,6 @@ function getTreeCache(eviction, maxSize, mapNodeValue, name) {
4128
4364
  case 'most-recent':
4129
4365
  return treeCacheLRU({ name, maxSize: 1, mapNodeValue });
4130
4366
  }
4131
- throw err(`Unrecognized eviction policy ${eviction}`);
4132
4367
  }
4133
4368
 
4134
4369
  /**
@@ -4295,10 +4530,9 @@ function selector(options) {
4295
4530
  }
4296
4531
  function updateDeps(store, state, deps, executionID) {
4297
4532
  var _a, _b, _c, _d, _e, _f, _g;
4298
- if (executionID != null &&
4299
- (isLatestExecution(store, executionID) ||
4300
- state.version === ((_b = (_a = store.getState()) === null || _a === void 0 ? void 0 : _a.currentTree) === null || _b === void 0 ? void 0 : _b.version) ||
4301
- state.version === ((_d = (_c = store.getState()) === null || _c === void 0 ? void 0 : _c.nextTree) === null || _d === void 0 ? void 0 : _d.version))) {
4533
+ if ((executionID != null && isLatestExecution(store, executionID)) ||
4534
+ state.version === ((_b = (_a = store.getState()) === null || _a === void 0 ? void 0 : _a.currentTree) === null || _b === void 0 ? void 0 : _b.version) ||
4535
+ state.version === ((_d = (_c = store.getState()) === null || _c === void 0 ? void 0 : _c.nextTree) === null || _d === void 0 ? void 0 : _d.version)) {
4302
4536
  saveDepsToStore(key, deps, store, (_g = (_f = (_e = store.getState()) === null || _e === void 0 ? void 0 : _e.nextTree) === null || _f === void 0 ? void 0 : _f.version) !== null && _g !== void 0 ? _g : store.getState().currentTree.version);
4303
4537
  }
4304
4538
  for (const nodeKey of deps) {
@@ -4563,7 +4797,8 @@ function selector(options) {
4563
4797
  discoveredDependencyNodeKeys.clear();
4564
4798
  invalidateSelector(treeState);
4565
4799
  cache.clear();
4566
- markRecoilValueModified(store, recoilValue);
4800
+ // Don't call markRecoilValueModified here as it causes nested state updates
4801
+ // The caller (like refreshRecoilValue) should handle marking as dirty
4567
4802
  }
4568
4803
  if (set != null) {
4569
4804
  const selectorSet = (store, state, newValue) => {
@@ -5054,12 +5289,7 @@ function getCache(eviction, maxSize, mapKey) {
5054
5289
  }
5055
5290
 
5056
5291
  /**
5057
- * Copyright (c) Meta Platforms, Inc. and affiliates.
5058
- *
5059
- * This source code is licensed under the MIT license found in the
5060
- * LICENSE file in the root directory of this source tree.
5061
- *
5062
- * @oncall recoil
5292
+ * TypeScript port of Recoil_atomFamily.js
5063
5293
  */
5064
5294
  function atomFamily(options) {
5065
5295
  var _a, _b;
@@ -5169,12 +5399,7 @@ function constSelector(constant) {
5169
5399
  }
5170
5400
 
5171
5401
  /**
5172
- * Copyright (c) Meta Platforms, Inc. and affiliates.
5173
- *
5174
- * This source code is licensed under the MIT license found in the
5175
- * LICENSE file in the root directory of this source tree.
5176
- *
5177
- * @oncall recoil
5402
+ * TypeScript port of Recoil_errorSelector.js
5178
5403
  */
5179
5404
  const throwingSelector = selectorFamily({
5180
5405
  key: '__error',
@@ -5190,14 +5415,7 @@ function errorSelector(message) {
5190
5415
  }
5191
5416
 
5192
5417
  /**
5193
- * Copyright (c) Meta Platforms, Inc. and affiliates.
5194
- *
5195
- * This source code is licensed under the MIT license found in the
5196
- * LICENSE file in the root directory of this source tree.
5197
- *
5198
- * Wraps another recoil value and prevents writing to it.
5199
- *
5200
- * @oncall recoil
5418
+ * TypeScript port of Recoil_readOnlySelector.js
5201
5419
  */
5202
5420
  function readOnlySelector(atom) {
5203
5421
  return atom;
@@ -5258,11 +5476,21 @@ const waitForAny = selectorFamily({
5258
5476
  const deps = unwrapDependencies(dependencies);
5259
5477
  const [results, exceptions] = concurrentRequests(get, deps);
5260
5478
  if (exceptions.some(exp => !isPromise(exp))) {
5479
+ // If all are errors (no promises), waitForAny should throw the first error
5480
+ if (exceptions.every(exp => isError(exp))) {
5481
+ const firstError = exceptions.find(isError);
5482
+ if (firstError) {
5483
+ throw firstError;
5484
+ }
5485
+ }
5261
5486
  return wrapLoadables(dependencies, results, exceptions);
5262
5487
  }
5263
- return new Promise(resolve => {
5488
+ return new Promise((resolve, reject) => {
5489
+ let pendingCount = 0;
5490
+ let settledCount = 0;
5264
5491
  for (const [i, exp] of exceptions.entries()) {
5265
5492
  if (isPromise(exp)) {
5493
+ pendingCount++;
5266
5494
  exp
5267
5495
  .then(result => {
5268
5496
  results[i] = result;
@@ -5271,7 +5499,18 @@ const waitForAny = selectorFamily({
5271
5499
  })
5272
5500
  .catch(error => {
5273
5501
  exceptions[i] = error;
5274
- resolve(wrapLoadables(dependencies, results, exceptions));
5502
+ settledCount++;
5503
+ // Only resolve with error if ALL promises have settled/failed
5504
+ if (settledCount === pendingCount) {
5505
+ // All promises have settled with errors, so reject with the first error
5506
+ const firstError = exceptions.find(isError);
5507
+ if (firstError) {
5508
+ reject(firstError);
5509
+ }
5510
+ else {
5511
+ resolve(wrapLoadables(dependencies, results, exceptions));
5512
+ }
5513
+ }
5275
5514
  });
5276
5515
  }
5277
5516
  }