@bluelibs/runner 3.2.0 → 3.3.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 (95) hide show
  1. package/README.md +482 -34
  2. package/dist/cli/extract-docs.d.ts +2 -0
  3. package/dist/cli/extract-docs.js +88 -0
  4. package/dist/cli/extract-docs.js.map +1 -0
  5. package/dist/define.d.ts +21 -1
  6. package/dist/define.js +95 -23
  7. package/dist/define.js.map +1 -1
  8. package/dist/defs/core.d.ts +144 -0
  9. package/dist/defs/core.js +6 -0
  10. package/dist/defs/core.js.map +1 -0
  11. package/dist/defs/symbols.d.ts +42 -0
  12. package/dist/defs/symbols.js +45 -0
  13. package/dist/defs/symbols.js.map +1 -0
  14. package/dist/defs/tags.d.ts +70 -0
  15. package/dist/defs/tags.js +6 -0
  16. package/dist/defs/tags.js.map +1 -0
  17. package/dist/defs.d.ts +168 -16
  18. package/dist/defs.js +41 -14
  19. package/dist/defs.js.map +1 -1
  20. package/dist/docs/introspect.d.ts +7 -0
  21. package/dist/docs/introspect.js +199 -0
  22. package/dist/docs/introspect.js.map +1 -0
  23. package/dist/docs/markdown.d.ts +2 -0
  24. package/dist/docs/markdown.js +148 -0
  25. package/dist/docs/markdown.js.map +1 -0
  26. package/dist/docs/model.d.ts +62 -0
  27. package/dist/docs/model.js +33 -0
  28. package/dist/docs/model.js.map +1 -0
  29. package/dist/express/docsRouter.d.ts +12 -0
  30. package/dist/express/docsRouter.js +54 -0
  31. package/dist/express/docsRouter.js.map +1 -0
  32. package/dist/globals/globalMiddleware.d.ts +1 -0
  33. package/dist/globals/globalMiddleware.js +2 -0
  34. package/dist/globals/globalMiddleware.js.map +1 -1
  35. package/dist/globals/middleware/timeout.middleware.d.ts +8 -0
  36. package/dist/globals/middleware/timeout.middleware.js +35 -0
  37. package/dist/globals/middleware/timeout.middleware.js.map +1 -0
  38. package/dist/index.d.ts +4 -2
  39. package/dist/index.js +5 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/models/DependencyProcessor.js +2 -2
  42. package/dist/models/DependencyProcessor.js.map +1 -1
  43. package/dist/models/Store.d.ts +1 -1
  44. package/dist/models/StoreConstants.d.ts +1 -1
  45. package/dist/models/StoreConstants.js +2 -1
  46. package/dist/models/StoreConstants.js.map +1 -1
  47. package/dist/models/TaskRunner.d.ts +2 -3
  48. package/dist/models/TaskRunner.js +1 -2
  49. package/dist/models/TaskRunner.js.map +1 -1
  50. package/dist/testing.d.ts +24 -0
  51. package/dist/testing.js +41 -0
  52. package/dist/testing.js.map +1 -0
  53. package/dist/types/dependencies.d.ts +47 -18
  54. package/dist/types/event.d.ts +49 -0
  55. package/dist/types/event.js +4 -0
  56. package/dist/types/event.js.map +1 -0
  57. package/dist/types/index.d.ts +4 -10
  58. package/dist/types/index.js +8 -7
  59. package/dist/types/index.js.map +1 -1
  60. package/dist/types/metadata.d.ts +75 -0
  61. package/dist/types/metadata.js +3 -0
  62. package/dist/types/metadata.js.map +1 -0
  63. package/dist/types/middleware.d.ts +43 -18
  64. package/dist/types/middleware.js +0 -3
  65. package/dist/types/middleware.js.map +1 -1
  66. package/dist/types/resource.d.ts +96 -0
  67. package/dist/types/resource.js +3 -0
  68. package/dist/types/resource.js.map +1 -0
  69. package/dist/types/symbols.d.ts +17 -0
  70. package/dist/types/symbols.js +18 -3
  71. package/dist/types/symbols.js.map +1 -1
  72. package/dist/types/task.d.ts +68 -0
  73. package/dist/types/task.js +3 -0
  74. package/dist/types/task.js.map +1 -0
  75. package/package.json +4 -4
  76. package/src/__tests__/benchmark/task-benchmark.test.ts +132 -0
  77. package/src/__tests__/createTestResource.test.ts +139 -0
  78. package/src/__tests__/globals/timeout.middleware.test.ts +88 -0
  79. package/src/__tests__/models/EventManager.test.ts +39 -6
  80. package/src/__tests__/models/Semaphore.test.ts +1 -1
  81. package/src/__tests__/override.test.ts +104 -0
  82. package/src/__tests__/run.overrides.test.ts +50 -21
  83. package/src/__tests__/run.test.ts +19 -0
  84. package/src/__tests__/tags.test.ts +396 -0
  85. package/src/__tests__/tools/getCallerFile.test.ts +9 -11
  86. package/src/__tests__/typesafety.test.ts +109 -1
  87. package/src/define.ts +128 -24
  88. package/src/defs.ts +174 -22
  89. package/src/globals/globalMiddleware.ts +2 -0
  90. package/src/globals/middleware/timeout.middleware.ts +46 -0
  91. package/src/index.ts +6 -0
  92. package/src/models/DependencyProcessor.ts +2 -10
  93. package/src/models/StoreConstants.ts +2 -1
  94. package/src/models/TaskRunner.ts +1 -3
  95. package/src/testing.ts +66 -0
