@dmop/puru 0.1.10 → 0.1.12

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/llms-full.txt CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > A thread pool with Go-style concurrency primitives for JavaScript
4
4
 
5
- puru manages a pool of worker threads and provides a simple API to run functions off the main thread. No separate worker files, no manual message passing.
5
+ puru manages a pool of worker threads and provides a simple API to run functions off the main thread. Zero runtime dependencies. No separate worker files, no manual message passing.
6
6
 
7
7
  Works on Node.js and Bun. Deno support coming soon.
8
8
 
@@ -16,7 +16,7 @@ bun add @dmop/puru
16
16
  ## Quick Start
17
17
 
18
18
  ```typescript
19
- import { spawn, chan, WaitGroup, select, after } from '@dmop/puru'
19
+ import { spawn, chan, WaitGroup, ErrGroup, select, after, Timer, Mutex, RWMutex, Cond, background, withTimeout } from '@dmop/puru'
20
20
 
21
21
  // CPU work — runs in a dedicated worker thread
22
22
  const { result } = spawn(() => fibonacci(40))
@@ -66,10 +66,19 @@ Options:
66
66
  - priority: 'low' | 'normal' | 'high' (default: 'normal')
67
67
  - concurrent: boolean (default: false)
68
68
  - channels: Record<string, Channel<unknown>> — pass channels to the worker
69
+ - ctx: Context — auto-cancel the task when the context is cancelled
69
70
 
70
71
  Exclusive mode (default): the function gets a dedicated thread. Use for CPU-heavy work.
71
72
  Concurrent mode ({ concurrent: true }): multiple tasks share a thread's event loop. Use for async/I/O work.
72
73
 
74
+ Context integration — tasks auto-cancel when the context expires:
75
+
76
+ ```typescript
77
+ const [ctx, cancel] = withTimeout(background(), 5000)
78
+ const { result } = spawn(() => heavyWork(), { ctx })
79
+ // task auto-cancels after 5s
80
+ ```
81
+
73
82
  Functions must be self-contained — they cannot capture variables from the enclosing scope:
74
83
 
75
84
  ```typescript
@@ -89,6 +98,10 @@ const ch = chan<string>() // unbuffered, capacity 0
89
98
  await ch.send(42)
90
99
  const value = await ch.recv() // 42
91
100
 
101
+ // Inspect buffer state (like Go's len(ch) and cap(ch))
102
+ ch.len // number of buffered values
103
+ ch.cap // buffer capacity
104
+
92
105
  ch.close()
93
106
  await ch.recv() // null (closed)
94
107
 
@@ -96,6 +109,10 @@ await ch.recv() // null (closed)
96
109
  for await (const value of ch) {
97
110
  process(value)
98
111
  }
112
+
113
+ // Directional views (like Go's chan<- T and <-chan T)
114
+ const sender = ch.sendOnly() // only send() and close()
115
+ const receiver = ch.recvOnly() // only recv() and async iteration
99
116
  ```
100
117
 
101
118
  Channels in workers — pass channels to spawn() and use them across worker threads:
@@ -129,9 +146,10 @@ for (let i = 0; i < 4; i++) {
129
146
 
130
147
  ### WaitGroup
131
148
 
132
- Structured concurrency. Spawn multiple tasks, wait for all.
149
+ Structured concurrency. Spawn multiple tasks, wait for all. Accepts optional Context.
133
150
 
134
151
  ```typescript
152
+ // Without context
135
153
  const wg = new WaitGroup()
136
154
  wg.spawn(() => cpuWork()) // exclusive
137
155
  wg.spawn(() => fetchData(), { concurrent: true }) // concurrent
@@ -140,11 +158,18 @@ const results = await wg.wait() // like Promise.all
140
158
  const settled = await wg.waitSettled() // like Promise.allSettled
141
159
 
142
160
  wg.cancel() // cancel all tasks
