valdres 1.0.0-beta.14 → 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 +211 -82
- package/dist/types/lib/cleanupOrphanedDeps.d.ts +9 -0
- package/dist/types/lib/flushPendingOrphanCleanup.d.ts +7 -0
- package/dist/types/lib/mountAtom.d.ts +1 -21
- package/dist/types/lib/noteDependencyGraphChanged.d.ts +7 -0
- package/dist/types/lib/queueOrphanCleanup.d.ts +9 -0
- package/dist/types/types/StoreData.d.ts +28 -0
- package/package.json +1 -1
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)
|
|
@@ -1773,8 +1788,12 @@ var setAtom = (atom, newValue, data, skipOnSet = false) => {
|
|
|
1773
1788
|
}
|
|
1774
1789
|
let syncValue = validateSchema(atom, newValue, data);
|
|
1775
1790
|
const areEqual = isPromiseLike(currentValue) ? currentValue === syncValue : atom.equal(currentValue, syncValue);
|
|
1776
|
-
if (areEqual)
|
|
1791
|
+
if (areEqual) {
|
|
1792
|
+
if (data.parent && !data.values.has(atom)) {
|
|
1793
|
+
return setValueInData(atom, syncValue, data);
|
|
1794
|
+
}
|
|
1777
1795
|
return syncValue;
|
|
1796
|
+
}
|
|
1778
1797
|
syncValue = setValueInData(atom, syncValue, data);
|
|
1779
1798
|
if (atom.onSet && !skipOnSet)
|
|
1780
1799
|
atom.onSet(syncValue, data);
|
|
@@ -2040,6 +2059,10 @@ Object.defineProperties(lazyProto, {
|
|
|
2040
2059
|
subscriptionsRequireEqualCheck: makeLazyGetter("subscriptionsRequireEqualCheck"),
|
|
2041
2060
|
stateDependents: makeLazyGetter("stateDependents"),
|
|
2042
2061
|
stateDependencies: makeLazyGetter("stateDependencies"),
|
|
2062
|
+
dependencyOrder: makeLazyGetter("dependencyOrder"),
|
|
2063
|
+
cycleRiskInClosure: makeLazyGetter("cycleRiskInClosure"),
|
|
2064
|
+
acyclicDependencyVersion: makeLazyGetter("acyclicDependencyVersion"),
|
|
2065
|
+
orphanCleanupVersion: makeLazyGetter("orphanCleanupVersion"),
|
|
2043
2066
|
mounts: makeLazyGetter("mounts"),
|
|
2044
2067
|
liveDependentCount: makeLazyGetter("liveDependentCount"),
|
|
2045
2068
|
mountInClosure: makeLazyGetter("mountInClosure"),
|
|
@@ -2055,6 +2078,10 @@ function createStoreData(id, parent, options) {
|
|
|
2055
2078
|
if (enumerable)
|
|
2056
2079
|
data.enumerable = true;
|
|
2057
2080
|
data.values = enumerable ? new Map : new WeakMap;
|
|
2081
|
+
data.nextDependencyOrder = 0;
|
|
2082
|
+
data.dependencyGraphVersion = 0;
|
|
2083
|
+
data.pendingOrphanCleanup = undefined;
|
|
2084
|
+
data.orphanCleanupScheduled = false;
|
|
2058
2085
|
data.scopes = new Map;
|
|
2059
2086
|
data.scopeValueIndex = new WeakMap;
|
|
2060
2087
|
data.pendingDefaults = new WeakMap;
|
|
@@ -2085,6 +2112,56 @@ var deleteFamilyAtom = (atom, data) => {
|
|
|
2085
2112
|
propagateDeletedAtoms([atom], data, undefined, undefined, undefined, undefined, "delete");
|
|
2086
2113
|
};
|
|
2087
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
|
+
|
|
2088
2165
|
// src/lib/onStoreChange.ts
|
|
2089
2166
|
var onStoreChange = (callback, data, options) => {
|
|
2090
2167
|
const atoms = options?.atoms ?? true;
|
|
@@ -2186,6 +2263,26 @@ var deleteMaxAgeCleanup = (data, state) => {
|
|
|
2186
2263
|
maxAgeCleanups.get(data)?.delete(state);
|
|
2187
2264
|
};
|
|
2188
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
|
+
|
|
2189
2286
|
// src/lib/unsubscribe.ts
|
|
2190
2287
|
var unsubscribe = (state, subscription, data) => {
|
|
2191
2288
|
const subscribers = data.subscriptions.get(state);
|
|
@@ -2211,49 +2308,12 @@ var unsubscribe = (state, subscription, data) => {
|
|
|
2211
2308
|
}
|
|
2212
2309
|
data.subscriptions.delete(state);
|
|
2213
2310
|
onLastDirectSubscriber(state, data);
|
|
2214
|
-
if (isSelector(state) &&
|
|
2311
|
+
if (isSelector(state) && data.cycleRiskInClosure.has(state) && regionHasCycle(state, data)) {
|
|
2215
2312
|
reconcileLivenessAfterChurn(new Set([state]), data);
|
|
2216
2313
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
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);
|
|
2314
|
+
if (!isLive(state, data)) {
|
|
2315
|
+
unmountOrphanedDeps(state, data);
|
|
2316
|
+
queueOrphanCleanup(state, data);
|
|
2257
2317
|
}
|
|
2258
2318
|
}
|
|
2259
2319
|
}
|
|
@@ -3032,6 +3092,8 @@ function storeFromStoreData(data, detach) {
|
|
|
3032
3092
|
return _pendingTxn;
|
|
3033
3093
|
};
|
|
3034
3094
|
const getDefault = (state) => {
|
|
3095
|
+
if (data.pendingOrphanCleanup)
|
|
3096
|
+
flushPendingOrphanCleanup(data);
|
|
3035
3097
|
if (data.values.has(state)) {
|
|
3036
3098
|
if (!isCachedValueStale(state, data)) {
|
|
3037
3099
|
return data.values.get(state);
|
|
@@ -3064,12 +3126,16 @@ function storeFromStoreData(data, detach) {
|
|
|
3064
3126
|
};
|
|
3065
3127
|
const getBatched = (state) => {
|
|
3066
3128
|
if (_pendingTxn) {
|
|
3129
|
+
if (data.pendingOrphanCleanup)
|
|
3130
|
+
flushPendingOrphanCleanup(data);
|
|
3067
3131
|
return _pendingTxn.get(state);
|
|
3068
3132
|
}
|
|
3069
3133
|
return getDefault(state);
|
|
3070
3134
|
};
|
|
3071
3135
|
const get = data.batchUpdates ? getBatched : getDefault;
|
|
3072
3136
|
const setDefault = (state, value) => {
|
|
3137
|
+
if (data.pendingOrphanCleanup)
|
|
3138
|
+
flushPendingOrphanCleanup(data);
|
|
3073
3139
|
if (isAtom(state))
|
|
3074
3140
|
return setAtom(state, value, data);
|
|
3075
3141
|
if (isSelector(state))
|
|
@@ -3077,6 +3143,8 @@ function storeFromStoreData(data, detach) {
|
|
|
3077
3143
|
throw new Error(InvalidStateSetError);
|
|
3078
3144
|
};
|
|
3079
3145
|
const setBatched = (state, value) => {
|
|
3146
|
+
if (data.pendingOrphanCleanup)
|
|
3147
|
+
flushPendingOrphanCleanup(data);
|
|
3080
3148
|
if (isAtom(state)) {
|
|
3081
3149
|
return ensurePendingTxn().set(state, value);
|
|
3082
3150
|
}
|
|
@@ -3086,29 +3154,45 @@ function storeFromStoreData(data, detach) {
|
|
|
3086
3154
|
};
|
|
3087
3155
|
const set = data.batchUpdates ? setBatched : setDefault;
|
|
3088
3156
|
const reset = (atom) => {
|
|
3157
|
+
if (data.pendingOrphanCleanup)
|
|
3158
|
+
flushPendingOrphanCleanup(data);
|
|
3089
3159
|
if (data.batchUpdates)
|
|
3090
3160
|
flushPendingTxn();
|
|
3091
3161
|
return resetAtom(atom, data);
|
|
3092
3162
|
};
|
|
3093
3163
|
const del = (atom) => {
|
|
3164
|
+
if (data.pendingOrphanCleanup)
|
|
3165
|
+
flushPendingOrphanCleanup(data);
|
|
3094
3166
|
if (data.batchUpdates)
|
|
3095
3167
|
flushPendingTxn();
|
|
3096
3168
|
return deleteFamilyAtom(atom, data);
|
|
3097
3169
|
};
|
|
3098
3170
|
const unset = (atom) => {
|
|
3171
|
+
if (data.pendingOrphanCleanup)
|
|
3172
|
+
flushPendingOrphanCleanup(data);
|
|
3099
3173
|
if (data.batchUpdates)
|
|
3100
3174
|
flushPendingTxn();
|
|
3101
3175
|
return unsetValue(atom, data);
|
|
3102
3176
|
};
|
|
3103
|
-
const sub = (state, callback, deepEqualCheckBeforeCallback = true) =>
|
|
3177
|
+
const sub = (state, callback, deepEqualCheckBeforeCallback = true) => {
|
|
3178
|
+
if (data.pendingOrphanCleanup)
|
|
3179
|
+
flushPendingOrphanCleanup(data);
|
|
3180
|
+
return subscribe(state, callback, deepEqualCheckBeforeCallback, data);
|
|
3181
|
+
};
|
|
3104
3182
|
const txn = (callback, name) => {
|
|
3183
|
+
if (data.pendingOrphanCleanup)
|
|
3184
|
+
flushPendingOrphanCleanup(data);
|
|
3105
3185
|
if (data.batchUpdates)
|
|
3106
3186
|
flushPendingTxn();
|
|
3107
3187
|
return transaction(callback, data, name);
|
|
3108
3188
|
};
|
|
3109
3189
|
const onChange = (callback, options) => onStoreChange(callback, data, options);
|
|
3110
3190
|
const storeOnCommitEnd = (callback) => onCommitEnd(callback, data);
|
|
3111
|
-
const storeSnapshot = () =>
|
|
3191
|
+
const storeSnapshot = () => {
|
|
3192
|
+
if (data.pendingOrphanCleanup)
|
|
3193
|
+
flushPendingOrphanCleanup(data);
|
|
3194
|
+
return snapshot(data);
|
|
3195
|
+
};
|
|
3112
3196
|
const scope = (scopeId, callback) => {
|
|
3113
3197
|
if (callback) {
|
|
3114
3198
|
if (!data.scopes.has(scopeId)) {
|
|
@@ -3212,7 +3296,30 @@ var propagateMountMarkerUp = (state, data) => {
|
|
|
3212
3296
|
}
|
|
3213
3297
|
}
|
|
3214
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
|
+
};
|
|
3215
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
|
+
}
|
|
3216
3323
|
if (!hasMountInClosure(dep, data))
|
|
3217
3324
|
return;
|
|
3218
3325
|
if (data.mountInClosure.has(selector))
|
|
@@ -3295,36 +3402,53 @@ var onLiveDependencyRemoved = (dep, data) => {
|
|
|
3295
3402
|
propagateNotLive(dep, data);
|
|
3296
3403
|
}
|
|
3297
3404
|
};
|
|
3298
|
-
var
|
|
3299
|
-
|
|
3405
|
+
var seedClosureHasCycle = (seed, data, graphVersion, acyclicAtVersion) => {
|
|
3406
|
+
if (acyclicAtVersion.get(seed) === graphVersion)
|
|
3407
|
+
return false;
|
|
3300
3408
|
const onPath = new Set;
|
|
3301
|
-
|
|
3302
|
-
|
|
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();
|
|
3303
3420
|
continue;
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
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)) {
|
|
3319
3441
|
return true;
|
|
3320
|
-
|
|
3321
|
-
continue;
|
|
3322
|
-
onPath.add(dep);
|
|
3323
|
-
stack.push({
|
|
3324
|
-
node: dep,
|
|
3325
|
-
it: (data.stateDependencies.get(dep) ?? EMPTY).values()
|
|
3326
|
-
});
|
|
3442
|
+
}
|
|
3327
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;
|
|
3328
3452
|
}
|
|
3329
3453
|
return false;
|
|
3330
3454
|
};
|
|
@@ -3344,7 +3468,7 @@ var endLivenessPass = (data) => {
|
|
|
3344
3468
|
data.livenessSeeds = undefined;
|
|
3345
3469
|
data.livenessLazyArmed = false;
|
|
3346
3470
|
data.livenessRemovalArmed = false;
|
|
3347
|
-
if (seeds && seeds.size > 0 && (lazyArmed || removalArmed && regionHasCycle(seeds, data))) {
|
|
3471
|
+
if (seeds && seeds.size > 0 && (lazyArmed || removalArmed && seededRegionMayHaveCycle(seeds, data) && regionHasCycle(seeds, data))) {
|
|
3348
3472
|
return seeds;
|
|
3349
3473
|
}
|
|
3350
3474
|
return null;
|
|
@@ -3687,6 +3811,7 @@ function atom(defaultValue, options) {
|
|
|
3687
3811
|
return created;
|
|
3688
3812
|
}
|
|
3689
3813
|
// src/lib/stableStringify.ts
|
|
3814
|
+
var compareStrings = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
3690
3815
|
var stableStringifyRecurse = (x, key) => {
|
|
3691
3816
|
if (typeof x === "string" && !x.includes('"') && !x.includes("\\")) {
|
|
3692
3817
|
return `"${x}"`;
|
|
@@ -3720,14 +3845,18 @@ var stableStringifyRecurse = (x, key) => {
|
|
|
3720
3845
|
return stableStringifyRecurse(x.toJSON(key), key);
|
|
3721
3846
|
}
|
|
3722
3847
|
if (x instanceof Map) {
|
|
3723
|
-
const
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
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)})__`;
|
|
3728
3856
|
}
|
|
3729
3857
|
if (x instanceof Set) {
|
|
3730
|
-
|
|
3858
|
+
const values = Array.from(x, (v) => stableStringifyRecurse(v)).sort();
|
|
3859
|
+
return `__SET(${stableStringifyRecurse(values, key)})__`;
|
|
3731
3860
|
}
|
|
3732
3861
|
if (Symbol !== undefined && x[Symbol.iterator] != null && typeof x[Symbol.iterator] === "function") {
|
|
3733
3862
|
return stableStringifyRecurse(Array.from(x), key);
|
|
@@ -4091,9 +4220,9 @@ var isFamilySelector = (state) => isFamilyState(state) && isSelector(state);
|
|
|
4091
4220
|
// src/index.ts
|
|
4092
4221
|
var slot = valdresGlobal();
|
|
4093
4222
|
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.
|
|
4223
|
+
throw new Error(`Error! An instance of valdres is already loaded. Loaded: ${slot.version}. Attempted to load: ${"1.0.0-beta.15"}`);
|
|
4095
4224
|
} else {
|
|
4096
|
-
slot.version = "1.0.0-beta.
|
|
4225
|
+
slot.version = "1.0.0-beta.15";
|
|
4097
4226
|
}
|
|
4098
4227
|
export {
|
|
4099
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