@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,772 @@
|
|
|
1
|
+
import { defineTask, defineResource } from "../../define";
|
|
2
|
+
import { run } from "../../run";
|
|
3
|
+
import {
|
|
4
|
+
cacheResource,
|
|
5
|
+
cacheMiddleware,
|
|
6
|
+
cacheFactoryTask,
|
|
7
|
+
ICacheInstance,
|
|
8
|
+
} from "../../globals/middleware/cache.middleware";
|
|
9
|
+
import { LRUCache } from "lru-cache";
|
|
10
|
+
|
|
11
|
+
describe("Caching System", () => {
|
|
12
|
+
describe("Cache Resource", () => {
|
|
13
|
+
it("should initialize with default cache factory task", async () => {
|
|
14
|
+
const app = defineResource({
|
|
15
|
+
id: "app",
|
|
16
|
+
register: [cacheResource, cacheMiddleware],
|
|
17
|
+
dependencies: { cache: cacheResource },
|
|
18
|
+
async init(_, { cache }) {
|
|
19
|
+
expect(cache.cacheFactoryTask).toBeDefined();
|
|
20
|
+
expect(cache.async).toBeUndefined();
|
|
21
|
+
expect(cache.defaultOptions).toEqual({
|
|
22
|
+
ttl: 10000,
|
|
23
|
+
max: 100,
|
|
24
|
+
ttlAutopurge: true,
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await run(app);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should create separate cache instances per task", async () => {
|
|
33
|
+
const testTask = defineTask({
|
|
34
|
+
id: "test.task",
|
|
35
|
+
middleware: [cacheMiddleware],
|
|
36
|
+
run: async () => Date.now(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const app = defineResource({
|
|
40
|
+
id: "app",
|
|
41
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
42
|
+
dependencies: { testTask, cache: cacheResource },
|
|
43
|
+
async init(_, { testTask, cache }) {
|
|
44
|
+
const firstRun = await testTask();
|
|
45
|
+
const secondRun = await testTask();
|
|
46
|
+
|
|
47
|
+
expect(firstRun).toBe(secondRun);
|
|
48
|
+
expect(cache.map.size).toBe(1);
|
|
49
|
+
expect(cache.map.has("test.task")).toBe(true);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await run(app);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("Cache Factory Task Override", () => {
|
|
58
|
+
it("should allow overriding the cache factory task", async () => {
|
|
59
|
+
class CustomCache implements ICacheInstance {
|
|
60
|
+
store = new Map<string, any>();
|
|
61
|
+
customFlag = true;
|
|
62
|
+
|
|
63
|
+
get(key: string) {
|
|
64
|
+
return this.store.get(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
set(key: string, value: any) {
|
|
68
|
+
this.store.set(key, value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clear() {
|
|
72
|
+
this.store.clear();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const customCacheFactoryTask = defineTask({
|
|
77
|
+
id: "globals.tasks.cacheFactory",
|
|
78
|
+
run: async (options: any) => {
|
|
79
|
+
return new CustomCache();
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const testTask = defineTask({
|
|
84
|
+
id: "custom.factory.task",
|
|
85
|
+
middleware: [cacheMiddleware],
|
|
86
|
+
run: async (input: string) => input.toUpperCase(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const app = defineResource({
|
|
90
|
+
id: "app",
|
|
91
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
92
|
+
overrides: [customCacheFactoryTask],
|
|
93
|
+
dependencies: { testTask, cache: cacheResource },
|
|
94
|
+
async init(_, { testTask, cache }) {
|
|
95
|
+
const result1 = await testTask("test");
|
|
96
|
+
const result2 = await testTask("test");
|
|
97
|
+
|
|
98
|
+
expect(result1).toBe("TEST");
|
|
99
|
+
expect(result2).toBe("TEST");
|
|
100
|
+
|
|
101
|
+
const cacheInstance = cache.map.get("custom.factory.task") as any;
|
|
102
|
+
expect(cacheInstance.customFlag).toBe(true);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await run(app);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should allow Redis-like cache factory task", async () => {
|
|
110
|
+
class RedisLikeCache implements ICacheInstance {
|
|
111
|
+
private store = new Map<string, { value: any; expiry?: number }>();
|
|
112
|
+
|
|
113
|
+
get(key: string) {
|
|
114
|
+
const entry = this.store.get(key);
|
|
115
|
+
if (!entry) return undefined;
|
|
116
|
+
|
|
117
|
+
if (entry.expiry && Date.now() > entry.expiry) {
|
|
118
|
+
this.store.delete(key);
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return entry.value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
set(key: string, value: any) {
|
|
126
|
+
// Simulate Redis with TTL
|
|
127
|
+
const ttl = 1000; // 1 second TTL
|
|
128
|
+
this.store.set(key, {
|
|
129
|
+
value,
|
|
130
|
+
expiry: Date.now() + ttl,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
clear() {
|
|
135
|
+
this.store.clear();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const redisCacheFactoryTask = defineTask({
|
|
140
|
+
id: "globals.tasks.cacheFactory",
|
|
141
|
+
run: async (options: any) => {
|
|
142
|
+
return new RedisLikeCache();
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
let callCount = 0;
|
|
147
|
+
const testTask = defineTask({
|
|
148
|
+
id: "redis.cache.task",
|
|
149
|
+
middleware: [cacheMiddleware],
|
|
150
|
+
run: async () => {
|
|
151
|
+
callCount++;
|
|
152
|
+
return `result-${callCount}`;
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const app = defineResource({
|
|
157
|
+
id: "app",
|
|
158
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
159
|
+
overrides: [redisCacheFactoryTask],
|
|
160
|
+
dependencies: { testTask },
|
|
161
|
+
async init(_, { testTask }) {
|
|
162
|
+
const result1 = await testTask();
|
|
163
|
+
const result2 = await testTask(); // Should be cached
|
|
164
|
+
|
|
165
|
+
expect(result1).toBe(result2);
|
|
166
|
+
expect(callCount).toBe(1);
|
|
167
|
+
|
|
168
|
+
// Wait for Redis-like TTL to expire
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
170
|
+
|
|
171
|
+
const result3 = await testTask(); // Should be new result
|
|
172
|
+
expect(result3).not.toBe(result1);
|
|
173
|
+
expect(callCount).toBe(2);
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await run(app);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("Cache Middleware", () => {
|
|
182
|
+
it("should return cached results for same inputs", async () => {
|
|
183
|
+
const testTask = defineTask({
|
|
184
|
+
id: "cached.task",
|
|
185
|
+
middleware: [cacheMiddleware],
|
|
186
|
+
run: async (input: number) => input * 2,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const app = defineResource({
|
|
190
|
+
id: "app",
|
|
191
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
192
|
+
dependencies: { testTask },
|
|
193
|
+
async init(_, { testTask }) {
|
|
194
|
+
const result1 = await testTask(2);
|
|
195
|
+
const result2 = await testTask(2);
|
|
196
|
+
const result3 = await testTask(3);
|
|
197
|
+
|
|
198
|
+
expect(result1).toBe(4);
|
|
199
|
+
expect(result2).toBe(4);
|
|
200
|
+
expect(result3).toBe(6);
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await run(app);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should respect TTL configuration", async () => {
|
|
208
|
+
let callCount = 0;
|
|
209
|
+
const testTask = defineTask({
|
|
210
|
+
id: "ttl.task",
|
|
211
|
+
middleware: [cacheMiddleware.with({ ttl: 100, ttlAutopurge: true })], // Short TTL
|
|
212
|
+
run: async () => {
|
|
213
|
+
callCount++;
|
|
214
|
+
return `result-${callCount}`;
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const app = defineResource({
|
|
219
|
+
id: "app",
|
|
220
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
221
|
+
dependencies: { testTask },
|
|
222
|
+
async init(_, { testTask }) {
|
|
223
|
+
const firstRun = await testTask();
|
|
224
|
+
const secondRun = await testTask(); // Should be cached
|
|
225
|
+
|
|
226
|
+
// Wait for TTL to expire
|
|
227
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
228
|
+
|
|
229
|
+
const thirdRun = await testTask(); // Should be a new result
|
|
230
|
+
|
|
231
|
+
expect(firstRun).toBe(secondRun); // Both should be cached
|
|
232
|
+
expect(callCount).toBe(2); // Called twice - once initially, once after TTL expiry
|
|
233
|
+
expect(thirdRun).not.toBe(firstRun); // Different result after TTL
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await run(app);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should handle custom key builders", async () => {
|
|
241
|
+
const customMiddleware = cacheMiddleware.with({
|
|
242
|
+
keyBuilder: (taskId: string, input: any) => `${taskId}-${input.id}`,
|
|
243
|
+
ttl: 1000,
|
|
244
|
+
ttlAutopurge: true,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const testTask = defineTask({
|
|
248
|
+
id: "custom.key.task",
|
|
249
|
+
middleware: [customMiddleware],
|
|
250
|
+
run: async (input: { id: string }) => input,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const app = defineResource({
|
|
254
|
+
id: "app",
|
|
255
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
256
|
+
dependencies: { testTask },
|
|
257
|
+
async init(_, { testTask }) {
|
|
258
|
+
const input1 = { id: "1", data: "test" };
|
|
259
|
+
const input2 = { id: "1", data: "modified" };
|
|
260
|
+
|
|
261
|
+
const result1 = await testTask(input1);
|
|
262
|
+
const result2 = await testTask(input2);
|
|
263
|
+
|
|
264
|
+
expect(result1).toEqual(input1);
|
|
265
|
+
expect(result2).toEqual(input1); // Same ID should cache
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await run(app);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("Error Handling", () => {
|
|
274
|
+
it("should not cache errors by default", async () => {
|
|
275
|
+
let callCount = 0;
|
|
276
|
+
const errorTask = defineTask({
|
|
277
|
+
id: "error.task",
|
|
278
|
+
middleware: [cacheMiddleware],
|
|
279
|
+
run: async () => {
|
|
280
|
+
callCount++;
|
|
281
|
+
throw new Error("Failed");
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const app = defineResource({
|
|
286
|
+
id: "app",
|
|
287
|
+
register: [cacheResource, cacheMiddleware, errorTask],
|
|
288
|
+
dependencies: { errorTask },
|
|
289
|
+
async init(_, { errorTask }) {
|
|
290
|
+
await expect(errorTask()).rejects.toThrow("Failed");
|
|
291
|
+
await expect(errorTask()).rejects.toThrow("Failed");
|
|
292
|
+
expect(callCount).toBe(2);
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await run(app);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("should not cache errors by default (errors throw through)", async () => {
|
|
300
|
+
let callCount = 0;
|
|
301
|
+
const errorTask = defineTask({
|
|
302
|
+
id: "cached.error.task",
|
|
303
|
+
middleware: [
|
|
304
|
+
cacheMiddleware.with({
|
|
305
|
+
ttl: 1000,
|
|
306
|
+
ttlAutopurge: true,
|
|
307
|
+
}),
|
|
308
|
+
],
|
|
309
|
+
run: async () => {
|
|
310
|
+
callCount++;
|
|
311
|
+
throw new Error("Cached error");
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const app = defineResource({
|
|
316
|
+
id: "app",
|
|
317
|
+
register: [cacheResource, cacheMiddleware, errorTask],
|
|
318
|
+
dependencies: { errorTask },
|
|
319
|
+
async init(_, { errorTask }) {
|
|
320
|
+
await expect(errorTask()).rejects.toThrow("Cached error");
|
|
321
|
+
await expect(errorTask()).rejects.toThrow("Cached error");
|
|
322
|
+
expect(callCount).toBe(2); // Called twice since errors aren't cached
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await run(app);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("Cache Invalidation", () => {
|
|
331
|
+
it("should clear cache instances when resource is disposed", async () => {
|
|
332
|
+
let executionCount = 0;
|
|
333
|
+
const testTask = defineTask({
|
|
334
|
+
id: "disposal.task",
|
|
335
|
+
middleware: [cacheMiddleware],
|
|
336
|
+
run: async () => {
|
|
337
|
+
executionCount++;
|
|
338
|
+
return `result-${executionCount}`;
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const result = await run(
|
|
343
|
+
defineResource({
|
|
344
|
+
id: "app",
|
|
345
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
346
|
+
dependencies: { testTask, cache: cacheResource },
|
|
347
|
+
async init(_, { testTask, cache }) {
|
|
348
|
+
const firstRun = await testTask();
|
|
349
|
+
const secondRun = await testTask(); // Should be cached
|
|
350
|
+
|
|
351
|
+
expect(firstRun).toBe(secondRun);
|
|
352
|
+
expect(executionCount).toBe(1);
|
|
353
|
+
expect(cache.map.size).toBe(1);
|
|
354
|
+
|
|
355
|
+
return cache;
|
|
356
|
+
},
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Dispose the resource - this should clear all cache instances
|
|
361
|
+
await result.dispose();
|
|
362
|
+
|
|
363
|
+
// Verify cache instances were cleared during disposal
|
|
364
|
+
expect(result.value.map.size).toBe(1); // Map still exists but instances are cleared
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe("Async Cache Handlers", () => {
|
|
369
|
+
class AsyncMockCache implements ICacheInstance {
|
|
370
|
+
store = new Map<string, any>();
|
|
371
|
+
async get(key: string) {
|
|
372
|
+
return this.store.get(key);
|
|
373
|
+
}
|
|
374
|
+
async set(key: string, value: any) {
|
|
375
|
+
this.store.set(key, value);
|
|
376
|
+
}
|
|
377
|
+
async clear() {
|
|
378
|
+
this.store.clear();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const asyncCacheFactoryTask = defineTask({
|
|
383
|
+
id: "globals.tasks.cacheFactory",
|
|
384
|
+
run: async (options: any) => {
|
|
385
|
+
return new AsyncMockCache();
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("should handle async cache operations", async () => {
|
|
390
|
+
const testTask = defineTask({
|
|
391
|
+
id: "task",
|
|
392
|
+
middleware: [cacheMiddleware],
|
|
393
|
+
run: async (input: number) => input * 2,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const asyncCacheResource = defineResource({
|
|
397
|
+
id: "globals.resources.cache",
|
|
398
|
+
register: [asyncCacheFactoryTask],
|
|
399
|
+
dependencies: { cacheFactoryTask: asyncCacheFactoryTask },
|
|
400
|
+
init: async (config: any, { cacheFactoryTask }) => ({
|
|
401
|
+
map: new Map<string, AsyncMockCache>(),
|
|
402
|
+
cacheFactoryTask,
|
|
403
|
+
async: true,
|
|
404
|
+
defaultOptions: { ttl: 10 * 1000, ...config?.defaultOptions },
|
|
405
|
+
}),
|
|
406
|
+
dispose: async (cache) => {
|
|
407
|
+
await Promise.all(
|
|
408
|
+
[...cache.map.values()].map((instance) => instance.clear())
|
|
409
|
+
);
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const app = defineResource({
|
|
414
|
+
id: "app",
|
|
415
|
+
register: [asyncCacheResource, cacheMiddleware, testTask],
|
|
416
|
+
dependencies: { testTask },
|
|
417
|
+
async init(_, { testTask }) {
|
|
418
|
+
const result1 = await testTask(2);
|
|
419
|
+
const result2 = await testTask(2);
|
|
420
|
+
expect(result1).toBe(4);
|
|
421
|
+
expect(result2).toBe(4);
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
await run(app);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe("Complex Input Serialization", () => {
|
|
430
|
+
it("should handle complex object inputs", async () => {
|
|
431
|
+
const testTask = defineTask({
|
|
432
|
+
id: "complex.object.task",
|
|
433
|
+
middleware: [cacheMiddleware],
|
|
434
|
+
run: async (input: { nested: { data: string }; array: number[] }) =>
|
|
435
|
+
JSON.stringify(input),
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const app = defineResource({
|
|
439
|
+
id: "app",
|
|
440
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
441
|
+
dependencies: { testTask },
|
|
442
|
+
async init(_, { testTask }) {
|
|
443
|
+
const complexInput = { nested: { data: "test" }, array: [1, 2, 3] };
|
|
444
|
+
const result1 = await testTask(complexInput);
|
|
445
|
+
const result2 = await testTask(complexInput);
|
|
446
|
+
|
|
447
|
+
expect(result1).toBe(result2);
|
|
448
|
+
expect(JSON.parse(result1)).toEqual(complexInput);
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await run(app);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("should handle null and undefined inputs", async () => {
|
|
456
|
+
const testTask = defineTask({
|
|
457
|
+
id: "null.undefined.task",
|
|
458
|
+
middleware: [cacheMiddleware],
|
|
459
|
+
run: async (input: any) => `result-${input}`,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const app = defineResource({
|
|
463
|
+
id: "app",
|
|
464
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
465
|
+
dependencies: { testTask },
|
|
466
|
+
async init(_, { testTask }) {
|
|
467
|
+
const nullResult1 = await testTask(null);
|
|
468
|
+
const nullResult2 = await testTask(null);
|
|
469
|
+
const undefinedResult1 = await testTask(undefined);
|
|
470
|
+
const undefinedResult2 = await testTask(undefined);
|
|
471
|
+
|
|
472
|
+
expect(nullResult1).toBe(nullResult2);
|
|
473
|
+
expect(undefinedResult1).toBe(undefinedResult2);
|
|
474
|
+
expect(nullResult1).not.toBe(undefinedResult1);
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
await run(app);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("should handle array inputs with different orders", async () => {
|
|
482
|
+
const testTask = defineTask({
|
|
483
|
+
id: "array.order.task",
|
|
484
|
+
middleware: [cacheMiddleware],
|
|
485
|
+
run: async (input: number[]) => input.reduce((a, b) => a + b, 0),
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const app = defineResource({
|
|
489
|
+
id: "app",
|
|
490
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
491
|
+
dependencies: { testTask },
|
|
492
|
+
async init(_, { testTask }) {
|
|
493
|
+
const result1 = await testTask([1, 2, 3]);
|
|
494
|
+
const result2 = await testTask([1, 2, 3]);
|
|
495
|
+
const result3 = await testTask([3, 2, 1]); // Different order
|
|
496
|
+
|
|
497
|
+
expect(result1).toBe(result2);
|
|
498
|
+
expect(result1).toBe(6);
|
|
499
|
+
expect(result3).toBe(6);
|
|
500
|
+
// Arrays with different order create different cache keys due to JSON.stringify
|
|
501
|
+
// So result1 and result3 are from different cache entries (both computed)
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
await run(app);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe("Cache Invalidation and Limits", () => {
|
|
510
|
+
it("should respect max cache size", async () => {
|
|
511
|
+
const testTask = defineTask({
|
|
512
|
+
id: "max.size.task",
|
|
513
|
+
middleware: [cacheMiddleware.with({ max: 2 })],
|
|
514
|
+
run: async (input: number) => input * 2,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const app = defineResource({
|
|
518
|
+
id: "app",
|
|
519
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
520
|
+
dependencies: { testTask, cache: cacheResource },
|
|
521
|
+
async init(_, { testTask, cache }) {
|
|
522
|
+
await testTask(1);
|
|
523
|
+
await testTask(2);
|
|
524
|
+
await testTask(3); // Should evict first entry
|
|
525
|
+
|
|
526
|
+
const cacheInstance = cache.map.get("max.size.task");
|
|
527
|
+
expect(cacheInstance).toBeDefined();
|
|
528
|
+
// LRU should maintain size limit
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
await run(app);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("should handle cache clear during execution", async () => {
|
|
536
|
+
let executionCount = 0;
|
|
537
|
+
const testTask = defineTask({
|
|
538
|
+
id: "clear.during.exec.task",
|
|
539
|
+
middleware: [cacheMiddleware],
|
|
540
|
+
run: async (input: number) => {
|
|
541
|
+
executionCount++;
|
|
542
|
+
return input * 2;
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const app = defineResource({
|
|
547
|
+
id: "app",
|
|
548
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
549
|
+
dependencies: { testTask, cache: cacheResource },
|
|
550
|
+
async init(_, { testTask, cache }) {
|
|
551
|
+
const firstResult = await testTask(5);
|
|
552
|
+
|
|
553
|
+
// Clear cache manually
|
|
554
|
+
cache.map.get("clear.during.exec.task")?.clear();
|
|
555
|
+
|
|
556
|
+
const secondResult = await testTask(5);
|
|
557
|
+
|
|
558
|
+
expect(firstResult).toBe(secondResult);
|
|
559
|
+
expect(executionCount).toBe(2); // Function called twice due to cache clear
|
|
560
|
+
expect(cache.map.size).toBe(1); // Cache instance still exists but is cleared
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
await run(app);
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
describe("Concurrent Access", () => {
|
|
569
|
+
it("should handle concurrent calls to same task", async () => {
|
|
570
|
+
let executionCount = 0;
|
|
571
|
+
const slowTask = defineTask({
|
|
572
|
+
id: "concurrent.task",
|
|
573
|
+
middleware: [cacheMiddleware],
|
|
574
|
+
run: async (input: number) => {
|
|
575
|
+
executionCount++;
|
|
576
|
+
// Shorter delay to avoid timeout
|
|
577
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
578
|
+
return input * 2;
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const app = defineResource({
|
|
583
|
+
id: "app",
|
|
584
|
+
register: [cacheResource, cacheMiddleware, slowTask],
|
|
585
|
+
dependencies: { slowTask },
|
|
586
|
+
async init(_, { slowTask }) {
|
|
587
|
+
// Test basic caching behavior instead of race conditions
|
|
588
|
+
const result1 = await slowTask(10);
|
|
589
|
+
const result2 = await slowTask(10);
|
|
590
|
+
|
|
591
|
+
expect(result1).toBe(20);
|
|
592
|
+
expect(result2).toBe(20);
|
|
593
|
+
expect(result1).toBe(result2); // Should be cached
|
|
594
|
+
expect(executionCount).toBe(1); // Only executed once
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
await run(app);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
describe("Memory and Disposal", () => {
|
|
603
|
+
it("should properly dispose async cache handlers", async () => {
|
|
604
|
+
class AsyncDisposableCache implements ICacheInstance {
|
|
605
|
+
store = new Map<string, any>();
|
|
606
|
+
disposed = false;
|
|
607
|
+
|
|
608
|
+
async get(key: string) {
|
|
609
|
+
if (this.disposed) throw new Error("Cache disposed");
|
|
610
|
+
return this.store.get(key);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async set(key: string, value: any) {
|
|
614
|
+
if (this.disposed) throw new Error("Cache disposed");
|
|
615
|
+
this.store.set(key, value);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async clear() {
|
|
619
|
+
this.disposed = true;
|
|
620
|
+
this.store.clear();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const disposableCacheFactoryTask = defineTask({
|
|
625
|
+
id: "globals.tasks.cacheFactory",
|
|
626
|
+
run: async (options: any) => {
|
|
627
|
+
return new AsyncDisposableCache();
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
const disposableCacheResource = defineResource({
|
|
632
|
+
id: "globals.resources.cache",
|
|
633
|
+
register: [disposableCacheFactoryTask],
|
|
634
|
+
dependencies: { cacheFactoryTask: disposableCacheFactoryTask },
|
|
635
|
+
init: async (config: any, { cacheFactoryTask }) => ({
|
|
636
|
+
map: new Map<string, AsyncDisposableCache>(),
|
|
637
|
+
cacheFactoryTask,
|
|
638
|
+
async: true,
|
|
639
|
+
defaultOptions: { ttl: 10 * 1000, ...config?.defaultOptions },
|
|
640
|
+
}),
|
|
641
|
+
dispose: async (cache) => {
|
|
642
|
+
await Promise.all(
|
|
643
|
+
[...cache.map.values()].map((instance) => instance.clear())
|
|
644
|
+
);
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const testTask = defineTask({
|
|
649
|
+
id: "disposal.test.task",
|
|
650
|
+
middleware: [cacheMiddleware],
|
|
651
|
+
run: async () => "test",
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const result = await run(
|
|
655
|
+
defineResource({
|
|
656
|
+
id: "app",
|
|
657
|
+
register: [disposableCacheResource, cacheMiddleware, testTask],
|
|
658
|
+
dependencies: { testTask, cache: disposableCacheResource },
|
|
659
|
+
async init(_, { testTask, cache }) {
|
|
660
|
+
await testTask();
|
|
661
|
+
return cache;
|
|
662
|
+
},
|
|
663
|
+
})
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
// Manually dispose to trigger cleanup
|
|
667
|
+
await result.dispose();
|
|
668
|
+
|
|
669
|
+
// Verify cache was disposed
|
|
670
|
+
const cacheInstance = result.value.map.get("disposal.test.task") as any;
|
|
671
|
+
expect(cacheInstance?.disposed).toBe(true);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
describe("Configuration Validation", () => {
|
|
676
|
+
it("should handle custom keyBuilder configuration properly", async () => {
|
|
677
|
+
const customMiddleware = cacheMiddleware.with({
|
|
678
|
+
keyBuilder: (taskId: string, input: any) => `custom-${taskId}-${input}`,
|
|
679
|
+
ttl: 1000,
|
|
680
|
+
ttlAutopurge: true,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const testTask = defineTask({
|
|
684
|
+
id: "custom.keybuilder.task",
|
|
685
|
+
middleware: [customMiddleware],
|
|
686
|
+
run: async (input: string) => `result-${input}`,
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const app = defineResource({
|
|
690
|
+
id: "app",
|
|
691
|
+
register: [cacheResource, cacheMiddleware, testTask],
|
|
692
|
+
dependencies: { testTask },
|
|
693
|
+
async init(_, { testTask }) {
|
|
694
|
+
const result1 = await testTask("test");
|
|
695
|
+
const result2 = await testTask("test");
|
|
696
|
+
|
|
697
|
+
expect(result1).toBe("result-test");
|
|
698
|
+
expect(result2).toBe(result1); // Should be cached
|
|
699
|
+
},
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
await run(app);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("should validate cache handler interface", async () => {
|
|
706
|
+
class InvalidCache {
|
|
707
|
+
// Missing required methods
|
|
708
|
+
store = new Map();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const invalidCacheFactoryTask = defineTask({
|
|
712
|
+
id: "globals.tasks.cacheFactory",
|
|
713
|
+
run: async (options: any) => {
|
|
714
|
+
return new InvalidCache() as any;
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
const invalidCacheResource = defineResource({
|
|
719
|
+
id: "globals.resources.cache",
|
|
720
|
+
register: [invalidCacheFactoryTask],
|
|
721
|
+
dependencies: { cacheFactoryTask: invalidCacheFactoryTask },
|
|
722
|
+
init: async (config: any, { cacheFactoryTask }) => ({
|
|
723
|
+
map: new Map(),
|
|
724
|
+
cacheFactoryTask,
|
|
725
|
+
defaultOptions: { ttl: 10 * 1000, ...config?.defaultOptions },
|
|
726
|
+
}),
|
|
727
|
+
dispose: async () => {},
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const testTask = defineTask({
|
|
731
|
+
id: "invalid.handler.task",
|
|
732
|
+
middleware: [cacheMiddleware],
|
|
733
|
+
run: async () => "test",
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
const app = defineResource({
|
|
737
|
+
id: "app",
|
|
738
|
+
register: [invalidCacheResource, cacheMiddleware, testTask],
|
|
739
|
+
dependencies: { testTask },
|
|
740
|
+
async init(_, { testTask }) {
|
|
741
|
+
// Should fail when trying to use invalid cache handler
|
|
742
|
+
await testTask(); // This should trigger the error
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
await expect(run(app)).rejects.toThrow();
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
describe("Validation", () => {
|
|
751
|
+
it("should throw error when used without task context", async () => {
|
|
752
|
+
const invalidResource = defineResource({
|
|
753
|
+
id: "invalid.resource",
|
|
754
|
+
middleware: [cacheMiddleware],
|
|
755
|
+
init: async () => "test",
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const app = defineResource({
|
|
759
|
+
id: "app",
|
|
760
|
+
register: [cacheResource, cacheMiddleware, invalidResource],
|
|
761
|
+
dependencies: { invalidResource },
|
|
762
|
+
async init(_, { invalidResource }) {
|
|
763
|
+
// Should throw during initialization
|
|
764
|
+
},
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
await expect(run(app)).rejects.toThrow(
|
|
768
|
+
"Cache middleware can only be used in tasks"
|
|
769
|
+
);
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
});
|