ts-ioc-container 66.0.0 → 68.0.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 (49) hide show
  1. package/README.md +256 -106
  2. package/cjm/hooks/HookCollector.js +34 -0
  3. package/cjm/hooks/combinators.js +27 -0
  4. package/cjm/hooks/hook.js +8 -22
  5. package/cjm/index.js +18 -29
  6. package/cjm/utils/getConstructorChain.js +13 -0
  7. package/cjm/utils/memoize.js +18 -0
  8. package/cjm/utils/task.js +23 -0
  9. package/esm/hooks/HookCollector.js +29 -0
  10. package/esm/hooks/combinators.js +21 -0
  11. package/esm/hooks/hook.js +5 -17
  12. package/esm/index.js +6 -10
  13. package/esm/utils/getConstructorChain.js +9 -0
  14. package/esm/utils/memoize.js +14 -0
  15. package/esm/utils/task.js +18 -0
  16. package/package.json +1 -1
  17. package/typings/hooks/HookCollector.d.ts +28 -0
  18. package/typings/hooks/{resolveHooks.d.ts → combinators.d.ts} +2 -0
  19. package/typings/hooks/hook.d.ts +2 -7
  20. package/typings/index.d.ts +6 -10
  21. package/typings/utils/getConstructorChain.d.ts +1 -0
  22. package/typings/utils/memoize.d.ts +1 -0
  23. package/typings/utils/task.d.ts +3 -0
  24. package/cjm/hooks/AsyncHookExecutionStrategy.js +0 -15
  25. package/cjm/hooks/HookExecutionStrategy.js +0 -68
  26. package/cjm/hooks/ParallelAsync.js +0 -17
  27. package/cjm/hooks/SequentialAsync.js +0 -15
  28. package/cjm/hooks/SequentialSync.js +0 -14
  29. package/cjm/hooks/onConstruct.js +0 -18
  30. package/cjm/hooks/onResolved.js +0 -27
  31. package/cjm/hooks/onScopeDisposed.js +0 -20
  32. package/cjm/hooks/resolveHooks.js +0 -15
  33. package/esm/hooks/AsyncHookExecutionStrategy.js +0 -11
  34. package/esm/hooks/HookExecutionStrategy.js +0 -62
  35. package/esm/hooks/ParallelAsync.js +0 -13
  36. package/esm/hooks/SequentialAsync.js +0 -11
  37. package/esm/hooks/SequentialSync.js +0 -10
  38. package/esm/hooks/onConstruct.js +0 -13
  39. package/esm/hooks/onResolved.js +0 -21
  40. package/esm/hooks/onScopeDisposed.js +0 -15
  41. package/esm/hooks/resolveHooks.js +0 -11
  42. package/typings/hooks/AsyncHookExecutionStrategy.d.ts +0 -10
  43. package/typings/hooks/HookExecutionStrategy.d.ts +0 -35
  44. package/typings/hooks/ParallelAsync.d.ts +0 -5
  45. package/typings/hooks/SequentialAsync.d.ts +0 -5
  46. package/typings/hooks/SequentialSync.d.ts +0 -4
  47. package/typings/hooks/onConstruct.d.ts +0 -9
  48. package/typings/hooks/onResolved.d.ts +0 -10
  49. 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
 
