Skip to content

Split large messages sent from background page to devpanel #1706

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ninety-files-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'remotedev-redux-devtools-extension': patch
---

Split large messages sent from background page to devpanel
103 changes: 93 additions & 10 deletions extension/src/background/store/apiMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ interface SerializedStateMessage<S, A extends Action<string>> {
readonly committedState: boolean;
}

type UpdateStateRequest<S, A extends Action<string>> =
export type UpdateStateRequest<S, A extends Action<string>> =
| InitMessage<S, A>
| LiftedMessage
| SerializedPartialStateMessage
Expand All @@ -169,6 +169,30 @@ interface UpdateStateAction<S, A extends Action<string>> {
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 =
| StartAction
| StopAction
Expand All @@ -177,11 +201,16 @@ export type TabMessage =
| ImportAction
| ActionAction
| ExportAction;
export type PanelMessage<S, A extends Action<string>> =
| NAAction
export type PanelMessageWithoutNA<S, A extends Action<string>> =
| ErrorMessage
| UpdateStateAction<S, A>
| 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 =
| NAAction
| ErrorMessage
Expand All @@ -193,7 +222,7 @@ type TabPort = Omit<chrome.runtime.Port, 'postMessage'> & {
};
type PanelPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: <S, A extends Action<string>>(
message: PanelMessage<S, A>,
message: PanelMessageWithSplitAction<S, A>,
) => void;
};
type MonitorPort = Omit<chrome.runtime.Port, 'postMessage'> & {
Expand Down Expand Up @@ -229,21 +258,75 @@ type MonitorAction<S, A extends Action<string>> =
| UpdateStateAction<S, A>
| 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>>(
action: MonitorAction<S, A>,
tabId?: string | number,
verbose?: boolean,
) {
Object.keys(connections.monitor).forEach((id) => {
connections.monitor[id].postMessage(
for (const monitorPort of Object.values(connections.monitor)) {
monitorPort.postMessage(
verbose || action.type === 'ERROR' || action.type === SET_PERSIST
? action
: { 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 {
Expand Down
1 change: 1 addition & 0 deletions extension/src/contentScript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DispatchAction as AppDispatchAction,
} from '@redux-devtools/app';
import { LiftedState } from '@redux-devtools/instrument';

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
Expand Down
57 changes: 53 additions & 4 deletions extension/src/devpanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@ import React, { CSSProperties, ReactNode } from 'react';
import { createRoot, Root } from 'react-dom/client';
import { Provider } from 'react-redux';
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 configureStore from './store/panelStore';

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 { PersistGate } from 'redux-persist/integration/react';

Expand Down Expand Up @@ -90,19 +99,59 @@ function renderNA() {
}, 3500);
}

let splitMessage: SplitUpdateStateRequest<unknown, Action<string>>;

function init(id: number) {
renderNA();
bgConnection = chrome.runtime.connect({
name: id ? id.toString() : undefined,
});
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.id === id) renderNA();
else store!.dispatch({ type: REMOVE_INSTANCE, id: message.id });
} else {
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>);
}
}
},
);
Expand Down
Loading