@codefast/di 0.11.0 → 0.13.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 (79) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +128 -31
  3. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +21 -28
  4. package/dist/container/{binding-builders.js → binding-chain.js} +53 -163
  5. package/dist/container/container.d.ts +11 -3
  6. package/dist/container/container.js +176 -102
  7. package/dist/core/binding-builders.d.ts +117 -0
  8. package/dist/core/binding-declaration.d.ts +9 -2
  9. package/dist/core/binding-declaration.js +5 -4
  10. package/dist/core/binding.d.ts +21 -149
  11. package/dist/core/binding.js +15 -25
  12. package/dist/core/module.d.ts +1 -1
  13. package/dist/core/registry.d.ts +9 -24
  14. package/dist/core/registry.js +30 -155
  15. package/dist/core/tag.js +1 -1
  16. package/dist/decorators/inject.d.ts +3 -1
  17. package/dist/decorators/inject.js +5 -5
  18. package/dist/decorators/injectable.js +2 -2
  19. package/dist/decorators/lifecycle.d.ts +20 -0
  20. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +7 -3
  21. package/dist/decorators/{decorator-metadata.js → metadata-record.js} +1 -1
  22. package/dist/{errors/errors.d.ts → errors.d.ts} +29 -19
  23. package/dist/{errors/errors.js → errors.js} +43 -25
  24. package/dist/index.d.ts +12 -10
  25. package/dist/index.js +6 -6
  26. package/dist/injection/descriptor.d.ts +1 -1
  27. package/dist/introspection/dependency-graph.d.ts +1 -1
  28. package/dist/introspection/dependency-graph.js +3 -4
  29. package/dist/introspection/explanation.d.ts +101 -0
  30. package/dist/introspection/explanation.js +169 -0
  31. package/dist/introspection/inspector.d.ts +9 -2
  32. package/dist/introspection/inspector.js +25 -20
  33. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +11 -2
  34. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +51 -28
  35. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +23 -6
  36. package/dist/lifecycle/{scope-manager.js → scopes.js} +93 -20
  37. package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +1 -1
  38. package/dist/metadata/{metadata-keys.js → keys.js} +1 -1
  39. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  40. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  41. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  42. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  43. package/dist/metadata/types.js +1 -0
  44. package/dist/metadata/verify.d.ts +43 -0
  45. package/dist/metadata/verify.js +133 -0
  46. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +1 -1
  47. package/dist/metadata/{verifying-metadata-reader.js → verifying-reader.js} +2 -2
  48. package/dist/resolution/cache/activation-need.d.ts +1 -1
  49. package/dist/resolution/cache/activation-need.js +5 -1
  50. package/dist/resolution/cache/class-introspector.d.ts +27 -36
  51. package/dist/resolution/cache/class-introspector.js +61 -159
  52. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -2
  53. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +40 -8
  54. package/dist/resolution/context.d.ts +1 -1
  55. package/dist/resolution/context.js +1 -1
  56. package/dist/resolution/{path/resolution-path.d.ts → path.d.ts} +12 -2
  57. package/dist/resolution/{path/resolution-path.js → path.js} +23 -3
  58. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +2 -4
  59. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +2 -4
  60. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  61. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +4 -4
  62. package/dist/resolution/resolver.d.ts +19 -7
  63. package/dist/resolution/resolver.js +343 -125
  64. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  65. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  66. package/dist/resolution/select/constraints.js +1 -1
  67. package/package.json +80 -72
  68. package/dist/core/binding-scope.d.ts +0 -12
  69. package/dist/core/binding-scope.js +0 -12
  70. package/dist/decorators/lifecycle-decorators.d.ts +0 -14
  71. /package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +0 -0
  72. /package/dist/{ambient/active-container.js → ambient-container.js} +0 -0
  73. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  74. /package/dist/decorators/{decorator-metadata.d.ts → metadata-record.d.ts} +0 -0
  75. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  76. /package/dist/injection/{resolve-options.js → dependency-slot.js} +0 -0
  77. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  78. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  79. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
@@ -1,19 +1,21 @@
1
1
  import { bindingSlotToString, NO_INSTANCE } from "#core/binding";
2
2
  import { slotNameCriterionOf } from "#core/tag";
3
3
  import { tokenName } from "#core/token";
4
- import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#errors/errors";
5
- import { loneTagBesideNameOf, resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf, } from "#injection/resolve-options";
6
- import { SCOPED_MISS } from "#lifecycle/scope-manager";
4
+ import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#errors";
5
+ import { loneTagBesideNameOf, resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf, } from "#injection/dependency-slot";
6
+ import { SCOPED_MISS } from "#lifecycle/scopes";
7
7
  import { settleInOrder } from "#resolution/async-fan-out";
8
8
  import { ActivationNeedCache } from "#resolution/cache/activation-need";
9
- import { BindingLookupCache } from "#resolution/cache/binding-lookup-cache";
10
9
  import { ClassIntrospector } from "#resolution/cache/class-introspector";
10
+ import { BindingLookupCache } from "#resolution/cache/lookup";
11
11
  import { AsyncLevelContext, DefaultConstraintContext, DefaultResolutionContext } from "#resolution/context";
