valdres 1.0.0-beta.13 → 1.0.0-beta.15

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.js CHANGED
@@ -112,15 +112,15 @@ var equal = (a, b, updatedAtomsSet) => {
112
112
  }
113
113
  };
114
114
 
115
+ // src/utils/isSelector.ts
116
+ var isSelector = (state) => state && Object.hasOwn(state, "get");
117
+
115
118
  // src/utils/isAtom.ts
116
119
  var isAtom = (state) => state && Object.hasOwn(state, "defaultValue");
117
120
 
118
121
  // src/utils/isGlobalAtom.ts
119
122
  var isGlobalAtom = (state) => state && Object.hasOwn(state, "stores");
120
123
 
121
- // src/utils/isSelector.ts
122
- var isSelector = (state) => state && Object.hasOwn(state, "get");
123
-
124
124
  // src/utils/isAtomFamily.ts
125
125
  var isAtomFamily = (state) => state && Object.hasOwn(state, "__valdresAtomFamilyMap");
126
126
 
@@ -346,6 +346,14 @@ ${generateSelectorTrace(this.selectors)}`;
346
346
  }
347
347
  }
348
348
 
349
+ // src/lib/noteDependencyGraphChanged.ts
350
+ var noteDependencyGraphChanged = (selector, data) => {
351
+ if (!data.dependencyOrder.has(selector)) {
352
+ data.dependencyOrder.set(selector, data.nextDependencyOrder++);
353
+ }
354
+ data.dependencyGraphVersion++;
355
+ };
356
+
349
357
  // src/lib/asyncDependencyTracking.ts
350
358
  var pendingAsyncDeps = new WeakMap;
351
359
 
@@ -375,6 +383,7 @@ var lateGet = (state, selector, data) => {
375
383
  }
376
384
  const isNewDep = !deps.has(state);
377
385
  if (isNewDep) {
386
+ noteDependencyGraphChanged(selector, data);
378
387
  deps.add(state);
379
388
  const dependents = getOrInitDependentsSet(state, data);
380
389
  dependents.add(selector);
@@ -812,6 +821,7 @@ var evaluateSelector = (selector, data, initializedAtomsSet, circularDependencyS
812
821
  depsChanged = true;
813
822
  }
814
823
  if (depsChanged || !currentDependencies) {
824
+ noteDependencyGraphChanged(selector, data);
815
825
  if (data.livenessPassActive && currentDependencies) {
816
826
  (data.livenessSeeds ??= new Set).add(selector);
817
827
  if (!depsChangeOut)
@@ -901,8 +911,13 @@ var handleSelectorResult = (value, selector, data) => {
901
911
  const currentDeps = data.stateDependencies.get(selector);
902
912
  if (currentDeps) {
903
913
  const selectorIsLive = isLive(selector, data);
914
+ let graphChangeNoted = false;
904
915
  for (const dep of currentDeps) {
905
916
  if (!evalDeps.has(dep)) {
917
+ if (!graphChangeNoted) {
918
+ noteDependencyGraphChanged(selector, data);
919
+ graphChangeNoted = true;
920
+ }
906
921
  currentDeps.delete(dep);
907
922
  const dependents2 = data.stateDependents.get(dep);
908
923
  if (dependents2)
@@ -1391,6 +1406,24 @@ var propagateDownstreamTopo = (seeds, data, collectedSubscribers, updatedInitial
1391
1406
  }
1392
1407
  const needsEval = new Set(seeds);
1393
1408
  let graphMutated = false;
1409
+ let resweep;
1410
+ const markForResweep = (selector) => {
1411
+ graphMutated = true;
1412
+ if (!resweep)
1413
+ resweep = new Set;
1414
+ const stack = [selector];
1415
+ for (let i = 0;i < stack.length; i++) {
1416
+ const s = stack[i];
1417
+ if (resweep.has(s))
1418
+ continue;
1419
+ resweep.add(s);
1420
+ const downstream = data.stateDependents.get(s);
1421
+ if (downstream) {
1422
+ for (const d of downstream)
1423
+ stack.push(d);
1424
+ }
1425
+ }
1426
+ };
1394
1427
  const advance = (selector, propagateChange) => {
1395
1428
  const downstream = data.stateDependents.get(selector);
1396
1429
  if (!downstream)
@@ -1406,7 +1439,13 @@ var propagateDownstreamTopo = (seeds, data, collectedSubscribers, updatedInitial
1406
1439
  }
1407
1440
  continue;
1408
1441
  }
1409
- const c = (pending.get(d) ?? 0) - 1;
1442
+ const cur = pending.get(d);
1443
+ if (cur === undefined) {
1444
+ if (propagateChange)
1445
+ markForResweep(d);
1446
+ continue;
1447
+ }
1448
+ const c = cur - 1;
1410
1449
  pending.set(d, c);
1411
1450
  if (propagateChange)
1412
1451
  needsEval.add(d);
@@ -1418,12 +1457,18 @@ var propagateDownstreamTopo = (seeds, data, collectedSubscribers, updatedInitial
1418
1457
  let head = 0;
1419
1458
  while (head < ready.length) {
1420
1459
  const selector = ready[head++];
1460
+ if (resweep?.has(selector))
1461
+ continue;
1462
+ if (!pending.has(selector))
1463
+ continue;
1421
1464
  const currentValue = data.values.get(selector);
1422
1465
  if (isPromiseLike(currentValue) && isInitOnly) {
1466
+ pending.delete(selector);
1423
1467
  advance(selector, false);
1424
1468
  continue;
1425
1469
  }
1426
1470
  if (!needsEval.has(selector)) {
1471
+ pending.delete(selector);
1427
1472
  advance(selector, false);
1428
1473
  continue;
1429
1474
  }
@@ -1431,6 +1476,7 @@ var propagateDownstreamTopo = (seeds, data, collectedSubscribers, updatedInitial
1431
1476
  const subscribers = data.subscriptions.get(selector);
1432
1477
  if (!isPromiseLike(currentValue) && (!dependents || dependents.size === 0) && (!subscribers || subscribers.size === 0)) {
1433
1478
  data.values.delete(selector);
1479
+ pending.delete(selector);
1434
1480
  advance(selector, false);
1435
1481
  continue;
1436
1482
  }
@@ -1456,6 +1502,7 @@ var propagateDownstreamTopo = (seeds, data, collectedSubscribers, updatedInitial
1456
1502
  }
1457
1503
  }
1458
1504
  }
1505
+ pending.delete(selector);
1459
1506
  advance(selector, wasValueUpdated);
1460
1507
  if (wasValueUpdated) {
1461
1508
  if (changedSelectors)
@@ -1466,9 +1513,9 @@ var propagateDownstreamTopo = (seeds, data, collectedSubscribers, updatedInitial
1466
1513
  }
1467
1514
  if (!graphMutated)
1468
1515
  return;
1469
- let stranded;
1516
+ let stranded = resweep;
1470
1517
  for (const s of closure) {
1471
- if (needsEval.has(s) && (pending.get(s) ?? 0) > 0) {
1518
+ if (needsEval.has(s) && pending.has(s)) {
1472
1519
  if (!stranded)
1473
1520
  stranded = new Set;
1474
1521
  stranded.add(s);
@@ -1477,50 +1524,81 @@ var propagateDownstreamTopo = (seeds, data, collectedSubscribers, updatedInitial
1477
1524
  if (!stranded)
1478
1525
  return;
1479
1526
  let work = stranded;
1480
- while (work.size > 0) {
1481
- const next = new Set;
1482
- for (const selector of work) {
1483
- const currentValue = data.values.get(selector);
1484
- if (isPromiseLike(currentValue) && isInitOnly)
1485
- continue;
1486
- const dependents = data.stateDependents.get(selector);
1487
- const subscribers = data.subscriptions.get(selector);
1488
- if (!isPromiseLike(currentValue) && (!dependents || dependents.size === 0) && (!subscribers || subscribers.size === 0)) {
1489
- data.values.delete(selector);
1490
- continue;
1491
- }
1492
- depsChange.added = undefined;
1493
- depsChange.removed = undefined;
1494
- const wasValueUpdated = reEvaluateSelector(selector, data, updatedInitializedAtoms, depsChange, currentValue);
1495
- const added = depsChange.added;
1496
- const removed = depsChange.removed;
1497
- if ((added || removed) && isLive(selector, data)) {
1498
- if (added) {
1499
- for (const dep of added) {
1500
- onLiveDependencyAdded(dep, data);
1501
- mountTransitiveDeps(dep, data);
1502
- }
1503
- }
1504
- if (removed) {
1505
- for (const dep of removed) {
1506
- onLiveDependencyRemoved(dep, data);
1507
- unmountOrphanedDeps(dep, data);
1508
- }
1527
+ const hasUnsettledDependency = (selector) => {
1528
+ const deps = data.stateDependencies.get(selector);
1529
+ if (!deps)
1530
+ return false;
1531
+ for (const dep of deps) {
1532
+ if (dep !== selector && work.has(dep))
1533
+ return true;
1534
+ }
1535
+ return false;
1536
+ };
1537
+ const enqueueDownstream = (selector) => {
1538
+ const downstream = data.stateDependents.get(selector);
1539
+ if (!downstream)
1540
+ return;
1541
+ for (const d of downstream) {
1542
+ work.add(d);
1543
+ }
1544
+ };
1545
+ const evaluateStrandedSelector = (selector) => {
1546
+ const currentValue = data.values.get(selector);
1547
+ if (isPromiseLike(currentValue) && isInitOnly)
1548
+ return false;
1549
+ const dependents = data.stateDependents.get(selector);
1550
+ const subscribers = data.subscriptions.get(selector);
1551
+ if (!isPromiseLike(currentValue) && (!dependents || dependents.size === 0) && (!subscribers || subscribers.size === 0)) {
1552
+ data.values.delete(selector);
1553
+ return false;
1554
+ }
1555
+ depsChange.added = undefined;
1556
+ depsChange.removed = undefined;
1557
+ const wasValueUpdated = reEvaluateSelector(selector, data, updatedInitializedAtoms, depsChange, currentValue);
1558
+ const added = depsChange.added;
1559
+ const removed = depsChange.removed;
1560
+ if ((added || removed) && isLive(selector, data)) {
1561
+ if (added) {
1562
+ for (const dep of added) {
1563
+ onLiveDependencyAdded(dep, data);
1564
+ mountTransitiveDeps(dep, data);
1509
1565
  }
1510
1566
  }
1511
- if (wasValueUpdated) {
1512
- if (changedSelectors)
1513
- changedSelectors.add(selector);
1514
- if (subscribers)
1515
- addSetToSet(subscribers, collectedSubscribers);
1516
- const downstream = data.stateDependents.get(selector);
1517
- if (downstream) {
1518
- for (const d of downstream)
1519
- next.add(d);
1567
+ if (removed) {
1568
+ for (const dep of removed) {
1569
+ onLiveDependencyRemoved(dep, data);
1570
+ unmountOrphanedDeps(dep, data);
1520
1571
  }
1521
1572
  }
1522
1573
  }
1523
- work = next;
1574
+ if (wasValueUpdated) {
1575
+ if (changedSelectors)
1576
+ changedSelectors.add(selector);
1577
+ if (subscribers)
1578
+ addSetToSet(subscribers, collectedSubscribers);
1579
+ enqueueDownstream(selector);
1580
+ }
1581
+ return wasValueUpdated;
1582
+ };
1583
+ while (work.size > 0) {
1584
+ let progressed = false;
1585
+ const batch = [...work];
1586
+ for (const selector of batch) {
1587
+ if (!work.has(selector))
1588
+ continue;
1589
+ if (hasUnsettledDependency(selector))
1590
+ continue;
1591
+ work.delete(selector);
1592
+ progressed = true;
1593
+ evaluateStrandedSelector(selector);
1594
+ }
1595
+ if (progressed)
1596
+ continue;
1597
+ const cyclicBatch = [...work];
1598
+ work = new Set;
1599
+ for (const selector of cyclicBatch) {
1600
+ evaluateStrandedSelector(selector);
1601
+ }
1524
1602
  }
1525
1603
  };
1526
1604
  var orderInitialSelectors = (selectors, data) => {
@@ -1710,8 +1788,12 @@ var setAtom = (atom, newValue, data, skipOnSet = false) => {
1710
1788
  }
1711
1789
  let syncValue = validateSchema(atom, newValue, data);
1712
1790
  const areEqual = isPromiseLike(currentValue) ? currentValue === syncValue : atom.equal(currentValue, syncValue);
1713
- if (areEqual)
1791
+ if (areEqual) {
1792
+ if (data.parent && !data.values.has(atom)) {
1793
+ return setValueInData(atom, syncValue, data);
1794
+ }
1714
1795
  return syncValue;
1796
+ }
1715
1797
  syncValue = setValueInData(atom, syncValue, data);
1716
1798
  if (atom.onSet && !skipOnSet)
1717
1799
  atom.onSet(syncValue, data);
@@ -1977,6 +2059,10 @@ Object.defineProperties(lazyProto, {
1977
2059
  subscriptionsRequireEqualCheck: makeLazyGetter("subscriptionsRequireEqualCheck"),
1978
2060
  stateDependents: makeLazyGetter("stateDependents"),
1979
2061
  stateDependencies: makeLazyGetter("stateDependencies"),
2062
+ dependencyOrder: makeLazyGetter("dependencyOrder"),
2063
+ cycleRiskInClosure: makeLazyGetter("cycleRiskInClosure"),
2064
+ acyclicDependencyVersion: makeLazyGetter("acyclicDependencyVersion"),
2065
+ orphanCleanupVersion: makeLazyGetter("orphanCleanupVersion"),
1980
2066
  mounts: makeLazyGetter("mounts"),
1981
2067
  liveDependentCount: makeLazyGetter("liveDependentCount"),
1982
2068
  mountInClosure: makeLazyGetter("mountInClosure"),
@@ -1992,6 +2078,10 @@ function createStoreData(id, parent, options) {
1992
2078
  if (enumerable)
1993
2079
  data.enumerable = true;
1994
2080
  data.values = enumerable ? new Map : new WeakMap;
2081
+ data.nextDependencyOrder = 0;
2082
+ data.dependencyGraphVersion = 0;
2083
+ data.pendingOrphanCleanup = undefined;
2084
+ data.orphanCleanupScheduled = false;
1995
2085
  data.scopes = new Map;
1996
2086
  data.scopeValueIndex = new WeakMap;
1997
2087
  data.pendingDefaults = new WeakMap;
@@ -2022,6 +2112,56 @@ var deleteFamilyAtom = (atom, data) => {
2022
2112
  propagateDeletedAtoms([atom], data, undefined, undefined, undefined, undefined, "delete");
2023
2113
  };
2024
2114
 
2115
+ // src/lib/cleanupOrphanedDeps.ts
2116
+ var cleanupOrphanedDeps = (state, data) => {
2117
+ if (isLive(state, data))
2118
+ return;
2119
+ const graphVersion = data.dependencyGraphVersion;
2120
+ const cleanedAtVersion = data.orphanCleanupVersion;
2121
+ if (cleanedAtVersion.get(state) === graphVersion)
2122
+ return;
2123
+ const stack = [state];
2124
+ while (stack.length > 0) {
2125
+ const current = stack.pop();
2126
+ if (cleanedAtVersion.get(current) === graphVersion)
2127
+ continue;
2128
+ if (isLive(current, data))
2129
+ continue;
2130
+ cleanedAtVersion.set(current, graphVersion);
2131
+ const dependents = data.stateDependents.get(current);
2132
+ const deps = data.stateDependencies.get(current);
2133
+ if (deps) {
2134
+ for (const dep of deps) {
2135
+ data.stateDependents.get(dep)?.delete(current);
2136
+ }
2137
+ data.stateDependencies.delete(current);
2138
+ data.values.delete(current);
2139
+ data.abortControllers.delete(current);
2140
+ if (dependents) {
2141
+ for (const dependent of dependents)
2142
+ stack.push(dependent);
2143
+ }
2144
+ for (const dep of deps)
2145
+ stack.push(dep);
2146
+ continue;
2147
+ }
2148
+ if (dependents) {
2149
+ for (const dependent of dependents)
2150
+ stack.push(dependent);
2151
+ }
2152
+ }
2153
+ };
2154
+
2155
+ // src/lib/flushPendingOrphanCleanup.ts
2156
+ var flushPendingOrphanCleanup = (data) => {
2157
+ const pending = data.pendingOrphanCleanup;
2158
+ if (!pending)
2159
+ return;
2160
+ data.pendingOrphanCleanup = undefined;
2161
+ for (const state of pending)
2162
+ cleanupOrphanedDeps(state, data);
2163
+ };
2164
+
2025
2165
  // src/lib/onStoreChange.ts
2026
2166
  var onStoreChange = (callback, data, options) => {
2027
2167
  const atoms = options?.atoms ?? true;
@@ -2123,6 +2263,26 @@ var deleteMaxAgeCleanup = (data, state) => {
2123
2263
  maxAgeCleanups.get(data)?.delete(state);
2124
2264
  };
2125
2265
 
2266
+ // src/lib/queueOrphanCleanup.ts
2267
+ var queueOrphanCleanup = (state, data) => {
2268
+ if (!data.stateDependencies.has(state) && !data.stateDependents.get(state)?.size) {
2269
+ return;
2270
+ }
2271
+ let pending = data.pendingOrphanCleanup;
2272
+ if (!pending) {
2273
+ pending = new Set;
2274
+ data.pendingOrphanCleanup = pending;
2275
+ }
2276
+ pending.add(state);
2277
+ if (data.orphanCleanupScheduled)
2278
+ return;
2279
+ data.orphanCleanupScheduled = true;
2280
+ queueMicrotask(() => {
2281
+ data.orphanCleanupScheduled = false;
2282
+ flushPendingOrphanCleanup(data);
2283
+ });
2284
+ };
2285
+
2126
2286
  // src/lib/unsubscribe.ts
2127
2287
  var unsubscribe = (state, subscription, data) => {
2128
2288
  const subscribers = data.subscriptions.get(state);
@@ -2148,40 +2308,14 @@ var unsubscribe = (state, subscription, data) => {
2148
2308
  }
2149
2309
  data.subscriptions.delete(state);
2150
2310
  onLastDirectSubscriber(state, data);
2151
- if (isSelector(state) && regionHasCycle(new Set([state]), data)) {
2311
+ if (isSelector(state) && data.cycleRiskInClosure.has(state) && regionHasCycle(state, data)) {
2152
2312
  reconcileLivenessAfterChurn(new Set([state]), data);
2153
2313
  }
2154
- unmountOrphanedDeps(state, data);
2155
- cleanupOrphanedDeps(state, data);
2156
- }
2157
- }
2158
- };
2159
- var cleanupOrphanedDeps = (state, data, visited = new Set) => {
2160
- if (visited.has(state))
2161
- return;
2162
- visited.add(state);
2163
- if (isLive(state, data))
2164
- return;
2165
- const deps = data.stateDependencies.get(state);
2166
- if (deps) {
2167
- for (const dep of deps) {
2168
- const depDependents = data.stateDependents.get(dep);
2169
- if (depDependents) {
2170
- depDependents.delete(state);
2314
+ if (!isLive(state, data)) {
2315
+ unmountOrphanedDeps(state, data);
2316
+ queueOrphanCleanup(state, data);
2171
2317
  }
2172
2318
  }
2173
- data.stateDependencies.delete(state);
2174
- data.values.delete(state);
2175
- data.abortControllers.delete(state);
2176
- for (const dep of deps) {
2177
- cleanupOrphanedDeps(dep, data, visited);
2178
- }
2179
- }
2180
- const dependents = data.stateDependents.get(state);
2181
- if (dependents) {
2182
- for (const dep of [...dependents]) {
2183
- cleanupOrphanedDeps(dep, data, visited);
2184
- }
2185
2319
  }
2186
2320
  };
2187
2321
 
@@ -2958,6 +3092,8 @@ function storeFromStoreData(data, detach) {
2958
3092
  return _pendingTxn;
2959
3093
  };
2960
3094
  const getDefault = (state) => {
3095
+ if (data.pendingOrphanCleanup)
3096
+ flushPendingOrphanCleanup(data);
2961
3097
  if (data.values.has(state)) {
2962
3098
  if (!isCachedValueStale(state, data)) {
2963
3099
  return data.values.get(state);
@@ -2990,12 +3126,16 @@ function storeFromStoreData(data, detach) {
2990
3126
  };
2991
3127
  const getBatched = (state) => {
2992
3128
  if (_pendingTxn) {
3129
+ if (data.pendingOrphanCleanup)
3130
+ flushPendingOrphanCleanup(data);
2993
3131
  return _pendingTxn.get(state);
2994
3132
  }
2995
3133
  return getDefault(state);
2996
3134
  };
2997
3135
  const get = data.batchUpdates ? getBatched : getDefault;
2998
3136
  const setDefault = (state, value) => {
3137
+ if (data.pendingOrphanCleanup)
3138
+ flushPendingOrphanCleanup(data);
2999
3139
  if (isAtom(state))
3000
3140
  return setAtom(state, value, data);
3001
3141
  if (isSelector(state))
@@ -3003,6 +3143,8 @@ function storeFromStoreData(data, detach) {
3003
3143
  throw new Error(InvalidStateSetError);
3004
3144
  };
3005
3145
  const setBatched = (state, value) => {
3146
+ if (data.pendingOrphanCleanup)
3147
+ flushPendingOrphanCleanup(data);
3006
3148
  if (isAtom(state)) {
3007
3149
  return ensurePendingTxn().set(state, value);
3008
3150
  }
@@ -3012,29 +3154,45 @@ function storeFromStoreData(data, detach) {
3012
3154
  };
3013
3155
  const set = data.batchUpdates ? setBatched : setDefault;
3014
3156
  const reset = (atom) => {
3157
+ if (data.pendingOrphanCleanup)
3158
+ flushPendingOrphanCleanup(data);
3015
3159
  if (data.batchUpdates)
3016
3160
  flushPendingTxn();
3017
3161
  return resetAtom(atom, data);
3018
3162
  };
3019
3163
  const del = (atom) => {
3164
+ if (data.pendingOrphanCleanup)
3165
+ flushPendingOrphanCleanup(data);
3020
3166
  if (data.batchUpdates)
3021
3167
  flushPendingTxn();
3022
3168
  return deleteFamilyAtom(atom, data);
3023
3169
  };
3024
3170
  const unset = (atom) => {
3171
+ if (data.pendingOrphanCleanup)
3172
+ flushPendingOrphanCleanup(data);
3025
3173
  if (data.batchUpdates)
3026
3174
  flushPendingTxn();
3027
3175
  return unsetValue(atom, data);
3028
3176
  };
3029
- const sub = (state, callback, deepEqualCheckBeforeCallback = true) => subscribe(state, callback, deepEqualCheckBeforeCallback, data);
3177
+ const sub = (state, callback, deepEqualCheckBeforeCallback = true) => {
3178
+ if (data.pendingOrphanCleanup)
3179
+ flushPendingOrphanCleanup(data);
3180
+ return subscribe(state, callback, deepEqualCheckBeforeCallback, data);
3181
+ };
3030
3182
  const txn = (callback, name) => {
3183
+ if (data.pendingOrphanCleanup)
3184
+ flushPendingOrphanCleanup(data);
3031
3185
  if (data.batchUpdates)
3032
3186
  flushPendingTxn();
3033
3187
  return transaction(callback, data, name);
3034
3188
  };
3035
3189
  const onChange = (callback, options) => onStoreChange(callback, data, options);
3036
3190
  const storeOnCommitEnd = (callback) => onCommitEnd(callback, data);
3037
- const storeSnapshot = () => snapshot(data);
3191
+ const storeSnapshot = () => {
3192
+ if (data.pendingOrphanCleanup)
3193
+ flushPendingOrphanCleanup(data);
3194
+ return snapshot(data);
3195
+ };
3038
3196
  const scope = (scopeId, callback) => {
3039
3197
  if (callback) {
3040
3198
  if (!data.scopes.has(scopeId)) {
@@ -3138,7 +3296,30 @@ var propagateMountMarkerUp = (state, data) => {
3138
3296
  }
3139
3297
  }
3140
3298
  };
3299
+ var propagateCycleRiskUp = (state, data) => {
3300
+ if (data.cycleRiskInClosure.has(state))
3301
+ return;
3302
+ data.cycleRiskInClosure.set(state, true);
3303
+ const stack = [state];
3304
+ while (stack.length > 0) {
3305
+ const current = stack.pop();
3306
+ const parents = data.stateDependents.get(current);
3307
+ if (!parents)
3308
+ continue;
3309
+ for (const parent of parents) {
3310
+ if (data.cycleRiskInClosure.has(parent))
3311
+ continue;
3312
+ data.cycleRiskInClosure.set(parent, true);
3313
+ stack.push(parent);
3314
+ }
3315
+ }
3316
+ };
3141
3317
  var noteDependencyAdded = (selector, dep, data) => {
3318
+ const selectorOrder = data.dependencyOrder.get(selector);
3319
+ const depOrder = data.dependencyOrder.get(dep);
3320
+ if (data.cycleRiskInClosure.has(dep) || selectorOrder === undefined || isSelector(dep) && (depOrder === undefined || depOrder >= selectorOrder)) {
3321
+ propagateCycleRiskUp(selector, data);
3322
+ }
3142
3323
  if (!hasMountInClosure(dep, data))
3143
3324
  return;
3144
3325
  if (data.mountInClosure.has(selector))
@@ -3221,36 +3402,53 @@ var onLiveDependencyRemoved = (dep, data) => {
3221
3402
  propagateNotLive(dep, data);
3222
3403
  }
3223
3404
  };
3224
- var regionHasCycle = (seeds, data) => {
3225
- const done = new Set;
3405
+ var seedClosureHasCycle = (seed, data, graphVersion, acyclicAtVersion) => {
3406
+ if (acyclicAtVersion.get(seed) === graphVersion)
3407
+ return false;
3226
3408
  const onPath = new Set;
3227
- for (const seed of seeds) {
3228
- if (done.has(seed))
3409
+ const stack = [
3410
+ { node: seed, it: (data.stateDependencies.get(seed) ?? EMPTY).values() }
3411
+ ];
3412
+ onPath.add(seed);
3413
+ while (stack.length > 0) {
3414
+ const frame = stack[stack.length - 1];
3415
+ const next = frame.it.next();
3416
+ if (next.done) {
3417
+ onPath.delete(frame.node);
3418
+ acyclicAtVersion.set(frame.node, graphVersion);
3419
+ stack.pop();
3229
3420
  continue;
3230
- const stack = [
3231
- { node: seed, it: (data.stateDependencies.get(seed) ?? EMPTY).values() }
3232
- ];
3233
- onPath.add(seed);
3234
- while (stack.length > 0) {
3235
- const frame = stack[stack.length - 1];
3236
- const next = frame.it.next();
3237
- if (next.done) {
3238
- onPath.delete(frame.node);
3239
- done.add(frame.node);
3240
- stack.pop();
3241
- continue;
3242
- }
3243
- const dep = next.value;
3244
- if (onPath.has(dep))
3421
+ }
3422
+ const dep = next.value;
3423
+ if (onPath.has(dep))
3424
+ return true;
3425
+ if (acyclicAtVersion.get(dep) === graphVersion)
3426
+ continue;
3427
+ onPath.add(dep);
3428
+ stack.push({
3429
+ node: dep,
3430
+ it: (data.stateDependencies.get(dep) ?? EMPTY).values()
3431
+ });
3432
+ }
3433
+ return false;
3434
+ };
3435
+ var regionHasCycle = (seeds, data) => {
3436
+ const graphVersion = data.dependencyGraphVersion;
3437
+ const acyclicAtVersion = data.acyclicDependencyVersion;
3438
+ if (seeds instanceof Set) {
3439
+ for (const seed of seeds) {
3440
+ if (seedClosureHasCycle(seed, data, graphVersion, acyclicAtVersion)) {
3245
3441
  return true;
3246
- if (done.has(dep))
3247
- continue;
3248
- onPath.add(dep);
3249
- stack.push({
3250
- node: dep,
3251
- it: (data.stateDependencies.get(dep) ?? EMPTY).values()
3252
- });
3442
+ }
3253
3443
  }
3444
+ return false;
3445
+ }
3446
+ return seedClosureHasCycle(seeds, data, graphVersion, acyclicAtVersion);
3447
+ };
3448
+ var seededRegionMayHaveCycle = (seeds, data) => {
3449
+ for (const seed of seeds) {
3450
+ if (data.cycleRiskInClosure.has(seed))
3451
+ return true;
3254
3452
  }
3255
3453
  return false;
3256
3454
  };
@@ -3270,7 +3468,7 @@ var endLivenessPass = (data) => {
3270
3468
  data.livenessSeeds = undefined;
3271
3469
  data.livenessLazyArmed = false;
3272
3470
  data.livenessRemovalArmed = false;
3273
- if (seeds && seeds.size > 0 && (lazyArmed || removalArmed && regionHasCycle(seeds, data))) {
3471
+ if (seeds && seeds.size > 0 && (lazyArmed || removalArmed && seededRegionMayHaveCycle(seeds, data) && regionHasCycle(seeds, data))) {
3274
3472
  return seeds;
3275
3473
  }
3276
3474
  return null;
@@ -3613,6 +3811,7 @@ function atom(defaultValue, options) {
3613
3811
  return created;
3614
3812
  }
3615
3813
  // src/lib/stableStringify.ts
3814
+ var compareStrings = (a, b) => a < b ? -1 : a > b ? 1 : 0;
3616
3815
  var stableStringifyRecurse = (x, key) => {
3617
3816
  if (typeof x === "string" && !x.includes('"') && !x.includes("\\")) {
3618
3817
  return `"${x}"`;
@@ -3646,14 +3845,18 @@ var stableStringifyRecurse = (x, key) => {
3646
3845
  return stableStringifyRecurse(x.toJSON(key), key);
3647
3846
  }
3648
3847
  if (x instanceof Map) {
3649
- const obj = {};
3650
- for (const [k, v] of x) {
3651
- obj[typeof k === "string" ? k : stringify(k, opt)] = v;
3652
- }
3653
- return stableStringifyRecurse(obj, key);
3848
+ const entries = Array.from(x, ([k, v]) => [
3849
+ stableStringifyRecurse(k),
3850
+ stableStringifyRecurse(v)
3851
+ ]).sort(([ak, av], [bk, bv]) => {
3852
+ const keyOrder = compareStrings(ak, bk);
3853
+ return keyOrder || compareStrings(av, bv);
3854
+ });
3855
+ return `__MAP(${stableStringifyRecurse(entries, key)})__`;
3654
3856
  }
3655
3857
  if (x instanceof Set) {
3656
- return stableStringifyRecurse(Array.from(x).sort((a, b) => stableStringifyRecurse(a).localeCompare(stableStringifyRecurse(b))), key);
3858
+ const values = Array.from(x, (v) => stableStringifyRecurse(v)).sort();
3859
+ return `__SET(${stableStringifyRecurse(values, key)})__`;
3657
3860
  }
3658
3861
  if (Symbol !== undefined && x[Symbol.iterator] != null && typeof x[Symbol.iterator] === "function") {
3659
3862
  return stableStringifyRecurse(Array.from(x), key);
@@ -4017,9 +4220,9 @@ var isFamilySelector = (state) => isFamilyState(state) && isSelector(state);
4017
4220
  // src/index.ts
4018
4221
  var slot = valdresGlobal();
4019
4222
  if (slot.version) {
4020
- throw new Error(`Error! An instance of valdres is already loaded. Loaded: ${slot.version}. Attempted to load: ${"1.0.0-beta.13"}`);
4223
+ throw new Error(`Error! An instance of valdres is already loaded. Loaded: ${slot.version}. Attempted to load: ${"1.0.0-beta.15"}`);
4021
4224
  } else {
4022
- slot.version = "1.0.0-beta.13";
4225
+ slot.version = "1.0.0-beta.15";
4023
4226
  }
4024
4227
  export {
4025
4228
  store,
@@ -0,0 +1,9 @@
1
+ import type { State } from "../types/State";
2
+ import type { StoreData } from "../types/StoreData";
3
+ /**
4
+ * Remove non-live states from the dependency graph, clear selector caches, and
5
+ * recursively clean orphaned dependencies/dependents. Visit marks persist for
6
+ * one topology generation so every state is processed once across a queued
7
+ * sibling-unsubscribe burst.
8
+ */
9
+ export declare const cleanupOrphanedDeps: (state: State, data: StoreData) => void;
@@ -0,0 +1,7 @@
1
+ import type { StoreData } from "../types/StoreData";
2
+ /**
3
+ * Drain this store's queued orphan roots synchronously. Public store operations
4
+ * call this first so microtask batching does not change their observable cache
5
+ * semantics.
6
+ */
7
+ export declare const flushPendingOrphanCleanup: (data: StoreData) => void;
@@ -41,27 +41,7 @@ export declare const onLiveDependencyAdded: (dep: State, data: StoreData) => voi
41
41
  * if the contribution was the last one keeping dep alive.
42
42
  */
43
43
  export declare const onLiveDependencyRemoved: (dep: State, data: StoreData) => void;
44
- /**
45
- * Does the DOWNWARD (dependency) closure of `seeds` contain a directed cycle?
46
- *
47
- * This is the exact gate for the removal-armed liveness reconcile. Both bugs the
48
- * reconcile fixes provably require a cycle inside the affected region:
49
- * - FREEZE: a still-live selector S is stranded only if `propagateNotLive`,
50
- * walking DOWN from a removed dep D, reaches back to S — i.e. D depends
51
- * (transitively) on S while S reads D, a cycle through D and S.
52
- * - LEAK: a reference count fails to drain only a cycle; a DAG always drains
53
- * via the `prev === 1` guard.
54
- * So on an acyclic region the incremental onLiveDependencyRemoved + propagateNotLive
55
- * already equals ground truth and the reconcile is a no-op — skip it.
56
- *
57
- * Iterative DFS over `data.stateDependencies` (down-edges only) restricted to the
58
- * seeds' closure; an `onPath` (gray) set detects a back-edge, a `done` (black) set
59
- * memoizes fully-explored acyclic subgraphs. Only selectors are keyed in
60
- * `stateDependencies`; atoms and atom-family members are graph sinks (no
61
- * out-edges), so a region of pure atom deps returns false in O(seeds).
62
- * (selectorFamily members ARE selectors and do have out-edges.)
63
- */
64
- export declare const regionHasCycle: (seeds: Set<State>, data: StoreData) => boolean;
44
+ export declare const regionHasCycle: (seeds: Set<State> | State, data: StoreData) => boolean;
65
45
  /**
66
46
  * Begin a liveness-reconcile pass. Returns true iff THIS call owns the pass (the
67
47
  * outermost one) — a nested pass returns false and must not release or reconcile.
@@ -0,0 +1,7 @@
1
+ import type { State } from "../types/State";
2
+ import type { StoreData } from "../types/StoreData";
3
+ /**
4
+ * Invalidate topology-sensitive teardown caches after graph construction or
5
+ * churn, and assign the selector's stable per-store materialization order.
6
+ */
7
+ export declare const noteDependencyGraphChanged: (selector: State, data: StoreData) => void;
@@ -0,0 +1,9 @@
1
+ import type { State } from "../types/State";
2
+ import type { StoreData } from "../types/StoreData";
3
+ /**
4
+ * Batch orphan graph/value cleanup across a synchronous unsubscribe burst.
5
+ * This one-microtask delay is intentional: lifecycle cleanup already ran
6
+ * synchronously, and every public store operation drains this queue before it
7
+ * can observe or mutate selector caches.
8
+ */
9
+ export declare const queueOrphanCleanup: (state: State, data: StoreData) => void;
@@ -16,6 +16,34 @@ export type StoreData = {
16
16
  subscriptionsRequireEqualCheck: WeakMap<WeakKey, boolean>;
17
17
  stateDependents: WeakMap<WeakKey, any>;
18
18
  stateDependencies: WeakMap<WeakKey, any>;
19
+ /** Stable per-store order assigned when a selector first materializes its
20
+ * dependency set. An edge to an equal/newer selector violates this order,
21
+ * making a directed cycle possible in that closure. */
22
+ dependencyOrder: WeakMap<WeakKey, number>;
23
+ nextDependencyOrder: number;
24
+ /** Monotonic, conservative marker: present when a state's downward closure
25
+ * may contain an edge that violates `dependencyOrder`. Every real cycle has
26
+ * at least one such edge, so absence proves the closure acyclic in O(1).
27
+ * Stale positives after edge removal only cost a fallback DFS. */
28
+ cycleRiskInClosure: WeakMap<WeakKey, true>;
29
+ /** Monotonic generation for dependency-graph materialization/churn. Selector
30
+ * evaluation increments it when a dependency set is created or changed.
31
+ * Orphan teardown only removes edges and deliberately leaves it unchanged:
32
+ * deletion cannot create a cycle, so a synchronous unsubscribe burst can
33
+ * reuse both negative cycle proofs and completed orphan-walk visits. */
34
+ dependencyGraphVersion: number;
35
+ /** `state -> dependencyGraphVersion` for closures proven acyclic. A cached
36
+ * negative remains valid while teardown only deletes edges; any normal graph
37
+ * construction/churn bumps `dependencyGraphVersion` and invalidates it. */
38
+ acyclicDependencyVersion: WeakMap<WeakKey, number>;
39
+ /** `state -> dependencyGraphVersion` for non-live states whose orphan graph
40
+ * work has completed. This is the shared visited set for a deletion-only
41
+ * unsubscribe burst, without retaining states strongly between calls. */
42
+ orphanCleanupVersion: WeakMap<WeakKey, number>;
43
+ /** Strong only until the queued microtask drains it. Batches orphan graph
44
+ * cleanup roots across a synchronous unsubscribe burst. */
45
+ pendingOrphanCleanup?: Set<WeakKey>;
46
+ orphanCleanupScheduled: boolean;
19
47
  mounts: WeakMap<WeakKey, {
20
48
  cleanup?: () => void;
21
49
  }>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "valdres",
3
3
  "description": "Fast atom-based state management for JavaScript. Inspired by Recoil and Jotai.",
4
- "version": "1.0.0-beta.13",
4
+ "version": "1.0.0-beta.15",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Eigil Sagafos"