@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/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/index.cjs +647 -0
- package/dist/index.d.cts +411 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +411 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +625 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +60 -0
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
|