@ersbeth/picoflow 2.1.0 → 2.3.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 +22 -0
- package/SKILL.md +11 -1
- package/dist/picoflow.js +449 -299
- package/dist/types/api/base/flowConfig.d.ts +17 -0
- package/dist/types/api/base/flowDisposable.d.ts +0 -1
- package/dist/types/api/base/flowErrors.d.ts +1 -0
- package/dist/types/api/base/flowObservable.d.ts +0 -1
- package/dist/types/api/base/flowPrimitiveOptions.d.ts +14 -0
- package/dist/types/api/base/flowSubscribable.d.ts +0 -1
- package/dist/types/api/base/flowTracker.d.ts +0 -1
- package/dist/types/api/base/index.d.ts +3 -1
- package/dist/types/api/index.d.ts +0 -1
- package/dist/types/api/nodes/async/flowConstantAsync.d.ts +4 -3
- package/dist/types/api/nodes/async/flowDerivationAsync.d.ts +3 -3
- package/dist/types/api/nodes/async/flowStateAsync.d.ts +4 -3
- package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts +3 -2
- package/dist/types/api/nodes/async/index.d.ts +0 -1
- package/dist/types/api/nodes/collections/flowArray.d.ts +3 -2
- package/dist/types/api/nodes/collections/flowMap.d.ts +3 -2
- package/dist/types/api/nodes/collections/index.d.ts +0 -1
- package/dist/types/api/nodes/flowEffect.d.ts +0 -1
- package/dist/types/api/nodes/flowSignal.d.ts +3 -2
- package/dist/types/api/nodes/flowValue.d.ts +0 -1
- package/dist/types/api/nodes/index.d.ts +0 -1
- package/dist/types/api/nodes/sync/flowConstant.d.ts +3 -2
- package/dist/types/api/nodes/sync/flowDerivation.d.ts +3 -2
- package/dist/types/api/nodes/sync/flowState.d.ts +4 -3
- package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts +3 -2
- package/dist/types/api/nodes/sync/index.d.ts +0 -1
- package/dist/types/api/nodes/utils.d.ts +0 -1
- package/dist/types/base/dependenciesRegistry.d.ts +1 -0
- package/dist/types/base/dependentsRegistry.d.ts +1 -0
- package/dist/types/base/disposable.d.ts +8 -4
- package/dist/types/base/errors.d.ts +31 -0
- package/dist/types/base/executionStack.d.ts +5 -9
- package/dist/types/base/index.d.ts +3 -1
- package/dist/types/base/node.d.ts +4 -4
- package/dist/types/base/observable.d.ts +1 -2
- package/dist/types/base/observer.d.ts +10 -2
- package/dist/types/converters/index.d.ts +0 -1
- package/dist/types/converters/solid.d.ts +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/nodes/actionNode.d.ts +1 -23
- package/dist/types/nodes/arrayNode.d.ts +1 -26
- package/dist/types/nodes/effectNode.d.ts +1 -32
- package/dist/types/nodes/index.d.ts +0 -1
- package/dist/types/nodes/mapNode.d.ts +1 -20
- package/dist/types/nodes/signalNode.d.ts +1 -10
- package/dist/types/nodes/valueAsyncNode.d.ts +1 -23
- package/dist/types/nodes/valueNode.d.ts +1 -20
- package/dist/types/nodes/valueSyncNode.d.ts +1 -23
- package/dist/types/schedulers/asyncResolver.d.ts +1 -19
- package/dist/types/schedulers/asyncScheduler.d.ts +1 -21
- package/dist/types/schedulers/index.d.ts +0 -1
- package/dist/types/schedulers/pendingError.d.ts +1 -9
- package/dist/types/schedulers/scheduler.d.ts +1 -10
- package/dist/types/schedulers/syncResolver.d.ts +1 -18
- package/dist/types/schedulers/syncScheduler.d.ts +1 -19
- package/package.json +14 -11
- package/dist/types/api/base/flowDisposable.d.ts.map +0 -1
- package/dist/types/api/base/flowObservable.d.ts.map +0 -1
- package/dist/types/api/base/flowSubscribable.d.ts.map +0 -1
- package/dist/types/api/base/flowTracker.d.ts.map +0 -1
- package/dist/types/api/base/index.d.ts.map +0 -1
- package/dist/types/api/index.d.ts.map +0 -1
- package/dist/types/api/nodes/async/flowConstantAsync.d.ts.map +0 -1
- package/dist/types/api/nodes/async/flowDerivationAsync.d.ts.map +0 -1
- package/dist/types/api/nodes/async/flowStateAsync.d.ts.map +0 -1
- package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts.map +0 -1
- package/dist/types/api/nodes/async/index.d.ts.map +0 -1
- package/dist/types/api/nodes/collections/flowArray.d.ts.map +0 -1
- package/dist/types/api/nodes/collections/flowMap.d.ts.map +0 -1
- package/dist/types/api/nodes/collections/index.d.ts.map +0 -1
- package/dist/types/api/nodes/flowEffect.d.ts.map +0 -1
- package/dist/types/api/nodes/flowSignal.d.ts.map +0 -1
- package/dist/types/api/nodes/flowValue.d.ts.map +0 -1
- package/dist/types/api/nodes/index.d.ts.map +0 -1
- package/dist/types/api/nodes/sync/flowConstant.d.ts.map +0 -1
- package/dist/types/api/nodes/sync/flowDerivation.d.ts.map +0 -1
- package/dist/types/api/nodes/sync/flowState.d.ts.map +0 -1
- package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts.map +0 -1
- package/dist/types/api/nodes/sync/index.d.ts.map +0 -1
- package/dist/types/api/nodes/utils.d.ts.map +0 -1
- package/dist/types/base/disposable.d.ts.map +0 -1
- package/dist/types/base/executionStack.d.ts.map +0 -1
- package/dist/types/base/index.d.ts.map +0 -1
- package/dist/types/base/node.d.ts.map +0 -1
- package/dist/types/base/observable.d.ts.map +0 -1
- package/dist/types/base/observer.d.ts.map +0 -1
- package/dist/types/converters/index.d.ts.map +0 -1
- package/dist/types/converters/solid.d.ts.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/nodes/actionNode.d.ts.map +0 -1
- package/dist/types/nodes/arrayNode.d.ts.map +0 -1
- package/dist/types/nodes/effectNode.d.ts.map +0 -1
- package/dist/types/nodes/index.d.ts.map +0 -1
- package/dist/types/nodes/mapNode.d.ts.map +0 -1
- package/dist/types/nodes/signalNode.d.ts.map +0 -1
- package/dist/types/nodes/valueAsyncNode.d.ts.map +0 -1
- package/dist/types/nodes/valueNode.d.ts.map +0 -1
- package/dist/types/nodes/valueSyncNode.d.ts.map +0 -1
- package/dist/types/schedulers/asyncResolver.d.ts.map +0 -1
- package/dist/types/schedulers/asyncScheduler.d.ts.map +0 -1
- package/dist/types/schedulers/index.d.ts.map +0 -1
- package/dist/types/schedulers/pendingError.d.ts.map +0 -1
- package/dist/types/schedulers/scheduler.d.ts.map +0 -1
- package/dist/types/schedulers/syncResolver.d.ts.map +0 -1
- package/dist/types/schedulers/syncScheduler.d.ts.map +0 -1
package/dist/picoflow.js
CHANGED
|
@@ -1,5 +1,310 @@
|
|
|
1
1
|
import { createResource, onMount, createEffect, resetErrorBoundaries, onCleanup } from 'solid-js';
|
|
2
2
|
|
|
3
|
+
class DependenciesRegistry {
|
|
4
|
+
_dependencies = /* @__PURE__ */ new Set();
|
|
5
|
+
register(dependency, dependent) {
|
|
6
|
+
this._dependencies.add(dependency);
|
|
7
|
+
dependency.registerDependent(dependent);
|
|
8
|
+
}
|
|
9
|
+
unregister(dependency, dependent) {
|
|
10
|
+
this._dependencies.delete(dependency);
|
|
11
|
+
dependency.unregisterDependent(dependent);
|
|
12
|
+
}
|
|
13
|
+
clear(dependent) {
|
|
14
|
+
this._dependencies.forEach((dependency) => {
|
|
15
|
+
dependency.unregisterDependent(dependent);
|
|
16
|
+
});
|
|
17
|
+
this._dependencies.clear();
|
|
18
|
+
}
|
|
19
|
+
forEach(fn) {
|
|
20
|
+
Array.from(this._dependencies).forEach(fn);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class DependentsRegistry {
|
|
25
|
+
_dependents = /* @__PURE__ */ new Set();
|
|
26
|
+
register(dependent) {
|
|
27
|
+
this._dependents.add(dependent);
|
|
28
|
+
}
|
|
29
|
+
unregister(dependent) {
|
|
30
|
+
this._dependents.delete(dependent);
|
|
31
|
+
}
|
|
32
|
+
notifyAll() {
|
|
33
|
+
this._dependents.forEach((dependent) => {
|
|
34
|
+
dependent.notify();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
forEach(fn) {
|
|
38
|
+
Array.from(this._dependents).forEach(fn);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class PicoFlowError extends Error {
|
|
43
|
+
constructor(message) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "PicoFlowError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
class PicoFlowDisposedError extends PicoFlowError {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "PicoFlowDisposedError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
class PicoFlowCycleError extends PicoFlowError {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "PicoFlowCycleError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
class PicoFlowInternalError extends PicoFlowError {
|
|
61
|
+
constructor(message) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "PicoFlowInternalError";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class Disposable {
|
|
68
|
+
_disposed = false;
|
|
69
|
+
_name;
|
|
70
|
+
constructor(name) {
|
|
71
|
+
this._name = name;
|
|
72
|
+
}
|
|
73
|
+
get disposed() {
|
|
74
|
+
return this._disposed;
|
|
75
|
+
}
|
|
76
|
+
/** Builds the disposed-primitive error, naming the primitive when a `name` was given at creation. */
|
|
77
|
+
_disposedError() {
|
|
78
|
+
return new PicoFlowDisposedError(
|
|
79
|
+
this._name ? `[PicoFlow] Primitive "${this._name}" is disposed` : "[PicoFlow] Primitive is disposed"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class ExecutionStack {
|
|
85
|
+
static _MAX_FLUSH_STEPS = 1e4;
|
|
86
|
+
static _pendingQueue = [];
|
|
87
|
+
static _effectQueue = [];
|
|
88
|
+
static _executionScheduled;
|
|
89
|
+
static _coalesceResetScheduled = false;
|
|
90
|
+
/** Per-effect resets accumulated for the microtask scheduled below, so notifying more than one
|
|
91
|
+
* effect in the same sync turn doesn't discard every reset after the first (A2-SEM-5). */
|
|
92
|
+
static _coalesceResets = /* @__PURE__ */ new Set();
|
|
93
|
+
/** Routes any error escaping the flush loop; defaults to logging rather than crashing (A2-SEM-2, A2-SEC-1). */
|
|
94
|
+
static _onFlushError = (error) => console.error(error);
|
|
95
|
+
/**
|
|
96
|
+
* True once the current synchronous call stack has finished and microtasks have started.
|
|
97
|
+
* EffectNode uses this to treat microtask notifications (e.g. async resolve) differently
|
|
98
|
+
* from coalesced sync-batch notifications.
|
|
99
|
+
*/
|
|
100
|
+
static _pastSyncTurn = false;
|
|
101
|
+
/** @internal Used by EffectNode to distinguish sync-batch from microtask notifications. */
|
|
102
|
+
static get pastSyncTurn() {
|
|
103
|
+
return ExecutionStack._pastSyncTurn;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* @internal Runs after the current sync turn ends (queueMicrotask).
|
|
107
|
+
* Marks pastSyncTurn so the next coalesced effect notify can bump its epoch,
|
|
108
|
+
* and resets per-effect coalesce flags via onReset.
|
|
109
|
+
*/
|
|
110
|
+
static scheduleCoalesceReset(onReset) {
|
|
111
|
+
ExecutionStack._coalesceResets.add(onReset);
|
|
112
|
+
if (ExecutionStack._coalesceResetScheduled) return;
|
|
113
|
+
ExecutionStack._coalesceResetScheduled = true;
|
|
114
|
+
queueMicrotask(() => {
|
|
115
|
+
ExecutionStack._pastSyncTurn = true;
|
|
116
|
+
ExecutionStack._coalesceResetScheduled = false;
|
|
117
|
+
const resets = ExecutionStack._coalesceResets;
|
|
118
|
+
ExecutionStack._coalesceResets = /* @__PURE__ */ new Set();
|
|
119
|
+
resets.forEach((reset) => {
|
|
120
|
+
reset();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/** @internal Wired to the public `onFlushError()` API in `src/api/base/flowConfig.ts`. */
|
|
125
|
+
static setFlushErrorHandler(handler) {
|
|
126
|
+
ExecutionStack._onFlushError = handler;
|
|
127
|
+
}
|
|
128
|
+
static pushPending(node) {
|
|
129
|
+
ExecutionStack._beginSyncTurn();
|
|
130
|
+
ExecutionStack._scheduleExecution();
|
|
131
|
+
ExecutionStack._pendingQueue.push(node);
|
|
132
|
+
}
|
|
133
|
+
static pushEffect(effect) {
|
|
134
|
+
ExecutionStack._beginSyncTurn();
|
|
135
|
+
ExecutionStack._scheduleExecution();
|
|
136
|
+
ExecutionStack._effectQueue.push(effect);
|
|
137
|
+
}
|
|
138
|
+
static _beginSyncTurn() {
|
|
139
|
+
ExecutionStack._pastSyncTurn = false;
|
|
140
|
+
}
|
|
141
|
+
static _scheduleExecution() {
|
|
142
|
+
if (ExecutionStack._executionScheduled) return;
|
|
143
|
+
ExecutionStack._executionScheduled = new Promise((resolve) => {
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
ExecutionStack._executionScheduled = void 0;
|
|
146
|
+
ExecutionStack._execute();
|
|
147
|
+
resolve();
|
|
148
|
+
}, 0);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
static _drain(queue) {
|
|
152
|
+
for (let i = 0; i < queue.length; i++) {
|
|
153
|
+
if (i >= ExecutionStack._MAX_FLUSH_STEPS) {
|
|
154
|
+
ExecutionStack._pendingQueue.length = 0;
|
|
155
|
+
ExecutionStack._effectQueue.length = 0;
|
|
156
|
+
ExecutionStack._onFlushError(
|
|
157
|
+
new PicoFlowCycleError("[PicoFlow] Reactive update cycle exceeded maximum depth")
|
|
158
|
+
);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const node = queue[i];
|
|
162
|
+
try {
|
|
163
|
+
node?.execute();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
ExecutionStack._onFlushError(error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
queue.length = 0;
|
|
169
|
+
}
|
|
170
|
+
static _execute() {
|
|
171
|
+
ExecutionStack._drain(ExecutionStack._pendingQueue);
|
|
172
|
+
ExecutionStack._drain(ExecutionStack._effectQueue);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
class Node extends Disposable {
|
|
177
|
+
_dependenciesRegistry = new DependenciesRegistry();
|
|
178
|
+
_dependentsRegistry = new DependentsRegistry();
|
|
179
|
+
_status = "resolved";
|
|
180
|
+
get status() {
|
|
181
|
+
if (this.disposed) throw this._disposedError();
|
|
182
|
+
return this._status;
|
|
183
|
+
}
|
|
184
|
+
set status(status) {
|
|
185
|
+
if (this.disposed) throw this._disposedError();
|
|
186
|
+
this._status = status;
|
|
187
|
+
}
|
|
188
|
+
registerDependency(dependency) {
|
|
189
|
+
if (this._disposed) throw this._disposedError();
|
|
190
|
+
this._dependenciesRegistry.register(dependency, this);
|
|
191
|
+
}
|
|
192
|
+
unregisterDependency(dependency) {
|
|
193
|
+
if (this._disposed) throw this._disposedError();
|
|
194
|
+
this._dependenciesRegistry.unregister(dependency, this);
|
|
195
|
+
}
|
|
196
|
+
clearDependencies() {
|
|
197
|
+
if (this.disposed) throw this._disposedError();
|
|
198
|
+
this._dependenciesRegistry.clear(this);
|
|
199
|
+
}
|
|
200
|
+
registerDependent(dependent) {
|
|
201
|
+
if (this.disposed) throw this._disposedError();
|
|
202
|
+
this._dependentsRegistry.register(dependent);
|
|
203
|
+
}
|
|
204
|
+
unregisterDependent(dependent) {
|
|
205
|
+
if (this.disposed) throw this._disposedError();
|
|
206
|
+
this._dependentsRegistry.unregister(dependent);
|
|
207
|
+
}
|
|
208
|
+
markDependencyDisposed() {
|
|
209
|
+
}
|
|
210
|
+
notifyDependents() {
|
|
211
|
+
if (this.disposed) return;
|
|
212
|
+
this._dependentsRegistry.notifyAll();
|
|
213
|
+
}
|
|
214
|
+
watch(tracker) {
|
|
215
|
+
if (this.disposed) throw this._disposedError();
|
|
216
|
+
tracker.registerDependency(this);
|
|
217
|
+
}
|
|
218
|
+
trigger() {
|
|
219
|
+
if (this.disposed) throw this._disposedError();
|
|
220
|
+
this.notifyDependents();
|
|
221
|
+
}
|
|
222
|
+
dispose() {
|
|
223
|
+
if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
224
|
+
this._dependentsRegistry.forEach((dependant) => {
|
|
225
|
+
dependant.markDependencyDisposed();
|
|
226
|
+
dependant.unregisterDependency(this);
|
|
227
|
+
this.unregisterDependent(dependant);
|
|
228
|
+
});
|
|
229
|
+
this._dependenciesRegistry.forEach((dependency) => {
|
|
230
|
+
this.unregisterDependency(dependency);
|
|
231
|
+
});
|
|
232
|
+
this._disposed = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
class Observable extends Disposable {
|
|
237
|
+
_dependentsRegistry = new DependentsRegistry();
|
|
238
|
+
_status = "resolved";
|
|
239
|
+
get status() {
|
|
240
|
+
if (this.disposed) throw this._disposedError();
|
|
241
|
+
return this._status;
|
|
242
|
+
}
|
|
243
|
+
set status(status) {
|
|
244
|
+
if (this.disposed) throw this._disposedError();
|
|
245
|
+
this._status = status;
|
|
246
|
+
}
|
|
247
|
+
registerDependent(dependent) {
|
|
248
|
+
if (this.disposed) throw this._disposedError();
|
|
249
|
+
this._dependentsRegistry.register(dependent);
|
|
250
|
+
}
|
|
251
|
+
unregisterDependent(dependent) {
|
|
252
|
+
if (this.disposed) throw this._disposedError();
|
|
253
|
+
this._dependentsRegistry.unregister(dependent);
|
|
254
|
+
}
|
|
255
|
+
notifyDependents() {
|
|
256
|
+
if (this.disposed) return;
|
|
257
|
+
this._dependentsRegistry.notifyAll();
|
|
258
|
+
}
|
|
259
|
+
watch(tracker) {
|
|
260
|
+
if (this.disposed) throw this._disposedError();
|
|
261
|
+
tracker.registerDependency(this);
|
|
262
|
+
}
|
|
263
|
+
trigger() {
|
|
264
|
+
if (this.disposed) throw this._disposedError();
|
|
265
|
+
this.notifyDependents();
|
|
266
|
+
}
|
|
267
|
+
dispose() {
|
|
268
|
+
if (this._disposed) throw this._disposedError();
|
|
269
|
+
this._dependentsRegistry.forEach((dependant) => {
|
|
270
|
+
dependant.markDependencyDisposed();
|
|
271
|
+
dependant.unregisterDependency(this);
|
|
272
|
+
this.unregisterDependent(dependant);
|
|
273
|
+
});
|
|
274
|
+
this._disposed = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
class Observer extends Disposable {
|
|
279
|
+
_dependenciesRegistry = new DependenciesRegistry();
|
|
280
|
+
/** Set when a dependency disposed while this observer was queued; consumed by the next execute(). */
|
|
281
|
+
_dependencyDisposed = false;
|
|
282
|
+
markDependencyDisposed() {
|
|
283
|
+
this._dependencyDisposed = true;
|
|
284
|
+
}
|
|
285
|
+
registerDependency(dependency) {
|
|
286
|
+
if (this._disposed) throw this._disposedError();
|
|
287
|
+
this._dependenciesRegistry.register(dependency, this);
|
|
288
|
+
}
|
|
289
|
+
unregisterDependency(dependency) {
|
|
290
|
+
if (this._disposed) throw this._disposedError();
|
|
291
|
+
this._dependenciesRegistry.unregister(dependency, this);
|
|
292
|
+
}
|
|
293
|
+
clearDependencies() {
|
|
294
|
+
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
295
|
+
this._dependenciesRegistry.clear(this);
|
|
296
|
+
}
|
|
297
|
+
dispose() {
|
|
298
|
+
if (this._disposed) throw this._disposedError();
|
|
299
|
+
this._dependenciesRegistry.clear(this);
|
|
300
|
+
this._disposed = true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function onFlushError(handler) {
|
|
305
|
+
ExecutionStack.setFlushErrorHandler(handler);
|
|
306
|
+
}
|
|
307
|
+
|
|
3
308
|
function isDisposable(obj) {
|
|
4
309
|
return obj !== null && obj !== void 0 && typeof obj.dispose === "function";
|
|
5
310
|
}
|
|
@@ -84,14 +389,14 @@ class AsyncScheduler {
|
|
|
84
389
|
this._attachHandlers(this._resolver);
|
|
85
390
|
}
|
|
86
391
|
get settled() {
|
|
87
|
-
if (this._disposed) throw new
|
|
392
|
+
if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
|
|
88
393
|
return this._settled.promise;
|
|
89
394
|
}
|
|
90
395
|
get disposed() {
|
|
91
396
|
return this._disposed;
|
|
92
397
|
}
|
|
93
398
|
overwrite(promise) {
|
|
94
|
-
if (this._disposed) throw new
|
|
399
|
+
if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
|
|
95
400
|
if (this._resolver.finished) {
|
|
96
401
|
this._resolver = new AsyncResolver(this._compute);
|
|
97
402
|
this._settled = Promise.withResolvers();
|
|
@@ -100,7 +405,7 @@ class AsyncScheduler {
|
|
|
100
405
|
this._resolver.overwrite(promise);
|
|
101
406
|
}
|
|
102
407
|
compute() {
|
|
103
|
-
if (this._disposed) throw new
|
|
408
|
+
if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
|
|
104
409
|
if (this._resolver.finished) {
|
|
105
410
|
this._resolver = new AsyncResolver(this._compute);
|
|
106
411
|
this._settled = Promise.withResolvers();
|
|
@@ -192,7 +497,7 @@ class SyncScheduler {
|
|
|
192
497
|
return this._settled.promise;
|
|
193
498
|
}
|
|
194
499
|
overwrite(value) {
|
|
195
|
-
if (this._disposed) throw new
|
|
500
|
+
if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
|
|
196
501
|
if (this._resolver.finished) {
|
|
197
502
|
this._settled = Promise.withResolvers();
|
|
198
503
|
this._resolver = new SyncResolver(this._compute, this._onResolve, this._onReject);
|
|
@@ -200,7 +505,7 @@ class SyncScheduler {
|
|
|
200
505
|
this._resolver.overwrite(value);
|
|
201
506
|
}
|
|
202
507
|
compute() {
|
|
203
|
-
if (this._disposed) throw new
|
|
508
|
+
if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
|
|
204
509
|
if (this._resolver.finished) {
|
|
205
510
|
this._settled = Promise.withResolvers();
|
|
206
511
|
this._resolver = new SyncResolver(this._compute, this._onResolve, this._onReject);
|
|
@@ -213,229 +518,6 @@ class SyncScheduler {
|
|
|
213
518
|
}
|
|
214
519
|
}
|
|
215
520
|
|
|
216
|
-
class Disposable {
|
|
217
|
-
_disposed = false;
|
|
218
|
-
get disposed() {
|
|
219
|
-
return this._disposed;
|
|
220
|
-
}
|
|
221
|
-
dispose() {
|
|
222
|
-
if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
223
|
-
this._disposed = true;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
class ExecutionStack {
|
|
228
|
-
static _MAX_FLUSH_STEPS = 1e4;
|
|
229
|
-
static _pendingQueue = [];
|
|
230
|
-
static _effectQueue = [];
|
|
231
|
-
static _executionScheduled;
|
|
232
|
-
static _coalesceResetScheduled = false;
|
|
233
|
-
/**
|
|
234
|
-
* True once the current synchronous call stack has finished and microtasks have started.
|
|
235
|
-
* EffectNode uses this to treat microtask notifications (e.g. async resolve) differently
|
|
236
|
-
* from coalesced sync-batch notifications.
|
|
237
|
-
*/
|
|
238
|
-
static _pastSyncTurn = false;
|
|
239
|
-
/** @internal Used by EffectNode to distinguish sync-batch from microtask notifications. */
|
|
240
|
-
static get pastSyncTurn() {
|
|
241
|
-
return ExecutionStack._pastSyncTurn;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* @internal Runs after the current sync turn ends (queueMicrotask).
|
|
245
|
-
* Marks pastSyncTurn so the next coalesced effect notify can bump its epoch,
|
|
246
|
-
* and resets per-effect coalesce flags via onReset.
|
|
247
|
-
*/
|
|
248
|
-
static scheduleCoalesceReset(onReset) {
|
|
249
|
-
if (ExecutionStack._coalesceResetScheduled) return;
|
|
250
|
-
ExecutionStack._coalesceResetScheduled = true;
|
|
251
|
-
queueMicrotask(() => {
|
|
252
|
-
ExecutionStack._pastSyncTurn = true;
|
|
253
|
-
ExecutionStack._coalesceResetScheduled = false;
|
|
254
|
-
onReset();
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
static pushPending(node) {
|
|
258
|
-
ExecutionStack._beginSyncTurn();
|
|
259
|
-
ExecutionStack._scheduleExecution();
|
|
260
|
-
ExecutionStack._pendingQueue.push(node);
|
|
261
|
-
}
|
|
262
|
-
static pushEffect(effect) {
|
|
263
|
-
ExecutionStack._beginSyncTurn();
|
|
264
|
-
ExecutionStack._scheduleExecution();
|
|
265
|
-
ExecutionStack._effectQueue.push(effect);
|
|
266
|
-
}
|
|
267
|
-
static _beginSyncTurn() {
|
|
268
|
-
ExecutionStack._pastSyncTurn = false;
|
|
269
|
-
}
|
|
270
|
-
static _scheduleExecution() {
|
|
271
|
-
if (ExecutionStack._executionScheduled) return;
|
|
272
|
-
ExecutionStack._executionScheduled = new Promise((resolve) => {
|
|
273
|
-
setTimeout(() => {
|
|
274
|
-
ExecutionStack._executionScheduled = void 0;
|
|
275
|
-
ExecutionStack._execute();
|
|
276
|
-
resolve();
|
|
277
|
-
}, 0);
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
static _drain(queue) {
|
|
281
|
-
for (let i = 0; i < queue.length; i++) {
|
|
282
|
-
if (i >= ExecutionStack._MAX_FLUSH_STEPS) {
|
|
283
|
-
ExecutionStack._pendingQueue.length = 0;
|
|
284
|
-
ExecutionStack._effectQueue.length = 0;
|
|
285
|
-
throw new Error("[PicoFlow] Reactive update cycle exceeded maximum depth");
|
|
286
|
-
}
|
|
287
|
-
const node = queue[i];
|
|
288
|
-
node?.execute();
|
|
289
|
-
}
|
|
290
|
-
queue.length = 0;
|
|
291
|
-
}
|
|
292
|
-
static _execute() {
|
|
293
|
-
ExecutionStack._drain(ExecutionStack._pendingQueue);
|
|
294
|
-
ExecutionStack._drain(ExecutionStack._effectQueue);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
class Node extends Disposable {
|
|
299
|
-
_dependencies = /* @__PURE__ */ new Set();
|
|
300
|
-
_dependents = /* @__PURE__ */ new Set();
|
|
301
|
-
_status = "resolved";
|
|
302
|
-
get status() {
|
|
303
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
304
|
-
return this._status;
|
|
305
|
-
}
|
|
306
|
-
set status(status) {
|
|
307
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
308
|
-
this._status = status;
|
|
309
|
-
}
|
|
310
|
-
registerDependency(dependency) {
|
|
311
|
-
if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
312
|
-
this._dependencies.add(dependency);
|
|
313
|
-
dependency.registerDependent(this);
|
|
314
|
-
}
|
|
315
|
-
unregisterDependency(dependency) {
|
|
316
|
-
if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
317
|
-
this._dependencies.delete(dependency);
|
|
318
|
-
dependency.unregisterDependent(this);
|
|
319
|
-
}
|
|
320
|
-
clearDependencies() {
|
|
321
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
322
|
-
this._dependencies.forEach((dependency) => {
|
|
323
|
-
dependency.unregisterDependent(this);
|
|
324
|
-
});
|
|
325
|
-
this._dependencies.clear();
|
|
326
|
-
}
|
|
327
|
-
registerDependent(dependent) {
|
|
328
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
329
|
-
this._dependents.add(dependent);
|
|
330
|
-
}
|
|
331
|
-
unregisterDependent(dependent) {
|
|
332
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
333
|
-
this._dependents.delete(dependent);
|
|
334
|
-
}
|
|
335
|
-
notifyDependents() {
|
|
336
|
-
if (this.disposed) return;
|
|
337
|
-
this._dependents.forEach((dependent) => {
|
|
338
|
-
dependent.notify();
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
watch(tracker) {
|
|
342
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
343
|
-
tracker.registerDependency(this);
|
|
344
|
-
}
|
|
345
|
-
trigger() {
|
|
346
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
347
|
-
this.notifyDependents();
|
|
348
|
-
}
|
|
349
|
-
notify() {
|
|
350
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
351
|
-
if (this.status === "dirty") return;
|
|
352
|
-
this.status = "dirty";
|
|
353
|
-
this.notifyDependents();
|
|
354
|
-
}
|
|
355
|
-
dispose() {
|
|
356
|
-
if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
357
|
-
Array.from(this._dependents).forEach((dependant) => {
|
|
358
|
-
dependant.unregisterDependency(this);
|
|
359
|
-
this.unregisterDependent(dependant);
|
|
360
|
-
});
|
|
361
|
-
Array.from(this._dependencies).forEach((dependency) => {
|
|
362
|
-
this.unregisterDependency(dependency);
|
|
363
|
-
});
|
|
364
|
-
this._disposed = true;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
class Observable extends Disposable {
|
|
369
|
-
_dependents = /* @__PURE__ */ new Set();
|
|
370
|
-
_status = "resolved";
|
|
371
|
-
get status() {
|
|
372
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
373
|
-
return this._status;
|
|
374
|
-
}
|
|
375
|
-
set status(status) {
|
|
376
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
377
|
-
this._status = status;
|
|
378
|
-
}
|
|
379
|
-
registerDependent(dependent) {
|
|
380
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
381
|
-
this._dependents.add(dependent);
|
|
382
|
-
}
|
|
383
|
-
unregisterDependent(dependent) {
|
|
384
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
385
|
-
this._dependents.delete(dependent);
|
|
386
|
-
}
|
|
387
|
-
notifyDependents() {
|
|
388
|
-
if (this.disposed) return;
|
|
389
|
-
this._dependents.forEach((dependent) => {
|
|
390
|
-
dependent.notify();
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
watch(tracker) {
|
|
394
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
395
|
-
tracker.registerDependency(this);
|
|
396
|
-
}
|
|
397
|
-
trigger() {
|
|
398
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
399
|
-
this.notifyDependents();
|
|
400
|
-
}
|
|
401
|
-
dispose() {
|
|
402
|
-
if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
403
|
-
Array.from(this._dependents).forEach((dependant) => {
|
|
404
|
-
dependant.unregisterDependency(this);
|
|
405
|
-
this.unregisterDependent(dependant);
|
|
406
|
-
});
|
|
407
|
-
this._disposed = true;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
class Observer extends Disposable {
|
|
412
|
-
_dependencies = /* @__PURE__ */ new Set();
|
|
413
|
-
registerDependency(dependency) {
|
|
414
|
-
if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
415
|
-
this._dependencies.add(dependency);
|
|
416
|
-
dependency.registerDependent(this);
|
|
417
|
-
}
|
|
418
|
-
unregisterDependency(dependency) {
|
|
419
|
-
if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
420
|
-
this._dependencies.delete(dependency);
|
|
421
|
-
dependency.unregisterDependent(this);
|
|
422
|
-
}
|
|
423
|
-
clearDependencies() {
|
|
424
|
-
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
425
|
-
this._dependencies.forEach((dependency) => {
|
|
426
|
-
dependency.unregisterDependent(this);
|
|
427
|
-
});
|
|
428
|
-
this._dependencies.clear();
|
|
429
|
-
}
|
|
430
|
-
dispose() {
|
|
431
|
-
if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
432
|
-
Array.from(this._dependencies).forEach((dependency) => {
|
|
433
|
-
this.unregisterDependency(dependency);
|
|
434
|
-
});
|
|
435
|
-
this._disposed = true;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
521
|
class EffectNode extends Observer {
|
|
440
522
|
_data;
|
|
441
523
|
_onData;
|
|
@@ -449,6 +531,8 @@ class EffectNode extends Observer {
|
|
|
449
531
|
_notifyEpoch = 0;
|
|
450
532
|
/** How many notification epochs have been executed so far. */
|
|
451
533
|
_executedEpoch = 0;
|
|
534
|
+
/** Callbacks registered via {@link onDispose}, run once after dispose() tears down the graph edges. */
|
|
535
|
+
_disposeListeners = [];
|
|
452
536
|
constructor(data, onData, onError, onPending) {
|
|
453
537
|
super();
|
|
454
538
|
this._data = data;
|
|
@@ -458,8 +542,23 @@ class EffectNode extends Observer {
|
|
|
458
542
|
this._notifyEpoch = 1;
|
|
459
543
|
this.execute();
|
|
460
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* Registers a callback to run once this effect is disposed, after the graph edges are torn
|
|
547
|
+
* down. Lets a caller that wraps an effect (e.g. {@link ActionNode.subscribe}) attach its own
|
|
548
|
+
* teardown without patching `dispose` on the returned instance (A2-ARCH-8).
|
|
549
|
+
* @internal
|
|
550
|
+
*/
|
|
551
|
+
onDispose(fn) {
|
|
552
|
+
this._disposeListeners.push(fn);
|
|
553
|
+
}
|
|
554
|
+
dispose() {
|
|
555
|
+
super.dispose();
|
|
556
|
+
this._disposeListeners.forEach((fn) => {
|
|
557
|
+
fn();
|
|
558
|
+
});
|
|
559
|
+
}
|
|
461
560
|
notify() {
|
|
462
|
-
if (this._disposed) throw
|
|
561
|
+
if (this._disposed) throw this._disposedError();
|
|
463
562
|
if (this._queued) {
|
|
464
563
|
if (!this._coalescedWhileQueued) {
|
|
465
564
|
this._coalescedWhileQueued = true;
|
|
@@ -482,6 +581,11 @@ class EffectNode extends Observer {
|
|
|
482
581
|
this._executedEpoch = this._notifyEpoch;
|
|
483
582
|
return;
|
|
484
583
|
}
|
|
584
|
+
if (this._dependencyDisposed) {
|
|
585
|
+
this._dependencyDisposed = false;
|
|
586
|
+
this._executedEpoch = this._notifyEpoch;
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
485
589
|
const targetEpoch = this._notifyEpoch;
|
|
486
590
|
while (this._executedEpoch < targetEpoch) {
|
|
487
591
|
this._executedEpoch++;
|
|
@@ -489,6 +593,11 @@ class EffectNode extends Observer {
|
|
|
489
593
|
this._executedEpoch = this._notifyEpoch;
|
|
490
594
|
return;
|
|
491
595
|
}
|
|
596
|
+
if (this._dependencyDisposed) {
|
|
597
|
+
this._dependencyDisposed = false;
|
|
598
|
+
this._executedEpoch = this._notifyEpoch;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
492
601
|
try {
|
|
493
602
|
this.clearDependencies();
|
|
494
603
|
const data = this._data(this);
|
|
@@ -508,11 +617,27 @@ class EffectNode extends Observer {
|
|
|
508
617
|
}
|
|
509
618
|
}
|
|
510
619
|
|
|
620
|
+
const PICOFLOW_VALUE_NODE = /* @__PURE__ */ Symbol.for("picoflow.node");
|
|
511
621
|
class ValueNode extends Node {
|
|
622
|
+
[PICOFLOW_VALUE_NODE] = true;
|
|
512
623
|
_value;
|
|
513
624
|
_error;
|
|
625
|
+
/** Builds the "unreachable state" error, naming the primitive when a `name` was given at creation. */
|
|
626
|
+
_internalError() {
|
|
627
|
+
return new PicoFlowInternalError(
|
|
628
|
+
this._name ? `[PicoFlow] Internal error (${this._name})` : "[PicoFlow] Internal error"
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Hook called just before a tracked read throws `PendingError`, i.e. a dependent registered while
|
|
633
|
+
* this node is mid-flight and has no other way to learn the computation later settled (A2-PERF-4).
|
|
634
|
+
* No-op by default; overridden by {@link ValueAsyncNode} to know when its own post-resolve
|
|
635
|
+
* `notifyDependents()` call cannot be skipped even if the resolved value is unchanged.
|
|
636
|
+
*/
|
|
637
|
+
_markPendingRead() {
|
|
638
|
+
}
|
|
514
639
|
watch(tracker) {
|
|
515
|
-
if (this.disposed) throw
|
|
640
|
+
if (this.disposed) throw this._disposedError();
|
|
516
641
|
super.watch(tracker);
|
|
517
642
|
switch (this.status) {
|
|
518
643
|
case "resolved":
|
|
@@ -520,26 +645,28 @@ class ValueNode extends Node {
|
|
|
520
645
|
case "error":
|
|
521
646
|
throw this._error;
|
|
522
647
|
case "pending": {
|
|
648
|
+
this._markPendingRead();
|
|
523
649
|
throw new PendingError(this._scheduler.settled);
|
|
524
650
|
}
|
|
525
651
|
case "dirty": {
|
|
526
652
|
this.execute();
|
|
527
653
|
switch (this.status) {
|
|
528
654
|
case "pending":
|
|
655
|
+
this._markPendingRead();
|
|
529
656
|
throw new PendingError(this._scheduler.settled);
|
|
530
657
|
case "resolved":
|
|
531
658
|
return;
|
|
532
659
|
case "error":
|
|
533
660
|
throw this._error;
|
|
534
661
|
case "dirty": {
|
|
535
|
-
throw
|
|
662
|
+
throw this._internalError();
|
|
536
663
|
}
|
|
537
664
|
}
|
|
538
665
|
}
|
|
539
666
|
}
|
|
540
667
|
}
|
|
541
668
|
notify() {
|
|
542
|
-
if (this.disposed) throw
|
|
669
|
+
if (this.disposed) throw this._disposedError();
|
|
543
670
|
if (this.status === "dirty") return;
|
|
544
671
|
if (this.status === "pending") {
|
|
545
672
|
ExecutionStack.pushPending(this);
|
|
@@ -552,7 +679,7 @@ class ValueNode extends Node {
|
|
|
552
679
|
this._scheduler.dispose();
|
|
553
680
|
}
|
|
554
681
|
get(tracker) {
|
|
555
|
-
if (this.disposed) throw
|
|
682
|
+
if (this.disposed) throw this._disposedError();
|
|
556
683
|
super.watch(tracker);
|
|
557
684
|
switch (this.status) {
|
|
558
685
|
case "resolved":
|
|
@@ -560,6 +687,7 @@ class ValueNode extends Node {
|
|
|
560
687
|
case "error":
|
|
561
688
|
throw this._error;
|
|
562
689
|
case "pending": {
|
|
690
|
+
this._markPendingRead();
|
|
563
691
|
throw new PendingError(this._scheduler.settled);
|
|
564
692
|
}
|
|
565
693
|
case "dirty": {
|
|
@@ -570,17 +698,18 @@ class ValueNode extends Node {
|
|
|
570
698
|
case "error":
|
|
571
699
|
throw this._error;
|
|
572
700
|
case "pending": {
|
|
701
|
+
this._markPendingRead();
|
|
573
702
|
throw new PendingError(this._scheduler.settled);
|
|
574
703
|
}
|
|
575
704
|
case "dirty": {
|
|
576
|
-
throw
|
|
705
|
+
throw this._internalError();
|
|
577
706
|
}
|
|
578
707
|
}
|
|
579
708
|
}
|
|
580
709
|
}
|
|
581
710
|
}
|
|
582
711
|
async pick() {
|
|
583
|
-
if (this.disposed) throw
|
|
712
|
+
if (this.disposed) throw this._disposedError();
|
|
584
713
|
switch (this.status) {
|
|
585
714
|
case "resolved":
|
|
586
715
|
return this._value;
|
|
@@ -588,7 +717,7 @@ class ValueNode extends Node {
|
|
|
588
717
|
await this._scheduler.settled;
|
|
589
718
|
if (this.status === "resolved") return this._value;
|
|
590
719
|
if (this.status === "error") throw this._error;
|
|
591
|
-
throw
|
|
720
|
+
throw this._internalError();
|
|
592
721
|
}
|
|
593
722
|
case "error":
|
|
594
723
|
throw this._error;
|
|
@@ -601,12 +730,12 @@ class ValueNode extends Node {
|
|
|
601
730
|
await this._scheduler.settled;
|
|
602
731
|
if (this.status === "resolved") return this._value;
|
|
603
732
|
if (this.status === "error") throw this._error;
|
|
604
|
-
throw
|
|
733
|
+
throw this._internalError();
|
|
605
734
|
}
|
|
606
735
|
case "error":
|
|
607
736
|
throw this._error;
|
|
608
737
|
case "dirty": {
|
|
609
|
-
throw
|
|
738
|
+
throw this._internalError();
|
|
610
739
|
}
|
|
611
740
|
}
|
|
612
741
|
}
|
|
@@ -619,7 +748,7 @@ class ValueNode extends Node {
|
|
|
619
748
|
this._scheduler.compute();
|
|
620
749
|
}
|
|
621
750
|
subscribe(onValue, onError, onPending) {
|
|
622
|
-
if (this.disposed) throw
|
|
751
|
+
if (this.disposed) throw this._disposedError();
|
|
623
752
|
const effect = new EffectNode((t) => this.get(t), onValue, onError, onPending);
|
|
624
753
|
return effect;
|
|
625
754
|
}
|
|
@@ -628,8 +757,17 @@ class ValueNode extends Node {
|
|
|
628
757
|
class ValueAsyncNode extends ValueNode {
|
|
629
758
|
_scheduler;
|
|
630
759
|
_compute;
|
|
631
|
-
|
|
632
|
-
|
|
760
|
+
/** Set by {@link ValueNode._markPendingRead} when a dependent registers while this node is mid-flight. */
|
|
761
|
+
_hasPendingReader = false;
|
|
762
|
+
/**
|
|
763
|
+
* True only while the in-flight computation was started by `set()`, which already ran its own
|
|
764
|
+
* eager `notifyDependents()` cascade before this resolution. `refresh()` and a recompute pulled
|
|
765
|
+
* lazily after an upstream `notify()` have no such pre-cascade — for them, `_onResolve`'s own
|
|
766
|
+
* `notifyDependents()` call is the only notification, so it must never be skipped (A2-PERF-4).
|
|
767
|
+
*/
|
|
768
|
+
_pendingFromSet = false;
|
|
769
|
+
constructor(promiseOrCompute, name) {
|
|
770
|
+
super(name);
|
|
633
771
|
if (typeof promiseOrCompute === "function") {
|
|
634
772
|
this._compute = () => promiseOrCompute(this, this._value);
|
|
635
773
|
} else {
|
|
@@ -644,10 +782,18 @@ class ValueAsyncNode extends ValueNode {
|
|
|
644
782
|
}
|
|
645
783
|
_onResolve(value) {
|
|
646
784
|
if (this.disposed) return;
|
|
785
|
+
const changed = value !== this._value;
|
|
786
|
+
const hadPendingReader = this._hasPendingReader;
|
|
787
|
+
const pendingFromSet = this._pendingFromSet;
|
|
788
|
+
this._hasPendingReader = false;
|
|
789
|
+
this._pendingFromSet = false;
|
|
647
790
|
this.status = "resolved";
|
|
648
791
|
this._value = value;
|
|
649
792
|
this._error = void 0;
|
|
650
|
-
this.notifyDependents();
|
|
793
|
+
if (changed || hadPendingReader || !pendingFromSet) this.notifyDependents();
|
|
794
|
+
}
|
|
795
|
+
_markPendingRead() {
|
|
796
|
+
this._hasPendingReader = true;
|
|
651
797
|
}
|
|
652
798
|
_onReject(error) {
|
|
653
799
|
if (this.disposed) return;
|
|
@@ -656,12 +802,13 @@ class ValueAsyncNode extends ValueNode {
|
|
|
656
802
|
this.notifyDependents();
|
|
657
803
|
}
|
|
658
804
|
set(promiseOrUpdater) {
|
|
659
|
-
if (this.disposed) throw
|
|
805
|
+
if (this.disposed) throw this._disposedError();
|
|
660
806
|
if (typeof promiseOrUpdater === "function") {
|
|
661
807
|
const updater = promiseOrUpdater;
|
|
662
808
|
switch (this.status) {
|
|
663
809
|
case "resolved": {
|
|
664
810
|
this.status = "pending";
|
|
811
|
+
this._pendingFromSet = true;
|
|
665
812
|
this._scheduler.overwrite(updater(this._value));
|
|
666
813
|
this.notifyDependents();
|
|
667
814
|
return;
|
|
@@ -670,6 +817,7 @@ class ValueAsyncNode extends ValueNode {
|
|
|
670
817
|
case "error":
|
|
671
818
|
case "dirty": {
|
|
672
819
|
this.status = "pending";
|
|
820
|
+
this._pendingFromSet = true;
|
|
673
821
|
this._scheduler.overwrite(updater(this._value));
|
|
674
822
|
this.notifyDependents();
|
|
675
823
|
return;
|
|
@@ -677,38 +825,39 @@ class ValueAsyncNode extends ValueNode {
|
|
|
677
825
|
}
|
|
678
826
|
} else {
|
|
679
827
|
this.status = "pending";
|
|
828
|
+
this._pendingFromSet = true;
|
|
680
829
|
const promise = promiseOrUpdater;
|
|
681
830
|
this._scheduler.overwrite(promise);
|
|
682
831
|
this.notifyDependents();
|
|
683
832
|
}
|
|
684
833
|
}
|
|
685
834
|
refresh() {
|
|
686
|
-
if (this.disposed) throw
|
|
835
|
+
if (this.disposed) throw this._disposedError();
|
|
687
836
|
this.execute();
|
|
688
837
|
}
|
|
689
838
|
}
|
|
690
839
|
|
|
691
|
-
function constantAsync(valueOrInitializer) {
|
|
692
|
-
return new ValueAsyncNode(valueOrInitializer);
|
|
840
|
+
function constantAsync(valueOrInitializer, options) {
|
|
841
|
+
return new ValueAsyncNode(valueOrInitializer, options?.name);
|
|
693
842
|
}
|
|
694
843
|
|
|
695
|
-
function derivationAsync(compute) {
|
|
696
|
-
return new ValueAsyncNode(compute);
|
|
844
|
+
function derivationAsync(compute, options) {
|
|
845
|
+
return new ValueAsyncNode(compute, options?.name);
|
|
697
846
|
}
|
|
698
847
|
|
|
699
|
-
function stateAsync(valueOrInitializer) {
|
|
700
|
-
return new ValueAsyncNode(valueOrInitializer);
|
|
848
|
+
function stateAsync(valueOrInitializer, options) {
|
|
849
|
+
return new ValueAsyncNode(valueOrInitializer, options?.name);
|
|
701
850
|
}
|
|
702
851
|
|
|
703
|
-
function writableDerivationAsync(compute) {
|
|
704
|
-
return new ValueAsyncNode(compute);
|
|
852
|
+
function writableDerivationAsync(compute, options) {
|
|
853
|
+
return new ValueAsyncNode(compute, options?.name);
|
|
705
854
|
}
|
|
706
855
|
|
|
707
856
|
class ValueSyncNode extends ValueNode {
|
|
708
857
|
_scheduler;
|
|
709
858
|
_compute;
|
|
710
|
-
constructor(valueOrCompute) {
|
|
711
|
-
super();
|
|
859
|
+
constructor(valueOrCompute, name) {
|
|
860
|
+
super(name);
|
|
712
861
|
if (typeof valueOrCompute === "function") {
|
|
713
862
|
this._compute = () => valueOrCompute(this, this._value);
|
|
714
863
|
} else {
|
|
@@ -734,7 +883,7 @@ class ValueSyncNode extends ValueNode {
|
|
|
734
883
|
this._error = error;
|
|
735
884
|
}
|
|
736
885
|
set(valueOrUpdater) {
|
|
737
|
-
if (this.disposed) throw
|
|
886
|
+
if (this.disposed) throw this._disposedError();
|
|
738
887
|
if (typeof valueOrUpdater === "function") {
|
|
739
888
|
const updater = valueOrUpdater;
|
|
740
889
|
switch (this.status) {
|
|
@@ -778,7 +927,7 @@ class ValueSyncNode extends ValueNode {
|
|
|
778
927
|
}
|
|
779
928
|
}
|
|
780
929
|
refresh() {
|
|
781
|
-
if (this.disposed) throw
|
|
930
|
+
if (this.disposed) throw this._disposedError();
|
|
782
931
|
const currentValue = this._value;
|
|
783
932
|
const currentStatus = this.status;
|
|
784
933
|
this.execute();
|
|
@@ -807,7 +956,7 @@ class ActionNode extends ValueSyncNode {
|
|
|
807
956
|
}
|
|
808
957
|
}
|
|
809
958
|
set(valueOrUpdater) {
|
|
810
|
-
if (this.disposed) throw
|
|
959
|
+
if (this.disposed) throw this._disposedError();
|
|
811
960
|
if (typeof valueOrUpdater === "function") {
|
|
812
961
|
const updater = valueOrUpdater;
|
|
813
962
|
const nextValue2 = updater(this._value);
|
|
@@ -822,7 +971,7 @@ class ActionNode extends ValueSyncNode {
|
|
|
822
971
|
super.set(nextValue);
|
|
823
972
|
}
|
|
824
973
|
subscribe(onValue, onError, onPending) {
|
|
825
|
-
if (this.disposed) throw
|
|
974
|
+
if (this.disposed) throw this._disposedError();
|
|
826
975
|
const slot = { queue: [] };
|
|
827
976
|
this._slots.add(slot);
|
|
828
977
|
const effect = new EffectNode(
|
|
@@ -841,11 +990,9 @@ class ActionNode extends ValueSyncNode {
|
|
|
841
990
|
onError,
|
|
842
991
|
onPending
|
|
843
992
|
);
|
|
844
|
-
|
|
845
|
-
effect.dispose = () => {
|
|
993
|
+
effect.onDispose(() => {
|
|
846
994
|
this._slots.delete(slot);
|
|
847
|
-
|
|
848
|
-
};
|
|
995
|
+
});
|
|
849
996
|
return effect;
|
|
850
997
|
}
|
|
851
998
|
dispose() {
|
|
@@ -856,8 +1003,8 @@ class ActionNode extends ValueSyncNode {
|
|
|
856
1003
|
|
|
857
1004
|
class ArrayNode extends ValueSyncNode {
|
|
858
1005
|
$lastAction;
|
|
859
|
-
constructor(value = []) {
|
|
860
|
-
super(value);
|
|
1006
|
+
constructor(value = [], name) {
|
|
1007
|
+
super(value, name);
|
|
861
1008
|
this.$lastAction = new ActionNode({
|
|
862
1009
|
type: "set",
|
|
863
1010
|
setItems: value,
|
|
@@ -873,11 +1020,11 @@ class ArrayNode extends ValueSyncNode {
|
|
|
873
1020
|
return [...values];
|
|
874
1021
|
}
|
|
875
1022
|
get length() {
|
|
876
|
-
if (this.disposed) throw
|
|
1023
|
+
if (this.disposed) throw this._disposedError();
|
|
877
1024
|
return this._value.length;
|
|
878
1025
|
}
|
|
879
1026
|
set(itemsOrUpdater) {
|
|
880
|
-
if (this.disposed) throw
|
|
1027
|
+
if (this.disposed) throw this._disposedError();
|
|
881
1028
|
const previousValue = this._value;
|
|
882
1029
|
super.set(itemsOrUpdater);
|
|
883
1030
|
this.$lastAction.set({
|
|
@@ -888,8 +1035,8 @@ class ArrayNode extends ValueSyncNode {
|
|
|
888
1035
|
return previousValue;
|
|
889
1036
|
}
|
|
890
1037
|
update(index, item) {
|
|
891
|
-
if (this.disposed) throw
|
|
892
|
-
if (index < 0 || index >= this._value.length) {
|
|
1038
|
+
if (this.disposed) throw this._disposedError();
|
|
1039
|
+
if (index < 0 || index >= this._value.length || !Number.isInteger(index)) {
|
|
893
1040
|
throw new Error("[PicoFlow] Index out of bounds");
|
|
894
1041
|
}
|
|
895
1042
|
const previousValue = this._value[index];
|
|
@@ -904,13 +1051,13 @@ class ArrayNode extends ValueSyncNode {
|
|
|
904
1051
|
return previousValue;
|
|
905
1052
|
}
|
|
906
1053
|
push(item) {
|
|
907
|
-
if (this.disposed) throw
|
|
1054
|
+
if (this.disposed) throw this._disposedError();
|
|
908
1055
|
this._value.push(item);
|
|
909
1056
|
this.notifyDependents();
|
|
910
1057
|
this.$lastAction.set({ type: "push", addedItem: item });
|
|
911
1058
|
}
|
|
912
1059
|
pop() {
|
|
913
|
-
if (this.disposed) throw
|
|
1060
|
+
if (this.disposed) throw this._disposedError();
|
|
914
1061
|
if (this._value.length === 0) return void 0;
|
|
915
1062
|
const item = this._value.pop();
|
|
916
1063
|
this.notifyDependents();
|
|
@@ -918,13 +1065,13 @@ class ArrayNode extends ValueSyncNode {
|
|
|
918
1065
|
return item;
|
|
919
1066
|
}
|
|
920
1067
|
unshift(item) {
|
|
921
|
-
if (this.disposed) throw
|
|
1068
|
+
if (this.disposed) throw this._disposedError();
|
|
922
1069
|
this._value.unshift(item);
|
|
923
1070
|
this.notifyDependents();
|
|
924
1071
|
this.$lastAction.set({ type: "unshift", addedItem: item });
|
|
925
1072
|
}
|
|
926
1073
|
shift() {
|
|
927
|
-
if (this.disposed) throw
|
|
1074
|
+
if (this.disposed) throw this._disposedError();
|
|
928
1075
|
if (this._value.length === 0) return void 0;
|
|
929
1076
|
const item = this._value.shift();
|
|
930
1077
|
this.notifyDependents();
|
|
@@ -932,7 +1079,7 @@ class ArrayNode extends ValueSyncNode {
|
|
|
932
1079
|
return item;
|
|
933
1080
|
}
|
|
934
1081
|
splice(start, deleteCount, ...newItems) {
|
|
935
|
-
if (this.disposed) throw
|
|
1082
|
+
if (this.disposed) throw this._disposedError();
|
|
936
1083
|
const items = this._value.splice(start, deleteCount, ...newItems);
|
|
937
1084
|
this.notifyDependents();
|
|
938
1085
|
this.$lastAction.set({
|
|
@@ -945,7 +1092,7 @@ class ArrayNode extends ValueSyncNode {
|
|
|
945
1092
|
return items;
|
|
946
1093
|
}
|
|
947
1094
|
clear() {
|
|
948
|
-
if (this.disposed) throw
|
|
1095
|
+
if (this.disposed) throw this._disposedError();
|
|
949
1096
|
const previousValue = [...this._value];
|
|
950
1097
|
this._value = [];
|
|
951
1098
|
this.notifyDependents();
|
|
@@ -961,14 +1108,14 @@ class ArrayNode extends ValueSyncNode {
|
|
|
961
1108
|
}
|
|
962
1109
|
}
|
|
963
1110
|
|
|
964
|
-
function array(initial) {
|
|
965
|
-
return new ArrayNode(initial);
|
|
1111
|
+
function array(initial, options) {
|
|
1112
|
+
return new ArrayNode(initial, options?.name);
|
|
966
1113
|
}
|
|
967
1114
|
|
|
968
1115
|
class MapNode extends ValueSyncNode {
|
|
969
1116
|
$lastAction;
|
|
970
|
-
constructor(value = /* @__PURE__ */ new Map()) {
|
|
971
|
-
super(value);
|
|
1117
|
+
constructor(value = /* @__PURE__ */ new Map(), name) {
|
|
1118
|
+
super(value, name);
|
|
972
1119
|
this.$lastAction = new ActionNode({
|
|
973
1120
|
type: "set",
|
|
974
1121
|
setMap: value,
|
|
@@ -976,7 +1123,7 @@ class MapNode extends ValueSyncNode {
|
|
|
976
1123
|
});
|
|
977
1124
|
}
|
|
978
1125
|
add(key, value) {
|
|
979
|
-
if (this.disposed) throw
|
|
1126
|
+
if (this.disposed) throw this._disposedError();
|
|
980
1127
|
if (this._value.has(key)) {
|
|
981
1128
|
throw new Error("[PicoFlow] Key already exists");
|
|
982
1129
|
}
|
|
@@ -985,7 +1132,7 @@ class MapNode extends ValueSyncNode {
|
|
|
985
1132
|
this.$lastAction.set({ type: "add", key, addedValue: value });
|
|
986
1133
|
}
|
|
987
1134
|
update(key, value) {
|
|
988
|
-
if (this.disposed) throw
|
|
1135
|
+
if (this.disposed) throw this._disposedError();
|
|
989
1136
|
if (!this._value.has(key)) throw new Error("[PicoFlow] Key does not exist");
|
|
990
1137
|
const previousValue = this._value.get(key);
|
|
991
1138
|
this._value.set(key, value);
|
|
@@ -999,7 +1146,7 @@ class MapNode extends ValueSyncNode {
|
|
|
999
1146
|
return previousValue;
|
|
1000
1147
|
}
|
|
1001
1148
|
delete(key) {
|
|
1002
|
-
if (this.disposed) throw
|
|
1149
|
+
if (this.disposed) throw this._disposedError();
|
|
1003
1150
|
if (!this._value.has(key)) throw new Error("[PicoFlow] Key does not exist");
|
|
1004
1151
|
const value = this._value.get(key);
|
|
1005
1152
|
this._value.delete(key);
|
|
@@ -1008,7 +1155,7 @@ class MapNode extends ValueSyncNode {
|
|
|
1008
1155
|
return value;
|
|
1009
1156
|
}
|
|
1010
1157
|
set(mapOrUpdater) {
|
|
1011
|
-
if (this.disposed) throw
|
|
1158
|
+
if (this.disposed) throw this._disposedError();
|
|
1012
1159
|
const previousValue = this._value;
|
|
1013
1160
|
super.set(mapOrUpdater);
|
|
1014
1161
|
this.$lastAction.set({
|
|
@@ -1019,7 +1166,7 @@ class MapNode extends ValueSyncNode {
|
|
|
1019
1166
|
return previousValue;
|
|
1020
1167
|
}
|
|
1021
1168
|
clear() {
|
|
1022
|
-
if (this.disposed) throw
|
|
1169
|
+
if (this.disposed) throw this._disposedError();
|
|
1023
1170
|
const previousValue = this._value;
|
|
1024
1171
|
this._value = /* @__PURE__ */ new Map();
|
|
1025
1172
|
this.notifyDependents();
|
|
@@ -1035,11 +1182,11 @@ class MapNode extends ValueSyncNode {
|
|
|
1035
1182
|
}
|
|
1036
1183
|
}
|
|
1037
1184
|
|
|
1038
|
-
function map(initial) {
|
|
1185
|
+
function map(initial, options) {
|
|
1039
1186
|
if (initial instanceof Map) {
|
|
1040
|
-
return new MapNode(initial);
|
|
1187
|
+
return new MapNode(initial, options?.name);
|
|
1041
1188
|
}
|
|
1042
|
-
return new MapNode(new Map(initial ? Object.entries(initial) : []));
|
|
1189
|
+
return new MapNode(new Map(initial ? Object.entries(initial) : []), options?.name);
|
|
1043
1190
|
}
|
|
1044
1191
|
|
|
1045
1192
|
function subscribe(data, onData, onError, onPending) {
|
|
@@ -1048,30 +1195,30 @@ function subscribe(data, onData, onError, onPending) {
|
|
|
1048
1195
|
|
|
1049
1196
|
class SignalNode extends Observable {
|
|
1050
1197
|
subscribe(onTrigger, onError, onPending) {
|
|
1051
|
-
if (this.disposed) throw
|
|
1198
|
+
if (this.disposed) throw this._disposedError();
|
|
1052
1199
|
const effect = new EffectNode((t) => this.watch(t), onTrigger, onError, onPending);
|
|
1053
1200
|
return effect;
|
|
1054
1201
|
}
|
|
1055
1202
|
}
|
|
1056
1203
|
|
|
1057
|
-
function signal() {
|
|
1058
|
-
return new SignalNode();
|
|
1204
|
+
function signal(options) {
|
|
1205
|
+
return new SignalNode(options?.name);
|
|
1059
1206
|
}
|
|
1060
1207
|
|
|
1061
|
-
function constant(initializer) {
|
|
1062
|
-
return new ValueSyncNode(initializer);
|
|
1208
|
+
function constant(initializer, options) {
|
|
1209
|
+
return new ValueSyncNode(initializer, options?.name);
|
|
1063
1210
|
}
|
|
1064
1211
|
|
|
1065
|
-
function derivation(compute) {
|
|
1066
|
-
return new ValueSyncNode(compute);
|
|
1212
|
+
function derivation(compute, options) {
|
|
1213
|
+
return new ValueSyncNode(compute, options?.name);
|
|
1067
1214
|
}
|
|
1068
1215
|
|
|
1069
|
-
function state(valueOrInitializer) {
|
|
1070
|
-
return new ValueSyncNode(valueOrInitializer);
|
|
1216
|
+
function state(valueOrInitializer, options) {
|
|
1217
|
+
return new ValueSyncNode(valueOrInitializer, options?.name);
|
|
1071
1218
|
}
|
|
1072
1219
|
|
|
1073
|
-
function writableDerivation(compute) {
|
|
1074
|
-
return new ValueSyncNode(compute);
|
|
1220
|
+
function writableDerivation(compute, options) {
|
|
1221
|
+
return new ValueSyncNode(compute, options?.name);
|
|
1075
1222
|
}
|
|
1076
1223
|
|
|
1077
1224
|
function describeFlowInput(value) {
|
|
@@ -1082,6 +1229,9 @@ function describeFlowInput(value) {
|
|
|
1082
1229
|
}
|
|
1083
1230
|
return typeof value;
|
|
1084
1231
|
}
|
|
1232
|
+
function isValueNode(flow) {
|
|
1233
|
+
return typeof flow === "object" && flow !== null && PICOFLOW_VALUE_NODE in flow;
|
|
1234
|
+
}
|
|
1085
1235
|
function fromNode(node, options) {
|
|
1086
1236
|
const [resource, { refetch }] = createResource(() => node.pick());
|
|
1087
1237
|
let fx;
|
|
@@ -1122,7 +1272,7 @@ function fromGetter(getter) {
|
|
|
1122
1272
|
return fromNode(derivation, { disposeNode: true });
|
|
1123
1273
|
}
|
|
1124
1274
|
function from(flow) {
|
|
1125
|
-
if (flow
|
|
1275
|
+
if (isValueNode(flow)) {
|
|
1126
1276
|
return fromNode(flow);
|
|
1127
1277
|
}
|
|
1128
1278
|
if (typeof flow === "function") {
|
|
@@ -1131,4 +1281,4 @@ function from(flow) {
|
|
|
1131
1281
|
throw new Error(`[PicoFlow] from(): expected a FlowValue or getter function, received ${describeFlowInput(flow)}`);
|
|
1132
1282
|
}
|
|
1133
1283
|
|
|
1134
|
-
export { array, constant, constantAsync, derivation, derivationAsync, from, isDisposable, map, signal, state, stateAsync, subscribe, writableDerivation, writableDerivationAsync };
|
|
1284
|
+
export { PicoFlowCycleError, PicoFlowDisposedError, PicoFlowError, PicoFlowInternalError, array, constant, constantAsync, derivation, derivationAsync, from, isDisposable, map, onFlushError, signal, state, stateAsync, subscribe, writableDerivation, writableDerivationAsync };
|