161
+
162
+ // With context — all tasks auto-cancel when context expires
163
+ const [ctx, cancel] = withTimeout(background(), 5000)
164
+ const wg2 = new WaitGroup(ctx)
165
+ wg2.spawn(() => cpuWork())
166
+ const results2 = await wg2.wait()
167
+ cancel()
143
168
  ```
144
169
 
145
170
  ### ErrGroup
146
171
 
147
- Like WaitGroup, but cancels all remaining tasks on first error (modeled after Go's golang.org/x/sync/errgroup).
172
+ Like WaitGroup, but cancels all remaining tasks on first error — in-flight workers are terminated and queued tasks are discarded (modeled after Go's golang.org/x/sync/errgroup). Accepts optional Context. Supports setLimit() for concurrency throttling.
148
173
 
149
174
  ```typescript
150
175
  const eg = new ErrGroup()
@@ -160,6 +185,28 @@ try {
160
185
  }
161
186
  ```
162
187
 
188
+ setLimit(n) — limit max concurrent tasks (like Go's errgroup.SetLimit). Must be called before any spawn().
189
+
190
+ ```typescript
191
+ const eg = new ErrGroup()
192
+ eg.setLimit(4) // max 4 tasks in flight
193
+
194
+ for (const url of urls) {
195
+ eg.spawn(() => fetch(url).then(r => r.json()), { concurrent: true })
196
+ }
197
+ const results = await eg.wait() // runs 4 at a time
198
+ ```
199
+
200
+ With context:
201
+
202
+ ```typescript
203
+ const [ctx, cancel] = withTimeout(background(), 5000)
204
+ const eg = new ErrGroup(ctx) // auto-cancel all tasks on timeout
205
+ eg.setLimit(2)
206
+ // ...
207
+ cancel()
208
+ ```
209
+
163
210
  ### Mutex
164
211
 
165
212
  Async mutual exclusion. Serialize access to shared resources under concurrency.
@@ -178,6 +225,31 @@ try { /* critical section */ }
178
225
  finally { mu.unlock() }
179
226
  ```
180
227
 
228
+ ### RWMutex
229
+
230
+ Read-write mutex. Multiple concurrent readers, exclusive writers. Like Go's sync.RWMutex.
231
+
232
+ Use instead of Mutex when reads vastly outnumber writes.
233
+
234
+ ```typescript
235
+ const rw = new RWMutex()
236
+
237
+ // Many readers can run concurrently
238
+ const data = await rw.withRLock(() => cache.get('config'))
239
+
240
+ // Writers get exclusive access
241
+ await rw.withLock(() => cache.set('config', newValue))
242
+
243
+ // Manual API
244
+ await rw.rLock()
245
+ try { /* read */ }
246
+ finally { rw.rUnlock() }
247
+
248
+ await rw.lock()
249
+ try { /* write */ }
250
+ finally { rw.unlock() }
251
+ ```
252
+
181
253
  ### Once<T>
182
254
 
183
255
  Run a function exactly once, even if called concurrently. All callers get the same result.
@@ -188,17 +260,45 @@ const conn = await once.do(() => createExpensiveConnection())
188
260
  // Subsequent calls return the cached result
189
261
  ```
190
262
 
263
+ ### Cond
264
+
265
+ Condition variable with Wait/Signal/Broadcast. Like Go's sync.Cond.
266
+
267
+ ```typescript
268
+ const mu = new Mutex()
269
+ const cond = new Cond(mu)
270
+ let ready = false
271
+
272
+ // Waiter
273
+ await mu.lock()
274
+ while (!ready) await cond.wait() // releases lock, waits, re-acquires
275
+ mu.unlock()
276
+
277
+ // Signaler
278
+ await mu.lock()
279
+ ready = true
280
+ cond.signal() // wake one waiter
281
+ // cond.broadcast() // wake all waiters
282
+ mu.unlock()
283
+ ```
284
+
191
285
  ### select(cases, opts?)
192
286
 
193
- Wait for the first of multiple promises to resolve, like Go's select.
287
+ Wait for the first of multiple promises to resolve, like Go's select. Supports both recv and send cases.
194
288
 
195
289
  ```typescript
