@alterior/tasks 3.5.3 → 3.5.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alterior/tasks",
3
- "version": "3.5.3",
3
+ "version": "3.5.6",
4
4
  "description": "Flexible background task system",
5
5
  "author": "The Alterior Project (https://github.com/alterior-mvc)",
6
6
  "license": "MIT",
@@ -40,11 +40,11 @@
40
40
  "docs": "typedoc ."
41
41
  },
42
42
  "dependencies": {
43
- "@alterior/annotations": "^3.4.0",
43
+ "@alterior/annotations": "^3.5.6",
44
44
  "@alterior/common": "^3.4.0",
45
- "@alterior/di": "^3.5.3",
46
- "@alterior/logging": "^3.5.3",
47
- "@alterior/runtime": "^3.5.3",
45
+ "@alterior/di": "^3.5.6",
46
+ "@alterior/logging": "^3.5.6",
47
+ "@alterior/runtime": "^3.5.6",
48
48
  "bull": "^3.18.0",
49
49
  "kind-of": ">=6.0.3",
50
50
  "tslib": "^2.3.1"
@@ -60,5 +60,5 @@
60
60
  "swagger-editor-dist": "^3.9.0",
61
61
  "wtfnode": "^0.7.0"
62
62
  },
63
- "gitHead": "808d85ec078d91261d8388c717d77c02aefc568a"
63
+ "gitHead": "220e169fc8c855dc0935620bdb5efb1e9a2b86a7"
64
64
  }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './tasks';
