@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
@@ -0,0 +1,68 @@
1
+ import type { ITaskMeta } from './metadata';
2
+ import type { DependencyMapType, DependencyValuesType } from './dependencies';
3
+ import type { IEventDefinition, IEvent, IEventEmission } from './event';
4
+ import type { MiddlewareAttachments } from './middleware';
5
+ export type BeforeRunEventPayload<TInput> = {
6
+ input: TInput;
7
+ };
8
+ export type AfterRunEventPayload<TInput, TOutput> = {
9
+ input: TInput;
10
+ output: TOutput extends Promise<infer U> ? U : TOutput;
11
+ setOutput(newOutput: TOutput extends Promise<infer U> ? U : TOutput): void;
12
+ };
13
+ export type OnErrorEventPayload = {
14
+ error: any;
15
+ /**
16
+ * This function can be called to suppress the error from being thrown.
17
+ */
18
+ suppress(): void;
19
+ };
20
+ type ExtractEventParams<T> = T extends IEventDefinition<infer P> ? P : never;
21
+ export interface ITaskDefinition<TInput = any, TOutput extends Promise<any> = any, TDependencies extends DependencyMapType = {}, TOn extends "*" | IEventDefinition<any> | undefined = undefined> {
22
+ /**
23
+ * Stable identifier. If omitted, an anonymous id is generated from file path
24
+ * (see README: Anonymous IDs).
25
+ */
26
+ id?: string | symbol;
27
+ /**
28
+ * Access other tasks/resources/events. Can be an object or a function when
29
+ * you need late or config‑dependent resolution.
30
+ */
31
+ dependencies?: TDependencies | (() => TDependencies);
32
+ /** Middleware applied around task execution. */
33
+ middleware?: MiddlewareAttachments[];
34
+ /**
35
+ * Listen to events in a simple way
36
+ */
37
+ on?: TOn;
38
+ /**
39
+ * This makes sense only when `on` is specified to provide the order of the execution.
40
+ * The event with the lowest order will be executed first.
41
+ */
42
+ listenerOrder?: number;
43
+ /** Optional metadata used for docs, filtering and tooling. */
44
+ meta?: ITaskMeta;
45
+ /**
46
+ * The task body. If `on` is set, the input is an `IEventEmission`. Otherwise,
47
+ * it's the declared input type.
48
+ */
49
+ run: (input: TOn extends undefined ? TInput : IEventEmission<TOn extends "*" ? any : ExtractEventParams<TOn>>, dependencies: DependencyValuesType<TDependencies>) => TOutput;
50
+ }
51
+ /**
52
+ * This is the response after the definition has been prepared. TODO: better naming?
53
+ */
54
+ export interface ITask<TInput = any, TOutput extends Promise<any> = any, TDependencies extends DependencyMapType = {}, TOn extends "*" | IEventDefinition<any> | undefined = undefined> extends ITaskDefinition<TInput, TOutput, TDependencies, TOn> {
55
+ id: string | symbol;
56
+ dependencies: TDependencies | (() => TDependencies);
57
+ computedDependencies?: DependencyValuesType<TDependencies>;
58
+ middleware: MiddlewareAttachments[];
59
+ /**
60
+ * These events are automatically populated after the task has been defined.
61
+ */
62
+ events: {
63
+ beforeRun: IEvent<BeforeRunEventPayload<TInput>>;
64
+ afterRun: IEvent<AfterRunEventPayload<TInput, TOutput>>;
65
+ onError: IEvent<OnErrorEventPayload>;
66
+ };
67
+ }
68
+ export {};
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=task.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.js","sourceRoot":"","sources":["../../src/types/task.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bluelibs/runner",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "BlueLibs Runner",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
@@ -26,10 +26,10 @@
26
26
  "@types/graphql": "^0.11.3",
27
27
  "@types/jest": "^27.0.0",
28
28
  "@types/node": "^20.0.0",
29
- "@typescript-eslint/eslint-plugin": "2.3.0",
30
- "@typescript-eslint/parser": "2.3.0",
29
+ "@typescript-eslint/eslint-plugin": "8.39.0",
30
+ "@typescript-eslint/parser": "8.39.0",
31
31
  "benchmark": "^2.1.4",
32
- "eslint": "^6.6.0",
32
+ "eslint": "^9.32.0",
33
33
  "eslint-config-prettier": "6.3.0",
34
34
  "eslint-plugin-prettier": "3.1.1",
