@loopback/context 3.5.1 → 3.8.1

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 (71) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/binding-config.js +1 -0
  3. package/dist/binding-config.js.map +1 -1
  4. package/dist/binding-decorator.d.ts +1 -2
  5. package/dist/binding-decorator.js +1 -0
  6. package/dist/binding-decorator.js.map +1 -1
  7. package/dist/binding-filter.d.ts +2 -2
  8. package/dist/binding-filter.js +4 -3
  9. package/dist/binding-filter.js.map +1 -1
  10. package/dist/binding-inspector.d.ts +14 -7
  11. package/dist/binding-inspector.js +14 -6
  12. package/dist/binding-inspector.js.map +1 -1
  13. package/dist/binding-key.d.ts +5 -0
  14. package/dist/binding-key.js +104 -90
  15. package/dist/binding-key.js.map +1 -1
  16. package/dist/binding-sorter.js +1 -0
  17. package/dist/binding-sorter.js.map +1 -1
  18. package/dist/binding.d.ts +84 -7
  19. package/dist/binding.js +120 -35
  20. package/dist/binding.js.map +1 -1
  21. package/dist/context-subscription.js +1 -0
  22. package/dist/context-subscription.js.map +1 -1
  23. package/dist/context-tag-indexer.js +1 -0
  24. package/dist/context-tag-indexer.js.map +1 -1
  25. package/dist/context-view.js +1 -0
  26. package/dist/context-view.js.map +1 -1
  27. package/dist/context.js +2 -2
  28. package/dist/context.js.map +1 -1
  29. package/dist/index.js +4 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/inject-config.js +1 -0
  32. package/dist/inject-config.js.map +1 -1
  33. package/dist/inject.d.ts +13 -6
  34. package/dist/inject.js +6 -8
  35. package/dist/inject.js.map +1 -1
  36. package/dist/interception-proxy.js +1 -0
  37. package/dist/interception-proxy.js.map +1 -1
  38. package/dist/interceptor-chain.d.ts +39 -4
  39. package/dist/interceptor-chain.js +26 -4
  40. package/dist/interceptor-chain.js.map +1 -1
  41. package/dist/interceptor.d.ts +28 -3
  42. package/dist/interceptor.js +54 -0
  43. package/dist/interceptor.js.map +1 -1
  44. package/dist/invocation.js +1 -0
  45. package/dist/invocation.js.map +1 -1
  46. package/dist/keys.d.ts +5 -0
  47. package/dist/keys.js +6 -0
  48. package/dist/keys.js.map +1 -1
  49. package/dist/resolution-session.d.ts +26 -6
  50. package/dist/resolution-session.js +1 -0
  51. package/dist/resolution-session.js.map +1 -1
  52. package/dist/resolver.js +1 -0
  53. package/dist/resolver.js.map +1 -1
  54. package/dist/value-promise.d.ts +8 -4
  55. package/dist/value-promise.js +17 -0
  56. package/dist/value-promise.js.map +1 -1
  57. package/package.json +13 -13
  58. package/src/binding-decorator.ts +2 -3
  59. package/src/binding-filter.ts +8 -7
  60. package/src/binding-inspector.ts +32 -9
  61. package/src/binding-key.ts +12 -0
  62. package/src/binding.ts +210 -44
  63. package/src/context.ts +2 -2
  64. package/src/inject.ts +22 -16
  65. package/src/interceptor-chain.ts +66 -7
  66. package/src/interceptor.ts +85 -2
  67. package/src/keys.ts +6 -0
  68. package/src/resolution-session.ts +30 -2
  69. package/src/value-promise.ts +13 -0
  70. package/index.d.ts +0 -6
  71. package/index.js +0 -6
package/src/binding.ts CHANGED
@@ -9,11 +9,13 @@ import {BindingAddress, BindingKey} from './binding-key';
9
9
  import {Context} from './context';
10
10
  import {inspectInjections} from './inject';
11
11
  import {createProxyWithInterceptors} from './interception-proxy';
12
+ import {invokeMethod} from './invocation';
12
13
  import {JSONObject} from './json-types';
