ts-ioc-container 67.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 (37) hide show
  1. package/README.md +237 -92
  2. package/cjm/hooks/HookCollector.js +34 -0
  3. package/cjm/hooks/hook.js +3 -11
  4. package/cjm/index.js +10 -21
  5. package/cjm/utils/getConstructorChain.js +13 -0
  6. package/cjm/utils/memoize.js +18 -0
  7. package/esm/hooks/HookCollector.js +29 -0
  8. package/esm/hooks/hook.js +1 -9
  9. package/esm/index.js +3 -7
  10. package/esm/utils/getConstructorChain.js +9 -0
  11. package/esm/utils/memoize.js +14 -0
  12. package/package.json +1 -1
  13. package/typings/hooks/HookCollector.d.ts +28 -0
  14. package/typings/index.d.ts +3 -7
  15. package/typings/utils/getConstructorChain.d.ts +1 -0
  16. package/typings/utils/memoize.d.ts +1 -0
  17. package/cjm/hooks/HookExecutionStrategy.js +0 -48
  18. package/cjm/hooks/ParallelAsync.js +0 -11
  19. package/cjm/hooks/SequentialAsync.js +0 -11
  20. package/cjm/hooks/SequentialSync.js +0 -12
  21. package/cjm/hooks/onConstruct.js +0 -18
  22. package/cjm/hooks/onResolved.js +0 -27
  23. package/cjm/hooks/onScopeDisposed.js +0 -20
  24. package/esm/hooks/HookExecutionStrategy.js +0 -44
  25. package/esm/hooks/ParallelAsync.js +0 -7
  26. package/esm/hooks/SequentialAsync.js +0 -7
  27. package/esm/hooks/SequentialSync.js +0 -8
  28. package/esm/hooks/onConstruct.js +0 -13
  29. package/esm/hooks/onResolved.js +0 -21
  30. package/esm/hooks/onScopeDisposed.js +0 -15
  31. package/typings/hooks/HookExecutionStrategy.d.ts +0 -33
  32. package/typings/hooks/ParallelAsync.d.ts +0 -4
  33. package/typings/hooks/SequentialAsync.d.ts +0 -4
  34. package/typings/hooks/SequentialSync.d.ts +0 -4
  35. package/typings/hooks/onConstruct.d.ts +0 -9
  36. package/typings/hooks/onResolved.d.ts +0 -10
  37. 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
 
@@ -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,10 +2811,18 @@ 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
- 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:
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:
2818
2826
 
2819
2827
  ```typescript
2820
2828
  class OrderService {
@@ -2842,42 +2850,115 @@ A member carries exactly one hook per key, so decorating the same member twice
2842
2850
  under one key replaces the earlier hook rather than adding to it — decorators
2843
2851
  are applied bottom-up, so the topmost one is the one that stays.
2844
2852
 
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 |
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:
2859
2873
 
2860
2874
  ```typescript
2861
- const container = new Container()
2862
- .useModule(new OnConstructModule(new SequentialSync({ key: 'onConstruct' })))
2863
- .useModule(new OnDisposeModule(new ParallelAsync({ key: 'onScopeDisposed' })));
2875
+ // yours, like the hook keys
2876
+ type HookRunner = (actions: HookAction[], context: ExecutionContext) => void;
2877
+
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
+ };
2884
+
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
+ };
2895
+
2896
+ // every member started at once
2897
+ const atOnce: HookRunner = (actions) => {
2898
+ runAtOnce(actions.map(toTask));
2899
+ };
2900
+
2864
2901
  ```
2865
2902
 
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.
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.
2873
2906
 
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.
2907
+ ```typescript
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));
2932
+ ```
2877
2933
 
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.
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 })`.
2938
+
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.
2945
+
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.
2881
2962
 
2882
2963
  ### Hook domains
2883
2964
 
@@ -2921,22 +3002,22 @@ and `registered` (`ITypedEvent<[IProvider, DependencyKey, IContainer]>`);
2921
3002
  `TypedEvent` itself is exported for your own events: `subscribe` / `unsubscribe`
2922
3003
  / `emit` / `dispose`, with `ITypedEvent` as the subscriber-only view to hand out.
2923
3004
 
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`.
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)).
2927
3007
 
2928
- ### OnConstruct
3008
+ ### Construct hooks
2929
3009
 
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
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
2934
3015
  > 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
3016
+ > instance rather than what the caller receives. A resolve hook runs on every
2936
3017
  > dependency leaving a provider, after the whole decorate chain — the same
2937
3018
  > 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.
3019
+ > Reach for construct hooks only when you mean "this class was just
3020
+ > constructed" specifically.
2940
3021
 
2941
3022
  ```typescript
2942
3023
  import 'reflect-metadata';
@@ -2944,12 +3025,16 @@ import {
2944
3025
  Container,
2945
3026
  type HookFn,
2946
3027
  type IContainer,
3028
+ hook,
3029
+ HookCollector,
3030
+ type HookType,
3031
+ type IContainerModule,
2947
3032
  inject,
2948
- onConstruct,
2949
- OnConstructModule,
2950
3033
  Registration as R,
2951
- SequentialAsync,
2952
- SequentialSync,
3034
+ runInOrder,
3035
+ toTask,
3036
+ type ExecutionContext,
3037
+ type HookAction,
2953
3038
  } from 'ts-ioc-container';
2954
3039
 
