@teqfw/di 1.3.0 → 2.0.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +173 -259
  3. package/dist/esm.js +1 -1
  4. package/dist/umd.js +1 -1
  5. package/package.json +16 -10
  6. package/src/AGENTS.md +177 -0
  7. package/src/Config/NamespaceRegistry.mjs +210 -0
  8. package/src/Container/Instantiate/ExportSelector.mjs +39 -0
  9. package/src/Container/Instantiate/Instantiator.mjs +143 -0
  10. package/src/Container/Lifecycle/Registry.mjs +81 -0
  11. package/src/Container/Resolve/GraphResolver.mjs +119 -0
  12. package/src/Container/Resolver.mjs +175 -0
  13. package/src/Container/Wrapper/Executor.mjs +71 -0
  14. package/src/Container.mjs +380 -0
  15. package/src/Def/Parser.mjs +146 -0
  16. package/src/Dto/DepId.mjs +131 -0
  17. package/src/Dto/Resolver/Config/Namespace.mjs +48 -0
  18. package/src/Dto/Resolver/Config.mjs +58 -0
  19. package/src/Enum/Composition.mjs +11 -0
  20. package/src/Enum/Life.mjs +11 -0
  21. package/src/Enum/Platform.mjs +12 -0
  22. package/src/Internal/Logger.mjs +54 -0
  23. package/types.d.ts +53 -26
  24. package/src/Api/Container/Config.js +0 -73
  25. package/src/Api/Container/Parser/Chunk.js +0 -27
  26. package/src/Api/Container/Parser.js +0 -34
  27. package/src/Api/Container/PostProcessor/Chunk.js +0 -17
  28. package/src/Api/Container/PostProcessor.js +0 -29
  29. package/src/Api/Container/PreProcessor/Chunk.js +0 -19
  30. package/src/Api/Container/PreProcessor.js +0 -27
  31. package/src/Api/Container/Resolver.js +0 -18
  32. package/src/Api/Container.js +0 -19
  33. package/src/Container/A/Composer/A/SpecParser.js +0 -86
  34. package/src/Container/A/Composer.js +0 -69
  35. package/src/Container/A/Parser/Chunk/Def.js +0 -69
  36. package/src/Container/A/Parser/Chunk/V02X.js +0 -66
  37. package/src/Container/Config.js +0 -93
  38. package/src/Container/Parser.js +0 -48
  39. package/src/Container/PostProcessor.js +0 -32
  40. package/src/Container/PreProcessor.js +0 -34
  41. package/src/Container/Resolver.js +0 -80
  42. package/src/Container.js +0 -187
  43. package/src/Defs.js +0 -22
  44. package/src/DepId.js +0 -52
  45. package/src/Pre/Replace.js +0 -80
  46. package/teqfw.json +0 -8
