@getstrata/core 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/dist/core/cache/createCacheStore.d.ts +3 -1
- package/dist/core/cache/redisCacheStore.d.ts +21 -8
- package/dist/core/cache/repository.d.ts +3 -2
- package/dist/core/cache/store.d.ts +4 -1
- package/dist/core/cache/taggedCache.d.ts +1 -1
- package/dist/core/contracts/di.d.ts +4 -2
- package/dist/core/contracts/serviceTokens.d.ts +6 -1
- package/dist/core/database/baseRepository.d.ts +0 -1
- package/dist/core/database/index.d.ts +1 -1
- package/dist/core/database/model.d.ts +51 -22
- package/dist/core/database/repositoryQuery.d.ts +16 -1
- package/dist/core/database/types.d.ts +25 -2
- package/dist/core/database/whereBuilder.d.ts +5 -5
- package/dist/core/events/deferredModelEvents.d.ts +2 -1
- package/dist/core/http/loginThrottleMiddleware.d.ts +6 -5
- package/dist/core/http/memoryThrottleMiddleware.d.ts +11 -3
- package/dist/core/http/scimThrottleMiddleware.d.ts +12 -3
- package/dist/core/http/throttleMiddleware.d.ts +24 -6
- package/dist/core/lifecycle/gracefulShutdown.d.ts +33 -5
- package/dist/core/queue/index.d.ts +7 -0
- package/dist/core/queue/redisQueue.d.ts +1 -0
- package/dist/core/queue/resilientQueue.d.ts +2 -2
- package/dist/core/runtime/applicationRegistry.d.ts +2 -1
- package/dist/core/runtime/boundedThrottleStore.d.ts +29 -0
- package/dist/core/scheduler/leases.d.ts +40 -0
- package/dist/core/scheduler/schedule.d.ts +29 -5
- package/dist/core/tenant/tenantContext.d.ts +5 -4
- package/dist/core/tenant/tenantMiddleware.d.ts +7 -1
- package/dist/core/tracing/otel.d.ts +28 -26
- package/dist/core/tracing/tracingMiddleware.d.ts +4 -1
- package/dist/entries/audit/exportAuditLogs.js +14 -2
- package/dist/entries/auth/basicAuthGuard.js +80 -0
- package/dist/entries/auth/jwtGuard.js +80 -0
- package/dist/entries/auth/scimAuthMiddleware.js +17 -7
- package/dist/entries/auth/sessionGuard.js +80 -0
- package/dist/entries/cache/createCacheStore.js +365 -119
- package/dist/entries/cache/repository.js +5 -0
- package/dist/entries/contracts/serviceTokens.js +83 -0
- package/dist/entries/database/factory.js +0 -3
- package/dist/entries/database/model.js +1 -2486
- package/dist/entries/database/repositoryQuery.js +67 -9
- package/dist/entries/http/bodySizeLimitMiddleware.js +76 -10
- package/dist/entries/http/memoryThrottleMiddleware.js +276 -21
- package/dist/entries/http/scimThrottleMiddleware.js +576 -54
- package/dist/entries/http/throttleMiddleware.js +65 -16
- package/dist/entries/jobs/exportAuditLogsJob.js +44 -7
- package/dist/entries/jobs/invalidateCacheTagsJob.js +30 -5
- package/dist/entries/queue/createAppQueue.js +67 -6
- package/dist/entries/queue/publicQueue.js +67 -6
- package/dist/entries/queue/queueMetrics.js +67 -6
- package/dist/entries/queue/redisQueue.js +3 -0
- package/dist/entries/queue.js +30 -5
- package/dist/entries/scheduler/schedule.js +1 -72
- package/dist/entries/tenant/databaseTenantContext.js +14 -2
- package/dist/entries/tenant/tenantDatabaseScope.js +14 -2
- package/dist/entries/tracing/tracingMiddleware.js +186 -454
- package/dist/framework/public-api.d.ts +13 -9
- package/dist/index.js +1745 -633
- package/dist/types/services.d.ts +3 -3
- package/package.json +17 -8
|
@@ -29,7 +29,7 @@ var init_asyncContextStore = () => {};
|
|
|
29
29
|
// ../../src/core/http/throttleMiddleware.ts
|
|
30
30
|
import { createHash } from "crypto";
|
|
31
31
|
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
32
|
-
import { currentTenant
|
|
32
|
+
import { currentTenant } from "@getstrata/core/tenant/tenantContext";
|
|
33
33
|
var {RedisClient } = globalThis.Bun;
|
|
34
34
|
|
|
35
35
|
// ../../src/core/runtime/appEnv.ts
|
|
@@ -397,28 +397,32 @@ if ttl < 0 then
|
|
|
397
397
|
end
|
|
398
398
|
return count
|
|
399
399
|
`;
|
|
400
|
-
function redisThrottleKey(request, prefix, identity) {
|
|
400
|
+
function redisThrottleKey(request, prefix, identity, tenantId = currentTenant()?.id ?? null) {
|
|
401
401
|
const bucket = JSON.stringify([
|
|
402
|
-
|
|
402
|
+
tenantId,
|
|
403
403
|
request.method,
|
|
404
404
|
currentRequestMeta2().routeTemplate ?? "__unmatched__",
|
|
405
405
|
identity
|
|
406
406
|
]);
|
|
407
407
|
return namespacedRedisKey(`${prefix}:${createHash("sha256").update(bucket).digest("hex")}`);
|
|
408
408
|
}
|
|
409
|
-
async function consumeRedisThrottle(client, key, decaySeconds, timeoutMs = 1000) {
|
|
410
|
-
|
|
409
|
+
async function consumeRedisThrottle(client, key, decaySeconds, timeoutMs = 1000, signal) {
|
|
410
|
+
const expiryMs = Math.max(1, Math.ceil(decaySeconds * 1000));
|
|
411
|
+
if (!Number.isFinite(decaySeconds) || decaySeconds < 0 || !Number.isSafeInteger(expiryMs) || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
|
|
411
412
|
throw new Error("Invalid throttle expiry or command timeout.");
|
|
412
413
|
}
|
|
414
|
+
if (signal?.aborted)
|
|
415
|
+
throw new Error("Throttle consumer is disposed.");
|
|
416
|
+
let onAbort;
|
|
413
417
|
let timer;
|
|
414
418
|
try {
|
|
419
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
420
|
+
onAbort = () => reject(new Error("Throttle consumer is disposed."));
|
|
421
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
422
|
+
});
|
|
415
423
|
const result = await Promise.race([
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
"1",
|
|
419
|
-
key,
|
|
420
|
-
String(Math.max(1, Math.ceil(decaySeconds * 1000)))
|
|
421
|
-
]),
|
|
424
|
+
cancelled,
|
|
425
|
+
client.send("EVAL", [CONSUME_THROTTLE, "1", key, String(expiryMs)]),
|
|
422
426
|
new Promise((_resolve, reject) => {
|
|
423
427
|
timer = setTimeout(() => reject(new Error("Throttle store deadline exceeded.")), timeoutMs);
|
|
424
428
|
})
|
|
@@ -429,11 +433,16 @@ async function consumeRedisThrottle(client, key, decaySeconds, timeoutMs = 1000)
|
|
|
429
433
|
return count;
|
|
430
434
|
} finally {
|
|
431
435
|
clearTimeout(timer);
|
|
436
|
+
if (onAbort)
|
|
437
|
+
signal?.removeEventListener("abort", onAbort);
|
|
432
438
|
}
|
|
433
439
|
}
|
|
434
440
|
function createRedisThrottleConsumer(options) {
|
|
435
441
|
let ownedClient;
|
|
436
|
-
|
|
442
|
+
const controller = new AbortController;
|
|
443
|
+
const consume = async (key, decaySeconds) => {
|
|
444
|
+
if (controller.signal.aborted)
|
|
445
|
+
throw new Error("Throttle consumer is disposed.");
|
|
437
446
|
if (!options.redisClient && !ownedClient) {
|
|
438
447
|
ownedClient = new RedisClient(options.redisUrl, { autoReconnect: false, maxRetries: 0 });
|
|
439
448
|
}
|
|
@@ -441,7 +450,10 @@ function createRedisThrottleConsumer(options) {
|
|
|
441
450
|
if (!client)
|
|
442
451
|
throw new Error("Throttle client is required.");
|
|
443
452
|
try {
|
|
444
|
-
|
|
453
|
+
const attempts = await consumeRedisThrottle(client, key, decaySeconds, options.commandTimeoutMs, controller.signal);
|
|
454
|
+
if (controller.signal.aborted)
|
|
455
|
+
throw new Error("Throttle consumer is disposed.");
|
|
456
|
+
return attempts;
|
|
445
457
|
} catch (error) {
|
|
446
458
|
if (client === ownedClient) {
|
|
447
459
|
ownedClient.close();
|
|
@@ -450,6 +462,17 @@ function createRedisThrottleConsumer(options) {
|
|
|
450
462
|
throw error;
|
|
451
463
|
}
|
|
452
464
|
};
|
|
465
|
+
return Object.assign(consume, {
|
|
466
|
+
isDisposed: () => controller.signal.aborted,
|
|
467
|
+
dispose() {
|
|
468
|
+
if (controller.signal.aborted)
|
|
469
|
+
return;
|
|
470
|
+
controller.abort();
|
|
471
|
+
const client = ownedClient;
|
|
472
|
+
ownedClient = undefined;
|
|
473
|
+
client?.close();
|
|
474
|
+
}
|
|
475
|
+
});
|
|
453
476
|
}
|
|
454
477
|
function throttleUnavailableResponse() {
|
|
455
478
|
return Response.json({ error: "Rate limiting is temporarily unavailable." }, {
|
|
@@ -457,23 +480,48 @@ function throttleUnavailableResponse() {
|
|
|
457
480
|
headers: { "retry-after": "1" }
|
|
458
481
|
});
|
|
459
482
|
}
|
|
483
|
+
function resolveThrottleQuota(request, options, identity = resolveThrottleIdentity(request)) {
|
|
484
|
+
const limit = options.quotaPolicy ? options.quotaPolicy({
|
|
485
|
+
request,
|
|
486
|
+
tenant: currentTenant(),
|
|
487
|
+
identity,
|
|
488
|
+
routeTemplate: currentRequestMeta2().routeTemplate ?? "__unmatched__",
|
|
489
|
+
maxAttempts: options.maxAttempts
|
|
490
|
+
}) : options.maxAttempts;
|
|
491
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
492
|
+
throw new Error("Invalid application quota.");
|
|
493
|
+
}
|
|
494
|
+
return limit;
|
|
495
|
+
}
|
|
460
496
|
function createThrottleMiddleware(options) {
|
|
497
|
+
if (!Number.isSafeInteger(options.maxAttempts) || options.maxAttempts < 0) {
|
|
498
|
+
throw new Error("Throttle maxAttempts must be a non-negative safe integer.");
|
|
499
|
+
}
|
|
461
500
|
const consume = createRedisThrottleConsumer(options);
|
|
462
501
|
const prefix = options.keyPrefix ?? "throttle";
|
|
463
|
-
|
|
464
|
-
const
|
|
502
|
+
const middleware = async (request, next) => {
|
|
503
|
+
const identity = resolveThrottleIdentity(request);
|
|
504
|
+
let maxAttempts;
|
|
505
|
+
try {
|
|
506
|
+
maxAttempts = resolveThrottleQuota(request, options, identity);
|
|
507
|
+
} catch {
|
|
508
|
+
return throttleUnavailableResponse();
|
|
509
|
+
}
|
|
510
|
+
const key = redisThrottleKey(request, prefix, identity);
|
|
465
511
|
let attempts;
|
|
466
512
|
try {
|
|
467
513
|
attempts = await consume(key, options.decaySeconds);
|
|
468
514
|
} catch {
|
|
469
515
|
return throttleUnavailableResponse();
|
|
470
516
|
}
|
|
471
|
-
|
|
517
|
+
if (consume.isDisposed())
|
|
518
|
+
return throttleUnavailableResponse();
|
|
472
519
|
if (attempts > maxAttempts) {
|
|
473
520
|
return await tooManyRequestsResponse(request, "Too many requests.", options.decaySeconds);
|
|
474
521
|
}
|
|
475
522
|
return await next();
|
|
476
523
|
};
|
|
524
|
+
return Object.assign(middleware, { dispose: () => consume.dispose() });
|
|
477
525
|
}
|
|
478
526
|
export {
|
|
479
527
|
consumeRedisThrottle,
|
|
@@ -481,5 +529,6 @@ export {
|
|
|
481
529
|
createThrottleMiddleware,
|
|
482
530
|
redisThrottleKey,
|
|
483
531
|
resolveThrottleIdentity,
|
|
532
|
+
resolveThrottleQuota,
|
|
484
533
|
throttleUnavailableResponse
|
|
485
534
|
};
|
|
@@ -306,9 +306,12 @@ async function runWithDeferredModelEvents(callback) {
|
|
|
306
306
|
const existing = deferredModelEvents.getStore();
|
|
307
307
|
if (existing) {
|
|
308
308
|
existing.frames.push([]);
|
|
309
|
+
existing.rollbackFrames.push([]);
|
|
309
310
|
try {
|
|
310
311
|
const result = await callback();
|
|
311
312
|
const nested = existing.frames.pop() ?? [];
|
|
313
|
+
const rollback = existing.rollbackFrames.pop() ?? [];
|
|
314
|
+
existing.rollbackFrames.at(-1)?.push(...rollback);
|
|
312
315
|
const parent = existing.frames[existing.frames.length - 1];
|
|
313
316
|
if (parent) {
|
|
314
317
|
parent.push(...nested);
|
|
@@ -316,11 +319,20 @@ async function runWithDeferredModelEvents(callback) {
|
|
|
316
319
|
return result;
|
|
317
320
|
} catch (error) {
|
|
318
321
|
existing.frames.pop();
|
|
322
|
+
for (const rollback of (existing.rollbackFrames.pop() ?? []).reverse())
|
|
323
|
+
rollback();
|
|
319
324
|
throw error;
|
|
320
325
|
}
|
|
321
326
|
}
|
|
322
|
-
const state = { frames: [[]] };
|
|
323
|
-
|
|
327
|
+
const state = { frames: [[]], rollbackFrames: [[]] };
|
|
328
|
+
let result;
|
|
329
|
+
try {
|
|
330
|
+
result = await deferredModelEvents.run(state, callback);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
for (const rollback of (state.rollbackFrames[0] ?? []).reverse())
|
|
333
|
+
rollback();
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
324
336
|
await dispatchQueuedModelEvents(state.frames[0] ?? []);
|
|
325
337
|
return result;
|
|
326
338
|
}
|
|
@@ -1450,12 +1462,37 @@ class SyncQueue {
|
|
|
1450
1462
|
}
|
|
1451
1463
|
|
|
1452
1464
|
class AsyncQueue {
|
|
1465
|
+
active = new Set;
|
|
1466
|
+
closing;
|
|
1467
|
+
closed = false;
|
|
1468
|
+
assertOpen() {
|
|
1469
|
+
if (this.closed)
|
|
1470
|
+
throw new Error("Async queue is closed.");
|
|
1471
|
+
}
|
|
1472
|
+
enqueue(task, onError) {
|
|
1473
|
+
this.assertOpen();
|
|
1474
|
+
const running = new Promise((resolve) => {
|
|
1475
|
+
setTimeout(() => {
|
|
1476
|
+
Promise.resolve().then(task).catch(onError).finally(resolve);
|
|
1477
|
+
}, 0);
|
|
1478
|
+
});
|
|
1479
|
+
this.active.add(running);
|
|
1480
|
+
running.then(() => {
|
|
1481
|
+
this.active.delete(running);
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1453
1484
|
async dispatch(job, payload) {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1485
|
+
this.enqueue(() => job.handle(payload), (error) => {
|
|
1486
|
+
console.error("[AsyncQueue] Job failed:", error);
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
close() {
|
|
1490
|
+
this.closing ??= (async () => {
|
|
1491
|
+
while (this.active.size)
|
|
1492
|
+
await Promise.all([...this.active]);
|
|
1493
|
+
this.closed = true;
|
|
1494
|
+
})();
|
|
1495
|
+
return this.closing;
|
|
1459
1496
|
}
|
|
1460
1497
|
}
|
|
1461
1498
|
function createQueue(driver) {
|
|
@@ -57,12 +57,37 @@ class SyncQueue {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
class AsyncQueue {
|
|
60
|
+
active = new Set;
|
|
61
|
+
closing;
|
|
62
|
+
closed = false;
|
|
63
|
+
assertOpen() {
|
|
64
|
+
if (this.closed)
|
|
65
|
+
throw new Error("Async queue is closed.");
|
|
66
|
+
}
|
|
67
|
+
enqueue(task, onError) {
|
|
68
|
+
this.assertOpen();
|
|
69
|
+
const running = new Promise((resolve) => {
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
Promise.resolve().then(task).catch(onError).finally(resolve);
|
|
72
|
+
}, 0);
|
|
73
|
+
});
|
|
74
|
+
this.active.add(running);
|
|
75
|
+
running.then(() => {
|
|
76
|
+
this.active.delete(running);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
60
79
|
async dispatch(job, payload) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
this.enqueue(() => job.handle(payload), (error) => {
|
|
81
|
+
console.error("[AsyncQueue] Job failed:", error);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
close() {
|
|
85
|
+
this.closing ??= (async () => {
|
|
86
|
+
while (this.active.size)
|
|
87
|
+
await Promise.all([...this.active]);
|
|
88
|
+
this.closed = true;
|
|
89
|
+
})();
|
|
90
|
+
return this.closing;
|
|
66
91
|
}
|
|
67
92
|
}
|
|
68
93
|
function createQueue(driver) {
|
|
@@ -451,6 +451,9 @@ class RedisQueue {
|
|
|
451
451
|
constructor(redisUrl) {
|
|
452
452
|
this.client = new RedisClient(redisUrl);
|
|
453
453
|
}
|
|
454
|
+
close() {
|
|
455
|
+
this.client.close();
|
|
456
|
+
}
|
|
454
457
|
async dispatch(job, payload) {
|
|
455
458
|
const name = jobRegistry.resolveName(job);
|
|
456
459
|
if (!name) {
|
|
@@ -579,15 +582,75 @@ async function countPendingQueueJobs(redisUrl) {
|
|
|
579
582
|
}
|
|
580
583
|
}
|
|
581
584
|
|
|
585
|
+
// ../../src/core/queue/index.ts
|
|
586
|
+
class Job {
|
|
587
|
+
static jobName;
|
|
588
|
+
maxAttempts;
|
|
589
|
+
backoffMs;
|
|
590
|
+
priority;
|
|
591
|
+
constructor() {
|
|
592
|
+
const jobName = this.constructor.jobName;
|
|
593
|
+
if (jobName) {
|
|
594
|
+
jobRegistry.track(jobName, this);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
class SyncQueue {
|
|
600
|
+
async dispatch(job, payload) {
|
|
601
|
+
await job.handle(payload);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
class AsyncQueue {
|
|
606
|
+
active = new Set;
|
|
607
|
+
closing;
|
|
608
|
+
closed = false;
|
|
609
|
+
assertOpen() {
|
|
610
|
+
if (this.closed)
|
|
611
|
+
throw new Error("Async queue is closed.");
|
|
612
|
+
}
|
|
613
|
+
enqueue(task, onError) {
|
|
614
|
+
this.assertOpen();
|
|
615
|
+
const running = new Promise((resolve) => {
|
|
616
|
+
setTimeout(() => {
|
|
617
|
+
Promise.resolve().then(task).catch(onError).finally(resolve);
|
|
618
|
+
}, 0);
|
|
619
|
+
});
|
|
620
|
+
this.active.add(running);
|
|
621
|
+
running.then(() => {
|
|
622
|
+
this.active.delete(running);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
async dispatch(job, payload) {
|
|
626
|
+
this.enqueue(() => job.handle(payload), (error) => {
|
|
627
|
+
console.error("[AsyncQueue] Job failed:", error);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
close() {
|
|
631
|
+
this.closing ??= (async () => {
|
|
632
|
+
while (this.active.size)
|
|
633
|
+
await Promise.all([...this.active]);
|
|
634
|
+
this.closed = true;
|
|
635
|
+
})();
|
|
636
|
+
return this.closing;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function createQueue(driver) {
|
|
640
|
+
return driver === "async" ? new AsyncQueue : new SyncQueue;
|
|
641
|
+
}
|
|
642
|
+
|
|
582
643
|
// ../../src/core/queue/resilientQueue.ts
|
|
583
|
-
class ResilientQueue {
|
|
644
|
+
class ResilientQueue extends AsyncQueue {
|
|
584
645
|
failedJobs;
|
|
585
646
|
asyncDispatch;
|
|
586
647
|
constructor(failedJobs, asyncDispatch = false) {
|
|
648
|
+
super();
|
|
587
649
|
this.failedJobs = failedJobs;
|
|
588
650
|
this.asyncDispatch = asyncDispatch;
|
|
589
651
|
}
|
|
590
652
|
async dispatch(job, payload) {
|
|
653
|
+
this.assertOpen();
|
|
591
654
|
const name = jobRegistry.resolveName(job);
|
|
592
655
|
if (!name) {
|
|
593
656
|
throw new Error("Job is not registered with the queue worker registry.");
|
|
@@ -598,11 +661,9 @@ class ResilientQueue {
|
|
|
598
661
|
attempts: 0
|
|
599
662
|
};
|
|
600
663
|
if (this.asyncDispatch) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
});
|
|
605
|
-
}, 0);
|
|
664
|
+
this.enqueue(() => runQueueJob(envelope, this.failedJobs), (error) => {
|
|
665
|
+
console.error("[ResilientQueue] Job failed:", error);
|
|
666
|
+
});
|
|
606
667
|
return;
|
|
607
668
|
}
|
|
608
669
|
await runQueueJob(envelope, this.failedJobs);
|
|
@@ -451,6 +451,9 @@ class RedisQueue {
|
|
|
451
451
|
constructor(redisUrl) {
|
|
452
452
|
this.client = new RedisClient(redisUrl);
|
|
453
453
|
}
|
|
454
|
+
close() {
|
|
455
|
+
this.client.close();
|
|
456
|
+
}
|
|
454
457
|
async dispatch(job, payload) {
|
|
455
458
|
const name = jobRegistry.resolveName(job);
|
|
456
459
|
if (!name) {
|
|
@@ -579,15 +582,75 @@ async function countPendingQueueJobs(redisUrl) {
|
|
|
579
582
|
}
|
|
580
583
|
}
|
|
581
584
|
|
|
585
|
+
// ../../src/core/queue/index.ts
|
|
586
|
+
class Job {
|
|
587
|
+
static jobName;
|
|
588
|
+
maxAttempts;
|
|
589
|
+
backoffMs;
|
|
590
|
+
priority;
|
|
591
|
+
constructor() {
|
|
592
|
+
const jobName = this.constructor.jobName;
|
|
593
|
+
if (jobName) {
|
|
594
|
+
jobRegistry.track(jobName, this);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
class SyncQueue {
|
|
600
|
+
async dispatch(job, payload) {
|
|
601
|
+
await job.handle(payload);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
class AsyncQueue {
|
|
606
|
+
active = new Set;
|
|
607
|
+
closing;
|
|
608
|
+
closed = false;
|
|
609
|
+
assertOpen() {
|
|
610
|
+
if (this.closed)
|
|
611
|
+
throw new Error("Async queue is closed.");
|
|
612
|
+
}
|
|
613
|
+
enqueue(task, onError) {
|
|
614
|
+
this.assertOpen();
|
|
615
|
+
const running = new Promise((resolve) => {
|
|
616
|
+
setTimeout(() => {
|
|
617
|
+
Promise.resolve().then(task).catch(onError).finally(resolve);
|
|
618
|
+
}, 0);
|
|
619
|
+
});
|
|
620
|
+
this.active.add(running);
|
|
621
|
+
running.then(() => {
|
|
622
|
+
this.active.delete(running);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
async dispatch(job, payload) {
|
|
626
|
+
this.enqueue(() => job.handle(payload), (error) => {
|
|
627
|
+
console.error("[AsyncQueue] Job failed:", error);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
close() {
|
|
631
|
+
this.closing ??= (async () => {
|
|
632
|
+
while (this.active.size)
|
|
633
|
+
await Promise.all([...this.active]);
|
|
634
|
+
this.closed = true;
|
|
635
|
+
})();
|
|
636
|
+
return this.closing;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function createQueue(driver) {
|
|
640
|
+
return driver === "async" ? new AsyncQueue : new SyncQueue;
|
|
641
|
+
}
|
|
642
|
+
|
|
582
643
|
// ../../src/core/queue/resilientQueue.ts
|
|
583
|
-
class ResilientQueue {
|
|
644
|
+
class ResilientQueue extends AsyncQueue {
|
|
584
645
|
failedJobs;
|
|
585
646
|
asyncDispatch;
|
|
586
647
|
constructor(failedJobs, asyncDispatch = false) {
|
|
648
|
+
super();
|
|
587
649
|
this.failedJobs = failedJobs;
|
|
588
650
|
this.asyncDispatch = asyncDispatch;
|
|
589
651
|
}
|
|
590
652
|
async dispatch(job, payload) {
|
|
653
|
+
this.assertOpen();
|
|
591
654
|
const name = jobRegistry.resolveName(job);
|
|
592
655
|
if (!name) {
|
|
593
656
|
throw new Error("Job is not registered with the queue worker registry.");
|
|
@@ -598,11 +661,9 @@ class ResilientQueue {
|
|
|
598
661
|
attempts: 0
|
|
599
662
|
};
|
|
600
663
|
if (this.asyncDispatch) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
});
|
|
605
|
-
}, 0);
|
|
664
|
+
this.enqueue(() => runQueueJob(envelope, this.failedJobs), (error) => {
|
|
665
|
+
console.error("[ResilientQueue] Job failed:", error);
|
|
666
|
+
});
|
|
606
667
|
return;
|
|
607
668
|
}
|
|
608
669
|
await runQueueJob(envelope, this.failedJobs);
|
|
@@ -454,6 +454,9 @@ class RedisQueue {
|
|
|
454
454
|
constructor(redisUrl) {
|
|
455
455
|
this.client = new RedisClient(redisUrl);
|
|
456
456
|
}
|
|
457
|
+
close() {
|
|
458
|
+
this.client.close();
|
|
459
|
+
}
|
|
457
460
|
async dispatch(job, payload) {
|
|
458
461
|
const name = jobRegistry.resolveName(job);
|
|
459
462
|
if (!name) {
|
|
@@ -582,15 +585,75 @@ async function countPendingQueueJobs(redisUrl) {
|
|
|
582
585
|
}
|
|
583
586
|
}
|
|
584
587
|
|
|
588
|
+
// ../../src/core/queue/index.ts
|
|
589
|
+
class Job {
|
|
590
|
+
static jobName;
|
|
591
|
+
maxAttempts;
|
|
592
|
+
backoffMs;
|
|
593
|
+
priority;
|
|
594
|
+
constructor() {
|
|
595
|
+
const jobName = this.constructor.jobName;
|
|
596
|
+
if (jobName) {
|
|
597
|
+
jobRegistry.track(jobName, this);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
class SyncQueue {
|
|
603
|
+
async dispatch(job, payload) {
|
|
604
|
+
await job.handle(payload);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
class AsyncQueue {
|
|
609
|
+
active = new Set;
|
|
610
|
+
closing;
|
|
611
|
+
closed = false;
|
|
612
|
+
assertOpen() {
|
|
613
|
+
if (this.closed)
|
|
614
|
+
throw new Error("Async queue is closed.");
|
|
615
|
+
}
|
|
616
|
+
enqueue(task, onError) {
|
|
617
|
+
this.assertOpen();
|
|
618
|
+
const running = new Promise((resolve) => {
|
|
619
|
+
setTimeout(() => {
|
|
620
|
+
Promise.resolve().then(task).catch(onError).finally(resolve);
|
|
621
|
+
}, 0);
|
|
622
|
+
});
|
|
623
|
+
this.active.add(running);
|
|
624
|
+
running.then(() => {
|
|
625
|
+
this.active.delete(running);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
async dispatch(job, payload) {
|
|
629
|
+
this.enqueue(() => job.handle(payload), (error) => {
|
|
630
|
+
console.error("[AsyncQueue] Job failed:", error);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
close() {
|
|
634
|
+
this.closing ??= (async () => {
|
|
635
|
+
while (this.active.size)
|
|
636
|
+
await Promise.all([...this.active]);
|
|
637
|
+
this.closed = true;
|
|
638
|
+
})();
|
|
639
|
+
return this.closing;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function createQueue(driver) {
|
|
643
|
+
return driver === "async" ? new AsyncQueue : new SyncQueue;
|
|
644
|
+
}
|
|
645
|
+
|
|
585
646
|
// ../../src/core/queue/resilientQueue.ts
|
|
586
|
-
class ResilientQueue {
|
|
647
|
+
class ResilientQueue extends AsyncQueue {
|
|
587
648
|
failedJobs;
|
|
588
649
|
asyncDispatch;
|
|
589
650
|
constructor(failedJobs, asyncDispatch = false) {
|
|
651
|
+
super();
|
|
590
652
|
this.failedJobs = failedJobs;
|
|
591
653
|
this.asyncDispatch = asyncDispatch;
|
|
592
654
|
}
|
|
593
655
|
async dispatch(job, payload) {
|
|
656
|
+
this.assertOpen();
|
|
594
657
|
const name = jobRegistry.resolveName(job);
|
|
595
658
|
if (!name) {
|
|
596
659
|
throw new Error("Job is not registered with the queue worker registry.");
|
|
@@ -601,11 +664,9 @@ class ResilientQueue {
|
|
|
601
664
|
attempts: 0
|
|
602
665
|
};
|
|
603
666
|
if (this.asyncDispatch) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
});
|
|
608
|
-
}, 0);
|
|
667
|
+
this.enqueue(() => runQueueJob(envelope, this.failedJobs), (error) => {
|
|
668
|
+
console.error("[ResilientQueue] Job failed:", error);
|
|
669
|
+
});
|
|
609
670
|
return;
|
|
610
671
|
}
|
|
611
672
|
await runQueueJob(envelope, this.failedJobs);
|
package/dist/entries/queue.js
CHANGED
|
@@ -57,12 +57,37 @@ class SyncQueue {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
class AsyncQueue {
|
|
60
|
+
active = new Set;
|
|
61
|
+
closing;
|
|
62
|
+
closed = false;
|
|
63
|
+
assertOpen() {
|
|
64
|
+
if (this.closed)
|
|
65
|
+
throw new Error("Async queue is closed.");
|
|
66
|
+
}
|
|
67
|
+
enqueue(task, onError) {
|
|
68
|
+
this.assertOpen();
|
|
69
|
+
const running = new Promise((resolve) => {
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
Promise.resolve().then(task).catch(onError).finally(resolve);
|
|
72
|
+
}, 0);
|
|
73
|
+
});
|
|
74
|
+
this.active.add(running);
|
|
75
|
+
running.then(() => {
|
|
76
|
+
this.active.delete(running);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
60
79
|
async dispatch(job, payload) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
this.enqueue(() => job.handle(payload), (error) => {
|
|
81
|
+
console.error("[AsyncQueue] Job failed:", error);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
close() {
|
|
85
|
+
this.closing ??= (async () => {
|
|
86
|
+
while (this.active.size)
|
|
87
|
+
await Promise.all([...this.active]);
|
|
88
|
+
this.closed = true;
|
|
89
|
+
})();
|
|
90
|
+
return this.closing;
|
|
66
91
|
}
|
|
67
92
|
}
|
|
68
93
|
function createQueue(driver) {
|