2955
3040
  const execute: HookFn = (ctx) => {
@@ -2960,6 +3045,37 @@ const executeAsync: HookFn = async (ctx) => {
2960
3045
  await ctx.invokeMethod({ args: ctx.resolveArgs() });
2961
3046
  };
2962
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
+
2963
3079
  describe('onConstruct', function () {
2964
3080
  it('should run initialization method after dependencies are resolved', function () {
2965
3081
  class DatabaseConnection {
@@ -2973,9 +3089,8 @@ describe('onConstruct', function () {
2973
3089
  }
2974
3090
  }
2975
3091
 
2976
- // The module takes a strategy for how the hooks run; the strategy is keyed to the hooks it runs.
2977
3092
  const container = new Container()
2978
- .useModule(new OnConstructModule(new SequentialSync({ key: 'onConstruct' })))
3093
+ .useModule(onConstructModule(run()))
2979
3094
  .addRegistration(R.fromValue('postgres://localhost:5432').bindTo('ConnectionString'));
2980
3095
 
2981
3096
  const db = container.resolve(DatabaseConnection);
@@ -2984,7 +3099,7 @@ describe('onConstruct', function () {
2984
3099
  expect(db.connectionString).toBe('postgres://localhost:5432');
2985
3100
  });
2986
3101
 
2987
- 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 () {
2988
3103
  const failure = new Error('boom');
2989
3104
 
2990
3105
  class BrokenService {
@@ -2996,12 +3111,9 @@ describe('onConstruct', function () {
2996
3111
 
2997
3112
  let captured: { ex: unknown; scope: IContainer } | undefined;
2998
3113
  const container = new Container().useModule(
2999
- new OnConstructModule(
3000
- new SequentialSync({
3001
- key: 'onConstruct',
3002
- onError: (scope) => (ex) => {
3003
- captured = { ex, scope };
3004
- },
3114
+ onConstructModule(
3115
+ run((scope) => (ex) => {
3116
+ captured = { ex, scope };
3005
3117
  }),
3006
3118
  ),
3007
3119
  );
@@ -3011,7 +3123,7 @@ describe('onConstruct', function () {
3011
3123
  expect(captured?.scope).toBe(container);
3012
3124
  });
3013
3125
 
3014
- 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 () {
3015
3127
  class BrokenService {
3016
3128
  @onConstruct(() => {
3017
3129
  throw new Error('boom');
@@ -3021,12 +3133,9 @@ describe('onConstruct', function () {
3021
3133
 
3022
3134
  let scope: IContainer | undefined;
3023
3135
  const container = new Container().useModule(
3024
- new OnConstructModule(
3025
- new SequentialSync({
3026
- key: 'onConstruct',
3027
- onError: (s) => () => {
3028
- scope = s;
3029
- },
3136
+ onConstructModule(
3137
+ run((s) => () => {
3138
+ scope = s;
3030
3139
  }),
3031
3140
  ),
3032
3141
  );
@@ -3060,9 +3169,9 @@ describe('onConstruct', function () {
3060
3169
  }
3061
3170
  }
3062
3171
 
3063
- // 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.
3064
3173
  const container = new Container()
3065
- .useModule(new OnConstructModule(new SequentialAsync({ key: 'onConstruct' })))
3174
+ .useModule(onConstructModule(run()))
3066
3175
  .addRegistration(R.fromValue('postgres://localhost:5432').bindTo('ConnectionString'));
3067
3176
 
3068
3177
  const db = container.resolve(DatabaseConnection);
@@ -3076,7 +3185,7 @@ describe('onConstruct', function () {
3076
3185
  expect(db.connectionString).toBe('postgres://localhost:5432');
3077
3186
  });
3078
3187
 
3079
- 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 () {
3080
3189
  const failure = new Error('boom');
3081
3190
 
3082
3191
  class BrokenService {
@@ -3086,12 +3195,9 @@ describe('onConstruct', function () {
3086
3195
 
3087
3196
  let captured: { ex: unknown; scope: IContainer } | undefined;
3088
3197
  const container = new Container().useModule(
3089
- new OnConstructModule(
3090
- new SequentialAsync({
3091
- key: 'onConstruct',
3092
- onError: (scope) => (ex) => {
3093
- captured = { ex, scope };
3094
- },
3198
+ onConstructModule(
3199
+ run((scope) => (ex) => {
3200
+ captured = { ex, scope };
3095
3201
  }),
3096
3202
  ),
3097
3203
  );
@@ -3108,27 +3214,60 @@ describe('onConstruct', function () {
3108
3214
 
3109
3215
  ```
3110
3216
 
3111
- ### OnScopeDisposed
3217
+ ### Scope disposal hooks
3112
3218
 
3113
3219
  ```typescript
3114
3220
  import 'reflect-metadata';
3115
3221
  import {
3116
- OnDisposeModule,
3117
3222
  bindTo,
3118
3223
  Container,
3119
3224
  type HookFn,
3225
+ hook,
3226
+ HookCollector,
3227
+ type HookType,
3120
3228
  inject,
3121
- onScopeDisposed,
3122
3229
  register,
3123
3230
  Registration as R,
3124
- SequentialSync,
3125
3231
  singleton,
3232
+ type ExecutionContext,
3233
+ type HookAction,
3234
+ type IContainerModule,
3126
3235
  } from 'ts-ioc-container';
3127
3236
 
3128
3237
  const execute: HookFn = (ctx) => {
3129
3238
  ctx.invokeMethod({ args: ctx.resolveArgs() });
3130
3239
  };
3131
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
+
3132
3271
  @register(bindTo('logsRepo'), singleton())
3133
3272
  class LogsRepo {
3134
3273
  savedLogs: string[] = [];
@@ -3157,7 +3296,7 @@ class Logger {
3157
3296
  describe('onScopeDisposed', function () {
3158
3297
  it('should invoke hooks on all instances when container is disposed', function () {
3159
3298
  const container = new Container()
3160
- .useModule(new OnDisposeModule(new SequentialSync({ key: 'onScopeDisposed' })))
3299
+ .useModule(onScopeDisposedModule(run))
3161
3300
  .addRegistration(R.fromClass(Logger))
3162
3301
  .addRegistration(R.fromClass(LogsRepo));
3163
3302
 
@@ -3177,7 +3316,7 @@ describe('onScopeDisposed', function () {
3177
3316
 
3178
3317
  ```typescript
3179
3318
  import 'reflect-metadata';
3180
- import { Container, hook, injectProp, Registration, sequential, SequentialSync } from 'ts-ioc-container';
3319
+ import { Container, hook, HookCollector, injectProp, Registration, sequential, toTask } from 'ts-ioc-container';
3181
3320
 
3182
3321
  /**
3183
3322
  * UI Components - Property Injection
@@ -3192,8 +3331,8 @@ import { Container, hook, injectProp, Registration, sequential, SequentialSync }
3192
3331
 
3193
3332
  describe('inject property', () => {
3194
3333
  it('should inject property', () => {
3195
- // Strategy for the 'onInit' lifecycle hook
3196
- const onInitStrategy = new SequentialSync({ key: 'onInit' });
3334
+ // Collector for the 'onInit' lifecycle hook
3335
+ const onInit = new HookCollector({ key: 'onInit' });
3197
3336
 
3198
3337
  class UserViewModel {
3199
3338
  // Inject 'GreetingService' into 'greeting' property during 'onInit'
@@ -3210,15 +3349,18 @@ describe('inject property', () => {
3210
3349
  // 1. Create instance (dependencies not yet injected)
3211
3350
  const viewModel = container.resolve(UserViewModel);
3212
3351
 
3213
- // 2. Run lifecycle hooks to inject properties
3214
- 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());
3215
3357
 
3216
3358
  expect(viewModel.greetingService).toBe('Hello');
3217
3359
  expect(viewModel.display()).toBe('Hello User');
3218
3360
  });
3219
3361
 
3220
3362
  it('should read the applied instance property via getProperty', () => {
3221
- const onInitStrategy = new SequentialSync({ key: 'onInit' });
3363
+ const onInit = new HookCollector({ key: 'onInit' });
3222
3364
 
3223
3365
  let injectedValue: unknown;
3224
3366
 
@@ -3235,7 +3377,10 @@ describe('inject property', () => {
3235
3377
  const container = new Container().addRegistration(Registration.fromValue('Hello').bindToKey('GreetingService'));
3236
3378
 
3237
3379
  const viewModel = container.resolve(UserViewModel);
3238
- onInitStrategy.execute(viewModel, { scope: container });
3380
+ onInit
3381
+ .getActions(viewModel, { scope: container })
3382
+ .map(toTask)
3383
+ .forEach((task) => task());
3239
3384
 
3240
3385
  expect(injectedValue).toBe('Hello');
3241
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;
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)
package/cjm/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.onConstruct = exports.injectProp = exports.createHookExecutionContext = exports.createHookContextFactory = exports.HookContext = exports.toHookFn = exports.hasHooks = exports.hook = exports.getHooks = exports.TypedEventDisposedError = exports.UnsupportedTokenTypeError = exports.CannonSingletonApplyTwiceError = exports.ProviderDisposedError = exports.ContainerDisposedError = exports.MethodNotImplementedError = exports.DependencyMissingKeyError = exports.ContainerNotFoundError = exports.DependencyNotFoundError = exports.ContainerError = exports.Registration = exports.onResolve = exports.appendArgsFn = exports.appendArgs = exports.decorate = exports.singleton = exports.autoResolve = exports.lazy = exports.scopeAccess = exports.scope = exports.bindTo = exports.register = exports.toRegistrationFn = exports.toProviderFn = exports.toBindToken = exports.registerPipe = exports.isProviderPipe = exports.Provider = exports.ProxyInjector = exports.SimpleInjector = exports.resolveArgs = exports.argsFn = exports.args = exports.arg = exports.inject = exports.MetadataInjector = exports.Injector = exports.EmptyContainer = exports.AutoResolveModule = exports.Container = exports.isDependencyKey = void 0;
4
- exports.throttle = exports.runAtOnce = exports.runInOrder = exports.handleAsyncError = exports.handleError = exports.getMethodTags = exports.addMethodTag = exports.getMethodLabels = exports.addMethodLabel = exports.getMethodMeta = exports.addMethodMeta = exports.getParamTags = exports.addParamTag = exports.getParamLabels = exports.addParamLabel = exports.getParamMeta = exports.addParamMeta = exports.getClassTags = exports.addClassTag = exports.getClassLabels = exports.addClassLabel = exports.getClassMeta = exports.addClassMeta = exports.resolveConstructor = exports.GroupInstanceToken = exports.ConstantToken = exports.FunctionToken = exports.SingleToken = exports.ClassToken = exports.toSingleAlias = exports.SingleAliasToken = exports.toGroupAlias = exports.GroupAliasToken = exports.argToToken = exports.toMappedToken = exports.toToken = exports.InjectionToken = exports.ParallelAsync = exports.SequentialAsync = exports.SequentialSync = exports.HookExecutionStrategy = exports.oncePerInstance = exports.parallel = exports.sequential = exports.resolved = exports.OnResolvedModule = exports.onResolved = exports.OnDisposeModule = exports.onScopeDisposed = exports.OnConstructModule = void 0;
5
- exports.Is = exports.unwrapProxy = exports.ProxyRegistry = exports.pipe = exports.select = exports.TypedEvent = exports.once = exports.shallowCache = exports.debounce = void 0;
3
+ exports.sequential = exports.injectProp = exports.createHookExecutionContext = exports.createHookContextFactory = exports.HookContext = exports.toHookFn = exports.hasHooks = exports.hook = exports.getHooks = exports.TypedEventDisposedError = exports.UnsupportedTokenTypeError = exports.CannonSingletonApplyTwiceError = exports.ProviderDisposedError = exports.ContainerDisposedError = exports.MethodNotImplementedError = exports.DependencyMissingKeyError = exports.ContainerNotFoundError = exports.DependencyNotFoundError = exports.ContainerError = exports.Registration = exports.onResolve = exports.appendArgsFn = exports.appendArgs = exports.decorate = exports.singleton = exports.autoResolve = exports.lazy = exports.scopeAccess = exports.scope = exports.bindTo = exports.register = exports.toRegistrationFn = exports.toProviderFn = exports.toBindToken = exports.registerPipe = exports.isProviderPipe = exports.Provider = exports.ProxyInjector = exports.SimpleInjector = exports.resolveArgs = exports.argsFn = exports.args = exports.arg = exports.inject = exports.MetadataInjector = exports.Injector = exports.EmptyContainer = exports.AutoResolveModule = exports.Container = exports.isDependencyKey = void 0;
4
+ exports.ProxyRegistry = exports.pipe = exports.select = exports.TypedEvent = exports.getConstructorChain = exports.memoize = exports.once = exports.shallowCache = exports.debounce = exports.throttle = exports.runAtOnce = exports.runInOrder = exports.handleAsyncError = exports.handleError = exports.getMethodTags = exports.addMethodTag = exports.getMethodLabels = exports.addMethodLabel = exports.getMethodMeta = exports.addMethodMeta = exports.getParamTags = exports.addParamTag = exports.getParamLabels = exports.addParamLabel = exports.getParamMeta = exports.addParamMeta = exports.getClassTags = exports.addClassTag = exports.getClassLabels = exports.addClassLabel = exports.getClassMeta = exports.addClassMeta = exports.resolveConstructor = exports.GroupInstanceToken = exports.ConstantToken = exports.FunctionToken = exports.SingleToken = exports.ClassToken = exports.toSingleAlias = exports.SingleAliasToken = exports.toGroupAlias = exports.GroupAliasToken = exports.argToToken = exports.toMappedToken = exports.toToken = exports.InjectionToken = exports.toTask = exports.HookCollector = exports.oncePerInstance = exports.parallel = void 0;
5
+ exports.Is = exports.unwrapProxy = void 0;
6
6
  var IContainer_1 = require("./container/IContainer");
7
7
  Object.defineProperty(exports, "isDependencyKey", { enumerable: true, get: function () { return IContainer_1.isDependencyKey; } });
8
8
  var Container_1 = require("./container/Container");
@@ -76,28 +76,13 @@ Object.defineProperty(exports, "createHookContextFactory", { enumerable: true, g
76
76
  Object.defineProperty(exports, "createHookExecutionContext", { enumerable: true, get: function () { return HookContext_1.createHookExecutionContext; } });
77
77
  var injectProp_1 = require("./hooks/injectProp");
78
78
  Object.defineProperty(exports, "injectProp", { enumerable: true, get: function () { return injectProp_1.injectProp; } });
79
- var onConstruct_1 = require("./hooks/onConstruct");
80
- Object.defineProperty(exports, "onConstruct", { enumerable: true, get: function () { return onConstruct_1.onConstruct; } });
81
- Object.defineProperty(exports, "OnConstructModule", { enumerable: true, get: function () { return onConstruct_1.OnConstructModule; } });
82
- var onScopeDisposed_1 = require("./hooks/onScopeDisposed");
83
- Object.defineProperty(exports, "onScopeDisposed", { enumerable: true, get: function () { return onScopeDisposed_1.onScopeDisposed; } });
84
- Object.defineProperty(exports, "OnDisposeModule", { enumerable: true, get: function () { return onScopeDisposed_1.OnDisposeModule; } });
85
- var onResolved_1 = require("./hooks/onResolved");
86
- Object.defineProperty(exports, "onResolved", { enumerable: true, get: function () { return onResolved_1.onResolved; } });
87
- Object.defineProperty(exports, "OnResolvedModule", { enumerable: true, get: function () { return onResolved_1.OnResolvedModule; } });
88
- Object.defineProperty(exports, "resolved", { enumerable: true, get: function () { return onResolved_1.resolved; } });
89
79
  var combinators_1 = require("./hooks/combinators");
90
80
  Object.defineProperty(exports, "sequential", { enumerable: true, get: function () { return combinators_1.sequential; } });
91
81
  Object.defineProperty(exports, "parallel", { enumerable: true, get: function () { return combinators_1.parallel; } });
92
82
  Object.defineProperty(exports, "oncePerInstance", { enumerable: true, get: function () { return combinators_1.oncePerInstance; } });
93
- var HookExecutionStrategy_1 = require("./hooks/HookExecutionStrategy");
94
- Object.defineProperty(exports, "HookExecutionStrategy", { enumerable: true, get: function () { return HookExecutionStrategy_1.HookExecutionStrategy; } });
95
- var SequentialSync_1 = require("./hooks/SequentialSync");
96
- Object.defineProperty(exports, "SequentialSync", { enumerable: true, get: function () { return SequentialSync_1.SequentialSync; } });
97
- var SequentialAsync_1 = require("./hooks/SequentialAsync");
98
- Object.defineProperty(exports, "SequentialAsync", { enumerable: true, get: function () { return SequentialAsync_1.SequentialAsync; } });
99
- var ParallelAsync_1 = require("./hooks/ParallelAsync");
100
- Object.defineProperty(exports, "ParallelAsync", { enumerable: true, get: function () { return ParallelAsync_1.ParallelAsync; } });
83
+ var HookCollector_1 = require("./hooks/HookCollector");
84
+ Object.defineProperty(exports, "HookCollector", { enumerable: true, get: function () { return HookCollector_1.HookCollector; } });
85
+ Object.defineProperty(exports, "toTask", { enumerable: true, get: function () { return HookCollector_1.toTask; } });
101
86
  var InjectionToken_1 = require("./token/InjectionToken");
102
87
  Object.defineProperty(exports, "InjectionToken", { enumerable: true, get: function () { return InjectionToken_1.InjectionToken; } });
103
88
  var toToken_1 = require("./token/toToken");
@@ -157,6 +142,10 @@ var shallowCache_1 = require("./utils/shallowCache");
157
142
  Object.defineProperty(exports, "shallowCache", { enumerable: true, get: function () { return shallowCache_1.shallowCache; } });
158
143
  var once_1 = require("./utils/once");
159
144
  Object.defineProperty(exports, "once", { enumerable: true, get: function () { return once_1.once; } });
145
+ var memoize_1 = require("./utils/memoize");
146
+ Object.defineProperty(exports, "memoize", { enumerable: true, get: function () { return memoize_1.memoize; } });
147
+ var getConstructorChain_1 = require("./utils/getConstructorChain");
148
+ Object.defineProperty(exports, "getConstructorChain", { enumerable: true, get: function () { return getConstructorChain_1.getConstructorChain; } });
160
149
  var TypedEvent_1 = require("./utils/TypedEvent");
161
150
  Object.defineProperty(exports, "TypedEvent", { enumerable: true, get: function () { return TypedEvent_1.TypedEvent; } });
162
151
  var select_1 = require("./select");
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getConstructorChain = void 0;
4
+ const getConstructorChain = (ctor) => {
5
+ const chain = [];
6
+ let current = ctor;
7
+ while (typeof current === 'function' && current !== Function.prototype) {
8
+ chain.push(current);
9
+ current = Object.getPrototypeOf(current);
10
+ }
11
+ return chain;
12
+ };
13
+ exports.getConstructorChain = getConstructorChain;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoize = void 0;
4
+ const memoize = (fn) => {
5
+ const cache = new WeakMap();
6
+ return (target, key) => {
7
+ let byKey = cache.get(target);
8
+ if (!byKey) {
9
+ byKey = new Map();
10
+ cache.set(target, byKey);
11
+ }
12
+ if (!byKey.has(key)) {
13
+ byKey.set(key, fn(target, key));
14
+ }
15
+ return byKey.get(key);
16
+ };
17
+ };
18
+ exports.memoize = memoize;
@@ -0,0 +1,29 @@
1
+ import { resolveConstructor } from '../metadata/target.js';
2
+ import { memoize } from '../utils/memoize.js';
3
+ import { getHooks, hasHooks, toHookFn } from './hook.js';
4
+ import { createHookExecutionContext } from './HookContext.js';
5
+ export class HookCollector {
6
+ key;
7
+ options;
8
+ hooksOf = memoize(getHooks);
9
+ constructor({ key, createExecutionContext = createHookExecutionContext, mapExecutionContext = (context) => context, predicate = () => true, }) {
10
+ this.key = key;
11
+ this.options = { createExecutionContext, mapExecutionContext, predicate };
12
+ }
13
+ hasHooks(target) {
14
+ return hasHooks(target, this.key);
15
+ }
16
+ getActions(target, { scope, createExecutionContext = this.options.createExecutionContext, mapExecutionContext = this.options.mapExecutionContext, predicate = this.options.predicate, }) {
17
+ const actions = [];
18
+ for (const [methodName, fn] of this.hooksOf(resolveConstructor(target), this.key)) {
19
+ if (predicate(methodName)) {
20
+ actions.push({
21
+ hook: toHookFn(fn),
22
+ context: mapExecutionContext(createExecutionContext(target, scope, methodName)),
23
+ });
24
+ }
25
+ }
26
+ return actions;
27
+ }
28
+ }
29
+ export const toTask = ({ hook, context }) => () => hook(context);
package/esm/hooks/hook.js CHANGED
@@ -1,18 +1,10 @@
1
1
  import { Is } from '../utils/basic.js';
2
2
  import { resolveConstructor } from '../metadata/target.js';
3
+ import { getConstructorChain } from '../utils/getConstructorChain.js';
3
4
  const isHookClassConstructor = (execute) => {
4
5
  return Is.constructor(execute) && execute.prototype.execute;
5
6
  };
6
7
  export const toHookFn = (execute) => isHookClassConstructor(execute) ? (context) => context.scope.resolve(execute).execute(context) : execute;
7
- const getConstructorChain = (ctor) => {
8
- const chain = [];
9
- let current = ctor;
10
- while (typeof current === 'function' && current !== Function.prototype) {
11
- chain.push(current);
12
- current = Object.getPrototypeOf(current);
13
- }
14
- return chain;
15
- };
16
8
  export function getHooks(target, key) {
17
9
  const merged = new Map();
18
10
  for (const ctor of getConstructorChain(resolveConstructor(target)).reverse()) {
package/esm/index.js CHANGED
@@ -22,14 +22,8 @@ export { TypedEventDisposedError } from './errors/TypedEventDisposedError.js';
22
22
  export { getHooks, hook, hasHooks, toHookFn, } from './hooks/hook.js';
23
23
  export { HookContext, createHookContextFactory, createHookExecutionContext, } from './hooks/HookContext.js';
24
24
  export { injectProp } from './hooks/injectProp.js';
25
- export { onConstruct, OnConstructModule } from './hooks/onConstruct.js';
26
- export { onScopeDisposed, OnDisposeModule } from './hooks/onScopeDisposed.js';
27
- export { onResolved, OnResolvedModule, resolved } from './hooks/onResolved.js';
28
25
  export { sequential, parallel, oncePerInstance } from './hooks/combinators.js';
29
- export { HookExecutionStrategy, } from './hooks/HookExecutionStrategy.js';
30
- export { SequentialSync } from './hooks/SequentialSync.js';
31
- export { SequentialAsync } from './hooks/SequentialAsync.js';
32
- export { ParallelAsync } from './hooks/ParallelAsync.js';
26
+ export { HookCollector, toTask, } from './hooks/HookCollector.js';
33
27
  export { InjectionToken } from './token/InjectionToken.js';
34
28
  export { toToken, toMappedToken, argToToken } from './token/toToken.js';
35
29
  export { GroupAliasToken, toGroupAlias } from './token/GroupAliasToken.js';
@@ -49,6 +43,8 @@ export { throttle } from './utils/throttle.js';
49
43
  export { debounce } from './utils/debounce.js';
50
44
  export { shallowCache } from './utils/shallowCache.js';
51
45
  export { once } from './utils/once.js';
46
+ export { memoize } from './utils/memoize.js';
47
+ export { getConstructorChain } from './utils/getConstructorChain.js';
52
48
  export { TypedEvent } from './utils/TypedEvent.js';
53
49
  export { select } from './select.js';
54
50
  export { pipe } from './utils/fp.js';
@@ -0,0 +1,9 @@
1
+ export const getConstructorChain = (ctor) => {
2
+ const chain = [];
3
+ let current = ctor;
4
+ while (typeof current === 'function' && current !== Function.prototype) {
5
+ chain.push(current);
6
+ current = Object.getPrototypeOf(current);
7
+ }
8
+ return chain;
9
+ };
@@ -0,0 +1,14 @@
1
+ export const memoize = (fn) => {
2
+ const cache = new WeakMap();
3
+ return (target, key) => {
4
+ let byKey = cache.get(target);
5
+ if (!byKey) {
6
+ byKey = new Map();
7
+ cache.set(target, byKey);
8
+ }
9
+ if (!byKey.has(key)) {
10
+ byKey.set(key, fn(target, key));
11
+ }
12
+ return byKey.get(key);
13
+ };
14
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-ioc-container",
3
- "version": "67.0.0",
3
+ "version": "68.0.0",
4
4
  "description": "Fast, lightweight TypeScript dependency injection container with a clean API, scoped lifecycles, decorators, tokens, hooks, lazy injection, customizable providers, and no global container objects.",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -0,0 +1,28 @@
1
+ import type { ExecutionContext } from '../ExecutionContext.js';
2
+ import { type Task } from '../utils/task.js';
3
+ import { type Instance } from '../utils/basic.js';
4
+ import { type HookFn } from './hook.js';
5
+ import { type CreateHookExecutionContext, type IHookContext } from './HookContext.js';
6
+ export type MapHookExecutionContext = (context: IHookContext) => IHookContext;
7
+ export type HookAction = {
8
+ hook: HookFn;
9
+ context: IHookContext;
10
+ };
11
+ export type HookCollectorOptions = {
12
+ createExecutionContext?: CreateHookExecutionContext;
13
+ mapExecutionContext?: MapHookExecutionContext;
14
+ predicate?: (methodName: string) => boolean;
15
+ };
16
+ export type HookCollectionContext = ExecutionContext & HookCollectorOptions;
17
+ export type HookCollectorProps = HookCollectorOptions & {
18
+ key: string | symbol;
19
+ };
20
+ export declare class HookCollector {
21
+ private readonly key;
22
+ private readonly options;
23
+ private readonly hooksOf;
24
+ constructor({ key, createExecutionContext, mapExecutionContext, predicate, }: HookCollectorProps);
25
+ hasHooks(target: Instance): boolean;
26
+ getActions(target: Instance, { scope, createExecutionContext, mapExecutionContext, predicate, }: HookCollectionContext): HookAction[];
27
+ }
28
+ export declare const toTask: ({ hook, context }: HookAction) => Task;
@@ -23,14 +23,8 @@ export { TypedEventDisposedError } from './errors/TypedEventDisposedError.js';
23
23
  export { getHooks, hook, hasHooks, toHookFn, type HookFn, type HookClass, type HookType, type InjectFn, type HooksOfClass, } from './hooks/hook.js';
24
24
  export { HookContext, createHookContextFactory, createHookExecutionContext, type CreateHookExecutionContext, type IHookContext, } from './hooks/HookContext.js';
25
25
  export { injectProp } from './hooks/injectProp.js';
26
- export { onConstruct, OnConstructModule } from './hooks/onConstruct.js';
27
- export { onScopeDisposed, OnDisposeModule } from './hooks/onScopeDisposed.js';
28
- export { onResolved, OnResolvedModule, resolved } from './hooks/onResolved.js';
29
26
  export { sequential, parallel, oncePerInstance, type ResolvedObjectHook } from './hooks/combinators.js';
30
- export { HookExecutionStrategy, type HookExecutionContext, type HookExecutionOptions, type HookExecutionStrategyProps, type MapHookExecutionContext, type MemberHook, type OnErrorHandler, } from './hooks/HookExecutionStrategy.js';
31
- export { SequentialSync } from './hooks/SequentialSync.js';
32
- export { SequentialAsync } from './hooks/SequentialAsync.js';
33
- export { ParallelAsync } from './hooks/ParallelAsync.js';
27
+ export { HookCollector, toTask, type HookAction, type HookCollectionContext, type HookCollectorOptions, type HookCollectorProps, type MapHookExecutionContext, } from './hooks/HookCollector.js';
34
28
  export { InjectionToken } from './token/InjectionToken.js';
35
29
  export { type Injectable, toToken, toMappedToken, argToToken } from './token/toToken.js';
36
30
  export { GroupAliasToken, toGroupAlias } from './token/GroupAliasToken.js';
@@ -50,6 +44,8 @@ export { throttle } from './utils/throttle.js';
50
44
  export { debounce } from './utils/debounce.js';
51
45
  export { shallowCache } from './utils/shallowCache.js';
52
46
  export { once } from './utils/once.js';
47
+ export { memoize } from './utils/memoize.js';
48
+ export { getConstructorChain } from './utils/getConstructorChain.js';
53
49
  export { TypedEvent, type ITypedEvent, type TypedEventListener, type Unsubscribe } from './utils/TypedEvent.js';
54
50
  export { type ExecutionContext } from './ExecutionContext.js';
55
51
  export { select } from './select.js';
@@ -0,0 +1 @@
1
+ export declare const getConstructorChain: (ctor: unknown) => object[];
@@ -0,0 +1 @@
1
+ export declare const memoize: <T extends object, K, R>(fn: (target: T, key: K) => R) => ((target: T, key: K) => R);
@@ -1,48 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HookExecutionStrategy = void 0;
4
- const target_1 = require("../metadata/target");
5
- const hook_1 = require("./hook");
6
- const HookContext_1 = require("./HookContext");
7
- class HookExecutionStrategy {
8
- key;
9
- onError;
10
- options;
11
- hooksByClass = new WeakMap();
12
- constructor({ key, onError, createExecutionContext = HookContext_1.createHookExecutionContext, mapExecutionContext = (context) => context, predicate = () => true, }) {
13
- this.key = key;
14
- this.onError = onError;
15
- this.options = { createExecutionContext, mapExecutionContext, predicate };
16
- }
17
- hasHooks(target) {
18
- return (0, hook_1.hasHooks)(target, this.key);
19
- }
20
- execute(target, { scope, ...overrides }) {
21
- const report = (ex) => this.onError?.(scope)(ex);
22
- try {
23
- this.processHooks(this.collect(target, scope, overrides))?.catch(report);
24
- }
25
- catch (ex) {
26
- report(ex);
27
- }
28
- }
29
- collect(target, scope, { createExecutionContext = this.options.createExecutionContext, mapExecutionContext = this.options.mapExecutionContext, predicate = this.options.predicate, }) {
30
- const members = [];
31
- for (const { methodName, hook } of this.hooksOf(target)) {
32
- if (predicate(methodName)) {
33
- members.push({ hook, context: mapExecutionContext(createExecutionContext(target, scope, methodName)) });
34
- }
35
- }
36
- return members;
37
- }
38
- hooksOf(target) {
39
- const Target = (0, target_1.resolveConstructor)(target);
40
- let hooks = this.hooksByClass.get(Target);
41
- if (!hooks) {
42
- hooks = Array.from((0, hook_1.getHooks)(Target, this.key), ([methodName, fn]) => ({ methodName, hook: (0, hook_1.toHookFn)(fn) }));
43
- this.hooksByClass.set(Target, hooks);
44
- }
45
- return hooks;
46
- }
47
- }
48
- exports.HookExecutionStrategy = HookExecutionStrategy;
@@ -1,11 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ParallelAsync = void 0;
4
- const HookExecutionStrategy_1 = require("./HookExecutionStrategy");
5
- const task_1 = require("../utils/task");
6
- class ParallelAsync extends HookExecutionStrategy_1.HookExecutionStrategy {
7
- processHooks(members) {
8
- return (0, task_1.runAtOnce)(members.map(({ hook, context }) => () => hook(context)));
9
- }
10
- }
11
- exports.ParallelAsync = ParallelAsync;
@@ -1,11 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SequentialAsync = void 0;
4
- const HookExecutionStrategy_1 = require("./HookExecutionStrategy");
5
- const task_1 = require("../utils/task");
6
- class SequentialAsync extends HookExecutionStrategy_1.HookExecutionStrategy {
7
- processHooks(members) {
8
- return (0, task_1.runInOrder)(members.map(({ hook, context }) => () => hook(context)));
9
- }
10
- }
11
- exports.SequentialAsync = SequentialAsync;
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SequentialSync = void 0;
4
- const HookExecutionStrategy_1 = require("./HookExecutionStrategy");
5
- class SequentialSync extends HookExecutionStrategy_1.HookExecutionStrategy {
6
- processHooks(members) {
7
- for (const { hook, context } of members) {
8
- hook(context);
9
- }
10
- }
11
- }
12
- exports.SequentialSync = SequentialSync;
@@ -1,18 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OnConstructModule = exports.onConstruct = void 0;
4
- const hook_1 = require("./hook");
5
- const onConstruct = (fn) => (0, hook_1.hook)('onConstruct', fn);
6
- exports.onConstruct = onConstruct;
7
- class OnConstructModule {
8
- strategy;
9
- constructor(strategy) {
10
- this.strategy = strategy;
11
- }
12
- applyTo(container) {
13
- container.getInjector().onConstructed((instance, scope) => {
14
- this.strategy.execute(instance, { scope });
15
- });
16
- }
17
- }
18
- exports.OnConstructModule = OnConstructModule;
@@ -1,27 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolved = exports.OnResolvedModule = exports.onResolved = void 0;
4
- const IRegistration_1 = require("../registration/IRegistration");
5
- const basic_1 = require("../utils/basic");
6
- const hook_1 = require("./hook");
7
- const onResolved = (fn) => (0, hook_1.hook)('onResolved', fn);
8
- exports.onResolved = onResolved;
9
- const runHooks = (strategy) => (dependency, scope) => {
10
- if (basic_1.Is.object(dependency)) {
11
- strategy.execute(dependency, { scope });
12
- }
13
- };
14
- class OnResolvedModule {
15
- runHooks;
16
- constructor(strategy) {
17
- this.runHooks = runHooks(strategy);
18
- }
19
- applyTo(container) {
20
- container.registered.subscribe((provider) => {
21
- provider.onResolved(this.runHooks);
22
- });
23
- }
24
- }
25
- exports.OnResolvedModule = OnResolvedModule;
26
- const resolved = (strategy) => (0, IRegistration_1.registerPipe)((p) => p.onResolved(runHooks(strategy)));
27
- exports.resolved = resolved;
@@ -1,20 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OnDisposeModule = exports.onScopeDisposed = void 0;
4
- const hook_1 = require("./hook");
5
- const onScopeDisposed = (fn) => (0, hook_1.hook)('onScopeDisposed', fn);
6
- exports.onScopeDisposed = onScopeDisposed;
7
- class OnDisposeModule {
8
- strategy;
9
- constructor(strategy) {
10
- this.strategy = strategy;
11
- }
12
- applyTo(container) {
13
- container.scopeDisposed.subscribe((scope) => {
14
- for (const instance of scope.getInstances()) {
15
- this.strategy.execute(instance, { scope });
16
- }
17
- });
18
- }
19
- }
20
- exports.OnDisposeModule = OnDisposeModule;
@@ -1,44 +0,0 @@
1
- import { resolveConstructor } from '../metadata/target.js';
2
- import { getHooks, hasHooks, toHookFn } from './hook.js';
3
- import { createHookExecutionContext } from './HookContext.js';
4
- export class HookExecutionStrategy {
5
- key;
6
- onError;
7
- options;
8
- hooksByClass = new WeakMap();
9
- constructor({ key, onError, createExecutionContext = createHookExecutionContext, mapExecutionContext = (context) => context, predicate = () => true, }) {
10
- this.key = key;
11
- this.onError = onError;
12
- this.options = { createExecutionContext, mapExecutionContext, predicate };
13
- }
14
- hasHooks(target) {
15
- return hasHooks(target, this.key);
16
- }
17
- execute(target, { scope, ...overrides }) {
18
- const report = (ex) => this.onError?.(scope)(ex);
19
- try {
20
- this.processHooks(this.collect(target, scope, overrides))?.catch(report);
21
- }
22
- catch (ex) {
23
- report(ex);
24
- }
25
- }
26
- collect(target, scope, { createExecutionContext = this.options.createExecutionContext, mapExecutionContext = this.options.mapExecutionContext, predicate = this.options.predicate, }) {
27
- const members = [];
28
- for (const { methodName, hook } of this.hooksOf(target)) {
29
- if (predicate(methodName)) {
30
- members.push({ hook, context: mapExecutionContext(createExecutionContext(target, scope, methodName)) });
31
- }
32
- }
33
- return members;
34
- }
35
- hooksOf(target) {
36
- const Target = resolveConstructor(target);
37
- let hooks = this.hooksByClass.get(Target);
38
- if (!hooks) {
39
- hooks = Array.from(getHooks(Target, this.key), ([methodName, fn]) => ({ methodName, hook: toHookFn(fn) }));
40
- this.hooksByClass.set(Target, hooks);
41
- }
42
- return hooks;
43
- }
44
- }
@@ -1,7 +0,0 @@
1
- import { HookExecutionStrategy } from './HookExecutionStrategy.js';
2
- import { runAtOnce } from '../utils/task.js';
3
- export class ParallelAsync extends HookExecutionStrategy {
4
- processHooks(members) {
5
- return runAtOnce(members.map(({ hook, context }) => () => hook(context)));
6
- }
7
- }
@@ -1,7 +0,0 @@
1
- import { HookExecutionStrategy } from './HookExecutionStrategy.js';
2
- import { runInOrder } from '../utils/task.js';
3
- export class SequentialAsync extends HookExecutionStrategy {
4
- processHooks(members) {
5
- return runInOrder(members.map(({ hook, context }) => () => hook(context)));
6
- }
7
- }
@@ -1,8 +0,0 @@
1
- import { HookExecutionStrategy } from './HookExecutionStrategy.js';
2
- export class SequentialSync extends HookExecutionStrategy {
3
- processHooks(members) {
4
- for (const { hook, context } of members) {
5
- hook(context);
6
- }
7
- }
8
- }
@@ -1,13 +0,0 @@
1
- import { hook } from './hook.js';
2
- export const onConstruct = (fn) => hook('onConstruct', fn);
3
- export class OnConstructModule {
4
- strategy;
5
- constructor(strategy) {
6
- this.strategy = strategy;
7
- }
8
- applyTo(container) {
9
- container.getInjector().onConstructed((instance, scope) => {
10
- this.strategy.execute(instance, { scope });
11
- });
12
- }
13
- }
@@ -1,21 +0,0 @@
1
- import { registerPipe } from '../registration/IRegistration.js';
2
- import { Is } from '../utils/basic.js';
3
- import { hook } from './hook.js';
4
- export const onResolved = (fn) => hook('onResolved', fn);
5
- const runHooks = (strategy) => (dependency, scope) => {
6
- if (Is.object(dependency)) {
7
- strategy.execute(dependency, { scope });
8
- }
9
- };
10
- export class OnResolvedModule {
11
- runHooks;
12
- constructor(strategy) {
13
- this.runHooks = runHooks(strategy);
14
- }
15
- applyTo(container) {
16
- container.registered.subscribe((provider) => {
17
- provider.onResolved(this.runHooks);
18
- });
19
- }
20
- }
21
- export const resolved = (strategy) => registerPipe((p) => p.onResolved(runHooks(strategy)));
@@ -1,15 +0,0 @@
1
- import { hook } from './hook.js';
2
- export const onScopeDisposed = (fn) => hook('onScopeDisposed', fn);
3
- export class OnDisposeModule {
4
- strategy;
5
- constructor(strategy) {
6
- this.strategy = strategy;
7
- }
8
- applyTo(container) {
9
- container.scopeDisposed.subscribe((scope) => {
10
- for (const instance of scope.getInstances()) {
11
- this.strategy.execute(instance, { scope });
12
- }
13
- });
14
- }
15
- }
@@ -1,33 +0,0 @@
1
- import type { IContainer } from '../container/IContainer.js';
2
- import type { ExecutionContext } from '../ExecutionContext.js';
3
- import { type Instance } from '../utils/basic.js';
4
- import { type HookFn } from './hook.js';
5
- import { type CreateHookExecutionContext, type IHookContext } from './HookContext.js';
6
- export type MapHookExecutionContext = (context: IHookContext) => IHookContext;
7
- export type OnErrorHandler = (scope: IContainer) => (error: unknown) => void;
8
- export type HookExecutionOptions = {
9
- createExecutionContext?: CreateHookExecutionContext;
10
- mapExecutionContext?: MapHookExecutionContext;
11
- predicate?: (methodName: string) => boolean;
12
- };
13
- export type HookExecutionContext = ExecutionContext & HookExecutionOptions;
14
- export type HookExecutionStrategyProps = HookExecutionOptions & {
15
- key: string | symbol;
16
- onError?: OnErrorHandler;
17
- };
18
- export type MemberHook = {
19
- hook: HookFn;
20
- context: IHookContext;
21
- };
22
- export declare abstract class HookExecutionStrategy {
23
- private readonly key;
24
- private readonly onError?;
25
- private readonly options;
26
- private readonly hooksByClass;
27
- constructor({ key, onError, createExecutionContext, mapExecutionContext, predicate, }: HookExecutionStrategyProps);
28
- hasHooks(target: Instance): boolean;
29
- execute(target: Instance, { scope, ...overrides }: HookExecutionContext): void;
30
- protected abstract processHooks(members: MemberHook[]): void | Promise<void>;
31
- private collect;
32
- private hooksOf;
33
- }
@@ -1,4 +0,0 @@
1
- import { HookExecutionStrategy, type MemberHook } from './HookExecutionStrategy.js';
2
- export declare class ParallelAsync extends HookExecutionStrategy {
3
- protected processHooks(members: MemberHook[]): void | Promise<void>;
4
- }
@@ -1,4 +0,0 @@
1
- import { HookExecutionStrategy, type MemberHook } from './HookExecutionStrategy.js';
2
- export declare class SequentialAsync extends HookExecutionStrategy {
3
- protected processHooks(members: MemberHook[]): void | Promise<void>;
4
- }
@@ -1,4 +0,0 @@
1
- import { HookExecutionStrategy, type MemberHook } from './HookExecutionStrategy.js';
2
- export declare class SequentialSync extends HookExecutionStrategy {
3
- protected processHooks(members: MemberHook[]): void;
4
- }
@@ -1,9 +0,0 @@
1
- import { type HookType } from './hook.js';
2
- import type { IContainer, IContainerModule } from '../container/IContainer.js';
3
- import { type HookExecutionStrategy } from './HookExecutionStrategy.js';
4
- export declare const onConstruct: (fn: HookType) => (target: object, propertyKey: string | symbol) => void;
5
- export declare class OnConstructModule implements IContainerModule {
6
- private readonly strategy;
7
- constructor(strategy: HookExecutionStrategy);
8
- applyTo(container: IContainer): void;
9
- }
@@ -1,10 +0,0 @@
1
- import type { IContainer, IContainerModule } from '../container/IContainer.js';
2
- import { type HookExecutionStrategy } from './HookExecutionStrategy.js';
3
- import { type HookType } from './hook.js';
4
- export declare const onResolved: (fn: HookType) => (target: object, propertyKey: string | symbol) => void;
5
- export declare class OnResolvedModule implements IContainerModule {
6
- private readonly runHooks;
7
- constructor(strategy: HookExecutionStrategy);
8
- applyTo(container: IContainer): void;
9
- }
10
- export declare const resolved: <T = unknown>(strategy: HookExecutionStrategy) => import("../registration/IRegistration.js").ProviderPipe<T>;
@@ -1,9 +0,0 @@
1
- import { type HookType } from './hook.js';
2
- import type { IContainer, IContainerModule } from '../container/IContainer.js';
3
- import { type HookExecutionStrategy } from './HookExecutionStrategy.js';
4
- export declare const onScopeDisposed: (fn: HookType) => (target: object, propertyKey: string | symbol) => void;
5
- export declare class OnDisposeModule implements IContainerModule {
6
- private readonly strategy;
7
- constructor(strategy: HookExecutionStrategy);
8
- applyTo(container: IContainer): void;
9
- }