@@ -162,7 +162,7 @@ describe('Quickstart', function () {
162
162
  - Lazy token: `select.token('Service').lazy()`
163
163
  - Inject decorator: `@inject('Key')`
164
164
  - Map an injected value: `@inject('Key', sanitize(), validate())`
165
- - Property inject: `@hook('onInit', append(injectProp('Key')))`
165
+ - Property inject: `@hook('onInit', injectProp('Key'))`
166
166
 
167
167
  > [!TIP]
168
168
  > For classes, prefer the `@register(bindTo('Key'))` decorator over the fluent
@@ -2385,7 +2385,7 @@ Sometimes you don't want to change the dependency, only to react to it. Use the
2385
2385
 
2386
2386
  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
2387
 
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.
2388
+ 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
2389
 
2390
2390
  ```typescript
2391
2391
  import 'reflect-metadata';
@@ -2811,67 +2811,154 @@ describe('Container Modules', function () {
2811
2811
 
2812
2812
  Sometimes you need to invoke methods after construct or dispose of class. This is what hooks are for.
2813
2813
 
2814
- The generic `@hook` decorator takes a hook key and a map function
2815
- `(...prev: HookType[]) => HookType[]`, where `prev` is the list of hooks already
2816
- registered on the class for the decorated member. Use `appendHooks` /
2817
- `prependHooks` (exported as `append` / `prepend` too) to place new hooks around
2818
- the existing ones:
2814
+ `@hook(key, hook)` is the only hook decorator the library ships, and the key is
2815
+ yours: there is no `@onConstruct` or `@onScopeDisposed` in the package, because
2816
+ each is one line of your own code
2817
+ ([ADR 0017](../../adr/0017-no-predefined-hook-keys.md)).
2818
+
2819
+ ```typescript
2820
+ // yours, named in your vocabulary
2821
+ const onScopeDisposed = (fn: HookType) => hook('onScopeDisposed', fn);
2822
+ ```
2823
+
2824
+ A decorated member takes **one** hook. Several hooks are combined at the
2825
+ declaration site, by the combinator that says how they relate:
2819
2826
 
2820
2827
  ```typescript
2821
2828
  class OrderService {
2822
- @hook('actions', append(validate, persist))
2823
- @hook('actions', prepend(authorize))
2829
+ @hook('actions', sequential(authorize, validate, persist))
2824
2830
  submit() {}
2831
+
2832
+ @onScopeDisposed(parallel(flushMetrics, closeSocket))
2833
+ destroy() {}
2825
2834
  }
2826
2835
  ```
2827
2836
 
2828
- Decorators are applied bottom-up, so `authorize` runs first, then `validate` and
2829
- `persist`. Any other map function works as well — for example
2830
- `(...prev) => [...prev].reverse()` to reorder, or `() => [onlyThisOne]` to
2831
- replace the accumulated hooks.
2837
+ - `sequential(...hooks)` runs them in declaration order, awaiting each one that
2838
+ goes async before the next.
2839
+ - `parallel(...hooks)` starts them all at once and settles when every one has.
2840
+ - `oncePerInstance(hook)` runs its hook a single time per instance, however
2841
+ often the event fires.
2842
+
2843
+ They compose, because each returns an ordinary `HookFn`:
2844
+ `oncePerInstance(sequential(connect, warmUp))`, or a `parallel(...)` nested
2845
+ inside a `sequential(...)`. A hook class (`HookType`) may be passed anywhere a
2846
+ hook function can. Writing your own combinator needs nothing from the library
2847
+ beyond `toHookFn`.
2848
+
2849
+ A member carries exactly one hook per key, so decorating the same member twice
2850
+ under one key replaces the earlier hook rather than adding to it — decorators
2851
+ are applied bottom-up, so the topmost one is the one that stays.
2852
+
2853
+ Every hook may be sync or async — the one decorator takes both, so there is no
2854
+ separate async form to reach for. *Running* them is not the library's job at
2855
+ all, and neither is deciding when to collect them
2856
+ ([ADR 0016](../../adr/0016-collect-hooks-let-the-caller-run-them.md),
2857
+ [ADR 0018](../../adr/0018-no-hook-modules.md)). The library answers one
2858
+ question — which hooks does this object declare, and against what context do
2859
+ they run — and the container's events are where you ask it.
2860
+
2861
+ A **`HookCollector`** is keyed to one of your hook keys and answers a single
2862
+ question: `getActions(target, { scope })` returns one **`HookAction`** per
2863
+ decorated member — the hook resolved to a function, bound to the
2864
+ `IHookContext` it runs against (which carries the member's own `methodName`). It performs nothing, and how the
2865
+ hooks *within* one member relate was already settled by the combinator at the
2866
+ declaration site ([ADR 0015](../../adr/0015-one-hook-per-member.md)). It reads a
2867
+ class's metadata once and reuses it, so collecting on a hot event is cheap.
2868
+
2869
+ A **runner** performs them. Order, awaiting and failure handling are decided
2870
+ there and nowhere else; `toTask` turns an action into the `Task` that
2871
+ `runInOrder` and `runAtOnce` take. The library exports no type for it — it
2872
+ never calls a runner, nor is it handed one — so name the shape yourself:
2832
2873
 
2833
- `@onConstruct` and `@onScopeDisposed` keep their variadic signature and
2834
- compensate for the bottom-up application order, so stacked decorators run in
2835
- declaration order: `@onConstruct(h1) @onConstruct(h2)` runs `h1` before `h2`.
2874
+ ```typescript
2875
+ // yours, like the hook keys
2876
+ type HookRunner = (actions: HookAction[], context: ExecutionContext) => void;
2836
2877
 
2837
- Every hook may be sync or async — one decorator and one module take both, so
2838
- there is no separate async form to reach for. *How* the hooks run is a separate
2839
- choice: each module takes a `HookExecutionStrategy`, keyed to the hooks it runs
2840
- (`onConstruct`, `onScopeDisposed`, `onResolved`, or a custom key), and the
2841
- strategy decides the order, what is awaited, and where a failure goes
2842
- ([ADR 0014](../../adr/0014-hook-execution-strategy.md)):
2878
+ // members one after another, never awaited: sync hooks finish before this returns
2879
+ const immediate: HookRunner = (actions) => {
2880
+ for (const { hook, context } of actions) {
2881
+ hook(context);
2882
+ }
2883
+ };
2843
2884
 
2844
- | Strategy | Members (decorated methods) | Hooks of one member | Awaits |
2845
- | ----------------- | --------------------------- | ------------------------------------------- | ------ |
2846
- | `SequentialSync` | one after another | in declaration order | no |
2847
- | `SequentialAsync` | one after another | in order, or all at once (`methodStrategy`) | yes |
2848
- | `ParallelAsync` | all at once | in order, or all at once (`methodStrategy`) | yes |
2885
+ // members one after another, awaiting any that goes async; both kinds of failure reported
2886
+ const inOrder =
2887
+ (onError: (scope: IContainer) => (error: unknown) => void): HookRunner =>
2888
+ (actions, { scope }) => {
2889
+ try {
2890
+ runInOrder(actions.map(toTask))?.catch(onError(scope));
2891
+ } catch (ex) {
2892
+ onError(scope)(ex);
2893
+ }
2894
+ };
2849
2895
 
2850
- The async strategies require `methodStrategy` (`'sequential'` or `'parallel'`):
2851
- how the hooks of one member relate is named at the construction site rather
2852
- than left to a default.
2896
+ // every member started at once
2897
+ const atOnce: HookRunner = (actions) => {
2898
+ runAtOnce(actions.map(toTask));
2899
+ };
2900
+
2901
+ ```
2902
+
2903
+ Then wire collecting to the event you want it on. There is no
2904
+ `OnConstructModule` in the package: an `IContainerModule` is one method, and
2905
+ these are the whole of what the modules this library used to ship contained.
2853
2906
 
2854
2907
  ```typescript
2855
- const container = new Container()
2856
- .useModule(new OnConstructModule(new SequentialSync({ key: 'onConstruct' })))
2857
- .useModule(new OnDisposeModule(new ParallelAsync({ key: 'onScopeDisposed', methodStrategy: 'parallel' })));
2908
+ const onConstructHooks = new HookCollector({ key: 'onConstruct' });
2909
+ const onScopeDisposedHooks = new HookCollector({ key: 'onScopeDisposed' });
2910
+
2911
+ // construction is the injector's event, and one injector backs the whole scope tree
2912
+ const constructModule = (run: HookRunner): IContainerModule => ({
2913
+ applyTo: (container) =>
2914
+ container.getInjector().onConstructed((instance, scope) => {
2915
+ run(onConstructHooks.getActions(instance, { scope }), { scope });
2916
+ }),
2917
+ });
2918
+
2919
+ // disposal is a scope event; collecting every instance into one list lets the
2920
+ // runner order the instances as well as the members
2921
+ const disposeModule = (run: HookRunner): IContainerModule => ({
2922
+ applyTo: (container) =>
2923
+ container.scopeDisposed.subscribe((scope) => {
2924
+ run(
2925
+ scope.getInstances().flatMap((instance) => onScopeDisposedHooks.getActions(instance, { scope })),
2926
+ { scope },
2927
+ );
2928
+ }),
2929
+ });
2930
+
2931
+ const container = new Container().useModule(constructModule(inOrder(report))).useModule(disposeModule(atOnce));
2858
2932
  ```
2859
2933
 
2860
- Resolution and disposal stay synchronous under every strategy: a run stays
2861
- synchronous until a hook returns a promise, so sync hooks finish before
2862
- `resolve` (or `dispose`) returns, and async ones are started there and settle
2863
- afterwards. Instances that must expose readiness should publish it themselves,
2864
- for example by storing the pending promise on the instance. The sync strategy
2865
- never awaits — a hook that returns a promise under it is started but not
2866
- observed.
2934
+ For resolve hooks, reach the providers through `registered.subscribe(...)` and
2935
+ `provider.onResolved(...)`, or pipe a single registration with
2936
+ [`onResolve`](#on-resolve). Narrowing is the collector's:
2937
+ `new HookCollector({ key: 'onConstruct', predicate })`.
2867
2938
 
2868
- `strategy.execute(instance, { scope })` runs the hooks of a custom key by hand;
2869
- `predicate`, `createExecutionContext` and `mapExecutionContext` can be set on
2870
- the strategy or per call.
2939
+ Resolution and disposal stay synchronous unless the runner makes them
2940
+ otherwise: `runInOrder` and `runAtOnce` stay synchronous until a hook returns a
2941
+ promise, so sync hooks finish before `resolve` (or `dispose`) returns and async
2942
+ ones are started there and settle afterwards. Instances that must expose
2943
+ readiness should publish it themselves, for example by storing the pending
2944
+ promise on the instance.
2871
2945
 
2872
- A strategy takes an optional `onError: (scope) => (error) => void`, which
2873
- receives both kinds of failure — what a sync hook threw and what an async hook
2874
- rejected with. Without one, failures are dropped.
2946
+ Failures belong to the runner too — nothing in the library catches what a hook
2947
+ throws or rejects with. A runner which neither guards nor awaits lets a sync
2948
+ throw propagate out of `resolve` / `dispose` and drops an async rejection.
2949
+
2950
+ A custom key is collected and run the same way, with no module in between:
2951
+
2952
+ ```typescript
2953
+ const workflow = new HookCollector({ key: 'workflow' });
2954
+
2955
+ container.getInjector().onConstructed((instance, scope) => {
2956
+ runInOrder(workflow.getActions(instance, { scope }).map(toTask));
2957
+ });
2958
+ ```
2959
+
2960
+ `predicate`, `createExecutionContext` and `mapExecutionContext` can be set on
2961
+ the collector or overridden per `getActions` call.
2875
2962
 
2876
2963
  ### Hook domains
2877
2964
 
@@ -2915,35 +3002,39 @@ and `registered` (`ITypedEvent<[IProvider, DependencyKey, IContainer]>`);
2915
3002
  `TypedEvent` itself is exported for your own events: `subscribe` / `unsubscribe`
2916
3003
  / `emit` / `dispose`, with `ITypedEvent` as the subscriber-only view to hand out.
2917
3004
 
2918
- The built-in modules are all container modules: `OnConstructModule` reaches the
2919
- injector through `getInjector()`, `OnDisposeModule` subscribes to `scopeDisposed`,
2920
- and `OnResolvedModule` reaches every provider through `registered`.
3005
+ These four events are the whole surface hooks are wired to; the library ships no
3006
+ module over them ([ADR 0018](../../adr/0018-no-hook-modules.md)).
2921
3007
 
2922
- ### OnConstruct
3008
+ ### Construct hooks
2923
3009
 
2924
- > **Prefer `@onResolved` — or the [`onResolve`](#on-resolve) pipe — over
2925
- > `@onConstruct`.** Construction is the *injector's* event, so `@onConstruct`
2926
- > fires only for dependencies the injector builds: a `fromValue` constant or a
2927
- > factory registration never triggers it. It also observes the instance before
3010
+ > **Prefer collecting on resolve — through `registered` / `onResolved`, or the
3011
+ > [`onResolve`](#on-resolve) pipe — over collecting on construction.**
3012
+ > Construction is the *injector's* event, so it fires only for dependencies the
3013
+ > injector builds: a `fromValue` constant or a factory registration never
3014
+ > triggers it. It also observes the instance before
2928
3015
  > the provider's `decorate(...)` chain wraps it, so a hook sees the bare
2929
- > instance rather than what the caller receives. `@onResolved` runs on every
3016
+ > instance rather than what the caller receives. A resolve hook runs on every
2930
3017
  > dependency leaving a provider, after the whole decorate chain — the same
2931
3018
  > ordering, awaiting and error handling, on what the caller actually gets.
2932
- > Reach for `@onConstruct` only when you mean "this class was just constructed"
2933
- > specifically.
3019
+ > Reach for construct hooks only when you mean "this class was just
3020
+ > constructed" specifically.
2934
3021
 
2935
3022
  ```typescript
2936
3023
  import 'reflect-metadata';
2937
3024
  import {
2938
- OnConstructModule,
2939
3025
  Container,
2940
3026
  type HookFn,
2941
3027
  type IContainer,
3028
+ hook,
3029
+ HookCollector,
3030
+ type HookType,
3031
+ type IContainerModule,
2942
3032
  inject,
2943
- onConstruct,
2944
3033
  Registration as R,
2945
- SequentialAsync,
2946
- SequentialSync,
3034
+ runInOrder,
3035
+ toTask,
3036
+ type ExecutionContext,
3037
+ type HookAction,
2947
3038
  } from 'ts-ioc-container';
2948
3039
 
2949
3040
  const execute: HookFn = (ctx) => {
@@ -2954,6 +3045,37 @@ const executeAsync: HookFn = async (ctx) => {
2954
3045
  await ctx.invokeMethod({ args: ctx.resolveArgs() });
2955
3046
  };
2956
3047
 
3048
+ // The library ships no construct decorator: the key, the decorator which writes
3049
+ // it and the collector which reads it are all ours.
3050
+ const onConstruct = (fn: HookType) => hook('onConstruct', fn);
3051
+ const onConstructHooks = new HookCollector({ key: 'onConstruct' });
3052
+
3053
+ // Running the collected hooks is ours too, and so is naming the shape that does
3054
+ // it: the library neither calls a runner nor is handed one.
3055
+ type HookRunner = (actions: HookAction[], context: ExecutionContext) => void;
3056
+
3057
+ // This runner keeps the
3058
+ // actions in declaration order, stays synchronous until one returns a promise,
3059
+ // and reports a throw and a rejection alike.
3060
+ const run =
3061
+ (onError: (scope: IContainer) => (ex: unknown) => void = () => () => {}): HookRunner =>
3062
+ (actions, { scope }) => {
3063
+ try {
3064
+ runInOrder(actions.map(toTask))?.catch(onError(scope));
3065
+ } catch (ex) {
3066
+ onError(scope)(ex);
3067
+ }
3068
+ };
3069
+
3070
+ // Construction is the injector's event, and hanging the collection off it is
3071
+ // ours: the library ships no module for that, and a module is just an `applyTo`.
3072
+ const onConstructModule = (run: HookRunner): IContainerModule => ({
3073
+ applyTo: (container) =>
3074
+ container.getInjector().onConstructed((instance, scope) => {
3075
+ run(onConstructHooks.getActions(instance, { scope }), { scope });
3076
+ }),
3077
+ });
3078
+
2957
3079
  describe('onConstruct', function () {
2958
3080
  it('should run initialization method after dependencies are resolved', function () {
2959
3081
  class DatabaseConnection {
@@ -2967,9 +3089,8 @@ describe('onConstruct', function () {
2967
3089
  }
2968
3090
  }
2969
3091
 
2970
- // The module takes a strategy for how the hooks run; the strategy is keyed to the hooks it runs.
2971
3092
  const container = new Container()
2972
- .useModule(new OnConstructModule(new SequentialSync({ key: 'onConstruct' })))
3093
+ .useModule(onConstructModule(run()))
2973
3094
  .addRegistration(R.fromValue('postgres://localhost:5432').bindTo('ConnectionString'));
2974
3095
 
2975
3096
  const db = container.resolve(DatabaseConnection);
@@ -2978,7 +3099,7 @@ describe('onConstruct', function () {
2978
3099
  expect(db.connectionString).toBe('postgres://localhost:5432');
2979
3100
  });
2980
3101
 
2981
- it('should forward hook exceptions to the onError handler with the scope', function () {
3102
+ it('should forward hook exceptions to the runner’s error handler with the scope', function () {
2982
3103
  const failure = new Error('boom');
2983
3104
 
2984
3105
  class BrokenService {
@@ -2990,12 +3111,9 @@ describe('onConstruct', function () {
2990
3111
 
2991
3112
  let captured: { ex: unknown; scope: IContainer } | undefined;
2992
3113
  const container = new Container().useModule(
2993
- new OnConstructModule(
2994
- new SequentialSync({
2995
- key: 'onConstruct',
2996
- onError: (scope) => (ex) => {
2997
- captured = { ex, scope };
2998
- },
3114
+ onConstructModule(
3115
+ run((scope) => (ex) => {
3116
+ captured = { ex, scope };
2999
3117
  }),
3000
3118
  ),
3001
3119
  );
@@ -3005,7 +3123,7 @@ describe('onConstruct', function () {
3005
3123
  expect(captured?.scope).toBe(container);
3006
3124
  });
3007
3125
 
3008
- it('should expose the resolving scope to the onError handler', function () {
3126
+ it('should expose the resolving scope to the runner’s error handler', function () {
3009
3127
  class BrokenService {
3010
3128
  @onConstruct(() => {
3011
3129
  throw new Error('boom');
@@ -3015,12 +3133,9 @@ describe('onConstruct', function () {
3015
3133
 
3016
3134
  let scope: IContainer | undefined;
3017
3135
  const container = new Container().useModule(
3018
- new OnConstructModule(
3019
- new SequentialSync({
3020
- key: 'onConstruct',
3021
- onError: (s) => () => {
3022
- scope = s;
3023
- },
3136
+ onConstructModule(
3137
+ run((s) => () => {
3138
+ scope = s;
3024
3139
  }),
3025
3140
  ),
3026
3141
  );
@@ -3054,9 +3169,9 @@ describe('onConstruct', function () {
3054
3169
  }
3055
3170
  }
3056
3171
 
3057
- // An async strategy awaits the hooks; resolution itself still does not wait for them.
3172
+ // The runner awaits the hooks; resolution itself still does not wait for them.
3058
3173
  const container = new Container()
3059
- .useModule(new OnConstructModule(new SequentialAsync({ key: 'onConstruct', methodStrategy: 'sequential' })))
3174
+ .useModule(onConstructModule(run()))
3060
3175
  .addRegistration(R.fromValue('postgres://localhost:5432').bindTo('ConnectionString'));
3061
3176
 
3062
3177
  const db = container.resolve(DatabaseConnection);
@@ -3070,7 +3185,7 @@ describe('onConstruct', function () {
3070
3185
  expect(db.connectionString).toBe('postgres://localhost:5432');
3071
3186
  });
3072
3187
 
3073
- it('should forward rejected hooks to the onError handler with the scope', async function () {
3188
+ it('should forward rejected hooks to the runner’s error handler with the scope', async function () {
3074
3189
  const failure = new Error('boom');
3075
3190
 
3076
3191
  class BrokenService {
@@ -3080,13 +3195,9 @@ describe('onConstruct', function () {
3080
3195
 
3081
3196
  let captured: { ex: unknown; scope: IContainer } | undefined;
3082
3197
  const container = new Container().useModule(
3083
- new OnConstructModule(
3084
- new SequentialAsync({
3085
- key: 'onConstruct',
3086
- methodStrategy: 'sequential',
3087
- onError: (scope) => (ex) => {
3088
- captured = { ex, scope };
3089
- },
3198
+ onConstructModule(
3199
+ run((scope) => (ex) => {
3200
+ captured = { ex, scope };
3090
3201
  }),
3091
3202
  ),
3092
3203
  );
@@ -3103,27 +3214,60 @@ describe('onConstruct', function () {
3103
3214
 
3104
3215
  ```
3105
3216
 
3106
- ### OnScopeDisposed
3217
+ ### Scope disposal hooks
3107
3218
 
3108
3219
  ```typescript
3109
3220
  import 'reflect-metadata';
3110
3221
  import {
3111
- OnDisposeModule,
3112
3222
  bindTo,
3113
3223
  Container,
3114
3224
  type HookFn,
3225
+ hook,
3226
+ HookCollector,
3227
+ type HookType,
3115
3228
  inject,
3116
- onScopeDisposed,
3117
3229
  register,
3118
3230
  Registration as R,
3119
- SequentialSync,
3120
3231
  singleton,
3232
+ type ExecutionContext,
3233
+ type HookAction,
3234
+ type IContainerModule,
3121
3235
  } from 'ts-ioc-container';
3122
3236
 
3123
3237
  const execute: HookFn = (ctx) => {
3124
3238
  ctx.invokeMethod({ args: ctx.resolveArgs() });
3125
3239
  };
3126
3240
 
3241
+ // The library ships no dispose decorator: the key, the decorator which writes it
3242
+ // and the collector which reads it are all ours.
3243
+ const onScopeDisposed = (fn: HookType) => hook('onScopeDisposed', fn);
3244
+ const onScopeDisposedHooks = new HookCollector({ key: 'onScopeDisposed' });
3245
+
3246
+ // Naming the shape which performs collected actions is ours: the library
3247
+ // neither calls a runner nor is handed one.
3248
+ type HookRunner = (actions: HookAction[], context: ExecutionContext) => void;
3249
+
3250
+ // This runner performs the collected hooks in order and never awaits.
3251
+ const run: HookRunner = (actions) => {
3252
+ for (const { hook, context } of actions) {
3253
+ hook(context);
3254
+ }
3255
+ };
3256
+
3257
+ // Disposal is a scope event, and hanging the collection off it is ours: the
3258
+ // library ships no module for that, and a module is just an `applyTo`. Every
3259
+ // instance of the scope is collected into one list, so the runner orders the
3260
+ // instances as well as the members.
3261
+ const onScopeDisposedModule = (run: HookRunner): IContainerModule => ({
3262
+ applyTo: (container) =>
3263
+ container.scopeDisposed.subscribe((scope) => {
3264
+ run(
3265
+ scope.getInstances().flatMap((instance) => onScopeDisposedHooks.getActions(instance, { scope })),
3266
+ { scope },
3267
+ );
3268
+ }),
3269
+ });
3270
+
3127
3271
  @register(bindTo('logsRepo'), singleton())
3128
3272
  class LogsRepo {
3129
3273
  savedLogs: string[] = [];
@@ -3152,7 +3296,7 @@ class Logger {
3152
3296
  describe('onScopeDisposed', function () {
3153
3297
  it('should invoke hooks on all instances when container is disposed', function () {
3154
3298
  const container = new Container()
3155
- .useModule(new OnDisposeModule(new SequentialSync({ key: 'onScopeDisposed' })))
3299
+ .useModule(onScopeDisposedModule(run))
3156
3300
  .addRegistration(R.fromClass(Logger))
3157
3301
  .addRegistration(R.fromClass(LogsRepo));
3158
3302
 
@@ -3172,7 +3316,7 @@ describe('onScopeDisposed', function () {
3172
3316
 
3173
3317
  ```typescript
3174
3318
  import 'reflect-metadata';
3175
- import { append, Container, hook, SequentialSync, injectProp, Registration } from 'ts-ioc-container';
3319
+ import { Container, hook, HookCollector, injectProp, Registration, sequential, toTask } from 'ts-ioc-container';
3176
3320
 
3177
3321
  /**
3178
3322
  * UI Components - Property Injection
@@ -3187,12 +3331,12 @@ import { append, Container, hook, SequentialSync, injectProp, Registration } fro
3187
3331
 
3188
3332
  describe('inject property', () => {
3189
3333
  it('should inject property', () => {
3190
- // Strategy for the 'onInit' lifecycle hook
3191
- const onInitStrategy = new SequentialSync({ key: 'onInit' });
3334
+ // Collector for the 'onInit' lifecycle hook
3335
+ const onInit = new HookCollector({ key: 'onInit' });
3192
3336
 
3193
3337
  class UserViewModel {
3194
3338
  // Inject 'GreetingService' into 'greeting' property during 'onInit'
3195
- @hook('onInit', append(injectProp('GreetingService')))
3339
+ @hook('onInit', injectProp('GreetingService'))
3196
3340
  greetingService!: string;
3197
3341
 
3198
3342
  display(): string {
@@ -3205,22 +3349,25 @@ describe('inject property', () => {
3205
3349
  // 1. Create instance (dependencies not yet injected)
3206
3350
  const viewModel = container.resolve(UserViewModel);
3207
3351
 
3208
- // 2. Run lifecycle hooks to inject properties
3209
- onInitStrategy.execute(viewModel, { scope: container });
3352
+ // 2. Collect the lifecycle hooks and run them to inject properties
3353
+ onInit
3354
+ .getActions(viewModel, { scope: container })
3355
+ .map(toTask)
3356
+ .forEach((task) => task());
3210
3357
 
3211
3358
  expect(viewModel.greetingService).toBe('Hello');
3212
3359
  expect(viewModel.display()).toBe('Hello User');
3213
3360
  });
3214
3361
 
3215
3362
  it('should read the applied instance property via getProperty', () => {
3216
- const onInitStrategy = new SequentialSync({ key: 'onInit' });
3363
+ const onInit = new HookCollector({ key: 'onInit' });
3217
3364
 
3218
3365
  let injectedValue: unknown;
3219
3366
 
3220
3367
  class UserViewModel {
3221
3368
  @hook(
3222
3369
  'onInit',
3223
- append(injectProp('GreetingService'), (context) => {
3370
+ sequential(injectProp('GreetingService'), (context) => {
3224
3371
  injectedValue = context.getProperty();
3225
3372
  }),
3226
3373
  )
@@ -3230,7 +3377,10 @@ describe('inject property', () => {
3230
3377
  const container = new Container().addRegistration(Registration.fromValue('Hello').bindToKey('GreetingService'));
3231
3378
 
3232
3379
  const viewModel = container.resolve(UserViewModel);
3233
- onInitStrategy.execute(viewModel, { scope: container });
3380
+ onInit
3381
+ .getActions(viewModel, { scope: container })
3382
+ .map(toTask)
3383
+ .forEach((task) => task());
3234
3384
 
3235
3385
  expect(injectedValue).toBe('Hello');
3236
3386
  });
@@ -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;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.oncePerInstance = exports.parallel = exports.sequential = void 0;
4
+ const task_1 = require("../utils/task");
5
+ const hook_1 = require("./hook");
6
+ const sequential = (...hooks) => {
7
+ const fns = hooks.map(hook_1.toHookFn);
8
+ return (context) => (0, task_1.runInOrder)(fns.map((fn) => () => fn(context)));
9
+ };
10
+ exports.sequential = sequential;
11
+ const parallel = (...hooks) => {
12
+ const fns = hooks.map(hook_1.toHookFn);
13
+ return (context) => (0, task_1.runAtOnce)(fns.map((fn) => () => fn(context)));
14
+ };
15
+ exports.parallel = parallel;
16
+ const oncePerInstance = (execute) => {
17
+ const invokedInstances = new WeakSet();
18
+ const fn = (0, hook_1.toHookFn)(execute);
19
+ return (context) => {
20
+ if (invokedInstances.has(context.instance)) {
21
+ return;
22
+ }
23
+ invokedInstances.add(context.instance);
24
+ return fn(context);
25
+ };
26
+ };
27
+ exports.oncePerInstance = oncePerInstance;