@bluelibs/runner 3.3.2 → 3.4.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 (76) hide show
  1. package/README.md +491 -74
  2. package/dist/define.d.ts +5 -5
  3. package/dist/define.js +22 -2
  4. package/dist/define.js.map +1 -1
  5. package/dist/defs.d.ts +55 -21
  6. package/dist/defs.js.map +1 -1
  7. package/dist/defs.returnTag.d.ts +36 -0
  8. package/dist/defs.returnTag.js +4 -0
  9. package/dist/defs.returnTag.js.map +1 -0
  10. package/dist/errors.d.ts +60 -10
  11. package/dist/errors.js +103 -12
  12. package/dist/errors.js.map +1 -1
  13. package/dist/globals/globalMiddleware.d.ts +4 -4
  14. package/dist/globals/globalResources.d.ts +28 -10
  15. package/dist/globals/middleware/cache.middleware.d.ts +9 -9
  16. package/dist/globals/resources/queue.resource.d.ts +5 -2
  17. package/dist/index.d.ts +33 -14
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/models/DependencyProcessor.js +4 -4
  21. package/dist/models/DependencyProcessor.js.map +1 -1
  22. package/dist/models/EventManager.js +10 -1
  23. package/dist/models/EventManager.js.map +1 -1
  24. package/dist/models/Logger.d.ts +8 -0
  25. package/dist/models/Logger.js +24 -0
  26. package/dist/models/Logger.js.map +1 -1
  27. package/dist/models/OverrideManager.js +1 -1
  28. package/dist/models/OverrideManager.js.map +1 -1
  29. package/dist/models/ResourceInitializer.d.ts +2 -2
  30. package/dist/models/ResourceInitializer.js.map +1 -1
  31. package/dist/models/Store.d.ts +5 -3
  32. package/dist/models/Store.js +7 -1
  33. package/dist/models/Store.js.map +1 -1
  34. package/dist/models/StoreConstants.d.ts +6 -3
  35. package/dist/models/StoreRegistry.d.ts +5 -3
  36. package/dist/models/StoreRegistry.js +17 -1
  37. package/dist/models/StoreRegistry.js.map +1 -1
  38. package/dist/models/StoreTypes.d.ts +1 -1
  39. package/dist/models/StoreValidator.js +5 -5
  40. package/dist/models/StoreValidator.js.map +1 -1
  41. package/dist/models/TaskRunner.js +10 -0
  42. package/dist/models/TaskRunner.js.map +1 -1
  43. package/dist/run.d.ts +3 -3
  44. package/dist/run.js +1 -1
  45. package/dist/run.js.map +1 -1
  46. package/dist/t1.d.ts +1 -0
  47. package/dist/t1.js +13 -0
  48. package/dist/t1.js.map +1 -0
  49. package/dist/testing.d.ts +1 -1
  50. package/package.json +2 -2
  51. package/src/__tests__/errors.test.ts +92 -11
  52. package/src/__tests__/models/EventManager.test.ts +0 -1
  53. package/src/__tests__/models/Logger.test.ts +82 -5
  54. package/src/__tests__/models/Store.test.ts +57 -0
  55. package/src/__tests__/recursion/c.resource.ts +1 -1
  56. package/src/__tests__/run.overrides.test.ts +3 -3
  57. package/src/__tests__/typesafety.test.ts +112 -9
  58. package/src/__tests__/validation-edge-cases.test.ts +111 -0
  59. package/src/__tests__/validation-interface.test.ts +428 -0
  60. package/src/define.ts +47 -15
  61. package/src/defs.returnTag.ts +91 -0
  62. package/src/defs.ts +84 -27
  63. package/src/errors.ts +95 -23
  64. package/src/index.ts +1 -0
  65. package/src/models/DependencyProcessor.ts +9 -5
  66. package/src/models/EventManager.ts +12 -3
  67. package/src/models/Logger.ts +28 -0
  68. package/src/models/OverrideManager.ts +2 -7
  69. package/src/models/ResourceInitializer.ts +8 -3
  70. package/src/models/Store.ts +12 -3
  71. package/src/models/StoreRegistry.ts +27 -2
  72. package/src/models/StoreTypes.ts +1 -1
  73. package/src/models/StoreValidator.ts +6 -6
  74. package/src/models/TaskRunner.ts +10 -1
  75. package/src/run.ts +8 -5
  76. package/src/testing.ts +1 -1