35
35
  "express": "^5.1.0",
@@ -0,0 +1,132 @@
1
+ import { defineTask, defineResource, defineMiddleware } from "../../define";
2
+ import { run } from "../../run";
3
+
4
+ // Benchmarks are environment-sensitive; keep skipped by default.
5
+ describe("Task benchmarks - sync vs async", () => {
6
+ const iterations = 500;
7
+
8
+ it("compares sync vs async task execution without middleware", async () => {
9
+ const syncTask = defineTask<number, any>({
10
+ id: "bench.syncTask",
11
+ // Intentionally synchronous
12
+ run: ((n: number) => {
13
+ let acc = 0;
14
+ for (let i = 0; i < 200; i++) acc += (n + i) % 7;
15
+ return acc;
16
+ }) as any,
17
+ } as any);
18
+
19
+ const asyncTask = defineTask<number, Promise<number>>({
20
+ id: "bench.asyncTask",
21
+ run: async (n: number) => {
22
+ let acc = 0;
23
+ for (let i = 0; i < 200; i++) acc += (n + i) % 7;
24
+ return acc;
25
+ },
26
+ });
27
+
28
+ const app = defineResource({
29
+ id: "bench.app",
30
+ register: [syncTask, asyncTask],
31
+ dependencies: { syncTask, asyncTask },
32
+ async init(_, { syncTask, asyncTask }) {
33
+ // Warm-up
34
+ await asyncTask(0);
35
+ await syncTask(0 as any);
36
+
37
+ const syncStart = performance.now();
38
+ for (let i = 0; i < iterations; i++) {
39
+ // Note: sync path still awaited at the TaskRunner boundary
40
+ await syncTask(i as any);
41
+ }
42
+ const syncTime = performance.now() - syncStart;
43
+
44
+ const asyncStart = performance.now();
45
+ for (let i = 0; i < iterations; i++) {
46
+ await asyncTask(i);
47
+ }
48
+ const asyncTime = performance.now() - asyncStart;
49
+
50
+ // Log metrics for manual inspection
51
+ // eslint-disable-next-line no-console
52
+ console.log(
53
+ `Task benchmark (iterations=${iterations}) -> sync: ${syncTime.toFixed(
54
+ 2
55
+ )}ms, async: ${asyncTime.toFixed(2)}ms`
56
+ );
57
+ },
58
+ });
59
+
60
+ const { dispose } = await run(app);
61
+ await dispose();
62
+ });
63
+
64
+ it("compares with a chain of pass-through middlewares", async () => {
65
+ const chainLength = 10;
66
+
67
+ const middlewares = Array.from({ length: chainLength }, (_, idx) =>
68
+ defineMiddleware({
69
+ id: `mw.${idx}`,
70
+ // Return the result of next() directly; no extra async wrapper here
71
+ run: ({ next }: any) => next(),
72
+ })
73
+ );
74
+
75
+ const syncTask = defineTask<number, any>({
76
+ id: "bench.syncTask.withMw",
77
+ middleware: middlewares,
78
+ // Intentionally synchronous
79
+ run: ((n: number) => {
80
+ let acc = 0;
81
+ for (let i = 0; i < 200; i++) acc += (n + i) % 7;
82
+ return acc;
83
+ }) as any,
84
+ } as any);
85
+
86
+ const asyncTask = defineTask<number, Promise<number>>({
87
+ id: "bench.asyncTask.withMw",
88
+ middleware: middlewares,
89
+ run: async (n: number) => {
90
+ let acc = 0;
91
+ for (let i = 0; i < 200; i++) acc += (n + i) % 7;
92
+ return acc;
93
+ },
94
+ });
95
+
96
+ const app = defineResource({
97
+ id: "bench.app.mw",
98
+ register: [...middlewares, syncTask, asyncTask],
99
+ dependencies: { syncTask, asyncTask },
100
+ async init(_, { syncTask, asyncTask }) {
101
+ // Warm-up
102
+ await asyncTask(0);
103
+ await syncTask(0 as any);
104
+
105
+ const iters = 5000;
106
+ const t0 = process.hrtime.bigint();
107
+ for (let i = 0; i < iters; i++) {
108
+ await syncTask(i as any);
109
+ }
110
+ const t1 = process.hrtime.bigint();
111
+ const syncNs = Number(t1 - t0);
112
+
113
+ const t2 = process.hrtime.bigint();
114
+ for (let i = 0; i < iters; i++) {
115
+ await asyncTask(i);
116
+ }
117
+ const t3 = process.hrtime.bigint();
118
+ const asyncNs = Number(t3 - t2);
119
+
120
+ // eslint-disable-next-line no-console
121
+ console.log(
122
+ `Task benchmark with ${chainLength} middlewares (iterations=${iters}) -> sync: ${(
123
+ syncNs / 1e6
124
+ ).toFixed(2)}ms, async: ${(asyncNs / 1e6).toFixed(2)}ms`
125
+ );
126
+ },
127
+ });
128
+
129
+ const { dispose } = await run(app);
130
+ await dispose();
131
+ });
132
+ });
@@ -0,0 +1,139 @@
1
+ import {
2
+ task,
3
+ resource,
4
+ override,
5
+ run,
6
+ createTestResource,
7
+ globals,
8
+ event,
9
+ } from "..";
10
+
11
+ describe("createTestResource", () => {
12
+ it("runs tasks within the full ecosystem and returns results", async () => {
13
+ const double = task({ id: "t.double", run: async (x: number) => x * 2 });
14
+
15
+ const app = resource({
16
+ id: "app.root",
17
+ register: [double],
18
+ });
19
+
20
+ const harness = createTestResource(app);
21
+ const { value: t, dispose } = await run(harness);
22
+
23
+ const result = await t.runTask(double, 21);
24
+ expect(result).toBe(42);
25
+
26
+ await dispose();
27
+ });
28
+
29
+ it("supports overrides for integration tests", async () => {
30
+ const db = resource({ id: "db", init: async () => ({ kind: "real" }) });
31
+ const getDbKind = task({
32
+ id: "t.db.kind",
33
+ dependencies: { db },
34
+ run: async (_, { db }) => db.kind,
35
+ });
36
+ const app = resource({ id: "app", register: [db, getDbKind] });
37
+
38
+ const mockDb = override(db, { init: async () => ({ kind: "mock" }) });
39
+
40
+ const harness = createTestResource(app, { overrides: [mockDb] });
41
+ const { value: t, dispose } = await run(harness);
42
+
43
+ const kind = await t.runTask(getDbKind, undefined);
44
+ expect(kind).toBe("mock");
45
+
46
+ await dispose();
47
+ });
48
+
49
+ it("allows accessing resource values and subscribing to events", async () => {
50
+ const eventsSeen: Array<any> = [];
51
+
52
+ const say = task({ id: "t.say", run: async (m: string) => m });
53
+
54
+ const listener = task({
55
+ id: "tests.log-listener",
56
+ on: globals.events.tasks.beforeRun,
57
+ run: async (e: any) => eventsSeen.push(e.data),
58
+ });
59
+
60
+ const app = resource({ id: "app2", register: [say, listener] });
61
+
62
+ const harness = createTestResource(app);
63
+ const { value: t, dispose } = await run(harness);
64
+
65
+ // Subscribe via facade as well (no-op if using global path instead)
66
+ // t.on(globals.events.log, (e) => eventsSeen.push(e.data));
67
+
68
+ await t.runTask(say, "hello");
69
+
70
+ // We can also query resource values (will often be undefined for pure tasks)
71
+ expect(t.getResource("app2")).toBeUndefined();
72
+
73
+ // At least one event was recorded via the listener
74
+ expect(Array.isArray(eventsSeen)).toBe(true);
75
+
76
+ await dispose();
77
+ });
78
+
79
+ it("supports multiple harness instances without id collisions", async () => {
80
+ const add1 = task({ id: "t.add1", run: async (n: number) => n + 1 });
81
+ const app = resource({ id: "app.multi", register: [add1] });
82
+
83
+ const h1 = createTestResource(app);
84
+ const h2 = createTestResource(app);
85
+
86
+ const [r1, r2] = await Promise.all([run(h1), run(h2)]);
87
+
88
+ const v1 = await r1.value.runTask(add1, 1);
89
+ const v2 = await r2.value.runTask(add1, 2);
90
+ expect(v1).toBe(2);
91
+ expect(v2).toBe(3);
92
+
93
+ await Promise.all([r1.dispose(), r2.dispose()]);
94
+ });
95
+
96
+ it("runTask is typesafe in tests", async () => {
97
+ const sum = task<{ a: number; b: number }, Promise<number>>({
98
+ id: "t.sum",
99
+ run: async (i) => i.a + i.b,
100
+ });
101
+
102
+ const upper = task<{ v: string }, Promise<string>>({
103
+ id: "t.upper",
104
+ run: async (i) => i.v.toUpperCase(),
105
+ });
106
+
107
+ const usesUpper = task<
108
+ { n: number },
109
+ Promise<number>,
110
+ { upper: typeof upper }
111
+ >({
112
+ id: "t.usesUpper",
113
+ dependencies: { upper },
114
+ run: async (i, d) => Number(await d.upper({ v: String(i.n) })),
115
+ });
116
+
117
+ const app = resource({
118
+ id: "app.types",
119
+ register: [sum, upper, usesUpper],
120
+ });
121
+ const { value: t, dispose } = await run(createTestResource(app));
122
+
123
+ const ok1: number | undefined = await t.runTask(sum, { a: 1, b: 2 });
124
+ // Type-only checks (do not execute)
125
+ const typeOnly = () => {
126
+ // @ts-expect-error bad input
127
+ t.runTask(sum, { a: 1 });
128
+ // @ts-expect-error missing input
129
+ t.runTask(sum as any);
130
+ };
131
+ void typeOnly;
132
+
133
+ const ok2: number | undefined = await t.runTask(usesUpper, {
134
+ n: 3,
135
+ } as const);
136
+
137
+ await dispose();
138
+ });
139
+ });
@@ -0,0 +1,88 @@
1
+ import { defineResource, defineTask } from "../../define";
2
+ import { run } from "../../run";
3
+ import { timeoutMiddleware } from "../../globals/middleware/timeout.middleware";
4
+
5
+ const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));
6
+
7
+ describe("Timeout Middleware", () => {
8
+ it("should abort long-running tasks after ttl", async () => {
9
+ const slowTask = defineTask({
10
+ id: "timeout.slowTask",
11
+ middleware: [timeoutMiddleware.with({ ttl: 20 })],
12
+ run: async () => {
13
+ await sleep(50);
14
+ return "done";
15
+ },
16
+ });
17
+
18
+ const app = defineResource({
19
+ id: "app",
20
+ register: [slowTask],
21
+ dependencies: { slowTask },
22
+ async init(_, { slowTask }) {
23
+ await expect(slowTask()).rejects.toThrow(/timed out/i);
24
+ },
25
+ });
26
+
27
+ await run(app);
28
+ });
29
+
30
+ it("should allow tasks to complete before ttl", async () => {
31
+ const fastTask = defineTask({
32
+ id: "timeout.fastTask",
33
+ middleware: [timeoutMiddleware.with({ ttl: 50 })],
34
+ run: async () => {
35
+ await sleep(10);
36
+ return "ok";
37
+ },
38
+ });
39
+
40
+ const app = defineResource({
41
+ id: "app",
42
+ register: [fastTask],
43
+ dependencies: { fastTask },
44
+ async init(_, { fastTask }) {
45
+ await expect(fastTask()).resolves.toBe("ok");
46
+ },
47
+ });
48
+
49
+ await run(app);
50
+ });
51
+
52
+ it("should timeout resource initialization", async () => {
53
+ const slowResource = defineResource({
54
+ id: "timeout.slowResource",
55
+ middleware: [timeoutMiddleware.with({ ttl: 20 })],
56
+ async init() {
57
+ await sleep(50);
58
+ return "ready";
59
+ },
60
+ });
61
+
62
+ const app = defineResource({
63
+ id: "app",
64
+ register: [slowResource],
65
+ });
66
+
67
+ await expect(run(app)).rejects.toThrow(/timed out/i);
68
+ });
69
+
70
+ it("should throw immediately when ttl is 0", async () => {
71
+ const task = defineTask({
72
+ id: "timeout.immediate",
73
+ middleware: [timeoutMiddleware.with({ ttl: 0 })],
74
+ run: async () => "never",
75
+ });
76
+
77
+ const app = defineResource({
78
+ id: "app",
79
+ register: [task],
80
+ dependencies: { task },
81
+ async init(_, { task }) {
82
+ await expect(task()).rejects.toThrow(/timed out/i);
83
+ },
84
+ });
85
+
86
+ await run(app);
87
+ });
88
+ });
@@ -1,4 +1,9 @@
1
- import { IEvent, IEventEmission, symbolEvent } from "../../defs";
1
+ import {
2
+ IEvent,
3
+ IEventEmission,
4
+ symbolEvent,
5
+ symbolFilePath,
6
+ } from "../../defs";
2
7
  import { Errors } from "../../errors";
