@codefast/di 0.11.0 → 0.12.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 (77) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +26 -12
  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 -2
  6. package/dist/container/container.js +107 -64
  7. package/dist/core/binding-builders.d.ts +114 -0
  8. package/dist/core/binding-declaration.d.ts +7 -0
  9. package/dist/core/binding-declaration.js +3 -2
  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.js +5 -5
  17. package/dist/decorators/injectable.js +2 -2
  18. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +3 -3
  19. package/dist/decorators/{decorator-metadata.js → metadata-record.js} +1 -1
  20. package/dist/{errors/errors.d.ts → errors.d.ts} +7 -6
  21. package/dist/{errors/errors.js → errors.js} +8 -7
  22. package/dist/index.d.ts +12 -10
  23. package/dist/index.js +6 -6
  24. package/dist/injection/descriptor.d.ts +1 -1
  25. package/dist/introspection/dependency-graph.d.ts +1 -1
  26. package/dist/introspection/dependency-graph.js +3 -4
  27. package/dist/introspection/explanation.d.ts +101 -0
  28. package/dist/introspection/explanation.js +151 -0
  29. package/dist/introspection/inspector.d.ts +9 -2
  30. package/dist/introspection/inspector.js +25 -20
  31. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
  32. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +45 -28
  33. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
  34. package/dist/lifecycle/{scope-manager.js → scopes.js} +8 -19
  35. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  36. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  37. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  38. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  39. package/dist/metadata/types.js +1 -0
  40. package/dist/metadata/verify.d.ts +43 -0
  41. package/dist/metadata/verify.js +133 -0
  42. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +1 -1
  43. package/dist/metadata/{verifying-metadata-reader.js → verifying-reader.js} +2 -2
  44. package/dist/resolution/cache/activation-need.d.ts +1 -1
  45. package/dist/resolution/cache/activation-need.js +5 -1
  46. package/dist/resolution/cache/class-introspector.d.ts +27 -36
  47. package/dist/resolution/cache/class-introspector.js +61 -159
  48. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -2
  49. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +40 -8
  50. package/dist/resolution/context.d.ts +1 -1
  51. package/dist/resolution/context.js +1 -1
  52. package/dist/resolution/{path/resolution-path.js → path.js} +1 -1
  53. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +2 -4
  54. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +2 -4
  55. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  56. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +4 -4
  57. package/dist/resolution/resolver.d.ts +7 -7
  58. package/dist/resolution/resolver.js +308 -121
  59. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  60. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  61. package/dist/resolution/select/constraints.js +1 -1
  62. package/package.json +79 -71
  63. package/dist/core/binding-scope.d.ts +0 -12
  64. package/dist/core/binding-scope.js +0 -12
  65. /package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +0 -0
  66. /package/dist/{ambient/active-container.js → ambient-container.js} +0 -0
  67. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  68. /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
  69. /package/dist/decorators/{decorator-metadata.d.ts → metadata-record.d.ts} +0 -0
  70. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  71. /package/dist/injection/{resolve-options.js → dependency-slot.js} +0 -0
  72. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  73. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  74. /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
  75. /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
  76. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
  77. /package/dist/resolution/{path/resolution-path.d.ts → path.d.ts} +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, 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
@@ -514,7 +516,7 @@ export class DependencyResolver {
514
516
  }
515
517
  return this.resolve(token, options, resolutionStack);
516
518
  },