package/src/define.ts CHANGED
@@ -33,8 +33,10 @@ import {
33
33
  symbolResourceWithConfig,
34
34
  symbolResource,
35
35
  symbolMiddleware,
36
+ ITaskMeta,
37
+ IResourceMeta,
36
38
  } from "./defs";
37
- import { Errors } from "./errors";
39
+ import { MiddlewareAlreadyGlobalError, ValidationError } from "./errors";
38
40
  import { generateCallerIdFromFile, getCallerFile } from "./tools/getCallerFile";
39
41
 
40
42
  // Helper function to get the caller file
@@ -43,10 +45,11 @@ export function defineTask<
43
45
  Input = undefined,
44
46
  Output extends Promise<any> = any,
45
47
  Deps extends DependencyMapType = any,
46
- TOn extends "*" | IEventDefinition | undefined = undefined
48
+ TOn extends "*" | IEventDefinition | undefined = undefined,
49
+ TMeta extends ITaskMeta = any
47
50
  >(
48
- taskConfig: ITaskDefinition<Input, Output, Deps, TOn>
49
- ): ITask<Input, Output, Deps, TOn> {
51
+ taskConfig: ITaskDefinition<Input, Output, Deps, TOn, TMeta>
52
+ ): ITask<Input, Output, Deps, TOn, TMeta> {
50
53
  /**
51
54
  * Creates a task definition.
52
55
  * - Generates an anonymous id based on file path when `id` is omitted
@@ -65,6 +68,7 @@ export function defineTask<
65
68
  run: taskConfig.run,
66
69
  on: taskConfig.on,
67
70
  listenerOrder: taskConfig.listenerOrder,
71
+ inputSchema: taskConfig.inputSchema,
68
72
  events: {
69
73
  beforeRun: {
70
74
  ...defineEvent({
@@ -91,19 +95,28 @@ export function defineTask<
91
95
  [symbolFilePath]: getCallerFile(),
92
96
  },
93
97
  },
94
- meta: taskConfig.meta || {},
98
+ meta: taskConfig.meta || ({} as TMeta),
95
99
  // autorun,
96
100
  };
97
101
  }
98
102
 
99
103
  export function defineResource<
100
104
  TConfig = void,
101
- TValue = any,
105
+ TValue extends Promise<any> = Promise<any>,
102
106
  TDeps extends DependencyMapType = {},
103
- TPrivate = any
107
+ TPrivate = any,
108
+ TMeta extends IResourceMeta = any
104
109
  >(
105
- constConfig: IResourceDefinition<TConfig, TValue, TDeps, TPrivate>
106
- ): IResource<TConfig, TValue, TDeps, TPrivate> {
110
+ constConfig: IResourceDefinition<
111
+ TConfig,
112
+ TValue,
113
+ TDeps,
114
+ TPrivate,
115
+ any,
116
+ any,
117
+ TMeta
118
+ >
119
+ ): IResource<TConfig, TValue, TDeps, TPrivate, TMeta> {
107
120
  /**
108
121
  * Creates a resource definition.
109
122
  * - Generates anonymous id when omitted (resource or index flavor)
@@ -129,7 +142,17 @@ export function defineResource<
129
142
  overrides: constConfig.overrides || [],
130
143
  init: constConfig.init,
131
144
  context: constConfig.context,
145
+ configSchema: constConfig.configSchema,
132
146
  with: function (config: TConfig) {
147
+ // Validate config with schema if provided (fail fast)
148
+ if (this.configSchema) {
149
+ try {
150
+ config = this.configSchema.parse(config);
151
+ } catch (error) {
152
+ throw new ValidationError("Resource config", this.id, error instanceof Error ? error : new Error(String(error)));
153
+ }
154
+ }
155
+
133
156
  return {
134
157
  [symbolResourceWithConfig]: true,
135
158
  id: this.id,
@@ -164,7 +187,7 @@ export function defineResource<
164
187
  [symbolFilePath]: filePath,
165
188
  },
166
189
  },
167
- meta: constConfig.meta || {},
190
+ meta: (constConfig.meta || {}) as TMeta,
168
191
  middleware: constConfig.middleware || [],
169
192
  };
170
193
  }
@@ -182,7 +205,7 @@ export function defineIndex<
182
205
  ? T[K]["resource"]
183
206
  : T[K];
184
207
  } & DependencyMapType
185
- >(items: T): IResource<void, DependencyValuesType<D>, D> {
208
+ >(items: T): IResource<void, Promise<DependencyValuesType<D>>, D> {
186
209
  // Build dependency map from given items; unwrap `.with()` to the base resource
187
210
  const dependencies = {} as D;
188
211
  const register: RegisterableItems[] = [];
@@ -263,6 +286,15 @@ export function defineMiddleware<
263
286
  return {
264
287
  ...object,
265
288
  with: (config: TConfig) => {
289
+ // Validate config with schema if provided (fail fast)
290
+ if (object.configSchema) {
291
+ try {
292
+ config = object.configSchema.parse(config);
293
+ } catch (error) {
294
+ throw new ValidationError("Middleware config", object.id, error instanceof Error ? error : new Error(String(error)));
295
+ }
296
+ }
297
+
266
298
  return {
267
299
  ...object,
268
300
  [symbolMiddlewareConfigured]: true,
@@ -280,7 +312,7 @@ export function defineMiddleware<
280
312
  [symbolMiddlewareEverywhereTasks]: tasks,
281
313
  [symbolMiddlewareEverywhereResources]: resources,
282
314
  everywhere() {
283
- throw Errors.middlewareAlreadyGlobal(object.id);
315
+ throw new MiddlewareAlreadyGlobalError(object.id);
284
316
  },
285
317
  };
286
318
  },
@@ -343,9 +375,9 @@ export function defineOverride(
343
375
  * - `.with(config)` to create configured instances
344
376
  * - `.extract(tags)` to extract this tag from a list of tags
345
377
  */
346
- export function defineTag<TConfig = void>(
347
- definition: ITagDefinition<TConfig>
348
- ): ITag<TConfig> {
378
+ export function defineTag<TConfig = void, TEnforceContract = void>(
379
+ definition: ITagDefinition<TConfig, TEnforceContract>
380
+ ): ITag<TConfig, TEnforceContract> {
349
381
  const id = definition.id;
350
382
 
351
383
  return {
@@ -0,0 +1,91 @@
1
+ // HasContracts<Meta> → true if contracts present, else false
2
+
3
+ import { ITag, ITagWithConfig } from "./defs";
4
+ import { IMeta } from "./defs";
5
+
6
+ // Keep these param names aligned with your defs.ts: ITag<TConfig, TEnforceContract>
7
+ type NonVoid<T> = [T] extends [void] ? never : T;
8
+
9
+ type ExtractReturnFromTag<T> = T extends ITagWithConfig<any, infer R>
10
+ ? NonVoid<R>
11
+ : T extends ITag<any, infer R>
12
+ ? NonVoid<R>
13
+ : never;
14
+
15
+ type IsTuple<T extends readonly unknown[]> = number extends T["length"]
16
+ ? false
17
+ : true;
18
+
19
+ type FilterContracts<
20
+ TTags extends readonly unknown[],
21
+ Acc extends readonly unknown[] = []
22
+ > = TTags extends readonly [infer H, ...infer R]
23
+ ? ExtractReturnFromTag<H> extends never
24
+ ? FilterContracts<R, Acc>
25
+ : FilterContracts<R, [...Acc, ExtractReturnFromTag<H>]>
26
+ : Acc;
27
+
28
+ export type ExtractContractsFromTags<TTags extends readonly unknown[]> =
29
+ IsTuple<TTags> extends true
30
+ ? FilterContracts<TTags>
31
+ : Array<ExtractReturnFromTag<TTags[number]>>;
32
+
33
+ export type ExtractTagsWithNonVoidReturnTypeFromMeta<TMeta extends IMeta> =
34
+ TMeta extends { tags?: infer TTags }
35
+ ? TTags extends readonly unknown[]
36
+ ? ExtractContractsFromTags<TTags>
37
+ : []
38
+ : [];
39
+
40
+ type IsNeverTuple<T extends readonly unknown[]> = T extends [] ? true : false;
41
+
42
+ export type HasContracts<T extends IMeta> =
43
+ ExtractTagsWithNonVoidReturnTypeFromMeta<T> extends never[] ? false : true; // HasContracts and enforcement
44
+
45
+ // Ensure a response type satisfies ALL contracts (intersection)
46
+ type UnionToIntersection<U> = (
47
+ U extends any ? (arg: U) => void : never
48
+ ) extends (arg: infer I) => void
49
+ ? I
50
+ : never;
51
+
52
+ type ContractsUnion<TMeta extends IMeta> =
53
+ ExtractTagsWithNonVoidReturnTypeFromMeta<TMeta> extends readonly (infer U)[]
54
+ ? U
55
+ : never;
56
+
57
+ type ContractsIntersection<TMeta extends IMeta> = UnionToIntersection<
58
+ ContractsUnion<TMeta>
59
+ >;
60
+
61
+ /**
62
+ * Pretty-print helper to expand intersections for better IDE display.
63
+ */
64
+ type Simplify<T> = { [K in keyof T]: T[K] } & {};
65
+
66
+ /**
67
+ * Verbose compile-time error surfaced when a value does not satisfy
68
+ * the intersection of all tag-enforced contracts.
69
+ *
70
+ * Intersected with `never` in call sites when desired to ensure assignment
71
+ * fails while still surfacing a readable shape in tooltips.
72
+ */
73
+ export type ContractViolationError<TMeta extends IMeta, TActual> = {
74
+ message: "Value does not satisfy all tag contracts";
75
+ expected: Simplify<ContractsIntersection<TMeta>>;
76
+ received: TActual;
77
+ };
78
+
79
+ export type EnsureResponseSatisfiesContracts<TMeta extends IMeta, TResponse> = [
80
+ ContractsUnion<TMeta>
81
+ ] extends [never]
82
+ ? TResponse // no contracts, allow as-is
83
+ : TResponse extends Promise<infer U>
84
+ ? Promise<
85
+ U extends ContractsIntersection<TMeta>
86
+ ? U
87
+ : ContractViolationError<TMeta, U>
88
+ >
89
+ : TResponse extends ContractsIntersection<TMeta>
90
+ ? TResponse
91
+ : ContractViolationError<TMeta, TResponse>;
package/src/defs.ts CHANGED
@@ -15,6 +15,23 @@
15
15
  */
16
16
 
17
17
  import { MiddlewareEverywhereOptions } from "./define";
18
+ import {
19
+ EnsureResponseSatisfiesContracts,
20
+ HasContracts,
21
+ } from "./defs.returnTag";
22
+
23
+ /**
24
+ * Generic validation schema interface that can be implemented by any validation library.
25
+ * Compatible with Zod, Yup, Joi, and other validation libraries.
26
+ */
27
+ export interface IValidationSchema<T = any> {
28
+ /**
29
+ * Parse and validate the input data.
30
+ * Should throw an error if validation fails.
31
+ * Can transform the data if the schema supports transformations.
32
+ */
33
+ parse(input: unknown): T;
34
+ }
18
35
 
19
36
  // Re-export public cache type so consumers don’t import from internals.
20
37
  export { ICacheInstance } from "./globals/middleware/cache.middleware";
@@ -57,17 +74,17 @@ export const symbolIndexResource: unique symbol = Symbol(
57
74
  "runner.indexResource"
58
75
  );
59
76
 
60
- export interface ITagDefinition<TConfig = void> {
77
+ export interface ITagDefinition<TConfig = void, TEnforceContract = void> {
61
78
  id: string | symbol;
62
79
  }
63
80
 
64
81
  /**
65
82
  * A configured instance of a tag as produced by `ITag.with()`.
66
83
  */
67
- export interface ITagWithConfig<TConfig = void> {
84
+ export interface ITagWithConfig<TConfig = void, TEnforceContract = void> {
68
85
  id: string | symbol;
69
86
  /** The tag definition used to produce this configured instance. */
70
- tag: ITag<TConfig>;
87
+ tag: ITag<TConfig, TEnforceContract>;
71
88
  /** The configuration captured for this tag instance. */
72
89
  config: TConfig;
73
90
  }
@@ -76,16 +93,19 @@ export interface ITagWithConfig<TConfig = void> {
76
93
  * A tag definition (builder). Use `.with(config)` to obtain configured instances,
77
94
  * and `.extract(tags)` to find either a configured instance or the bare tag in a list.
78
95
  */
79
- export interface ITag<TConfig = void> extends ITagDefinition<TConfig> {
96
+ export interface ITag<TConfig = void, TEnforceContract = void>
97
+ extends ITagDefinition<TConfig, TEnforceContract> {
80
98
  /**
81
99
  * Creates a configured instance of the tag.
82
100
  */
83
- with(config: TConfig): ITagWithConfig<TConfig>;
101
+ with(config: TConfig): ITagWithConfig<TConfig, TEnforceContract>;
84
102
  /**
85
103
  * Extracts either a configured instance or the bare tag from a list of tags
86
104
  * or from a taggable object (`{ meta: { tags?: [] } }`).
87
105
  */
88
- extract(target: TagType[] | ITaggable): ExtractedTagResult<TConfig> | null;
106
+ extract(
107
+ target: TagType[] | ITaggable
108
+ ): ExtractedTagResult<TConfig, TEnforceContract> | null;
89
109
  [symbolFilePath]: string;
90
110
  }
91
111
 
@@ -96,9 +116,9 @@ export interface ITag<TConfig = void> extends ITagDefinition<TConfig> {
96
116
  */
97
117
  export type TagType =
98
118
  | string
99
- | ITag<void>
100
- | ITag<{ [K in any]?: any }>
101
- | ITagWithConfig<any>;
119
+ | ITag<void, any>
120
+ | ITag<{ [K in any]?: any }, any>
121
+ | ITagWithConfig<any, any>;
102
122
 
103
123
  /**
104
124
  * Conditional result type for `ITag.extract`:
@@ -106,7 +126,7 @@ export type TagType =
106
126
  * - For optional object config → identifier with optional config
107
127
  * - For required config → identifier with required config
108
128
  */
109
- export type ExtractedTagResult<TConfig> = {} extends TConfig
129
+ export type ExtractedTagResult<TConfig, TEnforceContract> = {} extends TConfig
110
130
  ? { id: string | symbol; config?: TConfig }
111
131
  : { id: string | symbol; config: TConfig };
112
132
 
@@ -119,7 +139,6 @@ export interface ITaggable {
119
139
  tags?: TagType[];
120
140
  };
121
141
  }
122
-
123
142
  /**
124
143
  * Common metadata you can attach to tasks/resources/events/middleware.
125
144
  * Useful for docs, filtering and middleware decisions.
@@ -149,7 +168,9 @@ export type DependencyMapType = Record<
149
168
  type ExtractTaskInput<T> = T extends ITask<infer I, any, infer D> ? I : never;
150
169
  type ExtractTaskOutput<T> = T extends ITask<any, infer O, infer D> ? O : never;
151
170
  type ExtractResourceValue<T> = T extends IResource<any, infer V, infer D>
152
- ? V
171
+ ? V extends Promise<infer U>
172
+ ? U
173
+ : V
153
174
  : never;
154
175
 
155
176
  type ExtractEventParams<T> = T extends IEvent<infer P> ? P : never;
@@ -213,7 +234,8 @@ export interface ITaskDefinition<
213
234
  TInput = any,
214
235
  TOutput extends Promise<any> = any,
215
236
  TDependencies extends DependencyMapType = {},
216
- TOn extends "*" | IEventDefinition<any> | undefined = undefined // Adding a generic to track 'on' type
237
+ TOn extends "*" | IEventDefinition<any> | undefined = undefined, // Adding a generic to track 'on' type,
238
+ TMeta extends ITaskMeta = any
217
239
  > {
218
240
  /**
219
241
  * Stable identifier. If omitted, an anonymous id is generated from file path
@@ -237,7 +259,12 @@ export interface ITaskDefinition<
237
259
  */
238
260
  listenerOrder?: number;
239
261
  /** Optional metadata used for docs, filtering and tooling. */
240
- meta?: ITaskMeta;
262
+ meta?: TMeta;
263
+ /**
264
+ * Optional validation schema for runtime input validation.
265
+ * When provided, task input will be validated before execution.
266
+ */
267
+ inputSchema?: IValidationSchema<TInput>;
241
268
  /**
242
269
  * The task body. If `on` is set, the input is an `IEventEmission`. Otherwise,
243
270
  * it's the declared input type.
@@ -247,7 +274,9 @@ export interface ITaskDefinition<
247
274
  ? TInput
248
275
  : IEventEmission<TOn extends "*" ? any : ExtractEventParams<TOn>>,
249
276
  dependencies: DependencyValuesType<TDependencies>
250
- ) => TOutput;
277
+ ) => HasContracts<TMeta> extends true
278
+ ? EnsureResponseSatisfiesContracts<TMeta, TOutput>
279
+ : TOutput;
251
280
  }
252
281
 
253
282
  export type BeforeRunEventPayload<TInput> = {
@@ -284,8 +313,9 @@ export interface ITask<
284
313
  TInput = any,
285
314
  TOutput extends Promise<any> = any,
286
315
  TDependencies extends DependencyMapType = {},
287
- TOn extends "*" | IEventDefinition<any> | undefined = undefined
288
- > extends ITaskDefinition<TInput, TOutput, TDependencies, TOn> {
316
+ TOn extends "*" | IEventDefinition<any> | undefined = undefined,
317
+ TMeta extends ITaskMeta = any
318
+ > extends ITaskDefinition<TInput, TOutput, TDependencies, TOn, TMeta> {
289
319
  id: string | symbol;
290
320
  dependencies: TDependencies | (() => TDependencies);
291
321
  computedDependencies?: DependencyValuesType<TDependencies>;
@@ -304,11 +334,12 @@ export interface ITask<
304
334
 
305
335
  export interface IResourceDefinition<
306
336
  TConfig = any,
307
- TValue = unknown,
337
+ TValue extends Promise<any> = Promise<any>,
308
338
  TDependencies extends DependencyMapType = {},
309
339
  TContext = any,
310
340
  THooks = any,
311
- TRegisterableItems = any
341
+ TRegisterableItems = any,
342
+ TMeta extends IResourceMeta = any
312
343
  > {
313
344
  /** Stable identifier. Omit to get an anonymous id. */
314
345
  id?: string | symbol;
@@ -329,7 +360,9 @@ export interface IResourceDefinition<
329
360
  config: TConfig,
330
361
  dependencies: DependencyValuesType<TDependencies>,
331
362
  context: TContext
332
- ) => Promise<TValue>;
363
+ ) => HasContracts<TMeta> extends true
364
+ ? EnsureResponseSatisfiesContracts<TMeta, TValue>
365
+ : TValue;
333
366
  /**
334
367
  * Clean-up function for the resource. This is called when the resource is no longer needed.
335
368
  *
@@ -340,12 +373,17 @@ export interface IResourceDefinition<
340
373
  */
341
374
  dispose?: (
342
375
  this: any,
343
- value: TValue,
376
+ value: TValue extends Promise<infer U> ? U : TValue,
344
377
  config: TConfig,
345
378
  dependencies: DependencyValuesType<TDependencies>,
346
379
  context: TContext
347
380
  ) => Promise<void>;
348
- meta?: IResourceMeta;
381
+ meta?: TMeta;
382
+ /**
383
+ * Optional validation schema for runtime config validation.
384
+ * When provided, resource config will be validated when .with() is called.
385
+ */
386
+ configSchema?: IValidationSchema<TConfig>;
349
387
  /**
350
388
  * Safe overrides to swap behavior while preserving identities. See
351
389
  * README: Overrides.
@@ -370,10 +408,19 @@ export interface IResourceDefinition<
370
408
 
371
409
  export interface IResource<
372
410
  TConfig = void,
373
- TValue = any,
411
+ TValue extends Promise<any> = Promise<any>,
374
412
  TDependencies extends DependencyMapType = any,
375
- TContext = any
376
- > extends IResourceDefinition<TConfig, TValue, TDependencies, TContext> {
413
+ TContext = any,
414
+ TMeta extends IResourceMeta = any
415
+ > extends IResourceDefinition<
416
+ TConfig,
417
+ TValue,
418
+ TDependencies,
419
+ TContext,
420
+ any,
421
+ any,
422
+ TMeta
423
+ > {
377
424
  id: string | symbol;
378
425
  with(config: TConfig): IResourceWithConfig<TConfig, TValue, TDependencies>;
379
426
  register:
@@ -396,7 +443,7 @@ export interface IResource<
396
443
 
397
444
  export interface IResourceWithConfig<
398
445
  TConfig = any,
399
- TValue = any,
446
+ TValue extends Promise<any> = Promise<any>,
400
447
  TDependencies extends DependencyMapType = any
401
448
  > {
402
449
  /** The id of the underlying resource. */
@@ -415,6 +462,11 @@ export interface IEventDefinition<TPayload = void> {
415
462
  /** Stable identifier. Omit to get an anonymous id. */
416
463
  id?: string | symbol;
417
464
  meta?: IEventMeta;
465
+ /**
466
+ * Optional validation schema for runtime payload validation.
467
+ * When provided, event payload will be validated when emitted.
468
+ */
469
+ payloadSchema?: IValidationSchema<TPayload>;
418
470
  }
419
471
 
420
472
  export interface IEvent<TPayload = any> extends IEventDefinition<TPayload> {
@@ -469,6 +521,11 @@ export interface IMiddlewareDefinition<
469
521
  id?: string | symbol;
470
522
  /** Static or lazy dependency map. */
471
523
  dependencies?: TDependencies | ((config: TConfig) => TDependencies);
524
+ /**
525
+ * Optional validation schema for runtime config validation.
526
+ * When provided, middleware config will be validated when .with() is called.
527
+ */
528
+ configSchema?: IValidationSchema<TConfig>;
472
529
  /**
473
530
  * The middleware body, called with task/resource execution input.
474
531
  */
@@ -530,7 +587,7 @@ export interface IMiddlewareExecutionInput<
530
587
  };
531
588
  /** Resource hook: present when wrapping init/dispose. */
532
589
  resource?: {
533
- definition: IResource<TResourceConfig>;
590
+ definition: IResource<TResourceConfig, any, any, any, any>;
534
591
  config: TResourceConfig;
535
592
  };
536
593
  next: (
package/src/errors.ts CHANGED
@@ -1,33 +1,105 @@
1
- import { ITask, IResource } from "./defs";
1
+ /**
2
+ * Base error class for all BlueLibs Runner errors
3
+ */
4
+ export class RuntimeError extends Error {
5
+ constructor(message: string) {
6
+ super(message);
7
+ this.name = "RuntimeError";
8
+ }
9
+ }
2
10
 
3
- export const Errors = {
4
- duplicateRegistration: (type: string, id: string | symbol) =>
5
- new Error(`${type} "${id.toString()}" already registered`),
11
+ /**
12
+ * Error thrown when attempting to register a component with a duplicate ID
13
+ */
14
+ export class DuplicateRegistrationError extends RuntimeError {
15
+ constructor(type: string, id: string | symbol) {
16
+ super(`${type} "${id.toString()}" already registered`);
17
+ this.name = "DuplicateRegistrationError";
18
+ }
19
+ }
6
20
 
7
- dependencyNotFound: (key: string | symbol) =>
8
- new Error(
21
+ /**
22
+ * Error thrown when a dependency is not found in the registry
23
+ */
24
+ export class DependencyNotFoundError extends RuntimeError {
25
+ constructor(key: string | symbol) {
26
+ super(
9
27
  `Dependency ${key.toString()} not found. Did you forget to register it through a resource?`
10
- ),
28
+ );
29
+ this.name = "DependencyNotFoundError";
30
+ }
31
+ }
11
32
 
12
- unknownItemType: (item: any) => new Error(`Unknown item type: ${item}`),
33
+ /**
34
+ * Error thrown when an unknown item type is encountered
35
+ */
36
+ export class UnknownItemTypeError extends RuntimeError {
37
+ constructor(item: any) {
38
+ super(`Unknown item type: ${item}`);
39
+ this.name = "UnknownItemTypeError";
40
+ }
41
+ }
13
42
 
14
- circularDependencies: (cycles: string[]) =>
15
- new Error(`Circular dependencies detected: ${cycles.join(", ")}`),
43
+ /**
44
+ * Error thrown when circular dependencies are detected
45
+ */
46
+ export class CircularDependenciesError extends RuntimeError {
47
+ constructor(cycles: string[]) {
48
+ super(`Circular dependencies detected: ${cycles.join(", ")}`);
49
+ this.name = "CircularDependenciesError";
50
+ }
51
+ }
16
52
 
17
- eventNotFound: (id: string | symbol) =>
18
- new Error(
19
- `Event "${id.toString()}" not found. Did you forget to register it?`
20
- ),
53
+ /**
54
+ * Error thrown when an event is not found in the registry
55
+ */
56
+ export class EventNotFoundError extends RuntimeError {
57
+ constructor(id: string | symbol) {
58
+ super(`Event "${id.toString()}" not found. Did you forget to register it?`);
59
+ this.name = "EventNotFoundError";
60
+ }
61
+ }
21
62
 
22
- middlewareAlreadyGlobal: (id: string | symbol) =>
23
- new Error(
63
+ /**
64
+ * Error thrown when attempting to make a middleware global when it's already global
65
+ */
66
+ export class MiddlewareAlreadyGlobalError extends RuntimeError {
67
+ constructor(id: string | symbol) {
68
+ super(
24
69
  "Cannot call .everywhere() on an already global middleware: " +
25
- id.toString
26
- ),
70
+ id.toString()
71
+ );
72
+ this.name = "MiddlewareAlreadyGlobalError";
73
+ }
74
+ }
27
75
 
28
- locked: (what: string | symbol) =>
29
- new Error(`Cannot modify the ${what.toString()} when it is locked.`),
76
+ /**
77
+ * Error thrown when attempting to modify a locked component
78
+ */
79
+ export class LockedError extends RuntimeError {
80
+ constructor(what: string | symbol) {
81
+ super(`Cannot modify the ${what.toString()} when it is locked.`);
82
+ this.name = "LockedError";
83
+ }
84
+ }
30
85
 
31
- storeAlreadyInitialized: () =>
32
- new Error("Store already initialized. Cannot reinitialize."),
33
- };
86
+ /**
87
+ * Error thrown when attempting to initialize a store that's already initialized
88
+ */
89
+ export class StoreAlreadyInitializedError extends RuntimeError {
90
+ constructor() {
91
+ super("Store already initialized. Cannot reinitialize.");
92
+ this.name = "StoreAlreadyInitializedError";
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Error thrown when validation fails for task input, resource config, middleware config, or event payload
98
+ */
99
+ export class ValidationError extends RuntimeError {
100
+ constructor(type: string, id: string | symbol, originalError: Error | string) {
101
+ const errorMessage = originalError instanceof Error ? originalError.message : String(originalError);
102
+ super(`${type} validation failed for ${id.toString()}: ${errorMessage}`);
103
+ this.name = "ValidationError";
104
+ }
105
+ }
package/src/index.ts CHANGED
@@ -36,3 +36,4 @@ export {
36
36
 
37
37
  export * as definitions from "./defs";
38
38
  export { Semaphore, Store, EventManager, TaskRunner, Queue } from "./models";
39
+ export * as Errors from "./errors";
@@ -13,7 +13,11 @@ import * as utils from "../define";
13
13
  import { EventManager } from "./EventManager";
14
14
  import { ResourceInitializer } from "./ResourceInitializer";
15
15
  import { TaskRunner } from "./TaskRunner";
16
- import { Errors } from "../errors";
16
+ import {
17
+ DependencyNotFoundError,
18
+ EventNotFoundError,
19
+ UnknownItemTypeError,
20
+ } from "../errors";
17
21
  import { Logger } from "./Logger";
18
22
 
19
23
  /**
@@ -148,7 +152,7 @@ export class DependencyProcessor {
148
152
  });
149
153
  } else {
150
154
  if (this.store.events.get(eventDefinition.id) === undefined) {
151
- throw Errors.eventNotFound(eventDefinition.id);
155
+ throw new EventNotFoundError(eventDefinition.id);
152
156
  }
153
157
  this.eventManager.addListener(eventDefinition, handler, {
154
158
  order: task.task.listenerOrder || 0,
@@ -179,7 +183,7 @@ export class DependencyProcessor {
179
183
  } else if (utils.isEvent(object)) {
180
184
  return this.extractEventDependency(object, source);
181
185
  } else {
182
- throw Errors.unknownItemType(object);
186
+ throw new UnknownItemTypeError(object);
183
187
  }
184
188
  }
185
189
 
@@ -197,7 +201,7 @@ export class DependencyProcessor {
197
201
  async extractTaskDependency(object: ITask<any, any, {}>) {
198
202
  const storeTask = this.store.tasks.get(object.id);
199
203
  if (storeTask === undefined) {
200
- throw Errors.dependencyNotFound(`Task ${object.id.toString()}`);
204
+ throw new DependencyNotFoundError(`Task ${object.id.toString()}`);
201
205
  }
202
206
 
203
207
  if (!storeTask.isInitialized) {
@@ -221,7 +225,7 @@ export class DependencyProcessor {
221
225
  // check if it exists in the store with the value
222
226
  const storeResource = this.store.resources.get(object.id);
223
227
  if (storeResource === undefined) {
224
- throw Errors.dependencyNotFound(`Resource ${object.id.toString()}`);
228
+ throw new DependencyNotFoundError(`Resource ${object.id.toString()}`);
225
229
  }
226
230
 
227
231
  const { resource, config } = storeResource;