@teqfw/di 2.5.1 → 2.6.1

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/src/Container.mjs CHANGED
@@ -5,440 +5,52 @@
5
5
  * @description DI container orchestration entry point.
6
6
  */
7
7
 
8
- import TeqFw_Di_Def_Parser from './Def/Parser.mjs';
9
- import {Factory as TeqFw_Di_Dto_Resolver_Config_Factory} from './Dto/Resolver/Config.mjs';
10
- import TeqFw_Di_Resolver from './Container/Resolver.mjs';
11
- import TeqFw_Di_Container_Resolve_GraphResolver from './Container/Resolve/GraphResolver.mjs';
12
- import TeqFw_Di_Container_Instantiate_Instantiator from './Container/Instantiate/Instantiator.mjs';
13
- import TeqFw_Di_Container_Lifecycle_Registry from './Container/Lifecycle/Registry.mjs';
14
- import TeqFw_Di_Container_Wrapper_Executor from './Container/Wrapper/Executor.mjs';
15
- import TeqFw_Di_Internal_Logger, {TeqFw_Di_Internal_Logger_Noop} from './Internal/Logger.mjs';
16
-
17
- /**
18
- * @typedef {'notConfigured'|'operational'|'failed'} TeqFw_Di_Container_State
19
- */
8
+ import TeqFw_Di_Container_Runtime from './Container/Runtime.mjs';
20
9
 
21
10
  /**
22
11
  * Container orchestration boundary.
23
12
  *
24
- * Executes immutable linking pipeline:
25
- * parse -> preprocess -> resolve graph -> instantiate -> postprocess ->
26
- * lifecycle -> freeze -> return.
13
+ * Delegates all runtime behavior to an internal runtime coordinator while
14
+ * preserving the public API and contract surface.
27
15
  *
28
16
  * @LLM-DOC
29
17
  * Spec: ./ctx/docs/code/components/container.md
30
18
  */