517
- // Dispatches exactly as #resolveDepAsync does, for the async lane's escapes.
519
+ // Dispatches exactly as #settleDep does, for the async lane's escapes.
518
520
  resolveEscapedAsync: (token, options, arity, resolutionStack) => {
519
521
  if (arity === "all") {
520
522
  return this.resolveAllAsync(token, options, resolutionStack, UNOWNED_BRANCH);
@@ -691,10 +693,13 @@ export class DependencyResolver {
691
693
  case "dynamic-async":
692
694
  throw asyncResolutionErrorFor(binding, resolutionStack);
693
695
  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);
696
+ // One lookup of the class's facts serves the params, the accessor need and the construction.
697
+ const introspector = this.#introspector();
698
+ const target = binding.target;
699
+ const facts = introspector.facts(target);
700
+ const params = this.#constructorParams(target, facts);
701
+ const needsActiveContainer = introspector.needsActiveContainerOf(target, facts);
702
+ return introspector.construct(target, params.length === 0 ? NO_ARGUMENTS : this.#resolveDeps(params, resolutionStack), needsActiveContainer, needsActiveContainer ? this.#ambientResolutionFor(resolutionStack) : undefined);
698
703
  }
699
704
  case "resolved": {
700
705
  const deps = this.#resolveDeps(binding.deps, resolutionStack);
@@ -716,8 +721,12 @@ export class DependencyResolver {
716
721
  * @remarks A class the metadata reader knows nothing about is constructible only if it declares
717
722
  * no parameters; anything else is a missing `@injectable()`.
718
723
  */
719
- #constructorParams(target) {
720
- const meta = this.#introspector().constructorMetadata(target);
724
+ #constructorParams(target, facts) {
725
+ // Metadata is fixed once a class is defined, so the answer is too; only a throw is left unsettled.
726
+ return (facts.params ??= this.#settleConstructorParams(target, facts));
727
+ }
728
+ #settleConstructorParams(target, facts) {
729
+ const meta = this.#introspector().constructorMetadataOf(target, facts);
721
730
  if (meta !== undefined) {
722
731
  return meta.params;
723
732
  }
@@ -824,30 +833,35 @@ export class DependencyResolver {
824
833
  for (let index = 0; index < candidates.length; index += 1) {
825
834
  resolved[index] = this.#resolveCandidateSync(candidates[index], undefined, resolutionStack);
826
835
  }
827
- // The members this read materialised may have made the list stable for the next one.
828
- this.#settleCollectionValues(memo);
836
+ this.#settleOnRepeat(memo);
829
837
  return resolved;
830
838
  }
831
- /** The async twin of `resolveRootCollection`: a stable value list settles at once, candidates fan out as usual. */
839
+ /** The async twin of `resolveRootCollection`: a stable value list answers at once, candidates fan out as usual. */
832
840
  resolveRootCollectionAsync(token) {
833
841
  // The async lane appends to its branch and never unwinds, so it works on a stack of its own.
834
842
  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());
843
+ try {
844
+ // Selection runs `when()` predicates and follows aliases, either of which may throw.
845
+ const memo = this.#rootCollection(token, resolutionStack);
846
+ if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
847
+ return Promise.resolve(memo.values.slice());
848
+ }
849
+ return asPromise(this.#settleCandidates(memo.candidates, undefined, resolutionStack, UNOWNED_BRANCH, (values) => {
850
+ this.#settleOnRepeat(memo);
851
+ return values;
852
+ }));
853
+ }
854
+ catch (collectionError) {
855
+ return Promise.reject(collectionError);
838
856
  }
839
- return settleInOrder(memo.candidates.map((candidate) => this.#resolveCandidateAsync(candidate, undefined, resolutionStack, UNOWNED_BRANCH)), (values) => {
840
- this.#settleCollectionValues(memo);
841
- return values;
842
- });
843
857
  }
844
858
  /**
845
859
  * The memoized candidate list of a root-level, options-less collection, built on its first read.
846
860
  *
847
861
  * @remarks Sound because a `when()` predicate is pure over its context and the root context is a
848
862
  * 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.
863
+ * memo is stamped with. From the second read on, the value list is kept too while every member is
864
+ * a hook-free constant and no activation hook exists anywhere in the chain.
851
865
  */
852
866
  #rootCollection(token, resolutionStack) {
853
867
  const memo = this.#lookup.collection(token);
@@ -855,11 +869,20 @@ export class DependencyResolver {
855
869
  return memo;
856
870
  }
857
871
  const candidates = this.#candidateBindings(token, undefined, resolutionStack);
858
- const entry = { candidates, values: undefined, activationVersion: -1 };
859
- this.#settleCollectionValues(entry);
872
+ const entry = { candidates, values: undefined, activationVersion: -1, readBefore: false };
860
873
  this.#lookup.rememberCollection(token, entry);
861
874
  return entry;
862
875
  }
