@orkestrel/workflow 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,8 @@
1
1
  # @orkestrel/workflow
2
2
 
3
- A typed workflow engine for the `@orkestrel` line a serializable
4
- `Workflow → Phase → Task` tree that a UI or an LLM authors as pure JSON, and
5
- a PURE `WorkflowRunner` engine executes by COMPOSING the shipped substrate (a
6
- per-phase `Runner`, `Abort`, `Timeout`, `Budget`, and a cooperative
7
- cross-environment `Scheduler`) rather than re-implementing its own
8
- concurrency / retry / abort machinery. Each task's `run` is a plain
9
- behavior-name string resolved once into a `handler`; wiring a task to a tool
10
- or an agent is an opt-in adapter shipped by the separate `@orkestrel/tool`
11
- package, composed into the caller's own `functions` registry.
3
+ A typed, host-independent workflow engine for the `@orkestrel` line. It keeps
4
+ work as a serializable `Workflow → Phase → Task` tree and executes task behavior
5
+ through a caller-supplied function registry on a cooperative scheduler.
12
6
 
13
7
  ## Install
14
8
 
@@ -25,30 +19,29 @@ npm install @orkestrel/workflow
25
19
 
26
20
  ## Status
27
21
 
28
- Pre-release (`0.0.1`): the definition contract, the live entity tree, the
29
- pure-engine runner (a `run`-string / `handler` model the opt-in tool/agent
30
- adapters and the depth/cycle-bounded agent-native recursion now ship in the
31
- separate `@orkestrel/tool` package), the durable `WorkflowStore`
32
- (in-memory + driver-pluggable), and the cooperative `Scheduler` (the
33
- cross-environment default plus the browser and Node environment backends)
34
- are all implemented and tested, but the public API is still unstable and
35
- may change without notice. The live tree also supports runtime-only
36
- `pause` / `resume` / `wait` (at both the workflow and phase tiers) plus a
37
- gated `add` / `remove` / `move` / `update` structural-mutation API
38
- (pending-suffix positions, append-only on a running phase), a hard
39
- `destroy` teardown alongside the runner's existing graceful `stop`, and a
40
- `WorkflowRunnerInterface.execute` overload that drives an already-built,
41
- caller-owned live tree instead of only building one from a definition. See
42
- [guides/src/workflow.md](./guides/src/workflow.md) for the full documented
43
- surface.
22
+ Pre-release: the definition contract, live entity tree, runner,
23
+ cooperative schedulers, and durable stores are implemented and tested. A task
24
+ can publish its current note, progress, operations, constraints, pulse, and
25
+ signal; observers can derive silence without polling. Workflow, phase, and task
26
+ execution can pause, resume, wait, skip, stop, and destroy according to the
27
+ documented lifecycle.
28
+
29
+ Snapshots are exact JSON values with owned nested data. Attempts, checkpoints,
30
+ settlements, and final state can be persisted through memory or database-backed
31
+ stores, then explicitly restored or recovered without reusing a consumed
32
+ attempt. Runtime pause gates are intentionally not persisted.
33
+
34
+ Provider sessions, external processes, MCP projection, journals, leases, and
35
+ distributed fencing remain integration concerns rather than core workflow
36
+ behavior. See [guides/src/workflow.md](./guides/src/workflow.md) for the complete
37
+ shipped contract. The proposed integration architecture now lives with the
38
+ package that implements it, as `plan/PROPOSAL.md` in `@orkestrel/supervisor`.
44
39
 
45
40
  ## Package
46
41
 
47
- Published as three environment-scoped entry points per the `exports` field
48
- in `package.json`: `.` (the shared, environment-agnostic core — the
49
- definition/entity/runner surface plus the cross-environment `Scheduler`
50
- default), `./browser` (adds the browser-native scheduler backends), and
51
- `./server` (adds the Node-native scheduler backend). Core ships dual
42
+ Published as three environment-scoped entry points: `.` provides the shared
43
+ environment-agnostic core and default scheduler, `./browser` adds browser-native
44
+ schedulers, and `./server` adds the Node-native scheduler. Core ships dual
52
45
  ESM+CJS builds; `./browser` is ESM-only.
53
46
 
54
47
  ## License
@@ -16,23 +16,17 @@ import { SchedulerPriority } from '../core/index.ts';
16
16
  * never an `as` (AGENTS §14). Where the API is absent (Firefox today, older engines),
17
17
  * it **falls back** to a `setTimeout(0)` macrotask — still a real host-turn, just
18
18
  * without priority. `delay(ms)` is always a real `setTimeout`.
