@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/AGENTS.md +184 -0
- package/README.md +160 -372
- package/dist/index.cjs +175 -149
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +398 -17
- package/dist/index.d.ts +398 -17
- package/dist/index.js +175 -149
- package/dist/index.js.map +1 -1
- package/llms-full.txt +286 -0
- package/llms.txt +45 -0
- package/package.json +38 -5
package/AGENTS.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# puru — Guide for AI Assistants
|
|
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.
|
|
4
|
+
|
|
5
|
+
Full API reference: https://raw.githubusercontent.com/dmop/puru/main/llms-full.txt
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @dmop/puru
|
|
11
|
+
# or
|
|
12
|
+
bun add @dmop/puru
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## The Most Important Rule
|
|
16
|
+
|
|
17
|
+
Functions passed to `spawn()` are serialized via `.toString()` and sent to a worker thread. **They cannot access variables from the outer scope.**
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// WRONG — closes over `data`, will throw ReferenceError at runtime
|
|
21
|
+
const data = { id: 1 }
|
|
22
|
+
spawn(() => processData(data))
|
|
23
|
+
|
|
24
|
+
// WRONG — closes over `processData` imported in the outer file
|
|
25
|
+
import { processData } from './utils'
|
|
26
|
+
spawn(() => processData({ id: 1 }))
|
|
27
|
+
|
|
28
|
+
// RIGHT — inline everything the function needs
|
|
29
|
+
spawn(() => {
|
|
30
|
+
function processData(d: { id: number }) {
|
|
31
|
+
return d.id * 2
|
|
32
|
+
}
|
|
33
|
+
return processData({ id: 1 })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// RIGHT — inline the data as a literal
|
|
37
|
+
spawn(() => {
|
|
38
|
+
const data = { id: 1 }
|
|
39
|
+
return data.id * 2
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Helper functions used inside `spawn()` must also be defined inside the function body:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// WRONG
|
|
47
|
+
function fibonacci(n: number): number {
|
|
48
|
+
if (n <= 1) return n
|
|
49
|
+
return fibonacci(n - 1) + fibonacci(n - 2)
|
|
50
|
+
}
|
|
51
|
+
spawn(() => fibonacci(40)) // ReferenceError: fibonacci is not defined
|
|
52
|
+
|
|
53
|
+
// RIGHT
|
|
54
|
+
spawn(() => {
|
|
55
|
+
function fibonacci(n: number): number {
|
|
56
|
+
if (n <= 1) return n
|
|
57
|
+
return fibonacci(n - 1) + fibonacci(n - 2)
|
|
58
|
+
}
|
|
59
|
+
return fibonacci(40)
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Common Patterns
|
|
64
|
+
|
|
65
|
+
### CPU-bound work (exclusive mode)
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { spawn } from '@dmop/puru'
|
|
69
|
+
|
|
70
|
+
const { result } = spawn(() => {
|
|
71
|
+
// define everything you need inside
|
|
72
|
+
function crunch(n: number) {
|
|
73
|
+
let sum = 0
|
|
74
|
+
for (let i = 0; i < n; i++) sum += i
|
|
75
|
+
return sum
|
|
76
|
+
}
|
|
77
|
+
return crunch(1_000_000)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
console.log(await result)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Multiple tasks in parallel (`task()`)
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { task } from '@dmop/puru'
|
|
87
|
+
|
|
88
|
+
const items = [1, 2, 3, 4]
|
|
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
|
+
### Cross-thread channels (fan-out)
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { chan, spawn } from '@dmop/puru'
|
|
127
|
+
|
|
128
|
+
const input = chan<number>(50)
|
|
129
|
+
const output = chan<number>(50)
|
|
130
|
+
|
|
131
|
+
// 4 worker threads pulling from the same channel
|
|
132
|
+
for (let i = 0; i < 4; i++) {
|
|
133
|
+
spawn(async ({ input, output }) => {
|
|
134
|
+
for await (const n of input) {
|
|
135
|
+
await output.send(n * 2)
|
|
136
|
+
}
|
|
137
|
+
}, { channels: { input, output } })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Producer
|
|
141
|
+
for (let i = 0; i < 100; i++) await input.send(i)
|
|
142
|
+
input.close()
|
|
143
|
+
|
|
144
|
+
// Consume results
|
|
145
|
+
for await (const result of output) {
|
|
146
|
+
console.log(result)
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## What Can Be Sent Through Channels
|
|
151
|
+
|
|
152
|
+
Channel values must be **structured-cloneable**:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// OK
|
|
156
|
+
ch.send(42)
|
|
157
|
+
ch.send('hello')
|
|
158
|
+
ch.send({ id: 1, name: 'foo' })
|
|
159
|
+
ch.send([1, 2, 3])
|
|
160
|
+
|
|
161
|
+
// NOT OK — will throw
|
|
162
|
+
ch.send(() => {}) // functions
|
|
163
|
+
ch.send(Symbol('x')) // symbols
|
|
164
|
+
ch.send(new WeakRef({})) // WeakRefs
|
|
165
|
+
ch.send(null) // null is the "closed" sentinel — use undefined instead
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Testing
|
|
169
|
+
|
|
170
|
+
Use the inline adapter so tests run on the main thread without real workers:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { configure } from '@dmop/puru'
|
|
174
|
+
|
|
175
|
+
// In your test setup file
|
|
176
|
+
configure({ adapter: 'inline' })
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Runtimes
|
|
180
|
+
|
|
181
|
+
- Node.js >= 20: full support
|
|
182
|
+
- Bun: full support
|
|
183
|
+
- Deno: planned
|
|
184
|
+
- Cloudflare Workers / Vercel Edge: not supported (no thread API)
|