Skip to content

Commit 4dbce9e

Browse files
authored
Add applyPatches function to BaseControllerV2 (#980)
1 parent 3454eeb commit 4dbce9e

File tree

2 files changed

+106
-5
lines changed

2 files changed

+106
-5
lines changed

packages/base-controller/src/BaseControllerV2.test.ts

+78-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,12 @@ class CountController extends BaseController<
7373
state: Draft<CountControllerState>,
7474
) => void | CountControllerState,
7575
) {
76-
super.update(callback);
76+
const res = super.update(callback);
77+
return res;
78+
}
79+
80+
applyPatches(patches: Patch[]) {
81+
super.applyPatches(patches);
7782
}
7883

7984
destroy() {
@@ -170,6 +175,29 @@ describe('BaseController', () => {
170175
expect(controller.state).toStrictEqual({ count: 1 });
171176
});
172177

178+
it('should return next state, patches and inverse patches after an update', () => {
179+
const controller = new CountController({
180+
messenger: getCountMessenger(),
181+
name: 'CountController',
182+
state: { count: 0 },
183+
metadata: countControllerStateMetadata,
184+
});
185+
186+
const returnObj = controller.update((draft) => {
187+
draft.count += 1;
188+
});
189+
190+
expect(returnObj).not.toBeUndefined();
191+
expect(returnObj.nextState).toStrictEqual({ count: 1 });
192+
expect(returnObj.patches).toStrictEqual([
193+
{ op: 'replace', path: ['count'], value: 1 },
194+
]);
195+
196+
expect(returnObj.inversePatches).toStrictEqual([
197+
{ op: 'replace', path: ['count'], value: 0 },
198+
]);
199+
});
200+
173201
it('should throw an error if update callback modifies draft and returns value', () => {
174202
const controller = new CountController({
175203
messenger: getCountMessenger(),
@@ -188,6 +216,55 @@ describe('BaseController', () => {
188216
);
189217
});
190218

219+
it('should allow for applying immer patches to state', () => {
220+
const controller = new CountController({
221+
messenger: getCountMessenger(),
222+
name: 'CountController',
223+
state: { count: 0 },
224+
metadata: countControllerStateMetadata,
225+
});
226+
227+
const returnObj = controller.update((draft) => {
228+
draft.count += 1;
229+
});
230+
231+
controller.applyPatches(returnObj.inversePatches);
232+
233+
expect(controller.state).toStrictEqual({ count: 0 });
234+
});
235+
236+
it('should inform subscribers of state changes as a result of applying patches', () => {
237+
const controllerMessenger = new ControllerMessenger<
238+
never,
239+
CountControllerEvent
240+
>();
241+
const controller = new CountController({
242+
messenger: getCountMessenger(controllerMessenger),
243+
name: 'CountController',
244+
state: { count: 0 },
245+
metadata: countControllerStateMetadata,
246+
});
247+
const listener1 = sinon.stub();
248+
249+
controllerMessenger.subscribe('CountController:stateChange', listener1);
250+
const { inversePatches } = controller.update(() => {
251+
return { count: 1 };
252+
});
253+
254+
controller.applyPatches(inversePatches);
255+
256+
expect(listener1.callCount).toStrictEqual(2);
257+
expect(listener1.firstCall.args).toStrictEqual([
258+
{ count: 1 },
259+
[{ op: 'replace', path: [], value: { count: 1 } }],
260+
]);
261+
262+
expect(listener1.secondCall.args).toStrictEqual([
263+
{ count: 0 },
264+
[{ op: 'replace', path: [], value: { count: 0 } }],
265+
]);
266+
});
267+
191268
it('should inform subscribers of state changes', () => {
192269
const controllerMessenger = new ControllerMessenger<
193270
never,

packages/base-controller/src/BaseControllerV2.ts

+28-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { enablePatches, produceWithPatches } from 'immer';
1+
import { enablePatches, produceWithPatches, applyPatches } from 'immer';
22

33
// Imported separately because only the type is used
44
// eslint-disable-next-line no-duplicate-imports
@@ -146,12 +146,17 @@ export class BaseController<
146146
*
147147
* @param callback - Callback for updating state, passed a draft state
148148
* object. Return a new state object or mutate the draft to update state.
149+
* @returns An object that has the next state, patches applied in the update and inverse patches to
150+
* rollback the update.
149151
*/
150-
protected update(callback: (state: Draft<S>) => void | S) {
152+
protected update(callback: (state: Draft<S>) => void | S): {
153+
nextState: S;
154+
patches: Patch[];
155+
inversePatches: Patch[];
156+
} {
151157
// We run into ts2589, "infinite type depth", if we don't cast
152158
// produceWithPatches here.
153-
// The final, omitted member of the returned tuple are the inverse patches.
154-
const [nextState, patches] = (
159+
const [nextState, patches, inversePatches] = (
155160
produceWithPatches as unknown as (
156161
state: S,
157162
cb: typeof callback,
@@ -164,6 +169,25 @@ export class BaseController<
164169
nextState,
165170
patches,
166171
);
172+
173+
return { nextState, patches, inversePatches };
174+
}
175+
176+
/**
177+
* Applies immer patches to the current state. The patches come from the
178+
* update function itself and can either be normal or inverse patches.
179+
*
180+
* @param patches - An array of immer patches that are to be applied to make
181+
* or undo changes.
182+
*/
183+
protected applyPatches(patches: Patch[]) {
184+
const nextState = applyPatches(this.internalState, patches);
185+
this.internalState = nextState;
186+
this.messagingSystem.publish(
187+
`${this.name}:stateChange` as Namespaced<N, any>,
188+
nextState,
189+
patches,
190+
);
167191
}
168192

169193
/**

0 commit comments

Comments
 (0)