|
import { reactive } from 'vue' |
|
import { getStorage, setStorage } from './storage' |
|
import type { Bridge } from './bridge' |
|
import { isBrowser, isMac } from './env' |
|
|
|
|
|
const internalSharedData = { |
|
openInEditorHost: '/', |
|
componentNameStyle: 'class', |
|
theme: 'auto', |
|
displayDensity: 'low', |
|
timeFormat: 'default', |
|
recordVuex: true, |
|
cacheVuexSnapshotsEvery: 50, |
|
cacheVuexSnapshotsLimit: 10, |
|
snapshotLoading: false, |
|
componentEventsEnabled: true, |
|
performanceMonitoringEnabled: true, |
|
editableProps: false, |
|
logDetected: true, |
|
vuexNewBackend: false, |
|
vuexAutoload: false, |
|
vuexGroupGettersByModule: true, |
|
showMenuScrollTip: true, |
|
timelineRecording: false, |
|
timelineTimeGrid: true, |
|
timelineScreenshots: false, |
|
menuStepScrolling: isMac, |
|
pluginPermissions: {} as any, |
|
pluginSettings: {} as any, |
|
pageConfig: {} as any, |
|
legacyApps: false, |
|
trackUpdates: true, |
|
flashUpdates: false, |
|
debugInfo: false, |
|
isBrowser, |
|
} |
|
|
|
type TSharedData = typeof internalSharedData |
|
|
|
const persisted = [ |
|
'componentNameStyle', |
|
'theme', |
|
'displayDensity', |
|
'recordVuex', |
|
'editableProps', |
|
'logDetected', |
|
'vuexNewBackend', |
|
'vuexAutoload', |
|
'vuexGroupGettersByModule', |
|
'timeFormat', |
|
'showMenuScrollTip', |
|
'timelineRecording', |
|
'timelineTimeGrid', |
|
'timelineScreenshots', |
|
'menuStepScrolling', |
|
'pluginPermissions', |
|
'pluginSettings', |
|
'performanceMonitoringEnabled', |
|
'componentEventsEnabled', |
|
'trackUpdates', |
|
'flashUpdates', |
|
'debugInfo', |
|
] |
|
|
|
const storageVersion = '6.0.0-alpha.1' |
|
|
|
|
|
|
|
let bridge |
|
|
|
|
|
let persist = false |
|
let data |
|
|
|
let initRetryInterval |
|
let initRetryCount = 0 |
|
|
|
export interface SharedDataParams { |
|
bridge: Bridge |
|
persist: boolean |
|
} |
|
|
|
const initCbs = [] |
|
|
|
export function initSharedData(params: SharedDataParams): Promise<void> { |
|
return new Promise((resolve) => { |
|
|
|
bridge = params.bridge |
|
persist = !!params.persist |
|
|
|
if (persist) { |
|
if (process.env.NODE_ENV !== 'production') { |
|
|
|
console.log('[shared data] Master init in progress...') |
|
} |
|
|
|
persisted.forEach((key) => { |
|
const value = getStorage(`vue-devtools-${storageVersion}:shared-data:${key}`) |
|
if (value !== null) { |
|
internalSharedData[key] = value |
|
} |
|
}) |
|
bridge.on('shared-data:load', () => { |
|
|
|
Object.keys(internalSharedData).forEach((key) => { |
|
sendValue(key, internalSharedData[key]) |
|
}) |
|
bridge.send('shared-data:load-complete') |
|
}) |
|
bridge.on('shared-data:init-complete', () => { |
|
if (process.env.NODE_ENV !== 'production') { |
|
|
|
console.log('[shared data] Master init complete') |
|
} |
|
clearInterval(initRetryInterval) |
|
resolve() |
|
}) |
|
|
|
bridge.send('shared-data:master-init-waiting') |
|
|
|
bridge.on('shared-data:minion-init-waiting', () => { |
|
bridge.send('shared-data:master-init-waiting') |
|
}) |
|
|
|
initRetryCount = 0 |
|
clearInterval(initRetryInterval) |
|
initRetryInterval = setInterval(() => { |
|
if (process.env.NODE_ENV !== 'production') { |
|
|
|
console.log('[shared data] Master init retrying...') |
|
} |
|
bridge.send('shared-data:master-init-waiting') |
|
initRetryCount++ |
|
if (initRetryCount > 30) { |
|
clearInterval(initRetryInterval) |
|
console.error('[shared data] Master init failed') |
|
} |
|
}, 2000) |
|
} |
|
else { |
|
if (process.env.NODE_ENV !== 'production') { |
|
|
|
console.log('[shared data] Minion init in progress...') |
|
} |
|
bridge.on('shared-data:master-init-waiting', () => { |
|
if (process.env.NODE_ENV !== 'production') { |
|
|
|
console.log('[shared data] Minion loading data...') |
|
} |
|
|
|
bridge.send('shared-data:load') |
|
bridge.once('shared-data:load-complete', () => { |
|
if (process.env.NODE_ENV !== 'production') { |
|
|
|
console.log('[shared data] Minion init complete') |
|
} |
|
bridge.send('shared-data:init-complete') |
|
resolve() |
|
}) |
|
}) |
|
bridge.send('shared-data:minion-init-waiting') |
|
} |
|
|
|
data = reactive({ ...internalSharedData }) |
|
|
|
|
|
bridge.on('shared-data:set', ({ key, value }) => { |
|
setValue(key, value) |
|
}) |
|
|
|
initCbs.forEach(cb => cb()) |
|
}) |
|
} |
|
|
|
export function onSharedDataInit(cb) { |
|
initCbs.push(cb) |
|
return () => { |
|
const index = initCbs.indexOf(cb) |
|
if (index !== -1) { |
|
initCbs.splice(index, 1) |
|
} |
|
} |
|
} |
|
|
|
let watchers: Partial<Record<keyof TSharedData, ((value: any, oldValue: any) => unknown)[]>> = {} |
|
|
|
export function destroySharedData() { |
|
bridge.removeAllListeners('shared-data:set') |
|
watchers = {} |
|
} |
|
|
|
function setValue(key: string, value: any) { |
|
|
|
if (persist && persisted.includes(key)) { |
|
setStorage(`vue-devtools-${storageVersion}:shared-data:${key}`, value) |
|
} |
|
const oldValue = data[key] |
|
data[key] = value |
|
const handlers = watchers[key] |
|
if (handlers) { |
|
handlers.forEach(h => h(value, oldValue)) |
|
} |
|
|
|
return true |
|
} |
|
|
|
function sendValue(key: string, value: any) { |
|
bridge && bridge.send('shared-data:set', { |
|
key, |
|
value, |
|
}) |
|
} |
|
|
|
export function watchSharedData< |
|
TKey extends keyof TSharedData, |
|
>(prop: TKey, handler: (value: TSharedData[TKey], oldValue: TSharedData[TKey]) => unknown) { |
|
const list = watchers[prop] || (watchers[prop] = []) |
|
list.push(handler) |
|
return () => { |
|
const index = list.indexOf(handler) |
|
if (index !== -1) { |
|
list.splice(index, 1) |
|
} |
|
} |
|
} |
|
|
|
const proxy: Partial<typeof internalSharedData> = {} |
|
Object.keys(internalSharedData).forEach((key) => { |
|
Object.defineProperty(proxy, key, { |
|
configurable: false, |
|
get: () => data[key], |
|
set: (value) => { |
|
sendValue(key, value) |
|
setValue(key, value) |
|
}, |
|
}) |
|
}) |
|
|
|
export const SharedData = proxy |
|
|