diff --git a/extension/src/app/api/importState.ts b/extension/src/app/api/importState.ts index 74f8da51..bacd30b9 100644 --- a/extension/src/app/api/importState.ts +++ b/extension/src/app/api/importState.ts @@ -1,6 +1,11 @@ import mapValues from 'lodash/mapValues'; import jsan from 'jsan'; import seralizeImmutable from '@redux-devtools/serialize/lib/immutable/serialize'; +import { + Config, + SerializeWithImmutable, +} from '../../browser/extension/inject/pageScript'; +import Immutable from 'immutable'; function deprecate(param: string) { // eslint-disable-next-line no-console @@ -9,14 +14,34 @@ function deprecate(param: string) { ); } +interface SerializeWithRequiredImmutable extends SerializeWithImmutable { + readonly immutable: typeof Immutable; +} + +function isSerializeWithImmutable( + serialize: boolean | SerializeWithImmutable +): serialize is SerializeWithRequiredImmutable { + return !!(serialize as SerializeWithImmutable).immutable; +} + +interface SerializeWithRequiredReviver extends SerializeWithImmutable { + readonly reviver: (key: string, value: unknown) => unknown; +} + +function isSerializeWithReviver( + serialize: boolean | SerializeWithImmutable +): serialize is SerializeWithRequiredReviver { + return !!(serialize as SerializeWithImmutable).immutable; +} + export default function importState( - state, - { deserializeState, deserializeAction, serialize } + state: string | undefined, + { deserializeState, deserializeAction, serialize }: Config ) { if (!state) return undefined; let parse = jsan.parse; if (serialize) { - if (serialize.immutable) { + if (isSerializeWithImmutable(serialize)) { parse = (v) => jsan.parse( v, @@ -27,7 +52,7 @@ export default function importState( serialize.reviver ).reviver ); - } else if (serialize.reviver) { + } else if (isSerializeWithReviver(serialize)) { parse = (v) => jsan.parse(v, serialize.reviver); } } diff --git a/extension/src/app/api/index.ts b/extension/src/app/api/index.ts index a3a7d8ed..d10768f5 100644 --- a/extension/src/app/api/index.ts +++ b/extension/src/app/api/index.ts @@ -9,8 +9,14 @@ import { Config } from '../../browser/extension/inject/pageScript'; import { Action } from 'redux'; import { LiftedState, PerformAction } from '@redux-devtools/instrument'; import { LibConfig } from '@redux-devtools/app/lib/actions'; +import { ContentScriptToPageScriptMessage } from '../../browser/extension/inject/contentScript'; +import { Position } from './openWindow'; -const listeners = {}; +const listeners: { + [instanceId: string]: + | ((message: ContentScriptToPageScriptMessage) => void) + | ((message: ContentScriptToPageScriptMessage) => void)[]; +} = {}; export const source = '@devtools-page'; function windowReplacer(key: string, value: unknown) { @@ -129,15 +135,15 @@ interface InitMessage> { libConfig?: LibConfig; } -interface SerializedPartialLiftedState> { +interface SerializedPartialLiftedState { readonly stagedActionIds: readonly number[]; readonly currentStateIndex: number; readonly nextActionId: number; } -interface SerializedPartialStateMessage> { +interface SerializedPartialStateMessage { readonly type: 'PARTIAL_STATE'; - readonly payload: SerializedPartialLiftedState; + readonly payload: SerializedPartialLiftedState; readonly source: typeof source; readonly instanceId: number; readonly maxAge: number; @@ -177,27 +183,41 @@ interface SerializedStateMessage> { readonly computedStates: string; readonly committedState: boolean; } -export type PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance< + +interface OpenMessage { + readonly source: typeof source; + readonly type: 'OPEN'; + readonly position: Position; +} + +export type PageScriptToContentScriptMessageForwardedToMonitors< S, A extends Action > = | InitMessage | LiftedMessage - | SerializedPartialStateMessage + | SerializedPartialStateMessage | SerializedExportMessage | SerializedActionMessage - | SerializedStateMessage + | SerializedStateMessage; + +export type PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance< + S, + A extends Action +> = + | PageScriptToContentScriptMessageForwardedToMonitors | ErrorMessage - | InitInstanceMessage | GetReportMessage - | StopMessage; + | StopMessage + | OpenMessage; export type PageScriptToContentScriptMessageWithoutDisconnect< S, A extends Action > = | PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance - | InitInstancePageScriptToContentScriptMessage; + | InitInstancePageScriptToContentScriptMessage + | InitInstanceMessage; export type PageScriptToContentScriptMessage> = | PageScriptToContentScriptMessageWithoutDisconnect @@ -262,7 +282,7 @@ function amendActionType( return { action, timestamp, stack }; } -export interface LiftedMessage { +interface LiftedMessage { readonly type: 'LIFTED'; readonly liftedState: { readonly isPaused: boolean | undefined }; readonly instanceId: number; @@ -303,7 +323,7 @@ interface StateMessage> { readonly libConfig?: LibConfig; } -interface ErrorMessage { +export interface ErrorMessage { readonly type: 'ERROR'; readonly payload: unknown; readonly source: typeof source; @@ -412,7 +432,7 @@ export function sendMessage( toContentScript(message, config.serialize, config.serialize); } -function handleMessages(event) { +function handleMessages(event: MessageEvent) { if (process.env.BABEL_ENV !== 'test' && (!event || event.source !== window)) { return; } @@ -420,33 +440,38 @@ function handleMessages(event) { if (!message || message.source !== '@devtools-extension') return; Object.keys(listeners).forEach((id) => { if (message.id && id !== message.id) return; - if (typeof listeners[id] === 'function') listeners[id](message); + const listenersForId = listeners[id]; + if (typeof listenersForId === 'function') listenersForId(message); else { - listeners[id].forEach((fn) => { + listenersForId.forEach((fn) => { fn(message); }); } }); } -export function setListener(onMessage, instanceId) { +export function setListener( + onMessage: (message: ContentScriptToPageScriptMessage) => void, + instanceId: number +) { listeners[instanceId] = onMessage; window.addEventListener('message', handleMessages, false); } -const liftListener = (listener, config) => (message) => { - let data = {}; - if (message.type === 'IMPORT') { - data.type = 'DISPATCH'; - data.payload = { - type: 'IMPORT_STATE', - ...importState(message.state, config), - }; - } else { - data = message; - } - listener(data); -}; +const liftListener = + (listener, config: Config) => (message: ContentScriptToPageScriptMessage) => { + let data = {}; + if (message.type === 'IMPORT') { + data.type = 'DISPATCH'; + data.payload = { + type: 'IMPORT_STATE', + ...importState(message.state, config), + }; + } else { + data = message; + } + listener(data); + }; export function disconnect() { window.removeEventListener('message', handleMessages); @@ -471,7 +496,7 @@ export function connect(preConfig: Config) { let delayedActions = []; let delayedStates = []; - const rootListener = (action) => { + const rootListener = (action: ContentScriptToPageScriptMessage) => { if (autoPause) { if (action.type === 'START') isPaused = false; else if (action.type === 'STOP') isPaused = true; @@ -495,11 +520,14 @@ export function connect(preConfig: Config) { const subscribe = (listener) => { if (!listener) return undefined; const liftedListener = liftListener(listener, config); - listeners[id].push(liftedListener); + const listenersForId = listeners[id] as (( + message: ContentScriptToPageScriptMessage + ) => void)[]; + listenersForId.push(liftedListener); return function unsubscribe() { - const index = listeners[id].indexOf(liftedListener); - listeners[id].splice(index, 1); + const index = listenersForId.indexOf(liftedListener); + listenersForId.splice(index, 1); }; }; diff --git a/extension/src/app/api/notifyErrors.ts b/extension/src/app/api/notifyErrors.ts index 8b226945..569cbb3a 100644 --- a/extension/src/app/api/notifyErrors.ts +++ b/extension/src/app/api/notifyErrors.ts @@ -1,4 +1,4 @@ -let handleError: () => boolean; +let handleError: (() => boolean) | undefined; let lastTime = 0; function createExpBackoffTimer(step: number) { @@ -42,7 +42,7 @@ function catchErrors(e: ErrorEvent) { postError(e.message); } -export default function notifyErrors(onError: () => boolean) { +export default function notifyErrors(onError?: () => boolean) { handleError = onError; window.addEventListener('error', catchErrors, false); } diff --git a/extension/src/app/api/openWindow.ts b/extension/src/app/api/openWindow.ts index c997d104..51f9d145 100644 --- a/extension/src/app/api/openWindow.ts +++ b/extension/src/app/api/openWindow.ts @@ -1,12 +1,18 @@ +import { Action } from 'redux'; +import { PageScriptToContentScriptMessage } from './index'; + export type Position = 'left' | 'right' | 'bottom' | 'panel' | 'remote'; -export default function openWindow(position?: Position) { - window.postMessage( - { - source: '@devtools-page', - type: 'OPEN', - position: position || 'right', - }, - '*' - ); +function post>( + message: PageScriptToContentScriptMessage +) { + window.postMessage(message, '*'); +} + +export default function openWindow(position?: Position) { + post({ + source: '@devtools-page', + type: 'OPEN', + position: position || 'right', + }); } diff --git a/extension/src/app/middlewares/api.ts b/extension/src/app/middlewares/api.ts index 7625c8d9..7e605729 100644 --- a/extension/src/app/middlewares/api.ts +++ b/extension/src/app/middlewares/api.ts @@ -15,11 +15,21 @@ import { getReport } from '../../browser/extension/background/logging'; import { CustomAction, DispatchAction as AppDispatchAction, + LibConfig, LiftedActionAction, StoreAction, } from '@redux-devtools/app/lib/actions'; import { Action, Dispatch } from 'redux'; -import { ContentScriptToBackgroundMessage } from '../../browser/extension/inject/contentScript'; +import { + ContentScriptToBackgroundMessage, + SplitMessage, +} from '../../browser/extension/inject/contentScript'; +import { + ErrorMessage, + PageScriptToContentScriptMessageForwardedToMonitors, + PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance, +} from '../api'; +import { LiftedState } from '@redux-devtools/instrument'; interface TabMessageBase { readonly type: string; @@ -72,8 +82,85 @@ interface NAAction { readonly id: string; } -interface UpdateStateAction { +interface InitMessage> { + readonly type: 'INIT'; + readonly payload: string; + readonly instanceId: string; + readonly source: '@devtools-page'; + action?: string; + name?: string | undefined; + liftedState?: LiftedState; + libConfig?: LibConfig; +} + +interface LiftedMessage { + readonly type: 'LIFTED'; + readonly liftedState: { readonly isPaused: boolean | undefined }; + readonly instanceId: string; + readonly source: '@devtools-page'; +} + +interface SerializedPartialLiftedState { + readonly stagedActionIds: readonly number[]; + readonly currentStateIndex: number; + readonly nextActionId: number; +} + +interface SerializedPartialStateMessage { + readonly type: 'PARTIAL_STATE'; + readonly payload: SerializedPartialLiftedState; + readonly source: '@devtools-page'; + readonly instanceId: string; + 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: '@devtools-page'; + readonly instanceId: string; +} + +interface SerializedActionMessage { + readonly type: 'ACTION'; + readonly payload: string; + readonly source: '@devtools-page'; + readonly instanceId: string; + readonly action: string; + readonly maxAge: number; + readonly nextActionId: number; +} + +interface SerializedStateMessage> { + readonly type: 'STATE'; + readonly payload: Omit< + LiftedState, + 'actionsById' | 'computedStates' | 'committedState' + >; + readonly source: '@devtools-page'; + readonly instanceId: string; + readonly libConfig?: LibConfig; + readonly actionsById: string; + readonly computedStates: string; + readonly committedState: boolean; +} + +type UpdateStateRequest> = + | InitMessage + | LiftedMessage + | SerializedPartialStateMessage + | SerializedExportMessage + | SerializedActionMessage + | SerializedStateMessage; + +interface UpdateStateAction> { readonly type: typeof UPDATE_STATE; + readonly request?: UpdateStateRequest; + readonly id?: string | number; } export type TabMessage = @@ -84,17 +171,27 @@ export type TabMessage = | ImportAction | ActionAction | ExportAction; -type PanelMessage = NAAction; -type MonitorMessage = UpdateStateAction; +type PanelMessage> = + | NAAction + | ErrorMessage + | UpdateStateAction; +type MonitorMessage> = + | NAAction + | ErrorMessage + | UpdateStateAction; type TabPort = Omit & { postMessage: (message: TabMessage) => void; }; type PanelPort = Omit & { - postMessage: (message: PanelMessage) => void; + postMessage: >( + message: PanelMessage + ) => void; }; type MonitorPort = Omit & { - postMessage: (message: MonitorMessage) => void; + postMessage: >( + message: MonitorMessage + ) => void; }; const CONNECTED = 'socket/CONNECTED'; @@ -108,17 +205,25 @@ const connections: { panel: {}, monitor: {}, }; -const chunks = {}; +const chunks: { + [instanceId: string]: PageScriptToContentScriptMessageForwardedToMonitors< + unknown, + Action + >; +} = {}; let monitors = 0; let isMonitored = false; const getId = (sender: chrome.runtime.MessageSender, name?: string) => sender.tab ? sender.tab.id! : name || sender.id!; -type MonitorAction = NAAction; +type MonitorAction> = + | NAAction + | ErrorMessage + | UpdateStateAction; -function toMonitors( - action: MonitorAction, +function toMonitors>( + action: MonitorAction, tabId?: string | number, verbose?: boolean ) { @@ -195,12 +300,14 @@ function togglePersist() { } } -type BackgroundStoreMessage = unknown; +type BackgroundStoreMessage> = + | PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance + | SplitMessage; type BackgroundStoreResponse = { readonly options: Options }; // Receive messages from content scripts -function messaging( - request: BackgroundStoreMessage, +function messaging>( + request: BackgroundStoreMessage, sender: chrome.runtime.MessageSender, sendResponse?: (response?: BackgroundStoreResponse) => void ) { @@ -259,9 +366,13 @@ function messaging( return; } - const action = { type: UPDATE_STATE, request, id: tabId }; + const action: UpdateStateAction = { + type: UPDATE_STATE, + request, + id: tabId, + }; const instanceId = `${tabId}/${request.instanceId}`; - if (request.split) { + if ('split' in request) { if (request.split === 'start') { chunks[instanceId] = request; return; diff --git a/extension/src/browser/extension/background/logging.ts b/extension/src/browser/extension/background/logging.ts index bc5a231c..38735253 100644 --- a/extension/src/browser/extension/background/logging.ts +++ b/extension/src/browser/extension/background/logging.ts @@ -1,6 +1,10 @@ import { LIFTED_ACTION } from '@redux-devtools/app/lib/constants/actionTypes'; -export function getReport(reportId, tabId, instanceId) { +export function getReport( + reportId: string, + tabId: string | number, + instanceId: number +) { chrome.storage.local.get(['s:hostname', 's:port', 's:secure'], (options) => { if (!options['s:hostname'] || !options['s:port']) return; const url = `${options['s:secure'] ? 'https' : 'http'}://${ diff --git a/extension/src/browser/extension/devpanel/index.tsx b/extension/src/browser/extension/devpanel/index.tsx index 5b58b884..acf68e36 100644 --- a/extension/src/browser/extension/devpanel/index.tsx +++ b/extension/src/browser/extension/devpanel/index.tsx @@ -18,7 +18,7 @@ const messageStyle: CSSProperties = { textAlign: 'center', }; -let rendered: boolean; +let rendered: boolean | undefined; let store: Store | undefined; let bgConnection: chrome.runtime.Port; let naTimeout: NodeJS.Timeout; diff --git a/extension/src/browser/extension/inject/contentScript.ts b/extension/src/browser/extension/inject/contentScript.ts index 671a7ae9..f163ddff 100644 --- a/extension/src/browser/extension/inject/contentScript.ts +++ b/extension/src/browser/extension/inject/contentScript.ts @@ -158,6 +158,8 @@ interface SplitMessageBase { } interface SplitMessageStart extends SplitMessageBase { + readonly instanceId: number; + readonly source: typeof pageSource; readonly split: 'start'; } @@ -174,7 +176,10 @@ interface SplitMessageEnd extends SplitMessageBase { readonly split: 'end'; } -type SplitMessage = SplitMessageStart | SplitMessageChunk | SplitMessageEnd; +export type SplitMessage = + | SplitMessageStart + | SplitMessageChunk + | SplitMessageEnd; function tryCatch>( fn: ( diff --git a/extension/src/browser/extension/inject/pageScript.ts b/extension/src/browser/extension/inject/pageScript.ts index 3fbf7c5c..930f655e 100644 --- a/extension/src/browser/extension/inject/pageScript.ts +++ b/extension/src/browser/extension/inject/pageScript.ts @@ -36,11 +36,7 @@ import { getSerializeParameter, Serialize, } from '../../../app/api'; -import { - LiftedAction, - LiftedState, - PerformAction, -} from '@redux-devtools/instrument'; +import { LiftedAction, LiftedState } from '@redux-devtools/instrument'; import { CustomAction, DispatchAction, @@ -63,7 +59,7 @@ function deprecateParam(oldParam: string, newParam: string) { /* eslint-enable no-console */ } -interface SerializeWithImmutable extends Serialize { +export interface SerializeWithImmutable extends Serialize { readonly immutable?: typeof Immutable; readonly refs?: (new (data: any) => unknown)[] | null; } @@ -81,12 +77,12 @@ export interface ConfigWithExpandedMaxAge { | boolean | ((key: string, value: unknown) => unknown) | Serialize; - readonly statesFilter?: (state: S, index: number) => S; + readonly statesFilter?: (state: S, index?: number) => S; readonly actionsFilter?: >( action: A, id: number ) => A; - readonly stateSanitizer?: (state: S, index: number) => S; + readonly stateSanitizer?: (state: S, index?: number) => S; readonly actionSanitizer?: >( action: A, id: number @@ -117,6 +113,7 @@ export interface ConfigWithExpandedMaxAge { name?: string; readonly autoPause?: boolean; readonly features?: Features; + readonly type?: string; } export interface Config extends ConfigWithExpandedMaxAge { @@ -131,7 +128,7 @@ interface ReduxDevtoolsExtension { ): Store; (config?: Config): StoreEnhancer; open: (position?: Position) => void; - notifyErrors: (onError: () => boolean) => void; + notifyErrors: (onError?: () => boolean) => void; disconnect: () => void; }