3
8
  import { EventManager } from "../../models/EventManager";
4
9
 
@@ -8,7 +13,11 @@ describe("EventManager", () => {
8
13
 
9
14
  beforeEach(() => {
10
15
  eventManager = new EventManager();
11
- eventDefinition = { id: "testEvent", [symbolEvent]: true };
16
+ eventDefinition = {
17
+ id: "testEvent",
18
+ [symbolEvent]: true,
19
+ [symbolFilePath]: "test.ts",
20
+ };
12
21
  });
13
22
 
14
23
  it("should add and emit event listener", async () => {
@@ -165,10 +174,12 @@ describe("EventManager", () => {
165
174
  const eventDef1: IEvent<string> = {
166
175
  id: "event1",
167
176
  [symbolEvent]: true,
177
+ [symbolFilePath]: "test.ts",
168
178
  };
169
179
  const eventDef2: IEvent<string> = {
170
180
  id: "event2",
171
181
  [symbolEvent]: true,
182
+ [symbolFilePath]: "test.ts",
172
183
  };
173
184
 
174
185
  const handler1 = jest.fn();
@@ -215,10 +226,12 @@ describe("EventManager", () => {
215
226
  const eventDef1: IEvent<string> = {
216
227
  id: "event1",
217
228
  [symbolEvent]: true,
229
+ [symbolFilePath]: "test.ts",
218
230
  };
219
231
  const eventDef2: IEvent<string> = {
220
232
  id: "event2",
221
233
  [symbolEvent]: true,
234
+ [symbolFilePath]: "test.ts",
222
235
  };
223
236
 
224
237
  const handler = jest.fn();
@@ -249,10 +262,12 @@ describe("EventManager", () => {
249
262
  const eventDef1: IEvent<string> = {
250
263
  id: "event1",
251
264
  [symbolEvent]: true,
265
+ [symbolFilePath]: "test.ts",
252
266
  };
253
267
  const eventDef2: IEvent<string> = {
254
268
  id: "event2",
255
269
  [symbolEvent]: true,
270
+ [symbolFilePath]: "test.ts",
256
271
  };
257
272
 
258
273
  const handler1 = jest.fn();
@@ -392,6 +407,7 @@ describe("EventManager", () => {
392
407
  const voidEventDefinition: IEvent<void> = {
393
408
  id: "voidEvent",
394
409
  [symbolEvent]: true,
410
+ [symbolFilePath]: "test.ts",
395
411
  };
396
412
 
397
413
  eventManager.addListener(voidEventDefinition, handler);
@@ -445,8 +461,16 @@ describe("EventManager", () => {
445
461
  });
446
462
 
447
463
  it("should invalidate all caches when adding global listeners", async () => {
448
- const event1: IEvent<string> = { id: "event1", [symbolEvent]: true };
449
- const event2: IEvent<string> = { id: "event2", [symbolEvent]: true };
464
+ const event1: IEvent<string> = {
465
+ id: "event1",
466
+ [symbolEvent]: true,
467
+ [symbolFilePath]: "test.ts",
468
+ };
469
+ const event2: IEvent<string> = {
470
+ id: "event2",
471
+ [symbolEvent]: true,
472
+ [symbolFilePath]: "test.ts",
473
+ };
450
474
 
451
475
  const handler1 = jest.fn();
452
476
  const handler2 = jest.fn();
@@ -473,6 +497,7 @@ describe("EventManager", () => {
473
497
  const emptyEventDef: IEvent<string> = {
474
498
  id: "emptyEvent",
475
499
  [symbolEvent]: true,
500
+ [symbolFilePath]: "test.ts",
476
501
  };
477
502
 
478
503
  // Should return immediately without creating event object
@@ -528,8 +553,16 @@ describe("EventManager", () => {
528
553
  });
529
554
 
530
555
  it("should reuse cached results across different event types", async () => {
531
- const event1: IEvent<string> = { id: "event1", [symbolEvent]: true };
532
- const event2: IEvent<string> = { id: "event2", [symbolEvent]: true };
556
+ const event1: IEvent<string> = {
557
+ id: "event1",
558
+ [symbolEvent]: true,
559
+ [symbolFilePath]: "test.ts",
560
+ };
561
+ const event2: IEvent<string> = {
562
+ id: "event2",
563
+ [symbolEvent]: true,
564
+ [symbolFilePath]: "test.ts",
565
+ };
533
566
 
534
567
  const handler1 = jest.fn();
535
568
  const handler2 = jest.fn();
@@ -178,7 +178,7 @@ describe("Semaphore", () => {
178
178
 
179
179
  const elapsed = Date.now() - startTime;
180
180
  expect(elapsed).toBeGreaterThanOrEqual(100);
181
- expect(elapsed).toBeLessThan(200); // Should not wait much longer
181
+ expect(elapsed).toBeLessThan(300); // Should not wait much longer
182
182
  });
183
183
 
184
184
  it("should timeout withPermit operation", async () => {
@@ -0,0 +1,104 @@
1
+ import { definitions, task, resource, middleware, override } from "..";
2
+
3
+ describe("override() helper", () => {
4
+ it("should preserve id and override run for tasks", async () => {
5
+ const base = task({
6
+ id: "test.task",
7
+ run: async () => "base",
8
+ });
9
+
10
+ const changed = override(base, {
11
+ run: async () => "changed",
12
+ meta: { title: "Updated" },
13
+ });
14
+
15
+ expect(changed).not.toBe(base);
16
+ expect(changed.id).toBe(base.id);
17
+ expect(await base.run(undefined as any, {} as any)).toBe("base");
18
+ expect(await changed.run(undefined as any, {} as any)).toBe("changed");
19
+ expect(changed.meta?.title).toBe("Updated");
20
+ });
21
+
22
+ it("should preserve id and override init for resources", async () => {
23
+ const base = resource({
24
+ id: "test.resource",
25
+ init: async () => 1,
26
+ });
27
+
28
+ const changed = override(base, {
29
+ init: async () => 2,
30
+ meta: { description: "Updated" },
31
+ });
32
+
33
+ expect(changed).not.toBe(base);
34
+ expect(changed.id).toBe(base.id);
35
+ // Call the init functions directly (without runner) to validate override
36
+ // Signatures: init(config, deps, ctx)
37
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
38
+ const v1 = await base.init!(undefined as any, {} as any, undefined as any);
39
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
40
+ const v2 = await changed.init!(
41
+ undefined as any,
42
+ {} as any,
43
+ undefined as any
44
+ );
45
+ expect(v1).toBe(1);
46
+ expect(v2).toBe(2);
47
+ expect(changed.meta?.description).toBe("Updated");
48
+ });
49
+
50
+ it("should preserve id and override run for middleware", async () => {
51
+ const mw = middleware({
52
+ id: "test.middleware",
53
+ run: async ({ next }) => {
54
+ return next();
55
+ },
56
+ });
57
+
58
+ const changed = override(mw, {
59
+ run: async ({ task, next }) => {
60
+ const result = await next(task?.input as any);
61
+ return { wrapped: result } as any;
62
+ },
63
+ });
64
+
65
+ expect(changed).not.toBe(mw);
66
+ expect(changed.id).toBe(mw.id);
67
+
68
+ const input = {
69
+ task: { definition: undefined as any, input: 123 },
70
+ next: async () => 456,
71
+ } as definitions.IMiddlewareExecutionInput<any, any>;
72
+
73
+ const baseResult = await mw.run(input, {} as any, undefined as any);
74
+ const changedResult = await changed.run(input, {} as any, undefined as any);
75
+ expect(baseResult).toBe(456);
76
+ expect(changedResult).toEqual({ wrapped: 456 });
77
+ });
78
+
79
+ it("should be type-safe: cannot override id on task/resource/middleware", () => {
80
+ const t = task({ id: "tt", run: async () => undefined });
81
+ const r = resource({ id: "rr", init: async () => undefined });
82
+ const m = middleware({ id: "mm", run: async ({ next }) => next() });
83
+
84
+ // @ts-expect-error id cannot be overridden
85
+ override(t, { id: "new" });
86
+
87
+ // @ts-expect-error id cannot be overridden
88
+ override(r, { id: "new" });
89
+
90
+ // @ts-expect-error id cannot be overridden
91
+ override(m, { id: "new" });
92
+
93
+ expect(true).toBe(true);
94
+ });
95
+
96
+ it("should handle undefined patch (robustness)", () => {
97
+ const base = task({ id: "robust.task", run: async () => 1 });
98
+ const changed = (override as any)(base, undefined);
99
+
100
+ expect(changed).not.toBe(base);
101
+ expect(changed.id).toBe(base.id);
102
+ expect(changed.run).toBe(base.run);
103
+ });
104
+ });