@orkestrel/workflow 0.0.1

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.
@@ -0,0 +1,129 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/server/NodeScheduler.ts
3
+ /**
4
+ * The Node {@link SchedulerInterface} — the server-native cooperative-yield backend.
5
+ *
6
+ * @remarks
7
+ * - **`yield` is a `setImmediate` host-turn.** `yield()` waits on `setImmediate`, the
8
+ * canonical Node "give the host a turn" — it runs AFTER the current operation and
9
+ * any pending I/O callbacks, so the event loop genuinely regains control before
10
+ * resuming (unlike a microtask, which drains within the current task). `delay(ms)`
11
+ * waits on a real `setTimeout`.
12
+ * - **Abort fidelity is verbatim.** A pending `yield` / `delay` rejects with
13
+ * `signal.reason` exactly — the value the caller passed, never wrapped or replaced.
14
+ * The discipline mirrors the cross-environment default's `#sleep`: an already-aborted
15
+ * signal rejects immediately WITHOUT arming a timer; otherwise the timer is armed and
16
+ * a `{ once: true }` abort listener attached, and the two settle paths are mutually
17
+ * exclusive — the timer path removes the listener before resolving (so a later abort
18
+ * cannot reach `reject`), and the abort path clears the timer before rejecting (so the
19
+ * macrotask cannot reach `resolve`). The promise settles exactly once, with no leaked
20
+ * timer and no leaked listener. It deliberately does NOT use `node:timers/promises`,
21
+ * whose `{ signal }` option rejects with a Node `AbortError` (`code: 'ABORT_ERR'`)
22
+ * rather than the caller's `signal.reason` — that would break reason fidelity, so the
23
+ * timer and listener are hand-rolled to match the contract.
24
+ * - **Priority is accepted but a no-op.** Node has no priority primitive (no equivalent
25
+ * of the browser's `scheduler.postTask` priorities), so `options.priority` is accepted
26
+ * for contract compliance and ignored — every yield/delay is uniform.
27
+ * - **Event-free.** A pure functional primitive — no Emitter, no events.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { createAbort } from '@src/core'
32
+ * import { NodeScheduler } from '@src/server'
33
+ *
34
+ * const abort = createAbort()
35
+ * const scheduler = new NodeScheduler()
36
+ * while (!abort.signal.aborted) {
37
+ * doSomeWork()
38
+ * await scheduler.yield({ signal: abort.signal }) // a setImmediate host-turn
39
+ * }
40
+ * ```
41
+ */
42
+ var NodeScheduler = class {
43
+ /**
44
+ * Yield control back to the event loop via `setImmediate` so pending I/O and timers
45
+ * can run, then resume; abort rejects with `signal.reason`.
46
+ */
47
+ yield(options) {
48
+ return this.#immediate(options?.signal);
49
+ }
50
+ /**
51
+ * Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with
52
+ * `signal.reason`.
53
+ *
54
+ * @remarks
55
+ * `ms` should be a non-negative finite number. The primitive does no validation: it
56
+ * passes `ms` straight to the host `setTimeout`, which clamps a negative value or
57
+ * `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than
58
+ * throwing.
59
+ */
60
+ delay(ms, options) {
61
+ return this.#sleep(ms, options?.signal);
62
+ }
63
+ #immediate(signal) {
64
+ if (signal?.aborted === true) return Promise.reject(signal.reason);
65
+ return new Promise((resolve, reject) => {
66
+ const onAbort = () => {
67
+ clearImmediate(handle);
68
+ reject(signal?.reason);
69
+ };
70
+ const handle = setImmediate(() => {
71
+ signal?.removeEventListener("abort", onAbort);
72
+ resolve();
73
+ });
74
+ signal?.addEventListener("abort", onAbort, { once: true });
75
+ });
76
+ }
77
+ #sleep(ms, signal) {
78
+ if (signal?.aborted === true) return Promise.reject(signal.reason);
79
+ return new Promise((resolve, reject) => {
80
+ const onAbort = () => {
81
+ clearTimeout(handle);
82
+ reject(signal?.reason);
83
+ };
84
+ const handle = setTimeout(() => {
85
+ signal?.removeEventListener("abort", onAbort);
86
+ resolve();
87
+ }, ms);
88
+ signal?.addEventListener("abort", onAbort, { once: true });
89
+ });
90
+ }
91
+ };
92
+ //#endregion
93
+ //#region src/server/factories.ts
94
+ /**
95
+ * Create the Node-native cooperative-yield {@link SchedulerInterface} — `yield()` is a
96
+ * `setImmediate` host-turn (the canonical Node "give the event loop a turn"), `delay(ms)`
97
+ * a real `setTimeout`.
98
+ *
99
+ * @remarks
100
+ * Use it on a server instead of the cross-environment `createScheduler` when a yield
101
+ * should hand the event loop a full turn (after pending I/O) via `setImmediate` rather
102
+ * than a zero-delay timer. Both methods are abort-aware: pass `options.signal` and a
103
+ * pending yield/delay rejects with the signal's `reason` (verbatim, with full
104
+ * timer/listener cleanup). `options.priority` is accepted for contract compliance but a
105
+ * no-op — Node has no priority primitive.
106
+ *
107
+ * @returns A {@link SchedulerInterface} backed by Node's `setImmediate` / `setTimeout`
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * import { createAbort } from '@src/core'
112
+ * import { createNodeScheduler } from '@src/server'
113
+ *
114
+ * const abort = createAbort()
115
+ * const scheduler = createNodeScheduler()
116
+ * while (!abort.signal.aborted) {
117
+ * doSomeWork()
118
+ * await scheduler.yield({ signal: abort.signal })
119
+ * }
120
+ * ```
121
+ */
122
+ function createNodeScheduler() {
123
+ return new NodeScheduler();
124
+ }
125
+ //#endregion
126
+ exports.NodeScheduler = NodeScheduler;
127
+ exports.createNodeScheduler = createNodeScheduler;
128
+
129
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["#immediate","#sleep"],"sources":["../../../src/server/NodeScheduler.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { SchedulerInterface, SchedulerOptions } from '@src/core'\n\n/**\n * The Node {@link SchedulerInterface} — the server-native cooperative-yield backend.\n *\n * @remarks\n * - **`yield` is a `setImmediate` host-turn.** `yield()` waits on `setImmediate`, the\n * canonical Node \"give the host a turn\" — it runs AFTER the current operation and\n * any pending I/O callbacks, so the event loop genuinely regains control before\n * resuming (unlike a microtask, which drains within the current task). `delay(ms)`\n * waits on a real `setTimeout`.\n * - **Abort fidelity is verbatim.** A pending `yield` / `delay` rejects with\n * `signal.reason` exactly — the value the caller passed, never wrapped or replaced.\n * The discipline mirrors the cross-environment default's `#sleep`: an already-aborted\n * signal rejects immediately WITHOUT arming a timer; otherwise the timer is armed and\n * a `{ once: true }` abort listener attached, and the two settle paths are mutually\n * exclusive — the timer path removes the listener before resolving (so a later abort\n * cannot reach `reject`), and the abort path clears the timer before rejecting (so the\n * macrotask cannot reach `resolve`). The promise settles exactly once, with no leaked\n * timer and no leaked listener. It deliberately does NOT use `node:timers/promises`,\n * whose `{ signal }` option rejects with a Node `AbortError` (`code: 'ABORT_ERR'`)\n * rather than the caller's `signal.reason` — that would break reason fidelity, so the\n * timer and listener are hand-rolled to match the contract.\n * - **Priority is accepted but a no-op.** Node has no priority primitive (no equivalent\n * of the browser's `scheduler.postTask` priorities), so `options.priority` is accepted\n * for contract compliance and ignored — every yield/delay is uniform.\n * - **Event-free.** A pure functional primitive — no Emitter, no events.\n *\n * @example\n * ```ts\n * import { createAbort } from '@src/core'\n * import { NodeScheduler } from '@src/server'\n *\n * const abort = createAbort()\n * const scheduler = new NodeScheduler()\n * while (!abort.signal.aborted) {\n * \tdoSomeWork()\n * \tawait scheduler.yield({ signal: abort.signal }) // a setImmediate host-turn\n * }\n * ```\n */\nexport class NodeScheduler implements SchedulerInterface {\n\t/**\n\t * Yield control back to the event loop via `setImmediate` so pending I/O and timers\n\t * can run, then resume; abort rejects with `signal.reason`.\n\t */\n\tyield(options?: SchedulerOptions): Promise<void> {\n\t\treturn this.#immediate(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 `setImmediate` host-turn for `yield`. Resolves after the immediate callback\n\t// fires (after the current operation and pending I/O); rejects with `signal.reason`\n\t// if the signal is already aborted (no immediate armed) or aborts while pending.\n\t// The two settle paths disarm each other: the immediate path removes the abort\n\t// listener before resolving; the abort path clears the immediate before rejecting —\n\t// so it settles exactly once, with no leaked handle and no leaked listener.\n\t#immediate(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 onAbort = () => {\n\t\t\t\tclearImmediate(handle)\n\t\t\t\treject(signal?.reason)\n\t\t\t}\n\t\t\tconst handle = setImmediate(() => {\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\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\n\t// `#immediate`, with a timer in place of the immediate: an already-aborted signal\n\t// rejects without arming; otherwise the timer path removes the listener before\n\t// resolving and the abort path clears the timer before rejecting with `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 onAbort = () => {\n\t\t\t\tclearTimeout(handle)\n\t\t\t\treject(signal?.reason)\n\t\t\t}\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\tsignal?.addEventListener('abort', onAbort, { once: true })\n\t\t})\n\t}\n}\n","import type { SchedulerInterface } from '@src/core'\nimport { NodeScheduler } from './NodeScheduler.js'\n\n/**\n * Create the Node-native cooperative-yield {@link SchedulerInterface} — `yield()` is a\n * `setImmediate` host-turn (the canonical Node \"give the event loop a turn\"), `delay(ms)`\n * a real `setTimeout`.\n *\n * @remarks\n * Use it on a server instead of the cross-environment `createScheduler` when a yield\n * should hand the event loop a full turn (after pending I/O) via `setImmediate` rather\n * than a zero-delay timer. Both methods are abort-aware: pass `options.signal` and a\n * pending yield/delay rejects with the signal's `reason` (verbatim, with full\n * timer/listener cleanup). `options.priority` is accepted for contract compliance but a\n * no-op — Node has no priority primitive.\n *\n * @returns A {@link SchedulerInterface} backed by Node's `setImmediate` / `setTimeout`\n *\n * @example\n * ```ts\n * import { createAbort } from '@src/core'\n * import { createNodeScheduler } from '@src/server'\n *\n * const abort = createAbort()\n * const scheduler = createNodeScheduler()\n * while (!abort.signal.aborted) {\n * \tdoSomeWork()\n * \tawait scheduler.yield({ signal: abort.signal })\n * }\n * ```\n */\nexport function createNodeScheduler(): SchedulerInterface {\n\treturn new NodeScheduler()\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAa,gBAAb,MAAyD;;;;;CAKxD,MAAM,SAA2C;EAChD,OAAO,KAAKA,WAAW,SAAS,MAAM;CACvC;;;;;;;;;;;CAYA,MAAM,IAAY,SAA2C;EAC5D,OAAO,KAAKC,OAAO,IAAI,SAAS,MAAM;CACvC;CAUA,WAAW,QAAqC;EAC/C,IAAI,QAAQ,YAAY,MAAM,OAAO,QAAQ,OAAO,OAAO,MAAM;EACjE,OAAO,IAAI,SAAe,SAAS,WAAW;GAC7C,MAAM,gBAAgB;IACrB,eAAe,MAAM;IACrB,OAAO,QAAQ,MAAM;GACtB;GACA,MAAM,SAAS,mBAAmB;IACjC,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,QAAQ;GACT,CAAC;GACD,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,gBAAgB;IACrB,aAAa,MAAM;IACnB,OAAO,QAAQ,MAAM;GACtB;GACA,MAAM,SAAS,iBAAiB;IAC/B,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,QAAQ;GACT,GAAG,EAAE;GACL,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAC1D,CAAC;CACF;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC1EA,SAAgB,sBAA0C;CACzD,OAAO,IAAI,cAAc;AAC1B"}
@@ -0,0 +1,93 @@
1
+ import { SchedulerInterface } from '../core/index.js';
2
+ import { SchedulerOptions } from '../core/index.js';
3
+
4
+ /**
5
+ * Create the Node-native cooperative-yield {@link SchedulerInterface} — `yield()` is a
6
+ * `setImmediate` host-turn (the canonical Node "give the event loop a turn"), `delay(ms)`
7
+ * a real `setTimeout`.
8
+ *
9
+ * @remarks
10
+ * Use it on a server instead of the cross-environment `createScheduler` when a yield
11
+ * should hand the event loop a full turn (after pending I/O) via `setImmediate` rather
12
+ * than a zero-delay timer. Both methods are abort-aware: pass `options.signal` and a
13
+ * pending yield/delay rejects with the signal's `reason` (verbatim, with full
14
+ * timer/listener cleanup). `options.priority` is accepted for contract compliance but a
15
+ * no-op — Node has no priority primitive.
16
+ *
17
+ * @returns A {@link SchedulerInterface} backed by Node's `setImmediate` / `setTimeout`
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { createAbort } from '../core/index.js'
22
+ * import { createNodeScheduler } from '@src/server'
23
+ *
24
+ * const abort = createAbort()
25
+ * const scheduler = createNodeScheduler()
26
+ * while (!abort.signal.aborted) {
27
+ * doSomeWork()
28
+ * await scheduler.yield({ signal: abort.signal })
29
+ * }
30
+ * ```
31
+ */
32
+ export declare function createNodeScheduler(): SchedulerInterface;
33
+
34
+ /**
35
+ * The Node {@link SchedulerInterface} — the server-native cooperative-yield backend.
36
+ *
37
+ * @remarks
38
+ * - **`yield` is a `setImmediate` host-turn.** `yield()` waits on `setImmediate`, the
39
+ * canonical Node "give the host a turn" — it runs AFTER the current operation and
40
+ * any pending I/O callbacks, so the event loop genuinely regains control before
41
+ * resuming (unlike a microtask, which drains within the current task). `delay(ms)`
42
+ * waits on a real `setTimeout`.
43
+ * - **Abort fidelity is verbatim.** A pending `yield` / `delay` rejects with
44
+ * `signal.reason` exactly — the value the caller passed, never wrapped or replaced.
45
+ * The discipline mirrors the cross-environment default's `#sleep`: an already-aborted
46
+ * signal rejects immediately WITHOUT arming a timer; otherwise the timer is armed and
47
+ * a `{ once: true }` abort listener attached, and the two settle paths are mutually
48
+ * exclusive — the timer path removes the listener before resolving (so a later abort
49
+ * cannot reach `reject`), and the abort path clears the timer before rejecting (so the
50
+ * macrotask cannot reach `resolve`). The promise settles exactly once, with no leaked
51
+ * timer and no leaked listener. It deliberately does NOT use `node:timers/promises`,
52
+ * whose `{ signal }` option rejects with a Node `AbortError` (`code: 'ABORT_ERR'`)
53
+ * rather than the caller's `signal.reason` — that would break reason fidelity, so the
54
+ * timer and listener are hand-rolled to match the contract.
55
+ * - **Priority is accepted but a no-op.** Node has no priority primitive (no equivalent
56
+ * of the browser's `scheduler.postTask` priorities), so `options.priority` is accepted
57
+ * for contract compliance and ignored — every yield/delay is uniform.
58
+ * - **Event-free.** A pure functional primitive — no Emitter, no events.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * import { createAbort } from '../core/index.js'
63
+ * import { NodeScheduler } from '@src/server'
64
+ *
65
+ * const abort = createAbort()
66
+ * const scheduler = new NodeScheduler()
67
+ * while (!abort.signal.aborted) {
68
+ * doSomeWork()
69
+ * await scheduler.yield({ signal: abort.signal }) // a setImmediate host-turn
70
+ * }
71
+ * ```
72
+ */
73
+ export declare class NodeScheduler implements SchedulerInterface {
74
+ #private;
75
+ /**
76
+ * Yield control back to the event loop via `setImmediate` so pending I/O and timers
77
+ * can run, then resume; abort rejects with `signal.reason`.
78
+ */
79
+ yield(options?: SchedulerOptions): Promise<void>;
80
+ /**
81
+ * Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with
82
+ * `signal.reason`.
83
+ *
84
+ * @remarks
85
+ * `ms` should be a non-negative finite number. The primitive does no validation: it
86
+ * passes `ms` straight to the host `setTimeout`, which clamps a negative value or
87
+ * `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than
88
+ * throwing.
89
+ */
90
+ delay(ms: number, options?: SchedulerOptions): Promise<void>;
91
+ }
92
+
93
+ export { }
@@ -0,0 +1,93 @@
1
+ import { SchedulerInterface } from '../core/index.js';
2
+ import { SchedulerOptions } from '../core/index.js';
3
+
4
+ /**
5
+ * Create the Node-native cooperative-yield {@link SchedulerInterface} — `yield()` is a
6
+ * `setImmediate` host-turn (the canonical Node "give the event loop a turn"), `delay(ms)`
7
+ * a real `setTimeout`.
8
+ *
9
+ * @remarks
10
+ * Use it on a server instead of the cross-environment `createScheduler` when a yield
11
+ * should hand the event loop a full turn (after pending I/O) via `setImmediate` rather
12
+ * than a zero-delay timer. Both methods are abort-aware: pass `options.signal` and a
13
+ * pending yield/delay rejects with the signal's `reason` (verbatim, with full
14
+ * timer/listener cleanup). `options.priority` is accepted for contract compliance but a
15
+ * no-op — Node has no priority primitive.
16
+ *
17
+ * @returns A {@link SchedulerInterface} backed by Node's `setImmediate` / `setTimeout`
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { createAbort } from '../core/index.js'
22
+ * import { createNodeScheduler } from '@src/server'
23
+ *
24
+ * const abort = createAbort()
25
+ * const scheduler = createNodeScheduler()
26
+ * while (!abort.signal.aborted) {
27
+ * doSomeWork()
28
+ * await scheduler.yield({ signal: abort.signal })
29
+ * }
30
+ * ```
31
+ */
32
+ export declare function createNodeScheduler(): SchedulerInterface;
33
+
34
+ /**
35
+ * The Node {@link SchedulerInterface} — the server-native cooperative-yield backend.
36
+ *
37
+ * @remarks
38
+ * - **`yield` is a `setImmediate` host-turn.** `yield()` waits on `setImmediate`, the
39
+ * canonical Node "give the host a turn" — it runs AFTER the current operation and
40
+ * any pending I/O callbacks, so the event loop genuinely regains control before
41
+ * resuming (unlike a microtask, which drains within the current task). `delay(ms)`
42
+ * waits on a real `setTimeout`.
43
+ * - **Abort fidelity is verbatim.** A pending `yield` / `delay` rejects with
44
+ * `signal.reason` exactly — the value the caller passed, never wrapped or replaced.
45
+ * The discipline mirrors the cross-environment default's `#sleep`: an already-aborted
46
+ * signal rejects immediately WITHOUT arming a timer; otherwise the timer is armed and
47
+ * a `{ once: true }` abort listener attached, and the two settle paths are mutually
48
+ * exclusive — the timer path removes the listener before resolving (so a later abort
49
+ * cannot reach `reject`), and the abort path clears the timer before rejecting (so the
50
+ * macrotask cannot reach `resolve`). The promise settles exactly once, with no leaked
51
+ * timer and no leaked listener. It deliberately does NOT use `node:timers/promises`,
52
+ * whose `{ signal }` option rejects with a Node `AbortError` (`code: 'ABORT_ERR'`)
53
+ * rather than the caller's `signal.reason` — that would break reason fidelity, so the
54
+ * timer and listener are hand-rolled to match the contract.
55
+ * - **Priority is accepted but a no-op.** Node has no priority primitive (no equivalent
56
+ * of the browser's `scheduler.postTask` priorities), so `options.priority` is accepted
57
+ * for contract compliance and ignored — every yield/delay is uniform.
58
+ * - **Event-free.** A pure functional primitive — no Emitter, no events.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * import { createAbort } from '../core/index.js'
63
+ * import { NodeScheduler } from '@src/server'
64
+ *
65
+ * const abort = createAbort()
66
+ * const scheduler = new NodeScheduler()
67
+ * while (!abort.signal.aborted) {
68
+ * doSomeWork()
69
+ * await scheduler.yield({ signal: abort.signal }) // a setImmediate host-turn
70
+ * }
71
+ * ```
72
+ */
73
+ export declare class NodeScheduler implements SchedulerInterface {
74
+ #private;
75
+ /**
76
+ * Yield control back to the event loop via `setImmediate` so pending I/O and timers
77
+ * can run, then resume; abort rejects with `signal.reason`.
78
+ */
79
+ yield(options?: SchedulerOptions): Promise<void>;
80
+ /**
81
+ * Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with
82
+ * `signal.reason`.
83
+ *
84
+ * @remarks
85
+ * `ms` should be a non-negative finite number. The primitive does no validation: it
86
+ * passes `ms` straight to the host `setTimeout`, which clamps a negative value or
87
+ * `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than
88
+ * throwing.
89
+ */
90
+ delay(ms: number, options?: SchedulerOptions): Promise<void>;
91
+ }
92
+
93
+ export { }
@@ -0,0 +1,127 @@
1
+ //#region src/server/NodeScheduler.ts
2
+ /**
3
+ * The Node {@link SchedulerInterface} — the server-native cooperative-yield backend.
4
+ *
5
+ * @remarks
6
+ * - **`yield` is a `setImmediate` host-turn.** `yield()` waits on `setImmediate`, the
7
+ * canonical Node "give the host a turn" — it runs AFTER the current operation and
8
+ * any pending I/O callbacks, so the event loop genuinely regains control before
9
+ * resuming (unlike a microtask, which drains within the current task). `delay(ms)`
10
+ * waits on a real `setTimeout`.
11
+ * - **Abort fidelity is verbatim.** A pending `yield` / `delay` rejects with
12
+ * `signal.reason` exactly — the value the caller passed, never wrapped or replaced.
13
+ * The discipline mirrors the cross-environment default's `#sleep`: an already-aborted
14
+ * signal rejects immediately WITHOUT arming a timer; otherwise the timer is armed and
15
+ * a `{ once: true }` abort listener attached, and the two settle paths are mutually
16
+ * exclusive — the timer path removes the listener before resolving (so a later abort
17
+ * cannot reach `reject`), and the abort path clears the timer before rejecting (so the
18
+ * macrotask cannot reach `resolve`). The promise settles exactly once, with no leaked
19
+ * timer and no leaked listener. It deliberately does NOT use `node:timers/promises`,
20
+ * whose `{ signal }` option rejects with a Node `AbortError` (`code: 'ABORT_ERR'`)
21
+ * rather than the caller's `signal.reason` — that would break reason fidelity, so the
22
+ * timer and listener are hand-rolled to match the contract.
23
+ * - **Priority is accepted but a no-op.** Node has no priority primitive (no equivalent
24
+ * of the browser's `scheduler.postTask` priorities), so `options.priority` is accepted
25
+ * for contract compliance and ignored — every yield/delay is uniform.
26
+ * - **Event-free.** A pure functional primitive — no Emitter, no events.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { createAbort } from '@src/core'
31
+ * import { NodeScheduler } from '@src/server'
32
+ *
33
+ * const abort = createAbort()
34
+ * const scheduler = new NodeScheduler()
35
+ * while (!abort.signal.aborted) {
36
+ * doSomeWork()
37
+ * await scheduler.yield({ signal: abort.signal }) // a setImmediate host-turn
38
+ * }
39
+ * ```
40
+ */
41
+ var NodeScheduler = class {
42
+ /**
43
+ * Yield control back to the event loop via `setImmediate` so pending I/O and timers
44
+ * can run, then resume; abort rejects with `signal.reason`.
45
+ */
46
+ yield(options) {
47
+ return this.#immediate(options?.signal);
48
+ }
49
+ /**
50
+ * Resume after at least `ms` milliseconds via `setTimeout`; abort rejects with
51
+ * `signal.reason`.
52
+ *
53
+ * @remarks
54
+ * `ms` should be a non-negative finite number. The primitive does no validation: it
55
+ * passes `ms` straight to the host `setTimeout`, which clamps a negative value or
56
+ * `NaN` to ~0 — so an out-of-domain `ms` resolves on the next host turn rather than
57
+ * throwing.
58
+ */
59
+ delay(ms, options) {
60
+ return this.#sleep(ms, options?.signal);
61
+ }
62
+ #immediate(signal) {
63
+ if (signal?.aborted === true) return Promise.reject(signal.reason);
64
+ return new Promise((resolve, reject) => {
65
+ const onAbort = () => {
66
+ clearImmediate(handle);
67
+ reject(signal?.reason);
68
+ };
69
+ const handle = setImmediate(() => {
70
+ signal?.removeEventListener("abort", onAbort);
71
+ resolve();
72
+ });
73
+ signal?.addEventListener("abort", onAbort, { once: true });
74
+ });
75
+ }
76
+ #sleep(ms, signal) {
77
+ if (signal?.aborted === true) return Promise.reject(signal.reason);
78
+ return new Promise((resolve, reject) => {
79
+ const onAbort = () => {
80
+ clearTimeout(handle);
81
+ reject(signal?.reason);
82
+ };
83
+ const handle = setTimeout(() => {
84
+ signal?.removeEventListener("abort", onAbort);
85
+ resolve();
86
+ }, ms);
87
+ signal?.addEventListener("abort", onAbort, { once: true });
88
+ });
89
+ }
90
+ };
91
+ //#endregion
92
+ //#region src/server/factories.ts
93
+ /**
94
+ * Create the Node-native cooperative-yield {@link SchedulerInterface} — `yield()` is a
95
+ * `setImmediate` host-turn (the canonical Node "give the event loop a turn"), `delay(ms)`
96
+ * a real `setTimeout`.
97
+ *
98
+ * @remarks
99
+ * Use it on a server instead of the cross-environment `createScheduler` when a yield
100
+ * should hand the event loop a full turn (after pending I/O) via `setImmediate` rather
101
+ * than a zero-delay timer. Both methods are abort-aware: pass `options.signal` and a
102
+ * pending yield/delay rejects with the signal's `reason` (verbatim, with full
103
+ * timer/listener cleanup). `options.priority` is accepted for contract compliance but a
104
+ * no-op — Node has no priority primitive.
105
+ *
106
+ * @returns A {@link SchedulerInterface} backed by Node's `setImmediate` / `setTimeout`
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * import { createAbort } from '@src/core'
111
+ * import { createNodeScheduler } from '@src/server'
112
+ *
113
+ * const abort = createAbort()
114
+ * const scheduler = createNodeScheduler()
115
+ * while (!abort.signal.aborted) {
116
+ * doSomeWork()
117
+ * await scheduler.yield({ signal: abort.signal })
118
+ * }
119
+ * ```
120
+ */
121
+ function createNodeScheduler() {
122
+ return new NodeScheduler();
123
+ }
124
+ //#endregion
125
+ export { NodeScheduler, createNodeScheduler };
126
+
127
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["#immediate","#sleep"],"sources":["../../../src/server/NodeScheduler.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { SchedulerInterface, SchedulerOptions } from '@src/core'\n\n/**\n * The Node {@link SchedulerInterface} — the server-native cooperative-yield backend.\n *\n * @remarks\n * - **`yield` is a `setImmediate` host-turn.** `yield()` waits on `setImmediate`, the\n * canonical Node \"give the host a turn\" — it runs AFTER the current operation and\n * any pending I/O callbacks, so the event loop genuinely regains control before\n * resuming (unlike a microtask, which drains within the current task). `delay(ms)`\n * waits on a real `setTimeout`.\n * - **Abort fidelity is verbatim.** A pending `yield` / `delay` rejects with\n * `signal.reason` exactly — the value the caller passed, never wrapped or replaced.\n * The discipline mirrors the cross-environment default's `#sleep`: an already-aborted\n * signal rejects immediately WITHOUT arming a timer; otherwise the timer is armed and\n * a `{ once: true }` abort listener attached, and the two settle paths are mutually\n * exclusive — the timer path removes the listener before resolving (so a later abort\n * cannot reach `reject`), and the abort path clears the timer before rejecting (so the\n * macrotask cannot reach `resolve`). The promise settles exactly once, with no leaked\n * timer and no leaked listener. It deliberately does NOT use `node:timers/promises`,\n * whose `{ signal }` option rejects with a Node `AbortError` (`code: 'ABORT_ERR'`)\n * rather than the caller's `signal.reason` — that would break reason fidelity, so the\n * timer and listener are hand-rolled to match the contract.\n * - **Priority is accepted but a no-op.** Node has no priority primitive (no equivalent\n * of the browser's `scheduler.postTask` priorities), so `options.priority` is accepted\n * for contract compliance and ignored — every yield/delay is uniform.\n * - **Event-free.** A pure functional primitive — no Emitter, no events.\n *\n * @example\n * ```ts\n * import { createAbort } from '@src/core'\n * import { NodeScheduler } from '@src/server'\n *\n * const abort = createAbort()\n * const scheduler = new NodeScheduler()\n * while (!abort.signal.aborted) {\n * \tdoSomeWork()\n * \tawait scheduler.yield({ signal: abort.signal }) // a setImmediate host-turn\n * }\n * ```\n */\nexport class NodeScheduler implements SchedulerInterface {\n\t/**\n\t * Yield control back to the event loop via `setImmediate` so pending I/O and timers\n\t * can run, then resume; abort rejects with `signal.reason`.\n\t */\n\tyield(options?: SchedulerOptions): Promise<void> {\n\t\treturn this.#immediate(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 `setImmediate` host-turn for `yield`. Resolves after the immediate callback\n\t// fires (after the current operation and pending I/O); rejects with `signal.reason`\n\t// if the signal is already aborted (no immediate armed) or aborts while pending.\n\t// The two settle paths disarm each other: the immediate path removes the abort\n\t// listener before resolving; the abort path clears the immediate before rejecting —\n\t// so it settles exactly once, with no leaked handle and no leaked listener.\n\t#immediate(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 onAbort = () => {\n\t\t\t\tclearImmediate(handle)\n\t\t\t\treject(signal?.reason)\n\t\t\t}\n\t\t\tconst handle = setImmediate(() => {\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\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\n\t// `#immediate`, with a timer in place of the immediate: an already-aborted signal\n\t// rejects without arming; otherwise the timer path removes the listener before\n\t// resolving and the abort path clears the timer before rejecting with `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 onAbort = () => {\n\t\t\t\tclearTimeout(handle)\n\t\t\t\treject(signal?.reason)\n\t\t\t}\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\tsignal?.addEventListener('abort', onAbort, { once: true })\n\t\t})\n\t}\n}\n","import type { SchedulerInterface } from '@src/core'\nimport { NodeScheduler } from './NodeScheduler.js'\n\n/**\n * Create the Node-native cooperative-yield {@link SchedulerInterface} — `yield()` is a\n * `setImmediate` host-turn (the canonical Node \"give the event loop a turn\"), `delay(ms)`\n * a real `setTimeout`.\n *\n * @remarks\n * Use it on a server instead of the cross-environment `createScheduler` when a yield\n * should hand the event loop a full turn (after pending I/O) via `setImmediate` rather\n * than a zero-delay timer. Both methods are abort-aware: pass `options.signal` and a\n * pending yield/delay rejects with the signal's `reason` (verbatim, with full\n * timer/listener cleanup). `options.priority` is accepted for contract compliance but a\n * no-op — Node has no priority primitive.\n *\n * @returns A {@link SchedulerInterface} backed by Node's `setImmediate` / `setTimeout`\n *\n * @example\n * ```ts\n * import { createAbort } from '@src/core'\n * import { createNodeScheduler } from '@src/server'\n *\n * const abort = createAbort()\n * const scheduler = createNodeScheduler()\n * while (!abort.signal.aborted) {\n * \tdoSomeWork()\n * \tawait scheduler.yield({ signal: abort.signal })\n * }\n * ```\n */\nexport function createNodeScheduler(): SchedulerInterface {\n\treturn new NodeScheduler()\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAa,gBAAb,MAAyD;;;;;CAKxD,MAAM,SAA2C;EAChD,OAAO,KAAKA,WAAW,SAAS,MAAM;CACvC;;;;;;;;;;;CAYA,MAAM,IAAY,SAA2C;EAC5D,OAAO,KAAKC,OAAO,IAAI,SAAS,MAAM;CACvC;CAUA,WAAW,QAAqC;EAC/C,IAAI,QAAQ,YAAY,MAAM,OAAO,QAAQ,OAAO,OAAO,MAAM;EACjE,OAAO,IAAI,SAAe,SAAS,WAAW;GAC7C,MAAM,gBAAgB;IACrB,eAAe,MAAM;IACrB,OAAO,QAAQ,MAAM;GACtB;GACA,MAAM,SAAS,mBAAmB;IACjC,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,QAAQ;GACT,CAAC;GACD,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,gBAAgB;IACrB,aAAa,MAAM;IACnB,OAAO,QAAQ,MAAM;GACtB;GACA,MAAM,SAAS,iBAAiB;IAC/B,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,QAAQ;GACT,GAAG,EAAE;GACL,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;EAC1D,CAAC;CACF;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC1EA,SAAgB,sBAA0C;CACzD,OAAO,IAAI,cAAc;AAC1B"}
package/package.json ADDED
@@ -0,0 +1,111 @@
1
+ {
2
+ "name": "@orkestrel/workflow",
3
+ "version": "0.0.1",
4
+ "description": "A typed workflow engine for the @orkestrel line — a serializable Workflow → Phase → Task tree run by a composed runner on a cooperative scheduler. Part of the @orkestrel line.",
5
+ "keywords": [
6
+ "orchestration",
7
+ "runner",
8
+ "scheduler",
9
+ "task",
10
+ "typescript",
11
+ "workflow"
12
+ ],
13
+ "homepage": "https://github.com/orkestrel/workflow#readme",
14
+ "bugs": "https://github.com/orkestrel/workflow/issues",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/orkestrel/workflow.git"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "main": "./dist/src/core/index.cjs",
27
+ "module": "./dist/src/core/index.js",
28
+ "exports": {
29
+ ".": {
30
+ "import": {
31
+ "types": "./dist/src/core/index.d.ts",
32
+ "default": "./dist/src/core/index.js"
33
+ },
34
+ "require": {
35
+ "types": "./dist/src/core/index.d.cts",
36
+ "default": "./dist/src/core/index.cjs"
37
+ }
38
+ },
39
+ "./browser": {
40
+ "import": {
41
+ "types": "./dist/src/browser/index.d.ts",
42
+ "default": "./dist/src/browser/index.js"
43
+ }
44
+ },
45
+ "./server": {
46
+ "import": {
47
+ "types": "./dist/src/server/index.d.ts",
48
+ "default": "./dist/src/server/index.js"
49
+ },
50
+ "require": {
51
+ "types": "./dist/src/server/index.d.cts",
52
+ "default": "./dist/src/server/index.cjs"
53
+ }
54
+ },
55
+ "./package.json": "./package.json"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "scripts": {
61
+ "clean": "node -e \"try{require('node:fs').rmSync('dist',{recursive:true,force:true})}catch{}\"",
62
+ "copy": "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
63
+ "tmp:txt": "node -e \"const fs=require('node:fs'),p=require('node:path');function walk(d){for(const e of fs.readdirSync(d,{withFileTypes:true})){const f=p.join(d,e.name);if(e.isDirectory()){walk(f)}else if(!e.name.endsWith('.md')&&!e.name.endsWith('.txt')){const t=f+'.txt';if(!fs.existsSync(t)){fs.renameSync(f,t)}else{console.warn('Skipping '+f+' — target exists: '+t)}}}}try{walk('tmp')}catch(e){if(e.code!=='ENOENT')throw e}\"",
64
+ "lint": "oxlint --config .oxlintrc.json --fix .",
65
+ "check": "tsc --noEmit --project tsconfig.json",
66
+ "check:src": "npm run check:src:core && npm run check:src:browser && npm run check:src:server",
67
+ "check:src:core": "tsc --noEmit -p configs/src/tsconfig.core.json",
68
+ "check:src:browser": "tsc --noEmit -p configs/src/tsconfig.browser.json",
69
+ "check:src:server": "tsc --noEmit -p configs/src/tsconfig.server.json",
70
+ "format": "oxfmt --config .oxfmtrc.json --write .",
71
+ "format:check": "oxfmt --config .oxfmtrc.json --check .",
72
+ "lint:check": "oxlint --config .oxlintrc.json .",
73
+ "test": "npm run test:src && npm run test:guides",
74
+ "test:src": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core --project src:browser --project src:server",
75
+ "test:src:core": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
76
+ "test:src:browser": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:browser",
77
+ "test:src:server": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:server",
78
+ "test:guides": "vitest run --config vite.config.ts --reporter=dot --project guides",
79
+ "build": "npm run clean && npm run build:src",
80
+ "build:src": "npm run build:src:core && npm run build:src:browser && npm run build:src:server",
81
+ "build:src:core": "vite build --config configs/src/vite.core.config.ts && npm run copy dist/src/core/index.d.ts dist/src/core/index.d.cts",
82
+ "build:src:browser": "vite build --config configs/src/vite.browser.config.ts",
83
+ "build:src:server": "vite build --config configs/src/vite.server.config.ts && npm run copy dist/src/server/index.d.ts dist/src/server/index.d.cts",
84
+ "prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run check:src && npm run build && npm test"
85
+ },
86
+ "dependencies": {
87
+ "@orkestrel/abort": "^0.0.1",
88
+ "@orkestrel/agent": "^0.0.1",
89
+ "@orkestrel/budget": "^0.0.1",
90
+ "@orkestrel/contract": "^0.0.1",
91
+ "@orkestrel/database": "^0.0.2",
92
+ "@orkestrel/emitter": "^0.0.1",
93
+ "@orkestrel/queue": "^0.0.1",
94
+ "@orkestrel/timeout": "^0.0.1"
95
+ },
96
+ "devDependencies": {
97
+ "@microsoft/api-extractor": "^7.58.9",
98
+ "@orkestrel/guide": "^0.0.1",
99
+ "@types/node": "^26.1.1",
100
+ "@vitest/browser-playwright": "^4.1.10",
101
+ "oxfmt": "^0.58.0",
102
+ "oxlint": "^1.73.0",
103
+ "typescript": "^6.0.3",
104
+ "vite": "^8.1.4",
105
+ "vite-plugin-dts": "^5.0.3",
106
+ "vitest": "^4.1.10"
107
+ },
108
+ "engines": {
109
+ "node": ">=24"
110
+ }
111
+ }