/* * Copyright (C) 2021-2022 Kristóf Marussy * * This file is part of Sophie. * * Sophie is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * SPDX-License-Identifier: AGPL-3.0-only */ import { Action, action, MainToRendererIpcMessage, RendererToMainIpcMessage, sharedStore, SharedStoreListener, SophieRenderer, } from '@sophie/shared'; import { ipcRenderer } from 'electron'; import log from 'loglevel'; import type { IJsonPatch } from 'mobx-state-tree'; class SharedStoreConnector { private onSharedStoreChangeCalled = false; private listener: SharedStoreListener | null = null; constructor(private readonly allowReplaceListener: boolean) { ipcRenderer.on( MainToRendererIpcMessage.SharedStorePatch, (_event, patch) => { try { // `mobx-state-tree` will validate the patch, so we can safely cast here. this.listener?.onPatch(patch as IJsonPatch); } catch (err) { log.error('Shared store listener onPatch failed', err); this.listener = null; } }, ); } async onSharedStoreChange(listener: SharedStoreListener): Promise { if (this.onSharedStoreChangeCalled && !this.allowReplaceListener) { throw new Error('Shared store change listener was already set'); } this.onSharedStoreChangeCalled = true; let success = false; let snapshot: unknown | null = null; try { snapshot = await ipcRenderer.invoke( RendererToMainIpcMessage.GetSharedStoreSnapshot, ); success = true; } catch (err) { log.error('Failed to get initial shared store snapshot', err); } if (success) { if (sharedStore.is(snapshot)) { listener.onSnapshot(snapshot); this.listener = listener; return; } log.error('Got invalid initial shared store snapshot', snapshot); } throw new Error('Failed to connect to shared store'); } } function dispatchAction(actionToDispatch: Action): void { // Let the full zod parse error bubble up to the main world, // since all data it may contain was provided from the main world. const parsedAction = action.parse(actionToDispatch); try { ipcRenderer.send(RendererToMainIpcMessage.DispatchAction, parsedAction); } catch (err) { // Do not leak IPC failure details into the main world. const message = 'Failed to dispatch action'; log.error(message, actionToDispatch, err); throw new Error(message); } } export default function createSophieRenderer( allowReplaceListener: boolean, ): SophieRenderer { const connector = new SharedStoreConnector(allowReplaceListener); return { onSharedStoreChange: connector.onSharedStoreChange.bind(connector), dispatchAction, }; }