@pumped-fn/lite 1.7.0 → 1.9.0
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/CHANGELOG.md +16 -0
- package/MIGRATION.md +422 -0
- package/PATTERNS.md +362 -0
- package/README.md +31 -0
- package/dist/index.cjs +38 -8
- package/dist/index.d.cts +21 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +21 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +38 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @pumped-fn/lite
|
|
2
2
|
|
|
3
|
+
## 1.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9e1f827: Add `name` property to ExecutionContext for extension visibility
|
|
8
|
+
|
|
9
|
+
- ExecutionContext now exposes `name: string | undefined` (lazy-computed)
|
|
10
|
+
- Name resolution: exec name > flow name > undefined
|
|
11
|
+
- OTEL extension uses `ctx.name` with configurable `defaultFlowName` fallback
|
|
12
|
+
|
|
13
|
+
## 1.8.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 36105b0: Add `seek()` and `seekTag()` methods to `ContextData` for hierarchical data lookup across ExecutionContext parent chain. Also add PATTERNS.md architectural documentation and include MIGRATION.md in package.
|
|
18
|
+
|
|
3
19
|
## 1.7.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# Migration Guide: @pumped-fn/core-next → @pumped-fn/lite
|
|
2
|
+
|
|
3
|
+
This guide helps AI agents migrate code from `@pumped-fn/core-next` to `@pumped-fn/lite`.
|
|
4
|
+
|
|
5
|
+
## Quick Reference
|
|
6
|
+
|
|
7
|
+
| core-next | lite | Notes |
|
|
8
|
+
|-----------|------|-------|
|
|
9
|
+
| `provide(factory)` | `atom({ factory })` | No deps |
|
|
10
|
+
| `derive(deps, factory)` | `atom({ deps, factory })` | With deps |
|
|
11
|
+
| `Core.Executor<T>` | `Lite.Atom<T>` | Type alias |
|
|
12
|
+
| `Core.Accessor<T>` | `Lite.Controller<T>` | Renamed + reactive |
|
|
13
|
+
| `Core.Controller` | `ResolveContext` | Factory context |
|
|
14
|
+
| `scope.accessor(exec)` | `scope.controller(atom)` | Get controller |
|
|
15
|
+
| `accessor.on(fn)` | `ctrl.on(event, fn)` | Event filtering: `'resolved'`, `'resolving'`, `'*'` |
|
|
16
|
+
| `Promised<T>` | `Promise<T>` | Use native |
|
|
17
|
+
| `multi()` | ❌ Not available | Use Map pattern |
|
|
18
|
+
| `standardSchema` | ❌ Not available | Validate manually |
|
|
19
|
+
| `errors.*` | `Error` | Simple errors |
|
|
20
|
+
|
|
21
|
+
## Step-by-Step Migration
|
|
22
|
+
|
|
23
|
+
### 1. Update Imports
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// BEFORE (core-next)
|
|
27
|
+
import {
|
|
28
|
+
provide,
|
|
29
|
+
derive,
|
|
30
|
+
preset,
|
|
31
|
+
createScope,
|
|
32
|
+
flow,
|
|
33
|
+
tag,
|
|
34
|
+
tags,
|
|
35
|
+
resolves,
|
|
36
|
+
extension,
|
|
37
|
+
Promised,
|
|
38
|
+
multi,
|
|
39
|
+
standardSchema,
|
|
40
|
+
} from '@pumped-fn/core-next'
|
|
41
|
+
import type { Core, Flow, Tag } from '@pumped-fn/core-next'
|
|
42
|
+
|
|
43
|
+
// AFTER (lite)
|
|
44
|
+
import {
|
|
45
|
+
atom,
|
|
46
|
+
preset,
|
|
47
|
+
createScope,
|
|
48
|
+
flow,
|
|
49
|
+
tag,
|
|
50
|
+
tags,
|
|
51
|
+
controller,
|
|
52
|
+
} from '@pumped-fn/lite'
|
|
53
|
+
import type { Lite } from '@pumped-fn/lite'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. Migrate Executors to Atoms
|
|
57
|
+
|
|
58
|
+
#### Simple Executor (no dependencies)
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// BEFORE (core-next)
|
|
62
|
+
const configExecutor = provide((ctrl) => {
|
|
63
|
+
ctrl.cleanup(() => console.log('cleanup'))
|
|
64
|
+
return { port: 3000 }
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// AFTER (lite)
|
|
68
|
+
const configAtom = atom({
|
|
69
|
+
factory: (ctx) => {
|
|
70
|
+
ctx.cleanup(() => console.log('cleanup'))
|
|
71
|
+
return { port: 3000 }
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Executor with Dependencies
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// BEFORE (core-next)
|
|
80
|
+
const serverExecutor = derive(
|
|
81
|
+
{ config: configExecutor },
|
|
82
|
+
(ctrl, { config }) => {
|
|
83
|
+
return createServer(config.port)
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// AFTER (lite)
|
|
88
|
+
const serverAtom = atom({
|
|
89
|
+
deps: { config: configAtom },
|
|
90
|
+
factory: (ctx, { config }) => {
|
|
91
|
+
return createServer(config.port)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Lazy/Accessor Dependencies
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// BEFORE (core-next)
|
|
100
|
+
const dbExecutor = derive(
|
|
101
|
+
{ config: configExecutor.lazy },
|
|
102
|
+
(ctrl, { config }) => {
|
|
103
|
+
return connectDb(config.get().connectionString)
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// AFTER (lite)
|
|
108
|
+
const dbAtom = atom({
|
|
109
|
+
deps: { config: controller(configAtom) },
|
|
110
|
+
factory: (ctx, { config }) => {
|
|
111
|
+
return connectDb(config.get().connectionString)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 3. Migrate Types
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// BEFORE (core-next)
|
|
120
|
+
const myExecutor: Core.Executor<Config> = provide(...)
|
|
121
|
+
const myAccessor: Core.Accessor<Config> = scope.accessor(myExecutor)
|
|
122
|
+
type MyOutput = Core.InferOutput<typeof myExecutor>
|
|
123
|
+
|
|
124
|
+
// AFTER (lite)
|
|
125
|
+
const myAtom: Lite.Atom<Config> = atom(...)
|
|
126
|
+
const myController: Lite.Controller<Config> = scope.controller(myAtom)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 4. Migrate Scope Usage
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// BEFORE (core-next)
|
|
133
|
+
const scope = createScope({
|
|
134
|
+
presets: [preset(configExecutor, mockConfig)],
|
|
135
|
+
extensions: [loggingExtension],
|
|
136
|
+
})
|
|
137
|
+
const config = await scope.resolve(configExecutor)
|
|
138
|
+
const accessor = scope.accessor(configExecutor)
|
|
139
|
+
|
|
140
|
+
// AFTER (lite)
|
|
141
|
+
const scope = createScope({
|
|
142
|
+
presets: [preset(configAtom, mockConfig)],
|
|
143
|
+
extensions: [loggingExtension],
|
|
144
|
+
})
|
|
145
|
+
const config = await scope.resolve(configAtom)
|
|
146
|
+
const ctrl = scope.controller(configAtom)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 5. Migrate Flows
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// BEFORE (core-next)
|
|
153
|
+
const handleRequest = flow(
|
|
154
|
+
{
|
|
155
|
+
input: requestSchema, // StandardSchema validation
|
|
156
|
+
output: responseSchema,
|
|
157
|
+
deps: { db: dbExecutor },
|
|
158
|
+
tags: [apiTag('users')],
|
|
159
|
+
},
|
|
160
|
+
async (ctx, input, { db }) => {
|
|
161
|
+
return db.query(input.userId)
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// Execution
|
|
166
|
+
const result = await scope.exec(handleRequest, { userId: '123' })
|
|
167
|
+
|
|
168
|
+
// AFTER (lite) - Optional parse validation
|
|
169
|
+
const handleRequest = flow({
|
|
170
|
+
name: 'handleRequest',
|
|
171
|
+
deps: { db: dbAtom },
|
|
172
|
+
tags: [apiTag('users')],
|
|
173
|
+
parse: (raw) => {
|
|
174
|
+
const obj = raw as Record<string, unknown>
|
|
175
|
+
if (typeof obj.userId !== 'string') throw new Error('userId required')
|
|
176
|
+
return { userId: obj.userId }
|
|
177
|
+
},
|
|
178
|
+
factory: async (ctx, { db }) => {
|
|
179
|
+
// ctx.input is typed as { userId: string }
|
|
180
|
+
return db.query(ctx.input.userId)
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Execution via context
|
|
185
|
+
const context = scope.createContext()
|
|
186
|
+
const result = await context.exec({
|
|
187
|
+
flow: handleRequest,
|
|
188
|
+
input: { userId: '123' }
|
|
189
|
+
})
|
|
190
|
+
await context.close()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 6. Migrate Tags
|
|
194
|
+
|
|
195
|
+
Tags work the same in both packages, with lite adding optional `parse` for validation:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Same in both
|
|
199
|
+
const tenantId = tag<string>({ label: 'tenantId' })
|
|
200
|
+
|
|
201
|
+
// lite-only: tag with parse validation
|
|
202
|
+
const userId = tag({
|
|
203
|
+
label: 'userId',
|
|
204
|
+
parse: (raw) => {
|
|
205
|
+
if (typeof raw !== 'string') throw new Error('Must be string')
|
|
206
|
+
if (raw.length < 1) throw new Error('Cannot be empty')
|
|
207
|
+
return raw
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
userId('abc-123') // OK - returns Tagged<string>
|
|
212
|
+
userId(123) // Throws ParseError
|
|
213
|
+
|
|
214
|
+
const myAtom = atom({
|
|
215
|
+
deps: { tenant: tags.required(tenantId) },
|
|
216
|
+
factory: (ctx, { tenant }) => {
|
|
217
|
+
console.log('Tenant:', tenant)
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 7. Migrate Extensions
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// BEFORE (core-next) - Full lifecycle with wrap()
|
|
226
|
+
const loggingExt = extension({
|
|
227
|
+
name: 'logging',
|
|
228
|
+
wrap(scope, next, operation) {
|
|
229
|
+
if (operation.kind === 'resolve') {
|
|
230
|
+
console.log('Resolving:', operation.executor)
|
|
231
|
+
}
|
|
232
|
+
return next()
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// AFTER (lite) - Simplified 4-hook interface
|
|
237
|
+
const loggingExt: Lite.Extension = {
|
|
238
|
+
name: 'logging',
|
|
239
|
+
wrapResolve: async (next, atom, scope) => {
|
|
240
|
+
console.log('Resolving atom...')
|
|
241
|
+
const result = await next()
|
|
242
|
+
console.log('Resolved:', result)
|
|
243
|
+
return result
|
|
244
|
+
},
|
|
245
|
+
wrapExec: async (next, target, ctx) => {
|
|
246
|
+
console.log('Executing...')
|
|
247
|
+
return next()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 8. Handle Removed Features
|
|
253
|
+
|
|
254
|
+
#### Multi-Executor Pools
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// BEFORE (core-next)
|
|
258
|
+
const connectionPool = multi.provide({
|
|
259
|
+
key: z.string(),
|
|
260
|
+
factory: (ctrl, key) => createConnection(key)
|
|
261
|
+
})
|
|
262
|
+
const conn = await scope.resolve(connectionPool('db-primary'))
|
|
263
|
+
|
|
264
|
+
// AFTER (lite) - Use Map pattern
|
|
265
|
+
const connections = new Map<string, Connection>()
|
|
266
|
+
const connectionAtom = atom({
|
|
267
|
+
factory: async (ctx) => {
|
|
268
|
+
const key = tags.required(connectionKeyTag).get(ctx.scope)
|
|
269
|
+
if (!connections.has(key)) {
|
|
270
|
+
connections.set(key, createConnection(key))
|
|
271
|
+
}
|
|
272
|
+
return connections.get(key)!
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### StandardSchema Validation
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// BEFORE (core-next)
|
|
281
|
+
const userFlow = flow({
|
|
282
|
+
input: z.object({ id: z.string() }), // Auto-validates
|
|
283
|
+
...
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// AFTER (lite) - Use parse function
|
|
287
|
+
import { z } from 'zod'
|
|
288
|
+
|
|
289
|
+
const userSchema = z.object({ id: z.string() })
|
|
290
|
+
|
|
291
|
+
const userFlow = flow({
|
|
292
|
+
name: 'userFlow',
|
|
293
|
+
parse: (raw) => userSchema.parse(raw), // Validates before factory
|
|
294
|
+
factory: async (ctx) => {
|
|
295
|
+
// ctx.input is typed as { id: string }
|
|
296
|
+
return ctx.input.id
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### Promised Class
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// BEFORE (core-next)
|
|
305
|
+
const promised = scope.exec(myFlow, input)
|
|
306
|
+
promised.finally(() => console.log('done'))
|
|
307
|
+
const result = await promised
|
|
308
|
+
|
|
309
|
+
// AFTER (lite)
|
|
310
|
+
const context = scope.createContext()
|
|
311
|
+
const result = await context.exec({ flow: myFlow, input })
|
|
312
|
+
.finally(() => console.log('done'))
|
|
313
|
+
await context.close()
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### resolves() Helper
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
// BEFORE (core-next)
|
|
320
|
+
const { config, db, cache } = await resolves(scope, {
|
|
321
|
+
config: configExecutor,
|
|
322
|
+
db: dbExecutor,
|
|
323
|
+
cache: cacheExecutor,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// AFTER (lite) - Parallel resolution
|
|
327
|
+
const [config, db, cache] = await Promise.all([
|
|
328
|
+
scope.resolve(configAtom),
|
|
329
|
+
scope.resolve(dbAtom),
|
|
330
|
+
scope.resolve(cacheAtom),
|
|
331
|
+
])
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### 9. Migrate Reactivity
|
|
335
|
+
|
|
336
|
+
Lite has built-in reactivity via Controller that core-next lacks:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// lite-only feature: self-invalidation
|
|
340
|
+
const configAtom = atom({
|
|
341
|
+
factory: async (ctx) => {
|
|
342
|
+
const config = await fetchConfig()
|
|
343
|
+
|
|
344
|
+
const interval = setInterval(() => ctx.invalidate(), 30_000)
|
|
345
|
+
ctx.cleanup(() => clearInterval(interval))
|
|
346
|
+
|
|
347
|
+
return config
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
// lite-only: subscribe to changes with state filtering
|
|
352
|
+
const ctrl = scope.controller(configAtom)
|
|
353
|
+
|
|
354
|
+
// Subscribe to specific events
|
|
355
|
+
ctrl.on('resolved', () => {
|
|
356
|
+
console.log('Config resolved:', ctrl.get())
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
ctrl.on('resolving', () => {
|
|
360
|
+
console.log('Config is re-resolving...')
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
// Subscribe to all state changes
|
|
364
|
+
ctrl.on('*', () => {
|
|
365
|
+
console.log('Config state changed:', ctrl.state)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
// Fine-grained subscriptions with select()
|
|
369
|
+
const portSelect = scope.select(configAtom, (config) => config.port)
|
|
370
|
+
portSelect.subscribe(() => {
|
|
371
|
+
console.log('Port changed:', portSelect.get())
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Migration Checklist
|
|
376
|
+
|
|
377
|
+
- [ ] Update package.json dependency
|
|
378
|
+
- [ ] Change import statements
|
|
379
|
+
- [ ] Rename `provide()` → `atom({ factory })`
|
|
380
|
+
- [ ] Rename `derive()` → `atom({ deps, factory })`
|
|
381
|
+
- [ ] Rename `.lazy` → `controller()`
|
|
382
|
+
- [ ] Rename `Core.*` types → `Lite.*` types
|
|
383
|
+
- [ ] Rename `scope.accessor()` → `scope.controller()`
|
|
384
|
+
- [ ] Update flow execution to use `context.exec()`
|
|
385
|
+
- [ ] Add manual validation if using StandardSchema
|
|
386
|
+
- [ ] Replace `multi()` with Map-based patterns
|
|
387
|
+
- [ ] Replace `Promised` with native Promise
|
|
388
|
+
- [ ] Replace `resolves()` with `Promise.all()`
|
|
389
|
+
- [ ] Update extension `wrap()` to `wrapResolve()`/`wrapExec()`
|
|
390
|
+
- [ ] Run type checker: `pnpm -F @pumped-fn/lite typecheck`
|
|
391
|
+
- [ ] Run tests
|
|
392
|
+
|
|
393
|
+
## When NOT to Migrate
|
|
394
|
+
|
|
395
|
+
Keep using `@pumped-fn/core-next` if you need:
|
|
396
|
+
|
|
397
|
+
- StandardSchema validation (automatic flow input/output validation)
|
|
398
|
+
- Multi-executor pools (`multi()`)
|
|
399
|
+
- Journaling/debugging features
|
|
400
|
+
- Rich error hierarchy with context
|
|
401
|
+
- O(1) tag lookup (lite uses O(n))
|
|
402
|
+
- `Promised` class utilities
|
|
403
|
+
|
|
404
|
+
## Feature Comparison
|
|
405
|
+
|
|
406
|
+
| Feature | lite | core-next |
|
|
407
|
+
|---------|------|-----------|
|
|
408
|
+
| Atoms/Executors | ✅ | ✅ |
|
|
409
|
+
| Flows | ✅ | ✅ |
|
|
410
|
+
| Tags | ✅ | ✅ |
|
|
411
|
+
| Extensions | ✅ (4 hooks) | ✅ (full) |
|
|
412
|
+
| Schema validation | ❌ | ✅ |
|
|
413
|
+
| Journaling | ❌ | ✅ |
|
|
414
|
+
| Multi-executor | ❌ | ✅ |
|
|
415
|
+
| Promised class | ❌ | ✅ |
|
|
416
|
+
| Rich errors | ❌ | ✅ |
|
|
417
|
+
| Controller reactivity | ✅ | ❌ |
|
|
418
|
+
| Self-invalidation | ✅ | ❌ |
|
|
419
|
+
| Fine-grained select() | ✅ | ❌ |
|
|
420
|
+
| Tag/Flow parse functions | ✅ | ❌ |
|
|
421
|
+
| Bundle size | <17KB | ~75KB |
|
|
422
|
+
| Dependencies | 0 | 0 |
|