876
+ // The first read interprets and the read that repeats it settles the value list, as a plan compiles on repeat;
877
+ // the members either read materialised may have made the list stable for the next one.
878
+ #settleOnRepeat(entry) {
879
+ if (entry.readBefore) {
880
+ this.#settleCollectionValues(entry);
881
+ }
882
+ else {
883
+ entry.readBefore = true;
884
+ }
885
+ }
863
886
  /**
864
887
  * Fills a collection memo's value list once every member is stable: a hook-free constant, or a
865
888
  * hook-free singleton whose instance is cached — anything that changes either bumps a registry
@@ -894,6 +917,18 @@ export class DependencyResolver {
894
917
  const selected = selectAllBindings(allBindings, options, this.#makeConstraintContext(resolutionStack, options));
895
918
  return this.#withoutDanglingAliases(selected, options, resolutionStack);
896
919
  }
920
+ // A chain none of whose registries ever held an alias has none to drop, which spares the scan.
921
+ #chainHeldAlias() {
922
+ if (this.#registry.hasHeldAlias) {
923
+ return true;
924
+ }
925
+ for (let current = this.#parent; current !== undefined; current = current.#parent) {
926
+ if (current.#registry.hasHeldAlias) {
927
+ return true;
928
+ }
929
+ }
930
+ return false;
931
+ }
897
932
  /**
898
933
  * The candidates with any alias whose chain ends nowhere dropped, so a fan-out skips a dangling
899
934
  * alias rather than throwing on it.
@@ -902,6 +937,9 @@ export class DependencyResolver {
902
937
  * is returned untouched, so nothing is allocated for it.
903
938
  */
904
939
  #withoutDanglingAliases(candidates, options, resolutionStack) {
940
+ if (!this.#chainHeldAlias()) {
941
+ return candidates;
942
+ }
905
943
  let hasAlias = false;
906
944
  for (let index = 0; index < candidates.length; index += 1) {
907
945
  if (candidates[index].kind === "alias") {
@@ -936,35 +974,55 @@ export class DependencyResolver {
936
974
  }
937
975
  return this.#resolveAsyncDefaultEntry(entry.binding, entry.owner, resolutionStack, branchDepth);
938
976
  }
977
+ // A `#settle*` lane answers with the value when its level completed in this tick and with a promise
978
+ // only where something had to be awaited, so a singleton or scoped instance built without yielding
979
+ // is cached before the fan-out that started it moves on to a sibling that may read it synchronously.
939
980
  #resolveAsyncDefaultEntry(binding, owner, resolutionStack, branchDepth) {
981
+ try {
982
+ return asPromise(this.#settleDefaultEntry(binding, owner, resolutionStack, branchDepth));
983
+ }
984
+ catch (entryError) {
985
+ // Not `async`: a failure is turned into the rejection the entry point promises.
986
+ return Promise.reject(entryError);
987
+ }
988
+ }
989
+ /** `resolveAsyncFromContext` for a dependency slot: the same lanes, answered in this tick where they can be. */
990
+ #settleDefault(token, resolutionStack, branchDepth) {
991
+ const fastBinding = this.#registry.getFastDefault(token);
992
+ if (fastBinding !== undefined && fastBinding.kind !== "alias") {
993
+ return this.#settleDefaultEntry(fastBinding, this, resolutionStack, branchDepth);
994
+ }
995
+ const entry = this.#lookup.defaultEntry(token);
996
+ if (entry === null) {
997
+ return this.#settleRequest(token, undefined, resolutionStack, branchDepth);
998
+ }
999
+ return this.#settleDefaultEntry(entry.binding, entry.owner, resolutionStack, branchDepth);
1000
+ }
1001
+ #settleDefaultEntry(binding, owner, resolutionStack, branchDepth) {
940
1002
  if (owner.#isPlainConstant(binding)) {
941
- return Promise.resolve(binding.value);
1003
+ return binding.value;
942
1004
  }
943
1005
  const scope = binding.scope;
944
1006
  if (scope === "transient") {
945
1007
  if ((binding.kind === "dynamic" || binding.kind === "dynamic-async") && !owner.#hasAnyActivation(binding)) {
946
- return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
1008
+ return this.#settleTransientFactory(binding, resolutionStack, branchDepth);
947
1009
  }
948
1010
  }
949
1011
  else if (scope === "singleton") {
950
1012
  if (binding.instance !== NO_INSTANCE) {
951
- return Promise.resolve(binding.instance);
1013
+ return binding.instance;
952
1014
  }
953
1015
  if (owner !== this) {
954
- return owner.#resolveBindingAsync(binding, undefined, resolutionStack, branchDepth, owner);
1016
+ return owner.#settleBinding(binding, undefined, resolutionStack, branchDepth, owner);
955
1017
  }
956
1018
  }
