@codefast/di 0.8.1 → 0.10.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 (56) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/LICENSE +1 -1
  3. package/README.md +29 -20
  4. package/dist/ambient/active-container.d.ts +13 -0
  5. package/dist/ambient/active-container.js +24 -0
  6. package/dist/container/binding-builders.d.ts +42 -20
  7. package/dist/container/binding-builders.js +155 -87
  8. package/dist/container/container.d.ts +10 -10
  9. package/dist/container/container.js +55 -23
  10. package/dist/core/binding.d.ts +55 -57
  11. package/dist/core/binding.js +11 -37
  12. package/dist/core/constraint-requirement.d.ts +19 -4
  13. package/dist/core/constraint-requirement.js +19 -9
  14. package/dist/core/registry.d.ts +47 -4
  15. package/dist/core/registry.js +361 -159
  16. package/dist/core/state-epoch.d.ts +16 -0
  17. package/dist/core/state-epoch.js +21 -0
  18. package/dist/core/tag.d.ts +2 -2
  19. package/dist/core/tag.js +2 -2
  20. package/dist/core/token.d.ts +24 -4
  21. package/dist/core/token.js +1 -1
  22. package/dist/core/types.d.ts +15 -6
  23. package/dist/decorators/inject.d.ts +1 -1
  24. package/dist/decorators/inject.js +6 -4
  25. package/dist/errors/diagnostics.d.ts +2 -0
  26. package/dist/errors/errors.d.ts +33 -1
  27. package/dist/errors/errors.js +44 -3
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.js +1 -1
  30. package/dist/injection/descriptor.d.ts +16 -8
  31. package/dist/injection/descriptor.js +3 -1
  32. package/dist/injection/resolve-options.d.ts +6 -0
  33. package/dist/injection/resolve-options.js +16 -0
  34. package/dist/introspection/dependency-graph.js +10 -5
  35. package/dist/introspection/inspector.d.ts +3 -1
  36. package/dist/introspection/inspector.js +10 -24
  37. package/dist/lifecycle/lifecycle-manager.js +10 -8
  38. package/dist/lifecycle/scope-manager.js +1 -1
  39. package/dist/metadata/metadata-reader-token.js +1 -1
  40. package/dist/resolution/cache/activation-need.d.ts +2 -0
  41. package/dist/resolution/cache/activation-need.js +13 -6
  42. package/dist/resolution/cache/binding-lookup-cache.d.ts +34 -1
  43. package/dist/resolution/cache/binding-lookup-cache.js +96 -12
  44. package/dist/resolution/cache/class-introspector.d.ts +1 -1
  45. package/dist/resolution/cache/class-introspector.js +28 -14
  46. package/dist/resolution/context.d.ts +8 -8
  47. package/dist/resolution/plan/instantiation-plan.d.ts +12 -0
  48. package/dist/resolution/plan/instantiation-plan.js +156 -65
  49. package/dist/resolution/plan/plan-codegen.d.ts +100 -0
  50. package/dist/resolution/plan/plan-codegen.js +185 -0
  51. package/dist/resolution/resolver.d.ts +14 -4
  52. package/dist/resolution/resolver.js +375 -134
  53. package/dist/resolution/select/binding-select.js +12 -7
  54. package/dist/resolution/select/constraints.d.ts +7 -4
  55. package/dist/resolution/select/constraints.js +34 -13
  56. package/package.json +14 -40
