@orkestrel/worker 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.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/src/core/index.cjs +169 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +193 -0
- package/dist/src/core/index.d.ts +193 -0
- package/dist/src/core/index.js +167 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/server/index.cjs +360 -0
- package/dist/src/server/index.cjs.map +1 -0
- package/dist/src/server/index.d.cts +299 -0
- package/dist/src/server/index.d.ts +299 -0
- package/dist/src/server/index.js +354 -0
- package/dist/src/server/index.js.map +1 -0
- package/package.json +99 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orkestrel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @orkestrel/worker
|
|
2
|
+
|
|
3
|
+
A typed, resource-backed **job worker** for the `@orkestrel` line: a `Worker`
|
|
4
|
+
is a `Queue` (`@orkestrel/queue`) whose handler runs against an automatically
|
|
5
|
+
acquired resource leased from a `Pool` (`@orkestrel/pool`) — released when the
|
|
6
|
+
job settles, even on throw. Composition, not reimplementation: all
|
|
7
|
+
concurrency, retries, per-attempt timeout, abort, and durability are the
|
|
8
|
+
Queue's; all idle reuse and `max` backpressure are the Pool's. The worker is
|
|
9
|
+
observable (a typed `emitter` re-exposes the underlying queue's job lifecycle
|
|
10
|
+
— `enqueue` / `start` / `retry` / `success` / `failure` / `abort` / `drain`).
|
|
11
|
+
For CPU-parallel work, the server surface's `createNodeWorker` specializes the
|
|
12
|
+
core `createWorker` over a pool of `node:worker_threads`, crossing the
|
|
13
|
+
structured-clone boundary with zero `as` via `input` / `result` guards. Part
|
|
14
|
+
of the `@orkestrel` line.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install @orkestrel/worker
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- Node.js >= 24
|
|
25
|
+
- ESM and CommonJS builds ship for both the core and server entry points
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createWorker } from '@orkestrel/worker'
|
|
31
|
+
|
|
32
|
+
const worker = createWorker<Query, Connection, Rows>({
|
|
33
|
+
pool: { create: () => connect(), destroy: (connection) => connection.close() },
|
|
34
|
+
handler: (query, connection, { signal }) => connection.run(query, signal),
|
|
35
|
+
concurrency: 4, // up to four jobs in flight; the pool defaults its `max` to match
|
|
36
|
+
retries: 1,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const rows = await worker.enqueue(query)
|
|
40
|
+
worker.destroy() // tears down the queue, then the pool
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
CPU-parallel jobs over `node:worker_threads`:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createNodeWorker } from '@orkestrel/worker/server'
|
|
47
|
+
|
|
48
|
+
const isNumber = (value: unknown): value is number => typeof value === 'number'
|
|
49
|
+
|
|
50
|
+
const worker = createNodeWorker({
|
|
51
|
+
script: new URL('./double.js', import.meta.url),
|
|
52
|
+
input: isNumber,
|
|
53
|
+
result: isNumber,
|
|
54
|
+
concurrency: 4,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const doubled = await worker.enqueue(21) // 42, computed on a worker thread
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Guide
|
|
61
|
+
|
|
62
|
+
For the full surface — the `Worker` facade, `createNodeWorker` / `serveWorker`,
|
|
63
|
+
the durable `createJSONQueueStore`, the observable `emitter`, and usage
|
|
64
|
+
patterns — see [`guides/src/worker.md`](guides/src/worker.md).
|
|
65
|
+
|
|
66
|
+
## Package
|
|
67
|
+
|
|
68
|
+
Published with two entry points per the `exports` field in `package.json`:
|
|
69
|
+
the environment-agnostic core (`.`) and the Node-only server surface
|
|
70
|
+
(`./server`).
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT © [Orkestrel](https://github.com/orkestrel) — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
3
|
+
let _orkestrel_pool = require("@orkestrel/pool");
|
|
4
|
+
let _orkestrel_queue = require("@orkestrel/queue");
|
|
5
|
+
//#region src/core/Worker.ts
|
|
6
|
+
/**
|
|
7
|
+
* A resource-backed job worker — a thin facade composing a `Queue` (`@orkestrel/queue`)
|
|
8
|
+
* with a `Pool` (`@orkestrel/pool`).
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* - **Composition, not reimplementation.** The Worker owns a `Pool` (built from
|
|
12
|
+
* `options.pool`) and a `Queue` whose handler ACQUIRES a pooled resource, runs the
|
|
13
|
+
* user handler against it, and RELEASES it in a `finally`. All concurrency, retries,
|
|
14
|
+
* timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.
|
|
15
|
+
* - **Resource ↔ concurrency.** The pool's `max` defaults to the worker's
|
|
16
|
+
* `concurrency` (default `1`), so at most one resource exists per in-flight job and
|
|
17
|
+
* idle resources are reused across jobs.
|
|
18
|
+
* - **Acquire over the attempt signal.** Each job acquires using the attempt's
|
|
19
|
+
* `execution.signal`, so an `abort` / `timeout` while waiting for a resource rejects
|
|
20
|
+
* the acquire — the Queue then handles retry / rejection, and there is no token to
|
|
21
|
+
* release (the resource was never leased).
|
|
22
|
+
* - **Lifecycle (§10).** `enqueue` / `restore` / `start` / `stop` / `pause` / `resume` /
|
|
23
|
+
* `abort` / `clear` delegate to the queue; `count` / `active` / `paused` / `stopped`
|
|
24
|
+
* read it. `destroy` destroys the queue then tears the pool down, idempotently.
|
|
25
|
+
* - **Durability.** An optional `store` is passed straight through to the queue, so the
|
|
26
|
+
* worker's outstanding jobs persist; `restore` re-runs them (delegated to the queue).
|
|
27
|
+
* - **Observable (§13).** The owned {@link emitter} ({@link WorkerEventMap}) RE-EXPOSES the
|
|
28
|
+
* underlying queue's job lifecycle (`enqueue` / `start` / `retry` / `success` / `failure` /
|
|
29
|
+
* `abort` / `drain`) as the worker's OWN events — bridged from the inner queue's emitter at
|
|
30
|
+
* construction — so a consumer observes the worker without reaching through to internals.
|
|
31
|
+
* The bridge re-emits directly on the worker's own emitter; the worker emitter isolates a
|
|
32
|
+
* listener throw and routes it to its `error` handler (the `error` option), so a buggy
|
|
33
|
+
* worker observer can never corrupt the inner queue or pool — the bridge listener never
|
|
34
|
+
* throws, so the inner queue's own emit stays balanced. The pool's create / acquire /
|
|
35
|
+
* release events stay the pool's internal concern (a Worker manages its own resources);
|
|
36
|
+
* observe a `Pool` directly for those.
|
|
37
|
+
*/
|
|
38
|
+
var Worker = class {
|
|
39
|
+
#queue;
|
|
40
|
+
#pool;
|
|
41
|
+
#emitter;
|
|
42
|
+
#destroyed = false;
|
|
43
|
+
constructor(options) {
|
|
44
|
+
const concurrency = Math.max(1, options.concurrency ?? 1);
|
|
45
|
+
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
46
|
+
on: options?.on,
|
|
47
|
+
error: options?.error
|
|
48
|
+
});
|
|
49
|
+
this.#pool = new _orkestrel_pool.Pool({
|
|
50
|
+
...options.pool,
|
|
51
|
+
max: options.pool.max ?? concurrency
|
|
52
|
+
});
|
|
53
|
+
this.#queue = new _orkestrel_queue.Queue({
|
|
54
|
+
handler: async (input, execution) => {
|
|
55
|
+
const token = await this.#pool.acquire(execution.signal);
|
|
56
|
+
try {
|
|
57
|
+
return await options.handler(input, token.value, execution);
|
|
58
|
+
} finally {
|
|
59
|
+
token.release();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
concurrency,
|
|
63
|
+
retries: options.retries,
|
|
64
|
+
timeout: options.timeout,
|
|
65
|
+
store: options.store
|
|
66
|
+
});
|
|
67
|
+
this.#bridge();
|
|
68
|
+
}
|
|
69
|
+
get emitter() {
|
|
70
|
+
return this.#emitter;
|
|
71
|
+
}
|
|
72
|
+
get count() {
|
|
73
|
+
return this.#queue.count;
|
|
74
|
+
}
|
|
75
|
+
get active() {
|
|
76
|
+
return this.#queue.active;
|
|
77
|
+
}
|
|
78
|
+
get paused() {
|
|
79
|
+
return this.#queue.paused;
|
|
80
|
+
}
|
|
81
|
+
get stopped() {
|
|
82
|
+
return this.#queue.stopped;
|
|
83
|
+
}
|
|
84
|
+
enqueue(input, options) {
|
|
85
|
+
return this.#queue.enqueue(input, options);
|
|
86
|
+
}
|
|
87
|
+
restore() {
|
|
88
|
+
return this.#queue.restore();
|
|
89
|
+
}
|
|
90
|
+
start() {
|
|
91
|
+
this.#queue.start();
|
|
92
|
+
}
|
|
93
|
+
stop() {
|
|
94
|
+
this.#queue.stop();
|
|
95
|
+
}
|
|
96
|
+
pause() {
|
|
97
|
+
this.#queue.pause();
|
|
98
|
+
}
|
|
99
|
+
resume() {
|
|
100
|
+
this.#queue.resume();
|
|
101
|
+
}
|
|
102
|
+
abort(reason) {
|
|
103
|
+
this.#queue.abort(reason);
|
|
104
|
+
}
|
|
105
|
+
clear() {
|
|
106
|
+
this.#queue.clear();
|
|
107
|
+
}
|
|
108
|
+
destroy() {
|
|
109
|
+
if (this.#destroyed) return;
|
|
110
|
+
this.#destroyed = true;
|
|
111
|
+
this.#queue.destroy();
|
|
112
|
+
this.#pool.destroy();
|
|
113
|
+
}
|
|
114
|
+
#bridge() {
|
|
115
|
+
const queue = this.#queue.emitter;
|
|
116
|
+
queue.on("enqueue", (id) => this.#emitter.emit("enqueue", id));
|
|
117
|
+
queue.on("start", (id) => this.#emitter.emit("start", id));
|
|
118
|
+
queue.on("retry", (id, attempt) => this.#emitter.emit("retry", id, attempt));
|
|
119
|
+
queue.on("success", (id, result) => this.#emitter.emit("success", id, result));
|
|
120
|
+
queue.on("failure", (id, error) => this.#emitter.emit("failure", id, error));
|
|
121
|
+
queue.on("abort", (reason) => this.#emitter.emit("abort", reason));
|
|
122
|
+
queue.on("drain", () => this.#emitter.emit("drain"));
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/core/factories.ts
|
|
127
|
+
/**
|
|
128
|
+
* Create a resource-backed job worker — a `Queue` (`@orkestrel/queue`) marrying a `Pool`
|
|
129
|
+
* (`@orkestrel/pool`). Each enqueued input runs through the handler against an
|
|
130
|
+
* automatically acquired pooled resource (released when the job settles), with the
|
|
131
|
+
* queue's bounded concurrency, retries, and per-attempt timeout / abort.
|
|
132
|
+
*
|
|
133
|
+
* @remarks
|
|
134
|
+
* The pool's `max` defaults to `concurrency`, so resources match the jobs in flight and
|
|
135
|
+
* are reused across jobs. A handler that throws still releases its resource (the
|
|
136
|
+
* acquire/release pair brackets the call in a `finally`), so a later job reuses it. The
|
|
137
|
+
* lifecycle (`start` / `stop` / `pause` / `resume` / `abort` / `clear` / `destroy`)
|
|
138
|
+
* delegates to the queue; `destroy` also tears the pool down. Observable (§13): a typed
|
|
139
|
+
* `emitter` surfaces the queue lifecycle (`enqueue` / `start` / `success` / `failure` / …).
|
|
140
|
+
*
|
|
141
|
+
* @typeParam TInput - The work input each job carries
|
|
142
|
+
* @typeParam TResource - The pooled resource each job runs against
|
|
143
|
+
* @typeParam TResult - The value the handler resolves for a job
|
|
144
|
+
* @param options - The `handler` and `pool` plus optional `concurrency` (default `1`),
|
|
145
|
+
* `retries` (default `0`), and a default per-attempt `timeout` in milliseconds
|
|
146
|
+
* @returns A working {@link WorkerInterface}
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* import { createWorker } from '@src/core'
|
|
151
|
+
*
|
|
152
|
+
* const worker = createWorker<Query, Connection, Rows>({
|
|
153
|
+
* pool: { create: () => connect(), destroy: (connection) => connection.close() },
|
|
154
|
+
* handler: (query, connection, { signal }) => connection.run(query, signal),
|
|
155
|
+
* concurrency: 4,
|
|
156
|
+
* retries: 1,
|
|
157
|
+
* })
|
|
158
|
+
*
|
|
159
|
+
* const rows = await worker.enqueue(query)
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
function createWorker(options) {
|
|
163
|
+
return new Worker(options);
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
exports.Worker = Worker;
|
|
167
|
+
exports.createWorker = createWorker;
|
|
168
|
+
|
|
169
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["#queue","#pool","#emitter","#bridge","#destroyed"],"sources":["../../../src/core/Worker.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { QueueEntryOptions } from '@orkestrel/queue'\nimport type { WorkerEventMap, WorkerInterface, WorkerOptions } from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { Pool } from '@orkestrel/pool'\nimport { Queue } from '@orkestrel/queue'\n\n/**\n * A resource-backed job worker — a thin facade composing a `Queue` (`@orkestrel/queue`)\n * with a `Pool` (`@orkestrel/pool`).\n *\n * @remarks\n * - **Composition, not reimplementation.** The Worker owns a `Pool` (built from\n * `options.pool`) and a `Queue` whose handler ACQUIRES a pooled resource, runs the\n * user handler against it, and RELEASES it in a `finally`. All concurrency, retries,\n * timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.\n * - **Resource ↔ concurrency.** The pool's `max` defaults to the worker's\n * `concurrency` (default `1`), so at most one resource exists per in-flight job and\n * idle resources are reused across jobs.\n * - **Acquire over the attempt signal.** Each job acquires using the attempt's\n * `execution.signal`, so an `abort` / `timeout` while waiting for a resource rejects\n * the acquire — the Queue then handles retry / rejection, and there is no token to\n * release (the resource was never leased).\n * - **Lifecycle (§10).** `enqueue` / `restore` / `start` / `stop` / `pause` / `resume` /\n * `abort` / `clear` delegate to the queue; `count` / `active` / `paused` / `stopped`\n * read it. `destroy` destroys the queue then tears the pool down, idempotently.\n * - **Durability.** An optional `store` is passed straight through to the queue, so the\n * worker's outstanding jobs persist; `restore` re-runs them (delegated to the queue).\n * - **Observable (§13).** The owned {@link emitter} ({@link WorkerEventMap}) RE-EXPOSES the\n * underlying queue's job lifecycle (`enqueue` / `start` / `retry` / `success` / `failure` /\n * `abort` / `drain`) as the worker's OWN events — bridged from the inner queue's emitter at\n * construction — so a consumer observes the worker without reaching through to internals.\n * The bridge re-emits directly on the worker's own emitter; the worker emitter isolates a\n * listener throw and routes it to its `error` handler (the `error` option), so a buggy\n * worker observer can never corrupt the inner queue or pool — the bridge listener never\n * throws, so the inner queue's own emit stays balanced. The pool's create / acquire /\n * release events stay the pool's internal concern (a Worker manages its own resources);\n * observe a `Pool` directly for those.\n */\nexport class Worker<TInput, TResource, TResult> implements WorkerInterface<TInput, TResult> {\n\treadonly #queue: Queue<TInput, TResult>\n\treadonly #pool: Pool<TResource>\n\t// The PUSH observation surface (§13) — the worker's OWN emitter, fed by the queue→worker\n\t// bridge. The emitter isolates a worker observer's throw (routing it to the `error`\n\t// handler), so it never escapes into queue or pool.\n\treadonly #emitter: Emitter<WorkerEventMap<TResult>>\n\t#destroyed = false\n\n\tconstructor(options: WorkerOptions<TInput, TResource, TResult>) {\n\t\tconst concurrency = Math.max(1, options.concurrency ?? 1)\n\t\tthis.#emitter = new Emitter<WorkerEventMap<TResult>>({ on: options?.on, error: options?.error })\n\t\tthis.#pool = new Pool<TResource>({\n\t\t\t...options.pool,\n\t\t\tmax: options.pool.max ?? concurrency,\n\t\t})\n\t\tthis.#queue = new Queue<TInput, TResult>({\n\t\t\thandler: async (input, execution) => {\n\t\t\t\tconst token = await this.#pool.acquire(execution.signal)\n\t\t\t\ttry {\n\t\t\t\t\treturn await options.handler(input, token.value, execution)\n\t\t\t\t} finally {\n\t\t\t\t\ttoken.release()\n\t\t\t\t}\n\t\t\t},\n\t\t\tconcurrency,\n\t\t\tretries: options.retries,\n\t\t\ttimeout: options.timeout,\n\t\t\tstore: options.store,\n\t\t})\n\t\tthis.#bridge()\n\t}\n\n\tget emitter(): EmitterInterface<WorkerEventMap<TResult>> {\n\t\treturn this.#emitter\n\t}\n\n\tget count(): number {\n\t\treturn this.#queue.count\n\t}\n\n\tget active(): number {\n\t\treturn this.#queue.active\n\t}\n\n\tget paused(): boolean {\n\t\treturn this.#queue.paused\n\t}\n\n\tget stopped(): boolean {\n\t\treturn this.#queue.stopped\n\t}\n\n\tenqueue(input: TInput, options?: QueueEntryOptions): Promise<TResult> {\n\t\treturn this.#queue.enqueue(input, options)\n\t}\n\n\trestore(): Promise<void> {\n\t\treturn this.#queue.restore()\n\t}\n\n\tstart(): void {\n\t\tthis.#queue.start()\n\t}\n\n\tstop(): void {\n\t\tthis.#queue.stop()\n\t}\n\n\tpause(): void {\n\t\tthis.#queue.pause()\n\t}\n\n\tresume(): void {\n\t\tthis.#queue.resume()\n\t}\n\n\tabort(reason?: unknown): void {\n\t\tthis.#queue.abort(reason)\n\t}\n\n\tclear(): void {\n\t\tthis.#queue.clear()\n\t}\n\n\tdestroy(): void {\n\t\tif (this.#destroyed) return\n\t\tthis.#destroyed = true\n\t\tthis.#queue.destroy()\n\t\tvoid this.#pool.destroy()\n\t}\n\n\t// Bridge the inner queue's lifecycle onto the worker's OWN emitter, once at construction.\n\t// Each listener re-emits the queue event directly on the worker's emitter, which isolates a\n\t// worker observer's throw (routing it to the worker's `error` handler). Because the bridge\n\t// listener itself never throws, the queue's own `#emitter.emit` — which invoked this\n\t// listener — sees no throw, so the inner queue's engine stays balanced regardless of what a\n\t// worker observer does. The events are already post-transition (they fire from the queue's\n\t// own post-settle / post-wake emits), so this stays observation.\n\t#bridge(): void {\n\t\tconst queue = this.#queue.emitter\n\t\tqueue.on('enqueue', (id) => this.#emitter.emit('enqueue', id))\n\t\tqueue.on('start', (id) => this.#emitter.emit('start', id))\n\t\tqueue.on('retry', (id, attempt) => this.#emitter.emit('retry', id, attempt))\n\t\tqueue.on('success', (id, result) => this.#emitter.emit('success', id, result))\n\t\tqueue.on('failure', (id, error) => this.#emitter.emit('failure', id, error))\n\t\tqueue.on('abort', (reason) => this.#emitter.emit('abort', reason))\n\t\tqueue.on('drain', () => this.#emitter.emit('drain'))\n\t}\n}\n","import type { WorkerInterface, WorkerOptions } from './types.js'\nimport { Worker } from './Worker.js'\n\n/**\n * Create a resource-backed job worker — a `Queue` (`@orkestrel/queue`) marrying a `Pool`\n * (`@orkestrel/pool`). Each enqueued input runs through the handler against an\n * automatically acquired pooled resource (released when the job settles), with the\n * queue's bounded concurrency, retries, and per-attempt timeout / abort.\n *\n * @remarks\n * The pool's `max` defaults to `concurrency`, so resources match the jobs in flight and\n * are reused across jobs. A handler that throws still releases its resource (the\n * acquire/release pair brackets the call in a `finally`), so a later job reuses it. The\n * lifecycle (`start` / `stop` / `pause` / `resume` / `abort` / `clear` / `destroy`)\n * delegates to the queue; `destroy` also tears the pool down. Observable (§13): a typed\n * `emitter` surfaces the queue lifecycle (`enqueue` / `start` / `success` / `failure` / …).\n *\n * @typeParam TInput - The work input each job carries\n * @typeParam TResource - The pooled resource each job runs against\n * @typeParam TResult - The value the handler resolves for a job\n * @param options - The `handler` and `pool` plus optional `concurrency` (default `1`),\n * `retries` (default `0`), and a default per-attempt `timeout` in milliseconds\n * @returns A working {@link WorkerInterface}\n *\n * @example\n * ```ts\n * import { createWorker } from '@src/core'\n *\n * const worker = createWorker<Query, Connection, Rows>({\n * \tpool: { create: () => connect(), destroy: (connection) => connection.close() },\n * \thandler: (query, connection, { signal }) => connection.run(query, signal),\n * \tconcurrency: 4,\n * \tretries: 1,\n * })\n *\n * const rows = await worker.enqueue(query)\n * ```\n */\nexport function createWorker<TInput, TResource, TResult>(\n\toptions: WorkerOptions<TInput, TResource, TResult>,\n): WorkerInterface<TInput, TResult> {\n\treturn new Worker(options)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,SAAb,MAA4F;CAC3F;CACA;CAIA;CACA,aAAa;CAEb,YAAY,SAAoD;EAC/D,MAAM,cAAc,KAAK,IAAI,GAAG,QAAQ,eAAe,CAAC;EACxD,KAAKE,WAAW,IAAI,mBAAA,QAAiC;GAAE,IAAI,SAAS;GAAI,OAAO,SAAS;EAAM,CAAC;EAC/F,KAAKD,QAAQ,IAAI,gBAAA,KAAgB;GAChC,GAAG,QAAQ;GACX,KAAK,QAAQ,KAAK,OAAO;EAC1B,CAAC;EACD,KAAKD,SAAS,IAAI,iBAAA,MAAuB;GACxC,SAAS,OAAO,OAAO,cAAc;IACpC,MAAM,QAAQ,MAAM,KAAKC,MAAM,QAAQ,UAAU,MAAM;IACvD,IAAI;KACH,OAAO,MAAM,QAAQ,QAAQ,OAAO,MAAM,OAAO,SAAS;IAC3D,UAAU;KACT,MAAM,QAAQ;IACf;GACD;GACA;GACA,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,OAAO,QAAQ;EAChB,CAAC;EACD,KAAKE,QAAQ;CACd;CAEA,IAAI,UAAqD;EACxD,OAAO,KAAKD;CACb;CAEA,IAAI,QAAgB;EACnB,OAAO,KAAKF,OAAO;CACpB;CAEA,IAAI,SAAiB;EACpB,OAAO,KAAKA,OAAO;CACpB;CAEA,IAAI,SAAkB;EACrB,OAAO,KAAKA,OAAO;CACpB;CAEA,IAAI,UAAmB;EACtB,OAAO,KAAKA,OAAO;CACpB;CAEA,QAAQ,OAAe,SAA+C;EACrE,OAAO,KAAKA,OAAO,QAAQ,OAAO,OAAO;CAC1C;CAEA,UAAyB;EACxB,OAAO,KAAKA,OAAO,QAAQ;CAC5B;CAEA,QAAc;EACb,KAAKA,OAAO,MAAM;CACnB;CAEA,OAAa;EACZ,KAAKA,OAAO,KAAK;CAClB;CAEA,QAAc;EACb,KAAKA,OAAO,MAAM;CACnB;CAEA,SAAe;EACd,KAAKA,OAAO,OAAO;CACpB;CAEA,MAAM,QAAwB;EAC7B,KAAKA,OAAO,MAAM,MAAM;CACzB;CAEA,QAAc;EACb,KAAKA,OAAO,MAAM;CACnB;CAEA,UAAgB;EACf,IAAI,KAAKI,YAAY;EACrB,KAAKA,aAAa;EAClB,KAAKJ,OAAO,QAAQ;EACpB,KAAUC,MAAM,QAAQ;CACzB;CASA,UAAgB;EACf,MAAM,QAAQ,KAAKD,OAAO;EAC1B,MAAM,GAAG,YAAY,OAAO,KAAKE,SAAS,KAAK,WAAW,EAAE,CAAC;EAC7D,MAAM,GAAG,UAAU,OAAO,KAAKA,SAAS,KAAK,SAAS,EAAE,CAAC;EACzD,MAAM,GAAG,UAAU,IAAI,YAAY,KAAKA,SAAS,KAAK,SAAS,IAAI,OAAO,CAAC;EAC3E,MAAM,GAAG,YAAY,IAAI,WAAW,KAAKA,SAAS,KAAK,WAAW,IAAI,MAAM,CAAC;EAC7E,MAAM,GAAG,YAAY,IAAI,UAAU,KAAKA,SAAS,KAAK,WAAW,IAAI,KAAK,CAAC;EAC3E,MAAM,GAAG,UAAU,WAAW,KAAKA,SAAS,KAAK,SAAS,MAAM,CAAC;EACjE,MAAM,GAAG,eAAe,KAAKA,SAAS,KAAK,OAAO,CAAC;CACpD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC9GA,SAAgB,aACf,SACmC;CACnC,OAAO,IAAI,OAAO,OAAO;AAC1B"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
2
|
+
import { EmitterHooks } from '@orkestrel/emitter';
|
|
3
|
+
import { EmitterInterface } from '@orkestrel/emitter';
|
|
4
|
+
import { PoolOptions } from '@orkestrel/pool';
|
|
5
|
+
import { QueueEntryOptions } from '@orkestrel/queue';
|
|
6
|
+
import { QueueExecution } from '@orkestrel/queue';
|
|
7
|
+
import { QueueStoreInterface } from '@orkestrel/queue';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a resource-backed job worker — a `Queue` (`@orkestrel/queue`) marrying a `Pool`
|
|
11
|
+
* (`@orkestrel/pool`). Each enqueued input runs through the handler against an
|
|
12
|
+
* automatically acquired pooled resource (released when the job settles), with the
|
|
13
|
+
* queue's bounded concurrency, retries, and per-attempt timeout / abort.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* The pool's `max` defaults to `concurrency`, so resources match the jobs in flight and
|
|
17
|
+
* are reused across jobs. A handler that throws still releases its resource (the
|
|
18
|
+
* acquire/release pair brackets the call in a `finally`), so a later job reuses it. The
|
|
19
|
+
* lifecycle (`start` / `stop` / `pause` / `resume` / `abort` / `clear` / `destroy`)
|
|
20
|
+
* delegates to the queue; `destroy` also tears the pool down. Observable (§13): a typed
|
|
21
|
+
* `emitter` surfaces the queue lifecycle (`enqueue` / `start` / `success` / `failure` / …).
|
|
22
|
+
*
|
|
23
|
+
* @typeParam TInput - The work input each job carries
|
|
24
|
+
* @typeParam TResource - The pooled resource each job runs against
|
|
25
|
+
* @typeParam TResult - The value the handler resolves for a job
|
|
26
|
+
* @param options - The `handler` and `pool` plus optional `concurrency` (default `1`),
|
|
27
|
+
* `retries` (default `0`), and a default per-attempt `timeout` in milliseconds
|
|
28
|
+
* @returns A working {@link WorkerInterface}
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { createWorker } from '@src/core'
|
|
33
|
+
*
|
|
34
|
+
* const worker = createWorker<Query, Connection, Rows>({
|
|
35
|
+
* pool: { create: () => connect(), destroy: (connection) => connection.close() },
|
|
36
|
+
* handler: (query, connection, { signal }) => connection.run(query, signal),
|
|
37
|
+
* concurrency: 4,
|
|
38
|
+
* retries: 1,
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* const rows = await worker.enqueue(query)
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function createWorker<TInput, TResource, TResult>(options: WorkerOptions<TInput, TResource, TResult>): WorkerInterface<TInput, TResult>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A resource-backed job worker — a thin facade composing a `Queue` (`@orkestrel/queue`)
|
|
48
|
+
* with a `Pool` (`@orkestrel/pool`).
|
|
49
|
+
*
|
|
50
|
+
* @remarks
|
|
51
|
+
* - **Composition, not reimplementation.** The Worker owns a `Pool` (built from
|
|
52
|
+
* `options.pool`) and a `Queue` whose handler ACQUIRES a pooled resource, runs the
|
|
53
|
+
* user handler against it, and RELEASES it in a `finally`. All concurrency, retries,
|
|
54
|
+
* timeout, and lifecycle are the Queue's — the Worker adds only the resource pairing.
|
|
55
|
+
* - **Resource ↔ concurrency.** The pool's `max` defaults to the worker's
|
|
56
|
+
* `concurrency` (default `1`), so at most one resource exists per in-flight job and
|
|
57
|
+
* idle resources are reused across jobs.
|
|
58
|
+
* - **Acquire over the attempt signal.** Each job acquires using the attempt's
|
|
59
|
+
* `execution.signal`, so an `abort` / `timeout` while waiting for a resource rejects
|
|
60
|
+
* the acquire — the Queue then handles retry / rejection, and there is no token to
|
|
61
|
+
* release (the resource was never leased).
|
|
62
|
+
* - **Lifecycle (§10).** `enqueue` / `restore` / `start` / `stop` / `pause` / `resume` /
|
|
63
|
+
* `abort` / `clear` delegate to the queue; `count` / `active` / `paused` / `stopped`
|
|
64
|
+
* read it. `destroy` destroys the queue then tears the pool down, idempotently.
|
|
65
|
+
* - **Durability.** An optional `store` is passed straight through to the queue, so the
|
|
66
|
+
* worker's outstanding jobs persist; `restore` re-runs them (delegated to the queue).
|
|
67
|
+
* - **Observable (§13).** The owned {@link emitter} ({@link WorkerEventMap}) RE-EXPOSES the
|
|
68
|
+
* underlying queue's job lifecycle (`enqueue` / `start` / `retry` / `success` / `failure` /
|
|
69
|
+
* `abort` / `drain`) as the worker's OWN events — bridged from the inner queue's emitter at
|
|
70
|
+
* construction — so a consumer observes the worker without reaching through to internals.
|
|
71
|
+
* The bridge re-emits directly on the worker's own emitter; the worker emitter isolates a
|
|
72
|
+
* listener throw and routes it to its `error` handler (the `error` option), so a buggy
|
|
73
|
+
* worker observer can never corrupt the inner queue or pool — the bridge listener never
|
|
74
|
+
* throws, so the inner queue's own emit stays balanced. The pool's create / acquire /
|
|
75
|
+
* release events stay the pool's internal concern (a Worker manages its own resources);
|
|
76
|
+
* observe a `Pool` directly for those.
|
|
77
|
+
*/
|
|
78
|
+
declare class Worker_2<TInput, TResource, TResult> implements WorkerInterface<TInput, TResult> {
|
|
79
|
+
#private;
|
|
80
|
+
constructor(options: WorkerOptions<TInput, TResource, TResult>);
|
|
81
|
+
get emitter(): EmitterInterface<WorkerEventMap<TResult>>;
|
|
82
|
+
get count(): number;
|
|
83
|
+
get active(): number;
|
|
84
|
+
get paused(): boolean;
|
|
85
|
+
get stopped(): boolean;
|
|
86
|
+
enqueue(input: TInput, options?: QueueEntryOptions): Promise<TResult>;
|
|
87
|
+
restore(): Promise<void>;
|
|
88
|
+
start(): void;
|
|
89
|
+
stop(): void;
|
|
90
|
+
pause(): void;
|
|
91
|
+
resume(): void;
|
|
92
|
+
abort(reason?: unknown): void;
|
|
93
|
+
clear(): void;
|
|
94
|
+
destroy(): void;
|
|
95
|
+
}
|
|
96
|
+
export { Worker_2 as Worker }
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The push observation surface of a {@link WorkerInterface} (AGENTS §13) — the job
|
|
100
|
+
* lifecycle a fire-and-forget observer subscribes to, surfacing the underlying queue's
|
|
101
|
+
* moments so a Worker consumer never reaches through to the internal `Queue`.
|
|
102
|
+
*
|
|
103
|
+
* @typeParam TResult - The value a job resolves (the `success` payload), mirroring the
|
|
104
|
+
* {@link WorkerInterface}'s own `TResult`.
|
|
105
|
+
*
|
|
106
|
+
* @remarks
|
|
107
|
+
* A Worker is a `Queue`⨉`Pool` facade (both from their own `@orkestrel` packages); this
|
|
108
|
+
* map RE-EXPOSES the queue lifecycle the worker surfaces (`enqueue` / `start` / `retry` /
|
|
109
|
+
* `success` / `failure` / `abort` / `drain`) as the worker's OWN events — wired from the
|
|
110
|
+
* underlying queue's emitter at construction, so a buggy observer is isolated exactly as
|
|
111
|
+
* on the queue (a throw routes to the worker emitter's `error` handler, AGENTS §13). The
|
|
112
|
+
* pool's create / acquire / release events stay the pool's internal concern (a Worker
|
|
113
|
+
* manages its own resources); a consumer who wants them observes a `Pool` directly.
|
|
114
|
+
* Declared as a `type` alias (§4.5).
|
|
115
|
+
*/
|
|
116
|
+
export declare type WorkerEventMap<TResult> = {
|
|
117
|
+
/** A job was accepted — its id (delegated from the underlying queue's `enqueue`). */
|
|
118
|
+
readonly enqueue: readonly [id: string];
|
|
119
|
+
/** A job's attempt began running — its id. */
|
|
120
|
+
readonly start: readonly [id: string];
|
|
121
|
+
/** A failed job attempt is being retried — its id + the next (1-based) attempt index. */
|
|
122
|
+
readonly retry: readonly [id: string, attempt: number];
|
|
123
|
+
/** A job settled successfully — its id + the resolved result. */
|
|
124
|
+
readonly success: readonly [id: string, result: TResult];
|
|
125
|
+
/** A job settled with a terminal failure — its id + the error. */
|
|
126
|
+
readonly failure: readonly [id: string, error: unknown];
|
|
127
|
+
/** The worker was aborted — the cancel reason. */
|
|
128
|
+
readonly abort: readonly [reason: unknown];
|
|
129
|
+
/** The worker went idle — no pending jobs and none in flight. */
|
|
130
|
+
readonly drain: readonly [];
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** Runs one worker job with a leased pool resource. */
|
|
134
|
+
export declare type WorkerHandler<TInput, TResource, TResult> = (input: TInput, resource: TResource, execution: QueueExecution) => Promise<TResult> | TResult;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A resource-backed job worker — a Queue whose handler runs against a pooled resource.
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* Exposes a typed {@link emitter} (AGENTS §13) carrying the job lifecycle
|
|
141
|
+
* ({@link WorkerEventMap}) — the underlying queue's moments re-exposed as the worker's own,
|
|
142
|
+
* so a consumer never reaches through to internals. Emitting is observation-only: a buggy
|
|
143
|
+
* observer is isolated exactly as on the queue (a throw routes to the emitter's `error`
|
|
144
|
+
* handler, the `error` option).
|
|
145
|
+
*/
|
|
146
|
+
export declare interface WorkerInterface<TInput, TResult> {
|
|
147
|
+
readonly emitter: EmitterInterface<WorkerEventMap<TResult>>;
|
|
148
|
+
readonly count: number;
|
|
149
|
+
readonly active: number;
|
|
150
|
+
readonly paused: boolean;
|
|
151
|
+
readonly stopped: boolean;
|
|
152
|
+
enqueue(input: TInput, options?: QueueEntryOptions): Promise<TResult>;
|
|
153
|
+
/** Re-enqueue outstanding entries loaded from the store; no-op without a store. */
|
|
154
|
+
restore(): Promise<void>;
|
|
155
|
+
start(): void;
|
|
156
|
+
stop(): void;
|
|
157
|
+
pause(): void;
|
|
158
|
+
resume(): void;
|
|
159
|
+
abort(reason?: unknown): void;
|
|
160
|
+
clear(): void;
|
|
161
|
+
destroy(): void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Options for `createWorker`.
|
|
166
|
+
*
|
|
167
|
+
* @remarks
|
|
168
|
+
* - `handler` — runs each job against an acquired pool resource; rejecting triggers a
|
|
169
|
+
* retry while attempts remain (delegated to the underlying queue).
|
|
170
|
+
* - `pool` — the {@link PoolOptions} for the resource the handler runs against; its
|
|
171
|
+
* `max` defaults to `concurrency` so resources match the jobs in flight.
|
|
172
|
+
* - `concurrency` — the maximum jobs in flight at once; defaults to `1`. Floored at `1`.
|
|
173
|
+
* - `retries` — the default extra attempts per job on failure; defaults to `0`.
|
|
174
|
+
* - `timeout` — the default per-attempt deadline in milliseconds; defaults to none.
|
|
175
|
+
* - `store` — durable backing; outstanding entries survive a restart; call
|
|
176
|
+
* `restore()` to re-run them.
|
|
177
|
+
* - `on` — the reserved {@link EmitterHooks} key (§8): initial listeners for the worker's
|
|
178
|
+
* {@link WorkerEventMap} (the job lifecycle it surfaces from its underlying queue), wired
|
|
179
|
+
* at construction.
|
|
180
|
+
*/
|
|
181
|
+
export declare interface WorkerOptions<TInput, TResource, TResult> {
|
|
182
|
+
readonly on?: EmitterHooks<WorkerEventMap<TResult>>;
|
|
183
|
+
/** The emitter's listener-error handler (AGENTS §13) — a listener throw routes here, not to a domain event. */
|
|
184
|
+
readonly error?: EmitterErrorHandler;
|
|
185
|
+
readonly handler: WorkerHandler<TInput, TResource, TResult>;
|
|
186
|
+
readonly pool: PoolOptions<TResource>;
|
|
187
|
+
readonly concurrency?: number;
|
|
188
|
+
readonly retries?: number;
|
|
189
|
+
readonly timeout?: number;
|
|
190
|
+
readonly store?: QueueStoreInterface<TInput>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { }
|