@bluelibs/runner 2.2.3 → 3.0.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 (204) hide show
  1. package/README.md +1315 -942
  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 +22 -3
  9. package/dist/define.js +52 -8
  10. package/dist/define.js.map +1 -1
  11. package/dist/defs.d.ts +52 -31
  12. package/dist/defs.js +10 -2
  13. package/dist/defs.js.map +1 -1
  14. package/dist/errors.js +1 -1
  15. package/dist/errors.js.map +1 -1
  16. package/dist/event.types.d.ts +18 -0
  17. package/dist/event.types.js +4 -0
  18. package/dist/event.types.js.map +1 -0
  19. package/dist/examples/registrator-example.d.ts +122 -0
  20. package/dist/examples/registrator-example.js +147 -0
  21. package/dist/examples/registrator-example.js.map +1 -0
  22. package/dist/globals/globalEvents.d.ts +41 -0
  23. package/dist/globals/globalEvents.js +94 -0
  24. package/dist/globals/globalEvents.js.map +1 -0
  25. package/dist/globals/globalMiddleware.d.ts +23 -0
  26. package/dist/globals/globalMiddleware.js +15 -0
  27. package/dist/globals/globalMiddleware.js.map +1 -0
  28. package/dist/globals/globalResources.d.ts +27 -0
  29. package/dist/globals/globalResources.js +47 -0
  30. package/dist/globals/globalResources.js.map +1 -0
  31. package/dist/globals/middleware/cache.middleware.d.ts +34 -0
  32. package/dist/globals/middleware/cache.middleware.js +85 -0
  33. package/dist/globals/middleware/cache.middleware.js.map +1 -0
  34. package/dist/globals/middleware/requireContext.middleware.d.ts +6 -0
  35. package/dist/globals/middleware/requireContext.middleware.js +25 -0
  36. package/dist/globals/middleware/requireContext.middleware.js.map +1 -0
  37. package/dist/globals/middleware/retry.middleware.d.ts +20 -0
  38. package/dist/globals/middleware/retry.middleware.js +34 -0
  39. package/dist/globals/middleware/retry.middleware.js.map +1 -0
  40. package/dist/globals/resources/queue.resource.d.ts +7 -0
  41. package/dist/globals/resources/queue.resource.js +31 -0
  42. package/dist/globals/resources/queue.resource.js.map +1 -0
  43. package/dist/index.d.ts +45 -9
  44. package/dist/index.js +14 -9
  45. package/dist/index.js.map +1 -1
  46. package/dist/middleware.types.d.ts +40 -0
  47. package/dist/middleware.types.js +4 -0
  48. package/dist/middleware.types.js.map +1 -0
  49. package/dist/models/DependencyProcessor.d.ts +2 -1
  50. package/dist/models/DependencyProcessor.js +11 -13
  51. package/dist/models/DependencyProcessor.js.map +1 -1
  52. package/dist/models/EventManager.d.ts +5 -0
  53. package/dist/models/EventManager.js +44 -2
  54. package/dist/models/EventManager.js.map +1 -1
  55. package/dist/models/Logger.d.ts +30 -12
  56. package/dist/models/Logger.js +130 -42
  57. package/dist/models/Logger.js.map +1 -1
  58. package/dist/models/OverrideManager.d.ts +13 -0
  59. package/dist/models/OverrideManager.js +70 -0
  60. package/dist/models/OverrideManager.js.map +1 -0
  61. package/dist/models/Queue.d.ts +25 -0
  62. package/dist/models/Queue.js +54 -0
  63. package/dist/models/Queue.js.map +1 -0
  64. package/dist/models/ResourceInitializer.d.ts +5 -2
  65. package/dist/models/ResourceInitializer.js +20 -14
  66. package/dist/models/ResourceInitializer.js.map +1 -1
  67. package/dist/models/Semaphore.d.ts +61 -0
  68. package/dist/models/Semaphore.js +166 -0
  69. package/dist/models/Semaphore.js.map +1 -0
  70. package/dist/models/Store.d.ts +17 -72
  71. package/dist/models/Store.js +71 -269
  72. package/dist/models/Store.js.map +1 -1
  73. package/dist/models/StoreConstants.d.ts +11 -0
  74. package/dist/models/StoreConstants.js +18 -0
  75. package/dist/models/StoreConstants.js.map +1 -0
  76. package/dist/models/StoreRegistry.d.ts +25 -0
  77. package/dist/models/StoreRegistry.js +171 -0
  78. package/dist/models/StoreRegistry.js.map +1 -0
  79. package/dist/models/StoreTypes.d.ts +21 -0
  80. package/dist/models/StoreTypes.js +3 -0
  81. package/dist/models/StoreTypes.js.map +1 -0
  82. package/dist/models/StoreValidator.d.ts +10 -0
  83. package/dist/models/StoreValidator.js +41 -0
  84. package/dist/models/StoreValidator.js.map +1 -0
  85. package/dist/models/TaskRunner.js +39 -24
  86. package/dist/models/TaskRunner.js.map +1 -1
  87. package/dist/models/VarStore.d.ts +17 -0
  88. package/dist/models/VarStore.js +60 -0
  89. package/dist/models/VarStore.js.map +1 -0
  90. package/dist/models/index.d.ts +3 -0
  91. package/dist/models/index.js +3 -0
  92. package/dist/models/index.js.map +1 -1
  93. package/dist/resource.types.d.ts +31 -0
  94. package/dist/resource.types.js +3 -0
  95. package/dist/resource.types.js.map +1 -0
  96. package/dist/run.d.ts +4 -1
  97. package/dist/run.js +6 -3
  98. package/dist/run.js.map +1 -1
  99. package/dist/symbols.d.ts +24 -0
  100. package/dist/symbols.js +29 -0
  101. package/dist/symbols.js.map +1 -0
  102. package/dist/task.types.d.ts +55 -0
  103. package/dist/task.types.js +23 -0
  104. package/dist/task.types.js.map +1 -0
  105. package/dist/tools/registratorId.d.ts +4 -0
  106. package/dist/tools/registratorId.js +40 -0
  107. package/dist/tools/registratorId.js.map +1 -0
  108. package/dist/tools/simpleHash.d.ts +9 -0
  109. package/dist/tools/simpleHash.js +34 -0
  110. package/dist/tools/simpleHash.js.map +1 -0
  111. package/dist/types/base-interfaces.d.ts +18 -0
  112. package/dist/types/base-interfaces.js +6 -0
  113. package/dist/types/base-interfaces.js.map +1 -0
  114. package/dist/types/base.d.ts +13 -0
  115. package/dist/types/base.js +3 -0
  116. package/dist/types/base.js.map +1 -0
  117. package/dist/types/dependencies.d.ts +22 -0
  118. package/dist/types/dependencies.js +3 -0
  119. package/dist/types/dependencies.js.map +1 -0
  120. package/dist/types/dependency-core.d.ts +14 -0
  121. package/dist/types/dependency-core.js +5 -0
  122. package/dist/types/dependency-core.js.map +1 -0
  123. package/dist/types/events.d.ts +52 -0
  124. package/dist/types/events.js +6 -0
  125. package/dist/types/events.js.map +1 -0
  126. package/dist/types/hooks.d.ts +16 -0
  127. package/dist/types/hooks.js +5 -0
  128. package/dist/types/hooks.js.map +1 -0
  129. package/dist/types/index.d.ts +14 -0
  130. package/dist/types/index.js +27 -0
  131. package/dist/types/index.js.map +1 -0
  132. package/dist/types/meta.d.ts +13 -0
  133. package/dist/types/meta.js +5 -0
  134. package/dist/types/meta.js.map +1 -0
  135. package/dist/types/middleware.d.ts +38 -0
  136. package/dist/types/middleware.js +6 -0
  137. package/dist/types/middleware.js.map +1 -0
  138. package/dist/types/registerable.d.ts +10 -0
  139. package/dist/types/registerable.js +5 -0
  140. package/dist/types/registerable.js.map +1 -0
  141. package/dist/types/resources.d.ts +44 -0
  142. package/dist/types/resources.js +5 -0
  143. package/dist/types/resources.js.map +1 -0
  144. package/dist/types/symbols.d.ts +24 -0
  145. package/dist/types/symbols.js +30 -0
  146. package/dist/types/symbols.js.map +1 -0
  147. package/dist/types/tasks.d.ts +41 -0
  148. package/dist/types/tasks.js +5 -0
  149. package/dist/types/tasks.js.map +1 -0
  150. package/dist/types/utilities.d.ts +7 -0
  151. package/dist/types/utilities.js +5 -0
  152. package/dist/types/utilities.js.map +1 -0
  153. package/package.json +10 -6
  154. package/src/__tests__/benchmark/benchmark.test.ts +1 -1
  155. package/src/__tests__/context.test.ts +91 -0
  156. package/src/__tests__/errors.test.ts +8 -5
  157. package/src/__tests__/globalEvents.test.ts +1 -1
  158. package/src/__tests__/globals/cache.middleware.test.ts +772 -0
  159. package/src/__tests__/globals/queue.resource.test.ts +141 -0
  160. package/src/__tests__/globals/requireContext.middleware.test.ts +98 -0
  161. package/src/__tests__/globals/retry.middleware.test.ts +157 -0
  162. package/src/__tests__/index.helper.test.ts +55 -0
  163. package/src/__tests__/models/EventManager.test.ts +144 -0
  164. package/src/__tests__/models/Logger.test.ts +291 -34
  165. package/src/__tests__/models/Queue.test.ts +189 -0
  166. package/src/__tests__/models/ResourceInitializer.test.ts +8 -6
  167. package/src/__tests__/models/Semaphore.test.ts +713 -0
  168. package/src/__tests__/models/Store.test.ts +40 -0
  169. package/src/__tests__/models/TaskRunner.test.ts +86 -5
  170. package/src/__tests__/run.middleware.test.ts +166 -12
  171. package/src/__tests__/run.overrides.test.ts +13 -10
  172. package/src/__tests__/run.test.ts +363 -12
  173. package/src/__tests__/setOutput.test.ts +244 -0
  174. package/src/__tests__/tools/getCallerFile.test.ts +9 -9
  175. package/src/__tests__/typesafety.test.ts +54 -39
  176. package/src/context.ts +86 -0
  177. package/src/define.ts +84 -14
  178. package/src/defs.ts +91 -41
  179. package/src/errors.ts +3 -1
  180. package/src/{globalEvents.ts → globals/globalEvents.ts} +13 -12
  181. package/src/globals/globalMiddleware.ts +14 -0
  182. package/src/{globalResources.ts → globals/globalResources.ts} +14 -10
  183. package/src/globals/middleware/cache.middleware.ts +115 -0
  184. package/src/globals/middleware/requireContext.middleware.ts +36 -0
  185. package/src/globals/middleware/retry.middleware.ts +56 -0
  186. package/src/globals/resources/queue.resource.ts +34 -0
  187. package/src/index.ts +9 -5
  188. package/src/models/DependencyProcessor.ts +36 -40
  189. package/src/models/EventManager.ts +45 -5
  190. package/src/models/Logger.ts +170 -48
  191. package/src/models/OverrideManager.ts +84 -0
  192. package/src/models/Queue.ts +66 -0
  193. package/src/models/ResourceInitializer.ts +38 -20
  194. package/src/models/Semaphore.ts +208 -0
  195. package/src/models/Store.ts +94 -342
  196. package/src/models/StoreConstants.ts +17 -0
  197. package/src/models/StoreRegistry.ts +217 -0
  198. package/src/models/StoreTypes.ts +46 -0
  199. package/src/models/StoreValidator.ts +38 -0
  200. package/src/models/TaskRunner.ts +53 -40
  201. package/src/models/index.ts +3 -0
  202. package/src/run.ts +7 -4
  203. package/src/__tests__/index.ts +0 -15
  204. package/src/examples/express-mongo/index.ts +0 -1
