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