@mindees/core 0.3.0 → 0.5.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/dist/animation/animation.d.ts +69 -0
- package/dist/animation/animation.d.ts.map +1 -0
- package/dist/animation/animation.js +294 -0
- package/dist/animation/animation.js.map +1 -0
- package/dist/animation/easing.d.ts +25 -0
- package/dist/animation/easing.d.ts.map +1 -0
- package/dist/animation/easing.js +61 -0
- package/dist/animation/easing.js.map +1 -0
- package/dist/component/component.d.ts +26 -2
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/component.js +19 -1
- package/dist/component/component.js.map +1 -1
- package/dist/gesture/animated.d.ts +23 -0
- package/dist/gesture/animated.d.ts.map +1 -0
- package/dist/gesture/animated.js +53 -0
- package/dist/gesture/animated.js.map +1 -0
- package/dist/gesture/recognizers.d.ts +133 -0
- package/dist/gesture/recognizers.d.ts.map +1 -0
- package/dist/gesture/recognizers.js +507 -0
- package/dist/gesture/recognizers.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/reactive/reactive.d.ts +38 -21
- package/dist/reactive/reactive.d.ts.map +1 -1
- package/dist/reactive/reactive.js +88 -25
- package/dist/reactive/reactive.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts.map +1 -1
- package/dist/scheduler/scheduler.js +13 -0
- package/dist/scheduler/scheduler.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,23 +1,6 @@
|
|
|
1
|
+
import { Priority, Scheduler } from "../scheduler/scheduler.js";
|
|
2
|
+
|
|
1
3
|
//#region src/reactive/reactive.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* MindeesNative reactivity — fine-grained, glitch-free, lazy signals.
|
|
4
|
-
*
|
|
5
|
-
* Algorithm: push–pull with graph coloring (`CLEAN` / `CHECK` / `DIRTY`), in the
|
|
6
|
-
* lineage of SolidJS and the "reactively" library. A write *pushes* staleness
|
|
7
|
-
* markers through the observer graph; a read *pulls*, recomputing a node only
|
|
8
|
-
* when one of its sources actually changed. This guarantees:
|
|
9
|
-
*
|
|
10
|
-
* - **Glitch freedom** — no observer ever sees an inconsistent intermediate
|
|
11
|
-
* state (the classic diamond dependency recomputes its consumer exactly once).
|
|
12
|
-
* - **No redundant recomputation** — a node recomputes at most once per change,
|
|
13
|
-
* and an equal recomputation does not propagate to its observers.
|
|
14
|
-
* - **Deterministic, synchronous propagation** — effects run in a predictable
|
|
15
|
-
* order, batched to the end of the outermost write/batch.
|
|
16
|
-
* - **Complete disposal** — disposing an owner unlinks every subscription, so
|
|
17
|
-
* there are no leaked observers.
|
|
18
|
-
*
|
|
19
|
-
* @module
|
|
20
|
-
*/
|
|
21
4
|
/** @internal Phantom brand making {@link Owner} nominal — see below. */
|
|
22
5
|
declare const OWNER_BRAND: unique symbol;
|
|
23
6
|
/**
|
|
@@ -40,6 +23,12 @@ interface Owner {
|
|
|
40
23
|
}
|
|
41
24
|
/** Equality comparator used to decide whether a value actually changed. */
|
|
42
25
|
type EqualsFn<T> = (a: T, b: T) => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Inject (or clear) the {@link Scheduler} that `effect(fn, { priority: 'normal' })` defers through.
|
|
28
|
+
* With no scheduler (the default), `'normal'`-lane effects flush synchronously — so behavior is
|
|
29
|
+
* identical to today until a host wires one in. Pass `null` to detach.
|
|
30
|
+
*/
|
|
31
|
+
declare function setReactiveScheduler(scheduler: Scheduler | null): void;
|
|
43
32
|
/** A read accessor for a derived/reactive value. */
|
|
44
33
|
type Accessor<T> = () => T;
|
|
45
34
|
/** Options accepted by {@link signal}. */
|
|
@@ -112,7 +101,35 @@ declare const memo: typeof computed;
|
|
|
112
101
|
* return () => clearInterval(id) // cleanup
|
|
113
102
|
* })
|
|
114
103
|
*/
|
|
115
|
-
|
|
104
|
+
/** Options for {@link effect}. */
|
|
105
|
+
interface EffectOptions {
|
|
106
|
+
/**
|
|
107
|
+
* `'sync'` (default) flushes the effect inline on every change — the synchronous, glitch-free
|
|
108
|
+
* behavior. `'normal'` defers the effect through the injected {@link setReactiveScheduler scheduler}
|
|
109
|
+
* (interaction priority / deferred heavy work); with no scheduler it falls back to synchronous.
|
|
110
|
+
*/
|
|
111
|
+
priority?: Priority;
|
|
112
|
+
}
|
|
113
|
+
declare function effect(fn: () => void, options?: EffectOptions): () => void;
|
|
114
|
+
/**
|
|
115
|
+
* Apply the writes in `fn` as a low-priority **transition**: the writes take effect immediately
|
|
116
|
+
* (reads inside/after see the latest values), but the effects they invalidate are **deferred**
|
|
117
|
+
* through the injected {@link setReactiveScheduler scheduler} instead of running synchronously — so a
|
|
118
|
+
* heavy re-render triggered by, say, a keystroke doesn't block the interaction. With no scheduler
|
|
119
|
+
* injected, this is a plain synchronous batch (no deferral) — safe for SSR/tests.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* input.set(value) // urgent: the field updates now
|
|
123
|
+
* startTransition(() => query.set(value)) // the expensive results re-render can lag
|
|
124
|
+
*/
|
|
125
|
+
declare function startTransition(fn: () => void): void;
|
|
126
|
+
/**
|
|
127
|
+
* A **deferred view** of `source`: it tracks `source` on the scheduler's low-priority lane, so under
|
|
128
|
+
* sustained updates it lags behind the live value (the React `useDeferredValue` pattern — show stale
|
|
129
|
+
* results while the latest are computed). With no scheduler injected it mirrors `source`
|
|
130
|
+
* synchronously (no lag), so SSR/tests see the live value. Must be created inside an owner.
|
|
131
|
+
*/
|
|
132
|
+
declare function deferred<T>(source: Accessor<T>): Accessor<T>;
|
|
116
133
|
/**
|
|
117
134
|
* Batch multiple writes so dependent effects run once, after the batch. Reads
|
|
118
135
|
* inside a batch still observe the latest written values synchronously.
|
|
@@ -142,5 +159,5 @@ declare function getOwner(): Owner | null;
|
|
|
142
159
|
/** Run `fn` with `owner` as the active scope (e.g. to re-attach cleanups). */
|
|
143
160
|
declare function runWithOwner<T>(owner: Owner | null, fn: () => T): T;
|
|
144
161
|
//#endregion
|
|
145
|
-
export { Accessor, ComputedOptions, EqualsFn, Memo, Owner, Signal, SignalOptions, batch, computed, createRoot, effect, getOwner, memo, onCleanup, runWithOwner, signal, untrack };
|
|
162
|
+
export { Accessor, ComputedOptions, EffectOptions, EqualsFn, Memo, Owner, Signal, SignalOptions, batch, computed, createRoot, deferred, effect, getOwner, memo, onCleanup, runWithOwner, setReactiveScheduler, signal, startTransition, untrack };
|
|
146
163
|
//# sourceMappingURL=reactive.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactive.d.ts","names":[],"sources":["../../src/reactive/reactive.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"reactive.d.ts","names":[],"sources":["../../src/reactive/reactive.ts"],"mappings":";;;;cAwCc,WAAA;;AAgCuB;AAoDrC;;;;AAAgE;AA2XhE;;;;AAAiC;AAGjC;;UAlciB,KAAA;EAocE;EAAA,UAlcP,WAAW;AAAA;;KAcX,QAAA,OAAe,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC;;AAobhB;AAIrB;;;iBApYgB,oBAAA,CAAqB,SAA2B,EAAhB,SAAS;;KA2X7C,QAAA,YAAoB,CAAC;;UAGhB,aAAA;EAcN;EAZT,MAAA,GAAS,QAAQ,CAAC,CAAA;AAAA;;UAIH,MAAA;EAIJ;EAAA,IAFP,CAAA;EAEW;EAAf,GAAA,CAAI,KAAA,EAAO,CAAA,GAAI,CAAA;EAEG;EAAlB,MAAA,CAAO,EAAA,GAAK,IAAA,EAAM,CAAA,KAAM,CAAA,GAAI,CAAA;EAAJ;EAExB,IAAA,IAAQ,CAAA;AAAA;;UAIO,eAAA;EAJN;EAMT,MAAA,GAAS,QAAQ,CAAC,CAAA;AAAA;;UAIH,IAAA;EAJE;EAAA,IAMb,CAAA;EANJ;EAQA,IAAA,IAAQ,CAAC;AAAA;;AARU;AAIrB;;;;;;;iBAgBgB,MAAA,IAAU,KAAA,EAAO,CAAA,EAAG,OAAA,GAAU,aAAA,CAAc,CAAA,IAAK,MAAA,CAAO,CAAA;;AAZ7D;AAYX;;;;;;iBAiBgB,QAAA,IAAY,EAAA,QAAU,CAAA,EAAG,OAAA,GAAU,eAAA,CAAgB,CAAA,IAAK,IAAA,CAAK,CAAA;;cAYhE,IAAA,SAAI,QAAW;;;;;;;;;;;AA7B6C;AAiBzE;;;;;;;;;;;UAoCiB,aAAA;EApCW;;;;;EA0C1B,QAAA,GAAW,QAAQ;AAAA;AAAA,iBAGL,MAAA,CAAO,EAAA,cAAgB,OAAA,GAAU,aAAa;AAjC9D;;;;AAA4B;AAwB5B;;;;AAMqB;AAGrB;AAjCA,iBAmEgB,eAAA,CAAgB,EAAc;;;;;;;iBAe9B,QAAA,IAAY,MAAA,EAAQ,QAAA,CAAS,CAAA,IAAK,QAAA,CAAS,CAAA;AAf3D;;;;AAAA,iBAyBgB,KAAA,IAAS,EAAA,QAAU,CAAA,GAAI,CAAC;AAVxC;AAAA,iBAsBgB,OAAA,IAAW,EAAA,QAAU,CAAA,GAAI,CAAC;;;;;iBAc1B,SAAA,CAAU,EAAc;;;;;;;;;;AApCoB;AAU5D;iBA2CgB,UAAA,IAAc,EAAA,GAAK,OAAA,iBAAwB,CAAA,GAAI,CAAC;;iBAehD,QAAA,IAAY,KAAK;;iBAOjB,YAAA,IAAgB,KAAA,EAAO,KAAA,SAAc,EAAA,QAAU,CAAA,GAAI,CAAA"}
|
|
@@ -16,6 +16,26 @@ const effectQueue = [];
|
|
|
16
16
|
let flushing = false;
|
|
17
17
|
/** Safety valve against accidental infinite reactive loops. */
|
|
18
18
|
const MAX_FLUSH_ITERATIONS = 1e5;
|
|
19
|
+
/**
|
|
20
|
+
* The scheduler that `'normal'`-lane effects defer through. `null` by default — and while it is null
|
|
21
|
+
* EVERY effect (including `priority: 'normal'`) flushes synchronously, so the sync default is
|
|
22
|
+
* unchanged for all of SSR + tests until a host opts in. Set once at app bootstrap.
|
|
23
|
+
*/
|
|
24
|
+
let reactiveScheduler = null;
|
|
25
|
+
/** Monotonic counter for default `'normal'`-lane dedup keys. */
|
|
26
|
+
let effectKeySeq = 0;
|
|
27
|
+
/** >0 while inside a {@link startTransition}; effects staled during it defer (when a scheduler exists). */
|
|
28
|
+
let transitionDepth = 0;
|
|
29
|
+
/** Sync effects staled inside the current transition — treated as deferred for THIS drain only. */
|
|
30
|
+
const transitionTagged = /* @__PURE__ */ new Set();
|
|
31
|
+
/**
|
|
32
|
+
* Inject (or clear) the {@link Scheduler} that `effect(fn, { priority: 'normal' })` defers through.
|
|
33
|
+
* With no scheduler (the default), `'normal'`-lane effects flush synchronously — so behavior is
|
|
34
|
+
* identical to today until a host wires one in. Pass `null` to detach.
|
|
35
|
+
*/
|
|
36
|
+
function setReactiveScheduler(scheduler) {
|
|
37
|
+
reactiveScheduler = scheduler;
|
|
38
|
+
}
|
|
19
39
|
/** @internal Test-only handle to a node behind an accessor. Not public API. */
|
|
20
40
|
const NODE = Symbol("mindees.reactive.node");
|
|
21
41
|
var Computation = class {
|
|
@@ -29,6 +49,17 @@ var Computation = class {
|
|
|
29
49
|
equals;
|
|
30
50
|
isEffect;
|
|
31
51
|
/**
|
|
52
|
+
* Flush lane. `'sync'` (the default for every signal/computed and every plain `effect`) flushes
|
|
53
|
+
* inline exactly as before. Only `effect(fn, { priority: 'normal' })` sets `'normal'`, and even
|
|
54
|
+
* then it only defers when a scheduler is injected via {@link setReactiveScheduler} — otherwise it
|
|
55
|
+
* falls back to synchronous flush. This keeps the synchronous default provably untouched.
|
|
56
|
+
*/
|
|
57
|
+
lane = "sync";
|
|
58
|
+
/** A pending scheduled flush for a `'normal'`-lane effect (cancelled on disposal). */
|
|
59
|
+
pendingTask = null;
|
|
60
|
+
/** Scheduler dedup key for a `'normal'`-lane effect (so rapid re-stales coalesce). */
|
|
61
|
+
schedKey;
|
|
62
|
+
/**
|
|
32
63
|
* Whether {@link value} holds a real computed result yet. Derivations start
|
|
33
64
|
* uninitialized (their initial `value` is a placeholder); the first
|
|
34
65
|
* computation must NOT call `equals(oldValue, …)` against that placeholder —
|
|
@@ -80,7 +111,10 @@ var Computation = class {
|
|
|
80
111
|
if (this.state < state) {
|
|
81
112
|
const wasClean = this.state === CLEAN;
|
|
82
113
|
this.state = state;
|
|
83
|
-
if (this.isEffect && wasClean)
|
|
114
|
+
if (this.isEffect && wasClean) {
|
|
115
|
+
effectQueue.push(this);
|
|
116
|
+
if (transitionDepth > 0 && reactiveScheduler && this.lane === "sync") transitionTagged.add(this);
|
|
117
|
+
}
|
|
84
118
|
if (this.observers) for (const o of this.observers) o.markStale(CHECK);
|
|
85
119
|
}
|
|
86
120
|
}
|
|
@@ -208,6 +242,10 @@ function disposeComputation(node) {
|
|
|
208
242
|
unlinkSources(node);
|
|
209
243
|
node.observers = null;
|
|
210
244
|
node.state = DISPOSED;
|
|
245
|
+
if (node.pendingTask) {
|
|
246
|
+
node.pendingTask.cancel();
|
|
247
|
+
node.pendingTask = null;
|
|
248
|
+
}
|
|
211
249
|
}
|
|
212
250
|
}
|
|
213
251
|
function adopt(node) {
|
|
@@ -229,7 +267,18 @@ function flushEffects() {
|
|
|
229
267
|
}
|
|
230
268
|
const e = effectQueue[i];
|
|
231
269
|
i++;
|
|
232
|
-
if (e && e.state !== CLEAN && e.state !== DISPOSED)
|
|
270
|
+
if (e && e.state !== CLEAN && e.state !== DISPOSED) if (reactiveScheduler && (e.lane === "normal" || transitionTagged.has(e))) {
|
|
271
|
+
const node = e;
|
|
272
|
+
const sched = reactiveScheduler;
|
|
273
|
+
if (node.schedKey === void 0) node.schedKey = `mindees:effect:${effectKeySeq++}`;
|
|
274
|
+
node.pendingTask = sched.schedule(() => {
|
|
275
|
+
node.pendingTask = null;
|
|
276
|
+
if (node.state !== CLEAN && node.state !== DISPOSED) node.updateIfNecessary();
|
|
277
|
+
}, {
|
|
278
|
+
priority: "normal",
|
|
279
|
+
key: node.schedKey
|
|
280
|
+
});
|
|
281
|
+
} else try {
|
|
233
282
|
e.updateIfNecessary();
|
|
234
283
|
} catch (err) {
|
|
235
284
|
errors.push(err);
|
|
@@ -237,6 +286,7 @@ function flushEffects() {
|
|
|
237
286
|
}
|
|
238
287
|
} finally {
|
|
239
288
|
effectQueue.length = 0;
|
|
289
|
+
transitionTagged.clear();
|
|
240
290
|
flushing = false;
|
|
241
291
|
}
|
|
242
292
|
if (errors.length === 1) throw errors[0];
|
|
@@ -283,37 +333,50 @@ function computed(fn, options) {
|
|
|
283
333
|
}
|
|
284
334
|
/** Alias of {@link computed}. */
|
|
285
335
|
const memo = computed;
|
|
286
|
-
|
|
287
|
-
* Run a side effect that re-runs whenever its reactive dependencies change.
|
|
288
|
-
* Runs once immediately to establish dependencies.
|
|
289
|
-
*
|
|
290
|
-
* To clean up before each re-run and on disposal, either return a cleanup
|
|
291
|
-
* function from the effect, or call {@link onCleanup}. Any non-function return
|
|
292
|
-
* value is ignored (so expression-bodied effects like `() => list.push(x())`
|
|
293
|
-
* are fine).
|
|
294
|
-
*
|
|
295
|
-
* @returns A disposer that stops the effect and runs its cleanups.
|
|
296
|
-
*
|
|
297
|
-
* @example
|
|
298
|
-
* const stop = effect(() => console.log(count()))
|
|
299
|
-
* stop() // unsubscribe
|
|
300
|
-
*
|
|
301
|
-
* @example
|
|
302
|
-
* effect(() => {
|
|
303
|
-
* const id = setInterval(tick, 1000)
|
|
304
|
-
* return () => clearInterval(id) // cleanup
|
|
305
|
-
* })
|
|
306
|
-
*/
|
|
307
|
-
function effect(fn) {
|
|
336
|
+
function effect(fn, options) {
|
|
308
337
|
const node = new Computation(void 0, () => {
|
|
309
338
|
const result = fn();
|
|
310
339
|
if (typeof result === "function") onCleanup(result);
|
|
311
340
|
}, false, true);
|
|
341
|
+
if (options?.priority === "normal") {
|
|
342
|
+
node.lane = "normal";
|
|
343
|
+
node.schedKey = `mindees:effect:${effectKeySeq++}`;
|
|
344
|
+
}
|
|
312
345
|
adopt(node);
|
|
313
346
|
node.updateIfNecessary();
|
|
314
347
|
return () => disposeComputation(node);
|
|
315
348
|
}
|
|
316
349
|
/**
|
|
350
|
+
* Apply the writes in `fn` as a low-priority **transition**: the writes take effect immediately
|
|
351
|
+
* (reads inside/after see the latest values), but the effects they invalidate are **deferred**
|
|
352
|
+
* through the injected {@link setReactiveScheduler scheduler} instead of running synchronously — so a
|
|
353
|
+
* heavy re-render triggered by, say, a keystroke doesn't block the interaction. With no scheduler
|
|
354
|
+
* injected, this is a plain synchronous batch (no deferral) — safe for SSR/tests.
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* input.set(value) // urgent: the field updates now
|
|
358
|
+
* startTransition(() => query.set(value)) // the expensive results re-render can lag
|
|
359
|
+
*/
|
|
360
|
+
function startTransition(fn) {
|
|
361
|
+
transitionDepth++;
|
|
362
|
+
try {
|
|
363
|
+
batch(fn);
|
|
364
|
+
} finally {
|
|
365
|
+
transitionDepth--;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* A **deferred view** of `source`: it tracks `source` on the scheduler's low-priority lane, so under
|
|
370
|
+
* sustained updates it lags behind the live value (the React `useDeferredValue` pattern — show stale
|
|
371
|
+
* results while the latest are computed). With no scheduler injected it mirrors `source`
|
|
372
|
+
* synchronously (no lag), so SSR/tests see the live value. Must be created inside an owner.
|
|
373
|
+
*/
|
|
374
|
+
function deferred(source) {
|
|
375
|
+
const out = signal(source());
|
|
376
|
+
effect(() => out.set(source()), { priority: "normal" });
|
|
377
|
+
return () => out();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
317
380
|
* Batch multiple writes so dependent effects run once, after the batch. Reads
|
|
318
381
|
* inside a batch still observe the latest written values synchronously.
|
|
319
382
|
*/
|
|
@@ -388,6 +451,6 @@ function runWithOwner(owner, fn) {
|
|
|
388
451
|
}
|
|
389
452
|
}
|
|
390
453
|
//#endregion
|
|
391
|
-
export { batch, computed, createRoot, effect, getOwner, memo, onCleanup, runWithOwner, signal, untrack };
|
|
454
|
+
export { batch, computed, createRoot, deferred, effect, getOwner, memo, onCleanup, runWithOwner, setReactiveScheduler, signal, startTransition, untrack };
|
|
392
455
|
|
|
393
456
|
//# sourceMappingURL=reactive.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactive.js","names":[],"sources":["../../src/reactive/reactive.ts"],"sourcesContent":["/**\n * MindeesNative reactivity — fine-grained, glitch-free, lazy signals.\n *\n * Algorithm: push–pull with graph coloring (`CLEAN` / `CHECK` / `DIRTY`), in the\n * lineage of SolidJS and the \"reactively\" library. A write *pushes* staleness\n * markers through the observer graph; a read *pulls*, recomputing a node only\n * when one of its sources actually changed. This guarantees:\n *\n * - **Glitch freedom** — no observer ever sees an inconsistent intermediate\n * state (the classic diamond dependency recomputes its consumer exactly once).\n * - **No redundant recomputation** — a node recomputes at most once per change,\n * and an equal recomputation does not propagate to its observers.\n * - **Deterministic, synchronous propagation** — effects run in a predictable\n * order, batched to the end of the outermost write/batch.\n * - **Complete disposal** — disposing an owner unlinks every subscription, so\n * there are no leaked observers.\n *\n * @module\n */\n\n// ---------------------------------------------------------------------------\n// Ownership\n// ---------------------------------------------------------------------------\n\n/**\n * @internal Engine-side disposal scope. Computations and roots own the cleanups\n * and child computations created while they are the active owner; disposing an\n * owner tears all of them down. This concrete shape stays internal — see the\n * public {@link Owner} opaque handle.\n */\ninterface OwnerNode {\n /** Child computations created while this owner was active. */\n owned: AnyComputation[] | null\n /** Cleanup callbacks registered via {@link onCleanup}. */\n cleanups: Array<() => void> | null\n}\n\n/** @internal Phantom brand making {@link Owner} nominal — see below. */\ndeclare const OWNER_BRAND: unique symbol\n\n/**\n * An opaque disposal-scope handle. Obtain one with {@link getOwner} and re-enter\n * it with {@link runWithOwner}. Its internal shape — the reactive graph nodes it\n * owns — is intentionally not part of the public type surface, so the type-erased\n * {@link Computation} graph never leaks (no `any`, no internal mutable fields)\n * into consumers' types. Treat it as a token: hold it, pass it back; do not reach\n * inside it.\n *\n * It is **nominal** (branded with a private phantom symbol) so a structural\n * object literal — e.g. `{}` — is *not* assignable to it. Only a value handed out\n * by {@link getOwner} is a valid `Owner`; this prevents a fabricated owner from\n * flowing through {@link runWithOwner} into `onCleanup`/`adopt` and crashing on a\n * missing `owned`/`cleanups` field.\n */\nexport interface Owner {\n /** @internal Phantom brand — never present at runtime; blocks fabrication. */\n readonly [OWNER_BRAND]: never\n}\n\n// ---------------------------------------------------------------------------\n// Node state\n// ---------------------------------------------------------------------------\n\ntype State = 0 | 1 | 2 | 3\nconst CLEAN: State = 0\nconst CHECK: State = 1\nconst DIRTY: State = 2\nconst DISPOSED: State = 3\n\n/** Equality comparator used to decide whether a value actually changed. */\nexport type EqualsFn<T> = (a: T, b: T) => boolean\n\n// Default comparator: `Object.is`, matching context `select()` so the whole\n// package shares one semantics. Unlike `===`, this treats `NaN` as equal to\n// itself (so `set(NaN)` after `NaN` does not re-notify) and `-0`/`+0` as\n// different — the conventional choice for signal libraries.\nconst equalsDefault = (a: unknown, b: unknown): boolean => Object.is(a, b)\n\n/**\n * The reactive graph is intentionally type-erased: a node may observe, and be\n * observed by, computations of unrelated value types. Internal graph links use\n * this alias; the public API (`Signal<T>` / `Memo<T>` / `Accessor<T>`) stays\n * fully typed.\n */\n// biome-ignore lint/suspicious/noExplicitAny: type-erased reactive graph links\ntype AnyComputation = Computation<any>\n\n// ---------------------------------------------------------------------------\n// Globals (tracking + scheduling)\n// ---------------------------------------------------------------------------\n\n/** The computation currently executing, used for automatic dependency tracking. */\nlet currentObserver: AnyComputation | null = null\n/** The active disposal scope for onCleanup / child registration. */\nlet currentOwner: OwnerNode | null = null\n/** Outstanding `batch()` nesting depth; effects flush when this returns to 0. */\nlet batchDepth = 0\n/** Effects marked stale and awaiting a flush. */\nconst effectQueue: AnyComputation[] = []\n/** Guard against re-entrant flushes. */\nlet flushing = false\n/** Safety valve against accidental infinite reactive loops. */\nconst MAX_FLUSH_ITERATIONS = 100_000\n\n/** @internal Test-only handle to a node behind an accessor. Not public API. */\nexport const NODE: unique symbol = Symbol('mindees.reactive.node')\n\ninterface WithNode<T> {\n [NODE]: Computation<T>\n}\n\n// ---------------------------------------------------------------------------\n// Computation: the unit of reactivity (signal, computed, or effect)\n// ---------------------------------------------------------------------------\n\nclass Computation<T> implements OwnerNode {\n value: T\n fn: (() => T) | null\n state: State\n sources: AnyComputation[] | null = null\n observers: AnyComputation[] | null = null\n owned: AnyComputation[] | null = null\n cleanups: Array<() => void> | null = null\n equals: EqualsFn<T> | false\n readonly isEffect: boolean\n /**\n * Whether {@link value} holds a real computed result yet. Derivations start\n * uninitialized (their initial `value` is a placeholder); the first\n * computation must NOT call `equals(oldValue, …)` against that placeholder —\n * a custom comparator would receive `undefined` and could throw.\n */\n private initialized: boolean\n /**\n * True only while this node's own {@link update} is on the stack. Lets\n * {@link markStale} recognize a *self-write* — the body writing a signal the\n * node observes — instead of dropping the mark (the node is already DIRTY).\n */\n private running = false\n /**\n * Set by {@link markStale} when a self-write occurs mid-update. {@link update}'s\n * loop recomputes once more so the node converges on the value it just produced,\n * honoring the contract that a computation reflects its dependencies' latest\n * values. Reset at the start of every pass.\n */\n private restaleRequested = false\n\n constructor(value: T, fn: (() => T) | null, equals: EqualsFn<T> | false, isEffect: boolean) {\n this.value = value\n this.fn = fn\n this.equals = equals\n this.isEffect = isEffect\n // Signals (no fn) start CLEAN and already hold a real value; derivations\n // start DIRTY (compute lazily) and uninitialized.\n this.state = fn ? DIRTY : CLEAN\n this.initialized = fn === null\n }\n\n /** Read the current value, tracking a dependency if a computation is running. */\n read(): T {\n if (this.state === DISPOSED) return this.value\n if (currentObserver) link(currentObserver, this)\n if (this.fn) this.updateIfNecessary()\n return this.value\n }\n\n /** Write a new value (signals only); pushes staleness to observers. */\n write(value: T): T {\n if (this.equals !== false && this.equals(this.value, value)) return this.value\n this.value = value\n if (this.observers) {\n for (const o of this.observers) o.markStale(DIRTY)\n }\n if (batchDepth === 0) flushEffects()\n return value\n }\n\n /** Color this node (and, transitively, its observers) as stale. */\n markStale(state: State): void {\n // Self-write: this node is being marked stale while its own update() is\n // running (its body just wrote a signal it observes). It is already DIRTY, so\n // the gate below would silently drop the mark and the change would be lost.\n // Instead request one more recompute pass (handled by update()'s loop); don't\n // re-propagate here — the re-run will, once it produces a new value.\n if (this.running) {\n this.restaleRequested = true\n return\n }\n if (this.state < state) {\n const wasClean = this.state === CLEAN\n this.state = state\n if (this.isEffect && wasClean) effectQueue.push(this)\n if (this.observers) {\n for (const o of this.observers) o.markStale(CHECK)\n }\n }\n }\n\n /** Bring this node up to date, recomputing only if a source truly changed. */\n updateIfNecessary(): void {\n if (this.state === CLEAN || this.state === DISPOSED) return\n if (this.state === CHECK && this.sources) {\n for (const src of this.sources) {\n src.updateIfNecessary()\n if (this.state === DIRTY) break\n }\n }\n try {\n if (this.state === DIRTY) this.update()\n } finally {\n // Always settle to CLEAN (unless disposed) even if update() — the body or a\n // child cleanup — threw. Otherwise the node would stay DIRTY forever and\n // markStale's wasClean gate would never re-queue it (a permanent zombie).\n // Sources read before the throw are re-linked, so a later change recovers it.\n if (this.state !== DISPOSED) this.state = CLEAN\n }\n }\n\n /**\n * Recompute the derivation, re-tracking dependencies and notifying observers.\n *\n * Runs in a bounded loop. If the body writes a signal it itself observes (a\n * self-write), {@link markStale} sets {@link restaleRequested} rather than the\n * mark being lost, and we recompute again so the node converges on the value it\n * just produced. The loop is capped by {@link MAX_FLUSH_ITERATIONS} so a\n * non-terminating self-writer (e.g. `effect(() => a.set(a() + 1))`) throws\n * instead of hanging. A prior-run cleanup that throws during teardown must not\n * abort the re-track/recompute (that would strand the node's children and\n * dynamic deps); its error is captured and rethrown only after the node has\n * rebuilt a consistent graph.\n */\n private update(): void {\n const oldValue = this.value\n let cleanupError: unknown\n let hasCleanupError = false\n let iterations = 0\n this.running = true\n try {\n do {\n this.restaleRequested = false\n try {\n disposeChildren(this)\n } catch (err) {\n if (!hasCleanupError) {\n cleanupError = err\n hasCleanupError = true\n }\n }\n unlinkSources(this)\n\n const prevObserver = currentObserver\n const prevOwner = currentOwner\n currentObserver = this\n currentOwner = this\n try {\n // biome-ignore lint/style/noNonNullAssertion: update() only runs for derivations (fn != null).\n this.value = this.fn!()\n } finally {\n currentObserver = prevObserver\n currentOwner = prevOwner\n }\n\n if (++iterations > MAX_FLUSH_ITERATIONS) {\n this.restaleRequested = false\n throw new Error(\n 'MindeesNative: potential infinite reactive loop detected — a computation keeps writing a signal it reads.',\n )\n }\n } while (this.restaleRequested)\n } finally {\n this.running = false\n }\n\n // On the first computation there is no prior value to compare against, so\n // the result is always \"changed\". Afterwards, apply the equality check\n // (unless equals is false, meaning \"always changed\").\n const wasInitialized = this.initialized\n this.initialized = true\n const changed = !wasInitialized || this.equals === false || !this.equals(oldValue, this.value)\n if (changed && this.observers) {\n // Observers are already CHECK from the original push; promote them to DIRTY\n // so the in-flight pull recomputes them.\n for (const o of this.observers) {\n o.state = DIRTY\n }\n }\n\n // The graph is consistent again; now surface any error a previous-run cleanup\n // threw during teardown (the body still re-ran, so children/deps are rebuilt).\n if (hasCleanupError) throw cleanupError\n }\n}\n\n// ---------------------------------------------------------------------------\n// Graph maintenance\n// ---------------------------------------------------------------------------\n\nfunction link(observer: AnyComputation, source: AnyComputation): void {\n // Never subscribe a disposed observer — e.g. a node that disposed itself\n // mid-run and then read another signal. The subscription would leak: nothing\n // tears down a DISPOSED node again.\n if (observer.state === DISPOSED) return\n if (observer.sources === null) observer.sources = []\n const sources = observer.sources\n if (!sources.includes(source)) {\n sources.push(source)\n if (source.observers === null) source.observers = []\n source.observers.push(observer)\n }\n}\n\nfunction unlinkSources(node: AnyComputation): void {\n if (!node.sources) return\n for (const src of node.sources) {\n const obs = src.observers\n if (!obs) continue\n const idx = obs.indexOf(node)\n if (idx >= 0) {\n const last = obs.pop()\n if (last && idx < obs.length) obs[idx] = last\n }\n }\n node.sources = null\n}\n\nfunction disposeChildren(owner: OwnerNode): void {\n // Dispose every owned child AND run every cleanup even if some throw, so a\n // single faulty child/cleanup can't strand the rest or leak observers. `owned`\n // is nulled up front so a throw can't leave a half-disposed array behind.\n // Failures are collected and surfaced together afterward.\n const errors: unknown[] = []\n if (owner.owned) {\n const owned = owner.owned\n owner.owned = null\n for (const child of owned) {\n try {\n disposeComputation(child)\n } catch (err) {\n errors.push(err)\n }\n }\n }\n if (owner.cleanups) {\n const cleanups = owner.cleanups\n owner.cleanups = null\n for (const c of cleanups) {\n try {\n c()\n } catch (err) {\n errors.push(err)\n }\n }\n }\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) throw new AggregateError(errors, 'disposal threw')\n}\n\nfunction disposeComputation(node: AnyComputation): void {\n if (node.state === DISPOSED) return\n // If a node disposes itself mid-run, stop tracking and adopting onto it for the\n // rest of the (now-aborted) body: a later tracked read would otherwise\n // re-subscribe this DISPOSED node, and onCleanup/adopt would register work on a\n // dead scope — none of which is ever torn down again (a leak). Clearing the\n // globals makes read()/onCleanup/adopt no-op for the remainder of the body;\n // update()'s finally restores the previous owner afterward.\n if (node === currentObserver) currentObserver = null\n if (node === currentOwner) currentOwner = null\n try {\n disposeChildren(node)\n } finally {\n // Always fully unlink + mark disposed, even if a descendant cleanup threw,\n // so this node never leaks as a subscribed zombie. The error (if any) still\n // propagates to the caller, which aggregates it across siblings.\n unlinkSources(node)\n node.observers = null\n node.state = DISPOSED\n }\n}\n\nfunction adopt(node: AnyComputation): void {\n if (!currentOwner) return\n if (currentOwner.owned === null) currentOwner.owned = []\n currentOwner.owned.push(node)\n}\n\n// ---------------------------------------------------------------------------\n// Effect scheduling\n// ---------------------------------------------------------------------------\n\nfunction flushEffects(): void {\n if (flushing) return\n flushing = true\n // Isolate each effect: one throwing effect must not abort the flush or strand\n // the effects queued after it (each effect also self-recovers to CLEAN via\n // updateIfNecessary's finally). Errors are collected and surfaced after drain.\n const errors: unknown[] = []\n try {\n let i = 0\n let iterations = 0\n while (i < effectQueue.length) {\n if (++iterations > MAX_FLUSH_ITERATIONS) {\n effectQueue.length = 0\n throw new Error(\n 'MindeesNative: potential infinite reactive loop detected while flushing effects.',\n )\n }\n const e = effectQueue[i]\n i++\n if (e && e.state !== CLEAN && e.state !== DISPOSED) {\n try {\n e.updateIfNecessary()\n } catch (err) {\n errors.push(err)\n }\n }\n }\n } finally {\n effectQueue.length = 0\n flushing = false\n }\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) throw new AggregateError(errors, 'effect(s) threw during flush')\n}\n\nfunction attachNode<T, A extends object>(accessor: A, node: Computation<T>): A {\n ;(accessor as A & WithNode<T>)[NODE] = node\n return accessor\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** A read accessor for a derived/reactive value. */\nexport type Accessor<T> = () => T\n\n/** Options accepted by {@link signal}. */\nexport interface SignalOptions<T> {\n /** Custom equality. `false` means \"always notify\" (every write propagates). */\n equals?: EqualsFn<T> | false\n}\n\n/** A writable reactive value: call to read; `.set`/`.update` to write. */\nexport interface Signal<T> {\n /** Read the value (tracks a dependency inside a computation). */\n (): T\n /** Replace the value; returns the new value. */\n set(value: T): T\n /** Update from the previous value; returns the new value. */\n update(fn: (prev: T) => T): T\n /** Read without tracking a dependency. */\n peek(): T\n}\n\n/** Options accepted by {@link computed}. */\nexport interface ComputedOptions<T> {\n /** Custom equality used to decide whether downstream observers re-run. */\n equals?: EqualsFn<T> | false\n}\n\n/** A memoized derived value: call to read; `.peek` to read without tracking. */\nexport interface Memo<T> {\n /** Read the (lazily recomputed) value, tracking a dependency. */\n (): T\n /** Read without tracking a dependency. */\n peek(): T\n}\n\n/**\n * Create a writable reactive value.\n *\n * @example\n * const count = signal(0)\n * count() // read → 0\n * count.set(1) // write\n * count.update(n => n + 1)\n */\nexport function signal<T>(value: T, options?: SignalOptions<T>): Signal<T> {\n const node = new Computation<T>(value, null, options?.equals ?? equalsDefault, false)\n const accessor = (() => node.read()) as Signal<T>\n accessor.set = (v: T) => node.write(v)\n accessor.update = (fn: (prev: T) => T) => node.write(fn(node.value))\n accessor.peek = () => node.value\n return attachNode(accessor, node)\n}\n\n/**\n * Create a memoized derived value. The function re-runs only when one of the\n * reactive values it reads has actually changed, and only when the result is\n * observed (lazy).\n *\n * @example\n * const doubled = computed(() => count() * 2)\n */\nexport function computed<T>(fn: () => T, options?: ComputedOptions<T>): Memo<T> {\n const node = new Computation<T>(undefined as T, fn, options?.equals ?? equalsDefault, false)\n adopt(node)\n const accessor = (() => node.read()) as Memo<T>\n accessor.peek = () => {\n node.updateIfNecessary()\n return node.value\n }\n return attachNode(accessor, node)\n}\n\n/** Alias of {@link computed}. */\nexport const memo = computed\n\n/**\n * Run a side effect that re-runs whenever its reactive dependencies change.\n * Runs once immediately to establish dependencies.\n *\n * To clean up before each re-run and on disposal, either return a cleanup\n * function from the effect, or call {@link onCleanup}. Any non-function return\n * value is ignored (so expression-bodied effects like `() => list.push(x())`\n * are fine).\n *\n * @returns A disposer that stops the effect and runs its cleanups.\n *\n * @example\n * const stop = effect(() => console.log(count()))\n * stop() // unsubscribe\n *\n * @example\n * effect(() => {\n * const id = setInterval(tick, 1000)\n * return () => clearInterval(id) // cleanup\n * })\n */\nexport function effect(fn: () => void): () => void {\n const node = new Computation<void>(\n undefined,\n () => {\n const result: unknown = (fn as () => unknown)()\n if (typeof result === 'function') onCleanup(result as () => void)\n },\n false,\n true,\n )\n adopt(node)\n node.updateIfNecessary()\n return () => disposeComputation(node)\n}\n\n/**\n * Batch multiple writes so dependent effects run once, after the batch. Reads\n * inside a batch still observe the latest written values synchronously.\n */\nexport function batch<T>(fn: () => T): T {\n if (batchDepth > 0) return fn()\n batchDepth++\n try {\n return fn()\n } finally {\n batchDepth--\n flushEffects()\n }\n}\n\n/** Read reactive values without subscribing the current computation to them. */\nexport function untrack<T>(fn: () => T): T {\n const prev = currentObserver\n currentObserver = null\n try {\n return fn()\n } finally {\n currentObserver = prev\n }\n}\n\n/**\n * Register a cleanup to run before the owning computation re-runs and when it\n * is disposed. No-op outside a reactive scope.\n */\nexport function onCleanup(fn: () => void): void {\n if (!currentOwner) return\n if (currentOwner.cleanups === null) currentOwner.cleanups = []\n currentOwner.cleanups.push(fn)\n}\n\n/**\n * Create a non-tracked root scope that owns everything created within it. The\n * scope lives until the provided `dispose` function is called.\n *\n * @example\n * const dispose = createRoot((dispose) => {\n * effect(() => console.log(count()))\n * return dispose\n * })\n * dispose() // tear down the effect\n */\nexport function createRoot<T>(fn: (dispose: () => void) => T): T {\n const root: OwnerNode = { owned: null, cleanups: null }\n const prevObserver = currentObserver\n const prevOwner = currentOwner\n currentObserver = null\n currentOwner = root\n try {\n return fn(() => disposeChildren(root))\n } finally {\n currentObserver = prevObserver\n currentOwner = prevOwner\n }\n}\n\n/** The current owner scope, or `null` outside any reactive scope. */\nexport function getOwner(): Owner | null {\n // OwnerNode → the nominal public Owner. The brand is phantom (never present at\n // runtime), so the only honest way to produce an Owner is through this cast.\n return currentOwner as unknown as Owner | null\n}\n\n/** Run `fn` with `owner` as the active scope (e.g. to re-attach cleanups). */\nexport function runWithOwner<T>(owner: Owner | null, fn: () => T): T {\n const prev = currentOwner\n // `owner` is an opaque public handle; internally it is always an OwnerNode we\n // handed out via getOwner(). This is the one trusted boundary cast.\n currentOwner = owner as unknown as OwnerNode | null\n try {\n return fn()\n } finally {\n currentOwner = prev\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal test helpers (exported from this module only — NOT from the package\n// public entry point). Used to assert white-box invariants like leak-freedom.\n// ---------------------------------------------------------------------------\n\n/** @internal Number of live observers subscribed to the node behind `accessor`. */\nexport function _observerCount(accessor: object): number {\n const node = (accessor as Partial<WithNode<unknown>>)[NODE]\n return node?.observers?.length ?? 0\n}\n\n/** @internal Number of sources the node behind `accessor` depends on. */\nexport function _sourceCount(accessor: object): number {\n const node = (accessor as Partial<WithNode<unknown>>)[NODE]\n return node?.sources?.length ?? 0\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reactive.js","names":[],"sources":["../../src/reactive/reactive.ts"],"sourcesContent":["/**\n * MindeesNative reactivity — fine-grained, glitch-free, lazy signals.\n *\n * Algorithm: push–pull with graph coloring (`CLEAN` / `CHECK` / `DIRTY`), in the\n * lineage of SolidJS and the \"reactively\" library. A write *pushes* staleness\n * markers through the observer graph; a read *pulls*, recomputing a node only\n * when one of its sources actually changed. This guarantees:\n *\n * - **Glitch freedom** — no observer ever sees an inconsistent intermediate\n * state (the classic diamond dependency recomputes its consumer exactly once).\n * - **No redundant recomputation** — a node recomputes at most once per change,\n * and an equal recomputation does not propagate to its observers.\n * - **Deterministic, synchronous propagation** — effects run in a predictable\n * order, batched to the end of the outermost write/batch.\n * - **Complete disposal** — disposing an owner unlinks every subscription, so\n * there are no leaked observers.\n *\n * @module\n */\n\nimport type { Priority, ScheduledTask, Scheduler } from '../scheduler'\n\n// ---------------------------------------------------------------------------\n// Ownership\n// ---------------------------------------------------------------------------\n\n/**\n * @internal Engine-side disposal scope. Computations and roots own the cleanups\n * and child computations created while they are the active owner; disposing an\n * owner tears all of them down. This concrete shape stays internal — see the\n * public {@link Owner} opaque handle.\n */\ninterface OwnerNode {\n /** Child computations created while this owner was active. */\n owned: AnyComputation[] | null\n /** Cleanup callbacks registered via {@link onCleanup}. */\n cleanups: Array<() => void> | null\n}\n\n/** @internal Phantom brand making {@link Owner} nominal — see below. */\ndeclare const OWNER_BRAND: unique symbol\n\n/**\n * An opaque disposal-scope handle. Obtain one with {@link getOwner} and re-enter\n * it with {@link runWithOwner}. Its internal shape — the reactive graph nodes it\n * owns — is intentionally not part of the public type surface, so the type-erased\n * {@link Computation} graph never leaks (no `any`, no internal mutable fields)\n * into consumers' types. Treat it as a token: hold it, pass it back; do not reach\n * inside it.\n *\n * It is **nominal** (branded with a private phantom symbol) so a structural\n * object literal — e.g. `{}` — is *not* assignable to it. Only a value handed out\n * by {@link getOwner} is a valid `Owner`; this prevents a fabricated owner from\n * flowing through {@link runWithOwner} into `onCleanup`/`adopt` and crashing on a\n * missing `owned`/`cleanups` field.\n */\nexport interface Owner {\n /** @internal Phantom brand — never present at runtime; blocks fabrication. */\n readonly [OWNER_BRAND]: never\n}\n\n// ---------------------------------------------------------------------------\n// Node state\n// ---------------------------------------------------------------------------\n\ntype State = 0 | 1 | 2 | 3\nconst CLEAN: State = 0\nconst CHECK: State = 1\nconst DIRTY: State = 2\nconst DISPOSED: State = 3\n\n/** Equality comparator used to decide whether a value actually changed. */\nexport type EqualsFn<T> = (a: T, b: T) => boolean\n\n// Default comparator: `Object.is`, matching context `select()` so the whole\n// package shares one semantics. Unlike `===`, this treats `NaN` as equal to\n// itself (so `set(NaN)` after `NaN` does not re-notify) and `-0`/`+0` as\n// different — the conventional choice for signal libraries.\nconst equalsDefault = (a: unknown, b: unknown): boolean => Object.is(a, b)\n\n/**\n * The reactive graph is intentionally type-erased: a node may observe, and be\n * observed by, computations of unrelated value types. Internal graph links use\n * this alias; the public API (`Signal<T>` / `Memo<T>` / `Accessor<T>`) stays\n * fully typed.\n */\n// biome-ignore lint/suspicious/noExplicitAny: type-erased reactive graph links\ntype AnyComputation = Computation<any>\n\n// ---------------------------------------------------------------------------\n// Globals (tracking + scheduling)\n// ---------------------------------------------------------------------------\n\n/** The computation currently executing, used for automatic dependency tracking. */\nlet currentObserver: AnyComputation | null = null\n/** The active disposal scope for onCleanup / child registration. */\nlet currentOwner: OwnerNode | null = null\n/** Outstanding `batch()` nesting depth; effects flush when this returns to 0. */\nlet batchDepth = 0\n/** Effects marked stale and awaiting a flush. */\nconst effectQueue: AnyComputation[] = []\n/** Guard against re-entrant flushes. */\nlet flushing = false\n/** Safety valve against accidental infinite reactive loops. */\nconst MAX_FLUSH_ITERATIONS = 100_000\n\n/**\n * The scheduler that `'normal'`-lane effects defer through. `null` by default — and while it is null\n * EVERY effect (including `priority: 'normal'`) flushes synchronously, so the sync default is\n * unchanged for all of SSR + tests until a host opts in. Set once at app bootstrap.\n */\nlet reactiveScheduler: Scheduler | null = null\n/** Monotonic counter for default `'normal'`-lane dedup keys. */\nlet effectKeySeq = 0\n/** >0 while inside a {@link startTransition}; effects staled during it defer (when a scheduler exists). */\nlet transitionDepth = 0\n/** Sync effects staled inside the current transition — treated as deferred for THIS drain only. */\nconst transitionTagged = new Set<AnyComputation>()\n\n/**\n * Inject (or clear) the {@link Scheduler} that `effect(fn, { priority: 'normal' })` defers through.\n * With no scheduler (the default), `'normal'`-lane effects flush synchronously — so behavior is\n * identical to today until a host wires one in. Pass `null` to detach.\n */\nexport function setReactiveScheduler(scheduler: Scheduler | null): void {\n reactiveScheduler = scheduler\n}\n\n/** @internal Test-only handle to a node behind an accessor. Not public API. */\nexport const NODE: unique symbol = Symbol('mindees.reactive.node')\n\ninterface WithNode<T> {\n [NODE]: Computation<T>\n}\n\n// ---------------------------------------------------------------------------\n// Computation: the unit of reactivity (signal, computed, or effect)\n// ---------------------------------------------------------------------------\n\nclass Computation<T> implements OwnerNode {\n value: T\n fn: (() => T) | null\n state: State\n sources: AnyComputation[] | null = null\n observers: AnyComputation[] | null = null\n owned: AnyComputation[] | null = null\n cleanups: Array<() => void> | null = null\n equals: EqualsFn<T> | false\n readonly isEffect: boolean\n /**\n * Flush lane. `'sync'` (the default for every signal/computed and every plain `effect`) flushes\n * inline exactly as before. Only `effect(fn, { priority: 'normal' })` sets `'normal'`, and even\n * then it only defers when a scheduler is injected via {@link setReactiveScheduler} — otherwise it\n * falls back to synchronous flush. This keeps the synchronous default provably untouched.\n */\n lane: Priority = 'sync'\n /** A pending scheduled flush for a `'normal'`-lane effect (cancelled on disposal). */\n pendingTask: ScheduledTask | null = null\n /** Scheduler dedup key for a `'normal'`-lane effect (so rapid re-stales coalesce). */\n schedKey: string | undefined\n /**\n * Whether {@link value} holds a real computed result yet. Derivations start\n * uninitialized (their initial `value` is a placeholder); the first\n * computation must NOT call `equals(oldValue, …)` against that placeholder —\n * a custom comparator would receive `undefined` and could throw.\n */\n private initialized: boolean\n /**\n * True only while this node's own {@link update} is on the stack. Lets\n * {@link markStale} recognize a *self-write* — the body writing a signal the\n * node observes — instead of dropping the mark (the node is already DIRTY).\n */\n private running = false\n /**\n * Set by {@link markStale} when a self-write occurs mid-update. {@link update}'s\n * loop recomputes once more so the node converges on the value it just produced,\n * honoring the contract that a computation reflects its dependencies' latest\n * values. Reset at the start of every pass.\n */\n private restaleRequested = false\n\n constructor(value: T, fn: (() => T) | null, equals: EqualsFn<T> | false, isEffect: boolean) {\n this.value = value\n this.fn = fn\n this.equals = equals\n this.isEffect = isEffect\n // Signals (no fn) start CLEAN and already hold a real value; derivations\n // start DIRTY (compute lazily) and uninitialized.\n this.state = fn ? DIRTY : CLEAN\n this.initialized = fn === null\n }\n\n /** Read the current value, tracking a dependency if a computation is running. */\n read(): T {\n if (this.state === DISPOSED) return this.value\n if (currentObserver) link(currentObserver, this)\n if (this.fn) this.updateIfNecessary()\n return this.value\n }\n\n /** Write a new value (signals only); pushes staleness to observers. */\n write(value: T): T {\n if (this.equals !== false && this.equals(this.value, value)) return this.value\n this.value = value\n if (this.observers) {\n for (const o of this.observers) o.markStale(DIRTY)\n }\n if (batchDepth === 0) flushEffects()\n return value\n }\n\n /** Color this node (and, transitively, its observers) as stale. */\n markStale(state: State): void {\n // Self-write: this node is being marked stale while its own update() is\n // running (its body just wrote a signal it observes). It is already DIRTY, so\n // the gate below would silently drop the mark and the change would be lost.\n // Instead request one more recompute pass (handled by update()'s loop); don't\n // re-propagate here — the re-run will, once it produces a new value.\n if (this.running) {\n this.restaleRequested = true\n return\n }\n if (this.state < state) {\n const wasClean = this.state === CLEAN\n this.state = state\n if (this.isEffect && wasClean) {\n effectQueue.push(this)\n // Inside a startTransition (with a scheduler), tag a SYNC effect so this drain defers it —\n // without permanently changing its lane. No-op on every default path (transitionDepth 0).\n if (transitionDepth > 0 && reactiveScheduler && this.lane === 'sync') {\n transitionTagged.add(this)\n }\n }\n if (this.observers) {\n for (const o of this.observers) o.markStale(CHECK)\n }\n }\n }\n\n /** Bring this node up to date, recomputing only if a source truly changed. */\n updateIfNecessary(): void {\n if (this.state === CLEAN || this.state === DISPOSED) return\n if (this.state === CHECK && this.sources) {\n for (const src of this.sources) {\n src.updateIfNecessary()\n if (this.state === DIRTY) break\n }\n }\n try {\n if (this.state === DIRTY) this.update()\n } finally {\n // Always settle to CLEAN (unless disposed) even if update() — the body or a\n // child cleanup — threw. Otherwise the node would stay DIRTY forever and\n // markStale's wasClean gate would never re-queue it (a permanent zombie).\n // Sources read before the throw are re-linked, so a later change recovers it.\n if (this.state !== DISPOSED) this.state = CLEAN\n }\n }\n\n /**\n * Recompute the derivation, re-tracking dependencies and notifying observers.\n *\n * Runs in a bounded loop. If the body writes a signal it itself observes (a\n * self-write), {@link markStale} sets {@link restaleRequested} rather than the\n * mark being lost, and we recompute again so the node converges on the value it\n * just produced. The loop is capped by {@link MAX_FLUSH_ITERATIONS} so a\n * non-terminating self-writer (e.g. `effect(() => a.set(a() + 1))`) throws\n * instead of hanging. A prior-run cleanup that throws during teardown must not\n * abort the re-track/recompute (that would strand the node's children and\n * dynamic deps); its error is captured and rethrown only after the node has\n * rebuilt a consistent graph.\n */\n private update(): void {\n const oldValue = this.value\n let cleanupError: unknown\n let hasCleanupError = false\n let iterations = 0\n this.running = true\n try {\n do {\n this.restaleRequested = false\n try {\n disposeChildren(this)\n } catch (err) {\n if (!hasCleanupError) {\n cleanupError = err\n hasCleanupError = true\n }\n }\n unlinkSources(this)\n\n const prevObserver = currentObserver\n const prevOwner = currentOwner\n currentObserver = this\n currentOwner = this\n try {\n // biome-ignore lint/style/noNonNullAssertion: update() only runs for derivations (fn != null).\n this.value = this.fn!()\n } finally {\n currentObserver = prevObserver\n currentOwner = prevOwner\n }\n\n if (++iterations > MAX_FLUSH_ITERATIONS) {\n this.restaleRequested = false\n throw new Error(\n 'MindeesNative: potential infinite reactive loop detected — a computation keeps writing a signal it reads.',\n )\n }\n } while (this.restaleRequested)\n } finally {\n this.running = false\n }\n\n // On the first computation there is no prior value to compare against, so\n // the result is always \"changed\". Afterwards, apply the equality check\n // (unless equals is false, meaning \"always changed\").\n const wasInitialized = this.initialized\n this.initialized = true\n const changed = !wasInitialized || this.equals === false || !this.equals(oldValue, this.value)\n if (changed && this.observers) {\n // Observers are already CHECK from the original push; promote them to DIRTY\n // so the in-flight pull recomputes them.\n for (const o of this.observers) {\n o.state = DIRTY\n }\n }\n\n // The graph is consistent again; now surface any error a previous-run cleanup\n // threw during teardown (the body still re-ran, so children/deps are rebuilt).\n if (hasCleanupError) throw cleanupError\n }\n}\n\n// ---------------------------------------------------------------------------\n// Graph maintenance\n// ---------------------------------------------------------------------------\n\nfunction link(observer: AnyComputation, source: AnyComputation): void {\n // Never subscribe a disposed observer — e.g. a node that disposed itself\n // mid-run and then read another signal. The subscription would leak: nothing\n // tears down a DISPOSED node again.\n if (observer.state === DISPOSED) return\n if (observer.sources === null) observer.sources = []\n const sources = observer.sources\n if (!sources.includes(source)) {\n sources.push(source)\n if (source.observers === null) source.observers = []\n source.observers.push(observer)\n }\n}\n\nfunction unlinkSources(node: AnyComputation): void {\n if (!node.sources) return\n for (const src of node.sources) {\n const obs = src.observers\n if (!obs) continue\n const idx = obs.indexOf(node)\n if (idx >= 0) {\n const last = obs.pop()\n if (last && idx < obs.length) obs[idx] = last\n }\n }\n node.sources = null\n}\n\nfunction disposeChildren(owner: OwnerNode): void {\n // Dispose every owned child AND run every cleanup even if some throw, so a\n // single faulty child/cleanup can't strand the rest or leak observers. `owned`\n // is nulled up front so a throw can't leave a half-disposed array behind.\n // Failures are collected and surfaced together afterward.\n const errors: unknown[] = []\n if (owner.owned) {\n const owned = owner.owned\n owner.owned = null\n for (const child of owned) {\n try {\n disposeComputation(child)\n } catch (err) {\n errors.push(err)\n }\n }\n }\n if (owner.cleanups) {\n const cleanups = owner.cleanups\n owner.cleanups = null\n for (const c of cleanups) {\n try {\n c()\n } catch (err) {\n errors.push(err)\n }\n }\n }\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) throw new AggregateError(errors, 'disposal threw')\n}\n\nfunction disposeComputation(node: AnyComputation): void {\n if (node.state === DISPOSED) return\n // If a node disposes itself mid-run, stop tracking and adopting onto it for the\n // rest of the (now-aborted) body: a later tracked read would otherwise\n // re-subscribe this DISPOSED node, and onCleanup/adopt would register work on a\n // dead scope — none of which is ever torn down again (a leak). Clearing the\n // globals makes read()/onCleanup/adopt no-op for the remainder of the body;\n // update()'s finally restores the previous owner afterward.\n if (node === currentObserver) currentObserver = null\n if (node === currentOwner) currentOwner = null\n try {\n disposeChildren(node)\n } finally {\n // Always fully unlink + mark disposed, even if a descendant cleanup threw,\n // so this node never leaks as a subscribed zombie. The error (if any) still\n // propagates to the caller, which aggregates it across siblings.\n unlinkSources(node)\n node.observers = null\n node.state = DISPOSED\n // Cancel a pending deferred flush so a torn-down effect never runs against a dead graph.\n // (Always null on the synchronous default path — zero behavior change there.)\n if (node.pendingTask) {\n node.pendingTask.cancel()\n node.pendingTask = null\n }\n }\n}\n\nfunction adopt(node: AnyComputation): void {\n if (!currentOwner) return\n if (currentOwner.owned === null) currentOwner.owned = []\n currentOwner.owned.push(node)\n}\n\n// ---------------------------------------------------------------------------\n// Effect scheduling\n// ---------------------------------------------------------------------------\n\nfunction flushEffects(): void {\n if (flushing) return\n flushing = true\n // Isolate each effect: one throwing effect must not abort the flush or strand\n // the effects queued after it (each effect also self-recovers to CLEAN via\n // updateIfNecessary's finally). Errors are collected and surfaced after drain.\n const errors: unknown[] = []\n try {\n let i = 0\n let iterations = 0\n while (i < effectQueue.length) {\n if (++iterations > MAX_FLUSH_ITERATIONS) {\n effectQueue.length = 0\n throw new Error(\n 'MindeesNative: potential infinite reactive loop detected while flushing effects.',\n )\n }\n const e = effectQueue[i]\n i++\n if (e && e.state !== CLEAN && e.state !== DISPOSED) {\n // Defer when the effect's own lane is 'normal', OR it was staled inside a startTransition\n // (tagged for THIS drain only) — but only if a scheduler is injected. Otherwise sync.\n if (reactiveScheduler && (e.lane === 'normal' || transitionTagged.has(e))) {\n // Deferred lane: hand the flush to the scheduler under a stable key, so rapid re-stales\n // coalesce (latest wins) and the body runs later — reading the LATEST values at run time\n // (updateIfNecessary recomputes sources in order), so glitch-freedom is preserved.\n const node = e\n const sched = reactiveScheduler\n // A transition-tagged sync effect has no key yet; mint a stable one so its re-stales coalesce.\n if (node.schedKey === undefined) node.schedKey = `mindees:effect:${effectKeySeq++}`\n node.pendingTask = sched.schedule(\n () => {\n node.pendingTask = null\n if (node.state !== CLEAN && node.state !== DISPOSED) node.updateIfNecessary()\n },\n { priority: 'normal', key: node.schedKey },\n )\n } else {\n // Synchronous lane — byte-identical to the pre-scheduler behavior (and the path every\n // existing test + SSR takes, since `lane` is 'sync' and/or no scheduler is injected).\n try {\n e.updateIfNecessary()\n } catch (err) {\n errors.push(err)\n }\n }\n }\n }\n } finally {\n effectQueue.length = 0\n transitionTagged.clear() // transition tags apply only to the drain that observed them\n flushing = false\n }\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) throw new AggregateError(errors, 'effect(s) threw during flush')\n}\n\nfunction attachNode<T, A extends object>(accessor: A, node: Computation<T>): A {\n ;(accessor as A & WithNode<T>)[NODE] = node\n return accessor\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** A read accessor for a derived/reactive value. */\nexport type Accessor<T> = () => T\n\n/** Options accepted by {@link signal}. */\nexport interface SignalOptions<T> {\n /** Custom equality. `false` means \"always notify\" (every write propagates). */\n equals?: EqualsFn<T> | false\n}\n\n/** A writable reactive value: call to read; `.set`/`.update` to write. */\nexport interface Signal<T> {\n /** Read the value (tracks a dependency inside a computation). */\n (): T\n /** Replace the value; returns the new value. */\n set(value: T): T\n /** Update from the previous value; returns the new value. */\n update(fn: (prev: T) => T): T\n /** Read without tracking a dependency. */\n peek(): T\n}\n\n/** Options accepted by {@link computed}. */\nexport interface ComputedOptions<T> {\n /** Custom equality used to decide whether downstream observers re-run. */\n equals?: EqualsFn<T> | false\n}\n\n/** A memoized derived value: call to read; `.peek` to read without tracking. */\nexport interface Memo<T> {\n /** Read the (lazily recomputed) value, tracking a dependency. */\n (): T\n /** Read without tracking a dependency. */\n peek(): T\n}\n\n/**\n * Create a writable reactive value.\n *\n * @example\n * const count = signal(0)\n * count() // read → 0\n * count.set(1) // write\n * count.update(n => n + 1)\n */\nexport function signal<T>(value: T, options?: SignalOptions<T>): Signal<T> {\n const node = new Computation<T>(value, null, options?.equals ?? equalsDefault, false)\n const accessor = (() => node.read()) as Signal<T>\n accessor.set = (v: T) => node.write(v)\n accessor.update = (fn: (prev: T) => T) => node.write(fn(node.value))\n accessor.peek = () => node.value\n return attachNode(accessor, node)\n}\n\n/**\n * Create a memoized derived value. The function re-runs only when one of the\n * reactive values it reads has actually changed, and only when the result is\n * observed (lazy).\n *\n * @example\n * const doubled = computed(() => count() * 2)\n */\nexport function computed<T>(fn: () => T, options?: ComputedOptions<T>): Memo<T> {\n const node = new Computation<T>(undefined as T, fn, options?.equals ?? equalsDefault, false)\n adopt(node)\n const accessor = (() => node.read()) as Memo<T>\n accessor.peek = () => {\n node.updateIfNecessary()\n return node.value\n }\n return attachNode(accessor, node)\n}\n\n/** Alias of {@link computed}. */\nexport const memo = computed\n\n/**\n * Run a side effect that re-runs whenever its reactive dependencies change.\n * Runs once immediately to establish dependencies.\n *\n * To clean up before each re-run and on disposal, either return a cleanup\n * function from the effect, or call {@link onCleanup}. Any non-function return\n * value is ignored (so expression-bodied effects like `() => list.push(x())`\n * are fine).\n *\n * @returns A disposer that stops the effect and runs its cleanups.\n *\n * @example\n * const stop = effect(() => console.log(count()))\n * stop() // unsubscribe\n *\n * @example\n * effect(() => {\n * const id = setInterval(tick, 1000)\n * return () => clearInterval(id) // cleanup\n * })\n */\n/** Options for {@link effect}. */\nexport interface EffectOptions {\n /**\n * `'sync'` (default) flushes the effect inline on every change — the synchronous, glitch-free\n * behavior. `'normal'` defers the effect through the injected {@link setReactiveScheduler scheduler}\n * (interaction priority / deferred heavy work); with no scheduler it falls back to synchronous.\n */\n priority?: Priority\n}\n\nexport function effect(fn: () => void, options?: EffectOptions): () => void {\n const node = new Computation<void>(\n undefined,\n () => {\n const result: unknown = (fn as () => unknown)()\n if (typeof result === 'function') onCleanup(result as () => void)\n },\n false,\n true,\n )\n // Opt into the deferred lane. `effect(fn)` and `effect(fn, { priority: 'sync' })` are identical to\n // before (synchronous flush). `'normal'` only defers once a scheduler is injected.\n if (options?.priority === 'normal') {\n node.lane = 'normal'\n // ALWAYS a unique per-node key: this coalesces THIS effect's own rapid re-stales (latest wins)\n // without ever sharing a scheduler entry with another effect (which would cross-cancel them).\n node.schedKey = `mindees:effect:${effectKeySeq++}`\n }\n adopt(node)\n node.updateIfNecessary() // first run is ALWAYS synchronous (establishes deps + initial paint)\n return () => disposeComputation(node)\n}\n\n/**\n * Apply the writes in `fn` as a low-priority **transition**: the writes take effect immediately\n * (reads inside/after see the latest values), but the effects they invalidate are **deferred**\n * through the injected {@link setReactiveScheduler scheduler} instead of running synchronously — so a\n * heavy re-render triggered by, say, a keystroke doesn't block the interaction. With no scheduler\n * injected, this is a plain synchronous batch (no deferral) — safe for SSR/tests.\n *\n * @example\n * input.set(value) // urgent: the field updates now\n * startTransition(() => query.set(value)) // the expensive results re-render can lag\n */\nexport function startTransition(fn: () => void): void {\n transitionDepth++\n try {\n batch(fn) // coalesce the transition's writes into one flush; effects staled in it get tagged\n } finally {\n transitionDepth--\n }\n}\n\n/**\n * A **deferred view** of `source`: it tracks `source` on the scheduler's low-priority lane, so under\n * sustained updates it lags behind the live value (the React `useDeferredValue` pattern — show stale\n * results while the latest are computed). With no scheduler injected it mirrors `source`\n * synchronously (no lag), so SSR/tests see the live value. Must be created inside an owner.\n */\nexport function deferred<T>(source: Accessor<T>): Accessor<T> {\n const out = signal(source())\n effect(() => out.set(source()), { priority: 'normal' })\n return () => out()\n}\n\n/**\n * Batch multiple writes so dependent effects run once, after the batch. Reads\n * inside a batch still observe the latest written values synchronously.\n */\nexport function batch<T>(fn: () => T): T {\n if (batchDepth > 0) return fn()\n batchDepth++\n try {\n return fn()\n } finally {\n batchDepth--\n flushEffects()\n }\n}\n\n/** Read reactive values without subscribing the current computation to them. */\nexport function untrack<T>(fn: () => T): T {\n const prev = currentObserver\n currentObserver = null\n try {\n return fn()\n } finally {\n currentObserver = prev\n }\n}\n\n/**\n * Register a cleanup to run before the owning computation re-runs and when it\n * is disposed. No-op outside a reactive scope.\n */\nexport function onCleanup(fn: () => void): void {\n if (!currentOwner) return\n if (currentOwner.cleanups === null) currentOwner.cleanups = []\n currentOwner.cleanups.push(fn)\n}\n\n/**\n * Create a non-tracked root scope that owns everything created within it. The\n * scope lives until the provided `dispose` function is called.\n *\n * @example\n * const dispose = createRoot((dispose) => {\n * effect(() => console.log(count()))\n * return dispose\n * })\n * dispose() // tear down the effect\n */\nexport function createRoot<T>(fn: (dispose: () => void) => T): T {\n const root: OwnerNode = { owned: null, cleanups: null }\n const prevObserver = currentObserver\n const prevOwner = currentOwner\n currentObserver = null\n currentOwner = root\n try {\n return fn(() => disposeChildren(root))\n } finally {\n currentObserver = prevObserver\n currentOwner = prevOwner\n }\n}\n\n/** The current owner scope, or `null` outside any reactive scope. */\nexport function getOwner(): Owner | null {\n // OwnerNode → the nominal public Owner. The brand is phantom (never present at\n // runtime), so the only honest way to produce an Owner is through this cast.\n return currentOwner as unknown as Owner | null\n}\n\n/** Run `fn` with `owner` as the active scope (e.g. to re-attach cleanups). */\nexport function runWithOwner<T>(owner: Owner | null, fn: () => T): T {\n const prev = currentOwner\n // `owner` is an opaque public handle; internally it is always an OwnerNode we\n // handed out via getOwner(). This is the one trusted boundary cast.\n currentOwner = owner as unknown as OwnerNode | null\n try {\n return fn()\n } finally {\n currentOwner = prev\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal test helpers (exported from this module only — NOT from the package\n// public entry point). Used to assert white-box invariants like leak-freedom.\n// ---------------------------------------------------------------------------\n\n/** @internal Number of live observers subscribed to the node behind `accessor`. */\nexport function _observerCount(accessor: object): number {\n const node = (accessor as Partial<WithNode<unknown>>)[NODE]\n return node?.observers?.length ?? 0\n}\n\n/** @internal Number of sources the node behind `accessor` depends on. */\nexport function _sourceCount(accessor: object): number {\n const node = (accessor as Partial<WithNode<unknown>>)[NODE]\n return node?.sources?.length ?? 0\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler.d.ts","names":[],"sources":["../../src/scheduler/scheduler.ts"],"mappings":";;AAmBA;;;;AAAoB;AAGpB;;;;AAAgB;AAGhB;;;;;;;KANY,QAAA;AAaP;AAAA,KAVO,IAAA;;UAGK,eAAA;
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","names":[],"sources":["../../src/scheduler/scheduler.ts"],"mappings":";;AAmBA;;;;AAAoB;AAGpB;;;;AAAgB;AAGhB;;;;;;;KANY,QAAA;AAaP;AAAA,KAVO,IAAA;;UAGK,eAAA;EAiBf;EAfA,QAAA,GAAW,QAAQ;EA0BJ;;;;EArBf,GAAA;AAAA;;UAQe,aAAA;EAsBoB;EApBnC,MAAA;EAiCW;EAAA,SA/BF,OAAO;AAAA;;UASD,gBAAA;EAqCgB;;;;EAhC/B,OAAA,IAAW,KAAA;EAmBM;;;EAfjB,iBAAA,IAAqB,EAAA;AAAA;;;;cAaV,SAAA;EAAA,iBACM,IAAA;EAAA,iBACA,MAAA;EAAA,iBACA,KAAA;EAAA,QACT,eAAA;EAAA,QACA,QAAA;EAAA,iBACS,OAAA;EAAA,iBACA,iBAAA;cAEL,OAAA,GAAU,gBAAA;EAsEd;EAhER,QAAA,CAAS,IAAA,EAAM,IAAA,EAAM,OAAA,GAAU,eAAA,GAAkB,aAAA;EAuGzC;EAlFR,SAAA;EAwFkB;EAAA,IApDd,IAAA;EAAA,QAOI,YAAA;EAAA,QASA,GAAA;;;;;;;AAkD4D;UApB5D,QAAA;EAAA,QAMA,UAAA;AAAA;;iBAcM,eAAA,CAAgB,OAAA,GAAU,gBAAA,GAAmB,SAAS"}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
//#region src/scheduler/scheduler.ts
|
|
2
|
+
/** Safety valve: a single {@link Scheduler.flushSync} that drains more than this many tasks is
|
|
3
|
+
* treated as a runaway re-scheduling loop and aborted (mirrors the reactive flush guard). */
|
|
4
|
+
const MAX_DRAIN_ITERATIONS = 1e5;
|
|
2
5
|
const defaultScheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
|
|
3
6
|
Promise.resolve().then(cb);
|
|
4
7
|
};
|
|
@@ -42,7 +45,17 @@ var Scheduler = class {
|
|
|
42
45
|
if (this.flushing) return;
|
|
43
46
|
this.flushing = true;
|
|
44
47
|
try {
|
|
48
|
+
let iterations = 0;
|
|
45
49
|
while (this.sync.length > 0 || this.normal.length > 0) {
|
|
50
|
+
if (++iterations > MAX_DRAIN_ITERATIONS) {
|
|
51
|
+
this.sync.length = 0;
|
|
52
|
+
this.normal.length = 0;
|
|
53
|
+
this.keyed.clear();
|
|
54
|
+
this.run(() => {
|
|
55
|
+
throw new Error("MindeesNative: potential infinite scheduler loop — a task kept re-scheduling itself.");
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
46
59
|
const entry = this.sync.length > 0 ? this.sync.shift() : this.normal.shift();
|
|
47
60
|
if (!entry) continue;
|
|
48
61
|
this.clearKey(entry);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler.js","names":[],"sources":["../../src/scheduler/scheduler.ts"],"sourcesContent":["/**\n * MindeesNative scheduler — a small, deterministic priority scheduler.\n *\n * Two lanes:\n * - **`sync`** — high-priority work (interaction handlers, first frame). Drained\n * synchronously at the next flush point and always before the normal lane.\n * - **`normal`** — default work, drained on a microtask so multiple schedules in\n * the same tick coalesce into one flush.\n *\n * Tasks are **cancellable** (via the returned handle) and **dedupable** (two\n * tasks scheduled with the same `key` collapse to one — the latest callback\n * wins, preserving the earlier queue position). The scheduler never throws from\n * a task into the caller: task errors are collected and reported via an optional\n * `onError` hook, so one bad task can't stop the rest of the flush.\n *\n * @module\n */\n\n/** Scheduling lanes, highest priority first. */\nexport type Priority = 'sync' | 'normal'\n\n/** A unit of scheduled work. */\nexport type Task = () => void\n\n/** Options for {@link Scheduler.schedule}. */\nexport interface ScheduleOptions {\n /** Lane to run in. Defaults to `'normal'`. */\n priority?: Priority\n /**\n * Dedup key. Scheduling again with the same key replaces the pending task's\n * callback (latest wins) instead of enqueuing a second one.\n */\n key?: string\n}\n\n/** A handle to a scheduled task. */\nexport interface ScheduledTask {\n /** Remove the task if it hasn't run yet. Idempotent. */\n cancel(): void\n /** Whether the task is still pending (not yet run or cancelled). */\n readonly pending: boolean\n}\n\ninterface Entry {\n key: string | null\n fn: Task | null // null once cancelled\n}\n\n/** Options for {@link Scheduler}. */\nexport interface SchedulerOptions {\n /**\n * Called with any error thrown by a task. If omitted, errors are rethrown\n * asynchronously (so they surface to the host without aborting the flush).\n */\n onError?: (error: unknown) => void\n /**\n * Schedules a microtask. Injectable for testing; defaults to `queueMicrotask`.\n */\n scheduleMicrotask?: (cb: () => void) => void\n}\n\nconst defaultScheduleMicrotask: (cb: () => void) => void =\n typeof queueMicrotask === 'function'\n ? queueMicrotask\n : (cb) => {\n void Promise.resolve().then(cb)\n }\n\n/**\n * A deterministic two-lane priority scheduler. Create one with {@link createScheduler}.\n */\nexport class Scheduler {\n private readonly sync: Entry[] = []\n private readonly normal: Entry[] = []\n private readonly keyed = new Map<string, Entry>()\n private microtaskQueued = false\n private flushing = false\n private readonly onError: ((error: unknown) => void) | undefined\n private readonly scheduleMicrotask: (cb: () => void) => void\n\n constructor(options?: SchedulerOptions) {\n this.onError = options?.onError\n this.scheduleMicrotask = options?.scheduleMicrotask ?? defaultScheduleMicrotask\n }\n\n /** Schedule `task`. Returns a handle to cancel it or check its status. */\n schedule(task: Task, options?: ScheduleOptions): ScheduledTask {\n const priority = options?.priority ?? 'normal'\n const key = options?.key ?? null\n\n if (key !== null) {\n const existing = this.keyed.get(key)\n if (existing && existing.fn !== null) {\n // Dedup: replace the callback, keep the existing queue position.\n existing.fn = task\n return this.makeHandle(existing)\n }\n }\n\n const entry: Entry = { key, fn: task }\n if (key !== null) this.keyed.set(key, entry)\n ;(priority === 'sync' ? this.sync : this.normal).push(entry)\n this.requestFlush()\n return this.makeHandle(entry)\n }\n\n /** Run all pending tasks right now (sync lane first), draining both lanes. */\n flushSync(): void {\n if (this.flushing) return\n this.flushing = true\n try {\n // Drain in priority order. Re-check each loop so tasks scheduled by tasks\n // (e.g. a sync task that queues normal work) are handled in this flush.\n while (this.sync.length > 0 || this.normal.length > 0) {\n const entry = this.sync.length > 0 ? this.sync.shift() : this.normal.shift()\n if (!entry) continue\n this.clearKey(entry)\n const fn = entry.fn\n entry.fn = null\n if (fn) this.run(fn)\n }\n } finally {\n this.flushing = false\n this.microtaskQueued = false\n }\n }\n\n /** Number of pending tasks across both lanes (cancelled tasks excluded). */\n get size(): number {\n let n = 0\n for (const e of this.sync) if (e.fn !== null) n++\n for (const e of this.normal) if (e.fn !== null) n++\n return n\n }\n\n private requestFlush(): void {\n if (this.microtaskQueued || this.flushing) return\n this.microtaskQueued = true\n this.scheduleMicrotask(() => {\n this.microtaskQueued = false\n this.flushSync()\n })\n }\n\n private run(fn: Task): void {\n try {\n fn()\n } catch (error) {\n if (this.onError) {\n try {\n this.onError(error)\n } catch (hookError) {\n // A throwing onError hook must not abort the flush or strand the tasks\n // queued after it; surface it asynchronously like an unhandled task error.\n this.scheduleMicrotask(() => {\n throw hookError\n })\n }\n } else {\n // Surface without aborting the flush.\n this.scheduleMicrotask(() => {\n throw error\n })\n }\n }\n }\n\n /**\n * Remove an entry's dedup-key mapping, but only if the map still points at THIS\n * entry. Keys are reused over time (a 'render' key is scheduled, runs, then\n * scheduled again), so a stale handle or an already-dequeued entry must never\n * evict a newer live entry's mapping — doing so would break dedup and let two\n * same-key tasks both run.\n */\n private clearKey(entry: Entry): void {\n if (entry.key !== null && this.keyed.get(entry.key) === entry) {\n this.keyed.delete(entry.key)\n }\n }\n\n private makeHandle(entry: Entry): ScheduledTask {\n return {\n cancel: () => {\n entry.fn = null\n this.clearKey(entry)\n },\n get pending() {\n return entry.fn !== null\n },\n }\n }\n}\n\n/** Create a new {@link Scheduler}. */\nexport function createScheduler(options?: SchedulerOptions): Scheduler {\n return new Scheduler(options)\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"scheduler.js","names":[],"sources":["../../src/scheduler/scheduler.ts"],"sourcesContent":["/**\n * MindeesNative scheduler — a small, deterministic priority scheduler.\n *\n * Two lanes:\n * - **`sync`** — high-priority work (interaction handlers, first frame). Drained\n * synchronously at the next flush point and always before the normal lane.\n * - **`normal`** — default work, drained on a microtask so multiple schedules in\n * the same tick coalesce into one flush.\n *\n * Tasks are **cancellable** (via the returned handle) and **dedupable** (two\n * tasks scheduled with the same `key` collapse to one — the latest callback\n * wins, preserving the earlier queue position). The scheduler never throws from\n * a task into the caller: task errors are collected and reported via an optional\n * `onError` hook, so one bad task can't stop the rest of the flush.\n *\n * @module\n */\n\n/** Scheduling lanes, highest priority first. */\nexport type Priority = 'sync' | 'normal'\n\n/** A unit of scheduled work. */\nexport type Task = () => void\n\n/** Options for {@link Scheduler.schedule}. */\nexport interface ScheduleOptions {\n /** Lane to run in. Defaults to `'normal'`. */\n priority?: Priority\n /**\n * Dedup key. Scheduling again with the same key replaces the pending task's\n * callback (latest wins) instead of enqueuing a second one.\n */\n key?: string\n}\n\n/** Safety valve: a single {@link Scheduler.flushSync} that drains more than this many tasks is\n * treated as a runaway re-scheduling loop and aborted (mirrors the reactive flush guard). */\nconst MAX_DRAIN_ITERATIONS = 100_000\n\n/** A handle to a scheduled task. */\nexport interface ScheduledTask {\n /** Remove the task if it hasn't run yet. Idempotent. */\n cancel(): void\n /** Whether the task is still pending (not yet run or cancelled). */\n readonly pending: boolean\n}\n\ninterface Entry {\n key: string | null\n fn: Task | null // null once cancelled\n}\n\n/** Options for {@link Scheduler}. */\nexport interface SchedulerOptions {\n /**\n * Called with any error thrown by a task. If omitted, errors are rethrown\n * asynchronously (so they surface to the host without aborting the flush).\n */\n onError?: (error: unknown) => void\n /**\n * Schedules a microtask. Injectable for testing; defaults to `queueMicrotask`.\n */\n scheduleMicrotask?: (cb: () => void) => void\n}\n\nconst defaultScheduleMicrotask: (cb: () => void) => void =\n typeof queueMicrotask === 'function'\n ? queueMicrotask\n : (cb) => {\n void Promise.resolve().then(cb)\n }\n\n/**\n * A deterministic two-lane priority scheduler. Create one with {@link createScheduler}.\n */\nexport class Scheduler {\n private readonly sync: Entry[] = []\n private readonly normal: Entry[] = []\n private readonly keyed = new Map<string, Entry>()\n private microtaskQueued = false\n private flushing = false\n private readonly onError: ((error: unknown) => void) | undefined\n private readonly scheduleMicrotask: (cb: () => void) => void\n\n constructor(options?: SchedulerOptions) {\n this.onError = options?.onError\n this.scheduleMicrotask = options?.scheduleMicrotask ?? defaultScheduleMicrotask\n }\n\n /** Schedule `task`. Returns a handle to cancel it or check its status. */\n schedule(task: Task, options?: ScheduleOptions): ScheduledTask {\n const priority = options?.priority ?? 'normal'\n const key = options?.key ?? null\n\n if (key !== null) {\n const existing = this.keyed.get(key)\n if (existing && existing.fn !== null) {\n // Dedup: replace the callback, keep the existing queue position.\n existing.fn = task\n return this.makeHandle(existing)\n }\n }\n\n const entry: Entry = { key, fn: task }\n if (key !== null) this.keyed.set(key, entry)\n ;(priority === 'sync' ? this.sync : this.normal).push(entry)\n this.requestFlush()\n return this.makeHandle(entry)\n }\n\n /** Run all pending tasks right now (sync lane first), draining both lanes. */\n flushSync(): void {\n if (this.flushing) return\n this.flushing = true\n try {\n // Drain in priority order. Re-check each loop so tasks scheduled by tasks\n // (e.g. a sync task that queues normal work) are handled in this flush.\n let iterations = 0\n while (this.sync.length > 0 || this.normal.length > 0) {\n // Safety valve: a task that perpetually re-schedules (e.g. two deferred reactive effects\n // writing each other's deps) would drain forever. Cap it, clear both lanes, and surface\n // an error — mirroring the reactive loop guard so a deferred cycle fails loudly, not hangs.\n if (++iterations > MAX_DRAIN_ITERATIONS) {\n this.sync.length = 0\n this.normal.length = 0\n this.keyed.clear()\n this.run(() => {\n throw new Error(\n 'MindeesNative: potential infinite scheduler loop — a task kept re-scheduling itself.',\n )\n })\n return\n }\n const entry = this.sync.length > 0 ? this.sync.shift() : this.normal.shift()\n if (!entry) continue\n this.clearKey(entry)\n const fn = entry.fn\n entry.fn = null\n if (fn) this.run(fn)\n }\n } finally {\n this.flushing = false\n this.microtaskQueued = false\n }\n }\n\n /** Number of pending tasks across both lanes (cancelled tasks excluded). */\n get size(): number {\n let n = 0\n for (const e of this.sync) if (e.fn !== null) n++\n for (const e of this.normal) if (e.fn !== null) n++\n return n\n }\n\n private requestFlush(): void {\n if (this.microtaskQueued || this.flushing) return\n this.microtaskQueued = true\n this.scheduleMicrotask(() => {\n this.microtaskQueued = false\n this.flushSync()\n })\n }\n\n private run(fn: Task): void {\n try {\n fn()\n } catch (error) {\n if (this.onError) {\n try {\n this.onError(error)\n } catch (hookError) {\n // A throwing onError hook must not abort the flush or strand the tasks\n // queued after it; surface it asynchronously like an unhandled task error.\n this.scheduleMicrotask(() => {\n throw hookError\n })\n }\n } else {\n // Surface without aborting the flush.\n this.scheduleMicrotask(() => {\n throw error\n })\n }\n }\n }\n\n /**\n * Remove an entry's dedup-key mapping, but only if the map still points at THIS\n * entry. Keys are reused over time (a 'render' key is scheduled, runs, then\n * scheduled again), so a stale handle or an already-dequeued entry must never\n * evict a newer live entry's mapping — doing so would break dedup and let two\n * same-key tasks both run.\n */\n private clearKey(entry: Entry): void {\n if (entry.key !== null && this.keyed.get(entry.key) === entry) {\n this.keyed.delete(entry.key)\n }\n }\n\n private makeHandle(entry: Entry): ScheduledTask {\n return {\n cancel: () => {\n entry.fn = null\n this.clearKey(entry)\n },\n get pending() {\n return entry.fn !== null\n },\n }\n }\n}\n\n/** Create a new {@link Scheduler}. */\nexport function createScheduler(options?: SchedulerOptions): Scheduler {\n return new Scheduler(options)\n}\n"],"mappings":";;;AAqCA,MAAM,uBAAuB;AA4B7B,MAAM,2BACJ,OAAO,mBAAmB,aACtB,kBACC,OAAO;CACN,QAAa,QAAQ,EAAE,KAAK,EAAE;AAChC;;;;AAKN,IAAa,YAAb,MAAuB;CACrB,OAAiC,CAAC;CAClC,SAAmC,CAAC;CACpC,wBAAyB,IAAI,IAAmB;CAChD,kBAA0B;CAC1B,WAAmB;CACnB;CACA;CAEA,YAAY,SAA4B;EACtC,KAAK,UAAU,SAAS;EACxB,KAAK,oBAAoB,SAAS,qBAAqB;CACzD;;CAGA,SAAS,MAAY,SAA0C;EAC7D,MAAM,WAAW,SAAS,YAAY;EACtC,MAAM,MAAM,SAAS,OAAO;EAE5B,IAAI,QAAQ,MAAM;GAChB,MAAM,WAAW,KAAK,MAAM,IAAI,GAAG;GACnC,IAAI,YAAY,SAAS,OAAO,MAAM;IAEpC,SAAS,KAAK;IACd,OAAO,KAAK,WAAW,QAAQ;GACjC;EACF;EAEA,MAAM,QAAe;GAAE;GAAK,IAAI;EAAK;EACrC,IAAI,QAAQ,MAAM,KAAK,MAAM,IAAI,KAAK,KAAK;EAC1C,CAAC,aAAa,SAAS,KAAK,OAAO,KAAK,QAAQ,KAAK,KAAK;EAC3D,KAAK,aAAa;EAClB,OAAO,KAAK,WAAW,KAAK;CAC9B;;CAGA,YAAkB;EAChB,IAAI,KAAK,UAAU;EACnB,KAAK,WAAW;EAChB,IAAI;GAGF,IAAI,aAAa;GACjB,OAAO,KAAK,KAAK,SAAS,KAAK,KAAK,OAAO,SAAS,GAAG;IAIrD,IAAI,EAAE,aAAa,sBAAsB;KACvC,KAAK,KAAK,SAAS;KACnB,KAAK,OAAO,SAAS;KACrB,KAAK,MAAM,MAAM;KACjB,KAAK,UAAU;MACb,MAAM,IAAI,MACR,sFACF;KACF,CAAC;KACD;IACF;IACA,MAAM,QAAQ,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,OAAO,MAAM;IAC3E,IAAI,CAAC,OAAO;IACZ,KAAK,SAAS,KAAK;IACnB,MAAM,KAAK,MAAM;IACjB,MAAM,KAAK;IACX,IAAI,IAAI,KAAK,IAAI,EAAE;GACrB;EACF,UAAU;GACR,KAAK,WAAW;GAChB,KAAK,kBAAkB;EACzB;CACF;;CAGA,IAAI,OAAe;EACjB,IAAI,IAAI;EACR,KAAK,MAAM,KAAK,KAAK,MAAM,IAAI,EAAE,OAAO,MAAM;EAC9C,KAAK,MAAM,KAAK,KAAK,QAAQ,IAAI,EAAE,OAAO,MAAM;EAChD,OAAO;CACT;CAEA,eAA6B;EAC3B,IAAI,KAAK,mBAAmB,KAAK,UAAU;EAC3C,KAAK,kBAAkB;EACvB,KAAK,wBAAwB;GAC3B,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB,CAAC;CACH;CAEA,IAAY,IAAgB;EAC1B,IAAI;GACF,GAAG;EACL,SAAS,OAAO;GACd,IAAI,KAAK,SACP,IAAI;IACF,KAAK,QAAQ,KAAK;GACpB,SAAS,WAAW;IAGlB,KAAK,wBAAwB;KAC3B,MAAM;IACR,CAAC;GACH;QAGA,KAAK,wBAAwB;IAC3B,MAAM;GACR,CAAC;EAEL;CACF;;;;;;;;CASA,SAAiB,OAAoB;EACnC,IAAI,MAAM,QAAQ,QAAQ,KAAK,MAAM,IAAI,MAAM,GAAG,MAAM,OACtD,KAAK,MAAM,OAAO,MAAM,GAAG;CAE/B;CAEA,WAAmB,OAA6B;EAC9C,OAAO;GACL,cAAc;IACZ,MAAM,KAAK;IACX,KAAK,SAAS,KAAK;GACrB;GACA,IAAI,UAAU;IACZ,OAAO,MAAM,OAAO;GACtB;EACF;CACF;AACF;;AAGA,SAAgB,gBAAgB,SAAuC;CACrE,OAAO,IAAI,UAAU,OAAO;AAC9B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindees/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MindeesNative core — fine-grained reactivity (signals/computed/effect/batch), component model with selector-isolated context, priority scheduler, and a thread-pool abstraction (Web Worker + inline; native is a research track).",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|