@dmop/puru 0.1.4 → 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.cts 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';
@@ -10,46 +19,248 @@ interface SpawnResult<T> {
10
19
  cancel: () => void;
11
20
  }
12
21
 
13
- interface Channel<T> {
22
+ /**
23
+ * A Go-style channel for communicating between async tasks and across worker threads.
24
+ *
25
+ * Use `chan<T>(capacity?)` to create a channel. Values must be structured-cloneable
26
+ * (no functions, symbols, or WeakRefs). `null` cannot be sent — `recv()` returns
27
+ * `null` only when the channel is closed.
28
+ *
29
+ * @example
30
+ * const ch = chan<number>(10)
31
+ * await ch.send(42)
32
+ * const value = await ch.recv() // 42
33
+ * ch.close()
34
+ * await ch.recv() // null — channel closed
35
+ *
36
+ * @example
37
+ * // Async iteration ends automatically when the channel is closed
38
+ * for await (const item of ch) {
39
+ * process(item)
40
+ * }
41
+ */
42
+ interface Channel<T extends ChannelValue> {
14
43
  send(value: T): Promise<void>;
44
+ /** Resolves with the next value, or `null` if the channel is closed. */
15
45
  recv(): Promise<T | null>;
16
46
  close(): void;
17
47
  [Symbol.asyncIterator](): AsyncIterator<T>;
18
48
  }
19
- declare function chan<T>(capacity?: number): Channel<T>;
49
+ /**
50
+ * Create a Go-style channel for communicating between tasks and across worker threads.
51
+ *
52
+ * Provides backpressure: `send()` blocks when the buffer is full,
53
+ * `recv()` blocks when the buffer is empty. Channel values must be structured-cloneable
54
+ * (no functions, symbols, or WeakRefs). `null` cannot be sent — it signals closure.
55
+ *
56
+ * @param capacity Buffer size. `0` (default) = unbuffered: each `send()` blocks until a `recv()` is ready.
57
+ *
58
+ * @example
59
+ * const ch = chan<string>(5) // buffered channel, capacity 5
60
+ * await ch.send('hello')
61
+ * const msg = await ch.recv() // 'hello'
62
+ * ch.close()
63
+ *
64
+ * @example
65
+ * // Fan-out: multiple workers pulling from the same channel
66
+ * const input = chan<Job>(50)
67
+ * const output = chan<Result>(50)
68
+ *
69
+ * for (let i = 0; i < 4; i++) {
70
+ * spawn(async ({ input, output }) => {
71
+ * for await (const job of input) {
72
+ * await output.send(processJob(job))
73
+ * }
74
+ * }, { channels: { input, output } })
75
+ * }
76
+ */
77
+ declare function chan<T extends ChannelValue>(capacity?: number): Channel<T>;
20
78
 
21
- declare function spawn<T>(fn: (() => T | Promise<T>) | ((channels: Record<string, Channel<unknown>>) => T | Promise<T>), opts?: {
79
+ /**
80
+ * Run a function in a worker thread. Returns a handle with the result promise and a cancel function.
81
+ *
82
+ * **Functions must be self-contained** — they are serialized via `.toString()` and sent to a
83
+ * worker thread, so they cannot capture variables from the enclosing scope. Define everything
84
+ * you need inside the function body, or use `task()` to pass arguments.
85
+ *
86
+ * **Two modes:**
87
+ * - Default (exclusive): the function gets a dedicated thread. Best for CPU-bound work (> 5ms).
88
+ * - `{ concurrent: true }`: many tasks share a thread's event loop. Best for async/I/O work.
89
+ *
90
+ * @example
91
+ * // CPU-bound work — define helpers inside the function body
92
+ * const { result } = spawn(() => {
93
+ * function fibonacci(n: number): number {
94
+ * if (n <= 1) return n
95
+ * return fibonacci(n - 1) + fibonacci(n - 2)
96
+ * }
97
+ * return fibonacci(40)
98
+ * })
99
+ * console.log(await result)
100
+ *
101
+ * @example
102
+ * // I/O-bound work — concurrent mode shares threads efficiently
103
+ * const { result } = spawn(() => fetch('https://api.example.com').then(r => r.json()), {
104
+ * concurrent: true,
105
+ * })
106
+ *
107
+ * @example
108
+ * // Cancel a long-running task
109
+ * const { result, cancel } = spawn(() => longRunningTask())
110
+ * setTimeout(cancel, 5000)
111
+ *
112
+ * @example
113
+ * // Cross-thread channels — pass channels via opts.channels
114
+ * const ch = chan<number>(10)
115
+ * spawn(async ({ ch }) => {
116
+ * for (let i = 0; i < 100; i++) await ch.send(i)
117
+ * ch.close()
118
+ * }, { channels: { ch } })
119
+ */
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?: {
22
121
  priority?: 'low' | 'normal' | 'high';
23
122
  concurrent?: boolean;
24
- channels?: Record<string, Channel<unknown>>;
123
+ channels?: TChannels;
25
124
  }): SpawnResult<T>;
26
125
 
27
- declare class WaitGroup {
126
+ type SpawnChannels$1 = Record<string, Channel<ChannelValue>>;
127
+ /**
128
+ * Structured concurrency: spawn multiple tasks and wait for all to complete.
129
+ *
130
+ * Like `Promise.all`, but tasks run in worker threads across CPU cores. Results are
131
+ * returned in the order tasks were spawned. A shared `AbortSignal` lets long-running
132
+ * tasks observe cooperative cancellation via `cancel()`.
133
+ *
134
+ * For fail-fast behavior (cancel all on first error), use `ErrGroup` instead.
135
+ *
136
+ * @example
137
+ * // CPU-bound parallel work
138
+ * const wg = new WaitGroup()
139
+ * wg.spawn(() => { /* define helpers inside — no closure captures *\/ })
140
+ * wg.spawn(() => { /* another CPU task *\/ })
141
+ * const [r1, r2] = await wg.wait()
142
+ *
143
+ * @example
144
+ * // Mixed CPU + I/O
145
+ * wg.spawn(() => crunchNumbers(), )
146
+ * wg.spawn(() => fetch('https://api.example.com').then(r => r.json()), { concurrent: true })
147
+ * const results = await wg.wait()
148
+ *
149
+ * @example
150
+ * // Tolerate partial failures with waitSettled
151
+ * const settled = await wg.waitSettled()
152
+ * for (const r of settled) {
153
+ * if (r.status === 'fulfilled') use(r.value)
154
+ * else console.error(r.reason)
155
+ * }
156
+ */
157
+ declare class WaitGroup<T extends StructuredCloneValue = StructuredCloneValue> {
28
158
  private tasks;
29
159
  private controller;
160
+ /**
161
+ * An `AbortSignal` shared across all tasks in this group.
162
+ * Pass it into spawned functions so they can stop early when `cancel()` is called.
163
+ */
30
164
  get signal(): AbortSignal;
31
- spawn(fn: (() => unknown) | ((channels: Record<string, Channel<unknown>>) => unknown), opts?: {
165
+ /**
166
+ * Spawns a function on a worker thread and adds it to the group.
167
+ *
168
+ * @throws If the group has already been cancelled.
169
+ */
170
+ spawn<TChannels extends SpawnChannels$1 = Record<never, never>>(fn: (() => T | Promise<T>) | ((channels: TChannels) => T | Promise<T>), opts?: {
32
171
  concurrent?: boolean;
33
- channels?: Record<string, Channel<unknown>>;
172
+ channels?: TChannels;
34
173
  }): void;
35
- wait(): Promise<unknown[]>;
36
- waitSettled(): Promise<PromiseSettledResult<unknown>[]>;
174
+ /**
175
+ * Waits for all tasks to complete successfully.
176
+ * Rejects as soon as any task throws.
177
+ */
178
+ wait(): Promise<T[]>;
179
+ /**
180
+ * Waits for all tasks to settle (fulfilled or rejected) and returns each outcome.
181
+ * Never rejects — inspect each `PromiseSettledResult` to handle failures individually.
182
+ */
183
+ waitSettled(): Promise<PromiseSettledResult<T>[]>;
184
+ /**
185
+ * Cancels all tasks in the group and signals the shared `AbortSignal`.
186
+ * Already-settled tasks are unaffected.
187
+ */
37
188
  cancel(): void;
38
189
  }
39
190
 
40
- declare class ErrGroup {
191
+ type SpawnChannels = Record<string, Channel<ChannelValue>>;
192
+ /**
193
+ * Like `WaitGroup`, but cancels all remaining tasks on the first error.
194
+ *
195
+ * Modeled after Go's `golang.org/x/sync/errgroup`. Use when partial results are useless —
196
+ * if any task fails, there is no point waiting for the rest. Benchmarks show ~3.7x faster
197
+ * failure handling than waiting for all tasks to settle.
198
+ *
199
+ * For "wait for everything regardless of failures", use `WaitGroup` with `waitSettled()`.
200
+ *
201
+ * @example
202
+ * const eg = new ErrGroup()
203
+ * eg.spawn(() => run('fetchUser', userId))
204
+ * eg.spawn(() => run('fetchOrders', userId))
205
+ * eg.spawn(() => run('fetchAnalytics', userId))
206
+ *
207
+ * try {
208
+ * const [user, orders, analytics] = await eg.wait()
209
+ * } catch (err) {
210
+ * // First failure cancelled the rest — no partial data to clean up
211
+ * }
212
+ *
213
+ * @example
214
+ * // Observe cancellation inside a task via the shared signal
215
+ * const eg = new ErrGroup()
216
+ * eg.spawn(() => {
217
+ * // eg.signal is not directly available inside the worker —
218
+ * // use task() with register() and check a channel or AbortSignal instead
219
+ * })
220
+ */
221
+ declare class ErrGroup<T extends StructuredCloneValue = StructuredCloneValue> {
41
222
  private tasks;
42
223
  private controller;
43
224
  private firstError;
44
225
  private hasError;
45
226
  get signal(): AbortSignal;
46
- spawn(fn: () => unknown, opts?: {
227
+ spawn<TChannels extends SpawnChannels = Record<never, never>>(fn: (() => T | Promise<T>) | ((channels: TChannels) => T | Promise<T>), opts?: {
47
228
  concurrent?: boolean;
229
+ channels?: TChannels;
48
230
  }): void;
49
- wait(): Promise<unknown[]>;
231
+ wait(): Promise<T[]>;
50
232
  cancel(): void;
51
233
  }
52
234
 
235
+ /**
236
+ * Async mutual exclusion. Serializes access to shared state under concurrency.
237
+ *
238
+ * Prefer `withLock()` over manual `lock()`/`unlock()` — it automatically releases
239
+ * the lock even if the callback throws.
240
+ *
241
+ * Note: `Mutex` operates on the main thread (or whichever thread creates it).
242
+ * Worker threads do not share memory, so this is not useful for cross-thread locking.
243
+ * For cross-thread coordination, use channels instead.
244
+ *
245
+ * @example
246
+ * const mu = new Mutex()
247
+ *
248
+ * // withLock — recommended (auto-unlocks on error)
249
+ * const result = await mu.withLock(async () => {
250
+ * const current = await db.get('counter')
251
+ * await db.set('counter', current + 1)
252
+ * return current + 1
253
+ * })
254
+ *
255
+ * @example
256
+ * // Manual lock/unlock (use withLock instead when possible)
257
+ * await mu.lock()
258
+ * try {
259
+ * // critical section
260
+ * } finally {
261
+ * mu.unlock()
262
+ * }
263
+ */
53
264
  declare class Mutex {
54
265
  private queue;
55
266
  private locked;
@@ -59,6 +270,30 @@ declare class Mutex {
59
270
  get isLocked(): boolean;
60
271
  }
61
272
 
273
+ /**
274
+ * Run a function exactly once, even if called concurrently.
275
+ * All callers await the same promise and receive the same result.
276
+ *
277
+ * Use for lazy, one-time initialization of expensive resources (DB pools, ML models,
278
+ * config, etc.) that must be initialized at most once regardless of concurrent demand.
279
+ *
280
+ * @example
281
+ * const initDB = new Once<DBPool>()
282
+ *
283
+ * async function getDB() {
284
+ * return initDB.do(() => createPool({ max: 10 }))
285
+ * }
286
+ *
287
+ * // Safe under concurrent load — pool is created exactly once
288
+ * const [db1, db2] = await Promise.all([getDB(), getDB()])
289
+ * // db1 === db2 (same pool instance)
290
+ *
291
+ * @example
292
+ * // Check if initialization has already run
293
+ * if (!initDB.done) {
294
+ * console.log('not yet initialized')
295
+ * }
296
+ */
62
297
  declare class Once<T = void> {
63
298
  private promise;
64
299
  private called;
@@ -67,14 +302,99 @@ declare class Once<T = void> {
67
302
  reset(): void;
68
303
  }
69
304
 
70
- type SelectCase<T = unknown> = [Promise<T>, (value: T) => void];
305
+ type SelectCase<T = StructuredCloneValue> = [Promise<T>, (value: T) => void];
306
+ /**
307
+ * Options for `select()`.
308
+ *
309
+ * `default` makes the call non-blocking: if no case is immediately ready,
310
+ * the default handler runs instead of waiting. This mirrors Go's `select { default: ... }`.
311
+ */
71
312
  interface SelectOptions {
72
313
  default?: () => void;
73
314
  }
315
+ /**
316
+ * Wait for the first of multiple promises to resolve, like Go's `select`.
317
+ *
318
+ * Each case is a `[promise, handler]` tuple. The handler for the first settled
319
+ * promise is called with its value. All other handlers are ignored.
320
+ *
321
+ * If `opts.default` is provided, `select` becomes non-blocking: if no promise
322
+ * is already resolved, the default runs immediately (Go's `select { default: ... }`).
323
+ *
324
+ * Commonly used with `ch.recv()`, `after()`, and `spawn().result`.
325
+ *
326
+ * @example
327
+ * // Block until a channel message arrives or timeout after 5s
328
+ * await select([
329
+ * [ch.recv(), (value) => console.log('received', value)],
330
+ * [after(5000), () => console.log('timed out')],
331
+ * ])
332
+ *
333
+ * @example
334
+ * // Non-blocking: check a channel without waiting
335
+ * await select(
336
+ * [[ch.recv(), (value) => process(value)]],
337
+ * { default: () => console.log('channel empty — doing other work') },
338
+ * )
339
+ *
340
+ * @example
341
+ * // Race two worker results against a deadline
342
+ * const { result: fast } = spawn(() => quickSearch(query))
343
+ * const { result: deep } = spawn(() => deepSearch(query))
344
+ *
345
+ * let response: Result
346
+ * await select([
347
+ * [fast, (r) => { response = r }],
348
+ * [after(200), () => { response = { partial: true } }],
349
+ * ])
350
+ */
74
351
  declare function select(cases: SelectCase[], opts?: SelectOptions): Promise<void>;
75
352
 
353
+ /**
354
+ * Returns a promise that resolves after `ms` milliseconds.
355
+ *
356
+ * Designed for use with `select()` to add timeouts to channel operations or
357
+ * race a deadline against worker results. Also works as a simple async delay.
358
+ *
359
+ * @example
360
+ * // Timeout a channel receive after 2 seconds
361
+ * await select([
362
+ * [ch.recv(), (value) => handle(value)],
363
+ * [after(2000), () => handleTimeout()],
364
+ * ])
365
+ *
366
+ * @example
367
+ * // Simple delay
368
+ * await after(500)
369
+ * console.log('500ms later')
370
+ */
76
371
  declare function after(ms: number): Promise<void>;
77
372
 
373
+ /**
374
+ * A repeating timer that ticks at a fixed interval.
375
+ *
376
+ * Implements `AsyncIterable<void>` — use `for await...of` to run work on each tick.
377
+ * Call `stop()` to cancel the ticker and end the iteration.
378
+ *
379
+ * Create with the `ticker(ms)` factory function.
380
+ *
381
+ * @example
382
+ * const t = ticker(1000) // tick every second
383
+ * for await (const _ of t) {
384
+ * await doWork()
385
+ * if (shouldStop) t.stop() // ends the for-await loop
386
+ * }
387
+ *
388
+ * @example
389
+ * // Use with select() to process work on each tick with a timeout
390
+ * const t = ticker(5000)
391
+ * for await (const _ of t) {
392
+ * await select([
393
+ * [spawn(() => checkHealth()).result, (ok) => report(ok)],
394
+ * [after(4000), () => report('timeout')],
395
+ * ])
396
+ * }
397
+ */
78
398
  declare class Ticker {
79
399
  private interval;
80
400
  private resolve;
@@ -85,12 +405,59 @@ declare class Ticker {
85
405
  stop(): void;
86
406
  [Symbol.asyncIterator](): AsyncIterator<void>;
87
407
  }
408
+ /**
409
+ * Create a `Ticker` that fires every `ms` milliseconds.
410
+ *
411
+ * @example
412
+ * const t = ticker(500)
413
+ * for await (const _ of t) {
414
+ * console.log('tick')
415
+ * if (done) t.stop()
416
+ * }
417
+ */
88
418
  declare function ticker(ms: number): Ticker;
89
419
 
90
- type TaskFn = (...args: unknown[]) => unknown;
91
- declare function register(name: string, fn: TaskFn): void;
92
- declare function run<T = unknown>(name: string, ...args: unknown[]): Promise<T>;
420
+ /**
421
+ * Define a reusable task that runs in a worker thread.
422
+ *
423
+ * Returns a typed async function — call it like a regular async function,
424
+ * and it dispatches to the thread pool each time.
425
+ *
426
+ * Use task() when you have the same function to call many times with
427
+ * different arguments. For one-off work, use spawn() instead.
428
+ *
429
+ * Arguments must be JSON-serializable (no functions, symbols, undefined, or BigInt).
430
+ * The function itself must be self-contained — it cannot capture enclosing scope variables.
431
+ *
432
+ * @example
433
+ * const resizeImage = task((src: string, width: number, height: number) => {
434
+ * // runs in a worker thread
435
+ * return processPixels(src, width, height)
436
+ * })
437
+ *
438
+ * const result = await resizeImage('photo.jpg', 800, 600)
439
+ * const [a, b] = await Promise.all([resizeImage('a.jpg', 400, 300), resizeImage('b.jpg', 800, 600)])
440
+ */
441
+ declare function task<TArgs extends JsonValue[], TReturn extends StructuredCloneValue>(fn: (...args: TArgs) => TReturn | Promise<TReturn>): (...args: TArgs) => Promise<TReturn>;
93
442
 
443
+ /**
444
+ * Configure the global thread pool. **Must be called before the first `spawn()`.**
445
+ *
446
+ * After the pool is initialized, calling `configure()` throws. Call it once at
447
+ * application startup or in test setup.
448
+ *
449
+ * @example
450
+ * configure({
451
+ * maxThreads: 4, // default: os.availableParallelism()
452
+ * concurrency: 64, // max concurrent tasks per shared worker (default: 64)
453
+ * idleTimeout: 30_000, // kill idle workers after 30s (default: 30_000)
454
+ * adapter: 'auto', // 'auto' | 'node' | 'bun' | 'inline' (default: 'auto')
455
+ * })
456
+ *
457
+ * @example
458
+ * // In tests: run tasks on the main thread with no real workers
459
+ * configure({ adapter: 'inline' })
460
+ */
94
461
  declare function configure(opts: Partial<PuruConfig>): void;
95
462
 
96
463
  interface PoolStats {
@@ -119,10 +486,24 @@ interface PoolStats {
119
486
  }
120
487
  declare function stats(): PoolStats;
121
488
  declare function resize(maxThreads: number): void;
489
+ /**
490
+ * Gracefully shut down the thread pool.
491
+ *
492
+ * Rejects all queued tasks, waits for all workers to terminate, then clears
493
+ * the pool. Safe to call at process exit or at the end of a test suite.
494
+ *
495
+ * ```ts
496
+ * process.on('SIGTERM', async () => {
497
+ * await shutdown()
498
+ * process.exit(0)
499
+ * })
500
+ * ```
501
+ */
502
+ declare function shutdown(): Promise<void>;
122
503
 
123
504
  type Runtime = 'node' | 'deno' | 'bun' | 'browser';
124
505
  type Capability = 'full-threads' | 'single-thread';
125
506
  declare function detectRuntime(): Runtime;
126
507
  declare function detectCapability(): Capability;
127
508
 
128
- export { type Capability, type Channel, ErrGroup, Mutex, Once, type PoolStats, type PuruConfig, type Runtime, type SelectOptions, type SpawnResult, Ticker, WaitGroup, after, chan, configure, detectCapability, detectRuntime, register, resize, run, select, spawn, stats, ticker };
509
+ export { type Capability, type Channel, ErrGroup, Mutex, Once, type PoolStats, type PuruConfig, type Runtime, type SelectOptions, type SpawnResult, Ticker, WaitGroup, after, chan, configure, detectCapability, detectRuntime, resize, select, shutdown, spawn, stats, task, ticker };