ts-ioc-container 67.0.0 → 68.1.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 (43) hide show
  1. package/README.md +240 -92
  2. package/cjm/errors/ArgumentNotFoundError.js +12 -0
  3. package/cjm/hooks/HookCollector.js +34 -0
  4. package/cjm/hooks/hook.js +3 -11
  5. package/cjm/index.js +14 -21
  6. package/cjm/utils/array.js +10 -1
  7. package/cjm/utils/getConstructorChain.js +13 -0
  8. package/cjm/utils/memoize.js +18 -0
  9. package/esm/errors/ArgumentNotFoundError.js +8 -0
  10. package/esm/hooks/HookCollector.js +29 -0
  11. package/esm/hooks/hook.js +1 -9
  12. package/esm/index.js +5 -7
  13. package/esm/utils/array.js +8 -0
  14. package/esm/utils/getConstructorChain.js +9 -0
  15. package/esm/utils/memoize.js +14 -0
  16. package/package.json +1 -1
  17. package/typings/errors/ArgumentNotFoundError.d.ts +5 -0
  18. package/typings/hooks/HookCollector.d.ts +28 -0
  19. package/typings/index.d.ts +5 -7
  20. package/typings/utils/array.d.ts +2 -0
  21. package/typings/utils/getConstructorChain.d.ts +1 -0
  22. package/typings/utils/memoize.d.ts +1 -0
  23. package/cjm/hooks/HookExecutionStrategy.js +0 -48
  24. package/cjm/hooks/ParallelAsync.js +0 -11
  25. package/cjm/hooks/SequentialAsync.js +0 -11
  26. package/cjm/hooks/SequentialSync.js +0 -12
  27. package/cjm/hooks/onConstruct.js +0 -18
  28. package/cjm/hooks/onResolved.js +0 -27
  29. package/cjm/hooks/onScopeDisposed.js +0 -20
  30. package/esm/hooks/HookExecutionStrategy.js +0 -44
  31. package/esm/hooks/ParallelAsync.js +0 -7
  32. package/esm/hooks/SequentialAsync.js +0 -7
  33. package/esm/hooks/SequentialSync.js +0 -8
  34. package/esm/hooks/onConstruct.js +0 -13
  35. package/esm/hooks/onResolved.js +0 -21
  36. package/esm/hooks/onScopeDisposed.js +0 -15
  37. package/typings/hooks/HookExecutionStrategy.d.ts +0 -33
  38. package/typings/hooks/ParallelAsync.d.ts +0 -4
  39. package/typings/hooks/SequentialAsync.d.ts +0 -4
  40. package/typings/hooks/SequentialSync.d.ts +0 -4
  41. package/typings/hooks/onConstruct.d.ts +0 -9
  42. package/typings/hooks/onResolved.d.ts +0 -10
  43. package/typings/hooks/onScopeDisposed.d.ts +0 -9
package/README.md CHANGED
@@ -18,7 +18,7 @@ provider pipelines, aliases, and custom injector strategies.
18
18
  - clean API for classes, keys, tokens, aliases, and scopes
19
19
  - no global container object; pass containers and scopes explicitly
20
20
  - supports tagged application, request, transaction, page, and widget scopes
