Define page script to content script messages

This commit is contained in:
Nathan Bierema 2021-07-17 15:21:14 -04:00
parent 425c78b8fc
commit eb35441e17
3 changed files with 186 additions and 47 deletions

View File

@ -139,6 +139,7 @@ export interface PartialLiftedState<S, A extends Action<unknown>> {
readonly stagedActionIds: readonly number[]; readonly stagedActionIds: readonly number[];
readonly currentStateIndex: number; readonly currentStateIndex: number;
readonly nextActionId: number; readonly nextActionId: number;
readonly committedState?: S;
} }
export function startingFrom<S, A extends Action<unknown>>( export function startingFrom<S, A extends Action<unknown>>(

View File

@ -5,7 +5,6 @@ import { getActionsArray } from '@redux-devtools/utils';
import { getLocalFilter, isFiltered, PartialLiftedState } from './filters'; import { getLocalFilter, isFiltered, PartialLiftedState } from './filters';
import importState from './importState'; import importState from './importState';
import generateId from './generateInstanceId'; import generateId from './generateInstanceId';
import { PageScriptToContentScriptMessage } from '../../browser/extension/inject/contentScript';
import { Config } from '../../browser/extension/inject/pageScript'; import { Config } from '../../browser/extension/inject/pageScript';
import { Action } from 'redux'; import { Action } from 'redux';
import { LiftedState, PerformAction } from '@redux-devtools/instrument'; import { LiftedState, PerformAction } from '@redux-devtools/instrument';
@ -107,7 +106,100 @@ export function getSerializeParameter(
return value; 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<S, A extends Action<unknown>> {
readonly type: 'INIT';
readonly payload: string;
readonly instanceId: number;
readonly source: typeof source;
action?: string;
name?: string | undefined;
liftedState?: LiftedState<S, A, unknown>;
libConfig?: unknown;
}
interface SerializedPartialLiftedState<S, A extends Action<unknown>> {
readonly stagedActionIds: readonly number[];
readonly currentStateIndex: number;
readonly nextActionId: number;
}
interface SerializedPartialStateMessage<S, A extends Action<unknown>> {
readonly type: 'PARTIAL_STATE';
readonly payload: SerializedPartialLiftedState<S, A>;
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<S, A extends Action<unknown>> {
readonly type: 'STATE';
readonly payload: Omit<
LiftedState<S, A, unknown>,
'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<unknown>
> =
| InitInstancePageScriptToContentScriptMessage
| InitMessage<S, A>
| LiftedMessage
| SerializedPartialStateMessage<S, A>
| SerializedExportMessage
| SerializedActionMessage
| SerializedStateMessage<S, A>
| ErrorMessage
| InitInstanceMessage
| GetReportMessage
| StopMessage;
export type PageScriptToContentScriptMessage<S, A extends Action<unknown>> =
| PageScriptToContentScriptMessageWithoutDisconnect<S, A>
| DisconnectMessage;
function post<S, A extends Action<unknown>>(
message: PageScriptToContentScriptMessage<S, A>
) {
window.postMessage(message, '*'); window.postMessage(message, '*');
} }
@ -164,7 +256,7 @@ function amendActionType(
return { action, timestamp, stack }; return { action, timestamp, stack };
} }
interface LiftedMessage { export interface LiftedMessage {
readonly type: 'LIFTED'; readonly type: 'LIFTED';
readonly liftedState: { readonly isPaused: boolean }; readonly liftedState: { readonly isPaused: boolean };
readonly instanceId: number; readonly instanceId: number;
@ -250,28 +342,52 @@ export function toContentScript<S, A extends Action<unknown>>(
serializeAction?: Serialize | undefined serializeAction?: Serialize | undefined
) { ) {
if (message.type === 'ACTION') { if (message.type === 'ACTION') {
message.action = stringify(message.action, serializeAction); post({
message.payload = stringify(message.payload, serializeState); ...message,
} else if (message.type === 'STATE' || message.type === 'PARTIAL_STATE') { action: stringify(message.action, serializeAction),
payload: stringify(message.payload, serializeState),
});
} else if (message.type === 'STATE') {
const { actionsById, computedStates, committedState, ...rest } = const { actionsById, computedStates, committedState, ...rest } =
message.payload; message.payload;
message.payload = rest; post({
message.actionsById = stringify(actionsById, serializeAction); ...message,
message.computedStates = stringify(computedStates, serializeState); payload: rest,
message.committedState = typeof committedState !== 'undefined'; 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') { } else if (message.type === 'EXPORT') {
message.payload = stringify(message.payload, serializeAction); post({
if (typeof message.committedState !== 'undefined') { ...message,
message.committedState = stringify( payload: stringify(message.payload, serializeAction),
message.committedState, committedState:
serializeState 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; let amendedAction = action;
if (typeof config !== 'object') { if (typeof config !== 'object') {
// Legacy: sending actions not from connected part // Legacy: sending actions not from connected part
@ -427,8 +543,11 @@ export function connect(preConfig) {
sendMessage(amendedAction, amendedState, config); sendMessage(amendedAction, amendedState, config);
}; };
const init = (state, liftedData) => { const init = <S, A extends Action<unknown>>(
const message = { state: S,
liftedData: LiftedState<S, A, unknown>
) => {
const message: InitMessage<S, A> = {
type: 'INIT', type: 'INIT',
payload: stringify(state, config.serialize), payload: stringify(state, config.serialize),
instanceId: id, instanceId: id,
@ -454,8 +573,8 @@ export function connect(preConfig) {
post(message); post(message);
}; };
const error = (payload) => { const error = (payload: unknown) => {
post({ type: 'ERROR', payload, id, source }); post({ type: 'ERROR', payload, instanceId: id, source });
}; };
window.addEventListener('message', handleMessages, false); window.addEventListener('message', handleMessages, false);

View File

@ -4,6 +4,11 @@ import {
isAllowed, isAllowed,
} from '../options/syncOptions'; } from '../options/syncOptions';
import { TabMessage } from '../../../app/middlewares/api'; import { TabMessage } from '../../../app/middlewares/api';
import {
PageScriptToContentScriptMessage,
PageScriptToContentScriptMessageWithoutDisconnect,
} from '../../../app/api';
import { Action } from 'redux';
const source = '@devtools-extension'; const source = '@devtools-extension';
const pageSource = '@devtools-page'; const pageSource = '@devtools-page';
// Chrome message limit is 64 MB, but we're using 32 MB to include other object's parts // 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; bg = undefined;
} }
function tryCatch<A>( interface SplitMessageBase {
fn: (args: PageScriptToContentScriptMessage) => void, readonly type?: never;
args: PageScriptToContentScriptMessage }
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<S, A extends Action<unknown>>(
fn: (args: PageScriptToContentScriptMessage<S, A> | SplitMessage) => void,
args: PageScriptToContentScriptMessageWithoutDisconnect<S, A>
) { ) {
try { try {
return fn(args); return fn(args);
} catch (err) { } catch (err) {
if (err.message === 'Message length exceeded maximum allowed length.') { if (err.message === 'Message length exceeded maximum allowed length.') {
const instanceId = args.instanceId; const instanceId = args.instanceId;
const newArgs = { split: 'start' }; const newArgs: SplitMessageStart = {
const toSplit = []; split: 'start',
};
const toSplit: [string, string][] = [];
let size = 0; let size = 0;
let arg; let arg;
Object.keys(args).map((key) => { Object.keys(args).map((key) => {
arg = args[key]; arg = args[key as keyof typeof args];
if (typeof arg === 'string') { if (typeof arg === 'string') {
size += arg.length; size += arg.length;
if (size > maxChromeMsgSize) { if (size > maxChromeMsgSize) {
@ -86,7 +116,7 @@ function tryCatch<A>(
return; return;
} }
} }
newArgs[key] = arg; newArgs[key as keyof typeof newArgs] = arg;
}); });
fn(newArgs); fn(newArgs);
for (let i = 0; i < toSplit.length; i++) { for (let i = 0; i < toSplit.length; i++) {
@ -110,21 +140,6 @@ function tryCatch<A>(
} }
} }
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 { interface InitInstanceContentScriptToBackgroundMessage {
readonly name: 'INIT_INSTANCE'; readonly name: 'INIT_INSTANCE';
readonly instanceId: number; readonly instanceId: number;
@ -143,7 +158,9 @@ function postToBackground(message: ContentScriptToBackgroundMessage) {
bg!.postMessage(message); bg!.postMessage(message);
} }
function send(message: never) { function send<S, A extends Action<unknown>>(
message: PageScriptToContentScriptMessage<S, A> | SplitMessage
) {
if (!connected) connect(); if (!connected) connect();
if (message.type === 'INIT_INSTANCE') { if (message.type === 'INIT_INSTANCE') {
getOptionsFromBg(); getOptionsFromBg();
@ -154,7 +171,9 @@ function send(message: never) {
} }
// Resend messages from the page to the background script // Resend messages from the page to the background script
function handleMessages(event: MessageEvent<PageScriptToContentScriptMessage>) { function handleMessages<S, A extends Action<unknown>>(
event: MessageEvent<PageScriptToContentScriptMessage<S, A>>
) {
if (!isAllowed()) return; if (!isAllowed()) return;
if (!event || event.source !== window || typeof event.data !== 'object') { if (!event || event.source !== window || typeof event.data !== 'object') {
return; return;