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.d.mts +331 -0
- package/dist/index.mjs +1181 -0
- package/package.json +21 -17
- package/README.md +0 -356
- package/dist/index.cjs +0 -666
- package/dist/index.d.ts +0 -281
- package/dist/index.js +0 -666
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 };
|