12
- import { branchDepthOf, buildResolutionFrame, cycleNamesOf, enterSyncPath, extendResolutionBranch, leaveSyncPath, ROOT_BRANCH, UNOWNED_BRANCH, } from "#resolution/path/resolution-path";
13
- import { InstantiationPlanCompiler, PLAN_RETRY } from "#resolution/plan/instantiation-plan";
14
- import { matchesSlot, selectAllBindings, selectBinding } from "#resolution/select/binding-select";
12
+ import { branchDepthOf, buildResolutionFrame, cycleNamesOf, enterSyncPath, extendResolutionBranch, leaveSyncPath, missPathOf, ROOT_BRANCH, UNOWNED_BRANCH, } from "#resolution/path";
13
+ import { InstantiationPlanCompiler, PLAN_RETRY } from "#resolution/plan/compiler";
14
+ import { matchesSlot, selectAllBindings, selectBinding } from "#resolution/select/candidates";
15
15
  const EMPTY_FRAME_LIST = [];
16
16
  const EMPTY_PARAM_LIST = [];
17
+ // A class with no params is constructed with no arguments, so no dependency array is built for it.
18
+ const NO_ARGUMENTS = [];
17
19
  /** Plans compiled so far, with the `null` unplannable marks left out. */
18
20
  function countCompiledPlans(plans) {
19
21
  let count = 0;
@@ -190,9 +192,9 @@ export class DependencyResolver {
190
192
  * can answer still does.
191
193
  */
192
194
  #forwardingAliasOrParent(token, options, resolutionStack, singleCriterion) {
193
- const defaultSlot = this.#registry.getDefaultSlotBinding(token);
194
- if (defaultSlot !== undefined && defaultSlot.kind === "alias") {
195
- return { binding: defaultSlot, owner: this };
195
+ const defaultSlotAlias = this.#registry.defaultSlotAlias(token);
196
+ if (defaultSlotAlias !== undefined) {
197
+ return { binding: defaultSlotAlias, owner: this };
196
198
  }
197
199
  return this.#parent === undefined
198
200
  ? undefined
@@ -250,10 +252,11 @@ export class DependencyResolver {
250
252
  // dominated by that capture. Bindings under the token anywhere in the chain mean the request
251
253
  // matched none of them, so a child reports the same miss its parent would.
252
254
  const bound = this.#allBindingsFromChain(currentToken);
255
+ const path = missPathOf(resolutionStack, token, visitedAliasTokens);
253
256
  if (bound.length > 0) {
254
- throw new NoMatchingBindingError(tokenName(currentToken), options ?? {}, bound.map((binding) => bindingSlotToString(binding.slot)));
257
+ throw new NoMatchingBindingError(tokenName(currentToken), options ?? {}, bound.map((binding) => bindingSlotToString(binding.slot)), path);
255
258
  }
256
- throw new TokenNotBoundError(tokenName(currentToken));
259
+ throw new TokenNotBoundError(tokenName(currentToken), path);
257
260
  }
258
261
  return found;
259
262
  }
@@ -293,6 +296,36 @@ export class DependencyResolver {
293
296
  peekCandidateBindingsForValidate(token, options) {
294
297
  return this.#candidateBindings(token, options, []);
295
298
  }
299
+ /**
300
+ * The error `resolve` raises for a request nothing selects, or `undefined` while a `when()` candidate leaves it open.
301
+ *
302
+ * @remarks A predicate reads the resolution path, which a static walk does not have, so a slot-matching candidate
303
+ * that carries one may still be selected when the request is really made.
304
+ */
305
+ missForValidate(token, options, path) {
306
+ const bound = this.#allBindingsFromChain(token);
307
+ if (bound.length === 0) {
308
+ return new TokenNotBoundError(tokenName(token), path);
309
+ }
310
+ for (const binding of bound) {
311
+ if (binding.predicate !== undefined && !binding.isMany && matchesSlot(binding.slot, options)) {
312
+ return undefined;
313
+ }
314
+ }
315
+ return new NoMatchingBindingError(tokenName(token), options ?? {}, bound.map((binding) => bindingSlotToString(binding.slot)), path);
316
+ }
317
+ /** The resolver whose chain a binding this one can see resolves its dependencies from, as a singleton does. */
318
+ ownerForValidate(binding) {
319
+ return this.#ownerOf(binding);
320
+ }
321
+ /** Deactivates a scoped instance of a binding this resolver owns, with its hooks and reader, when it owes a teardown. */
322
+ deactivateScoped(binding, instance) {
323
+ const lifecycle = this.#lifecycle;
324
+ const reader = this.#metadataReader;
325
+ return lifecycle.owesDeactivation(binding, reader)
326
+ ? lifecycle.runDeactivation(binding, instance, reader)
327
+ : undefined;
328
+ }
296
329
  // ── Sync resolve ───────────────────────────────────────────────────────────────────────────────────────────────────
297
330
  resolveFromContext(token, resolutionStack) {
298
331
  // Hot lane: own-registry fast default. Fall back to the chain-versioned memo
@@ -514,7 +547,7 @@ export class DependencyResolver {
514
547
  }
515
548
  return this.resolve(token, options, resolutionStack);
516
549
  },
517
- // Dispatches exactly as #resolveDepAsync does, for the async lane's escapes.
550
+ // Dispatches exactly as #settleDep does, for the async lane's escapes.
518
551
  resolveEscapedAsync: (token, options, arity, resolutionStack) => {
519
552
  if (arity === "all") {
520
553
  return this.resolveAllAsync(token, options, resolutionStack, UNOWNED_BRANCH);
@@ -645,7 +678,7 @@ export class DependencyResolver {
645
678
  this.#scope.setSingleton(binding, activated);
646
679
  }
647
680
  else if (scope === "scoped") {
648
- this.#scope.setScoped(binding, activated);
681
+ this.#scope.setScoped(binding, activated, owner);
649
682
  }
650
683
  return activated;
651
684
  }
@@ -691,10 +724,13 @@ export class DependencyResolver {
691
724
  case "dynamic-async":
692
725
  throw asyncResolutionErrorFor(binding, resolutionStack);
693
726
  case "class": {
694
- const deps = this.#resolveDeps(this.#constructorParams(binding.target), resolutionStack);
695
- return this.#introspector().instantiate(binding.target, deps, this.#introspector().needsActiveContainer(binding.target)
696
- ? this.#ambientResolutionFor(resolutionStack)
697
- : undefined);
727
+ // One lookup of the class's facts serves the params, the accessor need and the construction.
728
+ const introspector = this.#introspector();
729
+ const target = binding.target;
730
+ const facts = introspector.facts(target);
731
+ const params = this.#constructorParams(target, facts);
732
+ const needsActiveContainer = introspector.needsActiveContainerOf(target, facts);
733
+ return introspector.construct(target, params.length === 0 ? NO_ARGUMENTS : this.#resolveDeps(params, resolutionStack), needsActiveContainer, needsActiveContainer ? this.#ambientResolutionFor(resolutionStack) : undefined);
698
734
  }
699
735
  case "resolved": {
700
736
  const deps = this.#resolveDeps(binding.deps, resolutionStack);
@@ -716,8 +752,12 @@ export class DependencyResolver {
716
752
  * @remarks A class the metadata reader knows nothing about is constructible only if it declares
717
753
  * no parameters; anything else is a missing `@injectable()`.
718
754
  */
719
- #constructorParams(target) {
720
- const meta = this.#introspector().constructorMetadata(target);
755
+ #constructorParams(target, facts) {
756
+ // Metadata is fixed once a class is defined, so the answer is too; only a throw is left unsettled.
757
+ return (facts.params ??= this.#settleConstructorParams(target, facts));
758
+ }
759
+ #settleConstructorParams(target, facts) {
760
+ const meta = this.#introspector().constructorMetadataOf(target, facts);
721
761
  if (meta !== undefined) {
722
762
  return meta.params;
723
763
  }
@@ -824,30 +864,35 @@ export class DependencyResolver {
824
864
  for (let index = 0; index < candidates.length; index += 1) {
825
865
  resolved[index] = this.#resolveCandidateSync(candidates[index], undefined, resolutionStack);
826
866
  }
827
- // The members this read materialised may have made the list stable for the next one.
828
- this.#settleCollectionValues(memo);
867
+ this.#settleOnRepeat(memo);
829
868
  return resolved;
830
869
  }
831
- /** The async twin of `resolveRootCollection`: a stable value list settles at once, candidates fan out as usual. */
870
+ /** The async twin of `resolveRootCollection`: a stable value list answers at once, candidates fan out as usual. */
832
871
  resolveRootCollectionAsync(token) {
833
872
  // The async lane appends to its branch and never unwinds, so it works on a stack of its own.
834
873
  const resolutionStack = [];
835
- const memo = this.#rootCollection(token, resolutionStack);
836
- if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
837
- return Promise.resolve(memo.values.slice());
874
+ try {
875
+ // Selection runs `when()` predicates and follows aliases, either of which may throw.
876
+ const memo = this.#rootCollection(token, resolutionStack);
877
+ if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
878
+ return Promise.resolve(memo.values.slice());
879
+ }
880
+ return asPromise(this.#settleCandidates(memo.candidates, undefined, resolutionStack, UNOWNED_BRANCH, (values) => {
881
+ this.#settleOnRepeat(memo);
882
+ return values;
883
+ }));
884
+ }
885
+ catch (collectionError) {
886
+ return Promise.reject(collectionError);
838
887
  }
839
- return settleInOrder(memo.candidates.map((candidate) => this.#resolveCandidateAsync(candidate, undefined, resolutionStack, UNOWNED_BRANCH)), (values) => {
840
- this.#settleCollectionValues(memo);
841
- return values;
842
- });
843
888
  }
844
889
  /**
845
890
  * The memoized candidate list of a root-level, options-less collection, built on its first read.
846
891
  *
847
892
  * @remarks Sound because a `when()` predicate is pure over its context and the root context is a
848
893
  * constant: the list can only change when a registry in the chain does, which is the version the
849
- * memo is stamped with. The value list is kept too while every member is a hook-free constant
850
- * and no activation hook exists anywhere in the chain.
894
+ * memo is stamped with. From the second read on, the value list is kept too while every member is
895
+ * a hook-free constant and no activation hook exists anywhere in the chain.
851
896
  */
852
897
  #rootCollection(token, resolutionStack) {
853
898
  const memo = this.#lookup.collection(token);
@@ -855,11 +900,20 @@ export class DependencyResolver {
855
900
  return memo;
856
901
  }
857
902
  const candidates = this.#candidateBindings(token, undefined, resolutionStack);
858
- const entry = { candidates, values: undefined, activationVersion: -1 };
859
- this.#settleCollectionValues(entry);
903
+ const entry = { candidates, values: undefined, activationVersion: -1, readBefore: false };
860
904
  this.#lookup.rememberCollection(token, entry);
861
905
  return entry;
862
906
  }
907
+ // The first read interprets and the read that repeats it settles the value list, as a plan compiles on repeat;
908
+ // the members either read materialised may have made the list stable for the next one.
909
+ #settleOnRepeat(entry) {
910
+ if (entry.readBefore) {
911
+ this.#settleCollectionValues(entry);
912
+ }
913
+ else {
914
+ entry.readBefore = true;
915
+ }
916
+ }
863
917
  /**
864
918
  * Fills a collection memo's value list once every member is stable: a hook-free constant, or a
865
919
  * hook-free singleton whose instance is cached — anything that changes either bumps a registry
@@ -894,6 +948,18 @@ export class DependencyResolver {
894
948
  const selected = selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionStack, options));
895
949
  return this.#withoutDanglingAliases(selected, options, resolutionStack);
896
950
  }
951
+ // A chain none of whose registries ever held an alias has none to drop, which spares the scan.
952
+ #chainHeldAlias() {
953
+ if (this.#registry.hasHeldAlias) {
954
+ return true;
955
+ }
956
+ for (let current = this.#parent; current !== undefined; current = current.#parent) {
957
+ if (current.#registry.hasHeldAlias) {
958
+ return true;
959
+ }
960
+ }
961
+ return false;
962
+ }
897
963
  /**
898
964
  * The candidates with any alias whose chain ends nowhere dropped, so a fan-out skips a dangling
899
965
  * alias rather than throwing on it.
@@ -902,6 +968,9 @@ export class DependencyResolver {
902
968
  * is returned untouched, so nothing is allocated for it.
903
969
  */
904
970
  #withoutDanglingAliases(candidates, options, resolutionStack) {
971
+ if (!this.#chainHeldAlias()) {
972
+ return candidates;
973
+ }
905
974
  let hasAlias = false;
906
975
  for (let index = 0; index < candidates.length; index += 1) {
907
976
  if (candidates[index].kind === "alias") {
@@ -936,35 +1005,55 @@ export class DependencyResolver {
936
1005
  }
937
1006
  return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionStack, branchDepth);
938
1007
  }
1008
+ // A `#settle*` lane answers with the value when its level completed in this tick and with a promise
1009
+ // only where something had to be awaited, so a singleton or scoped instance built without yielding
1010
+ // is cached before the fan-out that started it moves on to a sibling that may read it synchronously.
939
1011
  #resolveAsyncDefaultEntry(binding, owner, resolutionStack, branchDepth) {
1012
+ try {
1013
+ return asPromise(this.#settleDefaultEntry(binding, owner, resolutionStack, branchDepth));
1014
+ }
1015
+ catch (entryError) {
1016
+ // Not `async`: a failure is turned into the rejection the entry point promises.
1017
+ return Promise.reject(entryError);
1018
+ }
1019
+ }
1020
+ /** `resolveAsyncFromContext` for a dependency slot: the same lanes, answered in this tick where they can be. */
1021
+ #settleDefault(token, resolutionStack, branchDepth) {
1022
+ const fastBinding = this.#registry.getFastDefault(token);
1023
+ if (fastBinding !== undefined && fastBinding.kind !== "alias") {
1024
+ return this.#settleDefaultEntry(fastBinding, this, resolutionStack, branchDepth);
1025
+ }
1026
+ const entry = this.#lookup.defaultEntry(token);
1027
+ if (entry === null) {
1028
+ return this.#settleRequest(token, undefined, resolutionStack, branchDepth);
1029
+ }
1030
+ return this.#settleDefaultEntry(entry.binding, entry.owner, resolutionStack, branchDepth);
1031
+ }
1032
+ #settleDefaultEntry(binding, owner, resolutionStack, branchDepth) {
940
1033
  if (owner.#isPlainConstant(binding)) {
941
- return Promise.resolve(binding.value);
1034
+ return binding.value;
942
1035
  }
943
1036
  const scope = binding.scope;
944
1037
  if (scope === "transient") {
945
1038
  if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !owner.#hasAnyActivation(binding)) {
946
- return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
1039
+ return this.#settleTransientFactory(binding, resolutionStack, branchDepth);
947
1040
  }
948
1041
  }
