@rotorsoft/act-pg 0.18.5 → 0.19.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/README.md +40 -1
- package/package.json +4 -4
- package/dist/.tsbuildinfo +0 -1
- package/dist/@types/PostgresStore.d.ts +0 -238
- package/dist/@types/PostgresStore.d.ts.map +0 -1
- package/dist/@types/index.d.ts +0 -7
- package/dist/@types/index.d.ts.map +0 -1
- package/dist/@types/utils.d.ts +0 -2
- package/dist/@types/utils.d.ts.map +0 -1
- package/dist/index.cjs +0 -682
- package/dist/index.cjs.map +0 -1
- package/dist/index.js +0 -650
- package/dist/index.js.map +0 -1
package/dist/index.cjs
DELETED
|
@@ -1,682 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// src/index.ts
|
|
31
|
-
var index_exports = {};
|
|
32
|
-
__export(index_exports, {
|
|
33
|
-
PostgresStore: () => PostgresStore
|
|
34
|
-
});
|
|
35
|
-
module.exports = __toCommonJS(index_exports);
|
|
36
|
-
|
|
37
|
-
// src/PostgresStore.ts
|
|
38
|
-
var import_act = require("@rotorsoft/act");
|
|
39
|
-
var import_pg = __toESM(require("pg"), 1);
|
|
40
|
-
|
|
41
|
-
// src/utils.ts
|
|
42
|
-
var ISO_8601 = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(\.\d+)?(Z|[+-][0-2][0-9]:[0-5][0-9])?$/;
|
|
43
|
-
var dateReviver = (_key, value) => {
|
|
44
|
-
if (typeof value === "string" && ISO_8601.test(value)) {
|
|
45
|
-
return new Date(value);
|
|
46
|
-
}
|
|
47
|
-
return value;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// src/PostgresStore.ts
|
|
51
|
-
var logger = (0, import_act.log)();
|
|
52
|
-
var { Pool, types } = import_pg.default;
|
|
53
|
-
types.setTypeParser(
|
|
54
|
-
types.builtins.JSONB,
|
|
55
|
-
(val) => JSON.parse(val, dateReviver)
|
|
56
|
-
);
|
|
57
|
-
var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
58
|
-
var PG_UNIQUE_VIOLATION = "23505";
|
|
59
|
-
function assertSafeIdentifier(value, label) {
|
|
60
|
-
if (!SAFE_IDENTIFIER.test(value))
|
|
61
|
-
throw new Error(`Unsafe SQL identifier for ${label}: "${value}"`);
|
|
62
|
-
}
|
|
63
|
-
var DEFAULT_CONFIG = {
|
|
64
|
-
host: "localhost",
|
|
65
|
-
port: 5432,
|
|
66
|
-
database: "postgres",
|
|
67
|
-
user: "postgres",
|
|
68
|
-
password: "postgres",
|
|
69
|
-
schema: "public",
|
|
70
|
-
table: "events"
|
|
71
|
-
};
|
|
72
|
-
var PostgresStore = class {
|
|
73
|
-
_pool;
|
|
74
|
-
config;
|
|
75
|
-
_fqt;
|
|
76
|
-
_fqs;
|
|
77
|
-
/**
|
|
78
|
-
* Create a new PostgresStore instance.
|
|
79
|
-
* @param config Partial configuration (host, port, user, password, schema, table, etc.)
|
|
80
|
-
*/
|
|
81
|
-
constructor(config = {}) {
|
|
82
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
83
|
-
assertSafeIdentifier(this.config.schema, "schema");
|
|
84
|
-
assertSafeIdentifier(this.config.table, "table");
|
|
85
|
-
const { schema: _, table: __, ...poolConfig } = this.config;
|
|
86
|
-
this._pool = new Pool(poolConfig);
|
|
87
|
-
this._fqt = `"${this.config.schema}"."${this.config.table}"`;
|
|
88
|
-
this._fqs = `"${this.config.schema}"."${this.config.table}_streams"`;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Dispose of the store and close all database connections.
|
|
92
|
-
* @returns Promise that resolves when all connections are closed
|
|
93
|
-
*/
|
|
94
|
-
async dispose() {
|
|
95
|
-
await this._pool.end();
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Seed the database with required tables, indexes, and schema for event storage.
|
|
99
|
-
* @returns Promise that resolves when seeding is complete
|
|
100
|
-
* @throws Error if seeding fails
|
|
101
|
-
*/
|
|
102
|
-
async seed() {
|
|
103
|
-
const client = await this._pool.connect();
|
|
104
|
-
try {
|
|
105
|
-
await client.query("BEGIN");
|
|
106
|
-
await client.query(
|
|
107
|
-
`CREATE SCHEMA IF NOT EXISTS "${this.config.schema}";`
|
|
108
|
-
);
|
|
109
|
-
await client.query(
|
|
110
|
-
`CREATE TABLE IF NOT EXISTS ${this._fqt} (
|
|
111
|
-
id serial PRIMARY KEY,
|
|
112
|
-
name varchar(100) COLLATE pg_catalog."default" NOT NULL,
|
|
113
|
-
data jsonb,
|
|
114
|
-
stream varchar(100) COLLATE pg_catalog."default" NOT NULL,
|
|
115
|
-
version int NOT NULL,
|
|
116
|
-
created timestamptz NOT NULL DEFAULT now(),
|
|
117
|
-
meta jsonb
|
|
118
|
-
) TABLESPACE pg_default;`
|
|
119
|
-
);
|
|
120
|
-
await client.query(
|
|
121
|
-
`CREATE UNIQUE INDEX IF NOT EXISTS "${this.config.table}_stream_ix"
|
|
122
|
-
ON ${this._fqt} (stream COLLATE pg_catalog."default", version);`
|
|
123
|
-
);
|
|
124
|
-
await client.query(
|
|
125
|
-
`CREATE INDEX IF NOT EXISTS "${this.config.table}_name_ix"
|
|
126
|
-
ON ${this._fqt} (name COLLATE pg_catalog."default");`
|
|
127
|
-
);
|
|
128
|
-
await client.query(
|
|
129
|
-
`CREATE INDEX IF NOT EXISTS "${this.config.table}_created_id_ix"
|
|
130
|
-
ON ${this._fqt} (created, id);`
|
|
131
|
-
);
|
|
132
|
-
await client.query(
|
|
133
|
-
`CREATE INDEX IF NOT EXISTS "${this.config.table}_correlation_ix"
|
|
134
|
-
ON ${this._fqt} ((meta ->> 'correlation') COLLATE pg_catalog."default");`
|
|
135
|
-
);
|
|
136
|
-
await client.query(
|
|
137
|
-
`CREATE TABLE IF NOT EXISTS ${this._fqs} (
|
|
138
|
-
stream varchar(100) COLLATE pg_catalog."default" PRIMARY KEY,
|
|
139
|
-
source varchar(100) COLLATE pg_catalog."default",
|
|
140
|
-
at int NOT NULL DEFAULT -1,
|
|
141
|
-
retry smallint NOT NULL DEFAULT 0,
|
|
142
|
-
blocked boolean NOT NULL DEFAULT false,
|
|
143
|
-
error text,
|
|
144
|
-
leased_by text,
|
|
145
|
-
leased_until timestamptz
|
|
146
|
-
) TABLESPACE pg_default;`
|
|
147
|
-
);
|
|
148
|
-
await client.query(
|
|
149
|
-
`CREATE INDEX IF NOT EXISTS "${this.config.table}_streams_fetch_ix"
|
|
150
|
-
ON ${this._fqs} (blocked, at);`
|
|
151
|
-
);
|
|
152
|
-
await client.query("COMMIT");
|
|
153
|
-
logger.info(
|
|
154
|
-
`Seeded schema "${this.config.schema}" with table "${this.config.table}"`
|
|
155
|
-
);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
await client.query("ROLLBACK");
|
|
158
|
-
logger.error(error);
|
|
159
|
-
throw error;
|
|
160
|
-
} finally {
|
|
161
|
-
client.release();
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Drop all tables and schema created by the store (for testing or cleanup).
|
|
166
|
-
* @returns Promise that resolves when the schema is dropped
|
|
167
|
-
*/
|
|
168
|
-
async drop() {
|
|
169
|
-
await this._pool.query(
|
|
170
|
-
`
|
|
171
|
-
DO $$
|
|
172
|
-
BEGIN
|
|
173
|
-
IF EXISTS (SELECT 1 FROM information_schema.schemata
|
|
174
|
-
WHERE schema_name = '${this.config.schema}'
|
|
175
|
-
) THEN
|
|
176
|
-
EXECUTE 'DROP TABLE IF EXISTS ${this._fqt}';
|
|
177
|
-
EXECUTE 'DROP TABLE IF EXISTS ${this._fqs}';
|
|
178
|
-
IF '${this.config.schema}' <> 'public' THEN
|
|
179
|
-
EXECUTE 'DROP SCHEMA "${this.config.schema}" CASCADE';
|
|
180
|
-
END IF;
|
|
181
|
-
END IF;
|
|
182
|
-
END
|
|
183
|
-
$$;
|
|
184
|
-
`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Query events from the store, optionally filtered by stream, event name, time, etc.
|
|
189
|
-
*
|
|
190
|
-
* @param callback Function called for each event found
|
|
191
|
-
* @param query (Optional) Query filter (stream, names, before, after, etc.)
|
|
192
|
-
* @returns The number of events found
|
|
193
|
-
*
|
|
194
|
-
* @example
|
|
195
|
-
* await store.query((event) => console.log(event), { stream: "A" });
|
|
196
|
-
*/
|
|
197
|
-
async query(callback, query) {
|
|
198
|
-
const {
|
|
199
|
-
stream,
|
|
200
|
-
names,
|
|
201
|
-
before,
|
|
202
|
-
after,
|
|
203
|
-
limit,
|
|
204
|
-
created_before,
|
|
205
|
-
created_after,
|
|
206
|
-
backward,
|
|
207
|
-
correlation,
|
|
208
|
-
with_snaps = false
|
|
209
|
-
} = query || {};
|
|
210
|
-
let sql = `SELECT * FROM ${this._fqt}`;
|
|
211
|
-
const conditions = [];
|
|
212
|
-
const values = [];
|
|
213
|
-
if (query) {
|
|
214
|
-
if (typeof after !== "undefined") {
|
|
215
|
-
values.push(after);
|
|
216
|
-
conditions.push(`id>$${values.length}`);
|
|
217
|
-
} else {
|
|
218
|
-
conditions.push("id>-1");
|
|
219
|
-
}
|
|
220
|
-
if (stream) {
|
|
221
|
-
values.push(stream);
|
|
222
|
-
conditions.push(
|
|
223
|
-
query.stream_exact ? `stream = $${values.length}` : `stream ~ $${values.length}`
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
if (names?.length) {
|
|
227
|
-
values.push(names);
|
|
228
|
-
conditions.push(`name = ANY($${values.length})`);
|
|
229
|
-
}
|
|
230
|
-
if (before) {
|
|
231
|
-
values.push(before);
|
|
232
|
-
conditions.push(`id<$${values.length}`);
|
|
233
|
-
}
|
|
234
|
-
if (created_after) {
|
|
235
|
-
values.push(created_after.toISOString());
|
|
236
|
-
conditions.push(`created>$${values.length}`);
|
|
237
|
-
}
|
|
238
|
-
if (created_before) {
|
|
239
|
-
values.push(created_before.toISOString());
|
|
240
|
-
conditions.push(`created<$${values.length}`);
|
|
241
|
-
}
|
|
242
|
-
if (correlation) {
|
|
243
|
-
values.push(correlation);
|
|
244
|
-
conditions.push(`meta->>'correlation'=$${values.length}`);
|
|
245
|
-
}
|
|
246
|
-
if (!with_snaps) {
|
|
247
|
-
conditions.push(`name <> '${import_act.SNAP_EVENT}'`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (conditions.length) {
|
|
251
|
-
sql += " WHERE " + conditions.join(" AND ");
|
|
252
|
-
}
|
|
253
|
-
sql += ` ORDER BY id ${backward ? "DESC" : "ASC"}`;
|
|
254
|
-
if (limit) {
|
|
255
|
-
values.push(limit);
|
|
256
|
-
sql += ` LIMIT $${values.length}`;
|
|
257
|
-
}
|
|
258
|
-
const result = await this._pool.query(sql, values);
|
|
259
|
-
for (const row of result.rows) callback(row);
|
|
260
|
-
return result.rowCount ?? 0;
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Commit new events to the store for a given stream, with concurrency control.
|
|
264
|
-
*
|
|
265
|
-
* @param stream The stream name
|
|
266
|
-
* @param msgs Array of messages (event name and data)
|
|
267
|
-
* @param meta Event metadata (correlation, causation, etc.)
|
|
268
|
-
* @param expectedVersion (Optional) Expected stream version for concurrency control
|
|
269
|
-
* @returns Array of committed events
|
|
270
|
-
* @throws ConcurrencyError if the expected version does not match
|
|
271
|
-
*/
|
|
272
|
-
async commit(stream, msgs, meta, expectedVersion) {
|
|
273
|
-
if (msgs.length === 0) return [];
|
|
274
|
-
const client = await this._pool.connect();
|
|
275
|
-
let version = -1;
|
|
276
|
-
try {
|
|
277
|
-
await client.query("BEGIN");
|
|
278
|
-
const last = await client.query(
|
|
279
|
-
`SELECT version
|
|
280
|
-
FROM ${this._fqt}
|
|
281
|
-
WHERE stream=$1 ORDER BY version DESC LIMIT 1`,
|
|
282
|
-
[stream]
|
|
283
|
-
);
|
|
284
|
-
version = last.rowCount ? last.rows[0].version : -1;
|
|
285
|
-
if (typeof expectedVersion === "number" && version !== expectedVersion)
|
|
286
|
-
throw new import_act.ConcurrencyError(
|
|
287
|
-
stream,
|
|
288
|
-
version,
|
|
289
|
-
msgs,
|
|
290
|
-
expectedVersion
|
|
291
|
-
);
|
|
292
|
-
const committed = [];
|
|
293
|
-
for (const { name, data } of msgs) {
|
|
294
|
-
version++;
|
|
295
|
-
const sql = `
|
|
296
|
-
INSERT INTO ${this._fqt}(name, data, stream, version, meta)
|
|
297
|
-
VALUES($1, $2, $3, $4, $5) RETURNING *`;
|
|
298
|
-
const vals = [name, data, stream, version, meta];
|
|
299
|
-
try {
|
|
300
|
-
const { rows } = await client.query(sql, vals);
|
|
301
|
-
committed.push(rows.at(0));
|
|
302
|
-
} catch (error) {
|
|
303
|
-
if (error?.code === PG_UNIQUE_VIOLATION) {
|
|
304
|
-
throw new import_act.ConcurrencyError(
|
|
305
|
-
stream,
|
|
306
|
-
version - 1,
|
|
307
|
-
msgs,
|
|
308
|
-
expectedVersion ?? -1
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
throw error;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
await client.query(
|
|
315
|
-
`
|
|
316
|
-
NOTIFY "${this.config.table}", '${JSON.stringify({
|
|
317
|
-
operation: "INSERT",
|
|
318
|
-
id: committed[0].name,
|
|
319
|
-
position: committed[0].id
|
|
320
|
-
})}';
|
|
321
|
-
COMMIT;
|
|
322
|
-
`
|
|
323
|
-
).catch((error) => {
|
|
324
|
-
logger.error(error);
|
|
325
|
-
throw new import_act.ConcurrencyError(
|
|
326
|
-
stream,
|
|
327
|
-
version,
|
|
328
|
-
msgs,
|
|
329
|
-
expectedVersion || -1
|
|
330
|
-
);
|
|
331
|
-
});
|
|
332
|
-
return committed;
|
|
333
|
-
} catch (error) {
|
|
334
|
-
await client.query("ROLLBACK").catch(() => {
|
|
335
|
-
});
|
|
336
|
-
throw error;
|
|
337
|
-
} finally {
|
|
338
|
-
client.release();
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Atomically discovers and leases streams for reaction processing.
|
|
343
|
-
*
|
|
344
|
-
* Uses `FOR UPDATE SKIP LOCKED` to implement zero-contention competing consumers:
|
|
345
|
-
* - Workers never block each other — locked rows are silently skipped
|
|
346
|
-
* - Discovery and locking happen in a single atomic transaction
|
|
347
|
-
* - No wasted polls — every returned stream is exclusively owned
|
|
348
|
-
*
|
|
349
|
-
* @param lagging - Max streams from lagging frontier (ascending watermark)
|
|
350
|
-
* @param leading - Max streams from leading frontier (descending watermark)
|
|
351
|
-
* @param by - Lease holder identifier (UUID)
|
|
352
|
-
* @param millis - Lease duration in milliseconds
|
|
353
|
-
* @returns Leased streams with metadata
|
|
354
|
-
*/
|
|
355
|
-
async claim(lagging, leading, by, millis) {
|
|
356
|
-
const client = await this._pool.connect();
|
|
357
|
-
try {
|
|
358
|
-
await client.query("BEGIN");
|
|
359
|
-
const { rows } = await client.query(
|
|
360
|
-
`
|
|
361
|
-
WITH
|
|
362
|
-
available AS (
|
|
363
|
-
SELECT stream, source, at
|
|
364
|
-
FROM ${this._fqs} s
|
|
365
|
-
WHERE blocked = false
|
|
366
|
-
AND (leased_by IS NULL OR leased_until <= NOW())
|
|
367
|
-
AND (s.at < 0 OR EXISTS (
|
|
368
|
-
SELECT 1 FROM ${this._fqt} e
|
|
369
|
-
WHERE e.id > s.at
|
|
370
|
-
AND e.name <> '${import_act.SNAP_EVENT}'
|
|
371
|
-
AND (s.source IS NULL OR e.stream = COALESCE(s.source, s.stream))
|
|
372
|
-
LIMIT 1
|
|
373
|
-
))
|
|
374
|
-
FOR UPDATE SKIP LOCKED
|
|
375
|
-
),
|
|
376
|
-
lag AS (
|
|
377
|
-
SELECT stream, source, at, TRUE AS lagging
|
|
378
|
-
FROM available
|
|
379
|
-
ORDER BY at ASC
|
|
380
|
-
LIMIT $1
|
|
381
|
-
),
|
|
382
|
-
lead AS (
|
|
383
|
-
SELECT stream, source, at, FALSE AS lagging
|
|
384
|
-
FROM available
|
|
385
|
-
ORDER BY at DESC
|
|
386
|
-
LIMIT $2
|
|
387
|
-
),
|
|
388
|
-
combined AS (
|
|
389
|
-
SELECT DISTINCT ON (stream) stream, source, at, lagging
|
|
390
|
-
FROM (SELECT * FROM lag UNION ALL SELECT * FROM lead) t
|
|
391
|
-
ORDER BY stream, at
|
|
392
|
-
)
|
|
393
|
-
UPDATE ${this._fqs} s
|
|
394
|
-
SET
|
|
395
|
-
leased_by = $3,
|
|
396
|
-
leased_until = NOW() + ($4::integer || ' milliseconds')::interval,
|
|
397
|
-
retry = s.retry + 1
|
|
398
|
-
FROM combined c
|
|
399
|
-
WHERE s.stream = c.stream
|
|
400
|
-
RETURNING s.stream, s.source, s.at, s.retry, c.lagging
|
|
401
|
-
`,
|
|
402
|
-
[lagging, leading, by, millis]
|
|
403
|
-
);
|
|
404
|
-
await client.query("COMMIT");
|
|
405
|
-
return rows.map(({ stream, source, at, retry, lagging: lagging2 }) => ({
|
|
406
|
-
stream,
|
|
407
|
-
source: source ?? void 0,
|
|
408
|
-
at,
|
|
409
|
-
by,
|
|
410
|
-
retry,
|
|
411
|
-
lagging: lagging2
|
|
412
|
-
}));
|
|
413
|
-
} catch (error) {
|
|
414
|
-
await client.query("ROLLBACK").catch(() => {
|
|
415
|
-
});
|
|
416
|
-
logger.error(error);
|
|
417
|
-
return [];
|
|
418
|
-
} finally {
|
|
419
|
-
client.release();
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Registers streams for event processing.
|
|
424
|
-
* Upserts stream entries so they become visible to claim().
|
|
425
|
-
* Also returns the current max watermark across all subscriptions.
|
|
426
|
-
* @param streams - Streams to register with optional source.
|
|
427
|
-
* @returns subscribed count and current max watermark.
|
|
428
|
-
*/
|
|
429
|
-
async subscribe(streams) {
|
|
430
|
-
const client = await this._pool.connect();
|
|
431
|
-
try {
|
|
432
|
-
await client.query("BEGIN");
|
|
433
|
-
let subscribed = 0;
|
|
434
|
-
if (streams.length) {
|
|
435
|
-
const { rowCount } = await client.query(
|
|
436
|
-
`
|
|
437
|
-
INSERT INTO ${this._fqs} (stream, source)
|
|
438
|
-
SELECT s->>'stream', s->>'source'
|
|
439
|
-
FROM jsonb_array_elements($1::jsonb) AS s
|
|
440
|
-
ON CONFLICT (stream) DO NOTHING
|
|
441
|
-
`,
|
|
442
|
-
[JSON.stringify(streams)]
|
|
443
|
-
);
|
|
444
|
-
subscribed = rowCount ?? 0;
|
|
445
|
-
}
|
|
446
|
-
const { rows } = await client.query(
|
|
447
|
-
`SELECT COALESCE(MAX(at), -1) AS max FROM ${this._fqs}`
|
|
448
|
-
);
|
|
449
|
-
await client.query("COMMIT");
|
|
450
|
-
return { subscribed, watermark: rows[0]?.max ?? -1 };
|
|
451
|
-
} catch (error) {
|
|
452
|
-
await client.query("ROLLBACK").catch(() => {
|
|
453
|
-
});
|
|
454
|
-
logger.error(error);
|
|
455
|
-
return { subscribed: 0, watermark: -1 };
|
|
456
|
-
} finally {
|
|
457
|
-
client.release();
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* Acknowledge and release leases after processing, updating stream positions.
|
|
462
|
-
*
|
|
463
|
-
* @param leases - Leases to acknowledge, including last processed watermark and lease holder.
|
|
464
|
-
* @returns Acked leases.
|
|
465
|
-
*/
|
|
466
|
-
async ack(leases) {
|
|
467
|
-
const client = await this._pool.connect();
|
|
468
|
-
try {
|
|
469
|
-
await client.query("BEGIN");
|
|
470
|
-
const { rows } = await client.query(
|
|
471
|
-
`
|
|
472
|
-
WITH input AS (
|
|
473
|
-
SELECT * FROM jsonb_to_recordset($1::jsonb)
|
|
474
|
-
AS x(stream text, by text, at int, lagging boolean)
|
|
475
|
-
)
|
|
476
|
-
UPDATE ${this._fqs} AS s
|
|
477
|
-
SET
|
|
478
|
-
at = i.at,
|
|
479
|
-
retry = -1,
|
|
480
|
-
leased_by = NULL,
|
|
481
|
-
leased_until = NULL
|
|
482
|
-
FROM input i
|
|
483
|
-
WHERE s.stream = i.stream AND s.leased_by = i.by
|
|
484
|
-
RETURNING s.stream, s.source, s.at, i.by, s.retry, i.lagging
|
|
485
|
-
`,
|
|
486
|
-
[JSON.stringify(leases)]
|
|
487
|
-
);
|
|
488
|
-
await client.query("COMMIT");
|
|
489
|
-
return rows.map((row) => ({
|
|
490
|
-
stream: row.stream,
|
|
491
|
-
source: row.source ?? void 0,
|
|
492
|
-
at: row.at,
|
|
493
|
-
by: row.by,
|
|
494
|
-
retry: row.retry,
|
|
495
|
-
lagging: row.lagging
|
|
496
|
-
}));
|
|
497
|
-
} catch (error) {
|
|
498
|
-
await client.query("ROLLBACK").catch(() => {
|
|
499
|
-
});
|
|
500
|
-
logger.error(error);
|
|
501
|
-
return [];
|
|
502
|
-
} finally {
|
|
503
|
-
client.release();
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Block a stream for processing after failing to process and reaching max retries with blocking enabled.
|
|
508
|
-
* @param leases - Leases to block, including lease holder and last error message.
|
|
509
|
-
* @returns Blocked leases.
|
|
510
|
-
*/
|
|
511
|
-
async block(leases) {
|
|
512
|
-
const client = await this._pool.connect();
|
|
513
|
-
try {
|
|
514
|
-
await client.query("BEGIN");
|
|
515
|
-
const { rows } = await client.query(
|
|
516
|
-
`
|
|
517
|
-
WITH input AS (
|
|
518
|
-
SELECT * FROM jsonb_to_recordset($1::jsonb)
|
|
519
|
-
AS x(stream text, by text, error text, lagging boolean)
|
|
520
|
-
)
|
|
521
|
-
UPDATE ${this._fqs} AS s
|
|
522
|
-
SET blocked = true, error = i.error
|
|
523
|
-
FROM input i
|
|
524
|
-
WHERE s.stream = i.stream AND s.leased_by = i.by AND s.blocked = false
|
|
525
|
-
RETURNING s.stream, s.source, s.at, i.by, s.retry, s.error, i.lagging
|
|
526
|
-
`,
|
|
527
|
-
[JSON.stringify(leases)]
|
|
528
|
-
);
|
|
529
|
-
await client.query("COMMIT");
|
|
530
|
-
return rows.map((row) => ({
|
|
531
|
-
stream: row.stream,
|
|
532
|
-
source: row.source ?? void 0,
|
|
533
|
-
at: row.at,
|
|
534
|
-
by: row.by,
|
|
535
|
-
retry: row.retry,
|
|
536
|
-
lagging: row.lagging,
|
|
537
|
-
error: row.error
|
|
538
|
-
}));
|
|
539
|
-
} catch (error) {
|
|
540
|
-
await client.query("ROLLBACK").catch(() => {
|
|
541
|
-
});
|
|
542
|
-
logger.error(error);
|
|
543
|
-
return [];
|
|
544
|
-
} finally {
|
|
545
|
-
client.release();
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
/**
|
|
549
|
-
* Reset watermarks for the given streams to -1, clearing retry, blocked,
|
|
550
|
-
* error, and lease state so they can be replayed from the beginning.
|
|
551
|
-
* @param streams - Stream names to reset.
|
|
552
|
-
* @returns Count of streams that were actually reset.
|
|
553
|
-
*/
|
|
554
|
-
async reset(streams) {
|
|
555
|
-
if (!streams.length) return 0;
|
|
556
|
-
const { rowCount } = await this._pool.query(
|
|
557
|
-
`UPDATE ${this._fqs}
|
|
558
|
-
SET at = -1, retry = 0, blocked = false, error = NULL,
|
|
559
|
-
leased_by = NULL, leased_until = NULL
|
|
560
|
-
WHERE stream = ANY($1)`,
|
|
561
|
-
[streams]
|
|
562
|
-
);
|
|
563
|
-
return rowCount ?? 0;
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Streams subscription positions to a callback, ordered by stream name,
|
|
567
|
-
* along with the highest event id in the store.
|
|
568
|
-
*
|
|
569
|
-
* Filters (`stream`, `source`, `blocked`, `after`, `limit`) are applied
|
|
570
|
-
* server-side. `stream`/`source` are regex by default (`~`), or exact
|
|
571
|
-
* with `*_exact: true` — same convention as {@link Store.query}.
|
|
572
|
-
*
|
|
573
|
-
* @returns `maxEventId` and the `count` of positions emitted.
|
|
574
|
-
*/
|
|
575
|
-
async query_streams(callback, query) {
|
|
576
|
-
const limit = query?.limit ?? 100;
|
|
577
|
-
const conditions = [];
|
|
578
|
-
const values = [];
|
|
579
|
-
if (query?.stream !== void 0) {
|
|
580
|
-
values.push(query.stream);
|
|
581
|
-
conditions.push(
|
|
582
|
-
query.stream_exact ? `stream = $${values.length}` : `stream ~ $${values.length}`
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
if (query?.source !== void 0) {
|
|
586
|
-
conditions.push(`source IS NOT NULL`);
|
|
587
|
-
values.push(query.source);
|
|
588
|
-
conditions.push(
|
|
589
|
-
query.source_exact ? `source = $${values.length}` : `source ~ $${values.length}`
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
if (query?.blocked !== void 0) {
|
|
593
|
-
values.push(query.blocked);
|
|
594
|
-
conditions.push(`blocked = $${values.length}`);
|
|
595
|
-
}
|
|
596
|
-
if (query?.after !== void 0) {
|
|
597
|
-
values.push(query.after);
|
|
598
|
-
conditions.push(`stream > $${values.length}`);
|
|
599
|
-
}
|
|
600
|
-
let sql = `SELECT stream, source, at, retry, blocked, error, leased_by, leased_until FROM ${this._fqs}`;
|
|
601
|
-
if (conditions.length) sql += " WHERE " + conditions.join(" AND ");
|
|
602
|
-
values.push(limit);
|
|
603
|
-
sql += ` ORDER BY stream LIMIT $${values.length}`;
|
|
604
|
-
const client = await this._pool.connect();
|
|
605
|
-
try {
|
|
606
|
-
const [streamsResult, maxResult] = await Promise.all([
|
|
607
|
-
client.query(sql, values),
|
|
608
|
-
client.query(
|
|
609
|
-
`SELECT COALESCE(MAX(id), -1) AS m FROM ${this._fqt}`
|
|
610
|
-
)
|
|
611
|
-
]);
|
|
612
|
-
let count = 0;
|
|
613
|
-
for (const row of streamsResult.rows) {
|
|
614
|
-
callback({
|
|
615
|
-
stream: row.stream,
|
|
616
|
-
source: row.source ?? void 0,
|
|
617
|
-
at: row.at,
|
|
618
|
-
retry: row.retry,
|
|
619
|
-
blocked: row.blocked,
|
|
620
|
-
error: row.error ?? "",
|
|
621
|
-
leased_by: row.leased_by ?? void 0,
|
|
622
|
-
leased_until: row.leased_until ?? void 0
|
|
623
|
-
});
|
|
624
|
-
count++;
|
|
625
|
-
}
|
|
626
|
-
return { maxEventId: Number(maxResult.rows[0].m), count };
|
|
627
|
-
} finally {
|
|
628
|
-
client.release();
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
/**
|
|
632
|
-
* Atomically truncates streams and seeds each with a snapshot or tombstone.
|
|
633
|
-
* @param targets - Streams to truncate with optional snapshot state and meta.
|
|
634
|
-
* @returns Map keyed by stream name, each entry with `deleted` count and `committed` event.
|
|
635
|
-
*/
|
|
636
|
-
async truncate(targets) {
|
|
637
|
-
if (!targets.length) return /* @__PURE__ */ new Map();
|
|
638
|
-
const streams = targets.map((t) => t.stream);
|
|
639
|
-
const client = await this._pool.connect();
|
|
640
|
-
try {
|
|
641
|
-
await client.query("BEGIN");
|
|
642
|
-
await client.query(`DELETE FROM ${this._fqs} WHERE stream = ANY($1)`, [
|
|
643
|
-
streams
|
|
644
|
-
]);
|
|
645
|
-
const result = /* @__PURE__ */ new Map();
|
|
646
|
-
for (const { stream, snapshot, meta } of targets) {
|
|
647
|
-
const { rowCount } = await client.query(
|
|
648
|
-
`DELETE FROM ${this._fqt} WHERE stream = $1`,
|
|
649
|
-
[stream]
|
|
650
|
-
);
|
|
651
|
-
const name = snapshot !== void 0 ? import_act.SNAP_EVENT : import_act.TOMBSTONE_EVENT;
|
|
652
|
-
const { rows } = await client.query(
|
|
653
|
-
`INSERT INTO ${this._fqt}(name, data, stream, version, created, meta)
|
|
654
|
-
VALUES($1, $2, $3, 0, now(), $4) RETURNING *`,
|
|
655
|
-
[
|
|
656
|
-
name,
|
|
657
|
-
snapshot ?? {},
|
|
658
|
-
stream,
|
|
659
|
-
meta ?? { correlation: "", causation: {} }
|
|
660
|
-
]
|
|
661
|
-
);
|
|
662
|
-
result.set(stream, {
|
|
663
|
-
deleted: rowCount ?? 0,
|
|
664
|
-
committed: rows[0]
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
await client.query("COMMIT");
|
|
668
|
-
return result;
|
|
669
|
-
} catch (error) {
|
|
670
|
-
await client.query("ROLLBACK").catch(() => {
|
|
671
|
-
});
|
|
672
|
-
throw error;
|
|
673
|
-
} finally {
|
|
674
|
-
client.release();
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
};
|
|
678
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
679
|
-
0 && (module.exports = {
|
|
680
|
-
PostgresStore
|
|
681
|
-
});
|
|
682
|
-
//# sourceMappingURL=index.cjs.map
|