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 +230 -84
- 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 +29 -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);
|
|
@@ -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
|
-
|
|
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) &&
|
|
2328
|
+
if (isSelector(state) && data.cycleRiskInClosure.has(state) && regionHasCycle(state, data)) {
|
|
2215
2329
|
reconcileLivenessAfterChurn(new Set([state]), data);
|
|
2216
2330
|
}
|
|
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);
|
|
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) =>
|
|
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 = () =>
|
|
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
|
|
3299
|
-
|
|
3422
|
+
var seedClosureHasCycle = (seed, data, graphVersion, acyclicAtVersion) => {
|
|
3423
|
+
if (acyclicAtVersion.get(seed) === graphVersion)
|
|
3424
|
+
return false;
|
|
3300
3425
|
const onPath = new Set;
|
|
3301
|
-
|
|
3302
|
-
|
|
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
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
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
|
-
|
|
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
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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