@bluelibs/runner 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +797 -0
  3. package/dist/DependencyProcessor.d.ts +49 -0
  4. package/dist/DependencyProcessor.js +178 -0
  5. package/dist/DependencyProcessor.js.map +1 -0
  6. package/dist/EventManager.d.ts +13 -0
  7. package/dist/EventManager.js +58 -0
  8. package/dist/EventManager.js.map +1 -0
  9. package/dist/ResourceInitializer.d.ts +13 -0
  10. package/dist/ResourceInitializer.js +54 -0
  11. package/dist/ResourceInitializer.js.map +1 -0
  12. package/dist/Store.d.ts +62 -0
  13. package/dist/Store.js +186 -0
  14. package/dist/Store.js.map +1 -0
  15. package/dist/TaskRunner.d.ts +22 -0
  16. package/dist/TaskRunner.js +93 -0
  17. package/dist/TaskRunner.js.map +1 -0
  18. package/dist/define.d.ts +10 -0
  19. package/dist/define.js +111 -0
  20. package/dist/define.js.map +1 -0
  21. package/dist/defs.d.ts +127 -0
  22. package/dist/defs.js +12 -0
  23. package/dist/defs.js.map +1 -0
  24. package/dist/errors.d.ts +8 -0
  25. package/dist/errors.js +12 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/globalEvents.d.ts +36 -0
  28. package/dist/globalEvents.js +45 -0
  29. package/dist/globalEvents.js.map +1 -0
  30. package/dist/globalResources.d.ts +8 -0
  31. package/dist/globalResources.js +19 -0
  32. package/dist/globalResources.js.map +1 -0
  33. package/dist/index.d.ts +49 -0
  34. package/dist/index.js +25 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/run.d.ts +32 -0
  37. package/dist/run.js +39 -0
  38. package/dist/run.js.map +1 -0
  39. package/dist/tools/findCircularDependencies.d.ts +16 -0
  40. package/dist/tools/findCircularDependencies.js +53 -0
  41. package/dist/tools/findCircularDependencies.js.map +1 -0
  42. package/package.json +50 -0
  43. package/src/DependencyProcessor.ts +243 -0
  44. package/src/EventManager.ts +84 -0
  45. package/src/ResourceInitializer.ts +69 -0
  46. package/src/Store.ts +250 -0
  47. package/src/TaskRunner.ts +135 -0
  48. package/src/__tests__/EventManager.test.ts +96 -0
  49. package/src/__tests__/ResourceInitializer.test.ts +109 -0
  50. package/src/__tests__/Store.test.ts +143 -0
  51. package/src/__tests__/TaskRunner.test.ts +135 -0
  52. package/src/__tests__/benchmark/benchmark.test.ts +146 -0
  53. package/src/__tests__/errors.test.ts +268 -0
  54. package/src/__tests__/globalEvents.test.ts +99 -0
  55. package/src/__tests__/index.ts +9 -0
  56. package/src/__tests__/run.hooks.test.ts +110 -0
  57. package/src/__tests__/run.test.ts +614 -0
  58. package/src/__tests__/tools/findCircularDependencies.test.ts +217 -0
  59. package/src/define.ts +142 -0
  60. package/src/defs.ts +221 -0
  61. package/src/errors.ts +22 -0
  62. package/src/globalEvents.ts +64 -0
  63. package/src/globalResources.ts +19 -0
  64. package/src/index.ts +28 -0
  65. package/src/run.ts +98 -0
  66. package/src/tools/findCircularDependencies.ts +69 -0
