@blokjs/trigger-worker 0.2.1 → 0.6.1
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/__tests__/integration/nats-adapter.real-nats.test.ts +116 -0
- package/__tests__/integration/pgboss-adapter.real-pg.test.ts +164 -0
- package/__tests__/integration/rabbitmq-adapter.real-rabbitmq.test.ts +179 -0
- package/__tests__/integration/sqs-adapter.real-sqs.test.ts +228 -0
- package/dist/WorkerTrigger.d.ts +40 -4
- package/dist/WorkerTrigger.js +272 -40
- package/dist/adapters/BullMQAdapter.d.ts +1 -1
- package/dist/adapters/BullMQAdapter.js +5 -42
- package/dist/adapters/InMemoryAdapter.d.ts +1 -1
- package/dist/adapters/InMemoryAdapter.js +13 -12
- package/dist/adapters/KafkaAdapter.d.ts +62 -0
- package/dist/adapters/KafkaAdapter.js +236 -0
- package/dist/adapters/NATSAdapter.d.ts +110 -0
- package/dist/adapters/NATSAdapter.js +394 -0
- package/dist/adapters/PgBossAdapter.d.ts +56 -0
- package/dist/adapters/PgBossAdapter.js +251 -0
- package/dist/adapters/RabbitMQAdapter.d.ts +51 -0
- package/dist/adapters/RabbitMQAdapter.js +241 -0
- package/dist/adapters/RedisStreamsAdapter.d.ts +64 -0
- package/dist/adapters/RedisStreamsAdapter.js +240 -0
- package/dist/adapters/SQSAdapter.d.ts +61 -0
- package/dist/adapters/SQSAdapter.js +269 -0
- package/dist/adapters/factory.d.ts +34 -0
- package/dist/adapters/factory.js +103 -0
- package/dist/index.d.ts +25 -7
- package/dist/index.js +31 -16
- package/package.json +27 -5
- package/src/WorkerTrigger.test.ts +44 -14
- package/src/WorkerTrigger.ts +299 -27
- package/src/adapters/InMemoryAdapter.ts +9 -5
- package/src/adapters/KafkaAdapter.ts +277 -0
- package/src/adapters/NATSAdapter.ts +454 -0
- package/src/adapters/PgBossAdapter.ts +293 -0
- package/src/adapters/RabbitMQAdapter.ts +285 -0
- package/src/adapters/RedisStreamsAdapter.ts +286 -0
- package/src/adapters/SQSAdapter.ts +306 -0
- package/src/adapters/factory.test.ts +89 -0
- package/src/adapters/factory.ts +111 -0
- package/src/adapters/new-adapters.test.ts +130 -0
- package/src/index.ts +31 -4
- package/template/.env.example +13 -0
- package/template/package.json +45 -0
- package/template/src/Nodes.ts +10 -0
- package/template/src/Workflows.ts +8 -0
- package/template/src/index.ts +41 -0
- package/template/src/runner/WorkerServer.ts +34 -0
- package/template/src/runner/types/Workflows.ts +7 -0
- package/template/src/workflows/jobs/process-job.ts +47 -0
- package/template/tsconfig.json +31 -0
- package/template/vitest.config.ts +39 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KafkaAdapter — v0.7 PR 5 — Worker adapter backed by Apache Kafka via
|
|
3
|
+
* `kafkajs`. Consumes from a topic (the `queue` field) with a
|
|
4
|
+
* consumer-group identifier; produces via the same client.
|
|
5
|
+
*
|
|
6
|
+
* Kafka is fundamentally a streaming platform — not a queue — so a
|
|
7
|
+
* few semantics differ from BullMQ/SQS/RabbitMQ:
|
|
8
|
+
*
|
|
9
|
+
* - **Ordering**: per-partition, not per-topic. Set the partition
|
|
10
|
+
* key via the `dedupId` field on `addJob` to keep related
|
|
11
|
+
* messages on the same partition.
|
|
12
|
+
* - **Retries**: Kafka doesn't have a broker-side retry concept.
|
|
13
|
+
* The adapter re-throws on handler failure; offset commit is
|
|
14
|
+
* suppressed so the consumer re-polls the message on the next
|
|
15
|
+
* cycle. For real retry semantics, layer a dead-letter topic.
|
|
16
|
+
* - **Stats**: KafkaJS exposes consumer-group lag via its admin
|
|
17
|
+
* client; the lag count is reported as `waiting`. Other stats
|
|
18
|
+
* are tracked locally per consumer.
|
|
19
|
+
*
|
|
20
|
+
* Requires `kafkajs` as a peer dependency:
|
|
21
|
+
*
|
|
22
|
+
* bun add kafkajs
|
|
23
|
+
*
|
|
24
|
+
* Environment variables (read at adapter construction):
|
|
25
|
+
* - `KAFKA_BROKERS` — comma-separated list (default `localhost:9092`).
|
|
26
|
+
* - `KAFKA_CLIENT_ID` — client.id (default `"blok-worker"`).
|
|
27
|
+
* - `KAFKA_SASL_USERNAME` — SASL/PLAIN username (optional).
|
|
28
|
+
* - `KAFKA_SASL_PASSWORD` — SASL/PLAIN password (optional).
|
|
29
|
+
* - `KAFKA_SSL` — when `"true"`, enable TLS.
|
|
30
|
+
*/
|
|
31
|
+
import { v4 as uuid } from "uuid";
|
|
32
|
+
export class KafkaAdapter {
|
|
33
|
+
provider = "kafka";
|
|
34
|
+
config;
|
|
35
|
+
// biome-ignore lint/suspicious/noExplicitAny: kafkajs's exported `Kafka` constructor is loosely typed.
|
|
36
|
+
kafka = null;
|
|
37
|
+
handle = { consumers: new Map() };
|
|
38
|
+
connected = false;
|
|
39
|
+
stats = new Map();
|
|
40
|
+
constructor(config) {
|
|
41
|
+
this.config = {
|
|
42
|
+
brokers: config?.brokers ?? (process.env.KAFKA_BROKERS ?? "localhost:9092").split(",").map((s) => s.trim()),
|
|
43
|
+
clientId: config?.clientId ?? process.env.KAFKA_CLIENT_ID ?? "blok-worker",
|
|
44
|
+
saslUsername: config?.saslUsername ?? process.env.KAFKA_SASL_USERNAME,
|
|
45
|
+
saslPassword: config?.saslPassword ?? process.env.KAFKA_SASL_PASSWORD,
|
|
46
|
+
ssl: config?.ssl ?? process.env.KAFKA_SSL === "true",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async connect() {
|
|
50
|
+
if (this.connected)
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
// biome-ignore lint/suspicious/noExplicitAny: kafkajs is a runtime-loaded peer dep.
|
|
54
|
+
const kafkajs = await import("kafkajs");
|
|
55
|
+
const sasl = this.config.saslUsername && this.config.saslPassword
|
|
56
|
+
? { mechanism: "plain", username: this.config.saslUsername, password: this.config.saslPassword }
|
|
57
|
+
: undefined;
|
|
58
|
+
this.kafka = new kafkajs.Kafka({
|
|
59
|
+
clientId: this.config.clientId,
|
|
60
|
+
brokers: this.config.brokers,
|
|
61
|
+
ssl: this.config.ssl,
|
|
62
|
+
sasl,
|
|
63
|
+
});
|
|
64
|
+
this.handle.producer = this.kafka.producer();
|
|
65
|
+
await this.handle.producer?.connect();
|
|
66
|
+
this.handle.admin = this.kafka.admin();
|
|
67
|
+
await this.handle.admin?.connect();
|
|
68
|
+
this.connected = true;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
throw new Error(`[blok][kafka] connect failed: ${err.message}. Install kafkajs as a peer dependency: bun add kafkajs`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async disconnect() {
|
|
75
|
+
if (!this.connected)
|
|
76
|
+
return;
|
|
77
|
+
for (const [, consumer] of this.handle.consumers) {
|
|
78
|
+
try {
|
|
79
|
+
await consumer.disconnect();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* ignore */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
this.handle.consumers.clear();
|
|
86
|
+
try {
|
|
87
|
+
await this.handle.producer?.disconnect();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* ignore */
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await this.handle.admin?.disconnect();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* ignore */
|
|
97
|
+
}
|
|
98
|
+
this.connected = false;
|
|
99
|
+
}
|
|
100
|
+
async process(config, handler) {
|
|
101
|
+
if (!this.connected)
|
|
102
|
+
throw new Error("[blok][kafka] not connected. Call connect() first.");
|
|
103
|
+
const groupId = config.consumerGroup ?? `${config.queue}-group`;
|
|
104
|
+
const consumer = this.kafka.consumer({ groupId });
|
|
105
|
+
await consumer.connect();
|
|
106
|
+
await consumer.subscribe({ topic: config.queue, fromBeginning: config.fromBeginning === true });
|
|
107
|
+
this.handle.consumers.set(config.queue, consumer);
|
|
108
|
+
this.stats.set(config.queue, { completed: 0, failed: 0, active: 0 });
|
|
109
|
+
const stats = this.stats.get(config.queue);
|
|
110
|
+
await consumer.run({
|
|
111
|
+
autoCommit: config.ack !== false,
|
|
112
|
+
eachMessage: async ({ message, }) => {
|
|
113
|
+
const payloadString = message.value?.toString("utf8") ?? "";
|
|
114
|
+
let data;
|
|
115
|
+
try {
|
|
116
|
+
data = payloadString.length > 0 ? JSON.parse(payloadString) : null;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
data = payloadString;
|
|
120
|
+
}
|
|
121
|
+
const headers = {};
|
|
122
|
+
if (message.headers) {
|
|
123
|
+
for (const [k, v] of Object.entries(message.headers))
|
|
124
|
+
headers[k] = v?.toString("utf8") ?? "";
|
|
125
|
+
}
|
|
126
|
+
const job = {
|
|
127
|
+
id: message.key?.toString("utf8") ?? `${config.queue}:${message.offset}`,
|
|
128
|
+
data,
|
|
129
|
+
headers,
|
|
130
|
+
queue: config.queue,
|
|
131
|
+
priority: config.priority ?? 0,
|
|
132
|
+
attempts: 0,
|
|
133
|
+
maxRetries: config.retries ?? 0,
|
|
134
|
+
createdAt: new Date(Number.parseInt(message.timestamp, 10)),
|
|
135
|
+
timeout: config.timeout,
|
|
136
|
+
raw: message,
|
|
137
|
+
complete: async () => {
|
|
138
|
+
stats.completed += 1;
|
|
139
|
+
},
|
|
140
|
+
fail: async (_err) => {
|
|
141
|
+
stats.failed += 1;
|
|
142
|
+
throw _err;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
stats.active += 1;
|
|
146
|
+
try {
|
|
147
|
+
await handler(job);
|
|
148
|
+
stats.completed += 1;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
stats.failed += 1;
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
stats.active = Math.max(0, stats.active - 1);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async addJob(queue, data, opts) {
|
|
161
|
+
if (!this.connected)
|
|
162
|
+
throw new Error("[blok][kafka] not connected. Call connect() first.");
|
|
163
|
+
if (!this.handle.producer)
|
|
164
|
+
throw new Error("[blok][kafka] producer not initialized");
|
|
165
|
+
const key = opts?.jobId ?? uuid();
|
|
166
|
+
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
167
|
+
await this.handle.producer.send({
|
|
168
|
+
topic: queue,
|
|
169
|
+
messages: [
|
|
170
|
+
{
|
|
171
|
+
key,
|
|
172
|
+
value: payload,
|
|
173
|
+
headers: opts?.delay ? { "x-blok-delay-ms": String(opts.delay) } : undefined,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
return key;
|
|
178
|
+
}
|
|
179
|
+
async stopProcessing(queue) {
|
|
180
|
+
const consumer = this.handle.consumers.get(queue);
|
|
181
|
+
if (consumer) {
|
|
182
|
+
try {
|
|
183
|
+
await consumer.stop();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
/* ignore */
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
await consumer.disconnect();
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
/* ignore */
|
|
193
|
+
}
|
|
194
|
+
this.handle.consumers.delete(queue);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
isConnected() {
|
|
198
|
+
return this.connected;
|
|
199
|
+
}
|
|
200
|
+
async healthCheck() {
|
|
201
|
+
if (!this.connected || !this.handle.admin)
|
|
202
|
+
return false;
|
|
203
|
+
try {
|
|
204
|
+
await this.handle.admin.fetchTopicOffsets("__consumer_offsets");
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async getQueueStats(queue) {
|
|
212
|
+
const counters = this.stats.get(queue) ?? { completed: 0, failed: 0, active: 0 };
|
|
213
|
+
let waiting = 0;
|
|
214
|
+
if (this.handle.admin) {
|
|
215
|
+
try {
|
|
216
|
+
const offsets = await this.handle.admin.fetchTopicOffsets(queue);
|
|
217
|
+
// Approximate: total committed offsets across partitions. Real lag
|
|
218
|
+
// requires admin.fetchOffsets({ groupId }) — skipped here to keep
|
|
219
|
+
// the call cheap; production deployments should use Kafka's
|
|
220
|
+
// dedicated lag metrics anyway.
|
|
221
|
+
waiting = offsets.reduce((sum, p) => sum + Number.parseInt(p.offset, 10), 0);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
waiting = 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
waiting,
|
|
229
|
+
active: counters.active,
|
|
230
|
+
completed: counters.completed,
|
|
231
|
+
failed: counters.failed,
|
|
232
|
+
delayed: 0,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiS2Fma2FBZGFwdGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2FkYXB0ZXJzL0thZmthQWRhcHRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0E2Qkc7QUFHSCxPQUFPLEVBQUUsRUFBRSxJQUFJLElBQUksRUFBRSxNQUFNLE1BQU0sQ0FBQztBQXNDbEMsTUFBTSxPQUFPLFlBQVk7SUFDZixRQUFRLEdBQUcsT0FBZ0IsQ0FBQztJQUNwQixNQUFNLENBQWM7SUFDckMsdUdBQXVHO0lBQy9GLEtBQUssR0FBUSxJQUFJLENBQUM7SUFDbEIsTUFBTSxHQUFrQixFQUFFLFNBQVMsRUFBRSxJQUFJLEdBQUcsRUFBRSxFQUFFLENBQUM7SUFDakQsU0FBUyxHQUFHLEtBQUssQ0FBQztJQUNsQixLQUFLLEdBQW9DLElBQUksR0FBRyxFQUFFLENBQUM7SUFFM0QsWUFBWSxNQUE2QjtRQUN4QyxJQUFJLENBQUMsTUFBTSxHQUFHO1lBQ2IsT0FBTyxFQUFFLE1BQU0sRUFBRSxPQUFPLElBQUksQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLGFBQWEsSUFBSSxnQkFBZ0IsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUMzRyxRQUFRLEVBQUUsTUFBTSxFQUFFLFFBQVEsSUFBSSxPQUFPLENBQUMsR0FBRyxDQUFDLGVBQWUsSUFBSSxhQUFhO1lBQzFFLFlBQVksRUFBRSxNQUFNLEVBQUUsWUFBWSxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsbUJBQW1CO1lBQ3JFLFlBQVksRUFBRSxNQUFNLEVBQUUsWUFBWSxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsbUJBQW1CO1lBQ3JFLEdBQUcsRUFBRSxNQUFNLEVBQUUsR0FBRyxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsU0FBUyxLQUFLLE1BQU07U0FDcEQsQ0FBQztJQUNILENBQUM7SUFFRCxLQUFLLENBQUMsT0FBTztRQUNaLElBQUksSUFBSSxDQUFDLFNBQVM7WUFBRSxPQUFPO1FBQzNCLElBQUksQ0FBQztZQUNKLG9GQUFvRjtZQUNwRixNQUFNLE9BQU8sR0FBUSxNQUFNLE1BQU0sQ0FBQyxTQUFTLENBQUMsQ0FBQztZQUM3QyxNQUFNLElBQUksR0FDVCxJQUFJLENBQUMsTUFBTSxDQUFDLFlBQVksSUFBSSxJQUFJLENBQUMsTUFBTSxDQUFDLFlBQVk7Z0JBQ25ELENBQUMsQ0FBQyxFQUFFLFNBQVMsRUFBRSxPQUFPLEVBQUUsUUFBUSxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsWUFBWSxFQUFFLFFBQVEsRUFBRSxJQUFJLENBQUMsTUFBTSxDQUFDLFlBQVksRUFBRTtnQkFDaEcsQ0FBQyxDQUFDLFNBQVMsQ0FBQztZQUNkLElBQUksQ0FBQyxLQUFLLEdBQUcsSUFBSSxPQUFPLENBQUMsS0FBSyxDQUFDO2dCQUM5QixRQUFRLEVBQUUsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRO2dCQUM5QixPQUFPLEVBQUUsSUFBSSxDQUFDLE1BQU0sQ0FBQyxPQUFPO2dCQUM1QixHQUFHLEVBQUUsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHO2dCQUNwQixJQUFJO2FBQ0osQ0FBQyxDQUFDO1lBQ0gsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUM3QyxNQUFNLElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxFQUFFLE9BQU8sRUFBRSxDQUFDO1lBQ3RDLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsS0FBSyxFQUFFLENBQUM7WUFDdkMsTUFBTSxJQUFJLENBQUMsTUFBTSxDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsQ0FBQztZQUNuQyxJQUFJLENBQUMsU0FBUyxHQUFHLElBQUksQ0FBQztRQUN2QixDQUFDO1FBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztZQUNkLE1BQU0sSUFBSSxLQUFLLENBQ2QsaUNBQWtDLEdBQWEsQ0FBQyxPQUFPLHlEQUF5RCxDQUNoSCxDQUFDO1FBQ0gsQ0FBQztJQUNGLENBQUM7SUFFRCxLQUFLLENBQUMsVUFBVTtRQUNmLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUztZQUFFLE9BQU87UUFDNUIsS0FBSyxNQUFNLENBQUMsRUFBRSxRQUFRLENBQUMsSUFBSSxJQUFJLENBQUMsTUFBTSxDQUFDLFNBQVMsRUFBRSxDQUFDO1lBQ2xELElBQUksQ0FBQztnQkFDSixNQUFNLFFBQVEsQ0FBQyxVQUFVLEVBQUUsQ0FBQztZQUM3QixDQUFDO1lBQUMsTUFBTSxDQUFDO2dCQUNSLFlBQVk7WUFDYixDQUFDO1FBQ0YsQ0FBQztRQUNELElBQUksQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQzlCLElBQUksQ0FBQztZQUNKLE1BQU0sSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLEVBQUUsVUFBVSxFQUFFLENBQUM7UUFDMUMsQ0FBQztRQUFDLE1BQU0sQ0FBQztZQUNSLFlBQVk7UUFDYixDQUFDO1FBQ0QsSUFBSSxDQUFDO1lBQ0osTUFBTSxJQUFJLENBQUMsTUFBTSxDQUFDLEtBQUssRUFBRSxVQUFVLEVBQUUsQ0FBQztRQUN2QyxDQUFDO1FBQUMsTUFBTSxDQUFDO1lBQ1IsWUFBWTtRQUNiLENBQUM7UUFDRCxJQUFJLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQztJQUN4QixDQUFDO0lBRUQsS0FBSyxDQUFDLE9BQU8sQ0FBQyxNQUF5QixFQUFFLE9BQTBDO1FBQ2xGLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUztZQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsb0RBQW9ELENBQUMsQ0FBQztRQUMzRixNQUFNLE9BQU8sR0FBRyxNQUFNLENBQUMsYUFBYSxJQUFJLEdBQUcsTUFBTSxDQUFDLEtBQUssUUFBUSxDQUFDO1FBQ2hFLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsUUFBUSxDQUFDLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQztRQUNsRCxNQUFNLFFBQVEsQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUN6QixNQUFNLFFBQVEsQ0FBQyxTQUFTLENBQUMsRUFBRSxLQUFLLEVBQUUsTUFBTSxDQUFDLEtBQUssRUFBRSxhQUFhLEVBQUUsTUFBTSxDQUFDLGFBQWEsS0FBSyxJQUFJLEVBQUUsQ0FBQyxDQUFDO1FBQ2hHLElBQUksQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsS0FBSyxFQUFFLFFBQVEsQ0FBQyxDQUFDO1FBQ2xELElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsRUFBRSxTQUFTLEVBQUUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDckUsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBdUIsQ0FBQztRQUVqRSxNQUFNLFFBQVEsQ0FBQyxHQUFHLENBQUM7WUFDbEIsVUFBVSxFQUFFLE1BQU0sQ0FBQyxHQUFHLEtBQUssS0FBSztZQUNoQyxXQUFXLEVBQUUsS0FBSyxFQUFFLEVBQ25CLE9BQU8sR0FHUCxFQUFFLEVBQUU7Z0JBQ0osTUFBTSxhQUFhLEdBQUcsT0FBTyxDQUFDLEtBQUssRUFBRSxRQUFRLENBQUMsTUFBTSxDQUFDLElBQUksRUFBRSxDQUFDO2dCQUM1RCxJQUFJLElBQWEsQ0FBQztnQkFDbEIsSUFBSSxDQUFDO29CQUNKLElBQUksR0FBRyxhQUFhLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxhQUFhLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO2dCQUNwRSxDQUFDO2dCQUFDLE1BQU0sQ0FBQztvQkFDUixJQUFJLEdBQUcsYUFBYSxDQUFDO2dCQUN0QixDQUFDO2dCQUNELE1BQU0sT0FBTyxHQUEyQixFQUFFLENBQUM7Z0JBQzNDLElBQUksT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDO29CQUNyQixLQUFLLE1BQU0sQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLElBQUksTUFBTSxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDO3dCQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLEVBQUUsUUFBUSxDQUFDLE1BQU0sQ0FBQyxJQUFJLEVBQUUsQ0FBQztnQkFDOUYsQ0FBQztnQkFDRCxNQUFNLEdBQUcsR0FBYztvQkFDdEIsRUFBRSxFQUFFLE9BQU8sQ0FBQyxHQUFHLEVBQUUsUUFBUSxDQUFDLE1BQU0sQ0FBQyxJQUFJLEdBQUcsTUFBTSxDQUFDLEtBQUssSUFBSSxPQUFPLENBQUMsTUFBTSxFQUFFO29CQUN4RSxJQUFJO29CQUNKLE9BQU87b0JBQ1AsS0FBSyxFQUFFLE1BQU0sQ0FBQyxLQUFLO29CQUNuQixRQUFRLEVBQUUsTUFBTSxDQUFDLFFBQVEsSUFBSSxDQUFDO29CQUM5QixRQUFRLEVBQUUsQ0FBQztvQkFDWCxVQUFVLEVBQUUsTUFBTSxDQUFDLE9BQU8sSUFBSSxDQUFDO29CQUMvQixTQUFTLEVBQUUsSUFBSSxJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsU0FBUyxFQUFFLEVBQUUsQ0FBQyxDQUFDO29CQUMzRCxPQUFPLEVBQUUsTUFBTSxDQUFDLE9BQU87b0JBQ3ZCLEdBQUcsRUFBRSxPQUFPO29CQUNaLFFBQVEsRUFBRSxLQUFLLElBQUksRUFBRTt3QkFDcEIsS0FBSyxDQUFDLFNBQVMsSUFBSSxDQUFDLENBQUM7b0JBQ3RCLENBQUM7b0JBQ0QsSUFBSSxFQUFFLEtBQUssRUFBRSxJQUFXLEVBQUUsRUFBRTt3QkFDM0IsS0FBSyxDQUFDLE1BQU0sSUFBSSxDQUFDLENBQUM7d0JBQ2xCLE1BQU0sSUFBSSxDQUFDO29CQUNaLENBQUM7aUJBQ0QsQ0FBQztnQkFDRixLQUFLLENBQUMsTUFBTSxJQUFJLENBQUMsQ0FBQztnQkFDbEIsSUFBSSxDQUFDO29CQUNKLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDO29CQUNuQixLQUFLLENBQUMsU0FBUyxJQUFJLENBQUMsQ0FBQztnQkFDdEIsQ0FBQztnQkFBQyxPQUFPLEdBQUcsRUFBRSxDQUFDO29CQUNkLEtBQUssQ0FBQyxNQUFNLElBQUksQ0FBQyxDQUFDO29CQUNsQixNQUFNLEdBQUcsQ0FBQztnQkFDWCxDQUFDO3dCQUFTLENBQUM7b0JBQ1YsS0FBSyxDQUFDLE1BQU0sR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxLQUFLLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxDQUFDO2dCQUM5QyxDQUFDO1lBQ0YsQ0FBQztTQUNELENBQUMsQ0FBQztJQUNKLENBQUM7SUFFRCxLQUFLLENBQUMsTUFBTSxDQUNYLEtBQWEsRUFDYixJQUFhLEVBQ2IsSUFBZ0c7UUFFaEcsSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTO1lBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxvREFBb0QsQ0FBQyxDQUFDO1FBQzNGLElBQUksQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVE7WUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLHdDQUF3QyxDQUFDLENBQUM7UUFDckYsTUFBTSxHQUFHLEdBQUcsSUFBSSxFQUFFLEtBQUssSUFBSSxJQUFJLEVBQUUsQ0FBQztRQUNsQyxNQUFNLE9BQU8sR0FBRyxPQUFPLElBQUksS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUN2RSxNQUFNLElBQUksQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQztZQUMvQixLQUFLLEVBQUUsS0FBSztZQUNaLFFBQVEsRUFBRTtnQkFDVDtvQkFDQyxHQUFHO29CQUNILEtBQUssRUFBRSxPQUFPO29CQUNkLE9BQU8sRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsU0FBUztpQkFDNUU7YUFDRDtTQUNELENBQUMsQ0FBQztRQUNILE9BQU8sR0FBRyxDQUFDO0lBQ1osQ0FBQztJQUVELEtBQUssQ0FBQyxjQUFjLENBQUMsS0FBYTtRQUNqQyxNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDbEQsSUFBSSxRQUFRLEVBQUUsQ0FBQztZQUNkLElBQUksQ0FBQztnQkFDSixNQUFNLFFBQVEsQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUN2QixDQUFDO1lBQUMsTUFBTSxDQUFDO2dCQUNSLFlBQVk7WUFDYixDQUFDO1lBQ0QsSUFBSSxDQUFDO2dCQUNKLE1BQU0sUUFBUSxDQUFDLFVBQVUsRUFBRSxDQUFDO1lBQzdCLENBQUM7WUFBQyxNQUFNLENBQUM7Z0JBQ1IsWUFBWTtZQUNiLENBQUM7WUFDRCxJQUFJLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDckMsQ0FBQztJQUNGLENBQUM7SUFFRCxXQUFXO1FBQ1YsT0FBTyxJQUFJLENBQUMsU0FBUyxDQUFDO0lBQ3ZCLENBQUM7SUFFRCxLQUFLLENBQUMsV0FBVztRQUNoQixJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsSUFBSSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSztZQUFFLE9BQU8sS0FBSyxDQUFDO1FBQ3hELElBQUksQ0FBQztZQUNKLE1BQU0sSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsaUJBQWlCLENBQUMsb0JBQW9CLENBQUMsQ0FBQztZQUNoRSxPQUFPLElBQUksQ0FBQztRQUNiLENBQUM7UUFBQyxNQUFNLENBQUM7WUFDUixPQUFPLEtBQUssQ0FBQztRQUNkLENBQUM7SUFDRixDQUFDO0lBRUQsS0FBSyxDQUFDLGFBQWEsQ0FBQyxLQUFhO1FBQ2hDLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsU0FBUyxFQUFFLENBQUMsRUFBRSxNQUFNLEVBQUUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDLEVBQUUsQ0FBQztRQUNqRixJQUFJLE9BQU8sR0FBRyxDQUFDLENBQUM7UUFDaEIsSUFBSSxJQUFJLENBQUMsTUFBTSxDQUFDLEtBQUssRUFBRSxDQUFDO1lBQ3ZCLElBQUksQ0FBQztnQkFDSixNQUFNLE9BQU8sR0FBRyxNQUFNLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLGlCQUFpQixDQUFDLEtBQUssQ0FBQyxDQUFDO2dCQUNqRSxtRUFBbUU7Z0JBQ25FLGtFQUFrRTtnQkFDbEUsNERBQTREO2dCQUM1RCxnQ0FBZ0M7Z0JBQ2hDLE9BQU8sR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUMsR0FBRyxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsR0FBRyxHQUFHLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztZQUM5RSxDQUFDO1lBQUMsTUFBTSxDQUFDO2dCQUNSLE9BQU8sR0FBRyxDQUFDLENBQUM7WUFDYixDQUFDO1FBQ0YsQ0FBQztRQUNELE9BQU87WUFDTixPQUFPO1lBQ1AsTUFBTSxFQUFFLFFBQVEsQ0FBQyxNQUFNO1lBQ3ZCLFNBQVMsRUFBRSxRQUFRLENBQUMsU0FBUztZQUM3QixNQUFNLEVBQUUsUUFBUSxDQUFDLE1BQU07WUFDdkIsT0FBTyxFQUFFLENBQUM7U0FDVixDQUFDO0lBQ0gsQ0FBQztDQUNEIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBLYWZrYUFkYXB0ZXIg4oCUIHYwLjcgUFIgNSDigJQgV29ya2VyIGFkYXB0ZXIgYmFja2VkIGJ5IEFwYWNoZSBLYWZrYSB2aWFcbiAqIGBrYWZrYWpzYC4gQ29uc3VtZXMgZnJvbSBhIHRvcGljICh0aGUgYHF1ZXVlYCBmaWVsZCkgd2l0aCBhXG4gKiBjb25zdW1lci1ncm91cCBpZGVudGlmaWVyOyBwcm9kdWNlcyB2aWEgdGhlIHNhbWUgY2xpZW50LlxuICpcbiAqIEthZmthIGlzIGZ1bmRhbWVudGFsbHkgYSBzdHJlYW1pbmcgcGxhdGZvcm0g4oCUIG5vdCBhIHF1ZXVlIOKAlCBzbyBhXG4gKiBmZXcgc2VtYW50aWNzIGRpZmZlciBmcm9tIEJ1bGxNUS9TUVMvUmFiYml0TVE6XG4gKlxuICogICAtICoqT3JkZXJpbmcqKjogcGVyLXBhcnRpdGlvbiwgbm90IHBlci10b3BpYy4gU2V0IHRoZSBwYXJ0aXRpb25cbiAqICAgICBrZXkgdmlhIHRoZSBgZGVkdXBJZGAgZmllbGQgb24gYGFkZEpvYmAgdG8ga2VlcCByZWxhdGVkXG4gKiAgICAgbWVzc2FnZXMgb24gdGhlIHNhbWUgcGFydGl0aW9uLlxuICogICAtICoqUmV0cmllcyoqOiBLYWZrYSBkb2Vzbid0IGhhdmUgYSBicm9rZXItc2lkZSByZXRyeSBjb25jZXB0LlxuICogICAgIFRoZSBhZGFwdGVyIHJlLXRocm93cyBvbiBoYW5kbGVyIGZhaWx1cmU7IG9mZnNldCBjb21taXQgaXNcbiAqICAgICBzdXBwcmVzc2VkIHNvIHRoZSBjb25zdW1lciByZS1wb2xscyB0aGUgbWVzc2FnZSBvbiB0aGUgbmV4dFxuICogICAgIGN5Y2xlLiBGb3IgcmVhbCByZXRyeSBzZW1hbnRpY3MsIGxheWVyIGEgZGVhZC1sZXR0ZXIgdG9waWMuXG4gKiAgIC0gKipTdGF0cyoqOiBLYWZrYUpTIGV4cG9zZXMgY29uc3VtZXItZ3JvdXAgbGFnIHZpYSBpdHMgYWRtaW5cbiAqICAgICBjbGllbnQ7IHRoZSBsYWcgY291bnQgaXMgcmVwb3J0ZWQgYXMgYHdhaXRpbmdgLiBPdGhlciBzdGF0c1xuICogICAgIGFyZSB0cmFja2VkIGxvY2FsbHkgcGVyIGNvbnN1bWVyLlxuICpcbiAqIFJlcXVpcmVzIGBrYWZrYWpzYCBhcyBhIHBlZXIgZGVwZW5kZW5jeTpcbiAqXG4gKiAgICAgYnVuIGFkZCBrYWZrYWpzXG4gKlxuICogRW52aXJvbm1lbnQgdmFyaWFibGVzIChyZWFkIGF0IGFkYXB0ZXIgY29uc3RydWN0aW9uKTpcbiAqICAgLSBgS0FGS0FfQlJPS0VSU2AgICAgICAgICAgIOKAlCBjb21tYS1zZXBhcmF0ZWQgbGlzdCAoZGVmYXVsdCBgbG9jYWxob3N0OjkwOTJgKS5cbiAqICAgLSBgS0FGS0FfQ0xJRU5UX0lEYCAgICAgICAgIOKAlCBjbGllbnQuaWQgKGRlZmF1bHQgYFwiYmxvay13b3JrZXJcImApLlxuICogICAtIGBLQUZLQV9TQVNMX1VTRVJOQU1FYCAgICAg4oCUIFNBU0wvUExBSU4gdXNlcm5hbWUgKG9wdGlvbmFsKS5cbiAqICAgLSBgS0FGS0FfU0FTTF9QQVNTV09SRGAgICAgIOKAlCBTQVNML1BMQUlOIHBhc3N3b3JkIChvcHRpb25hbCkuXG4gKiAgIC0gYEtBRktBX1NTTGAgICAgICAgICAgICAgICDigJQgd2hlbiBgXCJ0cnVlXCJgLCBlbmFibGUgVExTLlxuICovXG5cbmltcG9ydCB0eXBlIHsgV29ya2VyVHJpZ2dlck9wdHMgfSBmcm9tIFwiQGJsb2tqcy9oZWxwZXJcIjtcbmltcG9ydCB7IHY0IGFzIHV1aWQgfSBmcm9tIFwidXVpZFwiO1xuaW1wb3J0IHR5cGUgeyBXb3JrZXJBZGFwdGVyLCBXb3JrZXJKb2IsIFdvcmtlclF1ZXVlU3RhdHMgfSBmcm9tIFwiLi4vV29ya2VyVHJpZ2dlclwiO1xuXG5leHBvcnQgaW50ZXJmYWNlIEthZmthQ29uZmlnIHtcblx0YnJva2Vyczogc3RyaW5nW107XG5cdGNsaWVudElkOiBzdHJpbmc7XG5cdHNhc2xVc2VybmFtZT86IHN0cmluZztcblx0c2FzbFBhc3N3b3JkPzogc3RyaW5nO1xuXHRzc2w6IGJvb2xlYW47XG59XG5cbmludGVyZmFjZSBLYWZrYUpzSGFuZGxlIHtcblx0cHJvZHVjZXI/OiB7XG5cdFx0Y29ubmVjdDogKCkgPT4gUHJvbWlzZTx2b2lkPjtcblx0XHRkaXNjb25uZWN0OiAoKSA9PiBQcm9taXNlPHZvaWQ+O1xuXHRcdHNlbmQ6IChhcmdzOiB1bmtub3duKSA9PiBQcm9taXNlPHVua25vd24+O1xuXHR9O1xuXHRjb25zdW1lcnM6IE1hcDxcblx0XHRzdHJpbmcsXG5cdFx0e1xuXHRcdFx0ZGlzY29ubmVjdDogKCkgPT4gUHJvbWlzZTx2b2lkPjtcblx0XHRcdHN0b3A6ICgpID0+IFByb21pc2U8dm9pZD47XG5cdFx0XHRydW46IChvcHRzOiB1bmtub3duKSA9PiBQcm9taXNlPHZvaWQ+O1xuXHRcdH1cblx0Pjtcblx0YWRtaW4/OiB7XG5cdFx0Y29ubmVjdDogKCkgPT4gUHJvbWlzZTx2b2lkPjtcblx0XHRkaXNjb25uZWN0OiAoKSA9PiBQcm9taXNlPHZvaWQ+O1xuXHRcdGZldGNoVG9waWNPZmZzZXRzOiAodG9waWM6IHN0cmluZykgPT4gUHJvbWlzZTxBcnJheTx7IHBhcnRpdGlvbjogbnVtYmVyOyBvZmZzZXQ6IHN0cmluZyB9Pj47XG5cdH07XG59XG5cbmludGVyZmFjZSBRdWV1ZVN0YXRzQ291bnRlcnMge1xuXHRjb21wbGV0ZWQ6IG51bWJlcjtcblx0ZmFpbGVkOiBudW1iZXI7XG5cdGFjdGl2ZTogbnVtYmVyO1xufVxuXG5leHBvcnQgY2xhc3MgS2Fma2FBZGFwdGVyIGltcGxlbWVudHMgV29ya2VyQWRhcHRlciB7XG5cdHJlYWRvbmx5IHByb3ZpZGVyID0gXCJrYWZrYVwiIGFzIGNvbnN0O1xuXHRwcml2YXRlIHJlYWRvbmx5IGNvbmZpZzogS2Fma2FDb25maWc7XG5cdC8vIGJpb21lLWlnbm9yZSBsaW50L3N1c3BpY2lvdXMvbm9FeHBsaWNpdEFueToga2Fma2FqcydzIGV4cG9ydGVkIGBLYWZrYWAgY29uc3RydWN0b3IgaXMgbG9vc2VseSB0eXBlZC5cblx0cHJpdmF0ZSBrYWZrYTogYW55ID0gbnVsbDtcblx0cHJpdmF0ZSBoYW5kbGU6IEthZmthSnNIYW5kbGUgPSB7IGNvbnN1bWVyczogbmV3IE1hcCgpIH07XG5cdHByaXZhdGUgY29ubmVjdGVkID0gZmFsc2U7XG5cdHByaXZhdGUgc3RhdHM6IE1hcDxzdHJpbmcsIFF1ZXVlU3RhdHNDb3VudGVycz4gPSBuZXcgTWFwKCk7XG5cblx0Y29uc3RydWN0b3IoY29uZmlnPzogUGFydGlhbDxLYWZrYUNvbmZpZz4pIHtcblx0XHR0aGlzLmNvbmZpZyA9IHtcblx0XHRcdGJyb2tlcnM6IGNvbmZpZz8uYnJva2VycyA/PyAocHJvY2Vzcy5lbnYuS0FGS0FfQlJPS0VSUyA/PyBcImxvY2FsaG9zdDo5MDkyXCIpLnNwbGl0KFwiLFwiKS5tYXAoKHMpID0+IHMudHJpbSgpKSxcblx0XHRcdGNsaWVudElkOiBjb25maWc/LmNsaWVudElkID8/IHByb2Nlc3MuZW52LktBRktBX0NMSUVOVF9JRCA/PyBcImJsb2std29ya2VyXCIsXG5cdFx0XHRzYXNsVXNlcm5hbWU6IGNvbmZpZz8uc2FzbFVzZXJuYW1lID8/IHByb2Nlc3MuZW52LktBRktBX1NBU0xfVVNFUk5BTUUsXG5cdFx0XHRzYXNsUGFzc3dvcmQ6IGNvbmZpZz8uc2FzbFBhc3N3b3JkID8/IHByb2Nlc3MuZW52LktBRktBX1NBU0xfUEFTU1dPUkQsXG5cdFx0XHRzc2w6IGNvbmZpZz8uc3NsID8/IHByb2Nlc3MuZW52LktBRktBX1NTTCA9PT0gXCJ0cnVlXCIsXG5cdFx0fTtcblx0fVxuXG5cdGFzeW5jIGNvbm5lY3QoKTogUHJvbWlzZTx2b2lkPiB7XG5cdFx0aWYgKHRoaXMuY29ubmVjdGVkKSByZXR1cm47XG5cdFx0dHJ5IHtcblx0XHRcdC8vIGJpb21lLWlnbm9yZSBsaW50L3N1c3BpY2lvdXMvbm9FeHBsaWNpdEFueToga2Fma2FqcyBpcyBhIHJ1bnRpbWUtbG9hZGVkIHBlZXIgZGVwLlxuXHRcdFx0Y29uc3Qga2Fma2FqczogYW55ID0gYXdhaXQgaW1wb3J0KFwia2Fma2Fqc1wiKTtcblx0XHRcdGNvbnN0IHNhc2wgPVxuXHRcdFx0XHR0aGlzLmNvbmZpZy5zYXNsVXNlcm5hbWUgJiYgdGhpcy5jb25maWcuc2FzbFBhc3N3b3JkXG5cdFx0XHRcdFx0PyB7IG1lY2hhbmlzbTogXCJwbGFpblwiLCB1c2VybmFtZTogdGhpcy5jb25maWcuc2FzbFVzZXJuYW1lLCBwYXNzd29yZDogdGhpcy5jb25maWcuc2FzbFBhc3N3b3JkIH1cblx0XHRcdFx0XHQ6IHVuZGVmaW5lZDtcblx0XHRcdHRoaXMua2Fma2EgPSBuZXcga2Fma2Fqcy5LYWZrYSh7XG5cdFx0XHRcdGNsaWVudElkOiB0aGlzLmNvbmZpZy5jbGllbnRJZCxcblx0XHRcdFx0YnJva2VyczogdGhpcy5jb25maWcuYnJva2Vycyxcblx0XHRcdFx0c3NsOiB0aGlzLmNvbmZpZy5zc2wsXG5cdFx0XHRcdHNhc2wsXG5cdFx0XHR9KTtcblx0XHRcdHRoaXMuaGFuZGxlLnByb2R1Y2VyID0gdGhpcy5rYWZrYS5wcm9kdWNlcigpO1xuXHRcdFx0YXdhaXQgdGhpcy5oYW5kbGUucHJvZHVjZXI/LmNvbm5lY3QoKTtcblx0XHRcdHRoaXMuaGFuZGxlLmFkbWluID0gdGhpcy5rYWZrYS5hZG1pbigpO1xuXHRcdFx0YXdhaXQgdGhpcy5oYW5kbGUuYWRtaW4/LmNvbm5lY3QoKTtcblx0XHRcdHRoaXMuY29ubmVjdGVkID0gdHJ1ZTtcblx0XHR9IGNhdGNoIChlcnIpIHtcblx0XHRcdHRocm93IG5ldyBFcnJvcihcblx0XHRcdFx0YFtibG9rXVtrYWZrYV0gY29ubmVjdCBmYWlsZWQ6ICR7KGVyciBhcyBFcnJvcikubWVzc2FnZX0uIEluc3RhbGwga2Fma2FqcyBhcyBhIHBlZXIgZGVwZW5kZW5jeTogYnVuIGFkZCBrYWZrYWpzYCxcblx0XHRcdCk7XG5cdFx0fVxuXHR9XG5cblx0YXN5bmMgZGlzY29ubmVjdCgpOiBQcm9taXNlPHZvaWQ+IHtcblx0XHRpZiAoIXRoaXMuY29ubmVjdGVkKSByZXR1cm47XG5cdFx0Zm9yIChjb25zdCBbLCBjb25zdW1lcl0gb2YgdGhpcy5oYW5kbGUuY29uc3VtZXJzKSB7XG5cdFx0XHR0cnkge1xuXHRcdFx0XHRhd2FpdCBjb25zdW1lci5kaXNjb25uZWN0KCk7XG5cdFx0XHR9IGNhdGNoIHtcblx0XHRcdFx0LyogaWdub3JlICovXG5cdFx0XHR9XG5cdFx0fVxuXHRcdHRoaXMuaGFuZGxlLmNvbnN1bWVycy5jbGVhcigpO1xuXHRcdHRyeSB7XG5cdFx0XHRhd2FpdCB0aGlzLmhhbmRsZS5wcm9kdWNlcj8uZGlzY29ubmVjdCgpO1xuXHRcdH0gY2F0Y2gge1xuXHRcdFx0LyogaWdub3JlICovXG5cdFx0fVxuXHRcdHRyeSB7XG5cdFx0XHRhd2FpdCB0aGlzLmhhbmRsZS5hZG1pbj8uZGlzY29ubmVjdCgpO1xuXHRcdH0gY2F0Y2gge1xuXHRcdFx0LyogaWdub3JlICovXG5cdFx0fVxuXHRcdHRoaXMuY29ubmVjdGVkID0gZmFsc2U7XG5cdH1cblxuXHRhc3luYyBwcm9jZXNzKGNvbmZpZzogV29ya2VyVHJpZ2dlck9wdHMsIGhhbmRsZXI6IChqb2I6IFdvcmtlckpvYikgPT4gUHJvbWlzZTx2b2lkPik6IFByb21pc2U8dm9pZD4ge1xuXHRcdGlmICghdGhpcy5jb25uZWN0ZWQpIHRocm93IG5ldyBFcnJvcihcIltibG9rXVtrYWZrYV0gbm90IGNvbm5lY3RlZC4gQ2FsbCBjb25uZWN0KCkgZmlyc3QuXCIpO1xuXHRcdGNvbnN0IGdyb3VwSWQgPSBjb25maWcuY29uc3VtZXJHcm91cCA/PyBgJHtjb25maWcucXVldWV9LWdyb3VwYDtcblx0XHRjb25zdCBjb25zdW1lciA9IHRoaXMua2Fma2EuY29uc3VtZXIoeyBncm91cElkIH0pO1xuXHRcdGF3YWl0IGNvbnN1bWVyLmNvbm5lY3QoKTtcblx0XHRhd2FpdCBjb25zdW1lci5zdWJzY3JpYmUoeyB0b3BpYzogY29uZmlnLnF1ZXVlLCBmcm9tQmVnaW5uaW5nOiBjb25maWcuZnJvbUJlZ2lubmluZyA9PT0gdHJ1ZSB9KTtcblx0XHR0aGlzLmhhbmRsZS5jb25zdW1lcnMuc2V0KGNvbmZpZy5xdWV1ZSwgY29uc3VtZXIpO1xuXHRcdHRoaXMuc3RhdHMuc2V0KGNvbmZpZy5xdWV1ZSwgeyBjb21wbGV0ZWQ6IDAsIGZhaWxlZDogMCwgYWN0aXZlOiAwIH0pO1xuXHRcdGNvbnN0IHN0YXRzID0gdGhpcy5zdGF0cy5nZXQoY29uZmlnLnF1ZXVlKSBhcyBRdWV1ZVN0YXRzQ291bnRlcnM7XG5cblx0XHRhd2FpdCBjb25zdW1lci5ydW4oe1xuXHRcdFx0YXV0b0NvbW1pdDogY29uZmlnLmFjayAhPT0gZmFsc2UsXG5cdFx0XHRlYWNoTWVzc2FnZTogYXN5bmMgKHtcblx0XHRcdFx0bWVzc2FnZSxcblx0XHRcdH06IHtcblx0XHRcdFx0bWVzc2FnZTogeyBrZXk/OiBCdWZmZXI7IHZhbHVlPzogQnVmZmVyOyBvZmZzZXQ6IHN0cmluZzsgdGltZXN0YW1wOiBzdHJpbmc7IGhlYWRlcnM/OiBSZWNvcmQ8c3RyaW5nLCBCdWZmZXI+IH07XG5cdFx0XHR9KSA9PiB7XG5cdFx0XHRcdGNvbnN0IHBheWxvYWRTdHJpbmcgPSBtZXNzYWdlLnZhbHVlPy50b1N0cmluZyhcInV0ZjhcIikgPz8gXCJcIjtcblx0XHRcdFx0bGV0IGRhdGE6IHVua25vd247XG5cdFx0XHRcdHRyeSB7XG5cdFx0XHRcdFx0ZGF0YSA9IHBheWxvYWRTdHJpbmcubGVuZ3RoID4gMCA/IEpTT04ucGFyc2UocGF5bG9hZFN0cmluZykgOiBudWxsO1xuXHRcdFx0XHR9IGNhdGNoIHtcblx0XHRcdFx0XHRkYXRhID0gcGF5bG9hZFN0cmluZztcblx0XHRcdFx0fVxuXHRcdFx0XHRjb25zdCBoZWFkZXJzOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0ge307XG5cdFx0XHRcdGlmIChtZXNzYWdlLmhlYWRlcnMpIHtcblx0XHRcdFx0XHRmb3IgKGNvbnN0IFtrLCB2XSBvZiBPYmplY3QuZW50cmllcyhtZXNzYWdlLmhlYWRlcnMpKSBoZWFkZXJzW2tdID0gdj8udG9TdHJpbmcoXCJ1dGY4XCIpID8/IFwiXCI7XG5cdFx0XHRcdH1cblx0XHRcdFx0Y29uc3Qgam9iOiBXb3JrZXJKb2IgPSB7XG5cdFx0XHRcdFx0aWQ6IG1lc3NhZ2Uua2V5Py50b1N0cmluZyhcInV0ZjhcIikgPz8gYCR7Y29uZmlnLnF1ZXVlfToke21lc3NhZ2Uub2Zmc2V0fWAsXG5cdFx0XHRcdFx0ZGF0YSxcblx0XHRcdFx0XHRoZWFkZXJzLFxuXHRcdFx0XHRcdHF1ZXVlOiBjb25maWcucXVldWUsXG5cdFx0XHRcdFx0cHJpb3JpdHk6IGNvbmZpZy5wcmlvcml0eSA/PyAwLFxuXHRcdFx0XHRcdGF0dGVtcHRzOiAwLFxuXHRcdFx0XHRcdG1heFJldHJpZXM6IGNvbmZpZy5yZXRyaWVzID8/IDAsXG5cdFx0XHRcdFx0Y3JlYXRlZEF0OiBuZXcgRGF0ZShOdW1iZXIucGFyc2VJbnQobWVzc2FnZS50aW1lc3RhbXAsIDEwKSksXG5cdFx0XHRcdFx0dGltZW91dDogY29uZmlnLnRpbWVvdXQsXG5cdFx0XHRcdFx0cmF3OiBtZXNzYWdlLFxuXHRcdFx0XHRcdGNvbXBsZXRlOiBhc3luYyAoKSA9PiB7XG5cdFx0XHRcdFx0XHRzdGF0cy5jb21wbGV0ZWQgKz0gMTtcblx0XHRcdFx0XHR9LFxuXHRcdFx0XHRcdGZhaWw6IGFzeW5jIChfZXJyOiBFcnJvcikgPT4ge1xuXHRcdFx0XHRcdFx0c3RhdHMuZmFpbGVkICs9IDE7XG5cdFx0XHRcdFx0XHR0aHJvdyBfZXJyO1xuXHRcdFx0XHRcdH0sXG5cdFx0XHRcdH07XG5cdFx0XHRcdHN0YXRzLmFjdGl2ZSArPSAxO1xuXHRcdFx0XHR0cnkge1xuXHRcdFx0XHRcdGF3YWl0IGhhbmRsZXIoam9iKTtcblx0XHRcdFx0XHRzdGF0cy5jb21wbGV0ZWQgKz0gMTtcblx0XHRcdFx0fSBjYXRjaCAoZXJyKSB7XG5cdFx0XHRcdFx0c3RhdHMuZmFpbGVkICs9IDE7XG5cdFx0XHRcdFx0dGhyb3cgZXJyO1xuXHRcdFx0XHR9IGZpbmFsbHkge1xuXHRcdFx0XHRcdHN0YXRzLmFjdGl2ZSA9IE1hdGgubWF4KDAsIHN0YXRzLmFjdGl2ZSAtIDEpO1xuXHRcdFx0XHR9XG5cdFx0XHR9LFxuXHRcdH0pO1xuXHR9XG5cblx0YXN5bmMgYWRkSm9iKFxuXHRcdHF1ZXVlOiBzdHJpbmcsXG5cdFx0ZGF0YTogdW5rbm93bixcblx0XHRvcHRzPzogeyBwcmlvcml0eT86IG51bWJlcjsgZGVsYXk/OiBudW1iZXI7IHJldHJpZXM/OiBudW1iZXI7IHRpbWVvdXQ/OiBudW1iZXI7IGpvYklkPzogc3RyaW5nIH0sXG5cdCk6IFByb21pc2U8c3RyaW5nPiB7XG5cdFx0aWYgKCF0aGlzLmNvbm5lY3RlZCkgdGhyb3cgbmV3IEVycm9yKFwiW2Jsb2tdW2thZmthXSBub3QgY29ubmVjdGVkLiBDYWxsIGNvbm5lY3QoKSBmaXJzdC5cIik7XG5cdFx0aWYgKCF0aGlzLmhhbmRsZS5wcm9kdWNlcikgdGhyb3cgbmV3IEVycm9yKFwiW2Jsb2tdW2thZmthXSBwcm9kdWNlciBub3QgaW5pdGlhbGl6ZWRcIik7XG5cdFx0Y29uc3Qga2V5ID0gb3B0cz8uam9iSWQgPz8gdXVpZCgpO1xuXHRcdGNvbnN0IHBheWxvYWQgPSB0eXBlb2YgZGF0YSA9PT0gXCJzdHJpbmdcIiA/IGRhdGEgOiBKU09OLnN0cmluZ2lmeShkYXRhKTtcblx0XHRhd2FpdCB0aGlzLmhhbmRsZS5wcm9kdWNlci5zZW5kKHtcblx0XHRcdHRvcGljOiBxdWV1ZSxcblx0XHRcdG1lc3NhZ2VzOiBbXG5cdFx0XHRcdHtcblx0XHRcdFx0XHRrZXksXG5cdFx0XHRcdFx0dmFsdWU6IHBheWxvYWQsXG5cdFx0XHRcdFx0aGVhZGVyczogb3B0cz8uZGVsYXkgPyB7IFwieC1ibG9rLWRlbGF5LW1zXCI6IFN0cmluZyhvcHRzLmRlbGF5KSB9IDogdW5kZWZpbmVkLFxuXHRcdFx0XHR9LFxuXHRcdFx0XSxcblx0XHR9KTtcblx0XHRyZXR1cm4ga2V5O1xuXHR9XG5cblx0YXN5bmMgc3RvcFByb2Nlc3NpbmcocXVldWU6IHN0cmluZyk6IFByb21pc2U8dm9pZD4ge1xuXHRcdGNvbnN0IGNvbnN1bWVyID0gdGhpcy5oYW5kbGUuY29uc3VtZXJzLmdldChxdWV1ZSk7XG5cdFx0aWYgKGNvbnN1bWVyKSB7XG5cdFx0XHR0cnkge1xuXHRcdFx0XHRhd2FpdCBjb25zdW1lci5zdG9wKCk7XG5cdFx0XHR9IGNhdGNoIHtcblx0XHRcdFx0LyogaWdub3JlICovXG5cdFx0XHR9XG5cdFx0XHR0cnkge1xuXHRcdFx0XHRhd2FpdCBjb25zdW1lci5kaXNjb25uZWN0KCk7XG5cdFx0XHR9IGNhdGNoIHtcblx0XHRcdFx0LyogaWdub3JlICovXG5cdFx0XHR9XG5cdFx0XHR0aGlzLmhhbmRsZS5jb25zdW1lcnMuZGVsZXRlKHF1ZXVlKTtcblx0XHR9XG5cdH1cblxuXHRpc0Nvbm5lY3RlZCgpOiBib29sZWFuIHtcblx0XHRyZXR1cm4gdGhpcy5jb25uZWN0ZWQ7XG5cdH1cblxuXHRhc3luYyBoZWFsdGhDaGVjaygpOiBQcm9taXNlPGJvb2xlYW4+IHtcblx0XHRpZiAoIXRoaXMuY29ubmVjdGVkIHx8ICF0aGlzLmhhbmRsZS5hZG1pbikgcmV0dXJuIGZhbHNlO1xuXHRcdHRyeSB7XG5cdFx0XHRhd2FpdCB0aGlzLmhhbmRsZS5hZG1pbi5mZXRjaFRvcGljT2Zmc2V0cyhcIl9fY29uc3VtZXJfb2Zmc2V0c1wiKTtcblx0XHRcdHJldHVybiB0cnVlO1xuXHRcdH0gY2F0Y2gge1xuXHRcdFx0cmV0dXJuIGZhbHNlO1xuXHRcdH1cblx0fVxuXG5cdGFzeW5jIGdldFF1ZXVlU3RhdHMocXVldWU6IHN0cmluZyk6IFByb21pc2U8V29ya2VyUXVldWVTdGF0cz4ge1xuXHRcdGNvbnN0IGNvdW50ZXJzID0gdGhpcy5zdGF0cy5nZXQocXVldWUpID8/IHsgY29tcGxldGVkOiAwLCBmYWlsZWQ6IDAsIGFjdGl2ZTogMCB9O1xuXHRcdGxldCB3YWl0aW5nID0gMDtcblx0XHRpZiAodGhpcy5oYW5kbGUuYWRtaW4pIHtcblx0XHRcdHRyeSB7XG5cdFx0XHRcdGNvbnN0IG9mZnNldHMgPSBhd2FpdCB0aGlzLmhhbmRsZS5hZG1pbi5mZXRjaFRvcGljT2Zmc2V0cyhxdWV1ZSk7XG5cdFx0XHRcdC8vIEFwcHJveGltYXRlOiB0b3RhbCBjb21taXR0ZWQgb2Zmc2V0cyBhY3Jvc3MgcGFydGl0aW9ucy4gUmVhbCBsYWdcblx0XHRcdFx0Ly8gcmVxdWlyZXMgYWRtaW4uZmV0Y2hPZmZzZXRzKHsgZ3JvdXBJZCB9KSDigJQgc2tpcHBlZCBoZXJlIHRvIGtlZXBcblx0XHRcdFx0Ly8gdGhlIGNhbGwgY2hlYXA7IHByb2R1Y3Rpb24gZGVwbG95bWVudHMgc2hvdWxkIHVzZSBLYWZrYSdzXG5cdFx0XHRcdC8vIGRlZGljYXRlZCBsYWcgbWV0cmljcyBhbnl3YXkuXG5cdFx0XHRcdHdhaXRpbmcgPSBvZmZzZXRzLnJlZHVjZSgoc3VtLCBwKSA9PiBzdW0gKyBOdW1iZXIucGFyc2VJbnQocC5vZmZzZXQsIDEwKSwgMCk7XG5cdFx0XHR9IGNhdGNoIHtcblx0XHRcdFx0d2FpdGluZyA9IDA7XG5cdFx0XHR9XG5cdFx0fVxuXHRcdHJldHVybiB7XG5cdFx0XHR3YWl0aW5nLFxuXHRcdFx0YWN0aXZlOiBjb3VudGVycy5hY3RpdmUsXG5cdFx0XHRjb21wbGV0ZWQ6IGNvdW50ZXJzLmNvbXBsZXRlZCxcblx0XHRcdGZhaWxlZDogY291bnRlcnMuZmFpbGVkLFxuXHRcdFx0ZGVsYXllZDogMCxcblx0XHR9O1xuXHR9XG59XG4iXX0=
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NATSAdapter - NATS JetStream worker adapter for WorkerTrigger
|
|
3
|
+
*
|
|
4
|
+
* Uses NATS JetStream for persistent background job processing with:
|
|
5
|
+
* - Pull-based consumers with configurable concurrency
|
|
6
|
+
* - Server-side retry config (max_deliver)
|
|
7
|
+
* - Ack wait for job timeouts
|
|
8
|
+
* - Priority via message headers
|
|
9
|
+
* - Delayed job scheduling
|
|
10
|
+
* - Queue statistics via consumer info
|
|
11
|
+
*
|
|
12
|
+
* Requires: npm install nats
|
|
13
|
+
*
|
|
14
|
+
* Environment variables:
|
|
15
|
+
* - NATS_SERVERS: Comma-separated NATS server URLs (default: localhost:4222)
|
|
16
|
+
* - NATS_TOKEN: Authentication token (optional)
|
|
17
|
+
* - NATS_USER: Username for auth (optional)
|
|
18
|
+
* - NATS_PASS: Password for auth (optional)
|
|
19
|
+
* - NATS_STREAM_NAME: JetStream stream name (default: blok-worker)
|
|
20
|
+
*/
|
|
21
|
+
import type { WorkerTriggerOpts } from "@blokjs/helper";
|
|
22
|
+
import type { WorkerAdapter, WorkerJob, WorkerQueueStats } from "../WorkerTrigger";
|
|
23
|
+
/**
|
|
24
|
+
* Tier 2 polish — compute the consumer-side hold time for a NATS message
|
|
25
|
+
* with an `x-delay` header. NATS JetStream stores `x-delay` as opaque
|
|
26
|
+
* metadata; the broker does NOT defer delivery on it. The consumer is
|
|
27
|
+
* responsible for honouring the delay between the message's first-publish
|
|
28
|
+
* timestamp and `createdMs + delay`.
|
|
29
|
+
*
|
|
30
|
+
* Returns the milliseconds to wait. Clamps to >= 0; returns 0 when the
|
|
31
|
+
* delay has already elapsed (the message was queued for longer than the
|
|
32
|
+
* delay) or when no delay was set.
|
|
33
|
+
*
|
|
34
|
+
* Exported for unit testability — the consumer message handler in
|
|
35
|
+
* `NATSWorkerAdapter.process()` mocks the NATS client extensively, so
|
|
36
|
+
* isolating the math here keeps the surface easy to verify.
|
|
37
|
+
*/
|
|
38
|
+
export declare function computeXDelayHoldMs(delay: number, createdMs: number, nowMs: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* NATS worker adapter configuration
|
|
41
|
+
*/
|
|
42
|
+
export interface NATSWorkerConfig {
|
|
43
|
+
/** NATS server URLs */
|
|
44
|
+
servers: string[];
|
|
45
|
+
/** Authentication token */
|
|
46
|
+
token?: string;
|
|
47
|
+
/** Username */
|
|
48
|
+
user?: string;
|
|
49
|
+
/** Password */
|
|
50
|
+
pass?: string;
|
|
51
|
+
/** JetStream stream name (default: "blok-worker") */
|
|
52
|
+
streamName?: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* NATSWorkerAdapter - NATS JetStream implementation of WorkerAdapter
|
|
56
|
+
*/
|
|
57
|
+
export declare class NATSWorkerAdapter implements WorkerAdapter {
|
|
58
|
+
readonly provider: "nats";
|
|
59
|
+
private nc;
|
|
60
|
+
private js;
|
|
61
|
+
private jsm;
|
|
62
|
+
private connected;
|
|
63
|
+
private config;
|
|
64
|
+
private consumers;
|
|
65
|
+
private consumeIterators;
|
|
66
|
+
constructor(config?: Partial<NATSWorkerConfig>);
|
|
67
|
+
/**
|
|
68
|
+
* Connect to NATS and initialize JetStream
|
|
69
|
+
*/
|
|
70
|
+
connect(): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Disconnect from NATS
|
|
73
|
+
*/
|
|
74
|
+
disconnect(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Start processing jobs from a queue
|
|
77
|
+
*/
|
|
78
|
+
process(config: WorkerTriggerOpts, handler: (job: WorkerJob) => Promise<void>): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Add a job to a worker queue
|
|
81
|
+
*/
|
|
82
|
+
addJob(queue: string, data: unknown, opts?: {
|
|
83
|
+
priority?: number;
|
|
84
|
+
delay?: number;
|
|
85
|
+
retries?: number;
|
|
86
|
+
timeout?: number;
|
|
87
|
+
jobId?: string;
|
|
88
|
+
}): Promise<string>;
|
|
89
|
+
/**
|
|
90
|
+
* Stop processing a specific queue
|
|
91
|
+
*/
|
|
92
|
+
stopProcessing(queue: string): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Check if connected
|
|
95
|
+
*/
|
|
96
|
+
isConnected(): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Health check
|
|
99
|
+
*/
|
|
100
|
+
healthCheck(): Promise<boolean>;
|
|
101
|
+
/**
|
|
102
|
+
* Get queue statistics from JetStream consumer info
|
|
103
|
+
*/
|
|
104
|
+
getQueueStats(queue: string): Promise<WorkerQueueStats>;
|
|
105
|
+
/**
|
|
106
|
+
* Ensure a JetStream stream exists with the given subjects
|
|
107
|
+
*/
|
|
108
|
+
private ensureStream;
|
|
109
|
+
}
|
|
110
|
+
export default NATSWorkerAdapter;
|