949
1042
  else if (scope === "singleton") {
950
1043
  if (binding.instance !== NO_INSTANCE) {
951
- return Promise.resolve(binding.instance);
1044
+ return binding.instance;
952
1045
  }
953
1046
  if (owner !== this) {
954
- return owner.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
1047
+ return owner.#settleBinding(binding, undefined, resolutionStack, branchDepth, owner);
955
1048
  }
956
1049
  }
957
- else if (this.#scope.isChild) {
958
- const cachedScoped = this.#scope.readScoped(binding.identifier);
1050
+ else {
1051
+ const cachedScoped = this.#readScoped(binding);
959
1052
  if (cachedScoped !== SCOPED_MISS) {
960
- return Promise.resolve(cachedScoped);
1053
+ return cachedScoped;
961
1054
  }
962
1055
  }
963
- else {
964
- // Not `#readScoped`: this entry point reports failure as a rejection, never a sync throw.
965
- return Promise.reject(new MissingScopeContextError(tokenName(binding.token)));
966
- }
967
- return this.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
1056
+ return this.#settleBinding(binding, undefined, resolutionStack, branchDepth, owner);
968
1057
  }
969
1058
  /**
970
1059
  * Instantiates one owned binding directly, bypassing selection.
@@ -973,18 +1062,38 @@ export class DependencyResolver {
973
1062
  * could pick a different candidate whose criteria are a subset of them.
974
1063
  */