@@ -1,7 +1,6 @@
1
1
  import { Logger, ILog, LogLevels } from "../../models/Logger";
2
2
  import { EventManager } from "../../models/EventManager";
3
- import { globalEvents } from "../../globalEvents";
4
- import { mock } from "node:test";
3
+ import { globalEvents } from "../../globals/globalEvents";
5
4
 
6
5
  describe("Logger", () => {
7
6
  let logger: Logger;
@@ -16,15 +15,19 @@ describe("Logger", () => {
16
15
  it("should emit a log event with correct data", async () => {
17
16
  const testData = "Test log message";
18
17
  const testLevel = "info";
19
- mockEventManager.emit = jest.fn();
18
+ mockEventManager.emit = jest.fn().mockResolvedValue(undefined);
19
+ mockEventManager.hasListeners = jest.fn().mockReturnValue(true);
20
20
 
21
- await logger.log(testLevel, testData);
21
+ logger.log(testLevel, testData);
22
+
23
+ // Wait for setImmediate to execute
24
+ await new Promise(setImmediate);
22
25
 
23
26
  expect(mockEventManager.emit).toHaveBeenCalledWith(
24
27
  globalEvents.log,
25
28
  expect.objectContaining({
26
29
  level: testLevel,
27
- data: testData,
30
+ message: testData,
28
31
  timestamp: expect.any(Date),
29
32
  }),
30
33
  "unknown"
@@ -41,21 +44,28 @@ describe("Logger", () => {
41
44
  "critical",
42
45
  ];
43
46
 
44
- mockEventManager.emit = jest.fn();
47
+ mockEventManager.emit = jest.fn().mockResolvedValue(undefined);
48
+ mockEventManager.hasListeners = jest.fn().mockReturnValue(true);
45
49
 
46
50
  for (const level of levels) {
47
- await logger.log(level, `Test ${level} message`, "testSource");
48
-
49
- expect(mockEventManager.emit).toHaveBeenCalledWith(
50
- globalEvents.log,
51
- expect.objectContaining({
52
- level,
53
- data: `Test ${level} message`,
54
- timestamp: expect.any(Date),
55
- }),
56
- "testSource"
57
- );
51
+ logger.log(level, `Test ${level} message`, { source: "testSource" });
58
52
  }
53
+
54
+ // Wait for setImmediate to execute
55
+ await new Promise(setImmediate);
56
+
57
+ // Check all calls were made
58
+ expect(mockEventManager.emit).toHaveBeenCalledTimes(levels.length);
59
+ });
60
+
61
+ it("should not emit events when there are no listeners", () => {
62
+ mockEventManager.emit = jest.fn().mockResolvedValue(undefined);
63
+ mockEventManager.hasListeners = jest.fn().mockReturnValue(false);
64
+
65
+ logger.log("info", "Test message");
66
+
67
+ // Should not emit events when no listeners
68
+ expect(mockEventManager.emit).not.toHaveBeenCalled();
59
69
  });
60
70
  });
61
71
 
@@ -74,47 +84,179 @@ describe("Logger", () => {
74
84
  const testLog: ILog = {
75
85
  level: "info",
76
86
  source: "test",
77
- data: "Test log message",
87
+ message: "Test log message",
78
88
  timestamp: new Date("2023-01-01T00:00:00Z"),
79
89
  };
80
90
 
81
91
  await logger.print(testLog);
82
92
 
83
93
  expect(consoleLogSpy).toHaveBeenCalledWith(
84
- expect.stringContaining("[INFO] (test) - Test log message")
94
+ expect.stringContaining("INFO")
85
95
  );
86
96
  });
87
97
 
88
- it("should handle Error objects in log data", async () => {
98
+ it("should handle Error objects in log data", () => {
89
99
  const testError = new Error("Test error");
90
100
  const testLog: ILog = {
91
101
  level: "error",
92
- data: testError,
102
+ message: "Operation failed",
103
+ error: {
104
+ name: testError.name,
105
+ message: testError.message,
106
+ stack: testError.stack,
107
+ },
93
108
  timestamp: new Date("2023-01-01T00:00:00Z"),
94
109
  };
95
110
 
96
- await logger.print(testLog);
111
+ logger.print(testLog);
112
+
113
+ expect(consoleLogSpy).toHaveBeenCalledWith(
114
+ expect.stringContaining("ERROR")
115
+ );
116
+ expect(consoleLogSpy).toHaveBeenCalledWith(
117
+ expect.stringContaining("Error: Error")
118
+ );
119
+ expect(consoleLogSpy).toHaveBeenCalledWith(
120
+ expect.stringContaining("Test error")
121
+ );
122
+ });
97
123
 
124
+ it("should handle error objects without stack trace", () => {
125
+ const testLog: ILog = {
126
+ level: "error",
127
+ message: "Operation failed",
128
+ error: {
129
+ name: "CustomError",
130
+ message: "Something went wrong",
131
+ },
132
+ timestamp: new Date("2023-01-01T00:00:00Z"),
133
+ };
134
+
135
+ logger.print(testLog);
136
+
137
+ expect(consoleLogSpy).toHaveBeenCalledWith(
138
+ expect.stringContaining("ERROR")
139
+ );
98
140
  expect(consoleLogSpy).toHaveBeenCalledWith(
99
- expect.stringContaining("Error: Error - Test error")
141
+ expect.stringContaining("Error: CustomError")
100
142
  );
101
143
  expect(consoleLogSpy).toHaveBeenCalledWith(
102
- expect.stringContaining("Stack Trace:")
144
+ expect.stringContaining("Something went wrong")
103
145
  );
104
146
  });
105
147
 
106
- it("should pretty-print JSON objects in log data", async () => {
148
+ it("should pretty-print structured data", () => {
107
149
  const testObject = { key: "value", nested: { foo: "bar" } };
108
150
  const testLog: ILog = {
109
151
  level: "debug",
152
+ message: "Debug info",
110
153
  data: testObject,
111
154
  timestamp: new Date("2023-01-01T00:00:00Z"),
112
155
  };
113
156
 
114
- await logger.print(testLog);
157
+ logger.print(testLog);
158
+
159
+ expect(consoleLogSpy).toHaveBeenCalledWith(
160
+ expect.stringContaining("DEBUG")
161
+ );
162
+ expect(consoleLogSpy).toHaveBeenCalledWith(
163
+ expect.stringContaining('"key": "value"')
164
+ );
165
+ });
166
+
167
+ it("should handle object message by stringifying it", () => {
168
+ const objectMessage = { type: "user", action: "login" };
169
+ const testLog: ILog = {
170
+ level: "info",
171
+ message: objectMessage as any,
172
+ timestamp: new Date("2023-01-01T00:00:00Z"),
173
+ };
174
+
175
+ logger.print(testLog);
176
+
177
+ expect(consoleLogSpy).toHaveBeenCalledWith(
178
+ expect.stringContaining("INFO")
179
+ );
180
+ expect(consoleLogSpy).toHaveBeenCalledWith(
181
+ expect.stringContaining(JSON.stringify(objectMessage, null, 2))
182
+ );
183
+ });
184
+
185
+ it("should print context information when present", () => {
186
+ const context = { userId: "123", requestId: "abc-def" };
187
+ const testLog: ILog = {
188
+ level: "warn",
189
+ message: "Warning message",
190
+ data: context,
191
+ timestamp: new Date("2023-01-01T00:00:00Z"),
192
+ };
193
+
194
+ logger.print(testLog);
195
+
196
+ expect(consoleLogSpy).toHaveBeenCalledWith(
197
+ expect.stringContaining("WARN")
198
+ );
199
+ expect(consoleLogSpy).toHaveBeenCalledWith(
200
+ expect.stringContaining('"userId": "123"')
201
+ );
202
+ });
203
+
204
+ it("should print context from context field", () => {
205
+ const context = { userId: "456", feature: "login", source: "auth" };
206
+ const testLog: ILog = {
207
+ level: "info",
208
+ message: "User action",
209
+ context: context,
210
+ timestamp: new Date("2023-01-01T00:00:00Z"),
211
+ };
212
+
213
+ logger.print(testLog);
214
+
215
+ expect(consoleLogSpy).toHaveBeenCalledWith(
216
+ expect.stringContaining("INFO")
217
+ );
218
+ // Should show context but filter out 'source' since it's shown separately
219
+ expect(consoleLogSpy).toHaveBeenCalledWith(
220
+ expect.stringContaining("Context:")
221
+ );
222
+ expect(consoleLogSpy).toHaveBeenCalledWith(
223
+ expect.stringContaining('"userId": "456"')
224
+ );
225
+ expect(consoleLogSpy).toHaveBeenCalledWith(
226
+ expect.stringContaining('"feature": "login"')
227
+ );
228
+ });
229
+
230
+ it("should handle empty context gracefully", () => {
231
+ const testLog: ILog = {
232
+ level: "debug",
233
+ message: "Debug message",
234
+ context: {},
235
+ timestamp: new Date("2023-01-01T00:00:00Z"),
236
+ };
237
+
238
+ logger.print(testLog);
239
+
240
+ expect(consoleLogSpy).toHaveBeenCalledWith(
241
+ expect.stringContaining("DEBUG")
242
+ );
243
+ // Should not print context section for empty context
244
+ expect(consoleLogSpy).not.toHaveBeenCalledWith(
245
+ expect.stringContaining("Context:")
246
+ );
247
+ });
248
+
249
+ it("should handle unknown log levels gracefully", () => {
250
+ const testLog: ILog = {
251
+ level: "unknown" as any,
252
+ message: "Unknown level message",
253
+ timestamp: new Date("2023-01-01T00:00:00Z"),
254
+ };
255
+
256
+ logger.print(testLog);
115
257
 
116
258
  expect(consoleLogSpy).toHaveBeenCalledWith(
117
- expect.stringContaining(JSON.stringify(testObject, null, 2))
259
+ expect.stringContaining("UNKNOWN")
118
260
  );
119
261
  });
120
262
  });
@@ -130,21 +272,123 @@ describe("Logger", () => {
130
272
  ];
131
273
 
132
274
  for (const level of testLevels) {
133
- it(`should call log method with ${level} level`, async () => {
275
+ it(`should call log method with ${level} level`, () => {
134
276
  const logSpy = jest.spyOn(logger, "log").mockImplementation();
135
277
 
136
- await logger[level]("Test log message");
278
+ logger[level]("Test log message");
137
279
 
138
280
  expect(logSpy).toHaveBeenCalledWith(
139
281
  level,
140
282
  "Test log message",
141
- undefined // the context parameter
283
+ {} // the LogInfo parameter
142
284
  );
143
285
  });
144
286
  }
145
287
  });
146
288
 
147
- it("should auto-print logs based on autoPrintLogsAfter option", async () => {
289
+ describe("with method", () => {
290
+ it("should create a new logger with additional context", () => {
291
+ const initialContext = { source: "initial" };
292
+ const loggerWithContext = new Logger(mockEventManager, initialContext);
293
+
294
+ const additionalContext = { userId: "123", feature: "auth" };
295
+ const newLogger = loggerWithContext.with(additionalContext);
296
+
297
+ expect(newLogger).toBeInstanceOf(Logger);
298
+ expect(newLogger).not.toBe(loggerWithContext);
299
+
300
+ // Test that the context is merged by checking the log call
301
+ const logSpy = jest.spyOn(newLogger, "log").mockImplementation();
302
+ newLogger.info("test message");
303
+
304
+ expect(logSpy).toHaveBeenCalledWith("info", "test message", {});
305
+ });
306
+
307
+ it("should override context values when keys overlap", () => {
308
+ const initialContext = { source: "initial", common: "old" };
309
+ const loggerWithContext = new Logger(mockEventManager, initialContext);
310
+
311
+ const newContext = { source: "override", newProp: "value" };
312
+ const newLogger = loggerWithContext.with(newContext);
313
+
314
+ // The new logger should have the overridden context
315
+ expect(newLogger).toBeInstanceOf(Logger);
316
+ });
317
+
318
+ it("should use bound context in log calls", async () => {
319
+ const boundContext = { source: "testSource", userId: "123" };
320
+ const loggerWithContext = new Logger(mockEventManager, boundContext);
321
+
322
+ mockEventManager.emit = jest.fn().mockResolvedValue(undefined);
323
+ mockEventManager.hasListeners = jest.fn().mockReturnValue(true);
324
+
325
+ loggerWithContext.log("info", "Test message");
326
+
327
+ await new Promise(setImmediate);
328
+
329
+ expect(mockEventManager.emit).toHaveBeenCalledWith(
330
+ globalEvents.log,
331
+ expect.objectContaining({
332
+ source: "testSource",
333
+ context: boundContext,
334
+ }),
335
+ "testSource"
336
+ );
337
+ });
338
+ });
339
+
340
+ describe("error handling", () => {
341
+ it("should handle event emission errors gracefully", async () => {
342
+ const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
343
+ const emitError = new Error("Event emission failed");
344
+
345
+ mockEventManager.emit = jest.fn().mockRejectedValue(emitError);
346
+ mockEventManager.hasListeners = jest.fn().mockReturnValue(true);
347
+
348
+ logger.log("error", "Test error message");
349
+
350
+ // Wait for setImmediate and promise rejection to be handled
351
+ await new Promise(setImmediate);
352
+ await new Promise((resolve) => setTimeout(resolve, 0));
353
+
354
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
355
+ "Logger event emission failed:",
356
+ emitError
357
+ );
358
+
359
+ consoleErrorSpy.mockRestore();
360
+ });
361
+
362
+ it("should extract error information from Error objects", async () => {
363
+ mockEventManager.emit = jest.fn().mockResolvedValue(undefined);
364
+ mockEventManager.hasListeners = jest.fn().mockReturnValue(true);
365
+
366
+ const testError = new Error("Test error message");
367
+ testError.name = "CustomError";
368
+
369
+ logger.log("error", "Operation failed", { error: testError });
370
+
371
+ // Wait for setImmediate to execute
372
+ await new Promise(setImmediate);
373
+
374
+ expect(mockEventManager.emit).toHaveBeenCalledWith(
375
+ globalEvents.log,
376
+ expect.objectContaining({
377
+ level: "error",
378
+ message: "Operation failed",
379
+ error: {
380
+ name: "CustomError",
381
+ message: "Test error message",
382
+ stack: expect.any(String),
383
+ },
384
+ timestamp: expect.any(Date),
385
+ }),
386
+ "unknown"
387
+ );
388
+ });
389
+ });
390
+
391
+ it("should auto-print logs based on autoPrintLogsAfter option", () => {
148
392
  const autoPrintLevel: LogLevels = "warn";
149
393
  logger.setPrintThreshold(autoPrintLevel);
150
394
  const consoleLogSpy = jest.spyOn(console, "log").mockImplementation();
@@ -160,7 +404,7 @@ describe("Logger", () => {
160
404
 
161
405
  for (const level of levels) {
162
406
  logger.setPrintThreshold(level);
163
- await logger.log(level, `Test ${level} message`);
407
+ logger.log(level, `Test ${level} message`);
164
408
 
165
409
  expect(consoleLogSpy).toHaveBeenCalledWith(
166
410
  expect.stringContaining(`Test ${level} message`)
@@ -169,8 +413,8 @@ describe("Logger", () => {
169
413
 
170
414
  // ensure events with a higher level thatn auto print level are printed, and lower levels are not
171
415
  logger.setPrintThreshold("error");
172
- await logger.log("info", "xx Test info message");
173
- await logger.log("error", "xx Test error message");
416
+ logger.log("info", "xx Test info message");
417
+ logger.log("error", "xx Test error message");
174
418
 
175
419
  expect(consoleLogSpy).toHaveBeenCalledWith(
176
420
  expect.stringContaining("xx Test error message")
@@ -182,4 +426,17 @@ describe("Logger", () => {
182
426
 
183
427
  consoleLogSpy.mockRestore();
184
428
  });
429
+
430
+ it("should disable auto-printing when setPrintThreshold is set to null", () => {
431
+ const consoleLogSpy = jest.spyOn(console, "log").mockImplementation();
432
+
433
+ // Set to null to disable auto-printing
434
+ logger.setPrintThreshold(null);
435
+
436
+ logger.log("error", "Should not be printed");
437
+
438
+ expect(consoleLogSpy).not.toHaveBeenCalled();
439
+
440
+ consoleLogSpy.mockRestore();
441
+ });
185
442
  });
@@ -0,0 +1,189 @@
1
+ // queue.test.ts
2
+ import { Queue } from "../.."; // <-- adjust path if needed
3
+
4
+ /* ------------------------------------------------------------------ */
5
+ /* Helpers */
6
+ /* ------------------------------------------------------------------ */
7
+
8
+ /** Flush native micro‑tasks once (Promise jobs / process.nextTick). */
9
+ const flushMicroTasks = () => Promise.resolve();
10
+
11
+ /** Small delay helper for tests */
12
+ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
13
+
14
+ /* ------------------------------------------------------------------ */
15
+ /* Tests */
16
+ /* ------------------------------------------------------------------ */
17
+
18
+ describe("Queue", () => {
19
+ it("runs tasks sequentially and returns their results in order", async () => {
20
+ const q = new Queue();
21
+
22
+ const started: number[] = [];
23
+ const finished: number[] = [];
24
+ let concurrent = 0;
25
+ let maxConcurrent = 0;
26
+
27
+ const taskFactory = (id: number) => async () => {
28
+ started.push(id);
29
+ concurrent++;
30
+ maxConcurrent = Math.max(maxConcurrent, concurrent);
31
+
32
+ // simulate async work with a real small delay
33
+ await delay(1);
34
+
35
+ finished.push(id);
36
+ concurrent--;
37
+ return id;
38
+ };
39
+
40
+ const p1 = q.run(taskFactory(1));
41
+ const p2 = q.run(taskFactory(2));
42
+ const p3 = q.run(taskFactory(3));
43
+
44
+ const results = await Promise.all([p1, p2, p3]);
45
+
46
+ expect(results).toEqual([1, 2, 3]);
47
+ expect(started).toEqual([1, 2, 3]);
48
+ expect(finished).toEqual([1, 2, 3]);
49
+ expect(maxConcurrent).toBe(1); // never overlapped
50
+ });
51
+
52
+ it("detects dead‑lock when a queued task enqueues another task", async () => {
53
+ const q = new Queue();
54
+
55
+ const deadlock = () => q.run(async () => "nested"); // <-- illegal
56
+
57
+ await expect(q.run(deadlock)).rejects.toThrow(/Dead‑lock/);
58
+ });
59
+
60
+ it("dispose() drains pending tasks and rejects new ones", async () => {
61
+ const q = new Queue();
62
+
63
+ const task = async () => {
64
+ await delay(1);
65
+ return "ok";
66
+ };
67
+
68
+ const p1 = q.run(task);
69
+ const p2 = q.run(task);
70
+
71
+ const disposeDone = q.dispose(); // default => { cancel: false }
72
+
73
+ await expect(disposeDone).resolves.toBeUndefined();
74
+ await expect(p1).resolves.toBe("ok");
75
+ await expect(p2).resolves.toBe("ok");
76
+
77
+ await expect(q.run(task)).rejects.toThrow(/disposed/);
78
+ });
79
+
80
+ it("dispose({ cancel: true }) aborts the running task", async () => {
81
+ jest.useFakeTimers();
82
+
83
+ const q = new Queue();
84
+
85
+ /** Long‑running task that cooperates with AbortSignal. */
86
+ const longTask = async (signal: AbortSignal) =>
87
+ new Promise<void>((_res, rej) => {
88
+ const tid = setTimeout(_res, 100); // never actually fires
89
+ signal.addEventListener("abort", () => {
90
+ clearTimeout(tid);
91
+ rej(new Error("aborted"));
92
+ });
93
+ });
94
+
95
+ const p = q.run(longTask);
96
+
97
+ // Let the task start
98
+ jest.advanceTimersByTime(0);
99
+ await flushMicroTasks();
100
+
101
+ const disposeDone = q.dispose({ cancel: true });
102
+
103
+ await flushMicroTasks(); // allow rejection to propagate
104
+
105
+ await expect(p).rejects.toThrow(/aborted/);
106
+ await expect(disposeDone).resolves.toBeUndefined();
107
+
108
+ jest.useRealTimers();
109
+ });
110
+
111
+ it("dispose() is idempotent - multiple calls should be safe", async () => {
112
+ const q = new Queue();
113
+
114
+ const task = async () => {
115
+ await delay(1);
116
+ return "ok";
117
+ };
118
+
119
+ const p = q.run(task);
120
+
121
+ // First dispose call
122
+ const dispose1 = q.dispose();
123
+ // Second dispose call should return immediately (if (this.disposed) return;)
124
+ const dispose2 = q.dispose();
125
+ // Third dispose call should also return immediately
126
+ const dispose3 = q.dispose();
127
+
128
+ await expect(dispose1).resolves.toBeUndefined();
129
+ await expect(dispose2).resolves.toBeUndefined();
130
+ await expect(dispose3).resolves.toBeUndefined();
131
+ await expect(p).resolves.toBe("ok");
132
+
133
+ // Further dispose calls after everything is settled should also be safe
134
+ const dispose4 = q.dispose();
135
+ await expect(dispose4).resolves.toBeUndefined();
136
+ });
137
+
138
+ it("dispose() properly handles rejected tail promises", async () => {
139
+ const q = new Queue();
140
+
141
+ // Run a task first to establish the normal tail chain
142
+ await q.run(async () => "setup");
143
+
144
+ // Directly set the tail to a promise that will reject
145
+ // This simulates an internal error scenario where the tail becomes rejected
146
+ const rejectingPromise = Promise.reject(
147
+ new Error("Simulated tail rejection")
148
+ );
149
+ (q as any).tail = rejectingPromise;
150
+
151
+ // Spy on the rejecting promise to verify the catch is called
152
+ const catchSpy = jest.spyOn(rejectingPromise, "catch");
153
+
154
+ // The dispose method should handle this rejection with: await this.tail.catch(() => {})
155
+ // This specifically tests the line: await this.tail.catch(() => {});
156
+ await expect(q.dispose()).resolves.toBeUndefined();
157
+
158
+ // Verify that the catch method was called (meaning the tail.catch() line was executed)
159
+ expect(catchSpy).toHaveBeenCalledWith(expect.any(Function));
160
+
161
+ // Verify the queue is properly disposed
162
+ await expect(q.run(async () => "test")).rejects.toThrow(/disposed/);
163
+ });
164
+
165
+ it("should propagate task exceptions to the caller", async () => {
166
+ const q = new Queue();
167
+
168
+ // Test that exceptions are propagated, not swallowed
169
+ const errorTask = async () => {
170
+ await delay(1);
171
+ throw new Error("Task exception");
172
+ };
173
+
174
+ const successTask = async () => {
175
+ await delay(1);
176
+ return "success";
177
+ };
178
+
179
+ // Exception should be catchable by the caller
180
+ await expect(q.run(errorTask)).rejects.toThrow("Task exception");
181
+
182
+ // Queue should still work for subsequent tasks
183
+ await expect(q.run(successTask)).resolves.toBe("success");
184
+
185
+ // Multiple exceptions should all be catchable
186
+ await expect(q.run(errorTask)).rejects.toThrow("Task exception");
187
+ await expect(q.run(errorTask)).rejects.toThrow("Task exception");
188
+ });
189
+ });
@@ -3,8 +3,8 @@ import { Store } from "../../models/Store";
3
3
  import { EventManager } from "../../models/EventManager";
4
4
  import { defineResource } from "../../define";
5
5
  import { Logger } from "../../models";
6
- import { globalResources } from "../../globalResources";
7
- import { globalEvents } from "../../globalEvents";
6
+ import { globalResources } from "../../globals/globalResources";
7
+ import { globalEvents } from "../../globals/globalEvents";
8
8
 
9
9
  describe("ResourceInitializer", () => {
10
10
  let store: Store;
@@ -36,10 +36,11 @@ describe("ResourceInitializer", () => {
36
36
  mockDependencies
37
37
  );
38
38
 
39
- expect(result).toBe("initialized value");
39
+ expect(result).toEqual({ value: "initialized value", context: undefined });
40
40
  expect(mockResource.init).toHaveBeenCalledWith(
41
41
  mockConfig,
42
- mockDependencies
42
+ mockDependencies,
43
+ undefined
43
44
  );
44
45
 
45
46
  expect(emitSpy).toHaveBeenCalledWith(
@@ -91,7 +92,8 @@ describe("ResourceInitializer", () => {
91
92
  expect(result).toBeUndefined();
92
93
  expect(mockResource.init).toHaveBeenCalledWith(
93
94
  mockConfig,
94
- mockDependencies
95
+ mockDependencies,
96
+ undefined
95
97
  );
96
98
  expect(emitSpy).toHaveBeenCalledWith(
97
99
  mockResource.events.beforeInit,
@@ -126,7 +128,7 @@ describe("ResourceInitializer", () => {
126
128
  mockDependencies
127
129
  );
128
130
 
129
- expect(result).toBeUndefined();
131
+ expect(result).toEqual({ value: undefined, context: undefined });
130
132
  expect(emitSpy).toHaveBeenCalledWith(
131
133
  mockResource.events.beforeInit,
132
134
  {