@codefast/di 0.3.16-canary.2 → 0.3.16-canary.3

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 (60) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/binding.d.mts +24 -24
  3. package/dist/constraints.d.mts +3 -3
  4. package/dist/container.mjs +82 -150
  5. package/dist/decorators/inject.d.mts +3 -2
  6. package/dist/decorators/inject.mjs +13 -40
  7. package/dist/decorators/injectable.mjs +4 -12
  8. package/dist/decorators/lifecycle-decorators.mjs +11 -72
  9. package/dist/dependency-graph.mjs +6 -4
  10. package/dist/graph-adapters/cytoscape.d.mts +12 -12
  11. package/dist/graph-adapters/cytoscape.mjs +7 -7
  12. package/dist/graph-adapters/reactflow.d.mts +15 -15
  13. package/dist/graph-adapters/reactflow.mjs +6 -9
  14. package/dist/index.d.mts +3 -3
  15. package/dist/inspector.d.mts +3 -2
  16. package/dist/inspector.mjs +7 -10
  17. package/dist/lifecycle.mjs +2 -12
  18. package/dist/metadata/metadata-keys.d.mts +8 -33
  19. package/dist/metadata/metadata-keys.mjs +8 -21
  20. package/dist/metadata/metadata-types.d.mts +5 -0
  21. package/dist/metadata/symbol-metadata-reader.d.mts +1 -0
  22. package/dist/metadata/symbol-metadata-reader.mjs +11 -33
  23. package/dist/registry.mjs +3 -22
  24. package/dist/resolve-options.d.mts +2 -2
  25. package/dist/resolve-options.mjs +10 -8
  26. package/dist/resolver.d.mts +5 -0
  27. package/dist/resolver.mjs +15 -21
  28. package/dist/types.d.mts +10 -4
  29. package/package.json +39 -13
  30. package/src/binding-scope.ts +26 -0
  31. package/src/binding-select.ts +167 -0
  32. package/src/binding.ts +281 -0
  33. package/src/constraints.ts +149 -0
  34. package/src/constructor-type.ts +19 -0
  35. package/src/container.ts +1213 -0
  36. package/src/decorators/inject.ts +233 -0
  37. package/src/decorators/injectable.ts +85 -0
  38. package/src/decorators/lifecycle-decorators.ts +55 -0
  39. package/src/dependency-graph.ts +116 -0
  40. package/src/environment.ts +232 -0
  41. package/src/errors.ts +262 -0
  42. package/src/graph-adapters/cytoscape.ts +64 -0
  43. package/src/graph-adapters/dot.ts +22 -0
  44. package/src/graph-adapters/reactflow.ts +58 -0
  45. package/src/index.ts +101 -0
  46. package/src/inspector.ts +133 -0
  47. package/src/lifecycle.ts +238 -0
  48. package/src/metadata/metadata-keys.ts +25 -0
  49. package/src/metadata/metadata-reader-token.ts +8 -0
  50. package/src/metadata/metadata-types.ts +53 -0
  51. package/src/metadata/symbol-metadata-reader.ts +57 -0
  52. package/src/module.ts +93 -0
  53. package/src/registry.ts +241 -0
  54. package/src/resolve-options.ts +42 -0
  55. package/src/resolver.ts +1837 -0
  56. package/src/scope.ts +77 -0
  57. package/src/token.ts +40 -0
  58. package/src/types.ts +145 -0
  59. package/dist/graph-adapters/types.d.mts +0 -2
  60. package/dist/graph-adapters/types.mjs +0 -1