975
1064
  warmBindingAsync(binding, options) {
976
- return this.#resolveBindingAsync(binding, options, [], ROOT_BRANCH, this);
1065
+ try {
1066
+ return asPromise(this.#settleBinding(binding, options, [], ROOT_BRANCH, this));
1067
+ }
1068
+ catch (bindingError) {
1069
+ return Promise.reject(bindingError);
1070
+ }
977
1071
  }
978
- async resolveAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
1072
+ resolveAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
1073
+ try {
1074
+ return asPromise(this.#settleRequest(token, options, resolutionStack, branchDepth, precomputedCriterion));
1075
+ }
1076
+ catch (requestError) {
1077
+ return Promise.reject(requestError);
1078
+ }
1079
+ }
1080
+ /** Selects the binding a request names and settles it on the async lane. */
1081
+ #settleRequest(token, options, resolutionStack, branchDepth, precomputedCriterion) {
979
1082
  const path = ownPrefixOf(resolutionStack, branchDepth);
980
1083
  const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
981
1084
  const { binding, owner } = this.#requireBinding(token, options, path, precomputedCriterion);
982
1085
  if (binding.scope === "singleton" && owner !== this) {
983
- return owner.#resolveBindingAsync(binding, options, path, depth, owner);
1086
+ return owner.#settleBinding(binding, options, path, depth, owner);
984
1087
  }
985
- return this.#resolveBindingAsync(binding, options, path, depth, owner);
1088
+ return this.#settleBinding(binding, options, path, depth, owner);
986
1089
  }