19
- * - **Abort fidelity is verbatim.** A pending `yield` / `delay` rejects with `signal.reason`
20
- * exactly the value the caller passed, never wrapped or replaced. The discipline
21
- * mirrors the cross-environment default's `#sleep`: an already-aborted signal rejects
22
- * immediately WITHOUT scheduling; otherwise the host-turn is scheduled and a
23
- * `{ once: true }` abort listener attached, and the two settle paths are mutually
24
- * exclusive — the turn path removes the listener before resolving, and the abort path
25
- * cancels the scheduled turn before rejecting. The promise settles exactly once, with no
26
- * leaked task/timer and no leaked listener. The caller's `signal` is NOT handed to
27
- * `postTask` (whose own abort would reject with a platform `AbortError`, not the
28
- * caller's `reason`); instead an internal controller cancels the posted task while this
29
- * scheduler rejects with the verbatim `signal.reason`.
19
+ * - **Abort fidelity is verbatim.** The shared `scheduleHost` lifecycle links an owned
20
+ * settlement composite before scheduling, preserving the exact caller reason without
21
+ * invoking caller-owned signal methods. The caller signal is NOT handed to `postTask`;
22
+ * an internal controller cancels that native task. An unexpected native promise rejection
23
+ * is routed back as the exact host failure instead of being discarded.
30
24
  * - **Event-free.** A pure functional primitive — no Emitter, no events.
31
25
  *
32
26
  * @example
33
27
  * ```ts
34
- * import { createAbort } from '@src/core'
35
- * import { BrowserScheduler } from '@src/browser'
28
+ * import { createAbort } from '@orkestrel/abort'
29
+ * import { BrowserScheduler } from '@orkestrel/workflow/browser'
36
30
  *
37
31
  * const abort = createAbort()
38
32
  * const scheduler = new BrowserScheduler()
@@ -73,15 +67,16 @@ export declare class BrowserScheduler implements SchedulerInterface {
73
67
  * The default browser scheduler: it honours `options.priority` (`user` /
74
68
  * `normal` / `background`) when `scheduler.postTask` is available and degrades to a plain
75
69
  * macrotask elsewhere. Both methods are abort-aware: pass `options.signal` and a pending
76
- * yield/delay rejects with the signal's `reason` verbatim, with full task/timer/listener
77
- * cleanup. Prefer {@link createFrameScheduler} for paint-aligned work or
70
+ * yield/delay rejects with the signal's exact `reason`; the shared owned-signal lifecycle
71
+ * cancels the native handle without invoking caller listener methods. Prefer
72
+ * {@link createFrameScheduler} for paint-aligned work or
78
73
  * {@link createIdleScheduler} for idle-time background work.
79
74
  *
80
75
  * @returns A {@link SchedulerInterface} backed by `scheduler.postTask` (or a macrotask)
81
76
  *
82
77
  * @example
83
78
  * ```ts
84
- * import { createBrowserScheduler } from '@src/browser'
79
+ * import { createBrowserScheduler } from '@orkestrel/workflow/browser'
85
80
  *
86
81
  * const scheduler = createBrowserScheduler()
87
82
  * await scheduler.yield({ priority: 'background' })
@@ -105,7 +100,7 @@ export declare function createBrowserScheduler(): SchedulerInterface;
105
100
  *
106
101
  * @example
107
102
  * ```ts
108
- * import { createFrameScheduler } from '@src/browser'
103
+ * import { createFrameScheduler } from '@orkestrel/workflow/browser'
109
104
  *
110
105
  * const scheduler = createFrameScheduler()
111
106
  * await scheduler.yield() // resumes before the next paint
@@ -129,7 +124,7 @@ export declare function createFrameScheduler(): SchedulerInterface;
129
124
  *
130
125
  * @example
131
126
  * ```ts
132
- * import { createIdleScheduler } from '@src/browser'
127
+ * import { createIdleScheduler } from '@orkestrel/workflow/browser'
133
128
  *
134
129
  * const scheduler = createIdleScheduler()
135
130
  * await scheduler.yield() // resumes when the host is idle
@@ -148,20 +143,15 @@ export declare function createIdleScheduler(): SchedulerInterface;
148
143
  * is hidden (the host throttles rAF). `delay(ms)` is a real `setTimeout`, unaligned to
149
144
  * frames. `options.priority` is accepted for contract compliance but a no-op — a frame
150
145
  * callback has no priority dimension.
151
- * - **Abort fidelity is verbatim, with cleanup.** A pending `yield` / `delay` rejects with
152
- * `signal.reason` exactly. The discipline mirrors the cross-environment default's
153
- * `#sleep`: an already-aborted signal rejects immediately WITHOUT scheduling a frame;
154
- * otherwise the frame is requested and a `{ once: true }` abort listener attached, and
155
- * the two settle paths are mutually exclusive — the frame path removes the listener
156
- * before resolving, and the abort path `cancelAnimationFrame`s the pending handle before
157
- * rejecting. The promise settles exactly once, with no leaked frame request and no
158
- * leaked listener.
146
+ * - **Abort fidelity is verbatim, with cleanup.** The shared `scheduleHost` lifecycle links
147
+ * an owned settlement composite before requesting a frame, never invokes caller-owned
148
+ * signal methods, and cancels the native handle when abort wins.
159
149
  * - **Event-free.** A pure functional primitive — no Emitter, no events.
160
150
  *
161
151
  * @example
162
152
  * ```ts
163
- * import { createAbort } from '@src/core'
164
- * import { FrameScheduler } from '@src/browser'
153
+ * import { createAbort } from '@orkestrel/abort'
154
+ * import { FrameScheduler } from '@orkestrel/workflow/browser'
165
155
  *
166
156
  * const abort = createAbort()
167
157
  * const scheduler = new FrameScheduler()
@@ -220,20 +210,15 @@ export declare interface IdleAPI {
220
210
  * `setTimeout(0)` macrotask — still a real host-turn, just not idle-gated. `delay(ms)` is
221
211
  * always a real `setTimeout`. `options.priority` is accepted for contract compliance but a
222
212
  * no-op — idle scheduling has no priority dimension.
223
- * - **Abort fidelity is verbatim, with cleanup.** A pending `yield` / `delay` rejects with
224
- * `signal.reason` exactly. The discipline mirrors the cross-environment default's
225
- * `#sleep`: an already-aborted signal rejects immediately WITHOUT scheduling; otherwise
226
- * the idle callback (or fallback timer) is requested and a `{ once: true }` abort listener
227
- * attached, and the two settle paths are mutually exclusive — the resume path removes the
228
- * listener before resolving, and the abort path `cancelIdleCallback`s (or `clearTimeout`s)
229
- * the pending handle before rejecting. The promise settles exactly once, with no leaked
230
- * callback/timer and no leaked listener.
213
+ * - **Abort fidelity is verbatim, with cleanup.** The shared `scheduleHost` lifecycle links
214
+ * an owned settlement composite before scheduling, never invokes caller-owned signal
215
+ * methods, and cancels the idle callback or fallback timer when abort wins.
231
216
  * - **Event-free.** A pure functional primitive — no Emitter, no events.
232
217
  *
233
218
  * @example
234
219
  * ```ts
235
- * import { createAbort } from '@src/core'
236
- * import { IdleScheduler } from '@src/browser'
220
+ * import { createAbort } from '@orkestrel/abort'
221
+ * import { IdleScheduler } from '@orkestrel/workflow/browser'
237
222
  *
238
223
  * const abort = createAbort()
239
224
  * const scheduler = new IdleScheduler()
@@ -1,4 +1,5 @@
1
- import { isFunction, isRecord } from "@orkestrel/contract";
1
+ import { scheduleHost } from "../core/index.js";
2
+ import { isFunction, isPromise, isRecord } from "@orkestrel/contract";
2
3
  //#region src/browser/constants.ts
3
4
  /**
4
5
  * The browser-native `postTask` priority for each portable {@link SchedulerPriority} — the
@@ -31,23 +32,17 @@ var POST_TASK_PRIORITY = Object.freeze({
31
32
  * never an `as` (AGENTS §14). Where the API is absent (Firefox today, older engines),
32
33
  * it **falls back** to a `setTimeout(0)` macrotask — still a real host-turn, just
33
34
  * without priority. `delay(ms)` is always a real `setTimeout`.
34
- * - **Abort fidelity is verbatim.** A pending `yield` / `delay` rejects with `signal.reason`
35
- * exactly the value the caller passed, never wrapped or replaced. The discipline
36
- * mirrors the cross-environment default's `#sleep`: an already-aborted signal rejects
37
- * immediately WITHOUT scheduling; otherwise the host-turn is scheduled and a
38
- * `{ once: true }` abort listener attached, and the two settle paths are mutually
39
- * exclusive — the turn path removes the listener before resolving, and the abort path
40
- * cancels the scheduled turn before rejecting. The promise settles exactly once, with no
41
- * leaked task/timer and no leaked listener. The caller's `signal` is NOT handed to
42
- * `postTask` (whose own abort would reject with a platform `AbortError`, not the
43
- * caller's `reason`); instead an internal controller cancels the posted task while this
44
- * scheduler rejects with the verbatim `signal.reason`.
35
+ * - **Abort fidelity is verbatim.** The shared `scheduleHost` lifecycle links an owned
36
+ * settlement composite before scheduling, preserving the exact caller reason without
37
+ * invoking caller-owned signal methods. The caller signal is NOT handed to `postTask`;
38
+ * an internal controller cancels that native task. An unexpected native promise rejection
39
+ * is routed back as the exact host failure instead of being discarded.
45
40
  * - **Event-free.** A pure functional primitive — no Emitter, no events.
46
41
  *
47
42
  * @example
48
43
  * ```ts
49
- * import { createAbort } from '@src/core'
50
- * import { BrowserScheduler } from '@src/browser'
44
+ * import { createAbort } from '@orkestrel/abort'
45
+ * import { BrowserScheduler } from '@orkestrel/workflow/browser'
51
46
  *
52
47
  * const abort = createAbort()
53
48
  * const scheduler = new BrowserScheduler()
@@ -65,7 +60,7 @@ var BrowserScheduler = class {
65
60
  */
66
61
  yield(options) {
67
62
  const post = this.#postTask();
68
- if (post === void 0) return this.#macrotask(options?.signal);
63
+ if (post === void 0) return this.#timer(0, options?.signal);
69
64
  return this.#yieldVia(post, options?.priority ?? "normal", options?.signal);
70
65
  }
71
66
  /**
@@ -89,42 +84,21 @@ var BrowserScheduler = class {
89
84
  return (callback, options) => Reflect.apply(post, candidate, [callback, options]);
90
85
  }
91
86
  #yieldVia(post, priority, signal) {
92
- if (signal?.aborted === true) return Promise.reject(signal.reason);
93
- return new Promise((resolve, reject) => {
87
+ return scheduleHost((complete, failure) => {
94
88
  const internal = new AbortController();
95
- const task = post(() => {
96
- signal?.removeEventListener("abort", onAbort);
97
- resolve();
98
- }, {
89
+ const task = post(complete, {
99
90
  priority: POST_TASK_PRIORITY[priority],
100
91
  signal: internal.signal
101
92
  });
102
- const onAbort = this.#abortTask.bind(this, internal, reject, signal);
103
- if (task instanceof Promise) task.catch(() => {});
104
- signal?.addEventListener("abort", onAbort, { once: true });
105
- });
106
- }
107
- #macrotask(signal) {
108
- return this.#timer(0, signal);
93
+ if (isPromise(task)) task.catch(failure);
94
+ return () => internal.abort();
95
+ }, signal);
109
96
  }
110
97
  #timer(ms, signal) {
111
- if (signal?.aborted === true) return Promise.reject(signal.reason);
112
- return new Promise((resolve, reject) => {
113
- const handle = setTimeout(() => {
114
- signal?.removeEventListener("abort", onAbort);
115
- resolve();
116
- }, ms);
117
- const onAbort = this.#abortTimeout.bind(this, handle, reject, signal);
118
- signal?.addEventListener("abort", onAbort, { once: true });
119
- });
120
- }
121
- #abortTask(internal, reject, signal) {
122
- internal.abort();
123
- reject(signal?.reason);
124
- }
125
- #abortTimeout(handle, reject, signal) {
126
- clearTimeout(handle);
127
- reject(signal?.reason);
98
+ return scheduleHost((complete) => {
99
+ const handle = setTimeout(complete, ms);
100
+ return () => clearTimeout(handle);
101
+ }, signal);
128
102
  }
129
103
  };
130
104
  //#endregion
@@ -140,20 +114,15 @@ var BrowserScheduler = class {
140
114
  * is hidden (the host throttles rAF). `delay(ms)` is a real `setTimeout`, unaligned to
141
115
  * frames. `options.priority` is accepted for contract compliance but a no-op — a frame
142
116
  * callback has no priority dimension.
143
- * - **Abort fidelity is verbatim, with cleanup.** A pending `yield` / `delay` rejects with
144
- * `signal.reason` exactly. The discipline mirrors the cross-environment default's
145
- * `#sleep`: an already-aborted signal rejects immediately WITHOUT scheduling a frame;
146
- * otherwise the frame is requested and a `{ once: true }` abort listener attached, and
147
- * the two settle paths are mutually exclusive — the frame path removes the listener
148
- * before resolving, and the abort path `cancelAnimationFrame`s the pending handle before
149
- * rejecting. The promise settles exactly once, with no leaked frame request and no
150
- * leaked listener.
117
+ * - **Abort fidelity is verbatim, with cleanup.** The shared `scheduleHost` lifecycle links
118
+ * an owned settlement composite before requesting a frame, never invokes caller-owned
119
+ * signal methods, and cancels the native handle when abort wins.
151
120
  * - **Event-free.** A pure functional primitive — no Emitter, no events.
152
121
  *
153
122
  * @example
154
123
  * ```ts
155
- * import { createAbort } from '@src/core'
156
- * import { FrameScheduler } from '@src/browser'
124
+ * import { createAbort } from '@orkestrel/abort'
125
+ * import { FrameScheduler } from '@orkestrel/workflow/browser'
157
126
  *
158
127
  * const abort = createAbort()
159
128
  * const scheduler = new FrameScheduler()
@@ -185,34 +154,16 @@ var FrameScheduler = class {
185
154
  return this.#sleep(ms, options?.signal);
186
155
  }
187
156
  #frame(signal) {
188
- if (signal?.aborted === true) return Promise.reject(signal.reason);
189
- return new Promise((resolve, reject) => {
190
- const handle = requestAnimationFrame(() => {
191
- signal?.removeEventListener("abort", onAbort);
192
- resolve();
193
- });
194
- const onAbort = this.#abortFrame.bind(this, handle, reject, signal);
195
- signal?.addEventListener("abort", onAbort, { once: true });
196
- });
157
+ return scheduleHost((complete) => {
158
+ const handle = requestAnimationFrame(complete);
159
+ return () => cancelAnimationFrame(handle);
160
+ }, signal);
197
161
  }
198
162
  #sleep(ms, signal) {
199
- if (signal?.aborted === true) return Promise.reject(signal.reason);
200
- return new Promise((resolve, reject) => {
201
- const handle = setTimeout(() => {
202
- signal?.removeEventListener("abort", onAbort);
203
- resolve();
204
- }, ms);
205
- const onAbort = this.#abortTimeout.bind(this, handle, reject, signal);
206
- signal?.addEventListener("abort", onAbort, { once: true });
207
- });
208
- }
209
- #abortFrame(handle, reject, signal) {
210
- cancelAnimationFrame(handle);
211
- reject(signal?.reason);
212
- }
213
- #abortTimeout(handle, reject, signal) {
214
- clearTimeout(handle);
215
- reject(signal?.reason);
163
+ return scheduleHost((complete) => {
164
+ const handle = setTimeout(complete, ms);
165
+ return () => clearTimeout(handle);
166
+ }, signal);
216
167
  }
217
168
  };
218
169
  //#endregion
@@ -231,20 +182,15 @@ var FrameScheduler = class {
231
182
  * `setTimeout(0)` macrotask — still a real host-turn, just not idle-gated. `delay(ms)` is
232
183
  * always a real `setTimeout`. `options.priority` is accepted for contract compliance but a
233
184
  * no-op — idle scheduling has no priority dimension.
234
- * - **Abort fidelity is verbatim, with cleanup.** A pending `yield` / `delay` rejects with
235
- * `signal.reason` exactly. The discipline mirrors the cross-environment default's
236
- * `#sleep`: an already-aborted signal rejects immediately WITHOUT scheduling; otherwise
237
- * the idle callback (or fallback timer) is requested and a `{ once: true }` abort listener
238
- * attached, and the two settle paths are mutually exclusive — the resume path removes the
239
- * listener before resolving, and the abort path `cancelIdleCallback`s (or `clearTimeout`s)
240
- * the pending handle before rejecting. The promise settles exactly once, with no leaked
241
- * callback/timer and no leaked listener.
185
+ * - **Abort fidelity is verbatim, with cleanup.** The shared `scheduleHost` lifecycle links
186
+ * an owned settlement composite before scheduling, never invokes caller-owned signal
187
+ * methods, and cancels the idle callback or fallback timer when abort wins.
242
188
  * - **Event-free.** A pure functional primitive — no Emitter, no events.
243
189
  *
244
190
  * @example
245
191
  * ```ts
246
- * import { createAbort } from '@src/core'
247
- * import { IdleScheduler } from '@src/browser'
192
+ * import { createAbort } from '@orkestrel/abort'
193
+ * import { IdleScheduler } from '@orkestrel/workflow/browser'
248
194
  *
249
195
  * const abort = createAbort()
250
196
  * const scheduler = new IdleScheduler()
@@ -288,26 +234,16 @@ var IdleScheduler = class {
288
234
  };
289
235
  }
290
236
  #idle(idle, signal) {
291
- if (signal?.aborted === true) return Promise.reject(signal.reason);
292
- return new Promise((resolve, reject) => {
293
- const handle = idle.request(() => {
294
- signal?.removeEventListener("abort", onAbort);
295
- resolve();
296
- });
297
- const onAbort = this.#abortIdle.bind(this, idle, handle, reject, signal);
298
- signal?.addEventListener("abort", onAbort, { once: true });
299
- });
237
+ return scheduleHost((complete) => {
238
+ const handle = idle.request(complete);
239
+ return () => idle.cancel(handle);
240
+ }, signal);
300
241
  }
301
242
  #sleep(ms, signal) {
302
- if (signal?.aborted === true) return Promise.reject(signal.reason);
303
- return new Promise((resolve, reject) => {
304
- const handle = setTimeout(() => {
305
- signal?.removeEventListener("abort", onAbort);
306
- resolve();
307
- }, ms);
308
- const onAbort = this.#abortTimeout.bind(this, handle, reject, signal);
309
- signal?.addEventListener("abort", onAbort, { once: true });
310
- });
243
+ return scheduleHost((complete) => {
244
+ const handle = setTimeout(complete, ms);
245
+ return () => clearTimeout(handle);
246
+ }, signal);
311
247
  }
312
248
  #request(request, callback) {
313
249
  return Number(Reflect.apply(request, globalThis, [callback]));
@@ -315,14 +251,6 @@ var IdleScheduler = class {
315
251
  #cancel(cancel, handle) {
316
252
  Reflect.apply(cancel, globalThis, [handle]);
317
253
  }
318
- #abortIdle(idle, handle, reject, signal) {
319
- idle.cancel(handle);
320
- reject(signal?.reason);
321
- }
322
- #abortTimeout(handle, reject, signal) {
323
- clearTimeout(handle);
324
- reject(signal?.reason);
325
- }
326
254
  };
327
255
  //#endregion
328
256
  //#region src/browser/factories.ts
@@ -336,15 +264,16 @@ var IdleScheduler = class {
336
264
  * The default browser scheduler: it honours `options.priority` (`user` /
337
265
  * `normal` / `background`) when `scheduler.postTask` is available and degrades to a plain
338
266
  * macrotask elsewhere. Both methods are abort-aware: pass `options.signal` and a pending
339
- * yield/delay rejects with the signal's `reason` verbatim, with full task/timer/listener
340
- * cleanup. Prefer {@link createFrameScheduler} for paint-aligned work or
267
+ * yield/delay rejects with the signal's exact `reason`; the shared owned-signal lifecycle
268
+ * cancels the native handle without invoking caller listener methods. Prefer
269
+ * {@link createFrameScheduler} for paint-aligned work or
341
270
  * {@link createIdleScheduler} for idle-time background work.
342
271
  *
343
272
  * @returns A {@link SchedulerInterface} backed by `scheduler.postTask` (or a macrotask)
344
273
  *
345
274
  * @example
346
275
  * ```ts
347
- * import { createBrowserScheduler } from '@src/browser'
276
+ * import { createBrowserScheduler } from '@orkestrel/workflow/browser'
348
277
  *
349
278
  * const scheduler = createBrowserScheduler()
350
279
  * await scheduler.yield({ priority: 'background' })
@@ -369,7 +298,7 @@ function createBrowserScheduler() {
369
298
  *
370
299
  * @example
371
300
  * ```ts
372
- * import { createFrameScheduler } from '@src/browser'
301
+ * import { createFrameScheduler } from '@orkestrel/workflow/browser'
373
302
  *
374
303
  * const scheduler = createFrameScheduler()
375
304
  * await scheduler.yield() // resumes before the next paint
@@ -394,7 +323,7 @@ function createFrameScheduler() {
394
323
  *
395
324
  * @example
396
325
  * ```ts
397
- * import { createIdleScheduler } from '@src/browser'
326
+ * import { createIdleScheduler } from '@orkestrel/workflow/browser'
398
327
  *
399
328
  * const scheduler = createIdleScheduler()
400
329
  * await scheduler.yield() // resumes when the host is idle
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#postTask","#macrotask","#yieldVia","#timer","#abortTask","#abortTimeout","#frame","#sleep","#abortFrame","#abortTimeout","#idleAPI","#sleep","#idle","#request","#cancel","#abortIdle","#abortTimeout"],"sources":["../../../src/browser/constants.ts","../../../src/browser/BrowserScheduler.ts","../../../src/browser/FrameScheduler.ts","../../../src/browser/IdleScheduler.ts","../../../src/browser/factories.ts"],"sourcesContent":["import type { SchedulerPriority } from '@src/core'\n\n/**\n * The browser-native `postTask` priority for each portable {@link SchedulerPriority} — the\n * Prioritized Task Scheduling API's three levels.\n *\n * @remarks\n * A `user` hint maps to the most urgent `'user-blocking'`, `normal` to the default\n * `'user-visible'`, and `background` to `'background'`. {@link BrowserScheduler} reads this\n * map to translate the caller's portable priority into the value passed to\n * `scheduler.postTask`, so the urgency hint is honoured by the host.\n */\nexport const POST_TASK_PRIORITY: Readonly<Record<SchedulerPriority, string>> = Object.freeze({\n\tuser: 'user-blocking',\n\tnormal: 'user-visible',\n\tbackground: 'background',\n})\n","import type { SchedulerInterface, SchedulerOptions, SchedulerPriority } from '@src/core'\nimport { isFunction, isRecord } from '@orkestrel/contract'\nimport { POST_TASK_PRIORITY } from './constants.js'\n\n/**\n * The browser {@link SchedulerInterface} — the browser-native cooperative-yield backend\n * built on the Prioritized Task Scheduling API (`scheduler.postTask`), falling back to a\n * zero-delay macrotask where it is absent.\n *\n * @remarks\n * - **`yield` prefers `scheduler.postTask`, honouring priority.** When `globalThis`\n * exposes a `scheduler` with a `postTask` method, `yield()` posts a task at the mapped\n * priority (`user` → `'user-blocking'`, `normal` → `'user-visible'`, `background` →\n * `'background'`), so the host genuinely regains control and the urgency hint is\n * honoured. The capability is feature-detected through guards (`isRecord` / `isFunction`),\n * never an `as` (AGENTS §14). Where the API is absent (Firefox today, older engines),\n * it **falls back** to a `setTimeout(0)` macrotask — still a real host-turn, just\n * without priority. `delay(ms)` is always a real `setTimeout`.\n * - **Abort fidelity is verbatim.** A pending `yield` / `delay` rejects with `signal.reason`\n * exactly — the value the caller passed, never wrapped or replaced. The discipline\n * mirrors the cross-environment default's `#sleep`: an already-aborted signal rejects\n * immediately WITHOUT scheduling; otherwise the host-turn is scheduled and a\n * `{ once: true }` abort listener attached, and the two settle paths are mutually\n * exclusive — the turn path removes the listener before resolving, and the abort path\n * cancels the scheduled turn before rejecting. The promise settles exactly once, with no\n * leaked task/timer and no leaked listener. The caller's `signal` is NOT handed to\n * `postTask` (whose own abort would reject with a platform `AbortError`, not the\n * caller's `reason`); instead an internal controller cancels the posted task while this\n * scheduler rejects with the verbatim `signal.reason`.\n * - **Event-free.** A pure functional primitive — no Emitter, no events.\n *\n * @example\n * ```ts\n * import { createAbort } from '@src/core'\n * import { BrowserScheduler } from '@src/browser'\n *\n * const abort = createAbort()\n * const scheduler = new BrowserScheduler()\n * while (!abort.signal.aborted) {\n * \tdoSomeWork()\n * \tawait scheduler.yield({ priority: 'background', signal: abort.signal })\n * }\n * ```\n */\nexport class BrowserScheduler implements SchedulerInterface {\n\t/**\n\t * Yield control to the host via `scheduler.postTask` at the given priority (or a\n\t * `setTimeout(0)` macrotask where the API is absent), then resume; abort rejects with\n\t * `signal.reason`.\n\t */\n\tyield(options?: SchedulerOptions): Promise<void> {\n\t\tconst post = this.#postTask()\n\t\tif (post === undefined) return this.#macrotask(options?.signal)\n\t\treturn this.#yieldVia(post, options?.priority ?? 'normal', options?.signal)\n\t}\n\n\t/**\n\t * Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with\n\t * `signal.reason`.\n\t *\n\t * @remarks\n\t * `ms` should be a non-negative finite number. The primitive does no validation: it\n\t * passes `ms` straight to the host `setTimeout`, which clamps a negative value or\n\t * `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than\n\t * throwing.\n\t */\n\tdelay(ms: number, options?: SchedulerOptions): Promise<void> {\n\t\treturn this.#timer(ms, options?.signal)\n\t}\n\n\t// === Private\n\n\t// Feature-detect the Prioritized Task Scheduling API through guards (no `as`): the\n\t// global `scheduler` must be a record carrying a callable `postTask`. Returns the\n\t// narrowed `postTask` function, or `undefined` when the API is absent (the fallback).\n\t#postTask(): ((callback: () => void, options: Record<string, unknown>) => unknown) | undefined {\n\t\tconst candidate: unknown = Reflect.get(globalThis, 'scheduler')\n\t\tif (!isRecord(candidate)) return undefined\n\t\tconst post = candidate.postTask\n\t\tif (!isFunction(post)) return undefined\n\t\treturn (callback, options) => Reflect.apply(post, candidate, [callback, options])\n\t}\n\n\t// A `scheduler.postTask` host-turn at the mapped priority. The caller's signal is NOT\n\t// passed to `postTask` (its abort rejects with a platform `AbortError`, not the\n\t// caller's `reason`); instead an internal controller cancels the posted task on abort\n\t// while this rejects with the verbatim `signal.reason`. Settle-once, no leak: the task\n\t// path removes the abort listener before resolving; the abort path aborts the internal\n\t// controller (cancelling the task) before rejecting.\n\t#yieldVia(\n\t\tpost: (callback: () => void, options: Record<string, unknown>) => unknown,\n\t\tpriority: SchedulerPriority,\n\t\tsignal?: AbortSignal,\n\t): Promise<void> {\n\t\tif (signal?.aborted === true) return Promise.reject(signal.reason)\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst internal = new AbortController()\n\t\t\tconst task = post(\n\t\t\t\t() => {\n\t\t\t\t\tsignal?.removeEventListener('abort', onAbort)\n\t\t\t\t\tresolve()\n\t\t\t\t},\n\t\t\t\t{ priority: POST_TASK_PRIORITY[priority], signal: internal.signal },\n\t\t\t)\n\t\t\tconst onAbort = this.#abortTask.bind(this, internal, reject, signal)\n\t\t\t// `postTask` returns a promise that rejects when the internal controller aborts;\n\t\t\t// swallow that rejection (the abort path already rejected with the real reason).\n\t\t\tif (task instanceof Promise) task.catch(() => {})\n\t\t\tsignal?.addEventListener('abort', onAbort, { once: true })\n\t\t})\n\t}\n\n\t// The `setTimeout(0)` macrotask fallback for `yield` when `postTask` is absent. Same\n\t// settle-once discipline as the core `#sleep`: an already-aborted signal rejects\n\t// without arming; otherwise the timer path removes the listener before resolving and\n\t// the abort path clears the timer before rejecting with `signal.reason`.\n\t#macrotask(signal?: AbortSignal): Promise<void> {\n\t\treturn this.#timer(0, signal)\n\t}\n\n\t// The abort-aware `setTimeout` sleep shared by `delay` and the `yield` macrotask\n\t// fallback. Settle-once, no leak (the core `#sleep` discipline): already-aborted →\n\t// reject without arming; the timer path removes the listener before resolving; the\n\t// abort path clears the timer before rejecting with the verbatim `signal.reason`.\n\t#timer(ms: number, signal?: AbortSignal): Promise<void> {\n\t\tif (signal?.aborted === true) return Promise.reject(signal.reason)\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst handle = setTimeout(() => {\n\t\t\t\tsignal?.removeEventListener('abort', onAbort) // load-bearing: prevents a post-resolve reject\n\t\t\t\tresolve()\n\t\t\t}, ms)\n\t\t\tconst onAbort = this.#abortTimeout.bind(this, handle, reject, signal)\n\t\t\tsignal?.addEventListener('abort', onAbort, { once: true })\n\t\t})\n\t}\n\n\t#abortTask(\n\t\tinternal: AbortController,\n\t\treject: (reason?: unknown) => void,\n\t\tsignal?: AbortSignal,\n\t): void {\n\t\tinternal.abort()\n\t\treject(signal?.reason)\n\t}\n\n\t#abortTimeout(\n\t\thandle: ReturnType<typeof setTimeout>,\n\t\treject: (reason?: unknown) => void,\n\t\tsignal?: AbortSignal,\n\t): void {\n\t\tclearTimeout(handle)\n\t\treject(signal?.reason)\n\t}\n}\n","import type { SchedulerInterface, SchedulerOptions } from '@src/core'\n\n/**\n * The frame-aligned {@link SchedulerInterface} — a browser cooperative-yield backend\n * whose `yield` resumes just before the next paint via `requestAnimationFrame`.\n *\n * @remarks\n * - **`yield` resumes before the next paint.** `yield()` waits on `requestAnimationFrame`,\n * so the resumption is aligned to the browser's render loop — ideal for work that\n * should batch per frame (animation, incremental DOM updates) and pause while the tab\n * is hidden (the host throttles rAF). `delay(ms)` is a real `setTimeout`, unaligned to\n * frames. `options.priority` is accepted for contract compliance but a no-op — a frame\n * callback has no priority dimension.\n * - **Abort fidelity is verbatim, with cleanup.** A pending `yield` / `delay` rejects with\n * `signal.reason` exactly. The discipline mirrors the cross-environment default's\n * `#sleep`: an already-aborted signal rejects immediately WITHOUT scheduling a frame;\n * otherwise the frame is requested and a `{ once: true }` abort listener attached, and\n * the two settle paths are mutually exclusive — the frame path removes the listener\n * before resolving, and the abort path `cancelAnimationFrame`s the pending handle before\n * rejecting. The promise settles exactly once, with no leaked frame request and no\n * leaked listener.\n * - **Event-free.** A pure functional primitive — no Emitter, no events.\n *\n * @example\n * ```ts\n * import { createAbort } from '@src/core'\n * import { FrameScheduler } from '@src/browser'\n *\n * const abort = createAbort()\n * const scheduler = new FrameScheduler()\n * while (!abort.signal.aborted) {\n * \trenderOneFrameOfWork()\n * \tawait scheduler.yield({ signal: abort.signal }) // resume before the next paint\n * }\n * ```\n */\nexport class FrameScheduler implements SchedulerInterface {\n\t/**\n\t * Yield control to the host until just before the next paint via\n\t * `requestAnimationFrame`, then resume; abort rejects with `signal.reason`.\n\t */\n\tyield(options?: SchedulerOptions): Promise<void> {\n\t\treturn this.#frame(options?.signal)\n\t}\n\n\t/**\n\t * Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with\n\t * `signal.reason`.\n\t *\n\t * @remarks\n\t * `ms` should be a non-negative finite number. The primitive does no validation: it\n\t * passes `ms` straight to the host `setTimeout`, which clamps a negative value or\n\t * `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than\n\t * throwing.\n\t */\n\tdelay(ms: number, options?: SchedulerOptions): Promise<void> {\n\t\treturn this.#sleep(ms, options?.signal)\n\t}\n\n\t// === Private\n\n\t// The `requestAnimationFrame` host-turn for `yield`. Resolves in the next frame\n\t// callback (before paint); rejects with `signal.reason` if already aborted (no frame\n\t// requested) or aborted while pending. Settle-once, no leak: the frame path removes the\n\t// abort listener before resolving; the abort path cancels the frame before rejecting.\n\t#frame(signal?: AbortSignal): Promise<void> {\n\t\tif (signal?.aborted === true) return Promise.reject(signal.reason)\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst handle = requestAnimationFrame(() => {\n\t\t\t\tsignal?.removeEventListener('abort', onAbort) // load-bearing: prevents a post-resolve reject\n\t\t\t\tresolve()\n\t\t\t})\n\t\t\tconst onAbort = this.#abortFrame.bind(this, handle, reject, signal)\n\t\t\tsignal?.addEventListener('abort', onAbort, { once: true })\n\t\t})\n\t}\n\n\t// The abort-aware `setTimeout` sleep for `delay`. Same settle-once discipline as the\n\t// core `#sleep`: already-aborted → reject without arming; the timer path removes the\n\t// listener before resolving; the abort path clears the timer before rejecting with the\n\t// verbatim `signal.reason`.\n\t#sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\t\tif (signal?.aborted === true) return Promise.reject(signal.reason)\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst handle = setTimeout(() => {\n\t\t\t\tsignal?.removeEventListener('abort', onAbort) // load-bearing: prevents a post-resolve reject\n\t\t\t\tresolve()\n\t\t\t}, ms)\n\t\t\tconst onAbort = this.#abortTimeout.bind(this, handle, reject, signal)\n\t\t\tsignal?.addEventListener('abort', onAbort, { once: true })\n\t\t})\n\t}\n\n\t#abortFrame(handle: number, reject: (reason?: unknown) => void, signal?: AbortSignal): void {\n\t\tcancelAnimationFrame(handle)\n\t\treject(signal?.reason)\n\t}\n\n\t#abortTimeout(\n\t\thandle: ReturnType<typeof setTimeout>,\n\t\treject: (reason?: unknown) => void,\n\t\tsignal?: AbortSignal,\n\t): void {\n\t\tclearTimeout(handle)\n\t\treject(signal?.reason)\n\t}\n}\n","import type { SchedulerInterface, SchedulerOptions } from '@src/core'\nimport type { AnyFunction } from '@orkestrel/contract'\nimport type { IdleAPI } from './types.js'\nimport { isFunction } from '@orkestrel/contract'\n\n/**\n * The idle-time {@link SchedulerInterface} — a browser cooperative-yield backend whose\n * `yield` resumes when the host is idle via `requestIdleCallback`, falling back to a\n * zero-delay macrotask where it is absent.\n *\n * @remarks\n * - **`yield` resumes during idle time.** When `globalThis` exposes `requestIdleCallback`,\n * `yield()` waits on it, so the resumption happens when the browser has spare time after\n * rendering and input — ideal for low-priority background work that must not contend with\n * the user. The capability is feature-detected through a guard (`isFunction`), never an\n * `as` (AGENTS §14). Where the API is absent (Safari today), it **falls back** to a\n * `setTimeout(0)` macrotask — still a real host-turn, just not idle-gated. `delay(ms)` is\n * always a real `setTimeout`. `options.priority` is accepted for contract compliance but a\n * no-op — idle scheduling has no priority dimension.\n * - **Abort fidelity is verbatim, with cleanup.** A pending `yield` / `delay` rejects with\n * `signal.reason` exactly. The discipline mirrors the cross-environment default's\n * `#sleep`: an already-aborted signal rejects immediately WITHOUT scheduling; otherwise\n * the idle callback (or fallback timer) is requested and a `{ once: true }` abort listener\n * attached, and the two settle paths are mutually exclusive — the resume path removes the\n * listener before resolving, and the abort path `cancelIdleCallback`s (or `clearTimeout`s)\n * the pending handle before rejecting. The promise settles exactly once, with no leaked\n * callback/timer and no leaked listener.\n * - **Event-free.** A pure functional primitive — no Emitter, no events.\n *\n * @example\n * ```ts\n * import { createAbort } from '@src/core'\n * import { IdleScheduler } from '@src/browser'\n *\n * const abort = createAbort()\n * const scheduler = new IdleScheduler()\n * while (!abort.signal.aborted) {\n * \tdoLowPriorityWork()\n * \tawait scheduler.yield({ signal: abort.signal }) // resume when the host is idle\n * }\n * ```\n */\nexport class IdleScheduler implements SchedulerInterface {\n\t/**\n\t * Yield control to the host until it is idle via `requestIdleCallback` (or a\n\t * `setTimeout(0)` macrotask where the API is absent), then resume; abort rejects with\n\t * `signal.reason`.\n\t */\n\tyield(options?: SchedulerOptions): Promise<void> {\n\t\tconst idle = this.#idleAPI()\n\t\tif (idle === undefined) return this.#sleep(0, options?.signal)\n\t\treturn this.#idle(idle, options?.signal)\n\t}\n\n\t/**\n\t * Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with\n\t * `signal.reason`.\n\t *\n\t * @remarks\n\t * `ms` should be a non-negative finite number. The primitive does no validation: it\n\t * passes `ms` straight to the host `setTimeout`, which clamps a negative value or\n\t * `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than\n\t * throwing.\n\t */\n\tdelay(ms: number, options?: SchedulerOptions): Promise<void> {\n\t\treturn this.#sleep(ms, options?.signal)\n\t}\n\n\t// === Private\n\n\t// Feature-detect `requestIdleCallback` / `cancelIdleCallback` off `globalThis` through\n\t// guards (no `as`): both must be callable. Returns the narrowed pair, or `undefined`\n\t// when the API is absent (the macrotask fallback).\n\t#idleAPI(): IdleAPI | undefined {\n\t\tconst request: unknown = Reflect.get(globalThis, 'requestIdleCallback')\n\t\tconst cancel: unknown = Reflect.get(globalThis, 'cancelIdleCallback')\n\t\tif (!isFunction(request) || !isFunction(cancel)) return undefined\n\t\treturn {\n\t\t\trequest: this.#request.bind(this, request),\n\t\t\tcancel: this.#cancel.bind(this, cancel),\n\t\t}\n\t}\n\n\t// The `requestIdleCallback` host-turn for `yield`. Resolves in the idle callback;\n\t// rejects with `signal.reason` if already aborted (nothing scheduled) or aborted while\n\t// pending. Settle-once, no leak: the resume path removes the abort listener before\n\t// resolving; the abort path cancels the idle callback before rejecting.\n\t#idle(idle: IdleAPI, signal?: AbortSignal): Promise<void> {\n\t\tif (signal?.aborted === true) return Promise.reject(signal.reason)\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst handle = idle.request(() => {\n\t\t\t\tsignal?.removeEventListener('abort', onAbort) // load-bearing: prevents a post-resolve reject\n\t\t\t\tresolve()\n\t\t\t})\n\t\t\tconst onAbort = this.#abortIdle.bind(this, idle, handle, reject, signal)\n\t\t\tsignal?.addEventListener('abort', onAbort, { once: true })\n\t\t})\n\t}\n\n\t// The abort-aware `setTimeout` sleep shared by `delay` and the `yield` macrotask\n\t// fallback. Same settle-once discipline as the core `#sleep`: already-aborted → reject\n\t// without arming; the timer path removes the listener before resolving; the abort path\n\t// clears the timer before rejecting with the verbatim `signal.reason`.\n\t#sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\t\tif (signal?.aborted === true) return Promise.reject(signal.reason)\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst handle = setTimeout(() => {\n\t\t\t\tsignal?.removeEventListener('abort', onAbort) // load-bearing: prevents a post-resolve reject\n\t\t\t\tresolve()\n\t\t\t}, ms)\n\t\t\tconst onAbort = this.#abortTimeout.bind(this, handle, reject, signal)\n\t\t\tsignal?.addEventListener('abort', onAbort, { once: true })\n\t\t})\n\t}\n\n\t#request(request: AnyFunction, callback: () => void): number {\n\t\treturn Number(Reflect.apply(request, globalThis, [callback]))\n\t}\n\n\t#cancel(cancel: AnyFunction, handle: number): void {\n\t\tReflect.apply(cancel, globalThis, [handle])\n\t}\n\n\t#abortIdle(\n\t\tidle: IdleAPI,\n\t\thandle: number,\n\t\treject: (reason?: unknown) => void,\n\t\tsignal?: AbortSignal,\n\t): void {\n\t\tidle.cancel(handle)\n\t\treject(signal?.reason)\n\t}\n\n\t#abortTimeout(\n\t\thandle: ReturnType<typeof setTimeout>,\n\t\treject: (reason?: unknown) => void,\n\t\tsignal?: AbortSignal,\n\t): void {\n\t\tclearTimeout(handle)\n\t\treject(signal?.reason)\n\t}\n}\n","import type { SchedulerInterface } from '@src/core'\nimport { BrowserScheduler } from './BrowserScheduler.js'\nimport { FrameScheduler } from './FrameScheduler.js'\nimport { IdleScheduler } from './IdleScheduler.js'\n\n/**\n * Create the browser-native cooperative-yield {@link SchedulerInterface} — `yield()` uses\n * the Prioritized Task Scheduling API (`scheduler.postTask`) at the requested priority\n * when present, falling back to a `setTimeout(0)` macrotask; `delay(ms)` is a real\n * `setTimeout`.\n *\n * @remarks\n * The default browser scheduler: it honours `options.priority` (`user` /\n * `normal` / `background`) when `scheduler.postTask` is available and degrades to a plain\n * macrotask elsewhere. Both methods are abort-aware: pass `options.signal` and a pending\n * yield/delay rejects with the signal's `reason` verbatim, with full task/timer/listener\n * cleanup. Prefer {@link createFrameScheduler} for paint-aligned work or\n * {@link createIdleScheduler} for idle-time background work.\n *\n * @returns A {@link SchedulerInterface} backed by `scheduler.postTask` (or a macrotask)\n *\n * @example\n * ```ts\n * import { createBrowserScheduler } from '@src/browser'\n *\n * const scheduler = createBrowserScheduler()\n * await scheduler.yield({ priority: 'background' })\n * ```\n */\nexport function createBrowserScheduler(): SchedulerInterface {\n\treturn new BrowserScheduler()\n}\n\n/**\n * Create the frame-aligned cooperative-yield {@link SchedulerInterface} — `yield()` resumes\n * just before the next paint via `requestAnimationFrame`; `delay(ms)` is a real\n * `setTimeout`.\n *\n * @remarks\n * Use it for work that should batch per render frame (animation, incremental DOM updates)\n * and naturally pause while the tab is hidden. `yield` is abort-aware: pass `options.signal`\n * and a pending yield rejects with the signal's `reason` verbatim, cancelling the pending\n * frame request. `options.priority` is accepted but a no-op — a frame callback has no\n * priority dimension.\n *\n * @returns A {@link SchedulerInterface} backed by `requestAnimationFrame`\n *\n * @example\n * ```ts\n * import { createFrameScheduler } from '@src/browser'\n *\n * const scheduler = createFrameScheduler()\n * await scheduler.yield() // resumes before the next paint\n * ```\n */\nexport function createFrameScheduler(): SchedulerInterface {\n\treturn new FrameScheduler()\n}\n\n/**\n * Create the idle-time cooperative-yield {@link SchedulerInterface} — `yield()` resumes when\n * the host is idle via `requestIdleCallback` when present, falling back to a `setTimeout(0)`\n * macrotask; `delay(ms)` is a real `setTimeout`.\n *\n * @remarks\n * Use it for low-priority background work that must not contend with rendering or input.\n * Where `requestIdleCallback` is absent (Safari today) it degrades to a plain macrotask.\n * `yield` is abort-aware: pass `options.signal` and a pending yield rejects with the\n * signal's `reason` verbatim, cancelling the pending idle callback. `options.priority` is\n * accepted but a no-op — idle scheduling has no priority dimension.\n *\n * @returns A {@link SchedulerInterface} backed by `requestIdleCallback` (or a macrotask)\n *\n * @example\n * ```ts\n * import { createIdleScheduler } from '@src/browser'\n *\n * const scheduler = createIdleScheduler()\n * await scheduler.yield() // resumes when the host is idle\n * ```\n */\nexport function createIdleScheduler(): SchedulerInterface {\n\treturn new IdleScheduler()\n}\n"],"mappings":";;;;;;;;;;;;AAYA,IAAa,qBAAkE,OAAO,OAAO;CAC5F,MAAM;CACN,QAAQ;CACR,YAAY;AACb,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC4BD,IAAa,mBAAb,MAA4D;;;;;;CAM3D,MAAM,SAA2C;EAChD,MAAM,OAAO,KAAKA,UAAU;EAC5B,IAAI,SAAS,KAAA,GAAW,OAAO,KAAKC,WAAW,SAAS,MAAM;EAC9D,OAAO,KAAKC,UAAU,MAAM,SAAS,YAAY,UAAU,SAAS,MAAM;CAC3E;;;;;;;;;;;CAYA,MAAM,IAAY,SAA2C;EAC5D,OAAO,KAAKC,OAAO,IAAI,SAAS,MAAM;CACvC;CAOA,YAA+F;EAC9F,MAAM,YAAqB,QAAQ,IAAI,YAAY,WAAW;EAC9D,IAAI,CAAC,SAAS,SAAS,GAAG,OAAO,KAAA;EACjC,MAAM,OAAO,UAAU;EACvB,IAAI,CAAC,WAAW,IAAI,GAAG,OAAO,KAAA;EAC9B,QAAQ,UAAU,YAAY,QAAQ,MAAM,MAAM,WAAW,CAAC,UAAU,OAAO,CAAC;CACjF;CAQA,UACC,MACA,UACA,QACgB;EAChB,IAAI,QAAQ,YAAY,MAAM,OAAO,QAAQ,OAAO,OAAO,MAAM;EACjE,OAAO,IAAI,SAAe,SAAS,WAAW;GAC7C,MAAM,WAAW,IAAI,gBAAgB;GACrC,MAAM,OAAO,WACN;IACL,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,QAAQ;GACT,GACA;IAAE,UAAU,mBAAmB;IAAW,QAAQ,SAAS;GAAO,CACnE;GACA,MAAM,UAAU,KAAKC,WAAW,KAAK,MAAM,UAAU,QAAQ,MAAM;GAGnE,IAAI,gBAAgB,SAAS,KAAK,YAAY,CAAC,CAAC;GAChD,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAC1D,CAAC;CACF;CAMA,WAAW,QAAqC;EAC/C,OAAO,KAAKD,OAAO,GAAG,MAAM;CAC7B;CAMA,OAAO,IAAY,QAAqC;EACvD,IAAI,QAAQ,YAAY,MAAM,OAAO,QAAQ,OAAO,OAAO,MAAM;EACjE,OAAO,IAAI,SAAe,SAAS,WAAW;GAC7C,MAAM,SAAS,iBAAiB;IAC/B,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,QAAQ;GACT,GAAG,EAAE;GACL,MAAM,UAAU,KAAKE,cAAc,KAAK,MAAM,QAAQ,QAAQ,MAAM;GACpE,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAC1D,CAAC;CACF;CAEA,WACC,UACA,QACA,QACO;EACP,SAAS,MAAM;EACf,OAAO,QAAQ,MAAM;CACtB;CAEA,cACC,QACA,QACA,QACO;EACP,aAAa,MAAM;EACnB,OAAO,QAAQ,MAAM;CACtB;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACrHA,IAAa,iBAAb,MAA0D;;;;;CAKzD,MAAM,SAA2C;EAChD,OAAO,KAAKC,OAAO,SAAS,MAAM;CACnC;;;;;;;;;;;CAYA,MAAM,IAAY,SAA2C;EAC5D,OAAO,KAAKC,OAAO,IAAI,SAAS,MAAM;CACvC;CAQA,OAAO,QAAqC;EAC3C,IAAI,QAAQ,YAAY,MAAM,OAAO,QAAQ,OAAO,OAAO,MAAM;EACjE,OAAO,IAAI,SAAe,SAAS,WAAW;GAC7C,MAAM,SAAS,4BAA4B;IAC1C,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,QAAQ;GACT,CAAC;GACD,MAAM,UAAU,KAAKC,YAAY,KAAK,MAAM,QAAQ,QAAQ,MAAM;GAClE,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAC1D,CAAC;CACF;CAMA,OAAO,IAAY,QAAqC;EACvD,IAAI,QAAQ,YAAY,MAAM,OAAO,QAAQ,OAAO,OAAO,MAAM;EACjE,OAAO,IAAI,SAAe,SAAS,WAAW;GAC7C,MAAM,SAAS,iBAAiB;IAC/B,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,QAAQ;GACT,GAAG,EAAE;GACL,MAAM,UAAU,KAAKC,cAAc,KAAK,MAAM,QAAQ,QAAQ,MAAM;GACpE,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAC1D,CAAC;CACF;CAEA,YAAY,QAAgB,QAAoC,QAA4B;EAC3F,qBAAqB,MAAM;EAC3B,OAAO,QAAQ,MAAM;CACtB;CAEA,cACC,QACA,QACA,QACO;EACP,aAAa,MAAM;EACnB,OAAO,QAAQ,MAAM;CACtB;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AChEA,IAAa,gBAAb,MAAyD;;;;;;CAMxD,MAAM,SAA2C;EAChD,MAAM,OAAO,KAAKC,SAAS;EAC3B,IAAI,SAAS,KAAA,GAAW,OAAO,KAAKC,OAAO,GAAG,SAAS,MAAM;EAC7D,OAAO,KAAKC,MAAM,MAAM,SAAS,MAAM;CACxC;;;;;;;;;;;CAYA,MAAM,IAAY,SAA2C;EAC5D,OAAO,KAAKD,OAAO,IAAI,SAAS,MAAM;CACvC;CAOA,WAAgC;EAC/B,MAAM,UAAmB,QAAQ,IAAI,YAAY,qBAAqB;EACtE,MAAM,SAAkB,QAAQ,IAAI,YAAY,oBAAoB;EACpE,IAAI,CAAC,WAAW,OAAO,KAAK,CAAC,WAAW,MAAM,GAAG,OAAO,KAAA;EACxD,OAAO;GACN,SAAS,KAAKE,SAAS,KAAK,MAAM,OAAO;GACzC,QAAQ,KAAKC,QAAQ,KAAK,MAAM,MAAM;EACvC;CACD;CAMA,MAAM,MAAe,QAAqC;EACzD,IAAI,QAAQ,YAAY,MAAM,OAAO,QAAQ,OAAO,OAAO,MAAM;EACjE,OAAO,IAAI,SAAe,SAAS,WAAW;GAC7C,MAAM,SAAS,KAAK,cAAc;IACjC,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,QAAQ;GACT,CAAC;GACD,MAAM,UAAU,KAAKC,WAAW,KAAK,MAAM,MAAM,QAAQ,QAAQ,MAAM;GACvE,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAC1D,CAAC;CACF;CAMA,OAAO,IAAY,QAAqC;EACvD,IAAI,QAAQ,YAAY,MAAM,OAAO,QAAQ,OAAO,OAAO,MAAM;EACjE,OAAO,IAAI,SAAe,SAAS,WAAW;GAC7C,MAAM,SAAS,iBAAiB;IAC/B,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,QAAQ;GACT,GAAG,EAAE;GACL,MAAM,UAAU,KAAKC,cAAc,KAAK,MAAM,QAAQ,QAAQ,MAAM;GACpE,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAC1D,CAAC;CACF;CAEA,SAAS,SAAsB,UAA8B;EAC5D,OAAO,OAAO,QAAQ,MAAM,SAAS,YAAY,CAAC,QAAQ,CAAC,CAAC;CAC7D;CAEA,QAAQ,QAAqB,QAAsB;EAClD,QAAQ,MAAM,QAAQ,YAAY,CAAC,MAAM,CAAC;CAC3C;CAEA,WACC,MACA,QACA,QACA,QACO;EACP,KAAK,OAAO,MAAM;EAClB,OAAO,QAAQ,MAAM;CACtB;CAEA,cACC,QACA,QACA,QACO;EACP,aAAa,MAAM;EACnB,OAAO,QAAQ,MAAM;CACtB;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;AChHA,SAAgB,yBAA6C;CAC5D,OAAO,IAAI,iBAAiB;AAC7B;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,uBAA2C;CAC1D,OAAO,IAAI,eAAe;AAC3B;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,sBAA0C;CACzD,OAAO,IAAI,cAAc;AAC1B"}
1
+ {"version":3,"file":"index.js","names":["#postTask","#timer","#yieldVia","#frame","#sleep","#idleAPI","#sleep","#idle","#request","#cancel"],"sources":["../../../src/browser/constants.ts","../../../src/browser/BrowserScheduler.ts","../../../src/browser/FrameScheduler.ts","../../../src/browser/IdleScheduler.ts","../../../src/browser/factories.ts"],"sourcesContent":["import type { SchedulerPriority } from '@src/core'\n\n/**\n * The browser-native `postTask` priority for each portable {@link SchedulerPriority} — the\n * Prioritized Task Scheduling API's three levels.\n *\n * @remarks\n * A `user` hint maps to the most urgent `'user-blocking'`, `normal` to the default\n * `'user-visible'`, and `background` to `'background'`. {@link BrowserScheduler} reads this\n * map to translate the caller's portable priority into the value passed to\n * `scheduler.postTask`, so the urgency hint is honoured by the host.\n */\nexport const POST_TASK_PRIORITY: Readonly<Record<SchedulerPriority, string>> = Object.freeze({\n\tuser: 'user-blocking',\n\tnormal: 'user-visible',\n\tbackground: 'background',\n})\n","import type { SchedulerInterface, SchedulerOptions, SchedulerPriority } from '@src/core'\nimport { scheduleHost } from '@src/core'\nimport { isFunction, isPromise, isRecord } from '@orkestrel/contract'\nimport { POST_TASK_PRIORITY } from './constants.js'\n\n/**\n * The browser {@link SchedulerInterface} — the browser-native cooperative-yield backend\n * built on the Prioritized Task Scheduling API (`scheduler.postTask`), falling back to a\n * zero-delay macrotask where it is absent.\n *\n * @remarks\n * - **`yield` prefers `scheduler.postTask`, honouring priority.** When `globalThis`\n * exposes a `scheduler` with a `postTask` method, `yield()` posts a task at the mapped\n * priority (`user` → `'user-blocking'`, `normal` → `'user-visible'`, `background` →\n * `'background'`), so the host genuinely regains control and the urgency hint is\n * honoured. The capability is feature-detected through guards (`isRecord` / `isFunction`),\n * never an `as` (AGENTS §14). Where the API is absent (Firefox today, older engines),\n * it **falls back** to a `setTimeout(0)` macrotask — still a real host-turn, just\n * without priority. `delay(ms)` is always a real `setTimeout`.\n * - **Abort fidelity is verbatim.** The shared `scheduleHost` lifecycle links an owned\n * settlement composite before scheduling, preserving the exact caller reason without\n * invoking caller-owned signal methods. The caller signal is NOT handed to `postTask`;\n * an internal controller cancels that native task. An unexpected native promise rejection\n * is routed back as the exact host failure instead of being discarded.\n * - **Event-free.** A pure functional primitive — no Emitter, no events.\n *\n * @example\n * ```ts\n * import { createAbort } from '@orkestrel/abort'\n * import { BrowserScheduler } from '@orkestrel/workflow/browser'\n *\n * const abort = createAbort()\n * const scheduler = new BrowserScheduler()\n * while (!abort.signal.aborted) {\n * \tdoSomeWork()\n * \tawait scheduler.yield({ priority: 'background', signal: abort.signal })\n * }\n * ```\n */\nexport class BrowserScheduler implements SchedulerInterface {\n\t/**\n\t * Yield control to the host via `scheduler.postTask` at the given priority (or a\n\t * `setTimeout(0)` macrotask where the API is absent), then resume; abort rejects with\n\t * `signal.reason`.\n\t */\n\tyield(options?: SchedulerOptions): Promise<void> {\n\t\tconst post = this.#postTask()\n\t\tif (post === undefined) return this.#timer(0, options?.signal)\n\t\treturn this.#yieldVia(post, options?.priority ?? 'normal', options?.signal)\n\t}\n\n\t/**\n\t * Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with\n\t * `signal.reason`.\n\t *\n\t * @remarks\n\t * `ms` should be a non-negative finite number. The primitive does no validation: it\n\t * passes `ms` straight to the host `setTimeout`, which clamps a negative value or\n\t * `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than\n\t * throwing.\n\t */\n\tdelay(ms: number, options?: SchedulerOptions): Promise<void> {\n\t\treturn this.#timer(ms, options?.signal)\n\t}\n\n\t// === Private\n\n\t// Feature-detect the Prioritized Task Scheduling API through guards (no `as`): the\n\t// global `scheduler` must be a record carrying a callable `postTask`. Returns the\n\t// narrowed `postTask` function, or `undefined` when the API is absent (the fallback).\n\t#postTask(): ((callback: () => void, options: Record<string, unknown>) => unknown) | undefined {\n\t\tconst candidate: unknown = Reflect.get(globalThis, 'scheduler')\n\t\tif (!isRecord(candidate)) return undefined\n\t\tconst post = candidate.postTask\n\t\tif (!isFunction(post)) return undefined\n\t\treturn (callback, options) => Reflect.apply(post, candidate, [callback, options])\n\t}\n\n\t// A `scheduler.postTask` boundary with an internal cancellation controller; `scheduleHost`\n\t// owns caller linking and first-settlement arbitration, including native promise failure.\n\t#yieldVia(\n\t\tpost: (callback: () => void, options: Record<string, unknown>) => unknown,\n\t\tpriority: SchedulerPriority,\n\t\tsignal?: AbortSignal,\n\t): Promise<void> {\n\t\treturn scheduleHost((complete, failure) => {\n\t\t\tconst internal = new AbortController()\n\t\t\tconst task = post(complete, {\n\t\t\t\tpriority: POST_TASK_PRIORITY[priority],\n\t\t\t\tsignal: internal.signal,\n\t\t\t})\n\t\t\tif (isPromise(task)) void task.catch(failure)\n\t\t\treturn () => internal.abort()\n\t\t}, signal)\n\t}\n\n\t// The browser timer boundary shared by `delay` and fallback `yield`.\n\t#timer(ms: number, signal?: AbortSignal): Promise<void> {\n\t\treturn scheduleHost((complete) => {\n\t\t\tconst handle = setTimeout(complete, ms)\n\t\t\treturn () => clearTimeout(handle)\n\t\t}, signal)\n\t}\n}\n","import type { SchedulerInterface, SchedulerOptions } from '@src/core'\nimport { scheduleHost } from '@src/core'\n\n/**\n * The frame-aligned {@link SchedulerInterface} — a browser cooperative-yield backend\n * whose `yield` resumes just before the next paint via `requestAnimationFrame`.\n *\n * @remarks\n * - **`yield` resumes before the next paint.** `yield()` waits on `requestAnimationFrame`,\n * so the resumption is aligned to the browser's render loop — ideal for work that\n * should batch per frame (animation, incremental DOM updates) and pause while the tab\n * is hidden (the host throttles rAF). `delay(ms)` is a real `setTimeout`, unaligned to\n * frames. `options.priority` is accepted for contract compliance but a no-op — a frame\n * callback has no priority dimension.\n * - **Abort fidelity is verbatim, with cleanup.** The shared `scheduleHost` lifecycle links\n * an owned settlement composite before requesting a frame, never invokes caller-owned\n * signal methods, and cancels the native handle when abort wins.\n * - **Event-free.** A pure functional primitive — no Emitter, no events.\n *\n * @example\n * ```ts\n * import { createAbort } from '@orkestrel/abort'\n * import { FrameScheduler } from '@orkestrel/workflow/browser'\n *\n * const abort = createAbort()\n * const scheduler = new FrameScheduler()\n * while (!abort.signal.aborted) {\n * \trenderOneFrameOfWork()\n * \tawait scheduler.yield({ signal: abort.signal }) // resume before the next paint\n * }\n * ```\n */\nexport class FrameScheduler implements SchedulerInterface {\n\t/**\n\t * Yield control to the host until just before the next paint via\n\t * `requestAnimationFrame`, then resume; abort rejects with `signal.reason`.\n\t */\n\tyield(options?: SchedulerOptions): Promise<void> {\n\t\treturn this.#frame(options?.signal)\n\t}\n\n\t/**\n\t * Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with\n\t * `signal.reason`.\n\t *\n\t * @remarks\n\t * `ms` should be a non-negative finite number. The primitive does no validation: it\n\t * passes `ms` straight to the host `setTimeout`, which clamps a negative value or\n\t * `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than\n\t * throwing.\n\t */\n\tdelay(ms: number, options?: SchedulerOptions): Promise<void> {\n\t\treturn this.#sleep(ms, options?.signal)\n\t}\n\n\t// === Private\n\n\t// The frame request boundary; `scheduleHost` owns cancellation lifecycle.\n\t#frame(signal?: AbortSignal): Promise<void> {\n\t\treturn scheduleHost((complete) => {\n\t\t\tconst handle = requestAnimationFrame(complete)\n\t\t\treturn () => cancelAnimationFrame(handle)\n\t\t}, signal)\n\t}\n\n\t// The browser timer boundary; `scheduleHost` owns cancellation lifecycle.\n\t#sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\t\treturn scheduleHost((complete) => {\n\t\t\tconst handle = setTimeout(complete, ms)\n\t\t\treturn () => clearTimeout(handle)\n\t\t}, signal)\n\t}\n}\n","import type { SchedulerInterface, SchedulerOptions } from '@src/core'\nimport { scheduleHost } from '@src/core'\nimport type { AnyFunction } from '@orkestrel/contract'\nimport type { IdleAPI } from './types.js'\nimport { isFunction } from '@orkestrel/contract'\n\n/**\n * The idle-time {@link SchedulerInterface} — a browser cooperative-yield backend whose\n * `yield` resumes when the host is idle via `requestIdleCallback`, falling back to a\n * zero-delay macrotask where it is absent.\n *\n * @remarks\n * - **`yield` resumes during idle time.** When `globalThis` exposes `requestIdleCallback`,\n * `yield()` waits on it, so the resumption happens when the browser has spare time after\n * rendering and input — ideal for low-priority background work that must not contend with\n * the user. The capability is feature-detected through a guard (`isFunction`), never an\n * `as` (AGENTS §14). Where the API is absent (Safari today), it **falls back** to a\n * `setTimeout(0)` macrotask — still a real host-turn, just not idle-gated. `delay(ms)` is\n * always a real `setTimeout`. `options.priority` is accepted for contract compliance but a\n * no-op — idle scheduling has no priority dimension.\n * - **Abort fidelity is verbatim, with cleanup.** The shared `scheduleHost` lifecycle links\n * an owned settlement composite before scheduling, never invokes caller-owned signal\n * methods, and cancels the idle callback or fallback timer when abort wins.\n * - **Event-free.** A pure functional primitive — no Emitter, no events.\n *\n * @example\n * ```ts\n * import { createAbort } from '@orkestrel/abort'\n * import { IdleScheduler } from '@orkestrel/workflow/browser'\n *\n * const abort = createAbort()\n * const scheduler = new IdleScheduler()\n * while (!abort.signal.aborted) {\n * \tdoLowPriorityWork()\n * \tawait scheduler.yield({ signal: abort.signal }) // resume when the host is idle\n * }\n * ```\n */\nexport class IdleScheduler implements SchedulerInterface {\n\t/**\n\t * Yield control to the host until it is idle via `requestIdleCallback` (or a\n\t * `setTimeout(0)` macrotask where the API is absent), then resume; abort rejects with\n\t * `signal.reason`.\n\t */\n\tyield(options?: SchedulerOptions): Promise<void> {\n\t\tconst idle = this.#idleAPI()\n\t\tif (idle === undefined) return this.#sleep(0, options?.signal)\n\t\treturn this.#idle(idle, options?.signal)\n\t}\n\n\t/**\n\t * Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with\n\t * `signal.reason`.\n\t *\n\t * @remarks\n\t * `ms` should be a non-negative finite number. The primitive does no validation: it\n\t * passes `ms` straight to the host `setTimeout`, which clamps a negative value or\n\t * `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than\n\t * throwing.\n\t */\n\tdelay(ms: number, options?: SchedulerOptions): Promise<void> {\n\t\treturn this.#sleep(ms, options?.signal)\n\t}\n\n\t// === Private\n\n\t// Feature-detect `requestIdleCallback` / `cancelIdleCallback` off `globalThis` through\n\t// guards (no `as`): both must be callable. Returns the narrowed pair, or `undefined`\n\t// when the API is absent (the macrotask fallback).\n\t#idleAPI(): IdleAPI | undefined {\n\t\tconst request: unknown = Reflect.get(globalThis, 'requestIdleCallback')\n\t\tconst cancel: unknown = Reflect.get(globalThis, 'cancelIdleCallback')\n\t\tif (!isFunction(request) || !isFunction(cancel)) return undefined\n\t\treturn {\n\t\t\trequest: this.#request.bind(this, request),\n\t\t\tcancel: this.#cancel.bind(this, cancel),\n\t\t}\n\t}\n\n\t// The idle callback boundary; `scheduleHost` owns cancellation lifecycle.\n\t#idle(idle: IdleAPI, signal?: AbortSignal): Promise<void> {\n\t\treturn scheduleHost((complete) => {\n\t\t\tconst handle = idle.request(complete)\n\t\t\treturn () => idle.cancel(handle)\n\t\t}, signal)\n\t}\n\n\t// The browser timer boundary shared by `delay` and fallback `yield`.\n\t#sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\t\treturn scheduleHost((complete) => {\n\t\t\tconst handle = setTimeout(complete, ms)\n\t\t\treturn () => clearTimeout(handle)\n\t\t}, signal)\n\t}\n\n\t#request(request: AnyFunction, callback: () => void): number {\n\t\treturn Number(Reflect.apply(request, globalThis, [callback]))\n\t}\n\n\t#cancel(cancel: AnyFunction, handle: number): void {\n\t\tReflect.apply(cancel, globalThis, [handle])\n\t}\n}\n","import type { SchedulerInterface } from '@src/core'\nimport { BrowserScheduler } from './BrowserScheduler.js'\nimport { FrameScheduler } from './FrameScheduler.js'\nimport { IdleScheduler } from './IdleScheduler.js'\n\n/**\n * Create the browser-native cooperative-yield {@link SchedulerInterface} — `yield()` uses\n * the Prioritized Task Scheduling API (`scheduler.postTask`) at the requested priority\n * when present, falling back to a `setTimeout(0)` macrotask; `delay(ms)` is a real\n * `setTimeout`.\n *\n * @remarks\n * The default browser scheduler: it honours `options.priority` (`user` /\n * `normal` / `background`) when `scheduler.postTask` is available and degrades to a plain\n * macrotask elsewhere. Both methods are abort-aware: pass `options.signal` and a pending\n * yield/delay rejects with the signal's exact `reason`; the shared owned-signal lifecycle\n * cancels the native handle without invoking caller listener methods. Prefer\n * {@link createFrameScheduler} for paint-aligned work or\n * {@link createIdleScheduler} for idle-time background work.\n *\n * @returns A {@link SchedulerInterface} backed by `scheduler.postTask` (or a macrotask)\n *\n * @example\n * ```ts\n * import { createBrowserScheduler } from '@orkestrel/workflow/browser'\n *\n * const scheduler = createBrowserScheduler()\n * await scheduler.yield({ priority: 'background' })\n * ```\n */\nexport function createBrowserScheduler(): SchedulerInterface {\n\treturn new BrowserScheduler()\n}\n\n/**\n * Create the frame-aligned cooperative-yield {@link SchedulerInterface} — `yield()` resumes\n * just before the next paint via `requestAnimationFrame`; `delay(ms)` is a real\n * `setTimeout`.\n *\n * @remarks\n * Use it for work that should batch per render frame (animation, incremental DOM updates)\n * and naturally pause while the tab is hidden. `yield` is abort-aware: pass `options.signal`\n * and a pending yield rejects with the signal's `reason` verbatim, cancelling the pending\n * frame request. `options.priority` is accepted but a no-op — a frame callback has no\n * priority dimension.\n *\n * @returns A {@link SchedulerInterface} backed by `requestAnimationFrame`\n *\n * @example\n * ```ts\n * import { createFrameScheduler } from '@orkestrel/workflow/browser'\n *\n * const scheduler = createFrameScheduler()\n * await scheduler.yield() // resumes before the next paint\n * ```\n */\nexport function createFrameScheduler(): SchedulerInterface {\n\treturn new FrameScheduler()\n}\n\n/**\n * Create the idle-time cooperative-yield {@link SchedulerInterface} — `yield()` resumes when\n * the host is idle via `requestIdleCallback` when present, falling back to a `setTimeout(0)`\n * macrotask; `delay(ms)` is a real `setTimeout`.\n *\n * @remarks\n * Use it for low-priority background work that must not contend with rendering or input.\n * Where `requestIdleCallback` is absent (Safari today) it degrades to a plain macrotask.\n * `yield` is abort-aware: pass `options.signal` and a pending yield rejects with the\n * signal's `reason` verbatim, cancelling the pending idle callback. `options.priority` is\n * accepted but a no-op — idle scheduling has no priority dimension.\n *\n * @returns A {@link SchedulerInterface} backed by `requestIdleCallback` (or a macrotask)\n *\n * @example\n * ```ts\n * import { createIdleScheduler } from '@orkestrel/workflow/browser'\n *\n * const scheduler = createIdleScheduler()\n * await scheduler.yield() // resumes when the host is idle\n * ```\n */\nexport function createIdleScheduler(): SchedulerInterface {\n\treturn new IdleScheduler()\n}\n"],"mappings":"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"}