package/src/Store.ts ADDED
@@ -0,0 +1,250 @@
1
+ import {
2
+ DependencyMapType,
3
+ DependencyValuesType,
4
+ IMiddlewareDefinition,
5
+ IEventDefinition,
6
+ IResource,
7
+ ITask,
8
+ IResourceWithConfig,
9
+ RegisterableItems,
10
+ symbols,
11
+ IMiddleware,
12
+ } from "./defs";
13
+ import * as utils from "./define";
14
+ import { IDependentNode } from "./tools/findCircularDependencies";
15
+ import { globalEventsArray } from "./globalEvents";
16
+ import { Errors } from "./errors";
17
+ import { globalResources } from "./globalResources";
18
+ import { EventManager } from "./EventManager";
19
+ import { TaskRunner } from "./TaskRunner";
20
+
21
+ export type ResourceStoreElementType<
22
+ C = any,
23
+ V = any,
24
+ D extends DependencyMapType = {}
25
+ > = {
26
+ resource: IResource<C, V, D>;
27
+ computedDependencies?: DependencyValuesType<D>;
28
+ config: C;
29
+ value: V;
30
+ isInitialized?: boolean;
31
+ };
32
+
33
+ export type TaskStoreElementType<
34
+ Input = any,
35
+ Output extends Promise<any> = any,
36
+ D extends DependencyMapType = any
37
+ > = {
38
+ task: ITask<Input, Output, D>;
39
+ computedDependencies: DependencyValuesType<D>;
40
+ isInitialized: boolean;
41
+ };
42
+
43
+ export type MiddlewareStoreElementType<TDeps extends DependencyMapType = any> =
44
+ {
45
+ middleware: IMiddleware<TDeps>;
46
+ computedDependencies: DependencyValuesType<TDeps>;
47
+ };
48
+
49
+ export type EventStoreElementType = {
50
+ event: IEventDefinition;
51
+ };
52
+
53
+ /**
54
+ * @internal This should be used for testing purposes only.
55
+ */
56
+ export class Store {
57
+ root!: ResourceStoreElementType;
58
+ public tasks: Map<string, TaskStoreElementType> = new Map();
59
+ public resources: Map<string, ResourceStoreElementType> = new Map();
60
+ public events: Map<string, EventStoreElementType> = new Map();
61
+ public middlewares: Map<string, MiddlewareStoreElementType> = new Map();
62
+
63
+ constructor(protected readonly eventManager: EventManager) {}
64
+
65
+ /**
66
+ * Store the root before beginning registration
67
+ * @param root
68
+ * @param config
69
+ */
70
+ initializeStore(root: IResource<any>, config: any) {
71
+ this.storeGenericItem(globalResources.eventManager.with(this.eventManager));
72
+ this.storeGenericItem(globalResources.store.with(this));
73
+
74
+ root.dependencies =
75
+ typeof root.dependencies === "function"
76
+ ? root.dependencies(config)
77
+ : root.dependencies;
78
+
79
+ this.root = {
80
+ resource: root,
81
+ computedDependencies: {},
82
+ config,
83
+ value: undefined,
84
+ isInitialized: false,
85
+ };
86
+
87
+ // register global events
88
+ globalEventsArray.forEach((event) => {
89
+ this.storeEvent(event);
90
+ });
91
+
92
+ this.resources.set(root.id, this.root);
93
+ }
94
+
95
+ /**
96
+ * Beginning with the root, we perform registering to the container all the resources, tasks, middleware and events.
97
+ * @param element
98
+ * @param config
99
+ */
100
+ computeRegisterOfResource<C>(element: IResource<C>, config?: C) {
101
+ const items =
102
+ typeof element.register === "function"
103
+ ? element.register(config as C)
104
+ : element.register;
105
+
106
+ for (const item of items) {
107
+ this.storeGenericItem<C>(item);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * middlewares are already stored in their final form and the check for them would be redundant
113
+ * @param id
114
+ */
115
+ protected checkIfIDExists(id: string): void | never {
116
+ if (this.tasks.has(id)) {
117
+ throw Errors.duplicateRegistration("Task", id);
118
+ }
119
+ if (this.resources.has(id)) {
120
+ throw Errors.duplicateRegistration("Resource", id);
121
+ }
122
+ if (this.events.has(id)) {
123
+ throw Errors.duplicateRegistration("Event", id);
124
+ }
125
+ }
126
+
127
+ public getGlobalMiddlewares(excludingIds: string[]): IMiddleware[] {
128
+ return Array.from(this.middlewares.values())
129
+ .filter((x) => x.middleware[symbols.middlewareGlobal])
130
+ .filter((x) => !excludingIds.includes(x.middleware.id))
131
+ .map((x) => x.middleware);
132
+ }
133
+
134
+ /**
135
+ * If you want to register something to the store you can use this function.
136
+ * @param item
137
+ */
138
+ public storeGenericItem<C>(item: RegisterableItems) {
139
+ if (utils.isTask(item)) {
140
+ this.storeTask<C>(item);
141
+ } else if (utils.isResource(item)) {
142
+ // Registration a simple resource, which is interpreted as a resource with no configuration.
143
+ this.storeResource<C>(item);
144
+ } else if (utils.isEvent(item)) {
145
+ this.storeEvent<C>(item);
146
+ } else if (utils.isMiddleware(item)) {
147
+ if (this.middlewares.has(item.id)) {
148
+ throw Errors.duplicateRegistration("Middleware", item.id);
149
+ }
150
+
151
+ item.dependencies =
152
+ typeof item.dependencies === "function"
153
+ ? item.dependencies()
154
+ : item.dependencies;
155
+
156
+ this.middlewares.set(item.id, {
157
+ middleware: item,
158
+ computedDependencies: {},
159
+ });
160
+ } else if (utils.isResourceWithConfig(item)) {
161
+ this.storeResourceWithConfig<C>(item);
162
+ } else {
163
+ throw Errors.unknownItemType(item);
164
+ }
165
+ }
166
+
167
+ public storeEvent<C>(item: IEventDefinition<void>) {
168
+ this.checkIfIDExists(item.id);
169
+
170
+ this.events.set(item.id, { event: item });
171
+ }
172
+
173
+ private storeResourceWithConfig<C>(item: IResourceWithConfig<any, any, any>) {
174
+ this.checkIfIDExists(item.resource.id);
175
+
176
+ this.resources.set(item.resource.id, {
177
+ resource: item.resource,
178
+ config: item.config,
179
+ value: undefined,
180
+ isInitialized: false,
181
+ });
182
+
183
+ this.computeRegisterOfResource(item.resource, item.config);
184
+ }
185
+
186
+ private storeResource<C>(item: IResource<any, any, any>) {
187
+ this.checkIfIDExists(item.id);
188
+
189
+ this.storeEvent(item.events.beforeInit);
190
+ this.storeEvent(item.events.afterInit);
191
+ this.storeEvent(item.events.onError);
192
+
193
+ item.dependencies =
194
+ typeof item.dependencies === "function"
195
+ ? item.dependencies()
196
+ : item.dependencies;
197
+
198
+ this.resources.set(item.id, {
199
+ resource: item,
200
+ config: {},
201
+ value: undefined,
202
+ isInitialized: false,
203
+ });
204
+
205
+ this.computeRegisterOfResource(item, {});
206
+ }
207
+
208
+ private storeTask<C>(item: ITask<any, any, {}>) {
209
+ this.checkIfIDExists(item.id);
210
+
211
+ item.dependencies =
212
+ typeof item.dependencies === "function"
213
+ ? item.dependencies()
214
+ : item.dependencies;
215
+
216
+ this.storeEvent(item.events.beforeRun);
217
+ this.storeEvent(item.events.afterRun);
218
+ this.storeEvent(item.events.onError);
219
+
220
+ this.tasks.set(item.id, {
221
+ task: item,
222
+ computedDependencies: {},
223
+ isInitialized: false,
224
+ });
225
+ }
226
+
227
+ getDependentNodes(): IDependentNode[] {
228
+ const depenedants: IDependentNode[] = [];
229
+ for (const task of this.tasks.values()) {
230
+ depenedants.push({
231
+ id: task.task.id,
232
+ dependencies: task.task.dependencies,
233
+ });
234
+ }
235
+ for (const middleware of this.middlewares.values()) {
236
+ depenedants.push({
237
+ id: middleware.middleware.id,
238
+ dependencies: middleware.middleware.dependencies,
239
+ });
240
+ }
241
+ for (const resource of this.resources.values()) {
242
+ depenedants.push({
243
+ id: resource.resource.id,
244
+ dependencies: resource.resource.dependencies || {},
245
+ });
246
+ }
247
+
248
+ return depenedants;
249
+ }
250
+ }
@@ -0,0 +1,135 @@
1
+ import { DependencyMapType, DependencyValuesType, ITask } from "./defs";
2
+ import { Errors } from "./errors";
3
+ import { EventManager } from "./EventManager";
4
+ import { globalEvents } from "./globalEvents";
5
+ import {
6
+ MiddlewareStoreElementType,
7
+ Store,
8
+ TaskStoreElementType,
9
+ } from "./Store";
10
+
11
+ export class TaskRunner {
12
+ protected readonly runnerStore = new Map<
13
+ string,
14
+ (input: any) => Promise<any>
15
+ >();
16
+
17
+ constructor(
18
+ protected readonly store: Store,
19
+ protected readonly eventManager: EventManager
20
+ ) {}
21
+
22
+ /**
23
+ * Begins the execution of an task. These are registered tasks and all sanity checks have been performed at this stage to ensure consistency of the object.
24
+ * This function can throw only if any of the event listeners or run function throws
25
+ */
26
+ public async run<
27
+ TInput,
28
+ TOutput extends Promise<any>,
29
+ TDeps extends DependencyMapType
30
+ >(
31
+ task: ITask<TInput, TOutput, TDeps>,
32
+ input: TInput,
33
+ taskDependencies?: DependencyValuesType<TDeps>
34
+ ): Promise<TOutput | undefined> {
35
+ let runner = this.runnerStore.get(task.id);
36
+ if (!runner) {
37
+ const storeTask = this.store.tasks.get(task.id) as TaskStoreElementType;
38
+ const deps = taskDependencies || storeTask.computedDependencies;
39
+
40
+ runner = this.createRunnerWithMiddleware<TInput, TOutput, TDeps>(
41
+ task,
42
+ deps
43
+ );
44
+
45
+ this.runnerStore.set(task.id, runner);
46
+ }
47
+
48
+ // begin by dispatching the event of creating it.
49
+ // then ensure the hooks are called
50
+ // then ensure the middleware are called
51
+ await this.eventManager.emit(task.events.beforeRun, { input });
52
+ await this.eventManager.emit(globalEvents.tasks.beforeRun, {
53
+ task,
54
+ input,
55
+ });
56
+
57
+ let error;
58
+ try {
59
+ // craft the next function starting from the first next function
60
+ const output = await runner(input);
61
+
62
+ await this.eventManager.emit(task.events.afterRun, { input, output });
63
+ await this.eventManager.emit(globalEvents.tasks.afterRun, {
64
+ task,
65
+ input,
66
+ output,
67
+ });
68
+
69
+ return output;
70
+ } catch (e) {
71
+ error = e;
72
+
73
+ // If you want to rewthrow the error, this should be done inside the onError event.
74
+ await this.eventManager.emit(task.events.onError, { error });
75
+ await this.eventManager.emit(globalEvents.tasks.onError, {
76
+ task,
77
+ error,
78
+ });
79
+
80
+ throw e;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Creates the function with the chain of middleware.
86
+ * @param task
87
+ * @param input
88
+ * @param taskDependencies
89
+ * @returns
90
+ */
91
+ protected createRunnerWithMiddleware<
92
+ TInput,
93
+ TOutput extends Promise<any>,
94
+ TDeps extends DependencyMapType
95
+ >(
96
+ task: ITask<TInput, TOutput, TDeps>,
97
+ taskDependencies: DependencyValuesType<{}>
98
+ ) {
99
+ // this is the final next()
100
+ let next = async (input) => {
101
+ return task.run.call(null, input, taskDependencies as any);
102
+ };
103
+
104
+ const existingMiddlewares = task.middleware;
105
+ const createdMiddlewares = [
106
+ ...this.store.getGlobalMiddlewares(existingMiddlewares.map((x) => x.id)),
107
+ ...existingMiddlewares,
108
+ ];
109
+
110
+ if (createdMiddlewares.length > 0) {
111
+ // we need to run the middleware in reverse order
112
+ // so we can chain the next function
113
+ for (let i = createdMiddlewares.length - 1; i >= 0; i--) {
114
+ const middleware = createdMiddlewares[i];
115
+ const storeMiddleware = this.store.middlewares.get(
116
+ middleware.id
117
+ ) as MiddlewareStoreElementType; // we know it exists because at this stage all sanity checks have been done.
118
+
119
+ const nextFunction = next;
120
+ next = async (input) => {
121
+ return storeMiddleware.middleware.run(
122
+ {
123
+ taskDefinition: task as any,
124
+ input,
125
+ next: nextFunction,
126
+ },
127
+ storeMiddleware.computedDependencies
128
+ );
129
+ };
130
+ }
131
+ }
132
+
133
+ return next;
134
+ }
135
+ }
@@ -0,0 +1,96 @@
1
+ import { IEvent, IEventDefinition } from "../defs";
2
+ import { EventManager } from "../EventManager";
3
+
4
+ describe("EventManager", () => {
5
+ let eventManager: EventManager;
6
+
7
+ beforeEach(() => {
8
+ eventManager = new EventManager();
9
+ });
10
+
11
+ const testEvent: IEventDefinition<string> = {
12
+ id: "test-event",
13
+ };
14
+
15
+ const createEvent = (data: string): IEvent<string> => ({
16
+ id: testEvent.id,
17
+ data,
18
+ });
19
+
20
+ describe("emit", () => {
21
+ it("should emit an event and call the appropriate listeners", async () => {
22
+ const listener1 = jest.fn();
23
+ const listener2 = jest.fn();
24
+
25
+ eventManager.addListener(testEvent, listener1);
26
+ eventManager.addListener(testEvent, listener2);
27
+
28
+ await eventManager.emit(testEvent, "test data");
29
+
30
+ expect(listener1).toHaveBeenCalledWith(createEvent("test data"));
31
+ expect(listener2).toHaveBeenCalledWith(createEvent("test data"));
32
+ });
33
+
34
+ it("should respect the order of listeners", async () => {
35
+ const calls: number[] = [];
36
+ const listener1 = jest.fn(() => calls.push(1));
37
+ const listener2 = jest.fn(() => calls.push(2));
38
+ const listener3 = jest.fn(() => calls.push(3));
39
+
40
+ eventManager.addListener(testEvent, listener2, { order: 2 });
41
+ eventManager.addListener(testEvent, listener1, { order: 1 });
42
+ eventManager.addListener(testEvent, listener3, { order: 3 });
43
+
44
+ await eventManager.emit(testEvent, "test data");
45
+
46
+ expect(calls).toEqual([1, 2, 3]);
47
+ });
48
+
49
+ it("should apply filters to listeners", async () => {
50
+ const listener = jest.fn();
51
+ const filter = jest.fn((event: IEvent<string>) => event.data === "pass");
52
+
53
+ eventManager.addListener(testEvent, listener, { filter });
54
+
55
+ await eventManager.emit(testEvent, "pass");
56
+ await eventManager.emit(testEvent, "fail");
57
+
58
+ expect(listener).toHaveBeenCalledTimes(1);
59
+ expect(listener).toHaveBeenCalledWith(createEvent("pass"));
60
+ });
61
+ });
62
+
63
+ describe("addListener", () => {
64
+ it("should add a listener for a single event", () => {
65
+ const listener = jest.fn();
66
+ eventManager.addListener(testEvent, listener);
67
+
68
+ // @ts-ignore: Accessing private property for testing
69
+ expect(eventManager.listeners.get(testEvent.id)).toHaveLength(1);
70
+ });
71
+
72
+ it("should add a listener for multiple events", () => {
73
+ const listener = jest.fn();
74
+ const event1: IEventDefinition = { id: "event1" };
75
+ const event2: IEventDefinition = { id: "event2" };
76
+
77
+ eventManager.addListener([event1, event2], listener);
78
+
79
+ // @ts-ignore: Accessing private property for testing
80
+ expect(eventManager.listeners.get(event1.id)).toHaveLength(1);
81
+ // @ts-ignore: Accessing private property for testing
82
+ expect(eventManager.listeners.get(event2.id)).toHaveLength(1);
83
+ });
84
+ });
85
+
86
+ describe("addGlobalListener", () => {
87
+ it("should add a global listener", async () => {
88
+ const globalListener = jest.fn();
89
+ eventManager.addGlobalListener(globalListener);
90
+
91
+ await eventManager.emit(testEvent, "test data");
92
+
93
+ expect(globalListener).toHaveBeenCalledWith(createEvent("test data"));
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,109 @@
1
+ import { ResourceInitializer } from "../ResourceInitializer";
2
+ import { Store } from "../Store";
3
+ import { EventManager } from "../EventManager";
4
+ import { defineResource } from "../define";
5
+
6
+ describe("ResourceInitializer", () => {
7
+ let store: Store;
8
+ let eventManager: EventManager;
9
+ let resourceInitializer: ResourceInitializer;
10
+
11
+ beforeEach(() => {
12
+ eventManager = new EventManager();
13
+ store = new Store(eventManager);
14
+ resourceInitializer = new ResourceInitializer(store, eventManager);
15
+ });
16
+
17
+ it("should initialize a resource and emit events", async () => {
18
+ const mockResource = defineResource({
19
+ id: "testResource",
20
+ init: jest.fn().mockResolvedValue("initialized value"),
21
+ });
22
+
23
+ const mockConfig = undefined;
24
+ const mockDependencies = {};
25
+
26
+ const emitSpy = jest.spyOn(eventManager, "emit");
27
+
28
+ const result = await resourceInitializer.initializeResource(
29
+ mockResource,
30
+ mockConfig,
31
+ mockDependencies
32
+ );
33
+
34
+ expect(result).toBe("initialized value");
35
+ expect(mockResource.init).toHaveBeenCalledWith(
36
+ mockConfig,
37
+ mockDependencies
38
+ );
39
+ expect(emitSpy).toHaveBeenCalledWith(mockResource.events.beforeInit, {
40
+ config: mockConfig,
41
+ });
42
+ expect(emitSpy).toHaveBeenCalledWith(mockResource.events.afterInit, {
43
+ config: mockConfig,
44
+ value: "initialized value",
45
+ });
46
+ });
47
+
48
+ it("should handle errors and emit onError event", async () => {
49
+ const mockError = new Error("Initialization error");
50
+ const mockResource = defineResource({
51
+ id: "testResource",
52
+ init: jest.fn().mockRejectedValue(mockError),
53
+ });
54
+
55
+ const mockConfig = undefined;
56
+ const mockDependencies = {};
57
+
58
+ const emitSpy = jest.spyOn(eventManager, "emit");
59
+
60
+ let result;
61
+ try {
62
+ result = await resourceInitializer.initializeResource(
63
+ mockResource,
64
+ mockConfig,
65
+ mockDependencies
66
+ );
67
+ } catch (e) {}
68
+
69
+ expect(result).toBeUndefined();
70
+ expect(mockResource.init).toHaveBeenCalledWith(
71
+ mockConfig,
72
+ mockDependencies
73
+ );
74
+ expect(emitSpy).toHaveBeenCalledTimes(4);
75
+ expect(emitSpy).toHaveBeenCalledWith(mockResource.events.beforeInit, {
76
+ config: mockConfig,
77
+ });
78
+ expect(emitSpy).toHaveBeenCalledWith(mockResource.events.onError, {
79
+ error: mockError,
80
+ });
81
+ });
82
+
83
+ it("should handle resources without init function", async () => {
84
+ const mockResource = defineResource<number>({
85
+ id: "testResource",
86
+ });
87
+
88
+ const mockConfig = 42;
89
+ const mockDependencies = {};
90
+
91
+ const emitSpy = jest.spyOn(eventManager, "emit");
92
+
93
+ const result = await resourceInitializer.initializeResource(
94
+ mockResource,
95
+ mockConfig,
96
+ mockDependencies
97
+ );
98
+
99
+ expect(result).toBeUndefined();
100
+ expect(emitSpy).toHaveBeenCalledTimes(4);
101
+ expect(emitSpy).toHaveBeenCalledWith(mockResource.events.beforeInit, {
102
+ config: mockConfig,
103
+ });
104
+ expect(emitSpy).toHaveBeenCalledWith(mockResource.events.afterInit, {
105
+ config: mockConfig,
106
+ value: undefined,
107
+ });
108
+ });
109
+ });