@codefast/di 0.9.0 → 0.10.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +6 -5
  3. package/dist/ambient/active-container.d.ts +14 -4
  4. package/dist/ambient/active-container.js +22 -1
  5. package/dist/container/binding-builders.d.ts +35 -13
  6. package/dist/container/binding-builders.js +174 -97
  7. package/dist/container/container.d.ts +10 -10
  8. package/dist/container/container.js +53 -36
  9. package/dist/core/binding-scope.d.ts +2 -2
  10. package/dist/core/binding.d.ts +42 -49
  11. package/dist/core/binding.js +12 -38
  12. package/dist/core/constraint-requirement.d.ts +1 -1
  13. package/dist/core/module.d.ts +3 -3
  14. package/dist/core/registry.d.ts +52 -9
  15. package/dist/core/registry.js +376 -162
  16. package/dist/core/state-epoch.d.ts +16 -0
  17. package/dist/core/state-epoch.js +21 -0
  18. package/dist/core/token.d.ts +1 -1
  19. package/dist/core/types.d.ts +12 -9
  20. package/dist/decorators/inject.d.ts +3 -3
  21. package/dist/decorators/inject.js +5 -5
  22. package/dist/decorators/injectable.d.ts +2 -2
  23. package/dist/decorators/injectable.js +2 -2
  24. package/dist/decorators/lifecycle-decorators.js +2 -2
  25. package/dist/errors/diagnostics.d.ts +2 -0
  26. package/dist/errors/errors.d.ts +29 -3
  27. package/dist/errors/errors.js +34 -2
  28. package/dist/index.d.ts +35 -35
  29. package/dist/index.js +19 -19
  30. package/dist/injection/descriptor.d.ts +9 -7
  31. package/dist/injection/descriptor.js +3 -1
  32. package/dist/injection/resolve-options.d.ts +9 -3
  33. package/dist/injection/resolve-options.js +17 -1
  34. package/dist/introspection/dependency-graph.d.ts +3 -3
  35. package/dist/introspection/dependency-graph.js +15 -10
  36. package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -1
  37. package/dist/introspection/graph-adapters/dot.d.ts +1 -1
  38. package/dist/introspection/graph-adapters/mermaid.d.ts +1 -1
  39. package/dist/introspection/graph-adapters/reactflow.d.ts +1 -1
  40. package/dist/introspection/inspector.d.ts +7 -5
  41. package/dist/introspection/inspector.js +13 -27
  42. package/dist/lifecycle/lifecycle-manager.d.ts +4 -4
  43. package/dist/lifecycle/lifecycle-manager.js +13 -11
  44. package/dist/lifecycle/scope-manager.d.ts +2 -2
  45. package/dist/lifecycle/scope-manager.js +4 -4
  46. package/dist/metadata/metadata-reader-token.d.ts +2 -2
  47. package/dist/metadata/metadata-reader-token.js +1 -1
  48. package/dist/metadata/metadata-types.d.ts +3 -3
  49. package/dist/metadata/symbol-metadata-reader.d.ts +3 -3
  50. package/dist/metadata/symbol-metadata-reader.js +1 -1
  51. package/dist/metadata/verifying-metadata-reader.d.ts +1 -1
  52. package/dist/metadata/verifying-metadata-reader.js +2 -2
  53. package/dist/resolution/cache/activation-need.d.ts +6 -4
  54. package/dist/resolution/cache/activation-need.js +13 -6
  55. package/dist/resolution/cache/binding-lookup-cache.d.ts +39 -6
  56. package/dist/resolution/cache/binding-lookup-cache.js +97 -13
  57. package/dist/resolution/cache/class-introspector.d.ts +6 -6
  58. package/dist/resolution/cache/class-introspector.js +82 -69
  59. package/dist/resolution/context.d.ts +11 -11
  60. package/dist/resolution/context.js +1 -1
  61. package/dist/resolution/path/resolution-path.d.ts +1 -1
  62. package/dist/resolution/path/resolution-path.js +1 -1
  63. package/dist/resolution/plan/instantiation-plan.d.ts +16 -4
  64. package/dist/resolution/plan/instantiation-plan.js +160 -69
  65. package/dist/resolution/plan/plan-codegen.d.ts +100 -0
  66. package/dist/resolution/plan/plan-codegen.js +185 -0
  67. package/dist/resolution/resolver.d.ts +26 -16
  68. package/dist/resolution/resolver.js +401 -147
  69. package/dist/resolution/select/binding-select.d.ts +6 -5
  70. package/dist/resolution/select/binding-select.js +17 -11
  71. package/dist/resolution/select/constraints.d.ts +3 -3
  72. package/dist/resolution/select/constraints.js +4 -4
  73. package/package.json +11 -3
