@pumped-fn/core-next 0.5.47 → 0.5.49

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/README.md CHANGED
@@ -1,122 +1,421 @@
1
- # Pumped fn
1
+ # @pumped-fn/core-next Expert (v2.0)
2
2
 
3
- Minimal set of library providing functional encapsulation.
3
+ TypeScript DI/reactive programming library. Function-based architecture, lazy evaluation, type-safe resolution, graph-based testing.
4
4
 
5
- # Usages
6
- - Assume all of those functions are from `@pumped-fn/core-next`
7
- - Operator are all directly exported from the package
8
- - Always try to infer instead of explicit declaration
5
+ ## Quick Patterns
9
6
 
10
- ## Container and resolver
11
- Fns helps you create multiple containers. Those are lazy by default and will only resolve via a scope
7
+ | Pattern | Code | Use When |
8
+ | ------------------ | -------------------------------------------- | -------------------- |
9
+ | **Basic DI** | `derive(deps, factory)` | Service dependencies |
10
+ | **No Deps** | `provide(factory)` | Config/singletons |
11
+ | **Reactive** | `derive(dep.reactive, f)` | Auto-updates needed |
12
+ | **Lazy** | `executor.lazy` | Defer resolution |
13
+ | **Static Control** | `executor.static` | Manual state control |
14
+ | **Test Mock** | `createScope(preset(exec, mock))` | Testing |
15
+ | **Cleanup** | `(ctl) => { ctl.cleanup(() => r.close()); }` | Resource disposal |
16
+ | **Batch Resolve** | `resolves(exec1, exec2, exec3)` | Multiple executors |
17
+ | **Flow Pattern** | `flow.provide(spec, factory)` | Structured workflows |
12
18
 
13
- A scope is a simple facility created using `createScope`
19
+ ## Critical Rules ⚠️
14
20
 
15
- ## Scope
21
+ | ✅ DO | ❌ NEVER |
22
+ | ----------------------------------------------- | --------------------------------------------- |
23
+ | Store main executor: `const e = provide(f)` | Store variants: `const r = e.reactive` |
24
+ | Use inline: `derive(e.reactive, f)` | Use with scope: `scope.update(e.reactive, v)` |
25
+ | Resolve first: `await resolve(e); update(e, v)` | Update unresolved: `update(e, v)` |
26
+ | Functions: `(db) => ({get: () => db.query()})` | Classes: `new Service(db)` |
27
+ | Graph testing: Test entire flows | Unit testing: Test isolated pieces |
16
28
 
17
- - `scope#resolve(executor)` will resolve to a `Promise<value>`
18
- - `scope#accessor(executor)` will return a Core.Accessor which can help to `get`, `resolve`, `lookup` or `subscribe`
29
+ ## Core API Reference
19
30
 
20
- ## Executor
21
- Executor is the container. Executor has different access modes for use as dependencies:
31
+ ### Executor Creation
22
32
 
23
- - **Default**: Returns the resolved value (non-reactive)
24
- - **`.reactive`**: Returns the resolved value and subscribes to future updates
25
- - **`.lazy`**: Returns an accessor without resolving the executor (deferred resolution)
26
- - **`.static`**: Returns an accessor with the executor resolved (controller pattern)
33
+ ```typescript
34
+ provide(factory, ...metas); // No dependencies
35
+ derive(deps, factory, ...metas); // With dependencies
36
+ derive([dep1, dep2, dep3], factory, ...metas); // With dependencies array. Factory will receive [resolved1, resolved2, resolved3]
37
+ derive({ dep1, dep2, dep3 }, factory, ...metas); // With dependencies object. Factory will receive { dep1: resolved1, dep2: resolved2, dep3: resolved 3}
38
+ preset(executor, value); // Override value
39
+ placeholder<T>(); // Throws if resolved
40
+ resolves(...executors); // Batch resolution helper
41
+ ```
27
42
 
28
- ## Usage
43
+ ### Scope Operations
29
44
 