2
+ export * from './task-runner';
3
+ export * from './task-worker';
4
+
5
+ export * from './tasks.module';
@@ -0,0 +1,33 @@
1
+ import { Injectable, Inject } from "@alterior/di";
2
+ import { QUEUE_OPTIONS, TaskJob, TaskAnnotation, TaskModuleOptionsRef, TaskModuleOptions, TaskWorkerRegistry, Constructor, Worker, RemoteWorker, RemoteService } from "./tasks";
3
+ import { InvalidOperationError, ArgumentError } from "@alterior/common";
4
+
5
+ @Injectable()
6
+ export class TaskRunner {
7
+ constructor(
8
+ private _registry : TaskWorkerRegistry
9
+ ) {
10
+ }
11
+
12
+ get registry() {
13
+ return this._registry;
14
+ }
15
+
16
+ /**
17
+ * Acquire a Remote for the given service where any calls to the remote will
18
+ * resolve to a QueueJob which can be further interacted with. Promise will
19
+ * resolve once the item has been successfully delivered to the event queue.
20
+ */
21
+ worker<T extends Worker>(workerClass : Constructor<T>): RemoteWorker<T> {
22
+ return this.registry.get<T>(workerClass).async;
23
+ }
24
+
25
+ /**
26
+ * Acquire a Remote for the given service where any calls to the remote will await
27
+ * the full completion of the remote call and resolve to the return value of the
28
+ * remote function. If you only want to enqueue a task, use `worker()` instead.
29
+ */
30
+ service<T extends Worker>(workerClass : Constructor<T>): RemoteService<T> {
31
+ return this.registry.get<T>(workerClass).sync;
32
+ }
33
+ }
@@ -0,0 +1,136 @@
1
+ import { InvalidOperationError, ArgumentError, ArgumentNullError } from "@alterior/common";
2
+ import { Injector, Provider, ReflectiveInjector } from "@alterior/di";
3
+ import { TaskAnnotation, TaskJob, TaskModuleOptions, TaskQueueClient, Worker } from "./tasks";
4
+ import { ApplicationOptions } from "@alterior/runtime";
5
+ import { Type } from "@alterior/runtime";
6
+ import * as Queue from "bull";
7
+ import { Logger } from "@alterior/logging";
8
+ import * as util from 'util';
9
+
10
+ export interface TaskHandler {
11
+ worker : Worker;
12
+ handler : (methodName : string, args : any[]) => Promise<any>;
13
+ }
14
+
15
+ export class TaskWorker {
16
+ constructor(
17
+ private _injector : Injector,
18
+ private _client : TaskQueueClient,
19
+ private _options : TaskModuleOptions,
20
+ private _appOptions : ApplicationOptions,
21
+ private _logger : Logger
22
+ ) {
23
+ if (!_injector)
24
+ throw new ArgumentNullError(`injector`);
25
+
26
+ if (!_options)
27
+ throw new ArgumentNullError(`options`);
28
+
29
+ if (!_appOptions)
30
+ throw new ArgumentNullError(`appOptions`);
31
+
32
+ }
33
+
34
+ public get injector() {
35
+ return this._injector;
36
+ }
37
+
38
+ public get options() {
39
+ return this._options;
40
+ }
41
+
42
+ public get appOptions() {
43
+ return this._appOptions;
44
+ }
45
+
46
+ private _taskHandlers = {};
47
+
48
+ get queue() {
49
+ return this._client.queue;
50
+ }
51
+
52
+ stop() {
53
+ this.queue.close();
54
+ }
55
+
56
+ start() {
57
+ this.queue.process(async (job : Queue.Job<TaskJob>, done) => {
58
+ let task = job.data;
59
+
60
+ if (!task || !task.id) {
61
+ console.log(`TaskWorker: Could not process invalid task:`);
62
+ console.dir(task, { depth: 3 });
63
+ console.log(`Associated job data:`);
64
+ console.dir(job, { depth: 3 });
65
+
66
+ await job.discard();
67
+
68
+ done(new Error(`Invalid job task`), null);
69
+ }
70
+
71
+ let handler : TaskHandler = this._taskHandlers[task.id];
72
+
73
+ if (!handler) {
74
+ console.error(`No handler for task ID '${task.id}'! Check that your worker class declares this task ID!`);
75
+
76
+ console.info(`Task was: `);
77
+ console.dir(task, { depth: 3 });
78
+
79
+ console.info(`Registered worker IDs: ${Object.keys(this._taskHandlers).join(', ')}`);
80
+ }
81
+
82
+ await this._logger.withContext(
83
+ { host: 'tasks', worker: handler.worker },
84
+ `TaskWorker | ${handler.worker.constructor.name}.${task.method}(${task.args.map(x => util.inspect(x, false, 2)).join(', ')})`,
85
+ async () => {
86
+ this._logger.info(`TaskWorker: ${task.method}() of worker ${handler.worker.constructor.name} (ID '${task.id}')`);
87
+ try {
88
+ let result = await handler.handler(task.method, task.args);
89
+ done(undefined, result);
90
+ } catch (e) {
91
+ console.error(`Caught error while running task ${job.data.id}.${job.data.method || 'execute'}():`);
92
+ console.error(e);
93
+
94
+ done(e);
95
+ }
96
+ }
97
+ );
98
+ });
99
+ }
100
+
101
+ registerHandler(name : string, handler : TaskHandler) {
102
+ this._taskHandlers[name] = handler;
103
+ }
104
+
105
+ registerClasses(taskClasses : Function[]) {
106
+
107
+ let providers : Provider[] = taskClasses as Provider[];
108
+ let ownInjector = ReflectiveInjector.resolveAndCreate(providers, this.injector);
109
+ let allRoutes = [];
110
+
111
+ let tasks = taskClasses.map(taskClass => {
112
+
113
+ let id = taskClass.name;
114
+ let annotation = TaskAnnotation.getForClass(taskClass);
115
+ let instance : Worker = ownInjector.get(taskClass);
116
+
117
+ if (instance.name)
118
+ id = instance.name;
119
+
120
+ if (annotation && annotation.id)
121
+ id = annotation.id;
122
+
123
+ this.registerHandler(id, {
124
+ worker: instance,
125
+ handler: async (methodName, args) => {
126
+ let impl = instance.constructor.prototype[methodName];
127
+
128
+ if (typeof impl !== 'function' || methodName.startsWith('_'))
129
+ throw new InvalidOperationError(`Invalid task method ${methodName}`);
130
+
131
+ return await instance[methodName](...args);
132
+ }
133
+ });
134
+ });
135
+ }
136
+ }
@@ -0,0 +1,99 @@
1
+ import { Module, Injectable, Optional } from "@alterior/di";
2
+ import { OnInit, Application, RolesService, Constructor } from "@alterior/runtime";
3
+ import * as Queue from "bull";
4
+ import { TaskModuleOptions, TaskModuleOptionsRef, TaskQueueClient, TaskWorkerRegistry } from "./tasks";
5
+ import { TaskRunner } from "./task-runner";
6
+ import { TaskWorker } from "./task-worker";
7
+ import { Logger, LoggingModule } from "@alterior/logging";
8
+
9
+ /**
10
+ * Import this into your application module to run tasks enqueued by other
11
+ * services on a shared queue. The tasks which can be processed are specified
12
+ * in the `tasks` field of one or more modules.
13
+ */
14
+ @Module({
15
+ providers: [
16
+ TaskQueueClient,
17
+ TaskWorkerRegistry,
18
+ TaskRunner
19
+ ],
20
+ imports: [
21
+ LoggingModule
22
+ ]
23
+ })
24
+ export class TasksModule implements OnInit {
25
+ constructor(
26
+ private app : Application,
27
+ private rolesService : RolesService,
28
+ private client : TaskQueueClient,
29
+ private workerRegistry : TaskWorkerRegistry,
30
+ private logger : Logger,
31
+ @Optional() private _options : TaskModuleOptionsRef
32
+ ) {
33
+
34
+ }
35
+
36
+ /**
37
+ * Used when importing this module from the root (app) module
38
+ * using the default configuration.
39
+ * Should be called only once in the application.
40
+ */
41
+ public static forRoot() {
42
+ return this.configure({});
43
+ }
44
+
45
+ /**
46
+ * Create a configured version of the WebServerModule that can be then
47
+ * be imported into an entry module (or feature module).
48
+ * @param options The options to use for the web server
49
+ */
50
+ public static configure(options : TaskModuleOptions) {
51
+ return {
52
+ $module: TasksModule,
53
+ providers: [
54
+ { provide: TaskModuleOptionsRef, useValue: new TaskModuleOptionsRef(options) }
55
+ ]
56
+ }
57
+ }
58
+
59
+ worker : TaskWorker;
60
+
61
+ get options(): TaskModuleOptions {
62
+ return this._options ? this._options.options : {} || {};
63
+ }
64
+
65
+ get tasks(): Function[] {
66
+ return [].concat(...this.app.runtime.definitions.map(x => x.metadata.tasks || []));
67
+ }
68
+
69
+ altOnInit() {
70
+
71
+ this.workerRegistry.registerClasses(this.tasks);
72
+
73
+ this.worker = new TaskWorker(
74
+ this.app.runtime.injector,
75
+ this.client,
76
+ this.options,
77
+ this.app.options,
78
+ this.logger
79
+ );
80
+ this.worker.registerClasses(this.tasks);
81
+
82
+ let self = this;
83
+
84
+ this.rolesService.registerRole({
85
+ identifier: 'task-worker',
86
+ instance: this,
87
+ name: 'Task Worker',
88
+ summary: 'Pulls from the task queue and executes them using task classes registered in the module tree',
89
+ async start() {
90
+ self.worker.start();
91
+ },
92
+
93
+ async stop() {
94
+ self.worker.stop();
95
+ }
96
+ })
97
+
98
+ }
99
+ }
package/src/tasks.ts ADDED
@@ -0,0 +1,229 @@
1
+ import { Annotation, MetadataName, AnnotationDecorator } from "@alterior/annotations";
2
+ import { Injectable, InjectionToken, Optional, Injector, Provider, ReflectiveInjector } from "@alterior/di";
3
+ import BullQueue from "bull";
4
+
5
+ export interface TaskModuleOptions {
6
+ queueName? : string;
7
+ queueOptions? : BullQueue.QueueOptions;
8
+ }
9
+
10
+
11
+ /**
12
+ * This injectable allows configuration of the task system.
13
+ * Include a provider for the injection token `QUEUE_OPTIONS`
14
+ * which provides an instance of this class.
15
+ *
16
+ * For instance: `[ provide: QUEUE_OPTIONS, useValue: new TaskClientOptionsRef({ optionsHere }) ]`
17
+ *
18
+ */
19
+ @Injectable()
20
+ export class TaskModuleOptionsRef {
21
+ constructor(options : TaskModuleOptions) {
22
+ this.options = options;
23
+ }
24
+
25
+ public options : TaskModuleOptions;
26
+ }
27
+
28
+ export interface TaskJob {
29
+ id : string;
30
+ method : string;
31
+ args : any[];
32
+ }
33
+
34
+ export const QUEUE_OPTIONS = new InjectionToken<BullQueue.QueueOptions>('QueueOptions');
35
+
36
+ @MetadataName('@alterior/tasks:Task')
37
+ export class TaskAnnotation extends Annotation {
38
+ constructor(readonly id? : string) {
39
+ super();
40
+ }
41
+ }
42
+
43
+ export const Task = TaskAnnotation.decorator();
44
+
45
+ export abstract class Worker {
46
+ abstract get name() : string;
47
+ get options() : JobOptions { return undefined; }
48
+
49
+ protected get currentJob() : QueueJob<TaskJob> {
50
+ return Zone.current.get('workerStateJob');
51
+ }
52
+ }
53
+
54
+ export type RemoteService<T> = {
55
+ [P in keyof T]:
56
+ T[P] extends (...args: any[]) => any ?
57
+ // methods
58
+ (ReturnType<T[P]> extends Promise<any> ?
59
+ T[P] // dont modify methods that are already promises
60
+ : (...args : Parameters<T[P]>) => Promise<ReturnType<T[P]>>
61
+ )
62
+ // fields
63
+ : never
64
+ ;
65
+ } & {
66
+ withOptions(options : JobOptions) : RemoteService<T>;
67
+ }
68
+
69
+ export type RemoteWorker<T> = {
70
+ [P in keyof T]:
71
+ T[P] extends (...args: any[]) => any ?
72
+ // methods
73
+ (...args : Parameters<T[P]>) => Promise<QueueJob<TaskJob>>
74
+
75
+ // fields
76
+ : never
77
+ ;
78
+ } & {
79
+ withOptions(options : JobOptions) : RemoteWorker<T>;
80
+ };
81
+
82
+ export type QueueOptions = BullQueue.QueueOptions;
83
+ export type JobOptions = BullQueue.JobOptions;
84
+ export type QueueJob<T> = BullQueue.Job<T>;
85
+ export type Queue<T> = BullQueue.Queue<T>;
86
+
87
+ export interface Constructor<T> {
88
+ new (...args) : T;
89
+ }
90
+
91
+ export type PromiseWrap<T> = T extends PromiseLike<any> ? T : Promise<T>;
92
+
93
+ export class TaskWorkerProxy {
94
+ private static create<T extends Worker>(handler : (key, ...args) => any): any {
95
+ return <RemoteWorker<T>> new Proxy({}, {
96
+ get(t, key : string, receiver) {
97
+ return (...args) => handler(key, ...args);
98
+ }
99
+ })
100
+ }
101
+
102
+ static createAsync<T extends Worker>(queueClient : TaskQueueClient, id : string, options? : JobOptions): RemoteWorker<T> {
103
+ return this.create(
104
+ (method, ...args) => {
105
+ if (method === 'withOptions')
106
+ return TaskWorkerProxy.createAsync(queueClient, id, Object.assign({}, options, args[0]));
107
+ else
108
+ return queueClient.enqueue({ id, method, args }, options)
109
+ }
110
+ );
111
+ }
112
+
113
+ static createSync<T extends Worker>(queueClient : TaskQueueClient, id : string, options? : JobOptions): RemoteService<T> {
114
+ return this.create(
115
+ (method, ...args) => {
116
+ if (method === 'withOptions')
117
+ return TaskWorkerProxy.createSync(queueClient, id, Object.assign({}, options, args[0]));
118
+ else
119
+ return queueClient.enqueue({ id, method, args }, options).then(v => v.finished);
120
+ }
121
+ );
122
+ }
123
+ }
124
+
125
+ interface TaskWorkerEntry<T extends Worker = any> {
126
+ type : Constructor<T>;
127
+ local : T;
128
+ async : RemoteWorker<T>;
129
+ sync : RemoteService<T>;
130
+ }
131
+
132
+
133
+ @Injectable()
134
+ export class TaskQueueClient {
135
+ constructor(
136
+ @Optional()
137
+ private optionsRef : TaskModuleOptionsRef
138
+ ) {
139
+ this._queue = new BullQueue(this.queueName, this.queueOptions);
140
+ }
141
+
142
+ _queue : BullQueue.Queue;
143
+
144
+ get queue(): Queue<TaskJob> {
145
+ return this._queue;
146
+ }
147
+
148
+ /**
149
+ * Get the task client options. See
150
+ */
151
+ get options(): TaskModuleOptions {
152
+ return (this.optionsRef ? this.optionsRef.options : undefined) || {};
153
+ }
154
+
155
+ get queueName(): string {
156
+ return this.options.queueName || 'alteriorTasks';
157
+ }
158
+
159
+ get queueOptions() {
160
+ let queueOptions = Object.assign({}, this.options.queueOptions);
161
+
162
+ if (!queueOptions.redis) {
163
+ queueOptions.redis = {
164
+ port: 6379,
165
+ host: '127.0.0.1',
166
+ db: 6
167
+ }
168
+ }
169
+
170
+ return queueOptions;
171
+ }
172
+
173
+ /**
174
+ * Enqueue a new task. To handle the task on the worker side, register for it with `.process()`
175
+ */
176
+ async enqueue(data : TaskJob, opts? : JobOptions): Promise<QueueJob<TaskJob>> {
177
+ return await this._queue.add(data, opts);
178
+ }
179
+ }
180
+
181
+ @Injectable()
182
+ export class TaskWorkerRegistry {
183
+ constructor(
184
+ private injector : Injector,
185
+ private client : TaskQueueClient
186
+ ) {
187
+
188
+ }
189
+
190
+ private _entries : { [name : string] : TaskWorkerEntry } = {};
191
+
192
+ private registerClass(injector : Injector, taskClass : Constructor<Worker>) {
193
+ let instance = <Worker> injector.get(taskClass);
194
+ let id = instance.name;
195
+
196
+ if (this._entries[id])
197
+ throw new Error(`Another worker is already registered with name '${id}'`);
198
+
199
+ this._entries[id] = {
200
+ type: <any> taskClass,
201
+ local: instance,
202
+ sync: TaskWorkerProxy.createSync(this.client, id, instance.options),
203
+ async: TaskWorkerProxy.createAsync(this.client, id, instance.options)
204
+ };
205
+ }
206
+
207
+ get all() : TaskWorkerEntry[] {
208
+ return Object.values(this._entries);
209
+ }
210
+
211
+ get<T extends Worker>(cls : Constructor<T>): TaskWorkerEntry<T> {
212
+ let entry = this.all.find(x => x.type === cls);
213
+
214
+ if (!entry)
215
+ throw new Error(`Worker class ${cls.name} is not registered. Add it to the 'tasks' property of a module or call TaskWorkerRegistry.register(${cls.name})`);
216
+
217
+ return <TaskWorkerEntry<T>> entry;
218
+ }
219
+
220
+ getByName(name : string) {
221
+ return this._entries[name];
222
+ }
223
+
224
+ registerClasses(classes : Function[]) {
225
+ let taskClasses : Constructor<Worker>[] = classes as any;
226
+ let ownInjector = ReflectiveInjector.resolveAndCreate(taskClasses as Provider[], this.injector);
227
+ taskClasses.forEach(taskClass => this.registerClass(ownInjector, taskClass));
228
+ }
229
+ }
package/dist.esm/test.js DELETED
@@ -1,7 +0,0 @@
1
- import "reflect-metadata";
2
- import { suite } from 'razmin';
3
- suite()
4
- .withTimeout(10 * 1000)
5
- .include(['**/*.test.js'])
6
- .run();
7
- //# sourceMappingURL=test.js.map