import Browser from 'webextension-polyfill'; import type { eventWithTime } from '@rrweb/types'; import Channel from '~/utils/channel'; import { LocalData, LocalDataKey, RecorderStatus, Settings, SyncData, SyncDataKey, } from '~/types'; import { pauseRecording, resumeRecording } from '~/utils/recording'; const channel = new Channel(); void (async () => { // assign default value to settings of this extension const result = ((await Browser.storage.sync.get(SyncDataKey.settings)) as SyncData) || undefined; const defaultSettings: Settings = {}; let settings = defaultSettings; if (result && result.settings) { setDefaultSettings(result.settings, defaultSettings); settings = result.settings; } await Browser.storage.sync.set({ settings, } as SyncData); // When tab is changed during the recording process, pause recording in the old tab and start a new one in the new tab. Browser.tabs.onActivated.addListener((activeInfo) => { Browser.storage.local .get(LocalDataKey.recorderStatus) .then(async (data) => { const localData = data as LocalData; if (!localData || !localData[LocalDataKey.recorderStatus]) return; let statusData = localData[LocalDataKey.recorderStatus]; let { status } = statusData; let bufferedEvents: eventWithTime[] | undefined; if (status === RecorderStatus.RECORDING) { const result = await pauseRecording( channel, RecorderStatus.PausedSwitch, statusData, ).catch(async () => { /** * This error happen when the old tab is closed. * In this case, the recording process would be stopped through Browser.tabs.onRemoved API. * So we just read the new status here. */ const localData = (await Browser.storage.local.get( LocalDataKey.recorderStatus, )) as LocalData; return { status: localData[LocalDataKey.recorderStatus], bufferedEvents, }; }); if (!result) return; statusData = result.status; status = statusData.status; bufferedEvents = result.bufferedEvents; } if (status === RecorderStatus.PausedSwitch) await resumeRecording( channel, activeInfo.tabId, statusData, bufferedEvents, ); }) .catch(() => { // the extension can't access to the tab }); }); // If the recording can't start on an invalid tab, resume it when the tab content is updated. Browser.tabs.onUpdated.addListener(function (tabId, info) { if (info.status !== 'complete') return; Browser.storage.local .get(LocalDataKey.recorderStatus) .then(async (data) => { const localData = data as LocalData; if (!localData || !localData[LocalDataKey.recorderStatus]) return; const { status, activeTabId } = localData[LocalDataKey.recorderStatus]; if (status !== RecorderStatus.PausedSwitch || activeTabId === tabId) return; await resumeRecording( channel, tabId, localData[LocalDataKey.recorderStatus], ); }) .catch(() => { // the extension can't access to the tab }); }); /** * When the current tab is closed, the recording events will be lost because this event is fired after it is closed. * This event listener is just used to make sure the recording status is updated. */ Browser.tabs.onRemoved.addListener((tabId) => { Browser.storage.local .get(LocalDataKey.recorderStatus) .then(async (data) => { const localData = data as LocalData; if (!localData || !localData[LocalDataKey.recorderStatus]) return; const { status, activeTabId, startTimestamp } = localData[LocalDataKey.recorderStatus]; if (activeTabId !== tabId || status !== RecorderStatus.RECORDING) return; // Update the recording status to make it resumable after users switch to other tabs. const statusData: LocalData[LocalDataKey.recorderStatus] = { status: RecorderStatus.PausedSwitch, activeTabId, startTimestamp, pausedTimestamp: Date.now(), }; await Browser.storage.local.set({ [LocalDataKey.recorderStatus]: statusData, }); }) .catch((err) => { console.error(err); }); }); })(); /** * Update existed settings with new settings. * Set new setting values if these properties don't exist in older versions. */ function setDefaultSettings( existedSettings: Record, newSettings: Record, ) { for (const i in newSettings) { // settings[i] contains key-value settings if ( typeof newSettings[i] === 'object' && !(newSettings[i] instanceof Array) && Object.keys(newSettings[i] as Record).length > 0 ) { if (existedSettings[i]) { setDefaultSettings( existedSettings[i] as Record, newSettings[i] as Record, ); } else { // settings[i] contains several setting items but these have not been set before existedSettings[i] = newSettings[i]; } } else if (existedSettings[i] === undefined) { // settings[i] is a single setting item and it has not been set before existedSettings[i] = newSettings[i]; } } }