diff --git a/src/main/components/store/model-store.tsx b/src/main/components/store/model-store.tsx index 08145397..f56de96f 100644 --- a/src/main/components/store/model-store.tsx +++ b/src/main/components/store/model-store.tsx @@ -50,6 +50,7 @@ export const createReduxStore = ( patcher && createPatcherReducer(patcher, { transform: (model) => ModelState.fromModel(model) as ModelState, + transformInverse: (state) => ModelState.toModel(state), merge, }); diff --git a/src/main/services/patcher/patcher-reducer.ts b/src/main/services/patcher/patcher-reducer.ts index e218721c..9753d924 100644 --- a/src/main/services/patcher/patcher-reducer.ts +++ b/src/main/services/patcher/patcher-reducer.ts @@ -12,6 +12,14 @@ export type PatcherReducerOptions = { */ transform?: (state: T) => U; + /** + * Transforms the state before applying the patch. This is useful + * when the internal store schema differs from the schema exposed to the outside. + * @param state The state in the internal schema. + * @returns The state in the external schema. + */ + transformInverse?: (state: U) => T; + /** * Merges the old state with the new state. This is useful when naive strategies * like `Object.assign` would trigger unwanted side-effects and more context-aware merging @@ -25,6 +33,7 @@ export type PatcherReducerOptions = { const _DefaultOptions = { transform: (state: any) => state, + transformInverse: (state: any) => state, merge: (oldState: any, newState: any) => ({ ...oldState, ...newState }), }; @@ -39,15 +48,16 @@ export function createPatcherReducer( options: PatcherReducerOptions = _DefaultOptions, ): Reducer { const transform = options.transform || _DefaultOptions.transform; + const transformInverse = options.transformInverse || _DefaultOptions.transformInverse; const merge = options.merge || _DefaultOptions.merge; - return (state = {} as U, action) => { + return (state, action) => { const { type, payload } = action; if (type === PatcherActionTypes.PATCH) { - const res = patcher.patch(payload); + const res = patcher.patch(payload, transformInverse(state as U)); if (res.patched) { - return merge(state, transform(res.result)); + return merge((state ?? {}) as U, transform(res.result)); } } diff --git a/src/main/services/patcher/patcher.ts b/src/main/services/patcher/patcher.ts index 1761829d..2b554e59 100644 --- a/src/main/services/patcher/patcher.ts +++ b/src/main/services/patcher/patcher.ts @@ -119,10 +119,11 @@ export class Patcher { * @param patch The patch to apply. * @returns The whether the state should change, and the new state of the object. */ - patch(patch: Patch | SignedPatch): { patched: boolean; result: T } { + patch(patch: Patch | SignedPatch, state?: T): { patched: boolean; result: T } { this.validate(); const verified = this.verifier.verified(patch); + this._snapshot = state ?? this._snapshot; if (verified && verified.length > 0) { this._snapshot = verified.reduce((state, p, index) => { diff --git a/src/tests/unit/services/patcher/patcher-reducer-test.ts b/src/tests/unit/services/patcher/patcher-reducer-test.ts index f941f0e6..1d85ed80 100644 --- a/src/tests/unit/services/patcher/patcher-reducer-test.ts +++ b/src/tests/unit/services/patcher/patcher-reducer-test.ts @@ -16,16 +16,27 @@ describe('test patcher reducer.', () => { test('calls given transform function for applying the patch.', () => { const cb = jest.fn((x) => ({ y: x.x })); + const inv = (x: any) => ({ x: x.y }); const patcher = new Patcher(); patcher.initialize({}); - const reducer = createPatcherReducer(patcher, { transform: cb }); + const reducer = createPatcherReducer(patcher, { transform: cb, transformInverse: inv }); const nextState = reducer({ y: 41 }, PatcherRepository.patch([{ op: 'add', path: '/x', value: 42 }])); expect(nextState).toEqual({ y: 42 }); }); + test('passes the state to the patcher.', () => { + const patcher = new Patcher(); + patcher.initialize({}); + + const reducer = createPatcherReducer(patcher); + const nextState = reducer({ y: 41 }, PatcherRepository.patch([{ op: 'add', path: '/x', value: 42 }])); + + expect(nextState).toEqual({ x: 42, y: 41 }); + }); + test('calls given merge function for applying the patch.', () => { const cb = jest.fn((x, y) => ({ ...x, ...y })); diff --git a/src/tests/unit/services/patcher/patcher-test.ts b/src/tests/unit/services/patcher/patcher-test.ts index 9b0d6943..095209ff 100644 --- a/src/tests/unit/services/patcher/patcher-test.ts +++ b/src/tests/unit/services/patcher/patcher-test.ts @@ -170,4 +170,11 @@ describe('patcher class.', () => { const res3 = patcher.patch([{ op: 'replace', path: '/x', value: 46, hash: '123' }]); expect(res3.patched).toBe(false); }); + + test('can update snapshot on patch.', () => { + const patcher = new Patcher(); + patcher.initialize({ x: 42, y: 43 }); + const res = patcher.patch([{ op: 'replace', path: '/x', value: 44 }], { y: 45 }); + expect(res.result).toEqual({ x: 44, y: 45 }); + }); });