import { LogContext } from '@rocicorp/logger';
import * as v from "../../../shared/src/valita.js";
import { Database } from "../../../zqlite/src/db.js";
import { deleteLiteDB } from "../db/delete-lite-db.js";
import { upgradeReplica } from "../services/change-source/replica-schema.js";
import { Notifier } from "../services/replicator/notifier.js";
import { getAscendingEvents, recordEvent, } from "../services/replicator/schema/replication-state.js";
export const replicaFileModeSchema = v.literalUnion('serving', 'serving-copy', 'backup');
export function replicaFileName(replicaFile, mode) {
    return mode === 'serving-copy' ? `${replicaFile}-serving-copy` : replicaFile;
}
const MILLIS_PER_HOUR = 1000 * 60 * 60;
const MB = 1024 * 1024;
async function connect(lc, { file, vacuumIntervalHours }, walMode, mode) {
    const replica = new Database(lc, file);
    // Perform any upgrades to the replica in case the backup is an
    // earlier version.
    await upgradeReplica(lc, `${mode}-replica`, file);
    // Start by folding any (e.g. restored) WAL(2) files into the main db.
    replica.pragma('journal_mode = delete');
    const [{ page_size: pageSize }] = replica.pragma('page_size');
    const [{ page_count: pageCount }] = replica.pragma('page_count');
    const [{ freelist_count: freelistCount }] = replica.pragma('freelist_count');
    const dbSize = ((pageCount * pageSize) / MB).toFixed(2);
    const freelistSize = ((freelistCount * pageSize) / MB).toFixed(2);
    // TODO: Consider adding a freelist size or ratio based vacuum trigger.
    lc.info?.(`Size of db ${file}: ${dbSize} MB (${freelistSize} MB freeable)`);
    // Check for the VACUUM threshold.
    const events = getAscendingEvents(replica);
    lc.debug?.(`Runtime events for db ${file}`, { events });
    if (vacuumIntervalHours !== undefined) {
        const millisSinceLastEvent = Date.now() - (events.at(-1)?.timestamp.getTime() ?? 0);
        if (millisSinceLastEvent / MILLIS_PER_HOUR > vacuumIntervalHours) {
            lc.info?.(`Performing maintenance cleanup on ${file}`);
            const t0 = performance.now();
            replica.unsafeMode(true);
            replica.pragma('journal_mode = OFF');
            // Clear the changeLog to reclaim as much space as possible. The
            // changeLog is only used for IVM advancements (i.e. from an initial
            // hydration), so it is fine for it to be empty at startup.
            replica.exec('DELETE FROM "_zero.changeLog"');
            const t1 = performance.now();
            lc.info?.(`Cleared _zero.changeLog (${t1 - t0} ms)`);
            replica.exec('VACUUM');
            recordEvent(replica, 'vacuum');
            replica.unsafeMode(false);
            const t2 = performance.now();
            lc.info?.(`VACUUM completed (${t2 - t1} ms)`);
        }
    }
    lc.info?.(`setting ${file} to ${walMode} mode`);
    replica.pragma(`journal_mode = ${walMode}`);
    // The duration of the loop in which the replicator attempts
    // to begin a transaction while litestream is performing a
    // checkpoint.
    replica.pragma('busy_timeout = 30000');
    replica.pragma('optimize = 0x10002');
    // Cap the running time of `PRAGMA optimize` calls that happen
    // after replicating schema changes. 1000 is the limit recommended
    // in https://sqlite.org/lang_analyze.html#approx
    replica.pragma('analysis_limit = 1000');
    lc.info?.(`optimized ${file}`);
    return replica;
}
export async function setupReplica(lc, mode, replicaOptions) {
    lc.info?.(`setting up ${mode} replica`);
    switch (mode) {
        case 'backup': {
            const replica = await connect(lc, replicaOptions, 'wal', mode);
            // https://litestream.io/tips/#disable-autocheckpoints-for-high-write-load-servers
            replica.pragma('wal_autocheckpoint = 0');
            return replica;
        }
        case 'serving-copy': {
            // In 'serving-copy' mode, the original file is being used for 'backup'
            // mode, so we make a copy for servicing sync requests.
            const { file } = replicaOptions;
            const copyLocation = replicaFileName(file, mode);
            deleteLiteDB(copyLocation);
            const start = Date.now();
            lc.info?.(`copying ${file} to ${copyLocation}`);
            const replica = new Database(lc, file);
            replica.prepare(`VACUUM INTO ?`).run(copyLocation);
            replica.close();
            lc.info?.(`finished copy (${Date.now() - start} ms)`);
            return connect(lc, { ...replicaOptions, file: copyLocation }, 'wal2', mode);
        }
        case 'serving':
            return connect(lc, replicaOptions, 'wal2', mode);
        default:
            throw new Error(`Invalid ReplicaMode ${mode}`);
    }
}
export function setUpMessageHandlers(lc, replicator, parent) {
    handleSubscriptionsFrom(lc, parent, replicator);
}
export function handleSubscriptionsFrom(lc, subscriber, notifier) {
    subscriber.onMessageType('subscribe', async () => {
        const subscription = notifier.subscribe();
        subscriber.on('close', () => {
            lc.debug?.(`closing replication subscription from ${subscriber.pid}`);
            subscription.cancel();
        });
        for await (const msg of subscription) {
            subscriber.send(['notify', msg]);
        }
    });
}
/**
 * Creates a Notifier to relay notifications the notifier of another Worker.
 * This does not send the initial subscription message. Use {@link subscribeTo}
 * to initiate the subscription.
 */
export function createNotifierFrom(_lc, source) {
    const notifier = new Notifier();
    source.onMessageType('notify', msg => notifier.notifySubscribers(msg));
    return notifier;
}
export function subscribeTo(_lc, source) {
    source.send(['subscribe', {}]);
}
//# sourceMappingURL=replicator.js.map