987
- async #resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) {
1090
+ /**
1091
+ * Materialises one binding on the async lane: the instance once nothing yielded, else its promise.
1092
+ *
1093
+ * @remarks A singleton or scoped instance built in this tick is cached in this tick, so a sibling
1094
+ * reading it synchronously finds it; only a materialisation still pending is published in flight.
1095
+ */
1096
+ #settleBinding(binding, options, resolutionStack, branchDepth, owner) {
988
1097
  if (owner.#isPlainConstant(binding)) {
989
1098
  return binding.value;
990
1099
  }
@@ -996,7 +1105,7 @@ export class DependencyResolver {
996
1105
  // In-flight dedup: concurrent callers share the first creation.
997
1106
  const inflight = this.#scope.getInflight(binding.identifier);
998
1107
  if (inflight !== undefined) {
999
- return inflight;
1108
+ return ownPromiseOf(inflight);
1000
1109
  }
1001
1110
  if (this.#scope.isClosed) {
1002
1111
  throw new DisposedContainerError();
@@ -1010,7 +1119,7 @@ export class DependencyResolver {
1010
1119
  // In-flight dedup, scoped flavor: one instance per scope even under concurrency.
1011
1120
  const inflight = this.#scope.getInflight(binding.identifier);
1012
1121
  if (inflight !== undefined) {
1013
- return inflight;
1122
+ return ownPromiseOf(inflight);
1014
1123
  }
1015
1124
  if (this.#scope.isClosed) {
1016
1125
  throw new DisposedContainerError();
@@ -1026,8 +1135,7 @@ export class DependencyResolver {
1026
1135
  if (branchDepth === ROOT_BRANCH) {
1027
1136
  this.#recentRootLevelContext = resolutionCtx;
1028
1137
  }
1029
- const dynamicResult = runFactoryPrefix(binding, resolutionCtx, levelStack);
1030
- return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
1138
+ return runFactoryPrefix(binding, resolutionCtx, levelStack);
1031
1139
  }
1032
1140
  const resolutionCtx = needsActivation || requiresResolutionContext(binding)
1033
1141
  ? new AsyncLevelContext(this, levelStack, options)
@@ -1035,33 +1143,41 @@ export class DependencyResolver {
1035
1143
  if (resolutionCtx !== undefined && branchDepth === ROOT_BRANCH) {
1036
1144
  this.#recentRootLevelContext = resolutionCtx;
1037
1145
  }
1146
+ const activated = this.#settleActivated(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner);
1147
+ if (scope === "transient") {
1148
+ return activated;
1149
+ }
1150
+ if (isPending(activated)) {
1151
+ return this.#publishInFlight(binding, activated, scope, owner);
1152
+ }
1038
1153
  if (scope === "singleton") {
1039
- // The promise is published before it settles, so concurrent callers dedup onto it.
1040
- const singletonPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
1154
+ this.#scope.setSingleton(binding, activated);
1155
+ }
1156
+ else {
1157
+ this.#scope.setScoped(binding, activated, owner);
1158
+ }
1159
+ return activated;
1160
+ }
1161
+ /**
1162
+ * Publishes a pending singleton or scoped materialisation so concurrent callers dedup onto it,
1163
+ * and caches what it settles to; the creator, like every joiner, is handed a promise of its own.
1164
+ */
1165
+ #publishInFlight(binding, pending, scope, owner) {
1166
+ const published = pending.then((activated) => {
1167
+ if (scope === "singleton") {
1041
1168
  this.#scope.setSingleton(binding, activated);
1042
- this.#scope.clearInflight(binding.identifier);
1043
- return activated;
1044
- }, (error) => {
1045
- this.#scope.clearInflight(binding.identifier);
1046
- throw error;
1047
- });
1048
- this.#scope.setInflight(binding.identifier, singletonPromise);
1049
- return await singletonPromise;
1050
- }
1051
- if (scope === "scoped") {
1052
- // Published before it settles, like the singleton lane: concurrent callers share one creation.
1053
- const scopedPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
1054
- this.#scope.setScoped(binding, activated);
1055
- this.#scope.clearInflight(binding.identifier);
1056
- return activated;
1057
- }, (error) => {
1058
- this.#scope.clearInflight(binding.identifier);
1059
- throw error;
1060
- });
1061
- this.#scope.setInflight(binding.identifier, scopedPromise);
1062
- return await scopedPromise;
1063
- }
1064
- return await this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner);
1169
+ }
1170
+ else {
1171
+ this.#scope.setScoped(binding, activated, owner);
1172
+ }
1173
+ this.#scope.clearInflight(binding.identifier);
1174
+ return activated;
1175
+ }, (error) => {
1176
+ this.#scope.clearInflight(binding.identifier);
1177
+ throw error;
1178
+ });
1179
+ this.#scope.setInflight(binding.identifier, published);
1180
+ return ownPromiseOf(published);
1065
1181
  }