@@ -0,0 +1,81 @@
1
+ // @ts-check
2
+
3
+ import TeqFw_Di_Enum_Composition from '../../Enum/Composition.mjs';
4
+ import TeqFw_Di_Enum_Life from '../../Enum/Life.mjs';
5
+
6
+ /**
7
+ * Lifecycle-stage registry for produced dependency values.
8
+ *
9
+ * Applies lifecycle caching policy to already instantiated values:
10
+ * - singleton factory values are cached by structural DepId identity;
11
+ * - transient values are never cached;
12
+ * - as-is composition is returned as produced without lifecycle caching.
13
+ */
14
+ export default class TeqFw_Di_Container_Lifecycle_Registry {
15
+
16
+ /**
17
+ * Creates lifecycle registry instance.
18
+ */
19
+ constructor(logger = null) {
20
+ /** @type {Map<string, unknown>} */
21
+ const singletonCache = new Map();
22
+ /** @type {{log(message: string): void}|null} */
23
+ const log = logger;
24
+
25
+ /**
26
+ * Builds deterministic cache key from structural DepId fields.
27
+ *
28
+ * @param {TeqFw_Di_DepId$DTO} depId
29
+ * @returns {string}
30
+ */
31
+ const buildKey = function (depId) {
32
+ /** @type {string} */
33
+ const wrappers = Array.isArray(depId.wrappers) ? depId.wrappers.join('|') : '';
34
+ return [
35
+ depId.platform,
36
+ depId.moduleName,
37
+ depId.exportName === null ? '' : depId.exportName,
38
+ depId.composition,
39
+ depId.life === null ? '' : depId.life,
40
+ wrappers,
41
+ ].join('::');
42
+ };
43
+
44
+ /**
45
+ * Returns value according to lifecycle policy.
46
+ *
47
+ * @param {TeqFw_Di_DepId$DTO} depId
48
+ * @param {() => unknown} producer
49
+ * @returns {unknown}
50
+ */
51
+ this.apply = function (depId, producer) {
52
+ if (depId.composition !== TeqFw_Di_Enum_Composition.FACTORY) {
53
+ if (log) log.log(`Lifecycle.apply: composition='${depId.composition}' cache=skip.`);
54
+ return producer();
55
+ }
56
+
57
+ if (depId.life === TeqFw_Di_Enum_Life.TRANSIENT) {
58
+ if (log) log.log('Lifecycle.apply: transient cache=skip.');
59
+ return producer();
60
+ }
61
+
62
+ if (depId.life === TeqFw_Di_Enum_Life.SINGLETON) {
63
+ /** @type {string} */
64
+ const key = buildKey(depId);
65
+ if (singletonCache.has(key)) {
66
+ if (log) log.log(`Lifecycle.cache: hit key='${key}'.`);
67
+ return singletonCache.get(key);
68
+ }
69
+ if (log) log.log(`Lifecycle.cache: miss key='${key}', create.`);
70
+ /** @type {unknown} */
71
+ const created = producer();
72
+ singletonCache.set(key, created);
73
+ if (log) log.log(`Lifecycle.cache: stored key='${key}'.`);
74
+ return created;
75
+ }
76
+
77
+ if (log) log.log('Lifecycle.apply: no lifecycle marker cache=skip.');
78
+ return producer();
79
+ };
80
+ }
81
+ }
@@ -0,0 +1,119 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Resolve-stage graph builder.
5
+ *
6
+ * Recursively resolves module namespaces and their declared `__deps__`
7
+ * into a deterministic map keyed by `${platform}::${moduleName}`.
8
+ */
9
+
10
+ /**
11
+ * @typedef {object} TeqFw_Di_Container_Resolve_GraphResolver_Dependencies
12
+ * @property {TeqFw_Di_Def_Parser} parser
13
+ * @property {TeqFw_Di_Resolver} resolver
14
+ * @property {{log(message: string): void}|null} [logger]
15
+ */
16
+
17
+ /**
18
+ * @typedef {{depId: TeqFw_Di_DepId$DTO, namespace: object}} TeqFw_Di_Container_Resolve_GraphResolver_Node
19
+ */
20
+
21
+ export default class TeqFw_Di_Container_Resolve_GraphResolver {
22
+
23
+ /**
24
+ * @param {TeqFw_Di_Container_Resolve_GraphResolver_Dependencies} deps
25
+ */
26
+ constructor({parser, resolver, logger = null}) {
27
+ /** @type {{log(message: string): void}|null} */
28
+ const log = logger;
29
+
30
+ /**
31
+ * @param {TeqFw_Di_DepId$DTO} depId
32
+ * @returns {string}
33
+ */
34
+ const makeNodeKey = function (depId) {
35
+ return `${depId.platform}::${depId.moduleName}`;
36
+ };
37
+
38
+ /**
39
+ * @param {TeqFw_Di_DepId$DTO} depId
40
+ * @returns {string}
41
+ */
42
+ const makeDepIdentity = function (depId) {
43
+ /** @type {string} */
44
+ const wrappers = Array.isArray(depId.wrappers) ? depId.wrappers.join('|') : '';
45
+ return [
46
+ depId.platform,
47
+ depId.moduleName,
48
+ depId.exportName === null ? '' : depId.exportName,
49
+ depId.composition,
50
+ depId.life === null ? '' : depId.life,
51
+ wrappers,
52
+ ].join('::');
53
+ };
54
+
55
+ /**
56
+ * @param {TeqFw_Di_DepId$DTO} depId
57
+ * @param {Map<string, TeqFw_Di_Container_Resolve_GraphResolver_Node>} out
58
+ * @param {Set<string>} stack
59
+ * @param {string[]} chain
60
+ * @returns {Promise<void>}
61
+ */
62
+ const walk = async function (depId, out, stack, chain) {
63
+ /** @type {string} */
64
+ const identity = makeDepIdentity(depId);
65
+ if (stack.has(identity)) {
66
+ /** @type {string} */
67
+ const cycle = [...chain, identity].join(' -> ');
68
+ throw new Error(`Cyclic dependency detected: ${cycle}`);
69
+ }
70
+
71
+ /** @type {string} */
72
+ const key = makeNodeKey(depId);
73
+ if (out.has(key)) return;
74
+
75
+ stack.add(identity);
76
+ chain.push(identity);
77
+ try {
78
+ /** @type {object} */
79
+ const namespace = await resolver.resolve(depId);
80
+ if (log) log.log(`GraphResolver.walk: resolved '${key}'.`);
81
+ out.set(key, {depId, namespace});
82
+
83
+ /** @type {unknown} */
84
+ const depsDecl = Reflect.get(namespace, '__deps__');
85
+ if (depsDecl === undefined) return;
86
+ /** @type {Record<string, unknown>} */
87
+ const depsMap = /** @type {Record<string, unknown>} */ (depsDecl);
88
+ for (const [, cdc] of Object.entries(depsMap)) {
89
+ /** @type {string} */
90
+ const nextCdc = /** @type {string} */ (cdc);
91
+ /** @type {TeqFw_Di_DepId$DTO} */
92
+ const nextDepId = parser.parse(nextCdc);
93
+ if (log) log.log(`GraphResolver.walk: edge '${key}' -> '${nextDepId.platform}::${nextDepId.moduleName}'.`);
94
+ await walk(nextDepId, out, stack, chain);
95
+ }
96
+ } finally {
97
+ chain.pop();
98
+ stack.delete(identity);
99
+ }
100
+ };
101
+
102
+ /**
103
+ * Resolves full dependency graph for a root depId.
104
+ *
105
+ * @param {TeqFw_Di_DepId$DTO} depId
106
+ * @returns {Promise<Map<string, TeqFw_Di_Container_Resolve_GraphResolver_Node>>}
107
+ */
108
+ this.resolve = async function (depId) {
109
+ /** @type {Map<string, TeqFw_Di_Container_Resolve_GraphResolver_Node>} */
110
+ const out = new Map();
111
+ /** @type {Set<string>} */
112
+ const stack = new Set();
113
+ /** @type {string[]} */
114
+ const chain = [];
115
+ await walk(depId, out, stack, chain);
116
+ return out;
117
+ };
118
+ }
119
+ }
@@ -0,0 +1,175 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {object} TeqFw_Di_Resolver_Dependencies
5
+ * @property {TeqFw_Di_Dto_Resolver_Config$DTO} config Resolver configuration DTO.
6
+ * @property {(specifier: string) => Promise<object>} [importFn] Import function override.
7
+ * @property {{log(message: string): void, error(message: string, error?: unknown): void}|null} [logger]
8
+ */
9
+
10
+ /**
11
+ * @typedef {{prefix: string, target: string, defaultExt: string}} TeqFw_Di_Resolver_NamespaceRule
12
+ */
13
+
14
+ /**
15
+ * Infrastructure resolver that derives module specifiers, loads module namespace objects,
16
+ * and caches them by `(platform, moduleName)`.
17
+ */
18
+ export default class TeqFw_Di_Resolver {
19
+ /**
20
+ * Initializes resolver with runtime dependencies.
21
+ *
22
+ * @param {TeqFw_Di_Resolver_Dependencies} param0 Resolver dependencies descriptor.
23
+ */
24
+ constructor({config, importFn = (specifier) => import(specifier), logger = null}) {
25
+ /** @type {Map<string, Promise<object>>} Cache keyed by `(platform,moduleName)`. */
26
+ const cache = new Map();
27
+ /** @type {TeqFw_Di_Dto_Resolver_Config$DTO} Original config reference captured from dependencies. */
28
+ const configInput = config;
29
+ /** @type {{nodeModulesRoot: (string|undefined), namespaces: TeqFw_Di_Resolver_NamespaceRule[]}|undefined} */
30
+ let configSnapshot;
31
+ /** @type {(specifier: string) => Promise<object>} Import function used for namespace loading. */
32
+ const importModule = importFn;
33
+ /** @type {{log(message: string): void, error(message: string, error?: unknown): void}|null} */
34
+ const log = logger;
35
+
36
+ /**
37
+ * Creates immutable-in-effect structural snapshot used for all post-start resolutions.
38
+ *
39
+ * @param {TeqFw_Di_Dto_Resolver_Config$DTO} input Resolver config DTO.
40
+ * @returns {{nodeModulesRoot: (string|undefined), namespaces: TeqFw_Di_Resolver_NamespaceRule[]}}
41
+ */
42
+ const makeConfigSnapshot = function (input) {
43
+ return {
44
+ nodeModulesRoot: input.nodeModulesRoot,
45
+ namespaces: input.namespaces.map((one) => ({
46
+ prefix: one.prefix,
47
+ target: one.target,
48
+ defaultExt: one.defaultExt,
49
+ })),
50
+ };
51
+ };
52
+
53
+ /**
54
+ * Selects namespace rule with deterministic longest-prefix match.
55
+ *
56
+ * @param {string} moduleName Teq module namespace.
57
+ * @returns {TeqFw_Di_Resolver_NamespaceRule}
58
+ */
59
+ const selectNamespaceRule = function (moduleName) {
60
+ /** @type {TeqFw_Di_Resolver_NamespaceRule|null} */
61
+ let found = null;
62
+ let foundLen = -1;
63
+ /** @type {TeqFw_Di_Resolver_NamespaceRule[]} */
64
+ const items = configSnapshot.namespaces;
65
+ for (const one of items) {
66
+ const match = moduleName.startsWith(one.prefix);
67
+ if (log) log.log(`Resolver.namespace: prefix='${one.prefix}' match=${String(match)} module='${moduleName}'.`);
68
+ if (!match) continue;
69
+ if (one.prefix.length > foundLen) {
70
+ found = one;
71
+ foundLen = one.prefix.length;
72
+ }
73
+ }
74
+ if (!found) throw new Error(`Namespace rule is not found for '${moduleName}'.`);
75
+ return found;
76
+ };
77
+
78
+ /**
79
+ * Appends default extension exactly once.
80
+ *
81
+ * @param {string} path Relative module path.
82
+ * @param {string} defaultExt Namespace default extension.
83
+ * @returns {string}
84
+ */
85
+ const appendExt = function (path, defaultExt) {
86
+ if (!defaultExt) return path;
87
+ if (path.endsWith(defaultExt)) return path;
88
+ return `${path}${defaultExt}`;
89
+ };
90
+
91
+ /**
92
+ * Joins namespace target and relative path without path normalization.
93
+ *
94
+ * @param {string} target Namespace target.
95
+ * @param {string} path Relative module path.
96
+ * @returns {string}
97
+ */
98
+ const join = function (target, path) {
99
+ if (!target) return path;
100
+ if (target.endsWith('/')) return `${target}${path}`;
101
+ return `${target}/${path}`;
102
+ };
103
+
104
+ /**
105
+ * Derives module specifier from depId structural fields.
106
+ *
107
+ * @param {TeqFw_Di_Enum_Platform[keyof TeqFw_Di_Enum_Platform]} platform DepId platform.
108
+ * @param {string} moduleName DepId module namespace.
109
+ * @returns {string}
110
+ */
111
+ const deriveSpecifier = function (platform, moduleName) {
112
+ if (platform === 'node') {
113
+ const specifier = `node:${moduleName}`;
114
+ if (log) log.log(`Resolver.specifier: module='${moduleName}' -> '${specifier}'.`);
115
+ return specifier;
116
+ }
117
+ if (platform === 'npm') {
118
+ const specifier = moduleName;
119
+ if (log) log.log(`Resolver.specifier: module='${moduleName}' -> '${specifier}'.`);
120
+ return specifier;
121
+ }
122
+ if (platform !== 'teq') throw new Error(`Unsupported platform: ${platform}`);
123
+
124
+ /** @type {TeqFw_Di_Resolver_NamespaceRule} */
125
+ const rule = selectNamespaceRule(moduleName);
126
+ const remainder = moduleName.slice(rule.prefix.length);
127
+ const relativePath = remainder.split('_').join('/');
128
+ const filePath = appendExt(relativePath, rule.defaultExt);
129
+ const specifier = join(rule.target, filePath);
130
+ if (log) log.log(`Resolver.specifier: module='${moduleName}' -> '${specifier}'.`);
131
+ return specifier;
132
+ };
133
+
134
+ /**
135
+ * Resolves module namespace object by depId platform and moduleName.
136
+ *
137
+ * @param {TeqFw_Di_DepId$DTO} depId Validated dependency identity DTO.
138
+ * @returns {Promise<object>} Promise resolved with ES module namespace object.
139
+ */
140
+ this.resolve = async function (depId) {
141
+ await Promise.resolve();
142
+
143
+ if (!configSnapshot) {
144
+ configSnapshot = makeConfigSnapshot(configInput);
145
+ }
146
+
147
+ const platform = depId.platform;
148
+ const moduleName = depId.moduleName;
149
+ const key = `${platform}::${moduleName}`;
150
+
151
+ if (cache.has(key)) {
152
+ if (log) log.log(`Resolver.cache: hit key='${key}'.`);
153
+ return cache.get(key);
154
+ }
155
+ if (log) log.log(`Resolver.cache: miss key='${key}'.`);
156
+
157
+ /** @type {Promise<object>} */
158
+ const promise = (async () => {
159
+ const specifier = deriveSpecifier(platform, moduleName);
160
+ if (log) log.log(`Resolver.import: '${specifier}'.`);
161
+ return importModule(specifier);
162
+ })();
163
+
164
+ cache.set(key, promise);
165
+
166
+ try {
167
+ return await promise;
168
+ } catch (error) {
169
+ cache.delete(key);
170
+ if (log) log.error(`Resolver.cache: evict key='${key}' after failure.`, error);
171
+ throw error;
172
+ }
173
+ };
174
+ }
175
+ }
@@ -0,0 +1,71 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Wrapper-stage executor.
5
+ *
6
+ * Executes wrapper pipeline declared in `depId.wrappers` using functions
7
+ * exported by the resolved module namespace.
8
+ */
9
+ export default class TeqFw_Di_Container_Wrapper_Executor {
10
+
11
+ /**
12
+ * Creates wrapper executor instance.
13
+ */
14
+ constructor() {
15
+ /**
16
+ * Detects Promise-like asynchronous return values.
17
+ *
18
+ * @param {unknown} value
19
+ * @returns {boolean}
20
+ */
21
+ const isThenable = function (value) {
22
+ if ((value === null) || (value === undefined)) return false;
23
+ const type = typeof value;
24
+ if ((type !== 'object') && (type !== 'function')) return false;
25
+ /** @type {{ then?: unknown }} */
26
+ const maybeThenable = value;
27
+ return (typeof maybeThenable.then === 'function');
28
+ };
29
+
30
+ /**
31
+ * Narrows unknown export to unary wrapper callable.
32
+ *
33
+ * @param {unknown} value
34
+ * @returns {value is (value: unknown) => unknown}
35
+ */
36
+ const isWrapper = function (value) {
37
+ return (typeof value === 'function');
38
+ };
39
+
40
+ /**
41
+ * Applies wrappers in declaration order.
42
+ *
43
+ * @param {TeqFw_Di_DepId$DTO} depId
44
+ * @param {unknown} value
45
+ * @param {object} moduleNamespace
46
+ * @returns {unknown}
47
+ */
48
+ this.execute = function (depId, value, moduleNamespace) {
49
+ /** @type {unknown} */
50
+ let current = value;
51
+ const wrappers = depId.wrappers;
52
+
53
+ for (const name of wrappers) {
54
+ if (!(name in moduleNamespace)) {
55
+ throw new Error(`Wrapper '${name}' is not found in module namespace.`);
56
+ }
57
+ /** @type {unknown} */
58
+ const candidate = moduleNamespace[name];
59
+ if (!isWrapper(candidate)) {
60
+ throw new Error(`Wrapper '${name}' must be callable.`);
61
+ }
62
+ current = candidate(current);
63
+ if (isThenable(current)) {
64
+ throw new Error(`Wrapper '${name}' must return synchronously (non-thenable).`);
65
+ }
66
+ }
67
+
68
+ return current;
69
+ };
70
+ }
71
+ }