@outray/runtime-shared 0.0.0-stage → 0.1.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/dist/buffered-logger.cjs +102 -0
  4. package/dist/buffered-logger.cjs.map +1 -0
  5. package/dist/buffered-logger.d.cts +26 -0
  6. package/dist/buffered-logger.d.ts +26 -0
  7. package/dist/buffered-logger.js +77 -0
  8. package/dist/buffered-logger.js.map +1 -0
  9. package/dist/chunk-7Z55KL36.js +1111 -0
  10. package/dist/chunk-7Z55KL36.js.map +1 -0
  11. package/dist/chunk-EBXUVSOO.js +37 -0
  12. package/dist/chunk-EBXUVSOO.js.map +1 -0
  13. package/dist/chunk-XAMVXW6B.js +19 -0
  14. package/dist/chunk-XAMVXW6B.js.map +1 -0
  15. package/dist/durable-queue.cjs +316 -0
  16. package/dist/durable-queue.cjs.map +1 -0
  17. package/dist/durable-queue.d.cts +41 -0
  18. package/dist/durable-queue.d.ts +41 -0
  19. package/dist/durable-queue.js +290 -0
  20. package/dist/durable-queue.js.map +1 -0
  21. package/dist/helpers.cjs +1167 -0
  22. package/dist/helpers.cjs.map +1 -0
  23. package/dist/helpers.d.cts +6 -0
  24. package/dist/helpers.d.ts +6 -0
  25. package/dist/helpers.js +14 -0
  26. package/dist/helpers.js.map +1 -0
  27. package/dist/postgres-ssl.cjs +37 -0
  28. package/dist/postgres-ssl.cjs.map +1 -0
  29. package/dist/postgres-ssl.d.cts +9 -0
  30. package/dist/postgres-ssl.d.ts +9 -0
  31. package/dist/postgres-ssl.js +12 -0
  32. package/dist/postgres-ssl.js.map +1 -0
  33. package/dist/protocol.cjs +38 -0
  34. package/dist/protocol.cjs.map +1 -0
  35. package/dist/protocol.d.cts +8 -0
  36. package/dist/protocol.d.ts +8 -0
  37. package/dist/protocol.js +13 -0
  38. package/dist/protocol.js.map +1 -0
  39. package/dist/public-hosts.cjs +116 -0
  40. package/dist/public-hosts.cjs.map +1 -0
  41. package/dist/public-hosts.d.cts +29 -0
  42. package/dist/public-hosts.d.ts +29 -0
  43. package/dist/public-hosts.js +82 -0
  44. package/dist/public-hosts.js.map +1 -0
  45. package/dist/tinybird-records.cjs +160 -0
  46. package/dist/tinybird-records.cjs.map +1 -0
  47. package/dist/tinybird-records.d.cts +62 -0
  48. package/dist/tinybird-records.d.ts +62 -0
  49. package/dist/tinybird-records.js +129 -0
  50. package/dist/tinybird-records.js.map +1 -0
  51. package/dist/tunnel-accounting.cjs +71 -0
  52. package/dist/tunnel-accounting.cjs.map +1 -0
  53. package/dist/tunnel-accounting.d.cts +12 -0
  54. package/dist/tunnel-accounting.d.ts +12 -0
  55. package/dist/tunnel-accounting.js +32 -0
  56. package/dist/tunnel-accounting.js.map +1 -0
  57. package/dist/tunnel-product-usage.cjs +107 -0
  58. package/dist/tunnel-product-usage.cjs.map +1 -0
  59. package/dist/tunnel-product-usage.d.cts +26 -0
  60. package/dist/tunnel-product-usage.d.ts +26 -0
  61. package/dist/tunnel-product-usage.js +81 -0
  62. package/dist/tunnel-product-usage.js.map +1 -0
  63. package/dist/types.cjs +19 -0
  64. package/dist/types.cjs.map +1 -0
  65. package/dist/types.d.cts +105 -0
  66. package/dist/types.d.ts +105 -0
  67. package/dist/types.js +1 -0
  68. package/dist/types.js.map +1 -0
  69. package/dist/usage-writer.cjs +43 -0
  70. package/dist/usage-writer.cjs.map +1 -0
  71. package/dist/usage-writer.d.cts +14 -0
  72. package/dist/usage-writer.d.ts +14 -0
  73. package/dist/usage-writer.js +7 -0
  74. package/dist/usage-writer.js.map +1 -0
  75. package/dist/utils.cjs +1171 -0
  76. package/dist/utils.cjs.map +1 -0
  77. package/dist/utils.d.cts +2 -0
  78. package/dist/utils.d.ts +2 -0
  79. package/dist/utils.js +19 -0
  80. package/dist/utils.js.map +1 -0
  81. package/dist/words.cjs +1136 -0
  82. package/dist/words.cjs.map +1 -0
  83. package/dist/words.d.cts +4 -0
  84. package/dist/words.d.ts +4 -0
  85. package/dist/words.js +9 -0
  86. package/dist/words.js.map +1 -0
  87. package/package.json +99 -4