@@ -0,0 +1,185 @@
1
+ import { NO_INSTANCE } from "#/core/binding";
2
+ /**
3
+ * Runs a plan's closure makes before the plan is generated as its own function.
4
+ *
5
+ * @remarks Below it a plan stays a closure, which is all a cold container or a per-request child
6
+ * ever runs; above it a plan pays one compile for call sites nothing else feeds.
7
+ *
8
+ * @since 0.10.0
9
+ */
10
+ export const PLAN_CODEGEN_THRESHOLD = 32;
11
+ const rejectWith = (error) => Promise.reject(error);
12
+ let codegenAvailable;
13
+ let generatedCount = 0;
14
+ /**
15
+ * Whether this runtime lets the engine compile a function from source.
16
+ *
17
+ * @remarks A Content Security Policy without `unsafe-eval` refuses the `Function` constructor; every
18
+ * plan then stays a closure, which behaves identically.
19
+ *
20
+ * @since 0.10.0
21
+ */
22
+ export function isPlanCodegenAvailable() {
23
+ if (codegenAvailable === undefined) {
24
+ try {
25
+ // The one probe of the constructor this module exists to use; a refusal here disables it for good.
26
+ // oxlint-disable-next-line typescript/no-implied-eval
27
+ codegenAvailable = new Function("return true")() === true;
28
+ }
29
+ catch {
30
+ codegenAvailable = false;
31
+ }
32
+ }
33
+ return codegenAvailable;
34
+ }
35
+ /**
36
+ * Generates a plan as a function of its own, or `null` when the runtime refuses to compile one.
37
+ *
38
+ * @remarks The source carries a serial so no two plans share a compilation-cache entry: V8 keys
39
+ * type feedback by function literal, and one literal per plan is the whole point.
40
+ *
41
+ * @since 0.10.0
42
+ */
43
+ export function generatePlan(node) {
44
+ if (!isPlanCodegenAvailable()) {
45
+ return null;
46
+ }
47
+ const emitter = new PlanEmitter();
48
+ return compileRendered(emitter, emitter.expression(node));
49
+ }
50
+ /**
51
+ * Generates an async plan as a function of its own, or `null` when the runtime refuses to compile one.
52
+ *
53
+ * @since 0.10.0
54
+ */
55
+ export function generateAsyncPlan(node) {
56
+ if (!isPlanCodegenAvailable()) {
57
+ return null;
58
+ }
59
+ const emitter = new PlanEmitter();
60
+ return compileRendered(emitter, emitter.asyncExpression(node));
61
+ }
62
+ function compileRendered(emitter, expression) {
63
+ generatedCount += 1;
64
+ const locals = emitter.locals.length === 0 ? "" : `let ${emitter.locals.join(", ")};`;
65
+ const body = `"use strict";/* plan ${String(generatedCount)} */${emitter.hoisted.join("")}return () => {${locals}return ${expression};};`;
66
+ try {
67
+ // Compiling from source is the mechanism: one function literal per plan is what gives it its own feedback.
68
+ // oxlint-disable-next-line typescript/no-implied-eval
69
+ const factory = new Function(...emitter.names, body);
70
+ return factory(...emitter.values);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Renders a plan tree as one expression over parameters that carry every value the plan closes over.
78
+ *
79
+ * @remarks A node that awaits its dependencies renders as an inner function of the same source, so
80
+ * every plan's awaiting nodes have call sites of their own too.
81
+ */
82
+ class PlanEmitter {
83
+ names = [];
84
+ values = [];
85
+ hoisted = [];
86
+ #localsByFunction = [[]];
87
+ #hoistedCount = 0;
88
+ #slotByValue = new Map();
89
+ /** The plan function's own temporaries. */
90
+ get locals() {
91
+ return this.#localsByFunction[0];
92
+ }
93
+ expression(node) {
94
+ switch (node.kind) {
95
+ case "construct":
96
+ return `new ${this.#slot(node.target, "C")}(${this.#list(node.deps)})`;
97
+ case "accessors":
98
+ return `${this.#slot(node.construct, "A")}([${this.#list(node.deps)}])`;
99
+ case "call": {
100
+ // The settle only ever throws, so it runs on the promise branch alone and the plain result returns as is.
101
+ const local = this.#local();
102
+ const call = `${this.#slot(node.factory, "F")}(${this.#list(node.deps)})`;
103
+ return `((${local} = ${call}) instanceof ${this.#slot(Promise, "P")} ? ${this.#slot(node.settle, "S")}(${local}) : ${local})`;
104
+ }
105
+ case "value":
106
+ return this.#slot(node.value, "V");
107
+ case "singleton":
108
+ return this.#singletonRead(node.binding, node.escape);
109
+ case "thunk":
110
+ return `${this.#slot(node.run, "T")}()`;
111
+ }
112
+ }
113
+ asyncExpression(node) {
114
+ switch (node.kind) {
115
+ case "construct": {
116
+ const target = this.#slot(node.target, "C");
117
+ return node.awaits
118
+ ? this.#settled(node.deps, (values) => `new ${target}(${values})`)
119
+ : `new ${target}(${this.#asyncList(node.deps)})`;
120
+ }
121
+ case "call": {
122
+ const factory = this.#slot(node.factory, "F");
123
+ return node.awaits
124
+ ? this.#settled(node.deps, (values) => `${factory}(${values})`)
125
+ : `${factory}(${this.#asyncList(node.deps)})`;
126
+ }
127
+ case "value":
128
+ return this.#slot(node.value, "V");
129
+ case "singleton":
130
+ return this.#singletonRead(node.binding, node.escape);
131
+ case "thunk":
132
+ return `${this.#slot(node.run, "T")}()`;
133
+ }
134
+ }
135
+ #list(deps) {
136
+ return deps.map((dep) => this.expression(dep)).join(", ");
137
+ }
138
+ #asyncList(deps) {
139
+ return deps.map((dep) => this.asyncExpression(dep)).join(", ");
140
+ }
141
+ // Every dependency starts in order, a sync throw becomes that slot's rejection so its siblings still
142
+ // start, and the node applies to the settled values — the interpreted async path, rendered.
143
+ #settled(deps, apply) {
144
+ const index = this.#hoistedCount;
145
+ this.#hoistedCount += 1;
146
+ const name = `n${String(index)}`;
147
+ const applyName = `a${String(index)}`;
148
+ const reject = this.#slot(rejectWith, "R");
149
+ const promise = this.#slot(Promise, "P");
150
+ this.#localsByFunction.push([]);
151
+ const pendings = [];
152
+ const statements = [];
153
+ for (let position = 0; position < deps.length; position += 1) {
154
+ const pending = `p${String(position)}`;
155
+ pendings.push(pending);
156
+ statements.push(`try{${pending}=${this.asyncExpression(deps[position])};}catch(e){${pending}=${reject}(e);}`);
157
+ }
158
+ const locals = [...pendings, ...this.#localsByFunction.pop()];
159
+ const values = deps.map((_dep, position) => `v[${String(position)}]`).join(",");
160
+ this.hoisted.push(`const ${applyName}=(v)=>${apply(values)};const ${name}=()=>{let ${locals.join(",")};${statements.join("")}return ${promise}.all([${pendings.join(",")}]).then(${applyName});};`);
161
+ return `${name}()`;
162
+ }
163
+ #local() {
164
+ const locals = this.#localsByFunction.at(-1);
165
+ const local = `t${String(locals.length)}`;
166
+ locals.push(local);
167
+ return local;
168
+ }
169
+ #singletonRead(binding, escape) {
170
+ const local = this.#local();
171
+ const slot = this.#slot(binding, "B");
172
+ return `((${local} = ${slot}.instance) === ${this.#slot(NO_INSTANCE, "N")} ? ${this.#slot(escape, "E")}() : ${local})`;
173
+ }
174
+ // One parameter per distinct value, so a class constructed four times is one constructor with four sites.
175
+ #slot(value, prefix) {
176
+ let name = this.#slotByValue.get(value);
177
+ if (name === undefined) {
178
+ name = `${prefix}${String(this.names.length)}`;
179
+ this.#slotByValue.set(value, name);
180
+ this.names.push(name);
181
+ this.values.push(value);
182
+ }
183
+ return name;
184
+ }
185
+ }
@@ -29,8 +29,8 @@ export declare class DependencyResolver implements ResolverCallbacks {
29
29
  constructor(registry: BindingRegistry, scope: ScopeManager, lifecycle: LifecycleManager, metadataReader: MetadataReader, container: Container, parent: DependencyResolver | undefined);
30
30
  /** The reader this resolver was built with, which is the one its container answers with. */
31
31
  get metadataReader(): MetadataReader;
32
- /** Structural counts for the {@link ResolutionDiagnostics} a container reports. */
33
- describeCaches(): Pick<ResolutionDiagnostics, "compiledPlanCount" | "compiledAsyncPlanCount" | "syncContextPoolSize">;
32
+ /** Structural counts and the resolver-owned collaborators built so far, for the {@link ResolutionDiagnostics} a container reports. */
33
+ describeCaches(): Pick<ResolutionDiagnostics, "compiledPlanCount" | "compiledAsyncPlanCount" | "generatedPlanCount" | "syncContextPoolSize" | "builtSubsystems">;
34
34
  /**
35
35
  * Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
36
36
  */
@@ -42,7 +42,17 @@ export declare class DependencyResolver implements ResolverCallbacks {
42
42
  resolveFromContext<Value>(token: Token<Value> | Constructor<Value>, resolutionStack: Array<ResolutionFrame>): Value;
43
43
  resolve<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>, precomputedCriterion?: BindingTag | null): Value;
44
44
  resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>, precomputedCriterion?: BindingTag | null): Value | undefined;
