@bluelibs/runner 2.2.4 → 3.1.0

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 (211) hide show
  1. package/README.md +1409 -935
  2. package/dist/common.types.d.ts +20 -0
  3. package/dist/common.types.js +4 -0
  4. package/dist/common.types.js.map +1 -0
  5. package/dist/context.d.ts +34 -0
  6. package/dist/context.js +58 -0
  7. package/dist/context.js.map +1 -0
  8. package/dist/define.d.ts +24 -5
  9. package/dist/define.js +89 -20
  10. package/dist/define.js.map +1 -1
  11. package/dist/defs.d.ts +109 -73
  12. package/dist/defs.js +12 -2
  13. package/dist/defs.js.map +1 -1
  14. package/dist/errors.d.ts +5 -5
  15. package/dist/errors.js +6 -5
  16. package/dist/errors.js.map +1 -1
  17. package/dist/event.types.d.ts +18 -0
  18. package/dist/event.types.js +4 -0
  19. package/dist/event.types.js.map +1 -0
  20. package/dist/examples/registrator-example.d.ts +122 -0
  21. package/dist/examples/registrator-example.js +147 -0
  22. package/dist/examples/registrator-example.js.map +1 -0
  23. package/dist/globals/globalEvents.d.ts +41 -0
  24. package/dist/globals/globalEvents.js +94 -0
  25. package/dist/globals/globalEvents.js.map +1 -0
  26. package/dist/globals/globalMiddleware.d.ts +23 -0
  27. package/dist/globals/globalMiddleware.js +15 -0
  28. package/dist/globals/globalMiddleware.js.map +1 -0
  29. package/dist/globals/globalResources.d.ts +27 -0
  30. package/dist/globals/globalResources.js +47 -0
  31. package/dist/globals/globalResources.js.map +1 -0
  32. package/dist/globals/middleware/cache.middleware.d.ts +34 -0
  33. package/dist/globals/middleware/cache.middleware.js +85 -0
  34. package/dist/globals/middleware/cache.middleware.js.map +1 -0
  35. package/dist/globals/middleware/requireContext.middleware.d.ts +6 -0
  36. package/dist/globals/middleware/requireContext.middleware.js +25 -0
  37. package/dist/globals/middleware/requireContext.middleware.js.map +1 -0
  38. package/dist/globals/middleware/retry.middleware.d.ts +20 -0
  39. package/dist/globals/middleware/retry.middleware.js +34 -0
  40. package/dist/globals/middleware/retry.middleware.js.map +1 -0
  41. package/dist/globals/resources/queue.resource.d.ts +7 -0
  42. package/dist/globals/resources/queue.resource.js +31 -0
  43. package/dist/globals/resources/queue.resource.js.map +1 -0
  44. package/dist/index.d.ts +54 -18
  45. package/dist/index.js +14 -9
  46. package/dist/index.js.map +1 -1
  47. package/dist/middleware.types.d.ts +40 -0
  48. package/dist/middleware.types.js +4 -0
  49. package/dist/middleware.types.js.map +1 -0
  50. package/dist/models/DependencyProcessor.d.ts +6 -5
  51. package/dist/models/DependencyProcessor.js +13 -15
  52. package/dist/models/DependencyProcessor.js.map +1 -1
  53. package/dist/models/EventManager.d.ts +9 -4
  54. package/dist/models/EventManager.js +44 -2
  55. package/dist/models/EventManager.js.map +1 -1
  56. package/dist/models/Logger.d.ts +30 -13
  57. package/dist/models/Logger.js +132 -54
  58. package/dist/models/Logger.js.map +1 -1
  59. package/dist/models/OverrideManager.d.ts +13 -0
  60. package/dist/models/OverrideManager.js +70 -0
  61. package/dist/models/OverrideManager.js.map +1 -0
  62. package/dist/models/Queue.d.ts +25 -0
  63. package/dist/models/Queue.js +54 -0
  64. package/dist/models/Queue.js.map +1 -0
  65. package/dist/models/ResourceInitializer.d.ts +5 -2
  66. package/dist/models/ResourceInitializer.js +22 -14
  67. package/dist/models/ResourceInitializer.js.map +1 -1
  68. package/dist/models/Semaphore.d.ts +61 -0
  69. package/dist/models/Semaphore.js +166 -0
  70. package/dist/models/Semaphore.js.map +1 -0
  71. package/dist/models/Store.d.ts +18 -73
  72. package/dist/models/Store.js +71 -269
  73. package/dist/models/Store.js.map +1 -1
  74. package/dist/models/StoreConstants.d.ts +11 -0
  75. package/dist/models/StoreConstants.js +18 -0
  76. package/dist/models/StoreConstants.js.map +1 -0
  77. package/dist/models/StoreRegistry.d.ts +25 -0
  78. package/dist/models/StoreRegistry.js +171 -0
  79. package/dist/models/StoreRegistry.js.map +1 -0
  80. package/dist/models/StoreTypes.d.ts +21 -0
  81. package/dist/models/StoreTypes.js +3 -0
  82. package/dist/models/StoreTypes.js.map +1 -0
  83. package/dist/models/StoreValidator.d.ts +10 -0
  84. package/dist/models/StoreValidator.js +41 -0
  85. package/dist/models/StoreValidator.js.map +1 -0
  86. package/dist/models/TaskRunner.d.ts +1 -1
  87. package/dist/models/TaskRunner.js +39 -24
  88. package/dist/models/TaskRunner.js.map +1 -1
  89. package/dist/models/VarStore.d.ts +17 -0
  90. package/dist/models/VarStore.js +60 -0
  91. package/dist/models/VarStore.js.map +1 -0
  92. package/dist/models/index.d.ts +3 -0
  93. package/dist/models/index.js +3 -0
  94. package/dist/models/index.js.map +1 -1
  95. package/dist/resource.types.d.ts +31 -0
  96. package/dist/resource.types.js +3 -0
  97. package/dist/resource.types.js.map +1 -0
  98. package/dist/run.d.ts +4 -1
  99. package/dist/run.js +6 -3
  100. package/dist/run.js.map +1 -1
  101. package/dist/symbols.d.ts +24 -0
  102. package/dist/symbols.js +29 -0
  103. package/dist/symbols.js.map +1 -0
  104. package/dist/task.types.d.ts +55 -0
  105. package/dist/task.types.js +23 -0
  106. package/dist/task.types.js.map +1 -0
  107. package/dist/tools/getCallerFile.d.ts +9 -1
  108. package/dist/tools/getCallerFile.js +41 -0
  109. package/dist/tools/getCallerFile.js.map +1 -1
  110. package/dist/tools/registratorId.d.ts +4 -0
  111. package/dist/tools/registratorId.js +40 -0
  112. package/dist/tools/registratorId.js.map +1 -0
  113. package/dist/tools/simpleHash.d.ts +9 -0
  114. package/dist/tools/simpleHash.js +34 -0
  115. package/dist/tools/simpleHash.js.map +1 -0
  116. package/dist/types/base-interfaces.d.ts +18 -0
  117. package/dist/types/base-interfaces.js +6 -0
  118. package/dist/types/base-interfaces.js.map +1 -0
  119. package/dist/types/base.d.ts +13 -0
  120. package/dist/types/base.js +3 -0
  121. package/dist/types/base.js.map +1 -0
  122. package/dist/types/dependencies.d.ts +22 -0
  123. package/dist/types/dependencies.js +3 -0
  124. package/dist/types/dependencies.js.map +1 -0
  125. package/dist/types/dependency-core.d.ts +14 -0
  126. package/dist/types/dependency-core.js +5 -0
  127. package/dist/types/dependency-core.js.map +1 -0
  128. package/dist/types/events.d.ts +52 -0
  129. package/dist/types/events.js +6 -0
  130. package/dist/types/events.js.map +1 -0
  131. package/dist/types/hooks.d.ts +16 -0
  132. package/dist/types/hooks.js +5 -0
  133. package/dist/types/hooks.js.map +1 -0
  134. package/dist/types/index.d.ts +14 -0
  135. package/dist/types/index.js +27 -0
  136. package/dist/types/index.js.map +1 -0
  137. package/dist/types/meta.d.ts +13 -0
  138. package/dist/types/meta.js +5 -0
  139. package/dist/types/meta.js.map +1 -0
  140. package/dist/types/middleware.d.ts +38 -0
  141. package/dist/types/middleware.js +6 -0
  142. package/dist/types/middleware.js.map +1 -0
  143. package/dist/types/registerable.d.ts +10 -0
  144. package/dist/types/registerable.js +5 -0
  145. package/dist/types/registerable.js.map +1 -0
  146. package/dist/types/resources.d.ts +44 -0
  147. package/dist/types/resources.js +5 -0
  148. package/dist/types/resources.js.map +1 -0
  149. package/dist/types/symbols.d.ts +24 -0
  150. package/dist/types/symbols.js +30 -0
  151. package/dist/types/symbols.js.map +1 -0
  152. package/dist/types/tasks.d.ts +41 -0
  153. package/dist/types/tasks.js +5 -0
  154. package/dist/types/tasks.js.map +1 -0
  155. package/dist/types/utilities.d.ts +7 -0
  156. package/dist/types/utilities.js +5 -0
  157. package/dist/types/utilities.js.map +1 -0
  158. package/package.json +10 -6
  159. package/src/__tests__/benchmark/benchmark.test.ts +1 -1
  160. package/src/__tests__/context.test.ts +91 -0
  161. package/src/__tests__/errors.test.ts +8 -5
  162. package/src/__tests__/globalEvents.test.ts +1 -1
  163. package/src/__tests__/globals/cache.middleware.test.ts +772 -0
  164. package/src/__tests__/globals/queue.resource.test.ts +141 -0
  165. package/src/__tests__/globals/requireContext.middleware.test.ts +98 -0
  166. package/src/__tests__/globals/retry.middleware.test.ts +157 -0
  167. package/src/__tests__/index.helper.test.ts +55 -0
  168. package/src/__tests__/models/EventManager.test.ts +157 -11
  169. package/src/__tests__/models/Logger.test.ts +291 -34
  170. package/src/__tests__/models/Queue.test.ts +189 -0
  171. package/src/__tests__/models/ResourceInitializer.test.ts +8 -6
  172. package/src/__tests__/models/Semaphore.test.ts +713 -0
  173. package/src/__tests__/models/Store.test.ts +40 -0
  174. package/src/__tests__/models/TaskRunner.test.ts +86 -5
  175. package/src/__tests__/run.anonymous.test.ts +679 -0
  176. package/src/__tests__/run.middleware.test.ts +312 -12
  177. package/src/__tests__/run.overrides.test.ts +13 -10
  178. package/src/__tests__/run.test.ts +364 -13
  179. package/src/__tests__/setOutput.test.ts +244 -0
  180. package/src/__tests__/tools/getCallerFile.test.ts +124 -9
  181. package/src/__tests__/typesafety.test.ts +71 -41
  182. package/src/context.ts +86 -0
  183. package/src/define.ts +129 -34
  184. package/src/defs.ts +156 -119
  185. package/src/errors.ts +15 -10
  186. package/src/{globalEvents.ts → globals/globalEvents.ts} +13 -12
  187. package/src/globals/globalMiddleware.ts +14 -0
  188. package/src/{globalResources.ts → globals/globalResources.ts} +14 -10
  189. package/src/globals/middleware/cache.middleware.ts +115 -0
  190. package/src/globals/middleware/requireContext.middleware.ts +36 -0
  191. package/src/globals/middleware/retry.middleware.ts +56 -0
  192. package/src/globals/resources/queue.resource.ts +34 -0
  193. package/src/index.ts +9 -5
  194. package/src/models/DependencyProcessor.ts +42 -49
  195. package/src/models/EventManager.ts +64 -13
  196. package/src/models/Logger.ts +181 -64
  197. package/src/models/OverrideManager.ts +84 -0
  198. package/src/models/Queue.ts +66 -0
  199. package/src/models/ResourceInitializer.ts +40 -20
  200. package/src/models/Semaphore.ts +208 -0
  201. package/src/models/Store.ts +94 -342
  202. package/src/models/StoreConstants.ts +17 -0
  203. package/src/models/StoreRegistry.ts +228 -0
  204. package/src/models/StoreTypes.ts +46 -0
  205. package/src/models/StoreValidator.ts +43 -0
  206. package/src/models/TaskRunner.ts +54 -41
  207. package/src/models/index.ts +3 -0
  208. package/src/run.ts +7 -4
  209. package/src/tools/getCallerFile.ts +54 -2
  210. package/src/__tests__/index.ts +0 -15
  211. package/src/examples/express-mongo/index.ts +0 -1