@@ -0,0 +1,290 @@
1
+ // src/durable-queue.ts
2
+ import { randomUUID } from "crypto";
3
+ import { hostname } from "os";
4
+ import { Redis } from "ioredis";
5
+ var ENQUEUE = `
6
+ local maxAge = tonumber(ARGV[2])
7
+ if maxAge > 0 then
8
+ local now = redis.call('TIME')
9
+ redis.call('XTRIM', KEYS[1], 'MINID', tostring((tonumber(now[1]) - maxAge) * 1000) .. '-0')
10
+ end
11
+ local additions = #ARGV - 2
12
+ if redis.call('XLEN', KEYS[1]) + additions > tonumber(ARGV[1]) then
13
+ return 0
14
+ end
15
+ for index = 3, #ARGV do
16
+ redis.call('XADD', KEYS[1], '*', 'payload', ARGV[index])
17
+ end
18
+ if maxAge > 0 then redis.call('EXPIRE', KEYS[1], maxAge) end
19
+ return additions
20
+ `;
21
+ var DEAD_LETTER = `
22
+ redis.call('XADD', KEYS[2], 'MAXLEN', tonumber(ARGV[6]), '*',
23
+ 'source_id', ARGV[2], 'error', ARGV[3], 'payload', ARGV[4])
24
+ local maxAge = tonumber(ARGV[5])
25
+ if maxAge > 0 then
26
+ local now = redis.call('TIME')
27
+ local cutoff = tostring((tonumber(now[1]) - maxAge) * 1000) .. '-0'
28
+ redis.call('XTRIM', KEYS[2], 'MINID', cutoff)
29
+ local sourceTime = tonumber(string.match(ARGV[2], '^(%d+)')) / 1000
30
+ local remaining = math.max(1, math.floor(sourceTime + maxAge - tonumber(now[1])))
31
+ local ttl = redis.call('TTL', KEYS[2])
32
+ if ttl < 0 or ttl > remaining then redis.call('EXPIRE', KEYS[2], remaining) end
33
+ end
34
+ redis.call('XACK', KEYS[1], ARGV[1], ARGV[2])
35
+ redis.call('XDEL', KEYS[1], ARGV[2])
36
+ return 1
37
+ `;
38
+ var PRUNE = `
39
+ local maxAge = tonumber(ARGV[1])
40
+ local now = redis.call('TIME')
41
+ local cutoff = tostring((tonumber(now[1]) - maxAge) * 1000) .. '-0'
42
+ for index = 1, #KEYS do
43
+ redis.call('XTRIM', KEYS[index], 'MINID', cutoff)
44
+ if redis.call('TTL', KEYS[index]) == -1 then redis.call('EXPIRE', KEYS[index], maxAge) end
45
+ end
46
+ return 1
47
+ `;
48
+ var sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
49
+ function fieldValue(fields, name) {
50
+ for (let index = 0; index < fields.length; index += 2) {
51
+ if (fields[index] === name) return fields[index + 1] ?? null;
52
+ }
53
+ return null;
54
+ }
55
+ var DurableQueue = class {
56
+ constructor(options) {
57
+ this.options = options;
58
+ const shared = {
59
+ lazyConnect: true,
60
+ connectTimeout: 3e3,
61
+ commandTimeout: 5e3,
62
+ maxRetriesPerRequest: 1
63
+ };
64
+ this.producer = new Redis(options.redisUrl, shared);
65
+ this.worker = new Redis(options.redisUrl, {
66
+ ...shared,
67
+ commandTimeout: void 0,
68
+ maxRetriesPerRequest: null
69
+ });
70
+ this.producer.on(
71
+ "error",
72
+ (error) => console.error(`${options.signalName} queue producer error`, error)
73
+ );
74
+ this.worker.on("error", (error) => {
75
+ if (!this.stopping)
76
+ console.error(`${options.signalName} queue worker error`, error);
77
+ });
78
+ }
79
+ options;
80
+ producer;
81
+ worker;
82
+ consumer = `${hostname()}:${process.pid}:${randomUUID()}`;
83
+ stopping = false;
84
+ workerPromise = null;
85
+ async enqueue(records) {
86
+ if (records.length === 0) return;
87
+ if (this.producer.status === "wait" || this.producer.status === "end") {
88
+ await this.producer.connect();
89
+ }
90
+ const payloads = queuePayloads(records, this.options.maxMessageBytes);
91
+ const result = await this.producer.eval(
92
+ ENQUEUE,
93
+ 1,
94
+ this.options.streamKey,
95
+ this.options.maxEntries,
96
+ this.options.maxAgeSeconds || 0,
97
+ ...payloads
98
+ );
99
+ if (result === 0) {
100
+ throw new Error(`${this.options.signalName} ingestion queue is full`);
101
+ }
102
+ if (Number(result) !== payloads.length) {
103
+ throw new Error(
104
+ `Could not durably enqueue ${this.options.signalName} data`
105
+ );
106
+ }
107
+ }
108
+ start() {
109
+ if (this.workerPromise) return;
110
+ this.workerPromise = this.run().catch((error) => {
111
+ if (!this.stopping)
112
+ console.error(
113
+ `${this.options.signalName} queue stopped unexpectedly`,
114
+ error
115
+ );
116
+ });
117
+ }
118
+ async run() {
119
+ while (!this.stopping) {
120
+ try {
121
+ if (this.worker.status === "wait" || this.worker.status === "end") {
122
+ await this.worker.connect();
123
+ }
124
+ await this.createConsumerGroup();
125
+ await this.pruneExpired();
126
+ await this.recoverStale();
127
+ let lastRecoveryAt = Date.now();
128
+ while (!this.stopping) {
129
+ if (Date.now() - lastRecoveryAt >= 3e4) {
130
+ await this.pruneExpired();
131
+ await this.recoverStale();
132
+ lastRecoveryAt = Date.now();
133
+ }
134
+ const result = await this.worker.xreadgroup(
135
+ "GROUP",
136
+ this.options.group,
137
+ this.consumer,
138
+ "COUNT",
139
+ this.options.batchSize,
140
+ "BLOCK",
141
+ 2e3,
142
+ "STREAMS",
143
+ this.options.streamKey,
144
+ ">"
145
+ );
146
+ const messages = result?.[0]?.[1] ?? [];
147
+ for (const message of messages) await this.deliver(message);
148
+ }
149
+ } catch (error) {
150
+ if (this.stopping) break;
151
+ console.error(
152
+ `${this.options.signalName} queue worker will retry`,
153
+ error
154
+ );
155
+ this.worker.disconnect(false);
156
+ await sleep(2e3);
157
+ }
158
+ }
159
+ }
160
+ async createConsumerGroup() {
161
+ try {
162
+ await this.worker.xgroup(
163
+ "CREATE",
164
+ this.options.streamKey,
165
+ this.options.group,
166
+ "0",
167
+ "MKSTREAM"
168
+ );
169
+ } catch (error) {
170
+ if (!(error instanceof Error) || !error.message.includes("BUSYGROUP"))
171
+ throw error;
172
+ }
173
+ }
174
+ async pruneExpired() {
175
+ if (!this.options.maxAgeSeconds) return;
176
+ await this.worker.eval(PRUNE, 2, this.options.streamKey, this.options.deadLetterKey, this.options.maxAgeSeconds);
177
+ }
178
+ async recoverStale() {
179
+ let cursor = "0-0";
180
+ do {
181
+ const result = await this.worker.xautoclaim(
182
+ this.options.streamKey,
183
+ this.options.group,
184
+ this.consumer,
185
+ this.options.claimIdleMs || 6e4,
186
+ cursor,
187
+ "COUNT",
188
+ this.options.batchSize
189
+ );
190
+ cursor = result[0];
191
+ for (const message of result[1] ?? []) await this.deliver(message);
192
+ } while (!this.stopping && cursor !== "0-0");
193
+ }
194
+ async deliver([id, fields]) {
195
+ const payload = fieldValue(fields, "payload");
196
+ if (!payload) {
197
+ await this.acknowledge(id);
198
+ return;
199
+ }
200
+ let records;
201
+ try {
202
+ records = JSON.parse(payload);
203
+ if (!Array.isArray(records)) throw new Error("payload is not an array");
204
+ } catch (error) {
205
+ await this.deadLetter(id, payload, error);
206
+ return;
207
+ }
208
+ for (let attempt = 1; attempt <= this.options.maxDeliveryAttempts; attempt++) {
209
+ try {
210
+ await this.options.deliver(records);
211
+ await this.acknowledge(id);
212
+ return;
213
+ } catch (error) {
214
+ if (attempt === this.options.maxDeliveryAttempts) {
215
+ await this.deadLetter(id, payload, error);
216
+ return;
217
+ }
218
+ await sleep(Math.min(1e3 * 2 ** (attempt - 1), 15e3));
219
+ }
220
+ }
221
+ }
222
+ async acknowledge(id) {
223
+ const result = await this.worker.multi().xack(this.options.streamKey, this.options.group, id).xdel(this.options.streamKey, id).exec();
224
+ if (!result || result.some(([error]) => error)) {
225
+ throw new Error(`${this.options.signalName} queue acknowledgement failed`);
226
+ }
227
+ }
228
+ async deadLetter(id, payload, error) {
229
+ const message = error instanceof Error ? error.message : "Unknown delivery failure";
230
+ console.error(
231
+ `${this.options.signalName} queue delivery failed permanently for ${id}: ${message}`
232
+ );
233
+ const result = await this.worker.eval(
234
+ DEAD_LETTER,
235
+ 2,
236
+ this.options.streamKey,
237
+ this.options.deadLetterKey,
238
+ this.options.group,
239
+ id,
240
+ message.slice(0, 1e3),
241
+ payload,
242
+ this.options.maxAgeSeconds || 0,
243
+ this.options.maxDeadLetterEntries || 1e3
244
+ );
245
+ if (Number(result) !== 1) {
246
+ throw new Error(`${this.options.signalName} dead-letter storage failed`);
247
+ }
248
+ }
249
+ async close() {
250
+ this.stopping = true;
251
+ this.worker.disconnect(false);
252
+ if (this.producer.status !== "end")
253
+ await this.producer.quit().catch(() => void 0);
254
+ await this.workerPromise;
255
+ }
256
+ };
257
+ function chunk(items, size) {
258
+ const chunks = [];
259
+ for (let index = 0; index < items.length; index += size) {
260
+ chunks.push(items.slice(index, index + size));
261
+ }
262
+ return chunks;
263
+ }
264
+ function queuePayloads(records, maxBytes) {
265
+ if (!maxBytes) return chunk(records, 250).map((rows) => JSON.stringify(rows));
266
+ const payloads = [];
267
+ let lines = [];
268
+ let bytes = 2;
269
+ for (const record of records) {
270
+ const line = JSON.stringify(record);
271
+ const size = Buffer.byteLength(line);
272
+ if (size + 2 > maxBytes) throw new Error("Analytics record exceeds the durable queue message size limit");
273
+ if (lines.length && (bytes + size + 1 > maxBytes || lines.length >= 250)) {
274
+ payloads.push(`[${lines.join(",")}]`);
275
+ lines = [];
276
+ bytes = 2;
277
+ }
278
+ bytes += size + (lines.length ? 1 : 0);
279
+ lines.push(line);
280
+ }
281
+ if (lines.length) payloads.push(`[${lines.join(",")}]`);
282
+ return payloads;
283
+ }
284
+ var durable_queue_default = { DurableQueue };
285
+ export {
286
+ DurableQueue,
287
+ durable_queue_default as default,
288
+ queuePayloads
289
+ };
290
+ //# sourceMappingURL=durable-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/durable-queue.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { hostname } from \"node:os\";\nimport { Redis } from \"ioredis\";\n\nexport interface DurableQueueOptions<T> {\n signalName: string;\n redisUrl: string;\n streamKey: string;\n deadLetterKey: string;\n group: string;\n maxEntries: number;\n batchSize: number;\n maxDeliveryAttempts: number;\n /** Optional storage bound; existing observability queues keep their policy. */\n maxAgeSeconds?: number;\n maxMessageBytes?: number;\n maxDeadLetterEntries?: number;\n claimIdleMs?: number;\n deliver: (records: T[]) => Promise<void>;\n}\n\ntype StreamMessage = [id: string, fields: string[]];\n\nconst ENQUEUE = `\nlocal maxAge = tonumber(ARGV[2])\nif maxAge > 0 then\n local now = redis.call('TIME')\n redis.call('XTRIM', KEYS[1], 'MINID', tostring((tonumber(now[1]) - maxAge) * 1000) .. '-0')\nend\nlocal additions = #ARGV - 2\nif redis.call('XLEN', KEYS[1]) + additions > tonumber(ARGV[1]) then\n return 0\nend\nfor index = 3, #ARGV do\n redis.call('XADD', KEYS[1], '*', 'payload', ARGV[index])\nend\nif maxAge > 0 then redis.call('EXPIRE', KEYS[1], maxAge) end\nreturn additions\n`;\n\nconst DEAD_LETTER = `\nredis.call('XADD', KEYS[2], 'MAXLEN', tonumber(ARGV[6]), '*',\n 'source_id', ARGV[2], 'error', ARGV[3], 'payload', ARGV[4])\nlocal maxAge = tonumber(ARGV[5])\nif maxAge > 0 then\n local now = redis.call('TIME')\n local cutoff = tostring((tonumber(now[1]) - maxAge) * 1000) .. '-0'\n redis.call('XTRIM', KEYS[2], 'MINID', cutoff)\n local sourceTime = tonumber(string.match(ARGV[2], '^(%d+)')) / 1000\n local remaining = math.max(1, math.floor(sourceTime + maxAge - tonumber(now[1])))\n local ttl = redis.call('TTL', KEYS[2])\n if ttl < 0 or ttl > remaining then redis.call('EXPIRE', KEYS[2], remaining) end\nend\nredis.call('XACK', KEYS[1], ARGV[1], ARGV[2])\nredis.call('XDEL', KEYS[1], ARGV[2])\nreturn 1\n`;\n\nconst PRUNE = `\nlocal maxAge = tonumber(ARGV[1])\nlocal now = redis.call('TIME')\nlocal cutoff = tostring((tonumber(now[1]) - maxAge) * 1000) .. '-0'\nfor index = 1, #KEYS do\n redis.call('XTRIM', KEYS[index], 'MINID', cutoff)\n if redis.call('TTL', KEYS[index]) == -1 then redis.call('EXPIRE', KEYS[index], maxAge) end\nend\nreturn 1\n`;\n\nconst sleep = (milliseconds: number) =>\n new Promise((resolve) => setTimeout(resolve, milliseconds));\n\nfunction fieldValue(fields: string[], name: string): string | null {\n for (let index = 0; index < fields.length; index += 2) {\n if (fields[index] === name) return fields[index + 1] ?? null;\n }\n return null;\n}\n\nexport class DurableQueue<T> {\n private readonly producer: Redis;\n private readonly worker: Redis;\n private readonly consumer = `${hostname()}:${process.pid}:${randomUUID()}`;\n private stopping = false;\n private workerPromise: Promise<void> | null = null;\n\n constructor(private readonly options: DurableQueueOptions<T>) {\n const shared = {\n lazyConnect: true,\n connectTimeout: 3_000,\n commandTimeout: 5_000,\n maxRetriesPerRequest: 1,\n } as const;\n this.producer = new Redis(options.redisUrl, shared);\n this.worker = new Redis(options.redisUrl, {\n ...shared,\n commandTimeout: undefined,\n maxRetriesPerRequest: null,\n });\n\n this.producer.on(\"error\", (error: Error) =>\n console.error(`${options.signalName} queue producer error`, error),\n );\n this.worker.on(\"error\", (error: Error) => {\n if (!this.stopping)\n console.error(`${options.signalName} queue worker error`, error);\n });\n }\n\n async enqueue(records: T[]): Promise<void> {\n if (records.length === 0) return;\n if (this.producer.status === \"wait\" || this.producer.status === \"end\") {\n await this.producer.connect();\n }\n\n const payloads = queuePayloads(records, this.options.maxMessageBytes);\n // Admission and enqueue must share one Redis operation across replicas.\n const result = await this.producer.eval(\n ENQUEUE, 1, this.options.streamKey, this.options.maxEntries,\n this.options.maxAgeSeconds || 0, ...payloads,\n );\n if (result === 0) {\n throw new Error(`${this.options.signalName} ingestion queue is full`);\n }\n if (Number(result) !== payloads.length) {\n throw new Error(\n `Could not durably enqueue ${this.options.signalName} data`,\n );\n }\n }\n\n start(): void {\n if (this.workerPromise) return;\n this.workerPromise = this.run().catch((error) => {\n if (!this.stopping)\n console.error(\n `${this.options.signalName} queue stopped unexpectedly`,\n error,\n );\n });\n }\n\n private async run(): Promise<void> {\n while (!this.stopping) {\n try {\n if (this.worker.status === \"wait\" || this.worker.status === \"end\") {\n await this.worker.connect();\n }\n await this.createConsumerGroup();\n await this.pruneExpired();\n await this.recoverStale();\n let lastRecoveryAt = Date.now();\n\n while (!this.stopping) {\n if (Date.now() - lastRecoveryAt >= 30_000) {\n await this.pruneExpired();\n await this.recoverStale();\n lastRecoveryAt = Date.now();\n }\n const result = (await this.worker.xreadgroup(\n \"GROUP\",\n this.options.group,\n this.consumer,\n \"COUNT\",\n this.options.batchSize,\n \"BLOCK\",\n 2_000,\n \"STREAMS\",\n this.options.streamKey,\n \">\",\n )) as [string, StreamMessage[]][] | null;\n\n const messages = result?.[0]?.[1] ?? [];\n for (const message of messages) await this.deliver(message);\n }\n } catch (error) {\n if (this.stopping) break;\n console.error(\n `${this.options.signalName} queue worker will retry`,\n error,\n );\n this.worker.disconnect(false);\n await sleep(2_000);\n }\n }\n }\n\n private async createConsumerGroup(): Promise<void> {\n try {\n await this.worker.xgroup(\n \"CREATE\",\n this.options.streamKey,\n this.options.group,\n \"0\",\n \"MKSTREAM\",\n );\n } catch (error) {\n if (!(error instanceof Error) || !error.message.includes(\"BUSYGROUP\"))\n throw error;\n }\n }\n\n private async pruneExpired(): Promise<void> {\n if (!this.options.maxAgeSeconds) return;\n await this.worker.eval(PRUNE, 2, this.options.streamKey, this.options.deadLetterKey, this.options.maxAgeSeconds);\n }\n\n private async recoverStale(): Promise<void> {\n let cursor = \"0-0\";\n do {\n const result = (await this.worker.xautoclaim(\n this.options.streamKey,\n this.options.group,\n this.consumer,\n this.options.claimIdleMs || 60_000,\n cursor,\n \"COUNT\",\n this.options.batchSize,\n )) as [string, StreamMessage[]];\n cursor = result[0];\n for (const message of result[1] ?? []) await this.deliver(message);\n } while (!this.stopping && cursor !== \"0-0\");\n }\n\n private async deliver([id, fields]: StreamMessage): Promise<void> {\n const payload = fieldValue(fields, \"payload\");\n if (!payload) {\n await this.acknowledge(id);\n return;\n }\n\n let records: T[];\n try {\n records = JSON.parse(payload) as T[];\n if (!Array.isArray(records)) throw new Error(\"payload is not an array\");\n } catch (error) {\n await this.deadLetter(id, payload, error);\n return;\n }\n\n for (\n let attempt = 1;\n attempt <= this.options.maxDeliveryAttempts;\n attempt++\n ) {\n try {\n await this.options.deliver(records);\n await this.acknowledge(id);\n return;\n } catch (error) {\n if (attempt === this.options.maxDeliveryAttempts) {\n await this.deadLetter(id, payload, error);\n return;\n }\n await sleep(Math.min(1_000 * 2 ** (attempt - 1), 15_000));\n }\n }\n }\n\n private async acknowledge(id: string): Promise<void> {\n const result = await this.worker\n .multi()\n .xack(this.options.streamKey, this.options.group, id)\n .xdel(this.options.streamKey, id)\n .exec();\n if (!result || result.some(([error]) => error)) {\n throw new Error(`${this.options.signalName} queue acknowledgement failed`);\n }\n }\n\n private async deadLetter(\n id: string,\n payload: string,\n error: unknown,\n ): Promise<void> {\n const message =\n error instanceof Error ? error.message : \"Unknown delivery failure\";\n console.error(\n `${this.options.signalName} queue delivery failed permanently for ${id}: ${message}`,\n );\n // Only acknowledge the source after dead-letter storage succeeded. Redis\n // MULTI continues after a failed XADD (for example OOM), which could discard\n // a record without storing its failed-delivery copy.\n const result = await this.worker.eval(\n DEAD_LETTER, 2, this.options.streamKey, this.options.deadLetterKey,\n this.options.group, id, message.slice(0, 1_000), payload,\n this.options.maxAgeSeconds || 0, this.options.maxDeadLetterEntries || 1_000,\n );\n if (Number(result) !== 1) {\n throw new Error(`${this.options.signalName} dead-letter storage failed`);\n }\n }\n\n async close(): Promise<void> {\n this.stopping = true;\n this.worker.disconnect(false);\n if (this.producer.status !== \"end\")\n await this.producer.quit().catch(() => undefined);\n await this.workerPromise;\n }\n}\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n const chunks: T[][] = [];\n for (let index = 0; index < items.length; index += size) {\n chunks.push(items.slice(index, index + size));\n }\n return chunks;\n}\n\nexport function queuePayloads<T>(records: T[], maxBytes?: number): string[] {\n if (!maxBytes) return chunk(records, 250).map((rows) => JSON.stringify(rows));\n const payloads: string[] = [];\n let lines: string[] = [];\n let bytes = 2;\n for (const record of records) {\n const line = JSON.stringify(record);\n const size = Buffer.byteLength(line);\n if (size + 2 > maxBytes) throw new Error(\"Analytics record exceeds the durable queue message size limit\");\n if (lines.length && (bytes + size + 1 > maxBytes || lines.length >= 250)) {\n payloads.push(`[${lines.join(\",\")}]`);\n lines = [];\n bytes = 2;\n }\n bytes += size + (lines.length ? 1 : 0);\n lines.push(line);\n }\n if (lines.length) payloads.push(`[${lines.join(\",\")}]`);\n return payloads;\n}\n\nexport default { DurableQueue };\n"],"mappings":"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","names":[]}