packages-ext/recoil-devtools/src/pages/Page/PageScript.js (165 lines of code) (raw):
/**
* (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
*
* Recoil DevTools browser extension.
*
* @emails oncall+recoil
* @flow strict-local
* @format
*/
'use strict';
import type {
DevToolsConnnectProps,
DevToolsOptions,
RecoilDevToolsActionsType,
RecoilSnapshot,
} from '../../types/DevtoolsTypes';
const {
ExtensionSource,
ExtensionSourceContentScript,
RecoilDevToolsActions,
} = require('../../constants/Constants');
const EvictableList = require('../../utils/EvictableList');
const {debug} = require('../../utils/Logger');
const {serialize} = require('../../utils/Serialization');
const DefaultCustomSerialize = (item: mixed, _: string): mixed => item;
async function normalizeSnapshot(
snapshot: ?RecoilSnapshot,
onlyDirty: boolean,
props: DevToolsConnnectProps,
): Promise<{modifiedValues?: {[string]: mixed}}> {
if (snapshot == null) {
return {modifiedValues: undefined};
}
const modifiedValues = {};
const release = snapshot.retain();
try {
const dirtyNodes = snapshot.getNodes_UNSTABLE({isModified: onlyDirty});
const customSerialize = props.serializeFn ?? DefaultCustomSerialize;
const subscribers = new Set();
// We wrap this loop into a promise to defer the execution
// of the second (subscribers) loop, so selector
// can settle before we check their values
await new Promise(resolve => {
for (const node of dirtyNodes) {
const info = snapshot.getInfo_UNSTABLE(node);
// We only accumulate subscribers if we are looking at only dirty nodes
if (onlyDirty) {
subscribers.add(...Array.from(info.subscribers.nodes));
}
modifiedValues[node.key] = {
content: serialize(
customSerialize(info.loadable?.contents, node.key),
props.maxDepth,
props.maxItems,
),
nodeType: info.type,
deps: Array.from(info.deps).map(n => n.key),
};
}
resolve();
});
for (const node of subscribers) {
if (node != null) {
const info = snapshot.getInfo_UNSTABLE(node);
modifiedValues[node.key] = {
content: serialize(
customSerialize(info.loadable?.contents, node.key),
props.maxDepth,
props.maxItems,
),
nodeType: info.type,
isSubscriber: true,
deps: Array.from(info.deps).map(n => n.key),
};
}
}
} finally {
release();
}
return {
modifiedValues,
};
}
type MessageEventFromBackground = {
data?: ?{
source: string,
action: RecoilDevToolsActionsType,
snapshotId?: number,
},
};
const __RECOIL_DEVTOOLS_EXTENSION__ = {
connect: (props: DevToolsConnnectProps) => {
debug('CONNECT_PAGE', props);
initConnection(props);
const {devMode, goToSnapshot, initialSnapshot} = props;
const previousSnapshots = new EvictableList<[RecoilSnapshot, () => void]>(
props.persistenceLimit,
([_, release]) => release(),
);
if (devMode && initialSnapshot != null) {
previousSnapshots.add([initialSnapshot, initialSnapshot.retain()]);
}
let loadedSnapshot, releaseLoadedSnapshot;
function setLoadedSnapshot(s) {
releaseLoadedSnapshot?.();
[loadedSnapshot, releaseLoadedSnapshot] = [s, s?.retain()];
}
setLoadedSnapshot(initialSnapshot);
const backgroundMessageListener = (message: MessageEventFromBackground) => {
if (message.data?.source === ExtensionSourceContentScript) {
if (message.data?.action === RecoilDevToolsActions.GO_TO_SNAPSHOT) {
const [snapshot] =
previousSnapshots.get(message.data?.snapshotId) ?? [];
setLoadedSnapshot(snapshot);
if (snapshot != null) {
goToSnapshot(snapshot);
}
}
}
};
window.addEventListener('message', backgroundMessageListener);
// This function is called when a trasaction is detected
return {
disconnect() {
window.removeEventListener('message', backgroundMessageListener);
},
async track(
txID: number,
snapshot: RecoilSnapshot,
_previousSnapshot: RecoilSnapshot,
) {
// if we just went to a snapshot, we don't need to record a new transaction
if (
loadedSnapshot != null &&
loadedSnapshot.getID() === snapshot.getID()
) {
return;
}
// reset the just loaded snapshot
setLoadedSnapshot(null);
// On devMode we accumulate the list of rpevious snapshots
// to be able to time travel
if (devMode) {
previousSnapshots.add([snapshot, snapshot.retain()]);
}
window.postMessage(
{
action: RecoilDevToolsActions.UPDATE,
source: ExtensionSource,
txID,
message: await normalizeSnapshot(snapshot, true, props),
},
'*',
);
},
};
},
};
async function initConnection(props: DevToolsConnnectProps) {
const initialValues = await normalizeSnapshot(
props.initialSnapshot,
false,
props,
);
window.postMessage(
{
action: RecoilDevToolsActions.INIT,
source: ExtensionSource,
props: {
devMode: props.devMode,
name: props?.name ?? document.title,
persistenceLimit: props.persistenceLimit,
initialValues: initialValues.modifiedValues,
},
},
'*',
);
}
window.__RECOIL_DEVTOOLS_EXTENSION__ = __RECOIL_DEVTOOLS_EXTENSION__;
if (__DEV__) {
window.__RECOIL_DEVTOOLS_EXTENSION_DEV__ = __RECOIL_DEVTOOLS_EXTENSION__;
}
debug('EXTENSION_EXPOSED');