Fill out more types

This commit is contained in:
Nathan Bierema 2021-07-18 15:19:44 -04:00
parent e07896be6a
commit 3cc1a62177
9 changed files with 251 additions and 75 deletions

View File

@ -1,6 +1,11 @@
import mapValues from 'lodash/mapValues'; import mapValues from 'lodash/mapValues';
import jsan from 'jsan'; import jsan from 'jsan';
import seralizeImmutable from '@redux-devtools/serialize/lib/immutable/serialize'; 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) { function deprecate(param: string) {
// eslint-disable-next-line no-console // 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( export default function importState(
state, state: string | undefined,
{ deserializeState, deserializeAction, serialize } { deserializeState, deserializeAction, serialize }: Config
) { ) {
if (!state) return undefined; if (!state) return undefined;
let parse = jsan.parse; let parse = jsan.parse;
if (serialize) { if (serialize) {
if (serialize.immutable) { if (isSerializeWithImmutable(serialize)) {
parse = (v) => parse = (v) =>
jsan.parse( jsan.parse(
v, v,
@ -27,7 +52,7 @@ export default function importState(
serialize.reviver serialize.reviver
).reviver ).reviver
); );
} else if (serialize.reviver) { } else if (isSerializeWithReviver(serialize)) {
parse = (v) => jsan.parse(v, serialize.reviver); parse = (v) => jsan.parse(v, serialize.reviver);
} }
} }

View File

@ -9,8 +9,14 @@ 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';
import { LibConfig } from '@redux-devtools/app/lib/actions'; 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'; export const source = '@devtools-page';
function windowReplacer(key: string, value: unknown) { function windowReplacer(key: string, value: unknown) {
@ -129,15 +135,15 @@ interface InitMessage<S, A extends Action<unknown>> {
libConfig?: LibConfig; libConfig?: LibConfig;
} }
interface SerializedPartialLiftedState<S, A extends Action<unknown>> { interface SerializedPartialLiftedState {
readonly stagedActionIds: readonly number[]; readonly stagedActionIds: readonly number[];
readonly currentStateIndex: number; readonly currentStateIndex: number;
readonly nextActionId: number; readonly nextActionId: number;
} }
interface SerializedPartialStateMessage<S, A extends Action<unknown>> { interface SerializedPartialStateMessage {
readonly type: 'PARTIAL_STATE'; readonly type: 'PARTIAL_STATE';
readonly payload: SerializedPartialLiftedState<S, A>; readonly payload: SerializedPartialLiftedState;
readonly source: typeof source; readonly source: typeof source;
readonly instanceId: number; readonly instanceId: number;
readonly maxAge: number; readonly maxAge: number;
@ -177,27 +183,41 @@ interface SerializedStateMessage<S, A extends Action<unknown>> {
readonly computedStates: string; readonly computedStates: string;
readonly committedState: boolean; readonly committedState: boolean;
} }
export type PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<
interface OpenMessage {
readonly source: typeof source;
readonly type: 'OPEN';
readonly position: Position;
}
export type PageScriptToContentScriptMessageForwardedToMonitors<
S, S,
A extends Action<unknown> A extends Action<unknown>
> = > =
| InitMessage<S, A> | InitMessage<S, A>
| LiftedMessage | LiftedMessage
| SerializedPartialStateMessage<S, A> | SerializedPartialStateMessage
| SerializedExportMessage | SerializedExportMessage
| SerializedActionMessage | SerializedActionMessage
| SerializedStateMessage<S, A> | SerializedStateMessage<S, A>;
export type PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<
S,
A extends Action<unknown>
> =
| PageScriptToContentScriptMessageForwardedToMonitors<S, A>
| ErrorMessage | ErrorMessage
| InitInstanceMessage
| GetReportMessage | GetReportMessage
| StopMessage; | StopMessage
| OpenMessage;
export type PageScriptToContentScriptMessageWithoutDisconnect< export type PageScriptToContentScriptMessageWithoutDisconnect<
S, S,
A extends Action<unknown> A extends Action<unknown>
> = > =
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A> | PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| InitInstancePageScriptToContentScriptMessage; | InitInstancePageScriptToContentScriptMessage
| InitInstanceMessage;
export type PageScriptToContentScriptMessage<S, A extends Action<unknown>> = export type PageScriptToContentScriptMessage<S, A extends Action<unknown>> =
| PageScriptToContentScriptMessageWithoutDisconnect<S, A> | PageScriptToContentScriptMessageWithoutDisconnect<S, A>
@ -262,7 +282,7 @@ function amendActionType(
return { action, timestamp, stack }; return { action, timestamp, stack };
} }
export interface LiftedMessage { interface LiftedMessage {
readonly type: 'LIFTED'; readonly type: 'LIFTED';
readonly liftedState: { readonly isPaused: boolean | undefined }; readonly liftedState: { readonly isPaused: boolean | undefined };
readonly instanceId: number; readonly instanceId: number;
@ -303,7 +323,7 @@ interface StateMessage<S, A extends Action<unknown>> {
readonly libConfig?: LibConfig; readonly libConfig?: LibConfig;
} }
interface ErrorMessage { export interface ErrorMessage {
readonly type: 'ERROR'; readonly type: 'ERROR';
readonly payload: unknown; readonly payload: unknown;
readonly source: typeof source; readonly source: typeof source;
@ -412,7 +432,7 @@ export function sendMessage(
toContentScript(message, config.serialize, config.serialize); toContentScript(message, config.serialize, config.serialize);
} }
function handleMessages(event) { function handleMessages(event: MessageEvent<ContentScriptToPageScriptMessage>) {
if (process.env.BABEL_ENV !== 'test' && (!event || event.source !== window)) { if (process.env.BABEL_ENV !== 'test' && (!event || event.source !== window)) {
return; return;
} }
@ -420,21 +440,26 @@ function handleMessages(event) {
if (!message || message.source !== '@devtools-extension') return; if (!message || message.source !== '@devtools-extension') return;
Object.keys(listeners).forEach((id) => { Object.keys(listeners).forEach((id) => {
if (message.id && id !== message.id) return; 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 { else {
listeners[id].forEach((fn) => { listenersForId.forEach((fn) => {
fn(message); fn(message);
}); });
} }
}); });
} }
export function setListener(onMessage, instanceId) { export function setListener(
onMessage: (message: ContentScriptToPageScriptMessage) => void,
instanceId: number
) {
listeners[instanceId] = onMessage; listeners[instanceId] = onMessage;
window.addEventListener('message', handleMessages, false); window.addEventListener('message', handleMessages, false);
} }
const liftListener = (listener, config) => (message) => { const liftListener =
(listener, config: Config) => (message: ContentScriptToPageScriptMessage) => {
let data = {}; let data = {};
if (message.type === 'IMPORT') { if (message.type === 'IMPORT') {
data.type = 'DISPATCH'; data.type = 'DISPATCH';
@ -471,7 +496,7 @@ export function connect(preConfig: Config) {
let delayedActions = []; let delayedActions = [];
let delayedStates = []; let delayedStates = [];
const rootListener = (action) => { const rootListener = (action: ContentScriptToPageScriptMessage) => {
if (autoPause) { if (autoPause) {
if (action.type === 'START') isPaused = false; if (action.type === 'START') isPaused = false;
else if (action.type === 'STOP') isPaused = true; else if (action.type === 'STOP') isPaused = true;
@ -495,11 +520,14 @@ export function connect(preConfig: Config) {
const subscribe = (listener) => { const subscribe = (listener) => {
if (!listener) return undefined; if (!listener) return undefined;
const liftedListener = liftListener(listener, config); const liftedListener = liftListener(listener, config);
listeners[id].push(liftedListener); const listenersForId = listeners[id] as ((
message: ContentScriptToPageScriptMessage
) => void)[];
listenersForId.push(liftedListener);
return function unsubscribe() { return function unsubscribe() {
const index = listeners[id].indexOf(liftedListener); const index = listenersForId.indexOf(liftedListener);
listeners[id].splice(index, 1); listenersForId.splice(index, 1);
}; };
}; };

View File

@ -1,4 +1,4 @@
let handleError: () => boolean; let handleError: (() => boolean) | undefined;
let lastTime = 0; let lastTime = 0;
function createExpBackoffTimer(step: number) { function createExpBackoffTimer(step: number) {
@ -42,7 +42,7 @@ function catchErrors(e: ErrorEvent) {
postError(e.message); postError(e.message);
} }
export default function notifyErrors(onError: () => boolean) { export default function notifyErrors(onError?: () => boolean) {
handleError = onError; handleError = onError;
window.addEventListener('error', catchErrors, false); window.addEventListener('error', catchErrors, false);
} }

View File

@ -1,12 +1,18 @@
import { Action } from 'redux';
import { PageScriptToContentScriptMessage } from './index';
export type Position = 'left' | 'right' | 'bottom' | 'panel' | 'remote'; export type Position = 'left' | 'right' | 'bottom' | 'panel' | 'remote';
function post<S, A extends Action<unknown>>(
message: PageScriptToContentScriptMessage<S, A>
) {
window.postMessage(message, '*');
}
export default function openWindow(position?: Position) { export default function openWindow(position?: Position) {
window.postMessage( post({
{
source: '@devtools-page', source: '@devtools-page',
type: 'OPEN', type: 'OPEN',
position: position || 'right', position: position || 'right',
}, });
'*'
);
} }

View File

@ -15,11 +15,21 @@ import { getReport } from '../../browser/extension/background/logging';
import { import {
CustomAction, CustomAction,
DispatchAction as AppDispatchAction, DispatchAction as AppDispatchAction,
LibConfig,
LiftedActionAction, LiftedActionAction,
StoreAction, StoreAction,
} from '@redux-devtools/app/lib/actions'; } from '@redux-devtools/app/lib/actions';
import { Action, Dispatch } from 'redux'; 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 { interface TabMessageBase {
readonly type: string; readonly type: string;
@ -72,8 +82,85 @@ interface NAAction {
readonly id: string; readonly id: string;
} }
interface UpdateStateAction { interface InitMessage<S, A extends Action<unknown>> {
readonly type: 'INIT';
readonly payload: string;
readonly instanceId: string;
readonly source: '@devtools-page';
action?: string;
name?: string | undefined;
liftedState?: LiftedState<S, A, unknown>;
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<S, A extends Action<unknown>> {
readonly type: 'STATE';
readonly payload: Omit<
LiftedState<S, A, unknown>,
'actionsById' | 'computedStates' | 'committedState'
>;
readonly source: '@devtools-page';
readonly instanceId: string;
readonly libConfig?: LibConfig;
readonly actionsById: string;
readonly computedStates: string;
readonly committedState: boolean;
}
type UpdateStateRequest<S, A extends Action<unknown>> =
| InitMessage<S, A>
| LiftedMessage
| SerializedPartialStateMessage
| SerializedExportMessage
| SerializedActionMessage
| SerializedStateMessage<S, A>;
interface UpdateStateAction<S, A extends Action<unknown>> {
readonly type: typeof UPDATE_STATE; readonly type: typeof UPDATE_STATE;
readonly request?: UpdateStateRequest<S, A>;
readonly id?: string | number;
} }
export type TabMessage = export type TabMessage =
@ -84,17 +171,27 @@ export type TabMessage =
| ImportAction | ImportAction
| ActionAction | ActionAction
| ExportAction; | ExportAction;
type PanelMessage = NAAction; type PanelMessage<S, A extends Action<unknown>> =
type MonitorMessage = UpdateStateAction; | NAAction
| ErrorMessage
| UpdateStateAction<S, A>;
type MonitorMessage<S, A extends Action<unknown>> =
| NAAction
| ErrorMessage
| UpdateStateAction<S, A>;
type TabPort = Omit<chrome.runtime.Port, 'postMessage'> & { type TabPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: (message: TabMessage) => void; postMessage: (message: TabMessage) => void;
}; };
type PanelPort = Omit<chrome.runtime.Port, 'postMessage'> & { type PanelPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: (message: PanelMessage) => void; postMessage: <S, A extends Action<unknown>>(
message: PanelMessage<S, A>
) => void;
}; };
type MonitorPort = Omit<chrome.runtime.Port, 'postMessage'> & { type MonitorPort = Omit<chrome.runtime.Port, 'postMessage'> & {
postMessage: (message: MonitorMessage) => void; postMessage: <S, A extends Action<unknown>>(
message: MonitorMessage<S, A>
) => void;
}; };
const CONNECTED = 'socket/CONNECTED'; const CONNECTED = 'socket/CONNECTED';
@ -108,17 +205,25 @@ const connections: {
panel: {}, panel: {},
monitor: {}, monitor: {},
}; };
const chunks = {}; const chunks: {
[instanceId: string]: PageScriptToContentScriptMessageForwardedToMonitors<
unknown,
Action<unknown>
>;
} = {};
let monitors = 0; let monitors = 0;
let isMonitored = false; let isMonitored = false;
const getId = (sender: chrome.runtime.MessageSender, name?: string) => const getId = (sender: chrome.runtime.MessageSender, name?: string) =>
sender.tab ? sender.tab.id! : name || sender.id!; sender.tab ? sender.tab.id! : name || sender.id!;
type MonitorAction = NAAction; type MonitorAction<S, A extends Action<unknown>> =
| NAAction
| ErrorMessage
| UpdateStateAction<S, A>;
function toMonitors( function toMonitors<S, A extends Action<unknown>>(
action: MonitorAction, action: MonitorAction<S, A>,
tabId?: string | number, tabId?: string | number,
verbose?: boolean verbose?: boolean
) { ) {
@ -195,12 +300,14 @@ function togglePersist() {
} }
} }
type BackgroundStoreMessage = unknown; type BackgroundStoreMessage<S, A extends Action<unknown>> =
| PageScriptToContentScriptMessageWithoutDisconnectOrInitInstance<S, A>
| SplitMessage;
type BackgroundStoreResponse = { readonly options: Options }; type BackgroundStoreResponse = { readonly options: Options };
// Receive messages from content scripts // Receive messages from content scripts
function messaging( function messaging<S, A extends Action<unknown>>(
request: BackgroundStoreMessage, request: BackgroundStoreMessage<S, A>,
sender: chrome.runtime.MessageSender, sender: chrome.runtime.MessageSender,
sendResponse?: (response?: BackgroundStoreResponse) => void sendResponse?: (response?: BackgroundStoreResponse) => void
) { ) {
@ -259,9 +366,13 @@ function messaging(
return; return;
} }
const action = { type: UPDATE_STATE, request, id: tabId }; const action: UpdateStateAction<S, A> = {
type: UPDATE_STATE,
request,
id: tabId,
};
const instanceId = `${tabId}/${request.instanceId}`; const instanceId = `${tabId}/${request.instanceId}`;
if (request.split) { if ('split' in request) {
if (request.split === 'start') { if (request.split === 'start') {
chunks[instanceId] = request; chunks[instanceId] = request;
return; return;

View File

@ -1,6 +1,10 @@
import { LIFTED_ACTION } from '@redux-devtools/app/lib/constants/actionTypes'; 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) => { chrome.storage.local.get(['s:hostname', 's:port', 's:secure'], (options) => {
if (!options['s:hostname'] || !options['s:port']) return; if (!options['s:hostname'] || !options['s:port']) return;
const url = `${options['s:secure'] ? 'https' : 'http'}://${ const url = `${options['s:secure'] ? 'https' : 'http'}://${

View File

@ -18,7 +18,7 @@ const messageStyle: CSSProperties = {
textAlign: 'center', textAlign: 'center',
}; };
let rendered: boolean; let rendered: boolean | undefined;
let store: Store<StoreState, StoreAction> | undefined; let store: Store<StoreState, StoreAction> | undefined;
let bgConnection: chrome.runtime.Port; let bgConnection: chrome.runtime.Port;
let naTimeout: NodeJS.Timeout; let naTimeout: NodeJS.Timeout;

View File

@ -158,6 +158,8 @@ interface SplitMessageBase {
} }
interface SplitMessageStart extends SplitMessageBase { interface SplitMessageStart extends SplitMessageBase {
readonly instanceId: number;
readonly source: typeof pageSource;
readonly split: 'start'; readonly split: 'start';
} }
@ -174,7 +176,10 @@ interface SplitMessageEnd extends SplitMessageBase {
readonly split: 'end'; readonly split: 'end';
} }
type SplitMessage = SplitMessageStart | SplitMessageChunk | SplitMessageEnd; export type SplitMessage =
| SplitMessageStart
| SplitMessageChunk
| SplitMessageEnd;
function tryCatch<S, A extends Action<unknown>>( function tryCatch<S, A extends Action<unknown>>(
fn: ( fn: (

View File

@ -36,11 +36,7 @@ import {
getSerializeParameter, getSerializeParameter,
Serialize, Serialize,
} from '../../../app/api'; } from '../../../app/api';
import { import { LiftedAction, LiftedState } from '@redux-devtools/instrument';
LiftedAction,
LiftedState,
PerformAction,
} from '@redux-devtools/instrument';
import { import {
CustomAction, CustomAction,
DispatchAction, DispatchAction,
@ -63,7 +59,7 @@ function deprecateParam(oldParam: string, newParam: string) {
/* eslint-enable no-console */ /* eslint-enable no-console */
} }
interface SerializeWithImmutable extends Serialize { export interface SerializeWithImmutable extends Serialize {
readonly immutable?: typeof Immutable; readonly immutable?: typeof Immutable;
readonly refs?: (new (data: any) => unknown)[] | null; readonly refs?: (new (data: any) => unknown)[] | null;
} }
@ -81,12 +77,12 @@ export interface ConfigWithExpandedMaxAge {
| boolean | boolean
| ((key: string, value: unknown) => unknown) | ((key: string, value: unknown) => unknown)
| Serialize; | Serialize;
readonly statesFilter?: <S>(state: S, index: number) => S; readonly statesFilter?: <S>(state: S, index?: number) => S;
readonly actionsFilter?: <A extends Action<unknown>>( readonly actionsFilter?: <A extends Action<unknown>>(
action: A, action: A,
id: number id: number
) => A; ) => A;
readonly stateSanitizer?: <S>(state: S, index: number) => S; readonly stateSanitizer?: <S>(state: S, index?: number) => S;
readonly actionSanitizer?: <A extends Action<unknown>>( readonly actionSanitizer?: <A extends Action<unknown>>(
action: A, action: A,
id: number id: number
@ -117,6 +113,7 @@ export interface ConfigWithExpandedMaxAge {
name?: string; name?: string;
readonly autoPause?: boolean; readonly autoPause?: boolean;
readonly features?: Features; readonly features?: Features;
readonly type?: string;
} }
export interface Config extends ConfigWithExpandedMaxAge { export interface Config extends ConfigWithExpandedMaxAge {
@ -131,7 +128,7 @@ interface ReduxDevtoolsExtension {
): Store<S, A>; ): Store<S, A>;
(config?: Config): StoreEnhancer; (config?: Config): StoreEnhancer;
open: (position?: Position) => void; open: (position?: Position) => void;
notifyErrors: (onError: () => boolean) => void; notifyErrors: (onError?: () => boolean) => void;
disconnect: () => void; disconnect: () => void;
} }