Skip to content

Commit

Permalink
v2.0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
aslilac committed May 23, 2020
1 parent c00561d commit ad851d1
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 154 deletions.
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"description": "Keep your store in sync across processes",
"description": "Make sure all your stores are on the same page",
"author": "McKayla Washburn",
"repository": "https://github.com/partheseas/electron-redux",
"bugs": "https://github.com/partheseas/electron-redux/issues",
Expand All @@ -19,15 +19,13 @@
"node": ">= 12"
},
"dependencies": {
"debug": "^4.0.0",
"flux-standard-action": "^2.0.0"
},
"peerDependencies": {
"electron": ">=8.0.0",
"redux": ">=4.0.0"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"electron": "^8.2.5",
"parcel": "^2.0.0-nightly.256",
"prettier": "^2.0.5",
Expand All @@ -36,6 +34,7 @@
},
"scripts": {
"build": "parcel build src/index.ts",
"fmt": "prettier --ignore-path .gitignore --write ."
"fmt": "prettier --ignore-path .gitignore --write .",
"test": "electron tests/main.js"
}
}
47 changes: 31 additions & 16 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import debug from "debug";
import { FluxStandardAction, isFSA } from "flux-standard-action";

type ActionMeta = {
scope?: "local" | string;
};

const log = debug("mckayla.electron-redux.validation");

const blacklist = [/^@@/, /^redux-form/];

// We use this variable to store a stack trace of where the middleware
// is first initialized, to assist in debugging if someone accidentally enables
// it twice. This can easily be caused by importing files that are shared between
// the main and renderer processes.
let previouslyInitialzed: Error;

export const preventDoubleInitialization = () => {
if (previouslyInitialzed) {
console.error(
new Error("electron-redux has already been attached to a store"),
);
console.error(previouslyInitialzed);
}

// We are intentionally not actually throwing the error here, we just
// want to capture the call stack for debugging.
previouslyInitialzed = new Error("Previously attached to a store at");
};

/**
* stopForwarding allows you to give it an action, and it will return an
* equivalent action that will only play in the current process
*/
export const stopForwarding = (
action: FluxStandardAction<string, unknown, ActionMeta>,
) => ({
Expand All @@ -19,20 +39,15 @@ export const stopForwarding = (
},
});

