@fluojs/queue 1.0.1 → 2.0.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/README.ko.md +70 -8
- package/README.md +70 -8
- package/dist/dead-letter-manager.d.ts +10 -1
- package/dist/dead-letter-manager.d.ts.map +1 -1
- package/dist/dead-letter-manager.js +62 -0
- package/dist/helpers.d.ts +3 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +6 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/module.d.ts +2 -2
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +88 -14
- package/dist/service.d.ts +50 -5
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +182 -44
- package/dist/status.d.ts +3 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +33 -0
- package/dist/tokens.d.ts +55 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +111 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-discovery.d.ts +2 -1
- package/dist/worker-discovery.d.ts.map +1 -1
- package/dist/worker-discovery.js +2 -2
- package/package.json +5 -5
package/dist/module.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
import { getRedisClientToken } from '@fluojs/redis';
|
|
1
2
|
import { defineModule } from '@fluojs/runtime';
|
|
3
|
+
import { APPLICATION_LOGGER, BOOTSTRAP_READY_SIGNAL, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
2
4
|
import { normalizePositiveInteger, normalizePositiveIntegerOrFalse, normalizeRateLimiter } from './helpers.js';
|
|
3
5
|
import { QueueLifecycleService } from './service.js';
|
|
4
|
-
import {
|
|
6
|
+
import { getQueueLifecycleServiceToken, getQueueModuleContextToken, getQueueOptionsToken, getQueueRedisClientToken as getQueueScopedRedisClientToken, getQueueToken, normalizeQueueScope, QUEUE } from './tokens.js';
|
|
7
|
+
function hasQueueRedisClient(value) {
|
|
8
|
+
if (typeof value !== 'object' || value === null) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const client = value;
|
|
12
|
+
return typeof client.duplicate === 'function' && typeof client.lrange === 'function' && typeof client.rpush === 'function' && typeof client.ltrim === 'function';
|
|
13
|
+
}
|
|
5
14
|
function normalizeQueueModuleOptions(options = {}) {
|
|
6
15
|
const defaultRateLimiter = normalizeRateLimiter(options.defaultRateLimiter);
|
|
16
|
+
const scope = normalizeQueueScope(options.scope);
|
|
7
17
|
return {
|
|
8
18
|
clientName: options.clientName,
|
|
19
|
+
...(scope ? {
|
|
20
|
+
scope
|
|
21
|
+
} : {}),
|
|
9
22
|
defaultAttempts: normalizePositiveInteger(options.defaultAttempts, 1),
|
|
10
23
|
defaultBackoff: options.defaultBackoff ? {
|
|
11
24
|
delayMs: options.defaultBackoff.delayMs,
|
|
@@ -14,20 +27,79 @@ function normalizeQueueModuleOptions(options = {}) {
|
|
|
14
27
|
defaultConcurrency: normalizePositiveInteger(options.defaultConcurrency, 1),
|
|
15
28
|
defaultDeadLetterMaxEntries: normalizePositiveIntegerOrFalse(options.defaultDeadLetterMaxEntries, 1_000),
|
|
16
29
|
defaultRateLimiter,
|
|
30
|
+
global: options.global ?? true,
|
|
17
31
|
workerShutdownTimeoutMs: normalizePositiveInteger(options.workerShutdownTimeoutMs, 30_000)
|
|
18
32
|
};
|
|
19
33
|
}
|
|
20
|
-
function
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
function getQueueProviderTokens(scope) {
|
|
35
|
+
return {
|
|
36
|
+
lifecycleServiceToken: getQueueLifecycleServiceToken(scope),
|
|
37
|
+
moduleContextToken: getQueueModuleContextToken(scope),
|
|
38
|
+
optionsToken: getQueueOptionsToken(scope),
|
|
39
|
+
queueRedisClientToken: getQueueScopedRedisClientToken(scope),
|
|
40
|
+
queueToken: getQueueToken(scope)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function moduleCanAccessQueueRegistration(compiledModule, moduleType) {
|
|
44
|
+
return compiledModule.type === moduleType || (compiledModule.definition.imports ?? []).includes(moduleType);
|
|
45
|
+
}
|
|
46
|
+
function canAccessRedisClient(compiledModules, moduleContext, redisToken) {
|
|
47
|
+
return compiledModules.some(compiledModule => moduleCanAccessQueueRegistration(compiledModule, moduleContext.moduleType) && compiledModule.accessibleTokens.has(redisToken));
|
|
48
|
+
}
|
|
49
|
+
function formatQueueScope(scope) {
|
|
50
|
+
return scope ?? 'default';
|
|
51
|
+
}
|
|
52
|
+
function createQueueProviders(normalizedOptions, moduleType) {
|
|
53
|
+
const tokens = getQueueProviderTokens(normalizedOptions.scope);
|
|
54
|
+
const providers = [{
|
|
55
|
+
provide: tokens.optionsToken,
|
|
56
|
+
useValue: normalizedOptions
|
|
57
|
+
}, {
|
|
58
|
+
provide: tokens.moduleContextToken,
|
|
59
|
+
useValue: {
|
|
60
|
+
moduleType,
|
|
61
|
+
scope: formatQueueScope(normalizedOptions.scope)
|
|
62
|
+
}
|
|
63
|
+
}, {
|
|
64
|
+
inject: [tokens.optionsToken, RUNTIME_CONTAINER, COMPILED_MODULES, tokens.moduleContextToken],
|
|
65
|
+
provide: tokens.queueRedisClientToken,
|
|
66
|
+
useFactory: async (...deps) => {
|
|
67
|
+
const [resolvedOptions, runtimeContainer, compiledModules, moduleContext] = deps;
|
|
68
|
+
const redisToken = getRedisClientToken(resolvedOptions.clientName);
|
|
69
|
+
if (!canAccessRedisClient(compiledModules, moduleContext, redisToken)) {
|
|
70
|
+
throw new Error(`@fluojs/queue cannot access Redis client token ${String(redisToken)} from queue scope "${moduleContext.scope}". Import and export the matching RedisModule.forRoot(...) registration through the same module graph.`);
|
|
71
|
+
}
|
|
72
|
+
if (!runtimeContainer.has(redisToken)) {
|
|
73
|
+
throw new Error('@fluojs/queue requires a registered Redis client with duplicate(), lrange(), rpush(), and ltrim() methods.');
|
|
74
|
+
}
|
|
75
|
+
const redisClient = await runtimeContainer.resolve(redisToken);
|
|
76
|
+
if (!hasQueueRedisClient(redisClient)) {
|
|
77
|
+
throw new Error('@fluojs/queue requires a Redis client with duplicate(), lrange(), rpush(), and ltrim() methods.');
|
|
78
|
+
}
|
|
79
|
+
return redisClient;
|
|
80
|
+
}
|
|
81
|
+
}, {
|
|
82
|
+
inject: [tokens.optionsToken, tokens.queueRedisClientToken, RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, BOOTSTRAP_READY_SIGNAL, tokens.moduleContextToken],
|
|
83
|
+
provide: tokens.lifecycleServiceToken,
|
|
84
|
+
useFactory: (...deps) => {
|
|
85
|
+
const typedDeps = deps;
|
|
86
|
+
return new QueueLifecycleService(...typedDeps);
|
|
87
|
+
}
|
|
88
|
+
}, {
|
|
89
|
+
inject: [tokens.lifecycleServiceToken],
|
|
90
|
+
provide: tokens.queueToken,
|
|
27
91
|
useFactory: service => ({
|
|
28
|
-
enqueue: job => service.enqueue(job)
|
|
92
|
+
enqueue: job => service.enqueue(job),
|
|
93
|
+
inspectDeadLetters: (jobName, options) => service.inspectDeadLetters(jobName, options)
|
|
29
94
|
})
|
|
30
95
|
}];
|
|
96
|
+
if (normalizedOptions.scope === undefined) {
|
|
97
|
+
providers.push({
|
|
98
|
+
provide: QueueLifecycleService,
|
|
99
|
+
useExisting: tokens.lifecycleServiceToken
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return providers;
|
|
31
103
|
}
|
|
32
104
|
|
|
33
105
|
/**
|
|
@@ -35,10 +107,10 @@ function createQueueProviders(options = {}) {
|
|
|
35
107
|
*/
|
|
36
108
|
export class QueueModule {
|
|
37
109
|
/**
|
|
38
|
-
* Registers queue providers
|
|
110
|
+
* Registers queue providers using canonical `forRoot(...)` semantics.
|
|
39
111
|
*
|
|
40
112
|
* @param options Queue runtime defaults used by discovered workers and enqueued jobs.
|
|
41
|
-
* @returns A module definition that exports `
|
|
113
|
+
* @returns A module definition that exports default queue tokens when `scope` is omitted, or scoped queue tokens when `scope` is set.
|
|
42
114
|
*
|
|
43
115
|
* @example
|
|
44
116
|
* ```ts
|
|
@@ -56,11 +128,13 @@ export class QueueModule {
|
|
|
56
128
|
* ```
|
|
57
129
|
*/
|
|
58
130
|
static forRoot(options = {}) {
|
|
131
|
+
const normalizedOptions = normalizeQueueModuleOptions(options);
|
|
132
|
+
const tokens = getQueueProviderTokens(normalizedOptions.scope);
|
|
59
133
|
class QueueModuleDefinition {}
|
|
60
134
|
return defineModule(QueueModuleDefinition, {
|
|
61
|
-
exports: [QueueLifecycleService, QUEUE],
|
|
62
|
-
global:
|
|
63
|
-
providers: createQueueProviders(
|
|
135
|
+
exports: normalizedOptions.scope === undefined ? [QueueLifecycleService, tokens.lifecycleServiceToken, QUEUE] : [tokens.lifecycleServiceToken, tokens.queueToken],
|
|
136
|
+
global: normalizedOptions.global,
|
|
137
|
+
providers: createQueueProviders(normalizedOptions, QueueModuleDefinition)
|
|
64
138
|
});
|
|
65
139
|
}
|
|
66
140
|
}
|
package/dist/service.d.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import type { Container } from '@fluojs/di';
|
|
2
2
|
import type { ApplicationLogger, CompiledModule, OnApplicationBootstrap, OnApplicationShutdown, OnModuleDestroy } from '@fluojs/runtime';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import type { BootstrapReadySignal } from '@fluojs/runtime/internal';
|
|
4
|
+
import { type ConnectionOptions } from 'bullmq';
|
|
5
|
+
import { type QueueRedisDeadLetterClient } from './dead-letter-manager.js';
|
|
6
|
+
import { type QueuePlatformStatusSnapshot } from './status.js';
|
|
7
|
+
import type { QueueModuleContext } from './tokens.js';
|
|
8
|
+
import type { NormalizedQueueModuleOptions, Queue, QueueDeadLetterInspectionOptions, QueueDeadLetterInspectionResult } from './types.js';
|
|
9
|
+
type QueueOwnedConnection = ConnectionOptions & {
|
|
10
|
+
connect(): Promise<unknown>;
|
|
11
|
+
disconnect(): void;
|
|
12
|
+
quit(): Promise<unknown>;
|
|
13
|
+
maxRetriesPerRequest?: number | null;
|
|
14
|
+
status?: string;
|
|
15
|
+
};
|
|
16
|
+
interface QueueBullMqConnectionOptions {
|
|
17
|
+
maxRetriesPerRequest: null;
|
|
18
|
+
}
|
|
19
|
+
interface QueueRedisClient extends QueueRedisDeadLetterClient {
|
|
20
|
+
duplicate(options?: QueueBullMqConnectionOptions): QueueOwnedConnection;
|
|
21
|
+
}
|
|
5
22
|
/**
|
|
6
23
|
* Lifecycle-managed queue runtime for worker discovery and job dispatch.
|
|
7
24
|
*
|
|
@@ -10,21 +27,27 @@ import type { NormalizedQueueModuleOptions, Queue } from './types.js';
|
|
|
10
27
|
*/
|
|
11
28
|
export declare class QueueLifecycleService implements Queue, OnApplicationBootstrap, OnApplicationShutdown, OnModuleDestroy {
|
|
12
29
|
private readonly options;
|
|
30
|
+
private readonly redisClient;
|
|
13
31
|
private readonly runtimeContainer;
|
|
14
32
|
private readonly compiledModules;
|
|
15
33
|
private readonly logger;
|
|
16
34
|
private readonly bootstrapReadySignal;
|
|
35
|
+
private readonly moduleContext;
|
|
17
36
|
private readonly descriptorsByJobType;
|
|
18
37
|
private readonly queuesByJobName;
|
|
19
38
|
private readonly workersByJobName;
|
|
20
39
|
private readonly ownedConnections;
|
|
21
40
|
private readonly deadLetterManager;
|
|
22
41
|
private readonly readyWorkers;
|
|
42
|
+
private readonly runningWorkerJobNames;
|
|
43
|
+
private readonly failedWorkerJobNames;
|
|
44
|
+
private readonly workerStartFailures;
|
|
45
|
+
private readonly compiledModulesByType;
|
|
23
46
|
private lifecycleState;
|
|
24
|
-
private redisClient;
|
|
25
47
|
private startPromise;
|
|
26
48
|
private shutdownPromise;
|
|
27
|
-
|
|
49
|
+
private startupFailureRollbackPromise;
|
|
50
|
+
constructor(options: NormalizedQueueModuleOptions, redisClient: QueueRedisClient, runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, bootstrapReadySignal?: BootstrapReadySignal, moduleContext?: QueueModuleContext);
|
|
28
51
|
onApplicationBootstrap(): Promise<void>;
|
|
29
52
|
onApplicationShutdown(): Promise<void>;
|
|
30
53
|
onModuleDestroy(): Promise<void>;
|
|
@@ -37,14 +60,30 @@ export declare class QueueLifecycleService implements Queue, OnApplicationBootst
|
|
|
37
60
|
* @throws {Error} When no worker is registered for the job type or the queue is not initialized.
|
|
38
61
|
*/
|
|
39
62
|
enqueue<TJob extends object>(job: TJob): Promise<string>;
|
|
63
|
+
/**
|
|
64
|
+
* Reads a bounded snapshot of dead-letter records for one queue job name.
|
|
65
|
+
*
|
|
66
|
+
* Inspection reads Redis without starting workers or lifecycle-gating the read. The backing Redis client must
|
|
67
|
+
* remain reachable; `RedisModule` owns that shared client's shutdown.
|
|
68
|
+
*
|
|
69
|
+
* @param jobName Queue worker job name whose dead letters should be inspected.
|
|
70
|
+
* @param options Optional bounded inspection settings.
|
|
71
|
+
* @returns Valid records in newest-first order plus the malformed count for the inspected window.
|
|
72
|
+
* @throws The backing Redis read error when that client is unavailable or already shut down.
|
|
73
|
+
*/
|
|
74
|
+
inspectDeadLetters(jobName: string, options?: QueueDeadLetterInspectionOptions): Promise<QueueDeadLetterInspectionResult>;
|
|
40
75
|
/**
|
|
41
76
|
* Creates a platform status snapshot for health checks and diagnostics.
|
|
42
77
|
*
|
|
43
78
|
* @returns A structured snapshot describing lifecycle state, discovered workers, and pending dead-letter writes.
|
|
44
79
|
*/
|
|
45
|
-
createPlatformStatusSnapshot():
|
|
80
|
+
createPlatformStatusSnapshot(): QueuePlatformStatusSnapshot;
|
|
46
81
|
private ensureStarted;
|
|
47
82
|
private startLifecycle;
|
|
83
|
+
private shouldDiscoverWorkersInModule;
|
|
84
|
+
private canReachQueueRegistration;
|
|
85
|
+
private exportsQueueRegistration;
|
|
86
|
+
private assertUniqueQueueScope;
|
|
48
87
|
private handleStartupFailure;
|
|
49
88
|
private resolveRedisClient;
|
|
50
89
|
private getRedisClient;
|
|
@@ -58,16 +97,22 @@ export declare class QueueLifecycleService implements Queue, OnApplicationBootst
|
|
|
58
97
|
private registerInitializedWorker;
|
|
59
98
|
private scheduleReadyWorkers;
|
|
60
99
|
private runWorker;
|
|
100
|
+
private markWorkerReadyWhenStarted;
|
|
101
|
+
private recordWorkerStartFailure;
|
|
102
|
+
private rollbackAfterWorkerStartupFailure;
|
|
103
|
+
private toErrorMessage;
|
|
61
104
|
private cleanupWorkerInitializationFailure;
|
|
62
105
|
private createOwnedConnection;
|
|
63
106
|
private executeWorker;
|
|
64
107
|
private resolveWorkerHandler;
|
|
65
108
|
private rehydrateWorkerPayload;
|
|
66
109
|
private shutdown;
|
|
110
|
+
private waitForStartupFailureRollback;
|
|
67
111
|
private waitForInFlightStartup;
|
|
68
112
|
private closeInitializedResources;
|
|
69
113
|
private tryCloseWorker;
|
|
70
114
|
private tryCloseQueue;
|
|
71
115
|
private tryCloseOwnedConnection;
|
|
72
116
|
}
|
|
117
|
+
export {};
|
|
73
118
|
//# sourceMappingURL=service.d.ts.map
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/dist/service.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
let _initClass;
|
|
2
|
-
function _applyDecs(e, t, n, r, o, i) { var a, c, u, s, f, l, p, d = Symbol.metadata || Symbol.for("Symbol.metadata"), m = Object.defineProperty, h = Object.create, y = [h(null), h(null)], v = t.length; function g(t, n, r) { return function (o, i) { n && (i = o, o = e); for (var a = 0; a < t.length; a++) i = t[a].apply(o, r ? [i] : []); return r ? i : o; }; } function b(e, t, n, r) { if ("function" != typeof e && (r || void 0 !== e)) throw new TypeError(t + " must " + (n || "be") + " a function" + (r ? "" : " or undefined")); return e; } function applyDec(e, t, n, r, o, i, u, s, f, l, p) { function d(e) { if (!p(e)) throw new TypeError("Attempted to access private element on non-instance"); } var h = [].concat(t[0]), v = t[3], w = !u, D = 1 === o, S = 3 === o, j = 4 === o, E = 2 === o; function I(t, n, r) { return function (o, i) { return n && (i = o, o = e), r && r(o), P[t].call(o, i); }; } if (!w) { var P = {}, k = [], F = S ? "get" : j || D ? "set" : "value"; if (f ? (l || D ? P = { get: _setFunctionName(function () { return v(this); }, r, "get"), set: function (e) { t[4](this, e); } } : P[F] = v, l || _setFunctionName(P[F], r, E ? "" : F)) : l || (P = Object.getOwnPropertyDescriptor(e, r)), !l && !f) { if ((c = y[+s][r]) && 7 !== (c ^ o)) throw Error("Decorating two elements with the same name (" + P[F].name + ") is not supported yet"); y[+s][r] = o < 3 ? 1 : o; } } for (var N = e, O = h.length - 1; O >= 0; O -= n ? 2 : 1) { var T = b(h[O], "A decorator", "be", !0), z = n ? h[O - 1] : void 0, A = {}, H = { kind: ["field", "accessor", "method", "getter", "setter", "class"][o], name: r, metadata: a, addInitializer: function (e, t) { if (e.v) throw new TypeError("attempted to call addInitializer after decoration was finished"); b(t, "An initializer", "be", !0), i.push(t); }.bind(null, A) }; if (w) c = T.call(z, N, H), A.v = 1, b(c, "class decorators", "return") && (N = c);else if (H.static = s, H.private = f, c = H.access = { has: f ? p.bind() : function (e) { return r in e; } }, j || (c.get = f ? E ? function (e) { return d(e), P.value; } : I("get", 0, d) : function (e) { return e[r]; }), E || S || (c.set = f ? I("set", 0, d) : function (e, t) { e[r] = t; }), N = T.call(z, D ? { get: P.get, set: P.set } : P[F], H), A.v = 1, D) { if ("object" == typeof N && N) (c = b(N.get, "accessor.get")) && (P.get = c), (c = b(N.set, "accessor.set")) && (P.set = c), (c = b(N.init, "accessor.init")) && k.unshift(c);else if (void 0 !== N) throw new TypeError("accessor decorators must return an object with get, set, or init properties or undefined"); } else b(N, (l ? "field" : "method") + " decorators", "return") && (l ? k.unshift(N) : P[F] = N); } return o < 2 && u.push(g(k, s, 1), g(i, s, 0)), l || w || (f ? D ? u.splice(-1, 0, I("get", s), I("set", s)) : u.push(E ? P[F] : b.call.bind(P[F])) : m(e, r, P)), N; } function w(e) { return m(e, d, { configurable: !0, enumerable: !0, value: a }); } return void 0 !== i && (a = i[d]), a = h(null == a ? null : a), f = [], l = function (e) { e && f.push(g(e)); }, p = function (t, r) { for (var i = 0; i < n.length; i++) { var a = n[i], c = a[1], l = 7 & c; if ((8 & c) == t && !l == r) { var p = a[2], d = !!a[3], m = 16 & c; applyDec(t ? e : e.prototype, a, m, d ? "#" + p : _toPropertyKey(p), l, l < 2 ? [] : t ? s = s || [] : u = u || [], f, !!t, d, r, t && d ? function (t) { return _checkInRHS(t) === e; } : o); } } }, p(8, 0), p(0, 0), p(8, 1), p(0, 1), l(u), l(s), c = f, v || w(e), { e: c, get c() { var n = []; return v && [w(e = applyDec(e, [t], r, e.name, 5, n)), g(n, 1)]; } }; }
|
|
3
|
-
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
4
|
-
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
5
|
-
function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
|
|
6
|
-
function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
|
|
7
|
-
import { Inject } from '@fluojs/core';
|
|
8
1
|
import { cloneWithFallback } from '@fluojs/core/internal';
|
|
9
|
-
import {
|
|
10
|
-
import { APPLICATION_LOGGER, BOOTSTRAP_READY_SIGNAL, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
2
|
+
import { getRedisComponentId } from '@fluojs/redis';
|
|
11
3
|
import { Queue as BullQueue, Worker as BullWorker } from 'bullmq';
|
|
12
4
|
import { QueueDeadLetterManager } from './dead-letter-manager.js';
|
|
13
5
|
import { normalizePositiveInteger, withTimeout } from './helpers.js';
|
|
14
6
|
import { createQueuePlatformStatusSnapshot } from './status.js';
|
|
15
|
-
import {
|
|
7
|
+
import { getQueueLifecycleServiceToken, getQueueToken, QUEUE } from './tokens.js';
|
|
16
8
|
import { discoverQueueWorkerDescriptors } from './worker-discovery.js';
|
|
17
9
|
const IMMEDIATE_BOOTSTRAP_READY_SIGNAL = {
|
|
18
10
|
wait: () => Promise.resolve()
|
|
@@ -22,7 +14,7 @@ function hasQueueRedisClient(value) {
|
|
|
22
14
|
return false;
|
|
23
15
|
}
|
|
24
16
|
const client = value;
|
|
25
|
-
return typeof client.duplicate === 'function' && typeof client.rpush === 'function' && typeof client.ltrim === 'function';
|
|
17
|
+
return typeof client.duplicate === 'function' && typeof client.lrange === 'function' && typeof client.rpush === 'function' && typeof client.ltrim === 'function';
|
|
26
18
|
}
|
|
27
19
|
function isQueuePayload(value) {
|
|
28
20
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -46,6 +38,27 @@ function toBullBackoff(backoff) {
|
|
|
46
38
|
type: backoff.type ?? 'fixed'
|
|
47
39
|
};
|
|
48
40
|
}
|
|
41
|
+
function isQueueModuleContext(value) {
|
|
42
|
+
if (typeof value !== 'object' || value === null) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const context = value;
|
|
46
|
+
return typeof context.moduleType === 'function' && typeof context.scope === 'string';
|
|
47
|
+
}
|
|
48
|
+
function collectQueueModuleScopeCount(compiledModules, scope) {
|
|
49
|
+
let count = 0;
|
|
50
|
+
for (const compiledModule of compiledModules) {
|
|
51
|
+
for (const provider of compiledModule.definition.providers ?? []) {
|
|
52
|
+
if (typeof provider !== 'object' || provider === null || !('useValue' in provider)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (isQueueModuleContext(provider.useValue) && provider.useValue.scope === scope) {
|
|
56
|
+
count += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return count;
|
|
61
|
+
}
|
|
49
62
|
async function closeConnection(connection) {
|
|
50
63
|
if (connection.status === 'end') {
|
|
51
64
|
return;
|
|
@@ -66,28 +79,35 @@ async function closeConnection(connection) {
|
|
|
66
79
|
* The service discovers `@QueueWorker()` providers during bootstrap, creates the
|
|
67
80
|
* BullMQ queues/workers they require, and shuts them down with the application.
|
|
68
81
|
*/
|
|
69
|
-
|
|
70
|
-
class QueueLifecycleService {
|
|
71
|
-
static {
|
|
72
|
-
[_QueueLifecycleServic, _initClass] = _applyDecs(this, [Inject(QUEUE_OPTIONS, RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, BOOTSTRAP_READY_SIGNAL)], []).c;
|
|
73
|
-
}
|
|
82
|
+
export class QueueLifecycleService {
|
|
74
83
|
descriptorsByJobType = new Map();
|
|
75
84
|
queuesByJobName = new Map();
|
|
76
85
|
workersByJobName = new Map();
|
|
77
86
|
ownedConnections = [];
|
|
78
87
|
deadLetterManager;
|
|
79
88
|
readyWorkers = [];
|
|
89
|
+
runningWorkerJobNames = new Set();
|
|
90
|
+
failedWorkerJobNames = new Set();
|
|
91
|
+
workerStartFailures = [];
|
|
92
|
+
compiledModulesByType;
|
|
80
93
|
lifecycleState = 'idle';
|
|
81
|
-
redisClient;
|
|
82
94
|
startPromise;
|
|
83
95
|
shutdownPromise;
|
|
84
|
-
|
|
96
|
+
startupFailureRollbackPromise;
|
|
97
|
+
constructor(options, redisClient, runtimeContainer, compiledModules, logger, bootstrapReadySignal = IMMEDIATE_BOOTSTRAP_READY_SIGNAL, moduleContext = {
|
|
98
|
+
moduleType: QueueLifecycleService,
|
|
99
|
+
scope: 'default'
|
|
100
|
+
}) {
|
|
85
101
|
this.options = options;
|
|
102
|
+
this.redisClient = redisClient;
|
|
86
103
|
this.runtimeContainer = runtimeContainer;
|
|
87
104
|
this.compiledModules = compiledModules;
|
|
88
105
|
this.logger = logger;
|
|
89
106
|
this.bootstrapReadySignal = bootstrapReadySignal;
|
|
107
|
+
this.moduleContext = moduleContext;
|
|
108
|
+
this.compiledModulesByType = new Map(this.compiledModules.map(compiledModule => [compiledModule.type, compiledModule]));
|
|
90
109
|
this.deadLetterManager = new QueueDeadLetterManager(this.options, this.logger, () => this.getRedisClient());
|
|
110
|
+
this.assertUniqueQueueScope();
|
|
91
111
|
}
|
|
92
112
|
async onApplicationBootstrap() {
|
|
93
113
|
await this.ensureStarted();
|
|
@@ -127,27 +147,47 @@ class QueueLifecycleService {
|
|
|
127
147
|
return queuedJob.id ?? '';
|
|
128
148
|
}
|
|
129
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Reads a bounded snapshot of dead-letter records for one queue job name.
|
|
152
|
+
*
|
|
153
|
+
* Inspection reads Redis without starting workers or lifecycle-gating the read. The backing Redis client must
|
|
154
|
+
* remain reachable; `RedisModule` owns that shared client's shutdown.
|
|
155
|
+
*
|
|
156
|
+
* @param jobName Queue worker job name whose dead letters should be inspected.
|
|
157
|
+
* @param options Optional bounded inspection settings.
|
|
158
|
+
* @returns Valid records in newest-first order plus the malformed count for the inspected window.
|
|
159
|
+
* @throws The backing Redis read error when that client is unavailable or already shut down.
|
|
160
|
+
*/
|
|
161
|
+
async inspectDeadLetters(jobName, options) {
|
|
162
|
+
return this.deadLetterManager.inspect(jobName, options);
|
|
163
|
+
}
|
|
164
|
+
|
|
130
165
|
/**
|
|
131
166
|
* Creates a platform status snapshot for health checks and diagnostics.
|
|
132
167
|
*
|
|
133
168
|
* @returns A structured snapshot describing lifecycle state, discovered workers, and pending dead-letter writes.
|
|
134
169
|
*/
|
|
135
170
|
createPlatformStatusSnapshot() {
|
|
171
|
+
const lastWorkerStartFailure = this.workerStartFailures[this.workerStartFailures.length - 1];
|
|
136
172
|
return createQueuePlatformStatusSnapshot({
|
|
137
173
|
dependencyId: getRedisComponentId(this.options.clientName),
|
|
138
174
|
lifecycleState: this.lifecycleState,
|
|
175
|
+
...(lastWorkerStartFailure ? {
|
|
176
|
+
lastWorkerStartFailure: lastWorkerStartFailure.message
|
|
177
|
+
} : {}),
|
|
139
178
|
pendingDeadLetterWrites: this.deadLetterManager.pendingWriteCount,
|
|
140
179
|
queuesReady: this.queuesByJobName.size,
|
|
180
|
+
workerStartFailures: this.workerStartFailures.length,
|
|
141
181
|
workerShutdownTimeoutMs: this.options.workerShutdownTimeoutMs,
|
|
142
182
|
workersDiscovered: this.descriptorsByJobType.size,
|
|
143
|
-
workersReady: this.
|
|
183
|
+
workersReady: this.runningWorkerJobNames.size
|
|
144
184
|
});
|
|
145
185
|
}
|
|
146
186
|
async ensureStarted() {
|
|
147
187
|
if (this.lifecycleState === 'started') {
|
|
148
188
|
return;
|
|
149
189
|
}
|
|
150
|
-
if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
|
|
190
|
+
if (this.lifecycleState === 'failed' || this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
|
|
151
191
|
throw new Error(`Queue lifecycle state is ${this.lifecycleState}.`);
|
|
152
192
|
}
|
|
153
193
|
if (!this.startPromise) {
|
|
@@ -163,10 +203,9 @@ class QueueLifecycleService {
|
|
|
163
203
|
this.startPromise = undefined;
|
|
164
204
|
}
|
|
165
205
|
async startLifecycle() {
|
|
166
|
-
const redis =
|
|
167
|
-
this.redisClient = redis;
|
|
206
|
+
const redis = this.resolveRedisClient();
|
|
168
207
|
this.descriptorsByJobType.clear();
|
|
169
|
-
for (const [jobType, descriptor] of discoverQueueWorkerDescriptors(this.compiledModules, this.options, this.logger)) {
|
|
208
|
+
for (const [jobType, descriptor] of discoverQueueWorkerDescriptors(this.compiledModules, this.options, this.logger, compiledModule => this.shouldDiscoverWorkersInModule(compiledModule))) {
|
|
170
209
|
this.descriptorsByJobType.set(jobType, descriptor);
|
|
171
210
|
}
|
|
172
211
|
await this.initializeWorkers(redis);
|
|
@@ -175,29 +214,55 @@ class QueueLifecycleService {
|
|
|
175
214
|
this.scheduleReadyWorkers();
|
|
176
215
|
}
|
|
177
216
|
}
|
|
217
|
+
shouldDiscoverWorkersInModule(compiledModule) {
|
|
218
|
+
if (this.options.global) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return this.canReachQueueRegistration(compiledModule);
|
|
222
|
+
}
|
|
223
|
+
canReachQueueRegistration(compiledModule, visited = new Set()) {
|
|
224
|
+
if (visited.has(compiledModule.type)) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
visited.add(compiledModule.type);
|
|
228
|
+
for (const importedModuleType of compiledModule.definition.imports ?? []) {
|
|
229
|
+
if (importedModuleType === this.moduleContext.moduleType) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
const importedModule = this.compiledModulesByType.get(importedModuleType);
|
|
233
|
+
if (!importedModule || !this.exportsQueueRegistration(importedModule)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (this.canReachQueueRegistration(importedModule, visited)) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
exportsQueueRegistration(compiledModule) {
|
|
243
|
+
return compiledModule.exportedTokens.has(QueueLifecycleService) || compiledModule.exportedTokens.has(QUEUE) || compiledModule.exportedTokens.has(getQueueLifecycleServiceToken(this.options.scope)) || compiledModule.exportedTokens.has(getQueueToken(this.options.scope));
|
|
244
|
+
}
|
|
245
|
+
assertUniqueQueueScope() {
|
|
246
|
+
const scopeCount = collectQueueModuleScopeCount(this.compiledModules, this.moduleContext.scope);
|
|
247
|
+
if (scopeCount > 1) {
|
|
248
|
+
throw new Error(`Duplicate @fluojs/queue scope "${this.moduleContext.scope}" registered. Provide a unique QueueModule.forRoot({ scope }) value for each scoped queue registration.`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
178
251
|
async handleStartupFailure() {
|
|
179
252
|
await this.closeInitializedResources();
|
|
253
|
+
await this.deadLetterManager.drainPendingWrites();
|
|
180
254
|
if (this.lifecycleState === 'starting') {
|
|
181
255
|
this.lifecycleState = 'idle';
|
|
182
256
|
}
|
|
183
|
-
this.redisClient = undefined;
|
|
184
257
|
this.startPromise = undefined;
|
|
185
258
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
throw new Error('@fluojs/queue requires a registered Redis client with duplicate(), rpush(), and ltrim() methods.');
|
|
259
|
+
resolveRedisClient() {
|
|
260
|
+
if (!hasQueueRedisClient(this.redisClient)) {
|
|
261
|
+
throw new Error('@fluojs/queue requires a Redis client with duplicate(), lrange(), rpush(), and ltrim() methods.');
|
|
190
262
|
}
|
|
191
|
-
|
|
192
|
-
if (!hasQueueRedisClient(redisClient)) {
|
|
193
|
-
throw new Error('@fluojs/queue requires a Redis client with duplicate(), rpush(), and ltrim() methods.');
|
|
194
|
-
}
|
|
195
|
-
return redisClient;
|
|
263
|
+
return this.redisClient;
|
|
196
264
|
}
|
|
197
265
|
getRedisClient() {
|
|
198
|
-
if (!this.redisClient) {
|
|
199
|
-
throw new Error('@fluojs/queue Redis client is not initialized.');
|
|
200
|
-
}
|
|
201
266
|
return this.redisClient;
|
|
202
267
|
}
|
|
203
268
|
async initializeWorkers(redis) {
|
|
@@ -278,23 +343,92 @@ class QueueLifecycleService {
|
|
|
278
343
|
descriptor,
|
|
279
344
|
worker
|
|
280
345
|
} of workers) {
|
|
346
|
+
if (this.lifecycleState !== 'started') {
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
281
349
|
this.runWorker(descriptor, worker);
|
|
350
|
+
if (this.lifecycleState !== 'started') {
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
282
353
|
}
|
|
283
354
|
}).catch(error => {
|
|
284
355
|
if (this.lifecycleState !== 'started') {
|
|
285
356
|
return;
|
|
286
357
|
}
|
|
358
|
+
this.lifecycleState = 'failed';
|
|
359
|
+
this.workerStartFailures.push({
|
|
360
|
+
jobName: '*',
|
|
361
|
+
message: this.toErrorMessage(error),
|
|
362
|
+
workerName: 'bootstrap-ready-signal'
|
|
363
|
+
});
|
|
287
364
|
this.logger.error('Failed to start queue workers after application bootstrap readiness.', error, 'QueueLifecycleService');
|
|
365
|
+
this.rollbackAfterWorkerStartupFailure();
|
|
288
366
|
});
|
|
289
367
|
}
|
|
290
368
|
runWorker(descriptor, worker) {
|
|
291
369
|
const runnableWorker = worker;
|
|
292
370
|
if (typeof runnableWorker.run !== 'function') {
|
|
371
|
+
this.recordWorkerStartFailure(descriptor, new Error(`Queue worker ${descriptor.workerName} cannot start because BullMQ Worker.run() is unavailable.`));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
let runResult;
|
|
375
|
+
try {
|
|
376
|
+
runResult = runnableWorker.run();
|
|
377
|
+
} catch (error) {
|
|
378
|
+
this.recordWorkerStartFailure(descriptor, error);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const runPromise = Promise.resolve(runResult);
|
|
382
|
+
void runPromise.catch(error => {
|
|
383
|
+
this.recordWorkerStartFailure(descriptor, error);
|
|
384
|
+
});
|
|
385
|
+
void this.markWorkerReadyWhenStarted(descriptor, runnableWorker, runPromise).catch(error => {
|
|
386
|
+
this.recordWorkerStartFailure(descriptor, error);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async markWorkerReadyWhenStarted(descriptor, worker, runPromise) {
|
|
390
|
+
if (typeof worker.waitUntilReady === 'function') {
|
|
391
|
+
await Promise.race([worker.waitUntilReady(), runPromise]);
|
|
392
|
+
} else {
|
|
393
|
+
await Promise.race([Promise.resolve(), runPromise]);
|
|
394
|
+
}
|
|
395
|
+
if (this.lifecycleState !== 'started' || this.failedWorkerJobNames.has(descriptor.jobName)) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this.runningWorkerJobNames.add(descriptor.jobName);
|
|
399
|
+
}
|
|
400
|
+
recordWorkerStartFailure(descriptor, error) {
|
|
401
|
+
if (this.lifecycleState === 'stopping' || this.lifecycleState === 'stopped') {
|
|
293
402
|
return;
|
|
294
403
|
}
|
|
295
|
-
|
|
296
|
-
|
|
404
|
+
if (this.failedWorkerJobNames.has(descriptor.jobName)) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
this.failedWorkerJobNames.add(descriptor.jobName);
|
|
408
|
+
this.runningWorkerJobNames.delete(descriptor.jobName);
|
|
409
|
+
this.workerStartFailures.push({
|
|
410
|
+
jobName: descriptor.jobName,
|
|
411
|
+
message: this.toErrorMessage(error),
|
|
412
|
+
workerName: descriptor.workerName
|
|
297
413
|
});
|
|
414
|
+
if (this.lifecycleState === 'started' || this.lifecycleState === 'starting') {
|
|
415
|
+
this.lifecycleState = 'failed';
|
|
416
|
+
}
|
|
417
|
+
this.logger.error(`Failed to start queue worker ${descriptor.workerName} after application bootstrap.`, error, 'QueueLifecycleService');
|
|
418
|
+
this.rollbackAfterWorkerStartupFailure();
|
|
419
|
+
}
|
|
420
|
+
rollbackAfterWorkerStartupFailure() {
|
|
421
|
+
if (!this.startupFailureRollbackPromise) {
|
|
422
|
+
this.startupFailureRollbackPromise = (async () => {
|
|
423
|
+
await this.closeInitializedResources();
|
|
424
|
+
await this.deadLetterManager.drainPendingWrites();
|
|
425
|
+
})().catch(rollbackError => {
|
|
426
|
+
this.logger.error('Failed to roll back queue resources after worker startup failure.', rollbackError, 'QueueLifecycleService');
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
toErrorMessage(error) {
|
|
431
|
+
return error instanceof Error ? error.message : String(error);
|
|
298
432
|
}
|
|
299
433
|
async cleanupWorkerInitializationFailure(resources) {
|
|
300
434
|
if (resources.worker) {
|
|
@@ -362,14 +496,21 @@ class QueueLifecycleService {
|
|
|
362
496
|
this.lifecycleState = 'stopping';
|
|
363
497
|
this.shutdownPromise = (async () => {
|
|
364
498
|
await this.waitForInFlightStartup();
|
|
499
|
+
await this.waitForStartupFailureRollback();
|
|
365
500
|
await this.closeInitializedResources();
|
|
366
501
|
await this.deadLetterManager.drainPendingWrites();
|
|
367
502
|
this.lifecycleState = 'stopped';
|
|
368
|
-
this.redisClient = undefined;
|
|
369
503
|
this.startPromise = undefined;
|
|
370
504
|
})();
|
|
371
505
|
await this.shutdownPromise;
|
|
372
506
|
}
|
|
507
|
+
async waitForStartupFailureRollback() {
|
|
508
|
+
if (!this.startupFailureRollbackPromise) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
await this.startupFailureRollbackPromise;
|
|
512
|
+
this.startupFailureRollbackPromise = undefined;
|
|
513
|
+
}
|
|
373
514
|
async waitForInFlightStartup() {
|
|
374
515
|
const startup = this.startPromise;
|
|
375
516
|
if (!startup) {
|
|
@@ -390,6 +531,7 @@ class QueueLifecycleService {
|
|
|
390
531
|
this.workersByJobName.clear();
|
|
391
532
|
this.queuesByJobName.clear();
|
|
392
533
|
this.readyWorkers.splice(0);
|
|
534
|
+
this.runningWorkerJobNames.clear();
|
|
393
535
|
for (const worker of workers) {
|
|
394
536
|
await this.tryCloseWorker(worker);
|
|
395
537
|
}
|
|
@@ -426,8 +568,4 @@ class QueueLifecycleService {
|
|
|
426
568
|
this.logger.error('Failed to close queue-owned Redis connection during shutdown.', error, 'QueueLifecycleService');
|
|
427
569
|
}
|
|
428
570
|
}
|
|
429
|
-
|
|
430
|
-
_initClass();
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
export { _QueueLifecycleServic as QueueLifecycleService };
|
|
571
|
+
}
|
package/dist/status.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { PlatformHealthReport, PlatformReadinessReport, PlatformSnapshot } from '@fluojs/runtime';
|
|
2
2
|
/** Lifecycle phases reported by the queue platform status adapter. */
|
|
3
|
-
export type QueueLifecycleState = 'idle' | 'starting' | 'started' | 'stopping' | 'stopped';
|
|
3
|
+
export type QueueLifecycleState = 'idle' | 'starting' | 'started' | 'stopping' | 'stopped' | 'failed';
|
|
4
4
|
/** Input payload used to derive queue readiness, health, and dependency details. */
|
|
5
5
|
export interface QueueStatusAdapterInput {
|
|
6
6
|
dependencyId?: string;
|
|
7
|
+
lastWorkerStartFailure?: string;
|
|
7
8
|
lifecycleState: QueueLifecycleState;
|
|
8
9
|
pendingDeadLetterWrites: number;
|
|
9
10
|
queuesReady: number;
|
|
11
|
+
workerStartFailures?: number;
|
|
10
12
|
workerShutdownTimeoutMs: number;
|
|
11
13
|
workersDiscovered: number;
|
|
12
14
|
workersReady: number;
|
package/dist/status.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEvG,sEAAsE;AACtE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEvG,sEAAsE;AACtE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEtG,oFAAoF;AACpF,MAAM,WAAW,uBAAuB;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,cAAc,EAAE,mBAAmB,CAAC;IACpC,uBAAuB,EAAE,MAAM,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uBAAuB,EAAE,MAAM,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,sFAAsF;AACtF,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,uBAAuB,CAAC;IACnC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAoHD;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,uBAAuB,GAAG,2BAA2B,CAuB7G"}
|