@rango-dev/queue-manager-core 0.1.10-next.69

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/src/queue.ts ADDED
@@ -0,0 +1,623 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { ManagerContext, QueueDef, QueueID } from './manager';
3
+ import { QueueStorage, Status } from './types';
4
+
5
+ type TaskId = string;
6
+ export type TaskState = {
7
+ status: Status;
8
+ blockedFor: any;
9
+ };
10
+
11
+ export interface QueueContext {
12
+ _queue?: {
13
+ id: string;
14
+ };
15
+ }
16
+ export type NextParams = {
17
+ context: ManagerContext;
18
+ };
19
+
20
+ export interface QueueState {
21
+ status: Status;
22
+ activeTaskIndex: number;
23
+ tasks: {
24
+ [key in TaskId]: TaskState;
25
+ };
26
+ }
27
+
28
+ export interface Task {
29
+ id: TaskId;
30
+ action: string;
31
+ }
32
+
33
+ export type InitTasks = {
34
+ tasks: Task[];
35
+ state: QueueState;
36
+ storage: QueueStorage;
37
+ };
38
+
39
+ export interface TaskEvent {
40
+ id: TaskId;
41
+ task: TaskState;
42
+ action: string;
43
+ }
44
+
45
+ export interface QueueEventHandlers {
46
+ // all tasks
47
+ onUpdateListStatus: (status: Status) => void;
48
+ // single task
49
+ onCreate: (event: TaskEvent) => void;
50
+ onUpdate: (event: TaskEvent) => void;
51
+ onStorageUpdate: (data: QueueStorage) => void;
52
+ onBlock: (
53
+ data: Omit<TaskEvent, 'task'> & { reason: Record<string, unknown> }
54
+ ) => void;
55
+ onUnblock: (data: { id: string }) => void;
56
+ }
57
+
58
+ interface QueueOptions {
59
+ id: QueueID;
60
+ events: QueueEventHandlers;
61
+ actions: QueueDef['actions'];
62
+ }
63
+
64
+ /**
65
+ *
66
+ * Notes on statuses:
67
+ * - Success: last task has success status
68
+ * - Failed: any of task has been failed
69
+ * - Running: first task is not on pending
70
+ * - Pending: default state. not started
71
+ *
72
+ */
73
+ class Queue {
74
+ public id: string;
75
+ public state: QueueState = {
76
+ status: Status.PENDING,
77
+ activeTaskIndex: 0,
78
+ tasks: {},
79
+ };
80
+ public tasks: Task[] = [];
81
+ private events: QueueOptions['events'];
82
+ private actions: QueueOptions['actions'];
83
+ private storage: QueueStorage = {};
84
+
85
+ constructor(options: QueueOptions) {
86
+ this.id = options.id;
87
+ this.events = options.events;
88
+ this.actions = options.actions;
89
+ }
90
+
91
+ /**
92
+ * Update queue status and trigger an event
93
+ *
94
+ */
95
+ private updateQueueStatus(status: Status) {
96
+ this.state.status = status;
97
+ this.events.onUpdateListStatus(status);
98
+ }
99
+
100
+ private updateActiveTaskIndex(index: number) {
101
+ this.state.activeTaskIndex = index;
102
+ }
103
+
104
+ /**
105
+ *
106
+ * Update task state (`status`, `blockedFor`) and trigger an event.
107
+ * @param id
108
+ * @param nextState
109
+ */
110
+ private updateTaskState(id: TaskId, nextState: Partial<TaskState>) {
111
+ if (nextState.status) {
112
+ this.state.tasks[id].status = nextState.status;
113
+ }
114
+ if (nextState.blockedFor) {
115
+ this.state.tasks[id].blockedFor = nextState.blockedFor;
116
+ }
117
+
118
+ const updatedTaskEvent = {
119
+ id: id,
120
+ task: this.get(id)!,
121
+ action: this.tasks.find((task) => task.id === id)!.action,
122
+ };
123
+ this.events.onUpdate(updatedTaskEvent);
124
+ }
125
+
126
+ /**
127
+ * Create a task by providing an `action` name.
128
+ *
129
+ * this method creating the task, push it to the queue's tasks and trigger an event.
130
+ * @param action
131
+ */
132
+ createTask(action: string) {
133
+ const id = uuidv4();
134
+ this.tasks.push({
135
+ action,
136
+ id,
137
+ });
138
+ this.state.tasks[id] = {
139
+ status: Status.PENDING,
140
+ blockedFor: null,
141
+ };
142
+
143
+ const createdTask = this.get(id);
144
+ this.events.onCreate({
145
+ id,
146
+ task: createdTask!,
147
+ action,
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Initilize a queue with some tasks, instead of an empty queue.
153
+ *
154
+ * Using it for recover the queue state from persistor.
155
+ *
156
+ */
157
+ public initTasks(info: InitTasks) {
158
+ this.state = info.state;
159
+ this.storage = info.storage;
160
+
161
+ info.tasks.forEach((task) => {
162
+ this.tasks.push(task);
163
+ const action = this.tasks.find((t) => t.id === task.id)!.action;
164
+ this.events.onCreate({
165
+ id: task.id,
166
+ task: this.get(task.id)!,
167
+ action,
168
+ });
169
+ });
170
+ }
171
+
172
+ public checkBlock() {
173
+ const currentActiveTask = this.getActiveTask();
174
+
175
+ if (!currentActiveTask) {
176
+ return;
177
+ }
178
+ const { task, state } = currentActiveTask;
179
+
180
+ if (state.status === Status.BLOCKED) {
181
+ this.events.onBlock({
182
+ action: task.action,
183
+ id: task.id,
184
+ reason: state.blockedFor,
185
+ });
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Getting task state by ID
191
+ */
192
+ public get(id: string): TaskState | null {
193
+ const task = this.state.tasks[id];
194
+
195
+ if (!task) return null;
196
+
197
+ return task;
198
+ }
199
+
200
+ /**
201
+ *
202
+ * Checking if status of the last is `SUCCESS` or not
203
+ *
204
+ */
205
+ private lastTaskIsSuccessful() {
206
+ const lastTask = this.tasks[this.tasks.length - 1];
207
+
208
+ // checking for empty lists.
209
+ if (lastTask) {
210
+ const lastTaskState = this.state.tasks[lastTask.id];
211
+
212
+ // Maybe we didn't create the state yet. It should has success status as well.
213
+ if (!!lastTaskState && lastTaskState.status === Status.SUCCESS) {
214
+ return true;
215
+ }
216
+ }
217
+
218
+ return false;
219
+ }
220
+
221
+ /**
222
+ * If the first task started (it's not PENDING),
223
+ * it means the queue has been ran.
224
+ *
225
+ * @returns
226
+ */
227
+ private firstTaskIsStarted() {
228
+ const firstTask = this.tasks[0];
229
+
230
+ // checking for empty lists.
231
+ if (firstTask) {
232
+ const firstTaskState = this.state.tasks[firstTask.id];
233
+
234
+ // Maybe we didn't create the state yet.
235
+ if (!!firstTaskState) {
236
+ if (firstTaskState.status !== Status.PENDING) {
237
+ return true;
238
+ }
239
+ }
240
+ }
241
+
242
+ return false;
243
+ }
244
+
245
+ /**
246
+ * Getting active task index from state and
247
+ * returns the task and its state.
248
+ *
249
+ */
250
+ private getActiveTask() {
251
+ // First try to get task with `activeTask`
252
+ const index = this.state.activeTaskIndex;
253
+ const task = this.tasks[index];
254
+
255
+ // Consider all the tasks has been executed.
256
+ if (!task) {
257
+ return null;
258
+ }
259
+
260
+ const state = this.state.tasks[task.id];
261
+
262
+ return { task, state, index };
263
+ }
264
+
265
+ /**
266
+ * Update and find the queue status by checking state of each tasks in the queue.
267
+ */
268
+ public check() {
269
+ const currentListStatus = this.state.status;
270
+ let nextListStatus = this.firstTaskIsStarted()
271
+ ? Status.RUNNING
272
+ : Status.PENDING;
273
+
274
+ if (this.lastTaskIsSuccessful()) {
275
+ nextListStatus = Status.SUCCESS;
276
+ } else {
277
+ // Is there any failed task?
278
+ for (const task of this.tasks) {
279
+ const state = this.state.tasks[task.id];
280
+
281
+ // If one item fails, we stop to work on the list.
282
+ if (state.status === Status.FAILED) {
283
+ nextListStatus = Status.FAILED;
284
+ break;
285
+ }
286
+ }
287
+ }
288
+
289
+ // We only update and trigger an event when there is a new value
290
+ if (nextListStatus !== currentListStatus) {
291
+ this.updateQueueStatus(nextListStatus);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Execute active task.
297
+ * Based on the state of active task, the behaviour is different. If status is:
298
+ * - Success -> we need to go to next task by updating the active index and try `next` on more time.
299
+ * - Failed, Running, or no active task -> doesn't do anything.
300
+ * - Pending or Blocked -> update the task and queue status to running, and execute the provided `action` for the task.
301
+ *
302
+ * The queue is a linked list somehow, active task means the pointer to where we are in the queue right now.
303
+ *
304
+ */
305
+ public next(params: NextParams) {
306
+ console.log('[next]', this.state, params);
307
+
308
+ this.check();
309
+
310
+ const currentActiveTask = this.getActiveTask();
311
+
312
+ if (!currentActiveTask) {
313
+ return;
314
+ }
315
+
316
+ const {
317
+ index: activeTaskIndex,
318
+ task: activeTask,
319
+ state: activeTaskState,
320
+ } = currentActiveTask;
321
+
322
+ // if `activeTask` is already done, we will go for next one.
323
+ if (activeTaskState.status === Status.SUCCESS) {
324
+ this.updateActiveTaskIndex(activeTaskIndex + 1);
325
+ this.next(params);
326
+ return;
327
+ }
328
+
329
+ if (activeTaskState.status === Status.FAILED) {
330
+ console.log('Task has been failed. It can not be proceed.');
331
+ return;
332
+ }
333
+
334
+ if (activeTaskState.status === Status.CANCELED) {
335
+ console.log('Task has been canceled. It can not be proceed.');
336
+ return;
337
+ }
338
+
339
+ if (activeTaskState.status === Status.RUNNING) {
340
+ console.log('Task is running. It can not be proceed.');
341
+ return;
342
+ }
343
+
344
+ if (
345
+ activeTaskState.status === Status.PENDING ||
346
+ activeTaskState.status === Status.BLOCKED
347
+ ) {
348
+ // Update task status to `running`
349
+ this.updateTaskState(activeTask.id, {
350
+ status: Status.RUNNING,
351
+ });
352
+ this.updateQueueStatus(Status.RUNNING);
353
+
354
+ // Try to execute task.
355
+ const execute = this.actions[activeTask.action];
356
+ execute({
357
+ context: this.getContext(params),
358
+ next: () => {
359
+ console.log('[execute][next]', params);
360
+ this.markCurrentTaskAsFinished(params);
361
+ },
362
+ retry: () => {
363
+ this.resume(params);
364
+ },
365
+ failed: () => {
366
+ this.updateTaskState(activeTask.id, {
367
+ status: Status.FAILED,
368
+ });
369
+ this.check();
370
+ },
371
+ schedule: (action) => {
372
+ this.createTask(action);
373
+ this.check();
374
+ },
375
+ getStorage: this.getStorage.bind(this),
376
+ setStorage: this.setStorage.bind(this),
377
+ block: (reason: Record<string, unknown>) => {
378
+ this.block({ reason });
379
+ },
380
+ unblock: () => {
381
+ this.unblock();
382
+ },
383
+ });
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Change the `status` of active task and queue to BLOCKED, then trigger an event.
389
+ */
390
+ public block({
391
+ reason,
392
+ silent = false,
393
+ }: {
394
+ reason: Record<string, unknown>;
395
+ silent?: boolean;
396
+ }) {
397
+ const currentActiveTask = this.getActiveTask();
398
+
399
+ if (!currentActiveTask) {
400
+ throw new Error("Task isn't exist.");
401
+ }
402
+
403
+ this.updateTaskState(currentActiveTask.task.id, {
404
+ status: Status.BLOCKED,
405
+ blockedFor: reason,
406
+ });
407
+ this.updateQueueStatus(Status.BLOCKED);
408
+
409
+ if (!silent) {
410
+ this.events.onBlock({
411
+ action: currentActiveTask.task.action,
412
+ id: currentActiveTask.task.id,
413
+ reason,
414
+ });
415
+ }
416
+ }
417
+
418
+ /**
419
+ * If the active task is `BLOCKED`, update the task status to `PENDING`, queue status to `RUNNING`
420
+ * then trigger an event.
421
+ */
422
+ public unblock() {
423
+ const currentActiveTask = this.getActiveTask();
424
+
425
+ if (
426
+ !currentActiveTask ||
427
+ currentActiveTask.state.status !== Status.BLOCKED
428
+ ) {
429
+ throw new Error('Task is not blocked.');
430
+ }
431
+
432
+ this.updateTaskState(currentActiveTask.task.id, {
433
+ status: Status.PENDING,
434
+ });
435
+ this.updateQueueStatus(Status.RUNNING);
436
+ this.events.onUnblock({ id: currentActiveTask.task.id });
437
+ }
438
+
439
+ /**
440
+ * If the active task is `BLOCKED` then execute the `action` for the task.
441
+ *
442
+ * It is useful for when we need to run a blocked task without changing the status of task at the first place.
443
+ * For scenarios like we have some conditions in `action` and needs to run the `action` again
444
+ * to check the conditions are met or not, if yes, so we can proceed the `action`.
445
+ *
446
+ */
447
+ public forceRun(params: NextParams) {
448
+ const currentTask = this.getActiveTask();
449
+
450
+ if (!currentTask || currentTask.state.status !== Status.BLOCKED) {
451
+ throw new Error('Task is not blocked.');
452
+ }
453
+
454
+ // Try to execute task.
455
+ const execute = this.actions[currentTask.task.action];
456
+ execute({
457
+ context: this.getContext(params),
458
+ next: () => {
459
+ console.log('[force run][execute][next]', params);
460
+
461
+ /*
462
+ NOTE:
463
+ When running `forceRun`, the status of task can be `BLOCKED`
464
+ So we need to change to `Running` first.
465
+ */
466
+ // Update task status to `running`
467
+ this.updateTaskState(currentTask.task.id, {
468
+ status: Status.RUNNING,
469
+ });
470
+ this.updateQueueStatus(Status.RUNNING);
471
+
472
+ this.markCurrentTaskAsFinished(params);
473
+ },
474
+ retry: () => {
475
+ this.resume(params);
476
+ },
477
+ failed: () => {
478
+ this.updateTaskState(currentTask.task.id, {
479
+ status: Status.FAILED,
480
+ });
481
+ this.check();
482
+ },
483
+ schedule: (action) => {
484
+ this.createTask(action);
485
+ this.check();
486
+ },
487
+ getStorage: this.getStorage.bind(this),
488
+ setStorage: this.setStorage.bind(this),
489
+ block: (reason: Record<string, unknown>) => {
490
+ this.block({ reason });
491
+ },
492
+ unblock: () => {
493
+ this.unblock();
494
+ },
495
+ });
496
+ }
497
+ private markCurrentTaskAsFinished(params: NextParams) {
498
+ this.check();
499
+ const activeTaskIndex = this.state.activeTaskIndex;
500
+ const activeTask = this.tasks[activeTaskIndex];
501
+
502
+ if (!activeTask) {
503
+ console.log("It seems this queue has been finished. Task doesn't exist.");
504
+ return;
505
+ }
506
+
507
+ const activeTaskState = this.state.tasks[activeTask.id];
508
+ if (activeTaskState.status === Status.RUNNING) {
509
+ this.updateTaskState(activeTask.id, {
510
+ status: Status.SUCCESS,
511
+ });
512
+ this.updateActiveTaskIndex(activeTaskIndex + 1);
513
+
514
+ const updatedTaskEvent = {
515
+ id: activeTask.id,
516
+ task: this.get(activeTask.id)!,
517
+ action: this.tasks.find((task) => task.id === activeTask.id)!.action,
518
+ };
519
+ this.events.onUpdate(updatedTaskEvent);
520
+ this.next(params);
521
+ } else {
522
+ console.log('There is no running task.');
523
+ }
524
+ }
525
+
526
+ /**
527
+ * If the active task is `RUNNING`, change it to `PENING` the try to run the task by calling `next`.
528
+ * If it's other than `RUNNING` we reset the queue state and run the queue from the beggining.
529
+ *
530
+ * @param params
531
+ * @returns
532
+ */
533
+ public resume(params: NextParams) {
534
+ const activeTaskIndex = this.state.activeTaskIndex;
535
+ const activeTask = this.tasks[activeTaskIndex];
536
+ if (!activeTask) {
537
+ console.log("It seems this queue has been finished. Task doesn't exist.");
538
+ return;
539
+ }
540
+
541
+ const activeTaskState = this.state.tasks[activeTask.id];
542
+ if (activeTaskState.status === Status.RUNNING) {
543
+ this.updateTaskState(activeTask.id, {
544
+ status: Status.PENDING,
545
+ });
546
+ this.next(params);
547
+ } else {
548
+ console.log('There is no running task. restart the queue', {
549
+ state: this.state.tasks,
550
+ activeTaskState,
551
+ activeTask,
552
+ activeTaskIndex,
553
+ });
554
+ this.resetState();
555
+ this.next(params);
556
+ }
557
+ }
558
+
559
+ /**
560
+ *
561
+ * Cancel the queue by changing active task and queue status to `CANCELED`.
562
+ *
563
+ */
564
+ public cancel() {
565
+ const currentActiveTask = this.getActiveTask();
566
+
567
+ if (
568
+ !currentActiveTask ||
569
+ [Status.FAILED, Status.CANCELED, Status.SUCCESS].includes(
570
+ currentActiveTask.state.status
571
+ )
572
+ ) {
573
+ return;
574
+ }
575
+
576
+ const { task } = currentActiveTask;
577
+
578
+ // Update task status to `canceled`
579
+ this.updateTaskState(task.id, {
580
+ status: Status.CANCELED,
581
+ });
582
+ const updatedTaskEvent = {
583
+ id: task.id,
584
+ task: this.get(task.id)!,
585
+ action: this.tasks.find((t) => t.id === task.id)!.action,
586
+ };
587
+ this.events.onUpdate(updatedTaskEvent);
588
+
589
+ // Update queue status to `canceled`
590
+ this.updateQueueStatus(Status.CANCELED);
591
+ }
592
+
593
+ /**
594
+ * Update queue status, and all the tasks inside queue to `PENDING`.
595
+ */
596
+ private resetState() {
597
+ this.state.activeTaskIndex = 0;
598
+ this.state.status = Status.PENDING;
599
+ Object.keys(this.state.tasks).forEach((id) => {
600
+ this.state.tasks[id].status = Status.PENDING;
601
+ });
602
+ }
603
+
604
+ public getStorage() {
605
+ return this.storage;
606
+ }
607
+ public setStorage(data: QueueStorage) {
608
+ this.storage = data;
609
+ this.events.onStorageUpdate(data);
610
+ return this.storage;
611
+ }
612
+
613
+ private getContext(params: NextParams): QueueContext & ManagerContext {
614
+ return {
615
+ ...params.context,
616
+ _queue: {
617
+ id: this.id,
618
+ },
619
+ };
620
+ }
621
+ }
622
+
623
+ export default Queue;
package/src/types.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { QueueID, QueueName } from './manager';
2
+ import Queue, { QueueState, Task } from './queue';
3
+
4
+ export enum Status {
5
+ PENDING = 'PENDING',
6
+ RUNNING = 'RUNNING',
7
+ FAILED = 'FAILED',
8
+ SUCCESS = 'SUCCESS',
9
+ CANCELED = 'CANCELED',
10
+ BLOCKED = 'BLOCKED',
11
+ }
12
+
13
+ export type QueueStorage = Record<string, unknown>;
14
+
15
+ export type QueueType = Queue;
16
+
17
+ export interface PersistedQueue {
18
+ id: QueueID;
19
+ createdAt: number;
20
+ name: QueueName;
21
+ status: Status;
22
+ state: QueueState;
23
+ tasks: Task[];
24
+ storage: QueueStorage;
25
+ }