30
45
  ```typescript
31
- const counter = provide(() => 0) // Core.Executor<number>
46
+ scope.resolve(executor); // Get value
47
+ scope.update(executor, value); // Update & trigger reactive
48
+ scope.onUpdate(executor, cb); // Subscribe to changes
49
+ scope.onChange(cb); // Global change events
50
+ scope.onRelease(executor, cb); // Cleanup events
51
+ scope.use(middleware); // Add middleware
52
+ scope.pod(...presets); // Create sub-scope
53
+ scope.dispose(); // Cleanup all
54
+ ```
32
55
 
33
- // different usage of derive, look at the argument of the callback
34
- const derivedValue = derive(counter, (counter) => { /* code */ })
35
- const derivedValue = derive({ counter }, ({ counter }) => { /* code */ })
36
- const derivedValue = derive([counter], ([counter]) => { /* code */ })
56
+ ### Variants (inline only)
57
+
58
+ ```typescript
59
+ executor.reactive // Triggers updates
60
+ executor.lazy // Returns Accessor<T>
61
+ executor.static // Returns Accessor<T> (eager)
62
+
63
+ // Accessor<T> interface:
64
+ accessor.get(): T // Current value
65
+ accessor.update(v | fn): void // Update value
66
+ accessor.resolve(force?): T // Re-resolve
67
+ accessor.release(): void // Release
68
+ accessor.lookup(): State<T> // Check state without resolving
37
69
  ```
38
70
 
71
+ ## Meta System 🏷️
72
+
73
+ Type-safe metadata with StandardSchema:
74
+
39
75
  ```typescript
40
- // container can also be reactive, the factory function will be recalled as those dependencies change
41
- const derivedValue = derive(counter.reactive, (counter) => /* this code will be called whenever counter got updated */)
76
+ // Define meta with schema
77
+ const serviceName = meta("service", string());
78
+ const version = meta("version", object({ major: number() }));
79
+
80
+ // Apply to executors
81
+ const api = provide(factory, serviceName("user-api"), version({ major: 2 }));
82
+
83
+ // Retrieve meta
84
+ serviceName.find(api); // 'user-api'
85
+ version.get(api); // [{ major: 2 }]
86
+ serviceName.some(api); // true
87
+
88
+ // Use in middleware
89
+ const telemetry = middleware({
90
+ init: (scope) =>
91
+ scope.onChange((event, exec) => {
92
+ const name = serviceName.find(exec);
93
+ if (name) metrics.record(event, name);
94
+ }),
95
+ });
96
+ ```
97
+
98
+ ## Controller Parameter 🎮
42
99
 
43
- // to update counter
44
- scope.update(counter, /* new value */)
100
+ Every factory receives controller with enhanced capabilities:
101
+
102
+ ```typescript
103
+ // provide((ctl) => ...) or derive(deps, (deps, ctl) => ...)
104
+ ctl.cleanup(() => dispose()); // Register cleanup
105
+ ctl.release(); // Release self
106
+ ctl.scope; // Access parent scope
107
+ ctl.meta; // Access executor metadata
108
+
109
+ // Scope access patterns
110
+ const manager = provide((ctl) => {
111
+ // Create sub-scope
112
+ const pod = ctl.scope.pod(preset(config, override));
113
+ ctl.cleanup(() => pod.dispose());
114
+
115
+ // Access other executors
116
+ const other = await ctl.scope.resolve(otherExec);
117
+
118
+ // Self-management
119
+ if (shouldStop) ctl.release();
120
+ });
45
121
  ```
46
122
 
47
- ### Controller Pattern with `.static`
123
+ ## Advanced Patterns
48
124
 
49
- The `.static` mode provides access to the executor's controller, allowing you to create services that can update state:
125
+ ### Static Accessor Control
50
126
 
51
127
  ```typescript
52
- const counter = provide(() => 0)
128
+ // Build control systems with .static
129
+ const ctrl = derive(config.static, (cfgAccessor) => ({
130
+ update: (v) => cfgAccessor.update(v),
131
+ reset: () => cfgAccessor.update(defaultConfig),
132
+ get: () => cfgAccessor.get(),
133
+ check: () => cfgAccessor.lookup(), // Check without resolving
134
+ }));
135
+
136
+ // Mixed reactive + control
137
+ const svc = derive([state.reactive, config.static], ([state, cfgCtl], ctl) => {
138
+ // React to state changes, control config
139
+ if (state.error) cfgCtl.update((c) => ({ ...c, retry: true }));
140
+ ctl.cleanup(() => console.log("cleaning"));
141
+ return { state, control: cfgCtl };
142
+ });
143
+ ```
53
144
 
