@dmop/puru 0.1.10 → 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
@@ -44,6 +44,28 @@ interface Channel<T extends ChannelValue> {
44
44
  /** Resolves with the next value, or `null` if the channel is closed. */
45
45
  recv(): Promise<T | null>;
46
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;
47
69
  [Symbol.asyncIterator](): AsyncIterator<T>;
48
70
  }
49
71
  /**
@@ -76,6 +98,79 @@ interface Channel<T extends ChannelValue> {
76
98
  */
77
99
  declare function chan<T extends ChannelValue>(capacity?: number): Channel<T>;
78
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;
173
+
79
174
  /**
80
175
  * Run a function in a worker thread. Returns a handle with the result promise and a cancel function.
81
176
  *
@@ -121,6 +216,7 @@ declare function spawn<T extends StructuredCloneValue, TChannels extends Record<
121
216
  priority?: 'low' | 'normal' | 'high';
122
217
  concurrent?: boolean;
123
218
  channels?: TChannels;
219
+ ctx?: Context;
124
220
  }): SpawnResult<T>;
125
221
 
126
222
  type SpawnChannels$1 = Record<string, Channel<ChannelValue>>;
@@ -157,6 +253,8 @@ type SpawnChannels$1 = Record<string, Channel<ChannelValue>>;
157
253
  declare class WaitGroup<T extends StructuredCloneValue = StructuredCloneValue> {
158
254
  private tasks;
159
255
  private controller;
256
+ private ctx?;
257
+ constructor(ctx?: Context);
160
258
  /**
161
259
  * An `AbortSignal` shared across all tasks in this group.
162
260
  * Pass it into spawned functions so they can stop early when `cancel()` is called.
@@ -223,11 +321,23 @@ declare class ErrGroup<T extends StructuredCloneValue = StructuredCloneValue> {
223
321
  private controller;
224
322
  private firstError;
225
323
  private hasError;
324
+ private ctx?;
325
+ private limit;
326
+ private inFlight;
327
+ private waiting;
328
+ constructor(ctx?: Context);
226
329
  get signal(): AbortSignal;
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;
227
336
  spawn<TChannels extends SpawnChannels = Record<never, never>>(fn: (() => T | Promise<T>) | ((channels: TChannels) => T | Promise<T>), opts?: {
228
337
  concurrent?: boolean;
229
338
  channels?: TChannels;
230
339
  }): void;
340
+ private doSpawn;
231
341
  wait(): Promise<T[]>;
232
342
  cancel(): void;
233
343
  }
@@ -269,6 +379,98 @@ declare class Mutex {
269
379
  withLock<T>(fn: () => T | Promise<T>): Promise<T>;
270
380
  get isLocked(): boolean;
271
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
+ }
272
474
 
273
475
  /**
274
476
  * Run a function exactly once, even if called concurrently.
@@ -302,7 +504,9 @@ declare class Once<T = void> {
302
504
  reset(): void;
303
505
  }
304
506
 
305
- type SelectCase<T = StructuredCloneValue> = [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;
306
510
  /**
307
511
  * Options for `select()`.
308
512
  *
@@ -318,10 +522,13 @@ interface SelectOptions {
318
522
  * Each case is a `[promise, handler]` tuple. The handler for the first settled
319
523
  * promise is called with its value. All other handlers are ignored.
320
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
+ *
321
528
  * If `opts.default` is provided, `select` becomes non-blocking: if no promise
322
529
  * is already resolved, the default runs immediately (Go's `select { default: ... }`).
323
530
  *
324
- * Commonly used with `ch.recv()`, `after()`, and `spawn().result`.
531
+ * Commonly used with `ch.recv()`, `ch.send()`, `after()`, and `spawn().result`.
325
532
  *
326
533
  * @example
327
534
  * // Block until a channel message arrives or timeout after 5s
@@ -338,6 +545,13 @@ interface SelectOptions {
338
545
  * )
339
546
  *
340
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
341
555
  * // Race two worker results against a deadline
342
556
  * const { result: fast } = spawn(() => quickSearch(query))
343
557
  * const { result: deep } = spawn(() => deepSearch(query))
@@ -417,6 +631,54 @@ declare class Ticker {
417
631
  */
418
632
  declare function ticker(ms: number): Ticker;
419
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
+
420
682
  /**
421
683
  * Define a reusable task that runs in a worker thread.
422
684
  *
@@ -506,4 +768,4 @@ type Capability = 'full-threads' | 'single-thread';
506
768
  declare function detectRuntime(): Runtime;
507
769
  declare function detectCapability(): Capability;
508
770
 
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 };
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 };
package/dist/index.d.ts CHANGED
@@ -44,6 +44,28 @@ interface Channel<T extends ChannelValue> {
44
44
  /** Resolves with the next value, or `null` if the channel is closed. */
