lonnymq 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1117 @@
1
+ const INTEGER_MAX = 2147483647;
2
+ //#endregion
3
+ //#region src/util/sql.ts
4
+ const value = (value) => ({
5
+ nodeType: "VALUE",
6
+ value
7
+ });
8
+ const ref = (value) => ({
9
+ nodeType: "REF",
10
+ value
11
+ });
12
+ const raw = (value) => ({
13
+ nodeType: "RAW",
14
+ value
15
+ });
16
+ const escapeString = (value) => {
17
+ return `'${value.replace(/'/g, "''")}'`;
18
+ };
19
+ const escapeValue = (value) => {
20
+ if (value === null) return "NULL";
21
+ else if (typeof value === "string") return escapeString(value);
22
+ else if (typeof value === "number") return value.toString();
23
+ else if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
24
+ else if (value instanceof Date) return `'${value.toISOString()}'`;
25
+ else if (typeof value === "bigint") return value.toString();
26
+ else throw new Error(`Unsupported value type: ${typeof value}`);
27
+ };
28
+ const escapeRef = (value) => {
29
+ return `"${value.replace(/"/g, "\"\"")}"`;
30
+ };
31
+ const escapeNode = (value) => {
32
+ if (value.nodeType === "VALUE") return escapeValue(value.value);
33
+ else if (value.nodeType === "REF") return escapeRef(value.value);
34
+ else if (value.nodeType === "RAW") return value.value;
35
+ else throw new Error("Unsupported SQL node type");
36
+ };
37
+ const fragment = (fragments, ...values) => {
38
+ const parts = [];
39
+ for (let ix = 0; ix < fragments.length; ix += 1) {
40
+ parts.push(fragments[ix]);
41
+ if (ix < values.length) parts.push(escapeNode(values[ix]));
42
+ }
43
+ return raw(parts.join(""));
44
+ };
45
+ //#endregion
46
+ //#region src/util/text.ts
47
+ const dedent = (value) => {
48
+ const lines = value.split("\n");
49
+ let minIndent = Number.MAX_SAFE_INTEGER;
50
+ for (const line of lines) {
51
+ if (line.trim().length === 0) continue;
52
+ const indent = line.search(/\S/);
53
+ minIndent = Math.min(minIndent, indent);
54
+ }
55
+ return lines.map((line) => line.slice(minIndent)).join("\n").trim();
56
+ };
57
+ //#endregion
58
+ //#region src/command/channel/release.ts
59
+ var Release = class {
60
+ schema;
61
+ channel;
62
+ createdAt;
63
+ constructor(params) {
64
+ this.schema = params.schema;
65
+ this.channel = params.channel;
66
+ this.createdAt = /* @__PURE__ */ new Date();
67
+ }
68
+ async execute(databaseClient) {
69
+ const result = await databaseClient.query(fragment`
70
+ SELECT * FROM ${ref(this.schema)}."channel_release"(
71
+ $1
72
+ )
73
+ `.value, [this.channel]).then((res) => res.rows[0]);
74
+ if (result.result_code === 9) return { resultType: "CHANNEL_NOT_FOUND" };
75
+ else if (result.result_code === 10) return { resultType: "CHANNEL_RELEASED" };
76
+ else throw new Error("Unexpected result");
77
+ }
78
+ };
79
+ //#endregion
80
+ //#region src/command/channel/set.ts
81
+ var Set = class {
82
+ schema;
83
+ channel;
84
+ maxConcurrency;
85
+ maxSize;
86
+ releaseIntervalMs;
87
+ createdAt;
88
+ constructor(params) {
89
+ this.schema = params.schema;
90
+ this.channel = params.channel;
91
+ if (!Number.isInteger(params.maxConcurrency) || params.maxConcurrency < 1 || params.maxConcurrency > 2147483647) throw new Error(`maxConcurrency must be an integer between 1 and ${INTEGER_MAX}`);
92
+ if (!Number.isInteger(params.maxSize) || params.maxSize < 1 || params.maxSize > 2147483647) throw new Error(`maxSize must be an integer between 1 and ${INTEGER_MAX}`);
93
+ if (!Number.isInteger(params.releaseIntervalMs) || params.releaseIntervalMs < 0 || params.releaseIntervalMs > 2147483647) throw new Error(`releaseIntervalMs must be an integer between 0 and ${INTEGER_MAX}`);
94
+ this.maxConcurrency = params.maxConcurrency;
95
+ this.maxSize = params.maxSize;
96
+ this.releaseIntervalMs = params.releaseIntervalMs;
97
+ this.createdAt = /* @__PURE__ */ new Date();
98
+ }
99
+ async execute(databaseClient) {
100
+ await databaseClient.query(fragment`
101
+ SELECT 1 FROM ${ref(this.schema)}."channel_set"(
102
+ $1,
103
+ $2::INTEGER,
104
+ $3::INTEGER,
105
+ $4::INTEGER
106
+ )
107
+ `.value, [
108
+ this.channel,
109
+ this.maxConcurrency,
110
+ this.maxSize,
111
+ this.releaseIntervalMs
112
+ ]);
113
+ }
114
+ };
115
+ //#endregion
116
+ //#region src/command/message/create.ts
117
+ var Create = class {
118
+ schema;
119
+ channel;
120
+ content;
121
+ dequeueAt;
122
+ constructor(params) {
123
+ this.schema = params.schema;
124
+ this.channel = params.channel;
125
+ this.content = params.content;
126
+ this.dequeueAt = params.dequeueAt;
127
+ }
128
+ async execute(databaseClient) {
129
+ const result = await databaseClient.query(fragment`
130
+ SELECT
131
+ result_code,
132
+ metadata
133
+ FROM ${ref(this.schema)}."message_create"(
134
+ $1,
135
+ $2,
136
+ $3::BIGINT
137
+ )
138
+ `.value, [
139
+ this.channel,
140
+ this.content,
141
+ this.dequeueAt
142
+ ]).then((res) => res.rows[0]);
143
+ if (result.result_code === 0) return {
144
+ resultType: "MESSAGE_CREATED",
145
+ id: BigInt(result.metadata.id)
146
+ };
147
+ else if (result.result_code === 1) return { resultType: "MESSAGE_DROPPED" };
148
+ else if (result.result_code === 9) return { resultType: "CHANNEL_NOT_FOUND" };
149
+ else throw new Error("Unexpected result code");
150
+ }
151
+ };
152
+ //#endregion
153
+ //#region src/command/message/dequeue.ts
154
+ var Dequeue = class {
155
+ schema;
156
+ lockMs;
157
+ constructor(params) {
158
+ this.schema = params.schema;
159
+ this.lockMs = params.lockMs;
160
+ }
161
+ async execute(databaseClient) {
162
+ const result = await databaseClient.query(fragment`
163
+ SELECT
164
+ result_code,
165
+ metadata,
166
+ content,
167
+ state
168
+ FROM ${ref(this.schema)}."message_dequeue"($1::BIGINT)
169
+ `.value, [this.lockMs]).then((res) => res.rows[0]);
170
+ if (result.result_code === 2) return { resultType: "MESSAGE_NOT_AVAILABLE" };
171
+ else if (result.result_code === 3) return {
172
+ resultType: "MESSAGE_DEQUEUED",
173
+ id: BigInt(result.metadata.id),
174
+ isUnlocked: result.metadata.is_unlocked,
175
+ content: result.content,
176
+ state: result.state,
177
+ numAttempts: result.metadata.num_attempts
178
+ };
179
+ else throw new Error("Unexpected dequeue result");
180
+ }
181
+ };
182
+ //#endregion
183
+ //#region src/command/message/complete.ts
184
+ var Complete = class {
185
+ schema;
186
+ id;
187
+ numAttempts;
188
+ constructor(params) {
189
+ this.schema = params.schema;
190
+ this.id = params.id;
191
+ this.numAttempts = params.numAttempts;
192
+ }
193
+ async execute(databaseClient) {
194
+ const result = await databaseClient.query(fragment`
195
+ SELECT * FROM ${ref(this.schema)}."message_complete"(
196
+ $1::BIGINT,
197
+ $2::BIGINT
198
+ )
199
+ `.value, [this.id.toString(), this.numAttempts]).then((res) => res.rows[0]);
200
+ if (result.result_code === 4) return { resultType: "MESSAGE_NOT_FOUND" };
201
+ else if (result.result_code === 5) return { resultType: "STATE_INVALID" };
202
+ else if (result.result_code === 6) return { resultType: "MESSAGE_COMPLETED" };
203
+ else throw new Error("Unexpected result");
204
+ }
205
+ };
206
+ //#endregion
207
+ //#region src/command/message/defer.ts
208
+ var Defer = class {
209
+ schema;
210
+ id;
211
+ numAttempts;
212
+ state;
213
+ dequeueAt;
214
+ constructor(params) {
215
+ this.schema = params.schema;
216
+ this.numAttempts = params.numAttempts;
217
+ this.id = params.id;
218
+ this.state = params.state;
219
+ this.dequeueAt = params.dequeueAt;
220
+ }
221
+ async execute(databaseClient) {
222
+ const result = await databaseClient.query(fragment`
223
+ SELECT * FROM ${ref(this.schema)}."message_defer"(
224
+ $1::BIGINT,
225
+ $2::BIGINT,
226
+ $3::BIGINT,
227
+ $4
228
+ )
229
+ `.value, [
230
+ this.id.toString(),
231
+ this.numAttempts,
232
+ this.dequeueAt,
233
+ this.state
234
+ ]).then((res) => res.rows[0]);
235
+ if (result.result_code === 4) return { resultType: "MESSAGE_NOT_FOUND" };
236
+ else if (result.result_code === 5) return { resultType: "STATE_INVALID" };
237
+ else if (result.result_code === 7) return { resultType: "MESSAGE_DEFERRED" };
238
+ else throw new Error("Unexpected result");
239
+ }
240
+ };
241
+ //#endregion
242
+ //#region src/command/message/heartbeat.ts
243
+ var Heartbeat = class {
244
+ schema;
245
+ id;
246
+ numAttempts;
247
+ lockMs;
248
+ constructor(params) {
249
+ this.schema = params.schema;
250
+ this.numAttempts = params.numAttempts;
251
+ this.id = params.id;
252
+ this.lockMs = params.lockMs;
253
+ }
254
+ async execute(databaseClient) {
255
+ const result = await databaseClient.query(fragment`
256
+ SELECT * FROM ${ref(this.schema)}."message_heartbeat"(
257
+ $1::BIGINT,
258
+ $2::BIGINT,
259
+ $3::BIGINT
260
+ )
261
+ `.value, [
262
+ this.id.toString(),
263
+ this.numAttempts,
264
+ this.lockMs
265
+ ]).then((res) => res.rows[0]);
266
+ if (result.result_code === 4) return { resultType: "MESSAGE_NOT_FOUND" };
267
+ else if (result.result_code === 5) return { resultType: "MESSAGE_STATE_INVALID" };
268
+ else if (result.result_code === 8) return { resultType: "MESSAGE_HEARTBEATED" };
269
+ else throw new Error("Unexpected result");
270
+ }
271
+ };
272
+ //#endregion
273
+ //#region src/install/function/epoch.ts
274
+ const epoch = (params) => {
275
+ return [fragment`
276
+ CREATE FUNCTION ${ref(params.schema)}."epoch" ()
277
+ RETURNS BIGINT AS $$
278
+ DECLARE
279
+ v_now TIMESTAMPTZ;
280
+ BEGIN
281
+ v_now := NOW();
282
+ RETURN
283
+ EXTRACT(EPOCH FROM v_now)::BIGINT * 1_000 +
284
+ EXTRACT(MILLISECOND FROM v_now)::BIGINT;
285
+ END;
286
+ $$ LANGUAGE plpgsql;
287
+ `];
288
+ };
289
+ //#endregion
290
+ //#region src/install/function/channel/release.ts
291
+ const release = (params) => {
292
+ return [fragment`
293
+ CREATE FUNCTION ${ref(params.schema)}."channel_release" (
294
+ p_text TEXT
295
+ ) RETURNS TABLE (
296
+ result_code INTEGER
297
+ ) AS $$
298
+ DECLARE
299
+ v_channel RECORD;
300
+ BEGIN
301
+ SELECT
302
+ "channel"."id",
303
+ "channel"."current_size"
304
+ FROM ${ref(params.schema)}."channel"
305
+ WHERE "name" = p_text
306
+ AND NOT "released"
307
+ FOR UPDATE
308
+ INTO v_channel;
309
+
310
+ IF v_channel."id" IS NULL THEN
311
+ RETURN QUERY SELECT
312
+ ${value(9)};
313
+ RETURN;
314
+ END IF;
315
+
316
+ IF v_channel."current_size" = 0 THEN
317
+ DELETE FROM ${ref(params.schema)}."channel"
318
+ WHERE "id" = v_channel."id";
319
+ ELSE
320
+ UPDATE ${ref(params.schema)}."channel" SET
321
+ "released" = TRUE
322
+ WHERE "id" = v_channel."id";
323
+ END IF;
324
+
325
+ RETURN QUERY SELECT
326
+ ${value(10)};
327
+ RETURN;
328
+ END;
329
+ $$ LANGUAGE plpgsql;
330
+ `];
331
+ };
332
+ //#endregion
333
+ //#region src/install/function/channel/set.ts
334
+ const set = (params) => {
335
+ return [fragment`
336
+ CREATE FUNCTION ${ref(params.schema)}."channel_set" (
337
+ p_text TEXT,
338
+ p_max_concurrency INTEGER,
339
+ p_max_size INTEGER,
340
+ p_release_interval_ms INTEGER
341
+ ) RETURNS VOID AS $$
342
+ BEGIN
343
+ INSERT INTO ${ref(params.schema)}."channel" (
344
+ "name",
345
+ "max_concurrency",
346
+ "max_size",
347
+ "current_concurrency",
348
+ "current_size",
349
+ "release_interval_ms",
350
+ "released",
351
+ "dequeue_prev_at"
352
+ ) VALUES (
353
+ p_text,
354
+ p_max_concurrency,
355
+ p_max_size,
356
+ 0,
357
+ 0,
358
+ p_release_interval_ms,
359
+ FALSE,
360
+ 0
361
+ ) ON CONFLICT ("name") WHERE NOT "released" DO UPDATE SET
362
+ "max_concurrency" = EXCLUDED."max_concurrency",
363
+ "max_size" = EXCLUDED."max_size",
364
+ "release_interval_ms" = EXCLUDED."release_interval_ms",
365
+ "released" = EXCLUDED."released",
366
+ "dequeue_next_at" = CASE
367
+ WHEN "channel"."message_id" IS NULL THEN NULL
368
+ ELSE GREATEST(
369
+ "channel"."message_dequeue_at",
370
+ "channel"."dequeue_prev_at" + EXCLUDED."release_interval_ms"
371
+ )
372
+ END;
373
+ END;
374
+ $$ LANGUAGE plpgsql;
375
+ `];
376
+ };
377
+ //#endregion
378
+ //#region src/install/function/message/create.ts
379
+ const create = (params) => {
380
+ return [fragment`
381
+ CREATE FUNCTION ${ref(params.schema)}."message_create" (
382
+ p_channel TEXT,
383
+ p_content BYTEA,
384
+ p_dequeue_at BIGINT
385
+ ) RETURNS TABLE (
386
+ result_code INTEGER,
387
+ metadata JSON
388
+ ) AS $$
389
+ DECLARE
390
+ v_now BIGINT;
391
+ v_dequeue_at BIGINT;
392
+ v_channel RECORD;
393
+ v_message RECORD;
394
+ BEGIN
395
+ v_now := ${ref(params.schema)}."epoch"();
396
+ v_dequeue_at := COALESCE(p_dequeue_at, v_now);
397
+
398
+ SELECT
399
+ "channel"."id",
400
+ "channel"."current_concurrency",
401
+ "channel"."current_size",
402
+ "channel"."max_concurrency",
403
+ "channel"."max_size",
404
+ "channel"."release_interval_ms",
405
+ "channel"."dequeue_prev_at",
406
+ "channel"."message_id",
407
+ "channel"."message_dequeue_at"
408
+ FROM ${ref(params.schema)}."channel"
409
+ WHERE "name" = p_channel
410
+ AND NOT "released"
411
+ FOR UPDATE
412
+ INTO v_channel;
413
+
414
+ IF v_channel."id" IS NULL THEN
415
+ RETURN QUERY SELECT
416
+ ${value(9)},
417
+ NULL::JSON;
418
+ RETURN;
419
+ END IF;
420
+
421
+ IF v_channel."current_size" >= v_channel."max_size" THEN
422
+ RETURN QUERY SELECT
423
+ ${value(1)},
424
+ NULL::JSON;
425
+ RETURN;
426
+ END IF;
427
+
428
+ INSERT INTO ${ref(params.schema)}."message" (
429
+ "channel_id",
430
+ "content",
431
+ "num_attempts",
432
+ "dequeue_at"
433
+ ) VALUES (
434
+ v_channel."id",
435
+ p_content,
436
+ 0,
437
+ v_dequeue_at
438
+ ) RETURNING
439
+ "id"
440
+ INTO v_message;
441
+
442
+ IF
443
+ v_channel."message_id" IS NULL OR
444
+ v_dequeue_at < v_channel."message_dequeue_at" OR
445
+ v_dequeue_at = v_channel."message_dequeue_at" AND v_message."id" < v_channel."message_id"
446
+ THEN
447
+ UPDATE ${ref(params.schema)}."channel" SET
448
+ "current_size" = v_channel."current_size" + 1,
449
+ "message_id" = v_message."id",
450
+ "message_dequeue_at" = v_dequeue_at,
451
+ "dequeue_next_at" = GREATEST(
452
+ v_channel."dequeue_prev_at" + v_channel."release_interval_ms",
453
+ v_dequeue_at
454
+ )
455
+ WHERE "id" = v_channel."id";
456
+ ELSE
457
+ UPDATE ${ref(params.schema)}."channel" SET
458
+ "current_size" = v_channel."current_size" + 1
459
+ WHERE "id" = v_channel."id";
460
+ END IF;
461
+
462
+ IF ${value(params.eventChannel !== null)} THEN
463
+ PERFORM PG_NOTIFY(
464
+ ${value(params.eventChannel)},
465
+ JSON_BUILD_OBJECT(
466
+ 'type', ${value(0)},
467
+ 'id', v_message."id"::TEXT,
468
+ 'dequeue_at', v_dequeue_at
469
+ )::TEXT
470
+ );
471
+ END IF;
472
+
473
+ RETURN QUERY SELECT
474
+ ${value(0)},
475
+ JSON_BUILD_OBJECT(
476
+ 'id', v_message."id"::TEXT
477
+ );
478
+ END;
479
+ $$ LANGUAGE plpgsql;
480
+ `];
481
+ };
482
+ //#endregion
483
+ //#region src/install/function/message/defer.ts
484
+ const defer = (params) => {
485
+ return [fragment`
486
+ CREATE FUNCTION ${ref(params.schema)}."message_defer" (
487
+ p_id BIGINT,
488
+ p_num_attempts BIGINT,
489
+ p_dequeue_at BIGINT,
490
+ p_state BYTEA
491
+ )
492
+ RETURNS TABLE (
493
+ result_code INTEGER
494
+ ) AS $$
495
+ DECLARE
496
+ v_now BIGINT;
497
+ v_channel RECORD;
498
+ v_message RECORD;
499
+ v_dequeue_at BIGINT;
500
+ BEGIN
501
+ v_now := ${ref(params.schema)}."epoch"();
502
+
503
+ SELECT
504
+ "message"."id",
505
+ "message"."channel_id",
506
+ "message"."num_attempts",
507
+ "message"."unlock_at"
508
+ FROM ${ref(params.schema)}."message"
509
+ WHERE "id" = p_id
510
+ FOR UPDATE
511
+ INTO v_message;
512
+
513
+ IF v_message."id" IS NULL THEN
514
+ RETURN QUERY SELECT
515
+ ${value(4)};
516
+ RETURN;
517
+ ELSIF v_message."unlock_at" IS NULL OR v_message."num_attempts" <> p_num_attempts THEN
518
+ RETURN QUERY SELECT
519
+ ${value(5)};
520
+ RETURN;
521
+ END IF;
522
+
523
+ SELECT
524
+ "channel"."current_concurrency",
525
+ "channel"."release_interval_ms",
526
+ "channel"."message_id",
527
+ "channel"."message_dequeue_at",
528
+ "channel"."dequeue_prev_at"
529
+ FROM ${ref(params.schema)}."channel"
530
+ WHERE "id" = v_message."channel_id"
531
+ FOR UPDATE
532
+ INTO v_channel;
533
+
534
+ v_dequeue_at := COALESCE(p_dequeue_at, v_now);
535
+
536
+ IF
537
+ v_channel."message_id" IS NULL OR
538
+ v_dequeue_at < v_channel."message_dequeue_at" OR
539
+ v_dequeue_at = v_channel."message_dequeue_at" AND v_message."id" < v_channel."message_id"
540
+ THEN
541
+ UPDATE ${ref(params.schema)}."channel" SET
542
+ "current_concurrency" = v_channel."current_concurrency" - 1,
543
+ "message_id" = v_message."id",
544
+ "message_dequeue_at" = v_dequeue_at,
545
+ "dequeue_next_at" = GREATEST(
546
+ v_channel."dequeue_prev_at" + v_channel."release_interval_ms",
547
+ v_dequeue_at
548
+ )
549
+ WHERE "id" = v_message."channel_id";
550
+ ELSE
551
+ UPDATE ${ref(params.schema)}."channel" SET
552
+ "current_concurrency" = v_channel."current_concurrency" - 1
553
+ WHERE "id" = v_message."channel_id";
554
+ END IF;
555
+
556
+ UPDATE ${ref(params.schema)}."message" SET
557
+ "state" = p_state,
558
+ "unlock_at" = NULL,
559
+ "dequeue_at" = v_dequeue_at
560
+ WHERE "id" = p_id;
561
+
562
+ IF ${value(params.eventChannel !== null)} THEN
563
+ PERFORM PG_NOTIFY(
564
+ ${value(params.eventChannel)},
565
+ JSON_BUILD_OBJECT(
566
+ 'type', ${value(7)},
567
+ 'dequeue_at', v_dequeue_at,
568
+ 'id', p_id::TEXT
569
+ )::TEXT
570
+ );
571
+ END IF;
572
+
573
+ RETURN QUERY SELECT
574
+ ${value(7)};
575
+ RETURN;
576
+ END;
577
+ $$ LANGUAGE plpgsql;
578
+ `];
579
+ };
580
+ //#endregion
581
+ //#region src/install/function/message/dequeue.ts
582
+ const queryNextLockedMessage = (params) => fragment`
583
+ SELECT
584
+ "message"."id",
585
+ "message"."state",
586
+ "message"."content",
587
+ "message"."channel_id",
588
+ "message"."unlock_at",
589
+ "message"."num_attempts"
590
+ FROM ${ref(params.schema)}."message"
591
+ WHERE "unlock_at" IS NOT NULL
592
+ AND "unlock_at" <= ${params.now}
593
+ ORDER BY "unlock_at" ASC
594
+ `;
595
+ const queryNextChannel = (params) => fragment`
596
+ SELECT
597
+ "channel"."id",
598
+ "channel"."release_interval_ms",
599
+ "channel"."message_id",
600
+ "channel"."dequeue_next_at",
601
+ "channel"."dequeue_prev_at",
602
+ "channel"."current_concurrency"
603
+ FROM ${ref(params.schema)}."channel"
604
+ WHERE "message_id" IS NOT NULL
605
+ AND "current_concurrency" < "max_concurrency"
606
+ ORDER BY "dequeue_next_at" ASC
607
+ `;
608
+ const queryNextChannelMessage = (params) => fragment`
609
+ SELECT
610
+ "message"."id",
611
+ "message"."dequeue_at"
612
+ FROM ${ref(params.schema)}."message"
613
+ WHERE "unlock_at" IS NULL
614
+ AND "channel_id" = ${params.channel}
615
+ ORDER BY "dequeue_at" ASC, "id" ASC
616
+ `;
617
+ const dequeue = (params) => {
618
+ const nextLockedMessage = queryNextLockedMessage({
619
+ now: fragment`v_now`,
620
+ schema: params.schema
621
+ });
622
+ const nextChannelMessage = queryNextChannelMessage({
623
+ channel: fragment`v_channel."id"`,
624
+ schema: params.schema
625
+ });
626
+ const nextChannel = queryNextChannel({ schema: params.schema });
627
+ return [fragment`
628
+ CREATE FUNCTION ${ref(params.schema)}."message_dequeue" (
629
+ p_lock_ms BIGINT
630
+ )
631
+ RETURNS TABLE (
632
+ result_code INTEGER,
633
+ content BYTEA,
634
+ state BYTEA,
635
+ metadata JSON
636
+ ) AS $$
637
+ DECLARE
638
+ v_now BIGINT;
639
+ v_channel RECORD;
640
+ v_message_locked RECORD;
641
+ v_message_dequeue RECORD;
642
+ v_message_next RECORD;
643
+ BEGIN
644
+ v_now := ${ref(params.schema)}."epoch"();
645
+
646
+ ${nextLockedMessage}
647
+ FOR UPDATE
648
+ SKIP LOCKED
649
+ LIMIT 1
650
+ INTO v_message_locked;
651
+
652
+ IF v_message_locked."id" IS NOT NULL THEN
653
+ UPDATE ${ref(params.schema)}."message" SET
654
+ "num_attempts" = v_message_locked."num_attempts" + 1,
655
+ "unlock_at" = v_now + p_lock_ms
656
+ WHERE "id" = v_message_locked."id";
657
+
658
+ RETURN QUERY SELECT
659
+ ${value(3)},
660
+ v_message_locked.content,
661
+ v_message_locked.state,
662
+ JSON_BUILD_OBJECT(
663
+ 'id', v_message_locked."id",
664
+ 'is_unlocked', TRUE,
665
+ 'num_attempts', v_message_locked."num_attempts" + 1
666
+ );
667
+ RETURN;
668
+ END IF;
669
+
670
+ ${nextChannel}
671
+ FOR UPDATE
672
+ SKIP LOCKED
673
+ LIMIT 1
674
+ INTO v_channel;
675
+
676
+ IF v_channel."id" IS NULL OR v_channel."dequeue_next_at" > v_now THEN
677
+ RETURN QUERY SELECT
678
+ ${value(2)},
679
+ NULL::BYTEA,
680
+ NULL::BYTEA,
681
+ NULL::JSON;
682
+ RETURN;
683
+ END IF;
684
+
685
+ SELECT
686
+ "message"."id",
687
+ "message"."channel_id",
688
+ "message"."content",
689
+ "message"."num_attempts",
690
+ "message"."state"
691
+ FROM ${ref(params.schema)}."message"
692
+ WHERE "id" = v_channel."message_id"
693
+ INTO v_message_dequeue;
694
+
695
+ UPDATE ${ref(params.schema)}."message" SET
696
+ "num_attempts" = v_message_dequeue."num_attempts" + 1,
697
+ "unlock_at" = v_now + p_lock_ms
698
+ WHERE "id" = v_message_dequeue."id";
699
+
700
+ ${nextChannelMessage}
701
+ LIMIT 1
702
+ INTO v_message_next;
703
+
704
+ IF v_message_next."id" IS NULL THEN
705
+ UPDATE ${ref(params.schema)}."channel" SET
706
+ "current_concurrency" = v_channel."current_concurrency" + 1,
707
+ "dequeue_prev_at" = v_now,
708
+ "message_id" = NULL
709
+ WHERE "id" = v_channel."id";
710
+ ELSE
711
+ UPDATE ${ref(params.schema)}."channel" SET
712
+ "current_concurrency" = v_channel."current_concurrency" + 1,
713
+ "message_id" = v_message_next."id",
714
+ "message_dequeue_at" = v_message_next."dequeue_at",
715
+ "dequeue_prev_at" = v_now,
716
+ "dequeue_next_at" = GREATEST(
717
+ v_message_next."dequeue_at",
718
+ v_now + v_channel."release_interval_ms"
719
+ )
720
+ WHERE "id" = v_channel."id";
721
+ END IF;
722
+
723
+ RETURN QUERY SELECT
724
+ ${value(3)},
725
+ v_message_dequeue."content",
726
+ v_message_dequeue."state",
727
+ JSON_BUILD_OBJECT(
728
+ 'id', v_message_dequeue."id"::TEXT,
729
+ 'is_unlocked', FALSE,
730
+ 'num_attempts', v_message_dequeue."num_attempts" + 1
731
+ );
732
+ RETURN;
733
+ END;
734
+ $$ LANGUAGE plpgsql;
735
+ `];
736
+ };
737
+ //#endregion
738
+ //#region src/install/function/message/heartbeat.ts
739
+ const heartbeat = (params) => {
740
+ return [fragment`
741
+ CREATE FUNCTION ${ref(params.schema)}."message_heartbeat" (
742
+ p_id BIGINT,
743
+ p_num_attempts BIGINT,
744
+ p_lock_ms BIGINT
745
+ )
746
+ RETURNS TABLE (
747
+ result_code INTEGER
748
+ ) AS $$
749
+ DECLARE
750
+ v_now BIGINT;
751
+ v_message RECORD;
752
+ BEGIN
753
+ v_now := ${ref(params.schema)}."epoch"();
754
+
755
+ SELECT
756
+ "message"."id",
757
+ "message"."num_attempts",
758
+ "message"."unlock_at"
759
+ FROM ${ref(params.schema)}."message"
760
+ WHERE "id" = p_id
761
+ FOR UPDATE
762
+ INTO v_message;
763
+
764
+ IF v_message."id" IS NULL THEN
765
+ RETURN QUERY SELECT
766
+ ${value(4)};
767
+ RETURN;
768
+ ELSIF v_message."unlock_at" IS NULL OR v_message."num_attempts" <> p_num_attempts THEN
769
+ RETURN QUERY SELECT
770
+ ${value(5)};
771
+ RETURN;
772
+ END IF;
773
+
774
+ UPDATE ${ref(params.schema)}."message" SET
775
+ "unlock_at" = GREATEST(
776
+ v_now + p_lock_ms,
777
+ v_message."unlock_at"
778
+ )
779
+ WHERE "id" = p_id;
780
+
781
+ RETURN QUERY SELECT
782
+ ${value(8)};
783
+ RETURN;
784
+ END;
785
+ $$ LANGUAGE plpgsql;
786
+ `];
787
+ };
788
+ //#endregion
789
+ //#region src/install/function/message/complete.ts
790
+ const complete = (params) => {
791
+ return [fragment`
792
+ CREATE FUNCTION ${ref(params.schema)}."message_complete" (
793
+ p_id BIGINT,
794
+ p_num_attempts BIGINT
795
+ )
796
+ RETURNS TABLE (
797
+ result_code INTEGER
798
+ ) AS $$
799
+ DECLARE
800
+ v_channel RECORD;
801
+ v_message RECORD;
802
+ BEGIN
803
+ SELECT
804
+ "message"."id",
805
+ "message"."channel_id",
806
+ "message"."num_attempts",
807
+ "message"."unlock_at"
808
+ FROM ${ref(params.schema)}."message"
809
+ WHERE "id" = p_id
810
+ FOR UPDATE
811
+ INTO v_message;
812
+
813
+ IF v_message."id" IS NULL THEN
814
+ RETURN QUERY SELECT
815
+ ${value(4)};
816
+ RETURN;
817
+ ELSIF v_message."unlock_at" IS NULL OR v_message."num_attempts" <> p_num_attempts THEN
818
+ RETURN QUERY SELECT
819
+ ${value(5)};
820
+ RETURN;
821
+ END IF;
822
+
823
+ SELECT
824
+ "channel"."id",
825
+ "channel"."released",
826
+ "channel"."current_size",
827
+ "channel"."current_concurrency"
828
+ FROM ${ref(params.schema)}."channel"
829
+ WHERE "id" = v_message."channel_id"
830
+ FOR UPDATE
831
+ INTO v_channel;
832
+
833
+ IF v_channel."released" AND v_channel."current_size" = 1 THEN
834
+ DELETE FROM ${ref(params.schema)}."channel"
835
+ WHERE "id" = v_channel."id";
836
+ ELSE
837
+ UPDATE ${ref(params.schema)}."channel" SET
838
+ "current_concurrency" = v_channel."current_concurrency" - 1,
839
+ "current_size" = v_channel."current_size" - 1
840
+ WHERE "id" = v_channel."id";
841
+ END IF;
842
+
843
+ IF ${value(params.eventChannel !== null)} THEN
844
+ PERFORM PG_NOTIFY(
845
+ ${value(params.eventChannel)},
846
+ JSON_BUILD_OBJECT(
847
+ 'type', ${value(6)},
848
+ 'id', p_id::TEXT
849
+ )::TEXT
850
+ );
851
+ END IF;
852
+
853
+ DELETE FROM ${ref(params.schema)}."message"
854
+ WHERE "id" = p_id;
855
+
856
+ RETURN QUERY SELECT
857
+ ${value(6)};
858
+ RETURN;
859
+ END;
860
+ $$ LANGUAGE plpgsql;
861
+ `];
862
+ };
863
+ //#endregion
864
+ //#region src/install/table/channel.ts
865
+ const channel = (params) => {
866
+ const dequeueIndex = [params.schema, "channel_dequeue_ix"].join("_");
867
+ const nameIndex = [params.schema, "channel_name_ix"].join("_");
868
+ return [
869
+ fragment`
870
+ CREATE TABLE ${ref(params.schema)}."channel" (
871
+ "id" BIGSERIAL NOT NULL,
872
+ "name" TEXT NOT NULL,
873
+ "released" BOOLEAN NOT NULL,
874
+ "max_concurrency" INTEGER NOT NULL,
875
+ "max_size" INTEGER NOT NULL,
876
+ "release_interval_ms" INTEGER NOT NULL,
877
+ "current_size" INTEGER NOT NULL,
878
+ "current_concurrency" INTEGER NOT NULL,
879
+ "message_id" BIGINT,
880
+ "message_dequeue_at" BIGINT,
881
+ "dequeue_prev_at" BIGINT NOT NULL,
882
+ "dequeue_next_at" BIGINT NULL,
883
+ PRIMARY KEY ("id")
884
+ );
885
+ `,
886
+ fragment`
887
+ CREATE UNIQUE INDEX ${ref(nameIndex)}
888
+ ON ${ref(params.schema)}."channel" (
889
+ "name"
890
+ ) WHERE NOT "released";
891
+ `,
892
+ fragment`
893
+ CREATE INDEX ${ref(dequeueIndex)}
894
+ ON ${ref(params.schema)}."channel" (
895
+ "dequeue_next_at" ASC
896
+ ) WHERE "message_id" IS NOT NULL
897
+ AND "current_concurrency" < "max_concurrency";
898
+ `
899
+ ];
900
+ };
901
+ //#endregion
902
+ //#region src/install/table/message.ts
903
+ const message = (params) => {
904
+ return [
905
+ fragment`
906
+ CREATE TABLE ${ref(params.schema)}."message" (
907
+ "id" BIGSERIAL NOT NULL,
908
+ "channel_id" BIGINT NOT NULL,
909
+ "content" BYTEA NOT NULL,
910
+ "state" BYTEA,
911
+ "num_attempts" BIGINT NOT NULL,
912
+ "dequeue_at" BIGINT NOT NULL,
913
+ "unlock_at" BIGINT,
914
+ PRIMARY KEY ("id")
915
+ );
916
+ `,
917
+ fragment`
918
+ CREATE INDEX "message_dequeue_ix"
919
+ ON ${ref(params.schema)}."message" (
920
+ "channel_id",
921
+ "dequeue_at" ASC,
922
+ "id" ASC
923
+ ) WHERE "unlock_at" IS NULL;
924
+ `,
925
+ fragment`
926
+ CREATE INDEX "message_locked_dequeue_ix"
927
+ ON ${ref(params.schema)}."message" (
928
+ "unlock_at" ASC
929
+ ) WHERE "unlock_at" IS NOT NULL;
930
+ `
931
+ ];
932
+ };
933
+ //#endregion
934
+ //#region src/queue/module/channel/message.ts
935
+ var Message$1 = class {
936
+ schema;
937
+ channel;
938
+ adaptor;
939
+ constructor(params) {
940
+ this.schema = params.schema;
941
+ this.adaptor = params.adaptor;
942
+ this.channel = params.channel;
943
+ }
944
+ async create(params) {
945
+ const adaptedClient = this.adaptor(params.databaseClient);
946
+ return await new Create({
947
+ schema: this.schema,
948
+ channel: this.channel,
949
+ content: params.content,
950
+ dequeueAt: params.dequeueAt ?? null
951
+ }).execute(adaptedClient);
952
+ }
953
+ };
954
+ //#endregion
955
+ //#region src/queue/module/channel/index.ts
956
+ var Channel = class {
957
+ schema;
958
+ adaptor;
959
+ channel;
960
+ message;
961
+ constructor(params) {
962
+ this.schema = params.schema;
963
+ this.adaptor = params.adaptor;
964
+ this.channel = params.channel;
965
+ this.message = new Message$1({
966
+ schema: params.schema,
967
+ adaptor: params.adaptor,
968
+ channel: params.channel
969
+ });
970
+ }
971
+ set(params) {
972
+ const adaptedClient = this.adaptor(params.databaseClient);
973
+ return new Set({
974
+ schema: this.schema,
975
+ channel: this.channel,
976
+ maxConcurrency: params.maxConcurrency ?? 2147483647,
977
+ maxSize: params.maxSize ?? 2147483647,
978
+ releaseIntervalMs: params.releaseIntervalMs ?? 0
979
+ }).execute(adaptedClient);
980
+ }
981
+ release(params) {
982
+ const adaptedClient = this.adaptor(params.databaseClient);
983
+ return new Release({
984
+ schema: this.schema,
985
+ channel: this.channel
986
+ }).execute(adaptedClient);
987
+ }
988
+ };
989
+ //#endregion
990
+ //#region src/event.ts
991
+ const decode = (payload) => {
992
+ const parsed = JSON.parse(payload);
993
+ if (parsed.type === 0) return {
994
+ eventType: "MESSAGE_CREATED",
995
+ id: parsed.id,
996
+ dequeueAt: Number(parsed.dequeue_at)
997
+ };
998
+ else if (parsed.type === 6) return {
999
+ eventType: "MESSAGE_COMPLETED",
1000
+ id: parsed.id
1001
+ };
1002
+ else if (parsed.type === 7) return {
1003
+ eventType: "MESSAGE_DEFERRED",
1004
+ id: parsed.id,
1005
+ dequeueAt: Number(parsed.dequeue_at)
1006
+ };
1007
+ else throw new Error("Unknown event type");
1008
+ };
1009
+ //#endregion
1010
+ //#region src/queue/message.ts
1011
+ var Message = class {
1012
+ schema;
1013
+ adaptor;
1014
+ id;
1015
+ isUnlocked;
1016
+ content;
1017
+ state;
1018
+ numAttempts;
1019
+ constructor(params) {
1020
+ this.schema = params.schema;
1021
+ this.adaptor = params.adaptor;
1022
+ this.id = params.id;
1023
+ this.isUnlocked = params.isUnlocked;
1024
+ this.content = params.content;
1025
+ this.state = params.state;
1026
+ this.numAttempts = params.numAttempts;
1027
+ }
1028
+ async defer(params) {
1029
+ const adaptedClient = this.adaptor(params.databaseClient);
1030
+ return new Defer({
1031
+ schema: this.schema,
1032
+ id: this.id,
1033
+ numAttempts: this.numAttempts,
1034
+ dequeueAt: params.dequeueAt ?? null,
1035
+ state: params.state ?? null
1036
+ }).execute(adaptedClient);
1037
+ }
1038
+ async complete(params) {
1039
+ const adaptedClient = this.adaptor(params.databaseClient);
1040
+ return new Complete({
1041
+ schema: this.schema,
1042
+ numAttempts: this.numAttempts,
1043
+ id: this.id
1044
+ }).execute(adaptedClient);
1045
+ }
1046
+ async heartbeat(params) {
1047
+ const adaptedClient = this.adaptor(params.databaseClient);
1048
+ return new Heartbeat({
1049
+ schema: this.schema,
1050
+ id: this.id,
1051
+ numAttempts: this.numAttempts,
1052
+ lockMs: params.lockMs
1053
+ }).execute(adaptedClient);
1054
+ }
1055
+ };
1056
+ //#endregion
1057
+ //#region src/queue/index.ts
1058
+ var Queue = class {
1059
+ schema;
1060
+ lockMs;
1061
+ adaptor;
1062
+ constructor(params) {
1063
+ this.schema = params.schema;
1064
+ this.lockMs = params.lockMs;
1065
+ this.adaptor = params.adaptor ? params.adaptor : (x) => x;
1066
+ }
1067
+ async dequeue(params) {
1068
+ const dequeue = new Dequeue({
1069
+ schema: this.schema,
1070
+ lockMs: this.lockMs
1071
+ });
1072
+ const adaptedClient = this.adaptor(params.databaseClient);
1073
+ const result = await dequeue.execute(adaptedClient);
1074
+ if (result.resultType === "MESSAGE_DEQUEUED") return {
1075
+ resultType: "MESSAGE_DEQUEUED",
1076
+ message: new Message({
1077
+ schema: this.schema,
1078
+ adaptor: this.adaptor,
1079
+ id: result.id,
1080
+ isUnlocked: result.isUnlocked,
1081
+ content: result.content,
1082
+ state: result.state,
1083
+ numAttempts: result.numAttempts
1084
+ })
1085
+ };
1086
+ else return result;
1087
+ }
1088
+ channel(channel) {
1089
+ return new Channel({
1090
+ adaptor: this.adaptor,
1091
+ schema: this.schema,
1092
+ channel
1093
+ });
1094
+ }
1095
+ install(params = {}) {
1096
+ return [
1097
+ channel,
1098
+ message,
1099
+ epoch,
1100
+ set,
1101
+ release,
1102
+ create,
1103
+ dequeue,
1104
+ complete,
1105
+ defer,
1106
+ heartbeat
1107
+ ].flatMap((install) => install({
1108
+ schema: this.schema,
1109
+ eventChannel: params.eventChannel ?? null
1110
+ })).map((sql) => dedent(sql.value));
1111
+ }
1112
+ static decode(payload) {
1113
+ return decode(payload);
1114
+ }
1115
+ };
1116
+ //#endregion
1117
+ export { Queue };