@dwwoelfel/lds
Version:
Logical decoding server for PostgreSQL, monitors for new/edited/deleted rows and announces them to interested clients.
185 lines • 6.33 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/* tslint:disable no-console */
const pg = require("pg");
const events_1 = require("events");
const fatal_error_1 = require("./fatal-error");
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
exports.changeToRecord = (change) => {
const { columnnames, columnvalues } = change;
return columnnames.reduce((memo, name, i) => {
memo[name] = columnvalues[i];
return memo;
}, {});
};
exports.changeToPk = (change) => {
return change.oldkeys.keyvalues;
};
const toLsnData = ([lsn, data]) => ({
lsn,
data: JSON.parse(data),
});
class PgLogicalDecoding extends events_1.EventEmitter {
constructor(connectionString, options) {
super();
this.onPoolError = (err) => {
if (this.client) {
this.client.then(c => c.release(err)).catch(() => {
// noop
});
}
this.client = null;
console.error("LDS pool error:", err.message);
// this.emit("error", err);
};
this.connectionString = connectionString;
const { tablePattern = "*.*", slotName = "postgraphile", temporary = false, } = options || {};
this.tablePattern = tablePattern;
this.slotName = slotName;
this.temporary = temporary;
// We just use the pool to get better error handling
this.pool = new pg.Pool({
connectionString: this.connectionString,
max: 1,
});
this.pool.on("error", this.onPoolError);
}
async dropStaleSlots() {
const client = await this.getClient();
try {
await client.query(`
with deleted_slots as (
delete from postgraphile_meta.logical_decoding_slots
where last_checkin < now() - interval '1 hour'
returning *
)
select pg_catalog.pg_drop_replication_slot(slot_name)
from deleted_slots
where exists (
select 1
from pg_catalog.pg_replication_slots
where pg_replication_slots.slot_name = deleted_slots.slot_name
)
`);
}
catch (e) {
if (e.code === "42P01") {
// The `postgraphile_meta.logical_decoding_slots` table doesn't exist.
// Ignore.
}
else {
console.error("Error clearing stale slots:", e.message);
}
}
}
async createSlot() {
const client = await this.getClient();
await this.trackSelf(client);
try {
await client.query(`SELECT pg_catalog.pg_create_logical_replication_slot($1, 'wal2json', $2)`, [this.slotName, !!this.temporary]);
}
catch (e) {
if (e.code === "58P01") {
const err = new fatal_error_1.default("Couldn't create replication slot, seems you don't have wal2json installed? Error: " +
e.message, e);
throw err;
}
else {
throw e;
}
}
}
async getChanges(uptoLsn = null, uptoNchanges = null) {
const client = await this.getClient();
await this.trackSelf(client);
try {
const { rows } = await client.query({
text: `SELECT lsn, data FROM pg_catalog.pg_logical_slot_get_changes($1, $2, $3, 'add-tables', $4::text, 'format-version', '1')`,
values: [this.slotName, uptoLsn, uptoNchanges, this.tablePattern],
rowMode: "array",
});
return rows.map(toLsnData);
}
catch (e) {
if (e.code === "42704") {
console.warn("Replication slot went away?");
await this.createSlot();
console.warn("Recreated slot; retrying getChanges (no further output implies success)");
await sleep(500);
return this.getChanges(uptoLsn, uptoNchanges);
}
throw e;
}
}
async close() {
if (!this.temporary) {
const client = await this.getClient();
await client.query("select pg_catalog.pg_drop_replication_slot($1)", [
this.slotName,
]);
await client.query("delete from postgraphile_meta.logical_decoding_slots where slot_name = $1", [this.slotName]);
}
if (this.client) {
try {
(await this.client).release();
}
catch (e) {
/*noop*/
}
this.client = null;
}
if (this.pool) {
await this.pool.end();
this.pool = null;
}
}
async installSchema() {
const client = await this.getClient();
await client.query(`
create schema if not exists postgraphile_meta;
create table if not exists postgraphile_meta.logical_decoding_slots (
slot_name text primary key,
last_checkin timestamptz not null default now()
);
`);
}
/****************************************************************************/
async getClient() {
if (!this.pool) {
throw new Error("Pool has been closed");
}
if (this.client) {
return this.client;
}
this.client = this.pool.connect();
return this.client.catch(e => {
this.client = null;
return Promise.reject(e);
});
}
async trackSelf(client, skipSchema = false) {
if (this.temporary) {
// No need to track temporary replication slots
return;
}
try {
await client.query(`
insert into postgraphile_meta.logical_decoding_slots(slot_name)
values ($1)
on conflict (slot_name)
do update set last_checkin = now();
`, [this.slotName]);
}
catch (e) {
if (!skipSchema) {
await this.installSchema();
return this.trackSelf(client, true);
}
else {
throw e;
}
}
}
}
exports.default = PgLogicalDecoding;
//# sourceMappingURL=pg-logical-decoding.js.map