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