@dmop/puru 0.1.5 → 0.1.11

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';
@@ -30,11 +39,33 @@ 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>;
37
46
  close(): void;
47
+ /** Number of values currently buffered. Like Go's `len(ch)`. */
48
+ readonly len: number;
49
+ /** Buffer capacity. Like Go's `cap(ch)`. */
50
+ readonly cap: number;
51
+ /** Returns a send-only view of this channel. Like Go's `chan<- T`. */
52
+ sendOnly(): SendOnly<T>;
53
+ /** Returns a receive-only view of this channel. Like Go's `<-chan T`. */
54
+ recvOnly(): RecvOnly<T>;
55
+ [Symbol.asyncIterator](): AsyncIterator<T>;
56
+ }
57
+ /** Send-only view of a channel. Like Go's `chan<- T`. */
58
+ interface SendOnly<T extends ChannelValue> {
59
+ send(value: T): Promise<void>;
60
+ close(): void;
61
+ readonly len: number;
62
+ readonly cap: number;
63
+ }
64
+ /** Receive-only view of a channel. Like Go's `<-chan T`. */
65
+ interface RecvOnly<T extends ChannelValue> {
66
+ recv(): Promise<T | null>;
67
+ readonly len: number;
68
+ readonly cap: number;
38
69
  [Symbol.asyncIterator](): AsyncIterator<T>;
39
70
  }
40
71
  /**
@@ -65,7 +96,80 @@ interface Channel<T> {
65
96
  * }, { channels: { input, output } })
66
97
  * }
67
98
  */
68
- declare function chan<T extends NonNullable<unknown>>(capacity?: number): Channel<T>;
99
+ declare function chan<T extends ChannelValue>(capacity?: number): Channel<T>;
100
+
101
+ /**
102
+ * Hierarchical cancellation, deadlines, and request-scoped values — modeled after Go's `context` package.
103
+ *
104
+ * Context is the glue that makes cancellation and timeouts composable. Derive child
105
+ * contexts from a parent: when the parent is cancelled, all children cancel automatically.
106
+ *
107
+ * @example
108
+ * // Timeout a group of tasks
109
+ * const [ctx, cancel] = withTimeout(background(), 5000)
110
+ * const wg = new WaitGroup()
111
+ * wg.spawn(() => longRunningWork())
112
+ * ctx.done().then(() => wg.cancel())
113
+ *
114
+ * @example
115
+ * // Nested deadlines
116
+ * const [parent, cancelParent] = withCancel(background())
117
+ * const [child, _] = withTimeout(parent, 1000)
118
+ * // child cancels after 1s OR when cancelParent() is called — whichever comes first
119
+ *
120
+ * @example
121
+ * // Request-scoped values
122
+ * const reqCtx = withValue(background(), 'requestId', 'abc-123')
123
+ * reqCtx.value('requestId') // 'abc-123'
124
+ */
125
+ /** Returned by `ctx.err` when the context was explicitly cancelled. */
126
+ declare class CancelledError extends Error {
127
+ constructor(message?: string);
128
+ }
129
+ /** Returned by `ctx.err` when the context's deadline has passed. */
130
+ declare class DeadlineExceededError extends Error {
131
+ constructor();
132
+ }
133
+ type ContextError = CancelledError | DeadlineExceededError;
134
+ interface Context {
135
+ /** AbortSignal that fires when this context is cancelled or its deadline expires. */
136
+ readonly signal: AbortSignal;
137
+ /** The deadline for this context, or `null` if none was set. */
138
+ readonly deadline: Date | null;
139
+ /** The cancellation error, or `null` if the context is still active. */
140
+ readonly err: ContextError | null;
141
+ /** Retrieves a value stored in this context or any of its ancestors. */
142
+ value<T = unknown>(key: symbol | string): T | undefined;
143
+ /** Returns a promise that resolves when the context is cancelled. */
144
+ done(): Promise<void>;
145
+ }
146
+ type CancelFunc = (reason?: string) => void;
147
+ /**
148
+ * Returns the root context. It is never cancelled, has no deadline, and carries no values.
149
+ * All other contexts should derive from this.
150
+ */
151
+ declare function background(): Context;
152
+ /**
153
+ * Returns a child context and a cancel function. Calling `cancel()` cancels the child
154
+ * and all contexts derived from it. The child also cancels when the parent does.
155
+ */
156
+ declare function withCancel(parent: Context): [Context, CancelFunc];
157
+ /**
158
+ * Returns a child context that automatically cancels at the given `deadline`.
159
+ * If the parent has an earlier deadline, that deadline is inherited.
160
+ * The returned cancel function can cancel early and clears the timer.
161
+ */
162
+ declare function withDeadline(parent: Context, deadline: Date): [Context, CancelFunc];
163
+ /**
164
+ * Returns a child context that automatically cancels after `ms` milliseconds.
165
+ * Equivalent to `withDeadline(parent, new Date(Date.now() + ms))`.
166
+ */
167
+ declare function withTimeout(parent: Context, ms: number): [Context, CancelFunc];
168
+ /**
169
+ * Returns a child context carrying a key-value pair.
170
+ * Values are retrieved with `ctx.value(key)` and looked up through the ancestor chain.
171
+ */
172
+ declare function withValue<T = unknown>(parent: Context, key: symbol | string, value: T): Context;
69
173
 
