@mindees/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,7 @@
1
1
  //#region src/scheduler/scheduler.ts
2
+ /** Safety valve: a single {@link Scheduler.flushSync} that drains more than this many tasks is
3
+ * treated as a runaway re-scheduling loop and aborted (mirrors the reactive flush guard). */
4
+ const MAX_DRAIN_ITERATIONS = 1e5;
2
5
  const defaultScheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
3
6
  Promise.resolve().then(cb);
4
7
  };
@@ -42,7 +45,17 @@ var Scheduler = class {
42
45
  if (this.flushing) return;
43
46
  this.flushing = true;
44
47
  try {
48
+ let iterations = 0;
45
49
  while (this.sync.length > 0 || this.normal.length > 0) {
50
+ if (++iterations > MAX_DRAIN_ITERATIONS) {
51
+ this.sync.length = 0;
52
+ this.normal.length = 0;
53
+ this.keyed.clear();
54
+ this.run(() => {
55
+ throw new Error("MindeesNative: potential infinite scheduler loop — a task kept re-scheduling itself.");
56
+ });
57
+ return;
58
+ }
46
59
  const entry = this.sync.length > 0 ? this.sync.shift() : this.normal.shift();
47
60
  if (!entry) continue;
48
61
  this.clearKey(entry);
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.js","names":[],"sources":["../../src/scheduler/scheduler.ts"],"sourcesContent":["/**\n * MindeesNative scheduler — a small, deterministic priority scheduler.\n *\n * Two lanes:\n * - **`sync`** — high-priority work (interaction handlers, first frame). Drained\n * synchronously at the next flush point and always before the normal lane.\n * - **`normal`** — default work, drained on a microtask so multiple schedules in\n * the same tick coalesce into one flush.\n *\n * Tasks are **cancellable** (via the returned handle) and **dedupable** (two\n * tasks scheduled with the same `key` collapse to one — the latest callback\n * wins, preserving the earlier queue position). The scheduler never throws from\n * a task into the caller: task errors are collected and reported via an optional\n * `onError` hook, so one bad task can't stop the rest of the flush.\n *\n * @module\n */\n\n/** Scheduling lanes, highest priority first. */\nexport type Priority = 'sync' | 'normal'\n\n/** A unit of scheduled work. */\nexport type Task = () => void\n\n/** Options for {@link Scheduler.schedule}. */\nexport interface ScheduleOptions {\n /** Lane to run in. Defaults to `'normal'`. */\n priority?: Priority\n /**\n * Dedup key. Scheduling again with the same key replaces the pending task's\n * callback (latest wins) instead of enqueuing a second one.\n */\n key?: string\n}\n\n/** A handle to a scheduled task. */\nexport interface ScheduledTask {\n /** Remove the task if it hasn't run yet. Idempotent. */\n cancel(): void\n /** Whether the task is still pending (not yet run or cancelled). */\n readonly pending: boolean\n}\n\ninterface Entry {\n key: string | null\n fn: Task | null // null once cancelled\n}\n\n/** Options for {@link Scheduler}. */\nexport interface SchedulerOptions {\n /**\n * Called with any error thrown by a task. If omitted, errors are rethrown\n * asynchronously (so they surface to the host without aborting the flush).\n */\n onError?: (error: unknown) => void\n /**\n * Schedules a microtask. Injectable for testing; defaults to `queueMicrotask`.\n */\n scheduleMicrotask?: (cb: () => void) => void\n}\n\nconst defaultScheduleMicrotask: (cb: () => void) => void =\n typeof queueMicrotask === 'function'\n ? queueMicrotask\n : (cb) => {\n void Promise.resolve().then(cb)\n }\n\n/**\n * A deterministic two-lane priority scheduler. Create one with {@link createScheduler}.\n */\nexport class Scheduler {\n private readonly sync: Entry[] = []\n private readonly normal: Entry[] = []\n private readonly keyed = new Map<string, Entry>()\n private microtaskQueued = false\n private flushing = false\n private readonly onError: ((error: unknown) => void) | undefined\n private readonly scheduleMicrotask: (cb: () => void) => void\n\n constructor(options?: SchedulerOptions) {\n this.onError = options?.onError\n this.scheduleMicrotask = options?.scheduleMicrotask ?? defaultScheduleMicrotask\n }\n\n /** Schedule `task`. Returns a handle to cancel it or check its status. */\n schedule(task: Task, options?: ScheduleOptions): ScheduledTask {\n const priority = options?.priority ?? 'normal'\n const key = options?.key ?? null\n\n if (key !== null) {\n const existing = this.keyed.get(key)\n if (existing && existing.fn !== null) {\n // Dedup: replace the callback, keep the existing queue position.\n existing.fn = task\n return this.makeHandle(existing)\n }\n }\n\n const entry: Entry = { key, fn: task }\n if (key !== null) this.keyed.set(key, entry)\n ;(priority === 'sync' ? this.sync : this.normal).push(entry)\n this.requestFlush()\n return this.makeHandle(entry)\n }\n\n /** Run all pending tasks right now (sync lane first), draining both lanes. */\n flushSync(): void {\n if (this.flushing) return\n this.flushing = true\n try {\n // Drain in priority order. Re-check each loop so tasks scheduled by tasks\n // (e.g. a sync task that queues normal work) are handled in this flush.\n while (this.sync.length > 0 || this.normal.length > 0) {\n const entry = this.sync.length > 0 ? this.sync.shift() : this.normal.shift()\n if (!entry) continue\n this.clearKey(entry)\n const fn = entry.fn\n entry.fn = null\n if (fn) this.run(fn)\n }\n } finally {\n this.flushing = false\n this.microtaskQueued = false\n }\n }\n\n /** Number of pending tasks across both lanes (cancelled tasks excluded). */\n get size(): number {\n let n = 0\n for (const e of this.sync) if (e.fn !== null) n++\n for (const e of this.normal) if (e.fn !== null) n++\n return n\n }\n\n private requestFlush(): void {\n if (this.microtaskQueued || this.flushing) return\n this.microtaskQueued = true\n this.scheduleMicrotask(() => {\n this.microtaskQueued = false\n this.flushSync()\n })\n }\n\n private run(fn: Task): void {\n try {\n fn()\n } catch (error) {\n if (this.onError) {\n try {\n this.onError(error)\n } catch (hookError) {\n // A throwing onError hook must not abort the flush or strand the tasks\n // queued after it; surface it asynchronously like an unhandled task error.\n this.scheduleMicrotask(() => {\n throw hookError\n })\n }\n } else {\n // Surface without aborting the flush.\n this.scheduleMicrotask(() => {\n throw error\n })\n }\n }\n }\n\n /**\n * Remove an entry's dedup-key mapping, but only if the map still points at THIS\n * entry. Keys are reused over time (a 'render' key is scheduled, runs, then\n * scheduled again), so a stale handle or an already-dequeued entry must never\n * evict a newer live entry's mapping — doing so would break dedup and let two\n * same-key tasks both run.\n */\n private clearKey(entry: Entry): void {\n if (entry.key !== null && this.keyed.get(entry.key) === entry) {\n this.keyed.delete(entry.key)\n }\n }\n\n private makeHandle(entry: Entry): ScheduledTask {\n return {\n cancel: () => {\n entry.fn = null\n this.clearKey(entry)\n },\n get pending() {\n return entry.fn !== null\n },\n }\n }\n}\n\n/** Create a new {@link Scheduler}. */\nexport function createScheduler(options?: SchedulerOptions): Scheduler {\n return new Scheduler(options)\n}\n"],"mappings":";AA6DA,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,OAAO,KAAK,KAAK,SAAS,KAAK,KAAK,OAAO,SAAS,GAAG;IACrD,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}\n\n/** Options for {@link Scheduler}. */\nexport interface SchedulerOptions {\n /**\n * Called with any error thrown by a task. If omitted, errors are rethrown\n * asynchronously (so they surface to the host without aborting the flush).\n */\n onError?: (error: unknown) => void\n /**\n * Schedules a microtask. Injectable for testing; defaults to `queueMicrotask`.\n */\n scheduleMicrotask?: (cb: () => void) => void\n}\n\nconst defaultScheduleMicrotask: (cb: () => void) => void =\n typeof queueMicrotask === 'function'\n ? queueMicrotask\n : (cb) => {\n void Promise.resolve().then(cb)\n }\n\n/**\n * A deterministic two-lane priority scheduler. Create one with {@link createScheduler}.\n */\nexport class Scheduler {\n private readonly sync: Entry[] = []\n private readonly normal: Entry[] = []\n private readonly keyed = new Map<string, Entry>()\n private microtaskQueued = false\n private flushing = false\n private readonly onError: ((error: unknown) => void) | undefined\n private readonly scheduleMicrotask: (cb: () => void) => void\n\n constructor(options?: SchedulerOptions) {\n this.onError = options?.onError\n this.scheduleMicrotask = options?.scheduleMicrotask ?? defaultScheduleMicrotask\n }\n\n /** Schedule `task`. Returns a handle to cancel it or check its status. */\n schedule(task: Task, options?: ScheduleOptions): ScheduledTask {\n const priority = options?.priority ?? 'normal'\n const key = options?.key ?? null\n\n if (key !== null) {\n const existing = this.keyed.get(key)\n if (existing && existing.fn !== null) {\n // Dedup: replace the callback, keep the existing queue position.\n existing.fn = task\n return this.makeHandle(existing)\n }\n }\n\n const entry: Entry = { key, fn: task }\n if (key !== null) this.keyed.set(key, entry)\n ;(priority === 'sync' ? this.sync : this.normal).push(entry)\n this.requestFlush()\n return this.makeHandle(entry)\n }\n\n /** Run all pending tasks right now (sync lane first), draining both lanes. */\n flushSync(): void {\n if (this.flushing) return\n this.flushing = true\n try {\n // Drain in priority order. Re-check each loop so tasks scheduled by tasks\n // (e.g. a sync task that queues normal work) are handled in this flush.\n let iterations = 0\n while (this.sync.length > 0 || this.normal.length > 0) {\n // Safety valve: a task that perpetually re-schedules (e.g. two deferred reactive effects\n // writing each other's deps) would drain forever. Cap it, clear both lanes, and surface\n // an error — mirroring the reactive loop guard so a deferred cycle fails loudly, not hangs.\n if (++iterations > MAX_DRAIN_ITERATIONS) {\n this.sync.length = 0\n this.normal.length = 0\n this.keyed.clear()\n this.run(() => {\n throw new Error(\n 'MindeesNative: potential infinite scheduler loop — a task kept re-scheduling itself.',\n )\n })\n return\n }\n const entry = this.sync.length > 0 ? this.sync.shift() : this.normal.shift()\n if (!entry) continue\n this.clearKey(entry)\n const fn = entry.fn\n entry.fn = null\n if (fn) this.run(fn)\n }\n } finally {\n this.flushing = false\n this.microtaskQueued = false\n }\n }\n\n /** Number of pending tasks across both lanes (cancelled tasks excluded). */\n get size(): number {\n let n = 0\n for (const e of this.sync) if (e.fn !== null) n++\n for (const e of this.normal) if (e.fn !== null) n++\n return n\n }\n\n private requestFlush(): void {\n if (this.microtaskQueued || this.flushing) return\n this.microtaskQueued = true\n this.scheduleMicrotask(() => {\n this.microtaskQueued = false\n this.flushSync()\n })\n }\n\n private run(fn: Task): void {\n try {\n fn()\n } catch (error) {\n if (this.onError) {\n try {\n this.onError(error)\n } catch (hookError) {\n // A throwing onError hook must not abort the flush or strand the tasks\n // queued after it; surface it asynchronously like an unhandled task error.\n this.scheduleMicrotask(() => {\n throw hookError\n })\n }\n } else {\n // Surface without aborting the flush.\n this.scheduleMicrotask(() => {\n throw error\n })\n }\n }\n }\n\n /**\n * Remove an entry's dedup-key mapping, but only if the map still points at THIS\n * entry. Keys are reused over time (a 'render' key is scheduled, runs, then\n * scheduled again), so a stale handle or an already-dequeued entry must never\n * evict a newer live entry's mapping — doing so would break dedup and let two\n * same-key tasks both run.\n */\n private clearKey(entry: Entry): void {\n if (entry.key !== null && this.keyed.get(entry.key) === entry) {\n this.keyed.delete(entry.key)\n }\n }\n\n private makeHandle(entry: Entry): ScheduledTask {\n return {\n cancel: () => {\n entry.fn = null\n this.clearKey(entry)\n },\n get pending() {\n return entry.fn !== null\n },\n }\n }\n}\n\n/** Create a new {@link Scheduler}. */\nexport function createScheduler(options?: SchedulerOptions): Scheduler {\n return new Scheduler(options)\n}\n"],"mappings":";;;AAqCA,MAAM,uBAAuB;AA4B7B,MAAM,2BACJ,OAAO,mBAAmB,aACtB,kBACC,OAAO;CACN,QAAa,QAAQ,EAAE,KAAK,EAAE;AAChC;;;;AAKN,IAAa,YAAb,MAAuB;CACrB,OAAiC,CAAC;CAClC,SAAmC,CAAC;CACpC,wBAAyB,IAAI,IAAmB;CAChD,kBAA0B;CAC1B,WAAmB;CACnB;CACA;CAEA,YAAY,SAA4B;EACtC,KAAK,UAAU,SAAS;EACxB,KAAK,oBAAoB,SAAS,qBAAqB;CACzD;;CAGA,SAAS,MAAY,SAA0C;EAC7D,MAAM,WAAW,SAAS,YAAY;EACtC,MAAM,MAAM,SAAS,OAAO;EAE5B,IAAI,QAAQ,MAAM;GAChB,MAAM,WAAW,KAAK,MAAM,IAAI,GAAG;GACnC,IAAI,YAAY,SAAS,OAAO,MAAM;IAEpC,SAAS,KAAK;IACd,OAAO,KAAK,WAAW,QAAQ;GACjC;EACF;EAEA,MAAM,QAAe;GAAE;GAAK,IAAI;EAAK;EACrC,IAAI,QAAQ,MAAM,KAAK,MAAM,IAAI,KAAK,KAAK;EAC1C,CAAC,aAAa,SAAS,KAAK,OAAO,KAAK,QAAQ,KAAK,KAAK;EAC3D,KAAK,aAAa;EAClB,OAAO,KAAK,WAAW,KAAK;CAC9B;;CAGA,YAAkB;EAChB,IAAI,KAAK,UAAU;EACnB,KAAK,WAAW;EAChB,IAAI;GAGF,IAAI,aAAa;GACjB,OAAO,KAAK,KAAK,SAAS,KAAK,KAAK,OAAO,SAAS,GAAG;IAIrD,IAAI,EAAE,aAAa,sBAAsB;KACvC,KAAK,KAAK,SAAS;KACnB,KAAK,OAAO,SAAS;KACrB,KAAK,MAAM,MAAM;KACjB,KAAK,UAAU;MACb,MAAM,IAAI,MACR,sFACF;KACF,CAAC;KACD;IACF;IACA,MAAM,QAAQ,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,OAAO,MAAM;IAC3E,IAAI,CAAC,OAAO;IACZ,KAAK,SAAS,KAAK;IACnB,MAAM,KAAK,MAAM;IACjB,MAAM,KAAK;IACX,IAAI,IAAI,KAAK,IAAI,EAAE;GACrB;EACF,UAAU;GACR,KAAK,WAAW;GAChB,KAAK,kBAAkB;EACzB;CACF;;CAGA,IAAI,OAAe;EACjB,IAAI,IAAI;EACR,KAAK,MAAM,KAAK,KAAK,MAAM,IAAI,EAAE,OAAO,MAAM;EAC9C,KAAK,MAAM,KAAK,KAAK,QAAQ,IAAI,EAAE,OAAO,MAAM;EAChD,OAAO;CACT;CAEA,eAA6B;EAC3B,IAAI,KAAK,mBAAmB,KAAK,UAAU;EAC3C,KAAK,kBAAkB;EACvB,KAAK,wBAAwB;GAC3B,KAAK,kBAAkB;GACvB,KAAK,UAAU;EACjB,CAAC;CACH;CAEA,IAAY,IAAgB;EAC1B,IAAI;GACF,GAAG;EACL,SAAS,OAAO;GACd,IAAI,KAAK,SACP,IAAI;IACF,KAAK,QAAQ,KAAK;GACpB,SAAS,WAAW;IAGlB,KAAK,wBAAwB;KAC3B,MAAM;IACR,CAAC;GACH;QAGA,KAAK,wBAAwB;IAC3B,MAAM;GACR,CAAC;EAEL;CACF;;;;;;;;CASA,SAAiB,OAAoB;EACnC,IAAI,MAAM,QAAQ,QAAQ,KAAK,MAAM,IAAI,MAAM,GAAG,MAAM,OACtD,KAAK,MAAM,OAAO,MAAM,GAAG;CAE/B;CAEA,WAAmB,OAA6B;EAC9C,OAAO;GACL,cAAc;IACZ,MAAM,KAAK;IACX,KAAK,SAAS,KAAK;GACrB;GACA,IAAI,UAAU;IACZ,OAAO,MAAM,OAAO;GACtB;EACF;CACF;AACF;;AAGA,SAAgB,gBAAgB,SAAuC;CACrE,OAAO,IAAI,UAAU,OAAO;AAC9B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindees/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "MindeesNative core — fine-grained reactivity (signals/computed/effect/batch), component model with selector-isolated context, priority scheduler, and a thread-pool abstraction (Web Worker + inline; native is a research track).",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",