@tstdl/base 0.93.143 → 0.93.144
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 -0
- package/authentication/client/authentication.service.js +20 -11
- package/authentication/client/http-client.middleware.js +6 -3
- package/authentication/tests/authentication.client-middleware.test.js +28 -0
- package/authentication/tests/authentication.client-service-methods.test.js +38 -6
- package/package.json +3 -3
- package/task-queue/postgres/task-queue.js +11 -11
- package/task-queue/postgres/task.model.d.ts +0 -6
- package/task-queue/postgres/task.model.js +1 -7
- package/task-queue/task-queue.d.ts +12 -2
- package/task-queue/task-queue.js +5 -0
- package/task-queue/tests/coverage-branch.test.js +2 -2
- package/task-queue/tests/fan-out-spawning.test.js +3 -3
- package/task-queue/tests/queue.test.js +4 -4
|
@@ -31,6 +31,7 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
|
|
|
31
31
|
private clockOffset;
|
|
32
32
|
private initialized;
|
|
33
33
|
private refreshLoopPromise;
|
|
34
|
+
private loggingOut;
|
|
34
35
|
/**
|
|
35
36
|
* Observable for authentication errors.
|
|
36
37
|
* Emits when a refresh fails.
|
|
@@ -74,6 +74,7 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
74
74
|
clockOffset = 0;
|
|
75
75
|
initialized = false;
|
|
76
76
|
refreshLoopPromise;
|
|
77
|
+
loggingOut;
|
|
77
78
|
/**
|
|
78
79
|
* Observable for authentication errors.
|
|
79
80
|
* Emits when a refresh fails.
|
|
@@ -233,18 +234,26 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
233
234
|
* This will attempt to end the session on the server and then clear local credentials.
|
|
234
235
|
*/
|
|
235
236
|
async logout() {
|
|
236
|
-
|
|
237
|
-
await
|
|
238
|
-
|
|
239
|
-
timeout(logoutTimeout),
|
|
240
|
-
]).catch((error) => this.logger.error(error));
|
|
241
|
-
}
|
|
242
|
-
finally {
|
|
243
|
-
// Always clear the local token, even if the server call fails.
|
|
244
|
-
this.forceRefreshRequested.set(false);
|
|
245
|
-
this.setNewToken(undefined);
|
|
246
|
-
this.loggedOutBus.publishAndForget();
|
|
237
|
+
if (isDefined(this.loggingOut)) {
|
|
238
|
+
await this.loggingOut;
|
|
239
|
+
return;
|
|
247
240
|
}
|
|
241
|
+
this.loggingOut = (async () => {
|
|
242
|
+
try {
|
|
243
|
+
await Promise.race([
|
|
244
|
+
this.client.endSession(),
|
|
245
|
+
timeout(logoutTimeout),
|
|
246
|
+
]).catch((error) => this.logger.error(error));
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
// Always clear the local token, even if the server call fails.
|
|
250
|
+
this.forceRefreshRequested.set(false);
|
|
251
|
+
this.setNewToken(undefined);
|
|
252
|
+
this.loggedOutBus.publishAndForget();
|
|
253
|
+
this.loggingOut = undefined;
|
|
254
|
+
}
|
|
255
|
+
})();
|
|
256
|
+
await this.loggingOut;
|
|
248
257
|
}
|
|
249
258
|
/**
|
|
250
259
|
* Force an immediate refresh of the token.
|
|
@@ -36,14 +36,17 @@ export function waitForAuthenticationCredentialsMiddleware(authenticationService
|
|
|
36
36
|
*/
|
|
37
37
|
export function logoutOnUnauthorizedMiddleware(authenticationServiceOrProvider) {
|
|
38
38
|
const getAuthenticationService = cacheValueOrAsyncProvider(authenticationServiceOrProvider);
|
|
39
|
-
async function logoutOnUnauthorizedMiddleware(
|
|
39
|
+
async function logoutOnUnauthorizedMiddleware({ request }, next) {
|
|
40
40
|
try {
|
|
41
41
|
await next();
|
|
42
42
|
}
|
|
43
43
|
catch (error) {
|
|
44
44
|
if ((error instanceof HttpError) && (error.response?.statusCode == 401)) {
|
|
45
|
-
const
|
|
46
|
-
|
|
45
|
+
const endpoint = request.context?.endpoint;
|
|
46
|
+
if (endpoint?.data?.[dontWaitForValidToken] != true) {
|
|
47
|
+
const authenticationService = await getAuthenticationService();
|
|
48
|
+
await authenticationService.logout();
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
throw error;
|
|
49
52
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { of } from 'rxjs';
|
|
2
2
|
import { describe, expect, test, vi } from 'vitest';
|
|
3
3
|
import { HttpClientRequest, HttpClientResponse, HttpError, HttpErrorReason } from '../../http/index.js';
|
|
4
|
+
import { dontWaitForValidToken } from '../authentication.api.js';
|
|
4
5
|
import { logoutOnUnauthorizedMiddleware, waitForAuthenticationCredentialsMiddleware } from '../client/http-client.middleware.js';
|
|
5
6
|
describe('waitForAuthenticationCredentialsMiddleware', () => {
|
|
6
7
|
test('should wait for token and call next', async () => {
|
|
@@ -41,6 +42,33 @@ describe('logoutOnUnauthorizedMiddleware', () => {
|
|
|
41
42
|
await expect(middleware({ request }, next)).rejects.toThrow(HttpError);
|
|
42
43
|
expect(authenticationServiceMock.logout).toHaveBeenCalled();
|
|
43
44
|
});
|
|
45
|
+
test('should NOT call logout on 401 error if endpoint has dontWaitForValidToken', async () => {
|
|
46
|
+
const authenticationServiceMock = {
|
|
47
|
+
logout: vi.fn().mockResolvedValue(undefined),
|
|
48
|
+
};
|
|
49
|
+
const middleware = logoutOnUnauthorizedMiddleware(authenticationServiceMock);
|
|
50
|
+
const request = new HttpClientRequest('http://localhost');
|
|
51
|
+
request.context = {
|
|
52
|
+
endpoint: {
|
|
53
|
+
resource: 'end-session',
|
|
54
|
+
data: {
|
|
55
|
+
[dontWaitForValidToken]: true,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const response = new HttpClientResponse({
|
|
60
|
+
request,
|
|
61
|
+
statusCode: 401,
|
|
62
|
+
statusMessage: 'Unauthorized',
|
|
63
|
+
headers: {},
|
|
64
|
+
body: undefined,
|
|
65
|
+
closeHandler: () => { }
|
|
66
|
+
});
|
|
67
|
+
const error = new HttpError(HttpErrorReason.StatusCode, request, { response });
|
|
68
|
+
const next = vi.fn().mockRejectedValue(error);
|
|
69
|
+
await expect(middleware({ request }, next)).rejects.toThrow(HttpError);
|
|
70
|
+
expect(authenticationServiceMock.logout).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
44
72
|
test('should not call logout on other errors', async () => {
|
|
45
73
|
const authenticationServiceMock = {
|
|
46
74
|
logout: vi.fn().mockResolvedValue(undefined),
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest';
|
|
2
1
|
import { Subject } from 'rxjs';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
3
3
|
import { AuthenticationClientService } from '../../authentication/client/authentication.service.js';
|
|
4
4
|
import { AUTHENTICATION_API_CLIENT } from '../../authentication/client/tokens.js';
|
|
5
|
+
import { CancellationSignal, CancellationToken } from '../../cancellation/token.js';
|
|
6
|
+
import { Injector } from '../../injector/index.js';
|
|
5
7
|
import { Lock } from '../../lock/index.js';
|
|
6
8
|
import { Logger } from '../../logger/index.js';
|
|
7
9
|
import { MessageBus } from '../../message-bus/index.js';
|
|
8
|
-
import { Injector } from '../../injector/index.js';
|
|
9
|
-
import { CancellationSignal, CancellationToken } from '../../cancellation/token.js';
|
|
10
10
|
import { configureDefaultSignalsImplementation } from '../../signals/implementation/configure.js';
|
|
11
11
|
describe('AuthenticationClientService Methods', () => {
|
|
12
12
|
let injector;
|
|
13
13
|
let service;
|
|
14
14
|
let mockApiClient;
|
|
15
15
|
let mockLock;
|
|
16
|
-
let
|
|
16
|
+
let mockTokenUpdateBus;
|
|
17
|
+
let mockLoggedOutBus;
|
|
17
18
|
let mockLogger;
|
|
18
19
|
beforeEach(() => {
|
|
19
20
|
const storage = new Map();
|
|
@@ -46,9 +47,16 @@ describe('AuthenticationClientService Methods', () => {
|
|
|
46
47
|
return await callback({ lost: false });
|
|
47
48
|
}),
|
|
48
49
|
};
|
|
49
|
-
|
|
50
|
+
mockTokenUpdateBus = {
|
|
50
51
|
publishAndForget: vi.fn(),
|
|
51
52
|
messages$: new Subject(),
|
|
53
|
+
allMessages$: new Subject(),
|
|
54
|
+
dispose: vi.fn(),
|
|
55
|
+
};
|
|
56
|
+
mockLoggedOutBus = {
|
|
57
|
+
publishAndForget: vi.fn(),
|
|
58
|
+
messages$: new Subject(),
|
|
59
|
+
allMessages$: new Subject(),
|
|
52
60
|
dispose: vi.fn(),
|
|
53
61
|
};
|
|
54
62
|
mockLogger = {
|
|
@@ -59,7 +67,15 @@ describe('AuthenticationClientService Methods', () => {
|
|
|
59
67
|
};
|
|
60
68
|
injector.register(AUTHENTICATION_API_CLIENT, { useValue: mockApiClient });
|
|
61
69
|
injector.register(Lock, { useValue: mockLock });
|
|
62
|
-
injector.register(MessageBus, {
|
|
70
|
+
injector.register(MessageBus, {
|
|
71
|
+
useFactory: (argument) => {
|
|
72
|
+
if (argument === 'AuthenticationService:tokenUpdate')
|
|
73
|
+
return mockTokenUpdateBus;
|
|
74
|
+
if (argument === 'AuthenticationService:loggedOut')
|
|
75
|
+
return mockLoggedOutBus;
|
|
76
|
+
return undefined;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
63
79
|
injector.register(Logger, { useValue: mockLogger });
|
|
64
80
|
const disposeToken = new CancellationToken();
|
|
65
81
|
injector.register(CancellationSignal, { useValue: disposeToken.signal });
|
|
@@ -122,6 +138,22 @@ describe('AuthenticationClientService Methods', () => {
|
|
|
122
138
|
mockApiClient.unimpersonate.mockRejectedValue(new Error('Unimpersonation failed'));
|
|
123
139
|
await expect(service.unimpersonate()).rejects.toThrow('Unimpersonation failed');
|
|
124
140
|
});
|
|
141
|
+
test('logout should handle concurrent calls and avoid multiple api requests', async () => {
|
|
142
|
+
let resolveEndSession;
|
|
143
|
+
const endSessionPromise = new Promise((resolve) => {
|
|
144
|
+
resolveEndSession = resolve;
|
|
145
|
+
});
|
|
146
|
+
mockApiClient.endSession.mockReturnValue(endSessionPromise);
|
|
147
|
+
const logout1 = service.logout();
|
|
148
|
+
const logout2 = service.logout();
|
|
149
|
+
// logout1 and logout2 will be different promises because the method is async
|
|
150
|
+
expect(mockApiClient.endSession).toHaveBeenCalledTimes(1);
|
|
151
|
+
resolveEndSession(undefined);
|
|
152
|
+
await Promise.all([logout1, logout2]);
|
|
153
|
+
expect(service.isLoggedIn()).toBe(false);
|
|
154
|
+
expect(mockTokenUpdateBus.publishAndForget).toHaveBeenCalledWith(undefined);
|
|
155
|
+
expect(mockLoggedOutBus.publishAndForget).toHaveBeenCalled();
|
|
156
|
+
});
|
|
125
157
|
test('syncClock should handle errors gracefully', async () => {
|
|
126
158
|
mockApiClient.timestamp.mockRejectedValue(new Error('Time sync failed'));
|
|
127
159
|
await service.syncClock();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tstdl/base",
|
|
3
|
-
"version": "0.93.
|
|
3
|
+
"version": "0.93.144",
|
|
4
4
|
"author": "Patrick Hein",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -152,8 +152,8 @@
|
|
|
152
152
|
"type-fest": "^5.4"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
|
-
"@aws-sdk/client-s3": "^3.
|
|
156
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
155
|
+
"@aws-sdk/client-s3": "^3.999",
|
|
156
|
+
"@aws-sdk/s3-request-presigner": "^3.999",
|
|
157
157
|
"@genkit-ai/google-genai": "^1.29",
|
|
158
158
|
"@google-cloud/storage": "^7.19",
|
|
159
159
|
"@toon-format/toon": "^2.1.0",
|
|
@@ -73,10 +73,10 @@ import { Timer } from '../../utils/timer.js';
|
|
|
73
73
|
import { cancelableTimeout } from '../../utils/timing.js';
|
|
74
74
|
import { isArray, isDefined, isNotNull, isNull, isNumber, isString, isUndefined } from '../../utils/type-guards.js';
|
|
75
75
|
import { millisecondsPerMinute, millisecondsPerSecond } from '../../utils/units.js';
|
|
76
|
-
import { defaultQueueConfig, TaskDependencyType, TaskQueue, TaskStatus } from '../task-queue.js';
|
|
76
|
+
import { defaultQueueConfig, queueableOrWaitableStatuses, queueableStatuses, TaskDependencyType, TaskQueue, TaskStatus, terminalStatuses } from '../task-queue.js';
|
|
77
77
|
import { PostgresTaskQueueModuleConfig } from './module.js';
|
|
78
78
|
import { taskArchive as taskArchiveTable, taskDependency as taskDependencyTable, taskDependencyType, taskStatus, task as taskTable } from './schemas.js';
|
|
79
|
-
import {
|
|
79
|
+
import { PostgresTask, PostgresTaskArchive } from './task.model.js';
|
|
80
80
|
let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
81
81
|
#database = inject(Database);
|
|
82
82
|
#repository = injectRepository(PostgresTask);
|
|
@@ -189,7 +189,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
189
189
|
}));
|
|
190
190
|
const itemsWithIdempotency = entitiesWithIndex.filter((e) => isNotNull(e.entity.idempotencyKey));
|
|
191
191
|
const itemsWithoutIdempotency = entitiesWithIndex.filter((e) => isNull(e.entity.idempotencyKey));
|
|
192
|
-
const hasDependencies = itemsWithDistinctDependencies.some((item) => ((item.scheduleAfter?.length ?? 0) > 0) || ((item.completeAfter?.length ?? 0) > 0) || (isDefined(item.parentId) && (item.
|
|
192
|
+
const hasDependencies = itemsWithDistinctDependencies.some((item) => ((item.scheduleAfter?.length ?? 0) > 0) || ((item.completeAfter?.length ?? 0) > 0) || (isDefined(item.parentId) && (item.parentRequires != false) && !(isArray(item.parentRequires) && (item.parentRequires.length == 0))));
|
|
193
193
|
const mustUseTransaction = (entitiesWithIndex.length > 1) || hasDependencies;
|
|
194
194
|
const newTransaction = __addDisposableResource(env_1, (mustUseTransaction && isUndefined(options?.transaction)) ? await this.#repository.startTransaction() : undefined, true);
|
|
195
195
|
const transaction = newTransaction ?? options?.transaction;
|
|
@@ -258,13 +258,13 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
258
258
|
if (!processedTaskIds.has(task.id)) {
|
|
259
259
|
continue;
|
|
260
260
|
}
|
|
261
|
-
if (isDefined(item.parentId) && (item.
|
|
261
|
+
if (isDefined(item.parentId) && (item.parentRequires != false) && !(isArray(item.parentRequires) && (item.parentRequires.length == 0))) {
|
|
262
262
|
dependencies.push({
|
|
263
263
|
taskId: item.parentId,
|
|
264
264
|
dependencyTaskId: task.id,
|
|
265
265
|
type: TaskDependencyType.Child,
|
|
266
|
-
requiredStatuses: isArray(item.
|
|
267
|
-
? item.
|
|
266
|
+
requiredStatuses: isArray(item.parentRequires)
|
|
267
|
+
? item.parentRequires
|
|
268
268
|
: [TaskStatus.Completed],
|
|
269
269
|
});
|
|
270
270
|
}
|
|
@@ -506,7 +506,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
506
506
|
const timeout = options?.timeout ?? Infinity;
|
|
507
507
|
const interval = options?.interval ?? 1000;
|
|
508
508
|
const cancellationSignal = this.#cancellationSignal.optionallyInherit(options?.cancellationSignal);
|
|
509
|
-
const waitStatuses = options?.statuses ??
|
|
509
|
+
const waitStatuses = options?.statuses ?? terminalStatuses;
|
|
510
510
|
const messageBus$ = this.#messageBus.allMessages$.pipe(filter((namespace) => namespace == this.#namespace), throttleTime(500, undefined, { leading: true, trailing: true }));
|
|
511
511
|
const continue$ = merge(messageBus$, cancellationSignal);
|
|
512
512
|
const timer = Timer.startNew();
|
|
@@ -566,7 +566,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
566
566
|
const nonFinalizedTasks = await tx.pgTransaction
|
|
567
567
|
.select({ id: taskTable.id, namespace: taskTable.namespace })
|
|
568
568
|
.from(taskTable)
|
|
569
|
-
.where(and(eq(taskTable.namespace, this.#namespace), notInArray(taskTable.status,
|
|
569
|
+
.where(and(eq(taskTable.namespace, this.#namespace), notInArray(taskTable.status, terminalStatuses)))
|
|
570
570
|
.for('update');
|
|
571
571
|
if (nonFinalizedTasks.length > 0) {
|
|
572
572
|
const ids = nonFinalizedTasks.map((t) => t.id);
|
|
@@ -1065,7 +1065,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
1065
1065
|
status: taskTable.status,
|
|
1066
1066
|
});
|
|
1067
1067
|
for (const row of updatedRows) {
|
|
1068
|
-
if (
|
|
1068
|
+
if (terminalStatuses.includes(row.status)) {
|
|
1069
1069
|
terminalTasks.push({ id: row.id, status: row.status, namespace: row.namespace });
|
|
1070
1070
|
}
|
|
1071
1071
|
notifiedNamespaces.add(row.namespace);
|
|
@@ -1101,7 +1101,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
1101
1101
|
const rowsToArchive = await tx.pgTransaction
|
|
1102
1102
|
.select()
|
|
1103
1103
|
.from(taskTable)
|
|
1104
|
-
.where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.status,
|
|
1104
|
+
.where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.status, terminalStatuses), lte(taskTable.completeTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.retention, 'milliseconds')}`), notExists(tx.pgTransaction
|
|
1105
1105
|
.select({ id: childTaskTable.id })
|
|
1106
1106
|
.from(childTaskTable)
|
|
1107
1107
|
.where(eq(childTaskTable.parentId, taskTable.id))), notExists(tx.pgTransaction
|
|
@@ -1299,7 +1299,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
1299
1299
|
priorityAgeTimestamp: TRANSACTION_TIMESTAMP,
|
|
1300
1300
|
state: (options?.resetState == true) ? null : undefined,
|
|
1301
1301
|
})
|
|
1302
|
-
.where(and(eq(taskTable.id, id), or(inArray(taskTable.status, queueableStatuses), inArray(taskTable.status,
|
|
1302
|
+
.where(and(eq(taskTable.id, id), or(inArray(taskTable.status, queueableStatuses), inArray(taskTable.status, terminalStatuses), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP))));
|
|
1303
1303
|
}
|
|
1304
1304
|
notify(namespace = this.#namespace) {
|
|
1305
1305
|
this.#messageBus.publishAndForget(namespace);
|
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
import { BaseEntity, type Json, type Timestamp } from '../../orm/index.js';
|
|
3
3
|
import type { ObjectLiteral } from '../../types/types.js';
|
|
4
4
|
import { type Task, TaskDependencyType, TaskStatus } from '../task-queue.js';
|
|
5
|
-
export declare const terminalStatuses: TaskStatus[];
|
|
6
|
-
export declare const finalizedStatuses: TaskStatus[];
|
|
7
|
-
export declare const nonFinalizedStatuses: TaskStatus[];
|
|
8
|
-
export declare const queueableStatuses: TaskStatus[];
|
|
9
|
-
export declare const waitableStatuses: TaskStatus[];
|
|
10
|
-
export declare const queueableOrWaitableStatuses: TaskStatus[];
|
|
11
5
|
export declare abstract class PostgresTaskBase<Data extends ObjectLiteral = ObjectLiteral, State extends ObjectLiteral = ObjectLiteral, Result extends ObjectLiteral = ObjectLiteral> extends BaseEntity implements Task {
|
|
12
6
|
namespace: string;
|
|
13
7
|
type: string;
|
|
@@ -11,13 +11,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
11
11
|
import { BaseEntity, ForeignKey, Index, JsonProperty, Table, TimestampProperty, Unique, UuidProperty } from '../../orm/index.js';
|
|
12
12
|
import { Array as ArrayProperty, BooleanProperty, Enumeration, Integer, NumberProperty, StringProperty } from '../../schema/index.js';
|
|
13
13
|
import { isNotNull } from 'drizzle-orm';
|
|
14
|
-
import { TaskDependencyType, TaskStatus } from '../task-queue.js';
|
|
15
|
-
export const terminalStatuses = [TaskStatus.Completed, TaskStatus.Cancelled, TaskStatus.Dead, TaskStatus.TimedOut, TaskStatus.Expired, TaskStatus.Skipped, TaskStatus.Orphaned];
|
|
16
|
-
export const finalizedStatuses = terminalStatuses;
|
|
17
|
-
export const nonFinalizedStatuses = [TaskStatus.Pending, TaskStatus.Retrying, TaskStatus.Waiting, TaskStatus.WaitingChildren, TaskStatus.Running];
|
|
18
|
-
export const queueableStatuses = [TaskStatus.Pending, TaskStatus.Retrying];
|
|
19
|
-
export const waitableStatuses = [TaskStatus.Waiting, TaskStatus.WaitingChildren];
|
|
20
|
-
export const queueableOrWaitableStatuses = [...queueableStatuses, ...waitableStatuses];
|
|
14
|
+
import { queueableOrWaitableStatuses, queueableStatuses, TaskDependencyType, TaskStatus, terminalStatuses } from '../task-queue.js';
|
|
21
15
|
export class PostgresTaskBase extends BaseEntity {
|
|
22
16
|
namespace;
|
|
23
17
|
type;
|
|
@@ -95,10 +95,20 @@ export declare const TaskStatus: {
|
|
|
95
95
|
*/
|
|
96
96
|
readonly Orphaned: "orphaned";
|
|
97
97
|
};
|
|
98
|
+
export declare const terminalStatuses: ["completed", "cancelled", "dead", "timed-out", "expired", "skipped", "orphaned"];
|
|
99
|
+
export declare const nonTerminalStatuses: ["pending", "retrying", "waiting", "waiting-children", "running"];
|
|
100
|
+
export declare const queueableStatuses: ["pending", "retrying"];
|
|
101
|
+
export declare const waitableStatuses: ["waiting", "waiting-children"];
|
|
102
|
+
export declare const queueableOrWaitableStatuses: ["pending", "retrying", "waiting", "waiting-children"];
|
|
98
103
|
/**
|
|
99
104
|
* Type of task status.
|
|
100
105
|
*/
|
|
101
106
|
export type TaskStatus = EnumType<typeof TaskStatus>;
|
|
107
|
+
export type TerminalTaskStatus = typeof terminalStatuses[number];
|
|
108
|
+
export type NonTerminalTaskStatus = typeof nonTerminalStatuses[number];
|
|
109
|
+
export type QueueableTaskStatus = typeof queueableStatuses[number];
|
|
110
|
+
export type WaitableTaskStatus = typeof waitableStatuses[number];
|
|
111
|
+
export type QueueableOrWaitableTaskStatus = typeof queueableOrWaitableStatuses[number];
|
|
102
112
|
/**
|
|
103
113
|
* Represents the type of dependency between tasks.
|
|
104
114
|
*/
|
|
@@ -158,7 +168,7 @@ export type Task<Definitions extends TaskDefinitionMap = Record<string, {
|
|
|
158
168
|
unresolvedScheduleDependencies: number;
|
|
159
169
|
/** The number of unresolved completion dependencies. */
|
|
160
170
|
unresolvedCompleteDependencies: number;
|
|
161
|
-
/** Whether to skip the task if any of its dependencies fail (dependency finalized with a status not in `
|
|
171
|
+
/** Whether to skip the task if any of its dependencies fail (dependency finalized with a status not in `parentRequires`). */
|
|
162
172
|
abortOnDependencyFailure: boolean;
|
|
163
173
|
/** The data associated with the task. */
|
|
164
174
|
data: TaskData<Definitions, Type>;
|
|
@@ -242,7 +252,7 @@ export type EnqueueOptions = {
|
|
|
242
252
|
/** Whether to skip the task if any of its dependencies fail. */
|
|
243
253
|
abortOnDependencyFailure?: boolean;
|
|
244
254
|
/** The statuses the parent task should wait for this task to reach. */
|
|
245
|
-
|
|
255
|
+
parentRequires?: boolean | TerminalTaskStatus[];
|
|
246
256
|
/** The timestamp when the task should be processed. */
|
|
247
257
|
scheduleTimestamp?: number;
|
|
248
258
|
/** The duration (ms) before the task is considered expired. */
|
package/task-queue/task-queue.js
CHANGED
|
@@ -157,6 +157,11 @@ export const TaskStatus = defineEnum('TaskStatus', {
|
|
|
157
157
|
*/
|
|
158
158
|
Orphaned: 'orphaned',
|
|
159
159
|
});
|
|
160
|
+
export const terminalStatuses = [TaskStatus.Completed, TaskStatus.Cancelled, TaskStatus.Dead, TaskStatus.TimedOut, TaskStatus.Expired, TaskStatus.Skipped, TaskStatus.Orphaned];
|
|
161
|
+
export const nonTerminalStatuses = [TaskStatus.Pending, TaskStatus.Retrying, TaskStatus.Waiting, TaskStatus.WaitingChildren, TaskStatus.Running];
|
|
162
|
+
export const queueableStatuses = [TaskStatus.Pending, TaskStatus.Retrying];
|
|
163
|
+
export const waitableStatuses = [TaskStatus.Waiting, TaskStatus.WaitingChildren];
|
|
164
|
+
export const queueableOrWaitableStatuses = [...queueableStatuses, ...waitableStatuses];
|
|
160
165
|
/**
|
|
161
166
|
* Represents the type of dependency between tasks.
|
|
162
167
|
*/
|
|
@@ -305,9 +305,9 @@ describe('Task Queue Branch Coverage Enhancement', () => {
|
|
|
305
305
|
const updated = await pruneQueue.getTask(task.id);
|
|
306
306
|
expect(updated?.status).toBe(TaskStatus.TimedOut);
|
|
307
307
|
});
|
|
308
|
-
it('should handle enqueueMany with parentId and
|
|
308
|
+
it('should handle enqueueMany with parentId and parentRequires false', async () => {
|
|
309
309
|
const parent = await queue.enqueue('p', {});
|
|
310
|
-
const tasks = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id,
|
|
310
|
+
const tasks = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, parentRequires: false }], { returnTasks: true });
|
|
311
311
|
expect(tasks).toHaveLength(1);
|
|
312
312
|
expect(tasks[0]?.parentId).toBe(parent.id);
|
|
313
313
|
const updatedParent = await queue.getTask(parent.id);
|
|
@@ -45,11 +45,11 @@ describe('Fan-Out Spawning', () => {
|
|
|
45
45
|
const fParent = await queue.getTask(parent.id);
|
|
46
46
|
expect(fParent?.status).toBe(TaskStatus.Completed);
|
|
47
47
|
});
|
|
48
|
-
it('should NOT transition parent to Waiting if
|
|
48
|
+
it('should NOT transition parent to Waiting if parentRequires is false', async () => {
|
|
49
49
|
const parent = await queue.enqueue('parent', {});
|
|
50
50
|
const dParent = await queue.dequeue();
|
|
51
|
-
// Spawn child with
|
|
52
|
-
await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id,
|
|
51
|
+
// Spawn child with parentRequires: false
|
|
52
|
+
await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, parentRequires: false }], { returnTasks: true });
|
|
53
53
|
await queue.complete(dParent);
|
|
54
54
|
const uParent = await queue.getTask(parent.id);
|
|
55
55
|
expect(uParent?.status).toBe(TaskStatus.Completed); // Finished immediately
|
|
@@ -195,7 +195,7 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
195
195
|
const queueB = queueProvider.get(nameB);
|
|
196
196
|
const parent = await queueA.enqueue('test', { value: 'parent' });
|
|
197
197
|
expect(parent.unresolvedCompleteDependencies).toBe(0);
|
|
198
|
-
await queueB.enqueue('test', { value: 'child' }, { parentId: parent.id,
|
|
198
|
+
await queueB.enqueue('test', { value: 'child' }, { parentId: parent.id, parentRequires: true });
|
|
199
199
|
const updatedParent = await queueA.getTask(parent.id);
|
|
200
200
|
expect(updatedParent?.unresolvedCompleteDependencies).toBe(1);
|
|
201
201
|
await database.update(taskTable).set({ parentId: null }).where(or(eq(taskTable.namespace, nameA), eq(taskTable.namespace, nameB)));
|
|
@@ -254,7 +254,7 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
254
254
|
await q.enqueue('test', {}, {
|
|
255
255
|
idempotencyKey,
|
|
256
256
|
parentId: parent.id,
|
|
257
|
-
|
|
257
|
+
parentRequires: true,
|
|
258
258
|
});
|
|
259
259
|
// Dequeue and complete parent
|
|
260
260
|
const dequeuedParent = await q.dequeue();
|
|
@@ -263,10 +263,10 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
263
263
|
expect(updatedParent?.status).toBe(TaskStatus.Completed);
|
|
264
264
|
await q.clear();
|
|
265
265
|
});
|
|
266
|
-
it('should increment unresolvedCompleteDependencies for children with
|
|
266
|
+
it('should increment unresolvedCompleteDependencies for children with parentRequires: true (Bug 6-2)', async () => {
|
|
267
267
|
const parent = await queue.enqueue('parent', {});
|
|
268
268
|
expect(parent.unresolvedCompleteDependencies).toBe(0);
|
|
269
|
-
await queue.enqueue('child', {}, { parentId: parent.id,
|
|
269
|
+
await queue.enqueue('child', {}, { parentId: parent.id, parentRequires: true });
|
|
270
270
|
const updatedParent = await queue.getTask(parent.id);
|
|
271
271
|
expect(updatedParent?.unresolvedCompleteDependencies).toBe(1);
|
|
272
272
|
});
|