@@ -0,0 +1,141 @@
1
+ import { defineResource } from "../../define";
2
+ import { run } from "../../run";
3
+ import { queueResource } from "../../globals/resources/queue.resource";
4
+
5
+ describe("Queue Resource", () => {
6
+ it("should provide queue functionality with proper isolation and disposal", async () => {
7
+ let callCount = 0;
8
+ const executionOrder: number[] = [];
9
+ let successCallCount = 0;
10
+
11
+ const app = defineResource({
12
+ id: "app",
13
+ // Don't register queueResource - it's already registered globally
14
+ dependencies: { queue: queueResource },
15
+ async init(_, { queue }) {
16
+ // Test 1: Initialize with empty queue map
17
+ expect(queue.map).toBeInstanceOf(Map);
18
+ expect(queue.map.size).toBe(0);
19
+ expect(typeof queue.run).toBe("function");
20
+
21
+ // Test 2: Create and reuse queues by ID
22
+ const task = async () => {
23
+ callCount++;
24
+ return `result-${callCount}`;
25
+ };
26
+
27
+ const result1 = await queue.run("test-queue", task);
28
+ expect(queue.map.size).toBe(1);
29
+ expect(queue.map.has("test-queue")).toBe(true);
30
+
31
+ const result2 = await queue.run("test-queue", task);
32
+ expect(queue.map.size).toBe(1);
33
+ expect(result1).toBe("result-1");
34
+ expect(result2).toBe("result-2");
35
+
36
+ // Test 3: Create separate queues for different IDs
37
+ const task1 = async () => "queue1-result";
38
+ const task2 = async () => "queue2-result";
39
+
40
+ await queue.run("queue-1", task1);
41
+ await queue.run("queue-2", task2);
42
+
43
+ expect(queue.map.size).toBe(3); // test-queue, queue-1, queue-2
44
+ expect(queue.map.has("queue-1")).toBe(true);
45
+ expect(queue.map.has("queue-2")).toBe(true);
46
+
47
+ // Test 4: Ensure tasks run sequentially within the same queue
48
+ const taskFactory = (id: number) => async () => {
49
+ executionOrder.push(id);
50
+ await new Promise((resolve) => setTimeout(resolve, 1));
51
+ return id;
52
+ };
53
+
54
+ const promises = [
55
+ queue.run("sequential-queue", taskFactory(1)),
56
+ queue.run("sequential-queue", taskFactory(2)),
57
+ queue.run("sequential-queue", taskFactory(3)),
58
+ ];
59
+
60
+ const results = await Promise.all(promises);
61
+ expect(results).toEqual([1, 2, 3]);
62
+ expect(executionOrder).toEqual([1, 2, 3]);
63
+ expect(queue.map.size).toBe(4); // Added sequential-queue
64
+
65
+ // Test 5: Handle async tasks properly
66
+ const asyncTask = async () => {
67
+ await new Promise((resolve) => setTimeout(resolve, 5));
68
+ return "async-result";
69
+ };
70
+
71
+ const asyncResult = await queue.run("async-queue", asyncTask);
72
+ expect(asyncResult).toBe("async-result");
73
+ expect(queue.map.size).toBe(5); // Added async-queue
74
+
75
+ // Test 6: Handle errors in tasks without breaking the queue
76
+ const errorTask = async () => {
77
+ throw new Error("Task failed");
78
+ };
79
+
80
+ const successTask = async () => {
81
+ successCallCount++;
82
+ return `success-${successCallCount}`;
83
+ };
84
+
85
+ await expect(queue.run("error-queue", errorTask)).rejects.toThrow(
86
+ "Task failed"
87
+ );
88
+
89
+ const successResult = await queue.run("error-queue", successTask);
90
+ expect(successResult).toBe("success-1");
91
+ expect(queue.map.size).toBe(6); // Added error-queue
92
+
93
+ return queue;
94
+ },
95
+ });
96
+
97
+ const result = await run(app);
98
+
99
+ // Test 7: Dispose all queues when resource is disposed
100
+ expect(result.value.map.size).toBe(6);
101
+
102
+ await result.dispose();
103
+
104
+ // Try to run a task on a disposed queue - should reject
105
+ await expect(
106
+ result.value.run("test-queue", async () => "test")
107
+ ).rejects.toThrow(/disposed/);
108
+ });
109
+
110
+ it("should propagate task exceptions to the caller", async () => {
111
+ const app = defineResource({
112
+ id: "exception-test-app",
113
+ dependencies: { queue: queueResource },
114
+ async init(_, { queue }) {
115
+ // Test that exceptions from tasks are properly propagated
116
+ const errorTask = async () => {
117
+ throw new Error("Queue resource task error");
118
+ };
119
+
120
+ const successTask = async () => "success";
121
+
122
+ // Exception should be catchable by the caller
123
+ await expect(queue.run("error-queue", errorTask)).rejects.toThrow(
124
+ "Queue resource task error"
125
+ );
126
+
127
+ // Queue should still work for subsequent tasks
128
+ await expect(queue.run("error-queue", successTask)).resolves.toBe(
129
+ "success"
130
+ );
131
+
132
+ // Multiple exceptions should all be catchable
133
+ await expect(queue.run("error-queue", errorTask)).rejects.toThrow(
134
+ "Queue resource task error"
135
+ );
136
+ },
137
+ });
138
+
139
+ await run(app);
140
+ });
141
+ });
@@ -0,0 +1,98 @@
1
+ import { requireContextMiddleware } from "../../globals/middleware/requireContext.middleware";
2
+ import { ContextError } from "../../context";
3
+
4
+ /**
5
+ * Utility function to build a fake Context implementation that allows us to
6
+ * control the behaviour of the `use()` call during each test.
7
+ */
8
+ function createFakeContext<T>(useImplementation: () => T) {
9
+ return {
10
+ /** unique id is irrelevant for tests */
11
+ id: Symbol("fake-context"),
12
+ // `use` is what we care about – we wire whatever behaviour the test needs
13
+ use: jest.fn(useImplementation),
14
+ // The following members are not used by the middleware but are required
15
+ // to satisfy the `Context` interface.
16
+ provide: jest.fn(),
17
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
18
+ require: jest.fn() as any,
19
+ } as any;
20
+ }
21
+
22
+ describe("requireContextMiddleware", () => {
23
+ it("throws if the middleware receives no context in its config", async () => {
24
+ // Arrange → no context passed
25
+ const next = jest.fn();
26
+
27
+ // Act & Assert
28
+ await expect(
29
+ requireContextMiddleware.run({ next } as any, {} as any, {} as any)
30
+ ).rejects.toThrow(
31
+ "Context not available. Did you forget to pass 'context' to the middleware?"
32
+ );
33
+ });
34
+
35
+ it("throws ContextError when the context has not been provided", async () => {
36
+ // Arrange → a context whose `use` returns undefined, simulating missing provider
37
+ const fakeContext = createFakeContext(() => undefined);
38
+ const next = jest.fn();
39
+
40
+ // Act & Assert
41
+ await expect(
42
+ requireContextMiddleware.run(
43
+ { next } as any,
44
+ {} as any,
45
+ { context: fakeContext } as any
46
+ )
47
+ ).rejects.toBeInstanceOf(ContextError);
48
+ });
49
+
50
+ it("passes task.input to next() and returns its result when called within a task", async () => {
51
+ const fakeContext = createFakeContext(() => ({ user: "alice" }));
52
+ const task = { input: "payload" };
53
+ const expectedResult = "task-result";
54
+ const next = jest.fn().mockResolvedValue(expectedResult);
55
+
56
+ const result = await requireContextMiddleware.run(
57
+ { task, next } as any,
58
+ {} as any,
59
+ { context: fakeContext } as any
60
+ );
61
+
62
+ expect(next).toHaveBeenCalledTimes(1);
63
+ expect(next).toHaveBeenCalledWith(task.input);
64
+ expect(result).toBe(expectedResult);
65
+ });
66
+
67
+ it("passes resource.config to next() and returns its result when called within a resource", async () => {
68
+ const fakeContext = createFakeContext(() => ({ user: "bob" }));
69
+ const resource = { config: { url: "https://example.com" } };
70
+ const expectedResult = "resource-result";
71
+ const next = jest.fn().mockResolvedValue(expectedResult);
72
+
73
+ const result = await requireContextMiddleware.run(
74
+ { resource, next } as any,
75
+ {} as any,
76
+ { context: fakeContext } as any
77
+ );
78
+
79
+ expect(next).toHaveBeenCalledTimes(1);
80
+ expect(next).toHaveBeenCalledWith(resource.config);
81
+ expect(result).toBe(expectedResult);
82
+ });
83
+
84
+ it("calls next() with no arguments when neither task nor resource is provided", async () => {
85
+ const fakeContext = createFakeContext(() => ({ user: "charlie" }));
86
+ const next = jest.fn().mockResolvedValue("noop-result");
87
+
88
+ const result = await requireContextMiddleware.run(
89
+ { next } as any,
90
+ {} as any,
91
+ { context: fakeContext } as any
92
+ );
93
+
94
+ expect(next).toHaveBeenCalledTimes(1);
95
+ expect(next).toHaveBeenCalledWith();
96
+ expect(result).toBe("noop-result");
97
+ });
98
+ });
@@ -0,0 +1,157 @@
1
+ import { defineResource, defineTask } from "../../define";
2
+ import { retryMiddleware } from "../../globals/middleware/retry.middleware";
3
+ import { run } from "../../run";
4
+
5
+ describe("Retry Middleware", () => {
6
+ it("should retry failed operations with exponential backoff", async () => {
7
+ let attempt = 0;
8
+ const task = defineTask({
9
+ id: "flakyTask",
10
+ middleware: [
11
+ retryMiddleware.with({
12
+ retries: 3,
13
+ stopRetryIf: (e) => e.message.includes("FATAL"),
14
+ }),
15
+ ],
16
+ run: async () => {
17
+ attempt++;
18
+ if (attempt < 3) throw new Error("Temporary failure");
19
+ return "Success";
20
+ },
21
+ });
22
+
23
+ const app = defineResource({
24
+ id: "app",
25
+ register: [task],
26
+ dependencies: { task },
27
+ async init(_, { task }) {
28
+ const result = await task();
29
+ expect(result).toBe("Success");
30
+ expect(attempt).toBe(3);
31
+ },
32
+ });
33
+
34
+ await run(app);
35
+ });
36
+
37
+ it("should respect stopRetryIf condition", async () => {
38
+ const errorSpy = jest.fn();
39
+ const task = defineTask({
40
+ id: "fatalTask",
41
+ middleware: [
42
+ retryMiddleware.with({
43
+ retries: 3,
44
+ stopRetryIf: (e) => e.message === "FATAL",
45
+ }),
46
+ ],
47
+ run: async () => {
48
+ throw new Error("FATAL");
49
+ },
50
+ });
51
+
52
+ const app = defineResource({
53
+ id: "app",
54
+ register: [task],
55
+ dependencies: { task },
56
+ async init(_, { task }) {
57
+ await expect(task()).rejects.toThrow("FATAL");
58
+ expect(errorSpy).not.toHaveBeenCalled();
59
+ throw new Error("FATAL");
60
+ },
61
+ });
62
+
63
+ await run(app).catch(errorSpy);
64
+ expect(errorSpy).toHaveBeenCalledTimes(1);
65
+ });
66
+
67
+ it("should use custom delay strategy", async () => {
68
+ jest.useFakeTimers();
69
+ const delays: number[] = [];
70
+ const start = Date.now();
71
+
72
+ const task = defineTask({
73
+ id: "delayedTask",
74
+ middleware: [
75
+ retryMiddleware.with({
76
+ retries: 3,
77
+ delayStrategy: (attempt) => (attempt + 1) * 100, // Linear delay
78
+ }),
79
+ ],
80
+ run: async () => {
81
+ throw new Error("Retry me");
82
+ },
83
+ });
84
+
85
+ const app = defineResource({
86
+ id: "app",
87
+ register: [task],
88
+ dependencies: { task },
89
+ async init(_, { task }) {
90
+ task().catch(() => {});
91
+ },
92
+ });
93
+
94
+ await run(app);
95
+
96
+ // Advance timers through all retries
97
+ jest.advanceTimersByTime(1000);
98
+ const elapsed = Date.now() - start;
99
+
100
+ // Verify delay sequence: 100ms, 200ms, 300ms
101
+ expect(elapsed).toBeGreaterThanOrEqual(600); // 100+200+300=600
102
+ jest.useRealTimers();
103
+ });
104
+
105
+ it("should work with resource initialization", async () => {
106
+ let attempts = 0;
107
+ const resource = defineResource({
108
+ id: "flakyResource",
109
+ middleware: [
110
+ retryMiddleware.with({
111
+ retries: 2,
112
+ }),
113
+ ],
114
+ async init() {
115
+ attempts++;
116
+ if (attempts < 2) throw new Error("Resource init failed");
117
+ return "Resource ready";
118
+ },
119
+ });
120
+
121
+ const app = defineResource({
122
+ id: "app",
123
+ register: [resource],
124
+ dependencies: { resource },
125
+ async init(_, { resource }) {
126
+ expect(resource).toBe("Resource ready");
127
+ expect(attempts).toBe(2);
128
+ },
129
+ });
130
+
131
+ await run(app);
132
+ });
133
+
134
+ it("Should default to 3 retries", async () => {
135
+ let attempt = 0;
136
+ const task = defineTask({
137
+ id: "flakyTask",
138
+ middleware: [retryMiddleware],
139
+ run: async () => {
140
+ attempt++;
141
+ throw new Error("Temporary failure");
142
+ },
143
+ });
144
+
145
+ const app = defineResource({
146
+ id: "app",
147
+ register: [task],
148
+ dependencies: { task },
149
+ async init(_, { task }) {
150
+ await expect(task()).rejects.toThrow("Temporary failure");
151
+ },
152
+ });
153
+
154
+ await run(app);
155
+ expect(attempt).toBe(4); // fails once and retries 3 more times, logically
156
+ });
157
+ });
@@ -0,0 +1,55 @@
1
+ import { defineTask, defineResource, defineIndex } from "../define";
2
+ import { run } from "../run";
3
+ import { index } from "../index";
4
+
5
+ describe("index helper", () => {
6
+ it("should aggregate dependencies and expose proper types", async () => {
7
+ const userService = defineResource({
8
+ id: "user.service",
9
+ async init() {
10
+ return "USER";
11
+ },
12
+ });
13
+
14
+ const configuredService = defineResource({
15
+ id: "configured.service",
16
+ async init(config: { name: string }) {
17
+ return config.name;
18
+ },
19
+ });
20
+
21
+ const getUserTask = defineTask({
22
+ id: "task.getUser",
23
+ dependencies: { userService },
24
+ async run(_, { userService }) {
25
+ return userService;
26
+ },
27
+ });
28
+
29
+ // The helper under test
30
+ const services = index({
31
+ userService,
32
+ getUserTask,
33
+ configuredService: configuredService.with({ name: "configured" }),
34
+ });
35
+
36
+ const app = defineResource({
37
+ id: "app",
38
+ register: [services],
39
+ dependencies: { services },
40
+ async init(_, { services }) {
41
+ // Runtime assertions
42
+ expect(services.userService).toBe("USER");
43
+ const result = await services.getUserTask();
44
+ expect(result).toBe("USER");
45
+ expect(services.configuredService).toBe("configured");
46
+ // Type assertions
47
+ services.userService as string;
48
+ // @ts-expect-error – assigning to never should error
49
+ const neverValue: never = "bad";
50
+ },
51
+ });
52
+
53
+ await run(app);
54
+ });
55
+ });
@@ -1,10 +1,10 @@
1
- import { IEvent, IEventDefinition, symbolEvent } from "../../defs";
1
+ import { IEvent, IEventEmission, symbolEvent } from "../../defs";
2
2
  import { Errors } from "../../errors";
