Skip to content
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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
# Changelog


## v0.2.1

[compare changes](https://github.com/BanShan-Alec/electron-mobx-state-tree/compare/v0.2.0...v0.2.1)

### 🚀 Enhancements

- 优化类型提示 ([0ad5699](https://github.com/BanShan-Alec/electron-mobx-state-tree/commit/0ad5699))
- 完成MSTStore的实现;修复渲染进程状态重复被applyPatch的问题 ([ea26854](https://github.com/BanShan-Alec/electron-mobx-state-tree/commit/ea26854))
- 完善销毁MSTStore逻辑 ([a1a98d3](https://github.com/BanShan-Alec/electron-mobx-state-tree/commit/a1a98d3))
- 优化变量命名 ([a6003f3](https://github.com/BanShan-Alec/electron-mobx-state-tree/commit/a6003f3))
- 更新demo,补充备注 ([6c3dbb0](https://github.com/BanShan-Alec/electron-mobx-state-tree/commit/6c3dbb0))

### 🏡 Chore

- **release:** V0.2.0 ([b71715c](https://github.com/BanShan-Alec/electron-mobx-state-tree/commit/b71715c))
- 更新demo ([af4c84f](https://github.com/BanShan-Alec/electron-mobx-state-tree/commit/af4c84f))

### ❤️ Contributors

- 半山Alec <[email protected]>

## v0.2.0

[compare changes](https://github.com/BanShan-Alec/electron-mobx-state-tree/compare/v0.1.3...v0.2.0)
Expand Down
209 changes: 119 additions & 90 deletions lib/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ipcMain, type WebContents } from 'electron';
import { ipcMain, IpcMainEvent, type WebContents } from 'electron';
import { IPC_CHANNEL_NAME, isRenderer } from '.';
import { applyAction, getSnapshot, IAnyModelType, IAnyStateTreeNode, IModelType, onPatch } from 'mobx-state-tree';

Expand All @@ -9,149 +9,176 @@ export interface InitStoreOptionsType {
createStoreBefore?: boolean;
}

class MSTStore {
store: IAnyModelType;
observers: WebContents[] = [];
storeInstance: IAnyStateTreeNode = null;
latestPatcher?: WebContents;
destroy: () => void = () => void 0;

constructor(store: IAnyModelType) {
this.store = store;
}

create<T extends IModelType<any, any>>(
store: T,
snapshot?: Parameters<T['create']>[0],
options?: {
observers?: WebContents[];
}
): T['Type'] {
const { observers } = options || {};
this.storeInstance = store.create(snapshot);
// 保存观察者
this.observers = observers || [];
// 注册事件
ipcMain.on(`${IPC_CHANNEL_NAME}:callAction-${store.name}`, this.handleCallAction);
const offPatchListener = onPatch(this.storeInstance, this.handlePatch);
// 销毁事件
this.destroy = () => {
offPatchListener();
ipcMain.off(`${IPC_CHANNEL_NAME}:callAction-${this.store.name}`, this.handleCallAction);
this.observers = [];
this.storeInstance = null;
this.latestPatcher = undefined;
};

return this.storeInstance;
}

// 事件处理
private handleCallAction = (event: IpcMainEvent, data: any) => {
// TODO DEBUG
// console.log('callAction', data.actionObj, event.sender.id);

if (!data.actionObj) return;
this.latestPatcher = event.sender;
// applyAction后会马上触发onPatch是同步的
applyAction(this.storeInstance, data.actionObj);
};
private handlePatch = (patch: any) => {
// TODO DEBUG
// console.log('patch', patch, this.latestPatcher.id);

this.observers.forEach((observer) => {
if (observer.isDestroyed()) return;
// 避免重复发送Patch
if (observer === this.latestPatcher) return;
observer.send(`${IPC_CHANNEL_NAME}:patch-${this.store.name}`, { patch });
});
// 去除已经销毁的 observer
this.observers = this.observers.filter((observer) => !observer.isDestroyed());
this.latestPatcher = undefined;
};
}

class StoreManager {
static initialized = false;
private static storeMap: Map<string, IAnyModelType>;
private static storeObserverMap: Map<string, WebContents[]>;
private static storeInstanceMap: Map<string, IAnyStateTreeNode>;
private static storeDestroyMap: Map<string, () => void>;
private static mstStoreMap: Map<string, MSTStore>;

static init(options: InitStoreOptionsType[]) {
this.storeMap = new Map();
this.storeObserverMap = new Map();
this.storeInstanceMap = new Map();
this.storeDestroyMap = new Map();
this.initialized = true;
this.mstStoreMap = new Map();

options.forEach(({ store, createStoreBefore = false, snapshot, observers }) => {
// 等待注册
this.storeMap.set(store.name, store);
this.mstStoreMap.set(store.name, new MSTStore(store));
// 预先创建,不等待注册(用于主进程也消费Store实例的场景)
if (createStoreBefore) {
this.createStore(store, snapshot, { observers });
}
});

ipcMain.handle(`${IPC_CHANNEL_NAME}:register`, this.handleRegister);
ipcMain.on(`${IPC_CHANNEL_NAME}:destroy`, this.handleDestroy);
}

static destroy() {
ipcMain.off(`${IPC_CHANNEL_NAME}:register`, this.handleRegister);
ipcMain.off(`${IPC_CHANNEL_NAME}:destroy`, this.handleDestroy);
this.mstStoreMap.forEach((mstStore) => {
mstStore.destroy();
});
this.mstStoreMap.clear();
}

static getInstanceByName(storeName: string) {
return this.storeInstanceMap.get(storeName);
return this.mstStoreMap.get(storeName)?.storeInstance;
}

static getStoreByName(storeName: string) {
return this.storeMap.get(storeName);
return this.mstStoreMap.get(storeName)?.store;
}

static destroyStoreByName(storeName: string) {
const destroy = this.storeDestroyMap.get(storeName);
if (typeof destroy === 'function') destroy();
return this.mstStoreMap.get(storeName)?.destroy();
}

static addObserver(storeName: string, observer: WebContents) {
const observers = this.storeObserverMap.get(storeName);
if (typeof observers === 'undefined') return;

this.storeObserverMap.set(storeName, [...observers, observer]);
static addObservers(storeName: string, observer: WebContents[]) {
const mstStore = this.mstStoreMap.get(storeName);
if (!mstStore) return;
mstStore.observers.push(...observer);
}

static createStore<T extends IModelType<any, any>>(
store: T,
snapshot?: Parameters<T['create']>[0],
options?: {
observers?: WebContents[];
}
) {
static createStore: MSTStore['create'] = (store, snapshot, options) => {
try {
// 检查
if (isRenderer()) {
throw new Error('This module should be used in main process!');
}
if (!this.initialized) {
throw new Error('Please call initMST() before createStore()!');
const mstStore = this.mstStoreMap.get(store.name);

if (!mstStore) {
throw new Error(`Please add Store "${store.name}" to initMST() before createStore()!`);
}

if (this.storeInstanceMap.has(store.name)) {
throw new Error('Store name duplication!');
if (mstStore.storeInstance) {
throw new Error(`Store name "${store.name}" duplication!`);
}

// 实例化
const storeInstance = store.create(snapshot);
this.storeObserverMap.set(store.name, options?.observers || []);
this.storeInstanceMap.set(store.name, storeInstance);

// 事件处理
const handleCallAction = (event: any, data: any) => {
// TODO DEBUG
// console.log('callAction', data.actionObj);

if (!data.actionObj) return;
applyAction(storeInstance, data.actionObj);
};
const handlePatch = (patch: any) => {
// TODO DEBUG
// console.log('patch', patch);

const observers = this.storeObserverMap.get(store.name);
if (typeof observers === 'undefined') return;

observers.forEach((observer) => {
if (observer.isDestroyed()) return;
observer.send(`${IPC_CHANNEL_NAME}:patch-${store.name}`, { patch });
});
// 去除已经销毁的 observer
this.storeObserverMap.set(
store.name,
observers.filter((observer) => !observer.isDestroyed())
);
};
const handleDestroy = () => {
ipcMain.off(`${IPC_CHANNEL_NAME}:callAction-${store.name}`, handleCallAction);
this.storeObserverMap.delete(store.name);
this.storeInstanceMap.delete(store.name);
};

// 注册事件
ipcMain.on(`${IPC_CHANNEL_NAME}:callAction-${store.name}`, handleCallAction);
onPatch(storeInstance, handlePatch);
this.storeDestroyMap.set(store.name, handleDestroy);
const storeInstance = mstStore.create(store, snapshot, options);
// 返回实例
return storeInstance;
} catch (error: any) {
console.error(`[createStore error] ${error?.message}`);
error.message = `[Electron-MST error] ${error.message}`;
throw error;
}
}
}

export const initMST = (options: InitStoreOptionsType[]) => {
if (isRenderer()) throw new Error('This module should be used in main process!');
};

StoreManager.init(options);

ipcMain.handle(`${IPC_CHANNEL_NAME}:register`, async (event, data) => {
private static handleRegister = async (event: any, data: any) => {
if (!data.storeName) throw new Error('Store name is required!');
if (!data.snapshot) throw new Error('Store Snapshot is required!');

const store = StoreManager.getStoreByName(data.storeName);
const storeInstance = StoreManager.getInstanceByName(data.storeName);
if (!store) throw new Error('Store not found! Please add Store to initMST() first!');
const store = this.getStoreByName(data.storeName);
const storeInstance = this.getInstanceByName(data.storeName);
if (!store) throw new Error(`Store "${data.storeName}" not found! Please add Store to initMST() first!`);

// 已经存在的 store实例 直接返回快照
if (store && storeInstance) {
StoreManager.addObserver(data.storeName, event.sender);
this.addObservers(data.storeName, [event.sender]);
const snapshot = getSnapshot(storeInstance);
return snapshot;
}
// 不存在的则创建
StoreManager.createStore(store, data.snapshot, {
this.createStore(store, data.snapshot, {
observers: [event.sender],
});
return null;
});
};

ipcMain.on(`${IPC_CHANNEL_NAME}:destroy`, (event, data) => {
private static handleDestroy = (event: any, data: any) => {
if (!data.storeName) throw new Error('Store name is required!');
StoreManager.destroyStoreByName(data.storeName);
});
this.destroyStoreByName(data.storeName);
};
}

export const initMST = (options: InitStoreOptionsType[]) => {
if (isRenderer()) throw new Error('This module should be used in main process!');
StoreManager.init(options);
};

export const destroyMST = () => {
if (isRenderer()) throw new Error('This module should be used in main process!');
StoreManager.destroy();
};

export const destroyStore = (store: IAnyModelType) => {
Expand All @@ -163,7 +190,9 @@ export const destroyStoreByName = (storeName: string) => {
};

export const getStoreInstance = <T extends IModelType<any, any>>(store: T) => {
return StoreManager.getInstanceByName(store.name) as T['Type'];
const instance = StoreManager.getInstanceByName(store.name);
if (!instance) throw new Error(`Store "${store.name}" not found!`);
return instance as T['Type'];
};

export const getStoreInstanceByName = (storeName: string) => {
Expand Down
3 changes: 3 additions & 0 deletions lib/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const getStoreInstanceHandler = (storeName: string): ProxyHandler<any> => ({
return (...args: any) => {
try {
const res = value.apply(this, args);
// TODO FEATURE: 在同一事件循环中,合并多个相同path的action
window.ElectronMST.callAction(storeName, {
name: key as string,
path: getPath(target),
Expand All @@ -45,6 +46,8 @@ const initStore = async (storeName: string, storeInstance: any) => {
if (snapshot) applySnapshot(storeInstance, snapshot);

const offPatchListener = window.ElectronMST.onPatchChange(storeName, (patch: any) => {
// TODO DEBUG
// console.log(`[onPatchChange] ${storeName}`, patch);
applyPatch(storeInstance, patch);
});
window.addEventListener('beforeunload', () => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "electron-mst",
"version": "0.2.0",
"version": "0.2.1",
"description": "Synchronization status across multiple electron processes, powered by mobx-state-tree",
"author": "BanShan<[email protected]>",
"license": "MIT",
Expand Down
19 changes: 17 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ let App = (props: IProps) => {
//变量声明、解构
const {} = props;
const { count, add, isEven, user } = home$;
const { age, updateAge } = user$;
const { age, updateAge, updateName } = user$;
//组件状态

//网络IO
Expand All @@ -31,6 +31,9 @@ let App = (props: IProps) => {
useEffect(() => {
postMessage({ payload: 'removeLoading' }, '*');
}, []);
useEffect(() => {
console.log('count:', count);
}, [count]);

//组件渲染
return (
Expand All @@ -43,7 +46,18 @@ let App = (props: IProps) => {
</div>
<h1>Electron + Mobx State Tree</h1>
<div className="card">
<button className={isEven ? 'text-green-400' : 'text-white'} onClick={() => add()}>
<button
className={isEven ? 'text-green-400' : 'text-white'}
onClick={() => {
// React will merge the twice state update
// but `electron-mst` will not merge the action
add();
add();
// setTimeout(() => {
// add();
// });
}}
>
count is {count}
</button>
<p>
Expand All @@ -68,6 +82,7 @@ let App = (props: IProps) => {
<button
onClick={() => {
updateAge(age + 2);
updateName('Name' + Math.random().toFixed(2));
}}
>
Update Another User
Expand Down
Loading