957
- else if (this.#scope.isChild) {
958
- const cachedScoped = this.#scope.readScoped(binding.identifier);
1019
+ else {
1020
+ const cachedScoped = this.#readScoped(binding);
959
1021
  if (cachedScoped !== SCOPED_MISS) {
960
- return Promise.resolve(cachedScoped);
1022
+ return cachedScoped;
961
1023
  }
962
1024
  }
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);
1025
+ return this.#settleBinding(binding, undefined, resolutionStack, branchDepth, owner);
968
1026
  }
969
1027
  /**
970
1028
  * Instantiates one owned binding directly, bypassing selection.
@@ -973,18 +1031,38 @@ export class DependencyResolver {
973
1031
  * could pick a different candidate whose criteria are a subset of them.
974
1032
  */
975
1033
  warmBindingAsync(binding, options) {
976
- return this.#resolveBindingAsync(binding, options, [], ROOT_BRANCH, this);
1034
+ try {
1035
+ return asPromise(this.#settleBinding(binding, options, [], ROOT_BRANCH, this));
1036
+ }
1037
+ catch (bindingError) {
1038
+ return Promise.reject(bindingError);
1039
+ }
977
1040
  }
978
- async resolveAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
1041
+ resolveAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
1042
+ try {
1043
+ return asPromise(this.#settleRequest(token, options, resolutionStack, branchDepth, precomputedCriterion));
1044
+ }
1045
+ catch (requestError) {
1046
+ return Promise.reject(requestError);
1047
+ }
1048
+ }
1049
+ /** Selects the binding a request names and settles it on the async lane. */
1050
+ #settleRequest(token, options, resolutionStack, branchDepth, precomputedCriterion) {
979
1051
  const path = ownPrefixOf(resolutionStack, branchDepth);
980
1052
  const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
981
1053
  const { binding, owner } = this.#requireBinding(token, options, path, precomputedCriterion);
982
1054
  if (binding.scope === "singleton" && owner !== this) {
983
- return owner.#resolveBindingAsync(binding, options, path, depth, owner);
1055
+ return owner.#settleBinding(binding, options, path, depth, owner);
984
1056
  }
985
- return this.#resolveBindingAsync(binding, options, path, depth, owner);
1057
+ return this.#settleBinding(binding, options, path, depth, owner);
986
1058
  }
987
- async #resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner) {
1059
+ /**
1060
+ * Materialises one binding on the async lane: the instance once nothing yielded, else its promise.
1061
+ *
1062
+ * @remarks A singleton or scoped instance built in this tick is cached in this tick, so a sibling
1063
+ * reading it synchronously finds it; only a materialisation still pending is published in flight.
1064
+ */
1065
+ #settleBinding(binding, options, resolutionStack, branchDepth, owner) {
988
1066
  if (owner.#isPlainConstant(binding)) {
989
1067
  return binding.value;
990
1068
  }