/**
* validateAction ensures that the action meets the right format and isn't scoped locally
*/
export const validateAction = (
action: unknown,
): action is FluxStandardAction<string, unknown, ActionMeta> => {
isFSA;
if (!isFSA<string, unknown, ActionMeta>(action)) {
log("Only flux-standard-actions will be forwarded", action);
return false;
}

if (
action.meta?.scope === "local" ||
blacklist.some((rule) => rule.test(action.type))
)
return false;

return true;
return (
isFSA<string, unknown, ActionMeta>(action) &&
action.meta?.scope !== "local" &&
blacklist.every((rule) => !rule.test(action.type))
);
};
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from "./helpers";
export { stopForwarding } from "./helpers";
export * from "./middleware/syncMain";
export * from "./middleware/syncRenderer";
41 changes: 23 additions & 18 deletions src/middleware/syncMain.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import debug from "debug";
import { ipcMain, webContents } from "electron";
import {
Action,
Expand All @@ -8,11 +7,11 @@ import {
StoreEnhancer,
} from "redux";

const log = debug("mckayla.electron-redux.sync");

import { stopForwarding, validateAction } from "../helpers";

let previouslyInitialzed: Error;
import {
preventDoubleInitialization,
stopForwarding,
validateAction,
} from "../helpers";

const freeze = (_: string, value: unknown): unknown => {
if (value instanceof Map) {
Expand All @@ -31,37 +30,43 @@ const freeze = (_: string, value: unknown): unknown => {

const middleware: Middleware = (store) => {
ipcMain.handle("mckayla.electron-redux.FETCH_STATE", async () => {
// Stringify the current state, and freeze it to preserve certain types
// that you might want to use in your state, but aren't JSON serializable
// by default.
return JSON.stringify(store.getState(), freeze);
});

ipcMain.on("mckayla.electron-redux.ACTION", (event, action: Action) => {
// We received an action from a renderer
// Play it locally (in main)
store.dispatch(stopForwarding(action));
});

// We are intentionally not actually throwing the error here, we just
// want to capture the call stack for debugging.
previouslyInitialzed = new Error("Previously attached to a store at");
// Forward it to all of the other renderers
webContents.getAllWebContents().forEach((contents) => {
// Ignore the renderer that sent the action
if (contents.id !== event.sender.id) {
contents.send(
"mckayla.electron-redux.ACTION",
stopForwarding(action),
);
}
});
});

return (next) => (action) => {
if (validateAction(action)) {
log("forwarding following action to renderers");
webContents.getAllWebContents().forEach((contents) => {
console.log("contents.id", contents.id);
contents.send("mckayla.electron-redux.ACTION", action);
});
}

log("action:", action);
return next(action);
};
};

export const syncMain: StoreEnhancer = (createStore: StoreCreator) => {
if (previouslyInitialzed) {
console.error(
new Error("electron-redux has already been attached to a store"),
);
console.error(previouslyInitialzed);
}
preventDoubleInitialization();

return (reducer, state) => {
return createStore(reducer, state, applyMiddleware(middleware));
Expand Down
41 changes: 22 additions & 19 deletions src/middleware/syncRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import debug from "debug";
import { ipcRenderer } from "electron";
import {
Action,
Expand All @@ -8,9 +7,12 @@ import {
StoreCreator,
StoreEnhancer,
} from "redux";
import { stopForwarding, validateAction } from "../helpers";

const log = debug("mckayla.electron-redux.sync");
import {
preventDoubleInitialization,
stopForwarding,
validateAction,
} from "../helpers";

const hydrate = (_: string, value: any) => {
if (value?.__hydrate_type === "__hydrate_map") {
Expand All @@ -24,7 +26,7 @@ const hydrate = (_: string, value: any) => {
return value;
};

export async function getRendererState(callback: (state) => void) {
export async function getRendererState(callback: (state: unknown) => void) {
const state = await ipcRenderer.invoke(
"mckayla.electron-redux.FETCH_STATE",
);
Expand All @@ -36,19 +38,24 @@ export async function getRendererState(callback: (state) => void) {
);
}

// I just don't like the ().then() syntax
// TODO: Copy and paste shrug emoji
return callback(JSON.parse(state, hydrate));
// We do some fancy hydration on certain types like Map and Set.
// See also `freeze` in syncMain
callback(JSON.parse(state, hydrate));
}

/**
* This next bit is all just for being able to fill the store with the correct
* state asynchronously, because blocking the thread feels bad for potentially
* large stores.
*/
type InternalAction = ReturnType<typeof replaceState>;

const replaceState = <S>(state: S) => ({
type: "mckayla.electron-redux.REPLACE_STATE" as const,
payload: state,
meta: { scope: "local" },
});

type InternalAction = ReturnType<typeof replaceState>;

const wrapReducer = (reducer: Reducer) => <S, A extends Action>(
state: S,
action: InternalAction | A,
Expand All @@ -68,17 +75,16 @@ const middleware: Middleware = (store) => {

return (next) => (action) => {
if (validateAction(action)) {
// TODO: We need a way to send actions from one renderer to another
log("forwarding following action to main");
ipcRenderer.send("mckayla.electron-redux.ACTION", action);
}

log("action:", action);
return next(action);
};
};

export const syncRenderer: StoreEnhancer = (createStore: StoreCreator) => {
preventDoubleInitialization();

return (reducer, state) => {
const store = createStore(
wrapReducer(reducer),
Expand All @@ -89,19 +95,16 @@ export const syncRenderer: StoreEnhancer = (createStore: StoreCreator) => {
// This is the reason we need to be an enhancer, rather than a middleware.
// We use this (along with the wrapReducer function above) to dispatch an
// action that initializes the store without needing to fetch it synchronously.

// Also relevant, this is internally implemented using promises. It would be really
// cool to use async/await syntax but we need to return a Store, not a Promise.
getRendererState((state) => {
store.dispatch(replaceState(state));
});

// TypeScript is fucking dumb. If you return the call to createStore
// XXX: TypeScript is dumb. If you return the call to createStore
// immediately it's fine, but even assigning it to a constant and returning
// will make it freak out.
// will make it freak out. We fix this with the line below the return.
return store;
// Even though this line is unreachable, it fixes the type signature????
// What the actual fuck TypeScript?

// XXX: Even though this is unreachable, it fixes the type signature????
return (store as unknown) as any;
};
};
5 changes: 2 additions & 3 deletions tests/main.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
const log = require("debug")("mckayla.electron-redux.test");
const { app, BrowserWindow } = require("electron");
const path = require("path");
const url = require("url");

const { increment, store } = require("./store/main");

store.subscribe(() => {
log(store.getState());
console.log(store.getState());
});

setInterval(() => {
store.dispatch(increment());
}, 1000);
}, 10000);

const views = [];

Expand Down
3 changes: 1 addition & 2 deletions tests/renderer.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
const log = require("debug")("mckayla.electron-redux.test");
const { decrement, increment, store } = require("./store/renderer");

store.subscribe(() => {
const state = store.getState();
log(state);
console.log(state);
d.innerHTML = state.count;
});

Expand Down
Loading

0 comments on commit ad851d1

Please sign in to comment.