1066
1182
  /**
1067
1183
  * Settles this resolver's own `postConstruct` answer for a class binding a parent owns.
@@ -1077,15 +1193,23 @@ export class DependencyResolver {
1077
1193
  this.#introspector().discoverPostConstruct(binding.target);
1078
1194
  }
1079
1195
  }
1080
- async #instantiateAndActivateAsync(binding, ctx, resolutionStack, branchDepth, needsActivation, owner) {
1081
- const instance = await this.#instantiateAsync(binding, ctx, resolutionStack, branchDepth);
1196
+ /** The activated instance: built and activated in this tick when nothing yielded, else its promise. */
1197
+ #settleActivated(binding, ctx, resolutionStack, branchDepth, needsActivation, owner) {
1198
+ const instance = this.#settleInstance(binding, ctx, resolutionStack, branchDepth);
1199
+ if (isPending(instance)) {
1200
+ return instance.then((built) => this.#activateAsync(binding, ctx, built, needsActivation, owner));
1201
+ }
1202
+ return this.#activateAsync(binding, ctx, instance, needsActivation, owner);
1203
+ }
1204
+ #activateAsync(binding, ctx, instance, needsActivation, owner) {
1082
1205
  this.#mirrorPostConstructFromOwner(binding, owner);
1083
1206
  if (!owner.#activationNeed().refreshAfterFirstInstantiation(binding, needsActivation)) {
1084
1207
  return instance;
1085
1208
  }
1086
1209
  return owner.#lifecycle.runActivation(ctx, binding, instance, owner.#metadataReader);
1087
1210
  }
1088
- async #instantiateAsync(binding, ctx, resolutionStack, branchDepth) {
1211
+ /** The instance a binding builds: in this tick when every dependency settled in it, else its promise. */
1212
+ #settleInstance(binding, ctx, resolutionStack, branchDepth) {
1089
1213
  switch (binding.kind) {
1090
1214
  case "constant":
1091
1215
  return binding.value;
@@ -1094,59 +1218,94 @@ export class DependencyResolver {
1094
1218
  if (ctx === undefined) {
1095
1219
  throw new InternalError("dynamic binding requires resolution context");
1096
1220
  }
1097
- const factoryResult = runFactoryPrefix(binding, ctx, resolutionStack);
1098
- return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1221
+ return runFactoryPrefix(binding, ctx, resolutionStack);
1099
1222
  }
1100
1223
  case "class": {
1101
- const deps = await this.#resolveDepsAsync(this.#constructorParams(binding.target), resolutionStack, branchDepth);
1224
+ const introspector = this.#introspector();
1225
+ const target = binding.target;
1226
+ const facts = introspector.facts(target);
1227
+ const params = this.#constructorParams(target, facts);
1228
+ const needsActiveContainer = introspector.needsActiveContainerOf(target, facts);
1102
1229
  // Accessor initializers resolve synchronously, so the branch-owned path serves them directly.
1103
- return this.#introspector().instantiate(binding.target, deps, this.#introspector().needsActiveContainer(binding.target)
1104
- ? this.#ambientResolutionFor(resolutionStack)
1105
- : undefined);
1230
+ const ambient = needsActiveContainer ? this.#ambientResolutionFor(resolutionStack) : undefined;
1231
+ if (params.length === 0) {
1232
+ return introspector.construct(target, NO_ARGUMENTS, needsActiveContainer, ambient);
1233
+ }
1234
+ const deps = this.#settleDeps(params, resolutionStack, branchDepth);
1235
+ if (isPending(deps)) {
1236
+ return deps.then((settled) => introspector.construct(target, settled, needsActiveContainer, ambient));
1237
+ }
1238
+ return introspector.construct(target, deps, needsActiveContainer, ambient);
1106
1239
  }
1107
1240
  case "resolved": {
1108
- const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
1109
- const factoryResult = binding.factory(...deps);
1110
- return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1241
+ const factory = binding.factory;
1242
+ const deps = this.#settleDeps(binding.deps, resolutionStack, branchDepth);
1243
+ return isPending(deps) ? deps.then((settled) => factory(...settled)) : factory(...deps);
1111
1244
  }
1112
1245
  case "resolved-async": {
1113
- const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
1114
- return binding.factory(...deps);
1246
+ const factory = binding.factory;
1247
+ const deps = this.#settleDeps(binding.deps, resolutionStack, branchDepth);
1248
+ return isPending(deps) ? deps.then((settled) => factory(...settled)) : factory(...deps);
1115
1249
  }
1116
1250
  case "alias":
1117
1251
  throw new InternalError("alias should have been followed before instantiation");
1118
1252
  }
1119
1253
  }
1120
- async #resolveDepsAsync(deps, resolutionStack, branchDepth) {
1254
+ /**
1255
+ * One level's dependencies: their values when every one settled in this tick, else the fan-out
1256
+ * that settles them in declaration order.
1257
+ */
1258
+ #settleDeps(deps, resolutionStack, branchDepth) {
1121
1259
  const count = deps.length;
1122
1260
  if (count === 0) {
1123
1261
  return [];
1124
1262
  }
