// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. var { Tracker, Store, SyncEngine } = ChromeUtils.importESModule( 'resource://services-sync/engines.sys.mjs' ); var { CryptoWrapper } = ChromeUtils.importESModule('resource://services-sync/record.sys.mjs'); var { Utils } = ChromeUtils.importESModule('resource://services-sync/util.sys.mjs'); var { SCORE_INCREMENT_XLARGE } = ChromeUtils.importESModule( 'resource://services-sync/constants.sys.mjs' ); // Define ZenWorkspaceRecord function ZenWorkspaceRecord(collection, id) { CryptoWrapper.call(this, collection, id); } ZenWorkspaceRecord.prototype = Object.create(CryptoWrapper.prototype); ZenWorkspaceRecord.prototype.constructor = ZenWorkspaceRecord; ZenWorkspaceRecord.prototype._logName = 'Sync.Record.ZenWorkspace'; Utils.deferGetSet(ZenWorkspaceRecord, 'cleartext', [ 'name', 'icon', 'default', 'containerTabId', 'position', 'theme_type', 'theme_colors', 'theme_opacity', 'theme_rotation', 'theme_texture', ]); // Define ZenWorkspacesStore function ZenWorkspacesStore(name, engine) { Store.call(this, name, engine); } ZenWorkspacesStore.prototype = Object.create(Store.prototype); ZenWorkspacesStore.prototype.constructor = ZenWorkspacesStore; /** * Initializes the store by loading the current changeset. */ ZenWorkspacesStore.prototype.initialize = async function () { await Store.prototype.initialize.call(this); // Additional initialization if needed }; /** * Retrieves all workspace IDs from the storage. * @returns {Object} An object mapping workspace UUIDs to true. */ ZenWorkspacesStore.prototype.getAllIDs = async function () { try { const workspaces = await ZenWorkspacesStorage.getWorkspaces(); const ids = {}; for (const workspace of workspaces) { ids[workspace.uuid] = true; } return ids; } catch (error) { this._log.error('Error fetching all workspace IDs', error); throw error; } }; /** * Handles changing the ID of a workspace. * @param {String} oldID - The old UUID. * @param {String} newID - The new UUID. */ ZenWorkspacesStore.prototype.changeItemID = async function (oldID, newID) { try { const workspaces = await ZenWorkspacesStorage.getWorkspaces(); const workspace = workspaces.find((ws) => ws.uuid === oldID); if (workspace) { workspace.uuid = newID; await ZenWorkspacesStorage.saveWorkspace(workspace, false); // Mark the new ID as changed for sync await ZenWorkspacesStorage.markChanged(newID); } } catch (error) { this._log.error(`Error changing workspace ID from ${oldID} to ${newID}`, error); throw error; } }; /** * Checks if a workspace exists. * @param {String} id - The UUID of the workspace. * @returns {Boolean} True if the workspace exists, false otherwise. */ ZenWorkspacesStore.prototype.itemExists = async function (id) { try { const workspaces = await ZenWorkspacesStorage.getWorkspaces(); return workspaces.some((ws) => ws.uuid === id); } catch (error) { this._log.error(`Error checking if workspace exists with ID ${id}`, error); throw error; } }; /** * Creates a record for a workspace. * @param {String} id - The UUID of the workspace. * @param {String} collection - The collection name. * @returns {ZenWorkspaceRecord} The workspace record. */ ZenWorkspacesStore.prototype.createRecord = async function (id, collection) { try { const workspaces = await ZenWorkspacesStorage.getWorkspaces(); const workspace = workspaces.find((ws) => ws.uuid === id); const record = new ZenWorkspaceRecord(collection, id); if (workspace) { record.name = workspace.name; record.icon = workspace.icon; record.default = workspace.default; record.containerTabId = workspace.containerTabId; record.position = workspace.position; if (workspace.theme) { record.theme_type = workspace.theme.type; record.theme_colors = JSON.stringify(workspace.theme.gradientColors); record.theme_opacity = workspace.theme.opacity; record.theme_rotation = workspace.theme.rotation; record.theme_texture = workspace.theme.texture; } record.deleted = false; } else { record.deleted = true; } return record; } catch (error) { this._log.error(`Error creating record for workspace ID ${id}`, error); throw error; } }; /** * Creates a new workspace. * @param {ZenWorkspaceRecord} record - The workspace record to create. */ ZenWorkspacesStore.prototype.create = async function (record) { try { this._validateRecord(record); const workspace = { uuid: record.id, name: record.name, icon: record.icon, default: record.default, containerTabId: record.containerTabId, position: record.position, theme: record.theme_type ? { type: record.theme_type, gradientColors: JSON.parse(record.theme_colors), opacity: record.theme_opacity, rotation: record.theme_rotation, texture: record.theme_texture, } : null, }; await ZenWorkspacesStorage.saveWorkspace(workspace, false); } catch (error) { this._log.error(`Error creating workspace with ID ${record.id}`, error); throw error; } }; /** * Updates an existing workspace. * @param {ZenWorkspaceRecord} record - The workspace record to update. */ ZenWorkspacesStore.prototype.update = async function (record) { try { this._validateRecord(record); await this.create(record); // Reuse create for update } catch (error) { this._log.error(`Error updating workspace with ID ${record.id}`, error); throw error; } }; /** * Removes a workspace. * @param {ZenWorkspaceRecord} record - The workspace record to remove. */ ZenWorkspacesStore.prototype.remove = async function (record) { try { await ZenWorkspacesStorage.removeWorkspace(record.id, false); } catch (error) { this._log.error(`Error removing workspace with ID ${record.id}`, error); throw error; } }; /** * Wipes all workspaces from the storage. */ ZenWorkspacesStore.prototype.wipe = async function () { try { await ZenWorkspacesStorage.wipeAllWorkspaces(); } catch (error) { this._log.error('Error wiping all workspaces', error); throw error; } }; /** * Validates a workspace record. * @param {ZenWorkspaceRecord} record - The workspace record to validate. */ ZenWorkspacesStore.prototype._validateRecord = function (record) { if (!record.id || typeof record.id !== 'string') { throw new Error('Invalid workspace ID'); } if (!record.name || typeof record.name !== 'string') { throw new Error(`Invalid workspace name for ID ${record.id}`); } if (typeof record.default !== 'boolean') { record.default = false; } if (record.icon != null && typeof record.icon !== 'string') { throw new Error(`Invalid icon for workspace ID ${record.id}`); } if (record.containerTabId != null && typeof record.containerTabId !== 'number') { throw new Error(`Invalid containerTabId for workspace ID ${record.id}`); } if (record.position != null && typeof record.position !== 'number') { throw new Error(`Invalid position for workspace ID ${record.id}`); } // Validate theme properties if they exist if (record.theme_type) { if (typeof record.theme_type !== 'string') { throw new Error(`Invalid theme_type for workspace ID ${record.id}`); } if (!record.theme_colors || typeof record.theme_colors !== 'string') { throw new Error(`Invalid theme_colors for workspace ID ${record.id}`); } try { JSON.parse(record.theme_colors); } catch (e) { throw new Error( `Invalid theme_colors JSON for workspace ID ${record.id}. Error: ${e.message}` ); } if (record.theme_opacity != null && typeof record.theme_opacity !== 'number') { throw new Error(`Invalid theme_opacity for workspace ID ${record.id}`); } if (record.theme_rotation != null && typeof record.theme_rotation !== 'number') { throw new Error(`Invalid theme_rotation for workspace ID ${record.id}`); } if (record.theme_texture != null && typeof record.theme_texture !== 'number') { throw new Error(`Invalid theme_texture for workspace ID ${record.id}`); } } }; /** * Retrieves changed workspace IDs since the last sync. * @returns {Object} An object mapping workspace UUIDs to their change timestamps. */ ZenWorkspacesStore.prototype.getChangedIDs = async function () { try { return await ZenWorkspacesStorage.getChangedIDs(); } catch (error) { this._log.error('Error retrieving changed IDs from storage', error); throw error; } }; /** * Clears all recorded changes after a successful sync. */ ZenWorkspacesStore.prototype.clearChangedIDs = async function () { try { await ZenWorkspacesStorage.clearChangedIDs(); } catch (error) { this._log.error('Error clearing changed IDs in storage', error); throw error; } }; /** * Marks a workspace as changed. * @param {String} uuid - The UUID of the workspace that changed. */ ZenWorkspacesStore.prototype.markChanged = async function (uuid) { try { await ZenWorkspacesStorage.markChanged(uuid); } catch (error) { this._log.error(`Error marking workspace ${uuid} as changed`, error); throw error; } }; /** * Finalizes the store by ensuring all pending operations are completed. */ ZenWorkspacesStore.prototype.finalize = async function () { await Store.prototype.finalize.call(this); }; // Define ZenWorkspacesTracker function ZenWorkspacesTracker(name, engine) { Tracker.call(this, name, engine); this._ignoreAll = false; // Observe profile-before-change to stop the tracker gracefully Services.obs.addObserver(this.asyncObserver, 'profile-before-change'); } ZenWorkspacesTracker.prototype = Object.create(Tracker.prototype); ZenWorkspacesTracker.prototype.constructor = ZenWorkspacesTracker; /** * Retrieves changed workspace IDs by delegating to the store. * @returns {Object} An object mapping workspace UUIDs to their change timestamps. */ ZenWorkspacesTracker.prototype.getChangedIDs = async function () { try { return await this.engine._store.getChangedIDs(); } catch (error) { this._log.error('Error retrieving changed IDs from store', error); throw error; } }; /** * Clears all recorded changes after a successful sync. */ ZenWorkspacesTracker.prototype.clearChangedIDs = async function () { try { await this.engine._store.clearChangedIDs(); } catch (error) { this._log.error('Error clearing changed IDs in store', error); throw error; } }; /** * Called when the tracker starts. Registers observers to listen for workspace changes. */ ZenWorkspacesTracker.prototype.onStart = function () { if (this._started) { return; } this._log.trace('Starting tracker'); // Register observers for workspace changes Services.obs.addObserver(this.asyncObserver, 'zen-workspace-added'); Services.obs.addObserver(this.asyncObserver, 'zen-workspace-removed'); Services.obs.addObserver(this.asyncObserver, 'zen-workspace-updated'); this._started = true; }; /** * Called when the tracker stops. Unregisters observers. */ ZenWorkspacesTracker.prototype.onStop = function () { if (!this._started) { return; } this._log.trace('Stopping tracker'); // Unregister observers for workspace changes Services.obs.removeObserver(this.asyncObserver, 'zen-workspace-added'); Services.obs.removeObserver(this.asyncObserver, 'zen-workspace-removed'); Services.obs.removeObserver(this.asyncObserver, 'zen-workspace-updated'); this._started = false; }; /** * Handles observed events and marks workspaces as changed accordingly. * @param {nsISupports} subject - The subject of the notification. * @param {String} topic - The topic of the notification. * @param {String} data - Additional data (JSON stringified array of UUIDs). */ ZenWorkspacesTracker.prototype.observe = async function (subject, topic, data) { if (this.ignoreAll) { return; } try { switch (topic) { case 'profile-before-change': await this.stop(); break; case 'zen-workspace-removed': case 'zen-workspace-updated': case 'zen-workspace-added': { let workspaceIDs; if (data) { try { workspaceIDs = JSON.parse(data); if (!Array.isArray(workspaceIDs)) { throw new Error('Parsed data is not an array'); } } catch (parseError) { this._log.error(`Failed to parse workspace UUIDs from data: ${data}`, parseError); return; } } else { this._log.error(`No data received for event ${topic}`); return; } this._log.trace(`Observed ${topic} for UUIDs: ${workspaceIDs.join(', ')}`); // Process each UUID for (const workspaceID of workspaceIDs) { if (typeof workspaceID === 'string') { // Inform the store about the change await this.engine._store.markChanged(workspaceID); } else { this._log.warn(`Invalid workspace ID encountered: ${workspaceID}`); } } // Bump the score once after processing all changes if (workspaceIDs.length > 0) { this.score += SCORE_INCREMENT_XLARGE; } break; } } } catch (error) { this._log.error(`Error handling ${topic} in observe method`, error); } }; /** * Finalizes the tracker by ensuring all pending operations are completed. */ ZenWorkspacesTracker.prototype.finalize = async function () { await Tracker.prototype.finalize.call(this); }; // Define ZenWorkspacesEngine function ZenWorkspacesEngine(service) { SyncEngine.call(this, 'Workspaces', service); } ZenWorkspacesEngine.prototype = Object.create(SyncEngine.prototype); ZenWorkspacesEngine.prototype.constructor = ZenWorkspacesEngine; ZenWorkspacesEngine.prototype._storeObj = ZenWorkspacesStore; ZenWorkspacesEngine.prototype._trackerObj = ZenWorkspacesTracker; ZenWorkspacesEngine.prototype._recordObj = ZenWorkspaceRecord; ZenWorkspacesEngine.prototype.version = 2; ZenWorkspacesEngine.prototype.syncPriority = 10; ZenWorkspacesEngine.prototype.allowSkippedRecord = false; Object.setPrototypeOf(ZenWorkspacesEngine.prototype, SyncEngine.prototype);