@@ -1,7 +1,8 @@
1
- import { NO_INSTANCE } from "#/core/binding";
2
- import { tokenName } from "#/core/token";
3
- import { AsyncResolutionError } from "#/errors/errors";
4
- import { injectionSlotToResolveOptions } from "#/injection/resolve-options";
1
+ import { NO_INSTANCE } from "#core/binding";
2
+ import { tokenName } from "#core/token";
3
+ import { AsyncResolutionError } from "#errors/errors";
4
+ import { injectionSlotToResolveOptions } from "#injection/resolve-options";
5
+ import { generateAsyncPlan, generatePlan, isPlanCodegenAvailable, PLAN_CODEGEN_THRESHOLD, } from "#resolution/plan/plan-codegen";
5
6
  // Past this depth a dependency escapes to the runtime path rather than inlining further —
6
7
  // compiled closures nest one JS frame per level, and pathological graphs are the runtime's job.
7
8
  const PLAN_DEPTH_LIMIT = 32;
@@ -46,13 +47,45 @@ function settleThenApply(deps, apply) {
46
47
  */
47
48
  export class InstantiationPlanCompiler {
48
49
  #host;
50
+ #generatedPlanCount = 0;
49
51
  constructor(host) {
50
52
  this.#host = host;
51
53
  }
54
+ /** Plans this compiler has generated as functions of their own. */
55
+ get generatedPlanCount() {
56
+ return this.#generatedPlanCount;
57
+ }
52
58
  compile(binding) {
53
- return binding.kind === "class"
59
+ const compiled = binding.kind === "class"
54
60
  ? this.#compileClassPlan(binding, new Set(), 0, [])
55
61
  : this.#compileResolvedPlan(binding, new Set(), 0, []);
62
+ if (compiled === null || compiled === PLAN_RETRY) {
63
+ return compiled;
64
+ }
65
+ const host = this.#host;
66
+ return this.#tiered(compiled.run, () => generatePlan(compiled.node), (current, next) => host.replacePlan(binding, current, next));
67
+ }
68
+ /**
69
+ * The closure runs first; a plan that keeps running is generated as a function of its own and
70
+ * takes the closure's place in the host's map, so nothing resolved a few times ever compiles one.
71
+ */
72
+ #tiered(closure, generate, replace) {
73
+ if (!isPlanCodegenAvailable()) {
74
+ return closure;
75
+ }
76
+ let runs = 0;
77
+ const plan = () => {
78
+ runs += 1;
79
+ if (runs === PLAN_CODEGEN_THRESHOLD) {
80
+ const generated = generate();
81
+ if (generated !== null) {
82
+ this.#generatedPlanCount += 1;
83
+ }
84
+ replace(plan, generated ?? closure);
85
+ }
86
+ return closure();
87
+ };
88
+ return plan;
56
89
  }
57
90
  /**
58
91
  * Re-entry into the runtime resolver for a dependency the plan can't see through.
@@ -67,7 +100,7 @@ export class InstantiationPlanCompiler {
67
100
  const frames = ancestors.map((ancestor) => host.getResolutionFrame(ancestor));
68
101
  const depth = frames.length;
69
102
  let owned = [...frames];
70
- return () => {
103
+ const run = () => {
71
104
  const stack = owned ?? [...frames];
72
105
  owned = undefined;
73
106
  try {
@@ -79,37 +112,42 @@ export class InstantiationPlanCompiler {
79
112
  }
80
113
  }
81
114
  };
115
+ return { run, node: { kind: "thunk", run } };
82
116
  }
83
117
  // A resolved binding declares its deps as explicit descriptors — same rules as
84
118
  // class params, with the factory call (and its sync-only check) in place of `new`.
85
119
  #compileResolvedPlan(binding, compileStack, depth, ancestors) {
86
- if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding)) {
120
+ if (binding.activationHook !== undefined || this.#host.hasActivationHandlers(binding)) {
87
121
  return null;
88
122
  }
89
123
  const factory = binding.factory;
90
124
  const tokenDisplayName = tokenName(binding.token);
91
- const depThunks = new Array(binding.deps.length);
125
+ const deps = new Array(binding.deps.length);
92
126
  const depAncestors = [...ancestors, binding];
93
- compileStack.add(binding.id);
127
+ compileStack.add(binding.identifier);
94
128
  try {
95
129
  for (let index = 0; index < binding.deps.length; index += 1) {
96
- const thunk = this.#compileInjectionThunk(binding.deps[index], compileStack, depth, depAncestors);
97
- if (thunk === PLAN_RETRY) {
98
- return thunk;
130
+ const dep = this.#compileInjectionThunk(binding.deps[index], compileStack, depth, depAncestors);
131
+ if (dep === PLAN_RETRY) {
132
+ return dep;
99
133
  }
100
- depThunks[index] = thunk;
134
+ deps[index] = dep;
101
135
  }
102
136
  }
103
137
  finally {
104
- compileStack.delete(binding.id);
138
+ compileStack.delete(binding.identifier);
105
139
  }
106
- return () => {
107
- const factoryResult = factory(...depThunks.map((thunk) => thunk()));
140
+ const runs = deps.map((dep) => dep.run);
141
+ const settle = (factoryResult) => {
108
142
  if (factoryResult instanceof Promise) {
109
143
  throw new AsyncResolutionError(tokenDisplayName);
110
144
  }
111
145
  return factoryResult;
112
146
  };
147
+ return {
148
+ run: () => settle(factory(...runs.map((run) => run()))),
149
+ node: { kind: "call", factory, settle, deps: deps.map((dep) => dep.node) },
150
+ };
113
151
  }
114
152
  /**
115
153
  * One dependency of a plan node — a constructor param or a `toResolved` descriptor.
@@ -141,7 +179,7 @@ export class InstantiationPlanCompiler {
141
179
  return this.#compileDepThunk(entry, compileStack, depth, ancestors);
142
180
  }
143
181
  #compileClassPlan(binding, compileStack, depth, ancestors) {
144
- if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding)) {
182
+ if (binding.activationHook !== undefined || this.#host.hasActivationHandlers(binding)) {
145
183
  return null;
146
184
  }
147
185
  const target = binding.target;
@@ -149,77 +187,105 @@ export class InstantiationPlanCompiler {
149
187
  if (hasPostConstruct === undefined) {
150
188
  return PLAN_RETRY;
151
189
  }
152
- if (hasPostConstruct || this.#host.needsActiveContainer(target)) {
190
+ if (hasPostConstruct) {
191
+ return null;
192
+ }
193
+ // An accessor-injected class compiles only as the plan's root: its accessors resolve during
194
+ // construction with the class's own frame on the path, and a root has no static ancestors to replay.
195
+ const withAccessors = this.#host.needsActiveContainer(target);
196
+ if (withAccessors && depth !== 0) {
153
197
  return null;
154
198
  }
155
199
  const invokable = target;
156
200
  const meta = this.#host.getConstructorMetadata(target);
157
201
  if (meta === undefined) {
158
202
  // Metadata-less classes with required params throw on the runtime path — keep them there.
159
- return target.length === 0 ? () => new invokable() : null;
203
+ if (target.length !== 0) {
204
+ return null;
205
+ }
206
+ return this.#classNode(binding, invokable, withAccessors, []);
160
207
  }
161
208
  const params = meta.params;
162
209
  if (params.length === 0) {
163
- return () => new invokable();
210
+ return this.#classNode(binding, invokable, withAccessors, []);
164
211
  }
165
- const depThunks = new Array(params.length);
212
+ const deps = new Array(params.length);
166
213
  const depAncestors = [...ancestors, binding];
167
- compileStack.add(binding.id);
214
+ compileStack.add(binding.identifier);
168
215
  try {
169
216
  for (let index = 0; index < params.length; index += 1) {
170
- const thunk = this.#compileInjectionThunk(params[index], compileStack, depth, depAncestors);
171
- if (thunk === PLAN_RETRY) {
172
- return thunk;
217
+ const dep = this.#compileInjectionThunk(params[index], compileStack, depth, depAncestors);
218
+ if (dep === PLAN_RETRY) {
219
+ return dep;
173
220
  }
174
- depThunks[index] = thunk;
221
+ deps[index] = dep;
175
222
  }
176
223
  }
177
224
  finally {
178
- compileStack.delete(binding.id);
225
+ compileStack.delete(binding.identifier);
179
226
  }
180
- switch (depThunks.length) {
227
+ return this.#classNode(binding, invokable, withAccessors, deps);
228
+ }
229
+ // The closure is hand-written per arity up to three so the common shapes spread nothing.
230
+ #classNode(binding, invokable, withAccessors, deps) {
231
+ const nodes = deps.map((dep) => dep.node);
232
+ if (withAccessors) {
233
+ const host = this.#host;
234
+ const construct = (values) => host.constructWithAccessors(binding, invokable, values);
235
+ const runs = deps.map((dep) => dep.run);
236
+ return { run: () => construct(runs.map((run) => run())), node: { kind: "accessors", construct, deps: nodes } };
237
+ }
238
+ const node = { kind: "construct", target: invokable, deps: nodes };
239
+ switch (deps.length) {
240
+ case 0:
241
+ return { run: () => new invokable(), node };
181
242
  case 1: {
182
- const dep0 = depThunks[0];
183
- return () => new invokable(dep0());
243
+ const dep0 = deps[0].run;
244
+ return { run: () => new invokable(dep0()), node };
184
245
  }
185
246
  case 2: {
186
- const dep0 = depThunks[0];
187
- const dep1 = depThunks[1];
188
- return () => new invokable(dep0(), dep1());
247
+ const dep0 = deps[0].run;
248
+ const dep1 = deps[1].run;
249
+ return { run: () => new invokable(dep0(), dep1()), node };
189
250
  }
190
251
  case 3: {
191
- const dep0 = depThunks[0];
192
- const dep1 = depThunks[1];
193
- const dep2 = depThunks[2];
194
- return () => new invokable(dep0(), dep1(), dep2());
252
+ const dep0 = deps[0].run;
253
+ const dep1 = deps[1].run;
254
+ const dep2 = deps[2].run;
255
+ return { run: () => new invokable(dep0(), dep1(), dep2()), node };
256
+ }
257
+ default: {
258
+ const runs = deps.map((dep) => dep.run);
259
+ return { run: () => new invokable(...runs.map((run) => run())), node };
195
260
  }
196
- default:
197
- return () => new invokable(...depThunks.map((thunk) => thunk()));
198
261
  }
199
262
  }
200
263
  #compileDepThunk(entry, compileStack, depth, ancestors, options) {
201
264
  const { binding } = entry;
202
- if (binding.kind === "constant" && binding.onActivation === undefined) {
265
+ if (binding.kind === "constant" && binding.activationHook === undefined) {
203
266
  if (!this.#host.hasActivationHandlers(binding)) {
204
267
  const value = binding.value;
205
- return () => value;
268
+ return { run: () => value, node: { kind: "value", value } };
206
269
  }
207
270
  }
208
271
  const scope = binding.scope;
209
272
  if (scope === "singleton") {
210
273
  // Cached-singleton read; the first materialization escapes so it sees the same ancestors
211
274
  // (and therefore the same cycle detection) the interpreted path would have built.
212
- const escape = this.#compileEscapeThunk(binding.token, ancestors, "single", options);
275
+ const escape = this.#compileEscapeThunk(binding.token, ancestors, "single", options).run;
213
276
  const singletonBinding = binding;
214
- return () => {
215
- const cached = singletonBinding.instance;
216
- return cached === NO_INSTANCE ? escape() : cached;
277
+ return {
278
+ run: () => {
279
+ const cached = singletonBinding.instance;
280
+ return cached === NO_INSTANCE ? escape() : cached;
281
+ },
282
+ node: { kind: "singleton", binding: singletonBinding, escape },
217
283
  };
218
284
  }
219
285
  if (scope === "transient" &&
220
286
  binding.kind === "class" &&
221
287
  depth < PLAN_DEPTH_LIMIT &&
222
- !compileStack.has(binding.id)) {
288
+ !compileStack.has(binding.identifier)) {
223
289
  const inlined = this.#compileClassPlan(binding, compileStack, depth + 1, ancestors);
224
290
  if (inlined !== null) {
225
291
  return inlined;
@@ -244,18 +310,17 @@ export class InstantiationPlanCompiler {
244
310
  if (node === null || node === PLAN_RETRY) {
245
311
  return node;
246
312
  }
247
- return node.run;
313
+ const host = this.#host;
314
+ return this.#tiered(node.run, () => generateAsyncPlan(node.node), (current, next) => host.replaceAsyncPlan(binding, current, next));
248
315
  }
249
316
  #compileAsyncEscapeThunk(token, ancestors, arity = "single", options) {
250
317
  const host = this.#host;
251
318
  const frames = ancestors.map((ancestor) => host.getResolutionFrame(ancestor));
252
- return {
253
- run: () => host.resolveEscapedAsync(token, options, arity, [...frames]),
254
- promiseShape: "always",
255
- };
319
+ const run = () => host.resolveEscapedAsync(token, options, arity, [...frames]);
320
+ return { run, promiseShape: "always", node: { kind: "thunk", run } };
256
321
  }
257
322
  #compileAsyncClassNode(binding, compileStack, depth, ancestors) {
258
- if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding)) {
323
+ if (binding.activationHook !== undefined || this.#host.hasActivationHandlers(binding)) {
259
324
  return null;
260
325
  }
261
326
  const target = binding.target;
@@ -268,17 +333,22 @@ export class InstantiationPlanCompiler {
268
333
  }
269
334
  const invokable = target;
270
335
  const meta = this.#host.getConstructorMetadata(target);
336
+ const leaf = {
337
+ run: () => new invokable(),
338
+ promiseShape: "never",
339
+ node: { kind: "construct", target: invokable, deps: [], awaits: false },
340
+ };
271
341
  if (meta === undefined) {
272
342
  // Metadata-less classes with required params throw on the runtime path — keep them there.
273
- return target.length === 0 ? { run: () => new invokable(), promiseShape: "never" } : null;
343
+ return target.length === 0 ? leaf : null;
274
344
  }
275
345
  const params = meta.params;
276
346
  if (params.length === 0) {
277
- return { run: () => new invokable(), promiseShape: "never" };
347
+ return leaf;
278
348
  }
279
349
  const depThunks = new Array(params.length);
280
350
  const depAncestors = [...ancestors, binding];
281
- compileStack.add(binding.id);
351
+ compileStack.add(binding.identifier);
282
352
  try {
283
353
  for (let index = 0; index < params.length; index += 1) {
284
354
  const thunk = this.#compileAsyncInjectionThunk(params[index], compileStack, depth, depAncestors);
@@ -289,36 +359,43 @@ export class InstantiationPlanCompiler {
289
359
  }
290
360
  }
291
361
  finally {
292
- compileStack.delete(binding.id);
362
+ compileStack.delete(binding.identifier);
293
363
  }
364
+ const nodes = depThunks.map((thunk) => thunk.node);
294
365
  if (allSynchronous(depThunks)) {
366
+ const node = { kind: "construct", target: invokable, deps: nodes, awaits: false };
295
367
  switch (depThunks.length) {
296
368
  case 1: {
297
369
  const dep0 = depThunks[0].run;
298
- return { run: () => new invokable(dep0()), promiseShape: "never" };
370
+ return { run: () => new invokable(dep0()), promiseShape: "never", node };
299
371
  }
300
372
  case 2: {
301
373
  const dep0 = depThunks[0].run;
302
374
  const dep1 = depThunks[1].run;
303
- return { run: () => new invokable(dep0(), dep1()), promiseShape: "never" };
375
+ return { run: () => new invokable(dep0(), dep1()), promiseShape: "never", node };
304
376
  }
305
377
  default:
306
378
  return {
307
379
  run: () => new invokable(...depThunks.map((thunk) => thunk.run())),
308
380
  promiseShape: "never",
381
+ node,
309
382
  };
310
383
  }
311
384
  }
312
- return { run: settleThenApply(depThunks, (values) => new invokable(...values)), promiseShape: "always" };
385
+ return {
386
+ run: settleThenApply(depThunks, (values) => new invokable(...values)),
387
+ promiseShape: "always",
388
+ node: { kind: "construct", target: invokable, deps: nodes, awaits: true },
389
+ };
313
390
  }
314
391
  #compileAsyncResolvedNode(binding, compileStack, depth, ancestors) {
315
- if (binding.onActivation !== undefined || this.#host.hasActivationHandlers(binding)) {
392
+ if (binding.activationHook !== undefined || this.#host.hasActivationHandlers(binding)) {
316
393
  return null;
317
394
  }
318
395
  const factory = binding.factory;
319
396
  const depThunks = new Array(binding.deps.length);
320
397
  const depAncestors = [...ancestors, binding];
321
- compileStack.add(binding.id);
398
+ compileStack.add(binding.identifier);
322
399
  try {
323
400
  for (let index = 0; index < binding.deps.length; index += 1) {
324
401
  const thunk = this.#compileAsyncInjectionThunk(binding.deps[index], compileStack, depth, depAncestors);
@@ -329,15 +406,24 @@ export class InstantiationPlanCompiler {
329
406
  }
330
407
  }
331
408
  finally {
332
- compileStack.delete(binding.id);
409
+ compileStack.delete(binding.identifier);
333
410
  }
334
411
  // A sync-kind factory may still hand back a promise on this path — the async entry awaits it,
335
412
  // so the consumer is told to as well.
336
413
  const directShape = binding.kind === "resolved-async" ? "always" : "maybe";
414
+ const nodes = depThunks.map((thunk) => thunk.node);
337
415
  if (allSynchronous(depThunks)) {
338
- return { run: () => factory(...depThunks.map((thunk) => thunk.run())), promiseShape: directShape };
416
+ return {
417
+ run: () => factory(...depThunks.map((thunk) => thunk.run())),
418
+ promiseShape: directShape,
419
+ node: { kind: "call", factory, deps: nodes, awaits: false },
420
+ };
339
421
  }
340
- return { run: settleThenApply(depThunks, (values) => factory(...values)), promiseShape: "always" };
422
+ return {
423
+ run: settleThenApply(depThunks, (values) => factory(...values)),
424
+ promiseShape: "always",
425
+ node: { kind: "call", factory, deps: nodes, awaits: true },
426
+ };
341
427
  }
342
428
  /** Mirrors {@link InstantiationPlanCompiler.#compileInjectionThunk}, escaping through the async dispatch. */
343
429
  #compileAsyncInjectionThunk(descriptor, compileStack, depth, ancestors) {
@@ -364,28 +450,33 @@ export class InstantiationPlanCompiler {
364
450
  }
365
451
  #compileAsyncDepThunk(entry, compileStack, depth, ancestors, options) {
366
452
  const { binding } = entry;
367
- if (binding.kind === "constant" && binding.onActivation === undefined) {
453
+ if (binding.kind === "constant" && binding.activationHook === undefined) {
368
454
  if (!this.#host.hasActivationHandlers(binding)) {
369
455
  const value = binding.value;
370
456
  // The interpreted path funnels every dependency through an await, which unwraps a
371
457
  // promise-valued constant — so one compiles as needing that same await.
372
- return { run: () => value, promiseShape: value instanceof Promise ? "always" : "never" };
458
+ return {
459
+ run: () => value,
460
+ promiseShape: value instanceof Promise ? "always" : "never",
461
+ node: { kind: "value", value },
462
+ };
373
463
  }
374
464
  }
375
465
  const scope = binding.scope;
376
466
  if (scope === "singleton") {
377
467
  // Cached-singleton read; the cold materialization escapes with the same criteria.
378
- const escape = this.#compileAsyncEscapeThunk(binding.token, ancestors, "single", options);
468
+ const escape = this.#compileAsyncEscapeThunk(binding.token, ancestors, "single", options).run;
379
469
  const singletonBinding = binding;
380
470
  return {
381
471
  run: () => {
382
472
  const cached = singletonBinding.instance;
383
- return cached === NO_INSTANCE ? escape.run() : cached;
473
+ return cached === NO_INSTANCE ? escape() : cached;
384
474
  },
385
475
  promiseShape: "maybe",
476
+ node: { kind: "singleton", binding: singletonBinding, escape },
386
477
  };
387
478
  }
388
- if (scope === "transient" && depth < PLAN_DEPTH_LIMIT && !compileStack.has(binding.id)) {
479
+ if (scope === "transient" && depth < PLAN_DEPTH_LIMIT && !compileStack.has(binding.identifier)) {
389
480
  let inlined = null;
390
481
  if (binding.kind === "class") {
391
482
  inlined = this.#compileAsyncClassNode(binding, compileStack, depth + 1, ancestors);
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Generates a hot instantiation plan as a function of its own, so its call sites answer for one plan only.
3
+ */
4
+ import type { Binding } from "#core/binding";
5
+ import type { ConstructorInvocation } from "#core/constructor-type";
6
+ /**
7
+ * The number of runs a plan's closure makes before the plan is generated as its own function.
8
+ *
9
+ * @remarks Below it a plan stays a closure, which is all a cold container or a per-request child
10
+ * ever runs; above it a plan pays one compile for call sites nothing else feeds.
11
+ *
12
+ * @since 0.10.0
13
+ */
14
+ export declare const PLAN_CODEGEN_THRESHOLD = 32;
15
+ /**
16
+ * The shape of a compiled sync plan: what its closure does, stated as data the generator can read.
17
+ *
18
+ * @remarks Every leaf the compiler could not see through is a `thunk` and stays opaque — generated
19
+ * code calls it exactly as the closure did, so an escape keeps its frames, its dispatch and its errors.
20
+ *
21
+ * @since 0.10.0
22
+ */
23
+ export type PlanNode = {
24
+ readonly kind: "construct";
25
+ readonly target: ConstructorInvocation;
26
+ readonly deps: ReadonlyArray<PlanNode>;
27
+ } | {
28
+ readonly kind: "accessors";
29
+ readonly construct: (deps: Array<unknown>) => unknown;
30
+ readonly deps: ReadonlyArray<PlanNode>;
31
+ } | {
32
+ readonly kind: "call";
33
+ readonly factory: (...args: Array<unknown>) => unknown;
34
+ readonly settle: (result: unknown) => unknown;
35
+ readonly deps: ReadonlyArray<PlanNode>;
36
+ } | {
37
+ readonly kind: "value";
38
+ readonly value: unknown;
39
+ } | {
40
+ readonly kind: "singleton";
41
+ readonly binding: Binding;
42
+ readonly escape: () => unknown;
43
+ } | {
44
+ readonly kind: "thunk";
45
+ readonly run: () => unknown;
46
+ };
47
+ /**
48
+ * The shape of a compiled async plan.
49
+ *
50
+ * @remarks A node that `awaits` has a dependency that may yield a promise, so it runs as the
51
+ * interpreted async path does: every dependency starts in order, a sync throw becomes that slot's
52
+ * rejection, and the constructor or factory runs on the settled values.
53
+ *
54
+ * @since 0.10.0
55
+ */
56
+ export type AsyncPlanNode = {
57
+ readonly kind: "construct";
58
+ readonly target: ConstructorInvocation;
59
+ readonly deps: ReadonlyArray<AsyncPlanNode>;
60
+ readonly awaits: boolean;
61
+ } | {
62
+ readonly kind: "call";
63
+ readonly factory: (...args: Array<unknown>) => unknown;
64
+ readonly deps: ReadonlyArray<AsyncPlanNode>;
65
+ readonly awaits: boolean;
66
+ } | {
67
+ readonly kind: "value";
68
+ readonly value: unknown;
69
+ } | {
70
+ readonly kind: "singleton";
71
+ readonly binding: Binding;
72
+ readonly escape: () => unknown;
73
+ } | {
74
+ readonly kind: "thunk";
75
+ readonly run: () => unknown;
76
+ };
77
+ /**
78
+ * Whether this runtime lets the engine compile a function from source.
79
+ *
80
+ * @remarks A Content Security Policy without `unsafe-eval` refuses the `Function` constructor; every
81
+ * plan then stays a closure, which behaves identically.
82
+ *
83
+ * @since 0.10.0
84
+ */
85
+ export declare function isPlanCodegenAvailable(): boolean;
86
+ /**
87
+ * Generates a plan as a function of its own, or `null` when the runtime refuses to compile one.
88
+ *
89
+ * @remarks The source carries a serial so no two plans share a compilation-cache entry: V8 keys
90
+ * type feedback by function literal, and one literal per plan is the whole point.
91
+ *
92
+ * @since 0.10.0
93
+ */
94
+ export declare function generatePlan(node: PlanNode): (() => unknown) | null;
95
+ /**
96
+ * Generates an async plan as a function of its own, or `null` when the runtime refuses to compile one.
97
+ *
98
+ * @since 0.10.0
99
+ */
100
+ export declare function generateAsyncPlan(node: AsyncPlanNode): (() => unknown) | null;