45
45
  recv(): Promise<T | null>;
46
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;
47
69
  [Symbol.asyncIterator](): AsyncIterator<T>;
48
70
  }
49
71
  /**
@@ -76,6 +98,79 @@ interface Channel<T extends ChannelValue> {
76
98
  */
77
99
  declare function chan<T extends ChannelValue>(capacity?: number): Channel<T>;
78
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;
173
+
79
174
  /**
80
175
  * Run a function in a worker thread. Returns a handle with the result promise and a cancel function.
81
176
  *
@@ -121,6 +216,7 @@ declare function spawn<T extends StructuredCloneValue, TChannels extends Record<
121
216
  priority?: 'low' | 'normal' | 'high';
122
217
  concurrent?: boolean;
123
218
  channels?: TChannels;
219
+ ctx?: Context;
124
220
  }): SpawnResult<T>;
125
221
 
126
222
  type SpawnChannels$1 = Record<string, Channel<ChannelValue>>;
@@ -157,6 +253,8 @@ type SpawnChannels$1 = Record<string, Channel<ChannelValue>>;
157
253
  declare class WaitGroup<T extends StructuredCloneValue = StructuredCloneValue> {
158
254
  private tasks;
159
255
  private controller;
256
+ private ctx?;
257
+ constructor(ctx?: Context);
160
258
  /**
161
259
  * An `AbortSignal` shared across all tasks in this group.
162
260
  * Pass it into spawned functions so they can stop early when `cancel()` is called.
@@ -223,11 +321,23 @@ declare class ErrGroup<T extends StructuredCloneValue = StructuredCloneValue> {
223
321
  private controller;
224
322
  private firstError;
225
323
  private hasError;
324
+ private ctx?;
325
+ private limit;
326
+ private inFlight;
327
+ private waiting;
328
+ constructor(ctx?: Context);
226
329
  get signal(): AbortSignal;
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;
227
336
  spawn<TChannels extends SpawnChannels = Record<never, never>>(fn: (() => T | Promise<T>) | ((channels: TChannels) => T | Promise<T>), opts?: {
228
337
  concurrent?: boolean;
229
338
  channels?: TChannels;
230
339
  }): void;
340
+ private doSpawn;
231
341
  wait(): Promise<T[]>;
232
342
  cancel(): void;
233
343
  }
@@ -269,6 +379,98 @@ declare class Mutex {
269
379
  withLock<T>(fn: () => T | Promise<T>): Promise<T>;
270
380
  get isLocked(): boolean;
271
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
+ }
272
474
 
273
475
  /**
274
476
  * Run a function exactly once, even if called concurrently.
@@ -302,7 +504,9 @@ declare class Once<T = void> {
302
504
  reset(): void;
303
505
  }
304
506
 
305
- type SelectCase<T = StructuredCloneValue> = [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;
306
510
  /**
307
511
  * Options for `select()`.
308
512
  *
@@ -318,10 +522,13 @@ interface SelectOptions {
318
522
  * Each case is a `[promise, handler]` tuple. The handler for the first settled
319
523
  * promise is called with its value. All other handlers are ignored.
320
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
+ *
321
528
  * If `opts.default` is provided, `select` becomes non-blocking: if no promise
322
529
  * is already resolved, the default runs immediately (Go's `select { default: ... }`).
323
530
  *
324
- * Commonly used with `ch.recv()`, `after()`, and `spawn().result`.
531
+ * Commonly used with `ch.recv()`, `ch.send()`, `after()`, and `spawn().result`.
325
532
  *
326
533
  * @example
327
534
  * // Block until a channel message arrives or timeout after 5s
@@ -338,6 +545,13 @@ interface SelectOptions {
338
545
  * )
339
546
  *
340
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
341
555
  * // Race two worker results against a deadline
342
556
  * const { result: fast } = spawn(() => quickSearch(query))
343
557
  * const { result: deep } = spawn(() => deepSearch(query))
@@ -417,6 +631,54 @@ declare class Ticker {
417
631
  */
418
632
  declare function ticker(ms: number): Ticker;
419
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
+
420
682
  /**
421
683
  * Define a reusable task that runs in a worker thread.
422
684
  *
@@ -506,4 +768,4 @@ type Capability = 'full-threads' | 'single-thread';
506
768
  declare function detectRuntime(): Runtime;
507
769
  declare function detectCapability(): Capability;
508
770
 
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 };
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 };