1125
1263
  if (count === 1) {
1126
- return [await this.#resolveDepAsync(deps[0], resolutionStack, branchDepth)];
1264
+ const only = this.#settleDep(deps[0], resolutionStack, branchDepth);
1265
+ return isPending(only) ? only.then(settledAlone) : [only];
1127
1266
  }
1128
- // Siblings resolve concurrently and each extends the same branch, so the first appends in
1129
- // place and the rest copy the prefix — no caller has to isolate them.
1267
+ // Siblings start concurrently and each extends the same branch, so the first appends in
1268
+ // place and the rest copy the prefix — no caller has to isolate them. A sibling's sync throw
1269
+ // becomes its slot's rejection so the ones after it still start, as on every other lane.
1130
1270
  const pending = new Array(count);
1271
+ let yielded = false;
1131
1272
  for (let index = 0; index < count; index += 1) {
1132
- pending[index] = this.#resolveDepAsync(deps[index], resolutionStack, branchDepth);
1273
+ let settled;
1274
+ try {
1275
+ settled = this.#settleDep(deps[index], resolutionStack, branchDepth);
1276
+ }
1277
+ catch (dependencyError) {
1278
+ settled = Promise.reject(dependencyError);
1279
+ }
1280
+ if (isPending(settled)) {
1281
+ yielded = true;
1282
+ }
1283
+ pending[index] = settled;
1133
1284
  }
1134
- return settleInOrder(pending, identity);
1285
+ return yielded ? settleInOrder(pending, identity) : pending;
1135
1286
  }
1136
- #resolveDepAsync(dep, resolutionStack, branchDepth) {
1287
+ #settleDep(dep, resolutionStack, branchDepth) {
1137
1288
  const options = resolveOptionsForSlot(dep);
1138
1289
  if (dep.multi) {
1139
- return this.resolveAllAsync(dep.token, options, resolutionStack, branchDepth);
1290
+ return this.#settleCollection(dep.token, options, resolutionStack, branchDepth);
1140
1291
  }
1141
1292
  if (dep.optional) {
1142
- return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1293
+ return this.#settleOptionalRequest(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1143
1294
  }
1144
1295
  if (options === undefined) {
1145
- return this.resolveAsyncFromContext(dep.token, resolutionStack, branchDepth);
1296
+ return this.#settleDefault(dep.token, resolutionStack, branchDepth);
1297
+ }
1298
+ return this.#settleRequest(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1299
+ }
1300
+ resolveOptionalAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
1301
+ try {
1302
+ return asPromise(this.#settleOptionalRequest(token, options, resolutionStack, branchDepth, precomputedCriterion));
1303
+ }
1304
+ catch (requestError) {
1305
+ return Promise.reject(requestError);
1146
1306
  }
1147
- return this.resolveAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1148
1307
  }
1149
- async resolveOptionalAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
1308
+ #settleOptionalRequest(token, options, resolutionStack, branchDepth, precomputedCriterion) {
1150
1309
  const path = ownPrefixOf(resolutionStack, branchDepth);
1151
1310
  const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
1152
1311
  const singleCriterion = precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined);
@@ -1161,19 +1320,46 @@ export class DependencyResolver {
1161
1320
  }
1162
1321
  const { binding, owner } = entry;
1163
1322
  if (binding.scope === "singleton" && owner !== this) {
1164
- return owner.#resolveBindingAsync(binding, options, path, depth, owner);
1323
+ return owner.#settleBinding(binding, options, path, depth, owner);
1165
1324
  }
1166
- return this.#resolveBindingAsync(binding, options, path, depth, owner);
1325
+ return this.#settleBinding(binding, options, path, depth, owner);
1167
1326
  }
1168
- async resolveAllAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
1327
+ resolveAllAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
1328
+ try {
1329
+ return asPromise(this.#settleCollection(token, options, resolutionStack, branchDepth));
1330
+ }
1331
+ catch (collectionError) {
1332
+ return Promise.reject(collectionError);
1333
+ }
1334
+ }
1335
+ #settleCollection(token, options, resolutionStack, branchDepth) {
1169
1336
  const path = ownPrefixOf(resolutionStack, branchDepth);
1170
1337
  const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
1171
1338
  const candidates = this.#candidateBindings(token, options, path);
1172
- const pending = new Array(candidates.length);
1173
- for (let index = 0; index < candidates.length; index += 1) {
1174
- pending[index] = this.#resolveCandidateAsync(candidates[index], options, path, depth);
1339
+ return this.#settleCandidates(candidates, options, path, depth, identity);
1340
+ }
1341
+ /**
1342
+ * Every candidate of a fan-out, started in order: `apply` over their values when all settled in
1343
+ * this tick, else over the fan-out settled in declaration order.
1344
+ */
1345
+ #settleCandidates(candidates, options, resolutionStack, branchDepth, apply) {
1346
+ const count = candidates.length;
1347
+ const pending = new Array(count);
1348
+ let yielded = false;
1349
+ for (let index = 0; index < count; index += 1) {
1350
+ let settled;
1351
+ try {
1352
+ settled = this.#settleCandidate(candidates[index], options, resolutionStack, branchDepth);
1353
+ }
1354
+ catch (memberError) {
1355
+ settled = Promise.reject(memberError);
1356
+ }
1357
+ if (isPending(settled)) {
1358
+ yielded = true;
1359
+ }
1360
+ pending[index] = settled;
1175
1361
  }
1176
- return settleInOrder(pending, identity);
1362
+ return yielded ? settleInOrder(pending, apply) : apply(pending);
1177
1363
  }
1178
1364
  // ── Helpers ────────────────────────────────────────────────────────────────────────────────────────────────────────
