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 "../../../../types/pg.js";
import { id, lit } from "../../../../types/sql.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;
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
    )
  );
  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(lsn2) {
    writable.write(makeAck(lsn2));
    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, { returnJsonAsString: true });
  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.warn?.(`error from ${db.options.host}`, e)
    )
  );
  pipe({
    source: readable,
    sink: messages,
    parse: (buffer) => parseStreamMessage(lc, buffer, parser),
    // Allow a small buffer of messages to be queued in the subscription so
    // that the change-source loop can check the queue to determine if more
    // messages are immediately available.
    bufferMessages: 5
  });
  return {
    messages,
    acks: { push: sendAck }
  };
}
function formatPublicationNames(publications) {
  return publications.map((p) => lit(p).slice(1, -1)).join(",");
}
async function startReplicationStream(lc, session, slot, publications, lsn, maxAttempts) {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      const stream = session.unsafe(
        `START_REPLICATION SLOT ${id(slot)} LOGICAL ${fromBigInt(lsn)} (
        proto_version '1',
        publication_names '${formatPublicationNames(publications)}',
        messages 'true'
      )`
      ).execute();
      return await Promise.all([stream.readable(), stream.writable()]);
    } catch (e) {
      if (e instanceof postgres.PostgresError) {
        if (e.code === PG_OBJECT_IN_USE) {
          lc.warn?.(`attempt ${i + 1}: ${String(e)}`, e);
          await sleep(10);
          continue;
        }
        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) {
  if (buffer[0] !== 119 && buffer[0] !== 107) {
    lc.warn?.("Unknown message", buffer[0]);
    return null;
  }
  const lsn = buffer.readBigUInt64BE(1);
  if (buffer[0] === 119) {
    return [lsn, parser.parse(buffer.subarray(25))];
  }
  const shouldRespond = buffer.readInt8(17) !== 0;
  return [lsn, { tag: "keepalive", shouldRespond }];
}
function makeAck(lsn) {
  const microNow = BigInt(Date.now() - Date.UTC(2e3, 0, 1)) * BigInt(1e3);
  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;
}
export {
  subscribe
};
//# sourceMappingURL=stream.js.map
