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 +316 -113
- 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)
|
|
@@ -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
|
|
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) &&
|
|
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
|
-
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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 (
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
-
|
|
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) &&
|
|
2311
|
+
if (isSelector(state) && data.cycleRiskInClosure.has(state) && regionHasCycle(state, data)) {
|
|
2152
2312
|
reconcileLivenessAfterChurn(new Set([state]), data);
|
|
2153
2313
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
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) =>
|
|
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 = () =>
|
|
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
|
|
3225
|
-
|
|
3405
|
+
var seedClosureHasCycle = (seed, data, graphVersion, acyclicAtVersion) => {
|
|
3406
|
+
if (acyclicAtVersion.get(seed) === graphVersion)
|
|
3407
|
+
return false;
|
|
3226
3408
|
const onPath = new Set;
|
|
3227
|
-
|
|
3228
|
-
|
|
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
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
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
|
-
|
|
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
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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