package/src/module.ts ADDED
@@ -0,0 +1,93 @@
1
+ import type { BindToBuilder } from "#/binding";
2
+ import type { Constructor } from "#/types";
3
+ import type { Token } from "#/token";
4
+
5
+ // ── Branded types (runtime symbols for branding) ─────────────────────────────
6
+
7
+ const SYNC_MODULE_BRAND: unique symbol = Symbol("di:sync-module");
8
+ const ASYNC_MODULE_BRAND: unique symbol = Symbol("di:async-module");
9
+
10
+ /**
11
+ * @since 0.3.16-canary.0
12
+ */
13
+ export interface SyncModule {
14
+ readonly name: string;
15
+ readonly [SYNC_MODULE_BRAND]: true;
16
+ readonly _setup: (builder: ModuleBuilder) => void;
17
+ }
18
+
19
+ /**
20
+ * @since 0.3.16-canary.0
21
+ */
22
+ export interface AsyncModule {
23
+ readonly name: string;
24
+ readonly [ASYNC_MODULE_BRAND]: true;
25
+ readonly _setup: (builder: AsyncModuleBuilder) => Promise<void>;
26
+ }
27
+
28
+ // ── Builder interfaces ────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * @since 0.3.16-canary.0
32
+ */
33
+ export interface ModuleBuilder {
34
+ bind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
35
+ import(...modules: Array<SyncModule>): void;
36
+ }
37
+
38
+ /**
39
+ * @since 0.3.16-canary.0
40
+ */
41
+ export interface AsyncModuleBuilder {
42
+ bind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
43
+ import(...modules: Array<SyncModule | AsyncModule>): void;
44
+ }
45
+
46
+ // ── Static factories ──────────────────────────────────────────────────────────
47
+
48
+ /**
49
+ * @since 0.3.16-canary.0
50
+ */
51
+ export const SyncModule = {
52
+ create(name: string, setup: (builder: ModuleBuilder) => void): SyncModule {
53
+ return {
54
+ name,
55
+ [SYNC_MODULE_BRAND]: true as const,
56
+ _setup: setup,
57
+ } as SyncModule;
58
+ },
59
+ };
60
+
61
+ /**
62
+ * @since 0.3.16-canary.0
63
+ */
64
+ export const AsyncModule = {
65
+ create(name: string, setup: (builder: AsyncModuleBuilder) => Promise<void>): AsyncModule {
66
+ return {
67
+ name,
68
+ [ASYNC_MODULE_BRAND]: true as const,
69
+ _setup: setup,
70
+ } as AsyncModule;
71
+ },
72
+ };
73
+
74
+ // ── Module — unified API ──────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * @since 0.3.16-canary.0
78
+ */
79
+ export const Module = {
80
+ create(name: string, setup: (builder: ModuleBuilder) => void): SyncModule {
81
+ return SyncModule.create(name, setup);
82
+ },
83
+ createAsync(name: string, setup: (builder: AsyncModuleBuilder) => Promise<void>): AsyncModule {
84
+ return AsyncModule.create(name, setup);
85
+ },
86
+ };
87
+
88
+ /**
89
+ * @since 0.3.16-canary.0
90
+ */
91
+ export function isSyncModule(module: SyncModule | AsyncModule): module is SyncModule {
92
+ return (module as SyncModule)[SYNC_MODULE_BRAND] === true;
93
+ }
@@ -0,0 +1,241 @@
1
+ import type { Binding } from "#/binding";
2
+ import type { BindingIdentifier, Constructor, DependencyKey } from "#/types";
3
+ import type { Token } from "#/token";
4
+ import { bindingSlotEquals, bindingSlotToString } from "#/binding";
5
+
6
+ /**
7
+ * @since 0.3.16-canary.0
8
+ */
9
+ export class BindingRegistry {
10
+ // Map from token key -> array of bindings (order matters for last-wins)
11
+ private readonly _bindings = new Map<DependencyKey, Array<Binding>>();
12
+ // Fast lookup by binding ID
13
+ private readonly _byId = new Map<BindingIdentifier, Binding>();
14
+ // Fast lookup for slot { name, tags: [] }
15
+ private readonly _simpleNamed = new Map<DependencyKey, Map<string, Binding>>();
16
+ // Fast path for one default slot binding with no predicate
17
+ private readonly _fastDefault = new Map<DependencyKey, Binding>();
18
+ // Fast lookup for slot { name: undefined, tags: [[key, value]] } with no predicate
19
+ private readonly _simpleTagged = new Map<DependencyKey, Map<string, Map<unknown, Binding>>>();
20
+
21
+ /** Add or replace binding using slot-aware last-wins. */
22
+ add(binding: Binding): void {
23
+ const key = binding.token as DependencyKey;
24
+ // ✓ TS6.0: Map.getOrInsert (ES2025) replaces the manual get+check+set upsert
25
+ const bindingsForToken = this._bindings.getOrInsert(key, []);
26
+
27
+ // Only apply last-wins for slot-based bindings (not predicate-only)
28
+ if (!this._isPurePredicateBinding(binding)) {
29
+ const existingIndex = bindingsForToken.findIndex(
30
+ (candidate) =>
31
+ !this._isPurePredicateBinding(candidate) &&
32
+ bindingSlotEquals(candidate.slot, binding.slot),
33
+ );
34
+ if (existingIndex !== -1) {
35
+ const replacedBinding = bindingsForToken[existingIndex]!;
36
+ this._byId.delete(replacedBinding.id);
37
+ bindingsForToken.splice(existingIndex, 1);
38
+ }
39
+ }
40
+
41
+ bindingsForToken.push(binding);
42
+ this._byId.set(binding.id, binding);
43
+ this._indexSimpleNamedBinding(key, binding);
44
+ this._indexSimpleTaggedBinding(key, binding);
45
+ this._refreshFastDefaultForToken(key);
46
+ }
47
+
48
+ /** Remove all bindings for a token. Returns removed bindings. */
49
+ removeByToken(token: Token<unknown> | Constructor): Array<Binding> {
50
+ const key = token as DependencyKey;
51
+ const bindingsForToken = this._bindings.get(key) ?? [];
52
+ this._bindings.delete(key);
53
+ this._simpleNamed.delete(key);
54
+ this._simpleTagged.delete(key);
55
+ this._fastDefault.delete(key);
56
+ for (const binding of bindingsForToken) {
57
+ this._byId.delete(binding.id);
58
+ }
59
+ return bindingsForToken;
60
+ }
61
+
62
+ /** Remove a specific binding by ID. Returns the removed binding or undefined. */
63
+ removeById(id: BindingIdentifier): Binding | undefined {
64
+ const binding = this._byId.get(id);
65
+ if (binding === undefined) {
66
+ return undefined;
67
+ }
68
+ this._byId.delete(id);
69
+ const key = binding.token as DependencyKey;
70
+ const bindingsForToken = this._bindings.get(key);
71
+ if (bindingsForToken !== undefined) {
72
+ const bindingIndex = bindingsForToken.findIndex((candidate) => candidate.id === id);
73
+ if (bindingIndex !== -1) {
74
+ bindingsForToken.splice(bindingIndex, 1);
75
+ }
76
+ this._deindexSimpleNamedBinding(key, binding);
77
+ this._deindexSimpleTaggedBinding(key, binding);
78
+ if (bindingsForToken.length === 0) {
79
+ this._bindings.delete(key);
80
+ this._simpleNamed.delete(key);
81
+ this._simpleTagged.delete(key);
82
+ this._fastDefault.delete(key);
83
+ } else {
84
+ this._refreshFastDefaultForToken(key);
85
+ }
86
+ }
87
+ return binding;
88
+ }
89
+
90
+ /** Get all bindings for a token. */
91
+ getAll(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
92
+ return this._bindings.get(token as DependencyKey) ?? [];
93
+ }
94
+
95
+ /** Get binding by ID. */
96
+ getById(id: BindingIdentifier): Binding | undefined {
97
+ return this._byId.get(id);
98
+ }
99
+
100
+ /** Check if any binding exists for token. */
101
+ has(token: Token<unknown> | Constructor): boolean {
102
+ const key = token as DependencyKey;
103
+ const list = this._bindings.get(key);
104
+ return list !== undefined && list.length > 0;
105
+ }
106
+
107
+ /** All bindings in the registry. */
108
+ allBindings(): ReadonlyArray<Binding> {
109
+ const allBindings: Array<Binding> = [];
110
+ for (const bindingsForToken of this._bindings.values()) {
111
+ allBindings.push(...bindingsForToken);
112
+ }
113
+ return allBindings;
114
+ }
115
+
116
+ /** Remove all bindings. Returns all removed. */
117
+ clear(): ReadonlyArray<Binding> {
118
+ const all = this.allBindings();
119
+ this._bindings.clear();
120
+ this._byId.clear();
121
+ this._simpleNamed.clear();
122
+ this._simpleTagged.clear();
123
+ this._fastDefault.clear();
124
+ return all;
125
+ }
126
+
127
+ getSimpleNamed(token: Token<unknown> | Constructor, name: string): Binding | undefined {
128
+ return this._simpleNamed.get(token as DependencyKey)?.get(name);
129
+ }
130
+
131
+ getSimpleTagged(
132
+ token: Token<unknown> | Constructor,
133
+ tagKey: string,
134
+ tagValue: unknown,
135
+ ): Binding | undefined {
136
+ return this._simpleTagged
137
+ .get(token as DependencyKey)
138
+ ?.get(tagKey)
139
+ ?.get(tagValue);
140
+ }
141
+
142
+ getFastDefault(token: Token<unknown> | Constructor): Binding | undefined {
143
+ return this._fastDefault.get(token as DependencyKey);
144
+ }
145
+
146
+ /** Summarize available slot strings for a token (for error messages). */
147
+ availableSlotStrings(token: Token<unknown> | Constructor): Array<string> {
148
+ const bindingsForToken = this._bindings.get(token as DependencyKey) ?? [];
149
+ return bindingsForToken.map((binding) => bindingSlotToString(binding.slot));
150
+ }
151
+
152
+ private _indexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
153
+ const slot = binding.slot;
154
+ if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
155
+ return;
156
+ }
157
+ const [tagKey, tagValue] = slot.tags[0]!;
158
+ const byTagKey = this._simpleTagged.getOrInsert(
159
+ tokenKey,
160
+ new Map<string, Map<unknown, Binding>>(),
161
+ );
162
+ const byTagValue = byTagKey.getOrInsert(tagKey, new Map<unknown, Binding>());
163
+ byTagValue.set(tagValue, binding);
164
+ }
165
+
166
+ private _deindexSimpleTaggedBinding(tokenKey: DependencyKey, binding: Binding): void {
167
+ const slot = binding.slot;
168
+ if (slot.name !== undefined || slot.tags.length !== 1 || binding.predicate !== undefined) {
169
+ return;
170
+ }
171
+ const [tagKey, tagValue] = slot.tags[0]!;
172
+ const byTagKey = this._simpleTagged.get(tokenKey);
173
+ if (byTagKey === undefined) {
174
+ return;
175
+ }
176
+ const byTagValue = byTagKey.get(tagKey);
177
+ if (byTagValue === undefined) {
178
+ return;
179
+ }
180
+ const current = byTagValue.get(tagValue);
181
+ if (current?.id === binding.id) {
182
+ byTagValue.delete(tagValue);
183
+ if (byTagValue.size === 0) {
184
+ byTagKey.delete(tagKey);
185
+ if (byTagKey.size === 0) {
186
+ this._simpleTagged.delete(tokenKey);
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ private _isPurePredicateBinding(binding: Binding): boolean {
193
+ const slot = binding.slot;
194
+ const hasPredicate = binding.predicate !== undefined;
195
+ const hasConstraint = slot.name !== undefined || slot.tags.length > 0;
196
+ // Pure predicate = has predicate but no slot constraint (name/tags)
197
+ return hasPredicate && !hasConstraint;
198
+ }
199
+
200
+ private _indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
201
+ const slot = binding.slot;
202
+ if (slot.name === undefined || slot.tags.length > 0) {
203
+ return;
204
+ }
205
+ const bindingsByName = this._simpleNamed.getOrInsert(tokenKey, new Map<string, Binding>());
206
+ bindingsByName.set(slot.name, binding);
207
+ }
208
+
209
+ private _deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
210
+ const slot = binding.slot;
211
+ if (slot.name === undefined || slot.tags.length > 0) {
212
+ return;
213
+ }
214
+ const bindingsByName = this._simpleNamed.get(tokenKey);
215
+ if (bindingsByName === undefined) {
216
+ return;
217
+ }
218
+ const currentBinding = bindingsByName.get(slot.name);
219
+ if (currentBinding?.id === binding.id) {
220
+ bindingsByName.delete(slot.name);
221
+ if (bindingsByName.size === 0) {
222
+ this._simpleNamed.delete(tokenKey);
223
+ }
224
+ }
225
+ }
226
+
227
+ private _refreshFastDefaultForToken(tokenKey: DependencyKey): void {
228
+ const bindingsForToken = this._bindings.get(tokenKey);
229
+ if (bindingsForToken === undefined || bindingsForToken.length !== 1) {
230
+ this._fastDefault.delete(tokenKey);
231
+ return;
232
+ }
233
+ const onlyBinding = bindingsForToken[0]!;
234
+ const isDefaultSlot = onlyBinding.slot.name === undefined && onlyBinding.slot.tags.length === 0;
235
+ if (!isDefaultSlot || onlyBinding.predicate !== undefined) {
236
+ this._fastDefault.delete(tokenKey);
237
+ return;
238
+ }
239
+ this._fastDefault.set(tokenKey, onlyBinding);
240
+ }
241
+ }
@@ -0,0 +1,42 @@
1
+ import type { BindingSlot } from "#/binding";
2
+ import type { BindingTag, ResolveOptions } from "#/types";
3
+
4
+ /** Shared core: build a ResolveOptions from already-normalised name + tags. */
5
+ function buildOptions(
6
+ name: string | undefined,
7
+ tags: ReadonlyArray<BindingTag> | undefined,
8
+ ): ResolveOptions | undefined {
9
+ if (name === undefined && tags === undefined) {
10
+ return undefined;
11
+ }
12
+ const options: ResolveOptions = {};
13
+ if (name !== undefined) {
14
+ options.name = name;
15
+ }
16
+ if (tags !== undefined) {
17
+ options.tags = tags;
18
+ }
19
+ return options;
20
+ }
21
+
22
+ /**
23
+ * Builds a {@link ResolveOptions} safe for `exactOptionalPropertyTypes`:
24
+ * omits keys instead of assigning `undefined`.
25
+ *
26
+ * @since 0.3.16-canary.0
27
+ */
28
+ export function injectionSlotToResolveOptions(injectionSlot: {
29
+ readonly name?: string;
30
+ readonly tags?: ReadonlyArray<BindingTag>;
31
+ }): ResolveOptions | undefined {
32
+ return buildOptions(injectionSlot.name, injectionSlot.tags);
33
+ }
34
+
35
+ /**
36
+ * Hint from a binding {@link BindingSlot} (tags may be empty; omits when nothing to match).
37
+ *
38
+ * @since 0.3.16-canary.0
39
+ */
40
+ export function bindingSlotToResolveOptions(bindingSlot: BindingSlot): ResolveOptions | undefined {
41
+ return buildOptions(bindingSlot.name, bindingSlot.tags.length > 0 ? bindingSlot.tags : undefined);
42
+ }