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.
- package/README.md +256 -106
- package/cjm/hooks/HookCollector.js +34 -0
- package/cjm/hooks/combinators.js +27 -0
- package/cjm/hooks/hook.js +8 -22
- package/cjm/index.js +18 -29
- package/cjm/utils/getConstructorChain.js +13 -0
- package/cjm/utils/memoize.js +18 -0
- package/cjm/utils/task.js +23 -0
- package/esm/hooks/HookCollector.js +29 -0
- package/esm/hooks/combinators.js +21 -0
- package/esm/hooks/hook.js +5 -17
- package/esm/index.js +6 -10
- package/esm/utils/getConstructorChain.js +9 -0
- package/esm/utils/memoize.js +14 -0
- package/esm/utils/task.js +18 -0
- package/package.json +1 -1
- package/typings/hooks/HookCollector.d.ts +28 -0
- package/typings/hooks/{resolveHooks.d.ts → combinators.d.ts} +2 -0
- package/typings/hooks/hook.d.ts +2 -7
- package/typings/index.d.ts +6 -10
- package/typings/utils/getConstructorChain.d.ts +1 -0
- package/typings/utils/memoize.d.ts +1 -0
- package/typings/utils/task.d.ts +3 -0
- package/cjm/hooks/AsyncHookExecutionStrategy.js +0 -15
- package/cjm/hooks/HookExecutionStrategy.js +0 -68
- package/cjm/hooks/ParallelAsync.js +0 -17
- package/cjm/hooks/SequentialAsync.js +0 -15
- package/cjm/hooks/SequentialSync.js +0 -14
- package/cjm/hooks/onConstruct.js +0 -18
- package/cjm/hooks/onResolved.js +0 -27
- package/cjm/hooks/onScopeDisposed.js +0 -20
- package/cjm/hooks/resolveHooks.js +0 -15
- package/esm/hooks/AsyncHookExecutionStrategy.js +0 -11
- package/esm/hooks/HookExecutionStrategy.js +0 -62
- package/esm/hooks/ParallelAsync.js +0 -13
- package/esm/hooks/SequentialAsync.js +0 -11
- package/esm/hooks/SequentialSync.js +0 -10
- package/esm/hooks/onConstruct.js +0 -13
- package/esm/hooks/onResolved.js +0 -21
- package/esm/hooks/onScopeDisposed.js +0 -15
- package/esm/hooks/resolveHooks.js +0 -11
- package/typings/hooks/AsyncHookExecutionStrategy.d.ts +0 -10
- package/typings/hooks/HookExecutionStrategy.d.ts +0 -35
- package/typings/hooks/ParallelAsync.d.ts +0 -5
- package/typings/hooks/SequentialAsync.d.ts +0 -5
- package/typings/hooks/SequentialSync.d.ts +0 -4
- package/typings/hooks/onConstruct.d.ts +0 -9
- package/typings/hooks/onResolved.d.ts +0 -10
- 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`,
|
|
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
|
-
- [
|
|
59
|
-
- [
|
|
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` / `@
|
|
110
|
-
>
|
|
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',
|
|
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
|
|
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
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
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',
|
|
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
|
-
|
|
2829
|
-
|
|
2830
|
-
`(...
|
|
2831
|
-
|
|
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
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2874
|
+
```typescript
|
|
2875
|
+
// yours, like the hook keys
|
|
2876
|
+
type HookRunner = (actions: HookAction[], context: ExecutionContext) => void;
|
|
2836
2877
|
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
(
|
|
2841
|
-
|
|
2842
|
-
|
|
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
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
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
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
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
|
|
2856
|
-
|
|
2857
|
-
|
|
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
|
-
|
|
2861
|
-
|
|
2862
|
-
`
|
|
2863
|
-
|
|
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
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
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
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
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
|
-
|
|
2919
|
-
|
|
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
|
-
###
|
|
3008
|
+
### Construct hooks
|
|
2923
3009
|
|
|
2924
|
-
> **Prefer
|
|
2925
|
-
>
|
|
2926
|
-
>
|
|
2927
|
-
>
|
|
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.
|
|
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
|
|
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
|
-
|
|
2946
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
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
|
|
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
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
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
|
-
//
|
|
3172
|
+
// The runner awaits the hooks; resolution itself still does not wait for them.
|
|
3058
3173
|
const container = new Container()
|
|
3059
|
-
.useModule(
|
|
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
|
|
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
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
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
|
-
###
|
|
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(
|
|
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 {
|
|
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
|
-
//
|
|
3191
|
-
const
|
|
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',
|
|
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.
|
|
3209
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|