@mindees/core 0.22.4 → 0.22.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ import { ThreadPool, WorkerLike, WorkerPoolOptions, createInlineThreadPool, crea
14
14
  /** The npm package name. */
15
15
  declare const name = "@mindees/core";
16
16
  /** The package version. All `@mindees/*` packages share one locked version line. */
17
- declare const VERSION = "0.22.4";
17
+ declare const VERSION = "0.22.6";
18
18
  /**
19
19
  * Current maturity of this package. See the repository `STATUS.md`.
20
20
  *
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { createInlineThreadPool, createNativeThreadPool, createWorkerPool } from
12
12
  /** The npm package name. */
13
13
  const name = "@mindees/core";
14
14
  /** The package version. All `@mindees/*` packages share one locked version line. */
15
- const VERSION = "0.22.4";
15
+ const VERSION = "0.22.6";
16
16
  /**
17
17
  * Current maturity of this package. See the repository `STATUS.md`.
18
18
  *
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Maturity, PackageInfo } from './types'\n\n/**\n * Animation engine: reactive animated values + timing/spring drivers + interpolate, driven by an\n * injected frame source (RN Animated/Reanimated + Flutter AnimationController parity).\n */\nexport {\n _activeAnimationCount,\n _resetAnimation,\n type AnimatedValue,\n type AnimationHandle,\n animate,\n cubicBezier,\n type Easing,\n easeInOutQuad,\n easeInQuad,\n easeOutCubic,\n easeOutQuad,\n type FrameSource,\n getFrameSource,\n interpolate,\n linear,\n manualFrameSource,\n rafFrameSource,\n setFrameSource,\n spring,\n timing,\n} from './animation'\n/**\n * Component model: a renderer-agnostic element tree plus selector-based,\n * re-render-isolated context. (Phase 2)\n */\nexport {\n type Component,\n type Context,\n type ContextProvider,\n createContext,\n createElement,\n createProvider,\n ELEMENT_TYPE,\n type ElementType,\n Fragment,\n hasOwner,\n isElement,\n isKeyedRegion,\n isPortal,\n KEYED_REGION,\n type KeyedRegion,\n type KeyedRegionOptions,\n keyedRegion,\n type MindeesElement,\n type MindeesNode,\n PORTAL,\n type PortalRegion,\n portal,\n renderComponent,\n type SelectorEquals,\n} from './component'\nexport { NotImplementedError } from './errors'\n/**\n * Gesture recognizers: tap/longPress/pan/pinch/swipe → reactive state that drives styles and the\n * animation engine (RN Gesture Handler / Flutter GestureDetector parity).\n */\nexport {\n _setGestureClock,\n composeGestures,\n type GestureHandlers,\n longPress,\n normalizePointer,\n type PanEvent,\n type PanState,\n type PinchEvent,\n type PinchState,\n type PointerSample,\n pan,\n panAnimated,\n pinch,\n type Recognizer,\n type SwipeDirection,\n type SwipeEvent,\n swipe,\n type TapState,\n tap,\n} from './gesture'\nexport { notImplemented } from './not-implemented'\n/**\n * Fine-grained reactivity: signals, computed values, effects, batching, and\n * disposal scopes. This is the reactive core of MindeesNative.\n */\nexport {\n type Accessor,\n batch,\n type ComputedOptions,\n computed,\n createRoot,\n deferred,\n type EffectOptions,\n type EqualsFn,\n effect,\n getOwner,\n type Memo,\n memo,\n type Owner,\n on,\n onCleanup,\n runWithOwner,\n type Signal,\n type SignalOptions,\n setReactiveScheduler,\n signal,\n startTransition,\n untrack,\n} from './reactive'\n/**\n * Priority scheduler: two-lane (sync/normal), microtask-batched, with\n * cancellable and dedupable tasks. (Phase 2)\n */\nexport {\n createScheduler,\n type Priority,\n type ScheduledTask,\n type ScheduleOptions,\n Scheduler,\n type SchedulerOptions,\n type Task,\n} from './scheduler'\n/**\n * Threading abstraction: a {@link ThreadPool} contract with a working Web Worker\n * backend and an inline fallback. Native multi-threading is a research track. (Phase 2)\n */\nexport {\n createInlineThreadPool,\n createNativeThreadPool,\n createWorkerPool,\n type ThreadPool,\n type WorkerLike,\n type WorkerPoolOptions,\n} from './threading'\nexport type { Maturity, PackageInfo } from './types'\n\n/** The npm package name. */\nexport const name = '@mindees/core'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.22.4'\n\n/**\n * Current maturity of this package. See the repository `STATUS.md`.\n *\n * The reactivity layer (signals/computed/effect/batch), the component model with\n * selector-isolated context, the priority scheduler, and the thread-pool\n * abstraction (Web Worker + inline) are all implemented and tested. Native\n * multi-threading remains a research track (throws `NotImplementedError`).\n */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n"],"mappings":";;;;;;;;;;;;AA6IA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;;;;;;;;AAUvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Maturity, PackageInfo } from './types'\n\n/**\n * Animation engine: reactive animated values + timing/spring drivers + interpolate, driven by an\n * injected frame source (RN Animated/Reanimated + Flutter AnimationController parity).\n */\nexport {\n _activeAnimationCount,\n _resetAnimation,\n type AnimatedValue,\n type AnimationHandle,\n animate,\n cubicBezier,\n type Easing,\n easeInOutQuad,\n easeInQuad,\n easeOutCubic,\n easeOutQuad,\n type FrameSource,\n getFrameSource,\n interpolate,\n linear,\n manualFrameSource,\n rafFrameSource,\n setFrameSource,\n spring,\n timing,\n} from './animation'\n/**\n * Component model: a renderer-agnostic element tree plus selector-based,\n * re-render-isolated context. (Phase 2)\n */\nexport {\n type Component,\n type Context,\n type ContextProvider,\n createContext,\n createElement,\n createProvider,\n ELEMENT_TYPE,\n type ElementType,\n Fragment,\n hasOwner,\n isElement,\n isKeyedRegion,\n isPortal,\n KEYED_REGION,\n type KeyedRegion,\n type KeyedRegionOptions,\n keyedRegion,\n type MindeesElement,\n type MindeesNode,\n PORTAL,\n type PortalRegion,\n portal,\n renderComponent,\n type SelectorEquals,\n} from './component'\nexport { NotImplementedError } from './errors'\n/**\n * Gesture recognizers: tap/longPress/pan/pinch/swipe → reactive state that drives styles and the\n * animation engine (RN Gesture Handler / Flutter GestureDetector parity).\n */\nexport {\n _setGestureClock,\n composeGestures,\n type GestureHandlers,\n longPress,\n normalizePointer,\n type PanEvent,\n type PanState,\n type PinchEvent,\n type PinchState,\n type PointerSample,\n pan,\n panAnimated,\n pinch,\n type Recognizer,\n type SwipeDirection,\n type SwipeEvent,\n swipe,\n type TapState,\n tap,\n} from './gesture'\nexport { notImplemented } from './not-implemented'\n/**\n * Fine-grained reactivity: signals, computed values, effects, batching, and\n * disposal scopes. This is the reactive core of MindeesNative.\n */\nexport {\n type Accessor,\n batch,\n type ComputedOptions,\n computed,\n createRoot,\n deferred,\n type EffectOptions,\n type EqualsFn,\n effect,\n getOwner,\n type Memo,\n memo,\n type Owner,\n on,\n onCleanup,\n runWithOwner,\n type Signal,\n type SignalOptions,\n setReactiveScheduler,\n signal,\n startTransition,\n untrack,\n} from './reactive'\n/**\n * Priority scheduler: two-lane (sync/normal), microtask-batched, with\n * cancellable and dedupable tasks. (Phase 2)\n */\nexport {\n createScheduler,\n type Priority,\n type ScheduledTask,\n type ScheduleOptions,\n Scheduler,\n type SchedulerOptions,\n type Task,\n} from './scheduler'\n/**\n * Threading abstraction: a {@link ThreadPool} contract with a working Web Worker\n * backend and an inline fallback. Native multi-threading is a research track. (Phase 2)\n */\nexport {\n createInlineThreadPool,\n createNativeThreadPool,\n createWorkerPool,\n type ThreadPool,\n type WorkerLike,\n type WorkerPoolOptions,\n} from './threading'\nexport type { Maturity, PackageInfo } from './types'\n\n/** The npm package name. */\nexport const name = '@mindees/core'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.22.6'\n\n/**\n * Current maturity of this package. See the repository `STATUS.md`.\n *\n * The reactivity layer (signals/computed/effect/batch), the component model with\n * selector-isolated context, the priority scheduler, and the thread-pool\n * abstraction (Web Worker + inline) are all implemented and tested. Native\n * multi-threading remains a research track (throws `NotImplementedError`).\n */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n"],"mappings":";;;;;;;;;;;;AA6IA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;;;;;;;;AAUvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
@@ -1 +1 @@
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;;;;;;;;;;iBAmB1B,EAAA,OACd,IAAA,EAAM,QAAA,CAAS,CAAA,GACf,EAAA,GAAK,KAAA,EAAO,CAAA,EAAG,SAAA,EAAW,CAAA,cAAe,IAAA,EAAM,CAAA,iBAAkB,CAAA,EACjE,OAAA;EAAY,KAAA;AAAA,UACL,CAAA;;;AA7CmD;AAU5D;iBAwDgB,SAAA,CAAU,EAAc;;;;;;;;AAxDA;AAYxC;;;iBA6DgB,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"}
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,iBA4BgB,KAAA,IAAS,EAAA,QAAU,CAAA,GAAI,CAAC;AAbxC;AAAA,iBAyBgB,OAAA,IAAW,EAAA,QAAU,CAAA,GAAI,CAAC;;;;;;;;;;iBAmB1B,EAAA,OACd,IAAA,EAAM,QAAA,CAAS,CAAA,GACf,EAAA,GAAK,KAAA,EAAO,CAAA,EAAG,SAAA,EAAW,CAAA,cAAe,IAAA,EAAM,CAAA,iBAAkB,CAAA,EACjE,OAAA;EAAY,KAAA;AAAA,UACL,CAAA;;;AAhDmD;AAa5D;iBAwDgB,SAAA,CAAU,EAAc;;;;;;;;AAxDA;AAYxC;;;iBA6DgB,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"}
@@ -372,7 +372,7 @@ function startTransition(fn) {
372
372
  * synchronously (no lag), so SSR/tests see the live value. Must be created inside an owner.
373
373
  */
374
374
  function deferred(source) {
375
- const out = signal(source());
375
+ const out = signal(untrack(() => source()));
376
376
  effect(() => out.set(source()), { priority: "normal" });
377
377
  return () => out();
378
378
  }
@@ -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\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 * Make a computation react to an EXPLICIT dependency only — the body runs untracked, so signals it\n * reads don't subscribe; only `deps` does. Wrap it in `effect`/`memo`: `effect(on(() => a(), (av) => …))`\n * re-runs when `a` changes but not when other signals the body reads change. The callback receives the\n * current dep value, the previous dep value, and its own previous return. With `{ defer: true }` the\n * first run is skipped (deps are still tracked) — react only to *changes*, not the initial value.\n *\n * @example effect(on(() => userId(), (id) => { void load(id) }, { defer: true }))\n */\nexport function on<S, U>(\n deps: Accessor<S>,\n fn: (input: S, prevInput: S | undefined, prev: U | undefined) => U,\n options?: { defer?: boolean },\n): () => U | undefined {\n let defer = options?.defer ?? false\n let prevInput: S | undefined\n let prevResult: U | undefined\n return () => {\n const input = deps() // the ONLY tracked read\n if (defer) {\n defer = false\n prevInput = input\n return undefined\n }\n prevResult = untrack(() => fn(input, prevInput, prevResult))\n prevInput = input\n return prevResult\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 // Seed UNTRACKED: reading source() here would otherwise subscribe the ENCLOSING computation (the\n // effect/computed deferred() is created in) to source — defeating the deferral and leaking an effect\n // per re-run. The internal 'normal'-lane effect below still tracks source (lag-then-converge intact).\n const out = signal(untrack(() => 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 * Make a computation react to an EXPLICIT dependency only — the body runs untracked, so signals it\n * reads don't subscribe; only `deps` does. Wrap it in `effect`/`memo`: `effect(on(() => a(), (av) => …))`\n * re-runs when `a` changes but not when other signals the body reads change. The callback receives the\n * current dep value, the previous dep value, and its own previous return. With `{ defer: true }` the\n * first run is skipped (deps are still tracked) — react only to *changes*, not the initial value.\n *\n * @example effect(on(() => userId(), (id) => { void load(id) }, { defer: true }))\n */\nexport function on<S, U>(\n deps: Accessor<S>,\n fn: (input: S, prevInput: S | undefined, prev: U | undefined) => U,\n options?: { defer?: boolean },\n): () => U | undefined {\n let defer = options?.defer ?? false\n let prevInput: S | undefined\n let prevResult: U | undefined\n return () => {\n const input = deps() // the ONLY tracked read\n if (defer) {\n defer = false\n prevInput = input\n return undefined\n }\n prevResult = untrack(() => fn(input, prevInput, prevResult))\n prevInput = input\n return prevResult\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;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
+ {"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;EA2BJ;;;;EAtBf,GAAA;AAAA;;UAQe,aAAA;EAuBoB;EArBnC,MAAA;EAkCW;EAAA,SAhCF,OAAO;AAAA;;UAUD,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;EAsFd;EAhFR,QAAA,CAAS,IAAA,EAAM,IAAA,EAAM,OAAA,GAAU,eAAA,GAAkB,aAAA;EAuHzC;EAxFR,SAAA;EA8FkB;EAAA,IApDd,IAAA;EAAA,QAOI,YAAA;EAAA,QASA,GAAA;;;;;;;AAkD4D;UApB5D,QAAA;EAAA,QAMA,UAAA;AAAA;;iBAcM,eAAA,CAAgB,OAAA,GAAU,gBAAA,GAAmB,SAAS"}
@@ -27,13 +27,26 @@ var Scheduler = class {
27
27
  if (key !== null) {
28
28
  const existing = this.keyed.get(key);
29
29
  if (existing && existing.fn !== null) {
30
- existing.fn = task;
31
- return this.makeHandle(existing);
30
+ if (existing.lane === priority) {
31
+ existing.fn = task;
32
+ return this.makeHandle(existing);
33
+ }
34
+ existing.fn = null;
35
+ const moved = {
36
+ key,
37
+ fn: task,
38
+ lane: priority
39
+ };
40
+ this.keyed.set(key, moved);
41
+ (priority === "sync" ? this.sync : this.normal).push(moved);
42
+ this.requestFlush();
43
+ return this.makeHandle(moved);
32
44
  }
33
45
  }
34
46
  const entry = {
35
47
  key,
36
- fn: task
48
+ fn: task,
49
+ lane: priority
37
50
  };
38
51
  if (key !== null) this.keyed.set(key, entry);
39
52
  (priority === "sync" ? this.sync : this.normal).push(entry);
@@ -44,6 +57,7 @@ var Scheduler = class {
44
57
  flushSync() {
45
58
  if (this.flushing) return;
46
59
  this.flushing = true;
60
+ let overflowed = false;
47
61
  try {
48
62
  let iterations = 0;
49
63
  while (this.sync.length > 0 || this.normal.length > 0) {
@@ -51,10 +65,8 @@ var Scheduler = class {
51
65
  this.sync.length = 0;
52
66
  this.normal.length = 0;
53
67
  this.keyed.clear();
54
- this.run(() => {
55
- throw new Error("MindeesNative: potential infinite scheduler loop — a task kept re-scheduling itself.");
56
- });
57
- return;
68
+ overflowed = true;
69
+ break;
58
70
  }
59
71
  const entry = this.sync.length > 0 ? this.sync.shift() : this.normal.shift();
60
72
  if (!entry) continue;
@@ -67,6 +79,9 @@ var Scheduler = class {
67
79
  this.flushing = false;
68
80
  this.microtaskQueued = false;
69
81
  }
82
+ if (overflowed) this.run(() => {
83
+ throw new Error("MindeesNative: potential infinite scheduler loop — a task kept re-scheduling itself.");
84
+ });
70
85
  }
71
86
  /** Number of pending tasks across both lanes (cancelled tasks excluded). */
72
87
  get size() {
@@ -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/** 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"}
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 lane: Priority // which lane this entry lives in (so a re-key with a changed priority can relocate it)\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 if (existing.lane === priority) {\n // Same lane → replace the callback, keep the existing queue position (latest callback wins).\n existing.fn = task\n return this.makeHandle(existing)\n }\n // Changed priority → relocate to the requested lane so the latest PRIORITY wins too: tombstone\n // the old-lane entry (its slot is skipped via the null fn) and push a fresh one.\n existing.fn = null\n const moved: Entry = { key, fn: task, lane: priority }\n this.keyed.set(key, moved)\n ;(priority === 'sync' ? this.sync : this.normal).push(moved)\n this.requestFlush()\n return this.makeHandle(moved)\n }\n }\n\n const entry: Entry = { key, fn: task, lane: priority }\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 let overflowed = false\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 overflowed = true\n break\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 if (overflowed) {\n // Report the overflow OUTSIDE the flushing window: if `onError` schedules a recovery task,\n // requestFlush() would no-op while `flushing` is true and strand it — now it arms a fresh flush.\n this.run(() => {\n throw new Error(\n 'MindeesNative: potential infinite scheduler loop — a task kept re-scheduling itself.',\n )\n })\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":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"thread-pool.d.ts","names":[],"sources":["../../src/threading/thread-pool.ts"],"mappings":";;AA0BA;;;;;;;;;;;;;;;;;;;;;;UAAiB,UAAA;EAYF;AAAA;AAQf;;;;AAAoD;EAZlD,GAAA,UAAa,GAAA,GAAM,KAAA,EAAO,EAAA,KAAO,GAAA,EAAK,KAAA,EAAO,EAAA,GAAK,OAAA,CAAQ,GAAA;EA+BjC;EA7BzB,OAAA;EA6ByB;EAAA,SA3BhB,IAAA;AAAA;;;;;;iBAQK,sBAAA,IAA0B,UAAU;;UAmBnC,UAAA;EACf,WAAA,CAAY,OAAA;EACZ,SAAA;EACA,SAAA,IAAa,KAAA;IAAS,IAAA;EAAA;EACtB,OAAA,IAAW,KAAA;IAAS,OAAA;EAAA;AAAA;AAYU;AAAA,UARf,iBAAA;EA2Be;EAzB9B,IAAA;EAyBsE;;;;;EAnBtE,YAAA,QAAoB,UAAU;AAAA;;;;AAqGoB;;;;;;iBAlFpC,gBAAA,CAAiB,OAAA,EAAS,iBAAA,GAAoB,UAAU;;;;;;;;;iBAkFxD,sBAAA,IAA0B,UAAU"}
1
+ {"version":3,"file":"thread-pool.d.ts","names":[],"sources":["../../src/threading/thread-pool.ts"],"mappings":";;AA0BA;;;;;;;;;;;;;;;;;;;;;;UAAiB,UAAA;EAYF;AAAA;AAQf;;;;AAAoD;EAZlD,GAAA,UAAa,GAAA,GAAM,KAAA,EAAO,EAAA,KAAO,GAAA,EAAK,KAAA,EAAO,EAAA,GAAK,OAAA,CAAQ,GAAA;EA+BjC;EA7BzB,OAAA;EA6ByB;EAAA,SA3BhB,IAAA;AAAA;;;;;;iBAQK,sBAAA,IAA0B,UAAU;;UAmBnC,UAAA;EACf,WAAA,CAAY,OAAA;EACZ,SAAA;EACA,SAAA,IAAa,KAAA;IAAS,IAAA;EAAA;EACtB,OAAA,IAAW,KAAA;IAAS,OAAA;EAAA;AAAA;AAYU;AAAA,UARf,iBAAA;EA2Be;EAzB9B,IAAA;EAyBsE;;;;;EAnBtE,YAAA,QAAoB,UAAU;AAAA;;;;AAuGoB;;;;;;iBApFpC,gBAAA,CAAiB,OAAA,EAAS,iBAAA,GAAoB,UAAU;;;;;;;;;iBAoFxD,sBAAA,IAA0B,UAAU"}
@@ -75,7 +75,7 @@ function createWorkerPool(options) {
75
75
  else job.reject(new Error(data.error ?? "worker job failed"));
76
76
  };
77
77
  w.onerror = (event) => {
78
- if (disposed) return;
78
+ if (disposed || workers[index] !== w) return;
79
79
  rejectWorkerJobs(index, event.message ?? "worker error");
80
80
  try {
81
81
  w.terminate();
@@ -1 +1 @@
1
- {"version":3,"file":"thread-pool.js","names":[],"sources":["../../src/threading/thread-pool.ts"],"sourcesContent":["/**\n * MindeesNative threading abstraction.\n *\n * Defines the **contract** for offloading CPU-bound work off the main thread, so\n * the rest of the framework can parallelize without binding to a specific\n * backend. Two backends are anticipated:\n *\n * - **Web Worker backend** (`createWorkerPool`) — works today on web; runs each\n * job in a `Worker`.\n * - **Native multi-thread backend** — 🔬 research track. The interface is\n * defined here so the architecture is real; a native Rust-backed scheduler\n * will implement {@link ThreadPool} in a later phase. Until then,\n * {@link createInlineThreadPool} provides a correct, synchronous fallback so\n * callers are never blocked (Working-Code Doctrine: a real fallback, not a\n * lying stub).\n *\n * @module\n */\n\nimport { NotImplementedError } from '../errors'\n\n/**\n * A pool that runs a pure job function with a transferable argument and resolves\n * its result. Implementations may run jobs on worker threads, native threads, or\n * (as a fallback) inline on the calling thread.\n */\nexport interface ThreadPool {\n /**\n * Run `job(input)` and resolve its result.\n *\n * `job` must be a **pure, self-contained function** (it may be serialized and\n * re-created in another realm, so it cannot close over outer variables in\n * worker/native backends). `input` must be structured-cloneable.\n */\n run<In, Out>(job: (input: In) => Out, input: In): Promise<Out>\n /** Release all underlying resources (terminate workers, etc.). Idempotent. */\n dispose(): void\n /** Number of live workers/threads (0 for the inline fallback). */\n readonly size: number\n}\n\n/**\n * A synchronous, single-realm {@link ThreadPool}: runs each job inline on the\n * calling thread. This is the universal fallback — correct everywhere, with no\n * parallelism. Use it where workers aren't available (or in tests).\n */\nexport function createInlineThreadPool(): ThreadPool {\n let disposed = false\n return {\n run<In, Out>(job: (input: In) => Out, input: In): Promise<Out> {\n if (disposed) return Promise.reject(new Error('ThreadPool is disposed'))\n try {\n return Promise.resolve(job(input))\n } catch (error) {\n return Promise.reject(error as Error)\n }\n },\n dispose() {\n disposed = true\n },\n size: 0,\n }\n}\n\n/** Minimal structural subset of the DOM `Worker` we depend on. */\nexport interface WorkerLike {\n postMessage(message: unknown): void\n terminate(): void\n onmessage: ((event: { data: unknown }) => void) | null\n onerror: ((event: { message?: string }) => void) | null\n}\n\n/** Options for {@link createWorkerPool}. */\nexport interface WorkerPoolOptions {\n /** Number of workers to spawn. Defaults to 1. */\n size?: number\n /**\n * Factory that creates a worker which evaluates serialized jobs. Injectable so\n * the pool can be unit-tested without a real `Worker` and so the host app\n * controls how the worker module is bundled/loaded.\n */\n createWorker: () => WorkerLike\n}\n\ninterface PendingJob {\n resolve: (value: unknown) => void\n reject: (error: unknown) => void\n /** Index of the worker this job was dispatched to (for crash correlation). */\n workerIndex: number\n}\n\n/**\n * A {@link ThreadPool} backed by Web Workers. Round-robins jobs across `size`\n * workers. The caller supplies `createWorker`; each worker is expected to accept\n * `{ id, source, input }` messages and reply with `{ id, ok, result }` or\n * `{ id, ok: false, error }` (see the reference worker protocol in this package).\n *\n * @remarks Web-only today. The native multi-threaded backend is a research\n * track — see the module docs.\n */\nexport function createWorkerPool(options: WorkerPoolOptions): ThreadPool {\n const count = Math.max(1, options.size ?? 1)\n const workers: WorkerLike[] = []\n const pending = new Map<number, PendingJob>()\n let nextId = 0\n let rr = 0\n let disposed = false\n\n /** Reject and forget every in-flight job dispatched to a given worker. */\n function rejectWorkerJobs(workerIndex: number, message: string): void {\n for (const [id, job] of pending) {\n if (job.workerIndex === workerIndex) {\n pending.delete(id)\n job.reject(new Error(message))\n }\n }\n }\n\n /** Create a worker at `index`, wire its handlers, and store it in place. */\n function spawn(index: number): void {\n const w = options.createWorker()\n w.onmessage = (event) => {\n const data = event.data as { id: number; ok: boolean; result?: unknown; error?: string }\n const job = pending.get(data.id)\n if (!job) return\n pending.delete(data.id)\n if (data.ok) job.resolve(data.result)\n else job.reject(new Error(data.error ?? 'worker job failed'))\n }\n w.onerror = (event) => {\n if (disposed) return\n // A worker crash loses EVERY job in flight on it (a worker can carry many\n // concurrently). Reject all of them — correlated by worker index, so a\n // healthy worker's jobs are never touched — then replace the dead worker in\n // place so the pool stays live and `size` keeps reflecting reality.\n rejectWorkerJobs(index, event.message ?? 'worker error')\n try {\n w.terminate()\n } catch {\n // The worker is already dead; ignore terminate failures.\n }\n spawn(index)\n }\n workers[index] = w\n }\n\n for (let i = 0; i < count; i++) spawn(i)\n\n return {\n run<In, Out>(job: (input: In) => Out, input: In): Promise<Out> {\n if (disposed) return Promise.reject(new Error('ThreadPool is disposed'))\n const index = rr % workers.length\n rr++\n const worker = workers[index]\n if (!worker) return Promise.reject(new Error('no worker available'))\n const id = nextId++\n return new Promise<Out>((resolve, reject) => {\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject, workerIndex: index })\n worker.postMessage({ id, source: job.toString(), input })\n })\n },\n dispose() {\n if (disposed) return\n disposed = true\n for (const w of workers) w.terminate()\n for (const [, job] of pending) job.reject(new Error('ThreadPool disposed'))\n pending.clear()\n },\n get size() {\n return disposed ? 0 : workers.length\n },\n }\n}\n\n/**\n * 🔬 **Research track.** Placeholder for the native (Rust-backed) multi-threaded\n * pool. Not implemented — throws {@link NotImplementedError}. Use\n * {@link createWorkerPool} (web) or {@link createInlineThreadPool} (fallback)\n * today. Tracked for a later phase.\n *\n * @experimental\n */\nexport function createNativeThreadPool(): ThreadPool {\n throw new NotImplementedError('Native multi-threaded ThreadPool')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,yBAAqC;CACnD,IAAI,WAAW;CACf,OAAO;EACL,IAAa,KAAyB,OAAyB;GAC7D,IAAI,UAAU,OAAO,QAAQ,uBAAO,IAAI,MAAM,wBAAwB,CAAC;GACvE,IAAI;IACF,OAAO,QAAQ,QAAQ,IAAI,KAAK,CAAC;GACnC,SAAS,OAAO;IACd,OAAO,QAAQ,OAAO,KAAc;GACtC;EACF;EACA,UAAU;GACR,WAAW;EACb;EACA,MAAM;CACR;AACF;;;;;;;;;;AAsCA,SAAgB,iBAAiB,SAAwC;CACvE,MAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ,QAAQ,CAAC;CAC3C,MAAM,UAAwB,CAAC;CAC/B,MAAM,0BAAU,IAAI,IAAwB;CAC5C,IAAI,SAAS;CACb,IAAI,KAAK;CACT,IAAI,WAAW;;CAGf,SAAS,iBAAiB,aAAqB,SAAuB;EACpE,KAAK,MAAM,CAAC,IAAI,QAAQ,SACtB,IAAI,IAAI,gBAAgB,aAAa;GACnC,QAAQ,OAAO,EAAE;GACjB,IAAI,OAAO,IAAI,MAAM,OAAO,CAAC;EAC/B;CAEJ;;CAGA,SAAS,MAAM,OAAqB;EAClC,MAAM,IAAI,QAAQ,aAAa;EAC/B,EAAE,aAAa,UAAU;GACvB,MAAM,OAAO,MAAM;GACnB,MAAM,MAAM,QAAQ,IAAI,KAAK,EAAE;GAC/B,IAAI,CAAC,KAAK;GACV,QAAQ,OAAO,KAAK,EAAE;GACtB,IAAI,KAAK,IAAI,IAAI,QAAQ,KAAK,MAAM;QAC/B,IAAI,OAAO,IAAI,MAAM,KAAK,SAAS,mBAAmB,CAAC;EAC9D;EACA,EAAE,WAAW,UAAU;GACrB,IAAI,UAAU;GAKd,iBAAiB,OAAO,MAAM,WAAW,cAAc;GACvD,IAAI;IACF,EAAE,UAAU;GACd,QAAQ,CAER;GACA,MAAM,KAAK;EACb;EACA,QAAQ,SAAS;CACnB;CAEA,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,MAAM,CAAC;CAEvC,OAAO;EACL,IAAa,KAAyB,OAAyB;GAC7D,IAAI,UAAU,OAAO,QAAQ,uBAAO,IAAI,MAAM,wBAAwB,CAAC;GACvE,MAAM,QAAQ,KAAK,QAAQ;GAC3B;GACA,MAAM,SAAS,QAAQ;GACvB,IAAI,CAAC,QAAQ,OAAO,QAAQ,uBAAO,IAAI,MAAM,qBAAqB,CAAC;GACnE,MAAM,KAAK;GACX,OAAO,IAAI,SAAc,SAAS,WAAW;IAC3C,QAAQ,IAAI,IAAI;KAAW;KAAiC;KAAQ,aAAa;IAAM,CAAC;IACxF,OAAO,YAAY;KAAE;KAAI,QAAQ,IAAI,SAAS;KAAG;IAAM,CAAC;GAC1D,CAAC;EACH;EACA,UAAU;GACR,IAAI,UAAU;GACd,WAAW;GACX,KAAK,MAAM,KAAK,SAAS,EAAE,UAAU;GACrC,KAAK,MAAM,GAAG,QAAQ,SAAS,IAAI,uBAAO,IAAI,MAAM,qBAAqB,CAAC;GAC1E,QAAQ,MAAM;EAChB;EACA,IAAI,OAAO;GACT,OAAO,WAAW,IAAI,QAAQ;EAChC;CACF;AACF;;;;;;;;;AAUA,SAAgB,yBAAqC;CACnD,MAAM,IAAI,oBAAoB,kCAAkC;AAClE"}
1
+ {"version":3,"file":"thread-pool.js","names":[],"sources":["../../src/threading/thread-pool.ts"],"sourcesContent":["/**\n * MindeesNative threading abstraction.\n *\n * Defines the **contract** for offloading CPU-bound work off the main thread, so\n * the rest of the framework can parallelize without binding to a specific\n * backend. Two backends are anticipated:\n *\n * - **Web Worker backend** (`createWorkerPool`) — works today on web; runs each\n * job in a `Worker`.\n * - **Native multi-thread backend** — 🔬 research track. The interface is\n * defined here so the architecture is real; a native Rust-backed scheduler\n * will implement {@link ThreadPool} in a later phase. Until then,\n * {@link createInlineThreadPool} provides a correct, synchronous fallback so\n * callers are never blocked (Working-Code Doctrine: a real fallback, not a\n * lying stub).\n *\n * @module\n */\n\nimport { NotImplementedError } from '../errors'\n\n/**\n * A pool that runs a pure job function with a transferable argument and resolves\n * its result. Implementations may run jobs on worker threads, native threads, or\n * (as a fallback) inline on the calling thread.\n */\nexport interface ThreadPool {\n /**\n * Run `job(input)` and resolve its result.\n *\n * `job` must be a **pure, self-contained function** (it may be serialized and\n * re-created in another realm, so it cannot close over outer variables in\n * worker/native backends). `input` must be structured-cloneable.\n */\n run<In, Out>(job: (input: In) => Out, input: In): Promise<Out>\n /** Release all underlying resources (terminate workers, etc.). Idempotent. */\n dispose(): void\n /** Number of live workers/threads (0 for the inline fallback). */\n readonly size: number\n}\n\n/**\n * A synchronous, single-realm {@link ThreadPool}: runs each job inline on the\n * calling thread. This is the universal fallback — correct everywhere, with no\n * parallelism. Use it where workers aren't available (or in tests).\n */\nexport function createInlineThreadPool(): ThreadPool {\n let disposed = false\n return {\n run<In, Out>(job: (input: In) => Out, input: In): Promise<Out> {\n if (disposed) return Promise.reject(new Error('ThreadPool is disposed'))\n try {\n return Promise.resolve(job(input))\n } catch (error) {\n return Promise.reject(error as Error)\n }\n },\n dispose() {\n disposed = true\n },\n size: 0,\n }\n}\n\n/** Minimal structural subset of the DOM `Worker` we depend on. */\nexport interface WorkerLike {\n postMessage(message: unknown): void\n terminate(): void\n onmessage: ((event: { data: unknown }) => void) | null\n onerror: ((event: { message?: string }) => void) | null\n}\n\n/** Options for {@link createWorkerPool}. */\nexport interface WorkerPoolOptions {\n /** Number of workers to spawn. Defaults to 1. */\n size?: number\n /**\n * Factory that creates a worker which evaluates serialized jobs. Injectable so\n * the pool can be unit-tested without a real `Worker` and so the host app\n * controls how the worker module is bundled/loaded.\n */\n createWorker: () => WorkerLike\n}\n\ninterface PendingJob {\n resolve: (value: unknown) => void\n reject: (error: unknown) => void\n /** Index of the worker this job was dispatched to (for crash correlation). */\n workerIndex: number\n}\n\n/**\n * A {@link ThreadPool} backed by Web Workers. Round-robins jobs across `size`\n * workers. The caller supplies `createWorker`; each worker is expected to accept\n * `{ id, source, input }` messages and reply with `{ id, ok, result }` or\n * `{ id, ok: false, error }` (see the reference worker protocol in this package).\n *\n * @remarks Web-only today. The native multi-threaded backend is a research\n * track — see the module docs.\n */\nexport function createWorkerPool(options: WorkerPoolOptions): ThreadPool {\n const count = Math.max(1, options.size ?? 1)\n const workers: WorkerLike[] = []\n const pending = new Map<number, PendingJob>()\n let nextId = 0\n let rr = 0\n let disposed = false\n\n /** Reject and forget every in-flight job dispatched to a given worker. */\n function rejectWorkerJobs(workerIndex: number, message: string): void {\n for (const [id, job] of pending) {\n if (job.workerIndex === workerIndex) {\n pending.delete(id)\n job.reject(new Error(message))\n }\n }\n }\n\n /** Create a worker at `index`, wire its handlers, and store it in place. */\n function spawn(index: number): void {\n const w = options.createWorker()\n w.onmessage = (event) => {\n const data = event.data as { id: number; ok: boolean; result?: unknown; error?: string }\n const job = pending.get(data.id)\n if (!job) return\n pending.delete(data.id)\n if (data.ok) job.resolve(data.result)\n else job.reject(new Error(data.error ?? 'worker job failed'))\n }\n w.onerror = (event) => {\n // Ignore a late/duplicate error from a worker that was already replaced at this index — without\n // the identity check it would wrongly reject the LIVE replacement's jobs and evict it (same index).\n if (disposed || workers[index] !== w) return\n // A worker crash loses EVERY job in flight on it (a worker can carry many\n // concurrently). Reject all of them — correlated by worker index, so a\n // healthy worker's jobs are never touched — then replace the dead worker in\n // place so the pool stays live and `size` keeps reflecting reality.\n rejectWorkerJobs(index, event.message ?? 'worker error')\n try {\n w.terminate()\n } catch {\n // The worker is already dead; ignore terminate failures.\n }\n spawn(index)\n }\n workers[index] = w\n }\n\n for (let i = 0; i < count; i++) spawn(i)\n\n return {\n run<In, Out>(job: (input: In) => Out, input: In): Promise<Out> {\n if (disposed) return Promise.reject(new Error('ThreadPool is disposed'))\n const index = rr % workers.length\n rr++\n const worker = workers[index]\n if (!worker) return Promise.reject(new Error('no worker available'))\n const id = nextId++\n return new Promise<Out>((resolve, reject) => {\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject, workerIndex: index })\n worker.postMessage({ id, source: job.toString(), input })\n })\n },\n dispose() {\n if (disposed) return\n disposed = true\n for (const w of workers) w.terminate()\n for (const [, job] of pending) job.reject(new Error('ThreadPool disposed'))\n pending.clear()\n },\n get size() {\n return disposed ? 0 : workers.length\n },\n }\n}\n\n/**\n * 🔬 **Research track.** Placeholder for the native (Rust-backed) multi-threaded\n * pool. Not implemented — throws {@link NotImplementedError}. Use\n * {@link createWorkerPool} (web) or {@link createInlineThreadPool} (fallback)\n * today. Tracked for a later phase.\n *\n * @experimental\n */\nexport function createNativeThreadPool(): ThreadPool {\n throw new NotImplementedError('Native multi-threaded ThreadPool')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,yBAAqC;CACnD,IAAI,WAAW;CACf,OAAO;EACL,IAAa,KAAyB,OAAyB;GAC7D,IAAI,UAAU,OAAO,QAAQ,uBAAO,IAAI,MAAM,wBAAwB,CAAC;GACvE,IAAI;IACF,OAAO,QAAQ,QAAQ,IAAI,KAAK,CAAC;GACnC,SAAS,OAAO;IACd,OAAO,QAAQ,OAAO,KAAc;GACtC;EACF;EACA,UAAU;GACR,WAAW;EACb;EACA,MAAM;CACR;AACF;;;;;;;;;;AAsCA,SAAgB,iBAAiB,SAAwC;CACvE,MAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ,QAAQ,CAAC;CAC3C,MAAM,UAAwB,CAAC;CAC/B,MAAM,0BAAU,IAAI,IAAwB;CAC5C,IAAI,SAAS;CACb,IAAI,KAAK;CACT,IAAI,WAAW;;CAGf,SAAS,iBAAiB,aAAqB,SAAuB;EACpE,KAAK,MAAM,CAAC,IAAI,QAAQ,SACtB,IAAI,IAAI,gBAAgB,aAAa;GACnC,QAAQ,OAAO,EAAE;GACjB,IAAI,OAAO,IAAI,MAAM,OAAO,CAAC;EAC/B;CAEJ;;CAGA,SAAS,MAAM,OAAqB;EAClC,MAAM,IAAI,QAAQ,aAAa;EAC/B,EAAE,aAAa,UAAU;GACvB,MAAM,OAAO,MAAM;GACnB,MAAM,MAAM,QAAQ,IAAI,KAAK,EAAE;GAC/B,IAAI,CAAC,KAAK;GACV,QAAQ,OAAO,KAAK,EAAE;GACtB,IAAI,KAAK,IAAI,IAAI,QAAQ,KAAK,MAAM;QAC/B,IAAI,OAAO,IAAI,MAAM,KAAK,SAAS,mBAAmB,CAAC;EAC9D;EACA,EAAE,WAAW,UAAU;GAGrB,IAAI,YAAY,QAAQ,WAAW,GAAG;GAKtC,iBAAiB,OAAO,MAAM,WAAW,cAAc;GACvD,IAAI;IACF,EAAE,UAAU;GACd,QAAQ,CAER;GACA,MAAM,KAAK;EACb;EACA,QAAQ,SAAS;CACnB;CAEA,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,MAAM,CAAC;CAEvC,OAAO;EACL,IAAa,KAAyB,OAAyB;GAC7D,IAAI,UAAU,OAAO,QAAQ,uBAAO,IAAI,MAAM,wBAAwB,CAAC;GACvE,MAAM,QAAQ,KAAK,QAAQ;GAC3B;GACA,MAAM,SAAS,QAAQ;GACvB,IAAI,CAAC,QAAQ,OAAO,QAAQ,uBAAO,IAAI,MAAM,qBAAqB,CAAC;GACnE,MAAM,KAAK;GACX,OAAO,IAAI,SAAc,SAAS,WAAW;IAC3C,QAAQ,IAAI,IAAI;KAAW;KAAiC;KAAQ,aAAa;IAAM,CAAC;IACxF,OAAO,YAAY;KAAE;KAAI,QAAQ,IAAI,SAAS;KAAG;IAAM,CAAC;GAC1D,CAAC;EACH;EACA,UAAU;GACR,IAAI,UAAU;GACd,WAAW;GACX,KAAK,MAAM,KAAK,SAAS,EAAE,UAAU;GACrC,KAAK,MAAM,GAAG,QAAQ,SAAS,IAAI,uBAAO,IAAI,MAAM,qBAAqB,CAAC;GAC1E,QAAQ,MAAM;EAChB;EACA,IAAI,OAAO;GACT,OAAO,WAAW,IAAI,QAAQ;EAChC;CACF;AACF;;;;;;;;;AAUA,SAAgB,yBAAqC;CACnD,MAAM,IAAI,oBAAoB,kCAAkC;AAClE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindees/core",
3
- "version": "0.22.4",
3
+ "version": "0.22.6",
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",