@@ -996,7 +1074,7 @@ export class DependencyResolver {
996
1074
  // In-flight dedup: concurrent callers share the first creation.
997
1075
  const inflight = this.#scope.getInflight(binding.identifier);
998
1076
  if (inflight !== undefined) {
999
- return inflight;
1077
+ return ownPromiseOf(inflight);
1000
1078
  }
1001
1079
  if (this.#scope.isClosed) {
1002
1080
  throw new DisposedContainerError();
@@ -1010,7 +1088,7 @@ export class DependencyResolver {
1010
1088
  // In-flight dedup, scoped flavor: one instance per scope even under concurrency.
1011
1089
  const inflight = this.#scope.getInflight(binding.identifier);
1012
1090
  if (inflight !== undefined) {
1013
- return inflight;
1091
+ return ownPromiseOf(inflight);
1014
1092
  }
1015
1093
  if (this.#scope.isClosed) {
1016
1094
  throw new DisposedContainerError();
@@ -1026,8 +1104,7 @@ export class DependencyResolver {
1026
1104
  if (branchDepth === ROOT_BRANCH) {
1027
1105
  this.#recentRootLevelContext = resolutionCtx;
1028
1106
  }
1029
- const dynamicResult = runFactoryPrefix(binding, resolutionCtx, levelStack);
1030
- return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
1107
+ return runFactoryPrefix(binding, resolutionCtx, levelStack);
1031
1108
  }
1032
1109
  const resolutionCtx = needsActivation || requiresResolutionContext(binding)
1033
1110
  ? new AsyncLevelContext(this, levelStack, options)
@@ -1035,33 +1112,41 @@ export class DependencyResolver {
1035
1112
  if (resolutionCtx !== undefined && branchDepth === ROOT_BRANCH) {
1036
1113
  this.#recentRootLevelContext = resolutionCtx;
1037
1114
  }
1115
+ const activated = this.#settleActivated(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner);
1116
+ if (scope === "transient") {
1117
+ return activated;
1118
+ }
1119
+ if (isPending(activated)) {
1120
+ return this.#publishInFlight(binding, activated, scope);
1121
+ }
1038
1122
  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) => {
1123
+ this.#scope.setSingleton(binding, activated);
1124
+ }
1125
+ else {
1126
+ this.#scope.setScoped(binding, activated);
1127
+ }
1128
+ return activated;
1129
+ }
1130
+ /**
1131
+ * Publishes a pending singleton or scoped materialisation so concurrent callers dedup onto it,
1132
+ * and caches what it settles to; the creator, like every joiner, is handed a promise of its own.
1133
+ */
1134
+ #publishInFlight(binding, pending, scope) {
1135
+ const published = pending.then((activated) => {
1136
+ if (scope === "singleton") {
1041
1137
  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) => {
1138
+ }
1139
+ else {
1054
1140
  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);
1141
+ }
1142
+ this.#scope.clearInflight(binding.identifier);
1143
+ return activated;
1144
+ }, (error) => {
1145
+ this.#scope.clearInflight(binding.identifier);
1146
+ throw error;
1147
+ });
1148
+ this.#scope.setInflight(binding.identifier, published);
1149
+ return ownPromiseOf(published);
1065
1150
  }
1066
1151
  /**
1067
1152
  * Settles this resolver's own `postConstruct` answer for a class binding a parent owns.
@@ -1077,15 +1162,23 @@ export class DependencyResolver {
1077
1162
  this.#introspector().discoverPostConstruct(binding.target);
1078
1163
  }
1079
1164
  }
1080
- async #instantiateAndActivateAsync(binding, ctx, resolutionStack, branchDepth, needsActivation, owner) {
1081
- const instance = await this.#instantiateAsync(binding, ctx, resolutionStack, branchDepth);
1165
+ /** The activated instance: built and activated in this tick when nothing yielded, else its promise. */
1166
+ #settleActivated(binding, ctx, resolutionStack, branchDepth, needsActivation, owner) {
1167
+ const instance = this.#settleInstance(binding, ctx, resolutionStack, branchDepth);
1168
+ if (isPending(instance)) {
1169
+ return instance.then((built) => this.#activateAsync(binding, ctx, built, needsActivation, owner));
1170
+ }
1171
+ return this.#activateAsync(binding, ctx, instance, needsActivation, owner);
1172
+ }
1173
+ #activateAsync(binding, ctx, instance, needsActivation, owner) {
1082
1174
  this.#mirrorPostConstructFromOwner(binding, owner);
1083
1175
  if (!owner.#activationNeed().refreshAfterFirstInstantiation(binding, needsActivation)) {
1084
1176
  return instance;
1085
1177
  }
1086
1178
  return owner.#lifecycle.runActivation(ctx, binding, instance, owner.#metadataReader);
1087
1179
  }
1088
- async #instantiateAsync(binding, ctx, resolutionStack, branchDepth) {
1180
+ /** The instance a binding builds: in this tick when every dependency settled in it, else its promise. */
1181
+ #settleInstance(binding, ctx, resolutionStack, branchDepth) {
1089
1182
  switch (binding.kind) {
1090
1183
  case "constant":
1091
1184
  return binding.value;
@@ -1094,59 +1187,94 @@ export class DependencyResolver {
1094
1187
  if (ctx === undefined) {
1095
1188
  throw new InternalError("dynamic binding requires resolution context");
1096
1189
  }
1097
- const factoryResult = runFactoryPrefix(binding, ctx, resolutionStack);
1098
- return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
1190
+ return runFactoryPrefix(binding, ctx, resolutionStack);
1099
1191
  }
1100
1192
  case "class": {
1101
- const deps = await this.#resolveDepsAsync(this.#constructorParams(binding.target), resolutionStack, branchDepth);
1193
+ const introspector = this.#introspector();
1194
+ const target = binding.target;
1195
+ const facts = introspector.facts(target);
1196
+ const params = this.#constructorParams(target, facts);
1197
+ const needsActiveContainer = introspector.needsActiveContainerOf(target, facts);
1102
1198
  // 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);
1199
+ const ambient = needsActiveContainer ? this.#ambientResolutionFor(resolutionStack) : undefined;
1200
+ if (params.length === 0) {
1201
+ return introspector.construct(target, NO_ARGUMENTS, needsActiveContainer, ambient);
1202
+ }
1203
+ const deps = this.#settleDeps(params, resolutionStack, branchDepth);
1204
+ if (isPending(deps)) {
1205
+ return deps.then((settled) => introspector.construct(target, settled, needsActiveContainer, ambient));
1206
+ }
1207
+ return introspector.construct(target, deps, needsActiveContainer, ambient);
1106
1208
  }
1107
1209
  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);