70
174
  /**
71
175
  * Run a function in a worker thread. Returns a handle with the result promise and a cancel function.
@@ -108,12 +212,14 @@ declare function chan<T extends NonNullable<unknown>>(capacity?: number): Channe
108
212
  * ch.close()
109
213
  * }, { channels: { ch } })
110
214
  */
111
- declare function spawn<T>(fn: (() => T | Promise<T>) | ((channels: Record<string, Channel<unknown>>) => T | Promise<T>), opts?: {
215
+ 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
216
  priority?: 'low' | 'normal' | 'high';
113
217
  concurrent?: boolean;
114
- channels?: Record<string, Channel<unknown>>;
218
+ channels?: TChannels;
219
+ ctx?: Context;
115
220
  }): SpawnResult<T>;
116
221
 
222
+ type SpawnChannels$1 = Record<string, Channel<ChannelValue>>;
117
223
  /**
118
224
  * Structured concurrency: spawn multiple tasks and wait for all to complete.
119
225
  *
@@ -144,9 +250,11 @@ declare function spawn<T>(fn: (() => T | Promise<T>) | ((channels: Record<string
144
250
  * else console.error(r.reason)
145
251
  * }
146
252
  */
147
- declare class WaitGroup {
253
+ declare class WaitGroup<T extends StructuredCloneValue = StructuredCloneValue> {
148
254
  private tasks;
149
255
  private controller;
256
+ private ctx?;
257
+ constructor(ctx?: Context);
150
258
  /**
151
259
  * An `AbortSignal` shared across all tasks in this group.
152
260
  * Pass it into spawned functions so they can stop early when `cancel()` is called.
@@ -157,20 +265,20 @@ declare class WaitGroup {
157
265
  *
158
266
  * @throws If the group has already been cancelled.
159
267
  */
160
- spawn(fn: (() => unknown) | ((channels: Record<string, Channel<unknown>>) => unknown), opts?: {
268
+ spawn<TChannels extends SpawnChannels$1 = Record<never, never>>(fn: (() => T | Promise<T>) | ((channels: TChannels) => T | Promise<T>), opts?: {
161
269
  concurrent?: boolean;
162
- channels?: Record<string, Channel<unknown>>;
270
+ channels?: TChannels;
163
271
  }): void;
164
272
  /**
165
273
  * Waits for all tasks to complete successfully.
166
274
  * Rejects as soon as any task throws.
167
275
  */
168
- wait(): Promise<unknown[]>;
276
+ wait(): Promise<T[]>;
169
277
  /**
170
278
  * Waits for all tasks to settle (fulfilled or rejected) and returns each outcome.
171
279
  * Never rejects — inspect each `PromiseSettledResult` to handle failures individually.
172
280
  */
173
- waitSettled(): Promise<PromiseSettledResult<unknown>[]>;
281
+ waitSettled(): Promise<PromiseSettledResult<T>[]>;
174
282
  /**
175
283
  * Cancels all tasks in the group and signals the shared `AbortSignal`.
176
284
  * Already-settled tasks are unaffected.
@@ -178,6 +286,7 @@ declare class WaitGroup {
178
286
  cancel(): void;
179
287
  }
180
288
 
289
+ type SpawnChannels = Record<string, Channel<ChannelValue>>;
181
290
  /**
182
291
  * Like `WaitGroup`, but cancels all remaining tasks on the first error.
183
292
  *
@@ -207,17 +316,29 @@ declare class WaitGroup {
207
316
  * // use task() with register() and check a channel or AbortSignal instead
208
317
  * })
209
318
  */
210
- declare class ErrGroup {
319
+ declare class ErrGroup<T extends StructuredCloneValue = StructuredCloneValue> {
211
320
  private tasks;
212
321
  private controller;
213
322
  private firstError;
214
323
  private hasError;
324
+ private ctx?;
325
+ private limit;
326
+ private inFlight;
327
+ private waiting;
328
+ constructor(ctx?: Context);
215
329
  get signal(): AbortSignal;
216
- spawn(fn: (() => unknown) | ((channels: Record<string, Channel<unknown>>) => unknown), opts?: {
330
+ /**
331
+ * Set the maximum number of tasks that can run concurrently.
332
+ * Like Go's `errgroup.SetLimit()`. Must be called before any `spawn()`.
333
+ * A value of 0 (default) means unlimited.
334
+ */
335
+ setLimit(n: number): void;
336
+ spawn<TChannels extends SpawnChannels = Record<never, never>>(fn: (() => T | Promise<T>) | ((channels: TChannels) => T | Promise<T>), opts?: {
217
337
  concurrent?: boolean;
218
- channels?: Record<string, Channel<unknown>>;
338
+ channels?: TChannels;
219
339
  }): void;
220
- wait(): Promise<unknown[]>;
340
+ private doSpawn;
341
+ wait(): Promise<T[]>;
221
342
  cancel(): void;
222
343
  }
223
344
 
@@ -258,6 +379,98 @@ declare class Mutex {
258
379
  withLock<T>(fn: () => T | Promise<T>): Promise<T>;
259
380
  get isLocked(): boolean;
260
381
  }
382
+ /**
383
+ * Async read-write mutex. Multiple readers can hold the lock simultaneously,
384
+ * but writers get exclusive access. Modeled after Go's `sync.RWMutex`.
385
+ *
386
+ * Use this instead of `Mutex` when reads vastly outnumber writes — concurrent
387
+ * readers improve throughput without sacrificing write safety.
388
+ *
389
+ * Like `Mutex`, this operates on the main thread only. For cross-thread
390
+ * coordination, use channels instead.
391
+ *
392
+ * @example
393
+ * const rw = new RWMutex()
394
+ *
395
+ * // Multiple readers can run concurrently
396
+ * const data = await rw.withRLock(async () => {
397
+ * return await db.get('config')
398
+ * })
399
+ *
400
+ * // Writers get exclusive access
401
+ * await rw.withLock(async () => {
402
+ * await db.set('config', newValue)
403
+ * })
404
+ */
405
+ declare class RWMutex {
406
+ private readers;
407
+ private writing;
408
+ private readQueue;
409
+ private writeQueue;
410
+ rLock(): Promise<void>;
411
+ rUnlock(): void;
412
+ lock(): Promise<void>;
413
+ unlock(): void;
414
+ withRLock<T>(fn: () => T | Promise<T>): Promise<T>;
415
+ withLock<T>(fn: () => T | Promise<T>): Promise<T>;
416
+ get isLocked(): boolean;
417
+ private wakeReaders;
418
+ private wakeWriter;
419
+ }
420
+
421
+ /**
422
+ * Condition variable for async coordination. Modeled after Go's `sync.Cond`.
423
+ *
424
+ * A `Cond` is associated with a `Mutex`. Goroutines (async tasks) can:
425
+ * - `wait()` — atomically release the lock and suspend until signaled, then re-acquire the lock
426
+ * - `signal()` — wake one waiting task
427
+ * - `broadcast()` — wake all waiting tasks
428
+ *
429
+ * Always check the condition in a loop — spurious wakeups are possible if
430
+ * multiple waiters compete for the lock after `broadcast()`.
431
+ *
432
+ * @example
433
+ * const mu = new Mutex()
434
+ * const cond = new Cond(mu)
435
+ * let ready = false
436
+ *
437
+ * // Waiter
438
+ * await mu.lock()
439
+ * while (!ready) {
440
+ * await cond.wait()
441
+ * }
442
+ * console.log('ready!')
443
+ * mu.unlock()
444
+ *
445
+ * // Signaler (from another async context)
446
+ * await mu.lock()
447
+ * ready = true
448
+ * cond.signal()
449
+ * mu.unlock()
450
+ *
451
+ * @example
452
+ * // Broadcast to wake all waiters
453
+ * await mu.lock()
454
+ * ready = true
455
+ * cond.broadcast()
456
+ * mu.unlock()
457
+ */
458
+ declare class Cond {
459
+ private mu;
460
+ private waiters;
461
+ constructor(mu: Mutex);
462
+ /**
463
+ * Atomically releases the mutex, suspends the caller until `signal()` or `broadcast()`
464
+ * is called, then re-acquires the mutex before returning.
465
+ *
466
+ * Must be called while holding the mutex.
467
+ */
468
+ wait(): Promise<void>;
469
+ /** Wake one waiting task (if any). */
470
+ signal(): void;
471
+ /** Wake all waiting tasks. */
472
+ broadcast(): void;
473
+ }
261
474
 
262
475
  /**
263
476
  * Run a function exactly once, even if called concurrently.
@@ -291,7 +504,9 @@ declare class Once<T = void> {
291
504
  reset(): void;
292
505
  }
293
506
 
294
- type SelectCase<T = unknown> = [Promise<T>, (value: T) => void];
507
+ type RecvCase<T = StructuredCloneValue> = [Promise<T>, (value: T) => void];
508
+ type SendCase = [Promise<void>, () => void];
509
+ type SelectCase<T = StructuredCloneValue> = RecvCase<T> | SendCase;
295
510
  /**
296
511
  * Options for `select()`.
297
512
  *
@@ -307,10 +522,13 @@ interface SelectOptions {
307
522
  * Each case is a `[promise, handler]` tuple. The handler for the first settled
308
523
  * promise is called with its value. All other handlers are ignored.
309
524
  *
525
+ * **Recv cases:** `[ch.recv(), (value) => ...]` — handler receives the value.
526
+ * **Send cases:** `[ch.send(value), () => ...]` — handler is called when the send completes.
527
+ *
310
528
  * If `opts.default` is provided, `select` becomes non-blocking: if no promise
311
529
  * is already resolved, the default runs immediately (Go's `select { default: ... }`).
312
530
  *
313
- * Commonly used with `ch.recv()`, `after()`, and `spawn().result`.
531
+ * Commonly used with `ch.recv()`, `ch.send()`, `after()`, and `spawn().result`.
314
532
  *
315
533
  * @example
316
534
  * // Block until a channel message arrives or timeout after 5s
@@ -327,6 +545,13 @@ interface SelectOptions {
327
545
  * )
328
546
  *
329
547
  * @example
548
+ * // Select with send case — try to send or timeout
549
+ * await select([
550
+ * [ch.send(value), () => console.log('sent!')],
551
+ * [after(1000), () => console.log('send timed out')],
552
+ * ])
553
+ *
554
+ * @example
330
555
  * // Race two worker results against a deadline
331
556
  * const { result: fast } = spawn(() => quickSearch(query))
332
557
  * const { result: deep } = spawn(() => deepSearch(query))
@@ -406,6 +631,54 @@ declare class Ticker {
406
631
  */
407
632
  declare function ticker(ms: number): Ticker;
408
633
 
634
+ /**
635
+ * A one-shot timer that can be stopped and reset. Like Go's `time.Timer`.
636
+ *
637
+ * Unlike `after()` which is fire-and-forget, `Timer` gives you control:
638
+ * - `stop()` cancels a pending timer
639
+ * - `reset(ms)` reschedules without allocating a new object
640
+ *
641
+ * The `channel` property is a promise that resolves when the timer fires.
642
+ * After `stop()`, the promise never resolves. After `reset()`, a new `channel`
643
+ * promise is created.
644
+ *
645
+ * @example
646
+ * // Basic timeout with ability to cancel
647
+ * const t = new Timer(5000)
648
+ * await select([
649
+ * [ch.recv(), (v) => { t.stop(); handle(v) }],
650
+ * [t.channel, () => console.log('timed out')],
651
+ * ])
652
+ *
653
+ * @example
654
+ * // Reset a timer (e.g., debounce pattern)
655
+ * const t = new Timer(300)
656
+ * onInput(() => t.reset(300))
657
+ * await t.channel // fires 300ms after last input
658
+ */
659
+ declare class Timer {
660
+ private timer;
661
+ private _stopped;
662
+ /** Promise that resolves when the timer fires. Replaced on `reset()`. */
663
+ channel: Promise<void>;
664
+ constructor(ms: number);
665
+ private schedule;
666
+ /**
667
+ * Stop the timer. Returns `true` if the timer was pending (stopped before firing),
668
+ * `false` if it had already fired or was already stopped.
669
+ *
670
+ * After stopping, the current `channel` promise will never resolve.
671
+ */
672
+ stop(): boolean;
673
+ /**
674
+ * Reset the timer to fire after `ms` milliseconds.
675
+ * If the timer was pending, it is stopped first. Creates a new `channel` promise.
676
+ */
677
+ reset(ms: number): void;
678
+ /** Whether the timer has fired or been stopped. */
679
+ get stopped(): boolean;
680
+ }
681
+
409
682
  /**
410
683
  * Define a reusable task that runs in a worker thread.
411
684
  *
@@ -427,7 +700,7 @@ declare function ticker(ms: number): Ticker;
427
700
  * const result = await resizeImage('photo.jpg', 800, 600)
428
701
  * const [a, b] = await Promise.all([resizeImage('a.jpg', 400, 300), resizeImage('b.jpg', 800, 600)])
429
702
  */
430
- declare function task<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn | Promise<TReturn>): (...args: TArgs) => Promise<TReturn>;
703
+ declare function task<TArgs extends JsonValue[], TReturn extends StructuredCloneValue>(fn: (...args: TArgs) => TReturn | Promise<TReturn>): (...args: TArgs) => Promise<TReturn>;
431
704
 
432
705
  /**
433
706
  * Configure the global thread pool. **Must be called before the first `spawn()`.**
@@ -495,4 +768,4 @@ type Capability = 'full-threads' | 'single-thread';
495
768
  declare function detectRuntime(): Runtime;
496
769
  declare function detectCapability(): Capability;
497
770
 
498
- 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 };
771
+ export { type CancelFunc, CancelledError, type Capability, type Channel, Cond, type Context, type ContextError, DeadlineExceededError, ErrGroup, Mutex, Once, type PoolStats, type PuruConfig, RWMutex, type RecvOnly, type Runtime, type SelectOptions, type SendOnly, type SpawnResult, Ticker, Timer, WaitGroup, after, background, chan, configure, detectCapability, detectRuntime, resize, select, shutdown, spawn, stats, task, ticker, withCancel, withDeadline, withTimeout, withValue };