54
- // Create a controller service using .static
55
- const counterController = derive(counter.static, (counterCtl) => ({
56
- increment: () => counterCtl.update(current => current + 1),
57
- decrement: () => counterCtl.update(current => current - 1),
58
- reset: () => counterCtl.update(0),
59
- setValue: (value: number) => counterCtl.update(value)
60
- }))
145
+ ### Middleware System
61
146
 
62
- // Usage - no need to access scope directly
63
- const controller = await scope.resolve(counterController)
64
- await controller.increment() // Updates counter through its controller
147
+ ```typescript
148
+ // Enhanced middleware with events
149
+ const analytics = middleware({
150
+ init: (scope) => {
151
+ scope.onChange((event, executor, value) => {
152
+ // 'resolving' | 'resolved' | 'updated' | 'released'
153
+ track(event, meta("name").find(executor));
154
+ });
155
+
156
+ scope.onUpdate(userState, (accessor) => {
157
+ // React to specific executor updates
158
+ logUserChange(accessor.get());
159
+ });
160
+ },
161
+
162
+ // Transform values
163
+ resolve: (value, executor) => {
164
+ if (shouldSanitize(executor)) {
165
+ return preset(executor, sanitize(value));
166
+ }
167
+ return value;
168
+ },
169
+
170
+ dispose: async () => {
171
+ await analytics.flush();
172
+ },
173
+ });
65
174
  ```
66
175
 
67
- ### Lazy Resolution with `.lazy`
176
+ ## Service Architecture
68
177
 
69
- The `.lazy` mode provides access to the executor's accessor without resolving it, useful for deferred execution:
178
+ ### Function-Based (✅ Preferred)
70
179
 
71
180
  ```typescript
72
- const expensiveComputation = provide(() => {
73
- console.log('Computing expensive value...')
74
- return performExpensiveCalculation()
75
- })
181
+ // Handles async dependencies elegantly
182
+ const createAPI = async (config: Config, http: Http) => {
183
+ await http.connect(config.baseUrl); // Async init
184
+
185
+ return {
186
+ async get(path: string) {
187
+ return http.get(config.baseUrl + path);
188
+ },
189
+ async post(path: string, data: any) {
190
+ return http.post(config.baseUrl + path, data);
191
+ },
192
+ };
193
+ };
194
+
195
+ const api = derive([config.reactive, httpClient], createAPI);
196
+ ```
76
197
 
77
- // Get lazy accessor - computation hasn't run yet
78
- const lazyService = derive(expensiveComputation.lazy, (lazyCtl) => ({
79
- getResult: () => lazyCtl.resolve(), // Only compute when explicitly called
80
- isResolved: () => lazyCtl.lookup()?.kind === 'resolved'
81
- }))
198
+ ### Why Not Classes (❌)
82
199
 
83
- const service = await scope.resolve(lazyService)
84
- console.log(service.isResolved()) // false - not computed yet
85
- const result = await service.getResult() // Now computation runs
200
+ ```typescript
201
+ // PROBLEMS with classes:
202
+ class APIService {
203
+ constructor(private config: Config, private http: Http) {
204
+ // ❌ Can't handle async init in constructor
205
+ // ❌ Can't react to config changes
206
+ // ❌ Encourages mutable state
207
+ }
208
+ }
209
+
210
+ // ❌ Classes break reactive updates
211
+ const api = derive(
212
+ [config.reactive, http],
213
+ (cfg, http) => new APIService(cfg, http)
214
+ ); // Service never updates when config changes
86
215
  ```
87
216
 
