@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.
- package/README.md +1315 -942
- package/dist/common.types.d.ts +20 -0
- package/dist/common.types.js +4 -0
- package/dist/common.types.js.map +1 -0
- package/dist/context.d.ts +34 -0
- package/dist/context.js +58 -0
- package/dist/context.js.map +1 -0
- package/dist/define.d.ts +22 -3
- package/dist/define.js +52 -8
- package/dist/define.js.map +1 -1
- package/dist/defs.d.ts +52 -31
- package/dist/defs.js +10 -2
- package/dist/defs.js.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/event.types.d.ts +18 -0
- package/dist/event.types.js +4 -0
- package/dist/event.types.js.map +1 -0
- package/dist/examples/registrator-example.d.ts +122 -0
- package/dist/examples/registrator-example.js +147 -0
- package/dist/examples/registrator-example.js.map +1 -0
- package/dist/globals/globalEvents.d.ts +41 -0
- package/dist/globals/globalEvents.js +94 -0
- package/dist/globals/globalEvents.js.map +1 -0
- package/dist/globals/globalMiddleware.d.ts +23 -0
- package/dist/globals/globalMiddleware.js +15 -0
- package/dist/globals/globalMiddleware.js.map +1 -0
- package/dist/globals/globalResources.d.ts +27 -0
- package/dist/globals/globalResources.js +47 -0
- package/dist/globals/globalResources.js.map +1 -0
- package/dist/globals/middleware/cache.middleware.d.ts +34 -0
- package/dist/globals/middleware/cache.middleware.js +85 -0
- package/dist/globals/middleware/cache.middleware.js.map +1 -0
- package/dist/globals/middleware/requireContext.middleware.d.ts +6 -0
- package/dist/globals/middleware/requireContext.middleware.js +25 -0
- package/dist/globals/middleware/requireContext.middleware.js.map +1 -0
- package/dist/globals/middleware/retry.middleware.d.ts +20 -0
- package/dist/globals/middleware/retry.middleware.js +34 -0
- package/dist/globals/middleware/retry.middleware.js.map +1 -0
- package/dist/globals/resources/queue.resource.d.ts +7 -0
- package/dist/globals/resources/queue.resource.js +31 -0
- package/dist/globals/resources/queue.resource.js.map +1 -0
- package/dist/index.d.ts +45 -9
- package/dist/index.js +14 -9
- package/dist/index.js.map +1 -1
- package/dist/middleware.types.d.ts +40 -0
- package/dist/middleware.types.js +4 -0
- package/dist/middleware.types.js.map +1 -0
- package/dist/models/DependencyProcessor.d.ts +2 -1
- package/dist/models/DependencyProcessor.js +11 -13
- package/dist/models/DependencyProcessor.js.map +1 -1
- package/dist/models/EventManager.d.ts +5 -0
- package/dist/models/EventManager.js +44 -2
- package/dist/models/EventManager.js.map +1 -1
- package/dist/models/Logger.d.ts +30 -12
- package/dist/models/Logger.js +130 -42
- package/dist/models/Logger.js.map +1 -1
- package/dist/models/OverrideManager.d.ts +13 -0
- package/dist/models/OverrideManager.js +70 -0
- package/dist/models/OverrideManager.js.map +1 -0
- package/dist/models/Queue.d.ts +25 -0
- package/dist/models/Queue.js +54 -0
- package/dist/models/Queue.js.map +1 -0
- package/dist/models/ResourceInitializer.d.ts +5 -2
- package/dist/models/ResourceInitializer.js +20 -14
- package/dist/models/ResourceInitializer.js.map +1 -1
- package/dist/models/Semaphore.d.ts +61 -0
- package/dist/models/Semaphore.js +166 -0
- package/dist/models/Semaphore.js.map +1 -0
- package/dist/models/Store.d.ts +17 -72
- package/dist/models/Store.js +71 -269
- package/dist/models/Store.js.map +1 -1
- package/dist/models/StoreConstants.d.ts +11 -0
- package/dist/models/StoreConstants.js +18 -0
- package/dist/models/StoreConstants.js.map +1 -0
- package/dist/models/StoreRegistry.d.ts +25 -0
- package/dist/models/StoreRegistry.js +171 -0
- package/dist/models/StoreRegistry.js.map +1 -0
- package/dist/models/StoreTypes.d.ts +21 -0
- package/dist/models/StoreTypes.js +3 -0
- package/dist/models/StoreTypes.js.map +1 -0
- package/dist/models/StoreValidator.d.ts +10 -0
- package/dist/models/StoreValidator.js +41 -0
- package/dist/models/StoreValidator.js.map +1 -0
- package/dist/models/TaskRunner.js +39 -24
- package/dist/models/TaskRunner.js.map +1 -1
- package/dist/models/VarStore.d.ts +17 -0
- package/dist/models/VarStore.js +60 -0
- package/dist/models/VarStore.js.map +1 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.js +3 -0
- package/dist/models/index.js.map +1 -1
- package/dist/resource.types.d.ts +31 -0
- package/dist/resource.types.js +3 -0
- package/dist/resource.types.js.map +1 -0
- package/dist/run.d.ts +4 -1
- package/dist/run.js +6 -3
- package/dist/run.js.map +1 -1
- package/dist/symbols.d.ts +24 -0
- package/dist/symbols.js +29 -0
- package/dist/symbols.js.map +1 -0
- package/dist/task.types.d.ts +55 -0
- package/dist/task.types.js +23 -0
- package/dist/task.types.js.map +1 -0
- package/dist/tools/registratorId.d.ts +4 -0
- package/dist/tools/registratorId.js +40 -0
- package/dist/tools/registratorId.js.map +1 -0
- package/dist/tools/simpleHash.d.ts +9 -0
- package/dist/tools/simpleHash.js +34 -0
- package/dist/tools/simpleHash.js.map +1 -0
- package/dist/types/base-interfaces.d.ts +18 -0
- package/dist/types/base-interfaces.js +6 -0
- package/dist/types/base-interfaces.js.map +1 -0
- package/dist/types/base.d.ts +13 -0
- package/dist/types/base.js +3 -0
- package/dist/types/base.js.map +1 -0
- package/dist/types/dependencies.d.ts +22 -0
- package/dist/types/dependencies.js +3 -0
- package/dist/types/dependencies.js.map +1 -0
- package/dist/types/dependency-core.d.ts +14 -0
- package/dist/types/dependency-core.js +5 -0
- package/dist/types/dependency-core.js.map +1 -0
- package/dist/types/events.d.ts +52 -0
- package/dist/types/events.js +6 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/hooks.d.ts +16 -0
- package/dist/types/hooks.js +5 -0
- package/dist/types/hooks.js.map +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.js +27 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/meta.d.ts +13 -0
- package/dist/types/meta.js +5 -0
- package/dist/types/meta.js.map +1 -0
- package/dist/types/middleware.d.ts +38 -0
- package/dist/types/middleware.js +6 -0
- package/dist/types/middleware.js.map +1 -0
- package/dist/types/registerable.d.ts +10 -0
- package/dist/types/registerable.js +5 -0
- package/dist/types/registerable.js.map +1 -0
- package/dist/types/resources.d.ts +44 -0
- package/dist/types/resources.js +5 -0
- package/dist/types/resources.js.map +1 -0
- package/dist/types/symbols.d.ts +24 -0
- package/dist/types/symbols.js +30 -0
- package/dist/types/symbols.js.map +1 -0
- package/dist/types/tasks.d.ts +41 -0
- package/dist/types/tasks.js +5 -0
- package/dist/types/tasks.js.map +1 -0
- package/dist/types/utilities.d.ts +7 -0
- package/dist/types/utilities.js +5 -0
- package/dist/types/utilities.js.map +1 -0
- package/package.json +10 -6
- package/src/__tests__/benchmark/benchmark.test.ts +1 -1
- package/src/__tests__/context.test.ts +91 -0
- package/src/__tests__/errors.test.ts +8 -5
- package/src/__tests__/globalEvents.test.ts +1 -1
- package/src/__tests__/globals/cache.middleware.test.ts +772 -0
- package/src/__tests__/globals/queue.resource.test.ts +141 -0
- package/src/__tests__/globals/requireContext.middleware.test.ts +98 -0
- package/src/__tests__/globals/retry.middleware.test.ts +157 -0
- package/src/__tests__/index.helper.test.ts +55 -0
- package/src/__tests__/models/EventManager.test.ts +144 -0
- package/src/__tests__/models/Logger.test.ts +291 -34
- package/src/__tests__/models/Queue.test.ts +189 -0
- package/src/__tests__/models/ResourceInitializer.test.ts +8 -6
- package/src/__tests__/models/Semaphore.test.ts +713 -0
- package/src/__tests__/models/Store.test.ts +40 -0
- package/src/__tests__/models/TaskRunner.test.ts +86 -5
- package/src/__tests__/run.middleware.test.ts +166 -12
- package/src/__tests__/run.overrides.test.ts +13 -10
- package/src/__tests__/run.test.ts +363 -12
- package/src/__tests__/setOutput.test.ts +244 -0
- package/src/__tests__/tools/getCallerFile.test.ts +9 -9
- package/src/__tests__/typesafety.test.ts +54 -39
- package/src/context.ts +86 -0
- package/src/define.ts +84 -14
- package/src/defs.ts +91 -41
- package/src/errors.ts +3 -1
- package/src/{globalEvents.ts → globals/globalEvents.ts} +13 -12
- package/src/globals/globalMiddleware.ts +14 -0
- package/src/{globalResources.ts → globals/globalResources.ts} +14 -10
- package/src/globals/middleware/cache.middleware.ts +115 -0
- package/src/globals/middleware/requireContext.middleware.ts +36 -0
- package/src/globals/middleware/retry.middleware.ts +56 -0
- package/src/globals/resources/queue.resource.ts +34 -0
- package/src/index.ts +9 -5
- package/src/models/DependencyProcessor.ts +36 -40
- package/src/models/EventManager.ts +45 -5
- package/src/models/Logger.ts +170 -48
- package/src/models/OverrideManager.ts +84 -0
- package/src/models/Queue.ts +66 -0
- package/src/models/ResourceInitializer.ts +38 -20
- package/src/models/Semaphore.ts +208 -0
- package/src/models/Store.ts +94 -342
- package/src/models/StoreConstants.ts +17 -0
- package/src/models/StoreRegistry.ts +217 -0
- package/src/models/StoreTypes.ts +46 -0
- package/src/models/StoreValidator.ts +38 -0
- package/src/models/TaskRunner.ts +53 -40
- package/src/models/index.ts +3 -0
- package/src/run.ts +7 -4
- package/src/__tests__/index.ts +0 -15
- 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
|
+
});
|
|
@@ -404,4 +404,148 @@ describe("EventManager", () => {
|
|
|
404
404
|
})
|
|
405
405
|
);
|
|
406
406
|
});
|
|
407
|
+
|
|
408
|
+
describe("Performance Optimizations", () => {
|
|
409
|
+
it("should cache merged listeners for repeated emits", async () => {
|
|
410
|
+
const handler1 = jest.fn();
|
|
411
|
+
const handler2 = jest.fn();
|
|
412
|
+
|
|
413
|
+
eventManager.addListener(eventDefinition, handler1, { order: 1 });
|
|
414
|
+
eventManager.addGlobalListener(handler2, { order: 2 });
|
|
415
|
+
|
|
416
|
+
// First emit should build cache
|
|
417
|
+
await eventManager.emit(eventDefinition, "test1", "source");
|
|
418
|
+
// Second emit should use cache
|
|
419
|
+
await eventManager.emit(eventDefinition, "test2", "source");
|
|
420
|
+
|
|
421
|
+
expect(handler1).toHaveBeenCalledTimes(2);
|
|
422
|
+
expect(handler2).toHaveBeenCalledTimes(2);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("should invalidate cache when adding event listeners", async () => {
|
|
426
|
+
const handler1 = jest.fn();
|
|
427
|
+
const handler2 = jest.fn();
|
|
428
|
+
const handler3 = jest.fn();
|
|
429
|
+
|
|
430
|
+
eventManager.addListener(eventDefinition, handler1, { order: 2 });
|
|
431
|
+
await eventManager.emit(eventDefinition, "test1", "source");
|
|
432
|
+
|
|
433
|
+
// Add new listener - should invalidate cache for this event
|
|
434
|
+
eventManager.addListener(eventDefinition, handler2, { order: 1 });
|
|
435
|
+
eventManager.addGlobalListener(handler3, { order: 3 });
|
|
436
|
+
|
|
437
|
+
await eventManager.emit(eventDefinition, "test2", "source");
|
|
438
|
+
|
|
439
|
+
expect(handler1).toHaveBeenCalledTimes(2);
|
|
440
|
+
expect(handler2).toHaveBeenCalledTimes(1);
|
|
441
|
+
expect(handler3).toHaveBeenCalledTimes(1);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("should invalidate all caches when adding global listeners", async () => {
|
|
445
|
+
const event1: IEventDefinition<string> = { id: "event1", [symbolEvent]: true };
|
|
446
|
+
const event2: IEventDefinition<string> = { id: "event2", [symbolEvent]: true };
|
|
447
|
+
|
|
448
|
+
const handler1 = jest.fn();
|
|
449
|
+
const handler2 = jest.fn();
|
|
450
|
+
const globalHandler = jest.fn();
|
|
451
|
+
|
|
452
|
+
eventManager.addListener(event1, handler1);
|
|
453
|
+
eventManager.addListener(event2, handler2);
|
|
454
|
+
|
|
455
|
+
// Emit to build caches
|
|
456
|
+
await eventManager.emit(event1, "test1", "source");
|
|
457
|
+
await eventManager.emit(event2, "test2", "source");
|
|
458
|
+
|
|
459
|
+
// Add global listener - should invalidate all caches
|
|
460
|
+
eventManager.addGlobalListener(globalHandler);
|
|
461
|
+
|
|
462
|
+
// Emit again - global handler should be called
|
|
463
|
+
await eventManager.emit(event1, "test3", "source");
|
|
464
|
+
await eventManager.emit(event2, "test4", "source");
|
|
465
|
+
|
|
466
|
+
expect(globalHandler).toHaveBeenCalledTimes(2);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("should optimize for empty listener scenarios", async () => {
|
|
470
|
+
const emptyEventDef: IEventDefinition<string> = {
|
|
471
|
+
id: "emptyEvent",
|
|
472
|
+
[symbolEvent]: true,
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// Should return immediately without creating event object
|
|
476
|
+
await eventManager.emit(emptyEventDef, "test", "source");
|
|
477
|
+
|
|
478
|
+
// No errors should occur
|
|
479
|
+
expect(true).toBe(true);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("should handle high-frequency emissions efficiently", async () => {
|
|
483
|
+
const handler = jest.fn();
|
|
484
|
+
eventManager.addListener(eventDefinition, handler);
|
|
485
|
+
eventManager.addGlobalListener(jest.fn());
|
|
486
|
+
|
|
487
|
+
const emitCount = 1000;
|
|
488
|
+
const startTime = Date.now();
|
|
489
|
+
|
|
490
|
+
for (let i = 0; i < emitCount; i++) {
|
|
491
|
+
await eventManager.emit(eventDefinition, `test${i}`, "source");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const endTime = Date.now();
|
|
495
|
+
const duration = endTime - startTime;
|
|
496
|
+
|
|
497
|
+
expect(handler).toHaveBeenCalledTimes(emitCount);
|
|
498
|
+
// Should complete reasonably fast (adjust threshold as needed)
|
|
499
|
+
expect(duration).toBeLessThan(5000); // 5 seconds max for 1000 emissions
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("should efficiently handle mixed event and global listeners", async () => {
|
|
503
|
+
const results: string[] = [];
|
|
504
|
+
|
|
505
|
+
// Add many listeners with different orders
|
|
506
|
+
for (let i = 0; i < 10; i++) {
|
|
507
|
+
eventManager.addListener(
|
|
508
|
+
eventDefinition,
|
|
509
|
+
() => results.push(`event${i}`),
|
|
510
|
+
{ order: i * 2 }
|
|
511
|
+
);
|
|
512
|
+
eventManager.addGlobalListener(
|
|
513
|
+
() => results.push(`global${i}`),
|
|
514
|
+
{ order: i * 2 + 1 }
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
await eventManager.emit(eventDefinition, "test", "source");
|
|
519
|
+
|
|
520
|
+
// Should maintain correct order and call all listeners
|
|
521
|
+
expect(results).toHaveLength(20);
|
|
522
|
+
expect(results[0]).toBe("event0");
|
|
523
|
+
expect(results[1]).toBe("global0");
|
|
524
|
+
expect(results[18]).toBe("event9");
|
|
525
|
+
expect(results[19]).toBe("global9");
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("should reuse cached results across different event types", async () => {
|
|
529
|
+
const event1: IEventDefinition<string> = { id: "event1", [symbolEvent]: true };
|
|
530
|
+
const event2: IEventDefinition<string> = { id: "event2", [symbolEvent]: true };
|
|
531
|
+
|
|
532
|
+
const handler1 = jest.fn();
|
|
533
|
+
const handler2 = jest.fn();
|
|
534
|
+
const globalHandler = jest.fn();
|
|
535
|
+
|
|
536
|
+
eventManager.addListener(event1, handler1);
|
|
537
|
+
eventManager.addListener(event2, handler2);
|
|
538
|
+
eventManager.addGlobalListener(globalHandler);
|
|
539
|
+
|
|
540
|
+
// Multiple emits should use cached merged listeners
|
|
541
|
+
await eventManager.emit(event1, "test1", "source");
|
|
542
|
+
await eventManager.emit(event2, "test2", "source");
|
|
543
|
+
await eventManager.emit(event1, "test3", "source");
|
|
544
|
+
await eventManager.emit(event2, "test4", "source");
|
|
545
|
+
|
|
546
|
+
expect(handler1).toHaveBeenCalledTimes(2);
|
|
547
|
+
expect(handler2).toHaveBeenCalledTimes(2);
|
|
548
|
+
expect(globalHandler).toHaveBeenCalledTimes(4);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
407
551
|
});
|