13
14
  import {ContextTags} from './keys';
14
15
  import {Provider} from './provider';
15
16
  import {
16
17
  asResolutionOptions,
18
+ ResolutionContext,
17
19
  ResolutionOptions,
18
20
  ResolutionOptionsOrSession,
19
21
  ResolutionSession,
@@ -129,6 +131,56 @@ export enum BindingType {
129
131
  ALIAS = 'Alias',
130
132
  }
131
133
 
134
+ /**
135
+ * Binding source for `to`
136
+ */
137
+ export type ConstantBindingSource<T> = {
138
+ type: BindingType.CONSTANT;
139
+ value: T;
140
+ };
141
+
142
+ /**
143
+ * Binding source for `toDynamicValue`
144
+ */
145
+ export type DynamicValueBindingSource<T> = {
146
+ type: BindingType.DYNAMIC_VALUE;
147
+ value: ValueFactory<T> | DynamicValueProviderClass<T>;
148
+ };
149
+
150
+ /**
151
+ * Binding source for `toClass`
152
+ */
153
+ export type ClassBindingSource<T> = {
154
+ type: BindingType.CLASS;
155
+ value: Constructor<T>;
156
+ };
157
+
158
+ /**
159
+ * Binding source for `toProvider`
160
+ */
161
+ export type ProviderBindingSource<T> = {
162
+ type: BindingType.PROVIDER;
163
+ value: Constructor<Provider<T>>;
164
+ };
165
+
166
+ /**
167
+ * Binding source for `toAlias`
168
+ */
169
+ export type AliasBindingSource<T> = {
170
+ type: BindingType.ALIAS;
171
+ value: BindingAddress<T>;
172
+ };
173
+
174
+ /**
175
+ * Source for the binding, including the type and value
176
+ */
177
+ export type BindingSource<T> =
178
+ | ConstantBindingSource<T>
179
+ | DynamicValueBindingSource<T>
180
+ | ClassBindingSource<T>
181
+ | ProviderBindingSource<T>
182
+ | AliasBindingSource<T>;
183
+
132
184
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
185
  export type TagMap = MapObject<any>;
134
186
 
@@ -170,11 +222,62 @@ export type BindingEventListener = (
170
222
  event: BindingEvent,
171
223
  ) => void;
172
224
 
173
- type ValueGetter<T> = (
174
- ctx: Context,
175
- options: ResolutionOptions,
225
+ /**
226
+ * A factory function for `toDynamicValue`
227
+ */
228
+ export type ValueFactory<T = unknown> = (
229
+ resolutionCtx: ResolutionContext,
176
230
  ) => ValueOrPromise<T | undefined>;
177
231
 
232
+ /**
233
+ * A class with a static `value` method as the factory function for
234
+ * `toDynamicValue`.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * import {inject} from '@loopback/context';
239
+ *
240
+ * export class DynamicGreetingProvider {
241
+ * static value(@inject('currentUser') user: string) {
242
+ * return `Hello, ${user}`;
243
+ * }
244
+ * }
245
+ * ```
246
+ */
247
+ export interface DynamicValueProviderClass<T = unknown>
248
+ extends Constructor<unknown>,
249
+ Function {
250
+ value: (...args: BoundValue[]) => ValueOrPromise<T>;
251
+ }
252
+
253
+ /**
254
+ * Adapt the ValueFactoryProvider class to be a value factory
255
+ * @param provider - ValueFactoryProvider class
256
+ */
257
+ function toValueFactory<T = unknown>(
258
+ provider: DynamicValueProviderClass<T>,
259
+ ): ValueFactory<T> {
260
+ return resolutionCtx =>
261
+ invokeMethod(provider, 'value', resolutionCtx.context, [], {
262
+ skipInterceptors: true,
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Check if the factory is a value factory provider class
268
+ * @param factory - A factory function or a dynamic value provider class
269
+ */
270
+ export function isDynamicValueProviderClass<T = unknown>(
271
+ factory: unknown,
272
+ ): factory is DynamicValueProviderClass<T> {
273
+ // Not a class
274
+ if (typeof factory !== 'function' || !String(factory).startsWith('class ')) {
275
+ return false;
276
+ }
277
+ const valueMethod = (factory as DynamicValueProviderClass).value;
278
+ return typeof valueMethod === 'function';
279
+ }
280
+
178
281
  /**
179
282
  * Binding represents an entry in the `Context`. Each binding has a key and a
180
283
  * corresponding value getter.
@@ -199,27 +302,34 @@ export class Binding<T = BoundValue> extends EventEmitter {
199
302
  return this._scope ?? BindingScope.TRANSIENT;
200
303
  }
201
304
 
202
- private _type?: BindingType;
203
305
  /**
204
306
  * Type of the binding value getter
205
307
  */
206
308
  public get type(): BindingType | undefined {
207
- return this._type;
309
+ return this._source?.type;
208
310
  }
209
311
 
210
312
  private _cache: WeakMap<Context, T>;
211
- private _getValue: ValueGetter<T>;
313
+ private _getValue?: ValueFactory<T>;
212
314
 
213
- private _valueConstructor?: Constructor<T>;
214
- private _providerConstructor?: Constructor<Provider<T>>;
215
- private _alias?: BindingAddress<T>;
315
+ /**
316
+ * The original source value received from `to`, `toClass`, `toDynamicValue`,
317
+ * `toProvider`, or `toAlias`.
318
+ */
319
+ private _source?: BindingSource<T>;
320
+
321
+ public get source() {
322
+ return this._source;
323
+ }
216
324
 
217
325
  /**
218
326
  * For bindings bound via `toClass()`, this property contains the constructor
219
327
  * function of the class
220
328
  */
221
329
  public get valueConstructor(): Constructor<T> | undefined {
222
- return this._valueConstructor;
330
+ return this._source?.type === BindingType.CLASS
331
+ ? this._source?.value
332
+ : undefined;
223
333
  }
224
334
 
225
335
  /**
@@ -227,7 +337,9 @@ export class Binding<T = BoundValue> extends EventEmitter {
227
337
  * constructor function of the provider class
228
338
  */
229
339
  public get providerConstructor(): Constructor<Provider<T>> | undefined {
230
- return this._providerConstructor;
340
+ return this._source?.type === BindingType.PROVIDER
341
+ ? this._source?.value
342
+ : undefined;
231
343
  }
232
344
 
233
345
  constructor(key: BindingAddress<T>, public isLocked: boolean = false) {
@@ -269,6 +381,28 @@ export class Binding<T = BoundValue> extends EventEmitter {
269
381
  this._cache = new WeakMap();
270
382
  }
271
383
 
384
+ /**
385
+ * Invalidate the binding cache so that its value will be reloaded next time.
386
+ * This is useful to force reloading a singleton when its configuration or
387
+ * dependencies are changed.
388
+ * **WARNING**: The state held in the cached value will be gone.
389
+ *
390
+ * @param ctx - Context object
391
+ */
392
+ refresh(ctx: Context) {
393
+ if (!this._cache) return;
394
+ if (this.scope === BindingScope.SINGLETON) {
395
+ // Cache the value
396
+ const ownerCtx = ctx.getOwnerContext(this.key);
397
+ if (ownerCtx != null) {
398
+ this._cache.delete(ownerCtx);
399
+ }
400
+ } else if (this.scope === BindingScope.CONTEXT) {
401
+ // Cache the value at the current context
402
+ this._cache.delete(ctx);
403
+ }
404
+ }
405
+
272
406
  /**
273
407
  * This is an internal function optimized for performance.
274
408
  * Users should use `@inject(key)` or `ctx.get(key)` instead.
@@ -331,11 +465,17 @@ export class Binding<T = BoundValue> extends EventEmitter {
331
465
  }
332
466
  }
333
467
  const options = asResolutionOptions(optionsOrSession);
334
- if (this._getValue) {
468
+ if (typeof this._getValue === 'function') {
335
469
  const result = ResolutionSession.runWithBinding(
336
470
  s => {
337
471
  const optionsWithSession = Object.assign({}, options, {session: s});
338
- return this._getValue(ctx, optionsWithSession);
472
+ // We already test `this._getValue` is a function. It's safe to assert
473
+ // that `this._getValue` is not undefined.
474
+ return this._getValue!({
475
+ context: ctx,
476
+ binding: this,
477
+ options: optionsWithSession,
478
+ });
339
479
  },
340
480
  this,
341
481
  options.session,
@@ -442,16 +582,19 @@ export class Binding<T = BoundValue> extends EventEmitter {
442
582
  * Set the `_getValue` function
443
583
  * @param getValue - getValue function
444
584
  */
445
- private _setValueGetter(getValue: ValueGetter<T>) {
585
+ private _setValueGetter(getValue: ValueFactory<T>) {
446
586
  // Clear the cache
447
587
  this._clearCache();
448
- this._getValue = (ctx: Context, options: ResolutionOptions) => {
449
- if (options.asProxyWithInterceptors && this._type !== BindingType.CLASS) {
588
+ this._getValue = resolutionCtx => {
589
+ if (
590
+ resolutionCtx.options.asProxyWithInterceptors &&
591
+ this._source?.type !== BindingType.CLASS
592
+ ) {
450
593
  throw new Error(
451
- `Binding '${this.key}' (${this._type}) does not support 'asProxyWithInterceptors'`,
594
+ `Binding '${this.key}' (${this._source?.type}) does not support 'asProxyWithInterceptors'`,
452
595
  );
453
596
  }
454
- return getValue(ctx, options);
597
+ return getValue(resolutionCtx);
455
598
  };
456
599
  this.emitChangedEvent('value');
457
600
  }
@@ -494,7 +637,10 @@ export class Binding<T = BoundValue> extends EventEmitter {
494
637
  if (debug.enabled) {
495
638
  debug('Bind %s to constant:', this.key, value);
496
639
  }
497
- this._type = BindingType.CONSTANT;
640
+ this._source = {
641
+ type: BindingType.CONSTANT,
642
+ value,
643
+ };
498
644
  this._setValueGetter(() => value);
499
645
  return this;
500
646
  }
@@ -517,13 +663,25 @@ export class Binding<T = BoundValue> extends EventEmitter {
517
663
  * );
518
664
  * ```
519
665
  */
520
- toDynamicValue(factoryFn: () => ValueOrPromise<T>): this {
666
+ toDynamicValue(
667
+ factory: ValueFactory<T> | DynamicValueProviderClass<T>,
668
+ ): this {
521
669
  /* istanbul ignore if */
522
670
  if (debug.enabled) {
523
- debug('Bind %s to dynamic value:', this.key, factoryFn);
671
+ debug('Bind %s to dynamic value:', this.key, factory);
524
672
  }
525
- this._type = BindingType.DYNAMIC_VALUE;
526
- this._setValueGetter(ctx => factoryFn());
673
+ this._source = {
674
+ type: BindingType.DYNAMIC_VALUE,
675
+ value: factory,
676
+ };
677
+
678
+ let factoryFn: ValueFactory<T>;
679
+ if (isDynamicValueProviderClass(factory)) {
680
+ factoryFn = toValueFactory(factory);
681
+ } else {
682
+ factoryFn = factory;
683
+ }
684
+ this._setValueGetter(resolutionCtx => factoryFn(resolutionCtx));
527
685
  return this;
528
686
  }
529
687
 
@@ -548,12 +706,14 @@ export class Binding<T = BoundValue> extends EventEmitter {
548
706
  if (debug.enabled) {
549
707
  debug('Bind %s to provider %s', this.key, providerClass.name);
550
708
  }
551
- this._type = BindingType.PROVIDER;
552
- this._providerConstructor = providerClass;
553
- this._setValueGetter((ctx, options) => {
709
+ this._source = {
710
+ type: BindingType.PROVIDER,
711
+ value: providerClass,
712
+ };
713
+ this._setValueGetter(({context, options}) => {
554
714
  const providerOrPromise = instantiateClass<Provider<T>>(
555
715
  providerClass,
556
- ctx,
716
+ context,
557
717
  options.session,
558
718
  );
559
719
  return transformValueOrPromise(providerOrPromise, p => p.value());
@@ -573,17 +733,19 @@ export class Binding<T = BoundValue> extends EventEmitter {
573
733
  if (debug.enabled) {
574
734
  debug('Bind %s to class %s', this.key, ctor.name);
575
735
  }
576
- this._type = BindingType.CLASS;
577
- this._setValueGetter((ctx, options) => {
578
- const instOrPromise = instantiateClass(ctor, ctx, options.session);
736
+ this._source = {
737
+ type: BindingType.CLASS,
738
+ value: ctor,
739
+ };
740
+ this._setValueGetter(({context, options}) => {
741
+ const instOrPromise = instantiateClass(ctor, context, options.session);
579
742
  if (!options.asProxyWithInterceptors) return instOrPromise;
580
743
  return createInterceptionProxyFromInstance(
581
744
  instOrPromise,
582
- ctx,
745
+ context,
583
746
  options.session,
584
747
  );
585
748
  });
586
- this._valueConstructor = ctor;
587
749
  return this;
588
750
  }
589
751
 
@@ -597,10 +759,12 @@ export class Binding<T = BoundValue> extends EventEmitter {
597
759
  if (debug.enabled) {
598
760
  debug('Bind %s to alias %s', this.key, keyWithPath);
599
761
  }
600
- this._type = BindingType.ALIAS;
601
- this._alias = keyWithPath;
602
- this._setValueGetter((ctx, options) => {
603
- return ctx.getValueOrPromise(keyWithPath, options);
762
+ this._source = {
763
+ type: BindingType.ALIAS,
764
+ value: keyWithPath,
765
+ };
766
+ this._setValueGetter(({context, options}) => {
767
+ return context.getValueOrPromise(keyWithPath, options);
604
768
  });
605
769
  return this;
606
770
  }
@@ -647,14 +811,16 @@ export class Binding<T = BoundValue> extends EventEmitter {
647
811
  if (this.type != null) {
648
812
  json.type = this.type;
649
813
  }
650
- if (this._valueConstructor != null) {
651
- json.valueConstructor = this._valueConstructor.name;
652
- }
653
- if (this._providerConstructor != null) {
654
- json.providerConstructor = this._providerConstructor.name;
655
- }
656
- if (this._alias != null) {
657
- json.alias = this._alias.toString();
814
+ switch (this._source?.type) {
815
+ case BindingType.CLASS:
816
+ json.valueConstructor = this._source?.value.name;
817
+ break;
818
+ case BindingType.PROVIDER:
819
+ json.providerConstructor = this._source?.value.name;
820
+ break;
821
+ case BindingType.ALIAS:
822
+ json.alias = this._source?.value.toString();
823
+ break;
658
824
  }
659
825
  return json;
660
826
  }
package/src/context.ts CHANGED
@@ -5,7 +5,6 @@
5
5
 
6
6
  import debugFactory, {Debugger} from 'debug';
7
7
  import {EventEmitter} from 'events';
8
- import {v4 as uuidv4} from 'uuid';
9
8
  import {Binding, BindingInspectOptions, BindingTag} from './binding';
10
9
  import {
11
10
  ConfigurationResolver,
@@ -37,6 +36,7 @@ import {
37
36
  Constructor,
38
37
  getDeepProperty,
39
38
  isPromiseLike,
39
+ uuid,
40
40
  ValueOrPromise,
41
41
  } from './value-promise';
42
42
 
@@ -151,7 +151,7 @@ export class Context extends EventEmitter {
151
151
  }
152
152
 
153
153
  private generateName() {
154
- const id = uuidv4();
154
+ const id = uuid();
155
155
  if (this.constructor === Context) return id;
156
156
  return `${this.constructor.name}-${id}`;
157
157
  }
package/src/inject.ts CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  isBindingAddress,
21
21
  isBindingTagFilter,
22
22
  } from './binding-filter';
23
- import {BindingAddress} from './binding-key';
23
+ import {BindingAddress, BindingKey} from './binding-key';
24
24
  import {BindingComparator} from './binding-sorter';
25
25
  import {BindingCreationPolicy, Context} from './context';
26
26
  import {ContextView, createViewGetter} from './context-view';
@@ -40,8 +40,17 @@ const METHODS_KEY = MetadataAccessor.create<Injection, MethodDecorator>(
40
40
  'inject:methods',
41
41
  );
42
42
 
43
+ // TODO(rfeng): We may want to align it with `ValueFactory` interface that takes
44
+ // an argument of `ResolutionContext`.
43
45
  /**
44
- * A function to provide resolution of injected values
46
+ * A function to provide resolution of injected values.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const resolver: ResolverFunction = (ctx, injection, session) {
51
+ * return session.currentBinding?.key;
52
+ * }
53
+ * ```
45
54
  */
46
55
  export interface ResolverFunction {
47
56
  (
@@ -313,7 +322,7 @@ export namespace inject {
313
322
  * @param metadata - Metadata for the injection
314
323
  */
315
324
  export const binding = function injectBinding(
316
- bindingKey?: BindingAddress,
325
+ bindingKey?: string | BindingKey<unknown>,
317
326
  metadata?: InjectBindingMetadata,
318
327
  ) {
319
328
  metadata = Object.assign({decorator: '@inject.binding'}, metadata);
@@ -377,7 +386,7 @@ export namespace inject {
377
386
  * ```
378
387
  */
379
388
  export const context = function injectContext() {
380
- return inject('', {decorator: '@inject.context'}, ctx => ctx);
389
+ return inject('', {decorator: '@inject.context'}, (ctx: Context) => ctx);
381
390
  };
382
391
  }
383
392
 
@@ -604,22 +613,19 @@ export function describeInjectedArguments(
604
613
  * @param injection - Injection information
605
614
  */
606
615
  export function inspectTargetType(injection: Readonly<Injection>) {
607
- let type = MetadataInspector.getDesignTypeForProperty(
608
- injection.target,
609
- injection.member!,
610
- );
611
- if (type) {
612
- return type;
616
+ if (typeof injection.methodDescriptorOrParameterIndex === 'number') {
617
+ const designType = MetadataInspector.getDesignTypeForMethod(
618
+ injection.target,
619
+ injection.member!,
620
+ );
621
+ return designType.parameterTypes[
622
+ injection.methodDescriptorOrParameterIndex as number
623
+ ];
613
624
  }
614
- const designType = MetadataInspector.getDesignTypeForMethod(
625
+ return MetadataInspector.getDesignTypeForProperty(
615
626
  injection.target,
616
627
  injection.member!,
617
628
  );
618
- type =
619
- designType.parameterTypes[
620
- injection.methodDescriptorOrParameterIndex as number
621
- ];
622
- return type;
623
629
  }
624
630
 
625
631
  /**
@@ -12,28 +12,55 @@ import {InvocationResult} from './invocation';
12
12
  import {transformValueOrPromise, ValueOrPromise} from './value-promise';
13
13
  const debug = debugFactory('loopback:context:interceptor-chain');
14
14
 
15
+ /**
16
+ * Any type except `void`. We use this type to enforce that interceptor functions
17
+ * always return a value (including undefined or null).
18
+ */
19
+ export type NonVoid = string | number | boolean | null | undefined | object;
20
+
15
21
  /**
16
22
  * The `next` function that can be used to invoke next generic interceptor in
17
23
  * the chain
18
24
  */
19
- export type Next = () => ValueOrPromise<InvocationResult>;
25
+ export type Next = () => ValueOrPromise<NonVoid>;
20
26
 
21
27
  /**
22
28
  * An interceptor function to be invoked in a chain for the given context.
23
29
  * It serves as the base interface for various types of interceptors, such
24
30
  * as method invocation interceptor or request/response processing interceptor.
25
31
  *
32
+ * We choose `NonVoid` as the return type to avoid possible bugs that an
33
+ * interceptor forgets to return the value from `next()`. For example, the code
34
+ * below will fail to compile.
35
+ *
36
+ * ```ts
37
+ * const myInterceptor: Interceptor = async (ctx, next) {
38
+ * // preprocessing
39
+ * // ...
40
+ *
41
+ * // There is a subtle bug that the result from `next()` is not further
42
+ * // returned back to the upstream interceptors
43
+ * const result = await next();
44
+ *
45
+ * // postprocessing
46
+ * // ...
47
+ * // We must have `return ...` here
48
+ * // either return `result` or another value if the interceptor decides to
49
+ * // have its own response
50
+ * }
51
+ * ```
52
+ *
26
53
  * @typeParam C - `Context` class or a subclass of `Context`
27
54
  * @param context - Context object
28
55
  * @param next - A function to proceed with downstream interceptors or the
29
56
  * target operation
30
57
  *
31
- * @returns The invocation result as a value (sync) or promise (async)
58
+ * @returns The invocation result as a value (sync) or promise (async).
32
59
  */
33
60
  export type GenericInterceptor<C extends Context = Context> = (
34
61
  context: C,
35
62
  next: Next,
36
- ) => ValueOrPromise<InvocationResult>;
63
+ ) => ValueOrPromise<NonVoid>;
37
64
 
38
65
  /**
39
66
  * Interceptor function or a binding key that resolves a generic interceptor
@@ -53,8 +80,12 @@ class InterceptorChainState<C extends Context = Context> {
53
80
  /**
54
81
  * Create a state for the interceptor chain
55
82
  * @param interceptors - Interceptor functions or binding keys
83
+ * @param finalHandler - An optional final handler
56
84
  */
57
- constructor(private interceptors: GenericInterceptorOrKey<C>[]) {}
85
+ constructor(
86
+ public readonly interceptors: GenericInterceptorOrKey<C>[],
87
+ public readonly finalHandler: Next = () => undefined,
88
+ ) {}
58
89
 
59
90
  /**
60
91
  * Get the index for the current interceptor
@@ -138,12 +169,24 @@ export class GenericInterceptorChain<C extends Context = Context> {
138
169
  /**
139
170
  * Invoke the interceptor chain
140
171
  */
141
- invokeInterceptors(): ValueOrPromise<InvocationResult> {
172
+ invokeInterceptors(finalHandler?: Next): ValueOrPromise<InvocationResult> {
142
173
  // Create a state for each invocation to provide isolation
143
- const state = new InterceptorChainState<C>(this.getInterceptors());
174
+ const state = new InterceptorChainState<C>(
175
+ this.getInterceptors(),
176
+ finalHandler,
177
+ );
144
178
  return this.next(state);
145
179
  }
146
180
 
181
+ /**
182
+ * Use the interceptor chain as an interceptor
183
+ */
184
+ asInterceptor(): GenericInterceptor<C> {
185
+ return (ctx, next) => {
186
+ return this.invokeInterceptors(next);
187
+ };
188
+ }
189
+
147
190
  /**
148
191
  * Invoke downstream interceptors or the target method
149
192
  */
@@ -152,7 +195,7 @@ export class GenericInterceptorChain<C extends Context = Context> {
152
195
  ): ValueOrPromise<InvocationResult> {
153
196
  if (state.done()) {
154
197
  // No more interceptors
155
- return undefined;
198
+ return state.finalHandler();
156
199
  }
157
200
  // Invoke the next interceptor in the chain
158
201
  return this.invokeNextInterceptor(state);
@@ -206,3 +249,19 @@ export function invokeInterceptors<
206
249
  const chain = new GenericInterceptorChain(context, interceptors);
207
250
  return chain.invokeInterceptors();
208
251
  }
252
+
253
+ /**
254
+ * Compose a list of interceptors as a single interceptor
255
+ * @param interceptors - A list of interceptor functions or binding keys
256
+ */
257
+ export function composeInterceptors<C extends Context = Context>(
258
+ ...interceptors: GenericInterceptorOrKey<C>[]
259
+ ): GenericInterceptor<C> {
260
+ return (ctx, next) => {
261
+ const interceptor = new GenericInterceptorChain(
262
+ ctx,
263
+ interceptors,
264
+ ).asInterceptor();
265
+ return interceptor(ctx, next);
266
+ };
267
+ }