@travetto/worker 3.4.2 → 4.0.0-rc.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/README.md CHANGED
@@ -16,162 +16,7 @@ yarn add @travetto/worker
16
16
  This module provides the necessary primitives for handling dependent workers. A worker can be an individual actor or could be a pool of workers. Node provides ipc (inter-process communication) functionality out of the box. This module builds upon that by providing enhanced event management, richer process management, as well as constructs for orchestrating a conversation between two processes.
17
17
 
18
18
  ## Execution Pools
19
- With respect to managing multiple executions, [WorkPool](https://github.com/travetto/travetto/tree/main/module/worker/src/pool.ts#L31) is provided to allow for concurrent operation, and processing of jobs concurrently. To manage the flow of jobs, there are various [WorkSet](https://github.com/travetto/travetto/tree/main/module/worker/src/input/types.ts#L4) implementation that allow for a wide range of use cases.
20
-
21
- The only provided [WorkSet](https://github.com/travetto/travetto/tree/main/module/worker/src/input/types.ts#L4) is the [IterableWorkSet](https://github.com/travetto/travetto/tree/main/module/worker/src/input/iterable.ts#L11) which supports all `Iterable` and `Iterator` sources. Additionally, the module provides [ManualAsyncIterator](https://github.com/travetto/travetto/tree/main/module/worker/src/input/async-iterator.ts#L6) which allows for manual control of iteration, which is useful for event driven work loads.
22
-
23
- Below is a pool that will convert images on demand, while queuing as needed.
24
-
25
- **Code: Image processing queue, with a fixed batch/pool size**
26
- ```typescript
27
- import { ExecUtil, ExecutionState } from '@travetto/base';
28
- import { Worker, WorkPool, IterableWorkSet, ManualAsyncIterator } from '@travetto/worker';
29
-
30
- class ImageProcessor implements Worker<string> {
31
- active = false;
32
- proc: ExecutionState;
33
-
34
- get id(): number | undefined {
35
- return this.proc.process.pid;
36
- }
37
-
38
- async destroy(): Promise<void> {
39
- this.proc.process.kill();
40
- }
41
-
42
- async execute(path: string): Promise<void> {
43
- this.active = true;
44
- try {
45
- this.proc = ExecUtil.spawn('convert images', [path]);
46
- await this.proc;
47
- } catch {
48
- // Do nothing
49
- }
50
- this.active = false;
51
- }
52
- }
53
-
54
- export class ImageCompressor extends WorkPool<string> {
55
-
56
- pendingImages = new ManualAsyncIterator<string>();
57
-
58
- constructor() {
59
- super(async () => new ImageProcessor());
60
- }
61
-
62
- begin(): void {
63
- this.process(new IterableWorkSet(this.pendingImages));
64
- }
65
-
66
- convert(...images: string[]): void {
67
- this.pendingImages.add(images);
68
- }
69
- }
70
- ```
71
-
72
- Once a pool is constructed, it can be shutdown by calling the `.shutdown()` method, and awaiting the result.
19
+ With respect to managing multiple executions, [WorkPool](https://github.com/travetto/travetto/tree/main/module/worker/src/pool.ts#L35) is provided to allow for concurrent operation, and processing of jobs concurrently. To manage the flow of jobs, [WorkQueue](https://github.com/travetto/travetto/tree/main/module/worker/src/queue.ts#L6) is provided to support a wide range of use cases. [WorkQueue](https://github.com/travetto/travetto/tree/main/module/worker/src/queue.ts#L6) allows for manual control of iteration, which is useful for event driven work loads.
73
20
 
74
21
  ## IPC Support
75
22
  Within the `comm` package, there is support for two primary communication elements: [ChildCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/child.ts#L6) and [ParentCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/parent.ts#L10). Usually [ParentCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/parent.ts#L10) indicates it is the owner of the sub process. [ChildCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/child.ts#L6) indicates that it has been created/spawned/forked by the parent and will communicate back to it's parent. This generally means that a [ParentCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/parent.ts#L10) can be destroyed (i.e. killing the subprocess) where a [ChildCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/child.ts#L6) can only exit the process, but the channel cannot be destroyed.
76
-
77
- ### IPC as a Worker
78
- A common pattern is to want to model a sub process as a worker, to be a valid candidate in a [WorkPool](https://github.com/travetto/travetto/tree/main/module/worker/src/pool.ts#L31). The [WorkUtil](https://github.com/travetto/travetto/tree/main/module/worker/src/util.ts#L14) class provides a utility to facilitate this desire.
79
-
80
- **Code: Spawned Worker**
81
- ```typescript
82
- import { ExecutionState } from '@travetto/base';
83
-
84
- import { ParentCommChannel } from './comm/parent';
85
- import { Worker } from './pool';
86
-
87
- type Simple<V> = (ch: ParentCommChannel<V>) => Promise<unknown | void>;
88
- type Param<V, X> = (ch: ParentCommChannel<V>, input: X) => Promise<unknown | void>;
89
-
90
- const empty = async (): Promise<void> => { };
91
-
92
- /**
93
- * Spawned worker
94
- */
95
- export class WorkUtil {
96
- /**
97
- * Create a process channel worker from a given spawn config
98
- */
99
- static spawnedWorker<V, X>(
100
- worker: () => ExecutionState,
101
- init: Simple<V>,
102
- execute: Param<V, X>,
103
- destroy: Simple<V> = empty): Worker<X> {
104
- const channel = new ParentCommChannel<V>(worker());
105
- return {
106
- get id(): number | undefined { return channel.id; },
107
- get active(): boolean { return channel.active; },
108
- init: () => init(channel),
109
- execute: inp => execute(channel, inp),
110
- async destroy(): Promise<void> {
111
- await destroy(channel);
112
- await channel.destroy();
113
- },
114
- };
115
- }
116
- }
117
- ```
118
-
119
- When creating your work, via process spawning, you will need to provide the script (and any other features you would like in `SpawnConfig`). Additionally you must, at a minimum, provide functionality to run whenever an input element is up for grabs in the input source. This method will be provided the communication channel ([ParentCommChannel](https://github.com/travetto/travetto/tree/main/module/worker/src/comm/parent.ts#L10)) and the input value. A simple example could look like:
120
-
121
- **Code: Spawning Pool**
122
- ```typescript
123
- import { ExecUtil } from '@travetto/base';
124
- import { WorkPool, WorkUtil, IterableWorkSet } from '@travetto/worker';
125
-
126
- export async function main(): Promise<void> {
127
- const pool = new WorkPool(() =>
128
- WorkUtil.spawnedWorker<{ data: number }, number>(
129
- () => ExecUtil.spawn('trv', ['main', '@travetto/worker/doc/spawned.ts']),
130
- ch => ch.once('ready'), // Wait for child to indicate it is ready
131
- async (channel, inp) => {
132
- const res = channel.once('response'); // Register response listener
133
- channel.send('request', { data: inp }); // Send request
134
-
135
- const { data } = await res; // Get answer
136
- console.log('Request complete', { input: inp, output: data });
137
-
138
- if (!(inp + inp === data)) {
139
- // Ensure the answer is double the input
140
- throw new Error(`Did not get the double: inp=${inp}, data=${data}`);
141
- }
142
- }
143
- )
144
- );
145
- await pool.process(new IterableWorkSet([1, 2, 3, 4, 5]));
146
- }
147
- ```
148
-
149
- **Code: Spawned Worker**
150
- ```typescript
151
- import timers from 'timers/promises';
152
- import { ChildCommChannel } from '@travetto/worker';
153
-
154
- export async function main(): Promise<void> {
155
- const exec = new ChildCommChannel<{ data: string }>();
156
-
157
- exec.on('request', data =>
158
- exec.send('response', { data: (data.data + data.data) })); // When data is received, return double
159
-
160
- exec.send('ready'); // Indicate the child is ready to receive requests
161
-
162
- for await (const _ of timers.setInterval(5000)) {
163
- // Keep-alive
164
- }
165
- }
166
- ```
167
-
168
- **Terminal: Output**
169
- ```bash
170
- $ trv main doc/spawner.ts
171
-
172
- Request complete { input: 1, output: 2 }
173
- Request complete { input: 2, output: 4 }
174
- Request complete { input: 3, output: 6 }
175
- Request complete { input: 4, output: 8 }
176
- Request complete { input: 5, output: 10 }
177
- ```
package/__index__.ts CHANGED
@@ -2,11 +2,8 @@ export * from './src/comm/channel';
2
2
  export * from './src/comm/child';
3
3
  export * from './src/comm/parent';
4
4
  export * from './src/comm/types';
5
- export * from './src/input/iterable';
6
- export * from './src/input/async-iterator';
7
- export * from './src/input/types';
8
5
  export * from './src/support/barrier';
9
6
  export * from './src/support/timeout';
10
7
  export * from './src/support/error';
8
+ export * from './src/queue';
11
9
  export * from './src/pool';
12
- export * from './src/util';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/worker",
3
- "version": "3.4.2",
3
+ "version": "4.0.0-rc.1",
4
4
  "description": "Process management utilities, with a focus on inter-process communication",
5
5
  "keywords": [
6
6
  "exec",
@@ -25,7 +25,7 @@
25
25
  "directory": "module/worker"
26
26
  },
27
27
  "dependencies": {
28
- "@travetto/base": "^3.4.2",
28
+ "@travetto/base": "^4.0.0-rc.1",
29
29
  "generic-pool": "^3.9.0"
30
30
  },
31
31
  "travetto": {
@@ -1,7 +1,5 @@
1
- import { ChildProcess } from 'child_process';
2
- import { EventEmitter } from 'events';
3
-
4
- import { ExecUtil } from '@travetto/base';
1
+ import { ChildProcess } from 'node:child_process';
2
+ import { EventEmitter } from 'node:events';
5
3
 
6
4
  /**
7
5
  * Channel that represents communication between parent/child
@@ -56,8 +54,8 @@ export class ProcessCommChannel<T extends NodeJS.Process | ChildProcess, V = unk
56
54
  console.debug('Sending', { pid: this.#parentId, id: this.id, eventType });
57
55
  if (!this.#proc) {
58
56
  throw new Error('this.proc was not defined');
59
- } else if (this.#proc.send) {
60
- this.#proc.send({ ...(data ?? {}), type: eventType });
57
+ } else if (this.#proc.send && this.#proc.connected) {
58
+ this.#proc.send({ ...(data ?? {}), type: eventType }, (err) => err && console.error(err));
61
59
  } else {
62
60
  throw new Error('this.proc.send was not defined');
63
61
  }
@@ -92,7 +90,8 @@ export class ProcessCommChannel<T extends NodeJS.Process | ChildProcess, V = unk
92
90
  if (this.#proc) {
93
91
  console.debug('Killing', { pid: this.#parentId, id: this.id });
94
92
  if (this.#proc !== process) {
95
- ExecUtil.kill(this.#proc);
93
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
94
+ (this.#proc as ChildProcess).kill();
96
95
  }
97
96
  this.#proc = undefined;
98
97
  }
@@ -1,6 +1,6 @@
1
- import { ChildProcess } from 'child_process';
1
+ import { ChildProcess } from 'node:child_process';
2
2
 
3
- import { ShutdownManager, ExecutionState } from '@travetto/base';
3
+ import { ShutdownManager } from '@travetto/base';
4
4
 
5
5
  import { ProcessCommChannel } from './channel';
6
6
 
@@ -9,13 +9,12 @@ import { ProcessCommChannel } from './channel';
9
9
  */
10
10
  export class ParentCommChannel<U = unknown> extends ProcessCommChannel<ChildProcess, U> {
11
11
 
12
- #complete: ExecutionState['result'];
12
+ #complete: Promise<void>;
13
13
 
14
- constructor(state: ExecutionState) {
15
- super(state.process);
16
- ShutdownManager.onShutdown(this, { close: () => this.destroy() });
17
- this.#complete = state.result
18
- .finally(() => { this.proc = undefined; });
14
+ constructor(proc: ChildProcess) {
15
+ super(proc);
16
+ ShutdownManager.onGracefulShutdown(() => this.destroy(), this);
17
+ this.#complete = new Promise<void>(r => proc.on('close', r)).finally(() => { this.proc = undefined; });
19
18
  }
20
19
 
21
20
  /**
package/src/pool.ts CHANGED
@@ -1,211 +1,178 @@
1
- import os from 'os';
2
1
  import gp from 'generic-pool';
2
+ import os from 'node:os';
3
+ import timers from 'node:timers/promises';
3
4
 
4
- import { GlobalEnv, ShutdownManager, TimeUtil } from '@travetto/base';
5
+ import { Env, Util } from '@travetto/base';
5
6
 
6
- import { WorkSet } from './input/types';
7
- import { ManualAsyncIterator } from '../src/input/async-iterator';
7
+ import { WorkQueue } from './queue';
8
+
9
+ type ItrSource<I> = Iterable<I> | AsyncIterable<I>;
8
10
 
9
11
  /**
10
12
  * Worker definition
11
13
  */
12
- export interface Worker<X> {
13
- active: boolean;
14
- id: unknown;
14
+ export interface Worker<I, O = unknown> {
15
+ active?: boolean;
16
+ id?: unknown;
15
17
  init?(): Promise<unknown>;
16
- execute(input: X): Promise<unknown>;
17
- destroy(): Promise<void>;
18
+ execute(input: I, idx: number): Promise<O>;
19
+ destroy?(): Promise<void>;
18
20
  release?(): unknown;
19
21
  }
20
22
 
21
- type WorkPoolProcessConfig<X> = {
22
- shutdownOnComplete?: boolean;
23
- onComplete?: (ev: WorkCompletionEvent<X>) => (void | Promise<void>);
23
+ type WorkerInput<I, O> = (() => Worker<I, O>) | ((input: I, inputIdx: number) => Promise<O>);
24
+ type WorkPoolConfig<I, O> = gp.Options & {
25
+ onComplete?: (output: O, input: I, finishIdx: number) => void;
26
+ onError?(ev: Error, input: I, finishIdx: number): (unknown | Promise<unknown>);
27
+ shutdown?: AbortSignal;
24
28
  };
25
29
 
26
- type WorkCompletionEvent<X> = { idx: number, total?: number, value: X };
30
+ const isWorkerFactory = <I, O>(o: WorkerInput<I, O>): o is (() => Worker<I, O>) => o.length === 0;
27
31
 
28
32
  /**
29
33
  * Work pool support
30
34
  */
31
- export class WorkPool<X> {
35
+ export class WorkPool {
32
36
 
33
- static MAX_SIZE = os.cpus().length - 1;
37
+ static MAX_SIZE = os.availableParallelism();
34
38
  static DEFAULT_SIZE = Math.min(WorkPool.MAX_SIZE, 4);
35
39
 
36
- /**
37
- * Generic-pool pool
38
- */
39
- #pool: gp.Pool<Worker<X>>;
40
- /**
41
- * Number of acquisitions in process
42
- */
43
- #pendingAcquires = 0;
44
- /**
45
- * List of errors during processing
46
- */
47
- #errors: Error[] = [];
48
-
49
- /**
50
- * Error count during creation
51
- */
52
- #createErrors = 0;
40
+ /** Build worker pool */
41
+ static #buildPool<I, O>(worker: WorkerInput<I, O>, opts?: WorkPoolConfig<I, O>): gp.Pool<Worker<I, O>> {
42
+ let pendingAcquires = 0;
53
43
 
54
- /**
55
- * Are we tracing
56
- */
57
- #trace: boolean;
44
+ const trace = /@travetto\/worker/.test(Env.DEBUG.val ?? '');
58
45
 
59
- /**
60
- * Cleanup for shutdown
61
- */
62
- #shutdownCleanup?: () => void;
63
-
64
- /**
65
- *
66
- * @param getWorker Produces a new worker for the pool
67
- * @param opts Pool options
68
- */
69
- constructor(getWorker: () => Promise<Worker<X>> | Worker<X>, opts?: gp.Options) {
70
- const args = {
46
+ // Create the pool
47
+ const pool = gp.createPool({
48
+ async create() {
49
+ try {
50
+ pendingAcquires += 1;
51
+ const res = isWorkerFactory(worker) ? await worker() : { execute: worker };
52
+ res.id ??= Util.shortHash(`${Math.random()}`);
53
+
54
+ if (res.init) {
55
+ await res.init();
56
+ }
57
+
58
+ return res;
59
+ } finally {
60
+ pendingAcquires -= 1;
61
+ }
62
+ },
63
+ async destroy(x) {
64
+ if (trace) {
65
+ console.debug('Destroying', { pid: process.pid, worker: x.id });
66
+ }
67
+ return x.destroy?.();
68
+ },
69
+ validate: async (x: Worker<I, O>) => x.active ?? true
70
+ }, {
71
71
  max: WorkPool.DEFAULT_SIZE,
72
72
  min: 1,
73
73
  evictionRunIntervalMillis: 5000,
74
74
  ...(opts ?? {}),
75
- };
76
-
77
- // Create the pool
78
- this.#pool = gp.createPool({
79
- create: () => this.#createAndTrack(getWorker, args),
80
- destroy: x => this.destroy(x),
81
- validate: async (x: Worker<X>) => x.active
82
- }, args);
83
-
84
- this.#shutdownCleanup = ShutdownManager.onShutdown(`worker.pool.${this.constructor.name}`, () => {
85
- this.#shutdownCleanup = undefined;
86
- this.shutdown();
87
75
  });
88
76
 
89
- this.#trace = !!GlobalEnv.debug?.includes('@travetto/worker');
90
- }
91
-
92
- /**
93
- * Creates and tracks new worker
94
- */
95
- async #createAndTrack(getWorker: () => Promise<Worker<X>> | Worker<X>, opts: gp.Options): Promise<Worker<X>> {
96
- try {
97
- this.#pendingAcquires += 1;
98
- const res = await getWorker();
99
-
100
- if (res.init) {
101
- await res.init();
102
- }
103
-
104
- this.#createErrors = 0; // Reset errors on success
105
77
 
106
- return res;
107
- } catch (err) {
108
- if (this.#createErrors++ > opts.max!) { // If error count is bigger than pool size, we broke
109
- console.error('Failed in creating pool', { error: err });
110
- await ShutdownManager.exit(1);
78
+ // Listen for shutdown
79
+ opts?.shutdown?.addEventListener('abort', async () => {
80
+ while (pendingAcquires) {
81
+ await timers.setTimeout(10);
111
82
  }
112
- throw err;
113
- } finally {
114
- this.#pendingAcquires -= 1;
115
- }
116
- }
83
+ await pool.drain();
84
+ await pool.clear();
85
+ });
117
86
 
118
- /**
119
- * Destroy the worker
120
- */
121
- async destroy(worker: Worker<X>): Promise<void> {
122
- if (this.#trace) {
123
- console.debug('Destroying', { pid: process.pid, worker: worker.id });
124
- }
125
- return worker.destroy();
87
+ return pool;
126
88
  }
127
89
 
128
90
  /**
129
- * Free worker on completion
91
+ * Process a given input source and worker, and fire on completion
130
92
  */
131
- async release(worker: Worker<X>): Promise<void> {
132
- if (this.#trace) {
133
- console.debug('Releasing', { pid: process.pid, worker: worker.id });
134
- }
135
- try {
136
- if (worker.active) {
137
- try {
138
- await worker.release?.();
139
- } catch { }
140
- await this.#pool.release(worker);
141
- } else {
142
- await this.#pool.destroy(worker);
143
- }
144
- } catch { }
145
- }
93
+ static async run<I, O>(workerFactory: WorkerInput<I, O>, src: ItrSource<I>, opts: WorkPoolConfig<I, O> = {}): Promise<void> {
146
94
 
147
- /**
148
- * Process a given input source
149
- */
150
- async process(src: WorkSet<X>, cfg: WorkPoolProcessConfig<X> = {}): Promise<void> {
95
+ const trace = /@travetto\/worker/.test(Env.DEBUG.val ?? '');
151
96
  const pending = new Set<Promise<unknown>>();
152
- let count = 0;
97
+ const errors: Error[] = [];
98
+ let inputIdx = 0;
99
+ let finishIdx = 0;
153
100
 
154
- if (src.size && cfg.onComplete) {
155
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
156
- cfg.onComplete({ value: undefined as X, idx: 0, total: src.size });
157
- }
101
+ const pool = this.#buildPool(workerFactory, opts);
158
102
 
159
- while (await src.hasNext()) {
160
- const worker = (await this.#pool.acquire())!;
161
- if (this.#trace) {
103
+ for await (const nextInput of src) {
104
+ const worker = await pool.acquire()!;
105
+
106
+ if (trace) {
162
107
  console.debug('Acquired', { pid: process.pid, worker: worker.id });
163
108
  }
164
- const nextInput = await src.next();
165
109
 
166
- const completion = worker.execute(nextInput)
167
- .catch(err => this.#errors.push(err)) // Catch error
168
- .finally(() => this.release(worker))
169
- .finally(() => cfg.onComplete?.({ value: nextInput, idx: count += 1, total: src.size }));
110
+ const completion = worker.execute(nextInput, inputIdx += 1)
111
+ .then(v => opts.onComplete?.(v, nextInput, finishIdx += 1))
112
+ .catch(err => {
113
+ errors.push(err);
114
+ opts?.onError?.(err, nextInput, finishIdx += 1);
115
+ }) // Catch error
116
+ .finally(async () => {
117
+ if (trace) {
118
+ console.debug('Releasing', { pid: process.pid, worker: worker.id });
119
+ }
120
+ try {
121
+ if (worker.active ?? true) {
122
+ try {
123
+ await worker.release?.();
124
+ } catch { }
125
+ await pool.release(worker);
126
+ } else {
127
+ await pool.destroy(worker);
128
+ }
129
+ } catch { }
130
+ });
170
131
 
171
132
  completion.finally(() => pending.delete(completion));
172
-
173
133
  pending.add(completion);
174
134
  }
175
135
 
176
- try {
177
- await Promise.all(Array.from(pending));
136
+ await Promise.all(Array.from(pending));
178
137
 
179
- if (this.#errors.length) {
180
- throw this.#errors[0];
181
- }
182
- } finally {
183
- if (cfg.shutdownOnComplete !== false) {
184
- await this.shutdown();
185
- }
138
+ if (errors.length) {
139
+ throw errors[0];
186
140
  }
187
141
  }
188
142
 
189
143
  /**
190
- * Process a given input source as an async iterable, emitting on completion
144
+ * Process a given input source as an async iterable
191
145
  */
192
- iterateProcess(src: WorkSet<X>, shutdownOnComplete?: boolean): AsyncIterable<WorkCompletionEvent<X>> {
193
- const itr = new ManualAsyncIterator<WorkCompletionEvent<X>>();
194
- const res = this.process(src, { onComplete: ev => itr.add(ev), shutdownOnComplete });
146
+ static runStream<I, O>(worker: WorkerInput<I, O>, input: ItrSource<I>, opts?: WorkPoolConfig<I, O>): AsyncIterable<O> {
147
+ const itr = new WorkQueue<O>();
148
+ const res = this.run(worker, input, {
149
+ ...opts,
150
+ onComplete: (ev, inp, finishIdx) => {
151
+ itr.add(ev);
152
+ opts?.onComplete?.(ev, inp, finishIdx);
153
+ }
154
+ });
195
155
  res.finally(() => itr.close());
196
156
  return itr;
197
157
  }
198
158
 
199
159
  /**
200
- * Shutdown pool
160
+ * Process a given input source as an async iterable with progress information
201
161
  */
202
- async shutdown(): Promise<void> {
203
- this.#shutdownCleanup?.();
204
-
205
- while (this.#pendingAcquires) {
206
- await TimeUtil.wait(10);
207
- }
208
- await this.#pool.drain();
209
- await this.#pool.clear();
162
+ static runStreamProgress<I, O>(worker: WorkerInput<I, O>, input: ItrSource<I>, total: number, opts?: WorkPoolConfig<I, O>): AsyncIterable<{
163
+ idx: number;
164
+ value: O;
165
+ total: number;
166
+ }> {
167
+ const itr = new WorkQueue<{ idx: number, value: O, total: number }>();
168
+ const res = this.run(worker, input, {
169
+ ...opts,
170
+ onComplete: (ev, inp, finishIdx) => {
171
+ itr.add({ value: ev, idx: finishIdx, total });
172
+ opts?.onComplete?.(ev, inp, finishIdx);
173
+ }
174
+ });
175
+ res.finally(() => itr.close());
176
+ return itr;
210
177
  }
211
- }
178
+ }
@@ -1,9 +1,9 @@
1
1
  import { Util } from '@travetto/base';
2
2
 
3
3
  /**
4
- * Manual async iterator. Items are added manually, and consumed asynchronously
4
+ * WorkQueue, a manual async iterator. Items are added manually, and consumed asynchronously
5
5
  */
6
- export class ManualAsyncIterator<X> implements AsyncIterator<X>, AsyncIterable<X> {
6
+ export class WorkQueue<X> implements AsyncIterator<X>, AsyncIterable<X> {
7
7
 
8
8
  #queue: X[] = [];
9
9
  #done = false;
@@ -17,6 +17,9 @@ export class ManualAsyncIterator<X> implements AsyncIterator<X>, AsyncIterable<X
17
17
  this.#queue.push(...initial);
18
18
  this.#size = this.#queue.length;
19
19
  signal?.addEventListener('abort', () => this.close());
20
+ if (signal?.aborted) {
21
+ this.close();
22
+ }
20
23
  }
21
24
 
22
25
  // Allow for iteration
@@ -39,14 +42,20 @@ export class ManualAsyncIterator<X> implements AsyncIterator<X>, AsyncIterable<X
39
42
  * Queue next event to fire
40
43
  * @param {boolean} immediate Determines if item(s) should be append or prepended to the queue
41
44
  */
42
- add(item: X | X[], immediate = false): void {
43
- item = Array.isArray(item) ? item : [item];
44
- if (!immediate) {
45
- this.#queue.push(...item);
46
- } else {
47
- this.#queue.unshift(...item);
48
- }
49
- this.#size += item.length;
45
+ add(item: X, immediate = false): void {
46
+ this.#queue[immediate ? 'unshift' : 'push'](item);
47
+ this.#size += 1;
48
+ this.#ready.resolve();
49
+ }
50
+
51
+ /**
52
+ * Queue a list of data to stream
53
+ * @param {boolean} immediate Determines if item(s) should be append or prepended to the queue
54
+ */
55
+ addAll(items: Iterable<X>): void {
56
+ const copy = [...items];
57
+ this.#queue.push(...copy);
58
+ this.#size += copy.length;
50
59
  this.#ready.resolve();
51
60
  }
52
61
 
@@ -58,6 +67,15 @@ export class ManualAsyncIterator<X> implements AsyncIterator<X>, AsyncIterable<X
58
67
  this.#ready.resolve();
59
68
  }
60
69
 
70
+ /**
71
+ * Throw an error from the queue, rejecting and terminating immediately
72
+ */
73
+ async throw(e?: Error): Promise<IteratorResult<X>> {
74
+ this.#done = true;
75
+ this.#ready.reject(e);
76
+ return { value: undefined, done: this.#done };
77
+ }
78
+
61
79
  /**
62
80
  * Get size, will change as items are added
63
81
  */
@@ -1,6 +1,6 @@
1
- import { setTimeout } from 'timers/promises';
1
+ import { setTimeout } from 'node:timers/promises';
2
2
 
3
- import { ShutdownManager, TimeSpan, Util } from '@travetto/base';
3
+ import { TimeSpan, Util } from '@travetto/base';
4
4
 
5
5
  import { Timeout } from './timeout';
6
6
 
@@ -19,7 +19,11 @@ export class Barrier {
19
19
  static listenForUnhandled(): Promise<never> & { cancel: () => void } {
20
20
  const uncaught = Util.resolvablePromise<never>();
21
21
  const uncaughtWithCancel: typeof uncaught & { cancel?: () => void } = uncaught;
22
- const cancel = ShutdownManager.onUnhandled(err => { setTimeout(1).then(() => uncaught.reject(err)); return true; }, 0);
22
+ const onError = (err: Error): void => { setTimeout(1).then(() => uncaught.reject(err)); };
23
+ process.on('unhandledRejection', onError).on('uncaughtException', onError);
24
+ const cancel = (): void => {
25
+ process.off('unhandledRejection', onError).off('unhandledException', onError);
26
+ };
23
27
  uncaughtWithCancel.cancel = (): void => {
24
28
  cancel(); // Remove the handler
25
29
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -1,3 +1,4 @@
1
+ import { clearTimeout } from 'node:timers';
1
2
  import { TimeSpan, TimeUtil, Util } from '@travetto/base';
2
3
  import { ExecutionError } from './error';
3
4
 
@@ -6,7 +7,7 @@ import { ExecutionError } from './error';
6
7
  */
7
8
  export class Timeout extends ExecutionError {
8
9
 
9
- #id: NodeJS.Timer | undefined;
10
+ #id: ReturnType<typeof setTimeout> | undefined;
10
11
  #promise = Util.resolvablePromise();
11
12
  #duration: number;
12
13
 
@@ -1,73 +0,0 @@
1
- import { WorkSet } from './types';
2
-
3
- type Itr<T> = Iterator<T> | AsyncIterator<T>;
4
-
5
- const hasAsyncItr = (o: unknown): o is AsyncIterable<unknown> => !!o && typeof o === 'object' && Symbol.asyncIterator in o;
6
- const hasItr = (o: unknown): o is Iterable<unknown> => !!o && typeof o === 'object' && Symbol.iterator in o;
7
-
8
- /**
9
- * Basic input source given an iterable input
10
- */
11
- export class IterableWorkSet<X> implements WorkSet<X> {
12
-
13
- #src: Itr<X>;
14
- #ondeck?: X;
15
- #done = false;
16
- #size?: number;
17
-
18
- constructor(src: Iterable<X> | AsyncIterable<X> | (() => Generator<X>) | (() => AsyncGenerator<X>) | Itr<X>) {
19
- if ('next' in src) {
20
- this.#src = src;
21
- } else {
22
- if (hasAsyncItr(src)) {
23
- this.#src = src[Symbol.asyncIterator]();
24
- } else if (hasItr(src)) {
25
- this.#src = src[Symbol.iterator]();
26
- } else {
27
- this.#src = src();
28
- }
29
- }
30
- if (Array.isArray(src)) {
31
- this.#size = src.length;
32
- } else if (src instanceof Set) {
33
- this.#size = src.size;
34
- }
35
- }
36
-
37
- /**
38
- * Fetch next item from the iterable
39
- */
40
- async #primeNext(): Promise<void> {
41
- const res = await this.#src.next();
42
- this.#done = !!res.done;
43
- this.#ondeck = res.value;
44
- }
45
-
46
- /**
47
- * Determine if the iterable has more data
48
- */
49
- async hasNext(): Promise<boolean> {
50
- if (this.#ondeck === undefined) {
51
- await this.#primeNext();
52
- }
53
- return !this.#done;
54
- }
55
-
56
- /**
57
- * Fetch next item
58
- */
59
- async next(): Promise<X> {
60
- await this.hasNext();
61
-
62
- const out = this.#ondeck!;
63
- this.#ondeck = undefined;
64
- return out;
65
- }
66
-
67
- /**
68
- * Get size, if defined
69
- */
70
- get size(): number | undefined {
71
- return this.#size;
72
- }
73
- }
@@ -1,17 +0,0 @@
1
- /**
2
- * Definition for an work set
3
- */
4
- export interface WorkSet<X> {
5
- /**
6
- * Determines if there is more work to do
7
- */
8
- hasNext(): boolean | Promise<boolean>;
9
- /**
10
- * Get next item
11
- */
12
- next(): X | Promise<X>;
13
- /**
14
- * Total size of work set
15
- */
16
- size?: number;
17
- }
package/src/util.ts DELETED
@@ -1,35 +0,0 @@
1
- import { ExecutionState } from '@travetto/base';
2
-
3
- import { ParentCommChannel } from './comm/parent';
4
- import { Worker } from './pool';
5
-
6
- type Simple<V> = (ch: ParentCommChannel<V>) => Promise<unknown | void>;
7
- type Param<V, X> = (ch: ParentCommChannel<V>, input: X) => Promise<unknown | void>;
8
-
9
- const empty = async (): Promise<void> => { };
10
-
11
- /**
12
- * Spawned worker
13
- */
14
- export class WorkUtil {
15
- /**
16
- * Create a process channel worker from a given spawn config
17
- */
18
- static spawnedWorker<V, X>(
19
- worker: () => ExecutionState,
20
- init: Simple<V>,
21
- execute: Param<V, X>,
22
- destroy: Simple<V> = empty): Worker<X> {
23
- const channel = new ParentCommChannel<V>(worker());
24
- return {
25
- get id(): number | undefined { return channel.id; },
26
- get active(): boolean { return channel.active; },
27
- init: () => init(channel),
28
- execute: inp => execute(channel, inp),
29
- async destroy(): Promise<void> {
30
- await destroy(channel);
31
- await channel.destroy();
32
- },
33
- };
34
- }
35
- }