@bluelibs/runner 3.2.0 → 3.3.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 +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 +71 -0
- package/dist/define.js.map +1 -1
- package/dist/defs.d.ts +163 -4
- package/dist/defs.js +30 -0
- 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/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/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__/typesafety.test.ts +109 -1
- package/src/define.ts +97 -0
- package/src/defs.ts +168 -8
- 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
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
defineTask,
|
|
4
4
|
defineResource,
|
|
5
5
|
defineMiddleware,
|
|
6
|
+
defineOverride,
|
|
7
|
+
defineTag,
|
|
6
8
|
} from "../define";
|
|
7
9
|
import {
|
|
8
10
|
IEventDefinition,
|
|
@@ -12,8 +14,10 @@ import {
|
|
|
12
14
|
ITaskDefinition,
|
|
13
15
|
RegisterableItems,
|
|
14
16
|
} from "../defs";
|
|
17
|
+
import { createTestResource } from "..";
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
// This is skipped because we mostly check typesafety.
|
|
20
|
+
describe.skip("typesafety", () => {
|
|
17
21
|
it("tasks, resources: should have propper type safety for dependeices", async () => {
|
|
18
22
|
type InputTask = {
|
|
19
23
|
message: string;
|
|
@@ -209,4 +213,108 @@ describe("typesafety", () => {
|
|
|
209
213
|
|
|
210
214
|
expect(true).toBe(true);
|
|
211
215
|
});
|
|
216
|
+
|
|
217
|
+
it("createTestResource.runTask: should be type-safe", async () => {
|
|
218
|
+
type Input = { x: number };
|
|
219
|
+
type Output = Promise<number>;
|
|
220
|
+
|
|
221
|
+
const add = defineTask<Input, Output>({
|
|
222
|
+
id: "types.add",
|
|
223
|
+
run: async (i) => i.x + 1,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const depTask = defineTask<{ v: string }, Promise<string>>({
|
|
227
|
+
id: "types.dep",
|
|
228
|
+
run: async (i) => i.v.toUpperCase(),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const main = defineTask<Input, Output, { depTask: typeof depTask }>({
|
|
232
|
+
id: "types.main",
|
|
233
|
+
dependencies: { depTask },
|
|
234
|
+
run: async (i, d) => {
|
|
235
|
+
const v = await d.depTask({ v: String(i.x) });
|
|
236
|
+
return Number(v) + 1;
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const app = defineResource({
|
|
241
|
+
id: "types.app",
|
|
242
|
+
register: [add, depTask, main],
|
|
243
|
+
});
|
|
244
|
+
const harness = createTestResource(app);
|
|
245
|
+
|
|
246
|
+
// Types: input must match, override deps must match, output is awaited number
|
|
247
|
+
const { value: t } = await (await import("../run")).run(harness);
|
|
248
|
+
const r1: number | undefined = await t.runTask(add, { x: 1 });
|
|
249
|
+
// @ts-expect-error wrong input type
|
|
250
|
+
await t.runTask(add, { z: 1 });
|
|
251
|
+
// @ts-expect-error missing input
|
|
252
|
+
await t.runTask(add);
|
|
253
|
+
|
|
254
|
+
const r2: number | undefined = await t.runTask(main, { x: 2 });
|
|
255
|
+
|
|
256
|
+
// @ts-expect-error wrong deps override type
|
|
257
|
+
await t.runTask(main, { x: 2 }, { depTask: async (i: number) => "x" });
|
|
258
|
+
|
|
259
|
+
expect(true).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("should have propper type safety for overrides", async () => {
|
|
263
|
+
const task = defineTask({
|
|
264
|
+
id: "task",
|
|
265
|
+
run: async () => "Task executed",
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// @ts-expect-error
|
|
269
|
+
const overrideTask = defineOverride(task, {
|
|
270
|
+
run: async () => 234,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const resource = defineResource({
|
|
274
|
+
id: "resource",
|
|
275
|
+
register: [task],
|
|
276
|
+
init: async () => "Resource executed",
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const overrideResource = defineOverride(resource, {
|
|
280
|
+
init: async () => "Resource overridden",
|
|
281
|
+
});
|
|
282
|
+
// @ts-expect-error
|
|
283
|
+
defineOverride(resource, {
|
|
284
|
+
init: async () => 123, // bad type
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const middleware = defineMiddleware({
|
|
288
|
+
id: "middleware",
|
|
289
|
+
run: async () => "Middleware executed",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(true).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should have propper type safety for tags", async () => {
|
|
296
|
+
const tag = defineTag({ id: "tag" });
|
|
297
|
+
const tag2 = defineTag<{ value: number }>({ id: "tag2" });
|
|
298
|
+
const tag2optional = defineTag<{ value?: number }>({ id: "tag2" });
|
|
299
|
+
|
|
300
|
+
const tag3 = tag2.with({ value: 123 });
|
|
301
|
+
// @ts-expect-error
|
|
302
|
+
const tag4 = tag.with({ value: 123 });
|
|
303
|
+
|
|
304
|
+
const task = defineTask({
|
|
305
|
+
id: "task",
|
|
306
|
+
meta: {
|
|
307
|
+
tags: [
|
|
308
|
+
tag,
|
|
309
|
+
// @ts-expect-error
|
|
310
|
+
tag2,
|
|
311
|
+
tag2optional,
|
|
312
|
+
tag2.with({ value: 123 }),
|
|
313
|
+
tag3,
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(true).toBe(true);
|
|
319
|
+
});
|
|
212
320
|
});
|
package/src/define.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory functions for defining tasks, resources, events and middleware.
|
|
3
|
+
*
|
|
4
|
+
* These helpers create strongly-typed definitions while also wiring internal
|
|
5
|
+
* metadata: anonymous IDs, file path tags (for better debugging), lifecycle
|
|
6
|
+
* events, and global middleware flags. See README for high-level concepts.
|
|
7
|
+
*/
|
|
1
8
|
import {
|
|
2
9
|
ITask,
|
|
3
10
|
ITaskDefinition,
|
|
@@ -16,6 +23,11 @@ import {
|
|
|
16
23
|
symbolMiddlewareConfigured,
|
|
17
24
|
symbolFilePath,
|
|
18
25
|
symbolIndexResource,
|
|
26
|
+
ITag,
|
|
27
|
+
ITagDefinition,
|
|
28
|
+
ITagWithConfig,
|
|
29
|
+
TagType,
|
|
30
|
+
ITaggable,
|
|
19
31
|
} from "./defs";
|
|
20
32
|
import { Errors } from "./errors";
|
|
21
33
|
import { generateCallerIdFromFile, getCallerFile } from "./tools/getCallerFile";
|
|
@@ -30,6 +42,12 @@ export function defineTask<
|
|
|
30
42
|
>(
|
|
31
43
|
taskConfig: ITaskDefinition<Input, Output, Deps, TOn>
|
|
32
44
|
): ITask<Input, Output, Deps, TOn> {
|
|
45
|
+
/**
|
|
46
|
+
* Creates a task definition.
|
|
47
|
+
* - Generates an anonymous id based on file path when `id` is omitted
|
|
48
|
+
* - Wires lifecycle events: beforeRun, afterRun, onError
|
|
49
|
+
* - Carries through dependencies and middleware as declared
|
|
50
|
+
*/
|
|
33
51
|
const filePath = getCallerFile();
|
|
34
52
|
const isAnonymous = !Boolean(taskConfig.id);
|
|
35
53
|
const id = taskConfig.id || generateCallerIdFromFile(filePath, "task");
|
|
@@ -81,6 +99,12 @@ export function defineResource<
|
|
|
81
99
|
>(
|
|
82
100
|
constConfig: IResourceDefinition<TConfig, TValue, TDeps, TPrivate>
|
|
83
101
|
): IResource<TConfig, TValue, TDeps, TPrivate> {
|
|
102
|
+
/**
|
|
103
|
+
* Creates a resource definition.
|
|
104
|
+
* - Generates anonymous id when omitted (resource or index flavor)
|
|
105
|
+
* - Wires lifecycle events: beforeInit, afterInit, onError
|
|
106
|
+
* - Exposes `.with(config)` for config‑bound registration
|
|
107
|
+
*/
|
|
84
108
|
// The symbolFilePath might already come from defineIndex() for example
|
|
85
109
|
const filePath: string = constConfig[symbolFilePath] || getCallerFile();
|
|
86
110
|
const isIndexResource = constConfig[symbolIndexResource] || false;
|
|
@@ -152,6 +176,7 @@ export function defineIndex<
|
|
|
152
176
|
: T[K];
|
|
153
177
|
} & DependencyMapType
|
|
154
178
|
>(items: T): IResource<void, DependencyValuesType<D>, D> {
|
|
179
|
+
// Build dependency map from given items; unwrap `.with()` to the base resource
|
|
155
180
|
const dependencies = {} as D;
|
|
156
181
|
const register: RegisterableItems[] = [];
|
|
157
182
|
|
|
@@ -181,6 +206,10 @@ export function defineIndex<
|
|
|
181
206
|
export function defineEvent<TPayload = void>(
|
|
182
207
|
config?: IEventDefinition<TPayload>
|
|
183
208
|
): IEvent<TPayload> {
|
|
209
|
+
/**
|
|
210
|
+
* Creates an event definition. Anonymous ids are generated from file path
|
|
211
|
+
* when omitted. The returned object is branded for runtime checks.
|
|
212
|
+
*/
|
|
184
213
|
const callerFilePath = getCallerFile();
|
|
185
214
|
const eventConfig = config || {};
|
|
186
215
|
return {
|
|
@@ -208,6 +237,12 @@ export function defineMiddleware<
|
|
|
208
237
|
>(
|
|
209
238
|
middlewareDef: IMiddlewareDefinition<TConfig, TDependencies>
|
|
210
239
|
): IMiddleware<TConfig, TDependencies> {
|
|
240
|
+
/**
|
|
241
|
+
* Creates a middleware definition with:
|
|
242
|
+
* - Anonymous id generation when omitted
|
|
243
|
+
* - `.with(config)` to create configured instances
|
|
244
|
+
* - `.everywhere()` to mark as global (optionally scoping to tasks/resources)
|
|
245
|
+
*/
|
|
211
246
|
const filePath = getCallerFile();
|
|
212
247
|
const object = {
|
|
213
248
|
[symbols.filePath]: filePath,
|
|
@@ -266,3 +301,65 @@ export function isEvent(definition: any): definition is IEvent {
|
|
|
266
301
|
export function isMiddleware(definition: any): definition is IMiddleware {
|
|
267
302
|
return definition && definition[symbols.middleware];
|
|
268
303
|
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Override helper that preserves the original `id` and returns the same type.
|
|
307
|
+
* You can override any property except `id`.
|
|
308
|
+
*/
|
|
309
|
+
export function defineOverride<T extends ITask<any, any, any, any>>(
|
|
310
|
+
base: T,
|
|
311
|
+
patch: Omit<Partial<T>, "id">
|
|
312
|
+
): T;
|
|
313
|
+
export function defineOverride<T extends IResource<any, any, any, any>>(
|
|
314
|
+
base: T,
|
|
315
|
+
patch: Omit<Partial<T>, "id">
|
|
316
|
+
): T;
|
|
317
|
+
export function defineOverride<T extends IMiddleware<any, any>>(
|
|
318
|
+
base: T,
|
|
319
|
+
patch: Omit<Partial<T>, "id">
|
|
320
|
+
): T;
|
|
321
|
+
export function defineOverride(
|
|
322
|
+
base: ITask | IResource | IMiddleware,
|
|
323
|
+
patch: Record<string | symbol, unknown>
|
|
324
|
+
): ITask | IResource | IMiddleware {
|
|
325
|
+
const { id: _ignored, ...rest } = (patch || {}) as any;
|
|
326
|
+
// Ensure we never change the id, and merge overrides last
|
|
327
|
+
return {
|
|
328
|
+
...(base as any),
|
|
329
|
+
...rest,
|
|
330
|
+
id: (base as any).id,
|
|
331
|
+
} as any;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Creates a tag definition.
|
|
336
|
+
* - `.with(config)` to create configured instances
|
|
337
|
+
* - `.extract(tags)` to extract this tag from a list of tags
|
|
338
|
+
*/
|
|
339
|
+
export function defineTag<TConfig = void>(
|
|
340
|
+
definition: ITagDefinition<TConfig>
|
|
341
|
+
): ITag<TConfig> {
|
|
342
|
+
const id = definition.id;
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
id,
|
|
346
|
+
with(tagConfig: TConfig) {
|
|
347
|
+
return {
|
|
348
|
+
id,
|
|
349
|
+
tag: this,
|
|
350
|
+
config: tagConfig as any,
|
|
351
|
+
} as ITagWithConfig<TConfig>;
|
|
352
|
+
},
|
|
353
|
+
extract(target: TagType[] | ITaggable) {
|
|
354
|
+
const tags = Array.isArray(target) ? target : target?.meta?.tags || [];
|
|
355
|
+
for (const candidate of tags) {
|
|
356
|
+
if (typeof candidate === "string") continue;
|
|
357
|
+
// Configured instance
|
|
358
|
+
if (candidate.id === id) {
|
|
359
|
+
return candidate as ITagWithConfig<TConfig>;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
},
|
|
364
|
+
} as ITag<TConfig>;
|
|
365
|
+
}
|
package/src/defs.ts
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Core public TypeScript types for BlueLibs Runner.
|
|
3
|
+
*
|
|
4
|
+
* This file contains the strongly-typed contract for tasks, resources, events
|
|
5
|
+
* and middleware. It mirrors the mental model described in the README:
|
|
6
|
+
* - Tasks are functions (with lifecycle events)
|
|
7
|
+
* - Resources are singletons (with init/dispose hooks and lifecycle events)
|
|
8
|
+
* - Events are simple, strongly-typed emissions
|
|
9
|
+
* - Middleware can target both tasks and resources
|
|
10
|
+
*
|
|
11
|
+
* DX goals:
|
|
12
|
+
* - Crystal‑clear generics and helper types that infer dependency shapes
|
|
13
|
+
* - Friendly JSDoc you can hover in editors to understand usage instantly
|
|
14
|
+
* - Safe overrides and strong typing around config and register mechanics
|
|
15
|
+
*/
|
|
16
|
+
|
|
2
17
|
import { MiddlewareEverywhereOptions } from "./define";
|
|
3
18
|
|
|
19
|
+
// Re-export public cache type so consumers don’t import from internals.
|
|
4
20
|
export { ICacheInstance } from "./globals/middleware/cache.middleware";
|
|
5
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Internal brand symbols used to tag created objects at runtime and help with
|
|
24
|
+
* type‑narrowing. Prefer the `isTask`/`isResource`/`isEvent`/`isMiddleware`
|
|
25
|
+
* helpers instead of touching these directly.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
6
28
|
export const symbolTask: unique symbol = Symbol("runner.task");
|
|
7
29
|
export const symbolResource: unique symbol = Symbol("runner.resource");
|
|
8
30
|
export const symbolResourceWithConfig: unique symbol = Symbol(
|
|
@@ -23,14 +45,23 @@ export const symbolMiddlewareEverywhereResources: unique symbol = Symbol(
|
|
|
23
45
|
"runner.middlewareGlobalResources"
|
|
24
46
|
);
|
|
25
47
|
|
|
48
|
+
/** @internal Path to aid anonymous id generation and error messages */
|
|
26
49
|
export const symbolFilePath: unique symbol = Symbol("runner.filePath");
|
|
50
|
+
/** @internal Marks disposable instances */
|
|
27
51
|
export const symbolDispose: unique symbol = Symbol("runner.dispose");
|
|
52
|
+
/** @internal Link to internal Store */
|
|
28
53
|
export const symbolStore: unique symbol = Symbol("runner.store");
|
|
29
54
|
|
|
55
|
+
/** @internal Brand used by index() resources */
|
|
30
56
|
export const symbolIndexResource: unique symbol = Symbol(
|
|
31
57
|
"runner.indexResource"
|
|
32
58
|
);
|
|
33
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Convenience bag of internal symbols. Intended for framework internals;
|
|
62
|
+
* consumers should not rely on this shape.
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
34
65
|
export const symbols = {
|
|
35
66
|
task: symbolTask,
|
|
36
67
|
resource: symbolResource,
|
|
@@ -44,11 +75,76 @@ export const symbols = {
|
|
|
44
75
|
dispose: symbolDispose,
|
|
45
76
|
store: symbolStore,
|
|
46
77
|
};
|
|
78
|
+
export interface ITagDefinition<TConfig = void> {
|
|
79
|
+
id: string | symbol;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A configured instance of a tag as produced by `ITag.with()`.
|
|
84
|
+
*/
|
|
85
|
+
export interface ITagWithConfig<TConfig = void> {
|
|
86
|
+
id: string | symbol;
|
|
87
|
+
/** The tag definition used to produce this configured instance. */
|
|
88
|
+
tag: ITag<TConfig>;
|
|
89
|
+
/** The configuration captured for this tag instance. */
|
|
90
|
+
config: TConfig;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A tag definition (builder). Use `.with(config)` to obtain configured instances,
|
|
95
|
+
* and `.extract(tags)` to find either a configured instance or the bare tag in a list.
|
|
96
|
+
*/
|
|
97
|
+
export interface ITag<TConfig = void> extends ITagDefinition<TConfig> {
|
|
98
|
+
/**
|
|
99
|
+
* Creates a configured instance of the tag.
|
|
100
|
+
*/
|
|
101
|
+
with(config: TConfig): ITagWithConfig<TConfig>;
|
|
102
|
+
/**
|
|
103
|
+
* Extracts either a configured instance or the bare tag from a list of tags
|
|
104
|
+
* or from a taggable object (`{ meta: { tags?: [] } }`).
|
|
105
|
+
*/
|
|
106
|
+
extract(target: TagType[] | ITaggable): ExtractedTagResult<TConfig> | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Restrict bare tags to those whose config can be omitted (void or optional object),
|
|
111
|
+
* mirroring the same principle used for resources in `RegisterableItems`.
|
|
112
|
+
* Required-config tags must appear as configured instances.
|
|
113
|
+
*/
|
|
114
|
+
export type TagType =
|
|
115
|
+
| string
|
|
116
|
+
| ITag<void>
|
|
117
|
+
| ITag<{ [K in any]?: any }>
|
|
118
|
+
| ITagWithConfig<any>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Conditional result type for `ITag.extract`:
|
|
122
|
+
* - For void config → just the identifier
|
|
123
|
+
* - For optional object config → identifier with optional config
|
|
124
|
+
* - For required config → identifier with required config
|
|
125
|
+
*/
|
|
126
|
+
export type ExtractedTagResult<TConfig> = {} extends TConfig
|
|
127
|
+
? { id: string | symbol; config?: TConfig }
|
|
128
|
+
: { id: string | symbol; config: TConfig };
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Any object that can carry tags via metadata. This mirrors how tasks,
|
|
132
|
+
* resources, events, and middleware expose `meta.tags`.
|
|
133
|
+
*/
|
|
134
|
+
export interface ITaggable {
|
|
135
|
+
meta?: {
|
|
136
|
+
tags?: TagType[];
|
|
137
|
+
};
|
|
138
|
+
}
|
|
47
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Common metadata you can attach to tasks/resources/events/middleware.
|
|
142
|
+
* Useful for docs, filtering and middleware decisions.
|
|
143
|
+
*/
|
|
48
144
|
export interface IMeta {
|
|
49
145
|
title?: string;
|
|
50
146
|
description?: string;
|
|
51
|
-
tags?:
|
|
147
|
+
tags?: TagType[];
|
|
52
148
|
}
|
|
53
149
|
|
|
54
150
|
export interface ITaskMeta extends IMeta {}
|
|
@@ -56,7 +152,11 @@ export interface IResourceMeta extends IMeta {}
|
|
|
56
152
|
export interface IEventMeta extends IMeta {}
|
|
57
153
|
export interface IMiddlewareMeta extends IMeta {}
|
|
58
154
|
|
|
59
|
-
|
|
155
|
+
/**
|
|
156
|
+
* A mapping of dependency keys to Runner definitions. Used in `dependencies`
|
|
157
|
+
* for tasks and resources. Values are later transformed into the actual
|
|
158
|
+
* callable/value shape by `DependencyValuesType`.
|
|
159
|
+
*/
|
|
60
160
|
export type DependencyMapType = Record<
|
|
61
161
|
string,
|
|
62
162
|
ITask<any, any, any, any> | IResource<any, any, any> | IEventDefinition<any>
|
|
@@ -72,21 +172,28 @@ type ExtractResourceValue<T> = T extends IResource<any, infer V, infer D>
|
|
|
72
172
|
type ExtractEventParams<T> = T extends IEvent<infer P> ? P : never;
|
|
73
173
|
|
|
74
174
|
/**
|
|
75
|
-
*
|
|
175
|
+
* Task dependencies transform into callable functions: call with the task input
|
|
176
|
+
* and you receive the task output.
|
|
76
177
|
*/
|
|
77
178
|
type TaskDependency<I, O> = (...args: I extends null | void ? [] : [I]) => O;
|
|
78
179
|
/**
|
|
79
|
-
*
|
|
180
|
+
* Resource dependencies resolve to the resource's value directly.
|
|
80
181
|
*/
|
|
81
182
|
type ResourceDependency<V> = V;
|
|
82
183
|
/**
|
|
83
|
-
*
|
|
184
|
+
* Event dependencies resolve to an emitter function. If the payload type is
|
|
185
|
+
* `void`, the function can be called with zero args (or an empty object).
|
|
84
186
|
*/
|
|
85
187
|
type EventDependency<P> = P extends void
|
|
86
188
|
? (() => Promise<void>) & ((input?: Record<string, never>) => Promise<void>)
|
|
87
189
|
: (input: P) => Promise<void>;
|
|
88
190
|
|
|
89
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Transforms a dependency definition into the usable shape inside `run`/`init`:
|
|
193
|
+
* - Task -> callable function
|
|
194
|
+
* - Resource -> resolved value
|
|
195
|
+
* - Event -> emit function
|
|
196
|
+
*/
|
|
90
197
|
export type DependencyValueType<T> = T extends ITask<any, any, any>
|
|
91
198
|
? TaskDependency<ExtractTaskInput<T>, ExtractTaskOutput<T>>
|
|
92
199
|
: T extends IResource<any, any>
|
|
@@ -99,7 +206,13 @@ export type DependencyValuesType<T extends DependencyMapType> = {
|
|
|
99
206
|
[K in keyof T]: DependencyValueType<T[K]>;
|
|
100
207
|
};
|
|
101
208
|
|
|
102
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Anything you can put inside a resource's `register: []`.
|
|
211
|
+
* - Resources (with or without `.with()`)
|
|
212
|
+
* - Tasks
|
|
213
|
+
* - Middleware
|
|
214
|
+
* - Events
|
|
215
|
+
*/
|
|
103
216
|
export type RegisterableItems<T = any> =
|
|
104
217
|
| IResourceWithConfig<any>
|
|
105
218
|
| IResource<void, any, any, any> // For void configs
|
|
@@ -119,8 +232,17 @@ export interface ITaskDefinition<
|
|
|
119
232
|
TDependencies extends DependencyMapType = {},
|
|
120
233
|
TOn extends "*" | IEventDefinition<any> | undefined = undefined // Adding a generic to track 'on' type
|
|
121
234
|
> {
|
|
235
|
+
/**
|
|
236
|
+
* Stable identifier. If omitted, an anonymous id is generated from file path
|
|
237
|
+
* (see README: Anonymous IDs).
|
|
238
|
+
*/
|
|
122
239
|
id?: string | symbol;
|
|
240
|
+
/**
|
|
241
|
+
* Access other tasks/resources/events. Can be an object or a function when
|
|
242
|
+
* you need late or config‑dependent resolution.
|
|
243
|
+
*/
|
|
123
244
|
dependencies?: TDependencies | (() => TDependencies);
|
|
245
|
+
/** Middleware applied around task execution. */
|
|
124
246
|
middleware?: MiddlewareAttachments[];
|
|
125
247
|
/**
|
|
126
248
|
* Listen to events in a simple way
|
|
@@ -131,7 +253,12 @@ export interface ITaskDefinition<
|
|
|
131
253
|
* The event with the lowest order will be executed first.
|
|
132
254
|
*/
|
|
133
255
|
listenerOrder?: number;
|
|
256
|
+
/** Optional metadata used for docs, filtering and tooling. */
|
|
134
257
|
meta?: ITaskMeta;
|
|
258
|
+
/**
|
|
259
|
+
* The task body. If `on` is set, the input is an `IEventEmission`. Otherwise,
|
|
260
|
+
* it's the declared input type.
|
|
261
|
+
*/
|
|
135
262
|
run: (
|
|
136
263
|
input: TOn extends undefined
|
|
137
264
|
? TInput
|
|
@@ -198,11 +325,20 @@ export interface IResourceDefinition<
|
|
|
198
325
|
THooks = any,
|
|
199
326
|
TRegisterableItems = any
|
|
200
327
|
> {
|
|
328
|
+
/** Stable identifier. Omit to get an anonymous id. */
|
|
201
329
|
id?: string | symbol;
|
|
330
|
+
/** Static or lazy dependency map. Receives `config` when provided. */
|
|
202
331
|
dependencies?: TDependencies | ((config: TConfig) => TDependencies);
|
|
332
|
+
/**
|
|
333
|
+
* Register other registerables (resources/tasks/middleware/events). Accepts a
|
|
334
|
+
* static array or a function of `config` to support dynamic wiring.
|
|
335
|
+
*/
|
|
203
336
|
register?:
|
|
204
337
|
| Array<RegisterableItems>
|
|
205
338
|
| ((config: TConfig) => Array<RegisterableItems>);
|
|
339
|
+
/**
|
|
340
|
+
* Initialize and return the resource value. Called once during boot.
|
|
341
|
+
*/
|
|
206
342
|
init?: (
|
|
207
343
|
this: any,
|
|
208
344
|
config: TConfig,
|
|
@@ -225,8 +361,16 @@ export interface IResourceDefinition<
|
|
|
225
361
|
context: TContext
|
|
226
362
|
) => Promise<void>;
|
|
227
363
|
meta?: IResourceMeta;
|
|
364
|
+
/**
|
|
365
|
+
* Safe overrides to swap behavior while preserving identities. See
|
|
366
|
+
* README: Overrides.
|
|
367
|
+
*/
|
|
228
368
|
overrides?: Array<IResource | ITask | IMiddleware | IResourceWithConfig>;
|
|
369
|
+
/** Middleware applied around init/dispose. */
|
|
229
370
|
middleware?: MiddlewareAttachments[];
|
|
371
|
+
/**
|
|
372
|
+
* Create a private, mutable context shared between `init` and `dispose`.
|
|
373
|
+
*/
|
|
230
374
|
context?: () => TContext;
|
|
231
375
|
/**
|
|
232
376
|
* This is optional and used from an index resource to get the correct caller.
|
|
@@ -266,8 +410,11 @@ export interface IResourceWithConfig<
|
|
|
266
410
|
TValue = any,
|
|
267
411
|
TDependencies extends DependencyMapType = any
|
|
268
412
|
> {
|
|
413
|
+
/** The id of the underlying resource. */
|
|
269
414
|
id: string;
|
|
415
|
+
/** The underlying resource definition. */
|
|
270
416
|
resource: IResource<TConfig, TValue, TDependencies>;
|
|
417
|
+
/** The configuration captured by `.with(config)`. */
|
|
271
418
|
config: TConfig;
|
|
272
419
|
}
|
|
273
420
|
|
|
@@ -276,6 +423,7 @@ export type EventHandlerType<T = any> = (
|
|
|
276
423
|
) => any | Promise<any>;
|
|
277
424
|
|
|
278
425
|
export interface IEventDefinition<TPayload = void> {
|
|
426
|
+
/** Stable identifier. Omit to get an anonymous id. */
|
|
279
427
|
id?: string | symbol;
|
|
280
428
|
meta?: IEventMeta;
|
|
281
429
|
}
|
|
@@ -327,8 +475,13 @@ export interface IMiddlewareDefinition<
|
|
|
327
475
|
TConfig = any,
|
|
328
476
|
TDependencies extends DependencyMapType = any
|
|
329
477
|
> {
|
|
478
|
+
/** Stable identifier. Omit to get an anonymous id. */
|
|
330
479
|
id?: string | symbol;
|
|
480
|
+
/** Static or lazy dependency map. */
|
|
331
481
|
dependencies?: TDependencies | ((config: TConfig) => TDependencies);
|
|
482
|
+
/**
|
|
483
|
+
* The middleware body, called with task/resource execution input.
|
|
484
|
+
*/
|
|
332
485
|
run: (
|
|
333
486
|
input: IMiddlewareExecutionInput,
|
|
334
487
|
dependencies: DependencyValuesType<TDependencies>,
|
|
@@ -348,10 +501,15 @@ export interface IMiddleware<
|
|
|
348
501
|
|
|
349
502
|
id: string | symbol;
|
|
350
503
|
dependencies: TDependencies | (() => TDependencies);
|
|
504
|
+
/**
|
|
505
|
+
* Attach this middleware globally. Use options to scope to tasks/resources.
|
|
506
|
+
*/
|
|
351
507
|
everywhere(
|
|
352
508
|
config?: MiddlewareEverywhereOptions
|
|
353
509
|
): IMiddleware<TConfig, TDependencies>;
|
|
510
|
+
/** Current configuration object (empty by default). */
|
|
354
511
|
config: TConfig;
|
|
512
|
+
/** Configure the middleware and return a marked, configured instance. */
|
|
355
513
|
with: (config: TConfig) => IMiddlewareConfigured<TConfig, TDependencies>;
|
|
356
514
|
}
|
|
357
515
|
|
|
@@ -373,10 +531,12 @@ export interface IMiddlewareExecutionInput<
|
|
|
373
531
|
TTaskInput = any,
|
|
374
532
|
TResourceConfig = any
|
|
375
533
|
> {
|
|
534
|
+
/** Task hook: present when wrapping a task run. */
|
|
376
535
|
task?: {
|
|
377
536
|
definition: ITask<TTaskInput>;
|
|
378
537
|
input: TTaskInput;
|
|
379
538
|
};
|
|
539
|
+
/** Resource hook: present when wrapping init/dispose. */
|
|
380
540
|
resource?: {
|
|
381
541
|
definition: IResource<TResourceConfig>;
|
|
382
542
|
config: TResourceConfig;
|
|
@@ -3,6 +3,7 @@ import { defineMiddleware } from "../define";
|
|
|
3
3
|
import { cacheMiddleware } from "./middleware/cache.middleware";
|
|
4
4
|
import { requireContextMiddleware } from "./middleware/requireContext.middleware";
|
|
5
5
|
import { retryMiddleware } from "./middleware/retry.middleware";
|
|
6
|
+
import { timeoutMiddleware } from "./middleware/timeout.middleware";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Global middlewares
|
|
@@ -11,4 +12,5 @@ export const globalMiddlewares = {
|
|
|
11
12
|
requireContext: requireContextMiddleware,
|
|
12
13
|
retry: retryMiddleware,
|
|
13
14
|
cache: cacheMiddleware,
|
|
15
|
+
timeout: timeoutMiddleware,
|
|
14
16
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineMiddleware } from "../../define";
|
|
2
|
+
|
|
3
|
+
export interface TimeoutMiddlewareConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Maximum time in milliseconds before the wrapped operation is aborted
|
|
6
|
+
* and a timeout error is thrown. Defaults to 5000ms.
|
|
7
|
+
*/
|
|
8
|
+
ttl: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const timeoutMiddleware = defineMiddleware({
|
|
12
|
+
id: "globals.middleware.timeout",
|
|
13
|
+
async run({ task, resource, next }, _deps, config: TimeoutMiddlewareConfig) {
|
|
14
|
+
const input = task ? task.input : resource?.config;
|
|
15
|
+
|
|
16
|
+
const ttl = Math.max(0, config.ttl);
|
|
17
|
+
const message = `Operation timed out after ${ttl}ms`;
|
|
18
|
+
|
|
19
|
+
// Fast-path: immediate timeout
|
|
20
|
+
if (ttl === 0) {
|
|
21
|
+
const error = new Error(message);
|
|
22
|
+
(error as any).name = "TimeoutError";
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
|
|
28
|
+
// Create a timeout promise that rejects when aborted
|
|
29
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
30
|
+
const timeoutId = setTimeout(() => {
|
|
31
|
+
controller.abort();
|
|
32
|
+
const error = new Error(message);
|
|
33
|
+
(error as any).name = "TimeoutError";
|
|
34
|
+
reject(error);
|
|
35
|
+
}, ttl);
|
|
36
|
+
|
|
37
|
+
// Clean up timeout if abort signal fires for other reasons
|
|
38
|
+
controller.signal.addEventListener("abort", () => {
|
|
39
|
+
clearTimeout(timeoutId);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Race between the actual operation and the timeout
|
|
44
|
+
return Promise.race([next(input), timeoutPromise]);
|
|
45
|
+
},
|
|
46
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -4,12 +4,15 @@ import {
|
|
|
4
4
|
defineEvent,
|
|
5
5
|
defineMiddleware,
|
|
6
6
|
defineIndex,
|
|
7
|
+
defineTag,
|
|
8
|
+
defineOverride,
|
|
7
9
|
} from "./define";
|
|
8
10
|
import { createContext } from "./context";
|
|
9
11
|
import { globalEvents } from "./globals/globalEvents";
|
|
10
12
|
import { globalResources } from "./globals/globalResources";
|
|
11
13
|
import { globalMiddlewares } from "./globals/globalMiddleware";
|
|
12
14
|
import { run } from "./run";
|
|
15
|
+
import { createTestResource } from "./testing";
|
|
13
16
|
|
|
14
17
|
const globals = {
|
|
15
18
|
events: globalEvents,
|
|
@@ -24,8 +27,11 @@ export {
|
|
|
24
27
|
defineEvent as event,
|
|
25
28
|
defineMiddleware as middleware,
|
|
26
29
|
defineIndex as index,
|
|
30
|
+
defineTag as tag,
|
|
31
|
+
defineOverride as override,
|
|
27
32
|
run,
|
|
28
33
|
createContext,
|
|
34
|
+
createTestResource,
|
|
29
35
|
};
|
|
30
36
|
|
|
31
37
|
export * as definitions from "./defs";
|