1210
+ const factory = binding.factory;
1211
+ const deps = this.#settleDeps(binding.deps, resolutionStack, branchDepth);
1212
+ return isPending(deps) ? deps.then((settled) => factory(...settled)) : factory(...deps);
1111
1213
  }
1112
1214
  case "resolved-async": {
1113
- const deps = await this.#resolveDepsAsync(binding.deps, resolutionStack, branchDepth);
1114
- return binding.factory(...deps);
1215
+ const factory = binding.factory;
1216
+ const deps = this.#settleDeps(binding.deps, resolutionStack, branchDepth);
1217
+ return isPending(deps) ? deps.then((settled) => factory(...settled)) : factory(...deps);
1115
1218
  }
1116
1219
  case "alias":
1117
1220
  throw new InternalError("alias should have been followed before instantiation");
1118
1221
  }
1119
1222
  }
1120
- async #resolveDepsAsync(deps, resolutionStack, branchDepth) {
1223
+ /**
1224
+ * One level's dependencies: their values when every one settled in this tick, else the fan-out
1225
+ * that settles them in declaration order.
1226
+ */
1227
+ #settleDeps(deps, resolutionStack, branchDepth) {
1121
1228
  const count = deps.length;
1122
1229
  if (count === 0) {
1123
1230
  return [];
1124
1231
  }
1125
1232
  if (count === 1) {
1126
- return [await this.#resolveDepAsync(deps[0], resolutionStack, branchDepth)];
1233
+ const only = this.#settleDep(deps[0], resolutionStack, branchDepth);
1234
+ return isPending(only) ? only.then(settledAlone) : [only];
1127
1235
  }
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.
1236
+ // Siblings start concurrently and each extends the same branch, so the first appends in
1237
+ // place and the rest copy the prefix — no caller has to isolate them. A sibling's sync throw
1238
+ // becomes its slot's rejection so the ones after it still start, as on every other lane.
1130
1239
  const pending = new Array(count);
1240
+ let yielded = false;
1131
1241
  for (let index = 0; index < count; index += 1) {
1132
- pending[index] = this.#resolveDepAsync(deps[index], resolutionStack, branchDepth);
1242
+ let settled;
1243
+ try {
1244
+ settled = this.#settleDep(deps[index], resolutionStack, branchDepth);
1245
+ }
1246
+ catch (dependencyError) {
1247
+ settled = Promise.reject(dependencyError);
1248
+ }
1249
+ if (isPending(settled)) {
1250
+ yielded = true;
1251
+ }
1252
+ pending[index] = settled;
1133
1253
  }
1134
- return settleInOrder(pending, identity);
1254
+ return yielded ? settleInOrder(pending, identity) : pending;
1135
1255
  }
1136
- #resolveDepAsync(dep, resolutionStack, branchDepth) {
1256
+ #settleDep(dep, resolutionStack, branchDepth) {
1137
1257
  const options = resolveOptionsForSlot(dep);
1138
1258
  if (dep.multi) {
1139
- return this.resolveAllAsync(dep.token, options, resolutionStack, branchDepth);
1259
+ return this.#settleCollection(dep.token, options, resolutionStack, branchDepth);
1140
1260
  }
1141
1261
  if (dep.optional) {
1142
- return this.resolveOptionalAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1262
+ return this.#settleOptionalRequest(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1143
1263
  }
1144
1264
  if (options === undefined) {
1145
- return this.resolveAsyncFromContext(dep.token, resolutionStack, branchDepth);
1265
+ return this.#settleDefault(dep.token, resolutionStack, branchDepth);
1146
1266
  }
