diff --git a/extension/src/app/api/filters.ts b/extension/src/app/api/filters.ts index a631281a..85d01559 100644 --- a/extension/src/app/api/filters.ts +++ b/extension/src/app/api/filters.ts @@ -139,6 +139,7 @@ export interface PartialLiftedState> { readonly stagedActionIds: readonly number[]; readonly currentStateIndex: number; readonly nextActionId: number; + readonly committedState?: S; } export function startingFrom>( diff --git a/extension/src/app/api/index.ts b/extension/src/app/api/index.ts index bc740646..db266e6a 100644 --- a/extension/src/app/api/index.ts +++ b/extension/src/app/api/index.ts @@ -5,7 +5,6 @@ import { getActionsArray } from '@redux-devtools/utils'; import { getLocalFilter, isFiltered, PartialLiftedState } from './filters'; import importState from './importState'; import generateId from './generateInstanceId'; -import { PageScriptToContentScriptMessage } from '../../browser/extension/inject/contentScript'; import { Config } from '../../browser/extension/inject/pageScript'; import { Action } from 'redux'; import { LiftedState, PerformAction } from '@redux-devtools/instrument'; @@ -107,7 +106,100 @@ export function getSerializeParameter( return value; } -function post(message: PageScriptToContentScriptMessage) { +interface InitInstancePageScriptToContentScriptMessage { + readonly type: 'INIT_INSTANCE'; + readonly instanceId: number; + readonly source: typeof source; +} + +interface DisconnectMessage { + readonly type: 'DISCONNECT'; + readonly source: typeof source; +} + +interface InitMessage> { + readonly type: 'INIT'; + readonly payload: string; + readonly instanceId: number; + readonly source: typeof source; + action?: string; + name?: string | undefined; + liftedState?: LiftedState; + libConfig?: unknown; +} + +interface SerializedPartialLiftedState> { + readonly stagedActionIds: readonly number[]; + readonly currentStateIndex: number; + readonly nextActionId: number; +} + +interface SerializedPartialStateMessage> { + readonly type: 'PARTIAL_STATE'; + readonly payload: SerializedPartialLiftedState; + readonly source: typeof source; + readonly instanceId: number; + readonly maxAge: number; + readonly actionsById: string; + readonly computedStates: string; + readonly committedState: boolean; +} + +interface SerializedExportMessage { + readonly type: 'EXPORT'; + readonly payload: string; + readonly committedState: string | undefined; + readonly source: typeof source; + readonly instanceId: number; +} + +interface SerializedActionMessage { + readonly type: 'ACTION'; + readonly payload: string; + readonly source: typeof source; + readonly instanceId: number; + readonly action: string; + readonly maxAge: number; + readonly nextActionId: number; +} + +interface SerializedStateMessage> { + readonly type: 'STATE'; + readonly payload: Omit< + LiftedState, + 'actionsById' | 'computedStates' | 'committedState' + >; + readonly source: typeof source; + readonly instanceId: number; + readonly libConfig?: unknown; + readonly actionsById: string; + readonly computedStates: string; + readonly committedState: boolean; +} + +export type PageScriptToContentScriptMessageWithoutDisconnect< + S, + A extends Action +> = + | InitInstancePageScriptToContentScriptMessage + | InitMessage + | LiftedMessage + | SerializedPartialStateMessage + | SerializedExportMessage + | SerializedActionMessage + | SerializedStateMessage + | ErrorMessage + | InitInstanceMessage + | GetReportMessage + | StopMessage; + +export type PageScriptToContentScriptMessage> = + | PageScriptToContentScriptMessageWithoutDisconnect + | DisconnectMessage; + +function post>( + message: PageScriptToContentScriptMessage +) { window.postMessage(message, '*'); } @@ -164,7 +256,7 @@ function amendActionType( return { action, timestamp, stack }; } -interface LiftedMessage { +export interface LiftedMessage { readonly type: 'LIFTED'; readonly liftedState: { readonly isPaused: boolean }; readonly instanceId: number; @@ -250,28 +342,52 @@ export function toContentScript>( serializeAction?: Serialize | undefined ) { if (message.type === 'ACTION') { - message.action = stringify(message.action, serializeAction); - message.payload = stringify(message.payload, serializeState); - } else if (message.type === 'STATE' || message.type === 'PARTIAL_STATE') { + post({ + ...message, + action: stringify(message.action, serializeAction), + payload: stringify(message.payload, serializeState), + }); + } else if (message.type === 'STATE') { const { actionsById, computedStates, committedState, ...rest } = message.payload; - message.payload = rest; - message.actionsById = stringify(actionsById, serializeAction); - message.computedStates = stringify(computedStates, serializeState); - message.committedState = typeof committedState !== 'undefined'; + post({ + ...message, + payload: rest, + actionsById: stringify(actionsById, serializeAction), + computedStates: stringify(computedStates, serializeState), + committedState: typeof committedState !== 'undefined', + }); + } else if (message.type === 'PARTIAL_STATE') { + const { actionsById, computedStates, committedState, ...rest } = + message.payload; + post({ + ...message, + payload: rest, + actionsById: stringify(actionsById, serializeAction), + computedStates: stringify(computedStates, serializeState), + committedState: typeof committedState !== 'undefined', + }); } else if (message.type === 'EXPORT') { - message.payload = stringify(message.payload, serializeAction); - if (typeof message.committedState !== 'undefined') { - message.committedState = stringify( - message.committedState, - serializeState - ); - } + post({ + ...message, + payload: stringify(message.payload, serializeAction), + committedState: + typeof message.committedState !== 'undefined' + ? stringify(message.committedState, serializeState) + : (message.committedState as undefined), + }); + } else { + post(message); } - post(message); } -export function sendMessage(action, state, config, instanceId, name) { +export function sendMessage( + action, + state, + config: Config, + instanceId?: number, + name?: string +) { let amendedAction = action; if (typeof config !== 'object') { // Legacy: sending actions not from connected part @@ -427,8 +543,11 @@ export function connect(preConfig) { sendMessage(amendedAction, amendedState, config); }; - const init = (state, liftedData) => { - const message = { + const init = >( + state: S, + liftedData: LiftedState + ) => { + const message: InitMessage = { type: 'INIT', payload: stringify(state, config.serialize), instanceId: id, @@ -454,8 +573,8 @@ export function connect(preConfig) { post(message); }; - const error = (payload) => { - post({ type: 'ERROR', payload, id, source }); + const error = (payload: unknown) => { + post({ type: 'ERROR', payload, instanceId: id, source }); }; window.addEventListener('message', handleMessages, false); diff --git a/extension/src/browser/extension/inject/contentScript.ts b/extension/src/browser/extension/inject/contentScript.ts index 963e7b3c..2611ff2c 100644 --- a/extension/src/browser/extension/inject/contentScript.ts +++ b/extension/src/browser/extension/inject/contentScript.ts @@ -4,6 +4,11 @@ import { isAllowed, } from '../options/syncOptions'; import { TabMessage } from '../../../app/middlewares/api'; +import { + PageScriptToContentScriptMessage, + PageScriptToContentScriptMessageWithoutDisconnect, +} from '../../../app/api'; +import { Action } from 'redux'; const source = '@devtools-extension'; const pageSource = '@devtools-page'; // Chrome message limit is 64 MB, but we're using 32 MB to include other object's parts @@ -64,21 +69,46 @@ function handleDisconnect() { bg = undefined; } -function tryCatch( - fn: (args: PageScriptToContentScriptMessage) => void, - args: PageScriptToContentScriptMessage +interface SplitMessageBase { + readonly type?: never; +} + +interface SplitMessageStart extends SplitMessageBase { + readonly split: 'start'; +} + +interface SplitMessageChunk extends SplitMessageBase { + readonly instanceId: number; + readonly source: typeof pageSource; + readonly split: 'chunk'; + readonly chunk: [string, string]; +} + +interface SplitMessageEnd extends SplitMessageBase { + readonly instanceId: number; + readonly source: typeof pageSource; + readonly split: 'end'; +} + +type SplitMessage = SplitMessageStart | SplitMessageChunk | SplitMessageEnd; + +function tryCatch>( + fn: (args: PageScriptToContentScriptMessage | SplitMessage) => void, + args: PageScriptToContentScriptMessageWithoutDisconnect ) { try { return fn(args); } catch (err) { if (err.message === 'Message length exceeded maximum allowed length.') { const instanceId = args.instanceId; - const newArgs = { split: 'start' }; - const toSplit = []; + const newArgs: SplitMessageStart = { + split: 'start', + }; + const toSplit: [string, string][] = []; let size = 0; let arg; Object.keys(args).map((key) => { - arg = args[key]; + arg = args[key as keyof typeof args]; if (typeof arg === 'string') { size += arg.length; if (size > maxChromeMsgSize) { @@ -86,7 +116,7 @@ function tryCatch( return; } } - newArgs[key] = arg; + newArgs[key as keyof typeof newArgs] = arg; }); fn(newArgs); for (let i = 0; i < toSplit.length; i++) { @@ -110,21 +140,6 @@ function tryCatch( } } -interface InitInstancePageScriptToContentScriptMessage { - readonly type: 'INIT_INSTANCE'; - readonly instanceId: number; - readonly source: typeof pageSource; -} - -interface DisconnectMessage { - readonly type: 'DISCONNECT'; - readonly source: typeof pageSource; -} - -export type PageScriptToContentScriptMessage = - | InitInstancePageScriptToContentScriptMessage - | DisconnectMessage; - interface InitInstanceContentScriptToBackgroundMessage { readonly name: 'INIT_INSTANCE'; readonly instanceId: number; @@ -143,7 +158,9 @@ function postToBackground(message: ContentScriptToBackgroundMessage) { bg!.postMessage(message); } -function send(message: never) { +function send>( + message: PageScriptToContentScriptMessage | SplitMessage +) { if (!connected) connect(); if (message.type === 'INIT_INSTANCE') { getOptionsFromBg(); @@ -154,7 +171,9 @@ function send(message: never) { } // Resend messages from the page to the background script -function handleMessages(event: MessageEvent) { +function handleMessages>( + event: MessageEvent> +) { if (!isAllowed()) return; if (!event || event.source !== window || typeof event.data !== 'object') { return;