@ersbeth/picoflow 2.3.2 → 3.0.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 +34 -5
- package/SKILL.md +130 -16
- package/dist/index.d.ts +767 -0
- package/dist/index.js +544 -0
- package/dist/inspect.d.ts +252 -0
- package/dist/inspect.js +300 -0
- package/dist/shared.d.ts +361 -0
- package/dist/shared.js +1496 -0
- package/dist/shared2.d.ts +93 -0
- package/dist/shared2.js +59 -0
- package/dist/shared3.d.ts +54 -0
- package/dist/shared3.js +17 -0
- package/dist/{types/converters/solid.d.ts → solid.d.ts} +10 -8
- package/dist/solid.js +63 -0
- package/package.json +37 -25
- package/dist/picoflow.js +0 -1283
- package/dist/types/converters/index.d.ts +0 -1
- package/dist/types/core/api/base/flowConfig.d.ts +0 -17
- package/dist/types/core/api/base/flowDisposable.d.ts +0 -40
- package/dist/types/core/api/base/flowErrors.d.ts +0 -1
- package/dist/types/core/api/base/flowObservable.d.ts +0 -26
- package/dist/types/core/api/base/flowPrimitiveOptions.d.ts +0 -14
- package/dist/types/core/api/base/flowSubscribable.d.ts +0 -78
- package/dist/types/core/api/base/flowTracker.d.ts +0 -7
- package/dist/types/core/api/base/index.d.ts +0 -7
- package/dist/types/core/api/index.d.ts +0 -2
- package/dist/types/core/api/nodes/async/flowConstantAsync.d.ts +0 -32
- package/dist/types/core/api/nodes/async/flowDerivationAsync.d.ts +0 -37
- package/dist/types/core/api/nodes/async/flowStateAsync.d.ts +0 -42
- package/dist/types/core/api/nodes/async/flowWritableDerivationAsync.d.ts +0 -31
- package/dist/types/core/api/nodes/async/index.d.ts +0 -4
- package/dist/types/core/api/nodes/collections/flowArray.d.ts +0 -138
- package/dist/types/core/api/nodes/collections/flowMap.d.ts +0 -100
- package/dist/types/core/api/nodes/collections/index.d.ts +0 -2
- package/dist/types/core/api/nodes/flowEffect.d.ts +0 -28
- package/dist/types/core/api/nodes/flowSignal.d.ts +0 -26
- package/dist/types/core/api/nodes/flowValue.d.ts +0 -35
- package/dist/types/core/api/nodes/index.d.ts +0 -7
- package/dist/types/core/api/nodes/sync/flowConstant.d.ts +0 -30
- package/dist/types/core/api/nodes/sync/flowDerivation.d.ts +0 -37
- package/dist/types/core/api/nodes/sync/flowState.d.ts +0 -40
- package/dist/types/core/api/nodes/sync/flowWritableDerivation.d.ts +0 -29
- package/dist/types/core/api/nodes/sync/index.d.ts +0 -4
- package/dist/types/core/api/nodes/utils.d.ts +0 -22
- package/dist/types/core/base/dependenciesRegistry.d.ts +0 -1
- package/dist/types/core/base/dependentsRegistry.d.ts +0 -1
- package/dist/types/core/base/disposable.d.ts +0 -15
- package/dist/types/core/base/errors.d.ts +0 -31
- package/dist/types/core/base/executionStack.d.ts +0 -31
- package/dist/types/core/base/index.d.ts +0 -8
- package/dist/types/core/base/node.d.ts +0 -27
- package/dist/types/core/base/observable.d.ts +0 -34
- package/dist/types/core/base/observer.d.ts +0 -33
- package/dist/types/core/nodes/actionNode.d.ts +0 -1
- package/dist/types/core/nodes/arrayNode.d.ts +0 -1
- package/dist/types/core/nodes/effectNode.d.ts +0 -1
- package/dist/types/core/nodes/index.d.ts +0 -8
- package/dist/types/core/nodes/mapNode.d.ts +0 -1
- package/dist/types/core/nodes/signalNode.d.ts +0 -1
- package/dist/types/core/nodes/valueAsyncNode.d.ts +0 -1
- package/dist/types/core/nodes/valueNode.d.ts +0 -1
- package/dist/types/core/nodes/valueSyncNode.d.ts +0 -1
- package/dist/types/core/schedulers/asyncResolver.d.ts +0 -1
- package/dist/types/core/schedulers/asyncScheduler.d.ts +0 -1
- package/dist/types/core/schedulers/index.d.ts +0 -4
- package/dist/types/core/schedulers/pendingError.d.ts +0 -1
- package/dist/types/core/schedulers/scheduler.d.ts +0 -1
- package/dist/types/core/schedulers/syncResolver.d.ts +0 -1
- package/dist/types/core/schedulers/syncScheduler.d.ts +0 -1
- package/dist/types/index.d.ts +0 -2
package/dist/shared.js
ADDED
|
@@ -0,0 +1,1496 @@
|
|
|
1
|
+
//#region src/core/base/dependenciesRegistry.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared bookkeeping for the "set of sources this observer reads" half of the
|
|
4
|
+
* dependency graph (A2-ARCH-5). `Observer` and `Node` both hold one instead of
|
|
5
|
+
* each carrying their own `Set<IObservable<unknown>>` and reimplementing
|
|
6
|
+
* register/unregister/clear.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
var DependenciesRegistry = class {
|
|
10
|
+
_dependencies = /* @__PURE__ */ new Set();
|
|
11
|
+
register(dependency, dependent) {
|
|
12
|
+
this._dependencies.add(dependency);
|
|
13
|
+
dependency.registerDependent(dependent);
|
|
14
|
+
}
|
|
15
|
+
unregister(dependency, dependent) {
|
|
16
|
+
this._dependencies.delete(dependency);
|
|
17
|
+
dependency.unregisterDependent(dependent);
|
|
18
|
+
}
|
|
19
|
+
clear(dependent) {
|
|
20
|
+
this._dependencies.forEach((dependency) => {
|
|
21
|
+
dependency.unregisterDependent(dependent);
|
|
22
|
+
});
|
|
23
|
+
this._dependencies.clear();
|
|
24
|
+
}
|
|
25
|
+
forEach(fn) {
|
|
26
|
+
Array.from(this._dependencies).forEach(fn);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/core/base/dependentsRegistry.ts
|
|
32
|
+
/**
|
|
33
|
+
* Shared bookkeeping for the "set of watchers" half of the dependency graph
|
|
34
|
+
* (A2-ARCH-5). `Observable` and `Node` both hold one instead of each carrying
|
|
35
|
+
* their own `Set<IObserver>` and reimplementing register/unregister/notify.
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
var DependentsRegistry = class {
|
|
39
|
+
_dependents = /* @__PURE__ */ new Set();
|
|
40
|
+
register(dependent) {
|
|
41
|
+
this._dependents.add(dependent);
|
|
42
|
+
}
|
|
43
|
+
unregister(dependent) {
|
|
44
|
+
this._dependents.delete(dependent);
|
|
45
|
+
}
|
|
46
|
+
notifyAll() {
|
|
47
|
+
this._dependents.forEach((dependent) => {
|
|
48
|
+
dependent.notify();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
forEach(fn) {
|
|
52
|
+
Array.from(this._dependents).forEach(fn);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/core/base/errors.ts
|
|
58
|
+
/**
|
|
59
|
+
* Base class for picoflow's typed errors, so a consumer can `catch` one with a single
|
|
60
|
+
* `instanceof PicoFlowError` check instead of matching on message text. Every throw the library
|
|
61
|
+
* raises is one of its subclasses, and every message carries the `[PicoFlow]` prefix.
|
|
62
|
+
*
|
|
63
|
+
* It is never thrown directly: catch it to handle any picoflow failure, and narrow to a subclass to
|
|
64
|
+
* tell one kind from another.
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
var PicoFlowError = class extends Error {
|
|
68
|
+
constructor(message) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = "PicoFlowError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Thrown when a disposed primitive (or its underlying scheduler) is read, written, or otherwise
|
|
75
|
+
* used after `dispose()`.
|
|
76
|
+
* @public
|
|
77
|
+
*/
|
|
78
|
+
var PicoFlowDisposedError = class extends PicoFlowError {
|
|
79
|
+
constructor(message) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = "PicoFlowDisposedError";
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Thrown when the reactive flush detects a feedback loop that exceeds the maximum step guard.
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
var PicoFlowCycleError = class extends PicoFlowError {
|
|
89
|
+
constructor(message) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.name = "PicoFlowCycleError";
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Thrown when a node reaches a state its own implementation considers unreachable. Reaching this
|
|
96
|
+
* indicates a picoflow bug rather than a misuse of the public API.
|
|
97
|
+
* @public
|
|
98
|
+
*/
|
|
99
|
+
var PicoFlowInternalError = class extends PicoFlowError {
|
|
100
|
+
constructor(message) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.name = "PicoFlowInternalError";
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Thrown when a documented precondition of a public method is violated by the caller: a collection
|
|
107
|
+
* operation naming an entry that does not exist (or, for `add`, one that already does), and
|
|
108
|
+
* `from()`'s check on the value it was handed. Reaching this indicates a misuse of the API rather
|
|
109
|
+
* than a picoflow bug, and the call had no effect (#123).
|
|
110
|
+
* @public
|
|
111
|
+
*/
|
|
112
|
+
var PicoFlowPreconditionError = class extends PicoFlowError {
|
|
113
|
+
constructor(message) {
|
|
114
|
+
super(message);
|
|
115
|
+
this.name = "PicoFlowPreconditionError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Normalises a thrown value to an `Error`, so that every read path — `pick()`, `get()`, `value`,
|
|
120
|
+
* and `subscribe`'s `onError` — surfaces the same type whatever the computation threw (A2-SEC-3,
|
|
121
|
+
* #67). A genuine `Error` passes through by identity; anything else is wrapped, with its string
|
|
122
|
+
* form as the message and the original value as `cause`.
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
function toError(thrown) {
|
|
126
|
+
return thrown instanceof Error ? thrown : new Error(String(thrown), { cause: thrown });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/core/base/liveNodeRegistry.ts
|
|
131
|
+
/**
|
|
132
|
+
* Census of every graph participant ever created, held weakly (#50, A2-DX-2).
|
|
133
|
+
*
|
|
134
|
+
* It is what an inspection dump reads. The graph has no roots of its own, and the nodes a census
|
|
135
|
+
* exists to reveal — the ones leaked because nothing disposed them — are precisely the ones no
|
|
136
|
+
* handle points at any more, so a traversal from caller-supplied roots could never reach them.
|
|
137
|
+
*
|
|
138
|
+
* Tracking is unconditional: an opt-in census misses everything built before it was switched on,
|
|
139
|
+
* which is the confusing half-graph an introspection tool must never show. That puts its cost on
|
|
140
|
+
* every node's construction, a path picoflow is already slow on, so it is pared to one
|
|
141
|
+
* {@link WeakRef} and one array push. Two things it deliberately does not do: register with a
|
|
142
|
+
* `FinalizationRegistry` (whose `register` measured four times the cost of the reference it was
|
|
143
|
+
* there to clean up), and keep a `Set` (whose hashing measured half again the cost of the push).
|
|
144
|
+
*
|
|
145
|
+
* Entries are **not** removed on disposal: a disposed node something still references is forensic
|
|
146
|
+
* evidence, and a dump filters it out by status instead. What removes an entry is the garbage
|
|
147
|
+
* collector taking its target, after which the compaction below drops the empty reference — on
|
|
148
|
+
* every read, and while writing whenever the census has doubled since it was last compacted.
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
var LiveNodeRegistry = class LiveNodeRegistry {
|
|
152
|
+
static _refs = [];
|
|
153
|
+
/** Length the census has to reach before compacting is worth the walk. */
|
|
154
|
+
static _compactAt = 64;
|
|
155
|
+
/** Records a node. Called from the {@link Disposable} constructor, so nothing escapes the census. */
|
|
156
|
+
static track(node) {
|
|
157
|
+
if (LiveNodeRegistry._refs.length >= LiveNodeRegistry._compactAt) LiveNodeRegistry._compact();
|
|
158
|
+
LiveNodeRegistry._refs.push(new WeakRef(node));
|
|
159
|
+
}
|
|
160
|
+
/** Visits the nodes still alive, in creation order. Reads no status and touches no registry. */
|
|
161
|
+
static forEach(fn) {
|
|
162
|
+
LiveNodeRegistry._compact();
|
|
163
|
+
for (const ref of [...LiveNodeRegistry._refs]) {
|
|
164
|
+
const node = ref.deref();
|
|
165
|
+
if (node !== void 0) fn(node);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Empties the census.
|
|
170
|
+
*
|
|
171
|
+
* Test support, deliberately not part of the public `reset()`: that one returns the *scheduler*
|
|
172
|
+
* to its just-started state and documents that primitives created before it stay usable, so
|
|
173
|
+
* dropping them from the census there would hide live nodes. A suite asserting on the census
|
|
174
|
+
* needs the clean slate all the same, the census being process-global like the scheduler.
|
|
175
|
+
*/
|
|
176
|
+
static clear() {
|
|
177
|
+
LiveNodeRegistry._refs = [];
|
|
178
|
+
LiveNodeRegistry._compactAt = 64;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Drops the references whose target has been collected and sets the next compaction a doubling
|
|
182
|
+
* away, so the walk is amortised to a constant per node. In place, keeping creation order: a
|
|
183
|
+
* fresh array would allocate one slot per live node every time.
|
|
184
|
+
*/
|
|
185
|
+
static _compact() {
|
|
186
|
+
const refs = LiveNodeRegistry._refs;
|
|
187
|
+
let kept = 0;
|
|
188
|
+
for (let i = 0; i < refs.length; i++) {
|
|
189
|
+
const ref = refs[i];
|
|
190
|
+
if (ref.deref() !== void 0) refs[kept++] = ref;
|
|
191
|
+
}
|
|
192
|
+
refs.length = kept;
|
|
193
|
+
LiveNodeRegistry._compactAt = Math.max(64, kept * 2);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/core/base/disposable.ts
|
|
199
|
+
/**
|
|
200
|
+
* Base implementation of the disposable pattern for reactive primitives.
|
|
201
|
+
* Concrete subclasses implement `dispose()`, which is idempotent: a second call is a no-op.
|
|
202
|
+
*/
|
|
203
|
+
var Disposable = class Disposable {
|
|
204
|
+
static _nextId = 0;
|
|
205
|
+
_disposed = false;
|
|
206
|
+
_name;
|
|
207
|
+
/**
|
|
208
|
+
* Identity for introspection (#50): monotonic per process, assigned at construction and never
|
|
209
|
+
* reused, so the same node is recognisable across successive dumps — and so an unnamed one can
|
|
210
|
+
* still be labelled `kind#id` rather than being indistinguishable from its siblings.
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
213
|
+
_id = ++Disposable._nextId;
|
|
214
|
+
/**
|
|
215
|
+
* Which primitive this node belongs to, for introspection (#50).
|
|
216
|
+
*
|
|
217
|
+
* A plain field written once at construction rather than something an inspector works out
|
|
218
|
+
* afterwards: deriving it would mean keeping a map from every engine back to the facade that
|
|
219
|
+
* names it, and a `WeakMap.set` per node costs about as much as building the node did.
|
|
220
|
+
* @internal
|
|
221
|
+
*/
|
|
222
|
+
_kind = "unknown";
|
|
223
|
+
constructor(name) {
|
|
224
|
+
this._name = name;
|
|
225
|
+
LiveNodeRegistry.track(this);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The `name` given at creation, for introspection. A getter rather than making `_name` itself
|
|
229
|
+
* public: the field stays protected, as the error builders below are its only other readers.
|
|
230
|
+
* @internal
|
|
231
|
+
*/
|
|
232
|
+
get _inspectName() {
|
|
233
|
+
return this._name;
|
|
234
|
+
}
|
|
235
|
+
/** Builds the disposed-primitive error, naming the primitive when a `name` was given at creation. */
|
|
236
|
+
_disposedError() {
|
|
237
|
+
return new PicoFlowDisposedError(this._name ? `[PicoFlow] Primitive "${this._name}" is disposed` : "[PicoFlow] Primitive is disposed");
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Releases the primitive when it leaves a `using` block, by delegating to {@link dispose}.
|
|
241
|
+
*
|
|
242
|
+
* Declared once here rather than per subclass: the standard `Disposable` protocol keys off this
|
|
243
|
+
* symbol, and every primitive reaches it through this base (A2-ARCH-2).
|
|
244
|
+
*/
|
|
245
|
+
[Symbol.dispose]() {
|
|
246
|
+
this.dispose();
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/core/base/traceHub.ts
|
|
252
|
+
/**
|
|
253
|
+
* Where the engine reports what it is doing, for a trace to pick up (#50, A2-DX-2).
|
|
254
|
+
*
|
|
255
|
+
* The engine knows nothing about traces: it calls {@link emit} at the handful of points where
|
|
256
|
+
* something happens, and the first thing `emit` does is return unless someone is listening. That
|
|
257
|
+
* one boolean is the entire cost of tracing being available, which is what lets the hooks sit on
|
|
258
|
+
* the flush path at all.
|
|
259
|
+
*
|
|
260
|
+
* What a listener does with an event — turning a node into a readable reference, keeping a bounded
|
|
261
|
+
* history of them — belongs to the inspection layer, so that this stays free of any notion of how
|
|
262
|
+
* a node is described.
|
|
263
|
+
* @internal
|
|
264
|
+
*/
|
|
265
|
+
var TraceHub = class TraceHub {
|
|
266
|
+
/** Read before anything else on every hot path; kept in step with `_listeners` below. */
|
|
267
|
+
static enabled = false;
|
|
268
|
+
static _listeners = /* @__PURE__ */ new Set();
|
|
269
|
+
static emit(type, node) {
|
|
270
|
+
if (!TraceHub.enabled) return;
|
|
271
|
+
for (const listener of TraceHub._listeners) listener(type, node);
|
|
272
|
+
}
|
|
273
|
+
static listen(listener) {
|
|
274
|
+
TraceHub._listeners.add(listener);
|
|
275
|
+
TraceHub.enabled = true;
|
|
276
|
+
}
|
|
277
|
+
static unlisten(listener) {
|
|
278
|
+
TraceHub._listeners.delete(listener);
|
|
279
|
+
TraceHub.enabled = TraceHub._listeners.size > 0;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/core/base/executionStack.ts
|
|
285
|
+
/**
|
|
286
|
+
* Global scheduler that batches and executes reactive computations asynchronously.
|
|
287
|
+
*
|
|
288
|
+
* Flush order: pending value nodes first, then effects, both in the next task.
|
|
289
|
+
* That task is posted on a MessageChannel port, not setTimeout(0): the HTML spec clamps nested
|
|
290
|
+
* timers to >= 4 ms (~5-6 ms per reactive round measured on Chromium/Firefox, A2-PERF-1), while
|
|
291
|
+
* message tasks are never clamped — and fake timers cannot freeze them either (A2-RT-2).
|
|
292
|
+
* Queues are index-drained so items appended mid-flush are not lost (see _drain).
|
|
293
|
+
*/
|
|
294
|
+
var ExecutionStack = class ExecutionStack {
|
|
295
|
+
static _MAX_FLUSH_STEPS = 1e4;
|
|
296
|
+
static _pendingQueue = [];
|
|
297
|
+
static _effectQueue = [];
|
|
298
|
+
static _executionScheduled;
|
|
299
|
+
/** Resolver of _executionScheduled, invoked by the flush task once the queues have drained. */
|
|
300
|
+
static _executionResolve;
|
|
301
|
+
/**
|
|
302
|
+
* Bumped by reset(). A posted flush carries the generation it was scheduled under; a message
|
|
303
|
+
* arriving with a stale generation is a flush reset() cancelled, and is dropped on delivery —
|
|
304
|
+
* MessageChannel has no clearTimeout equivalent.
|
|
305
|
+
*/
|
|
306
|
+
static _flushGeneration = 0;
|
|
307
|
+
/** Lazily created channel carrying flush tasks; post schedules, receive drains. */
|
|
308
|
+
static _flushChannel;
|
|
309
|
+
/** Handle of the fallback timer backing _executionScheduled where MessageChannel is missing. */
|
|
310
|
+
static _executionTimer;
|
|
311
|
+
static _coalesceResetScheduled = false;
|
|
312
|
+
/** Per-effect resets accumulated for the microtask scheduled below, so notifying more than one
|
|
313
|
+
* effect in the same sync turn doesn't discard every reset after the first (A2-SEM-5). */
|
|
314
|
+
static _coalesceResets = /* @__PURE__ */ new Set();
|
|
315
|
+
/** Routes any error escaping the flush loop; defaults to logging rather than crashing (A2-SEM-2, A2-SEC-1). */
|
|
316
|
+
static _onFlushError = (error) => console.error(error);
|
|
317
|
+
/**
|
|
318
|
+
* True once the current synchronous call stack has finished and microtasks have started.
|
|
319
|
+
* EffectNode uses this to treat microtask notifications (e.g. async resolve) differently
|
|
320
|
+
* from coalesced sync-batch notifications.
|
|
321
|
+
*/
|
|
322
|
+
static _pastSyncTurn = false;
|
|
323
|
+
/** @internal Used by EffectNode to distinguish sync-batch from microtask notifications. */
|
|
324
|
+
static get pastSyncTurn() {
|
|
325
|
+
return ExecutionStack._pastSyncTurn;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* @internal Runs after the current sync turn ends (queueMicrotask).
|
|
329
|
+
* Marks pastSyncTurn so the next coalesced effect notify can bump its epoch,
|
|
330
|
+
* and resets per-effect coalesce flags via onReset.
|
|
331
|
+
*/
|
|
332
|
+
static scheduleCoalesceReset(onReset) {
|
|
333
|
+
ExecutionStack._coalesceResets.add(onReset);
|
|
334
|
+
if (ExecutionStack._coalesceResetScheduled) return;
|
|
335
|
+
ExecutionStack._coalesceResetScheduled = true;
|
|
336
|
+
queueMicrotask(() => {
|
|
337
|
+
ExecutionStack._pastSyncTurn = true;
|
|
338
|
+
ExecutionStack._coalesceResetScheduled = false;
|
|
339
|
+
const resets = ExecutionStack._coalesceResets;
|
|
340
|
+
ExecutionStack._coalesceResets = /* @__PURE__ */ new Set();
|
|
341
|
+
resets.forEach((reset) => {
|
|
342
|
+
reset();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/** @internal Wired to the public `onFlushError()` API in `src/api/base/flowConfig.ts`. */
|
|
347
|
+
static setFlushErrorHandler(handler) {
|
|
348
|
+
ExecutionStack._onFlushError = handler;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* @internal Wired to the public `reset()` API in `src/core/api/base/flowConfig.ts`.
|
|
352
|
+
*
|
|
353
|
+
* Discards every queue and flag, returning the scheduler to its just-started state (A2-ARCH-1).
|
|
354
|
+
* Queued work is dropped, not run: nodes left in the queues stay `dirty` and only recompute if
|
|
355
|
+
* something notifies them again.
|
|
356
|
+
*/
|
|
357
|
+
static reset() {
|
|
358
|
+
ExecutionStack._pendingQueue.length = 0;
|
|
359
|
+
ExecutionStack._effectQueue.length = 0;
|
|
360
|
+
ExecutionStack._flushGeneration++;
|
|
361
|
+
ExecutionStack._flushChannel?.receive.unref?.();
|
|
362
|
+
clearTimeout(ExecutionStack._executionTimer);
|
|
363
|
+
ExecutionStack._executionTimer = void 0;
|
|
364
|
+
ExecutionStack._executionScheduled = void 0;
|
|
365
|
+
ExecutionStack._executionResolve = void 0;
|
|
366
|
+
ExecutionStack._coalesceResets = /* @__PURE__ */ new Set();
|
|
367
|
+
ExecutionStack._coalesceResetScheduled = false;
|
|
368
|
+
ExecutionStack._pastSyncTurn = false;
|
|
369
|
+
ExecutionStack._onFlushError = (error) => console.error(error);
|
|
370
|
+
}
|
|
371
|
+
static pushPending(node) {
|
|
372
|
+
TraceHub.emit("queue-pending", node);
|
|
373
|
+
ExecutionStack._beginSyncTurn();
|
|
374
|
+
ExecutionStack._scheduleExecution();
|
|
375
|
+
ExecutionStack._pendingQueue.push(node);
|
|
376
|
+
}
|
|
377
|
+
static pushEffect(effect) {
|
|
378
|
+
TraceHub.emit("queue-effect", effect);
|
|
379
|
+
ExecutionStack._beginSyncTurn();
|
|
380
|
+
ExecutionStack._scheduleExecution();
|
|
381
|
+
ExecutionStack._effectQueue.push(effect);
|
|
382
|
+
}
|
|
383
|
+
static _beginSyncTurn() {
|
|
384
|
+
ExecutionStack._pastSyncTurn = false;
|
|
385
|
+
}
|
|
386
|
+
/** Lazily builds the channel carrying flush tasks and installs the drain handler. */
|
|
387
|
+
static _channel() {
|
|
388
|
+
if (!ExecutionStack._flushChannel) {
|
|
389
|
+
const { port1, port2 } = new MessageChannel();
|
|
390
|
+
const receive = port1;
|
|
391
|
+
receive.onmessage = (event) => {
|
|
392
|
+
if (event.data !== ExecutionStack._flushGeneration) return;
|
|
393
|
+
const resolve = ExecutionStack._executionResolve;
|
|
394
|
+
ExecutionStack._executionScheduled = void 0;
|
|
395
|
+
ExecutionStack._executionResolve = void 0;
|
|
396
|
+
receive.unref?.();
|
|
397
|
+
ExecutionStack._execute();
|
|
398
|
+
resolve?.();
|
|
399
|
+
};
|
|
400
|
+
receive.unref?.();
|
|
401
|
+
ExecutionStack._flushChannel = {
|
|
402
|
+
post: port2,
|
|
403
|
+
receive
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return ExecutionStack._flushChannel;
|
|
407
|
+
}
|
|
408
|
+
static _scheduleExecution() {
|
|
409
|
+
if (ExecutionStack._executionScheduled) return;
|
|
410
|
+
if (typeof MessageChannel === "undefined") {
|
|
411
|
+
ExecutionStack._executionScheduled = new Promise((resolve) => {
|
|
412
|
+
ExecutionStack._executionTimer = setTimeout(() => {
|
|
413
|
+
ExecutionStack._executionScheduled = void 0;
|
|
414
|
+
ExecutionStack._executionTimer = void 0;
|
|
415
|
+
ExecutionStack._execute();
|
|
416
|
+
resolve();
|
|
417
|
+
}, 0);
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const channel = ExecutionStack._channel();
|
|
422
|
+
ExecutionStack._executionScheduled = new Promise((resolve) => {
|
|
423
|
+
ExecutionStack._executionResolve = resolve;
|
|
424
|
+
});
|
|
425
|
+
channel.receive.ref?.();
|
|
426
|
+
channel.post.postMessage(ExecutionStack._flushGeneration);
|
|
427
|
+
}
|
|
428
|
+
static _drain(queue) {
|
|
429
|
+
for (let i = 0; i < queue.length; i++) {
|
|
430
|
+
if (i >= ExecutionStack._MAX_FLUSH_STEPS) {
|
|
431
|
+
const looping = queue[i];
|
|
432
|
+
if (looping) TraceHub.emit("cycle-error", looping);
|
|
433
|
+
ExecutionStack._pendingQueue.length = 0;
|
|
434
|
+
ExecutionStack._effectQueue.length = 0;
|
|
435
|
+
ExecutionStack._reportFlushError(new PicoFlowCycleError("[PicoFlow] Reactive update cycle exceeded maximum depth"));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const node = queue[i];
|
|
439
|
+
try {
|
|
440
|
+
node?.execute();
|
|
441
|
+
} catch (error) {
|
|
442
|
+
ExecutionStack._reportFlushError(error);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
queue.length = 0;
|
|
446
|
+
}
|
|
447
|
+
/** Invokes the flush-error hook under a guard of its own (A4-CORR-1). The hook exists so an
|
|
448
|
+
* error escaping the flush cannot become an uncaught exception; a hook that throws must not
|
|
449
|
+
* reintroduce one, nor abandon the effects queued behind it. Reported to console.error — the
|
|
450
|
+
* documented default, and the only reporter that cannot itself be the broken one. */
|
|
451
|
+
static _reportFlushError(error) {
|
|
452
|
+
try {
|
|
453
|
+
ExecutionStack._onFlushError(error);
|
|
454
|
+
} catch (handlerError) {
|
|
455
|
+
console.error("[PicoFlow] the onFlushError handler threw", handlerError);
|
|
456
|
+
console.error(error);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
static _execute() {
|
|
460
|
+
ExecutionStack._drain(ExecutionStack._pendingQueue);
|
|
461
|
+
ExecutionStack._drain(ExecutionStack._effectQueue);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region src/core/base/observer.ts
|
|
467
|
+
/**
|
|
468
|
+
* Narrows the public tracker type to the internal observer it always is.
|
|
469
|
+
*
|
|
470
|
+
* The one place this conversion happens, and the only cast left in the graph-wiring path. It is safe
|
|
471
|
+
* by construction rather than by inspection: {@link FlowTracker} is branded (A2-TYPE-3), the brand is
|
|
472
|
+
* declared only on {@link Observer} and {@link Node}, and both implement {@link IObserver} — so no
|
|
473
|
+
* value can reach here typed as a tracker without being one. A duck-typed object can still reach it
|
|
474
|
+
* at runtime through a double cast on the caller's side, which is exactly the A2-RT-3 behaviour left
|
|
475
|
+
* deliberately untouched.
|
|
476
|
+
*/
|
|
477
|
+
function asObserver(t) {
|
|
478
|
+
return t;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Base implementation managing dependency tracking for reactive observers.
|
|
482
|
+
*/
|
|
483
|
+
var Observer = class extends Disposable {
|
|
484
|
+
_dependenciesRegistry = new DependenciesRegistry();
|
|
485
|
+
/** Set when a dependency disposed while this observer was queued; consumed by the next execute(). */
|
|
486
|
+
_dependencyDisposed = false;
|
|
487
|
+
/** Set while this observer awaits a flush; cleared at the start of execute(). */
|
|
488
|
+
_queued = false;
|
|
489
|
+
/**
|
|
490
|
+
* Reports this observer's lifecycle. Never throws, so a debugger or a `console.log` can inspect
|
|
491
|
+
* a released effect — the same reasoning as the observable `status` getter (A2-ARCH-2).
|
|
492
|
+
*/
|
|
493
|
+
get status() {
|
|
494
|
+
if (this._disposed) return "disposed";
|
|
495
|
+
return this._queued ? "queued" : "idle";
|
|
496
|
+
}
|
|
497
|
+
/** Visits the sources this observer currently reads, for introspection — see {@link Observable._forEachDependent}. @internal */
|
|
498
|
+
_forEachDependency(fn) {
|
|
499
|
+
this._dependenciesRegistry.forEach(fn);
|
|
500
|
+
}
|
|
501
|
+
markDependencyDisposed() {
|
|
502
|
+
this._dependencyDisposed = true;
|
|
503
|
+
}
|
|
504
|
+
registerDependency(dependency) {
|
|
505
|
+
if (this._disposed) throw this._disposedError();
|
|
506
|
+
this._dependenciesRegistry.register(dependency, this);
|
|
507
|
+
}
|
|
508
|
+
unregisterDependency(dependency) {
|
|
509
|
+
if (this._disposed) throw this._disposedError();
|
|
510
|
+
this._dependenciesRegistry.unregister(dependency, this);
|
|
511
|
+
}
|
|
512
|
+
clearDependencies() {
|
|
513
|
+
if (this._disposed) throw this._disposedError();
|
|
514
|
+
this._dependenciesRegistry.clear(this);
|
|
515
|
+
}
|
|
516
|
+
dispose() {
|
|
517
|
+
if (this._disposed) return;
|
|
518
|
+
this._dependenciesRegistry.clear(this);
|
|
519
|
+
this._disposed = true;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
//#endregion
|
|
524
|
+
//#region src/core/base/observable.ts
|
|
525
|
+
/**
|
|
526
|
+
* Base implementation managing the dependency graph for reactive observables.
|
|
527
|
+
*/
|
|
528
|
+
var Observable = class extends Disposable {
|
|
529
|
+
_dependentsRegistry = new DependentsRegistry();
|
|
530
|
+
_status = "resolved";
|
|
531
|
+
get status() {
|
|
532
|
+
if (this._disposed) return "disposed";
|
|
533
|
+
return this._status;
|
|
534
|
+
}
|
|
535
|
+
set status(status) {
|
|
536
|
+
if (this._disposed) throw this._disposedError();
|
|
537
|
+
this._status = status;
|
|
538
|
+
}
|
|
539
|
+
registerDependent(dependent) {
|
|
540
|
+
if (this._disposed) throw this._disposedError();
|
|
541
|
+
this._dependentsRegistry.register(dependent);
|
|
542
|
+
}
|
|
543
|
+
unregisterDependent(dependent) {
|
|
544
|
+
if (this._disposed) throw this._disposedError();
|
|
545
|
+
this._dependentsRegistry.unregister(dependent);
|
|
546
|
+
}
|
|
547
|
+
notifyDependents() {
|
|
548
|
+
if (this._disposed) return;
|
|
549
|
+
this._dependentsRegistry.notifyAll();
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Visits the observers currently registered on this node, for introspection (#50).
|
|
553
|
+
*
|
|
554
|
+
* No disposal guard, unlike every mutating method here: an inspector must be able to describe a
|
|
555
|
+
* released node, and disposal has already emptied the registry, so a disposed node simply
|
|
556
|
+
* reports no dependent. Reading the edges registers nothing and computes nothing.
|
|
557
|
+
* @internal
|
|
558
|
+
*/
|
|
559
|
+
_forEachDependent(fn) {
|
|
560
|
+
this._dependentsRegistry.forEach(fn);
|
|
561
|
+
}
|
|
562
|
+
watch(tracker) {
|
|
563
|
+
if (this._disposed) throw this._disposedError();
|
|
564
|
+
asObserver(tracker).registerDependency(this);
|
|
565
|
+
}
|
|
566
|
+
trigger() {
|
|
567
|
+
if (this._disposed) throw this._disposedError();
|
|
568
|
+
this.notifyDependents();
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Hook for subclasses that also hold dependencies ({@link Node}): runs inside {@link dispose}
|
|
572
|
+
* after the dependents are detached and before the disposal flag is set, so the override can
|
|
573
|
+
* still call the guarded unregister methods (A2-ARCH-5, #111).
|
|
574
|
+
*/
|
|
575
|
+
_disposeDependencies() {}
|
|
576
|
+
dispose() {
|
|
577
|
+
if (this._disposed) return;
|
|
578
|
+
this._dependentsRegistry.forEach((dependant) => {
|
|
579
|
+
dependant.markDependencyDisposed();
|
|
580
|
+
dependant.unregisterDependency(this);
|
|
581
|
+
this.unregisterDependent(dependant);
|
|
582
|
+
});
|
|
583
|
+
this._disposeDependencies();
|
|
584
|
+
this._disposed = true;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/core/base/node.ts
|
|
590
|
+
/**
|
|
591
|
+
* Base implementation for reactive nodes that propagate changes through the dependency graph.
|
|
592
|
+
*
|
|
593
|
+
* A node is both observed and observing. The observed half — dependents registry, `status`,
|
|
594
|
+
* `watch`, `trigger`, `notifyDependents`, `dispose` — is inherited from {@link Observable} rather
|
|
595
|
+
* than copied (A2-ARCH-5, #34, #111); this class adds only the observing half, as guarded
|
|
596
|
+
* delegations to {@link DependenciesRegistry}, the same shape {@link Observer} has.
|
|
597
|
+
*/
|
|
598
|
+
var Node = class extends Observable {
|
|
599
|
+
_dependenciesRegistry = new DependenciesRegistry();
|
|
600
|
+
/** Set when a dependency disposed while this node was dirty; consumed by the next execute(). */
|
|
601
|
+
_dependencyDisposed = false;
|
|
602
|
+
registerDependency(dependency) {
|
|
603
|
+
if (this._disposed) throw this._disposedError();
|
|
604
|
+
this._dependenciesRegistry.register(dependency, this);
|
|
605
|
+
}
|
|
606
|
+
unregisterDependency(dependency) {
|
|
607
|
+
if (this._disposed) throw this._disposedError();
|
|
608
|
+
this._dependenciesRegistry.unregister(dependency, this);
|
|
609
|
+
}
|
|
610
|
+
clearDependencies() {
|
|
611
|
+
if (this._disposed) throw this._disposedError();
|
|
612
|
+
this._dependenciesRegistry.clear(this);
|
|
613
|
+
}
|
|
614
|
+
/** Visits the sources this node currently reads, for introspection — see {@link Observable._forEachDependent}. @internal */
|
|
615
|
+
_forEachDependency(fn) {
|
|
616
|
+
this._dependenciesRegistry.forEach(fn);
|
|
617
|
+
}
|
|
618
|
+
markDependencyDisposed() {
|
|
619
|
+
this._dependencyDisposed = true;
|
|
620
|
+
}
|
|
621
|
+
/** Runs inside Observable.dispose(), before the disposal flag is set. */
|
|
622
|
+
_disposeDependencies() {
|
|
623
|
+
this._dependenciesRegistry.forEach((dependency) => {
|
|
624
|
+
this.unregisterDependency(dependency);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region src/core/schedulers/pendingError.ts
|
|
631
|
+
/**
|
|
632
|
+
* Error thrown when an async computation is pending, encapsulating its promise for cascading dependencies.
|
|
633
|
+
* @internal
|
|
634
|
+
*/
|
|
635
|
+
var PendingError = class extends Error {
|
|
636
|
+
pendingPromise;
|
|
637
|
+
constructor(promise) {
|
|
638
|
+
super("[PicoFlow] Computation is pending");
|
|
639
|
+
this.name = "PendingError";
|
|
640
|
+
this.pendingPromise = promise;
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
//#endregion
|
|
645
|
+
//#region src/core/schedulers/asyncResolver.ts
|
|
646
|
+
/**
|
|
647
|
+
* Manages asynchronous computation lifecycle with iteration tracking to prevent race conditions.
|
|
648
|
+
* @internal
|
|
649
|
+
*/
|
|
650
|
+
var AsyncResolver = class {
|
|
651
|
+
_compute;
|
|
652
|
+
_computed = Promise.withResolvers();
|
|
653
|
+
_iteration = 0;
|
|
654
|
+
_aborted = false;
|
|
655
|
+
_finished = false;
|
|
656
|
+
/** Set when the computation rejected with `PendingError`: it waits on a dependency (#113). */
|
|
657
|
+
_blocked = false;
|
|
658
|
+
constructor(compute) {
|
|
659
|
+
this._compute = compute;
|
|
660
|
+
this._computed.promise.catch(() => {});
|
|
661
|
+
}
|
|
662
|
+
get computed() {
|
|
663
|
+
return this._computed.promise;
|
|
664
|
+
}
|
|
665
|
+
get aborted() {
|
|
666
|
+
return this._aborted;
|
|
667
|
+
}
|
|
668
|
+
get finished() {
|
|
669
|
+
return this._finished;
|
|
670
|
+
}
|
|
671
|
+
get blocked() {
|
|
672
|
+
return this._blocked;
|
|
673
|
+
}
|
|
674
|
+
compute() {
|
|
675
|
+
if (this._finished) throw new PicoFlowInternalError("[PicoFlow] AsyncResolver: Can't restart a settled resolver");
|
|
676
|
+
if (this._aborted) throw new PicoFlowInternalError("[PicoFlow] AsyncResolver: Can't restart an aborted resolver");
|
|
677
|
+
this._iteration++;
|
|
678
|
+
this._blocked = false;
|
|
679
|
+
const currentIteration = this._iteration;
|
|
680
|
+
this._compute().then((value) => {
|
|
681
|
+
if (this._iteration === currentIteration && !this._aborted) this._resolve(value);
|
|
682
|
+
}).catch((error) => {
|
|
683
|
+
if (this._iteration === currentIteration && !this._aborted) {
|
|
684
|
+
if (error instanceof PendingError) this._blocked = true;
|
|
685
|
+
else this._reject(error);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
overwrite(promise) {
|
|
690
|
+
if (this._finished) throw new PicoFlowInternalError("[PicoFlow] AsyncResolver: Can't overwrite a settled resolver");
|
|
691
|
+
if (this._aborted) throw new PicoFlowInternalError("[PicoFlow] AsyncResolver: Can't overwrite an aborted resolver");
|
|
692
|
+
this._iteration++;
|
|
693
|
+
this._blocked = false;
|
|
694
|
+
const currentIteration = this._iteration;
|
|
695
|
+
promise.then((value) => {
|
|
696
|
+
if (this._iteration === currentIteration && !this._aborted) this._resolve(value);
|
|
697
|
+
}).catch((error) => {
|
|
698
|
+
if (this._iteration === currentIteration && !this._aborted) this._reject(error);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
_resolve(value) {
|
|
702
|
+
this._finished = true;
|
|
703
|
+
this._computed.resolve(value);
|
|
704
|
+
}
|
|
705
|
+
_reject(error) {
|
|
706
|
+
this._finished = true;
|
|
707
|
+
this._computed.reject(error);
|
|
708
|
+
}
|
|
709
|
+
abort() {
|
|
710
|
+
this._aborted = true;
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
//#endregion
|
|
715
|
+
//#region src/core/schedulers/asyncScheduler.ts
|
|
716
|
+
/**
|
|
717
|
+
* Scheduler that manages asynchronous computation lifecycle with resolver instances.
|
|
718
|
+
* @internal
|
|
719
|
+
*/
|
|
720
|
+
var AsyncScheduler = class {
|
|
721
|
+
inline = false;
|
|
722
|
+
_compute;
|
|
723
|
+
_resolver;
|
|
724
|
+
_onResolve;
|
|
725
|
+
_onReject;
|
|
726
|
+
_settled = Promise.withResolvers();
|
|
727
|
+
_disposed = false;
|
|
728
|
+
constructor(compute, onResolve, onReject) {
|
|
729
|
+
this._compute = compute;
|
|
730
|
+
this._onResolve = onResolve;
|
|
731
|
+
this._onReject = onReject;
|
|
732
|
+
this._resolver = new AsyncResolver(compute);
|
|
733
|
+
this._attachHandlers(this._resolver);
|
|
734
|
+
}
|
|
735
|
+
get settled() {
|
|
736
|
+
if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
|
|
737
|
+
return this._settled.promise;
|
|
738
|
+
}
|
|
739
|
+
get disposed() {
|
|
740
|
+
return this._disposed;
|
|
741
|
+
}
|
|
742
|
+
get blocked() {
|
|
743
|
+
return this._resolver.blocked;
|
|
744
|
+
}
|
|
745
|
+
wrap(value) {
|
|
746
|
+
return Promise.resolve(value);
|
|
747
|
+
}
|
|
748
|
+
chain(input, fn) {
|
|
749
|
+
return input.then((value) => fn(value));
|
|
750
|
+
}
|
|
751
|
+
overwrite(promise) {
|
|
752
|
+
if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
|
|
753
|
+
if (this._resolver.finished) {
|
|
754
|
+
this._resolver = new AsyncResolver(this._compute);
|
|
755
|
+
this._settled = Promise.withResolvers();
|
|
756
|
+
this._attachHandlers(this._resolver);
|
|
757
|
+
}
|
|
758
|
+
this._resolver.overwrite(promise);
|
|
759
|
+
}
|
|
760
|
+
compute() {
|
|
761
|
+
if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
|
|
762
|
+
if (this._resolver.finished) {
|
|
763
|
+
this._resolver = new AsyncResolver(this._compute);
|
|
764
|
+
this._settled = Promise.withResolvers();
|
|
765
|
+
this._attachHandlers(this._resolver);
|
|
766
|
+
}
|
|
767
|
+
this._resolver.compute();
|
|
768
|
+
}
|
|
769
|
+
_attachHandlers(resolver) {
|
|
770
|
+
const settled = this._settled;
|
|
771
|
+
resolver.computed.then((value) => {
|
|
772
|
+
if (this._disposed) return;
|
|
773
|
+
this._onResolve(value);
|
|
774
|
+
}).catch((error) => {
|
|
775
|
+
if (this._disposed) return;
|
|
776
|
+
this._onReject(error);
|
|
777
|
+
}).finally(() => settled.resolve());
|
|
778
|
+
}
|
|
779
|
+
dispose() {
|
|
780
|
+
this._resolver.abort();
|
|
781
|
+
this._disposed = true;
|
|
782
|
+
this._settled.resolve();
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/core/schedulers/syncResolver.ts
|
|
788
|
+
/**
|
|
789
|
+
* Manages synchronous computation lifecycle with abort and settled-state guards.
|
|
790
|
+
* @internal
|
|
791
|
+
*/
|
|
792
|
+
var SyncResolver = class {
|
|
793
|
+
_compute;
|
|
794
|
+
_aborted = false;
|
|
795
|
+
_finished = false;
|
|
796
|
+
/** Set when the computation threw `PendingError`: it waits on a dependency (#113). */
|
|
797
|
+
_blocked = false;
|
|
798
|
+
_onValue;
|
|
799
|
+
_onError;
|
|
800
|
+
constructor(compute, onValue, onError) {
|
|
801
|
+
this._compute = compute;
|
|
802
|
+
this._onValue = onValue;
|
|
803
|
+
this._onError = onError;
|
|
804
|
+
}
|
|
805
|
+
get aborted() {
|
|
806
|
+
return this._aborted;
|
|
807
|
+
}
|
|
808
|
+
get finished() {
|
|
809
|
+
return this._finished;
|
|
810
|
+
}
|
|
811
|
+
get blocked() {
|
|
812
|
+
return this._blocked;
|
|
813
|
+
}
|
|
814
|
+
compute() {
|
|
815
|
+
if (this._finished) throw new PicoFlowInternalError("[PicoFlow] SyncResolver: Can't restart a settled resolver");
|
|
816
|
+
if (this._aborted) throw new PicoFlowInternalError("[PicoFlow] SyncResolver: Can't restart an aborted resolver");
|
|
817
|
+
this._blocked = false;
|
|
818
|
+
try {
|
|
819
|
+
const value = this._compute();
|
|
820
|
+
if (!this._finished && !this._aborted) {
|
|
821
|
+
this._finished = true;
|
|
822
|
+
this._onValue(value);
|
|
823
|
+
}
|
|
824
|
+
} catch (error) {
|
|
825
|
+
if (!this._finished && !this._aborted) {
|
|
826
|
+
if (error instanceof PendingError) this._blocked = true;
|
|
827
|
+
else {
|
|
828
|
+
this._finished = true;
|
|
829
|
+
this._onError(error);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
overwrite(value) {
|
|
835
|
+
if (this._finished) throw new PicoFlowInternalError("[PicoFlow] SyncResolver: Can't overwrite a settled resolver");
|
|
836
|
+
if (this._aborted) throw new PicoFlowInternalError("[PicoFlow] SyncResolver: Can't overwrite an aborted resolver");
|
|
837
|
+
this._blocked = false;
|
|
838
|
+
this._finished = true;
|
|
839
|
+
this._onValue(value);
|
|
840
|
+
}
|
|
841
|
+
abort() {
|
|
842
|
+
this._aborted = true;
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/core/schedulers/syncScheduler.ts
|
|
848
|
+
/**
|
|
849
|
+
* Scheduler that manages synchronous computation lifecycle with resolver instances.
|
|
850
|
+
* @internal
|
|
851
|
+
*/
|
|
852
|
+
var SyncScheduler = class {
|
|
853
|
+
inline = true;
|
|
854
|
+
_compute;
|
|
855
|
+
_resolver;
|
|
856
|
+
_onResolve;
|
|
857
|
+
_onReject;
|
|
858
|
+
_settled = Promise.withResolvers();
|
|
859
|
+
_disposed = false;
|
|
860
|
+
constructor(compute, onResolve, onReject) {
|
|
861
|
+
this._compute = compute;
|
|
862
|
+
this._onResolve = onResolve;
|
|
863
|
+
this._onReject = onReject;
|
|
864
|
+
this._resolver = this._newResolver();
|
|
865
|
+
}
|
|
866
|
+
get settled() {
|
|
867
|
+
return this._settled.promise;
|
|
868
|
+
}
|
|
869
|
+
get blocked() {
|
|
870
|
+
return this._resolver.blocked;
|
|
871
|
+
}
|
|
872
|
+
wrap(value) {
|
|
873
|
+
return value;
|
|
874
|
+
}
|
|
875
|
+
chain(input, fn) {
|
|
876
|
+
return fn(input);
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Each resolver settles the deferred that was current when it was created — captured, not read
|
|
880
|
+
* at settlement time: the node's resolution handler may re-enter `overwrite()` with a queued
|
|
881
|
+
* write (#43, #113), which swaps `_settled` for a fresh deferred; the `pick()` awaiting the old
|
|
882
|
+
* one must still be released. Same shape as AsyncScheduler._attachHandlers.
|
|
883
|
+
*/
|
|
884
|
+
_newResolver() {
|
|
885
|
+
const settled = this._settled;
|
|
886
|
+
return new SyncResolver(this._compute, (value) => {
|
|
887
|
+
this._onResolve(value);
|
|
888
|
+
settled.resolve();
|
|
889
|
+
}, (error) => {
|
|
890
|
+
this._onReject(error);
|
|
891
|
+
settled.resolve();
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
overwrite(value) {
|
|
895
|
+
if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
|
|
896
|
+
if (this._resolver.finished) {
|
|
897
|
+
this._settled = Promise.withResolvers();
|
|
898
|
+
this._resolver = this._newResolver();
|
|
899
|
+
}
|
|
900
|
+
this._resolver.overwrite(value);
|
|
901
|
+
}
|
|
902
|
+
compute() {
|
|
903
|
+
if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
|
|
904
|
+
if (this._resolver.finished) {
|
|
905
|
+
this._settled = Promise.withResolvers();
|
|
906
|
+
this._resolver = this._newResolver();
|
|
907
|
+
}
|
|
908
|
+
this._resolver.compute();
|
|
909
|
+
}
|
|
910
|
+
dispose() {
|
|
911
|
+
this._resolver.abort();
|
|
912
|
+
this._disposed = true;
|
|
913
|
+
this._settled.resolve();
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region src/core/nodes/effectNode.ts
|
|
919
|
+
/**
|
|
920
|
+
* Reactive effect that automatically re-executes when its dependencies change.
|
|
921
|
+
*
|
|
922
|
+
* Scheduling uses epoch-based coalescing:
|
|
923
|
+
* - Multiple dependency changes in the same synchronous turn produce one queue entry
|
|
924
|
+
* but may still require multiple runs when notifications arrive in later microtasks
|
|
925
|
+
* (e.g. async pending → resolved).
|
|
926
|
+
* - Re-notifications during execute() (cascading effects) enqueue a fresh run because
|
|
927
|
+
* _queued is cleared at the start of execute().
|
|
928
|
+
*
|
|
929
|
+
* @internal
|
|
930
|
+
*/
|
|
931
|
+
var EffectNode = class extends Observer {
|
|
932
|
+
_kind = "effect";
|
|
933
|
+
_data;
|
|
934
|
+
_onData;
|
|
935
|
+
_onError;
|
|
936
|
+
_onPending;
|
|
937
|
+
/** True while this effect is already waiting in the effect queue for the next flush. */
|
|
938
|
+
/** Guards the single coalesced bump of _notifyEpoch while _queued during one sync turn. */
|
|
939
|
+
_coalescedWhileQueued = false;
|
|
940
|
+
/** Monotonic count of notifications that must eventually be executed. */
|
|
941
|
+
_notifyEpoch = 0;
|
|
942
|
+
/** How many notification epochs have been executed so far. */
|
|
943
|
+
_executedEpoch = 0;
|
|
944
|
+
/** Callbacks registered via {@link onDispose}, run once after dispose() tears down the graph edges. */
|
|
945
|
+
_disposeListeners = [];
|
|
946
|
+
constructor(data, onData, onError, onPending, name) {
|
|
947
|
+
super(name);
|
|
948
|
+
this._data = data;
|
|
949
|
+
this._onData = onData;
|
|
950
|
+
this._onError = onError;
|
|
951
|
+
this._onPending = onPending;
|
|
952
|
+
this._notifyEpoch = 1;
|
|
953
|
+
this.execute();
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Registers a callback to run once this effect is disposed, after the graph edges are torn
|
|
957
|
+
* down. Lets a caller that wraps an effect (e.g. {@link ActionNode.subscribe}) attach its own
|
|
958
|
+
* teardown without patching `dispose` on the returned instance (A2-ARCH-8).
|
|
959
|
+
* @internal
|
|
960
|
+
*/
|
|
961
|
+
onDispose(fn) {
|
|
962
|
+
this._disposeListeners.push(fn);
|
|
963
|
+
}
|
|
964
|
+
dispose() {
|
|
965
|
+
if (this._disposed) return;
|
|
966
|
+
super.dispose();
|
|
967
|
+
this._disposeListeners.forEach((fn) => {
|
|
968
|
+
fn();
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
notify() {
|
|
972
|
+
if (this._disposed) throw this._disposedError();
|
|
973
|
+
if (this._queued) {
|
|
974
|
+
if (!this._coalescedWhileQueued) {
|
|
975
|
+
this._coalescedWhileQueued = true;
|
|
976
|
+
if (ExecutionStack.pastSyncTurn) this._notifyEpoch++;
|
|
977
|
+
}
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
this._notifyEpoch++;
|
|
981
|
+
this._queued = true;
|
|
982
|
+
ExecutionStack.scheduleCoalesceReset(() => {
|
|
983
|
+
this._coalescedWhileQueued = false;
|
|
984
|
+
});
|
|
985
|
+
ExecutionStack.pushEffect(this);
|
|
986
|
+
}
|
|
987
|
+
execute() {
|
|
988
|
+
this._queued = false;
|
|
989
|
+
if (this._disposed) {
|
|
990
|
+
this._executedEpoch = this._notifyEpoch;
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (this._dependencyDisposed) {
|
|
994
|
+
this._dependencyDisposed = false;
|
|
995
|
+
this._executedEpoch = this._notifyEpoch;
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const targetEpoch = this._notifyEpoch;
|
|
999
|
+
while (this._executedEpoch < targetEpoch) {
|
|
1000
|
+
this._executedEpoch++;
|
|
1001
|
+
if (this._disposed) {
|
|
1002
|
+
this._executedEpoch = this._notifyEpoch;
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (this._dependencyDisposed) {
|
|
1006
|
+
this._dependencyDisposed = false;
|
|
1007
|
+
this._executedEpoch = this._notifyEpoch;
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
TraceHub.emit("effect-run", this);
|
|
1011
|
+
try {
|
|
1012
|
+
this.clearDependencies();
|
|
1013
|
+
const data = this._data(this);
|
|
1014
|
+
this._onData(data);
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
if (error instanceof PendingError) this._onPending?.();
|
|
1017
|
+
else if (this._onError) this._onError(toError(error));
|
|
1018
|
+
else throw error;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
//#endregion
|
|
1025
|
+
//#region src/core/nodes/readableNode.ts
|
|
1026
|
+
/**
|
|
1027
|
+
* Global-registry symbol branding a {@link ReadableNode} instance, so `from()` can recognize a node
|
|
1028
|
+
* built by a different copy of the library — where its class objects, and thus `instanceof`, differ.
|
|
1029
|
+
* @internal
|
|
1030
|
+
*/
|
|
1031
|
+
const PICOFLOW_VALUE_NODE = Symbol.for("picoflow.node");
|
|
1032
|
+
/**
|
|
1033
|
+
* Public face of a reactive value: owns a {@link ValueCore} and implements `FlowValue` by delegating
|
|
1034
|
+
* to it. It has no graph membership of its own — the core is what other nodes depend on, what the
|
|
1035
|
+
* execution stack queues, and the tracker compute functions receive (A2-GOV-5, #71, #112 — step 2
|
|
1036
|
+
* of #110).
|
|
1037
|
+
*
|
|
1038
|
+
* This class carries the readers only. Each public primitive has a subclass that adds exactly the
|
|
1039
|
+
* mutators its interface declares — {@link ConstantNode}, {@link DerivationNode}, {@link StateNode},
|
|
1040
|
+
* {@link WritableDerivationNode}, {@link ArrayNode}, {@link MapNode} — so a method absent from a
|
|
1041
|
+
* primitive's type is absent from its prototype chain too (#114, step 4 of #110; closes #26, #32,
|
|
1042
|
+
* #108). `trigger()` is among those mutators: it stayed here one round longer, which let a constant
|
|
1043
|
+
* force its whole subtree to re-run, and moved down with the others in #132 (A3-ARCH-1). Sync and async are not classes: they differ by the core a facade is given, built with
|
|
1044
|
+
* {@link syncCore} or {@link asyncCore}.
|
|
1045
|
+
*
|
|
1046
|
+
* The core is a `#private` field, not an underscore-prefixed property: an ordinary property is
|
|
1047
|
+
* enumerable, so `JSON.stringify`, `Object.keys` or a spread of a primitive walked the whole engine —
|
|
1048
|
+
* current value included — into logs and persisted state (A2-SEC-5, #69). Subclasses reach it
|
|
1049
|
+
* through the protected `_core` accessor, a prototype getter nothing structural walks.
|
|
1050
|
+
* @internal
|
|
1051
|
+
*/
|
|
1052
|
+
var ReadableNode = class {
|
|
1053
|
+
[PICOFLOW_VALUE_NODE] = true;
|
|
1054
|
+
#core;
|
|
1055
|
+
constructor(core, kind = "unknown") {
|
|
1056
|
+
this.#core = core;
|
|
1057
|
+
core._kind = kind;
|
|
1058
|
+
}
|
|
1059
|
+
/** The engine this facade delegates to. Subclasses narrow the input type by overriding it. */
|
|
1060
|
+
get _core() {
|
|
1061
|
+
return this.#core;
|
|
1062
|
+
}
|
|
1063
|
+
get status() {
|
|
1064
|
+
return this._core.status;
|
|
1065
|
+
}
|
|
1066
|
+
get(tracker) {
|
|
1067
|
+
return this._core.read(tracker, () => this._core.current, () => {
|
|
1068
|
+
this._core.markPendingRead();
|
|
1069
|
+
throw new PendingError(this._core.settled);
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
watch(tracker) {
|
|
1073
|
+
this._core.read(tracker, () => void 0, () => {
|
|
1074
|
+
this._core.markPendingRead();
|
|
1075
|
+
throw new PendingError(this._core.settled);
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
async pick() {
|
|
1079
|
+
return await this._core.read(void 0, () => this._core.current, () => this._core.settle());
|
|
1080
|
+
}
|
|
1081
|
+
/** Passive snapshot (A2-ARCH-10, #80) — see {@link ValueCore.snapshot}. */
|
|
1082
|
+
get value() {
|
|
1083
|
+
return this._core.snapshot();
|
|
1084
|
+
}
|
|
1085
|
+
/** Passive read of the cached value (A2-ARCH-10, #80); it survives disposal, so this does too. */
|
|
1086
|
+
get latest() {
|
|
1087
|
+
return this._core.current;
|
|
1088
|
+
}
|
|
1089
|
+
subscribe(onValue, onError, onPending, options) {
|
|
1090
|
+
this._core.assertAlive();
|
|
1091
|
+
return new EffectNode((t) => this.get(t), onValue, onError, onPending, options?.name);
|
|
1092
|
+
}
|
|
1093
|
+
dispose() {
|
|
1094
|
+
this._core.dispose();
|
|
1095
|
+
}
|
|
1096
|
+
/** Releases the primitive when it leaves a `using` block, by delegating to {@link dispose}. */
|
|
1097
|
+
[Symbol.dispose]() {
|
|
1098
|
+
this.dispose();
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
//#endregion
|
|
1103
|
+
//#region src/core/nodes/valueCore.ts
|
|
1104
|
+
/**
|
|
1105
|
+
* Engine behind every reactive value: the state (`status`, value, error), the scheduler, the graph
|
|
1106
|
+
* membership inherited from {@link Node}, the write queue, and the one status machine every read
|
|
1107
|
+
* goes through.
|
|
1108
|
+
*
|
|
1109
|
+
* A core has no public face of its own — {@link ReadableNode} owns one and implements `FlowValue` by
|
|
1110
|
+
* delegation (A2-GOV-5, #71, #112 — step 2 of #110). The core is what the registries hold, what the
|
|
1111
|
+
* `ExecutionStack` queues, and the `FlowTracker` handed to compute functions.
|
|
1112
|
+
*
|
|
1113
|
+
* One class serves sync and async values (#113 — step 3 of #110): `I` is the scheduler's input
|
|
1114
|
+
* type (the value itself, or a promise of it), and everything that used to tell the two apart is
|
|
1115
|
+
* asked of the {@link Scheduler} — `inline`, `blocked`, `wrap`, `chain`.
|
|
1116
|
+
* @internal
|
|
1117
|
+
*/
|
|
1118
|
+
var ValueCore = class extends Node {
|
|
1119
|
+
_scheduler;
|
|
1120
|
+
_value;
|
|
1121
|
+
_error;
|
|
1122
|
+
/** Set by {@link markPendingRead} when a dependent registers while this node is mid-flight. */
|
|
1123
|
+
_hasPendingReader = false;
|
|
1124
|
+
/**
|
|
1125
|
+
* True only while the in-flight computation was started by a write, which already ran its own
|
|
1126
|
+
* eager `notifyDependents()` cascade before this resolution. `refresh()` and a recompute pulled
|
|
1127
|
+
* lazily after an upstream `notify()` have no such pre-cascade — for them, `_onResolve`'s own
|
|
1128
|
+
* `notifyDependents()` call is the only notification, so it must never be skipped (A2-PERF-4).
|
|
1129
|
+
*/
|
|
1130
|
+
_pendingFromSet = false;
|
|
1131
|
+
/** True once a value has resolved at least once, so `_value` is a real value and not the initial gap. */
|
|
1132
|
+
_hasResolved = false;
|
|
1133
|
+
/**
|
|
1134
|
+
* Writes that arrived while the node was mid-flight, applied in call order on the settled value
|
|
1135
|
+
* (A2-TYPE-1, #43). A `set` discards everything queued before it, so the queue is at most one
|
|
1136
|
+
* `set` followed by `update`s.
|
|
1137
|
+
*/
|
|
1138
|
+
_ops = [];
|
|
1139
|
+
/**
|
|
1140
|
+
* Change detection for this node. Defaults to `===` rather than `Object.is`, which is what
|
|
1141
|
+
* semantics guarantees 3 and 4 describe; a consumer opts out per primitive via the `equals`
|
|
1142
|
+
* option (A2-SEM-4, A2-ARCH-6).
|
|
1143
|
+
*/
|
|
1144
|
+
_equals;
|
|
1145
|
+
/**
|
|
1146
|
+
* Built from a compute function that tracks dependencies, as opposed to a value or a lazy
|
|
1147
|
+
* initializer. Decides whether a replacing write on a never-computed node must run the
|
|
1148
|
+
* computation first so the dependency edges exist — see {@link overwrite} (#109).
|
|
1149
|
+
*/
|
|
1150
|
+
_derivation;
|
|
1151
|
+
/**
|
|
1152
|
+
* Recompute on dirty instead of on read (#118): `notify()` enqueues this node in the flush, so
|
|
1153
|
+
* it computes without waiting for a reader. Only meaningful on a dependency-tracking compute
|
|
1154
|
+
* function, so the option is gated on `_derivation` — `state`, `constant`, the collections and
|
|
1155
|
+
* `signal` ignore it, as documented on {@link FlowPrimitiveOptions.eager}.
|
|
1156
|
+
*/
|
|
1157
|
+
_eager;
|
|
1158
|
+
constructor(input, options, makeScheduler, derivation = false) {
|
|
1159
|
+
super(options?.name);
|
|
1160
|
+
this._derivation = derivation;
|
|
1161
|
+
this._eager = derivation && (options?.eager ?? false);
|
|
1162
|
+
this._equals = options?.equals ?? ((previous, next) => previous === next);
|
|
1163
|
+
const builtFromValue = typeof input !== "function";
|
|
1164
|
+
const compute = builtFromValue ? () => input : () => input(this, this._value);
|
|
1165
|
+
this.status = "dirty";
|
|
1166
|
+
this._scheduler = makeScheduler(compute, (value) => this._onResolve(value), (error) => this._onReject(error));
|
|
1167
|
+
if (builtFromValue && this._scheduler.inline) this._scheduler.compute();
|
|
1168
|
+
if (this._eager) ExecutionStack.pushPending(this);
|
|
1169
|
+
}
|
|
1170
|
+
/** Throws the disposed-primitive error when the core has been released; the guard every facade method starts with. */
|
|
1171
|
+
assertAlive() {
|
|
1172
|
+
if (this._disposed) throw this._disposedError();
|
|
1173
|
+
}
|
|
1174
|
+
/** Builds the "unreachable state" error, naming the primitive when a `name` was given at creation. */
|
|
1175
|
+
internalError() {
|
|
1176
|
+
return new PicoFlowInternalError(this._name ? `[PicoFlow] Internal error (${this._name})` : "[PicoFlow] Internal error");
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Called just before a tracked read throws `PendingError`, i.e. a dependent registered while
|
|
1180
|
+
* this node is mid-flight and has no other way to learn the computation later settled — so the
|
|
1181
|
+
* post-resolve `notifyDependents()` cannot be skipped even if the value is unchanged (A2-PERF-4).
|
|
1182
|
+
*/
|
|
1183
|
+
markPendingRead() {
|
|
1184
|
+
this._hasPendingReader = true;
|
|
1185
|
+
}
|
|
1186
|
+
/** Whether the in-flight computation waits on a pending dependency — see {@link Scheduler.blocked}. */
|
|
1187
|
+
get blocked() {
|
|
1188
|
+
return this._scheduler.blocked;
|
|
1189
|
+
}
|
|
1190
|
+
/** Whether this core recomputes on dirty rather than on read (#118), for introspection (#50). @internal */
|
|
1191
|
+
get _isEager() {
|
|
1192
|
+
return this._eager;
|
|
1193
|
+
}
|
|
1194
|
+
/** The promise the scheduler settles on; what a pending tracked read hands to `PendingError`. */
|
|
1195
|
+
get settled() {
|
|
1196
|
+
return this._scheduler.settled;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* The one status machine behind `get()`, `watch()` and `pick()` (A2-GOV-5). `track` registers the
|
|
1200
|
+
* caller as a dependent first; a `dirty` core computes, then the settled status is dispatched
|
|
1201
|
+
* once: `resolved` → `onResolved`, `pending` → `onPending`, `error` → the error is thrown, and
|
|
1202
|
+
* `disposed` — reachable only when the computation just run disposed its own node — throws the
|
|
1203
|
+
* disposed error whichever way the node was being read (A2-ARCH-2).
|
|
1204
|
+
*/
|
|
1205
|
+
read(track, onResolved, onPending) {
|
|
1206
|
+
this.assertAlive();
|
|
1207
|
+
if (track) this.watch(track);
|
|
1208
|
+
if (this.status === "dirty") this.execute();
|
|
1209
|
+
switch (this.status) {
|
|
1210
|
+
case "resolved": return onResolved();
|
|
1211
|
+
case "error": throw this._error;
|
|
1212
|
+
case "pending": return onPending();
|
|
1213
|
+
case "disposed": throw this._disposedError();
|
|
1214
|
+
/* v8 ignore next 2 */
|
|
1215
|
+
case "dirty": throw this.internalError();
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
/** The raw cached value — what collections mutate in place, and what `latest` reports. */
|
|
1219
|
+
get current() {
|
|
1220
|
+
return this._value;
|
|
1221
|
+
}
|
|
1222
|
+
set current(value) {
|
|
1223
|
+
this._value = value;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Passive snapshot (A2-ARCH-10, #80). No disposal guard on purpose: `status` already reports
|
|
1227
|
+
* `"disposed"` instead of throwing, and an inspector must be able to read a released node.
|
|
1228
|
+
*/
|
|
1229
|
+
snapshot() {
|
|
1230
|
+
const status = this.status;
|
|
1231
|
+
switch (status) {
|
|
1232
|
+
case "resolved": return {
|
|
1233
|
+
status,
|
|
1234
|
+
data: this._value
|
|
1235
|
+
};
|
|
1236
|
+
case "error": return {
|
|
1237
|
+
status,
|
|
1238
|
+
error: this._error
|
|
1239
|
+
};
|
|
1240
|
+
default: return { status };
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Waits until the node leaves "pending". One settlement may not be enough: writes queued while
|
|
1245
|
+
* the node was mid-flight are applied on the settled value by re-overwriting the scheduler,
|
|
1246
|
+
* which hands out a fresh `settled` promise (A2-TYPE-1, #43).
|
|
1247
|
+
*/
|
|
1248
|
+
async settle() {
|
|
1249
|
+
while (this.status === "pending") {
|
|
1250
|
+
const settled = this._scheduler.settled;
|
|
1251
|
+
await settled;
|
|
1252
|
+
if (this.status === "disposed") throw this._disposedError();
|
|
1253
|
+
/* v8 ignore next 3 */
|
|
1254
|
+
if (this.status === "pending" && this._scheduler.settled === settled) throw this.internalError();
|
|
1255
|
+
}
|
|
1256
|
+
if (this.status === "resolved") return this._value;
|
|
1257
|
+
if (this.status === "error") throw this._error;
|
|
1258
|
+
throw this.internalError();
|
|
1259
|
+
}
|
|
1260
|
+
notify() {
|
|
1261
|
+
if (this._disposed) throw this._disposedError();
|
|
1262
|
+
if (this.status === "dirty") return;
|
|
1263
|
+
TraceHub.emit("notify", this);
|
|
1264
|
+
if (this.status === "pending" || this._eager) ExecutionStack.pushPending(this);
|
|
1265
|
+
this.status = "dirty";
|
|
1266
|
+
this.notifyDependents();
|
|
1267
|
+
}
|
|
1268
|
+
execute() {
|
|
1269
|
+
if (this._disposed) return;
|
|
1270
|
+
if (this.status !== "dirty") return;
|
|
1271
|
+
if (this._dependencyDisposed && this._hasResolved && this._error === void 0) {
|
|
1272
|
+
this._dependencyDisposed = false;
|
|
1273
|
+
this.status = "resolved";
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
this.run();
|
|
1277
|
+
}
|
|
1278
|
+
/** Unconditional computation: clears the dependencies and runs the scheduler. */
|
|
1279
|
+
run() {
|
|
1280
|
+
TraceHub.emit("execute", this);
|
|
1281
|
+
this.clearDependencies();
|
|
1282
|
+
this.status = "pending";
|
|
1283
|
+
this._scheduler.compute();
|
|
1284
|
+
}
|
|
1285
|
+
dispose() {
|
|
1286
|
+
this._ops = [];
|
|
1287
|
+
super.dispose();
|
|
1288
|
+
this._scheduler.dispose();
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* The `equals` comparator, guarded. `undefined` means it threw, in which case the node has
|
|
1292
|
+
* already been failed with that error and the caller must abandon what it was doing: the write
|
|
1293
|
+
* is not applied, the settlement is dropped (A3-SEM-2-b, #127).
|
|
1294
|
+
*
|
|
1295
|
+
* The two routes differ only in who notifies. At settlement `_onReject` is the node's own
|
|
1296
|
+
* failure handler and its notification rules already apply; a write call has no settlement to
|
|
1297
|
+
* notify for it, which is what `fail()` exists to cover (A3-SEM-2-a). `from` is a string rather
|
|
1298
|
+
* than a callback so a comparison allocates nothing — it runs on every settlement.
|
|
1299
|
+
*/
|
|
1300
|
+
_compare(previous, next, from) {
|
|
1301
|
+
try {
|
|
1302
|
+
return this._equals(previous, next);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
if (from === "settlement") this._onReject(error);
|
|
1305
|
+
else this.fail(error);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
_onResolve(value) {
|
|
1310
|
+
if (this._disposed) return;
|
|
1311
|
+
TraceHub.emit("resolve", this);
|
|
1312
|
+
if (this._ops.length > 0) {
|
|
1313
|
+
this._applyOps(this._scheduler.wrap(value));
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
let changed = true;
|
|
1317
|
+
if (this._hasResolved) {
|
|
1318
|
+
const same = this._compare(this._value, value, "settlement");
|
|
1319
|
+
if (same === void 0) return;
|
|
1320
|
+
changed = !same;
|
|
1321
|
+
}
|
|
1322
|
+
const hadPendingReader = this._hasPendingReader;
|
|
1323
|
+
const pendingFromSet = this._pendingFromSet;
|
|
1324
|
+
this._hasPendingReader = false;
|
|
1325
|
+
this._pendingFromSet = false;
|
|
1326
|
+
this.status = "resolved";
|
|
1327
|
+
this._value = value;
|
|
1328
|
+
this._hasResolved = true;
|
|
1329
|
+
this._error = void 0;
|
|
1330
|
+
if (this._scheduler.inline) return;
|
|
1331
|
+
if (changed || hadPendingReader || !pendingFromSet) this.notifyDependents();
|
|
1332
|
+
}
|
|
1333
|
+
_onReject(error) {
|
|
1334
|
+
if (this._disposed) return;
|
|
1335
|
+
TraceHub.emit("reject", this);
|
|
1336
|
+
const first = this._ops[0];
|
|
1337
|
+
if (first?.kind === "set") {
|
|
1338
|
+
this._applyOps(first.input, 1);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
this._ops = [];
|
|
1342
|
+
this.status = "error";
|
|
1343
|
+
this._error = toError(error);
|
|
1344
|
+
if (this._scheduler.inline) return;
|
|
1345
|
+
this.notifyDependents();
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Fails the node from a write-path callback that threw, the way a failing computation fails it:
|
|
1349
|
+
* the error is recorded and normalised through `toError`, `_value` is left on the last resolved
|
|
1350
|
+
* value, and a `set()` recovers the node (A3-SEM-2-a, #126).
|
|
1351
|
+
*
|
|
1352
|
+
* The notification is the one thing that does not simply delegate. Inline, `_onReject` returns
|
|
1353
|
+
* before notifying because an inline settlement always happens inside the call that will notify
|
|
1354
|
+
* — `_overwrite`, `refresh()`. A callback that threw before any of those ran has no such caller,
|
|
1355
|
+
* so the notification is issued here. A deferred scheduler is the opposite case: `_onReject`
|
|
1356
|
+
* notifies for itself, and a second call here would notify twice.
|
|
1357
|
+
*/
|
|
1358
|
+
fail(error) {
|
|
1359
|
+
this._onReject(error);
|
|
1360
|
+
if (this._scheduler.inline) this.notifyDependents();
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Chains the queued writes from `from` on `base` and hands the chain to the scheduler as a new
|
|
1364
|
+
* input: the node stays pending and dependents never observe the intermediate value. Called
|
|
1365
|
+
* from the settlement handlers, where the resolver is already marked finished, so `overwrite`
|
|
1366
|
+
* starts a fresh resolver with its own `settled`. Inline, the chain is folded on the spot and a
|
|
1367
|
+
* throwing updater fails the node like a computation would.
|
|
1368
|
+
*/
|
|
1369
|
+
_applyOps(base, from = 0) {
|
|
1370
|
+
const ops = this._ops;
|
|
1371
|
+
this._ops = [];
|
|
1372
|
+
let chain = base;
|
|
1373
|
+
try {
|
|
1374
|
+
for (let i = from; i < ops.length; i++) {
|
|
1375
|
+
const op = ops[i];
|
|
1376
|
+
chain = op.kind === "set" ? op.input : this._scheduler.chain(chain, op.updater);
|
|
1377
|
+
}
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
this._onReject(error);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
this.status = "pending";
|
|
1383
|
+
this._pendingFromSet = true;
|
|
1384
|
+
this._scheduler.overwrite(chain);
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* The replacing write behind `set()`. Returns whether the write is going to land: `false` only
|
|
1388
|
+
* when the comparator dropped it as unchanged, `true` when it applied now or was queued to apply
|
|
1389
|
+
* at settlement. Collections read that answer to keep `$lastAction` in step with the value view —
|
|
1390
|
+
* a dropped write must not emit a `set` action (A3-SEC-1, #129).
|
|
1391
|
+
*/
|
|
1392
|
+
overwrite(input) {
|
|
1393
|
+
this.assertAlive();
|
|
1394
|
+
if (this.status === "dirty" && this._derivation) this.execute();
|
|
1395
|
+
if (this.status === "pending" && this._scheduler.blocked) {
|
|
1396
|
+
this._ops = [{
|
|
1397
|
+
kind: "set",
|
|
1398
|
+
input
|
|
1399
|
+
}];
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
if (this.status === "resolved" && this._scheduler.inline) {
|
|
1403
|
+
const same = this._compare(this._value, input, "write");
|
|
1404
|
+
if (same === void 0 || same) return false;
|
|
1405
|
+
}
|
|
1406
|
+
this._ops = [];
|
|
1407
|
+
this._overwrite(input);
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
/** The deriving write behind `update()`. */
|
|
1411
|
+
update(updater) {
|
|
1412
|
+
this.assertAlive();
|
|
1413
|
+
if (this.status === "dirty") this.execute();
|
|
1414
|
+
switch (this.status) {
|
|
1415
|
+
case "resolved": {
|
|
1416
|
+
let next;
|
|
1417
|
+
try {
|
|
1418
|
+
next = updater(this._value);
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
this.fail(error);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (this._scheduler.inline) {
|
|
1424
|
+
const same = this._compare(this._value, next, "write");
|
|
1425
|
+
if (same === void 0 || same) return;
|
|
1426
|
+
}
|
|
1427
|
+
this._overwrite(next);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
case "pending":
|
|
1431
|
+
this._ops.push({
|
|
1432
|
+
kind: "update",
|
|
1433
|
+
updater
|
|
1434
|
+
});
|
|
1435
|
+
return;
|
|
1436
|
+
case "error": return;
|
|
1437
|
+
/* v8 ignore next 2 */
|
|
1438
|
+
case "disposed": throw this._disposedError();
|
|
1439
|
+
/* v8 ignore next 2 */
|
|
1440
|
+
case "dirty": throw this.internalError();
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
_overwrite(input) {
|
|
1444
|
+
this._ops = [];
|
|
1445
|
+
this.status = "pending";
|
|
1446
|
+
this._pendingFromSet = true;
|
|
1447
|
+
this._scheduler.overwrite(input);
|
|
1448
|
+
this.notifyDependents();
|
|
1449
|
+
}
|
|
1450
|
+
refresh() {
|
|
1451
|
+
this.assertAlive();
|
|
1452
|
+
const currentValue = this._value;
|
|
1453
|
+
const currentStatus = this.status;
|
|
1454
|
+
this.run();
|
|
1455
|
+
if (this.status === "pending" && !this._scheduler.blocked) return;
|
|
1456
|
+
switch (this.status) {
|
|
1457
|
+
case "resolved": {
|
|
1458
|
+
if (currentStatus !== "resolved") {
|
|
1459
|
+
this.notifyDependents();
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const same = this._compare(currentValue, this._value, "write");
|
|
1463
|
+
if (same === void 0 || same) return;
|
|
1464
|
+
this.notifyDependents();
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
case "error":
|
|
1468
|
+
case "pending":
|
|
1469
|
+
case "dirty":
|
|
1470
|
+
this.notifyDependents();
|
|
1471
|
+
return;
|
|
1472
|
+
case "disposed": return;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
/**
|
|
1477
|
+
* Builds the engine of a synchronous primitive: the input is the value itself and settlement is
|
|
1478
|
+
* inline. The facades take a built core, so which scheduler drives a primitive is decided here and
|
|
1479
|
+
* in {@link asyncCore} alone (#114, step 4 of #110). `derivation` marks a core built from a
|
|
1480
|
+
* dependency-tracking compute function rather than a lazy initializer (#109).
|
|
1481
|
+
* @internal
|
|
1482
|
+
*/
|
|
1483
|
+
function syncCore(input, options, derivation = false) {
|
|
1484
|
+
return new ValueCore(input, options, (compute, onResolve, onReject) => new SyncScheduler(compute, onResolve, onReject), derivation);
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Builds the engine of an asynchronous primitive: the input is a promise of the value and settlement
|
|
1488
|
+
* is deferred — see {@link syncCore}.
|
|
1489
|
+
* @internal
|
|
1490
|
+
*/
|
|
1491
|
+
function asyncCore(input, options, derivation = false) {
|
|
1492
|
+
return new ValueCore(input, options, (compute, onResolve, onReject) => new AsyncScheduler(compute, onResolve, onReject), derivation);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
//#endregion
|
|
1496
|
+
export { PicoFlowPreconditionError as _, ReadableNode as a, Observable as c, TraceHub as d, LiveNodeRegistry as f, PicoFlowInternalError as g, PicoFlowError as h, PICOFLOW_VALUE_NODE as i, Observer as l, PicoFlowDisposedError as m, asyncCore as n, EffectNode as o, PicoFlowCycleError as p, syncCore as r, Node as s, ValueCore as t, ExecutionStack as u };
|