@codefast/di 0.3.16-canary.2 → 0.4.0-canary.4

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 (65) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +5 -17
  3. package/dist/binding.d.mts +24 -24
  4. package/dist/constraints.d.mts +3 -3
  5. package/dist/container.d.mts +3 -3
  6. package/dist/container.mjs +89 -157
  7. package/dist/decorators/inject.d.mts +3 -2
  8. package/dist/decorators/inject.mjs +14 -41
  9. package/dist/decorators/injectable.mjs +4 -12
  10. package/dist/decorators/lifecycle-decorators.mjs +13 -74
  11. package/dist/dependency-graph.d.mts +1 -1
  12. package/dist/dependency-graph.mjs +6 -4
  13. package/dist/graph-adapters/cytoscape.d.mts +12 -12
  14. package/dist/graph-adapters/cytoscape.mjs +7 -7
  15. package/dist/graph-adapters/reactflow.d.mts +15 -15
  16. package/dist/graph-adapters/reactflow.mjs +6 -9
  17. package/dist/index.d.mts +6 -6
  18. package/dist/index.mjs +1 -1
  19. package/dist/inspector.d.mts +3 -2
  20. package/dist/inspector.mjs +7 -10
  21. package/dist/lifecycle.mjs +2 -12
  22. package/dist/metadata/metadata-keys.d.mts +8 -33
  23. package/dist/metadata/metadata-keys.mjs +8 -21
  24. package/dist/metadata/metadata-types.d.mts +5 -0
  25. package/dist/metadata/symbol-metadata-reader.d.mts +1 -0
  26. package/dist/metadata/symbol-metadata-reader.mjs +11 -33
  27. package/dist/module.mjs +1 -1
  28. package/dist/registry.mjs +3 -22
  29. package/dist/resolve-options.d.mts +2 -2
  30. package/dist/resolve-options.mjs +10 -8
  31. package/dist/resolver.d.mts +6 -1
  32. package/dist/resolver.mjs +15 -21
  33. package/dist/types.d.mts +10 -4
  34. package/package.json +40 -14
  35. package/src/binding-scope.ts +26 -0
  36. package/src/binding-select.ts +158 -0
  37. package/src/binding.ts +277 -0
  38. package/src/constraints.ts +121 -0
  39. package/src/constructor-type.ts +19 -0
  40. package/src/container.ts +1135 -0
  41. package/src/decorators/inject.ts +222 -0
  42. package/src/decorators/injectable.ts +85 -0
  43. package/src/decorators/lifecycle-decorators.ts +51 -0
  44. package/src/dependency-graph.ts +116 -0
  45. package/src/environment.ts +207 -0
  46. package/src/errors.ts +260 -0
  47. package/src/graph-adapters/cytoscape.ts +64 -0
  48. package/src/graph-adapters/dot.ts +22 -0
  49. package/src/graph-adapters/reactflow.ts +58 -0
  50. package/src/index.ts +101 -0
  51. package/src/inspector.ts +125 -0
  52. package/src/lifecycle.ts +217 -0
  53. package/src/metadata/metadata-keys.ts +25 -0
  54. package/src/metadata/metadata-reader-token.ts +8 -0
  55. package/src/metadata/metadata-types.ts +51 -0
  56. package/src/metadata/symbol-metadata-reader.ts +45 -0
  57. package/src/module.ts +93 -0
  58. package/src/registry.ts +232 -0
  59. package/src/resolve-options.ts +42 -0
  60. package/src/resolver.ts +1609 -0
  61. package/src/scope.ts +77 -0
  62. package/src/token.ts +40 -0
  63. package/src/types.ts +123 -0
  64. package/dist/graph-adapters/types.d.mts +0 -2
  65. package/dist/graph-adapters/types.mjs +0 -1
