@quilla-be-kit/messaging 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/event-bus/defaults.d.ts +5 -0
- package/dist/event-bus/defaults.d.ts.map +1 -0
- package/dist/event-bus/defaults.js +5 -0
- package/dist/event-bus/defaults.js.map +1 -0
- package/dist/event-bus/event-bus-consumer.interface.d.ts +8 -0
- package/dist/event-bus/event-bus-consumer.interface.d.ts.map +1 -0
- package/dist/event-bus/event-bus-consumer.interface.js +2 -0
- package/dist/event-bus/event-bus-consumer.interface.js.map +1 -0
- package/dist/event-bus/event-bus-entry.type.d.ts +19 -0
- package/dist/event-bus/event-bus-entry.type.d.ts.map +1 -0
- package/dist/event-bus/event-bus-entry.type.js +2 -0
- package/dist/event-bus/event-bus-entry.type.js.map +1 -0
- package/dist/event-bus/event-bus-publisher.interface.d.ts +14 -0
- package/dist/event-bus/event-bus-publisher.interface.d.ts.map +1 -0
- package/dist/event-bus/event-bus-publisher.interface.js +2 -0
- package/dist/event-bus/event-bus-publisher.interface.js.map +1 -0
- package/dist/event-bus/event-subscription.interface.d.ts +7 -0
- package/dist/event-bus/event-subscription.interface.d.ts.map +1 -0
- package/dist/event-bus/event-subscription.interface.js +2 -0
- package/dist/event-bus/event-subscription.interface.js.map +1 -0
- package/dist/event-bus/event.consumer.d.ts +66 -0
- package/dist/event-bus/event.consumer.d.ts.map +1 -0
- package/dist/event-bus/event.consumer.js +251 -0
- package/dist/event-bus/event.consumer.js.map +1 -0
- package/dist/event-bus/event.descriptor.d.ts +9 -0
- package/dist/event-bus/event.descriptor.d.ts.map +1 -0
- package/dist/event-bus/event.descriptor.js +7 -0
- package/dist/event-bus/event.descriptor.js.map +1 -0
- package/dist/event-bus/handler-entry.type.d.ts +8 -0
- package/dist/event-bus/handler-entry.type.d.ts.map +1 -0
- package/dist/event-bus/handler-entry.type.js +2 -0
- package/dist/event-bus/handler-entry.type.js.map +1 -0
- package/dist/event-bus/index.d.ts +10 -0
- package/dist/event-bus/index.d.ts.map +1 -0
- package/dist/event-bus/index.js +4 -0
- package/dist/event-bus/index.js.map +1 -0
- package/dist/event-bus/standard-schema.type.d.ts +33 -0
- package/dist/event-bus/standard-schema.type.d.ts.map +1 -0
- package/dist/event-bus/standard-schema.type.js +2 -0
- package/dist/event-bus/standard-schema.type.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/local-outbox/index.d.ts +6 -0
- package/dist/local-outbox/index.d.ts.map +1 -0
- package/dist/local-outbox/index.js +2 -0
- package/dist/local-outbox/index.js.map +1 -0
- package/dist/local-outbox/local-outbox-entry.type.d.ts +28 -0
- package/dist/local-outbox/local-outbox-entry.type.d.ts.map +1 -0
- package/dist/local-outbox/local-outbox-entry.type.js +2 -0
- package/dist/local-outbox/local-outbox-entry.type.js.map +1 -0
- package/dist/local-outbox/local-outbox-reader.interface.d.ts +8 -0
- package/dist/local-outbox/local-outbox-reader.interface.d.ts.map +1 -0
- package/dist/local-outbox/local-outbox-reader.interface.js +2 -0
- package/dist/local-outbox/local-outbox-reader.interface.js.map +1 -0
- package/dist/local-outbox/local-outbox-writer.interface.d.ts +6 -0
- package/dist/local-outbox/local-outbox-writer.interface.d.ts.map +1 -0
- package/dist/local-outbox/local-outbox-writer.interface.js +2 -0
- package/dist/local-outbox/local-outbox-writer.interface.js.map +1 -0
- package/dist/local-outbox/outbox.forwarder.d.ts +34 -0
- package/dist/local-outbox/outbox.forwarder.d.ts.map +1 -0
- package/dist/local-outbox/outbox.forwarder.js +109 -0
- package/dist/local-outbox/outbox.forwarder.js.map +1 -0
- package/dist/local-outbox/transaction-handle.interface.d.ts +6 -0
- package/dist/local-outbox/transaction-handle.interface.d.ts.map +1 -0
- package/dist/local-outbox/transaction-handle.interface.js +2 -0
- package/dist/local-outbox/transaction-handle.interface.js.map +1 -0
- package/dist/postgres/get-postgres-schema.d.ts +6 -0
- package/dist/postgres/get-postgres-schema.d.ts.map +1 -0
- package/dist/postgres/get-postgres-schema.js +56 -0
- package/dist/postgres/get-postgres-schema.js.map +1 -0
- package/dist/postgres/index.d.ts +4 -0
- package/dist/postgres/index.d.ts.map +1 -0
- package/dist/postgres/index.js +4 -0
- package/dist/postgres/index.js.map +1 -0
- package/dist/postgres/pg-event-bus.d.ts +25 -0
- package/dist/postgres/pg-event-bus.d.ts.map +1 -0
- package/dist/postgres/pg-event-bus.js +115 -0
- package/dist/postgres/pg-event-bus.js.map +1 -0
- package/dist/postgres/pg-local-outbox.d.ts +26 -0
- package/dist/postgres/pg-local-outbox.d.ts.map +1 -0
- package/dist/postgres/pg-local-outbox.js +102 -0
- package/dist/postgres/pg-local-outbox.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const DEFAULT_EVENTS_TABLE = 'events';
|
|
2
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
3
|
+
export class PgEventBus {
|
|
4
|
+
pool;
|
|
5
|
+
eventsTable;
|
|
6
|
+
maxRetries;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.pool = options.pool;
|
|
9
|
+
this.eventsTable = options.eventsTableName ?? DEFAULT_EVENTS_TABLE;
|
|
10
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
11
|
+
}
|
|
12
|
+
async publish(event) {
|
|
13
|
+
const publishedAt = new Date();
|
|
14
|
+
await this.pool.query(`INSERT INTO ${this.eventsTable}
|
|
15
|
+
(id, event_type, event_version, event_kind, payload, source_service,
|
|
16
|
+
aggregate_id, correlation_id, status, claimed_by, claimed_at,
|
|
17
|
+
retry_count, last_error, created_at, published_at)
|
|
18
|
+
VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`, [
|
|
19
|
+
event.id,
|
|
20
|
+
event.eventType,
|
|
21
|
+
event.eventVersion,
|
|
22
|
+
event.eventKind,
|
|
23
|
+
JSON.stringify(event.payload),
|
|
24
|
+
event.sourceService,
|
|
25
|
+
event.aggregateId ?? null,
|
|
26
|
+
event.correlationId ?? null,
|
|
27
|
+
'PENDING',
|
|
28
|
+
null,
|
|
29
|
+
null,
|
|
30
|
+
0,
|
|
31
|
+
null,
|
|
32
|
+
event.createdAt,
|
|
33
|
+
publishedAt,
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
// NOT EXISTS guards against claiming a second event for an aggregate while
|
|
37
|
+
// an earlier one is still CLAIMED. Advisory xact lock closes the race
|
|
38
|
+
// between NOT EXISTS and UPDATE across overlapping transactions.
|
|
39
|
+
async claim(instanceId, batchSize) {
|
|
40
|
+
const result = await this.pool.query(`WITH claimed AS (
|
|
41
|
+
SELECT id FROM ${this.eventsTable} e
|
|
42
|
+
WHERE e.status = 'PENDING'
|
|
43
|
+
AND NOT EXISTS (
|
|
44
|
+
SELECT 1 FROM ${this.eventsTable} e2
|
|
45
|
+
WHERE e2.aggregate_id IS NOT NULL
|
|
46
|
+
AND e2.aggregate_id = e.aggregate_id
|
|
47
|
+
AND e2.status = 'CLAIMED'
|
|
48
|
+
)
|
|
49
|
+
AND (e.aggregate_id IS NULL
|
|
50
|
+
OR pg_try_advisory_xact_lock(hashtext(e.aggregate_id)::bigint))
|
|
51
|
+
ORDER BY e.created_at
|
|
52
|
+
LIMIT $1
|
|
53
|
+
FOR UPDATE SKIP LOCKED
|
|
54
|
+
)
|
|
55
|
+
UPDATE ${this.eventsTable} t
|
|
56
|
+
SET status = 'CLAIMED', claimed_by = $2, claimed_at = $3
|
|
57
|
+
FROM claimed c
|
|
58
|
+
WHERE t.id = c.id
|
|
59
|
+
RETURNING t.*`, [batchSize, instanceId, new Date()]);
|
|
60
|
+
return result.rows.map((r) => this.rowToEntry(r));
|
|
61
|
+
}
|
|
62
|
+
async markDone(id) {
|
|
63
|
+
await this.pool.query(`DELETE FROM ${this.eventsTable} WHERE id = $1`, [id]);
|
|
64
|
+
}
|
|
65
|
+
async markFailed(id, reason) {
|
|
66
|
+
await this.pool.query(`UPDATE ${this.eventsTable}
|
|
67
|
+
SET retry_count = retry_count + 1,
|
|
68
|
+
last_error = $1,
|
|
69
|
+
status = CASE WHEN retry_count + 1 >= $3 THEN 'FAILED' ELSE 'PENDING' END,
|
|
70
|
+
claimed_by = NULL,
|
|
71
|
+
claimed_at = NULL
|
|
72
|
+
WHERE id = $2`, [reason, id, this.maxRetries]);
|
|
73
|
+
}
|
|
74
|
+
async resetStale(olderThan) {
|
|
75
|
+
const result = await this.pool.query(`UPDATE ${this.eventsTable}
|
|
76
|
+
SET status = 'PENDING', claimed_by = NULL, claimed_at = NULL
|
|
77
|
+
WHERE status = 'CLAIMED' AND claimed_at < $1`, [olderThan]);
|
|
78
|
+
return result.rowCount ?? 0;
|
|
79
|
+
}
|
|
80
|
+
async cleanupFailed(olderThan, options) {
|
|
81
|
+
const limit = options?.limit;
|
|
82
|
+
const result = limit
|
|
83
|
+
? await this.pool.query(`WITH cte AS (
|
|
84
|
+
SELECT id FROM ${this.eventsTable}
|
|
85
|
+
WHERE status = 'FAILED' AND created_at < $1
|
|
86
|
+
ORDER BY created_at ASC
|
|
87
|
+
LIMIT $2
|
|
88
|
+
)
|
|
89
|
+
DELETE FROM ${this.eventsTable} t
|
|
90
|
+
USING cte
|
|
91
|
+
WHERE t.id = cte.id`, [olderThan, limit])
|
|
92
|
+
: await this.pool.query(`DELETE FROM ${this.eventsTable} WHERE status = 'FAILED' AND created_at < $1`, [olderThan]);
|
|
93
|
+
return result.rowCount ?? 0;
|
|
94
|
+
}
|
|
95
|
+
rowToEntry(row) {
|
|
96
|
+
return {
|
|
97
|
+
id: row.id,
|
|
98
|
+
eventType: row.event_type,
|
|
99
|
+
eventVersion: row.event_version,
|
|
100
|
+
eventKind: row.event_kind,
|
|
101
|
+
payload: row.payload,
|
|
102
|
+
sourceService: row.source_service,
|
|
103
|
+
...(row.aggregate_id !== null ? { aggregateId: row.aggregate_id } : {}),
|
|
104
|
+
...(row.correlation_id !== null ? { correlationId: row.correlation_id } : {}),
|
|
105
|
+
status: row.status,
|
|
106
|
+
...(row.claimed_by !== null ? { claimedBy: row.claimed_by } : {}),
|
|
107
|
+
...(row.claimed_at !== null ? { claimedAt: row.claimed_at } : {}),
|
|
108
|
+
retryCount: row.retry_count,
|
|
109
|
+
...(row.last_error !== null ? { lastError: row.last_error } : {}),
|
|
110
|
+
createdAt: row.created_at,
|
|
111
|
+
publishedAt: row.published_at,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=pg-event-bus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-event-bus.js","sourceRoot":"","sources":["../../src/postgres/pg-event-bus.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
import type { LocalOutboxEntry, LocalOutboxInsertInput } from '../local-outbox/local-outbox-entry.type.js';
|
|
3
|
+
import type { LocalOutboxReader } from '../local-outbox/local-outbox-reader.interface.js';
|
|
4
|
+
import type { LocalOutboxWriter } from '../local-outbox/local-outbox-writer.interface.js';
|
|
5
|
+
import type { TransactionHandle } from '../local-outbox/transaction-handle.interface.js';
|
|
6
|
+
export type PgLocalOutboxOptions = {
|
|
7
|
+
readonly pool: Pool;
|
|
8
|
+
readonly tableName?: string;
|
|
9
|
+
readonly maxRetries?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare class PgLocalOutbox implements LocalOutboxWriter, LocalOutboxReader {
|
|
12
|
+
private readonly pool;
|
|
13
|
+
private readonly tableName;
|
|
14
|
+
private readonly maxRetries;
|
|
15
|
+
constructor(options: PgLocalOutboxOptions);
|
|
16
|
+
insert(entries: readonly LocalOutboxInsertInput[], trx: TransactionHandle): Promise<void>;
|
|
17
|
+
claim(instanceId: string, batchSize: number): Promise<readonly LocalOutboxEntry[]>;
|
|
18
|
+
markSent(id: string, publishedAt: Date): Promise<void>;
|
|
19
|
+
markFailed(id: string, reason: string): Promise<void>;
|
|
20
|
+
resetStale(olderThan: Date): Promise<number>;
|
|
21
|
+
cleanup(olderThan: Date, options?: {
|
|
22
|
+
readonly limit?: number;
|
|
23
|
+
}): Promise<number>;
|
|
24
|
+
private rowToEntry;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=pg-local-outbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-local-outbox.d.ts","sourceRoot":"","sources":["../../src/postgres/pg-local-outbox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EAEvB,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kDAAkD,CAAC;AAC1F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kDAAkD,CAAC;AAC1F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAC;AAuBzF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,qBAAa,aAAc,YAAW,iBAAiB,EAAE,iBAAiB;IACxE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,OAAO,EAAE,oBAAoB;IAMnC,MAAM,CAAC,OAAO,EAAE,SAAS,sBAAsB,EAAE,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAyCzF,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC;IAmBlF,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAStD,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAarD,UAAU,CAAC,SAAS,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;IAU5C,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAuBtF,OAAO,CAAC,UAAU;CAkBnB"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
const DEFAULT_TABLE_NAME = 'outbox_events';
|
|
3
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
4
|
+
const INSERT_COLUMN_COUNT = 14;
|
|
5
|
+
export class PgLocalOutbox {
|
|
6
|
+
pool;
|
|
7
|
+
tableName;
|
|
8
|
+
maxRetries;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.pool = options.pool;
|
|
11
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
|
|
12
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
13
|
+
}
|
|
14
|
+
async insert(entries, trx) {
|
|
15
|
+
if (entries.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
const params = [];
|
|
18
|
+
const valueGroups = [];
|
|
19
|
+
for (const [i, entry] of entries.entries()) {
|
|
20
|
+
const base = i * INSERT_COLUMN_COUNT;
|
|
21
|
+
valueGroups.push(`($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}::jsonb, ` +
|
|
22
|
+
`$${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10}, ` +
|
|
23
|
+
`$${base + 11}, $${base + 12}, $${base + 13}, $${base + 14})`);
|
|
24
|
+
params.push(entry.id ?? randomUUID(), entry.eventType, entry.eventVersion ?? 1, entry.eventKind, JSON.stringify(entry.payload), entry.aggregateId ?? null, entry.correlationId ?? null, 'PENDING', null, null, 0, null, null, entry.createdAt ?? new Date());
|
|
25
|
+
}
|
|
26
|
+
await trx.query(`INSERT INTO ${this.tableName}
|
|
27
|
+
(id, event_type, event_version, event_kind, payload,
|
|
28
|
+
aggregate_id, correlation_id, status, claimed_by, claimed_at,
|
|
29
|
+
retry_count, last_error, published_at, created_at)
|
|
30
|
+
VALUES ${valueGroups.join(', ')}`, params);
|
|
31
|
+
}
|
|
32
|
+
async claim(instanceId, batchSize) {
|
|
33
|
+
const result = await this.pool.query(`WITH claimed AS (
|
|
34
|
+
SELECT id FROM ${this.tableName}
|
|
35
|
+
WHERE status = 'PENDING'
|
|
36
|
+
ORDER BY created_at
|
|
37
|
+
LIMIT $1
|
|
38
|
+
FOR UPDATE SKIP LOCKED
|
|
39
|
+
)
|
|
40
|
+
UPDATE ${this.tableName} t
|
|
41
|
+
SET status = 'CLAIMED', claimed_by = $2, claimed_at = $3
|
|
42
|
+
FROM claimed c
|
|
43
|
+
WHERE t.id = c.id
|
|
44
|
+
RETURNING t.*`, [batchSize, instanceId, new Date()]);
|
|
45
|
+
return result.rows.map((r) => this.rowToEntry(r));
|
|
46
|
+
}
|
|
47
|
+
async markSent(id, publishedAt) {
|
|
48
|
+
await this.pool.query(`UPDATE ${this.tableName}
|
|
49
|
+
SET status = 'SENT', published_at = $1, claimed_by = NULL, claimed_at = NULL
|
|
50
|
+
WHERE id = $2`, [publishedAt, id]);
|
|
51
|
+
}
|
|
52
|
+
async markFailed(id, reason) {
|
|
53
|
+
await this.pool.query(`UPDATE ${this.tableName}
|
|
54
|
+
SET retry_count = retry_count + 1,
|
|
55
|
+
last_error = $1,
|
|
56
|
+
status = CASE WHEN retry_count + 1 >= $3 THEN 'FAILED' ELSE 'PENDING' END,
|
|
57
|
+
claimed_by = NULL,
|
|
58
|
+
claimed_at = NULL
|
|
59
|
+
WHERE id = $2`, [reason, id, this.maxRetries]);
|
|
60
|
+
}
|
|
61
|
+
async resetStale(olderThan) {
|
|
62
|
+
const result = await this.pool.query(`UPDATE ${this.tableName}
|
|
63
|
+
SET status = 'PENDING', claimed_by = NULL, claimed_at = NULL
|
|
64
|
+
WHERE status = 'CLAIMED' AND claimed_at < $1`, [olderThan]);
|
|
65
|
+
return result.rowCount ?? 0;
|
|
66
|
+
}
|
|
67
|
+
async cleanup(olderThan, options) {
|
|
68
|
+
const limit = options?.limit;
|
|
69
|
+
const result = limit
|
|
70
|
+
? await this.pool.query(`WITH cte AS (
|
|
71
|
+
SELECT id FROM ${this.tableName}
|
|
72
|
+
WHERE status = 'SENT' AND published_at < $1
|
|
73
|
+
ORDER BY published_at ASC
|
|
74
|
+
LIMIT $2
|
|
75
|
+
)
|
|
76
|
+
DELETE FROM ${this.tableName} t
|
|
77
|
+
USING cte
|
|
78
|
+
WHERE t.id = cte.id`, [olderThan, limit])
|
|
79
|
+
: await this.pool.query(`DELETE FROM ${this.tableName}
|
|
80
|
+
WHERE status = 'SENT' AND published_at < $1`, [olderThan]);
|
|
81
|
+
return result.rowCount ?? 0;
|
|
82
|
+
}
|
|
83
|
+
rowToEntry(row) {
|
|
84
|
+
return {
|
|
85
|
+
id: row.id,
|
|
86
|
+
eventType: row.event_type,
|
|
87
|
+
eventVersion: row.event_version,
|
|
88
|
+
eventKind: row.event_kind,
|
|
89
|
+
payload: row.payload,
|
|
90
|
+
...(row.aggregate_id !== null ? { aggregateId: row.aggregate_id } : {}),
|
|
91
|
+
...(row.correlation_id !== null ? { correlationId: row.correlation_id } : {}),
|
|
92
|
+
status: row.status,
|
|
93
|
+
...(row.claimed_by !== null ? { claimedBy: row.claimed_by } : {}),
|
|
94
|
+
...(row.claimed_at !== null ? { claimedAt: row.claimed_at } : {}),
|
|
95
|
+
retryCount: row.retry_count,
|
|
96
|
+
...(row.last_error !== null ? { lastError: row.last_error } : {}),
|
|
97
|
+
...(row.published_at !== null ? { publishedAt: row.published_at } : {}),
|
|
98
|
+
createdAt: row.created_at,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=pg-local-outbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-local-outbox.js","sourceRoot":"","sources":["../../src/postgres/pg-local-outbox.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quilla-be-kit/messaging",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Broker-agnostic messaging: event-bus + consumer interfaces, local-outbox pattern, outbox forwarder, Postgres reference implementation.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Max Martinez",
|
|
7
|
+
"homepage": "https://github.com/quilla-kit/quilla-be-kit/tree/main/packages/messaging",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/quilla-kit/quilla-be-kit.git",
|
|
11
|
+
"directory": "packages/messaging"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/quilla-kit/quilla-be-kit/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"messaging",
|
|
18
|
+
"event-bus",
|
|
19
|
+
"outbox",
|
|
20
|
+
"integration-events",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22"
|
|
26
|
+
},
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./postgres": {
|
|
34
|
+
"types": "./dist/postgres/index.d.ts",
|
|
35
|
+
"import": "./dist/postgres/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"main": "./dist/index.js",
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@quilla-be-kit/errors": "^0.2.0",
|
|
50
|
+
"@quilla-be-kit/execution-context": "^0.2.0",
|
|
51
|
+
"@quilla-be-kit/observability": "^0.2.0",
|
|
52
|
+
"@quilla-be-kit/runtime": "^0.2.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"pg": ">=8.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"pg": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/pg": "^8.15.5",
|
|
64
|
+
"pg": "^8.13.0",
|
|
65
|
+
"zod": "^4.0.0"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "tsc -b",
|
|
69
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
70
|
+
"test": "vitest run --passWithNoTests",
|
|
71
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
72
|
+
}
|
|
73
|
+
}
|