@spinajs/queue-sqs-transport 2.0.482 → 2.0.485
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/lib/cjs/connection-factory.d.ts +2 -0
- package/lib/cjs/connection-factory.d.ts.map +1 -0
- package/lib/cjs/connection-factory.js +24 -0
- package/lib/cjs/connection-factory.js.map +1 -0
- package/lib/cjs/connection.d.ts +99 -0
- package/lib/cjs/connection.d.ts.map +1 -0
- package/lib/cjs/connection.js +264 -0
- package/lib/cjs/connection.js.map +1 -0
- package/lib/cjs/index.d.ts +3 -0
- package/lib/cjs/index.d.ts.map +1 -0
- package/lib/cjs/index.js +19 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/cjs/package.json +1 -0
- package/lib/mjs/connection-factory.d.ts +2 -0
- package/lib/mjs/connection-factory.d.ts.map +1 -0
- package/lib/mjs/connection-factory.js +22 -0
- package/lib/mjs/connection-factory.js.map +1 -0
- package/lib/mjs/connection.d.ts +99 -0
- package/lib/mjs/connection.d.ts.map +1 -0
- package/lib/mjs/connection.js +258 -0
- package/lib/mjs/connection.js.map +1 -0
- package/lib/mjs/index.d.ts +3 -0
- package/lib/mjs/index.d.ts.map +1 -0
- package/lib/mjs/index.js +3 -0
- package/lib/mjs/index.js.map +1 -0
- package/lib/mjs/package.json +1 -0
- package/lib/tsconfig.cjs.tsbuildinfo +1 -0
- package/lib/tsconfig.mjs.tsbuildinfo +1 -0
- package/package.json +6 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-factory.d.ts","sourceRoot":"","sources":["../../src/connection-factory.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const di_1 = require("@spinajs/di");
|
|
4
|
+
const connection_js_1 = require("./connection.js");
|
|
5
|
+
const configuration_1 = require("@spinajs/configuration");
|
|
6
|
+
/**
|
|
7
|
+
* Create factory func that sets client-id to connection by app name.
|
|
8
|
+
* We cannot have two connections with the same ID, so by default we take app-name
|
|
9
|
+
* with NODE_ENV and then name from options.
|
|
10
|
+
*
|
|
11
|
+
* This way we can share the same config/connections across multiple apps.
|
|
12
|
+
*/
|
|
13
|
+
di_1.DI.register(async (container, options) => {
|
|
14
|
+
const cfg = container.get(configuration_1.Configuration);
|
|
15
|
+
const appName = cfg.get('app.name', 'no-app');
|
|
16
|
+
const env = cfg.get('process.env.APP_ENV', 'development');
|
|
17
|
+
const c = new connection_js_1.SqsQueueClient({
|
|
18
|
+
...options,
|
|
19
|
+
clientId: `${appName}-${env}-${options.name}`,
|
|
20
|
+
});
|
|
21
|
+
await c.resolve();
|
|
22
|
+
return c;
|
|
23
|
+
}).as(connection_js_1.SqsQueueClient);
|
|
24
|
+
//# sourceMappingURL=connection-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-factory.js","sourceRoot":"","sources":["../../src/connection-factory.ts"],"names":[],"mappings":";;AAAA,oCAAiC;AAEjC,mDAAiD;AACjD,0DAAuD;AAEvD;;;;;;GAMG;AACH,OAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,OAAgC,EAAE,EAAE;IAChE,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,6BAAa,CAAE,CAAC;IAC1C,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAS,UAAU,EAAE,QAAQ,CAAC,CAAC;IACtD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAS,qBAAqB,EAAE,aAAa,CAAC,CAAC;IAElE,MAAM,CAAC,GAAG,IAAI,8BAAc,CAAC;QAC3B,GAAG,OAAO;QACV,QAAQ,EAAE,GAAG,OAAO,IAAI,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE;KAC9C,CAAC,CAAC;IACH,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC;IAElB,OAAO,CAAC,CAAC;AACX,CAAC,CAAC,CAAC,EAAE,CAAC,8BAAc,CAAC,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { IQueueMessage, IQueueConnectionOptions, QueueClient, QueueMessage } from '@spinajs/queue';
|
|
2
|
+
import { Constructor } from '@spinajs/di';
|
|
3
|
+
import { SQSClient } from '@aws-sdk/client-sqs';
|
|
4
|
+
/**
|
|
5
|
+
* SQS specific connection options that can be passed through the generic
|
|
6
|
+
* {@link IQueueConnectionOptions.options} bag.
|
|
7
|
+
*/
|
|
8
|
+
export interface ISqsConnectionOptions {
|
|
9
|
+
region?: string;
|
|
10
|
+
queueUrl?: string;
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
waitTimeSeconds?: number;
|
|
13
|
+
visibilityTimeout?: number;
|
|
14
|
+
maxMessages?: number;
|
|
15
|
+
credentials?: {
|
|
16
|
+
accessKeyId: string;
|
|
17
|
+
secretAccessKey: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Base delay ( ms ) the poll loop sleeps after a failed ReceiveMessage before
|
|
21
|
+
* retrying. Escalates ( doubles ) on consecutive failures up to
|
|
22
|
+
* {@link receiveErrorBackoffMaxMs} and resets after a successful receive.
|
|
23
|
+
* Prevents a persistent fault ( deleted queue, revoked credentials ) from
|
|
24
|
+
* hot-looping the receive-error branch. Defaults to 500ms.
|
|
25
|
+
*/
|
|
26
|
+
receiveErrorBackoffMs?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Cap ( ms ) for the escalating receive-error backoff. Defaults to 10000ms.
|
|
29
|
+
*/
|
|
30
|
+
receiveErrorBackoffMaxMs?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Tracks a live subscription. Unlike STOMP there is no broker-side subscription
|
|
34
|
+
* object - a subscription here is a detached long-poll loop over one queue URL.
|
|
35
|
+
* The descriptor is the loop's control block: `running` gates the loop and the
|
|
36
|
+
* `AbortController` interrupts an in-flight ( up to WaitTimeSeconds ) ReceiveMessage
|
|
37
|
+
* so unsubscribe / dispose return promptly instead of blocking on the poll.
|
|
38
|
+
*/
|
|
39
|
+
interface ISqsSubscriptionDescriptor {
|
|
40
|
+
url: string;
|
|
41
|
+
callback: (e: IQueueMessage) => Promise<void>;
|
|
42
|
+
running: boolean;
|
|
43
|
+
controller: AbortController;
|
|
44
|
+
/**
|
|
45
|
+
* Set to true by {@link pollLoop} right before it returns, regardless of how it
|
|
46
|
+
* exits ( normal stop, abort or a swallowed error ). Lets callers / tests
|
|
47
|
+
* observe that the detached loop has actually wound down.
|
|
48
|
+
*/
|
|
49
|
+
exited: boolean;
|
|
50
|
+
}
|
|
51
|
+
export declare class SqsQueueClient extends QueueClient {
|
|
52
|
+
/**
|
|
53
|
+
* Underlying AWS SQS client. SQS is a plain HTTP service so there is no
|
|
54
|
+
* long-lived connection to keep alive - the client is created in {@link resolve}
|
|
55
|
+
* and reused for every {@link emit}.
|
|
56
|
+
*/
|
|
57
|
+
protected Sqs: SQSClient;
|
|
58
|
+
/**
|
|
59
|
+
* Active subscriptions keyed by queue URL. Each entry owns a detached poll loop.
|
|
60
|
+
*/
|
|
61
|
+
protected Subscriptions: Map<string, ISqsSubscriptionDescriptor>;
|
|
62
|
+
constructor(options: IQueueConnectionOptions);
|
|
63
|
+
resolve(): Promise<void>;
|
|
64
|
+
emit(message: IQueueMessage): Promise<void>;
|
|
65
|
+
subscribe(channelOrMessage: string | Constructor<QueueMessage>, callback: (e: IQueueMessage) => Promise<void>, _subscriptionId?: string, _durable?: boolean): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Long-poll loop for a single subscription. Runs until `desc.running` is cleared
|
|
68
|
+
* ( by unsubscribe / dispose ). On success the message is acked by DeleteMessage;
|
|
69
|
+
* on handler failure the message is left untouched so the SQS visibility timeout
|
|
70
|
+
* redelivers it and the redrive policy dead-letters after maxReceiveCount.
|
|
71
|
+
*/
|
|
72
|
+
protected pollLoop(desc: ISqsSubscriptionDescriptor): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Processes a single received SQS message: parse -> rehydrate -> handler -> ack.
|
|
75
|
+
* Guards against poison / non-object payloads so a bad message is skipped rather
|
|
76
|
+
* than crashing the loop. Keeps the ack/nack contract: delete on handler success,
|
|
77
|
+
* leave ( for redelivery / DLQ ) on handler failure.
|
|
78
|
+
*/
|
|
79
|
+
protected handleMessage(desc: ISqsSubscriptionDescriptor, m: {
|
|
80
|
+
Body?: string;
|
|
81
|
+
ReceiptHandle?: string;
|
|
82
|
+
}): Promise<void>;
|
|
83
|
+
unsubscribe(channelOrMessage: string | Constructor<QueueMessage>, _removeDurable?: boolean): void;
|
|
84
|
+
dispose(): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Abortable delay used by the receive-error backoff. Resolves after `ms`, or
|
|
87
|
+
* immediately if the descriptor's AbortController fires ( unsubscribe / dispose )
|
|
88
|
+
* so a disposed subscription never sits parked in a long backoff sleep. The
|
|
89
|
+
* loop re-checks `desc.running` right after, so an abort mid-sleep exits promptly.
|
|
90
|
+
*/
|
|
91
|
+
protected backoffSleep(ms: number, signal: AbortSignal): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Stops a subscription's poll loop: clears the running flag and aborts any
|
|
94
|
+
* in-flight ReceiveMessage. The loop observes both and exits.
|
|
95
|
+
*/
|
|
96
|
+
protected stop(desc: ISqsSubscriptionDescriptor): void;
|
|
97
|
+
}
|
|
98
|
+
export {};
|
|
99
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnG,OAAO,EAAE,WAAW,EAAgC,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,SAAS,EAAmE,MAAM,qBAAqB,CAAC;AAIjH;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;OAEG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;;;;GAMG;AACH,UAAU,0BAA0B;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,eAAe,CAAC;IAC5B;;;;OAIG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,qBAEa,cAAe,SAAQ,WAAW;IAC7C;;;;OAIG;IACH,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC;IAEzB;;OAEG;IACH,SAAS,CAAC,aAAa,0CAAiD;gBAE5D,OAAO,EAAE,uBAAuB;IAI/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBxB,IAAI,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAe3C,SAAS,CAAC,gBAAgB,EAAE,MAAM,GAAG,WAAW,CAAC,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BxL;;;;;OAKG;cACa,QAAQ,CAAC,IAAI,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC;IA2EzE;;;;;OAKG;cACa,aAAa,CAAC,IAAI,EAAE,0BAA0B,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDrH,WAAW,CAAC,gBAAgB,EAAE,MAAM,GAAG,WAAW,CAAC,YAAY,CAAC,EAAE,cAAc,CAAC,EAAE,OAAO,GAAG,IAAI;IAiB3F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAcrC;;;;;OAKG;IACH,SAAS,CAAC,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBtE;;;OAGG;IACH,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,0BAA0B,GAAG,IAAI;CAIvD"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.SqsQueueClient = void 0;
|
|
16
|
+
const queue_1 = require("@spinajs/queue");
|
|
17
|
+
const di_1 = require("@spinajs/di");
|
|
18
|
+
const client_sqs_1 = require("@aws-sdk/client-sqs");
|
|
19
|
+
const luxon_1 = require("luxon");
|
|
20
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
21
|
+
let SqsQueueClient = class SqsQueueClient extends queue_1.QueueClient {
|
|
22
|
+
constructor(options) {
|
|
23
|
+
super(options);
|
|
24
|
+
/**
|
|
25
|
+
* Active subscriptions keyed by queue URL. Each entry owns a detached poll loop.
|
|
26
|
+
*/
|
|
27
|
+
this.Subscriptions = new Map();
|
|
28
|
+
}
|
|
29
|
+
async resolve() {
|
|
30
|
+
const o = (this.Options.options ?? {});
|
|
31
|
+
// SQS is HTTP based - there is no eager connection to establish and no
|
|
32
|
+
// connection-level destination to validate here. Destinations are resolved
|
|
33
|
+
// per-message at emit time via getChannelForMessage, which consults the
|
|
34
|
+
// global queue.routing table first and only then falls back to the
|
|
35
|
+
// connection defaults - so a routing-only connection ( no defaultQueueChannel /
|
|
36
|
+
// defaultTopicChannel ) is perfectly valid. resolve()'s only job is to build
|
|
37
|
+
// the SQSClient; region/endpoint/credentials are all optional for the SDK.
|
|
38
|
+
this.Sqs = new client_sqs_1.SQSClient({
|
|
39
|
+
region: o.region,
|
|
40
|
+
endpoint: o.endpoint,
|
|
41
|
+
credentials: o.credentials,
|
|
42
|
+
});
|
|
43
|
+
this.Log.info(`SQS queue client ${this.Options.name} resolved ( region: ${o.region ?? '<default>'}, endpoint: ${o.endpoint ?? '<default>'} )`);
|
|
44
|
+
}
|
|
45
|
+
async emit(message) {
|
|
46
|
+
// routing[message.Name] || defaultQueueChannel|defaultTopicChannel - for SQS
|
|
47
|
+
// these entries are queue URLs. `Name`/`Type`/`JobId` ride the JSON body so
|
|
48
|
+
// the core consumer can rehydrate the message on the receiving side.
|
|
49
|
+
const urls = this.getChannelForMessage(message);
|
|
50
|
+
const body = JSON.stringify(message);
|
|
51
|
+
await Promise.all(urls.map((QueueUrl) => {
|
|
52
|
+
this.Log.trace(`Publishing ${message.Type} Name: ${message.Name} to SQS queue ${QueueUrl} ( ${this.Options.name} )`);
|
|
53
|
+
return this.Sqs.send(new client_sqs_1.SendMessageCommand({ QueueUrl, MessageBody: body }));
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
async subscribe(channelOrMessage, callback, _subscriptionId, _durable) {
|
|
57
|
+
// mirror STOMP's overload handling: a raw string is the queue URL itself,
|
|
58
|
+
// a message class is resolved through the routing table.
|
|
59
|
+
const urls = lodash_1.default.isString(channelOrMessage) ? [channelOrMessage] : this.getChannelForMessage(channelOrMessage);
|
|
60
|
+
urls.forEach((url) => {
|
|
61
|
+
if (this.Subscriptions.has(url)) {
|
|
62
|
+
this.Log.warn(`SQS queue ${url} already subscribed !`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const desc = {
|
|
66
|
+
url,
|
|
67
|
+
callback,
|
|
68
|
+
running: true,
|
|
69
|
+
controller: new AbortController(),
|
|
70
|
+
exited: false,
|
|
71
|
+
};
|
|
72
|
+
this.Subscriptions.set(url, desc);
|
|
73
|
+
// detached loop - subscribe returns as soon as the poll loop is started,
|
|
74
|
+
// it is NOT awaited. Errors inside the loop must never escape ( there is no
|
|
75
|
+
// caller to catch them ), so pollLoop swallows/logs everything itself.
|
|
76
|
+
void this.pollLoop(desc);
|
|
77
|
+
this.Log.success(`SQS queue ${url} subscribed and polling for messages !`);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Long-poll loop for a single subscription. Runs until `desc.running` is cleared
|
|
82
|
+
* ( by unsubscribe / dispose ). On success the message is acked by DeleteMessage;
|
|
83
|
+
* on handler failure the message is left untouched so the SQS visibility timeout
|
|
84
|
+
* redelivers it and the redrive policy dead-letters after maxReceiveCount.
|
|
85
|
+
*/
|
|
86
|
+
async pollLoop(desc) {
|
|
87
|
+
const o = (this.Options.options ?? {});
|
|
88
|
+
// Escalating backoff for the receive-error branch. Starts at the base delay,
|
|
89
|
+
// doubles on each consecutive failure up to the cap, and is reset to the base
|
|
90
|
+
// after any successful ReceiveMessage. Without this a persistent fault
|
|
91
|
+
// ( deleted queue, revoked credentials, permanently dead endpoint ) makes the
|
|
92
|
+
// SDK send throw immediately and the loop spins as a tight infinite loop,
|
|
93
|
+
// spamming logs and hammering the SQS API.
|
|
94
|
+
const baseBackoff = Math.max(0, o.receiveErrorBackoffMs ?? 500);
|
|
95
|
+
const maxBackoff = Math.max(baseBackoff, o.receiveErrorBackoffMaxMs ?? 10000);
|
|
96
|
+
let backoff = baseBackoff;
|
|
97
|
+
try {
|
|
98
|
+
while (desc.running) {
|
|
99
|
+
let res;
|
|
100
|
+
try {
|
|
101
|
+
res = await this.Sqs.send(new client_sqs_1.ReceiveMessageCommand({
|
|
102
|
+
QueueUrl: desc.url,
|
|
103
|
+
WaitTimeSeconds: o.waitTimeSeconds ?? 20,
|
|
104
|
+
MaxNumberOfMessages: o.maxMessages ?? 1,
|
|
105
|
+
VisibilityTimeout: o.visibilityTimeout,
|
|
106
|
+
}), { abortSignal: desc.controller.signal });
|
|
107
|
+
// a successful receive clears the fault - drop back to the base delay so a
|
|
108
|
+
// transient blip doesn't leave the loop permanently slow.
|
|
109
|
+
backoff = baseBackoff;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
// aborted by unsubscribe / dispose - the in-flight long poll was cut short
|
|
113
|
+
if (!desc.running) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
// receive error ( throttling, network blip, or a persistent fault ) - log
|
|
117
|
+
// and keep the loop alive, otherwise a single hiccup would silently stop
|
|
118
|
+
// consumption. Back off before retrying so a *persistent* fault can't
|
|
119
|
+
// hot-loop the receive; the sleep is abortable so unsubscribe / dispose
|
|
120
|
+
// still tears the loop down promptly instead of waiting out the delay.
|
|
121
|
+
this.Log.warn(`SQS receive error on ${desc.url} ( ${this.Options.name} ), backing off ${backoff}ms: ${err}`);
|
|
122
|
+
await this.backoffSleep(backoff, desc.controller.signal);
|
|
123
|
+
backoff = Math.min(backoff * 2, maxBackoff);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
for (const m of res?.Messages ?? []) {
|
|
127
|
+
// a concurrent unsubscribe / dispose may have flipped this mid-batch
|
|
128
|
+
if (!desc.running) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
// Defense-in-depth: NOTHING a single message does may escape this loop as
|
|
132
|
+
// an unhandled rejection ( the loop runs detached - there is no caller to
|
|
133
|
+
// catch it, and an escaped throw would crash the worker / Node process ).
|
|
134
|
+
// Any per-message failure is logged and the loop moves on.
|
|
135
|
+
try {
|
|
136
|
+
await this.handleMessage(desc, m);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
this.Log.error(`Unexpected error handling SQS message on ${desc.url} ( ${this.Options.name} ), skipping: ${err}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
// last-resort guard: even the loop scaffolding ( e.g. an unexpected throw from
|
|
146
|
+
// the receive-error branch ) must not reject the detached promise.
|
|
147
|
+
this.Log.error(`SQS poll loop for ${desc.url} ( ${this.Options.name} ) crashed unexpectedly: ${err}`);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
desc.exited = true;
|
|
151
|
+
this.Log.trace(`SQS poll loop for ${desc.url} ( ${this.Options.name} ) stopped`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Processes a single received SQS message: parse -> rehydrate -> handler -> ack.
|
|
156
|
+
* Guards against poison / non-object payloads so a bad message is skipped rather
|
|
157
|
+
* than crashing the loop. Keeps the ack/nack contract: delete on handler success,
|
|
158
|
+
* leave ( for redelivery / DLQ ) on handler failure.
|
|
159
|
+
*/
|
|
160
|
+
async handleMessage(desc, m) {
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = JSON.parse(m.Body);
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
// poison message - we can't tell its Type so we can't route it. Leave it:
|
|
167
|
+
// SQS redelivery + the redrive policy will dead-letter it after maxReceiveCount,
|
|
168
|
+
// which is preferable to deleting evidence of a producer bug.
|
|
169
|
+
this.Log.error(`Unparseable SQS message on ${desc.url} ( ${this.Options.name} ), leaving for redelivery / DLQ: ${e}`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// JSON.parse happily yields non-object values ( null, numbers, strings,
|
|
173
|
+
// booleans ) that would blow up the CreatedAt / callback access below with a
|
|
174
|
+
// TypeError. Treat them as poison and skip so the loop stays alive.
|
|
175
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
176
|
+
this.Log.error(`Non-object SQS message body on ${desc.url} ( ${this.Options.name} ), skipping`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const message = parsed;
|
|
180
|
+
// luxon DateTime serializes to an ISO string over the wire - rehydrate it
|
|
181
|
+
if (typeof message.CreatedAt === 'string') {
|
|
182
|
+
message.CreatedAt = luxon_1.DateTime.fromISO(message.CreatedAt);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await desc.callback(message);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
// NACK: do nothing. The message stays invisible only for the visibility
|
|
189
|
+
// timeout, after which SQS redelivers; the redrive policy dead-letters it
|
|
190
|
+
// once maxReceiveCount is hit. No manual retry / DLQ logic here.
|
|
191
|
+
this.Log.error(`Handler failed for ${message.Name} on ${desc.url}, leaving message for redelivery: ${err}`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// ACK: only delete once the handler has fully succeeded. A delete failure is
|
|
195
|
+
// non-fatal ( at-least-once: SQS will simply redeliver ) and is logged
|
|
196
|
+
// distinctly so it is never mistaken for a handler failure.
|
|
197
|
+
try {
|
|
198
|
+
await this.Sqs.send(new client_sqs_1.DeleteMessageCommand({ QueueUrl: desc.url, ReceiptHandle: m.ReceiptHandle }));
|
|
199
|
+
this.Log.trace(`Processed and acked ${message.Type} Name: ${message.Name} from ${desc.url} ( ${this.Options.name} )`);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
this.Log.error(`ack/delete failed for message ${message.Name} on ${desc.url} ( ${this.Options.name} ), it will redeliver: ${err}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
unsubscribe(channelOrMessage, _removeDurable) {
|
|
206
|
+
const urls = lodash_1.default.isString(channelOrMessage) ? [channelOrMessage] : this.getChannelForMessage(channelOrMessage);
|
|
207
|
+
urls.forEach((url) => {
|
|
208
|
+
const desc = this.Subscriptions.get(url);
|
|
209
|
+
if (!desc) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.stop(desc);
|
|
213
|
+
this.Subscriptions.delete(url);
|
|
214
|
+
this.Log.info(`Unsubscribed from SQS queue ${url} ( ${this.Options.name} )`);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async dispose() {
|
|
218
|
+
// stop every poll loop and interrupt any in-flight long poll so this returns
|
|
219
|
+
// promptly instead of waiting up to WaitTimeSeconds for the last receive.
|
|
220
|
+
for (const desc of this.Subscriptions.values()) {
|
|
221
|
+
this.stop(desc);
|
|
222
|
+
}
|
|
223
|
+
this.Subscriptions.clear();
|
|
224
|
+
this.Sqs?.destroy();
|
|
225
|
+
this.Log.info(`SQS queue client ${this.Options.name} disposed`);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Abortable delay used by the receive-error backoff. Resolves after `ms`, or
|
|
229
|
+
* immediately if the descriptor's AbortController fires ( unsubscribe / dispose )
|
|
230
|
+
* so a disposed subscription never sits parked in a long backoff sleep. The
|
|
231
|
+
* loop re-checks `desc.running` right after, so an abort mid-sleep exits promptly.
|
|
232
|
+
*/
|
|
233
|
+
backoffSleep(ms, signal) {
|
|
234
|
+
if (ms <= 0 || signal.aborted) {
|
|
235
|
+
return Promise.resolve();
|
|
236
|
+
}
|
|
237
|
+
return new Promise((resolve) => {
|
|
238
|
+
const onAbort = () => {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
resolve();
|
|
241
|
+
};
|
|
242
|
+
const timer = setTimeout(() => {
|
|
243
|
+
signal.removeEventListener('abort', onAbort);
|
|
244
|
+
resolve();
|
|
245
|
+
}, ms);
|
|
246
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Stops a subscription's poll loop: clears the running flag and aborts any
|
|
251
|
+
* in-flight ReceiveMessage. The loop observes both and exits.
|
|
252
|
+
*/
|
|
253
|
+
stop(desc) {
|
|
254
|
+
desc.running = false;
|
|
255
|
+
desc.controller.abort();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
exports.SqsQueueClient = SqsQueueClient;
|
|
259
|
+
exports.SqsQueueClient = SqsQueueClient = __decorate([
|
|
260
|
+
(0, di_1.PerInstanceCheck)(),
|
|
261
|
+
(0, di_1.Injectable)(queue_1.QueueClient),
|
|
262
|
+
__metadata("design:paramtypes", [Object])
|
|
263
|
+
], SqsQueueClient);
|
|
264
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/connection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,iBAAiB,CAAC"}
|
package/lib/cjs/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./connection-factory.js"), exports);
|
|
18
|
+
__exportStar(require("./connection.js"), exports);
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0DAAwC;AACxC,kDAAgC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-factory.d.ts","sourceRoot":"","sources":["../../src/connection-factory.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DI } from '@spinajs/di';
|
|
2
|
+
import { SqsQueueClient } from './connection.js';
|
|
3
|
+
import { Configuration } from '@spinajs/configuration';
|
|
4
|
+
/**
|
|
5
|
+
* Create factory func that sets client-id to connection by app name.
|
|
6
|
+
* We cannot have two connections with the same ID, so by default we take app-name
|
|
7
|
+
* with NODE_ENV and then name from options.
|
|
8
|
+
*
|
|
9
|
+
* This way we can share the same config/connections across multiple apps.
|
|
10
|
+
*/
|
|
11
|
+
DI.register(async (container, options) => {
|
|
12
|
+
const cfg = container.get(Configuration);
|
|
13
|
+
const appName = cfg.get('app.name', 'no-app');
|
|
14
|
+
const env = cfg.get('process.env.APP_ENV', 'development');
|
|
15
|
+
const c = new SqsQueueClient({
|
|
16
|
+
...options,
|
|
17
|
+
clientId: `${appName}-${env}-${options.name}`,
|
|
18
|
+
});
|
|
19
|
+
await c.resolve();
|
|
20
|
+
return c;
|
|
21
|
+
}).as(SqsQueueClient);
|
|
22
|
+
//# sourceMappingURL=connection-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-factory.js","sourceRoot":"","sources":["../../src/connection-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD;;;;;;GAMG;AACH,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,OAAgC,EAAE,EAAE;IAChE,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,aAAa,CAAE,CAAC;IAC1C,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAS,UAAU,EAAE,QAAQ,CAAC,CAAC;IACtD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAS,qBAAqB,EAAE,aAAa,CAAC,CAAC;IAElE,MAAM,CAAC,GAAG,IAAI,cAAc,CAAC;QAC3B,GAAG,OAAO;QACV,QAAQ,EAAE,GAAG,OAAO,IAAI,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE;KAC9C,CAAC,CAAC;IACH,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC;IAElB,OAAO,CAAC,CAAC;AACX,CAAC,CAAC,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { IQueueMessage, IQueueConnectionOptions, QueueClient, QueueMessage } from '@spinajs/queue';
|
|
2
|
+
import { Constructor } from '@spinajs/di';
|
|
3
|
+
import { SQSClient } from '@aws-sdk/client-sqs';
|
|
4
|
+
/**
|
|
5
|
+
* SQS specific connection options that can be passed through the generic
|
|
6
|
+
* {@link IQueueConnectionOptions.options} bag.
|
|
7
|
+
*/
|
|
8
|
+
export interface ISqsConnectionOptions {
|
|
9
|
+
region?: string;
|
|
10
|
+
queueUrl?: string;
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
waitTimeSeconds?: number;
|
|
13
|
+
visibilityTimeout?: number;
|
|
14
|
+
maxMessages?: number;
|
|
15
|
+
credentials?: {
|
|
16
|
+
accessKeyId: string;
|
|
17
|
+
secretAccessKey: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Base delay ( ms ) the poll loop sleeps after a failed ReceiveMessage before
|
|
21
|
+
* retrying. Escalates ( doubles ) on consecutive failures up to
|
|
22
|
+
* {@link receiveErrorBackoffMaxMs} and resets after a successful receive.
|
|
23
|
+
* Prevents a persistent fault ( deleted queue, revoked credentials ) from
|
|
24
|
+
* hot-looping the receive-error branch. Defaults to 500ms.
|
|
25
|
+
*/
|
|
26
|
+
receiveErrorBackoffMs?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Cap ( ms ) for the escalating receive-error backoff. Defaults to 10000ms.
|
|
29
|
+
*/
|
|
30
|
+
receiveErrorBackoffMaxMs?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Tracks a live subscription. Unlike STOMP there is no broker-side subscription
|
|
34
|
+
* object - a subscription here is a detached long-poll loop over one queue URL.
|
|
35
|
+
* The descriptor is the loop's control block: `running` gates the loop and the
|
|
36
|
+
* `AbortController` interrupts an in-flight ( up to WaitTimeSeconds ) ReceiveMessage
|
|
37
|
+
* so unsubscribe / dispose return promptly instead of blocking on the poll.
|
|
38
|
+
*/
|
|
39
|
+
interface ISqsSubscriptionDescriptor {
|
|
40
|
+
url: string;
|
|
41
|
+
callback: (e: IQueueMessage) => Promise<void>;
|
|
42
|
+
running: boolean;
|
|
43
|
+
controller: AbortController;
|
|
44
|
+
/**
|
|
45
|
+
* Set to true by {@link pollLoop} right before it returns, regardless of how it
|
|
46
|
+
* exits ( normal stop, abort or a swallowed error ). Lets callers / tests
|
|
47
|
+
* observe that the detached loop has actually wound down.
|
|
48
|
+
*/
|
|
49
|
+
exited: boolean;
|
|
50
|
+
}
|
|
51
|
+
export declare class SqsQueueClient extends QueueClient {
|
|
52
|
+
/**
|
|
53
|
+
* Underlying AWS SQS client. SQS is a plain HTTP service so there is no
|
|
54
|
+
* long-lived connection to keep alive - the client is created in {@link resolve}
|
|
55
|
+
* and reused for every {@link emit}.
|
|
56
|
+
*/
|
|
57
|
+
protected Sqs: SQSClient;
|
|
58
|
+
/**
|
|
59
|
+
* Active subscriptions keyed by queue URL. Each entry owns a detached poll loop.
|
|
60
|
+
*/
|
|
61
|
+
protected Subscriptions: Map<string, ISqsSubscriptionDescriptor>;
|
|
62
|
+
constructor(options: IQueueConnectionOptions);
|
|
63
|
+
resolve(): Promise<void>;
|
|
64
|
+
emit(message: IQueueMessage): Promise<void>;
|
|
65
|
+
subscribe(channelOrMessage: string | Constructor<QueueMessage>, callback: (e: IQueueMessage) => Promise<void>, _subscriptionId?: string, _durable?: boolean): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Long-poll loop for a single subscription. Runs until `desc.running` is cleared
|
|
68
|
+
* ( by unsubscribe / dispose ). On success the message is acked by DeleteMessage;
|
|
69
|
+
* on handler failure the message is left untouched so the SQS visibility timeout
|
|
70
|
+
* redelivers it and the redrive policy dead-letters after maxReceiveCount.
|
|
71
|
+
*/
|
|
72
|
+
protected pollLoop(desc: ISqsSubscriptionDescriptor): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Processes a single received SQS message: parse -> rehydrate -> handler -> ack.
|
|
75
|
+
* Guards against poison / non-object payloads so a bad message is skipped rather
|
|
76
|
+
* than crashing the loop. Keeps the ack/nack contract: delete on handler success,
|
|
77
|
+
* leave ( for redelivery / DLQ ) on handler failure.
|
|
78
|
+
*/
|
|
79
|
+
protected handleMessage(desc: ISqsSubscriptionDescriptor, m: {
|
|
80
|
+
Body?: string;
|
|
81
|
+
ReceiptHandle?: string;
|
|
82
|
+
}): Promise<void>;
|
|
83
|
+
unsubscribe(channelOrMessage: string | Constructor<QueueMessage>, _removeDurable?: boolean): void;
|
|
84
|
+
dispose(): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Abortable delay used by the receive-error backoff. Resolves after `ms`, or
|
|
87
|
+
* immediately if the descriptor's AbortController fires ( unsubscribe / dispose )
|
|
88
|
+
* so a disposed subscription never sits parked in a long backoff sleep. The
|
|
89
|
+
* loop re-checks `desc.running` right after, so an abort mid-sleep exits promptly.
|
|
90
|
+
*/
|
|
91
|
+
protected backoffSleep(ms: number, signal: AbortSignal): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Stops a subscription's poll loop: clears the running flag and aborts any
|
|
94
|
+
* in-flight ReceiveMessage. The loop observes both and exits.
|
|
95
|
+
*/
|
|
96
|
+
protected stop(desc: ISqsSubscriptionDescriptor): void;
|
|
97
|
+
}
|
|
98
|
+
export {};
|
|
99
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnG,OAAO,EAAE,WAAW,EAAgC,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,SAAS,EAAmE,MAAM,qBAAqB,CAAC;AAIjH;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;OAEG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;;;;GAMG;AACH,UAAU,0BAA0B;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,eAAe,CAAC;IAC5B;;;;OAIG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,qBAEa,cAAe,SAAQ,WAAW;IAC7C;;;;OAIG;IACH,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC;IAEzB;;OAEG;IACH,SAAS,CAAC,aAAa,0CAAiD;gBAE5D,OAAO,EAAE,uBAAuB;IAI/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBxB,IAAI,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAe3C,SAAS,CAAC,gBAAgB,EAAE,MAAM,GAAG,WAAW,CAAC,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BxL;;;;;OAKG;cACa,QAAQ,CAAC,IAAI,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC;IA2EzE;;;;;OAKG;cACa,aAAa,CAAC,IAAI,EAAE,0BAA0B,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDrH,WAAW,CAAC,gBAAgB,EAAE,MAAM,GAAG,WAAW,CAAC,YAAY,CAAC,EAAE,cAAc,CAAC,EAAE,OAAO,GAAG,IAAI;IAiB3F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAcrC;;;;;OAKG;IACH,SAAS,CAAC,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBtE;;;OAGG;IACH,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,0BAA0B,GAAG,IAAI;CAIvD"}
|