@pumped-fn/lite 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,647 @@
1
+
2
+ //#region src/symbols.ts
3
+ const atomSymbol = Symbol.for("@pumped-fn/lite/atom");
4
+ const flowSymbol = Symbol.for("@pumped-fn/lite/flow");
5
+ const tagSymbol = Symbol.for("@pumped-fn/lite/tag");
6
+ const taggedSymbol = Symbol.for("@pumped-fn/lite/tagged");
7
+ const controllerDepSymbol = Symbol.for("@pumped-fn/lite/controller-dep");
8
+ const presetSymbol = Symbol.for("@pumped-fn/lite/preset");
9
+ const controllerSymbol = Symbol.for("@pumped-fn/lite/controller");
10
+ const tagExecutorSymbol = Symbol.for("@pumped-fn/lite/tag-executor");
11
+
12
+ //#endregion
13
+ //#region src/tag.ts
14
+ function tag(options) {
15
+ const key = Symbol.for(`@pumped-fn/lite/tag/${options.label}`);
16
+ const hasDefault = "default" in options;
17
+ const defaultValue = hasDefault ? options.default : void 0;
18
+ function createTagged(value) {
19
+ return {
20
+ [taggedSymbol]: true,
21
+ key,
22
+ value
23
+ };
24
+ }
25
+ function get(source) {
26
+ const tags$1 = Array.isArray(source) ? source : source.tags ?? [];
27
+ for (let i = 0; i < tags$1.length; i++) if (tags$1[i].key === key) return tags$1[i].value;
28
+ if (hasDefault) return defaultValue;
29
+ throw new Error(`Tag "${options.label}" not found and has no default`);
30
+ }
31
+ function find(source) {
32
+ const tags$1 = Array.isArray(source) ? source : source.tags ?? [];
33
+ for (let i = 0; i < tags$1.length; i++) if (tags$1[i].key === key) return tags$1[i].value;
34
+ if (hasDefault) return defaultValue;
35
+ }
36
+ function collect(source) {
37
+ const tags$1 = Array.isArray(source) ? source : source.tags ?? [];
38
+ const result = [];
39
+ for (let i = 0; i < tags$1.length; i++) if (tags$1[i].key === key) result.push(tags$1[i].value);
40
+ return result;
41
+ }
42
+ return Object.assign(createTagged, {
43
+ [tagSymbol]: true,
44
+ key,
45
+ label: options.label,
46
+ hasDefault,
47
+ defaultValue,
48
+ get,
49
+ find,
50
+ collect
51
+ });
52
+ }
53
+ /**
54
+ * Type guard to check if a value is a Tag.
55
+ *
56
+ * @param value - The value to check
57
+ * @returns True if the value is a Tag, false otherwise
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * if (isTag(value)) {
62
+ * const tagged = value("myValue")
63
+ * }
64
+ * ```
65
+ */
66
+ function isTag(value) {
67
+ return typeof value === "function" && value[tagSymbol] === true;
68
+ }
69
+ /**
70
+ * Type guard to check if a value is a Tagged value.
71
+ *
72
+ * @param value - The value to check
73
+ * @returns True if the value is a Tagged value, false otherwise
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * if (isTagged(value)) {
78
+ * console.log(value.key, value.value)
79
+ * }
80
+ * ```
81
+ */
82
+ function isTagged(value) {
83
+ return typeof value === "object" && value !== null && value[taggedSymbol] === true;
84
+ }
85
+ /**
86
+ * Tag execution helpers for declaring how tags should be resolved from dependency sources.
87
+ */
88
+ const tags = {
89
+ required(tag$1) {
90
+ return {
91
+ [tagExecutorSymbol]: true,
92
+ tag: tag$1,
93
+ mode: "required"
94
+ };
95
+ },
96
+ optional(tag$1) {
97
+ return {
98
+ [tagExecutorSymbol]: true,
99
+ tag: tag$1,
100
+ mode: "optional"
101
+ };
102
+ },
103
+ all(tag$1) {
104
+ return {
105
+ [tagExecutorSymbol]: true,
106
+ tag: tag$1,
107
+ mode: "all"
108
+ };
109
+ }
110
+ };
111
+ /**
112
+ * Type guard to check if a value is a TagExecutor.
113
+ *
114
+ * @param value - The value to check
115
+ * @returns True if the value is a TagExecutor, false otherwise
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * if (isTagExecutor(value)) {
120
+ * console.log(value.mode, value.tag)
121
+ * }
122
+ * ```
123
+ */
124
+ function isTagExecutor(value) {
125
+ return typeof value === "object" && value !== null && tagExecutorSymbol in value;
126
+ }
127
+
128
+ //#endregion
129
+ //#region src/atom.ts
130
+ function atom(config) {
131
+ return {
132
+ [atomSymbol]: true,
133
+ factory: config.factory,
134
+ deps: config.deps,
135
+ tags: config.tags
136
+ };
137
+ }
138
+ /**
139
+ * Type guard to check if a value is an Atom.
140
+ *
141
+ * @param value - The value to check
142
+ * @returns True if the value is an Atom, false otherwise
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * if (isAtom(value)) {
147
+ * await scope.resolve(value)
148
+ * }
149
+ * ```
150
+ */
151
+ function isAtom(value) {
152
+ return typeof value === "object" && value !== null && value[atomSymbol] === true;
153
+ }
154
+ /**
155
+ * Wraps an Atom to receive a Controller instead of the resolved value.
156
+ * The Controller provides full lifecycle control: get, resolve, release, invalidate, and subscribe.
157
+ *
158
+ * @param atom - The Atom to wrap
159
+ * @returns A ControllerDep that resolves to a Controller for the Atom
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * const configAtom = atom({ factory: () => fetchConfig() })
164
+ * const serverAtom = atom({
165
+ * deps: { config: controller(configAtom) },
166
+ * factory: (ctx, { config }) => {
167
+ * const unsub = config.on(() => ctx.invalidate())
168
+ * ctx.cleanup(unsub)
169
+ * return createServer(config.get().port)
170
+ * }
171
+ * })
172
+ * ```
173
+ */
174
+ function controller(atom$1) {
175
+ return {
176
+ [controllerDepSymbol]: true,
177
+ atom: atom$1
178
+ };
179
+ }
180
+ /**
181
+ * Type guard to check if a value is a ControllerDep wrapper.
182
+ *
183
+ * @param value - The value to check
184
+ * @returns True if the value is a ControllerDep wrapper, false otherwise
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * if (isControllerDep(dep)) {
189
+ * const ctrl = scope.controller(dep.atom)
190
+ * }
191
+ * ```
192
+ */
193
+ function isControllerDep(value) {
194
+ return typeof value === "object" && value !== null && value[controllerDepSymbol] === true;
195
+ }
196
+
197
+ //#endregion
198
+ //#region src/flow.ts
199
+ function flow(config) {
200
+ return {
201
+ [flowSymbol]: true,
202
+ factory: config.factory,
203
+ deps: config.deps,
204
+ tags: config.tags
205
+ };
206
+ }
207
+ /**
208
+ * Type guard to check if a value is a Flow.
209
+ *
210
+ * @param value - The value to check
211
+ * @returns True if the value is a Flow, false otherwise
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * if (isFlow(value)) {
216
+ * await ctx.exec({ flow: value, input: data })
217
+ * }
218
+ * ```
219
+ */
220
+ function isFlow(value) {
221
+ return typeof value === "object" && value !== null && value[flowSymbol] === true;
222
+ }
223
+
224
+ //#endregion
225
+ //#region src/preset.ts
226
+ /**
227
+ * Creates a preset value for an Atom, overriding its factory within a scope.
228
+ *
229
+ * @param atom - The Atom to preset
230
+ * @param value - The preset value (can be a direct value or another Atom)
231
+ * @returns A Preset instance to be used in scope configuration
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * const scope = await createScope({
236
+ * presets: [preset(dbAtom, mockDatabase)]
237
+ * })
238
+ * ```
239
+ */
240
+ function preset(atom$1, value) {
241
+ return {
242
+ [presetSymbol]: true,
243
+ atom: atom$1,
244
+ value
245
+ };
246
+ }
247
+ /**
248
+ * Type guard to check if a value is a Preset.
249
+ *
250
+ * @param value - The value to check
251
+ * @returns True if the value is a Preset, false otherwise
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * if (isPreset(value)) {
256
+ * console.log(value.atom, value.value)
257
+ * }
258
+ * ```
259
+ */
260
+ function isPreset(value) {
261
+ return typeof value === "object" && value !== null && value[presetSymbol] === true;
262
+ }
263
+
264
+ //#endregion
265
+ //#region src/scope.ts
266
+ var ControllerImpl = class {
267
+ [controllerSymbol] = true;
268
+ constructor(atom$1, scope) {
269
+ this.atom = atom$1;
270
+ this.scope = scope;
271
+ }
272
+ get state() {
273
+ return this.scope.getEntry(this.atom)?.state ?? "idle";
274
+ }
275
+ get() {
276
+ const entry = this.scope.getEntry(this.atom);
277
+ if (!entry || entry.state === "idle") throw new Error("Atom not resolved");
278
+ if (entry.state === "failed" && entry.error) throw entry.error;
279
+ if (entry.state === "resolving" && entry.hasValue) return entry.value;
280
+ if (entry.state === "resolved" && entry.hasValue) return entry.value;
281
+ throw new Error("Atom not resolved");
282
+ }
283
+ async resolve() {
284
+ return this.scope.resolve(this.atom);
285
+ }
286
+ async release() {
287
+ return this.scope.release(this.atom);
288
+ }
289
+ invalidate() {
290
+ this.scope.invalidate(this.atom);
291
+ }
292
+ on(listener) {
293
+ return this.scope.addListener(this.atom, listener);
294
+ }
295
+ };
296
+ var ScopeImpl = class {
297
+ cache = /* @__PURE__ */ new Map();
298
+ presets = /* @__PURE__ */ new Map();
299
+ resolving = /* @__PURE__ */ new Set();
300
+ pending = /* @__PURE__ */ new Map();
301
+ stateListeners = /* @__PURE__ */ new Map();
302
+ invalidationQueue = /* @__PURE__ */ new Set();
303
+ invalidationScheduled = false;
304
+ extensions;
305
+ tags;
306
+ scheduleInvalidation(atom$1) {
307
+ this.invalidationQueue.add(atom$1);
308
+ if (!this.invalidationScheduled) {
309
+ this.invalidationScheduled = true;
310
+ queueMicrotask(() => this.flushInvalidations());
311
+ }
312
+ }
313
+ flushInvalidations() {
314
+ this.invalidationScheduled = false;
315
+ const atoms = [...this.invalidationQueue];
316
+ this.invalidationQueue.clear();
317
+ for (const atom$1 of atoms) this.invalidate(atom$1);
318
+ }
319
+ constructor(options) {
320
+ this.extensions = options?.extensions ?? [];
321
+ this.tags = options?.tags ?? [];
322
+ for (const p of options?.presets ?? []) this.presets.set(p.atom, p.value);
323
+ }
324
+ async init() {
325
+ for (const ext of this.extensions) if (ext.init) await ext.init(this);
326
+ }
327
+ getEntry(atom$1) {
328
+ return this.cache.get(atom$1);
329
+ }
330
+ getOrCreateEntry(atom$1) {
331
+ let entry = this.cache.get(atom$1);
332
+ if (!entry) {
333
+ entry = {
334
+ state: "idle",
335
+ hasValue: false,
336
+ cleanups: [],
337
+ listeners: /* @__PURE__ */ new Set(),
338
+ pendingInvalidate: false
339
+ };
340
+ this.cache.set(atom$1, entry);
341
+ }
342
+ return entry;
343
+ }
344
+ addListener(atom$1, listener) {
345
+ const entry = this.getOrCreateEntry(atom$1);
346
+ entry.listeners.add(listener);
347
+ return () => {
348
+ entry.listeners.delete(listener);
349
+ };
350
+ }
351
+ notifyListeners(atom$1) {
352
+ const entry = this.cache.get(atom$1);
353
+ if (entry) for (const listener of entry.listeners) listener();
354
+ }
355
+ emitStateChange(state, atom$1) {
356
+ const stateMap = this.stateListeners.get(state);
357
+ if (stateMap) {
358
+ const listeners = stateMap.get(atom$1);
359
+ if (listeners) for (const listener of listeners) listener();
360
+ }
361
+ }
362
+ on(event, atom$1, listener) {
363
+ let stateMap = this.stateListeners.get(event);
364
+ if (!stateMap) {
365
+ stateMap = /* @__PURE__ */ new Map();
366
+ this.stateListeners.set(event, stateMap);
367
+ }
368
+ let listeners = stateMap.get(atom$1);
369
+ if (!listeners) {
370
+ listeners = /* @__PURE__ */ new Set();
371
+ stateMap.set(atom$1, listeners);
372
+ }
373
+ listeners.add(listener);
374
+ const capturedStateMap = stateMap;
375
+ const capturedListeners = listeners;
376
+ return () => {
377
+ capturedListeners.delete(listener);
378
+ if (capturedListeners.size === 0) {
379
+ capturedStateMap.delete(atom$1);
380
+ if (capturedStateMap.size === 0) this.stateListeners.delete(event);
381
+ }
382
+ };
383
+ }
384
+ async resolve(atom$1) {
385
+ const entry = this.cache.get(atom$1);
386
+ if (entry?.state === "resolved") return entry.value;
387
+ const pendingPromise = this.pending.get(atom$1);
388
+ if (pendingPromise) return pendingPromise;
389
+ if (this.resolving.has(atom$1)) throw new Error("Circular dependency detected");
390
+ const presetValue = this.presets.get(atom$1);
391
+ if (presetValue !== void 0) {
392
+ if (isAtom(presetValue)) return this.resolve(presetValue);
393
+ const newEntry = this.getOrCreateEntry(atom$1);
394
+ newEntry.state = "resolved";
395
+ newEntry.value = presetValue;
396
+ newEntry.hasValue = true;
397
+ this.emitStateChange("resolved", atom$1);
398
+ this.notifyListeners(atom$1);
399
+ return newEntry.value;
400
+ }
401
+ this.resolving.add(atom$1);
402
+ const promise = this.doResolve(atom$1);
403
+ this.pending.set(atom$1, promise);
404
+ try {
405
+ return await promise;
406
+ } finally {
407
+ this.resolving.delete(atom$1);
408
+ this.pending.delete(atom$1);
409
+ }
410
+ }
411
+ async doResolve(atom$1) {
412
+ const entry = this.getOrCreateEntry(atom$1);
413
+ entry.state = "resolving";
414
+ this.emitStateChange("resolving", atom$1);
415
+ this.notifyListeners(atom$1);
416
+ const resolvedDeps = await this.resolveDeps(atom$1.deps);
417
+ const ctx = {
418
+ cleanup: (fn) => entry.cleanups.push(fn),
419
+ invalidate: () => {
420
+ this.scheduleInvalidation(atom$1);
421
+ },
422
+ scope: this
423
+ };
424
+ const factory = atom$1.factory;
425
+ const doResolve = async () => {
426
+ if (atom$1.deps && Object.keys(atom$1.deps).length > 0) return factory(ctx, resolvedDeps);
427
+ else return factory(ctx);
428
+ };
429
+ try {
430
+ const value = await this.applyResolveExtensions(atom$1, doResolve);
431
+ entry.state = "resolved";
432
+ entry.value = value;
433
+ entry.hasValue = true;
434
+ entry.error = void 0;
435
+ this.emitStateChange("resolved", atom$1);
436
+ this.notifyListeners(atom$1);
437
+ if (entry.pendingInvalidate) {
438
+ entry.pendingInvalidate = false;
439
+ this.scheduleInvalidation(atom$1);
440
+ }
441
+ return value;
442
+ } catch (err) {
443
+ entry.state = "failed";
444
+ entry.error = err instanceof Error ? err : new Error(String(err));
445
+ entry.value = void 0;
446
+ entry.hasValue = false;
447
+ this.emitStateChange("failed", atom$1);
448
+ this.notifyListeners(atom$1);
449
+ if (entry.pendingInvalidate) {
450
+ entry.pendingInvalidate = false;
451
+ this.scheduleInvalidation(atom$1);
452
+ }
453
+ throw entry.error;
454
+ }
455
+ }
456
+ async applyResolveExtensions(atom$1, doResolve) {
457
+ let next = doResolve;
458
+ for (let i = this.extensions.length - 1; i >= 0; i--) {
459
+ const ext = this.extensions[i];
460
+ if (ext?.wrapResolve) {
461
+ const currentNext = next;
462
+ next = ext.wrapResolve.bind(ext, currentNext, atom$1, this);
463
+ }
464
+ }
465
+ return next();
466
+ }
467
+ async resolveDeps(deps, tagSource) {
468
+ if (!deps) return {};
469
+ const result = {};
470
+ const tags$1 = tagSource ?? this.tags;
471
+ for (const [key, dep] of Object.entries(deps)) if (isAtom(dep)) result[key] = await this.resolve(dep);
472
+ else if (isControllerDep(dep)) result[key] = new ControllerImpl(dep.atom, this);
473
+ else if (tagExecutorSymbol in dep) {
474
+ const tagExecutor = dep;
475
+ switch (tagExecutor.mode) {
476
+ case "required":
477
+ result[key] = tagExecutor.tag.get(tags$1);
478
+ break;
479
+ case "optional":
480
+ result[key] = tagExecutor.tag.find(tags$1);
481
+ break;
482
+ case "all":
483
+ result[key] = tagExecutor.tag.collect(tags$1);
484
+ break;
485
+ }
486
+ }
487
+ return result;
488
+ }
489
+ controller(atom$1) {
490
+ return new ControllerImpl(atom$1, this);
491
+ }
492
+ invalidate(atom$1) {
493
+ const entry = this.cache.get(atom$1);
494
+ if (!entry) return;
495
+ if (entry.state === "idle") return;
496
+ if (entry.state === "resolving") {
497
+ entry.pendingInvalidate = true;
498
+ return;
499
+ }
500
+ this.doInvalidate(atom$1, entry);
501
+ }
502
+ async doInvalidate(atom$1, entry) {
503
+ const previousValue = entry.value;
504
+ for (let i = entry.cleanups.length - 1; i >= 0; i--) {
505
+ const cleanup = entry.cleanups[i];
506
+ if (cleanup) await cleanup();
507
+ }
508
+ entry.cleanups = [];
509
+ entry.state = "resolving";
510
+ entry.value = previousValue;
511
+ entry.error = void 0;
512
+ entry.pendingInvalidate = false;
513
+ this.pending.delete(atom$1);
514
+ this.resolving.delete(atom$1);
515
+ this.emitStateChange("resolving", atom$1);
516
+ this.notifyListeners(atom$1);
517
+ this.resolve(atom$1).catch(() => {});
518
+ }
519
+ async release(atom$1) {
520
+ const entry = this.cache.get(atom$1);
521
+ if (!entry) return;
522
+ for (let i = entry.cleanups.length - 1; i >= 0; i--) {
523
+ const cleanup = entry.cleanups[i];
524
+ if (cleanup) await cleanup();
525
+ }
526
+ this.cache.delete(atom$1);
527
+ }
528
+ async dispose() {
529
+ for (const ext of this.extensions) if (ext.dispose) await ext.dispose(this);
530
+ const atoms = Array.from(this.cache.keys());
531
+ for (const atom$1 of atoms) await this.release(atom$1);
532
+ }
533
+ createContext(options) {
534
+ return new ExecutionContextImpl(this, options);
535
+ }
536
+ };
537
+ var ExecutionContextImpl = class {
538
+ cleanups = [];
539
+ closed = false;
540
+ _input = void 0;
541
+ baseTags;
542
+ constructor(scope, options) {
543
+ this.scope = scope;
544
+ const ctxTags = options?.tags;
545
+ this.baseTags = ctxTags?.length ? [...ctxTags, ...scope.tags] : scope.tags;
546
+ }
547
+ get input() {
548
+ return this._input;
549
+ }
550
+ async exec(options) {
551
+ if (this.closed) throw new Error("ExecutionContext is closed");
552
+ if ("flow" in options) return this.execFlow(options);
553
+ else return this.execFn(options);
554
+ }
555
+ async execFlow(options) {
556
+ const { flow: flow$1, input, tags: execTags } = options;
557
+ const allTags = (execTags?.length ?? 0) > 0 || (flow$1.tags?.length ?? 0) > 0 ? [
558
+ ...execTags ?? [],
559
+ ...this.baseTags,
560
+ ...flow$1.tags ?? []
561
+ ] : this.baseTags;
562
+ const resolvedDeps = await this.scope.resolveDeps(flow$1.deps, allTags);
563
+ this._input = input;
564
+ const factory = flow$1.factory;
565
+ const doExec = async () => {
566
+ if (flow$1.deps && Object.keys(flow$1.deps).length > 0) return factory(this, resolvedDeps);
567
+ else return factory(this);
568
+ };
569
+ return this.applyExecExtensions(flow$1, doExec);
570
+ }
571
+ execFn(options) {
572
+ const { fn, params } = options;
573
+ const doExec = () => Promise.resolve(fn(...params));
574
+ return this.applyExecExtensions(fn, doExec);
575
+ }
576
+ async applyExecExtensions(target, doExec) {
577
+ let next = doExec;
578
+ for (let i = this.scope.extensions.length - 1; i >= 0; i--) {
579
+ const ext = this.scope.extensions[i];
580
+ if (ext?.wrapExec) {
581
+ const currentNext = next;
582
+ next = ext.wrapExec.bind(ext, currentNext, target, this);
583
+ }
584
+ }
585
+ return next();
586
+ }
587
+ onClose(fn) {
588
+ this.cleanups.push(fn);
589
+ }
590
+ async close() {
591
+ if (this.closed) return;
592
+ this.closed = true;
593
+ for (let i = this.cleanups.length - 1; i >= 0; i--) {
594
+ const cleanup = this.cleanups[i];
595
+ if (cleanup) await cleanup();
596
+ }
597
+ }
598
+ };
599
+ /**
600
+ * Creates a DI container that manages Atom resolution, caching, and lifecycle.
601
+ *
602
+ * @param options - Optional configuration for extensions, presets, and tags
603
+ * @returns A Promise that resolves to a Scope instance
604
+ *
605
+ * @example
606
+ * ```typescript
607
+ * const scope = await createScope({
608
+ * extensions: [loggingExtension],
609
+ * presets: [preset(dbAtom, testDb)]
610
+ * })
611
+ * const db = await scope.resolve(dbAtom)
612
+ * ```
613
+ */
614
+ async function createScope(options) {
615
+ const scope = new ScopeImpl(options);
616
+ await scope.init();
617
+ return scope;
618
+ }
619
+
620
+ //#endregion
621
+ //#region src/index.ts
622
+ const VERSION = "0.0.1";
623
+
624
+ //#endregion
625
+ exports.VERSION = VERSION;
626
+ exports.atom = atom;
627
+ exports.atomSymbol = atomSymbol;
628
+ exports.controller = controller;
629
+ exports.controllerDepSymbol = controllerDepSymbol;
630
+ exports.controllerSymbol = controllerSymbol;
631
+ exports.createScope = createScope;
632
+ exports.flow = flow;
633
+ exports.flowSymbol = flowSymbol;
634
+ exports.isAtom = isAtom;
635
+ exports.isControllerDep = isControllerDep;
636
+ exports.isFlow = isFlow;
637
+ exports.isPreset = isPreset;
638
+ exports.isTag = isTag;
639
+ exports.isTagExecutor = isTagExecutor;
640
+ exports.isTagged = isTagged;
641
+ exports.preset = preset;
642
+ exports.presetSymbol = presetSymbol;
643
+ exports.tag = tag;
644
+ exports.tagExecutorSymbol = tagExecutorSymbol;
645
+ exports.tagSymbol = tagSymbol;
646
+ exports.taggedSymbol = taggedSymbol;
647
+ exports.tags = tags;