Skip to content

Commit

Permalink
Bugfix/colliding patches (#344)
Browse files Browse the repository at this point in the history
  • Loading branch information
loreanvictor authored Feb 25, 2024
1 parent 6f5a797 commit cae2d6d
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 5 deletions.
1 change: 1 addition & 0 deletions src/main/components/store/model-store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const createReduxStore = (
patcher &&
createPatcherReducer<UMLModel, ModelState>(patcher, {
transform: (model) => ModelState.fromModel(model) as ModelState,
transformInverse: (state) => ModelState.toModel(state),
merge,
});

Expand Down
16 changes: 13 additions & 3 deletions src/main/services/patcher/patcher-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export type PatcherReducerOptions<T, U = T> = {
*/
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
Expand All @@ -25,6 +33,7 @@ export type PatcherReducerOptions<T, U = T> = {

const _DefaultOptions = {
transform: (state: any) => state,
transformInverse: (state: any) => state,
merge: (oldState: any, newState: any) => ({ ...oldState, ...newState }),
};

Expand All @@ -39,15 +48,16 @@ export function createPatcherReducer<T, U = T>(
options: PatcherReducerOptions<T, U> = _DefaultOptions,
): Reducer<U> {
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));
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/services/patcher/patcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,11 @@ export class Patcher<T> {
* @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) => {
Expand Down
13 changes: 12 additions & 1 deletion src/tests/unit/services/patcher/patcher-reducer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));

Expand Down
7 changes: 7 additions & 0 deletions src/tests/unit/services/patcher/patcher-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
});

0 comments on commit cae2d6d

Please sign in to comment.