@dmop/puru 0.1.5 → 0.1.10

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/dist/index.d.ts CHANGED
@@ -1,3 +1,12 @@
1
+ interface JsonObject {
2
+ [key: string]: JsonValue;
3
+ }
4
+ type JsonValue = null | string | number | boolean | JsonValue[] | JsonObject;
5
+ interface StructuredCloneObject {
6
+ [key: string]: StructuredCloneValue;
7
+ }
8
+ type StructuredCloneValue = void | null | undefined | string | number | boolean | bigint | Date | RegExp | Error | ArrayBuffer | ArrayBufferView | StructuredCloneValue[] | Map<StructuredCloneValue, StructuredCloneValue> | Set<StructuredCloneValue> | StructuredCloneObject;
9
+ type ChannelValue = Exclude<StructuredCloneValue, null>;
1
10
  interface PuruConfig {
2
11
  maxThreads: number;
3
12
  strategy: 'fifo' | 'work-stealing';
@@ -30,7 +39,7 @@ interface SpawnResult<T> {
30
39
  * process(item)
31
40
  * }
32
41
  */
33
- interface Channel<T> {
42
+ interface Channel<T extends ChannelValue> {
34
43
  send(value: T): Promise<void>;
35
44
  /** Resolves with the next value, or `null` if the channel is closed. */
36
45
  recv(): Promise<T | null>;
@@ -65,7 +74,7 @@ interface Channel<T> {
65
74
  * }, { channels: { input, output } })
66
75
  * }
67
76
  */
68
- declare function chan<T extends NonNullable<unknown>>(capacity?: number): Channel<T>;
77
+ declare function chan<T extends ChannelValue>(capacity?: number): Channel<T>;
69
78
 
70
79
  /**
71
80
  * Run a function in a worker thread. Returns a handle with the result promise and a cancel function.
@@ -108,12 +117,13 @@ declare function chan<T extends NonNullable<unknown>>(capacity?: number): Channe
108
117
  * ch.close()
109
118
  * }, { channels: { ch } })
110
119
  */
111
- declare function spawn<T>(fn: (() => T | Promise<T>) | ((channels: Record<string, Channel<unknown>>) => T | Promise<T>), opts?: {
120
+ declare function spawn<T extends StructuredCloneValue, TChannels extends Record<string, Channel<ChannelValue>> = Record<never, never>>(fn: (() => T | Promise<T>) | ((channels: TChannels) => T | Promise<T>), opts?: {
112
121
  priority?: 'low' | 'normal' | 'high';
113
122
  concurrent?: boolean;
114
- channels?: Record<string, Channel<unknown>>;
123
+ channels?: TChannels;
115
124
  }): SpawnResult<T>;
116
125
 
126
+ type SpawnChannels$1 = Record<string, Channel<ChannelValue>>;
117
127
  /**
118
128
  * Structured concurrency: spawn multiple tasks and wait for all to complete.
119
129
  *
@@ -144,7 +154,7 @@ declare function spawn<T>(fn: (() => T | Promise<T>) | ((channels: Record<string
144
154
  * else console.error(r.reason)
145
155
  * }
146
156
  */
147
- declare class WaitGroup {
157
+ declare class WaitGroup<T extends StructuredCloneValue = StructuredCloneValue> {
148
158
  private tasks;
149
159
  private controller;
150
160
  /**
@@ -157,20 +167,20 @@ declare class WaitGroup {
157
167
  *
158
168
  * @throws If the group has already been cancelled.
159
169
  */
160
- spawn(fn: (() => unknown) | ((channels: Record<string, Channel<unknown>>) => unknown), opts?: {
170
+ spawn<TChannels extends SpawnChannels$1 = Record<never, never>>(fn: (() => T | Promise<T>) | ((channels: TChannels) => T | Promise<T>), opts?: {
161
171
  concurrent?: boolean;
162
- channels?: Record<string, Channel<unknown>>;
172
+ channels?: TChannels;
163
173
  }): void;
164
174
  /**
165
175
  * Waits for all tasks to complete successfully.
166
176
  * Rejects as soon as any task throws.
167
177
  */
168
- wait(): Promise<unknown[]>;
178
+ wait(): Promise<T[]>;
169
179
  /**
170
180
  * Waits for all tasks to settle (fulfilled or rejected) and returns each outcome.
171
181
  * Never rejects — inspect each `PromiseSettledResult` to handle failures individually.
172
182
  */
173
- waitSettled(): Promise<PromiseSettledResult<unknown>[]>;
183
+ waitSettled(): Promise<PromiseSettledResult<T>[]>;
174
184
  /**
175
185
  * Cancels all tasks in the group and signals the shared `AbortSignal`.
176
186
  * Already-settled tasks are unaffected.
@@ -178,6 +188,7 @@ declare class WaitGroup {
178
188
  cancel(): void;
179
189
  }
180
190
 
191
+ type SpawnChannels = Record<string, Channel<ChannelValue>>;
181
192
  /**
182
193
  * Like `WaitGroup`, but cancels all remaining tasks on the first error.
183
194
  *
@@ -207,17 +218,17 @@ declare class WaitGroup {
207
218
  * // use task() with register() and check a channel or AbortSignal instead
208
219
  * })
209
220
  */
210
- declare class ErrGroup {
221
+ declare class ErrGroup<T extends StructuredCloneValue = StructuredCloneValue> {
211
222
  private tasks;
212
223
  private controller;
213
224
  private firstError;
214
225
  private hasError;
215
226
  get signal(): AbortSignal;
216
- spawn(fn: (() => unknown) | ((channels: Record<string, Channel<unknown>>) => unknown), opts?: {
227
+ spawn<TChannels extends SpawnChannels = Record<never, never>>(fn: (() => T | Promise<T>) | ((channels: TChannels) => T | Promise<T>), opts?: {
217
228
  concurrent?: boolean;
218
- channels?: Record<string, Channel<unknown>>;
229
+ channels?: TChannels;
219
230
  }): void;
220
- wait(): Promise<unknown[]>;
231
+ wait(): Promise<T[]>;
221
232
  cancel(): void;
222
233
  }
223
234
 
@@ -291,7 +302,7 @@ declare class Once<T = void> {
291
302
  reset(): void;
292
303
  }
293
304
 
294
- type SelectCase<T = unknown> = [Promise<T>, (value: T) => void];
305
+ type SelectCase<T = StructuredCloneValue> = [Promise<T>, (value: T) => void];
295
306
  /**
296
307
  * Options for `select()`.
297
308
  *
@@ -427,7 +438,7 @@ declare function ticker(ms: number): Ticker;
427
438
  * const result = await resizeImage('photo.jpg', 800, 600)
428
439
  * const [a, b] = await Promise.all([resizeImage('a.jpg', 400, 300), resizeImage('b.jpg', 800, 600)])
429
440
  */
430
- declare function task<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn | Promise<TReturn>): (...args: TArgs) => Promise<TReturn>;
441
+ declare function task<TArgs extends JsonValue[], TReturn extends StructuredCloneValue>(fn: (...args: TArgs) => TReturn | Promise<TReturn>): (...args: TArgs) => Promise<TReturn>;
431
442
 
432
443
  /**
433
444
  * Configure the global thread pool. **Must be called before the first `spawn()`.**
package/dist/index.js CHANGED
@@ -56,21 +56,20 @@ function getConfig() {
56
56
 
57
57
  // src/runtime.ts
58
58
  function detectRuntime() {
59
- if (typeof globalThis.Bun !== "undefined") return "bun";
60
- if (typeof globalThis.Deno !== "undefined") return "deno";
61
- if (typeof globalThis.process !== "undefined" && globalThis.process.versions?.node)
62
- return "node";
59
+ if ("Bun" in globalThis) return "bun";
60
+ if ("Deno" in globalThis) return "deno";
61
+ if (typeof globalThis.process !== "undefined" && globalThis.process.versions?.node) return "node";
63
62
  return "browser";
64
63
  }
65
64
  function detectCapability() {
66
65
  const runtime = detectRuntime();
67
66
  if (runtime === "node" || runtime === "bun") return "full-threads";
68
- if (typeof globalThis.Worker !== "undefined") return "full-threads";
67
+ if ("Worker" in globalThis) return "full-threads";
69
68
  return "single-thread";
70
69
  }
71
70
 
72
71
  // src/adapters/node.ts
73
- import { Worker as Worker2 } from "worker_threads";
72
+ import { Worker } from "worker_threads";
74
73
 
75
74
  // src/bootstrap.ts
76
75
  var CHANNEL_PROXY_CODE = `
@@ -286,7 +285,7 @@ self.postMessage({ type: 'ready' });
286
285
  var NodeManagedWorker = class {
287
286
  worker;
288
287
  constructor() {
289
- this.worker = new Worker2(NODE_BOOTSTRAP_CODE, { eval: true });
288
+ this.worker = new Worker(NODE_BOOTSTRAP_CODE, { eval: true });
290
289
  }
291
290
  get id() {
292
291
  return this.worker.threadId;
@@ -332,7 +331,11 @@ var BunManagedWorker = class {
332
331
  id;
333
332
  constructor() {
334
333
  this.id = ++workerIdCounter;
335
- this.worker = new Worker(getBootstrapFile());
334
+ const WorkerConstructor = globalThis.Worker;
335
+ if (!WorkerConstructor) {
336
+ throw new Error("Bun Worker constructor is not available in this runtime");
337
+ }
338
+ this.worker = new WorkerConstructor(getBootstrapFile());
336
339
  }
337
340
  postMessage(data) {
338
341
  this.worker.postMessage(data);
@@ -344,27 +347,28 @@ var BunManagedWorker = class {
344
347
  on(event, handler) {
345
348
  if (event === "message") {
346
349
  this.worker.addEventListener("message", (e) => {
350
+ ;
347
351
  handler(e.data);
348
352
  });
349
353
  } else if (event === "error") {
350
354
  this.worker.addEventListener("error", (e) => {
355
+ ;
351
356
  handler(e.error ?? new Error(e.message));
352
357
  });
353
358
  } else if (event === "exit") {
354
359
  this.worker.addEventListener("close", (e) => {
360
+ ;
355
361
  handler(e.code ?? 0);
356
362
  });
357
363
  }
358
364
  }
359
365
  unref() {
360
366
  if ("unref" in this.worker && typeof this.worker.unref === "function") {
361
- ;
362
367
  this.worker.unref();
363
368
  }
364
369
  }
365
370
  ref() {
366
371
  if ("ref" in this.worker && typeof this.worker.ref === "function") {
367
- ;
368
372
  this.worker.ref();
369
373
  }
370
374
  }
@@ -463,6 +467,9 @@ function chan(capacity = 0) {
463
467
  function getChannelById(id) {
464
468
  return channelRegistry.get(id);
465
469
  }
470
+ function getChannelId(channel) {
471
+ return channel._id;
472
+ }
466
473
 
467
474
  // src/adapters/inline.ts
468
475
  var inlineIdCounter = 0;
@@ -479,15 +486,14 @@ var InlineManagedWorker = class {
479
486
  this.emit("message", { type: "ready" });
480
487
  });
481
488
  }
482
- postMessage(data) {
489
+ postMessage(msg) {
483
490
  if (this.terminated) return;
484
- const msg = data;
485
491
  if (msg.type === "execute") {
486
- this.executeTask(msg.taskId, msg.fnStr, msg.concurrent ?? false, msg.channels);
492
+ this.executeTask(msg.taskId, msg.fnStr, msg.concurrent, msg.channels);
487
493
  } else if (msg.type === "cancel") {
488
494
  this.cancelledTasks.add(msg.taskId);
489
495
  } else if (msg.type === "channel-result") {
490
- this.emit("message", msg);
496
+ return;
491
497
  } else if (msg.type === "shutdown") {
492
498
  this.terminated = true;
493
499
  this.emit("exit", 0);
@@ -517,7 +523,6 @@ var InlineManagedWorker = class {
517
523
  }
518
524
  }
519
525
  buildChannelProxies(channels) {
520
- const self = this;
521
526
  const proxies = {};
522
527
  for (const [name, channelId] of Object.entries(channels)) {
523
528
  proxies[name] = {
@@ -539,13 +544,7 @@ var InlineManagedWorker = class {
539
544
  [Symbol.asyncIterator]() {
540
545
  const ch = getChannelById(channelId);
541
546
  if (!ch) throw new Error(`Channel ${channelId} not found`);
542
- return {
543
- async next() {
544
- const value = await ch.recv();
545
- if (value === null) return { done: true, value: void 0 };
546
- return { done: false, value };
547
- }
548
- };
547
+ return ch[Symbol.asyncIterator]();
549
548
  }
550
549
  };
551
550
  }
@@ -893,6 +892,15 @@ var WorkerPool = class {
893
892
  task2.reject(reason);
894
893
  }
895
894
  }
895
+ rejectExclusiveTaskForWorker(worker, reason) {
896
+ for (const [taskId, assignedWorker] of this.exclusiveWorkers) {
897
+ if (assignedWorker === worker) {
898
+ this.exclusiveWorkers.delete(taskId);
899
+ this.rejectTask(taskId, reason);
900
+ break;
901
+ }
902
+ }
903
+ }
896
904
  // --- Cancellation ---
897
905
  cancelTask(taskId) {
898
906
  const removed = this.removeFromQueue(taskId);
@@ -941,6 +949,10 @@ var WorkerPool = class {
941
949
  }
942
950
  this.concurrentQueues[priority] = [];
943
951
  }
952
+ for (const [taskId] of this.exclusiveWorkers) {
953
+ this.taskMap.delete(taskId);
954
+ }
955
+ this.exclusiveWorkers.clear();
944
956
  for (const [, taskSet] of this.sharedWorkers) {
945
957
  for (const taskId of taskSet) {
946
958
  this.taskMap.delete(taskId);
@@ -998,8 +1010,7 @@ var WorkerPool = class {
998
1010
  this.makeIdle(worker);
999
1011
  }
1000
1012
  };
1001
- worker.on("message", (msg) => {
1002
- const response = msg;
1013
+ worker.on("message", (response) => {
1003
1014
  if (response.type === "ready") {
1004
1015
  onReady();
1005
1016
  return;
@@ -1007,12 +1018,7 @@ var WorkerPool = class {
1007
1018
  this.handleWorkerMessage(worker, response);
1008
1019
  });
1009
1020
  worker.on("error", (err) => {
1010
- for (const [taskId, w] of this.exclusiveWorkers) {
1011
- if (w === worker) {
1012
- this.exclusiveWorkers.delete(taskId);
1013
- break;
1014
- }
1015
- }
1021
+ this.rejectExclusiveTaskForWorker(worker, err);
1016
1022
  const taskSet = this.sharedWorkers.get(worker);
1017
1023
  if (taskSet) {
1018
1024
  for (const taskId of taskSet) {
@@ -1032,12 +1038,7 @@ var WorkerPool = class {
1032
1038
  if (idleIdx !== -1) {
1033
1039
  this.idleWorkers.splice(idleIdx, 1);
1034
1040
  }
1035
- for (const [taskId, w] of this.exclusiveWorkers) {
1036
- if (w === worker) {
1037
- this.exclusiveWorkers.delete(taskId);
1038
- break;
1039
- }
1040
- }
1041
+ this.rejectExclusiveTaskForWorker(worker, new Error("Worker exited unexpectedly"));
1041
1042
  const taskSet = this.sharedWorkers.get(worker);
1042
1043
  if (taskSet) {
1043
1044
  for (const taskId of taskSet) {
@@ -1131,11 +1132,7 @@ function spawn(fn, opts) {
1131
1132
  if (opts?.channels) {
1132
1133
  channelMap = {};
1133
1134
  for (const [name, ch] of Object.entries(opts.channels)) {
1134
- const impl = ch;
1135
- if (!impl._id) {
1136
- throw new Error(`Channel "${name}" is not a valid puru channel`);
1137
- }
1138
- channelMap[name] = impl._id;
1135
+ channelMap[name] = getChannelId(ch);
1139
1136
  }
1140
1137
  }
1141
1138
  const task2 = {
@@ -1224,7 +1221,7 @@ var WaitGroup = class {
1224
1221
  var ErrGroup = class {
1225
1222
  tasks = [];
1226
1223
  controller = new AbortController();
1227
- firstError = void 0;
1224
+ firstError = null;
1228
1225
  hasError = false;
1229
1226
  get signal() {
1230
1227
  return this.controller.signal;
@@ -1245,7 +1242,7 @@ var ErrGroup = class {
1245
1242
  }
1246
1243
  async wait() {
1247
1244
  const settled = await Promise.allSettled(this.tasks.map((t) => t.result));
1248
- if (this.hasError) {
1245
+ if (this.hasError && this.firstError) {
1249
1246
  throw this.firstError;
1250
1247
  }
1251
1248
  return settled.map((r) => {