valdres 1.0.0-beta.14 → 1.0.0-beta.16

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);
@@ -740,7 +749,7 @@ var evaluateSelector = (selector, data, initializedAtomsSet, circularDependencyS
740
749
  const prevCtx = latestEvalContext.get(selector);
741
750
  if (prevCtx)
742
751
  prevCtx.revoked = true;
743
- const evalCtx = { revoked: false };
752
+ const evalCtx = { revoked: false, preserveSignalOnRevoke: false };
744
753
  latestEvalContext.set(selector, evalCtx);
745
754
  if (circularDependencySet.has(selector)) {
746
755
  throw new SelectorCircularDependencyError;
@@ -765,7 +774,9 @@ var evaluateSelector = (selector, data, initializedAtomsSet, circularDependencyS
765
774
  if (!controller) {
766
775
  controller = new AbortController;
767
776
  if (myEvalCtx.revoked) {
768
- controller.abort();
777
+ if (!myEvalCtx.preserveSignalOnRevoke) {
778
+ controller.abort();
779
+ }
769
780
  } else {
770
781
  data.abortControllers.set(selector, controller);
771
782
  }
@@ -812,6 +823,7 @@ var evaluateSelector = (selector, data, initializedAtomsSet, circularDependencyS
812
823
  depsChanged = true;
813
824
  }
814
825
  if (depsChanged || !currentDependencies) {
826
+ noteDependencyGraphChanged(selector, data);
815
827
  if (data.livenessPassActive && currentDependencies) {
816
828
  (data.livenessSeeds ??= new Set).add(selector);
817
829
  if (!depsChangeOut)
@@ -868,7 +880,10 @@ var handleSelectorResult = (value, selector, data) => {
868
880
  if (value instanceof SuspendAndWaitForResolveError) {
869
881
  data.abortControllers.delete(selector);
870
882
  const promise = value.promise;
883
+ const evaluationContext = data.latestEvalContext.get(selector);
871
884
  promise.then(() => {
885
+ if (!evaluationContext || evaluationContext.revoked || data.latestEvalContext.get(selector) !== evaluationContext)
886
+ return;
872
887
  if (!data.stateDependencies.has(selector))
873
888
  return;
874
889
  if (data.values.has(selector) && data.values.get(selector) !== promise)
@@ -886,7 +901,12 @@ var handleSelectorResult = (value, selector, data) => {
886
901
  });
887
902
  return promise;
888
903
  } else if (isPromiseLike(value)) {
904
+ const evaluationContext = data.latestEvalContext.get(selector);
889
905
  value.then((resolved) => {
906
+ if (!evaluationContext || evaluationContext.revoked || data.latestEvalContext.get(selector) !== evaluationContext) {
907
+ pendingAsyncDeps.delete(value);
908
+ return;
909
+ }
890
910
  if (!data.stateDependencies.has(selector)) {
891
911
  pendingAsyncDeps.delete(value);
892
912
  return;
@@ -901,8 +921,13 @@ var handleSelectorResult = (value, selector, data) => {
901
921
  const currentDeps = data.stateDependencies.get(selector);
902
922
  if (currentDeps) {
903
923
  const selectorIsLive = isLive(selector, data);
924
+ let graphChangeNoted = false;
904
925
  for (const dep of currentDeps) {
905
926
  if (!evalDeps.has(dep)) {
927
+ if (!graphChangeNoted) {
928
+ noteDependencyGraphChanged(selector, data);
929
+ graphChangeNoted = true;
930
+ }
906
931
  currentDeps.delete(dep);
907
932
  const dependents2 = data.stateDependents.get(dep);
908
933
  if (dependents2)
@@ -1773,8 +1798,12 @@ var setAtom = (atom, newValue, data, skipOnSet = false) => {
1773
1798
  }
1774
1799
  let syncValue = validateSchema(atom, newValue, data);
1775
1800
  const areEqual = isPromiseLike(currentValue) ? currentValue === syncValue : atom.equal(currentValue, syncValue);
1776
- if (areEqual)
1801
+ if (areEqual) {
1802
+ if (data.parent && !data.values.has(atom)) {
1803
+ return setValueInData(atom, syncValue, data);
1804
+ }
1777
1805
  return syncValue;
1806
+ }
1778
1807
  syncValue = setValueInData(atom, syncValue, data);
1779
1808
  if (atom.onSet && !skipOnSet)
1780
1809
  atom.onSet(syncValue, data);
@@ -2040,6 +2069,10 @@ Object.defineProperties(lazyProto, {
2040
2069
  subscriptionsRequireEqualCheck: makeLazyGetter("subscriptionsRequireEqualCheck"),
2041
2070
  stateDependents: makeLazyGetter("stateDependents"),
2042
2071
  stateDependencies: makeLazyGetter("stateDependencies"),
2072
+ dependencyOrder: makeLazyGetter("dependencyOrder"),
2073
+ cycleRiskInClosure: makeLazyGetter("cycleRiskInClosure"),
2074
+ acyclicDependencyVersion: makeLazyGetter("acyclicDependencyVersion"),
2075
+ orphanCleanupVersion: makeLazyGetter("orphanCleanupVersion"),
2043
2076
  mounts: makeLazyGetter("mounts"),
2044
2077
  liveDependentCount: makeLazyGetter("liveDependentCount"),
2045
2078
  mountInClosure: makeLazyGetter("mountInClosure"),
@@ -2055,6 +2088,10 @@ function createStoreData(id, parent, options) {
2055
2088
  if (enumerable)
2056
2089
  data.enumerable = true;
2057
2090
  data.values = enumerable ? new Map : new WeakMap;
2091
+ data.nextDependencyOrder = 0;
2092
+ data.dependencyGraphVersion = 0;
2093
+ data.pendingOrphanCleanup = undefined;
2094
+ data.orphanCleanupScheduled = false;
2058
2095
  data.scopes = new Map;
2059
2096
  data.scopeValueIndex = new WeakMap;
2060
2097
  data.pendingDefaults = new WeakMap;
@@ -2085,6 +2122,63 @@ var deleteFamilyAtom = (atom, data) => {
2085
2122
  propagateDeletedAtoms([atom], data, undefined, undefined, undefined, undefined, "delete");
2086
2123
  };
2087
2124
 
2125
+ // src/lib/cleanupOrphanedDeps.ts
2126
+ var cleanupOrphanedDeps = (state, data) => {
2127
+ if (isLive(state, data))
2128
+ return;
2129
+ const graphVersion = data.dependencyGraphVersion;
2130
+ const cleanedAtVersion = data.orphanCleanupVersion;
2131
+ if (cleanedAtVersion.get(state) === graphVersion)
2132
+ return;
2133
+ const stack = [state];
2134
+ while (stack.length > 0) {
2135
+ const current = stack.pop();
2136
+ if (cleanedAtVersion.get(current) === graphVersion)
2137
+ continue;
2138
+ if (isLive(current, data))
2139
+ continue;
2140
+ cleanedAtVersion.set(current, graphVersion);
2141
+ const dependents = data.stateDependents.get(current);
2142
+ const deps = data.stateDependencies.get(current);
2143
+ if (deps) {
2144
+ const selector = current;
2145
+ const evaluationContext = data.latestEvalContext.get(selector);
2146
+ if (evaluationContext) {
2147
+ evaluationContext.preserveSignalOnRevoke = true;
2148
+ evaluationContext.revoked = true;
2149
+ }
2150
+ data.latestEvalContext.delete(selector);
2151
+ for (const dep of deps) {
2152
+ data.stateDependents.get(dep)?.delete(current);
2153
+ }
2154
+ data.stateDependencies.delete(current);
2155
+ data.values.delete(current);
2156
+ data.abortControllers.delete(current);
2157
+ if (dependents) {
2158
+ for (const dependent of dependents)
2159
+ stack.push(dependent);
2160
+ }
2161
+ for (const dep of deps)
2162
+ stack.push(dep);
2163
+ continue;
2164
+ }
2165
+ if (dependents) {
2166
+ for (const dependent of dependents)
2167
+ stack.push(dependent);
2168
+ }
2169
+ }
2170
+ };
2171
+
2172
+ // src/lib/flushPendingOrphanCleanup.ts
2173
+ var flushPendingOrphanCleanup = (data) => {
2174
+ const pending = data.pendingOrphanCleanup;
2175
+ if (!pending)
2176
+ return;
2177
+ data.pendingOrphanCleanup = undefined;
2178
+ for (const state of pending)
2179
+ cleanupOrphanedDeps(state, data);
2180
+ };
2181
+
2088
2182
  // src/lib/onStoreChange.ts
2089
2183
  var onStoreChange = (callback, data, options) => {
2090
2184
  const atoms = options?.atoms ?? true;
@@ -2186,6 +2280,26 @@ var deleteMaxAgeCleanup = (data, state) => {
2186
2280
  maxAgeCleanups.get(data)?.delete(state);
2187
2281
  };
2188
2282
 
2283
+ // src/lib/queueOrphanCleanup.ts
2284
+ var queueOrphanCleanup = (state, data) => {
2285
+ if (!data.stateDependencies.has(state) && !data.stateDependents.get(state)?.size) {
2286
+ return;
2287
+ }
2288
+ let pending = data.pendingOrphanCleanup;
2289
+ if (!pending) {
2290
+ pending = new Set;
2291
+ data.pendingOrphanCleanup = pending;
2292
+ }
2293
+ pending.add(state);
2294
+ if (data.orphanCleanupScheduled)
2295
+ return;
2296
+ data.orphanCleanupScheduled = true;
2297
+ queueMicrotask(() => {
2298
+ data.orphanCleanupScheduled = false;
2299
+ flushPendingOrphanCleanup(data);
2300
+ });
2301
+ };
2302
+
2189
2303
  // src/lib/unsubscribe.ts
2190
2304
  var unsubscribe = (state, subscription, data) => {
2191
2305
  const subscribers = data.subscriptions.get(state);
@@ -2211,49 +2325,12 @@ var unsubscribe = (state, subscription, data) => {
2211
2325
  }
2212
2326
  data.subscriptions.delete(state);
2213
2327
  onLastDirectSubscriber(state, data);
2214
- if (isSelector(state) && regionHasCycle(new Set([state]), data)) {
2328
+ if (isSelector(state) && data.cycleRiskInClosure.has(state) && regionHasCycle(state, data)) {
2215
2329
  reconcileLivenessAfterChurn(new Set([state]), data);
2216
2330
  }
2217
- unmountOrphanedDeps(state, data);
2218
- cleanupOrphanedDeps(state, data);
2219
- }
2220
- }
2221
- };
2222
- var cleanupOrphanedDeps = (state, data) => {
2223
- const visited = new Set;
2224
- const stack = [state];
2225
- while (stack.length > 0) {
2226
- const current = stack.pop();
2227
- if (visited.has(current))
2228
- continue;
2229
- visited.add(current);
2230
- if (isLive(current, data))
2231
- continue;
2232
- const dependents = data.stateDependents.get(current);
2233
- const deps = data.stateDependencies.get(current);
2234
- if (deps) {
2235
- for (const dep of deps) {
2236
- const depDependents = data.stateDependents.get(dep);
2237
- if (depDependents) {
2238
- depDependents.delete(current);
2239
- }
2240
- }
2241
- data.stateDependencies.delete(current);
2242
- data.values.delete(current);
2243
- data.abortControllers.delete(current);
2244
- if (dependents) {
2245
- for (const dep of dependents) {
2246
- stack.push(dep);
2247
- }
2248
- }
2249
- for (const dep of deps) {
2250
- stack.push(dep);
2251
- }
2252
- continue;
2253
- }
2254
- if (dependents) {
2255
- for (const dep of dependents) {
2256
- stack.push(dep);
2331
+ if (!isLive(state, data)) {
2332
+ unmountOrphanedDeps(state, data);
2333
+ queueOrphanCleanup(state, data);
2257
2334
  }
2258
2335
  }
2259
2336
  }
@@ -3032,6 +3109,8 @@ function storeFromStoreData(data, detach) {
3032
3109
  return _pendingTxn;
3033
3110
  };
3034
3111
  const getDefault = (state) => {
3112
+ if (data.pendingOrphanCleanup)
3113
+ flushPendingOrphanCleanup(data);
3035
3114
  if (data.values.has(state)) {
3036
3115
  if (!isCachedValueStale(state, data)) {
3037
3116
  return data.values.get(state);
@@ -3064,12 +3143,16 @@ function storeFromStoreData(data, detach) {
3064
3143
  };
3065
3144
  const getBatched = (state) => {
3066
3145
  if (_pendingTxn) {
3146
+ if (data.pendingOrphanCleanup)
3147
+ flushPendingOrphanCleanup(data);
3067
3148
  return _pendingTxn.get(state);
3068
3149
  }
3069
3150
  return getDefault(state);
3070
3151
  };
3071
3152
  const get = data.batchUpdates ? getBatched : getDefault;
3072
3153
  const setDefault = (state, value) => {
3154
+ if (data.pendingOrphanCleanup)
3155
+ flushPendingOrphanCleanup(data);
3073
3156
  if (isAtom(state))
3074
3157
  return setAtom(state, value, data);
3075
3158
  if (isSelector(state))
@@ -3077,6 +3160,8 @@ function storeFromStoreData(data, detach) {
3077
3160
  throw new Error(InvalidStateSetError);
3078
3161
  };
3079
3162
  const setBatched = (state, value) => {
3163
+ if (data.pendingOrphanCleanup)
3164
+ flushPendingOrphanCleanup(data);
3080
3165
  if (isAtom(state)) {
3081
3166
  return ensurePendingTxn().set(state, value);
3082
3167
  }
@@ -3086,29 +3171,45 @@ function storeFromStoreData(data, detach) {
3086
3171
  };
3087
3172
  const set = data.batchUpdates ? setBatched : setDefault;
3088
3173
  const reset = (atom) => {
3174
+ if (data.pendingOrphanCleanup)
3175
+ flushPendingOrphanCleanup(data);
3089
3176
  if (data.batchUpdates)
3090
3177
  flushPendingTxn();
3091
3178
  return resetAtom(atom, data);
3092
3179
  };
3093
3180
  const del = (atom) => {
3181
+ if (data.pendingOrphanCleanup)
3182
+ flushPendingOrphanCleanup(data);
3094
3183
  if (data.batchUpdates)
3095
3184
  flushPendingTxn();
3096
3185
  return deleteFamilyAtom(atom, data);
3097
3186
  };
3098
3187
  const unset = (atom) => {
3188
+ if (data.pendingOrphanCleanup)
3189
+ flushPendingOrphanCleanup(data);
3099
3190
  if (data.batchUpdates)
3100
3191
  flushPendingTxn();
3101
3192
  return unsetValue(atom, data);
3102
3193
  };
3103
- const sub = (state, callback, deepEqualCheckBeforeCallback = true) => subscribe(state, callback, deepEqualCheckBeforeCallback, data);
3194
+ const sub = (state, callback, deepEqualCheckBeforeCallback = true) => {
3195
+ if (data.pendingOrphanCleanup)
3196
+ flushPendingOrphanCleanup(data);
3197
+ return subscribe(state, callback, deepEqualCheckBeforeCallback, data);
3198
+ };
3104
3199
  const txn = (callback, name) => {
3200
+ if (data.pendingOrphanCleanup)
3201
+ flushPendingOrphanCleanup(data);
3105
3202
  if (data.batchUpdates)
3106
3203
  flushPendingTxn();
3107
3204
  return transaction(callback, data, name);
3108
3205
  };
3109
3206
  const onChange = (callback, options) => onStoreChange(callback, data, options);
3110
3207
  const storeOnCommitEnd = (callback) => onCommitEnd(callback, data);
3111
- const storeSnapshot = () => snapshot(data);
3208
+ const storeSnapshot = () => {
3209
+ if (data.pendingOrphanCleanup)
3210
+ flushPendingOrphanCleanup(data);
3211
+ return snapshot(data);
3212
+ };
3112
3213
  const scope = (scopeId, callback) => {
3113
3214
  if (callback) {
3114
3215
  if (!data.scopes.has(scopeId)) {
@@ -3212,7 +3313,30 @@ var propagateMountMarkerUp = (state, data) => {
3212
3313
  }
3213
3314
  }
3214
3315
  };
3316
+ var propagateCycleRiskUp = (state, data) => {
3317
+ if (data.cycleRiskInClosure.has(state))
3318
+ return;
3319
+ data.cycleRiskInClosure.set(state, true);
3320
+ const stack = [state];
3321
+ while (stack.length > 0) {
3322
+ const current = stack.pop();
3323
+ const parents = data.stateDependents.get(current);
3324
+ if (!parents)
3325
+ continue;
3326
+ for (const parent of parents) {
3327
+ if (data.cycleRiskInClosure.has(parent))
3328
+ continue;
3329
+ data.cycleRiskInClosure.set(parent, true);
3330
+ stack.push(parent);
3331
+ }
3332
+ }
3333
+ };
3215
3334
  var noteDependencyAdded = (selector, dep, data) => {
3335
+ const selectorOrder = data.dependencyOrder.get(selector);
3336
+ const depOrder = data.dependencyOrder.get(dep);
3337
+ if (data.cycleRiskInClosure.has(dep) || selectorOrder === undefined || isSelector(dep) && (depOrder === undefined || depOrder >= selectorOrder)) {
3338
+ propagateCycleRiskUp(selector, data);
3339
+ }
3216
3340
  if (!hasMountInClosure(dep, data))
3217
3341
  return;
3218
3342
  if (data.mountInClosure.has(selector))
@@ -3295,36 +3419,53 @@ var onLiveDependencyRemoved = (dep, data) => {
3295
3419
  propagateNotLive(dep, data);
3296
3420
  }
3297
3421
  };
3298
- var regionHasCycle = (seeds, data) => {
3299
- const done = new Set;
3422
+ var seedClosureHasCycle = (seed, data, graphVersion, acyclicAtVersion) => {
3423
+ if (acyclicAtVersion.get(seed) === graphVersion)
3424
+ return false;
3300
3425
  const onPath = new Set;
3301
- for (const seed of seeds) {
3302
- if (done.has(seed))
3426
+ const stack = [
3427
+ { node: seed, it: (data.stateDependencies.get(seed) ?? EMPTY).values() }
3428
+ ];
3429
+ onPath.add(seed);
3430
+ while (stack.length > 0) {
3431
+ const frame = stack[stack.length - 1];
3432
+ const next = frame.it.next();
3433
+ if (next.done) {
3434
+ onPath.delete(frame.node);
3435
+ acyclicAtVersion.set(frame.node, graphVersion);
3436
+ stack.pop();
3303
3437
  continue;
3304
- const stack = [
3305
- { node: seed, it: (data.stateDependencies.get(seed) ?? EMPTY).values() }
3306
- ];
3307
- onPath.add(seed);
3308
- while (stack.length > 0) {
3309
- const frame = stack[stack.length - 1];
3310
- const next = frame.it.next();
3311
- if (next.done) {
3312
- onPath.delete(frame.node);
3313
- done.add(frame.node);
3314
- stack.pop();
3315
- continue;
3316
- }
3317
- const dep = next.value;
3318
- if (onPath.has(dep))
3438
+ }
3439
+ const dep = next.value;
3440
+ if (onPath.has(dep))
3441
+ return true;
3442
+ if (acyclicAtVersion.get(dep) === graphVersion)
3443
+ continue;
3444
+ onPath.add(dep);
3445
+ stack.push({
3446
+ node: dep,
3447
+ it: (data.stateDependencies.get(dep) ?? EMPTY).values()
3448
+ });
3449
+ }
3450
+ return false;
3451
+ };
3452
+ var regionHasCycle = (seeds, data) => {
3453
+ const graphVersion = data.dependencyGraphVersion;
3454
+ const acyclicAtVersion = data.acyclicDependencyVersion;
3455
+ if (seeds instanceof Set) {
3456
+ for (const seed of seeds) {
3457
+ if (seedClosureHasCycle(seed, data, graphVersion, acyclicAtVersion)) {
3319
3458
  return true;
3320
- if (done.has(dep))
3321
- continue;
3322
- onPath.add(dep);
3323
- stack.push({
3324
- node: dep,
3325
- it: (data.stateDependencies.get(dep) ?? EMPTY).values()
3326
- });
3459
+ }
3327
3460
  }
3461
+ return false;
3462
+ }
3463
+ return seedClosureHasCycle(seeds, data, graphVersion, acyclicAtVersion);
3464
+ };
3465
+ var seededRegionMayHaveCycle = (seeds, data) => {
3466
+ for (const seed of seeds) {
3467
+ if (data.cycleRiskInClosure.has(seed))
3468
+ return true;
3328
3469
  }
3329
3470
  return false;
3330
3471
  };
@@ -3344,7 +3485,7 @@ var endLivenessPass = (data) => {
3344
3485
  data.livenessSeeds = undefined;
3345
3486
  data.livenessLazyArmed = false;
3346
3487
  data.livenessRemovalArmed = false;
3347
- if (seeds && seeds.size > 0 && (lazyArmed || removalArmed && regionHasCycle(seeds, data))) {
3488
+ if (seeds && seeds.size > 0 && (lazyArmed || removalArmed && seededRegionMayHaveCycle(seeds, data) && regionHasCycle(seeds, data))) {
3348
3489
  return seeds;
3349
3490
  }
3350
3491
  return null;
@@ -3687,6 +3828,7 @@ function atom(defaultValue, options) {
3687
3828
  return created;
3688
3829
  }
3689
3830
  // src/lib/stableStringify.ts
3831
+ var compareStrings = (a, b) => a < b ? -1 : a > b ? 1 : 0;
3690
3832
  var stableStringifyRecurse = (x, key) => {
3691
3833
  if (typeof x === "string" && !x.includes('"') && !x.includes("\\")) {
3692
3834
  return `"${x}"`;
@@ -3720,14 +3862,18 @@ var stableStringifyRecurse = (x, key) => {
3720
3862
  return stableStringifyRecurse(x.toJSON(key), key);
3721
3863
  }
3722
3864
  if (x instanceof Map) {
3723
- const obj = {};
3724
- for (const [k, v] of x) {
3725
- obj[typeof k === "string" ? k : stringify(k, opt)] = v;
3726
- }
3727
- return stableStringifyRecurse(obj, key);
3865
+ const entries = Array.from(x, ([k, v]) => [
3866
+ stableStringifyRecurse(k),
3867
+ stableStringifyRecurse(v)
3868
+ ]).sort(([ak, av], [bk, bv]) => {
3869
+ const keyOrder = compareStrings(ak, bk);
3870
+ return keyOrder || compareStrings(av, bv);
3871
+ });
3872
+ return `__MAP(${stableStringifyRecurse(entries, key)})__`;
3728
3873
  }
3729
3874
  if (x instanceof Set) {
3730
- return stableStringifyRecurse(Array.from(x).sort((a, b) => stableStringifyRecurse(a).localeCompare(stableStringifyRecurse(b))), key);
3875
+ const values = Array.from(x, (v) => stableStringifyRecurse(v)).sort();
3876
+ return `__SET(${stableStringifyRecurse(values, key)})__`;
3731
3877
  }
3732
3878
  if (Symbol !== undefined && x[Symbol.iterator] != null && typeof x[Symbol.iterator] === "function") {
3733
3879
  return stableStringifyRecurse(Array.from(x), key);
@@ -4091,9 +4237,9 @@ var isFamilySelector = (state) => isFamilyState(state) && isSelector(state);
4091
4237
  // src/index.ts
4092
4238
  var slot = valdresGlobal();
4093
4239
  if (slot.version) {
4094
- throw new Error(`Error! An instance of valdres is already loaded. Loaded: ${slot.version}. Attempted to load: ${"1.0.0-beta.14"}`);
4240
+ throw new Error(`Error! An instance of valdres is already loaded. Loaded: ${slot.version}. Attempted to load: ${"1.0.0-beta.16"}`);
4095
4241
  } else {
4096
- slot.version = "1.0.0-beta.14";
4242
+ slot.version = "1.0.0-beta.16";
4097
4243
  }
4098
4244
  export {
4099
4245
  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
  }>;
@@ -92,6 +120,7 @@ export type StoreData = {
92
120
  * registration from store1's in-flight async eval. */
93
121
  latestEvalContext: WeakMap<Selector, {
94
122
  revoked: boolean;
123
+ preserveSignalOnRevoke: boolean;
95
124
  }>;
96
125
  /** Per-atom timestamp of the last value write, used for lazy
97
126
  * maxAge revalidation when the atom is unmounted (no active timer
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.14",
4
+ "version": "1.0.0-beta.16",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Eigil Sagafos"