@tstdl/base 0.93.100 → 0.93.102
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/authentication/client/authentication.service.d.ts +1 -1
- package/authentication/client/authentication.service.js +23 -11
- package/notification/api/index.d.ts +1 -0
- package/notification/api/index.js +1 -0
- package/notification/api/notification.api.d.ts +8 -16
- package/notification/api/notification.api.js +13 -26
- package/notification/index.d.ts +1 -1
- package/notification/index.js +1 -1
- package/notification/models/in-app-notification.model.d.ts +9 -4
- package/notification/models/in-app-notification.model.js +25 -10
- package/notification/models/index.d.ts +1 -1
- package/notification/models/index.js +1 -1
- package/notification/models/notification-log.model.d.ts +42 -5
- package/notification/models/notification-log.model.js +34 -20
- package/notification/models/notification-preference.model.d.ts +2 -2
- package/notification/models/notification-preference.model.js +9 -9
- package/notification/models/notification-type.model.d.ts +17 -0
- package/notification/models/{notification-category.model.js → notification-type.model.js} +12 -13
- package/notification/models/web-push-subscription.model.d.ts +2 -2
- package/notification/models/web-push-subscription.model.js +8 -7
- package/notification/server/api/notification.api-controller.d.ts +2 -2
- package/notification/server/api/notification.api-controller.js +4 -3
- package/notification/server/drizzle/{0000_glorious_randall.sql → 0000_shiny_the_anarchist.sql} +27 -32
- package/notification/server/drizzle/meta/0000_snapshot.json +179 -179
- package/notification/server/drizzle/meta/_journal.json +2 -2
- package/notification/server/module.d.ts +2 -0
- package/notification/server/module.js +1 -0
- package/notification/server/providers/channel-provider.d.ts +4 -3
- package/notification/server/providers/channel-provider.js +2 -1
- package/notification/server/providers/email-channel-provider.d.ts +3 -3
- package/notification/server/providers/email-channel-provider.js +7 -9
- package/notification/server/providers/in-app-channel-provider.d.ts +5 -5
- package/notification/server/providers/in-app-channel-provider.js +15 -16
- package/notification/server/providers/index.d.ts +1 -1
- package/notification/server/providers/index.js +1 -1
- package/notification/server/providers/web-push-channel-provider.d.ts +5 -4
- package/notification/server/providers/web-push-channel-provider.js +8 -7
- package/notification/server/schemas.d.ts +3 -3
- package/notification/server/schemas.js +3 -4
- package/notification/server/services/index.d.ts +2 -4
- package/notification/server/services/index.js +2 -4
- package/notification/server/services/notification-delivery.worker.d.ts +7 -1
- package/notification/server/services/notification-delivery.worker.js +49 -37
- package/notification/server/services/notification-sse.service.d.ts +4 -7
- package/notification/server/services/notification-sse.service.js +4 -11
- package/notification/server/services/notification-template.d.ts +2 -2
- package/notification/server/services/notification-template.js +3 -1
- package/notification/server/services/notification-template.service.d.ts +1 -1
- package/notification/server/services/notification-template.service.js +7 -3
- package/notification/server/services/notification-type.service.d.ts +11 -0
- package/notification/server/services/notification-type.service.js +41 -0
- package/notification/server/services/notification.service.d.ts +4 -5
- package/notification/server/services/notification.service.js +44 -27
- package/notification/tests/notification-api.test.js +95 -0
- package/notification/tests/notification-flow.test.js +174 -28
- package/notification/tests/notification-type.service.test.d.ts +1 -0
- package/notification/tests/notification-type.service.test.js +35 -0
- package/package.json +1 -1
- package/rate-limit/postgres/postgres-rate-limiter.d.ts +9 -4
- package/rate-limit/postgres/postgres-rate-limiter.js +17 -10
- package/rate-limit/rate-limiter.d.ts +6 -6
- package/rate-limit/tests/postgres-rate-limiter.test.js +1 -1
- package/task-queue/postgres/task-queue.js +1 -1
- package/task-queue/task-context.d.ts +1 -14
- package/task-queue/task-context.js +0 -30
- package/task-queue/task-queue.d.ts +4 -12
- package/task-queue/task-queue.js +38 -89
- package/task-queue/tests/extensive-dependencies.test.d.ts +1 -0
- package/task-queue/tests/extensive-dependencies.test.js +234 -0
- package/task-queue/tests/worker.test.js +0 -21
- package/task-queue/types.d.ts +1 -8
- package/notification/enums.d.ts +0 -22
- package/notification/enums.js +0 -19
- package/notification/models/notification-category.model.d.ts +0 -17
- package/notification/server/services/notification-category.service.d.ts +0 -11
- package/notification/server/services/notification-category.service.js +0 -41
- package/notification/server/services/notification-delivery.task.d.ts +0 -9
- package/notification/server/services/notification-delivery.task.js +0 -1
- package/notification/server/services/singleton.d.ts +0 -3
- package/notification/server/services/singleton.js +0 -10
- package/notification/tests/notification-category.service.test.js +0 -36
- package/notification/tests/test-notification.model.d.ts +0 -4
- package/notification/tests/test-notification.model.js +0 -25
- /package/notification/tests/{notification-category.service.test.d.ts → notification-api.test.d.ts} +0 -0
|
@@ -89,7 +89,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
89
89
|
#rateLimiter = inject(RateLimiter, {
|
|
90
90
|
resource: this.#namespace,
|
|
91
91
|
burstCapacity: this.#config.rateLimit ?? defaultQueueConfig.rateLimit,
|
|
92
|
-
|
|
92
|
+
refillInterval: this.#config.rateInterval ?? defaultQueueConfig.rateInterval,
|
|
93
93
|
});
|
|
94
94
|
#circuitBreaker = inject(CircuitBreaker, {
|
|
95
95
|
key: this.#namespace,
|
|
@@ -2,7 +2,7 @@ import type { CancellationSignal, CancellationToken } from '../cancellation/inde
|
|
|
2
2
|
import type { Logger } from '../logger/index.js';
|
|
3
3
|
import type { Transaction } from '../orm/server/index.js';
|
|
4
4
|
import { TaskQueue, type EnqueueManyItem, type EnqueueOptions } from './task-queue.js';
|
|
5
|
-
import type { TaskData, TaskDefinitionMap, TaskOfType, TaskResult, TaskState, TaskTypes
|
|
5
|
+
import type { TaskData, TaskDefinitionMap, TaskOfType, TaskResult, TaskState, TaskTypes } from './types.js';
|
|
6
6
|
export declare class TaskContext<Definitions extends TaskDefinitionMap, Type extends TaskTypes<Definitions>> {
|
|
7
7
|
#private;
|
|
8
8
|
constructor(queue: TaskQueue<Definitions>, task: TaskOfType<Definitions, Type>, signal: CancellationToken, logger: Logger);
|
|
@@ -43,16 +43,3 @@ export declare class TaskContext<Definitions extends TaskDefinitionMap, Type ext
|
|
|
43
43
|
transaction?: Transaction;
|
|
44
44
|
}): Promise<void>;
|
|
45
45
|
}
|
|
46
|
-
export declare class BatchTaskContext<Definitions extends TaskDefinitionMap, Type extends TaskTypes<Definitions>> {
|
|
47
|
-
#private;
|
|
48
|
-
constructor(queue: TaskQueue<Definitions>, tasks: TaskOfType<Definitions, Type>[], signal: CancellationToken, logger: Logger);
|
|
49
|
-
get tasks(): TaskOfType<Definitions, Type>[];
|
|
50
|
-
get signal(): CancellationSignal;
|
|
51
|
-
get logger(): Logger;
|
|
52
|
-
for<Type extends TaskTypes<Definitions>>(task: TaskOfType<Definitions, Type>): TaskContext<Definitions, Type>;
|
|
53
|
-
checkpointAll(options?: {
|
|
54
|
-
progresses?: number[];
|
|
55
|
-
states?: TasksStates<TaskOfType<Definitions, Type>[]>;
|
|
56
|
-
transaction?: Transaction;
|
|
57
|
-
}): Promise<void>;
|
|
58
|
-
}
|
|
@@ -92,33 +92,3 @@ export class TaskContext {
|
|
|
92
92
|
this.#signal.set();
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
export class BatchTaskContext {
|
|
96
|
-
#queue;
|
|
97
|
-
#tasks;
|
|
98
|
-
#logger;
|
|
99
|
-
#signal;
|
|
100
|
-
constructor(queue, tasks, signal, logger) {
|
|
101
|
-
this.#queue = queue;
|
|
102
|
-
this.#tasks = tasks;
|
|
103
|
-
this.#signal = signal;
|
|
104
|
-
this.#logger = logger.fork('Batch');
|
|
105
|
-
}
|
|
106
|
-
get tasks() {
|
|
107
|
-
return this.#tasks;
|
|
108
|
-
}
|
|
109
|
-
get signal() {
|
|
110
|
-
return this.#signal.signal;
|
|
111
|
-
}
|
|
112
|
-
get logger() {
|
|
113
|
-
return this.#logger;
|
|
114
|
-
}
|
|
115
|
-
for(task) {
|
|
116
|
-
return new TaskContext(this.#queue, task, this.#signal, this.#logger);
|
|
117
|
-
}
|
|
118
|
-
async checkpointAll(options) {
|
|
119
|
-
const validIds = await this.#queue.touchMany(this.#tasks, options);
|
|
120
|
-
if (validIds.length < this.#tasks.length) {
|
|
121
|
-
this.#logger.warn(`${this.#tasks.length - validIds.length} tasks in batch lost their lease during checkpoint`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
@@ -6,14 +6,14 @@ import type { Transaction } from '../orm/server/transaction.js';
|
|
|
6
6
|
import { Transactional } from '../orm/server/transactional.js';
|
|
7
7
|
import type { OneOrMany, Record, UndefinableJson } from '../types/types.js';
|
|
8
8
|
import { TaskQueueEnqueueBatch } from './enqueue-batch.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { ProcessWorker, TaskData, TaskDefinitionMap, TaskOfType, TaskProcessResultPayload, TaskResult, TasksResults, TasksStates, TaskState, TaskTypes } from './types.js';
|
|
10
10
|
export declare class TaskProcessResult<Result = unknown> {
|
|
11
11
|
readonly payload: TaskProcessResultPayload<Result>;
|
|
12
12
|
private constructor();
|
|
13
13
|
static Complete<Result>(result?: Result): TaskProcessResult<Result>;
|
|
14
|
-
static Fail(error: unknown, fatal?: boolean): TaskProcessResult
|
|
15
|
-
static RescheduleTo(timestamp: number): TaskProcessResult
|
|
16
|
-
static RescheduleBy(milliseconds: number): TaskProcessResult
|
|
14
|
+
static Fail<R>(error: unknown, fatal?: boolean): TaskProcessResult<R>;
|
|
15
|
+
static RescheduleTo<R>(timestamp: number): TaskProcessResult<R>;
|
|
16
|
+
static RescheduleBy<R>(milliseconds: number): TaskProcessResult<R>;
|
|
17
17
|
}
|
|
18
18
|
export declare const TaskStatus: {
|
|
19
19
|
/**
|
|
@@ -299,16 +299,8 @@ export declare abstract class TaskQueue<Definitions extends TaskDefinitionMap =
|
|
|
299
299
|
types?: Type[];
|
|
300
300
|
forceDequeue?: boolean;
|
|
301
301
|
}, handler: ProcessWorker<Definitions, Type>): void;
|
|
302
|
-
processBatch<Type extends TaskTypes<Definitions>>({ batchSize, concurrency, cancellationSignal, types, forceDequeue }: {
|
|
303
|
-
batchSize?: number;
|
|
304
|
-
concurrency?: number;
|
|
305
|
-
cancellationSignal: CancellationSignal;
|
|
306
|
-
types?: Type[];
|
|
307
|
-
forceDequeue?: boolean;
|
|
308
|
-
}, handler: ProcessBatchWorker<Definitions, Type>): void;
|
|
309
302
|
protected getTransactionalContextData(): QueueConfig & {
|
|
310
303
|
namespace: string;
|
|
311
304
|
};
|
|
312
305
|
private processWorker;
|
|
313
|
-
private processBatchWorker;
|
|
314
306
|
}
|
package/task-queue/task-queue.js
CHANGED
|
@@ -2,13 +2,12 @@ import { defineEnum } from '../enumeration/enumeration.js';
|
|
|
2
2
|
import { inject, injectArgument } from '../injector/index.js';
|
|
3
3
|
import { Logger } from '../logger/logger.js';
|
|
4
4
|
import { Transactional } from '../orm/server/transactional.js';
|
|
5
|
-
import { createArray } from '../utils/array/array.js';
|
|
6
5
|
import { currentTimestamp } from '../utils/date-time.js';
|
|
7
6
|
import { cancelableTimeout } from '../utils/timing.js';
|
|
8
|
-
import { isDefined, isString } from '../utils/type-guards.js';
|
|
7
|
+
import { isDefined, isString, isUndefined } from '../utils/type-guards.js';
|
|
9
8
|
import { millisecondsPerDay, millisecondsPerMinute, millisecondsPerSecond } from '../utils/units.js';
|
|
10
9
|
import { TaskQueueEnqueueBatch } from './enqueue-batch.js';
|
|
11
|
-
import {
|
|
10
|
+
import { TaskContext } from './task-context.js';
|
|
12
11
|
export class TaskProcessResult {
|
|
13
12
|
payload;
|
|
14
13
|
constructor(payload) {
|
|
@@ -89,114 +88,64 @@ export class TaskQueue extends Transactional {
|
|
|
89
88
|
void this.processWorker(cancellationSignal, handler, { types, forceDequeue });
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
|
-
processBatch({ batchSize = 10, concurrency = 1, cancellationSignal, types, forceDequeue }, handler) {
|
|
93
|
-
for (let i = 0; i < concurrency; i++) {
|
|
94
|
-
void this.processBatchWorker(batchSize, cancellationSignal, handler, { types, forceDequeue });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
91
|
getTransactionalContextData() {
|
|
98
92
|
return this.config;
|
|
99
93
|
}
|
|
100
94
|
async processWorker(cancellationSignal, handler, options) {
|
|
101
|
-
await this.
|
|
102
|
-
const
|
|
103
|
-
const context =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}, options);
|
|
107
|
-
}
|
|
108
|
-
async processBatchWorker(size, cancellationSignal, handler, options) {
|
|
109
|
-
for await (const tasks of this.getBatchConsumer(size, cancellationSignal, options)) {
|
|
110
|
-
const batchToken = cancellationSignal.createChild();
|
|
111
|
-
const context = new BatchTaskContext(this, tasks, batchToken, this.logger);
|
|
112
|
-
let activeTaskIds = new Set(tasks.map((t) => t.id));
|
|
113
|
-
context.logger.verbose(`Processing batch of ${tasks.length}`);
|
|
95
|
+
for await (const task of this.getConsumer(cancellationSignal, options)) {
|
|
96
|
+
const taskToken = cancellationSignal.createChild();
|
|
97
|
+
const context = new TaskContext(this, task, taskToken, this.logger);
|
|
98
|
+
let isTaskActive = true;
|
|
99
|
+
context.logger.verbose(`Processing task`);
|
|
114
100
|
void (async () => {
|
|
115
|
-
while (
|
|
116
|
-
await cancelableTimeout(Math.min(this.visibilityTimeout / 2, 5000),
|
|
117
|
-
if (
|
|
101
|
+
while (taskToken.isUnset) {
|
|
102
|
+
await cancelableTimeout(Math.min(this.visibilityTimeout / 2, 5000), taskToken);
|
|
103
|
+
if (taskToken.isSet) {
|
|
118
104
|
break;
|
|
119
105
|
}
|
|
120
106
|
try {
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
context.logger.warn(`Batch integrity compromised: ${lostCount} tasks lost lease. Aborting batch.`);
|
|
127
|
-
activeTaskIds = new Set(touchedIds);
|
|
128
|
-
batchToken.set();
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
activeTaskIds = new Set(touchedIds);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (activeTaskIds.size == 0 && batchToken.isUnset) {
|
|
135
|
-
context.logger.warn(`All tasks in batch lost lease. Stopping worker.`);
|
|
136
|
-
batchToken.set();
|
|
107
|
+
const touchedTask = await this.touch(task);
|
|
108
|
+
if (isUndefined(touchedTask)) {
|
|
109
|
+
context.logger.warn(`Task lost lease. Aborting.`);
|
|
110
|
+
isTaskActive = false;
|
|
111
|
+
taskToken.set();
|
|
137
112
|
}
|
|
138
113
|
}
|
|
139
114
|
catch (error) {
|
|
140
|
-
context.logger.error('Error touching
|
|
115
|
+
context.logger.error('Error touching task', error);
|
|
141
116
|
}
|
|
142
117
|
}
|
|
143
118
|
})();
|
|
144
119
|
try {
|
|
145
|
-
if (
|
|
146
|
-
throw new Error('
|
|
120
|
+
if (taskToken.isSet) {
|
|
121
|
+
throw new Error('Task cancelled before start');
|
|
147
122
|
}
|
|
148
|
-
const
|
|
149
|
-
if (isDefined(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
break;
|
|
166
|
-
default:
|
|
167
|
-
throw new Error(`Unsupported task result action.`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (completions.length > 0) {
|
|
171
|
-
context.logger.verbose(`Completing ${completions.length} tasks`);
|
|
172
|
-
await this.completeMany(completions.map((c) => c.task), { results: completions.map((c) => c.result) });
|
|
173
|
-
}
|
|
174
|
-
if (failures.length > 0) {
|
|
175
|
-
context.logger.verbose(`Failing ${failures.length} tasks`);
|
|
176
|
-
for (const item of failures) {
|
|
177
|
-
await this.fail(item.task, item.error, { fatal: item.fatal });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (reschedules.length > 0) {
|
|
181
|
-
context.logger.verbose(`Rescheduling ${reschedules.length} tasks`);
|
|
182
|
-
const reschedulesByTimestamp = new Map();
|
|
183
|
-
for (const item of reschedules) {
|
|
184
|
-
const ids = reschedulesByTimestamp.get(item.timestamp) ?? [];
|
|
185
|
-
ids.push(item.task.id);
|
|
186
|
-
reschedulesByTimestamp.set(item.timestamp, ids);
|
|
187
|
-
}
|
|
188
|
-
for (const [timestamp, ids] of reschedulesByTimestamp) {
|
|
189
|
-
await this.rescheduleMany(ids, timestamp);
|
|
190
|
-
}
|
|
123
|
+
const result = await handler(context);
|
|
124
|
+
if (isDefined(result) && isTaskActive) {
|
|
125
|
+
switch (result.payload.action) {
|
|
126
|
+
case 'complete':
|
|
127
|
+
context.logger.verbose(`Completing task`);
|
|
128
|
+
await this.complete(task, { result: result.payload.result });
|
|
129
|
+
break;
|
|
130
|
+
case 'fail':
|
|
131
|
+
context.logger.verbose(`Failing task`);
|
|
132
|
+
await this.fail(task, result.payload.error, { fatal: result.payload.fatal });
|
|
133
|
+
break;
|
|
134
|
+
case 'reschedule':
|
|
135
|
+
context.logger.verbose(`Rescheduling task`);
|
|
136
|
+
await this.reschedule(task.id, result.payload.timestamp);
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
throw new Error(`Unsupported task result action.`);
|
|
191
140
|
}
|
|
192
141
|
}
|
|
193
142
|
}
|
|
194
143
|
catch (error) {
|
|
195
|
-
context.logger.error('Error processing
|
|
196
|
-
await this.
|
|
144
|
+
context.logger.error('Error processing task', error);
|
|
145
|
+
await this.fail(task, error);
|
|
197
146
|
}
|
|
198
147
|
finally {
|
|
199
|
-
|
|
148
|
+
taskToken.set();
|
|
200
149
|
}
|
|
201
150
|
}
|
|
202
151
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { DependencyJoinMode, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
|
|
3
|
+
import { setupIntegrationTest } from '../../unit-test/index.js';
|
|
4
|
+
import { timeout } from '../../utils/timing.js';
|
|
5
|
+
describe('Extensive Task Queue Dependency Tests', () => {
|
|
6
|
+
let injector;
|
|
7
|
+
let queue;
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
({ injector } = await setupIntegrationTest({ modules: { taskQueue: true } }));
|
|
10
|
+
});
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
13
|
+
const queueName = `extensive-dep-queue-${Date.now()}-${Math.random()}`;
|
|
14
|
+
queue = queueProvider.get(queueName, {
|
|
15
|
+
visibilityTimeout: 1000,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await queue.clear();
|
|
20
|
+
});
|
|
21
|
+
afterAll(async () => {
|
|
22
|
+
await injector?.dispose();
|
|
23
|
+
});
|
|
24
|
+
async function waitForStatus(id, status) {
|
|
25
|
+
for (let i = 0; i < 100; i++) {
|
|
26
|
+
const task = await queue.getTask(id);
|
|
27
|
+
if (task?.status === status)
|
|
28
|
+
return;
|
|
29
|
+
await queue.processPendingFanIn();
|
|
30
|
+
await timeout(50);
|
|
31
|
+
}
|
|
32
|
+
const finalTask = await queue.getTask(id);
|
|
33
|
+
throw new Error(`Task ${id} did not reach status ${status}. Current status: ${finalTask?.status}`);
|
|
34
|
+
}
|
|
35
|
+
async function completeTask(type) {
|
|
36
|
+
const dequeued = await queue.dequeue({ types: [type] });
|
|
37
|
+
if (!dequeued)
|
|
38
|
+
throw new Error(`Could not dequeue task of type ${type}`);
|
|
39
|
+
await queue.complete(dequeued);
|
|
40
|
+
await queue.processPendingFanIn();
|
|
41
|
+
}
|
|
42
|
+
it('should handle complex mixed chain: A -> (B -> D, C -> E) -> F', async () => {
|
|
43
|
+
// F depends on D and E
|
|
44
|
+
const taskF = await queue.enqueue('F', {}, { scheduleAfterTags: ['mc-d', 'mc-e'] });
|
|
45
|
+
// D depends on B, E depends on C
|
|
46
|
+
const taskD = await queue.enqueue('D', {}, { tags: ['mc-d'], scheduleAfterTags: ['mc-b'] });
|
|
47
|
+
const taskE = await queue.enqueue('E', {}, { tags: ['mc-e'], scheduleAfterTags: ['mc-c'] });
|
|
48
|
+
// B and C depend on A
|
|
49
|
+
const taskB = await queue.enqueue('B', {}, { tags: ['mc-b'], scheduleAfterTags: ['mc-a'] });
|
|
50
|
+
const taskC = await queue.enqueue('C', {}, { tags: ['mc-c'], scheduleAfterTags: ['mc-a'] });
|
|
51
|
+
// A is the root
|
|
52
|
+
const taskA = await queue.enqueue('A', {}, { tags: ['mc-a'] });
|
|
53
|
+
expect(taskA.status).toBe(TaskStatus.Pending);
|
|
54
|
+
expect(taskB.status).toBe(TaskStatus.Waiting);
|
|
55
|
+
expect(taskC.status).toBe(TaskStatus.Waiting);
|
|
56
|
+
expect(taskD.status).toBe(TaskStatus.Waiting);
|
|
57
|
+
expect(taskE.status).toBe(TaskStatus.Waiting);
|
|
58
|
+
expect(taskF.status).toBe(TaskStatus.Waiting);
|
|
59
|
+
// 1. Complete A
|
|
60
|
+
await completeTask('A');
|
|
61
|
+
await waitForStatus(taskB.id, TaskStatus.Pending);
|
|
62
|
+
await waitForStatus(taskC.id, TaskStatus.Pending);
|
|
63
|
+
// 2. Complete B
|
|
64
|
+
await completeTask('B');
|
|
65
|
+
await waitForStatus(taskD.id, TaskStatus.Pending);
|
|
66
|
+
// 3. Complete C
|
|
67
|
+
await completeTask('C');
|
|
68
|
+
await waitForStatus(taskE.id, TaskStatus.Pending);
|
|
69
|
+
// F should still be waiting
|
|
70
|
+
const uF = await queue.getTask(taskF.id);
|
|
71
|
+
expect(uF?.status).toBe(TaskStatus.Waiting);
|
|
72
|
+
// 4. Complete D
|
|
73
|
+
await completeTask('D');
|
|
74
|
+
// F still waiting for E
|
|
75
|
+
expect((await queue.getTask(taskF.id))?.status).toBe(TaskStatus.Waiting);
|
|
76
|
+
// 5. Complete E
|
|
77
|
+
await completeTask('E');
|
|
78
|
+
await waitForStatus(taskF.id, TaskStatus.Pending);
|
|
79
|
+
});
|
|
80
|
+
it('should handle requested pattern: A -> B & C -> D', async () => {
|
|
81
|
+
const taskD = await queue.enqueue('D', {}, { scheduleAfterTags: ['B', 'C'] });
|
|
82
|
+
const taskB = await queue.enqueue('B', {}, { tags: ['B'], scheduleAfterTags: ['A'] });
|
|
83
|
+
const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['A'] });
|
|
84
|
+
const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
|
|
85
|
+
// Initial check
|
|
86
|
+
expect(taskA.status).toBe(TaskStatus.Pending);
|
|
87
|
+
expect(taskB.status).toBe(TaskStatus.Waiting);
|
|
88
|
+
expect(taskC.status).toBe(TaskStatus.Waiting);
|
|
89
|
+
expect(taskD.status).toBe(TaskStatus.Waiting);
|
|
90
|
+
// Complete A
|
|
91
|
+
await completeTask('A');
|
|
92
|
+
await waitForStatus(taskB.id, TaskStatus.Pending);
|
|
93
|
+
await waitForStatus(taskC.id, TaskStatus.Pending);
|
|
94
|
+
// B and C can be dequeued now (in any order)
|
|
95
|
+
const dB = await queue.dequeue({ types: ['B'] });
|
|
96
|
+
const dC = await queue.dequeue({ types: ['C'] });
|
|
97
|
+
expect(dB).toBeDefined();
|
|
98
|
+
expect(dC).toBeDefined();
|
|
99
|
+
// D still waiting
|
|
100
|
+
expect((await queue.getTask(taskD.id))?.status).toBe(TaskStatus.Waiting);
|
|
101
|
+
// Complete B
|
|
102
|
+
await queue.complete(dB);
|
|
103
|
+
await queue.processPendingFanIn();
|
|
104
|
+
expect((await queue.getTask(taskD.id))?.status).toBe(TaskStatus.Waiting);
|
|
105
|
+
// Complete C
|
|
106
|
+
await queue.complete(dC);
|
|
107
|
+
await waitForStatus(taskD.id, TaskStatus.Pending);
|
|
108
|
+
});
|
|
109
|
+
it('should strictly adhere to order: A -> B -> C', async () => {
|
|
110
|
+
const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['B'] });
|
|
111
|
+
const taskB = await queue.enqueue('B', {}, { tags: ['B'], scheduleAfterTags: ['A'] });
|
|
112
|
+
const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
|
|
113
|
+
// Try to dequeue B and C - should fail
|
|
114
|
+
const dC = await queue.dequeue({ types: ['C'] });
|
|
115
|
+
const dB = await queue.dequeue({ types: ['B'] });
|
|
116
|
+
expect(dC).toBeUndefined();
|
|
117
|
+
expect(dB).toBeUndefined();
|
|
118
|
+
// Complete A
|
|
119
|
+
const dA = await queue.dequeue({ types: ['A'] });
|
|
120
|
+
expect(dA?.id).toBe(taskA.id);
|
|
121
|
+
await queue.complete(dA);
|
|
122
|
+
await queue.processPendingFanIn();
|
|
123
|
+
// Now B should be available, but C still not
|
|
124
|
+
const dC_2 = await queue.dequeue({ types: ['C'] });
|
|
125
|
+
expect(dC_2).toBeUndefined();
|
|
126
|
+
const dB_2 = await queue.dequeue({ types: ['B'] });
|
|
127
|
+
expect(dB_2?.id).toBe(taskB.id);
|
|
128
|
+
await queue.complete(dB_2);
|
|
129
|
+
await queue.processPendingFanIn();
|
|
130
|
+
// Now C should be available
|
|
131
|
+
const dC_3 = await queue.dequeue({ types: ['C'] });
|
|
132
|
+
expect(dC_3?.id).toBe(taskC.id);
|
|
133
|
+
await queue.complete(dC_3);
|
|
134
|
+
});
|
|
135
|
+
it('should handle large fan-in: (T1, T2, T3, T4, T5) -> Result', async () => {
|
|
136
|
+
const tags = ['t1', 't2', 't3', 't4', 't5'];
|
|
137
|
+
const taskResult = await queue.enqueue('Result', {}, { scheduleAfterTags: tags });
|
|
138
|
+
for (const tag of tags) {
|
|
139
|
+
await queue.enqueue(`Task-${tag}`, {}, { tags: [tag] });
|
|
140
|
+
}
|
|
141
|
+
// Complete all but one
|
|
142
|
+
for (let i = 0; i < 4; i++) {
|
|
143
|
+
await completeTask(`Task-${tags[i]}`);
|
|
144
|
+
expect((await queue.getTask(taskResult.id))?.status).toBe(TaskStatus.Waiting);
|
|
145
|
+
}
|
|
146
|
+
// Complete the last one
|
|
147
|
+
await completeTask(`Task-${tags[4]}`);
|
|
148
|
+
await waitForStatus(taskResult.id, TaskStatus.Pending);
|
|
149
|
+
});
|
|
150
|
+
it('should handle large fan-in with OR: (T1, T2, T3, T4, T5) -> Result', async () => {
|
|
151
|
+
const tags = ['o1', 'o2', 'o3', 'o4', 'o5'];
|
|
152
|
+
const taskResult = await queue.enqueue('Result', {}, {
|
|
153
|
+
scheduleAfterTags: tags,
|
|
154
|
+
dependencyJoinMode: DependencyJoinMode.Or
|
|
155
|
+
});
|
|
156
|
+
for (const tag of tags) {
|
|
157
|
+
await queue.enqueue(`Task-${tag}`, {}, { tags: [tag] });
|
|
158
|
+
}
|
|
159
|
+
// Complete one
|
|
160
|
+
await completeTask(`Task-${tags[2]}`);
|
|
161
|
+
await waitForStatus(taskResult.id, TaskStatus.Pending);
|
|
162
|
+
});
|
|
163
|
+
it('should handle Diamond of Diamonds: A -> (B1, B2) -> C -> (D1, D2) -> E', async () => {
|
|
164
|
+
const taskE = await queue.enqueue('E', {}, { scheduleAfterTags: ['tag-d1', 'tag-d2'] });
|
|
165
|
+
const taskD1 = await queue.enqueue('D1', {}, { tags: ['tag-d1'], scheduleAfterTags: ['tag-c'] });
|
|
166
|
+
const taskD2 = await queue.enqueue('D2', {}, { tags: ['tag-d2'], scheduleAfterTags: ['tag-c'] });
|
|
167
|
+
const taskC = await queue.enqueue('C', {}, { tags: ['tag-c'], scheduleAfterTags: ['tag-b1', 'tag-b2'] });
|
|
168
|
+
const taskB1 = await queue.enqueue('B1', {}, { tags: ['tag-b1'], scheduleAfterTags: ['tag-a'] });
|
|
169
|
+
const taskB2 = await queue.enqueue('B2', {}, { tags: ['tag-b2'], scheduleAfterTags: ['tag-a'] });
|
|
170
|
+
const taskA = await queue.enqueue('A', {}, { tags: ['tag-a'] });
|
|
171
|
+
// Step by step completion
|
|
172
|
+
await completeTask('A');
|
|
173
|
+
await waitForStatus(taskB1.id, TaskStatus.Pending);
|
|
174
|
+
await waitForStatus(taskB2.id, TaskStatus.Pending);
|
|
175
|
+
await completeTask('B1');
|
|
176
|
+
expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
|
|
177
|
+
await completeTask('B2');
|
|
178
|
+
await waitForStatus(taskC.id, TaskStatus.Pending);
|
|
179
|
+
await completeTask('C');
|
|
180
|
+
await waitForStatus(taskD1.id, TaskStatus.Pending);
|
|
181
|
+
await waitForStatus(taskD2.id, TaskStatus.Pending);
|
|
182
|
+
await completeTask('D1');
|
|
183
|
+
expect((await queue.getTask(taskE.id))?.status).toBe(TaskStatus.Waiting);
|
|
184
|
+
await completeTask('D2');
|
|
185
|
+
await waitForStatus(taskE.id, TaskStatus.Pending);
|
|
186
|
+
});
|
|
187
|
+
it('should fail-fast entire branch if one dependency fails fatal', async () => {
|
|
188
|
+
// A -> B -> C
|
|
189
|
+
// -> D -> E
|
|
190
|
+
// (C & E) -> F
|
|
191
|
+
const taskF = await queue.enqueue('F', {}, { scheduleAfterTags: ['C', 'E'], failFast: true });
|
|
192
|
+
const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['B'], failFast: true });
|
|
193
|
+
const taskE = await queue.enqueue('E', {}, { tags: ['E'], scheduleAfterTags: ['D'], failFast: true });
|
|
194
|
+
const taskB = await queue.enqueue('B', {}, { tags: ['B'], scheduleAfterTags: ['A'], failFast: true });
|
|
195
|
+
const taskD = await queue.enqueue('D', {}, { tags: ['D'], scheduleAfterTags: ['A'], failFast: true });
|
|
196
|
+
const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
|
|
197
|
+
await completeTask('A');
|
|
198
|
+
await waitForStatus(taskB.id, TaskStatus.Pending);
|
|
199
|
+
await waitForStatus(taskD.id, TaskStatus.Pending);
|
|
200
|
+
// Fail B fatally
|
|
201
|
+
const dB = await queue.dequeue({ types: ['B'] });
|
|
202
|
+
await queue.fail(dB, new Error('fatal B'), { fatal: true });
|
|
203
|
+
await queue.processPendingFanIn();
|
|
204
|
+
// B failed fatally -> C should die -> F should die
|
|
205
|
+
await waitForStatus(taskC.id, TaskStatus.Dead);
|
|
206
|
+
await waitForStatus(taskF.id, TaskStatus.Dead);
|
|
207
|
+
// D and E should be unaffected (except E is still waiting for D)
|
|
208
|
+
const uD = await queue.getTask(taskD.id);
|
|
209
|
+
expect(uD?.status).toBe(TaskStatus.Pending);
|
|
210
|
+
});
|
|
211
|
+
it('should handle many-to-many dependencies', async () => {
|
|
212
|
+
// {A, B} -> {C, D} -> {E, F}
|
|
213
|
+
// Each of C, D depends on BOTH A and B.
|
|
214
|
+
// Each of E, F depends on BOTH C and D.
|
|
215
|
+
const taskE = await queue.enqueue('E', {}, { scheduleAfterTags: ['C', 'D'] });
|
|
216
|
+
const taskF = await queue.enqueue('F', {}, { scheduleAfterTags: ['C', 'D'] });
|
|
217
|
+
const taskC = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['A', 'B'] });
|
|
218
|
+
const taskD = await queue.enqueue('D', {}, { tags: ['D'], scheduleAfterTags: ['A', 'B'] });
|
|
219
|
+
const taskA = await queue.enqueue('A', {}, { tags: ['A'] });
|
|
220
|
+
const taskB = await queue.enqueue('B', {}, { tags: ['B'] });
|
|
221
|
+
await completeTask('A');
|
|
222
|
+
expect((await queue.getTask(taskC.id))?.status).toBe(TaskStatus.Waiting);
|
|
223
|
+
expect((await queue.getTask(taskD.id))?.status).toBe(TaskStatus.Waiting);
|
|
224
|
+
await completeTask('B');
|
|
225
|
+
await waitForStatus(taskC.id, TaskStatus.Pending);
|
|
226
|
+
await waitForStatus(taskD.id, TaskStatus.Pending);
|
|
227
|
+
await completeTask('C');
|
|
228
|
+
expect((await queue.getTask(taskE.id))?.status).toBe(TaskStatus.Waiting);
|
|
229
|
+
expect((await queue.getTask(taskF.id))?.status).toBe(TaskStatus.Waiting);
|
|
230
|
+
await completeTask('D');
|
|
231
|
+
await waitForStatus(taskE.id, TaskStatus.Pending);
|
|
232
|
+
await waitForStatus(taskF.id, TaskStatus.Pending);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -61,27 +61,6 @@ describe('Worker & Base Class Tests', () => {
|
|
|
61
61
|
expect(updated?.tries).toBe(1);
|
|
62
62
|
expect(updated?.error?.message).toBe('worker error');
|
|
63
63
|
});
|
|
64
|
-
it('should process batch of tasks', async () => {
|
|
65
|
-
await queue.enqueueMany([
|
|
66
|
-
{ type: 'batch', data: { v: 1 } },
|
|
67
|
-
{ type: 'batch', data: { v: 2 } },
|
|
68
|
-
{ type: 'batch', data: { v: 3 } },
|
|
69
|
-
]);
|
|
70
|
-
const processedBatch = [];
|
|
71
|
-
const token = new CancellationToken();
|
|
72
|
-
queue.processBatch({ batchSize: 2, cancellationSignal: token }, async (context) => {
|
|
73
|
-
expect(context.tasks.length).toBeLessThanOrEqual(2);
|
|
74
|
-
context.tasks.forEach(t => processedBatch.push(t.data['v']));
|
|
75
|
-
return context.tasks.map(() => TaskProcessResult.Complete());
|
|
76
|
-
});
|
|
77
|
-
for (let i = 0; i < 20; i++) {
|
|
78
|
-
if (processedBatch.length === 3)
|
|
79
|
-
break;
|
|
80
|
-
await timeout(100);
|
|
81
|
-
}
|
|
82
|
-
token.set();
|
|
83
|
-
expect(processedBatch.sort()).toEqual([1, 2, 3]);
|
|
84
|
-
});
|
|
85
64
|
it('should extend lease (heartbeat) during long processing', async () => {
|
|
86
65
|
const task = await queue.enqueue('long', {});
|
|
87
66
|
const token = new CancellationToken();
|
package/task-queue/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TaskContext } from './task-context.js';
|
|
2
2
|
import type { Task, TaskProcessResult } from './task-queue.js';
|
|
3
3
|
export type TaskDefinition<Data = unknown, State = unknown, Result = unknown> = {
|
|
4
4
|
data: Data;
|
|
@@ -39,10 +39,3 @@ export interface ProcessWorker<Definitions extends TaskDefinitionMap, Type exten
|
|
|
39
39
|
*/
|
|
40
40
|
(context: TaskContext<Definitions, Type>): TaskProcessResult<Definitions[Type]['result']> | Promise<TaskProcessResult<Definitions[Type]['result']>>;
|
|
41
41
|
}
|
|
42
|
-
export interface ProcessBatchWorker<Definitions extends TaskDefinitionMap, Type extends TaskTypes<Definitions>> {
|
|
43
|
-
/**
|
|
44
|
-
* A worker function that processes a batch of tasks.
|
|
45
|
-
* @param context The batch context providing tasks and helpers.
|
|
46
|
-
*/
|
|
47
|
-
(context: BatchTaskContext<Definitions, Type>): TaskProcessResult<Definitions[Type]['result']>[] | Promise<TaskProcessResult<Definitions[Type]['result']>[]>;
|
|
48
|
-
}
|
package/notification/enums.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type EnumType } from '../enumeration/index.js';
|
|
2
|
-
export declare const NotificationChannel: {
|
|
3
|
-
readonly InApp: "in-app";
|
|
4
|
-
readonly Email: "email";
|
|
5
|
-
readonly WebPush: "web-push";
|
|
6
|
-
};
|
|
7
|
-
export type NotificationChannel = EnumType<typeof NotificationChannel>;
|
|
8
|
-
export declare const NotificationPriority: {
|
|
9
|
-
readonly Low: "low";
|
|
10
|
-
readonly Medium: "medium";
|
|
11
|
-
readonly High: "high";
|
|
12
|
-
readonly Urgent: "urgent";
|
|
13
|
-
};
|
|
14
|
-
export type NotificationPriority = EnumType<typeof NotificationPriority>;
|
|
15
|
-
export declare const NotificationStatus: {
|
|
16
|
-
readonly Pending: "pending";
|
|
17
|
-
readonly Sent: "sent";
|
|
18
|
-
readonly Delivered: "delivered";
|
|
19
|
-
readonly Read: "read";
|
|
20
|
-
readonly Failed: "failed";
|
|
21
|
-
};
|
|
22
|
-
export type NotificationStatus = EnumType<typeof NotificationStatus>;
|
package/notification/enums.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { defineEnum } from '../enumeration/index.js';
|
|
2
|
-
export const NotificationChannel = defineEnum('NotificationChannel', {
|
|
3
|
-
InApp: 'in-app',
|
|
4
|
-
Email: 'email',
|
|
5
|
-
WebPush: 'web-push',
|
|
6
|
-
});
|
|
7
|
-
export const NotificationPriority = defineEnum('NotificationPriority', {
|
|
8
|
-
Low: 'low',
|
|
9
|
-
Medium: 'medium',
|
|
10
|
-
High: 'high',
|
|
11
|
-
Urgent: 'urgent',
|
|
12
|
-
});
|
|
13
|
-
export const NotificationStatus = defineEnum('NotificationStatus', {
|
|
14
|
-
Pending: 'pending',
|
|
15
|
-
Sent: 'sent',
|
|
16
|
-
Delivered: 'delivered',
|
|
17
|
-
Read: 'read',
|
|
18
|
-
Failed: 'failed',
|
|
19
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { TenantEntity } from '../../orm/index.js';
|
|
2
|
-
import { NotificationChannel } from '../enums.js';
|
|
3
|
-
export type ThrottlingConfig = {
|
|
4
|
-
limit: number;
|
|
5
|
-
intervalMs: number;
|
|
6
|
-
};
|
|
7
|
-
export type EscalationRule = {
|
|
8
|
-
delayMs: number;
|
|
9
|
-
channel: NotificationChannel;
|
|
10
|
-
};
|
|
11
|
-
export declare class NotificationCategory extends TenantEntity {
|
|
12
|
-
static readonly entityName = "NotificationCategory";
|
|
13
|
-
label: string;
|
|
14
|
-
key: string;
|
|
15
|
-
throttling: ThrottlingConfig | null;
|
|
16
|
-
escalations: EscalationRule[] | null;
|
|
17
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { Transactional } from '../../../orm/server/index.js';
|
|
2
|
-
import { NotificationCategory, type EscalationRule, type ThrottlingConfig } from '../../models/index.js';
|
|
3
|
-
export type CategoryInitializationData = {
|
|
4
|
-
label: string;
|
|
5
|
-
throttling?: ThrottlingConfig;
|
|
6
|
-
escalations?: EscalationRule[];
|
|
7
|
-
};
|
|
8
|
-
export declare class NotificationCategoryService extends Transactional {
|
|
9
|
-
readonly repository: import("../../../orm/server/index.js").EntityRepository<NotificationCategory>;
|
|
10
|
-
initializeCategories<T extends string>(tenantId: string, categoryData: Record<T, CategoryInitializationData>): Promise<Record<T, NotificationCategory>>;
|
|
11
|
-
}
|