"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
Object.defineProperty(exports, "__esModule", { value: true });
exports.TraceProcessorLeaseStore = void 0;
exports.resolveHolderTtlPolicy = resolveHolderTtlPolicy;
exports.getTraceProcessorLeaseStore = getTraceProcessorLeaseStore;
exports.setTraceProcessorLeaseStoreForTests = setTraceProcessorLeaseStoreForTests;
const uuid_1 = require("../utils/uuid");
const enterpriseDb_1 = require("./enterpriseDb");
const TERMINAL_STATES = new Set(['released', 'failed']);
const ACQUIRABLE_STATES = new Set([
    'pending',
    'starting',
    'ready',
    'idle',
    'active',
    'crashed',
    'restarting',
]);
const ALLOWED_TRANSITIONS = {
    pending: ['starting', 'draining', 'released', 'failed'],
    starting: ['ready', 'draining', 'crashed', 'failed'],
    ready: ['idle', 'active', 'draining', 'crashed', 'failed'],
    idle: ['active', 'draining', 'released', 'crashed', 'failed'],
    active: ['idle', 'draining', 'crashed', 'failed'],
    draining: ['released', 'failed'],
    released: [],
    crashed: ['restarting', 'draining', 'failed'],
    restarting: ['ready', 'draining', 'failed'],
    failed: [],
};
function assertNonEmpty(value, name) {
    if (!value.trim()) {
        throw new Error(`${name} is required`);
    }
}
function assertValidTransition(from, to) {
    if (from === to)
        return;
    if (!ALLOWED_TRANSITIONS[from].includes(to)) {
        throw new Error(`Invalid trace processor lease transition: ${from} -> ${to}`);
    }
}
function parseMetadata(raw) {
    if (!raw)
        return null;
    try {
        const parsed = JSON.parse(raw);
        return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
            ? parsed
            : null;
    }
    catch {
        return null;
    }
}
function metadataForHolder(holder) {
    const metadata = {
        ...(holder.metadata ?? {}),
        ...(holder.frontendVisibility ? { frontendVisibility: holder.frontendVisibility } : {}),
        ...(holder.sessionId ? { sessionId: holder.sessionId } : {}),
        ...(holder.runId ? { runId: holder.runId } : {}),
        ...(holder.reportId ? { reportId: holder.reportId } : {}),
    };
    return Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null;
}
function resolveHolderTtlPolicy(holder) {
    if (holder.holderType === 'frontend_http_rpc') {
        const visibility = holder.frontendVisibility ?? 'visible';
        if (visibility === 'hidden') {
            return {
                heartbeatTtlMs: 10 * 60 * 1000,
                idleTtlMs: 8 * 60 * 60 * 1000,
            };
        }
        if (visibility === 'offline') {
            return {
                heartbeatTtlMs: 30 * 60 * 1000,
                idleTtlMs: 30 * 60 * 1000,
            };
        }
        return {
            heartbeatTtlMs: 90 * 1000,
            idleTtlMs: 4 * 60 * 60 * 1000,
        };
    }
    if (holder.holderType === 'manual_register') {
        return {
            heartbeatTtlMs: 5 * 60 * 1000,
            idleTtlMs: 60 * 60 * 1000,
        };
    }
    if (holder.holderType === 'metric_backfill') {
        return {
            heartbeatTtlMs: 5 * 60 * 1000,
            idleTtlMs: 30 * 60 * 1000,
        };
    }
    return {
        heartbeatTtlMs: 5 * 60 * 1000,
        idleTtlMs: 24 * 60 * 60 * 1000,
    };
}
class TraceProcessorLeaseStore {
    constructor(db = (0, enterpriseDb_1.openEnterpriseDb)()) {
        this.db = db;
    }
    close() {
        this.db.close();
    }
    acquireHolder(scope, traceId, holder, options = {}) {
        assertNonEmpty(scope.tenantId, 'tenantId');
        assertNonEmpty(scope.workspaceId, 'workspaceId');
        assertNonEmpty(traceId, 'traceId');
        assertNonEmpty(holder.holderRef, 'holderRef');
        return this.db.transaction(() => {
            const now = options.now ?? Date.now();
            const blockingLease = this.findTraceLeaseByStates(scope, traceId, ['draining']);
            if (blockingLease) {
                throw new Error(`Trace processor lease ${blockingLease.id} is draining`);
            }
            const requestedMode = options.mode ?? 'shared';
            let lease = requestedMode === 'isolated'
                ? null
                : this.findAcquirableTraceLease(scope, traceId, requestedMode);
            if (!lease) {
                const leaseId = (0, uuid_1.uuidv4)();
                const ttl = resolveHolderTtlPolicy(holder);
                this.db.prepare(`
          INSERT INTO trace_processor_leases
            (id, tenant_id, workspace_id, trace_id, mode, state, heartbeat_at, expires_at)
          VALUES
            (?, ?, ?, ?, ?, 'pending', ?, ?)
        `).run(leaseId, scope.tenantId, scope.workspaceId, traceId, requestedMode, now, now + ttl.idleTtlMs);
                lease = this.mustGetLease(scope, leaseId);
            }
            if (!ACQUIRABLE_STATES.has(lease.state)) {
                throw new Error(`Trace processor lease ${lease.id} is not acquirable (${lease.state})`);
            }
            this.upsertHolder(lease.id, holder, now);
            const ttl = resolveHolderTtlPolicy(holder);
            const expiresAt = Math.max(lease.expires_at ?? 0, now + ttl.idleTtlMs);
            this.db.prepare(`
        UPDATE trace_processor_leases
        SET heartbeat_at = ?, expires_at = ?
        WHERE id = ?
      `).run(now, expiresAt, lease.id);
            this.refreshLeaseActivityState(scope, lease.id);
            return this.getLeaseById(scope, lease.id);
        })();
    }
    acquireHolderForLease(scope, leaseId, holder, options = {}) {
        assertNonEmpty(scope.tenantId, 'tenantId');
        assertNonEmpty(scope.workspaceId, 'workspaceId');
        assertNonEmpty(leaseId, 'leaseId');
        assertNonEmpty(holder.holderRef, 'holderRef');
        return this.db.transaction(() => {
            const now = options.now ?? Date.now();
            const lease = this.mustGetLease(scope, leaseId);
            const state = lease.state;
            if (!ACQUIRABLE_STATES.has(state)) {
                throw new Error(`Trace processor lease ${lease.id} is not acquirable (${state})`);
            }
            this.upsertHolder(lease.id, holder, now);
            const ttl = resolveHolderTtlPolicy(holder);
            const expiresAt = Math.max(lease.expires_at ?? 0, now + ttl.idleTtlMs);
            this.db.prepare(`
        UPDATE trace_processor_leases
        SET heartbeat_at = ?, expires_at = ?
        WHERE id = ?
      `).run(now, expiresAt, lease.id);
            this.refreshLeaseActivityState(scope, lease.id);
            return this.getLeaseById(scope, lease.id);
        })();
    }
    heartbeatHolder(scope, leaseId, holder, now = Date.now()) {
        assertNonEmpty(leaseId, 'leaseId');
        const lease = this.mustGetLease(scope, leaseId);
        const row = this.db.prepare(`
      SELECT id FROM trace_processor_holders
      WHERE lease_id = ? AND holder_type = ? AND holder_ref = ?
      LIMIT 1
    `).get(lease.id, holder.holderType, holder.holderRef);
        if (!row) {
            throw new Error(`Trace processor holder not found: ${holder.holderType}/${holder.holderRef}`);
        }
        const ttl = resolveHolderTtlPolicy(holder);
        this.db.prepare(`
      UPDATE trace_processor_holders
      SET heartbeat_at = ?, expires_at = ?, window_id = COALESCE(?, window_id), metadata_json = ?
      WHERE id = ?
    `).run(now, now + ttl.heartbeatTtlMs, holder.windowId ?? null, metadataForHolder(holder), row.id);
        this.db.prepare(`
      UPDATE trace_processor_leases
      SET heartbeat_at = ?, expires_at = MAX(COALESCE(expires_at, 0), ?)
      WHERE id = ?
    `).run(now, now + ttl.idleTtlMs, lease.id);
        this.refreshLeaseActivityState(scope, lease.id);
        return this.getLeaseById(scope, lease.id);
    }
    releaseHolder(scope, leaseId, holderType, holderRef) {
        assertNonEmpty(leaseId, 'leaseId');
        assertNonEmpty(holderRef, 'holderRef');
        const lease = this.mustGetLease(scope, leaseId);
        this.db.prepare(`
      DELETE FROM trace_processor_holders
      WHERE lease_id = ? AND holder_type = ? AND holder_ref = ?
    `).run(lease.id, holderType, holderRef);
        this.refreshLeaseActivityState(scope, lease.id);
        return this.getLeaseById(scope, lease.id);
    }
    markStarting(scope, leaseId) {
        return this.transition(scope, leaseId, 'starting');
    }
    markReady(scope, leaseId) {
        const lease = this.transition(scope, leaseId, 'ready');
        this.refreshLeaseActivityState(scope, lease.id);
        return this.getLeaseById(scope, lease.id);
    }
    markCrashed(scope, leaseId) {
        return this.transition(scope, leaseId, 'crashed');
    }
    markRestarting(scope, leaseId) {
        return this.transition(scope, leaseId, 'restarting');
    }
    markFailed(scope, leaseId) {
        return this.transition(scope, leaseId, 'failed');
    }
    beginDraining(scope, leaseId) {
        const lease = this.transition(scope, leaseId, 'draining');
        this.refreshLeaseActivityState(scope, lease.id);
        return this.getLeaseById(scope, lease.id);
    }
    recordRss(scope, leaseId, rssBytes) {
        const lease = this.mustGetLease(scope, leaseId);
        this.db.prepare(`
      UPDATE trace_processor_leases
      SET rss_bytes = ?, heartbeat_at = ?
      WHERE id = ?
    `).run(rssBytes, Date.now(), lease.id);
        return this.getLeaseById(scope, lease.id);
    }
    getLeaseById(scope, leaseId) {
        const row = this.db.prepare(`
      SELECT *
      FROM trace_processor_leases
      WHERE tenant_id = ? AND workspace_id = ? AND id = ?
      LIMIT 1
    `).get(scope.tenantId, scope.workspaceId, leaseId);
        return row ? this.mapLease(row) : null;
    }
    listLeases(scope, criteria = {}) {
        const params = [scope.tenantId, scope.workspaceId];
        const clauses = ['tenant_id = ?', 'workspace_id = ?'];
        if (criteria.traceId) {
            clauses.push('trace_id = ?');
            params.push(criteria.traceId);
        }
        if (criteria.states && criteria.states.length > 0) {
            clauses.push(`state IN (${criteria.states.map(() => '?').join(', ')})`);
            params.push(...criteria.states);
        }
        const rows = this.db.prepare(`
      SELECT *
      FROM trace_processor_leases
      WHERE ${clauses.join(' AND ')}
      ORDER BY heartbeat_at DESC, id ASC
    `).all(...params);
        return rows.map(row => this.mapLease(row));
    }
    hasActiveHolders(scope, traceId) {
        return this.listLeases(scope, { traceId })
            .some(lease => !TERMINAL_STATES.has(lease.state) && lease.holderCount > 0);
    }
    sweepExpired(now = Date.now()) {
        const holderResult = this.db.prepare(`
      DELETE FROM trace_processor_holders
      WHERE expires_at IS NOT NULL AND expires_at <= ?
    `).run(now);
        this.db.prepare(`
      UPDATE trace_processor_leases
      SET state = 'idle'
      WHERE state = 'active'
        AND id IN (
          SELECT l.id
          FROM trace_processor_leases l
          LEFT JOIN trace_processor_holders h ON h.lease_id = l.id
          GROUP BY l.id
          HAVING COUNT(h.id) = 0
        )
    `).run();
        const releasableRows = this.db.prepare(`
      SELECT l.*
      FROM trace_processor_leases l
      LEFT JOIN trace_processor_holders h ON h.lease_id = l.id
      WHERE l.state NOT IN ('released', 'failed')
        AND l.expires_at IS NOT NULL
        AND l.expires_at <= ?
      GROUP BY l.id
      HAVING COUNT(h.id) = 0
    `).all(now);
        for (const lease of releasableRows) {
            this.db.prepare(`
        UPDATE trace_processor_leases
        SET state = 'released'
        WHERE id = ?
      `).run(lease.id);
        }
        return {
            holdersRemoved: holderResult.changes,
            leasesReleased: releasableRows.length,
        };
    }
    transition(scope, leaseId, nextState) {
        const lease = this.mustGetLease(scope, leaseId);
        const currentState = lease.state;
        assertValidTransition(currentState, nextState);
        this.db.prepare(`
      UPDATE trace_processor_leases
      SET state = ?, heartbeat_at = ?
      WHERE id = ?
    `).run(nextState, Date.now(), lease.id);
        return this.getLeaseById(scope, lease.id);
    }
    upsertHolder(leaseId, holder, now) {
        const ttl = resolveHolderTtlPolicy(holder);
        const metadataJson = metadataForHolder(holder);
        const existing = this.db.prepare(`
      SELECT id FROM trace_processor_holders
      WHERE lease_id = ? AND holder_type = ? AND holder_ref = ?
      LIMIT 1
    `).get(leaseId, holder.holderType, holder.holderRef);
        if (existing) {
            this.db.prepare(`
        UPDATE trace_processor_holders
        SET window_id = ?, heartbeat_at = ?, expires_at = ?, metadata_json = ?
        WHERE id = ?
      `).run(holder.windowId ?? null, now, now + ttl.heartbeatTtlMs, metadataJson, existing.id);
            return;
        }
        this.db.prepare(`
      INSERT INTO trace_processor_holders
        (id, lease_id, holder_type, holder_ref, window_id, heartbeat_at, expires_at, created_at, metadata_json)
      VALUES
        (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run((0, uuid_1.uuidv4)(), leaseId, holder.holderType, holder.holderRef, holder.windowId ?? null, now, now + ttl.heartbeatTtlMs, now, metadataJson);
    }
    refreshLeaseActivityState(scope, leaseId) {
        const lease = this.mustGetLease(scope, leaseId);
        const state = lease.state;
        const holderCount = this.countHolders(lease.id);
        if (state === 'draining' && holderCount === 0) {
            this.db.prepare(`UPDATE trace_processor_leases SET state = 'released' WHERE id = ?`).run(lease.id);
            return;
        }
        if (state === 'ready' || state === 'idle' || state === 'active') {
            const nextState = holderCount > 0 ? 'active' : 'idle';
            if (nextState !== state) {
                this.db.prepare(`UPDATE trace_processor_leases SET state = ? WHERE id = ?`).run(nextState, lease.id);
            }
        }
    }
    findAcquirableTraceLease(scope, traceId, mode) {
        const rows = this.db.prepare(`
      SELECT *
      FROM trace_processor_leases
      WHERE tenant_id = ? AND workspace_id = ? AND trace_id = ?
        AND mode = ?
      ORDER BY heartbeat_at DESC, id ASC
    `).all(scope.tenantId, scope.workspaceId, traceId, mode);
        return rows.find(row => ACQUIRABLE_STATES.has(row.state)) ?? null;
    }
    findTraceLeaseByStates(scope, traceId, states) {
        const rows = this.db.prepare(`
      SELECT *
      FROM trace_processor_leases
      WHERE tenant_id = ? AND workspace_id = ? AND trace_id = ?
        AND state IN (${states.map(() => '?').join(', ')})
      ORDER BY heartbeat_at DESC, id ASC
      LIMIT 1
    `).all(scope.tenantId, scope.workspaceId, traceId, ...states);
        return rows[0] ?? null;
    }
    mustGetLease(scope, leaseId) {
        const row = this.db.prepare(`
      SELECT *
      FROM trace_processor_leases
      WHERE tenant_id = ? AND workspace_id = ? AND id = ?
      LIMIT 1
    `).get(scope.tenantId, scope.workspaceId, leaseId);
        if (!row) {
            throw new Error(`Trace processor lease not found: ${leaseId}`);
        }
        return row;
    }
    countHolders(leaseId) {
        const row = this.db.prepare(`
      SELECT COUNT(*) AS count
      FROM trace_processor_holders
      WHERE lease_id = ?
    `).get(leaseId);
        return row.count;
    }
    holdersForLease(leaseId) {
        const rows = this.db.prepare(`
      SELECT *
      FROM trace_processor_holders
      WHERE lease_id = ?
      ORDER BY created_at ASC, id ASC
    `).all(leaseId);
        return rows.map(row => ({
            id: row.id,
            leaseId: row.lease_id,
            holderType: row.holder_type,
            holderRef: row.holder_ref,
            windowId: row.window_id,
            heartbeatAt: row.heartbeat_at,
            expiresAt: row.expires_at,
            createdAt: row.created_at,
            metadata: parseMetadata(row.metadata_json),
        }));
    }
    mapLease(row) {
        const holders = this.holdersForLease(row.id);
        return {
            id: row.id,
            tenantId: row.tenant_id,
            workspaceId: row.workspace_id,
            traceId: row.trace_id,
            mode: row.mode,
            state: row.state,
            rssBytes: row.rss_bytes,
            heartbeatAt: row.heartbeat_at,
            expiresAt: row.expires_at,
            holderCount: holders.length,
            holders,
        };
    }
}
exports.TraceProcessorLeaseStore = TraceProcessorLeaseStore;
let singleton = null;
let singletonDbPath = null;
function getTraceProcessorLeaseStore() {
    const dbPath = (0, enterpriseDb_1.resolveEnterpriseDbPath)();
    if (!singleton || singletonDbPath !== dbPath) {
        try {
            singleton?.close();
        }
        catch {
            // Ignore stale singleton cleanup errors; a new DB handle is authoritative.
        }
        singleton = new TraceProcessorLeaseStore((0, enterpriseDb_1.openEnterpriseDb)(dbPath));
        singletonDbPath = dbPath;
    }
    return singleton;
}
function setTraceProcessorLeaseStoreForTests(store) {
    singleton = store;
    singletonDbPath = store ? (0, enterpriseDb_1.resolveEnterpriseDbPath)() : null;
}
//# sourceMappingURL=traceProcessorLeaseStore.js.map