@@ -3,6 +3,8 @@ import {
3
3
  defineTask,
4
4
  defineResource,
5
5
  defineMiddleware,
6
+ defineOverride,
7
+ defineTag,
6
8
  } from "../define";
7
9
  import {
8
10
  IEventDefinition,
@@ -12,8 +14,10 @@ import {
12
14
  ITaskDefinition,
13
15
  RegisterableItems,
14
16
  } from "../defs";
17
+ import { createTestResource } from "..";
15
18
 
16
- describe("typesafety", () => {
19
+ // This is skipped because we mostly check typesafety.
20
+ describe.skip("typesafety", () => {
17
21
  it("tasks, resources: should have propper type safety for dependeices", async () => {
18
22
  type InputTask = {
19
23
  message: string;
@@ -209,4 +213,108 @@ describe("typesafety", () => {
209
213
 
210
214
  expect(true).toBe(true);
211
215
  });
216
+
217
+ it("createTestResource.runTask: should be type-safe", async () => {
218
+ type Input = { x: number };
219
+ type Output = Promise<number>;
220
+
221
+ const add = defineTask<Input, Output>({
222
+ id: "types.add",
223
+ run: async (i) => i.x + 1,
224
+ });
225
+
226
+ const depTask = defineTask<{ v: string }, Promise<string>>({
227
+ id: "types.dep",
228
+ run: async (i) => i.v.toUpperCase(),
229
+ });
230
+
231
+ const main = defineTask<Input, Output, { depTask: typeof depTask }>({
232
+ id: "types.main",
233
+ dependencies: { depTask },
234
+ run: async (i, d) => {
235
+ const v = await d.depTask({ v: String(i.x) });
236
+ return Number(v) + 1;
237
+ },
238
+ });
239
+
240
+ const app = defineResource({
241
+ id: "types.app",
242
+ register: [add, depTask, main],
243
+ });
244
+ const harness = createTestResource(app);
245
+
246
+ // Types: input must match, override deps must match, output is awaited number
247
+ const { value: t } = await (await import("../run")).run(harness);
248
+ const r1: number | undefined = await t.runTask(add, { x: 1 });
249
+ // @ts-expect-error wrong input type
250
+ await t.runTask(add, { z: 1 });
251
+ // @ts-expect-error missing input
252
+ await t.runTask(add);
253
+
254
+ const r2: number | undefined = await t.runTask(main, { x: 2 });
255
+
256
+ // @ts-expect-error wrong deps override type
257
+ await t.runTask(main, { x: 2 }, { depTask: async (i: number) => "x" });
258
+
259
+ expect(true).toBe(true);
260
+ });
261
+
262
+ it("should have propper type safety for overrides", async () => {
263
+ const task = defineTask({
264
+ id: "task",
265
+ run: async () => "Task executed",
266
+ });
267
+
268
+ // @ts-expect-error
269
+ const overrideTask = defineOverride(task, {
270
+ run: async () => 234,
271
+ });
272
+
273
+ const resource = defineResource({
274
+ id: "resource",
275
+ register: [task],
276
+ init: async () => "Resource executed",
277
+ });
278
+
279
+ const overrideResource = defineOverride(resource, {
280
+ init: async () => "Resource overridden",
281
+ });
282
+ // @ts-expect-error
283
+ defineOverride(resource, {
284
+ init: async () => 123, // bad type
285
+ });
286
+
287
+ const middleware = defineMiddleware({
288
+ id: "middleware",
289
+ run: async () => "Middleware executed",
290
+ });
291
+
292
+ expect(true).toBe(true);
293
+ });
294
+
295
+ it("should have propper type safety for tags", async () => {
296
+ const tag = defineTag({ id: "tag" });
297
+ const tag2 = defineTag<{ value: number }>({ id: "tag2" });
298
+ const tag2optional = defineTag<{ value?: number }>({ id: "tag2" });
299
+
300
+ const tag3 = tag2.with({ value: 123 });
301
+ // @ts-expect-error
302
+ const tag4 = tag.with({ value: 123 });
303
+
304
+ const task = defineTask({
305
+ id: "task",
306
+ meta: {
307
+ tags: [
308
+ tag,
309
+ // @ts-expect-error
310
+ tag2,
311
+ tag2optional,
312
+ tag2.with({ value: 123 }),
313
+ tag3,
314
+ ],
315
+ },
316
+ });
317
+
318
+ expect(true).toBe(true);
319
+ });
212
320
  });
package/src/define.ts CHANGED
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Factory functions for defining tasks, resources, events and middleware.
3
+ *
4
+ * These helpers create strongly-typed definitions while also wiring internal
5
+ * metadata: anonymous IDs, file path tags (for better debugging), lifecycle
6
+ * events, and global middleware flags. See README for high-level concepts.
7
+ */
1
8
  import {
2
9
  ITask,
3
10
  ITaskDefinition,
@@ -6,7 +13,6 @@ import {
6
13
  IResourceDefinition,
7
14
  IEventDefinition,
8
15
  IMiddlewareDefinition,
9
- symbols,
10
16
  DependencyMapType,
11
17
  DependencyValuesType,
12
18
  IMiddleware,
@@ -16,6 +22,17 @@ import {
16
22
  symbolMiddlewareConfigured,
17
23
  symbolFilePath,
18
24
  symbolIndexResource,
25
+ ITag,
26
+ ITagDefinition,
27
+ ITagWithConfig,
28
+ TagType,
29
+ ITaggable,
30
+ symbolTask,
31
+ symbolMiddlewareEverywhereTasks,
32
+ symbolMiddlewareEverywhereResources,
33
+ symbolResourceWithConfig,
34
+ symbolResource,
35
+ symbolMiddleware,
19
36
  } from "./defs";
20
37
  import { Errors } from "./errors";
21
38
  import { generateCallerIdFromFile, getCallerFile } from "./tools/getCallerFile";
@@ -30,12 +47,18 @@ export function defineTask<
30
47
  >(
31
48
  taskConfig: ITaskDefinition<Input, Output, Deps, TOn>
32
49
  ): ITask<Input, Output, Deps, TOn> {
50
+ /**
51
+ * Creates a task definition.
52
+ * - Generates an anonymous id based on file path when `id` is omitted
53
+ * - Wires lifecycle events: beforeRun, afterRun, onError
54
+ * - Carries through dependencies and middleware as declared
55
+ */
33
56
  const filePath = getCallerFile();
34
57
  const isAnonymous = !Boolean(taskConfig.id);
35
58
  const id = taskConfig.id || generateCallerIdFromFile(filePath, "task");
36
59
  return {
37
- [symbols.task]: true,
38
- [symbols.filePath]: filePath,
60
+ [symbolTask]: true,
61
+ [symbolFilePath]: filePath,
39
62
  id,
40
63
  dependencies: taskConfig.dependencies || ({} as Deps),
41
64
  middleware: taskConfig.middleware || [],
@@ -49,7 +72,7 @@ export function defineTask<
49
72
  ? Symbol(`anonymous-task.events.beforeRun`)
50
73
  : `${id as string}.events.beforeRun`,
51
74
  }),
52
- [symbols.filePath]: getCallerFile(),
75
+ [symbolFilePath]: getCallerFile(),
53
76
  },
54
77
  afterRun: {
55
78
  ...defineEvent({
@@ -57,7 +80,7 @@ export function defineTask<
57
80
  ? Symbol(`anonymous-task.events.afterRun`)
58
81
  : `${id as string}.events.afterRun`,
59
82
  }),
60
- [symbols.filePath]: getCallerFile(),
83
+ [symbolFilePath]: getCallerFile(),
61
84
  },
62
85
  onError: {
63
86
  ...defineEvent({
@@ -65,7 +88,7 @@ export function defineTask<
65
88
  ? Symbol(`anonymous-task.events.onError`)
66
89
  : `${id as string}.events.onError`,
67
90
  }),
68
- [symbols.filePath]: getCallerFile(),
91
+ [symbolFilePath]: getCallerFile(),
69
92
  },
70
93
  },
71
94
  meta: taskConfig.meta || {},
@@ -81,6 +104,12 @@ export function defineResource<
81
104
  >(
82
105
  constConfig: IResourceDefinition<TConfig, TValue, TDeps, TPrivate>
83
106
  ): IResource<TConfig, TValue, TDeps, TPrivate> {
107
+ /**
108
+ * Creates a resource definition.
109
+ * - Generates anonymous id when omitted (resource or index flavor)
110
+ * - Wires lifecycle events: beforeInit, afterInit, onError
111
+ * - Exposes `.with(config)` for config‑bound registration
112
+ */
84
113
  // The symbolFilePath might already come from defineIndex() for example
85
114
  const filePath: string = constConfig[symbolFilePath] || getCallerFile();
86
115
  const isIndexResource = constConfig[symbolIndexResource] || false;
@@ -88,9 +117,11 @@ export function defineResource<
88
117
  const id =
89
118
  constConfig.id ||
90
119
  generateCallerIdFromFile(filePath, isIndexResource ? "index" : "resource");
120
+
91
121
  return {
92
- [symbols.resource]: true,
93
- [symbols.filePath]: filePath,
122
+ [symbolResource]: true,
123
+ [symbolFilePath]: filePath,
124
+ [symbolIndexResource]: isIndexResource,
94
125
  id,
95
126
  dependencies: constConfig.dependencies,
96
127
  dispose: constConfig.dispose,
@@ -100,7 +131,7 @@ export function defineResource<
100
131
  context: constConfig.context,
101
132
  with: function (config: TConfig) {
102
133
  return {
103
- [symbols.resourceWithConfig]: true,
134
+ [symbolResourceWithConfig]: true,
104
135
  id: this.id,
105
136
  resource: this,
106
137
  config,
@@ -114,7 +145,7 @@ export function defineResource<
114
145
  ? Symbol(`anonymous-resource.events.beforeInit`)
115
146
  : `${id as string}.events.beforeInit`,
116
147
  }),
117
- [symbols.filePath]: filePath,
148
+ [symbolFilePath]: filePath,
118
149
  },
119
150
  afterInit: {
120
151
  ...defineEvent({
@@ -122,7 +153,7 @@ export function defineResource<
122
153
  ? Symbol(`anonymous-resource.events.afterInit`)
123
154
  : `${id as string}.events.afterInit`,
124
155
  }),
125
- [symbols.filePath]: filePath,
156
+ [symbolFilePath]: filePath,
126
157
  },
127
158
  onError: {
128
159
  ...defineEvent({
@@ -130,7 +161,7 @@ export function defineResource<
130
161
  ? Symbol(`anonymous-resource.events.onError`)
131
162
  : `${id as string}.events.onError`,
132
163
  }),
133
- [symbols.filePath]: filePath,
164
+ [symbolFilePath]: filePath,
134
165
  },
135
166
  },
136
167
  meta: constConfig.meta || {},
@@ -152,6 +183,7 @@ export function defineIndex<
152
183
  : T[K];
153
184
  } & DependencyMapType
154
185
  >(items: T): IResource<void, DependencyValuesType<D>, D> {
186
+ // Build dependency map from given items; unwrap `.with()` to the base resource
155
187
  const dependencies = {} as D;
156
188
  const register: RegisterableItems[] = [];
157
189
 
@@ -173,20 +205,24 @@ export function defineIndex<
173
205
  async init(_, deps) {
174
206
  return deps as any;
175
207
  },
176
- [symbols.filePath]: callerFilePath,
177
- [symbols.indexResource]: true,
208
+ [symbolFilePath]: callerFilePath,
209
+ [symbolIndexResource]: true,
178
210
  });
179
211
  }
180
212
 
181
213
  export function defineEvent<TPayload = void>(
182
214
  config?: IEventDefinition<TPayload>
183
215
  ): IEvent<TPayload> {
216
+ /**
217
+ * Creates an event definition. Anonymous ids are generated from file path
218
+ * when omitted. The returned object is branded for runtime checks.
219
+ */
184
220
  const callerFilePath = getCallerFile();
185
221
  const eventConfig = config || {};
186
222
  return {
187
223
  ...eventConfig,
188
224
  id: eventConfig.id || generateCallerIdFromFile(callerFilePath, "event"),
189
- [symbols.filePath]: callerFilePath,
225
+ [symbolFilePath]: callerFilePath,
190
226
  [symbolEvent]: true, // This is a workaround
191
227
  };
192
228
  }
@@ -208,10 +244,16 @@ export function defineMiddleware<
208
244
  >(
209
245
  middlewareDef: IMiddlewareDefinition<TConfig, TDependencies>
210
246
  ): IMiddleware<TConfig, TDependencies> {
247
+ /**
248
+ * Creates a middleware definition with:
249
+ * - Anonymous id generation when omitted
250
+ * - `.with(config)` to create configured instances
251
+ * - `.everywhere()` to mark as global (optionally scoping to tasks/resources)
252
+ */
211
253
  const filePath = getCallerFile();
212
254
  const object = {
213
- [symbols.filePath]: filePath,
214
- [symbols.middleware]: true,
255
+ [symbolFilePath]: filePath,
256
+ [symbolMiddleware]: true,
215
257
  config: {} as TConfig,
216
258
  id: middlewareDef.id || generateCallerIdFromFile(filePath, "middleware"),
217
259
  ...middlewareDef,
@@ -235,8 +277,8 @@ export function defineMiddleware<
235
277
 
236
278
  return {
237
279
  ...object,
238
- [symbols.middlewareEverywhereTasks]: tasks,
239
- [symbols.middlewareEverywhereResources]: resources,
280
+ [symbolMiddlewareEverywhereTasks]: tasks,
281
+ [symbolMiddlewareEverywhereResources]: resources,
240
282
  everywhere() {
241
283
  throw Errors.middlewareAlreadyGlobal(object.id);
242
284
  },
@@ -246,23 +288,85 @@ export function defineMiddleware<
246
288
  }
247
289
 
248
290
  export function isTask(definition: any): definition is ITask {
249
- return definition && definition[symbols.task];
291
+ return definition && definition[symbolTask];
250
292
  }
251
293
 
252
294
  export function isResource(definition: any): definition is IResource {
253
- return definition && definition[symbols.resource];
295
+ return definition && definition[symbolResource];
254
296
  }
255
297
 
256
298
  export function isResourceWithConfig(
257
299
  definition: any
258
300
  ): definition is IResourceWithConfig {
259
- return definition && definition[symbols.resourceWithConfig];
301
+ return definition && definition[symbolResourceWithConfig];
260
302
  }
261
303
 
262
304
  export function isEvent(definition: any): definition is IEvent {
263
- return definition && definition[symbols.event];
305
+ return definition && definition[symbolEvent];
264
306
  }
265
307
 
266
308
  export function isMiddleware(definition: any): definition is IMiddleware {
267
- return definition && definition[symbols.middleware];
309
+ return definition && definition[symbolMiddleware];
310
+ }
311
+
312
+ /**
313
+ * Override helper that preserves the original `id` and returns the same type.
314
+ * You can override any property except `id`.
315
+ */
316
+ export function defineOverride<T extends ITask<any, any, any, any>>(
317
+ base: T,
318
+ patch: Omit<Partial<T>, "id">
319
+ ): T;
320
+ export function defineOverride<T extends IResource<any, any, any, any>>(
321
+ base: T,
322
+ patch: Omit<Partial<T>, "id">
323
+ ): T;
324
+ export function defineOverride<T extends IMiddleware<any, any>>(
325
+ base: T,
326
+ patch: Omit<Partial<T>, "id">
327
+ ): T;
328
+ export function defineOverride(
329
+ base: ITask | IResource | IMiddleware,
330
+ patch: Record<string | symbol, unknown>
331
+ ): ITask | IResource | IMiddleware {
332
+ const { id: _ignored, ...rest } = (patch || {}) as any;
333
+ // Ensure we never change the id, and merge overrides last
334
+ return {
335
+ ...(base as any),
336
+ ...rest,
337
+ id: (base as any).id,
338
+ } as any;
339
+ }
340
+
341
+ /**
342
+ * Creates a tag definition.
343
+ * - `.with(config)` to create configured instances
344
+ * - `.extract(tags)` to extract this tag from a list of tags
345
+ */
346
+ export function defineTag<TConfig = void>(
347
+ definition: ITagDefinition<TConfig>
348
+ ): ITag<TConfig> {
349
+ const id = definition.id;
350
+
351
+ return {
352
+ id,
353
+ with(tagConfig: TConfig) {
354
+ return {
355
+ id,
356
+ tag: this,
357
+ config: tagConfig as any,
358
+ } as ITagWithConfig<TConfig>;
359
+ },
360
+ extract(target: TagType[] | ITaggable) {
361
+ const tags = Array.isArray(target) ? target : target?.meta?.tags || [];
362
+ for (const candidate of tags) {
363
+ if (typeof candidate === "string") continue;
364
+ // Configured instance
365
+ if (candidate.id === id) {
366
+ return candidate as ITagWithConfig<TConfig>;
367
+ }
368
+ }
369
+ return null;
370
+ },
371
+ } as ITag<TConfig>;
268
372
  }