3
3
  import { EventManager } from "../../models/EventManager";
4
4
 
5
5
  describe("EventManager", () => {
6
6
  let eventManager: EventManager;
7
- let eventDefinition: IEventDefinition<string>;
7
+ let eventDefinition: IEvent<string>;
8
8
 
9
9
  beforeEach(() => {
10
10
  eventManager = new EventManager();
@@ -69,7 +69,7 @@ describe("EventManager", () => {
69
69
 
70
70
  it("should apply filters correctly", async () => {
71
71
  const handler = jest.fn();
72
- const filter = (event: IEvent<string>) => event.data === "allowed";
72
+ const filter = (event: IEventEmission<string>) => event.data === "allowed";
73
73
 
74
74
  eventManager.addListener(eventDefinition, handler, { filter });
75
75
 
@@ -97,6 +97,7 @@ describe("EventManager", () => {
97
97
  expect.objectContaining({
98
98
  id: "testEvent",
99
99
  data: "testData",
100
+ timestamp: expect.any(Date),
100
101
  })
101
102
  );
102
103
  });
@@ -161,11 +162,11 @@ describe("EventManager", () => {
161
162
  });
162
163
 
163
164
  it("should handle multiple events", async () => {
164
- const eventDef1: IEventDefinition<string> = {
165
+ const eventDef1: IEvent<string> = {
165
166
  id: "event1",
166
167
  [symbolEvent]: true,
167
168
  };
168
- const eventDef2: IEventDefinition<string> = {
169
+ const eventDef2: IEvent<string> = {
169
170
  id: "event2",
170
171
  [symbolEvent]: true,
171
172
  };
@@ -184,6 +185,7 @@ describe("EventManager", () => {
184
185
  expect.objectContaining({
185
186
  id: "event1",
186
187
  data: "data1",
188
+ timestamp: expect.any(Date),
187
189
  })
188
190
  );
189
191
 
@@ -210,11 +212,11 @@ describe("EventManager", () => {
210
212
  });
211
213
 
212
214
  it("should handle listeners added to multiple events", async () => {
213
- const eventDef1: IEventDefinition<string> = {
215
+ const eventDef1: IEvent<string> = {
214
216
  id: "event1",
215
217
  [symbolEvent]: true,
216
218
  };
217
- const eventDef2: IEventDefinition<string> = {
219
+ const eventDef2: IEvent<string> = {
218
220
  id: "event2",
219
221
  [symbolEvent]: true,
220
222
  };
@@ -244,11 +246,11 @@ describe("EventManager", () => {
244
246
  });
245
247
 
246
248
  it("should not affect other events when emitting one", async () => {
247
- const eventDef1: IEventDefinition<string> = {
249
+ const eventDef1: IEvent<string> = {
248
250
  id: "event1",
249
251
  [symbolEvent]: true,
250
252
  };
251
- const eventDef2: IEventDefinition<string> = {
253
+ const eventDef2: IEvent<string> = {
252
254
  id: "event2",
253
255
  [symbolEvent]: true,
254
256
  };
@@ -367,7 +369,7 @@ describe("EventManager", () => {
367
369
 
368
370
  it("should handle filters in global listeners", async () => {
369
371
  const handler = jest.fn();
370
- const filter = (event: IEvent<string>) => event.data === "allowed";
372
+ const filter = (event: IEventEmission<string>) => event.data === "allowed";
371
373
 
372
374
  eventManager.addGlobalListener(handler, { filter });
373
375
 
@@ -387,7 +389,7 @@ describe("EventManager", () => {
387
389
  it("should handle listeners with no data", async () => {
388
390
  const handler = jest.fn();
389
391
 
390
- const voidEventDefinition: IEventDefinition<void> = {
392
+ const voidEventDefinition: IEvent<void> = {
391
393
  id: "voidEvent",
392
394
  [symbolEvent]: true,
393
395
  };
@@ -401,7 +403,151 @@ describe("EventManager", () => {
401
403
  expect.objectContaining({
402
404
  id: "voidEvent",
403
405
  data: undefined,
406
+ timestamp: expect.any(Date),
404
407
  })
405
408
  );
406
409
  });
410
+
411
+ describe("Performance Optimizations", () => {
412
+ it("should cache merged listeners for repeated emits", async () => {
413
+ const handler1 = jest.fn();
414
+ const handler2 = jest.fn();
415
+
416
+ eventManager.addListener(eventDefinition, handler1, { order: 1 });
417
+ eventManager.addGlobalListener(handler2, { order: 2 });
418
+
419
+ // First emit should build cache
420
+ await eventManager.emit(eventDefinition, "test1", "source");
421
+ // Second emit should use cache
422
+ await eventManager.emit(eventDefinition, "test2", "source");
423
+
424
+ expect(handler1).toHaveBeenCalledTimes(2);
425
+ expect(handler2).toHaveBeenCalledTimes(2);
426
+ });
427
+
428
+ it("should invalidate cache when adding event listeners", async () => {
429
+ const handler1 = jest.fn();
430
+ const handler2 = jest.fn();
431
+ const handler3 = jest.fn();
432
+
433
+ eventManager.addListener(eventDefinition, handler1, { order: 2 });
434
+ await eventManager.emit(eventDefinition, "test1", "source");
435
+
436
+ // Add new listener - should invalidate cache for this event
437
+ eventManager.addListener(eventDefinition, handler2, { order: 1 });
438
+ eventManager.addGlobalListener(handler3, { order: 3 });
439
+
440
+ await eventManager.emit(eventDefinition, "test2", "source");
441
+
442
+ expect(handler1).toHaveBeenCalledTimes(2);
443
+ expect(handler2).toHaveBeenCalledTimes(1);
444
+ expect(handler3).toHaveBeenCalledTimes(1);
445
+ });
446
+
447
+ 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 };
450
+
451
+ const handler1 = jest.fn();
452
+ const handler2 = jest.fn();
453
+ const globalHandler = jest.fn();
454
+
455
+ eventManager.addListener(event1, handler1);
456
+ eventManager.addListener(event2, handler2);
457
+
458
+ // Emit to build caches
459
+ await eventManager.emit(event1, "test1", "source");
460
+ await eventManager.emit(event2, "test2", "source");
461
+
462
+ // Add global listener - should invalidate all caches
463
+ eventManager.addGlobalListener(globalHandler);
464
+
465
+ // Emit again - global handler should be called
466
+ await eventManager.emit(event1, "test3", "source");
467
+ await eventManager.emit(event2, "test4", "source");
468
+
469
+ expect(globalHandler).toHaveBeenCalledTimes(2);
470
+ });
471
+
472
+ it("should optimize for empty listener scenarios", async () => {
473
+ const emptyEventDef: IEvent<string> = {
474
+ id: "emptyEvent",
475
+ [symbolEvent]: true,
476
+ };
477
+
478
+ // Should return immediately without creating event object
479
+ await eventManager.emit(emptyEventDef, "test", "source");
480
+
481
+ // No errors should occur
482
+ expect(true).toBe(true);
483
+ });
484
+
485
+ it("should handle high-frequency emissions efficiently", async () => {
486
+ const handler = jest.fn();
487
+ eventManager.addListener(eventDefinition, handler);
488
+ eventManager.addGlobalListener(jest.fn());
489
+
490
+ const emitCount = 1000;
491
+ const startTime = Date.now();
492
+
493
+ for (let i = 0; i < emitCount; i++) {
494
+ await eventManager.emit(eventDefinition, `test${i}`, "source");
495
+ }
496
+
497
+ const endTime = Date.now();
498
+ const duration = endTime - startTime;
499
+
500
+ expect(handler).toHaveBeenCalledTimes(emitCount);
501
+ // Should complete reasonably fast (adjust threshold as needed)
502
+ expect(duration).toBeLessThan(5000); // 5 seconds max for 1000 emissions
503
+ });
504
+
505
+ it("should efficiently handle mixed event and global listeners", async () => {
506
+ const results: string[] = [];
507
+
508
+ // Add many listeners with different orders
509
+ for (let i = 0; i < 10; i++) {
510
+ eventManager.addListener(
511
+ eventDefinition,
512
+ () => results.push(`event${i}`),
513
+ { order: i * 2 }
514
+ );
515
+ eventManager.addGlobalListener(() => results.push(`global${i}`), {
516
+ order: i * 2 + 1,
517
+ });
518
+ }
519
+
520
+ await eventManager.emit(eventDefinition, "test", "source");
521
+
522
+ // Should maintain correct order and call all listeners
523
+ expect(results).toHaveLength(20);
524
+ expect(results[0]).toBe("event0");
525
+ expect(results[1]).toBe("global0");
526
+ expect(results[18]).toBe("event9");
527
+ expect(results[19]).toBe("global9");
528
+ });
529
+
530
+ 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 };
533
+
534
+ const handler1 = jest.fn();
535
+ const handler2 = jest.fn();
536
+ const globalHandler = jest.fn();
537
+
538
+ eventManager.addListener(event1, handler1);
539
+ eventManager.addListener(event2, handler2);
540
+ eventManager.addGlobalListener(globalHandler);
541
+
542
+ // Multiple emits should use cached merged listeners
543
+ await eventManager.emit(event1, "test1", "source");
544
+ await eventManager.emit(event2, "test2", "source");
545
+ await eventManager.emit(event1, "test3", "source");
546
+ await eventManager.emit(event2, "test4", "source");
547
+
548
+ expect(handler1).toHaveBeenCalledTimes(2);
549
+ expect(handler2).toHaveBeenCalledTimes(2);
550
+ expect(globalHandler).toHaveBeenCalledTimes(4);
551
+ });
552
+ });
407
553
  });