1147
- return this.resolveAsync(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1267
+ return this.#settleRequest(dep.token, options, resolutionStack, branchDepth, singleCriterionForSlot(dep));
1148
1268
  }
1149
- async resolveOptionalAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
1269
+ resolveOptionalAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH, precomputedCriterion) {
1270
+ try {
1271
+ return asPromise(this.#settleOptionalRequest(token, options, resolutionStack, branchDepth, precomputedCriterion));
1272
+ }
1273
+ catch (requestError) {
1274
+ return Promise.reject(requestError);
1275
+ }
1276
+ }
1277
+ #settleOptionalRequest(token, options, resolutionStack, branchDepth, precomputedCriterion) {
1150
1278
  const path = ownPrefixOf(resolutionStack, branchDepth);
1151
1279
  const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
1152
1280
  const singleCriterion = precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined);
@@ -1161,19 +1289,46 @@ export class DependencyResolver {
1161
1289
  }
1162
1290
  const { binding, owner } = entry;
1163
1291
  if (binding.scope === "singleton" && owner !== this) {
1164
- return owner.#resolveBindingAsync(binding, options, path, depth, owner);
1292
+ return owner.#settleBinding(binding, options, path, depth, owner);
1293
+ }
1294
+ return this.#settleBinding(binding, options, path, depth, owner);
1295
+ }
1296
+ resolveAllAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
1297
+ try {
1298
+ return asPromise(this.#settleCollection(token, options, resolutionStack, branchDepth));
1299
+ }
1300
+ catch (collectionError) {
1301
+ return Promise.reject(collectionError);
1165
1302
  }
1166
- return this.#resolveBindingAsync(binding, options, path, depth, owner);
1167
1303
  }
1168
- async resolveAllAsync(token, options, resolutionStack, branchDepth = UNOWNED_BRANCH) {
1304
+ #settleCollection(token, options, resolutionStack, branchDepth) {
1169
1305
  const path = ownPrefixOf(resolutionStack, branchDepth);
1170
1306
  const depth = path === resolutionStack ? branchDepth : branchDepthOf(path);
1171
1307
  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);
1308
+ return this.#settleCandidates(candidates, options, path, depth, identity);
1309
+ }
1310
+ /**
1311
+ * Every candidate of a fan-out, started in order: `apply` over their values when all settled in
1312
+ * this tick, else over the fan-out settled in declaration order.
1313
+ */
1314
+ #settleCandidates(candidates, options, resolutionStack, branchDepth, apply) {
1315
+ const count = candidates.length;
1316
+ const pending = new Array(count);
1317
+ let yielded = false;
1318
+ for (let index = 0; index < count; index += 1) {
1319
+ let settled;
1320
+ try {
1321
+ settled = this.#settleCandidate(candidates[index], options, resolutionStack, branchDepth);
1322
+ }
1323
+ catch (memberError) {
1324
+ settled = Promise.reject(memberError);
1325
+ }
1326
+ if (isPending(settled)) {
1327
+ yielded = true;
1328
+ }
1329
+ pending[index] = settled;
1175
1330
  }
1176
- return settleInOrder(pending, identity);
1331
+ return yielded ? settleInOrder(pending, apply) : apply(pending);
1177
1332
  }
1178
1333
  // ── Helpers ────────────────────────────────────────────────────────────────────────────────────────────────────────
1179
1334
  #allBindingsFromChain(token) {
@@ -1254,7 +1409,7 @@ export class DependencyResolver {
1254
1409
  if (!this.#scope.isChild) {
1255
1410
  throw new MissingScopeContextError(tokenName(binding.token));
1256
1411
  }
1257
- return this.#scope.readScoped(binding.identifier);
1412
+ return this.#scope.readScoped(binding.scopedCacheKey);
1258
1413
  }
1259
1414
  // The shared root context answers every top-level request; building one is the rarer half and
1260
1415
  // lives outside, so what a selection inlines is the test and not the literal.
@@ -1294,7 +1449,19 @@ export class DependencyResolver {
1294
1449
  binding.inFlight = false;
1295
1450
  }
1296
1451
  }
