@nagi-js/pgmq 0.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/LICENSE +21 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +117 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lymo, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Queue, Tx, Millis } from '@nagi-js/core';
|
|
2
|
+
import { Kysely } from 'kysely';
|
|
3
|
+
|
|
4
|
+
interface PgmqQueueOpts {
|
|
5
|
+
/** Kysely instance. The adapter does NOT own the connection lifecycle. */
|
|
6
|
+
readonly db: Kysely<unknown>;
|
|
7
|
+
/** PGMQ queue name. Default: "nagi". */
|
|
8
|
+
readonly queueName?: string;
|
|
9
|
+
/** Visibility timeout applied on every dequeue, in ms. Default: 30_000. */
|
|
10
|
+
readonly visibilityTimeoutMs?: Millis;
|
|
11
|
+
/**
|
|
12
|
+
* Use `pgmq.create_partitioned()` instead of `pgmq.create()` when
|
|
13
|
+
* `ensureSchema()` runs. Default: false.
|
|
14
|
+
*/
|
|
15
|
+
readonly partitioned?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Use `pgmq.archive()` instead of `pgmq.delete()` on ack — retains messages
|
|
18
|
+
* in the archive table for audit. Default: false.
|
|
19
|
+
*/
|
|
20
|
+
readonly archiveOnAck?: boolean;
|
|
21
|
+
}
|
|
22
|
+
interface PgmqQueue extends Queue {
|
|
23
|
+
/**
|
|
24
|
+
* Idempotent setup: installs the pgmq extension and creates the queue.
|
|
25
|
+
* Requires database privileges to `CREATE EXTENSION`. Suitable for dev/test;
|
|
26
|
+
* production should run these statements out-of-band.
|
|
27
|
+
*/
|
|
28
|
+
ensureSchema(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Returns a `Queue` whose operations execute on the supplied transaction.
|
|
31
|
+
* Pass `ctx.tx` to enqueue messages atomically with the handler's domain
|
|
32
|
+
* writes — the pgmq message commits with `step.completed` or rolls back
|
|
33
|
+
* with the handler. The user must have wired `@nagi-js/postgres` and
|
|
34
|
+
* augmented `Register.tx` so that `Tx` is a Kysely transaction at compile
|
|
35
|
+
* time; at runtime, `tx` must be the same Kysely handle the postgres store
|
|
36
|
+
* handed to `runStep`.
|
|
37
|
+
*/
|
|
38
|
+
withTx(tx: Tx): Queue;
|
|
39
|
+
}
|
|
40
|
+
declare function pgmqQueue(opts: PgmqQueueOpts): PgmqQueue;
|
|
41
|
+
|
|
42
|
+
export { type PgmqQueue, type PgmqQueueOpts, pgmqQueue };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { sql } from 'kysely';
|
|
2
|
+
|
|
3
|
+
// src/pgmq-queue.ts
|
|
4
|
+
var DEFAULT_QUEUE_NAME = "nagi";
|
|
5
|
+
var DEFAULT_VISIBILITY_TIMEOUT_MS = 3e4;
|
|
6
|
+
function pgmqQueue(opts) {
|
|
7
|
+
const db = opts.db;
|
|
8
|
+
const queueName = opts.queueName ?? DEFAULT_QUEUE_NAME;
|
|
9
|
+
const vtSeconds = Math.max(
|
|
10
|
+
1,
|
|
11
|
+
Math.ceil(
|
|
12
|
+
(opts.visibilityTimeoutMs ?? DEFAULT_VISIBILITY_TIMEOUT_MS) / 1e3
|
|
13
|
+
)
|
|
14
|
+
);
|
|
15
|
+
const partitioned = opts.partitioned ?? false;
|
|
16
|
+
const archiveOnAck = opts.archiveOnAck ?? false;
|
|
17
|
+
const config = { queueName, vtSeconds, archiveOnAck };
|
|
18
|
+
return {
|
|
19
|
+
...buildQueue(db, config),
|
|
20
|
+
async ensureSchema() {
|
|
21
|
+
await sql`CREATE EXTENSION IF NOT EXISTS pgmq`.execute(db);
|
|
22
|
+
if (partitioned) {
|
|
23
|
+
await sql`SELECT pgmq.create_partitioned(${queueName})`.execute(db);
|
|
24
|
+
} else {
|
|
25
|
+
await sql`SELECT pgmq.create(${queueName})`.execute(db);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
withTx(tx) {
|
|
29
|
+
return buildQueue(tx, config);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function buildQueue(executor, config) {
|
|
34
|
+
const { queueName, vtSeconds, archiveOnAck } = config;
|
|
35
|
+
return {
|
|
36
|
+
async enqueue(runId, stepId, options) {
|
|
37
|
+
const envelope = {
|
|
38
|
+
runId,
|
|
39
|
+
stepId,
|
|
40
|
+
attempt: options?.attempt ?? 1
|
|
41
|
+
};
|
|
42
|
+
const delaySeconds = Math.max(
|
|
43
|
+
0,
|
|
44
|
+
Math.ceil((options?.delayMs ?? 0) / 1e3)
|
|
45
|
+
);
|
|
46
|
+
await sql`SELECT pgmq.send(${queueName}, ${JSON.stringify(envelope)}::jsonb, ${delaySeconds}::int)`.execute(
|
|
47
|
+
executor
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
async dequeue({
|
|
51
|
+
count
|
|
52
|
+
}) {
|
|
53
|
+
const { rows } = await sql`SELECT msg_id, message FROM pgmq.read(${queueName}, ${vtSeconds}::int, ${count}::int)`.execute(
|
|
54
|
+
executor
|
|
55
|
+
);
|
|
56
|
+
return rows.map((row) => projectMessage(row.msg_id, row.message));
|
|
57
|
+
},
|
|
58
|
+
async ack(receipt) {
|
|
59
|
+
const msgId = parseReceipt(receipt);
|
|
60
|
+
if (archiveOnAck) {
|
|
61
|
+
await sql`SELECT pgmq.archive(${queueName}, ${msgId}::bigint)`.execute(
|
|
62
|
+
executor
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
await sql`SELECT pgmq.delete(${queueName}, ${msgId}::bigint)`.execute(
|
|
66
|
+
executor
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
async nack(receipt, options) {
|
|
71
|
+
const msgId = parseReceipt(receipt);
|
|
72
|
+
const delaySeconds = Math.max(
|
|
73
|
+
0,
|
|
74
|
+
Math.ceil((options?.delayMs ?? 0) / 1e3)
|
|
75
|
+
);
|
|
76
|
+
await sql`SELECT pgmq.set_vt(${queueName}, ${msgId}::bigint, ${delaySeconds}::int)`.execute(
|
|
77
|
+
executor
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
async extend(receipt, leaseMs) {
|
|
81
|
+
const msgId = parseReceipt(receipt);
|
|
82
|
+
const vt = Math.max(1, Math.ceil(leaseMs / 1e3));
|
|
83
|
+
await sql`SELECT pgmq.set_vt(${queueName}, ${msgId}::bigint, ${vt}::int)`.execute(
|
|
84
|
+
executor
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function projectMessage(rawMsgId, raw) {
|
|
90
|
+
if (raw === null || typeof raw !== "object" || typeof raw.runId !== "string" || typeof raw.stepId !== "string" || typeof raw.attempt !== "number") {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`pgmq: malformed message envelope ${JSON.stringify(raw)} \u2014 expected { runId, stepId, attempt }`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const envelope = raw;
|
|
96
|
+
return {
|
|
97
|
+
receipt: String(rawMsgId),
|
|
98
|
+
runId: envelope.runId,
|
|
99
|
+
stepId: envelope.stepId,
|
|
100
|
+
attempt: envelope.attempt,
|
|
101
|
+
payload: null
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function parseReceipt(receipt) {
|
|
105
|
+
try {
|
|
106
|
+
BigInt(receipt);
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`pgmq: malformed receipt ${JSON.stringify(receipt)} \u2014 expected stringified bigint msg_id`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return receipt;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { pgmqQueue };
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
|
117
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pgmq-queue.ts"],"names":[],"mappings":";;;AAaA,IAAM,kBAAA,GAAqB,MAAA;AAC3B,IAAM,6BAAA,GAAwC,GAAA;AAoDvC,SAAS,UAAU,IAAA,EAAgC;AACxD,EAAA,MAAM,KAAK,IAAA,CAAK,EAAA;AAChB,EAAA,MAAM,SAAA,GAAY,KAAK,SAAA,IAAa,kBAAA;AACpC,EAAA,MAAM,YAAY,IAAA,CAAK,GAAA;AAAA,IACrB,CAAA;AAAA,IACA,IAAA,CAAK,IAAA;AAAA,MAAA,CACF,IAAA,CAAK,uBAAuB,6BAAA,IAAiC;AAAA;AAChE,GACF;AACA,EAAA,MAAM,WAAA,GAAc,KAAK,WAAA,IAAe,KAAA;AACxC,EAAA,MAAM,YAAA,GAAe,KAAK,YAAA,IAAgB,KAAA;AAC1C,EAAA,MAAM,MAAA,GAAsB,EAAE,SAAA,EAAW,SAAA,EAAW,YAAA,EAAa;AAEjE,EAAA,OAAO;AAAA,IACL,GAAG,UAAA,CAAW,EAAA,EAAI,MAAM,CAAA;AAAA,IAExB,MAAM,YAAA,GAA8B;AAClC,MAAA,MAAM,GAAA,CAAA,mCAAA,CAAA,CAAyC,QAAQ,EAAE,CAAA;AACzD,MAAA,IAAI,WAAA,EAAa;AACf,QAAA,MAAM,GAAA,CAAA,+BAAA,EAAqC,SAAS,CAAA,CAAA,CAAA,CAAI,OAAA,CAAQ,EAAE,CAAA;AAAA,MACpE,CAAA,MAAO;AACL,QAAA,MAAM,GAAA,CAAA,mBAAA,EAAyB,SAAS,CAAA,CAAA,CAAA,CAAI,OAAA,CAAQ,EAAE,CAAA;AAAA,MACxD;AAAA,IACF,CAAA;AAAA,IAEA,OAAO,EAAA,EAAe;AAIpB,MAAA,OAAO,UAAA,CAAW,IAAkC,MAAM,CAAA;AAAA,IAC5D;AAAA,GACF;AACF;AAEA,SAAS,UAAA,CAAW,UAA2B,MAAA,EAA4B;AACzE,EAAA,MAAM,EAAE,SAAA,EAAW,SAAA,EAAW,YAAA,EAAa,GAAI,MAAA;AAE/C,EAAA,OAAO;AAAA,IACL,MAAM,OAAA,CACJ,KAAA,EACA,MAAA,EACA,OAAA,EACe;AACf,MAAA,MAAM,QAAA,GAA4B;AAAA,QAChC,KAAA;AAAA,QACA,MAAA;AAAA,QACA,OAAA,EAAS,SAAS,OAAA,IAAW;AAAA,OAC/B;AACA,MAAA,MAAM,eAAe,IAAA,CAAK,GAAA;AAAA,QACxB,CAAA;AAAA,QACA,IAAA,CAAK,IAAA,CAAA,CAAM,OAAA,EAAS,OAAA,IAAW,KAAK,GAAI;AAAA,OAC1C;AACA,MAAA,MAAM,GAAA,CAAA,iBAAA,EAAuB,SAAS,CAAA,EAAA,EAAK,IAAA,CAAK,UAAU,QAAQ,CAAC,CAAA,SAAA,EAAY,YAAY,CAAA,MAAA,CAAA,CAAS,OAAA;AAAA,QAClG;AAAA,OACF;AAAA,IACF,CAAA;AAAA,IAEA,MAAM,OAAA,CAAQ;AAAA,MACZ;AAAA,KACF,EAAuD;AACrD,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,GAAA,CAAA,sCAAA,EAGoB,SAAS,CAAA,EAAA,EAAK,SAAS,CAAA,OAAA,EAAU,KAAK,CAAA,MAAA,CAAA,CAAS,OAAA;AAAA,QACxF;AAAA,OACF;AACA,MAAA,OAAO,IAAA,CAAK,IAAI,CAAC,GAAA,KAAQ,eAAe,GAAA,CAAI,MAAA,EAAQ,GAAA,CAAI,OAAO,CAAC,CAAA;AAAA,IAClE,CAAA;AAAA,IAEA,MAAM,IAAI,OAAA,EAAgC;AACxC,MAAA,MAAM,KAAA,GAAQ,aAAa,OAAO,CAAA;AAClC,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,MAAM,GAAA,CAAA,oBAAA,EAA0B,SAAS,CAAA,EAAA,EAAK,KAAK,CAAA,SAAA,CAAA,CAAY,OAAA;AAAA,UAC7D;AAAA,SACF;AAAA,MACF,CAAA,MAAO;AACL,QAAA,MAAM,GAAA,CAAA,mBAAA,EAAyB,SAAS,CAAA,EAAA,EAAK,KAAK,CAAA,SAAA,CAAA,CAAY,OAAA;AAAA,UAC5D;AAAA,SACF;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IAEA,MAAM,IAAA,CACJ,OAAA,EACA,OAAA,EACe;AACf,MAAA,MAAM,KAAA,GAAQ,aAAa,OAAO,CAAA;AAGlC,MAAA,MAAM,eAAe,IAAA,CAAK,GAAA;AAAA,QACxB,CAAA;AAAA,QACA,IAAA,CAAK,IAAA,CAAA,CAAM,OAAA,EAAS,OAAA,IAAW,KAAK,GAAI;AAAA,OAC1C;AACA,MAAA,MAAM,yBAAyB,SAAS,CAAA,EAAA,EAAK,KAAK,CAAA,UAAA,EAAa,YAAY,CAAA,MAAA,CAAA,CAAS,OAAA;AAAA,QAClF;AAAA,OACF;AAAA,IACF,CAAA;AAAA,IAEA,MAAM,MAAA,CAAO,OAAA,EAAiB,OAAA,EAAgC;AAC5D,MAAA,MAAM,KAAA,GAAQ,aAAa,OAAO,CAAA;AAClC,MAAA,MAAM,EAAA,GAAK,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,IAAA,CAAK,OAAA,GAAU,GAAI,CAAC,CAAA;AAChD,MAAA,MAAM,yBAAyB,SAAS,CAAA,EAAA,EAAK,KAAK,CAAA,UAAA,EAAa,EAAE,CAAA,MAAA,CAAA,CAAS,OAAA;AAAA,QACxE;AAAA,OACF;AAAA,IACF;AAAA,GACF;AACF;AAEA,SAAS,cAAA,CACP,UACA,GAAA,EACc;AACd,EAAA,IACE,QAAQ,IAAA,IACR,OAAO,GAAA,KAAQ,QAAA,IACf,OAAQ,GAAA,CAA4B,KAAA,KAAU,QAAA,IAC9C,OAAQ,IAA6B,MAAA,KAAW,QAAA,IAChD,OAAQ,GAAA,CAA8B,YAAY,QAAA,EAClD;AACA,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,iCAAA,EAAoC,IAAA,CAAK,SAAA,CAAU,GAAG,CAAC,CAAA,2CAAA;AAAA,KACzD;AAAA,EACF;AACA,EAAA,MAAM,QAAA,GAAW,GAAA;AACjB,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,OAAO,QAAQ,CAAA;AAAA,IACxB,OAAO,QAAA,CAAS,KAAA;AAAA,IAChB,QAAQ,QAAA,CAAS,MAAA;AAAA,IACjB,SAAS,QAAA,CAAS,OAAA;AAAA,IAClB,OAAA,EAAS;AAAA,GACX;AACF;AAEA,SAAS,aAAa,OAAA,EAAyB;AAC7C,EAAA,IAAI;AACF,IAAA,MAAA,CAAO,OAAO,CAAA;AAAA,EAChB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,wBAAA,EAA2B,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA,0CAAA;AAAA,KACpD;AAAA,EACF;AACA,EAAA,OAAO,OAAA;AACT","file":"index.js","sourcesContent":["import type {\n AttemptNumber,\n Millis,\n Queue,\n QueueDequeueOpts,\n QueueEnqueueOpts,\n QueueMessage,\n RunId,\n StepId,\n Tx,\n} from \"@nagi-js/core\";\nimport { type Kysely, sql } from \"kysely\";\n\nconst DEFAULT_QUEUE_NAME = \"nagi\";\nconst DEFAULT_VISIBILITY_TIMEOUT_MS: Millis = 30_000;\n\nexport interface PgmqQueueOpts {\n /** Kysely instance. The adapter does NOT own the connection lifecycle. */\n readonly db: Kysely<unknown>;\n /** PGMQ queue name. Default: \"nagi\". */\n readonly queueName?: string;\n /** Visibility timeout applied on every dequeue, in ms. Default: 30_000. */\n readonly visibilityTimeoutMs?: Millis;\n /**\n * Use `pgmq.create_partitioned()` instead of `pgmq.create()` when\n * `ensureSchema()` runs. Default: false.\n */\n readonly partitioned?: boolean;\n /**\n * Use `pgmq.archive()` instead of `pgmq.delete()` on ack — retains messages\n * in the archive table for audit. Default: false.\n */\n readonly archiveOnAck?: boolean;\n}\n\nexport interface PgmqQueue extends Queue {\n /**\n * Idempotent setup: installs the pgmq extension and creates the queue.\n * Requires database privileges to `CREATE EXTENSION`. Suitable for dev/test;\n * production should run these statements out-of-band.\n */\n ensureSchema(): Promise<void>;\n /**\n * Returns a `Queue` whose operations execute on the supplied transaction.\n * Pass `ctx.tx` to enqueue messages atomically with the handler's domain\n * writes — the pgmq message commits with `step.completed` or rolls back\n * with the handler. The user must have wired `@nagi-js/postgres` and\n * augmented `Register.tx` so that `Tx` is a Kysely transaction at compile\n * time; at runtime, `tx` must be the same Kysely handle the postgres store\n * handed to `runStep`.\n */\n withTx(tx: Tx): Queue;\n}\n\ninterface MessageEnvelope {\n readonly runId: string;\n readonly stepId: string;\n readonly attempt: number;\n}\n\ninterface QueueConfig {\n readonly queueName: string;\n readonly vtSeconds: number;\n readonly archiveOnAck: boolean;\n}\n\nexport function pgmqQueue(opts: PgmqQueueOpts): PgmqQueue {\n const db = opts.db;\n const queueName = opts.queueName ?? DEFAULT_QUEUE_NAME;\n const vtSeconds = Math.max(\n 1,\n Math.ceil(\n (opts.visibilityTimeoutMs ?? DEFAULT_VISIBILITY_TIMEOUT_MS) / 1000,\n ),\n );\n const partitioned = opts.partitioned ?? false;\n const archiveOnAck = opts.archiveOnAck ?? false;\n const config: QueueConfig = { queueName, vtSeconds, archiveOnAck };\n\n return {\n ...buildQueue(db, config),\n\n async ensureSchema(): Promise<void> {\n await sql`CREATE EXTENSION IF NOT EXISTS pgmq`.execute(db);\n if (partitioned) {\n await sql`SELECT pgmq.create_partitioned(${queueName})`.execute(db);\n } else {\n await sql`SELECT pgmq.create(${queueName})`.execute(db);\n }\n },\n\n withTx(tx: Tx): Queue {\n // `Tx` is the user-augmented transaction type from `@nagi-js/core`.\n // When `@nagi-js/postgres` is wired and `Register.tx` is augmented to\n // a Kysely transaction, the cast is structurally sound at runtime.\n return buildQueue(tx as unknown as Kysely<unknown>, config);\n },\n };\n}\n\nfunction buildQueue(executor: Kysely<unknown>, config: QueueConfig): Queue {\n const { queueName, vtSeconds, archiveOnAck } = config;\n\n return {\n async enqueue(\n runId: RunId,\n stepId: StepId,\n options?: QueueEnqueueOpts,\n ): Promise<void> {\n const envelope: MessageEnvelope = {\n runId,\n stepId,\n attempt: options?.attempt ?? 1,\n };\n const delaySeconds = Math.max(\n 0,\n Math.ceil((options?.delayMs ?? 0) / 1000),\n );\n await sql`SELECT pgmq.send(${queueName}, ${JSON.stringify(envelope)}::jsonb, ${delaySeconds}::int)`.execute(\n executor,\n );\n },\n\n async dequeue({\n count,\n }: QueueDequeueOpts): Promise<readonly QueueMessage[]> {\n const { rows } = await sql<{\n msg_id: string | number | bigint;\n message: unknown;\n }>`SELECT msg_id, message FROM pgmq.read(${queueName}, ${vtSeconds}::int, ${count}::int)`.execute(\n executor,\n );\n return rows.map((row) => projectMessage(row.msg_id, row.message));\n },\n\n async ack(receipt: string): Promise<void> {\n const msgId = parseReceipt(receipt);\n if (archiveOnAck) {\n await sql`SELECT pgmq.archive(${queueName}, ${msgId}::bigint)`.execute(\n executor,\n );\n } else {\n await sql`SELECT pgmq.delete(${queueName}, ${msgId}::bigint)`.execute(\n executor,\n );\n }\n },\n\n async nack(\n receipt: string,\n options?: { readonly delayMs?: Millis },\n ): Promise<void> {\n const msgId = parseReceipt(receipt);\n // set_vt expects seconds offset from now. 0 = immediately re-visible.\n // Attempt counters live in the dispatcher — nack must not mutate them.\n const delaySeconds = Math.max(\n 0,\n Math.ceil((options?.delayMs ?? 0) / 1000),\n );\n await sql`SELECT pgmq.set_vt(${queueName}, ${msgId}::bigint, ${delaySeconds}::int)`.execute(\n executor,\n );\n },\n\n async extend(receipt: string, leaseMs: Millis): Promise<void> {\n const msgId = parseReceipt(receipt);\n const vt = Math.max(1, Math.ceil(leaseMs / 1000));\n await sql`SELECT pgmq.set_vt(${queueName}, ${msgId}::bigint, ${vt}::int)`.execute(\n executor,\n );\n },\n };\n}\n\nfunction projectMessage(\n rawMsgId: string | number | bigint,\n raw: unknown,\n): QueueMessage {\n if (\n raw === null ||\n typeof raw !== \"object\" ||\n typeof (raw as { runId?: unknown }).runId !== \"string\" ||\n typeof (raw as { stepId?: unknown }).stepId !== \"string\" ||\n typeof (raw as { attempt?: unknown }).attempt !== \"number\"\n ) {\n throw new Error(\n `pgmq: malformed message envelope ${JSON.stringify(raw)} — expected { runId, stepId, attempt }`,\n );\n }\n const envelope = raw as MessageEnvelope;\n return {\n receipt: String(rawMsgId),\n runId: envelope.runId as RunId,\n stepId: envelope.stepId as StepId,\n attempt: envelope.attempt as AttemptNumber,\n payload: null,\n };\n}\n\nfunction parseReceipt(receipt: string): string {\n try {\n BigInt(receipt);\n } catch {\n throw new Error(\n `pgmq: malformed receipt ${JSON.stringify(receipt)} — expected stringified bigint msg_id`,\n );\n }\n return receipt;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nagi-js/pgmq",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PGMQ Queue adapter for nagi.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"keywords": [
|
|
9
|
+
"nagi",
|
|
10
|
+
"workflow",
|
|
11
|
+
"pgmq",
|
|
12
|
+
"postgres",
|
|
13
|
+
"queue",
|
|
14
|
+
"outbox",
|
|
15
|
+
"adapter"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"registry": "https://registry.npmjs.org/",
|
|
23
|
+
"provenance": false
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@nagi-js/core": "0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"kysely": "^0.28.0"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/lymo-inc/nagi.git",
|
|
46
|
+
"directory": "packages/pgmq"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/lymo-inc/nagi#readme",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/lymo-inc/nagi/issues"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"dev": "tsup --watch",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"test:types": "vitest run --typecheck",
|
|
58
|
+
"typecheck": "tsc --noEmit"
|
|
59
|
+
}
|
|
60
|
+
}
|