21
- - decorator support with `@register`, `@inject`, `@onConstruct`, and `@onScopeDisposed`
21
+ - decorator support with `@register`, `@inject`, and `@hook` — lifecycle hook keys are yours to name
22
22
  - can [inject properties](#inject-property)
23
23
  - can inject [lazy dependencies](#lazy)
24
24
  - composable provider and registration pipelines
@@ -55,8 +55,8 @@ provider pipelines, aliases, and custom injector strategies.
55
55
  - [Module](#module)
56
56
  - [Hook](#hook) `@hook`
57
57
  - [Hook domains](#hook-domains) `ScopeHook` `InjectorHook` `ProviderHook`
58
- - [OnConstruct](#onconstruct) `@onConstruct`
59
- - [OnScopeDisposed](#onscopedisposed) `@onScopeDisposed`
58
+ - [Construct hooks](#construct-hooks) `onConstructed`
59
+ - [Scope disposal hooks](#scope-disposal-hooks) `scopeDisposed`
60
60
  - [Inject Property](#inject-property)
61
61
  - [Inject Method](#inject-method)
62
62
  - [Mock](#mock)
@@ -106,8 +106,8 @@ bundlers tree-shake unused exports.
106
106
  | Bun | ✅ | CJS + ESM | Runs the native ESM/CJS builds directly. |
107
107
 
108
108
  > [!NOTE]
109
- > The default `MetadataInjector` (and the `@inject` / `@onConstruct` /
110
- > `@onScopeDisposed` decorators) rely on `reflect-metadata`. It is declared as an
109
+ > The default `MetadataInjector` (and the `@inject` / `@hook` decorators) rely
110
+ > on `reflect-metadata`. It is declared as an
111
111
  > optional peer dependency — install it and import it once at your entrypoint.
112
112
  > `SimpleInjector` and `ProxyInjector` do not need it.
113
113
 
@@ -1792,6 +1792,8 @@ Constructor parameters that should pick up positional args from `ProviderOptions
1792
1792
 
1793
1793
  `argsFn(predicate)` is the general form: it iterates the runtime `args` array and returns the **first argument matching** `predicate(value, index)` — think `args.find(predicate)`. `arg(index)` is just a shortcut for matching by position: `arg(0)` is `argsFn((value, index) => index === 0)`. `args` is `(scope, options) => options.args`, i.e. it returns the runtime args array as-is. Every `InjectFn` receives `(scope, options)`, where `options.args` is the runtime args array.
1794
1794
 
1795
+ `findOrFail(predicate)` is the strict, variadic counterpart for places that take the raw args list — `singleton(findOrFail(isUserId))` keys a per-argument singleton, for example. It returns the first argument matching `predicate(value)` and throws `ArgumentNotFoundError` when none does, instead of silently handing out `undefined`.
1796
+
1795
1797
  ### Immutable token chaining
1796
1798
 
1797
1799
  `token.args(...)`, `token.argsFn(...)`, and `token.lazy()` all return **new token instances** — the parent token is never mutated. This allows the same token to be specialized in multiple independent ways (one-way linked list: parent → many children).
@@ -2385,7 +2387,7 @@ Sometimes you don't want to change the dependency, only to react to it. Use the
2385
2387
 
2386
2388
  Hooks run after the whole `decorate(...)` chain, so they always observe the fully decorated dependency, and their return value is ignored — `onResolve` can never swap the dependency out. They fire per resolution, which means a `singleton()` provider runs them only on the resolve that fills the cache.
2387
2389
 
2388
- This — or the `@onResolved` decorator over the same provider event — is the recommended way to react to a dependency; see [OnConstruct](#onconstruct) for why `@onConstruct` is the narrower tool.
2390
+ This — or a hook key collected over the same provider event — is the recommended way to react to a dependency; see [Construct hooks](#construct-hooks) for why construction is the narrower event.
2389
2391
 
2390
2392
  ```typescript
2391
2393
  import 'reflect-metadata';
@@ -2811,10 +2813,18 @@ describe('Container Modules', function () {
2811
2813
 
2812
2814
  Sometimes you need to invoke methods after construct or dispose of class. This is what hooks are for.
2813
2815
 
2814
- Every hook decorator — the generic `@hook(key, hook)` and `@onConstruct`,
2815
- `@onScopeDisposed`, `@onResolved` — takes **one** hook for the decorated member.
2816
- Several hooks are combined at the declaration site, by the combinator that says
2817
- how they relate:
2816
+ `@hook(key, hook)` is the only hook decorator the library ships, and the key is
2817
+ yours: there is no `@onConstruct` or `@onScopeDisposed` in the package, because
2818
+ each is one line of your own code
2819
+ ([ADR 0017](../../adr/0017-no-predefined-hook-keys.md)).
2820
+
2821
+ ```typescript
2822
+ // yours, named in your vocabulary
2823
+ const onScopeDisposed = (fn: HookType) => hook('onScopeDisposed', fn);
2824
+ ```
2825
+
2826
+ A decorated member takes **one** hook. Several hooks are combined at the
2827
+ declaration site, by the combinator that says how they relate:
2818
2828
 
2819
2829
  ```typescript
2820
2830
  class OrderService {
@@ -2842,42 +2852,115 @@ A member carries exactly one hook per key, so decorating the same member twice
2842
2852
  under one key replaces the earlier hook rather than adding to it — decorators
2843
2853
  are applied bottom-up, so the topmost one is the one that stays.
2844
2854
 
2845
- Every hook may be sync or async — one decorator and one module take both, so
2846
- there is no separate async form to reach for. *How* the hooks run is a separate
2847
- choice: each module takes a `HookExecutionStrategy`, keyed to the hooks it runs
2848
- (`onConstruct`, `onScopeDisposed`, `onResolved`, or a custom key). A strategy
2849
- decides how the **members** — the decorated methods — relate to each other,
2850
- what is awaited, and where a failure goes
2851
- ([ADR 0015](../../adr/0015-one-hook-per-member.md)); how the hooks *within* one
2852
- member relate is the combinator's job, not the strategy's:
2853
-
2854
- | Strategy | Members (decorated methods) | Awaits |
2855
- | ----------------- | --------------------------- | ------ |
2856
- | `SequentialSync` | one after another | no |
2857
- | `SequentialAsync` | one after another | yes |
2858
- | `ParallelAsync` | all at once | yes |
2855
+ Every hook may be sync or async — the one decorator takes both, so there is no
2856
+ separate async form to reach for. *Running* them is not the library's job at
2857
+ all, and neither is deciding when to collect them
2858
+ ([ADR 0016](../../adr/0016-collect-hooks-let-the-caller-run-them.md),
2859
+ [ADR 0018](../../adr/0018-no-hook-modules.md)). The library answers one
2860
+ question — which hooks does this object declare, and against what context do
2861
+ they run — and the container's events are where you ask it.
2862
+
2863
+ A **`HookCollector`** is keyed to one of your hook keys and answers a single
2864
+ question: `getActions(target, { scope })` returns one **`HookAction`** per
2865
+ decorated member — the hook resolved to a function, bound to the
2866
+ `IHookContext` it runs against (which carries the member's own `methodName`). It performs nothing, and how the
2867
+ hooks *within* one member relate was already settled by the combinator at the
2868
+ declaration site ([ADR 0015](../../adr/0015-one-hook-per-member.md)). It reads a
2869
+ class's metadata once and reuses it, so collecting on a hot event is cheap.
2870
+
2871
+ A **runner** performs them. Order, awaiting and failure handling are decided
2872
+ there and nowhere else; `toTask` turns an action into the `Task` that
2873
+ `runInOrder` and `runAtOnce` take. The library exports no type for it — it
2874
+ never calls a runner, nor is it handed one — so name the shape yourself:
2859
2875
 
2860
2876
  ```typescript
2861
- const container = new Container()
2862
- .useModule(new OnConstructModule(new SequentialSync({ key: 'onConstruct' })))
2863
- .useModule(new OnDisposeModule(new ParallelAsync({ key: 'onScopeDisposed' })));
2877
+ // yours, like the hook keys
2878
+ type HookRunner = (actions: HookAction[], context: ExecutionContext) => void;
2879
+
2880
+ // members one after another, never awaited: sync hooks finish before this returns
2881
+ const immediate: HookRunner = (actions) => {
2882
+ for (const { hook, context } of actions) {
2883
+ hook(context);
2884
+ }
2885
+ };
2886
+
2887
+ // members one after another, awaiting any that goes async; both kinds of failure reported
2888
+ const inOrder =
2889
+ (onError: (scope: IContainer) => (error: unknown) => void): HookRunner =>
2890
+ (actions, { scope }) => {
2891
+ try {
2892
+ runInOrder(actions.map(toTask))?.catch(onError(scope));
2893
+ } catch (ex) {
2894
+ onError(scope)(ex);
2895
+ }
2896
+ };
2897
+
2898
+ // every member started at once
2899
+ const atOnce: HookRunner = (actions) => {
2900
+ runAtOnce(actions.map(toTask));
2901
+ };
2902
+
2864
2903
  ```
2865
2904
 
2866
- Resolution and disposal stay synchronous under every strategy: a run stays
2867
- synchronous until a hook returns a promise, so sync hooks finish before
2868
- `resolve` (or `dispose`) returns, and async ones are started there and settle
2869
- afterwards. Instances that must expose readiness should publish it themselves,
2870
- for example by storing the pending promise on the instance. The sync strategy
2871
- never awaits — a hook that returns a promise under it is started but not
2872
- observed.
2905
+ Then wire collecting to the event you want it on. There is no
2906
+ `OnConstructModule` in the package: an `IContainerModule` is one method, and
2907
+ these are the whole of what the modules this library used to ship contained.
2873
2908
 
2874
- `strategy.execute(instance, { scope })` runs the hooks of a custom key by hand;
2875
- `predicate`, `createExecutionContext` and `mapExecutionContext` can be set on
2876
- the strategy or per call.
2909
+ ```typescript
2910
+ const onConstructHooks = new HookCollector({ key: 'onConstruct' });
2911
+ const onScopeDisposedHooks = new HookCollector({ key: 'onScopeDisposed' });
2912
+
2913
+ // construction is the injector's event, and one injector backs the whole scope tree
2914
+ const constructModule = (run: HookRunner): IContainerModule => ({
2915
+ applyTo: (container) =>
2916
+ container.getInjector().onConstructed((instance, scope) => {
2917
+ run(onConstructHooks.getActions(instance, { scope }), { scope });
2918
+ }),
2919
+ });
2920
+
2921
+ // disposal is a scope event; collecting every instance into one list lets the
2922
+ // runner order the instances as well as the members
2923
+ const disposeModule = (run: HookRunner): IContainerModule => ({
2924
+ applyTo: (container) =>
2925
+ container.scopeDisposed.subscribe((scope) => {
2926
+ run(
2927
+ scope.getInstances().flatMap((instance) => onScopeDisposedHooks.getActions(instance, { scope })),
2928
+ { scope },
2929
+ );
2930
+ }),
2931
+ });
2932
+
2933
+ const container = new Container().useModule(constructModule(inOrder(report))).useModule(disposeModule(atOnce));
2934
+ ```
2877
2935
 
2878
- A strategy takes an optional `onError: (scope) => (error) => void`, which
2879
- receives both kinds of failure — what a sync hook threw and what an async hook
2880
- rejected with. Without one, failures are dropped.
2936
+ For resolve hooks, reach the providers through `registered.subscribe(...)` and
2937
+ `provider.onResolved(...)`, or pipe a single registration with
2938
+ [`onResolve`](#on-resolve). Narrowing is the collector's:
2939
+ `new HookCollector({ key: 'onConstruct', predicate })`.
2940
+
2941
+ Resolution and disposal stay synchronous unless the runner makes them
2942
+ otherwise: `runInOrder` and `runAtOnce` stay synchronous until a hook returns a
2943
+ promise, so sync hooks finish before `resolve` (or `dispose`) returns and async
2944
+ ones are started there and settle afterwards. Instances that must expose
2945
+ readiness should publish it themselves, for example by storing the pending
2946
+ promise on the instance.
2947
+
2948
+ Failures belong to the runner too — nothing in the library catches what a hook
2949
+ throws or rejects with. A runner which neither guards nor awaits lets a sync
2950
+ throw propagate out of `resolve` / `dispose` and drops an async rejection.
2951
+
2952
+ A custom key is collected and run the same way, with no module in between:
2953
+
2954
+ ```typescript
2955
+ const workflow = new HookCollector({ key: 'workflow' });
2956
+
2957
+ container.getInjector().onConstructed((instance, scope) => {
2958
+ runInOrder(workflow.getActions(instance, { scope }).map(toTask));
2959
+ });
2960
+ ```
2961
+
2962
+ `predicate`, `createExecutionContext` and `mapExecutionContext` can be set on
2963
+ the collector or overridden per `getActions` call.
2881
2964
 
2882
2965
  ### Hook domains
2883
2966
 
@@ -2921,22 +3004,22 @@ and `registered` (`ITypedEvent<[IProvider, DependencyKey, IContainer]>`);
2921
3004
  `TypedEvent` itself is exported for your own events: `subscribe` / `unsubscribe`
2922
3005
  / `emit` / `dispose`, with `ITypedEvent` as the subscriber-only view to hand out.
2923
3006
 
2924
- The built-in modules are all container modules: `OnConstructModule` reaches the
2925
- injector through `getInjector()`, `OnDisposeModule` subscribes to `scopeDisposed`,
2926
- and `OnResolvedModule` reaches every provider through `registered`.
3007
+ These four events are the whole surface hooks are wired to; the library ships no
3008
+ module over them ([ADR 0018](../../adr/0018-no-hook-modules.md)).
2927
3009
 
2928
- ### OnConstruct
3010
+ ### Construct hooks
2929
3011
 
2930
- > **Prefer `@onResolved` — or the [`onResolve`](#on-resolve) pipe — over
2931
- > `@onConstruct`.** Construction is the *injector's* event, so `@onConstruct`
2932
- > fires only for dependencies the injector builds: a `fromValue` constant or a
2933
- > factory registration never triggers it. It also observes the instance before
3012
+ > **Prefer collecting on resolve — through `registered` / `onResolved`, or the
3013
+ > [`onResolve`](#on-resolve) pipe — over collecting on construction.**
3014
+ > Construction is the *injector's* event, so it fires only for dependencies the
3015
+ > injector builds: a `fromValue` constant or a factory registration never
3016
+ > triggers it. It also observes the instance before
2934
3017
  > the provider's `decorate(...)` chain wraps it, so a hook sees the bare
2935
- > instance rather than what the caller receives. `@onResolved` runs on every
3018
+ > instance rather than what the caller receives. A resolve hook runs on every
2936
3019
  > dependency leaving a provider, after the whole decorate chain — the same
2937
3020
  > ordering, awaiting and error handling, on what the caller actually gets.
2938
- > Reach for `@onConstruct` only when you mean "this class was just constructed"
2939
- > specifically.
3021
+ > Reach for construct hooks only when you mean "this class was just
3022
+ > constructed" specifically.
2940
3023
 
2941
3024
  ```typescript
2942
3025
  import 'reflect-metadata';
@@ -2944,12 +3027,16 @@ import {
2944
3027
  Container,
2945
3028
  type HookFn,
2946
3029
  type IContainer,
3030
+ hook,
3031
+ HookCollector,
3032
+ type HookType,
3033
+ type IContainerModule,
2947
3034
  inject,
2948
- onConstruct,
2949
- OnConstructModule,
2950
3035
  Registration as R,
2951
- SequentialAsync,
2952
- SequentialSync,
3036
+ runInOrder,
3037
+ toTask,
3038
+ type ExecutionContext,
3039
+ type HookAction,
2953
3040
  } from 'ts-ioc-container';
2954
3041
 
2955
3042
  const execute: HookFn = (ctx) => {
@@ -2960,6 +3047,37 @@ const executeAsync: HookFn = async (ctx) => {
2960
3047
  await ctx.invokeMethod({ args: ctx.resolveArgs() });
2961
3048
  };
2962
3049
 
3050
+ // The library ships no construct decorator: the key, the decorator which writes
3051
+ // it and the collector which reads it are all ours.
3052
+ const onConstruct = (fn: HookType) => hook('onConstruct', fn);
3053
+ const onConstructHooks = new HookCollector({ key: 'onConstruct' });
3054
+
3055
+ // Running the collected hooks is ours too, and so is naming the shape that does
3056
+ // it: the library neither calls a runner nor is handed one.
3057
+ type HookRunner = (actions: HookAction[], context: ExecutionContext) => void;
3058
+
3059
+ // This runner keeps the
3060
+ // actions in declaration order, stays synchronous until one returns a promise,
3061
+ // and reports a throw and a rejection alike.
3062
+ const run =
3063
+ (onError: (scope: IContainer) => (ex: unknown) => void = () => () => {}): HookRunner =>
3064
+ (actions, { scope }) => {
3065
+ try {
3066
+ runInOrder(actions.map(toTask))?.catch(onError(scope));
3067
+ } catch (ex) {
3068
+ onError(scope)(ex);
3069
+ }
3070
+ };
3071
+
3072
+ // Construction is the injector's event, and hanging the collection off it is
3073
+ // ours: the library ships no module for that, and a module is just an `applyTo`.
3074
+ const onConstructModule = (run: HookRunner): IContainerModule => ({
3075
+ applyTo: (container) =>
3076
+ container.getInjector().onConstructed((instance, scope) => {
3077
+ run(onConstructHooks.getActions(instance, { scope }), { scope });
3078
+ }),
3079
+ });
3080
+
2963
3081
  describe('onConstruct', function () {
2964
3082
  it('should run initialization method after dependencies are resolved', function () {
2965
3083
  class DatabaseConnection {
@@ -2973,9 +3091,8 @@ describe('onConstruct', function () {
2973
3091
  }
2974
3092
  }
2975
3093
 
2976
- // The module takes a strategy for how the hooks run; the strategy is keyed to the hooks it runs.
2977
3094
  const container = new Container()
2978
- .useModule(new OnConstructModule(new SequentialSync({ key: 'onConstruct' })))
3095
+ .useModule(onConstructModule(run()))
2979
3096
  .addRegistration(R.fromValue('postgres://localhost:5432').bindTo('ConnectionString'));
2980
3097
 
2981
3098
  const db = container.resolve(DatabaseConnection);
@@ -2984,7 +3101,7 @@ describe('onConstruct', function () {
2984
3101
  expect(db.connectionString).toBe('postgres://localhost:5432');
2985
3102
  });
2986
3103
 
2987
- it('should forward hook exceptions to the onError handler with the scope', function () {
3104
+ it('should forward hook exceptions to the runner’s error handler with the scope', function () {
2988
3105
  const failure = new Error('boom');
2989
3106
 
2990
3107
  class BrokenService {
@@ -2996,12 +3113,9 @@ describe('onConstruct', function () {
2996
3113
 
2997
3114
  let captured: { ex: unknown; scope: IContainer } | undefined;
2998
3115
  const container = new Container().useModule(
2999
- new OnConstructModule(
3000
- new SequentialSync({
3001
- key: 'onConstruct',
3002
- onError: (scope) => (ex) => {
3003
- captured = { ex, scope };
3004
- },
3116
+ onConstructModule(
3117
+ run((scope) => (ex) => {
3118
+ captured = { ex, scope };
3005
3119
  }),
3006
3120
  ),
3007
3121
  );
@@ -3011,7 +3125,7 @@ describe('onConstruct', function () {
3011
3125
  expect(captured?.scope).toBe(container);
3012
3126
  });
3013
3127
 
3014
- it('should expose the resolving scope to the onError handler', function () {
3128
+ it('should expose the resolving scope to the runner’s error handler', function () {
3015
3129
  class BrokenService {
3016
3130
  @onConstruct(() => {
3017
3131
  throw new Error('boom');
@@ -3021,12 +3135,9 @@ describe('onConstruct', function () {
3021
3135
 
3022
3136
  let scope: IContainer | undefined;
3023
3137
  const container = new Container().useModule(
3024
- new OnConstructModule(
3025
- new SequentialSync({
3026
- key: 'onConstruct',
3027
- onError: (s) => () => {
3028
- scope = s;
3029
- },
3138
+ onConstructModule(
3139
+ run((s) => () => {
3140
+ scope = s;
3030
3141
  }),
3031
3142
  ),
3032
3143
  );
@@ -3060,9 +3171,9 @@ describe('onConstruct', function () {
3060
3171
  }
3061
3172
  }
3062
3173
 
3063
- // An async strategy awaits the hooks; resolution itself still does not wait for them.
3174
+ // The runner awaits the hooks; resolution itself still does not wait for them.
3064
3175
  const container = new Container()
3065
- .useModule(new OnConstructModule(new SequentialAsync({ key: 'onConstruct' })))
3176
+ .useModule(onConstructModule(run()))
3066
3177
  .addRegistration(R.fromValue('postgres://localhost:5432').bindTo('ConnectionString'));
3067
3178
 
3068
3179
  const db = container.resolve(DatabaseConnection);
@@ -3076,7 +3187,7 @@ describe('onConstruct', function () {
3076
3187
  expect(db.connectionString).toBe('postgres://localhost:5432');
3077
3188
  });
3078
3189
 
3079
- it('should forward rejected hooks to the onError handler with the scope', async function () {
3190
+ it('should forward rejected hooks to the runner’s error handler with the scope', async function () {
3080
3191
  const failure = new Error('boom');
3081
3192
 
3082
3193
  class BrokenService {
@@ -3086,12 +3197,9 @@ describe('onConstruct', function () {
3086
3197
 
3087
3198
  let captured: { ex: unknown; scope: IContainer } | undefined;
3088
3199
  const container = new Container().useModule(
3089
- new OnConstructModule(
3090
- new SequentialAsync({
3091
- key: 'onConstruct',
3092
- onError: (scope) => (ex) => {
3093
- captured = { ex, scope };
3094
- },
3200
+ onConstructModule(
3201
+ run((scope) => (ex) => {
3202
+ captured = { ex, scope };
3095
3203
  }),
3096
3204
  ),
3097
3205
  );
@@ -3108,27 +3216,60 @@ describe('onConstruct', function () {
3108
3216
 
3109
3217
  ```
3110
3218
 
3111
- ### OnScopeDisposed
3219
+ ### Scope disposal hooks
3112
3220
 
3113
3221
  ```typescript
3114
3222
  import 'reflect-metadata';
3115
3223
  import {
3116
- OnDisposeModule,
3117
3224
  bindTo,
3118
3225
  Container,
3119
3226
  type HookFn,
3227
+ hook,
3228
+ HookCollector,
3229
+ type HookType,
3120
3230
  inject,
3121
- onScopeDisposed,
3122
3231
  register,
3123
3232
  Registration as R,
3124
- SequentialSync,
3125
3233
  singleton,
3234
+ type ExecutionContext,
3235
+ type HookAction,
3236
+ type IContainerModule,
3126
3237
  } from 'ts-ioc-container';
3127
3238
 
3128
3239
  const execute: HookFn = (ctx) => {
3129
3240
  ctx.invokeMethod({ args: ctx.resolveArgs() });
3130
3241
  };
3131
3242
 
3243
+ // The library ships no dispose decorator: the key, the decorator which writes it
3244
+ // and the collector which reads it are all ours.
3245
+ const onScopeDisposed = (fn: HookType) => hook('onScopeDisposed', fn);
3246
+ const onScopeDisposedHooks = new HookCollector({ key: 'onScopeDisposed' });
3247
+
3248
+ // Naming the shape which performs collected actions is ours: the library
3249
+ // neither calls a runner nor is handed one.
3250
+ type HookRunner = (actions: HookAction[], context: ExecutionContext) => void;
3251
+
3252
+ // This runner performs the collected hooks in order and never awaits.
3253
+ const run: HookRunner = (actions) => {
3254
+ for (const { hook, context } of actions) {
3255
+ hook(context);
3256
+ }
3257
+ };
3258
+
3259
+ // Disposal is a scope event, and hanging the collection off it is ours: the
3260
+ // library ships no module for that, and a module is just an `applyTo`. Every
3261
+ // instance of the scope is collected into one list, so the runner orders the
3262
+ // instances as well as the members.
3263
+ const onScopeDisposedModule = (run: HookRunner): IContainerModule => ({
3264
+ applyTo: (container) =>
3265
+ container.scopeDisposed.subscribe((scope) => {
3266
+ run(
3267
+ scope.getInstances().flatMap((instance) => onScopeDisposedHooks.getActions(instance, { scope })),
3268
+ { scope },
3269
+ );
3270
+ }),
3271
+ });
3272
+
3132
3273
  @register(bindTo('logsRepo'), singleton())
3133
3274
  class LogsRepo {
3134
3275
  savedLogs: string[] = [];
@@ -3157,7 +3298,7 @@ class Logger {
3157
3298
  describe('onScopeDisposed', function () {
3158
3299
  it('should invoke hooks on all instances when container is disposed', function () {
3159
3300
  const container = new Container()
3160
- .useModule(new OnDisposeModule(new SequentialSync({ key: 'onScopeDisposed' })))
3301
+ .useModule(onScopeDisposedModule(run))
3161
3302
  .addRegistration(R.fromClass(Logger))
3162
3303
  .addRegistration(R.fromClass(LogsRepo));
3163
3304
 
@@ -3177,7 +3318,7 @@ describe('onScopeDisposed', function () {
3177
3318
 
3178
3319
  ```typescript
3179
3320
  import 'reflect-metadata';
3180
- import { Container, hook, injectProp, Registration, sequential, SequentialSync } from 'ts-ioc-container';
3321
+ import { Container, hook, HookCollector, injectProp, Registration, sequential, toTask } from 'ts-ioc-container';
3181
3322
 
3182
3323
  /**
3183
3324
  * UI Components - Property Injection
@@ -3192,8 +3333,8 @@ import { Container, hook, injectProp, Registration, sequential, SequentialSync }
3192
3333
 
3193
3334
  describe('inject property', () => {
3194
3335
  it('should inject property', () => {
3195
- // Strategy for the 'onInit' lifecycle hook
3196
- const onInitStrategy = new SequentialSync({ key: 'onInit' });
3336
+ // Collector for the 'onInit' lifecycle hook
3337
+ const onInit = new HookCollector({ key: 'onInit' });
3197
3338
 
3198
3339
  class UserViewModel {
3199
3340
  // Inject 'GreetingService' into 'greeting' property during 'onInit'
@@ -3210,15 +3351,18 @@ describe('inject property', () => {
3210
3351
  // 1. Create instance (dependencies not yet injected)
3211
3352
  const viewModel = container.resolve(UserViewModel);
3212
3353
 
3213
- // 2. Run lifecycle hooks to inject properties
3214
- onInitStrategy.execute(viewModel, { scope: container });
3354
+ // 2. Collect the lifecycle hooks and run them to inject properties
3355
+ onInit
3356
+ .getActions(viewModel, { scope: container })
3357
+ .map(toTask)
3358
+ .forEach((task) => task());
3215
3359
 
3216
3360
  expect(viewModel.greetingService).toBe('Hello');
3217
3361
  expect(viewModel.display()).toBe('Hello User');
3218
3362
  });
3219
3363
 
3220
3364
  it('should read the applied instance property via getProperty', () => {
3221
- const onInitStrategy = new SequentialSync({ key: 'onInit' });
3365
+ const onInit = new HookCollector({ key: 'onInit' });
3222
3366
 
3223
3367
  let injectedValue: unknown;
3224
3368
 
@@ -3235,7 +3379,10 @@ describe('inject property', () => {
3235
3379
  const container = new Container().addRegistration(Registration.fromValue('Hello').bindToKey('GreetingService'));
3236
3380
 
3237
3381
  const viewModel = container.resolve(UserViewModel);
3238
- onInitStrategy.execute(viewModel, { scope: container });
3382
+ onInit
3383
+ .getActions(viewModel, { scope: container })
3384
+ .map(toTask)
3385
+ .forEach((task) => task());
3239
3386
 
3240
3387
  expect(injectedValue).toBe('Hello');
3241
3388
  });
@@ -3253,3 +3400,4 @@ The product-facing error contract is described in
3253
3400
  - [MethodNotImplementedError.ts](..%2F..%2Flib%2Ferrors%2FMethodNotImplementedError.ts)
3254
3401
  - [DependencyMissingKeyError.ts](..%2F..%2Flib%2Ferrors%2FDependencyMissingKeyError.ts)
3255
3402
  - [ContainerDisposedError.ts](..%2F..%2Flib%2Ferrors%2FContainerDisposedError.ts)
3403
+ - [ArgumentNotFoundError.ts](..%2F..%2Flib%2Ferrors%2FArgumentNotFoundError.ts)
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ArgumentNotFoundError = void 0;
4
+ const ContainerError_1 = require("./ContainerError");
5
+ class ArgumentNotFoundError extends ContainerError_1.ContainerError {
6
+ name = 'ArgumentNotFoundError';
7
+ constructor(message = 'Argument not found') {
8
+ super(message);
9
+ Object.setPrototypeOf(this, ArgumentNotFoundError.prototype);
10
+ }
11
+ }
12
+ exports.ArgumentNotFoundError = ArgumentNotFoundError;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toTask = exports.HookCollector = void 0;
4
+ const target_1 = require("../metadata/target");
5
+ const memoize_1 = require("../utils/memoize");
6
+ const hook_1 = require("./hook");
7
+ const HookContext_1 = require("./HookContext");
8
+ class HookCollector {
9
+ key;
10
+ options;
11
+ hooksOf = (0, memoize_1.memoize)(hook_1.getHooks);
12
+ constructor({ key, createExecutionContext = HookContext_1.createHookExecutionContext, mapExecutionContext = (context) => context, predicate = () => true, }) {
13
+ this.key = key;
14
+ this.options = { createExecutionContext, mapExecutionContext, predicate };
15
+ }
16
+ hasHooks(target) {
17
+ return (0, hook_1.hasHooks)(target, this.key);
18
+ }
19
+ getActions(target, { scope, createExecutionContext = this.options.createExecutionContext, mapExecutionContext = this.options.mapExecutionContext, predicate = this.options.predicate, }) {
20
+ const actions = [];
21
+ for (const [methodName, fn] of this.hooksOf((0, target_1.resolveConstructor)(target), this.key)) {
22
+ if (predicate(methodName)) {
23
+ actions.push({
24
+ hook: (0, hook_1.toHookFn)(fn),
25
+ context: mapExecutionContext(createExecutionContext(target, scope, methodName)),
26
+ });
27
+ }
28
+ }
29
+ return actions;
30
+ }
31
+ }
32
+ exports.HookCollector = HookCollector;
33
+ const toTask = ({ hook, context }) => () => hook(context);
34
+ exports.toTask = toTask;
package/cjm/hooks/hook.js CHANGED
@@ -5,23 +5,15 @@ exports.getHooks = getHooks;
5
5
  exports.hasHooks = hasHooks;
6
6
  const basic_1 = require("../utils/basic");
7
7
  const target_1 = require("../metadata/target");
8
+ const getConstructorChain_1 = require("../utils/getConstructorChain");
8
9
  const isHookClassConstructor = (execute) => {
9
10
  return basic_1.Is.constructor(execute) && execute.prototype.execute;
10
11
  };
11
12
  const toHookFn = (execute) => isHookClassConstructor(execute) ? (context) => context.scope.resolve(execute).execute(context) : execute;
12
13
  exports.toHookFn = toHookFn;
13
- const getConstructorChain = (ctor) => {
14
- const chain = [];
15
- let current = ctor;
16
- while (typeof current === 'function' && current !== Function.prototype) {
17
- chain.push(current);
18
- current = Object.getPrototypeOf(current);
19
- }
20
- return chain;
21
- };
22
14
  function getHooks(target, key) {
23
15
  const merged = new Map();
24
- for (const ctor of getConstructorChain((0, target_1.resolveConstructor)(target)).reverse()) {
16
+ for (const ctor of (0, getConstructorChain_1.getConstructorChain)((0, target_1.resolveConstructor)(target)).reverse()) {
25
17
  const ownHooks = Reflect.getOwnMetadata(key, ctor);
26
18
  if (ownHooks) {
27
19
  for (const [methodName, fn] of ownHooks) {
@@ -32,7 +24,7 @@ function getHooks(target, key) {
32
24
  return merged;
33
25
  }
34
26
  function hasHooks(target, key) {
35
- return getConstructorChain((0, target_1.resolveConstructor)(target)).some((ctor) => Reflect.hasOwnMetadata(key, ctor));
27
+ return (0, getConstructorChain_1.getConstructorChain)((0, target_1.resolveConstructor)(target)).some((ctor) => Reflect.hasOwnMetadata(key, ctor));
36
28
  }
37
29
  const hook = (key, fn) => (target, propertyKey) => {
38
30
  const hooks = Reflect.hasOwnMetadata(key, target.constructor)