@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/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # puru — Guide for AI Assistants
2
2
 
3
- puru is a thread pool library for JavaScript with Go-style concurrency primitives (channels, WaitGroup, select). It runs functions off the main thread with no worker files and no boilerplate.
3
+ puru is a thread pool library for JavaScript with Go-style concurrency primitives (channels, WaitGroup, ErrGroup, select, context, Mutex, RWMutex, Cond, Timer). It runs functions off the main thread with no worker files and no boilerplate.
4
4
 
5
5
  Full API reference: https://raw.githubusercontent.com/dmop/puru/main/llms-full.txt
6
6
 
@@ -80,68 +80,179 @@ const { result } = spawn(() => {
80
80
  console.log(await result)
81
81
  ```
82
82
 
83
- ### Multiple tasks in parallel (WaitGroup)
83
+ ### Multiple tasks in parallel (`task()`)
84
84
 
85
85
  ```typescript
86
- import { WaitGroup } from '@dmop/puru'
86
+ import { task } from '@dmop/puru'
87
87
 
88
88
  const items = [1, 2, 3, 4]
89
- const wg = new WaitGroup()
90
-
91
- for (const item of items) {
92
- const value = item // capture as a literal for each iteration
93
- wg.spawn(() => {
94
- // `value` is NOT captured from closure — this won't work
95
- // you must inline or use register()/run()
96
- })
89
+ const processItem = task((item: number) => item * 2)
90
+ const results = await Promise.all(items.map((item) => processItem(item)))
91
+ ```
92
+
93
+ ### Concurrent I/O (concurrent mode)
94
+
95
+ ```typescript
96
+ import { spawn } from '@dmop/puru'
97
+
98
+ const user = spawn(
99
+ () => fetch('https://api.example.com/users/1').then((r) => r.json()),
100
+ { concurrent: true },
101
+ )
102
+
103
+ const orders = spawn(
104
+ () => fetch('https://api.example.com/users/1/orders').then((r) => r.json()),
105
+ { concurrent: true },
106
+ )
107
+
108
+ const results = await Promise.all([user.result, orders.result])
109
+ ```
110
+
111
+ ### Cancel on first error (ErrGroup)
112
+
113
+ ```typescript
114
+ import { ErrGroup } from '@dmop/puru'
115
+
116
+ const eg = new ErrGroup()
117
+ eg.spawn(() => fetch('https://api.example.com/users/1').then((r) => r.json()), { concurrent: true })
118
+ eg.spawn(() => fetch('https://api.example.com/users/1/orders').then((r) => r.json()), { concurrent: true })
119
+
120
+ const [user, orders] = await eg.wait() // throws on first error, cancels the rest
121
+ ```
122
+
123
+ ### ErrGroup with concurrency limit
124
+
125
+ ```typescript
126
+ import { ErrGroup } from '@dmop/puru'
127
+
128
+ const eg = new ErrGroup()
129
+ eg.setLimit(4) // max 4 tasks in flight at once
130
+
131
+ for (const url of urls) {
132
+ eg.spawn(() => fetch(url).then(r => r.json()), { concurrent: true })
97
133
  }
134
+
135
+ const results = await eg.wait()
98
136
  ```
99
137
 
100
- To pass per-task data, use `register`/`run`:
138
+ ### Context-integrated spawn (auto-cancel)
101
139
 
102
140
  ```typescript
103
- import { register, run, WaitGroup } from '@dmop/puru'
141
+ import { spawn, background, withTimeout } from '@dmop/puru'
104
142
 
105
- // Register once at startup
106
- register('processItem', (item: number) => item * 2)
143
+ // Task auto-cancels when context expires — no manual wiring needed
144
+ const [ctx, cancel] = withTimeout(background(), 5000)
145
+ const { result } = spawn(() => heavyWork(), { ctx })
107
146
 
108
- const items = [1, 2, 3, 4]
109
- const wg = new WaitGroup()
110
- for (const item of items) {
111
- wg.spawn(() => run('processItem', item))
147
+ try {
148
+ console.log(await result)
149
+ } finally {
150
+ cancel()
112
151
  }
113
- const results = await wg.wait()
114
152
  ```
115
153
 
116
- ### Concurrent I/O (concurrent mode)
154
+ ### Context with WaitGroup / ErrGroup
117
155
 
118
156
  ```typescript
119
- import { WaitGroup } from '@dmop/puru'
157
+ import { background, withTimeout, WaitGroup } from '@dmop/puru'
120
158
 
121
- const urls = ['https://...', 'https://...']
122
- const wg = new WaitGroup()
159
+ const [ctx, cancel] = withTimeout(background(), 5000)
123
160
 
124
- for (const url of urls) {
125
- wg.spawn(() => run('fetchUrl', url), { concurrent: true })
161
+ // Pass context to WaitGroup — all tasks auto-cancel when ctx expires
162
+ const wg = new WaitGroup(ctx)
163
+ wg.spawn(() => { /* CPU work */ return 42 })
164
+ wg.spawn(() => fetch('https://api.example.com/data').then(r => r.json()), { concurrent: true })
165
+
166
+ try {
167
+ const results = await wg.wait()
168
+ } catch {
169
+ console.log('timed out or cancelled')
170
+ } finally {
171
+ cancel()
126
172
  }
173
+ ```
174
+
175
+ ### RWMutex (read-write lock)
176
+
177
+ ```typescript
178
+ import { RWMutex } from '@dmop/puru'
179
+
180
+ const rw = new RWMutex()
181
+
182
+ // Many readers can run concurrently
183
+ const data = await rw.withRLock(() => cache.get('config'))
127
184
 
128
- register('fetchUrl', (url: string) => fetch(url).then(r => r.json()))
129
- const results = await wg.wait()
185
+ // Writers get exclusive access
186
+ await rw.withLock(() => cache.set('config', newValue))
130
187
  ```
131
188
 
132
- ### Cancel on first error (ErrGroup)
189
+ ### Timer (resettable one-shot)
133
190
 
134
191
  ```typescript
135
- import { ErrGroup, register } from '@dmop/puru'
192
+ import { Timer, select } from '@dmop/puru'
136
193
 
137
- register('fetchUser', (id: number) => fetch(`/api/users/${id}`).then(r => r.json()))
138
- register('fetchOrders', (id: number) => fetch(`/api/orders/${id}`).then(r => r.json()))
194
+ const t = new Timer(5000)
139
195
 
140
- const eg = new ErrGroup()
141
- eg.spawn(() => run('fetchUser', 1))
142
- eg.spawn(() => run('fetchOrders', 1))
196
+ // Use with select for cancellable timeouts
197
+ await select([
198
+ [ch.recv(), (v) => { t.stop(); handle(v) }],
199
+ [t.channel, () => console.log('timed out')],
200
+ ])
143
201
 
144
- const [user, orders] = await eg.wait() // throws on first error, cancels the rest
202
+ // Reset for debounce patterns
203
+ t.reset(300)
204
+ ```
205
+
206
+ ### Cond (condition variable)
207
+
208
+ ```typescript
209
+ import { Mutex, Cond } from '@dmop/puru'
210
+
211
+ const mu = new Mutex()
212
+ const cond = new Cond(mu)
213
+ let ready = false
214
+
215
+ // Waiter
216
+ await mu.lock()
217
+ while (!ready) await cond.wait()
218
+ mu.unlock()
219
+
220
+ // Signaler
221
+ await mu.lock()
222
+ ready = true
223
+ cond.broadcast() // wake all waiters
224
+ mu.unlock()
225
+ ```
226
+
227
+ ### Directional channels
228
+
229
+ ```typescript
230
+ import { chan } from '@dmop/puru'
231
+ import type { SendOnly, RecvOnly } from '@dmop/puru'
232
+
233
+ const ch = chan<number>(10)
234
+
235
+ function producer(out: SendOnly<number>) {
236
+ await out.send(42)
237
+ out.close()
238
+ }
239
+
240
+ function consumer(inp: RecvOnly<number>) {
241
+ for await (const v of inp) console.log(v)
242
+ }
243
+
244
+ producer(ch.sendOnly())
245
+ consumer(ch.recvOnly())
246
+ ```
247
+
248
+ ### Channel inspection
249
+
250
+ ```typescript
251
+ const ch = chan<number>(100)
252
+ await ch.send(1)
253
+ await ch.send(2)
254
+ console.log(ch.len) // 2 (buffered values)
255
+ console.log(ch.cap) // 100 (buffer capacity)
145
256
  ```
146
257
 
147
258
  ### Cross-thread channels (fan-out)
@@ -202,7 +313,7 @@ configure({ adapter: 'inline' })
202
313
 
203
314
  ## Runtimes
204
315
 
205
- - Node.js >= 18: full support
316
+ - Node.js >= 20: full support
206
317
  - Bun: full support
207
318
  - Deno: planned
208
319
  - Cloudflare Workers / Vercel Edge: not supported (no thread API)