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