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