1297
- // Deliberately not `async`: that would allocate a state machine and a promise per level.
1452
+ /** A transient factory level with no activation: its factory's own result over the level's context. */
1453
+ #settleTransientFactory(binding, resolutionStack, branchDepth) {
1454
+ const frame = this.#getResolutionFrame(binding);
1455
+ const levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
1456
+ // Nothing this level appended is ever removed, so no level observes its own settlement.
1457
+ const ctx = new AsyncLevelContext(this, levelStack, undefined);
1458
+ if (branchDepth === ROOT_BRANCH) {
1459
+ this.#recentRootLevelContext = ctx;
1460
+ }
1461
+ return runFactoryPrefix(binding, ctx, levelStack);
1462
+ }
1463
+ // Deliberately not `async`: that would allocate a state machine and a promise per level. The
1464
+ // dominant lane keeps its body whole rather than reaching the settle core through a call.
1298
1465
  #resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth) {
1299
1466
  const frame = this.#getResolutionFrame(binding);
1300
1467
  let levelStack;
@@ -1405,33 +1572,33 @@ export class DependencyResolver {
1405
1572
  }
1406
1573
  return this.#resolveBinding(binding, options, resolutionStack, owner);
1407
1574
  }
1408
- #resolveCandidateAsync(binding, options, resolutionStack, branchDepth) {
1575
+ #settleCandidate(binding, options, resolutionStack, branchDepth) {
1409
1576
  if (binding.kind === "constant" && binding.activationHook === undefined && this.#chainActivationVersion() === 0) {
1410
- return Promise.resolve(binding.value);
1577
+ return binding.value;
1411
1578
  }
1412
1579
  const owner = this.#ownerOf(binding);
1413
1580
  if (owner.#isPlainConstant(binding)) {
1414
- return Promise.resolve(binding.value);
1581
+ return binding.value;
1415
1582
  }
1416
1583
  if (binding.kind === "alias") {
1417
- return this.resolveAsync(binding.target, options, resolutionStack, branchDepth);
1584
+ return this.#settleRequest(binding.target, options, resolutionStack, branchDepth);
1418
1585
  }
1419
1586
  if (binding.scope === "singleton") {
1420
1587
  if (binding.instance !== NO_INSTANCE) {
1421
- return Promise.resolve(binding.instance);
1588
+ return binding.instance;
1422
1589
  }
1423
- return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1590
+ return owner.#settleBinding(binding, options, resolutionStack, branchDepth, owner);
1424
1591
  }
1425
1592
  // 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.
1593
+ // options — takes the lane a single resolve takes, so a fan-out costs one factory result per
1594
+ // member rather than a promise on top of each.
1428
1595
  if (options === undefined &&
1429
1596
  binding.scope === "transient" &&
1430
1597
  (binding.kind === "dynamic" || binding.kind === "dynamic-async") &&
1431
1598
  !owner.#hasAnyActivation(binding)) {
1432
- return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
1599
+ return this.#settleTransientFactory(binding, resolutionStack, branchDepth);
1433
1600
  }
1434
- return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1601
+ return this.#settleBinding(binding, options, resolutionStack, branchDepth, owner);
1435
1602
  }
1436
1603
  /** The resolver whose registry holds `binding` — `this` (the common case) when it is own. */
1437
1604
  #ownerOf(binding) {
@@ -1547,6 +1714,26 @@ function asyncResolutionErrorFor(binding, resolutionStack) {
1547
1714
  function identity(values) {
1548
1715
  return values;
1549
1716
  }
1717
+ /** A settled value, as it is. */
1718
+ function settledValue(value) {
1719
+ return value;
1720
+ }
1721
+ /** A promise of its own over a shared in-flight one: the same settlement, never the stored object. */
1722
+ function ownPromiseOf(shared) {
1723
+ return shared.then(settledValue);
1724
+ }
1725
+ /** A lone dependency's settled value as the argument list its level applies. */
1726
+ function settledAlone(value) {
1727
+ return [value];
1728
+ }
1729
+ /** Whether an async-lane answer is still pending, narrowing it to the promise it is. */
1730
+ function isPending(answer) {
1731
+ return answer instanceof Promise;
1732
+ }
1733
+ /** A settled answer as the promise an async entry point hands out; a pending one passes through. */
1734
+ function asPromise(answer) {
1735
+ return answer instanceof Promise ? answer : Promise.resolve(answer);
1736
+ }
1550
1737
  /** Only a factory is handed the resolution context; everything else gets its deps directly. */
1551
1738
  function requiresResolutionContext(binding) {
1552
1739
  return binding.kind === "dynamic" || binding.kind === "dynamic-async";