1179
1365
  #allBindingsFromChain(token) {
@@ -1254,7 +1440,7 @@ export class DependencyResolver {
1254
1440
  if (!this.#scope.isChild) {
1255
1441
  throw new MissingScopeContextError(tokenName(binding.token));
1256
1442
  }
1257
- return this.#scope.readScoped(binding.identifier);
1443
+ return this.#scope.readScoped(binding.scopedCacheKey);
1258
1444
  }
1259
1445
  // The shared root context answers every top-level request; building one is the rarer half and
1260
1446
  // lives outside, so what a selection inlines is the test and not the literal.
@@ -1294,7 +1480,19 @@ export class DependencyResolver {
1294
1480
  binding.inFlight = false;
1295
1481
  }
1296
1482
  }
1297
- // Deliberately not `async`: that would allocate a state machine and a promise per level.
1483
+ /** A transient factory level with no activation: its factory's own result over the level's context. */
1484
+ #settleTransientFactory(binding, resolutionStack, branchDepth) {
1485
+ const frame = this.#getResolutionFrame(binding);
1486
+ const levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
1487
+ // Nothing this level appended is ever removed, so no level observes its own settlement.
1488
+ const ctx = new AsyncLevelContext(this, levelStack, undefined);
1489
+ if (branchDepth === ROOT_BRANCH) {
1490
+ this.#recentRootLevelContext = ctx;
1491
+ }
1492
+ return runFactoryPrefix(binding, ctx, levelStack);
1493
+ }
1494
+ // Deliberately not `async`: that would allocate a state machine and a promise per level. The
1495
+ // dominant lane keeps its body whole rather than reaching the settle core through a call.
1298
1496
  #resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth) {
1299
1497
  const frame = this.#getResolutionFrame(binding);
1300
1498
  let levelStack;
@@ -1405,33 +1603,33 @@ export class DependencyResolver {
1405
1603
  }
1406
1604
  return this.#resolveBinding(binding, options, resolutionStack, owner);
1407
1605
  }
1408
- #resolveCandidateAsync(binding, options, resolutionStack, branchDepth) {
1606
+ #settleCandidate(binding, options, resolutionStack, branchDepth) {
1409
1607
  if (binding.kind === "constant" && binding.activationHook === undefined && this.#chainActivationVersion() === 0) {
1410
- return Promise.resolve(binding.value);
1608
+ return binding.value;
1411
1609
  }
1412
1610
  const owner = this.#ownerOf(binding);
1413
1611
  if (owner.#isPlainConstant(binding)) {
1414
- return Promise.resolve(binding.value);
1612
+ return binding.value;
1415
1613
  }
1416
1614
  if (binding.kind === "alias") {
1417
- return this.resolveAsync(binding.target, options, resolutionStack, branchDepth);
1615
+ return this.#settleRequest(binding.target, options, resolutionStack, branchDepth);
1418
1616
  }
1419
1617
  if (binding.scope === "singleton") {
1420
1618
  if (binding.instance !== NO_INSTANCE) {
1421
- return Promise.resolve(binding.instance);
1619
+ return binding.instance;
1422
1620
  }
1423
- return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1621
+ return owner.#settleBinding(binding, options, resolutionStack, branchDepth, owner);
1424
1622
  }
1425
1623
  // The dominant collection member — a transient factory with no activation, asked with no
1426
- // options — takes the non-async lane a single resolve takes, so a fan-out costs one factory
1427
- // promise per member rather than a state machine on top of each.
1624
+ // options — takes the lane a single resolve takes, so a fan-out costs one factory result per
1625
+ // member rather than a promise on top of each.
1428
1626
  if (options === undefined &&
1429
1627
  binding.scope === "transient" &&
1430
1628
  (binding.kind === "dynamic" || binding.kind === "dynamic-async") &&
1431
1629
  !owner.#hasAnyActivation(binding)) {
1432
- return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
1630
+ return this.#settleTransientFactory(binding, resolutionStack, branchDepth);
1433
1631
  }
1434
- return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1632
+ return this.#settleBinding(binding, options, resolutionStack, branchDepth, owner);
1435
1633
  }
1436
1634
  /** The resolver whose registry holds `binding` — `this` (the common case) when it is own. */
1437
1635
  #ownerOf(binding) {
@@ -1547,6 +1745,26 @@ function asyncResolutionErrorFor(binding, resolutionStack) {
1547
1745
  function identity(values) {
1548
1746
  return values;
1549
1747
  }
1748
+ /** A settled value, as it is. */
1749
+ function settledValue(value) {
1750
+ return value;
1751
+ }
1752
+ /** A promise of its own over a shared in-flight one: the same settlement, never the stored object. */
1753
+ function ownPromiseOf(shared) {
1754
+ return shared.then(settledValue);
1755
+ }
1756
+ /** A lone dependency's settled value as the argument list its level applies. */
1757
+ function settledAlone(value) {
1758
+ return [value];
1759
+ }
1760
+ /** Whether an async-lane answer is still pending, narrowing it to the promise it is. */
1761
+ function isPending(answer) {
1762
+ return answer instanceof Promise;
1763
+ }
1764
+ /** A settled answer as the promise an async entry point hands out; a pending one passes through. */
1765
+ function asPromise(answer) {
1766
+ return answer instanceof Promise ? answer : Promise.resolve(answer);
1767
+ }
1550
1768
  /** Only a factory is handed the resolution context; everything else gets its deps directly. */
1551
1769
  function requiresResolutionContext(binding) {
1552
1770
  return binding.kind === "dynamic" || binding.kind === "dynamic-async";