217
+ ## Graph-Based Testing 🧪
218
+
219
+ Test entire dependency graphs, not units:
220
+
88
221
  ```typescript
89
- // life cycle. The cleanup will be called on [scope dispose], [or resource update], [or resource being released]
90
- const derivedValue = derive(counter.reactive, (counter, controller) => {
91
- // create resource ...
92
- controller.cleanup(() => { /* cleanup code */ })
222
+ // Configure entire system for testing
223
+ const testScope = createScope(
224
+ preset(environment, "test"),
225
+ preset(database, mockDb),
226
+ preset(logger, silentLogger)
227
+ );
228
+
229
+ // Test complete flows
230
+ test("user registration flow", async () => {
231
+ const scope = createScope(
232
+ preset(emailService, mockEmail),
233
+ preset(database, inMemoryDb)
234
+ );
235
+
236
+ try {
237
+ // Test entire registration graph
238
+ const registration = await scope.resolve(registrationFlow);
239
+ const result = await registration.execute({
240
+ email: "test@example.com",
241
+ password: "secure123",
242
+ });
243
+
244
+ // Verify side effects
245
+ expect(mockEmail.sent).toHaveLength(1);
246
+ expect(await inMemoryDb.users.count()).toBe(1);
247
+ } finally {
248
+ await scope.dispose();
249
+ }
250
+ });
251
+
252
+ // Concurrent testing with isolated scopes
253
+ await Promise.all([test1WithScope(), test2WithScope(), test3WithScope()]); // No interference between tests
254
+ ```
93
255
 
94
- return resource
95
- })
256
+ ### State Management Pattern
96
257
 
97
- // to release
98
- scope.release(derivedValue)
258
+ ```typescript
259
+ // Global state with reactive updates
260
+ const appState = provide(() => ({
261
+ user: null,
262
+ theme: "light",
263
+ notifications: [],
264
+ }));
265
+
266
+ // Derived states
267
+ const isAuthenticated = derive(
268
+ appState.reactive,
269
+ (state) => state.user !== null
270
+ );
271
+
272
+ const unreadCount = derive(
273
+ appState.reactive,
274
+ (state) => state.notifications.filter((n) => !n.read).length
275
+ );
276
+
277
+ // State updates
278
+ await scope.update(appState, (state) => ({
279
+ ...state,
280
+ user: { id: "123", name: "Alice" },
281
+ }));
282
+ ```
283
+
284
+ ## Performance Guidelines
285
+
286
+ ### Container Selection
287
+
288
+ | Type | Best For | Avoid When | Performance |
289
+ | ----------- | --------------------- | ----------------- | ---------------- |
290
+ | Normal | Standard deps | Need lazy loading | O(n) resolution |
291
+ | `.reactive` | Auto-updates | Static values | O(m) propagation |
292
+ | `.lazy` | Expensive/conditional | Always need value | Deferred O(n) |
293
+ | `.static` | Manual control | Frequent updates | O(1) access |
99
294
 
100
- // or be released as the dependency released
101
- scope.release(counter)
295
+ ### Optimization Patterns
296
+
297
+ ```typescript
298
+ // Conditional resolution with lazy
299
+ const service = derive(
300
+ { cfg: config.reactive, db: database.lazy },
301
+ async ({ cfg, db }) => {
302
+ // Only resolve DB if needed
303
+ if (cfg.useDb) {
304
+ const database = await db.resolve();
305
+ return createDbService(database);
306
+ }
307
+ return createMemoryService();
308
+ }
309
+ );
310
+
311
+ // Batch updates for performance
312
+ await scope.update(state, (s) => ({
313
+ ...s,
314
+ multiple: "changes",
315
+ at: "once",
316
+ })); // Single reactive propagation
102
317
  ```
103
318
 
104
- ## Meta
319
+ ## Debugging Tools
105
320
 
