@solidjs/signals 2.0.0-beta.8 → 2.0.0-rc.0
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/README.md +13 -9
- package/dist/dev.js +6078 -1440
- package/dist/node.cjs +7684 -3248
- package/dist/prod/affects.js +126 -0
- package/dist/prod/boundaries.js +572 -0
- package/dist/prod/core/action.js +139 -0
- package/dist/prod/core/async.js +553 -0
- package/dist/prod/core/constants.js +92 -0
- package/dist/prod/core/context.js +67 -0
- package/dist/prod/core/core.js +817 -0
- package/dist/prod/core/dev.js +3 -0
- package/dist/prod/core/effect.js +145 -0
- package/dist/prod/core/error.js +68 -0
- package/dist/prod/core/external.js +98 -0
- package/dist/prod/core/graph.js +104 -0
- package/dist/prod/core/heap.js +140 -0
- package/dist/prod/core/invariants.js +42 -0
- package/dist/prod/core/lanes.js +140 -0
- package/dist/prod/core/optimistic.js +245 -0
- package/dist/prod/core/owner.js +302 -0
- package/dist/prod/core/scheduler.js +749 -0
- package/dist/prod/core/verdict.js +369 -0
- package/dist/prod/index.js +45 -0
- package/dist/prod/map.js +317 -0
- package/dist/prod/signals.js +375 -0
- package/dist/prod/store/optimistic.js +217 -0
- package/dist/prod/store/projection.js +231 -0
- package/dist/prod/store/reconcile.js +707 -0
- package/dist/prod/store/store.js +1081 -0
- package/dist/prod/store/storePath.js +103 -0
- package/dist/prod/store/utils.js +328 -0
- package/dist/types/affects.d.ts +47 -0
- package/dist/types/boundaries.d.ts +93 -11
- package/dist/types/core/action.d.ts +63 -0
- package/dist/types/core/async.d.ts +16 -1
- package/dist/types/core/constants.d.ts +46 -0
- package/dist/types/core/context.d.ts +10 -2
- package/dist/types/core/core.d.ts +111 -17
- package/dist/types/core/dev.d.ts +17 -2
- package/dist/types/core/effect.d.ts +1 -2
- package/dist/types/core/error.d.ts +33 -0
- package/dist/types/core/external.d.ts +0 -11
- package/dist/types/core/graph.d.ts +2 -1
- package/dist/types/core/heap.d.ts +8 -0
- package/dist/types/core/index.d.ts +3 -2
- package/dist/types/core/invariants.d.ts +59 -0
- package/dist/types/core/lanes.d.ts +2 -0
- package/dist/types/core/optimistic.d.ts +6 -0
- package/dist/types/core/owner.d.ts +101 -3
- package/dist/types/core/scheduler.d.ts +97 -9
- package/dist/types/core/types.d.ts +68 -6
- package/dist/types/core/verdict.d.ts +2 -0
- package/dist/types/index.d.ts +4 -3
- package/dist/types/map.d.ts +50 -5
- package/dist/types/signals.d.ts +424 -19
- package/dist/types/store/index.d.ts +1 -1
- package/dist/types/store/optimistic.d.ts +39 -13
- package/dist/types/store/projection.d.ts +48 -18
- package/dist/types/store/reconcile.d.ts +46 -1
- package/dist/types/store/store.d.ts +164 -19
- package/dist/types/store/storePath.d.ts +28 -0
- package/dist/types/store/utils.d.ts +78 -7
- package/dist/types-cjs/affects.d.cts +47 -0
- package/dist/types-cjs/boundaries.d.cts +93 -11
- package/dist/types-cjs/core/action.d.cts +63 -0
- package/dist/types-cjs/core/async.d.cts +16 -1
- package/dist/types-cjs/core/constants.d.cts +46 -0
- package/dist/types-cjs/core/context.d.cts +10 -2
- package/dist/types-cjs/core/core.d.cts +111 -17
- package/dist/types-cjs/core/dev.d.cts +17 -2
- package/dist/types-cjs/core/effect.d.cts +1 -2
- package/dist/types-cjs/core/error.d.cts +33 -0
- package/dist/types-cjs/core/external.d.cts +0 -11
- package/dist/types-cjs/core/graph.d.cts +2 -1
- package/dist/types-cjs/core/heap.d.cts +8 -0
- package/dist/types-cjs/core/index.d.cts +3 -2
- package/dist/types-cjs/core/invariants.d.cts +59 -0
- package/dist/types-cjs/core/lanes.d.cts +2 -0
- package/dist/types-cjs/core/optimistic.d.cts +6 -0
- package/dist/types-cjs/core/owner.d.cts +101 -3
- package/dist/types-cjs/core/scheduler.d.cts +97 -9
- package/dist/types-cjs/core/types.d.cts +68 -6
- package/dist/types-cjs/core/verdict.d.cts +2 -0
- package/dist/types-cjs/index.d.cts +4 -3
- package/dist/types-cjs/map.d.cts +50 -5
- package/dist/types-cjs/signals.d.cts +424 -19
- package/dist/types-cjs/store/index.d.cts +1 -1
- package/dist/types-cjs/store/optimistic.d.cts +39 -13
- package/dist/types-cjs/store/projection.d.cts +48 -18
- package/dist/types-cjs/store/reconcile.d.cts +46 -1
- package/dist/types-cjs/store/store.d.cts +164 -19
- package/dist/types-cjs/store/storePath.d.cts +28 -0
- package/dist/types-cjs/store/utils.d.cts +78 -7
- package/package.json +14 -11
- package/dist/prod.js +0 -3577
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { REACTIVE_DISPOSED, STATUS_ERROR, EFFECT_USER, CONFIG_AUTO_DISPOSE, CONFIG_CHILDREN_FORBIDDEN, EFFECT_TRACKED, STATUS_PENDING, EFFECT_RENDER } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
import { createEffectNode, recompute, computed, staleValues } from "./core.js";
|
|
4
|
+
|
|
5
|
+
import { unwrapStatusError, StatusError } from "./error.js";
|
|
6
|
+
|
|
7
|
+
import { GlobalQueue, haltReactivity } from "./scheduler.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Effects are the leaf nodes of our reactive graph. When their sources change, they are
|
|
11
|
+
* automatically added to the queue of effects to re-execute, which will cause them to fetch their
|
|
12
|
+
* sources and recompute
|
|
13
|
+
*/ function effect(t, E, e, R) {
|
|
14
|
+
const r = !!R?.user;
|
|
15
|
+
const f = createEffectNode(t, E, e, r ? EFFECT_USER : EFFECT_RENDER, notifyEffectStatus, R);
|
|
16
|
+
recompute(f, true);
|
|
17
|
+
!R?.defer && (f.Re === EFFECT_USER || R?.schedule ? f.C.enqueue(f.Re, runEffect.bind(null, f)) : runEffect(f));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function notifyEffectStatus(t, E) {
|
|
21
|
+
// Use passed values if provided, otherwise read from node
|
|
22
|
+
const e = t !== undefined ? t : this.S;
|
|
23
|
+
const R = E !== undefined ? E : this._;
|
|
24
|
+
if (e & STATUS_ERROR) {
|
|
25
|
+
this.C.notify(this, STATUS_PENDING, 0);
|
|
26
|
+
if (this.Re === EFFECT_USER) {
|
|
27
|
+
// The error handler is the error arm of the effect phase (#2840 ruling):
|
|
28
|
+
// queue it like the effect function. It runs in the same imperative,
|
|
29
|
+
// writable scope, throws escalate the same way, and a held transition
|
|
30
|
+
// (or optimistic lane) defers it exactly as it defers the success arm.
|
|
31
|
+
// No payload is queued — the node already carries `_statusFlags`/`_error`,
|
|
32
|
+
// and the runner dispatches on them, so a recovery before the effect
|
|
33
|
+
// phase takes the success arm instead. Blocked forwards (explicit
|
|
34
|
+
// `status` arg without node-state writes) don't queue: the status
|
|
35
|
+
// re-propagates unblocked at commit.
|
|
36
|
+
if (this.S & STATUS_ERROR) {
|
|
37
|
+
this.Je = true;
|
|
38
|
+
this.C.enqueue(this.Re, this.Xe ??= runEffect.bind(null, this));
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!this.C.notify(this, STATUS_ERROR, STATUS_ERROR)) {
|
|
43
|
+
haltReactivity(unwrapStatusError(R));
|
|
44
|
+
throw R;
|
|
45
|
+
}
|
|
46
|
+
} else if (this.Re === EFFECT_RENDER) {
|
|
47
|
+
this.C.notify(this, STATUS_PENDING | STATUS_ERROR, e, R);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runEffect(t) {
|
|
52
|
+
if (!t.Je || t.se & REACTIVE_DISPOSED) return;
|
|
53
|
+
// Error arm (#2840), user effects only: a compute-phase error that is still
|
|
54
|
+
// the node's settled state at effect time runs the bundle's error handler in
|
|
55
|
+
// this same imperative, writable scope. Unwrap the StatusError used for
|
|
56
|
+
// source tracking — user code gets the error it threw, as boundaries do. No
|
|
57
|
+
// handler: log and keep the system alive (the run was skipped). A handler
|
|
58
|
+
// (or logging) consumes the error; a handler throw falls to the shared
|
|
59
|
+
// catch below and escalates boundary-or-halt like any effect-phase throw.
|
|
60
|
+
// Render effects bypass: their errors route to boundaries synchronously in
|
|
61
|
+
// notifyEffectStatus, and a runner queued by an earlier valueChanged in the
|
|
62
|
+
// same flush must not be hijacked by a later-arriving error status.
|
|
63
|
+
if (t.S & STATUS_ERROR && t.Re === EFFECT_USER) {
|
|
64
|
+
const E = unwrapStatusError(t._);
|
|
65
|
+
t._t = t.Ue;
|
|
66
|
+
t.Je = false;
|
|
67
|
+
try {
|
|
68
|
+
t.Et ? t.Et(E, () => {
|
|
69
|
+
const E = t.Tt;
|
|
70
|
+
t.Tt = undefined;
|
|
71
|
+
E?.();
|
|
72
|
+
}) : console.error(E);
|
|
73
|
+
} catch (E) {
|
|
74
|
+
if (!t.C.notify(t, STATUS_ERROR, STATUS_ERROR)) {
|
|
75
|
+
haltReactivity(E);
|
|
76
|
+
throw E;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const E = t.Tt;
|
|
82
|
+
t.Tt = undefined;
|
|
83
|
+
try {
|
|
84
|
+
E?.();
|
|
85
|
+
const e = t.ft(t.Ue, t._t);
|
|
86
|
+
if (false && e !== undefined && typeof e !== "function") ;
|
|
87
|
+
// The final cleanup is invoked by disposeChildren at true disposal.
|
|
88
|
+
t.Tt = e;
|
|
89
|
+
} catch (E) {
|
|
90
|
+
t._ = new StatusError(t, E);
|
|
91
|
+
t.S |= STATUS_ERROR;
|
|
92
|
+
if (!t.C.notify(t, STATUS_ERROR, STATUS_ERROR)) {
|
|
93
|
+
haltReactivity(E);
|
|
94
|
+
throw E;
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
t._t = t.Ue;
|
|
98
|
+
t.Je = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
GlobalQueue.et = runEffect;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Internal tracked effect - bypasses heap, goes directly to effect queue.
|
|
106
|
+
* Runs as a leaf owner: child primitives and onCleanup are forbidden (false throws).
|
|
107
|
+
* Uses stale reads.
|
|
108
|
+
*/ function trackedEffect(t, E) {
|
|
109
|
+
const run = () => {
|
|
110
|
+
if (!e.Je || e.se & REACTIVE_DISPOSED) return;
|
|
111
|
+
try {
|
|
112
|
+
e.Je = false;
|
|
113
|
+
recompute(e);
|
|
114
|
+
} finally {}
|
|
115
|
+
};
|
|
116
|
+
const e = computed(() => {
|
|
117
|
+
const E = e.Tt;
|
|
118
|
+
e.Tt = undefined;
|
|
119
|
+
E?.();
|
|
120
|
+
const R = staleValues(t);
|
|
121
|
+
e.Tt = R;
|
|
122
|
+
}, {
|
|
123
|
+
...E,
|
|
124
|
+
lazy: true
|
|
125
|
+
});
|
|
126
|
+
e.Tt = undefined;
|
|
127
|
+
e.T = e.T & ~CONFIG_AUTO_DISPOSE | CONFIG_CHILDREN_FORBIDDEN;
|
|
128
|
+
e.Je = true;
|
|
129
|
+
e.Re = EFFECT_TRACKED;
|
|
130
|
+
e.i = (t, E) => {
|
|
131
|
+
const R = t !== undefined ? t : e.S;
|
|
132
|
+
if (R & STATUS_ERROR) {
|
|
133
|
+
e.C.notify(e, STATUS_PENDING, 0);
|
|
134
|
+
const t = E !== undefined ? E : e._;
|
|
135
|
+
if (!e.C.notify(e, STATUS_ERROR, STATUS_ERROR)) {
|
|
136
|
+
haltReactivity(unwrapStatusError(t));
|
|
137
|
+
throw t;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
e.Ft = run;
|
|
142
|
+
e.C.enqueue(EFFECT_USER, run);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { effect, trackedEffect };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by a tracked read whose value is currently pending (an async memo /
|
|
3
|
+
* `createSignal(asyncFn)` / projection / store derivation that hasn't settled
|
|
4
|
+
* yet). Surfacing through the reactive graph is what suspends the consumer
|
|
5
|
+
* scope — the nearest enclosing `<Loading>` boundary catches the throw and
|
|
6
|
+
* renders its fallback until the source resolves.
|
|
7
|
+
*
|
|
8
|
+
* App code rarely catches this directly; `<Loading>` is the canonical
|
|
9
|
+
* handler. The error type is exposed for advanced cases — e.g. interop layers
|
|
10
|
+
* that bridge Solid's pending-throw protocol to a different async strategy,
|
|
11
|
+
* or tests that want to assert on the suspension shape.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* // Advanced: distinguish "not ready yet" from a real error in custom
|
|
16
|
+
* // boundary plumbing. App code should rely on `<Loading>` / `<Errored>`.
|
|
17
|
+
* try {
|
|
18
|
+
* const value = readReactiveSource();
|
|
19
|
+
* } catch (err) {
|
|
20
|
+
* if (err instanceof NotReadyError) throw err; // re-throw to suspend
|
|
21
|
+
* reportError(err);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
class NotReadyError extends Error {
|
|
26
|
+
source;
|
|
27
|
+
constructor(r) {
|
|
28
|
+
// Control-flow throw: it happens on every read of a pending source, so in
|
|
29
|
+
// production skip V8's eager stack capture (proportional to stack depth —
|
|
30
|
+
// real cost under SSR) by zeroing the V8-specific stackTraceLimit around
|
|
31
|
+
// super(). Dev keeps the stack for debuggability; non-V8 engines (no
|
|
32
|
+
// stackTraceLimit) take the plain path.
|
|
33
|
+
const o = Error;
|
|
34
|
+
const t = o.stackTraceLimit;
|
|
35
|
+
if (t !== undefined) o.stackTraceLimit = 0;
|
|
36
|
+
super();
|
|
37
|
+
if (t !== undefined) o.stackTraceLimit = t;
|
|
38
|
+
this.source = r;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class StatusError extends Error {
|
|
43
|
+
source;
|
|
44
|
+
constructor(r, o) {
|
|
45
|
+
super(o instanceof Error ? o.message : String(o), {
|
|
46
|
+
cause: o
|
|
47
|
+
});
|
|
48
|
+
this.source = r;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Return the user's error from an internal status wrapper. */ function unwrapStatusError(r) {
|
|
53
|
+
return r instanceof StatusError ? r.cause : r;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class NoOwnerError extends Error {
|
|
57
|
+
constructor() {
|
|
58
|
+
super("");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class ContextNotFoundError extends Error {
|
|
63
|
+
constructor() {
|
|
64
|
+
super("");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { ContextNotFoundError, NoOwnerError, NotReadyError, StatusError, unwrapStatusError };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { signal, setSignal, read } from "./core.js";
|
|
2
|
+
|
|
3
|
+
import { cleanup } from "./owner.js";
|
|
4
|
+
|
|
5
|
+
import { GlobalQueue } from "./scheduler.js";
|
|
6
|
+
|
|
7
|
+
let externalSourceConfig = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registers a factory that bridges external reactive systems (e.g. MobX, Vue refs)
|
|
11
|
+
* into Solid's tracking graph. Every computation will be wrapped so that the
|
|
12
|
+
* external library can track its own dependencies alongside Solid's.
|
|
13
|
+
*
|
|
14
|
+
* Multiple calls pipe together: each new factory wraps the previous one.
|
|
15
|
+
*
|
|
16
|
+
* @param config.factory receives `(fn, trigger)` — wrap fn execution in external tracking,
|
|
17
|
+
* call trigger when external deps change. Return `{ track, dispose }`.
|
|
18
|
+
* @param config.untrack optional wrapper for `untrack` — disables external tracking too.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* // Bridge an external "subscribe / notify" library into Solid's graph.
|
|
23
|
+
* // `factory` wraps every Solid compute so the external library can attach
|
|
24
|
+
* // its own dependency tracker; `trigger` re-runs the compute on external
|
|
25
|
+
* // change. `untrack` mirrors Solid's `untrack()` into the external library
|
|
26
|
+
* // so that reads inside `untrack(...)` don't get tracked twice.
|
|
27
|
+
* enableExternalSource({
|
|
28
|
+
* factory: (compute, trigger) => {
|
|
29
|
+
* const sub = externalLib.subscribe(trigger);
|
|
30
|
+
* return {
|
|
31
|
+
* track: prev => externalLib.run(() => compute(prev)),
|
|
32
|
+
* dispose: () => sub.unsubscribe()
|
|
33
|
+
* };
|
|
34
|
+
* },
|
|
35
|
+
* untrack: fn => externalLib.untracked(fn)
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
// Wires a freshly created computed through the active external-source bridge.
|
|
40
|
+
// Lives here (installed on GlobalQueue while a config is active) rather than
|
|
41
|
+
// inline in core: esbuild cannot literal-track the mutable config binding the
|
|
42
|
+
// way rollup does, so an inline `if (externalSourceConfig)` block ships in
|
|
43
|
+
// every bundle even though only enableExternalSource() can make it reachable.
|
|
44
|
+
function wireExternalSource(e) {
|
|
45
|
+
const n = signal(undefined, {
|
|
46
|
+
equals: false,
|
|
47
|
+
ownedWrite: true
|
|
48
|
+
});
|
|
49
|
+
const r = externalSourceConfig.factory(e.ce, () => {
|
|
50
|
+
setSignal(n, undefined);
|
|
51
|
+
});
|
|
52
|
+
cleanup(() => r.dispose());
|
|
53
|
+
e.ce = e => {
|
|
54
|
+
read(n);
|
|
55
|
+
return r.track(e);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function externalUntrack(e) {
|
|
60
|
+
return externalSourceConfig.untrack(e);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The hooks mirror the config's liveness exactly (installed on enable,
|
|
64
|
+
// removed on reset) so core's null checks stay equivalent to the old
|
|
65
|
+
// `externalSourceConfig` truthiness checks.
|
|
66
|
+
function syncExternalHooks() {
|
|
67
|
+
GlobalQueue.It = externalSourceConfig ? wireExternalSource : null;
|
|
68
|
+
GlobalQueue.St = externalSourceConfig ? externalUntrack : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function enableExternalSource(e) {
|
|
72
|
+
const {factory: n, untrack: r = e => e()} = e;
|
|
73
|
+
if (externalSourceConfig) {
|
|
74
|
+
const {factory: e, untrack: o} = externalSourceConfig;
|
|
75
|
+
externalSourceConfig = {
|
|
76
|
+
factory: (r, o) => {
|
|
77
|
+
const t = e(r, o);
|
|
78
|
+
const a = n(e => t.track(e), o);
|
|
79
|
+
return {
|
|
80
|
+
track: e => a.track(e),
|
|
81
|
+
dispose() {
|
|
82
|
+
a.dispose();
|
|
83
|
+
t.dispose();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
untrack: e => o(() => r(e))
|
|
88
|
+
};
|
|
89
|
+
} else {
|
|
90
|
+
externalSourceConfig = {
|
|
91
|
+
factory: n,
|
|
92
|
+
untrack: r
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
syncExternalHooks();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { enableExternalSource, externalSourceConfig };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { CONFIG_AUTO_DISPOSE, REACTIVE_ZOMBIE, STATUS_PENDING, REACTIVE_RECOMPUTING_DEPS } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
import { deleteFromHeap, queueFor } from "./heap.js";
|
|
4
|
+
|
|
5
|
+
import { disposeChildren } from "./owner.js";
|
|
6
|
+
|
|
7
|
+
import "./scheduler.js";
|
|
8
|
+
|
|
9
|
+
// https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L100
|
|
10
|
+
function unlinkSubs(l) {
|
|
11
|
+
const n = l.it;
|
|
12
|
+
const e = l.nt;
|
|
13
|
+
const u = l.le;
|
|
14
|
+
const s = l.ll;
|
|
15
|
+
if (u !== null) u.ll = s; else n.rt = s;
|
|
16
|
+
if (s !== null) s.le = u; else {
|
|
17
|
+
n.o = u;
|
|
18
|
+
if (u === null) {
|
|
19
|
+
n.ut?.();
|
|
20
|
+
// No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set.
|
|
21
|
+
// A pending node is exempt: its in-flight async work (or the
|
|
22
|
+
// transition holding it) is an observer — tearing down would orphan
|
|
23
|
+
// the work and re-execute it on the next read. The settle path runs
|
|
24
|
+
// this same last-one-out check when that observer releases (the
|
|
25
|
+
// untracked-read dispose in core.ts guards on pending identically).
|
|
26
|
+
const l = n;
|
|
27
|
+
l.ce && l.T & CONFIG_AUTO_DISPOSE && !(l.se & REACTIVE_ZOMBIE) && !(l.S & STATUS_PENDING) && unobserved(l);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return e;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function trimStaleDeps(l) {
|
|
34
|
+
const n = l.Ye;
|
|
35
|
+
let e = n !== null ? n.nt : l.tt;
|
|
36
|
+
if (e !== null) {
|
|
37
|
+
do {
|
|
38
|
+
e = unlinkSubs(e);
|
|
39
|
+
} while (e !== null);
|
|
40
|
+
if (n !== null) n.nt = null; else l.tt = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function unobserved(l) {
|
|
45
|
+
deleteFromHeap(l, queueFor(l));
|
|
46
|
+
let n = l.tt;
|
|
47
|
+
while (n !== null) {
|
|
48
|
+
n = unlinkSubs(n);
|
|
49
|
+
}
|
|
50
|
+
l.tt = null;
|
|
51
|
+
l.Ye = null;
|
|
52
|
+
disposeChildren(l, true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
|
|
56
|
+
function link(l, n, e = false) {
|
|
57
|
+
// Repeat touches within one pass AND-combine `_pendingObserver`: a probe
|
|
58
|
+
// read (`isPending(() => x())`) beside a value read of the same dep must
|
|
59
|
+
// not relabel the value dependency as probe-only — the value read is what
|
|
60
|
+
// real-error propagation and affects() coverage key off, regardless of
|
|
61
|
+
// read order within the computation.
|
|
62
|
+
const u = n.Ye;
|
|
63
|
+
if (u !== null && u.it === l) {
|
|
64
|
+
u.Ge &&= e;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let s = null;
|
|
68
|
+
const t = n.se & REACTIVE_RECOMPUTING_DEPS;
|
|
69
|
+
if (t) {
|
|
70
|
+
s = u !== null ? u.nt : n.tt;
|
|
71
|
+
if (s !== null && s.it === l) {
|
|
72
|
+
s.nl = n.Ze;
|
|
73
|
+
n.Ye = s;
|
|
74
|
+
// First touch of this pass: the previous pass's label is stale.
|
|
75
|
+
s.Ge = e;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// A link stamped with the current pass generation was created or reused
|
|
80
|
+
// in-order during this recompute, i.e. it already sits in the validated
|
|
81
|
+
// [deps.._depsTail] prefix — the O(1) equivalent of scanning the dep list
|
|
82
|
+
// (the old alien-signals `isValidLink` walk, O(n²) when a computation
|
|
83
|
+
// re-reads earlier deps non-consecutively, e.g. store leaf reads).
|
|
84
|
+
const i = l.rt;
|
|
85
|
+
if (i !== null && i.fe === n && (!t || i.nl === n.Ze)) {
|
|
86
|
+
// Gen-matched during a recompute = repeat touch this pass (AND); outside
|
|
87
|
+
// a recompute there is no pass boundary, so the latest read labels it.
|
|
88
|
+
if (t) i.Ge &&= e; else i.Ge = e;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const o = n.Ye = l.rt = {
|
|
92
|
+
it: l,
|
|
93
|
+
fe: n,
|
|
94
|
+
nt: s,
|
|
95
|
+
ll: i,
|
|
96
|
+
le: null,
|
|
97
|
+
nl: n.Ze,
|
|
98
|
+
Ge: e
|
|
99
|
+
};
|
|
100
|
+
if (u !== null) u.nt = o; else n.tt = o;
|
|
101
|
+
if (i !== null) i.le = o; else l.o = o;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { link, trimStaleDeps, unlinkSubs, unobserved };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, EFFECT_TRACKED, EFFECT_USER, REACTIVE_RECOMPUTING_DEPS, REACTIVE_MANUAL_WRITE, REACTIVE_ZOMBIE, REACTIVE_CHECK, REACTIVE_DIRTY } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
import { zombieQueue, dirtyQueue } from "./scheduler.js";
|
|
4
|
+
|
|
5
|
+
/** The queue a node belongs to, picked from its own zombie flag. */ function queueFor(e) {
|
|
6
|
+
return e.se & REACTIVE_ZOMBIE ? zombieQueue : dirtyQueue;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Schedule one subscriber to re-run on the next flush: tracked effects bypass
|
|
11
|
+
* the heap and go directly to their effect queue; everything else is inserted
|
|
12
|
+
* into its own (zombie-flag-routed) heap with the `_min` cursor pulled down.
|
|
13
|
+
*/ function enqueueSub(e) {
|
|
14
|
+
if (e.Re === EFFECT_TRACKED) {
|
|
15
|
+
const E = e;
|
|
16
|
+
if (!E.Je) {
|
|
17
|
+
E.Je = true;
|
|
18
|
+
E.C.enqueue(EFFECT_USER, E.Ft);
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const E = queueFor(e);
|
|
23
|
+
if (E.Ve > e.xe) E.Ve = e.xe;
|
|
24
|
+
insertIntoHeap(e, E);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function actualInsertIntoHeap(e, E) {
|
|
28
|
+
const t = (e.ke?.Nt ? e.ke.dt?.xe : e.ke?.xe) ?? -1;
|
|
29
|
+
if (t >= e.xe) e.xe = t + 1;
|
|
30
|
+
const n = e.xe;
|
|
31
|
+
const I = E.eE[n];
|
|
32
|
+
if (I === undefined) E.eE[n] = e; else {
|
|
33
|
+
const E = I.st;
|
|
34
|
+
E.ot = e;
|
|
35
|
+
e.st = E;
|
|
36
|
+
I.st = e;
|
|
37
|
+
}
|
|
38
|
+
if (n > E.EE) E.EE = n;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function insertIntoHeap(e, E) {
|
|
42
|
+
let t = e.se;
|
|
43
|
+
if (t & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_MANUAL_WRITE)) return;
|
|
44
|
+
if (t & REACTIVE_CHECK) {
|
|
45
|
+
e.se = t & -4 | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
|
|
46
|
+
} else {
|
|
47
|
+
e.se = t | REACTIVE_IN_HEAP;
|
|
48
|
+
// An unmarked node entering a marked heap invalidates the markHeap memo:
|
|
49
|
+
// `_marked` is only reset by runHeap, so a write between two mid-tick
|
|
50
|
+
// pulls (read-time markHeap + updateIfNecessary) would otherwise leave
|
|
51
|
+
// this node unmarked and every downstream pull stale until the next
|
|
52
|
+
// flush (#2922: the second `latest()` returned the first write's value).
|
|
53
|
+
if (E.tE && !(t & REACTIVE_DIRTY)) E.tE = false;
|
|
54
|
+
}
|
|
55
|
+
if (!(t & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(e, E);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function insertIntoHeapHeight(e, E) {
|
|
59
|
+
let t = e.se;
|
|
60
|
+
if (t & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_IN_HEAP_HEIGHT | REACTIVE_MANUAL_WRITE)) return;
|
|
61
|
+
e.se = t | REACTIVE_IN_HEAP_HEIGHT;
|
|
62
|
+
actualInsertIntoHeap(e, E);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function deleteFromHeap(e, E) {
|
|
66
|
+
const t = e.se;
|
|
67
|
+
if (!(t & (REACTIVE_IN_HEAP | REACTIVE_IN_HEAP_HEIGHT))) return;
|
|
68
|
+
e.se = t & -25;
|
|
69
|
+
const n = e.xe;
|
|
70
|
+
if (e.st === e) E.eE[n] = undefined; else {
|
|
71
|
+
const t = e.ot;
|
|
72
|
+
const I = E.eE[n];
|
|
73
|
+
const o = t ?? I;
|
|
74
|
+
if (e === I) E.eE[n] = t; else e.st.ot = t;
|
|
75
|
+
o.st = e.st;
|
|
76
|
+
}
|
|
77
|
+
e.st = e;
|
|
78
|
+
e.ot = undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function markHeap(e) {
|
|
82
|
+
if (e.tE) return;
|
|
83
|
+
e.tE = true;
|
|
84
|
+
for (let E = 0; E <= e.EE; E++) {
|
|
85
|
+
for (let t = e.eE[E]; t !== undefined; t = t.ot) {
|
|
86
|
+
if (t.se & REACTIVE_IN_HEAP) markNode(t);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function markNode(e, E = REACTIVE_DIRTY) {
|
|
92
|
+
const t = e.se;
|
|
93
|
+
if ((t & (REACTIVE_CHECK | REACTIVE_DIRTY)) >= E) return;
|
|
94
|
+
e.se = t & -4 | E;
|
|
95
|
+
for (let E = e.o; E !== null; E = E.le) {
|
|
96
|
+
markNode(E.fe, REACTIVE_CHECK);
|
|
97
|
+
}
|
|
98
|
+
if (e.u !== null) {
|
|
99
|
+
for (let E = e.u; E !== null; E = E.ae) {
|
|
100
|
+
for (let e = E.o; e !== null; e = e.le) {
|
|
101
|
+
markNode(e.fe, REACTIVE_CHECK);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function runHeap(e, E) {
|
|
108
|
+
e.tE = false;
|
|
109
|
+
for (e.Ve = 0; e.Ve <= e.EE; e.Ve++) {
|
|
110
|
+
let t = e.eE[e.Ve];
|
|
111
|
+
while (t !== undefined) {
|
|
112
|
+
if (t.se & REACTIVE_IN_HEAP) E(t); else adjustHeight(t, e);
|
|
113
|
+
t = e.eE[e.Ve];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
e.EE = 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function adjustHeight(e, E) {
|
|
120
|
+
deleteFromHeap(e, E);
|
|
121
|
+
let t = e.xe;
|
|
122
|
+
for (let E = e.tt; E; E = E.nt) {
|
|
123
|
+
const e = E.it;
|
|
124
|
+
const n = e.lt || e;
|
|
125
|
+
if (n.ce && n.xe >= t) t = n.xe + 1;
|
|
126
|
+
}
|
|
127
|
+
if (e.xe !== t) {
|
|
128
|
+
e.xe = t;
|
|
129
|
+
for (let E = e.o; E !== null; E = E.le) {
|
|
130
|
+
// Route each subscriber by its own zombie flag, mirroring the
|
|
131
|
+
// post-recompute height-adjust path. Inserting into the running `heap`
|
|
132
|
+
// unconditionally can park a zombie in `dirtyQueue` (or a live node in
|
|
133
|
+
// `zombieQueue`), breaking the flag/queue invariant `deleteFromHeap`
|
|
134
|
+
// relies on — the same corruption class as #2759.
|
|
135
|
+
insertIntoHeapHeight(E.fe, queueFor(E.fe));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { deleteFromHeap, enqueueSub, insertIntoHeap, insertIntoHeapHeight, markHeap, markNode, queueFor, runHeap };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INV-2: a node with an *active* override must be registered for reversion in
|
|
3
|
+
* the queue's or a transition's `_optimisticNodes`. An unregistered active
|
|
4
|
+
* override would survive transition completion forever. Runs at the end of
|
|
5
|
+
* every flush (not just quiescence — the invariant holds mid-transition).
|
|
6
|
+
* (There is no revert-target requirement: authoritative values commit
|
|
7
|
+
* silently into `_value` under the override mask — A17 — so reverting is
|
|
8
|
+
* just dropping the override.)
|
|
9
|
+
*/
|
|
10
|
+
function devCheckActiveOverrides(e) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** INV-1: an isPending() probe must never leak past its own call. */ function devCheckFlushStart() {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Companion-vs-oracle census (#2838 pre-work). A NON-ASSERTING diff logger:
|
|
20
|
+
* at the end of every flush it compares each live companion's cached state
|
|
21
|
+
* against a fresh oracle and logs every distinct divergence fingerprint once
|
|
22
|
+
* (console, `[census]` prefix). Legit lane-scoped windows will show up too —
|
|
23
|
+
* the census exists to enumerate the full taxonomy of mid-flight divergence
|
|
24
|
+
* so the write-driven redesign knows every case it must produce, not to
|
|
25
|
+
* judge them. Enabled only when the COMPANION_CENSUS env var is set (in
|
|
26
|
+
* addition to `false`), so normal test runs pay one boolean check.
|
|
27
|
+
*/ typeof globalThis !== "undefined" && !!globalThis.process?.env?.COMPANION_CENSUS;
|
|
28
|
+
|
|
29
|
+
function devCensusCompanions(e) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Quiescence checks. Run only when the system is fully drained: nothing
|
|
35
|
+
* scheduled, no active/stashed transitions, no live lanes. At that point no
|
|
36
|
+
* transition-scoped state may survive, and the lazily-created companions must
|
|
37
|
+
* agree with a fresh computation of their owner's state.
|
|
38
|
+
*/ function devCheckQuiescent(e) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { devCensusCompanions, devCheckActiveOverrides, devCheckFlushStart, devCheckQuiescent };
|