Skip to content

Create abstraction for communication #19

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
66 changes: 37 additions & 29 deletions src/devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ import {
LocalFilter,
State,
} from "@redux-devtools/utils";
import {
DevToolsPluginClient,
getDevToolsPluginClientAsync,
} from "expo/devtools";
import { stringify, parse } from "jsan";
import {
Action,
Expand All @@ -31,6 +27,7 @@ import {
} from "redux";

import configureStore from "./configureStore";
import { ProxyClient, ProxyClientFactory } from "./types";

function async(fn: () => unknown) {
setTimeout(fn, 0);
Expand Down Expand Up @@ -160,12 +157,12 @@ type Message<S, A extends Action<string>> =
| ActionMessage
| DispatchMessage<S, A>;

class DevToolsEnhancer<S, A extends Action<string>> {
export class DevToolsEnhancer<S, A extends Action<string>> {
// eslint-disable-next-line @typescript-eslint/ban-types
store!: EnhancedStore<S, A, {}>;
filters: LocalFilter | undefined;
instanceId?: string;
devToolsPluginClient?: DevToolsPluginClient;
proxyClient?: ProxyClient;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main idea is that we abstract communication logic. As for now, the getDevToolsPluginClientAsync and DevToolsPluginClient are hardcoded and tidly coupled. I wanted to allow to pass any method that returns a subset of DevToolsPluginClient that is used by redux-devtools-expo-dev-plugin. I assume that if the library wants to use more features of DevToolsPluginClient, then ProxyClient should be updated.

sendTo?: string;
instanceName: string | undefined;
appInstanceId!: string;
Expand All @@ -186,6 +183,13 @@ class DevToolsEnhancer<S, A extends Action<string>> {
lastAction?: unknown;
paused?: boolean;
locked?: boolean;
createProxyClient: ProxyClientFactory;

constructor(
proxyClientFactory: ProxyClientFactory,
) {
this.createProxyClient = proxyClientFactory;
}

getInstanceId() {
if (!this.instanceId) {
Expand Down Expand Up @@ -276,7 +280,7 @@ class DevToolsEnhancer<S, A extends Action<string>> {
} else if (action) {
message.action = action as ActionCreatorObject[];
}
this.devToolsPluginClient?.sendMessage("log", message);
this.proxyClient?.sendMessage("log", message);
}

dispatchRemotely(
Expand Down Expand Up @@ -374,21 +378,19 @@ class DevToolsEnhancer<S, A extends Action<string>> {
stop = async () => {
this.started = false;
this.isMonitored = false;
if (!this.devToolsPluginClient) return;
await this.devToolsPluginClient.closeAsync();
this.devToolsPluginClient = undefined;
if (!this.proxyClient) return;
await this.proxyClient.closeAsync();
this.proxyClient = undefined;
};

start = () => {
if (this.started) return;

(async () => {
try {
this.devToolsPluginClient = await getDevToolsPluginClientAsync(
"redux-devtools-expo-dev-plugin",
);
this.proxyClient = await this.createProxyClient();

this.devToolsPluginClient.addMessageListener(
this.proxyClient.addMessageListener(
"respond",
(data: Message<S, A>) => {
this.handleMessages(data);
Expand Down Expand Up @@ -505,16 +507,11 @@ class DevToolsEnhancer<S, A extends Action<string>> {
};
}

export default <S, A extends Action<string>>(
options?: Options<S, A>,
): StoreEnhancer => new DevToolsEnhancer<S, A>().enhance(options);

const compose =
(devToolsEnhancer: DevToolsEnhancer<unknown, Action<string>>) =>
(options: Options<unknown, Action<string>>) =>
(...funcs: StoreEnhancer[]) =>
(...args: unknown[]) => {
const devToolsEnhancer = new DevToolsEnhancer();

function preEnhancer(createStore: StoreEnhancerStoreCreator) {
return <S, A extends Action<string>>(
reducer: Reducer<S, A>,
Expand All @@ -539,14 +536,25 @@ const compose =
);
};

export function composeWithDevTools(
...funcs: [Options<unknown, Action<string>>] | StoreEnhancer[]
export function createComposeWithDevTools(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed composeWithDevTools to createComposeWithDevTools just to allow passing another communication factory. It should create what used to be composeWithDevTools. I need to have composeWithDevTools, with our communication client injected, to provide zero config setup for radon ide: https://github.com/software-mansion/radon-ide/blob/497669db377caf1ba8adca62a1ffafa88a1441d1/packages/vscode-extension/lib/plugins/redux-devtools.js#L75

proxyClientFactory: ProxyClientFactory,
) {
if (funcs.length === 0) {
return new DevToolsEnhancer().enhance();
}
if (funcs.length === 1 && typeof funcs[0] === "object") {
return compose(funcs[0]);
}
return compose({})(...(funcs as StoreEnhancer[]));
const devtoolsEnhancer = new DevToolsEnhancer<unknown, Action<string>>(
proxyClientFactory,
);
return function (
...funcs: [Options<unknown, Action<string>>] | StoreEnhancer[]
) {
if (funcs.length === 0) {
return devtoolsEnhancer.enhance();
}
if (funcs.length === 1 && typeof funcs[0] === "object") {
return compose(devtoolsEnhancer)(funcs[0]);
}
return compose(devtoolsEnhancer)({})(...(funcs as StoreEnhancer[]));
};
}

export const createDevToolsEnhancer = (createProxyClient: ProxyClientFactory) => <S, A extends Action<string>>(
options?: Options<S, A>,
): StoreEnhancer => new DevToolsEnhancer<S, A>(createProxyClient).enhance(options);
16 changes: 12 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
/// <reference types="node" />
import { compose } from "redux";

export let composeWithDevTools: typeof import("./devtools").composeWithDevTools;
let devtoolsEnhancer: typeof import("./devtools").default;
export let createComposeWithDevTools: typeof import("./devtools").createComposeWithDevTools;
export let composeWithDevTools: ReturnType<typeof createComposeWithDevTools>;

let createDevToolsEnhancer: typeof import("./devtools").createDevToolsEnhancer;
let devtoolsEnhancer: ReturnType<typeof createDevToolsEnhancer>;

if (process.env.NODE_ENV !== "production") {
devtoolsEnhancer = require("./devtools").default;
composeWithDevTools = require("./devtools").composeWithDevTools;
const getDevToolsPluginClientAsync = require('expo/devtools').getDevToolsPluginClientAsync;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the import of getDevToolsPluginClientAsync to the index so it's easier to replace the communication mechanism, and omit it during bundling: https://github.com/software-mansion-labs/redux-devtools-expo-dev-plugin/pull/2/files#diff-a2a171449d862fe29692ce031981047d7ab755ae7f84c707aef80701b3ea0c80

const createDevToolsEnhancer = require("./devtools").createDevToolsEnhancer;

devtoolsEnhancer = createDevToolsEnhancer(() => getDevToolsPluginClientAsync("redux-devtools-expo-dev-plugin"));
createComposeWithDevTools = require("./devtools").createComposeWithDevTools;
composeWithDevTools = createComposeWithDevTools(() => getDevToolsPluginClientAsync("redux-devtools-expo-dev-plugin"));
} else {
devtoolsEnhancer = () => (next) => next;
createComposeWithDevTools = () => compose;
composeWithDevTools = compose;
}

Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface ProxyClient {
sendMessage: (type: string, data?: any) => void;
addMessageListener: (method: string, listener: (params: any) => void) => void;
closeAsync: () => Promise<void>;
}

export type ProxyClientFactory = () => Promise<ProxyClient>;
52 changes: 26 additions & 26 deletions webui/src/middlewares/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,21 @@ import {
UPDATE_STATE,
UpdateReportsRequest,
} from "@redux-devtools/app-core";
import {
DevToolsPluginClient,
getDevToolsPluginClientAsync,
} from "expo/devtools";
import { getDevToolsPluginClientAsync } from "expo/devtools";
import { stringify } from "jsan";
import { Dispatch, MiddlewareAPI } from "redux";

import { EmitAction, StoreAction } from "../actions";
import * as actions from "../constants/socketActionTypes";
import { StoreState } from "../reducers";
import { ProxyClient, ProxyClientFactory } from "./types";
import { nonReduxDispatch } from "../utils/monitorActions";

let devToolsPluginClient: DevToolsPluginClient | undefined;
let proxyClient: ProxyClient | undefined;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, for the redux middleware part, I added an abstraction. Just, to allow using a different method that has the same interface as getDevToolsPluginClientAsync

let store: MiddlewareAPI<Dispatch<StoreAction>, StoreState>;

function emit({ message: type, instanceId, action, state }: EmitAction) {
devToolsPluginClient?.sendMessage("respond", {
proxyClient?.sendMessage("respond", {
type,
action,
state,
Expand Down Expand Up @@ -117,7 +115,7 @@ function monitoring(request: MonitoringRequest) {
instanceId === instances.selected &&
(request.type === "ACTION" || request.type === "STATE")
) {
devToolsPluginClient?.sendMessage("respond", {
proxyClient?.sendMessage("respond", {
type: "SYNC",
state: stringify(instances.states[instanceId]),
id: request.id,
Expand All @@ -126,17 +124,15 @@ function monitoring(request: MonitoringRequest) {
}
}

async function connect() {
async function connect(createProxyClient: ProxyClientFactory) {
if (process.env.NODE_ENV === "test") return;
try {
devToolsPluginClient = await getDevToolsPluginClientAsync(
"redux-devtools-expo-dev-plugin",
);
proxyClient = await createProxyClient();

const watcher = (request: UpdateReportsRequest) => {
store.dispatch({ type: UPDATE_REPORTS, request });
};
devToolsPluginClient.addMessageListener("log", (data) => {
proxyClient.addMessageListener("log", (data) => {
monitoring(data as MonitoringRequest);
watcher(data as UpdateReportsRequest);
});
Expand All @@ -151,20 +147,24 @@ async function connect() {
}
}

export function api(inStore: MiddlewareAPI<Dispatch<StoreAction>, StoreState>) {
store = inStore;
connect();
export function api(
createProxyClient: ProxyClientFactory = () => getDevToolsPluginClientAsync("redux-devtools-expo-dev-plugin"),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default it is still using getDevToolsPluginClientAsync. But on our branch I provide custom communication mechanism: https://github.com/software-mansion-labs/redux-devtools-expo-dev-plugin/pull/2/files#diff-cf56f00851c2aa7b536d9a15a35751d6ce4511a7b2ba5f353e059b6d0ba4dad4

) {
return function (inStore: MiddlewareAPI<Dispatch<StoreAction>, StoreState>) {
store = inStore;
connect(createProxyClient);

return (next: Dispatch<StoreAction>) => (action: StoreAction) => {
const result = next(action);
switch (action.type) {
case actions.EMIT:
if (devToolsPluginClient) emit(action);
break;
case LIFTED_ACTION:
dispatchRemoteAction(action);
break;
}
return result;
return (next: Dispatch<StoreAction>) => (action: StoreAction) => {
const result = next(action);
switch (action.type) {
case actions.EMIT:
if (proxyClient) emit(action);
break;
case LIFTED_ACTION:
dispatchRemoteAction(action);
break;
}
return result;
};
};
}
6 changes: 6 additions & 0 deletions webui/src/middlewares/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface ProxyClient {
sendMessage: (type: string, data?: any) => void;
addMessageListener: (method: string, listener: (params: any) => void) => void;
}

export type ProxyClientFactory = () => Promise<ProxyClient>;
2 changes: 1 addition & 1 deletion webui/src/store/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function configureStore() {
const store = createStore(
persistedReducer,
/// @ts-expect-error
composeEnhancers(applyMiddleware(...middlewares, api)),
composeEnhancers(applyMiddleware(...middlewares, api())),
);
const persistor = persistStore(store);
return { store, persistor };
Expand Down