106
- Meta are decorative information to the executor. Meta uses StandardSchema (zod, valibot, arktype etc) to enforce typing. `custom` is included within the library, it doesn't do any validation
321
+ ```typescript
322
+ // Comprehensive tracing
323
+ scope.onChange((event, exec, val) => {
324
+ const name = meta("name").find(exec);
325
+ console.log(`[${event}] ${name || "anonymous"}: ${val}`);
326
+ });
327
+
328
+ // State inspection
329
+ const state = accessor.lookup();
330
+ switch (state?.kind) {
331
+ case "resolved":
332
+ console.log("Value:", state.value);
333
+ break;
334
+ case "rejected":
335
+ console.error("Error:", state.error);
336
+ break;
337
+ case "pending":
338
+ console.log("Still resolving...");
339
+ break;
340
+ }
341
+
342
+ // Debug metadata
343
+ const debug = meta(
344
+ "debug",
345
+ object({
346
+ created: string(),
347
+ purpose: string(),
348
+ })
349
+ );
350
+
351
+ const svc = provide(
352
+ factory,
353
+ debug({
354
+ created: new Date().toISOString(),
355
+ purpose: "User authentication",
356
+ })
357
+ );
358
+ ```
359
+
360
+ ## Common Issues & Solutions
361
+
362
+ | Error | Cause | Fix |
363
+ | ----------------------- | ----------------------- | ---------------------------------------- |
364
+ | "Executor not resolved" | `.get()` before resolve | `await scope.resolve(e)` first |
365
+ | "Maximum call stack" | Circular reactive deps | Break with `.static` or restructure |
366
+ | Missing updates | Not using `.reactive` | Change to `derive(dep.reactive, f)` |
367
+ | Memory leaks | No cleanup/disposal | Add `ctl.cleanup()` or `scope.dispose()` |
368
+ | Type inference fails | Complex deps | Use `as const` or explicit types |
369
+ | Async in constructor | Using classes | Switch to functions |
370
+ | Flow validation fails | Schema mismatch | Check input/output schemas |
371
+
372
+ ## Decision Tree
373
+
374
+ ```
375
+ Need DI? → provide/derive
376
+ ├─ Changes? → .reactive
377
+ ├─ Expensive? → .lazy
378
+ ├─ Control? → .static
379
+ ├─ Testing? → preset + graph testing
380
+ └─ Workflow? → flow.provide/derive
381
+
382
+ Service Layer?
383
+ ├─ Sync deps → (deps) => service
384
+ ├─ Async deps → async (deps) => service
385
+ ├─ Cleanup → (deps, ctl) => { ctl.cleanup(...); }
386
+ ├─ Control → derive(e.static, (accessor) => controlApi)
387
+ ├─ Scope access → (ctl) => { /* use ctl.scope */ }
388
+ └─ Validation → flow.provide({ input, output }, factory)
389
+
390
+ State Management?
391
+ ├─ Local → single reactive executor
392
+ ├─ Derived → derive(state.reactive, transform)
393
+ ├─ Global → shared scope with executors
394
+ └─ Updates → scope.update(executor, value)
395
+
396
+ Testing Strategy?
397
+ ├─ Unit → Individual executor with mocks
398
+ ├─ Integration → Graph with presets
399
+ ├─ E2E → Full scope with real deps
400
+ └─ Concurrent → Isolated scopes
401
+ ```
402
+
403
+ ## Anti-Pattern Gallery
107
404
 
108
405
  ```typescript
109
- // adding debug name
110
- const debugName = meta('debug', custom<string>())
111
-
112
- const counter = provide(() => 0, debugName('counter'))
113
-
114
- // then meta can be accessed using accessor
115
- const derivedCounter = derive(counter.static, counter => {
116
- counter.metas // would give you access to the given meta
117
- // or
118
- debugName.find(counter) // should give you 'counter' | undefined
119
- // or
120
- debugName.get(counter) // will throw error if no value found
121
- })
122
- ```
406
+ // WRONG
407
+ const r = counter.reactive; // Storing variant
408
+ scope.onUpdate(counter.reactive, cb); // Using variant with scope
409
+ class Svc { constructor(db) {} } // Class with deps
410
+ await scope.update(exec, val); // Update before resolve
411
+ new Database() in derive() // Side effects in factory
412
+ derive([a, b, c, d, e, f, g]) // Too many deps (refactor!)
413
+
414
+ // ✅ CORRECT
415
+ const c = provide(() => 0); // Store main
416
+ derive(c.reactive, v => v * 2); // Inline variant
417
+ const svc = (db) => ({ query: db.q }); // Function factory
418
+ await scope.resolve(exec); scope.update(exec, val); // Resolve first
419
+ derive(deps, async () => new Database()) // Async for side effects
420
+ derive(serviceGroup, ([a, b, c]) => ...) // Group related deps
421
+ ```
@@ -0,0 +1,11 @@
1
+ //#region rolldown:runtime
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all) __defProp(target, name, {
5
+ get: all[name],
6
+ enumerable: true
7
+ });
8
+ };
9
+
10
+ //#endregion
11
+ export { __export };