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;
}
}
}