Skip to content

Commit b185652

Browse files
authored
feat: support for mutable data in apply() function (#55)
1 parent 723f9e9 commit b185652

File tree

4 files changed

+143
-13
lines changed

4 files changed

+143
-13
lines changed

src/apply.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { Draft, Options, Patches, DraftType, Operation } from './interface';
1+
import { Operation, DraftType } from './interface';
2+
import type {
3+
Draft,
4+
Patches,
5+
ApplyMutableOptions,
6+
ApplyOptions,
7+
ApplyResult,
8+
} from './interface';
29
import { deepClone, get, getType, isDraft, unescapePath } from './utils';
310
import { create } from './create';
411

@@ -23,14 +30,11 @@ import { create } from './create';
2330
* expect(state).toEqual(apply(baseState, patches));
2431
* ```
2532
*/
26-
export function apply<T extends object, F extends boolean = false>(
27-
state: T,
28-
patches: Patches,
29-
applyOptions?: Pick<
30-
Options<boolean, F>,
31-
Exclude<keyof Options<boolean, F>, 'enablePatches'>
32-
>
33-
) {
33+
export function apply<
34+
T extends object,
35+
F extends boolean = false,
36+
A extends ApplyOptions<F> = ApplyOptions<F>,
37+
>(state: T, patches: Patches, applyOptions?: A): ApplyResult<T, F, A> {
3438
let i: number;
3539
for (i = patches.length - 1; i >= 0; i -= 1) {
3640
const { value, op, path } = patches[i];
@@ -45,7 +49,7 @@ export function apply<T extends object, F extends boolean = false>(
4549
if (i > -1) {
4650
patches = patches.slice(i + 1);
4751
}
48-
const mutate = (draft: Draft<T>) => {
52+
const mutate = (draft: Draft<T> | T) => {
4953
patches.forEach((patch) => {
5054
const { path: _path, op } = patch;
5155
const path = unescapePath(_path);
@@ -119,15 +123,28 @@ export function apply<T extends object, F extends boolean = false>(
119123
}
120124
});
121125
};
126+
if ((applyOptions as ApplyMutableOptions)?.mutable) {
127+
if (__DEV__) {
128+
if (
129+
Object.keys(applyOptions!).filter((key) => key !== 'mutable').length
130+
) {
131+
console.warn(
132+
'The "mutable" option is not allowed to be used with other options.'
133+
);
134+
}
135+
}
136+
mutate(state);
137+
return undefined as ApplyResult<T, F, A>;
138+
}
122139
if (isDraft(state)) {
123140
if (applyOptions !== undefined) {
124141
throw new Error(`Cannot apply patches with options to a draft.`);
125142
}
126143
mutate(state as Draft<T>);
127-
return state;
144+
return state as ApplyResult<T, F, A>;
128145
}
129146
return create<T, F>(state, mutate, {
130147
...applyOptions,
131148
enablePatches: false,
132-
});
149+
}) as any;
133150
}

src/interface.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ export type Mark<O extends PatchesOptions, F extends boolean> = (
102102
? BaseMark
103103
: MarkWithCopy;
104104

105+
export interface ApplyMutableOptions {
106+
/**
107+
* If it's `true`, the state will be mutated directly.
108+
*/
109+
mutable?: boolean;
110+
}
111+
105112
export interface Options<O extends PatchesOptions, F extends boolean> {
106113
/**
107114
* In strict mode, Forbid accessing non-draftable values and forbid returning a non-draft value.
@@ -190,3 +197,16 @@ export type Draft<T> = T extends Primitive | AtomicObject
190197
: T extends object
191198
? DraftedObject<T>
192199
: T;
200+
201+
export type ApplyOptions<F extends boolean> =
202+
| Pick<
203+
Options<boolean, F>,
204+
Exclude<keyof Options<boolean, F>, 'enablePatches'>
205+
>
206+
| ApplyMutableOptions;
207+
208+
export type ApplyResult<
209+
T extends object,
210+
F extends boolean = false,
211+
A extends ApplyOptions<F> = ApplyOptions<F>
212+
> = A extends { mutable: true } ? void : T;

test/apply.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1658,3 +1658,46 @@ test('array - update primitive', () => {
16581658
d.a[2] += 1;
16591659
});
16601660
});
1661+
1662+
test('base - mutate', () => {
1663+
const baseState = {
1664+
a: {
1665+
c: 1,
1666+
},
1667+
};
1668+
const [state, patches, inversePatches] = create(
1669+
baseState,
1670+
(draft) => {
1671+
draft.a.c = 2;
1672+
},
1673+
{
1674+
enablePatches: true,
1675+
}
1676+
);
1677+
expect(state).toEqual({ a: { c: 2 } });
1678+
expect({ patches, inversePatches }).toEqual({
1679+
patches: [
1680+
{
1681+
op: 'replace',
1682+
path: ['a', 'c'],
1683+
value: 2,
1684+
},
1685+
],
1686+
inversePatches: [
1687+
{
1688+
op: 'replace',
1689+
path: ['a', 'c'],
1690+
value: 1,
1691+
},
1692+
],
1693+
});
1694+
const nextState = apply(baseState, patches);
1695+
expect(nextState).toEqual({ a: { c: 2 } });
1696+
expect(baseState).toEqual({ a: { c: 1 } });
1697+
1698+
const result = apply(baseState, patches, {
1699+
mutable: true,
1700+
});
1701+
expect(baseState).toEqual({ a: { c: 2 } });
1702+
expect(result).toBeUndefined();
1703+
});

test/dev.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable consistent-return */
22
/* eslint-disable no-param-reassign */
33
/* eslint-disable @typescript-eslint/ban-ts-comment */
4-
import { create } from '../src';
4+
import { apply, create } from '../src';
55

66
test('custom shallow copy without checking in prod mode', () => {
77
global.__DEV__ = false;
@@ -88,3 +88,53 @@ test('custom shallow copy with checking in dev mode', () => {
8888
`"You can't use mark and patches or auto freeze together."`
8989
);
9090
});
91+
92+
test('check warn when apply patches with other options', () => {
93+
{
94+
global.__DEV__ = true;
95+
const baseState = { foo: { bar: 'test' } };
96+
const warn = console.warn;
97+
jest.spyOn(console, 'warn').mockImplementation(() => {});
98+
apply(
99+
baseState,
100+
[
101+
{
102+
op: 'replace',
103+
path: ['foo', 'bar'],
104+
value: 'test2',
105+
},
106+
],
107+
{
108+
mutable: true,
109+
enableAutoFreeze: true,
110+
}
111+
);
112+
expect(console.warn).toHaveBeenCalledWith(
113+
'The "mutable" option is not allowed to be used with other options.'
114+
);
115+
}
116+
{
117+
global.__DEV__ = true;
118+
const baseState = { foo: { bar: 'test' } };
119+
const warn = console.warn;
120+
jest.spyOn(console, 'warn').mockImplementation(() => {});
121+
apply(
122+
baseState,
123+
[
124+
{
125+
op: 'replace',
126+
path: ['foo', 'bar'],
127+
value: 'test2',
128+
},
129+
],
130+
{
131+
mutable: true,
132+
enableAutoFreeze: true,
133+
mark: () => {},
134+
}
135+
);
136+
expect(console.warn).toHaveBeenCalledWith(
137+
'The "mutable" option is not allowed to be used with other options.'
138+
);
139+
}
140+
});

0 commit comments

Comments
 (0)