45
- resolveAll<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>): Array<Value>;
45
+ resolveAll<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>): ReadonlyArray<Value>;
46
+ /**
47
+ * A root-level, options-less collection read through its memo: the value list when it is stable,
48
+ * the candidate list otherwise.
49
+ *
50
+ * @remarks Its own entry rather than a branch in `resolveAll`, so the options lane keeps the exact
51
+ * shape it had; the container routes a top-level read with no options here.
52
+ */
53
+ resolveRootCollection<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<Value>;
54
+ /** The async twin of `resolveRootCollection`: a stable value list settles at once, candidates fan out as usual. */
55
+ resolveRootCollectionAsync<Value>(token: Token<Value> | Constructor<Value>): Promise<ReadonlyArray<Value>>;
46
56
  resolveAsyncFromContext<Value>(token: Token<Value> | Constructor<Value>, resolutionStack: Array<ResolutionFrame>, branchDepth: BranchDepth): Promise<Value>;
47
57
  /**
48
58
  * Instantiates one owned binding directly, bypassing selection.
@@ -53,7 +63,7 @@ export declare class DependencyResolver implements ResolverCallbacks {
53
63
  warmBindingAsync(binding: Binding, options: ResolveOptions | undefined): Promise<unknown>;
54
64
  resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>, branchDepth?: BranchDepth, precomputedCriterion?: BindingTag | null): Promise<Value>;
55
65
  resolveOptionalAsync<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>, branchDepth?: BranchDepth, precomputedCriterion?: BindingTag | null): Promise<Value | undefined>;
56
- resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>, branchDepth?: BranchDepth): Promise<Array<Value>>;
66
+ resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options: ResolveOptions | undefined, resolutionStack: Array<ResolutionFrame>, branchDepth?: BranchDepth): Promise<ReadonlyArray<Value>>;
57
67
  /**
58
68
  * Entry for a request a factory makes from inside an open synchronous cascade.
59
69
  *