import { PG_ADMIN_SHUTDOWN, PG_OBJECT_IN_USE, PG_OBJECT_NOT_IN_PREREQUISITE_STATE, } from '@drdgvhbh/postgres-error-codes';
import { defu } from 'defu';
import postgres, {} from 'postgres';
import { sleep } from "../../../../../../shared/src/sleep.js";
import { getTypeParsers } from "../../../../db/pg-type-parser.js";
import {} from "../../../../types/pg.js";
import { pipe } from "../../../../types/streams.js";
import { Subscription } from "../../../../types/subscription.js";
import { AutoResetSignal } from "../../../change-streamer/schema/tables.js";
import { fromBigInt } from "../lsn.js";
import { PgoutputParser } from "./pgoutput-parser.js";
const DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE = 5;
export async function subscribe(lc, db, slot, publications, lsn, retriesIfReplicationSlotActive = DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE, applicationName = 'zero-replicator') {
    const session = postgres(defu({
        max: 1,
        ['fetch_types']: false, // Necessary for the streaming protocol
        ['idle_timeout']: null,
        ['max_lifetime']: null,
        connection: {
            ['application_name']: applicationName,
            replication: 'database', // https://www.postgresql.org/docs/current/protocol-replication.html
        },
    }, 
    // ParsedOptions are technically compatible with Options, but happen
    // to not be typed that way. The postgres.js author does an equivalent
    // merge of ParsedOptions and Options here:
    // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/subscribe.js#L13
    db.options));
    // Postgres will send keepalives before timing out a wal_sender. It is possible that
    // these keepalives are not received if there is back-pressure in the replication
    // stream. To keep the connection alive, explicitly send keepalives if none have been
    // sent within the last 75% of the wal_sender_timeout.
    //
    // https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT
    const [{ walSenderTimeoutMs }] = await session `SELECT EXTRACT(EPOCH FROM (setting || unit)::interval) * 1000 
        AS "walSenderTimeoutMs" FROM pg_settings
        WHERE name = 'wal_sender_timeout'`.simple();
    const manualKeepaliveTimeout = Math.floor(walSenderTimeoutMs * 0.75);
    lc.info?.(`wal_sender_timeout: ${walSenderTimeoutMs}ms. ` +
        `Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms`);
    const [readable, writable] = await startReplicationStream(lc, session, slot, publications, lsn, retriesIfReplicationSlotActive + 1);
    let lastAckTime = Date.now();
    function sendAck(lsn) {
        writable.write(makeAck(lsn));
        lastAckTime = Date.now();
    }
    const livenessTimer = setInterval(() => {
        const now = Date.now();
        if (now - lastAckTime > manualKeepaliveTimeout) {
            sendAck(0n);
            lc.debug?.(`sent manual keepalive`);
        }
    }, manualKeepaliveTimeout / 5);
    let destroyed = false;
    const typeParsers = await getTypeParsers(db);
    const parser = new PgoutputParser(typeParsers);
    const messages = Subscription.create({
        cleanup: () => {
            destroyed = true;
            readable.destroyed || readable.destroy();
            clearInterval(livenessTimer);
            return session.end();
        },
    });
    readable.once('close', () => 
    // Only log a warning if the stream was not manually closed.
    destroyed || lc.warn?.(`replication stream closed by ${db.options.host}`));
    readable.once('error', e => 
    // Don't log the shutdown signal. This is the expected way for upstream
    // to close the connection (and will be logged downstream).
    (e instanceof postgres.PostgresError && e.code === PG_ADMIN_SHUTDOWN) ||
        lc.error?.(`error from ${db.options.host}`, e));
    pipe(readable, messages, buffer => parseStreamMessage(lc, buffer, parser));
    return {
        messages,
        acks: { push: sendAck },
    };
}
async function startReplicationStream(lc, session, slot, publications, lsn, maxAttempts) {
    for (let i = 0; i < maxAttempts; i++) {
        try {
            const stream = session
                .unsafe(`START_REPLICATION SLOT "${slot}" LOGICAL ${fromBigInt(lsn)} (
        proto_version '1', 
        publication_names '${publications}',
        messages 'true'
      )`)
                .execute();
            return await Promise.all([stream.readable(), stream.writable()]);
        }
        catch (e) {
            if (e instanceof postgres.PostgresError) {
                // error: replication slot "zero_slot_change_source_test_id" is active for PID 268
                if (e.code === PG_OBJECT_IN_USE) {
                    // The freeing up of the replication slot is not transactional;
                    // sometimes it takes time for Postgres to consider the slot
                    // inactive.
                    lc.warn?.(`attempt ${i + 1}: ${String(e)}`, e);
                    await sleep(10);
                    continue;
                }
                // error: This slot has been invalidated because it exceeded the maximum reserved size.
                // (This is a different manifestation of a slot being invalidated when
                //  the wal exceeds the max_slot_wal_keep_size)
                if (e.code === PG_OBJECT_NOT_IN_PREREQUISITE_STATE) {
                    lc.error?.(`error starting replication stream`, e);
                    throw new AutoResetSignal(`unable to start replication stream`, {
                        cause: e,
                    });
                }
            }
            throw e;
        }
    }
    throw new Error(`exceeded max attempts (${maxAttempts}) to start the Postgres stream`);
}
function parseStreamMessage(lc, buffer, parser) {
    // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-XLOGDATA
    if (buffer[0] !== 0x77 && buffer[0] !== 0x6b) {
        lc.warn?.('Unknown message', buffer[0]);
        return null;
    }
    const lsn = buffer.readBigUInt64BE(1);
    if (buffer[0] === 0x77) {
        // XLogData
        return [lsn, parser.parse(buffer.subarray(25))];
    }
    if (buffer.readInt8(17)) {
        // Primary keepalive message: shouldRespond
        lc.debug?.(`pg keepalive (shouldRespond: true) ${lsn}`);
        return [lsn, { tag: 'keepalive' }];
    }
    return null;
}
// https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-STANDBY-STATUS-UPDATE
function makeAck(lsn) {
    const microNow = BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000);
    const x = Buffer.alloc(34);
    x[0] = 'r'.charCodeAt(0);
    x.writeBigInt64BE(lsn, 1);
    x.writeBigInt64BE(lsn, 9);
    x.writeBigInt64BE(lsn, 17);
    x.writeBigInt64BE(microNow, 25);
    return x;
}
//# sourceMappingURL=stream.js.map