utilities_version_manager.js

const AUTO_VERSION_INACTIVITY_MS = 20 * 60 * 1000; // 20 minutes
const AUTO_VERSION_PUSH_MILESTONE = 50;

/**
 * Manages automatic and manual version snapshots for a server project.
 * Owned by Workspace — instantiated when a server project loads, destroyed on unload.
 */
export class VersionManager {
    /**
     * @param {string} projectId - ID of the server project to version.
     * @param {object} server - Server instance with createVersion()
     * @param {object} project - Active Project instance
     * @param {object} options - Configuration options.
     * @param {number} [options.autoVersionLimit=50] - Max auto-versions to keep (from user preferences)
     * @param {function} [options.onVersionSaved] - Called with the new version entry after a save
     */
    constructor(projectId, server, project, { autoVersionLimit = 50, onVersionSaved } = {}) {
        this.projectId = projectId;
        this.server = server;
        this.project = project;
        this.autoVersionLimit = autoVersionLimit;
        this._onVersionSaved = onVersionSaved ?? (() => {});

        this._pendingChanges = false;
        this._pushCount = 0;
        this._inactivityTimer = null;
        this._saving = false;
    }

    /**
     * Called by workspace._updateUndoRedoButtons() after every history.push().
     * @param {number} stackSize - current undo stack depth (unused, for future use)
     */
    onHistoryPush(stackSize) {
        this._pendingChanges = true;

        clearTimeout(this._inactivityTimer);
        this._inactivityTimer = setTimeout(() => {
            if (this._pendingChanges) this._triggerAutoVersion();
        }, AUTO_VERSION_INACTIVITY_MS);

        this._pushCount++;
        if (this._pushCount >= AUTO_VERSION_PUSH_MILESTONE) {
            this._pushCount = 0;
            this._triggerAutoVersion();
        }
    }

    /** Call after a successful server save to reset the pending-changes flag. */
    onProjectSaved() {
        this._pendingChanges = false;
        clearTimeout(this._inactivityTimer);
        this._inactivityTimer = null;
    }

    /**
     * Creates a named version snapshot.
     * @param {string|null} label - Version label, or null for unlabelled named version.
     * @returns {Promise<object>} The created version index entry.
     */
    async saveNamedVersion(label) {
        return await this._saveVersion(label || null);
    }

    /** Cleans up timers when the workspace unloads the project. */
    destroy() {
        clearTimeout(this._inactivityTimer);
        this._inactivityTimer = null;
    }

    // ── Private ──────────────────────────────────────────────────────────────

    /** Fires an auto-save if one is not already in progress. */
    _triggerAutoVersion() {
        if (this._saving) return;
        this._saving = true;
        this._saveVersion(null).finally(() => { this._saving = false; });
    }

    /**
     * @param {string|null} label - Version label, or null for an auto-version.
     * @returns {Promise<object>} The created version index entry.
     */
    async _saveVersion(label) {
        try {
            const meta = this.project.metadata();
            const entry = await this.server.createVersion(this.projectId, {
                label,
                project_name: meta.name,
                transcript: this.project.transcript().compileJSON(),
                speakers: meta.speakers,
                effects: this.project.effects ?? { ranges: [] },
            });
            this._pendingChanges = false;
            this._onVersionSaved(entry);
            return entry;
        } catch (e) {
            console.error('Version save failed:', e);
            throw e;
        }
    }
}