31
19
  export default class TeqFw_Di_Container {
32
- /**
33
- * Creates container instance in `notConfigured` state.
34
- */
35
20
  constructor() {
36
- /** @type {TeqFw_Di_Container_State} */
37
- let state = 'notConfigured';
38
- /** @type {((depId: TeqFw_Di_DepId__DTO) => TeqFw_Di_DepId__DTO)[]} */
39
- const preprocess = [];
40
- /** @type {((value: unknown) => unknown)[]} */
41
- const postprocess = [];
42
- /** @type {TeqFw_Di_Dto_Resolver_Config_Namespace__DTO[]} */
43
- const namespaceRoots = [];
44
- /** @type {Map<string, unknown>} */
45
- const mockRegistry = new Map();
46
- /** @type {WeakMap<object, object>} */
47
- const promiseSafeCache = new WeakMap();
48
- let testMode = false;
49
- let loggingEnabled = false;
50
-
51
- /** @type {TeqFw_Di_Def_Parser} */
52
- let parser = new TeqFw_Di_Def_Parser();
53
- /** @type {TeqFw_Di_Dto_Resolver_Config__Factory} */
54
- const configFactory = new TeqFw_Di_Dto_Resolver_Config_Factory();
55
- /** @type {TeqFw_Di_Resolver|undefined} */
56
- let resolver;
57
- /** @type {TeqFw_Di_Container_Resolve_GraphResolver|undefined} */
58
- let graphResolver;
59
- /** @type {TeqFw_Di_Container_Lifecycle_Registry|undefined} */
60
- let lifecycle;
61
- /** @type {{log(message: string): void, error(message: string, error?: unknown): void}} */
62
- let logger = TeqFw_Di_Internal_Logger_Noop;
63
- /** @type {TeqFw_Di_Container_Instantiate_Instantiator} */
64
- const instantiator = new TeqFw_Di_Container_Instantiate_Instantiator();
65
- /** @type {TeqFw_Di_Container_Wrapper_Executor} */
66
- const wrapperExecutor = new TeqFw_Di_Container_Wrapper_Executor();
67
-
68
- /**
69
- * @param {TeqFw_Di_DepId__DTO} depId
70
- * @returns {string}
71
- */
72
- const getKey = function (depId) {
73
- const exportName = depId.exportName === null ? '' : depId.exportName;
74
- const life = depId.life === null ? '' : depId.life;
75
- const wrappers = Array.isArray(depId.wrappers) ? depId.wrappers.join('|') : '';
76
- return [
77
- depId.platform,
78
- depId.moduleName,
79
- exportName,
80
- depId.composition,
81
- life,
82
- wrappers,
83
- ].join('::');
84
- };
85
-
86
- /**
87
- * Canonical structural identity excluding `origin`.
88
- *
89
- * @param {TeqFw_Di_DepId__DTO} depId
90
- * @returns {string}
91
- */
92
- const getMockKey = function (depId) {
93
- return getKey(depId);
94
- };
95
-
96
- /**
97
- * Freezes values that support freezing.
98
- *
99
- * @param {unknown} value
100
- * @returns {unknown}
101
- */
102
- const freeze = function (value) {
103
- if ((value === null) || (value === undefined)) return value;
104
- const type = typeof value;
105
- if ((type !== 'object') && (type !== 'function')) return value;
106
- if (Object.prototype.toString.call(value) === '[object Module]') return value;
107
- if (Object.isFrozen(value)) return value;
108
- try {
109
- Object.freeze(value);
110
- } catch (error) {
111
- logger.log(`Container.freeze: skipped (${String(error)}).`);
112
- }
113
- return value;
114
- };
115
-
116
- /**
117
- * Ensures async `get()` can resolve objects whose proxy `get('then')` throws.
118
- *
119
- * @param {unknown} value
120
- * @returns {unknown}
121
- */
122
- const asPromiseSafe = function (value) {
123
- if ((value === null) || (value === undefined)) return value;
124
- const type = typeof value;
125
- if ((type !== 'object') && (type !== 'function')) return value;
126
- const obj = /** @type {object} */ (value);
127
- if (promiseSafeCache.has(obj)) return promiseSafeCache.get(obj);
128
- try {
129
- void Reflect.get(obj, 'then');
130
- return value;
131
- } catch {
132
- const wrapped = new Proxy(obj, {
133
- get(target, property, receiver) {
134
- if (property === 'then') return undefined;
135
- return Reflect.get(target, property, receiver);
136
- },
137
- });
138
- promiseSafeCache.set(obj, wrapped);
139
- return wrapped;
140
- }
141
- };
142
-
143
- /**
144
- * Applies ordered preprocess pipeline.
145
- *
146
- * @param {TeqFw_Di_DepId__DTO} depId
147
- * @returns {TeqFw_Di_DepId__DTO}
148
- */
149
- const applyPreprocess = function (depId) {
150
- /** @type {TeqFw_Di_DepId__DTO} */
151
- let current = depId;
152
- for (const fn of preprocess) {
153
- current = fn(current);
154
- }
155
- return current;
156
- };
21
+ /** @type {TeqFw_Di_Container_Runtime} */
22
+ const runtime = new TeqFw_Di_Container_Runtime();
157
23
 
158
- /**
159
- * Applies ordered postprocess pipeline.
160
- *
161
- * @param {unknown} value
162
- * @returns {unknown}
163
- */
164
- const applyPostprocess = function (value) {
165
- /** @type {unknown} */
166
- let current = value;
167
- for (const fn of postprocess) {
168
- current = fn(current);
169
- }
170
- return current;
171
- };
172
-
173
- /**
174
- * @param {object} namespace
175
- * @param {TeqFw_Di_DepId__DTO} depId
176
- * @returns {Record<string, unknown>}
177
- */
178
- const readDepsDecl = function (namespace, depId) {
179
- /** @type {unknown} */
180
- const deps = Reflect.get(namespace, '__deps__');
181
- if (deps === undefined) return {};
182
- if ((deps === null) || (typeof deps !== 'object') || Array.isArray(deps)) {
183
- throw new Error('__deps__ must be a plain object.');
184
- }
185
- const exportName = depId.exportName === null ? 'default' : depId.exportName;
186
- const exportScoped = Reflect.get(/** @type {object} */ (deps), exportName);
187
- if ((exportScoped !== undefined) && (exportScoped !== null) && (typeof exportScoped === 'object') && !Array.isArray(exportScoped)) {
188
- const values = Object.values(/** @type {Record<string, unknown>} */ (exportScoped));
189
- if (!values.every((value) => typeof value === 'string')) {
190
- throw new Error('__deps__ export entries must map dependency names to CDC strings.');
191
- }
192
- return /** @type {Record<string, unknown>} */ (exportScoped);
193
- }
194
- if (exportName === 'default') {
195
- const values = Object.values(/** @type {Record<string, unknown>} */ (deps));
196
- if (values.every((value) => typeof value === 'string')) {
197
- return /** @type {Record<string, unknown>} */ (deps);
198
- }
199
- if (values.every((value) => (value !== null) && (typeof value === 'object') && !Array.isArray(value))) {
200
- return {};
201
- }
202
- throw new Error('__deps__ must be either flat or export-scoped.');
203
- }
204
- return {};
205
- };
206
-
207
- /**
208
- * @returns {void}
209
- */
210
- const assertBuilderStage = function () {
211
- if (state !== 'notConfigured') throw new Error('Container configuration is locked.');
212
- };
213
-
214
- /**
215
- * Emits builder-stage diagnostics only when logging is enabled.
216
- *
217
- * @param {string} message
218
- * @returns {void}
219
- */
220
- const logBuilder = function (message) {
221
- if (!loggingEnabled) return;
222
- logger.log(`Container.builder: ${message}`);
223
- };
224
-
225
- /**
226
- * Lazily creates infrastructure and locks builder configuration.
227
- *
228
- * @returns {void}
229
- */
230
- const initializeInfrastructure = function () {
231
- if (state !== 'notConfigured') return;
232
- logger.log('Container.transition: notConfigured -> operational.');
233
- state = 'operational';
234
- const resolverConfig = configFactory.create({
235
- namespaces: namespaceRoots,
236
- });
237
- if (typeof parser.setLogger === 'function') parser.setLogger(logger);
238
- resolver = new TeqFw_Di_Resolver({config: resolverConfig, logger});
239
- graphResolver = new TeqFw_Di_Container_Resolve_GraphResolver({parser, resolver, logger});
240
- lifecycle = new TeqFw_Di_Container_Lifecycle_Registry(logger);
241
- };
242
-
243
- /**
244
- * Registers preprocess extension before first resolution.
245
- *
246
- * @param {(depId: TeqFw_Di_DepId__DTO) => TeqFw_Di_DepId__DTO} fn
247
- * @returns {void}
248
- */
249
24
  this.addPreprocess = function (fn) {
250
- assertBuilderStage();
251
- logBuilder('addPreprocess().');
252
- preprocess.push(fn);
25
+ return runtime.addPreprocess(fn);
253
26
  };
254
27
 
255
- /**
256
- * Registers postprocess extension before first resolution.
257
- *
258
- * @param {(value: unknown) => unknown} fn
259
- * @returns {void}
260
- */
261
28
  this.addPostprocess = function (fn) {
262
- assertBuilderStage();
263
- logBuilder('addPostprocess().');
264
- postprocess.push(fn);
29
+ return runtime.addPostprocess(fn);
265
30
  };
266
31
 
267
- /**
268
- * Replaces default parser before first resolution.
269
- *
270
- * @param {TeqFw_Di_Def_Parser} next
271
- * @returns {void}
272
- */
273
32
  this.setParser = function (next) {
274
- assertBuilderStage();
275
- parser = next;
276
- if (typeof parser.setLogger === 'function') parser.setLogger(loggingEnabled ? logger : null);
277
- logBuilder('setParser().');
33
+ return runtime.setParser(next);
278
34
  };
279
35
 
280
- /**
281
- * Registers one resolver namespace root before first resolution.
282
- *
283
- * @param {string} prefix
284
- * @param {string} target
285
- * @param {string} defaultExt
286
- * @returns {void}
287
- */
288
36
  this.addNamespaceRoot = function (prefix, target, defaultExt) {
289
- assertBuilderStage();
290
- logBuilder(`addNamespaceRoot('${prefix}').`);
291
- namespaceRoots.push({prefix, target, defaultExt});
37
+ return runtime.addNamespaceRoot(prefix, target, defaultExt);
292
38
  };
293
39
 
294
- /**
295
- * Enables mock registration capability for test scenarios.
296
- *
297
- * @returns {void}
298
- */
299
40
  this.enableTestMode = function () {
300
- assertBuilderStage();
301
- logBuilder('enableTestMode().');
302
- testMode = true;
41
+ return runtime.enableTestMode();
303
42
  };
304
43
 
305
- /**
306
- * Enables diagnostic logging before first resolution.
307
- *
308
- * @returns {void}
309
- */
310
44
  this.enableLogging = function () {
311
- assertBuilderStage();
312
- if (loggingEnabled) return;
313
- loggingEnabled = true;
314
- logger = new TeqFw_Di_Internal_Logger();
315
- if (typeof parser.setLogger === 'function') parser.setLogger(logger);
316
- logger.log('Container.builder: enableLogging().');
45
+ return runtime.enableLogging();
317
46
  };
318
47
 
319
- /**
320
- * Registers test mock by CDC structural identity.
321
- *
322
- * @param {string} cdc
323
- * @param {unknown} mock
324
- * @returns {void}
325
- */
326
48
  this.register = function (cdc, mock) {
327
- assertBuilderStage();
328
- logBuilder(`register('${cdc}').`);
329
- if (testMode !== true) throw new Error('Container test mode is disabled.');
330
- const depId = parser.parse(cdc);
331
- mockRegistry.set(getMockKey(depId), mock);
49
+ return runtime.register(cdc, mock);
332
50
  };
333
51
 
334
- /**
335
- * Resolves dependency by CDC and returns frozen linked value.
336
- *
337
- * @param {string} cdc
338
- * @returns {Promise<any>}
339
- */
340
- this.get = async function (cdc) {
341
- if (state === 'failed') {
342
- logger.error(`Container.get: rejected in failed state cdc='${cdc}'.`);
343
- throw new Error('Container is in failed state.');
344
- }
345
-
346
- /** @type {string} */
347
- let stage = 'start';
348
- try {
349
- logger.log(`Container.get: cdc='${cdc}'.`);
350
- initializeInfrastructure();
351
- logger.log(`Container.state: '${state}'.`);
352
- /** @type {TeqFw_Di_DepId__DTO} */
353
- stage = 'parse';
354
- logger.log('Container.pipeline: parse:entry.');
355
- const parsed = parser.parse(cdc);
356
- logger.log(`Container.pipeline: parse:exit '${parsed.platform}::${parsed.moduleName}'.`);
357
- /** @type {TeqFw_Di_DepId__DTO} */
358
- stage = 'preprocess';
359
- logger.log('Container.pipeline: preprocess:entry.');
360
- const root = applyPreprocess(parsed);
361
- logger.log(`Container.pipeline: preprocess:exit '${root.platform}::${root.moduleName}'.`);
362
- if (testMode === true) {
363
- stage = 'mock';
364
- logger.log('Container.pipeline: mock-lookup:entry.');
365
- const key = getMockKey(root);
366
- if (mockRegistry.has(key)) {
367
- logger.log(`Container.pipeline: mock-lookup:hit '${key}'.`);
368
- stage = 'freeze';
369
- logger.log('Container.pipeline: freeze:entry.');
370
- const frozenMock = freeze(mockRegistry.get(key));
371
- logger.log('Container.pipeline: freeze:exit.');
372
- logger.log('Container.pipeline: return:success.');
373
- return asPromiseSafe(frozenMock);
374
- }
375
- logger.log(`Container.pipeline: mock-lookup:miss '${key}'.`);
376
- } else {
377
- logger.log('Container.pipeline: mock-lookup:disabled.');
378
- }
379
- /** @type {Map<string, {depId: TeqFw_Di_DepId__DTO, namespace: object}>} */
380
- stage = 'resolve';
381
- logger.log('Container.pipeline: resolve:entry.');
382
- const graph = await graphResolver.resolve(root);
383
- logger.log(`Container.pipeline: resolve:exit nodes=${graph.size}.`);
384
- /** @type {Map<string, unknown>} */
385
- const built = new Map();
386
-
387
- /**
388
- * @param {string} key
389
- * @returns {unknown}
390
- */
391
- const build = function (key) {
392
- if (built.has(key)) return built.get(key);
393
- if (!graph.has(key)) throw new Error(`Resolved graph node is missing for '${key}'.`);
394
-
395
- const node = graph.get(key);
396
- stage = 'lifecycle';
397
- logger.log(`Container.pipeline: lifecycle:entry '${node.depId.platform}::${node.depId.moduleName}'.`);
398
- const value = lifecycle.apply(node.depId, function () {
399
- stage = 'instantiate';
400
- logger.log(`Container.pipeline: instantiate:entry '${node.depId.platform}::${node.depId.moduleName}'.`);
401
- /** @type {Record<string, unknown>} */
402
- const deps = {};
403
- /** @type {Record<string, unknown>} */
404
- const depsDecl = readDepsDecl(node.namespace, node.depId);
405
- for (const [name, cdc] of Object.entries(depsDecl)) {
406
- /** @type {string} */
407
- const childCdc = /** @type {string} */ (cdc);
408
- /** @type {TeqFw_Di_DepId__DTO} */
409
- const childDepId = parser.parse(childCdc);
410
- deps[name] = build(getKey(childDepId));
411
- }
412
- /** @type {unknown} */
413
- const instantiated = instantiator.instantiate(node.depId, node.namespace, deps);
414
- logger.log(`Container.pipeline: instantiate:exit '${node.depId.platform}::${node.depId.moduleName}'.`);
415
- /** @type {unknown} */
416
- stage = 'postprocess';
417
- logger.log(`Container.pipeline: postprocess:entry '${node.depId.platform}::${node.depId.moduleName}'.`);
418
- const postprocessed = applyPostprocess(instantiated);
419
- logger.log(`Container.pipeline: postprocess:exit '${node.depId.platform}::${node.depId.moduleName}'.`);
420
- /** @type {unknown} */
421
- const wrapped = wrapperExecutor.execute(node.depId, postprocessed, node.namespace);
422
- stage = 'freeze';
423
- logger.log(`Container.pipeline: freeze:entry '${node.depId.platform}::${node.depId.moduleName}'.`);
424
- const frozen = freeze(wrapped);
425
- logger.log(`Container.pipeline: freeze:exit '${node.depId.platform}::${node.depId.moduleName}'.`);
426
- return frozen;
427
- });
428
- logger.log(`Container.pipeline: lifecycle:exit '${node.depId.platform}::${node.depId.moduleName}'.`);
429
- built.set(key, value);
430
- return value;
431
- };
432
-
433
- const result = build(getKey(root));
434
- logger.log('Container.pipeline: return:success.');
435
- return asPromiseSafe(result);
436
- } catch (error) {
437
- logger.error(`Container.pipeline: failed at stage='${stage}'.`, error);
438
- logger.log('Container.transition: operational -> failed.');
439
- state = 'failed';
440
- throw error;
441
- }
52
+ this.get = function (cdc) {
53
+ return runtime.get(cdc);
442
54
  };
443
55
  }
444
56
  }
@@ -0,0 +1,29 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @namespace TeqFw_Di_Internal_DependencyKey
5
+ * @description Shared helper for structural dependency identity keys.
6
+ */
7
+
8
+ /**
9
+ * Builds deterministic structural key for dependency identity.
10
+ *
11
+ * The key excludes `origin` and preserves field order used by cache and graph
12
+ * lookups across runtime components.
13
+ *
14
+ * @param {TeqFw_Di_DepId__DTO} depId Dependency identity DTO.
15
+ * @returns {string} Canonical structural key.
16
+ */
17
+ export function buildDependencyKey(depId) {
18
+ const exportName = depId.exportName === null ? '' : depId.exportName;
19
+ const life = depId.life === null ? '' : depId.life;
20
+ const wrappers = Array.isArray(depId.wrappers) ? depId.wrappers.join('|') : '';
21
+ return [
22
+ depId.platform,
23
+ depId.moduleName,
24
+ exportName,
25
+ depId.composition,
26
+ life,
27
+ wrappers,
28
+ ].join('::');
29
+ }
@@ -0,0 +1,42 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @namespace TeqFw_Di_Internal_DepsDecl
5
+ * @description Shared helper for reading `__deps__` declarations.
6
+ */
7
+
8
+ /**
9
+ * Reads dependency declaration map for the selected export of a module namespace.
10
+ *
11
+ * @param {object} namespace Loaded module namespace.
12
+ * @param {TeqFw_Di_DepId__DTO} depId Canonical dependency identity.
13
+ * @returns {Record<string, unknown>} Dependency declaration map.
14
+ */
15
+ export function readDepsDecl(namespace, depId) {
16
+ /** @type {unknown} */
17
+ const deps = Reflect.get(namespace, '__deps__');
18
+ if (deps === undefined) return {};
19
+ if ((deps === null) || (typeof deps !== 'object') || Array.isArray(deps)) {
20
+ throw new Error('__deps__ must be a plain object.');
21
+ }
22
+ const exportName = depId.exportName === null ? 'default' : depId.exportName;
23
+ const exportScoped = Reflect.get(/** @type {object} */ (deps), exportName);
24
+ if ((exportScoped !== undefined) && (exportScoped !== null) && (typeof exportScoped === 'object') && !Array.isArray(exportScoped)) {
25
+ const values = Object.values(/** @type {Record<string, unknown>} */ (exportScoped));
26
+ if (!values.every((value) => typeof value === 'string')) {
27
+ throw new Error('__deps__ export entries must map dependency names to CDC strings.');
28
+ }
29
+ return /** @type {Record<string, unknown>} */ (exportScoped);
30
+ }
31
+ if (exportName === 'default') {
32
+ const values = Object.values(/** @type {Record<string, unknown>} */ (deps));
33
+ if (values.every((value) => typeof value === 'string')) {
34
+ return /** @type {Record<string, unknown>} */ (deps);
35
+ }
36
+ if (values.every((value) => (value !== null) && (typeof value === 'object') && !Array.isArray(value))) {
37
+ return {};
38
+ }
39
+ throw new Error('__deps__ must be either flat or export-scoped.');
40
+ }
41
+ return {};
42
+ }
@@ -0,0 +1,39 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @namespace TeqFw_Di_Internal_PromiseSafe
5
+ * @description Helper for protecting async return values from unsafe `then` access.
6
+ */
7
+
8
+ /** @type {WeakMap<object, object>} */
9
+ const cache = new WeakMap();
10
+
11
+ /**
12
+ * Returns a value that is safe to hand to an async caller.
13
+ *
14
+ * If a proxy throws on `then` access, the value is wrapped in a proxy that
15
+ * hides `then` while preserving all other property access.
16
+ *
17
+ * @param {unknown} value Input value.
18
+ * @returns {unknown} Promise-safe value.
19
+ */
20
+ export function makePromiseSafe(value) {
21
+ if ((value === null) || (value === undefined)) return value;
22
+ const type = typeof value;
23
+ if ((type !== 'object') && (type !== 'function')) return value;
24
+ const obj = /** @type {object} */ (value);
25
+ if (cache.has(obj)) return cache.get(obj);
26
+ try {
27
+ void Reflect.get(obj, 'then');
28
+ return value;
29
+ } catch {
30
+ const wrapped = new Proxy(obj, {
31
+ get(target, property, receiver) {
32
+ if (property === 'then') return undefined;
33
+ return Reflect.get(target, property, receiver);
34
+ },
35
+ });
36
+ cache.set(obj, wrapped);
37
+ return wrapped;
38
+ }
39
+ }