196
- // Blockingwaits for first ready
290
+ // Recv case handler receives the value
197
291
  await select([
198
292
  [ch.recv(), (value) => console.log('received', value)],
199
293
  [after(5000), () => console.log('timeout')],
200
294
  ])
201
295
 
296
+ // Send case — handler runs when send completes
297
+ await select([
298
+ [ch.send(42), () => console.log('sent!')],
299
+ [after(1000), () => console.log('send timed out')],
300
+ ])
301
+
202
302
  // Non-blocking — returns immediately if nothing is ready (Go's select with default)
203
303
  await select(
204
304
  [[ch.recv(), (value) => process(value)]],
@@ -206,12 +306,12 @@ await select(
206
306
  )
207
307
  ```
208
308
 
209
- ### after(ms) / ticker(ms)
309
+ ### after(ms) / ticker(ms) / Timer(ms)
210
310
 
211
311
  Timers for use with select and async iteration.
212
312
 
213
313
  ```typescript
214
- await after(1000) // one-shot: resolves after 1 second
314
+ await after(1000) // one-shot: resolves after 1 second (fire-and-forget)
215
315
 
216
316
  // Repeating: tick every 500ms
217
317
  const t = ticker(500)
@@ -221,6 +321,73 @@ for await (const _ of t) {
221
321
  }
222
322
  ```
223
323
 
324
+ Timer — resettable one-shot timer. Unlike after(), can be stopped and reset.
325
+
326
+ ```typescript
327
+ const t = new Timer(5000)
328
+
329
+ // Use with select for cancellable timeouts
330
+ await select([
331
+ [ch.recv(), (v) => { t.stop(); handle(v) }],
332
+ [t.channel, () => console.log('timed out')],
333
+ ])
334
+
335
+ // Reset for debounce
336
+ t.reset(300) // reschedule, creates new channel promise
337
+ await t.channel
338
+
339
+ // Properties
340
+ t.stopped // true after firing or stop()
341
+ t.stop() // returns true if was pending
342
+ t.channel // promise that resolves when timer fires
343
+ ```
344
+
345
+ ### Context
346
+
347
+ Hierarchical cancellation, deadlines, and request-scoped values — modeled after Go's context package. Integrates with spawn(), WaitGroup, and ErrGroup.
348
+
349
+ ```typescript
350
+ import { background, withCancel, withTimeout, withDeadline, withValue } from '@dmop/puru'
351
+
352
+ // Root context — never cancelled
353
+ const root = background()
354
+
355
+ // Manual cancellation
356
+ const [ctx, cancel] = withCancel(root)
357
+ cancel() // or cancel('reason')
358
+ ctx.err // CancelledError
359
+
360
+ // Timeout
361
+ const [ctx2, cancel2] = withTimeout(root, 5000)
362
+ ctx2.deadline // Date ~5s from now
363
+ ctx2.done() // promise that resolves when cancelled/expired
364
+ cancel2() // cancel early, clears timer
365
+
366
+ // Absolute deadline
367
+ const [ctx3, cancel3] = withDeadline(root, new Date('2025-12-31'))
368
+
369
+ // Request-scoped values
370
+ const ctx4 = withValue(root, 'requestId', 'abc-123')
371
+ ctx4.value('requestId') // 'abc-123'
372
+
373
+ // Composition — child inherits parent's deadline and cancellation
374
+ const [parent] = withTimeout(root, 10000)
375
+ const child = withValue(parent, 'userId', '42')
376
+ // child cancels when parent does (10s timeout)
377
+ ```
378
+
379
+ Context interface:
380
+
381
+ ```typescript
382
+ interface Context {
383
+ readonly signal: AbortSignal
384
+ readonly deadline: Date | null
385
+ readonly err: CancelledError | DeadlineExceededError | null
386
+ value<T>(key: symbol | string): T | undefined
387
+ done(): Promise<void>
388
+ }
389
+ ```
390
+
224
391
  ### register(name, fn) / run(name, ...args)
225
392
 
226
393
  Named task registry. Register functions by name, call them by name.
package/llms.txt CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > A thread pool with Go-style concurrency primitives for JavaScript
4
4
 
5
- puru manages a pool of worker threads and provides a simple API to run functions off the main thread. No separate worker files, no manual message passing.
5
+ puru manages a pool of worker threads and provides a simple API to run functions off the main thread. Zero runtime dependencies. No separate worker files, no manual message passing.
6
6
 
7
7
  Works on Node.js and Bun. Deno support coming soon.
8
8
 
@@ -13,14 +13,18 @@ bun add @dmop/puru
13
13
 
14
14
  ## Core API
15
15
 
16
- - spawn(fn, opts?) — run a function in a worker thread, returns { result: Promise<T>, cancel() }
17
- - chan(capacity?) — Go-style channels for cross-thread communication
18
- - WaitGroup — spawn multiple tasks, wait for all (like Promise.all but off main thread)
19
- - ErrGroup — like WaitGroup but cancels all on first error
20
- - select(cases, opts?) — wait for first of multiple promises (like Go's select)
16
+ - spawn(fn, opts?) — run a function in a worker thread, returns { result: Promise<T>, cancel() }. Accepts { ctx } for auto-cancellation via context.
17
+ - chan(capacity?) — Go-style channels with len/cap inspection and directional views (sendOnly/recvOnly)
18
+ - WaitGroup(ctx?) — spawn multiple tasks, wait for all. Optional context for auto-cancellation.
19
+ - ErrGroup(ctx?) — like WaitGroup but cancels all on first error. Supports setLimit(n) for concurrency throttling.
20
+ - select(cases, opts?) — wait for first of multiple promises (like Go's select). Supports send and recv cases.
21
+ - context — background(), withCancel(), withTimeout(), withDeadline(), withValue(). Integrates with spawn/WaitGroup/ErrGroup.
21
22
  - Mutex — async mutual exclusion
23
+ - RWMutex — read-write mutex (concurrent readers, exclusive writers)
22
24
  - Once<T> — run a function exactly once, all callers get same result
25
+ - Cond — condition variable with wait/signal/broadcast
23
26
  - after(ms) / ticker(ms) — timers for use with select and async iteration
27
+ - Timer(ms) — resettable one-shot timer with stop/reset (unlike fire-and-forget after())
24
28
  - register(name, fn) / run(name, ...args) — named task registry
25
29
  - configure(opts?) — global config (maxThreads, concurrency, idleTimeout, adapter)
26
30
  - stats() / resize(n) — pool introspection and runtime scaling
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmop/puru",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "puru (プール) — A thread pool with Go-style concurrency primitives for JavaScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -29,6 +29,8 @@
29
29
  },
30
30
  "scripts": {
31
31
  "build": "tsup",
32
+ "format": "oxfmt src/",
33
+ "format:check": "oxfmt --check src/",
32
34
  "lint": "oxlint . --vitest-plugin --node-plugin --deny-warnings -D no-explicit-any",
33
35
  "test": "vitest run",
34
36
  "test:watch": "vitest",
@@ -74,6 +76,7 @@
74
76
  "@vitest/coverage-v8": "^3.2.4",
75
77
  "bun-types": "^1.3.11",
76
78
  "husky": "^9.1.7",
79
+ "oxfmt": "^0.43.0",
77
80
  "oxlint": "^1.58.0",
78
81
  "size-limit": "^12.0.1",
79
82
  "tsup": "^8.5.0",