Split large messages sent from background page to devpanel (#1706)

* Split messages sent to devpanel

* Types

* Create ninety-files-obey.md
This commit is contained in:
Nathan Bierema 2024-08-04 15:05:37 -04:00 committed by GitHub
parent 6cf528b4a0
commit 2163bc3f09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 152 additions and 14 deletions

View File

@ -0,0 +1,5 @@
---
'remotedev-redux-devtools-extension': patch
---
Split large messages sent from background page to devpanel

View File

@ -151,7 +151,7 @@ interface SerializedStateMessage<S, A extends Action<string>> {
readonly committedState: boolean; readonly committedState: boolean;
} }
type UpdateStateRequest<S, A extends Action<string>> = export type UpdateStateRequest<S, A extends Action<string>> =
| InitMessage<S, A> | InitMessage<S, A>
| LiftedMessage | LiftedMessage
| SerializedPartialStateMessage | SerializedPartialStateMessage
@ -169,6 +169,30 @@ interface UpdateStateAction<S, A extends Action<string>> {
readonly id: string | number; readonly id: string | number;
} }
type SplitUpdateStateRequestStart<S, A extends Action<string>> = {
split: 'start';
} & Partial<UpdateStateRequest<S, A>>;
interface SplitUpdateStateRequestChunk {
readonly split: 'chunk';
readonly chunk: [string, string];
}
interface SplitUpdateStateRequestEnd {
readonly split: 'end';
}
export type SplitUpdateStateRequest<S, A extends Action<string>> =
| SplitUpdateStateRequestStart<S, A>
| SplitUpdateStateRequestChunk
| SplitUpdateStateRequestEnd;
interface SplitUpdateStateAction<S, A extends Action<string>> {
readonly type: typeof UPDATE_STATE;
request: SplitUpdateStateRequest<S, A>;
readonly id: string | number;
}
export type TabMessage = export type TabMessage =
| StartAction | StartAction
| StopAction | StopAction
@ -177,11 +201,16 @@ export type TabMessage =
| ImportAction | ImportAction
| ActionAction | ActionAction
| ExportAction; | ExportAction;
export type PanelMessage<S, A extends Action<string>> = export type PanelMessageWithoutNA<S, A extends Action<string>> =
| NAAction
| ErrorMessage | ErrorMessage
| UpdateStateAction<S, A> | UpdateStateAction<S, A>
| SetPersistAction; | SetPersistAction;
export type PanelMessage<S, A extends Action<string>> =
| PanelMessageWithoutNA<S, A>
| NAAction;
export type PanelMessageWithSplitAction<S, A extends Action<string>> =
| PanelMessage<S, A>
| SplitUpdateStateAction<S, A>;
export type MonitorMessage = export type MonitorMessage =
| NAAction | NAAction
| ErrorMessage | ErrorMessage
@ -193,7 +222,7 @@ type TabPort = Omit<chrome.runtime.Port, 'postMessage'> & {
}; };
type PanelPort = Omit<chrome.runtime.Port, 'postMessage'> & { type PanelPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: <S, A extends Action<string>>( postMessage: <S, A extends Action<string>>(
message: PanelMessage<S, A>, message: PanelMessageWithSplitAction<S, A>,
) => void; ) => void;
}; };
type MonitorPort = Omit<chrome.runtime.Port, 'postMessage'> & { type MonitorPort = Omit<chrome.runtime.Port, 'postMessage'> & {
@ -229,21 +258,75 @@ type MonitorAction<S, A extends Action<string>> =
| UpdateStateAction<S, A> | UpdateStateAction<S, A>
| SetPersistAction; | SetPersistAction;
// Chrome message limit is 64 MB, but we're using 32 MB to include other object's parts
const maxChromeMsgSize = 32 * 1024 * 1024;
function toMonitors<S, A extends Action<string>>( function toMonitors<S, A extends Action<string>>(
action: MonitorAction<S, A>, action: MonitorAction<S, A>,
tabId?: string | number, tabId?: string | number,
verbose?: boolean, verbose?: boolean,
) { ) {
Object.keys(connections.monitor).forEach((id) => { for (const monitorPort of Object.values(connections.monitor)) {
connections.monitor[id].postMessage( monitorPort.postMessage(
verbose || action.type === 'ERROR' || action.type === SET_PERSIST verbose || action.type === 'ERROR' || action.type === SET_PERSIST
? action ? action
: { type: UPDATE_STATE }, : { type: UPDATE_STATE },
); );
}); }
Object.keys(connections.panel).forEach((id) => {
connections.panel[id].postMessage(action); for (const panelPort of Object.values(connections.panel)) {
}); try {
panelPort.postMessage(action);
} catch (err) {
if (
action.type !== UPDATE_STATE ||
err == null ||
(err as Error).message !==
'Message length exceeded maximum allowed length.'
) {
throw err;
}
const splitMessageStart: SplitUpdateStateRequestStart<S, A> = {
split: 'start',
};
const toSplit: [string, string][] = [];
let size = 0;
for (const [key, value] of Object.entries(
action.request as unknown as Record<string, unknown>,
)) {
if (typeof value === 'string') {
size += value.length;
if (size > maxChromeMsgSize) {
toSplit.push([key, value]);
continue;
}
}
(splitMessageStart as any)[key as keyof typeof splitMessageStart] =
value;
}
panelPort.postMessage({ ...action, request: splitMessageStart });
for (let i = 0; i < toSplit.length; i++) {
for (let j = 0; j < toSplit[i][1].length; j += maxChromeMsgSize) {
panelPort.postMessage({
...action,
request: {
split: 'chunk',
chunk: [
toSplit[i][0],
toSplit[i][1].substring(j, j + maxChromeMsgSize),
],
},
});
}
}
panelPort.postMessage({ ...action, request: { split: 'end' } });
}
}
} }
interface ImportMessage { interface ImportMessage {

View File

@ -16,6 +16,7 @@ import {
DispatchAction as AppDispatchAction, DispatchAction as AppDispatchAction,
} from '@redux-devtools/app'; } from '@redux-devtools/app';
import { LiftedState } from '@redux-devtools/instrument'; import { LiftedState } from '@redux-devtools/instrument';
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

View File

@ -3,12 +3,21 @@ import React, { CSSProperties, ReactNode } from 'react';
import { createRoot, Root } from 'react-dom/client'; import { createRoot, Root } from 'react-dom/client';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { Persistor } from 'redux-persist'; import { Persistor } from 'redux-persist';
import { REMOVE_INSTANCE, StoreAction } from '@redux-devtools/app'; import {
REMOVE_INSTANCE,
StoreAction,
UPDATE_STATE,
} from '@redux-devtools/app';
import App from '../app/App'; import App from '../app/App';
import configureStore from './store/panelStore'; import configureStore from './store/panelStore';
import { Action, Store } from 'redux'; import { Action, Store } from 'redux';
import type { PanelMessage } from '../background/store/apiMiddleware'; import {
PanelMessageWithoutNA,
PanelMessageWithSplitAction,
SplitUpdateStateRequest,
UpdateStateRequest,
} from '../background/store/apiMiddleware';
import type { StoreStateWithoutSocket } from './store/panelReducer'; import type { StoreStateWithoutSocket } from './store/panelReducer';
import { PersistGate } from 'redux-persist/integration/react'; import { PersistGate } from 'redux-persist/integration/react';
@ -90,19 +99,59 @@ function renderNA() {
}, 3500); }, 3500);
} }
let splitMessage: SplitUpdateStateRequest<unknown, Action<string>>;
function init(id: number) { function init(id: number) {
renderNA(); renderNA();
bgConnection = chrome.runtime.connect({ bgConnection = chrome.runtime.connect({
name: id ? id.toString() : undefined, name: id ? id.toString() : undefined,
}); });
bgConnection.onMessage.addListener( bgConnection.onMessage.addListener(
<S, A extends Action<string>>(message: PanelMessage<S, A>) => { <S, A extends Action<string>>(
message: PanelMessageWithSplitAction<S, A>,
) => {
if (message.type === 'NA') { if (message.type === 'NA') {
if (message.id === id) renderNA(); if (message.id === id) renderNA();
else store!.dispatch({ type: REMOVE_INSTANCE, id: message.id }); else store!.dispatch({ type: REMOVE_INSTANCE, id: message.id });
} else { } else {
if (!rendered) renderDevTools(); if (!rendered) renderDevTools();
store!.dispatch(message);
if (
message.type === UPDATE_STATE &&
(message.request as SplitUpdateStateRequest<S, A>).split
) {
const request = message.request as SplitUpdateStateRequest<S, A>;
if (request.split === 'start') {
splitMessage = request;
return;
}
if (request.split === 'chunk') {
if ((splitMessage as Record<string, unknown>)[request.chunk[0]]) {
(splitMessage as Record<string, unknown>)[request.chunk[0]] +=
request.chunk[1];
} else {
(splitMessage as Record<string, unknown>)[request.chunk[0]] =
request.chunk[1];
}
return;
}
if (request.split === 'end') {
store!.dispatch({
...message,
request: splitMessage as UpdateStateRequest<S, A>,
});
return;
}
throw new Error(
`Unable to process split message with type: ${(request as any).split}`,
);
} else {
store!.dispatch(message as PanelMessageWithoutNA<S, A>);
}
} }
}, },
); );