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