@@ -0,0 +1,158 @@
1
+ import type { Binding } from "#/binding";
2
+ import { AmbiguousBindingError } from "#/errors";
3
+ import type { BindingTag, ConstraintContext, ResolveOptions } from "#/types";
4
+
5
+ /**
6
+ * Select a single candidate from a list of bindings using slot matching + predicates.
7
+ * Returns undefined if no match, throws AmbiguousBindingError if multiple match.
8
+ *
9
+ * @since 0.3.16-canary.0
10
+ */
11
+ export function selectBinding(
12
+ bindings: ReadonlyArray<Binding>,
13
+ hint: ResolveOptions | undefined,
14
+ ctx: ConstraintContext,
15
+ tokenDisplayName: string,
16
+ ): Binding | undefined {
17
+ const candidates = filterBindings(bindings, hint, ctx);
18
+ if (candidates.length === 0) {
19
+ return undefined;
20
+ }
21
+ if (candidates.length === 1) {
22
+ return candidates[0];
23
+ }
24
+ // Multiple candidates — check if any is unambiguous (slot-based selection)
25
+ // Slot-based bindings already have last-wins applied in registry, so
26
+ // multiple candidates here means ambiguous predicate-only bindings
27
+ throw new AmbiguousBindingError(
28
+ tokenDisplayName,
29
+ candidates.map((c) => c.id),
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Select all candidates matching hint + predicates.
35
+ *
36
+ * @since 0.3.16-canary.0
37
+ */
38
+ export function selectAllBindings(
39
+ bindings: ReadonlyArray<Binding>,
40
+ hint: ResolveOptions | undefined,
41
+ ctx: ConstraintContext,
42
+ ): Array<Binding> {
43
+ return filterBindings(bindings, hint, ctx, "all");
44
+ }
45
+
46
+ function filterBindings(
47
+ bindings: ReadonlyArray<Binding>,
48
+ hint: ResolveOptions | undefined,
49
+ ctx: ConstraintContext,
50
+ selectionMode: "single" | "all" = "single",
51
+ ): Array<Binding> {
52
+ if (hint === undefined) {
53
+ const resultWithoutHint: Array<Binding> = [];
54
+ if (selectionMode === "all") {
55
+ for (const binding of bindings) {
56
+ if (matchesPredicate(binding, ctx)) {
57
+ resultWithoutHint.push(binding);
58
+ }
59
+ }
60
+ } else {
61
+ for (const binding of bindings) {
62
+ const slot = binding.slot;
63
+ if (slot.name === undefined && slot.tags.length === 0 && matchesPredicate(binding, ctx)) {
64
+ resultWithoutHint.push(binding);
65
+ }
66
+ }
67
+ }
68
+ return resultWithoutHint;
69
+ }
70
+
71
+ const result: Array<Binding> = [];
72
+ for (const binding of bindings) {
73
+ const slotMatched = selectionMode === "all" ? matchesSlotForResolveAll(binding, hint) : matchesSlot(binding, hint);
74
+ if (slotMatched && matchesPredicate(binding, ctx)) {
75
+ result.push(binding);
76
+ }
77
+ }
78
+ return result;
79
+ }
80
+
81
+ function matchesSlotForResolveAll(binding: Binding, hint: ResolveOptions | undefined): boolean {
82
+ const hasExplicitSlotFilter =
83
+ hint !== undefined &&
84
+ (hint.name !== undefined || (hint.tags !== undefined && hint.tags.length > 0) || hint.tag !== undefined);
85
+ if (!hasExplicitSlotFilter) {
86
+ return true;
87
+ }
88
+ return matchesSlot(binding, hint);
89
+ }
90
+
91
+ function matchesSlot(binding: Binding, hint: ResolveOptions | undefined): boolean {
92
+ const slot = binding.slot;
93
+ const hintName = hint?.name;
94
+ const hintTags = hint?.tags;
95
+ const singleHintTag = hint?.tag;
96
+ const hasHintTags = (hintTags?.length ?? 0) > 0 || singleHintTag !== undefined;
97
+
98
+ // Match by name
99
+ if (slot.name !== undefined) {
100
+ if (hintName === undefined) {
101
+ return false;
102
+ }
103
+ if (slot.name !== hintName) {
104
+ return false;
105
+ }
106
+ } else if (hintName !== undefined) {
107
+ // Binding has no name but hint requests a specific name — no match
108
+ return false;
109
+ }
110
+
111
+ // Match by tags — binding's tags must all be present in hint
112
+ if (slot.tags.length > 0) {
113
+ if (!hasHintTags) {
114
+ return false;
115
+ }
116
+ for (const [tagKey, tagValue] of slot.tags) {
117
+ if (!matchHintTag(tagKey, tagValue, hintTags, singleHintTag)) {
118
+ return false;
119
+ }
120
+ }
121
+ } else if (hasHintTags) {
122
+ // Binding has no tags but hint requires tags — no match for tagged slots
123
+ // But: default slot (no tags, no name) can match when hint has tags if there are no tag-slotted bindings
124
+ // Actually per spec: resolveAll with tags only returns bindings that have those tags
125
+ // and resolve with tags requires exact match
126
+ return false;
127
+ }
128
+
129
+ return true;
130
+ }
131
+
132
+ function matchHintTag(
133
+ tagKey: string,
134
+ tagValue: unknown,
135
+ hintTags: ReadonlyArray<BindingTag> | undefined,
136
+ singleHintTag: BindingTag | undefined,
137
+ ): boolean {
138
+ if (singleHintTag !== undefined && singleHintTag[0] === tagKey && Object.is(singleHintTag[1], tagValue)) {
139
+ return true;
140
+ }
141
+ if (hintTags === undefined || hintTags.length === 0) {
142
+ return false;
143
+ }
144
+ for (let index = 0; index < hintTags.length; index += 1) {
145
+ const hintTag = hintTags[index]!;
146
+ if (hintTag[0] === tagKey && Object.is(hintTag[1], tagValue)) {
147
+ return true;
148
+ }
149
+ }
150
+ return false;
151
+ }
152
+
153
+ function matchesPredicate(binding: Binding, ctx: ConstraintContext): boolean {
154
+ if (binding.predicate === undefined) {
155
+ return true;
156
+ }
157
+ return binding.predicate(ctx);
158
+ }
package/src/binding.ts ADDED
@@ -0,0 +1,277 @@
1
+ import type { InjectionDescriptor } from "#/decorators/inject";
2
+ import type { Token } from "#/token";
3
+ import type {
4
+ ActivationHandler,
5
+ BindingIdentifier,
6
+ BindingScope,
7
+ BindingTag,
8
+ Constructor,
9
+ DeactivationHandler,
10
+ DependencyKey,
11
+ ResolutionContext,
12
+ TokenValue,
13
+ ConstraintContext,
14
+ } from "#/types";
15
+
16
+ // ── BindingSlot ───────────────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * @since 0.3.16-canary.0
20
+ */
21
+ export interface BindingSlot {
22
+ readonly name: string | undefined;
23
+ readonly tags: ReadonlyArray<BindingTag>;
24
+ }
25
+
26
+ /**
27
+ * @since 0.3.16-canary.0
28
+ */
29
+ export function bindingSlotEquals(left: BindingSlot, right: BindingSlot): boolean {
30
+ if (left.name !== right.name) {
31
+ return false;
32
+ }
33
+ if (left.tags.length !== right.tags.length) {
34
+ return false;
35
+ }
36
+ for (const [tagKey, tagValue] of left.tags) {
37
+ if (!right.tags.some(([otherKey, otherValue]) => otherKey === tagKey && Object.is(otherValue, tagValue))) {
38
+ return false;
39
+ }
40
+ }
41
+ return true;
42
+ }
43
+
44
+ /**
45
+ * @since 0.3.16-canary.0
46
+ */
47
+ export const DEFAULT_BINDING_SLOT = { name: undefined, tags: [] } satisfies BindingSlot;
48
+
49
+ /**
50
+ * @since 0.3.16-canary.0
51
+ */
52
+ export function bindingSlotToString(slot: BindingSlot): string {
53
+ if (slot.name === undefined && slot.tags.length === 0) {
54
+ return "default";
55
+ }
56
+ const parts: Array<string> = [];
57
+ if (slot.name !== undefined) {
58
+ parts.push(`name:${slot.name}`);
59
+ }
60
+ for (const [tagKey, tagValue] of slot.tags) {
61
+ parts.push(`tag:${tagKey}=${String(tagValue)}`);
62
+ }
63
+ return parts.join(",");
64
+ }
65
+
66
+ // ── BindingBase ───────────────────────────────────────────────────────────────
67
+
68
+ interface BindingBase<Value> {
69
+ readonly id: BindingIdentifier;
70
+ readonly token: Token<Value> | Constructor<Value>;
71
+ readonly slot: BindingSlot;
72
+ readonly predicate?: (ctx: ConstraintContext) => boolean;
73
+ }
74
+
75
+ type BindingBaseKeys = keyof BindingBase<unknown>;
76
+
77
+ // ── Binding kinds ─────────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * @since 0.3.16-canary.0
81
+ */
82
+ export interface ClassBinding<Value> extends BindingBase<Value> {
83
+ readonly kind: "class";
84
+ readonly target: Constructor<Value>;
85
+ readonly scope: BindingScope;
86
+ readonly onActivation?: ActivationHandler<Value>;
87
+ readonly onDeactivation?: DeactivationHandler<Value>;
88
+ }
89
+
90
+ /**
91
+ * @since 0.3.16-canary.0
92
+ */
93
+ export interface DynamicBinding<Value> extends BindingBase<Value> {
94
+ readonly kind: "dynamic";
95
+ readonly factory: (ctx: ResolutionContext) => Value;
96
+ readonly scope: BindingScope;
97
+ readonly onActivation?: ActivationHandler<Value>;
98
+ readonly onDeactivation?: DeactivationHandler<Value>;
99
+ }
100
+
101
+ /**
102
+ * @since 0.3.16-canary.0
103
+ */
104
+ export interface DynamicAsyncBinding<Value> extends BindingBase<Value> {
105
+ readonly kind: "dynamic-async";
106
+ readonly factory: (ctx: ResolutionContext) => Promise<Value>;
107
+ readonly scope: BindingScope;
108
+ readonly onActivation?: ActivationHandler<Value>;
109
+ readonly onDeactivation?: DeactivationHandler<Value>;
110
+ }
111
+
112
+ /**
113
+ * @since 0.3.16-canary.0
114
+ */
115
+ export interface ResolvedBinding<Value> extends BindingBase<Value> {
116
+ readonly kind: "resolved";
117
+ readonly factory: (...args: Array<unknown>) => Value;
118
+ readonly deps: ReadonlyArray<InjectionDescriptor>;
119
+ readonly scope: BindingScope;
120
+ readonly onActivation?: ActivationHandler<Value>;
121
+ readonly onDeactivation?: DeactivationHandler<Value>;
122
+ }
123
+
124
+ /**
125
+ * @since 0.3.16-canary.0
126
+ */
127
+ export interface ResolvedAsyncBinding<Value> extends BindingBase<Value> {
128
+ readonly kind: "resolved-async";
129
+ readonly factory: (...args: Array<unknown>) => Promise<Value>;
130
+ readonly deps: ReadonlyArray<InjectionDescriptor>;
131
+ readonly scope: BindingScope;
132
+ readonly onActivation?: ActivationHandler<Value>;
133
+ readonly onDeactivation?: DeactivationHandler<Value>;
134
+ }
135
+
136
+ /**
137
+ * @since 0.3.16-canary.0
138
+ */
139
+ export interface ConstantBinding<Value> extends BindingBase<Value> {
140
+ readonly kind: "constant";
141
+ readonly value: Value;
142
+ readonly scope: "singleton";
143
+ readonly onActivation?: ActivationHandler<Value>;
144
+ readonly onDeactivation?: DeactivationHandler<Value>;
145
+ }
146
+
147
+ /**
148
+ * @since 0.3.16-canary.0
149
+ */
150
+ export interface AliasBinding<Value> extends BindingBase<Value> {
151
+ readonly kind: "alias";
152
+ readonly target: Token<Value> | Constructor<Value>;
153
+ }
154
+
155
+ /**
156
+ * @since 0.3.16-canary.0
157
+ */
158
+ export type Binding<Value = unknown> =
159
+ | ClassBinding<Value>
160
+ | DynamicBinding<Value>
161
+ | DynamicAsyncBinding<Value>
162
+ | ResolvedBinding<Value>
163
+ | ResolvedAsyncBinding<Value>
164
+ | ConstantBinding<Value>
165
+ | AliasBinding<Value>;
166
+
167
+ /**
168
+ * Builder-only payload before `id`, `token`, `slot`, and `predicate` are applied.
169
+ *
170
+ * @since 0.3.16-canary.0
171
+ */
172
+ export type PartialBinding<Value> =
173
+ | Omit<ClassBinding<Value>, BindingBaseKeys>
174
+ | Omit<DynamicBinding<Value>, BindingBaseKeys>
175
+ | Omit<DynamicAsyncBinding<Value>, BindingBaseKeys>
176
+ | Omit<ResolvedBinding<Value>, BindingBaseKeys>
177
+ | Omit<ResolvedAsyncBinding<Value>, BindingBaseKeys>
178
+ | Omit<ConstantBinding<Value>, BindingBaseKeys>
179
+ | Omit<AliasBinding<Value>, BindingBaseKeys>;
180
+
181
+ // ── ID generation ─────────────────────────────────────────────────────────────
182
+
183
+ let _idCounter = 0;
184
+ /**
185
+ * @since 0.3.16-canary.0
186
+ */
187
+ export function generateBindingId(): BindingIdentifier {
188
+ return String(++_idCounter) as BindingIdentifier;
189
+ }
190
+
191
+ // ── Builder interfaces ────────────────────────────────────────────────────────
192
+
193
+ /**
194
+ * Common slot-constraint + id methods shared by all concrete binding builders.
195
+ *
196
+ * @since 0.3.16-canary.0
197
+ */
198
+ export interface SlotConstrainedBuilder {
199
+ when(predicate: (ctx: ConstraintContext) => boolean): this;
200
+ whenNamed(name: string): this;
201
+ whenTagged(tag: string, value: unknown): this;
202
+ whenDefault(): this;
203
+ id(): BindingIdentifier;
204
+ }
205
+
206
+ /**
207
+ * @since 0.3.16-canary.0
208
+ */
209
+ export interface BindToBuilder<Value> {
210
+ to(type: Constructor<Value>): BindingBuilder<Value>;
211
+ toSelf(): BindingBuilder<Value>;
212
+ toConstantValue(value: Value): ConstantBindingBuilder<Value>;
213
+ toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value>;
214
+ toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value>;
215
+ toResolved<const Deps extends ReadonlyArray<DependencyKey>>(
216
+ factory: (...args: { [K in keyof Deps]: TokenValue<NoInfer<Deps>[K]> }) => Value,
217
+ deps: Deps,
218
+ ): BindingBuilder<Value>;
219
+ toResolvedAsync<const Deps extends ReadonlyArray<DependencyKey>>(
220
+ factory: (...args: { [K in keyof Deps]: TokenValue<NoInfer<Deps>[K]> }) => Promise<Value>,
221
+ deps: Deps,
222
+ ): BindingBuilder<Value>;
223
+ toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder;
224
+ }
225
+
226
+ /**
227
+ * @since 0.3.16-canary.0
228
+ */
229
+ export interface BindingBuilder<Value> extends SlotConstrainedBuilder {
230
+ singleton(): SingletonBindingBuilder<Value>;
231
+ transient(): TransientBindingBuilder<Value>;
232
+ scoped(): ScopedBindingBuilder<Value>;
233
+ }
234
+
235
+ /**
236
+ * @since 0.3.16-canary.0
237
+ */
238
+ export interface ConstantBindingBuilder<Value> extends SlotConstrainedBuilder {
239
+ onActivation(fn: ActivationHandler<Value>): SingletonLifecycleBuilder<Value>;
240
+ onDeactivation(fn: DeactivationHandler<Value>): SingletonLifecycleBuilder<Value>;
241
+ }
242
+
243
+ /**
244
+ * @since 0.3.16-canary.0
245
+ */
246
+ export interface AliasBindingBuilder extends SlotConstrainedBuilder {}
247
+
248
+ /**
249
+ * @since 0.3.16-canary.0
250
+ */
251
+ export interface SingletonBindingBuilder<Value> {
252
+ onActivation(fn: ActivationHandler<Value>): this;
253
+ onDeactivation(fn: DeactivationHandler<Value>): this;
254
+ id(): BindingIdentifier;
255
+ }
256
+
257
+ /**
258
+ * @since 0.3.16-canary.0
259
+ */
260
+ export interface TransientBindingBuilder<Value> {
261
+ onActivation(fn: ActivationHandler<Value>): this;
262
+ id(): BindingIdentifier;
263
+ }
264
+
265
+ /**
266
+ * @since 0.3.16-canary.0
267
+ */
268
+ export interface ScopedBindingBuilder<Value> extends TransientBindingBuilder<Value> {}
269
+
270
+ /**
271
+ * @since 0.3.16-canary.0
272
+ */
273
+ export interface SingletonLifecycleBuilder<Value> {
274
+ onActivation(fn: ActivationHandler<Value>): this;
275
+ onDeactivation(fn: DeactivationHandler<Value>): this;
276
+ id(): BindingIdentifier;
277
+ }
@@ -0,0 +1,121 @@
1
+ import type { Token } from "#/token";
2
+ import { tokenName } from "#/token";
3
+ import type { BindingTag, ConstraintContext, Constructor } from "#/types";
4
+
5
+ function tokenNameOf(token: Token<unknown> | Constructor): string {
6
+ return tokenName(token);
7
+ }
8
+
9
+ /**
10
+ * @since 0.3.16-canary.0
11
+ */
12
+ export function whenParentIs(token: Token<unknown> | Constructor): (constraintContext: ConstraintContext) => boolean {
13
+ const tokenDisplayName = tokenNameOf(token);
14
+ return (constraintContext) =>
15
+ constraintContext.parent !== undefined && constraintContext.parent.tokenName === tokenDisplayName;
16
+ }
17
+
18
+ /**
19
+ * @since 0.3.16-canary.0
20
+ */
21
+ export function whenNoParentIs(token: Token<unknown> | Constructor): (constraintContext: ConstraintContext) => boolean {
22
+ const tokenDisplayName = tokenNameOf(token);
23
+ return (constraintContext) =>
24
+ constraintContext.parent === undefined || constraintContext.parent.tokenName !== tokenDisplayName;
25
+ }
26
+
27
+ /**
28
+ * @since 0.3.16-canary.0
29
+ */
30
+ export function whenAnyAncestorIs(
31
+ token: Token<unknown> | Constructor,
32
+ ): (constraintContext: ConstraintContext) => boolean {
33
+ const tokenDisplayName = tokenNameOf(token);
34
+ return (constraintContext) =>
35
+ constraintContext.ancestors.some((ancestorFrame) => ancestorFrame.tokenName === tokenDisplayName);
36
+ }
37
+
38
+ /**
39
+ * @since 0.3.16-canary.0
40
+ */
41
+ export function whenNoAncestorIs(
42
+ token: Token<unknown> | Constructor,
43
+ ): (constraintContext: ConstraintContext) => boolean {
44
+ const tokenDisplayName = tokenNameOf(token);
45
+ return (constraintContext) =>
46
+ constraintContext.ancestors.every((ancestorFrame) => ancestorFrame.tokenName !== tokenDisplayName);
47
+ }
48
+
49
+ /**
50
+ * @since 0.3.16-canary.0
51
+ */
52
+ export function whenParentNamed(name: string): (constraintContext: ConstraintContext) => boolean {
53
+ return (constraintContext) => constraintContext.parent !== undefined && constraintContext.parent.slot.name === name;
54
+ }
55
+
56
+ /**
57
+ * @since 0.3.16-canary.0
58
+ */
59
+ export function whenAnyAncestorNamed(name: string): (constraintContext: ConstraintContext) => boolean {
60
+ return (constraintContext) => constraintContext.ancestors.some((ancestorFrame) => ancestorFrame.slot.name === name);
61
+ }
62
+
63
+ /**
64
+ * @since 0.3.16-canary.0
65
+ */
66
+ export function whenParentTagged(tag: string, value: unknown): (constraintContext: ConstraintContext) => boolean {
67
+ return (constraintContext) =>
68
+ constraintContext.parent !== undefined &&
69
+ constraintContext.parent.slot.tags.some(([tagKey, tagValue]) => tagKey === tag && Object.is(tagValue, value));
70
+ }
71
+
72
+ /**
73
+ * @since 0.3.16-canary.0
74
+ */
75
+ export function whenAnyAncestorTagged(tag: string, value: unknown): (constraintContext: ConstraintContext) => boolean {
76
+ return (constraintContext) =>
77
+ constraintContext.ancestors.some((ancestorFrame) =>
78
+ ancestorFrame.slot.tags.some(([tagKey, tagValue]) => tagKey === tag && Object.is(tagValue, value)),
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Matches when the direct parent slot carries **all** of the given tag pairs.
84
+ * Equivalent to AND-composing multiple `whenParentTagged` calls but evaluates
85
+ * in a single predicate invocation — no intermediate closure allocations.
86
+ *
87
+ * @since 0.3.16-canary.1
88
+ */
89
+ export function whenParentTaggedAll(
90
+ tags: ReadonlyArray<BindingTag>,
91
+ ): (constraintContext: ConstraintContext) => boolean {
92
+ return (constraintContext) => {
93
+ const { parent } = constraintContext;
94
+ if (parent === undefined) {
95
+ return false;
96
+ }
97
+ const { tags: parentTags } = parent.slot;
98
+ return tags.every(([tagKey, tagValue]) =>
99
+ parentTags.some(([otherKey, otherValue]) => otherKey === tagKey && Object.is(otherValue, tagValue)),
100
+ );
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Matches when at least one ancestor slot carries **all** of the given tag pairs.
106
+ * Equivalent to AND-composing multiple `whenAnyAncestorTagged` calls but evaluates
107
+ * in a single predicate invocation — no intermediate closure allocations.
108
+ *
109
+ * @since 0.3.16-canary.1
110
+ */
111
+ export function whenAnyAncestorTaggedAll(
112
+ tags: ReadonlyArray<BindingTag>,
113
+ ): (constraintContext: ConstraintContext) => boolean {
114
+ return (constraintContext) =>
115
+ constraintContext.ancestors.some((frame) => {
116
+ const { tags: frameTags } = frame.slot;
117
+ return tags.every(([tagKey, tagValue]) =>
118
+ frameTags.some(([otherKey, otherValue]) => otherKey === tagKey && Object.is(otherValue, tagValue)),
119
+ );
120
+ });
121
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A class (newable) that produces `Value`. Rest parameters are `never[]` so
3
+ * real classes with typed constructors remain assignable under
4
+ * `strictFunctionTypes` (unlike `unknown[]`, which is not assignable from
5
+ * narrower parameter types). Runtime construction still uses the real shape;
6
+ * this alias is the DI “class token” surface only.
7
+ *
8
+ * @since 0.3.16-canary.0
9
+ */
10
+ export type Constructor<Value = unknown> = new (...args: Array<never>) => Value;
11
+
12
+ /**
13
+ * Class constructor as invoked by the resolver after metadata-driven
14
+ * resolution of `unknown[]` dependencies — separate from {@link Constructor},
15
+ * which is the public assignable class token.
16
+ *
17
+ * @since 0.3.16-canary.0
18
+ */
19
+ export type ConstructorInvocation = new (...args: Array<unknown>) => unknown;