1
0

Mixins.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. /**
  2. * @module lib/Mixins
  3. * Allows for defining and applying mixin functions to allow multiple sources to modify a value in a controlled way.
  4. */
  5. /* eslint-disable @typescript-eslint/no-explicit-any */
  6. /** Full mixin object as it is stored in the instance's mixin array. */
  7. export type MixinObj<TArg, TCtx> = {
  8. /** The public identifier key (purpose) of the mixin */
  9. key: string;
  10. /** The mixin function */
  11. fn: (arg: TArg, ctx?: TCtx) => TArg;
  12. } & MixinConfig;
  13. /** Configuration object for a mixin function */
  14. export type MixinConfig = {
  15. /** The higher, the earlier the mixin will be applied. Supports negative numbers too. 0 by default. */
  16. priority: number;
  17. /** If true, no further mixins will be applied after this one. */
  18. stopPropagation: boolean;
  19. /** If set, the mixin will only be applied if the given signal is not aborted. */
  20. signal?: AbortSignal;
  21. }
  22. /**
  23. * The mixin class allows for defining and applying mixin functions to allow multiple sources to modify values in a controlled way.
  24. * Mixins are identified via their string key and can be added with {@linkcode add()}
  25. * When calling {@linkcode resolve()}, all registered mixin functions with the same key will be applied to the input value in the order of their priority.
  26. * If a mixin has the stopPropagation flag set to true, no further mixins will be applied after it.
  27. *
  28. * @example ```ts
  29. * const mathMixins = new Mixins<{
  30. * foo: (val: number) => number;
  31. * bar: (val: string, ctx: { baz: number }) => string;
  32. * }>();
  33. *
  34. * // will be applied second due to base priority of 0:
  35. * mathMixins.add("foo", (val) => val * 2);
  36. * // this mixin will be applied first, even though the above one was called first:
  37. * mathMixins.add("foo", (val) => val + 1, { priority: 1 });
  38. *
  39. * function getFoo() {
  40. * // 1. start with 5 as the base value
  41. * // 2. add 1
  42. * // 3. multiply by 2
  43. * // result: 12
  44. * return mathMixins.resolve("foo", 5);
  45. * }
  46. *
  47. *
  48. * mathMixins.add("bar", (val, ctx) => `${val} ${btoa(ctx.baz)}`);
  49. * mathMixins.add("bar", (val, ctx) => `${val}!`);
  50. *
  51. * function getBar() {
  52. * // 1. start with "Hello" as the base value
  53. * // 2. append base64-encoded value in ctx.baz
  54. * // 3. append "!"
  55. * // result: "Hello d29ybGQ=!"
  56. * return mathMixins.resolve("bar", "Hello", { baz: "world" });
  57. * }
  58. * ```
  59. */
  60. export class Mixins<TMixinMap extends Record<string, (arg: any, ctx?: any) => any>> {
  61. protected mixins: MixinObj<any, any>[] = [];
  62. protected readonly defaultConfig: MixinConfig;
  63. /**
  64. * Creates a new Mixins instance.
  65. * @param defaultConfigOverrides An object to override the default configuration values for all mixin functions.
  66. */
  67. constructor(defaultConfigOverrides: Partial<MixinConfig> = {}) {
  68. this.defaultConfig = {
  69. priority: 0,
  70. stopPropagation: false,
  71. ...defaultConfigOverrides,
  72. };
  73. }
  74. /**
  75. * Adds a modifier function to the mixin with the given {@linkcode mixinKey}.
  76. * @param mixinKey The key to identify the mixin function.
  77. * @param mixinFn The function to be called to apply the mixin.
  78. * @param config Configuration object to customize the mixin behavior.
  79. * @returns Returns a cleanup function, to be called when this mixin is no longer needed.
  80. */
  81. public add<
  82. TMixinKey extends string,
  83. TArg extends Parameters<TMixinMap[TMixinKey]>[0],
  84. TCtx extends Parameters<TMixinMap[TMixinKey]>[1],
  85. >(
  86. mixinKey: TMixinKey,
  87. mixinFn: (arg: TArg, ...ctx: TCtx extends undefined ? [void] : [TCtx]) => TArg,
  88. config: Partial<MixinConfig> = {}
  89. ): () => void {
  90. const mixin = {
  91. key: mixinKey as string,
  92. fn: mixinFn,
  93. ...this.defaultConfig,
  94. ...config,
  95. } as MixinObj<TArg, TCtx>;
  96. this.mixins.push(mixin);
  97. const clean = (): void => {
  98. this.mixins = this.mixins.filter((m) => m !== mixin);
  99. };
  100. config.signal?.addEventListener("abort", clean, { once: true });
  101. return clean;
  102. }
  103. /** Returns a list of all added mixins with their keys and configuration objects, but not their functions */
  104. public list(): ({ key: string; } & MixinConfig)[] {
  105. return this.mixins.map(({ fn: _f, ...rest }) => rest);
  106. }
  107. /**
  108. * Applies all mixins with the given key to the input value, respecting the priority and stopPropagation settings.
  109. * If additional context is set in the MixinMap, it will need to be passed as the third argument.
  110. * @returns The modified value after all mixins have been applied.
  111. */
  112. public resolve<
  113. TMixinKey extends keyof TMixinMap,
  114. TArg extends Parameters<TMixinMap[TMixinKey]>[0],
  115. TCtx extends Parameters<TMixinMap[TMixinKey]>[1],
  116. >(
  117. mixinKey: TMixinKey,
  118. inputValue: TArg,
  119. ...inputCtx: TCtx extends undefined ? [void] : [TCtx]
  120. ): TArg {
  121. const mixins = this.mixins.filter((m) => m.key === mixinKey);
  122. const sortedMixins = mixins.sort((a, b) => a.priority - b.priority);
  123. let result = inputValue;
  124. for(const mixin of sortedMixins) {
  125. result = mixin.fn(result, ...inputCtx);
  126. if(mixin.stopPropagation)
  127. break;
  128. }
  129. return result;
  130. }
  131. /** Removes all mixins with the given key */
  132. protected removeAll<TMixinKey extends keyof TMixinMap>(mixinKey: TMixinKey): void {
  133. this.mixins.filter((m) => m.key === mixinKey);
  134. this.mixins = this.mixins.filter((m) => m.key !== mixinKey);
  135. }
  136. }