From abf11be87c840fb88955c6bdbb032010d600b2d3 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:10:17 +0800 Subject: [PATCH 01/21] fix: report a circular reference only for an object that holds itself when freezing in development --- README.md | 1 + src/draft.ts | 2 +- src/interface.ts | 2 +- src/utils/deepFreeze.ts | 33 +++++------ src/utils/finalize.ts | 10 ---- test/index.test.ts | 57 +++++++++++++++++-- .../docs/advanced-guides/migration-v2.mdx | 1 + 7 files changed, 69 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index c3d0f2ce..5d0d6384 100644 --- a/README.md +++ b/README.md @@ -786,6 +786,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - In strict mode, development builds warn once when a recipe leaves 1,000 or more drafts unchanged, as a search through a large draft array does. - In strict mode, `rawReturn()` of a value without drafts no longer prints contradictory warnings. - With `enablePatches: { pathAsArray: false }`, development builds throw when a patch path would hold a Map key that is not a string, or a symbol key, as a string path cannot name such a key: applying the patch writes another key, for example `'1'` instead of `1`, or fails for a change below that key. Production builds still generate such patches, as v1 did, and still throw a `TypeError` for a symbol key. Keep the default array paths for such keys. +- With `enableAutoFreeze`, development builds report a circular reference only for an object that holds itself. A changed draft that holds its original, as `draft.prev = original(draft)` makes it, or that links to a shared object through another path no longer throws; production builds always froze such states. For a real cycle, the error names the path of the object that repeats, which can be one key longer than in v1. ### TypeScript diff --git a/src/draft.ts b/src/draft.ts index cefa4751..0551b844 100644 --- a/src/draft.ts +++ b/src/draft.ts @@ -470,7 +470,7 @@ export function finalizeDraft( : result; if (proxyDraft) revokeProxy(proxyDraft.finalities); if (enableAutoFreeze) { - deepFreeze(state, state, proxyDraft?.options.updatedValues); + deepFreeze(state); } return [ state, diff --git a/src/interface.ts b/src/interface.ts index c99fe349..434b5508 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -52,7 +52,7 @@ export interface ProxyDraft { copy: T | null; proxy: T | null; finalities: Finalities; - options: Options & { updatedValues?: WeakMap }; + options: Options; parent: ProxyDraft | null; key: string | number | symbol | undefined; // The drafts of a Set draft's items and the objects it added, with its items diff --git a/src/utils/deepFreeze.ts b/src/utils/deepFreeze.ts index 524a3d72..bdefb9c9 100644 --- a/src/utils/deepFreeze.ts +++ b/src/utils/deepFreeze.ts @@ -15,21 +15,20 @@ function isFreezable(value: any) { export function deepFreeze( target: any, subKey?: any, - updatedValues?: WeakMap, stack?: any[], keys?: any[] ) { if (__DEV__) { - updatedValues = updatedValues ?? new WeakMap(); stack = stack ?? []; keys = keys ?? []; - const value = updatedValues.has(target) - ? updatedValues.get(target) - : target; + // A cycle is an object that holds itself through its values. The copy of a + // changed draft is a different object from its original, so a copy that + // holds its original, as `draft.prev = original(draft)` makes it, is no + // cycle, and neither is a shared object reached through another path. if (stack.length > 0) { - const index = stack.indexOf(value); - if (value && typeof value === 'object' && index !== -1) { - if (stack[0] === value) { + const index = stack.indexOf(target); + if (target && typeof target === 'object' && index !== -1) { + if (stack[0] === target) { throw new Error(`Forbids circular reference`); } throw new Error( @@ -48,10 +47,10 @@ export function deepFreeze( .join('/')}` ); } - stack.push(value); + stack.push(target); keys.push(subKey); } else { - stack.push(value); + stack.push(target); } } if (Object.isFrozen(target) || isDraft(target)) { @@ -70,25 +69,22 @@ export function deepFreeze( target.set = target.clear = target.delete = throwFrozenError; Object.freeze(target); for (const [key, value] of target) { - if (isFreezable(key)) deepFreeze(key, key, updatedValues, stack, keys); - if (isFreezable(value)) - deepFreeze(value, key, updatedValues, stack, keys); + if (isFreezable(key)) deepFreeze(key, key, stack, keys); + if (isFreezable(value)) deepFreeze(value, key, stack, keys); } break; case DraftType.Set: target.add = target.clear = target.delete = throwFrozenError; Object.freeze(target); for (const value of target) { - if (isFreezable(value)) - deepFreeze(value, value, updatedValues, stack, keys); + if (isFreezable(value)) deepFreeze(value, value, stack, keys); } break; case DraftType.Array: Object.freeze(target); let index = 0; for (const value of target) { - if (isFreezable(value)) - deepFreeze(value, index, updatedValues, stack, keys); + if (isFreezable(value)) deepFreeze(value, index, stack, keys); index += 1; } break; @@ -97,8 +93,7 @@ export function deepFreeze( // ignore non-enumerable or symbol properties Object.keys(target).forEach((name) => { const value = target[name]; - if (isFreezable(value)) - deepFreeze(value, name, updatedValues, stack, keys); + if (isFreezable(value)) deepFreeze(value, name, stack, keys); }); } if (__DEV__) { diff --git a/src/utils/finalize.ts b/src/utils/finalize.ts index 29260135..b72246da 100644 --- a/src/utils/finalize.ts +++ b/src/utils/finalize.ts @@ -170,11 +170,6 @@ export function finalizeNode( if (proxyDraft.finalities === node.finalities) finalizeSetValue(proxyDraft); finalizePatches(proxyDraft, generatePatches, patches, inversePatches); - if (__DEV__ && parent.options.enableAutoFreeze) { - parent.options.updatedValues = - parent.options.updatedValues ?? new WeakMap(); - parent.options.updatedValues.set(updatedValue, proxyDraft.original); - } // final update value if (isMapLike) { copy.set(key, updatedValue); @@ -215,11 +210,6 @@ export function markFinalization(target: ProxyDraft, key: any, value: any) { // the Set; a draft of another producer may change after that. if (proxyDraft.finalities !== target.finalities) finalizeSetValue(target, true); - if (__DEV__ && target.options.enableAutoFreeze) { - target.options.updatedValues = - target.options.updatedValues ?? new WeakMap(); - target.options.updatedValues.set(updatedValue, proxyDraft.original); - } // final update value set(copy, key, updatedValue); } diff --git a/test/index.test.ts b/test/index.test.ts index df0a5b86..720960e0 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -2358,7 +2358,7 @@ test('circular reference - object - 1', () => { } ); }).toThrowErrorMatchingInlineSnapshot( - `[Error: Forbids circular reference: ~/a/b]` + `[Error: Forbids circular reference: ~/a/b/c1]` ); }); @@ -2398,7 +2398,7 @@ test('circular reference - object - 2', () => { } ); }).toThrowErrorMatchingInlineSnapshot( - `[Error: Forbids circular reference: ~/a/b]` + `[Error: Forbids circular reference: ~/a/b/c1]` ); }); @@ -2438,7 +2438,7 @@ test('circular reference - object - 3', () => { } ); }).toThrowErrorMatchingInlineSnapshot( - `[Error: Forbids circular reference: ~/a/b]` + `[Error: Forbids circular reference: ~/a/b/c1]` ); }); @@ -2513,7 +2513,7 @@ test('circular reference - array - 1', () => { } ); }).toThrowErrorMatchingInlineSnapshot( - `[Error: Forbids circular reference: ~/1/a]` + `[Error: Forbids circular reference: ~/1/a/b/b]` ); }); @@ -2591,7 +2591,7 @@ test('circular reference - set - 1', () => { } ); }).toThrowErrorMatchingInlineSnapshot( - `[Error: Forbids circular reference: ~/1/a/b]` + `[Error: Forbids circular reference: ~/1/a/b/c1]` ); }); @@ -2680,7 +2680,7 @@ test('circular reference - map - 1', () => { } ); }).toThrowErrorMatchingInlineSnapshot( - `[Error: Forbids circular reference: ~/[Symbol(1)]/a/b]` + `[Error: Forbids circular reference: ~/[Symbol(1)]/a/b/c1]` ); }); @@ -2799,6 +2799,51 @@ test('circular reference - map - 3 - 1', () => { }).toThrowErrorMatchingInlineSnapshot(`[Error: Forbids circular reference]`); }); +test('auto-freeze accepts a changed draft that holds its original', () => { + const base = { a: { n: 0 } }; + const state = create( + base, + (draft) => { + draft.a.n = 1; + // @ts-expect-error + draft.a.prev = original(draft.a); + }, + { + enableAutoFreeze: true, + } + ); + expect(state.a).toEqual({ n: 1, prev: { n: 0 } }); + // @ts-expect-error + expect(state.a.prev).toBe(base.a); + expect(Object.isFrozen(state.a)).toBe(true); + expect(Object.isFrozen(base.a)).toBe(true); +}); + +test('auto-freeze accepts a shared object linked through another path', () => { + const shared = { n: 0 }; + const base = { a: shared, b: shared, list: [shared, shared] }; + const state = create( + base, + (draft) => { + // @ts-expect-error + draft.a.link = draft.b; + draft.list[0].n = 5; + // @ts-expect-error + draft.list[0].other = draft.list[1]; + }, + { + enableAutoFreeze: true, + } + ); + expect(state.a).toEqual({ n: 0, link: shared }); + expect(state.b).toBe(shared); + expect(state.list[0]).toEqual({ n: 5, other: shared }); + expect(state.list[1]).toBe(shared); + expect(Object.isFrozen(state.a)).toBe(true); + expect(Object.isFrozen(state.list[0])).toBe(true); + expect(Object.isFrozen(shared)).toBe(true); +}); + test('can return an object that references itself', () => { const res = {}; // @ts-expect-error diff --git a/website/content/docs/advanced-guides/migration-v2.mdx b/website/content/docs/advanced-guides/migration-v2.mdx index d4ba16b6..86d215de 100644 --- a/website/content/docs/advanced-guides/migration-v2.mdx +++ b/website/content/docs/advanced-guides/migration-v2.mdx @@ -35,6 +35,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - In strict mode, development builds warn once when a recipe leaves 1,000 or more drafts unchanged, as a search through a large draft array does. - In strict mode, `rawReturn()` of a value without drafts no longer prints contradictory warnings. - With `enablePatches: { pathAsArray: false }`, development builds throw when a patch path would hold a Map key that is not a string, or a symbol key, as a string path cannot name such a key: applying the patch writes another key, for example `'1'` instead of `1`, or fails for a change below that key. Production builds still generate such patches, as v1 did, and still throw a `TypeError` for a symbol key. Keep the default array paths for such keys. +- With `enableAutoFreeze`, development builds report a circular reference only for an object that holds itself. A changed draft that holds its original, as `draft.prev = original(draft)` makes it, or that links to a shared object through another path no longer throws; production builds always froze such states. For a real cycle, the error names the path of the object that repeats, which can be one key longer than in v1. ## TypeScript From ad5269001ceb8d924bba08ccd17880e875549a66 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:11:36 +0800 Subject: [PATCH 02/21] fix: find a Set item of a patch path only at a numeric position --- README.md | 1 + src/apply.ts | 8 ++- test/apply.test.ts | 52 +++++++++++++++++++ .../docs/advanced-guides/migration-v2.mdx | 1 + 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5d0d6384..92925c8f 100644 --- a/README.md +++ b/README.md @@ -808,6 +808,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - Consume Map value and entry iterators, and all Set iterators, while their draft is active. After the producer finishes or fails, or a manual draft is finalized, an iterator cannot yield another value and throws a `TypeError`. Lazy iterator helpers follow the same lifetime; an exhausted iterator stays exhausted. Map keys are not drafted, and `keys()` continues to return a native iterator. - `apply()` copies the own symbol keys of patch values, and an own `__proto__` key, as `JSON.parse()` creates one, stays a data property. v1 dropped symbol keys there and turned an own `__proto__` key into the prototype of the copy. - `apply()` rejects a patch whose path ends in `__proto__` on an object or array, as it rejects `__proto__` earlier in a path. Coercible property keys, such as a boxed string, are checked after conversion too. With `mutable: true`, v1 assigned the value and so replaced the prototype of the object. +- In a patch path, `apply()` reads an item of a Set only at a numeric position, and throws for any other key. v1 read any property of an array of the Set's items there, which let a path through `__proto__` reach `Array.prototype`. - An object that `mark` makes draftable, such as a plain object under `markSimpleObject`, keeps an own `__proto__` key, as `JSON.parse()` creates one, as a property of its copy; v1 assigned it, which replaced the prototype of the copy. - A patch for a Map key that is an array holds the key as one path segment; v1 spread the array into the path, so applying the patch wrote to other keys. - `apply()` uses a Map key of a patch path as it is. v1 converted it to a string first, which threw for an object without a prototype. diff --git a/src/apply.ts b/src/apply.ts index eb2671b4..a0984495 100644 --- a/src/apply.ts +++ b/src/apply.ts @@ -140,8 +140,12 @@ export function apply< base = parentType === DraftType.Map ? base.get(key) - : // use `index` in Set draft - (parentType === DraftType.Set ? Array.from(base) : base)[key]; + : parentType === DraftType.Set + ? // A Set item is found by its position, a number, so that no + // other key, such as `__proto__`, reads a property of the + // array of its items. + Array.from(base)[+key] + : base[key]; if (typeof base !== 'object') { die(ErrorCode.CannotApplyPatch, path); } diff --git a/test/apply.test.ts b/test/apply.test.ts index 304ca9d0..aae3e2d5 100644 --- a/test/apply.test.ts +++ b/test/apply.test.ts @@ -1299,6 +1299,58 @@ test('a patch cannot set a prototype through the last segment of its path', () = expect(map.get('__proto__')).toBe(1); }); +test('a patch cannot reach a prototype through a Set position', () => { + const constructor = Array.prototype.constructor; + const cases: [string, string | number, unknown][] = [ + ['replace', 'injected', { admin: true }], + ['add', 0, { admin: true }], + ['replace', 'constructor', 'x'], + ]; + for (const [op, last, value] of cases) { + for (const nested of [false, true]) { + for (const asString of [false, true]) { + for (const options of [undefined, { mutable: true }]) { + const state: any = nested + ? { set: new Set([{ v: 1 }]) } + : new Set([{ v: 1 }]); + const keys = [...(nested ? ['set'] : []), '__proto__', last]; + const path = asString ? `/${keys.join('/')}` : keys; + try { + expect(() => + apply(state, [{ op, path, value }] as Patches, options) + ).toThrow(`Cannot apply patch at '${keys.join('/')}'.`); + } finally { + // Undo a change of the prototype, so that other tests still work + // if the check fails. + delete (Array.prototype as any).injected; + // eslint-disable-next-line no-extend-native + Array.prototype.length = 0; + // eslint-disable-next-line no-extend-native + Array.prototype.constructor = constructor; + } + expect(([] as any).injected).toBeUndefined(); + expect(([] as any)[0]).toBeUndefined(); + expect(Array.prototype.length).toBe(0); + expect([1].map((item) => item + 1)).toEqual([2]); + } + } + } + } + // A position, also given as a string, still finds its item. + for (const options of [undefined, { mutable: true }]) { + const state = { set: new Set([{ v: 1 }, { v: 2 }]) }; + const result = apply( + state, + [ + { op: 'replace', path: ['set', 1, 'v'], value: 3 }, + { op: 'replace', path: '/set/0/v', value: 4 }, + ] as Patches, + options + ); + expect([...(result ?? state).set]).toEqual([{ v: 4 }, { v: 3 }]); + } +}); + test.each([false, true])( 'coercible terminal keys cannot set a prototype (mutable: %s)', (mutable) => { diff --git a/website/content/docs/advanced-guides/migration-v2.mdx b/website/content/docs/advanced-guides/migration-v2.mdx index 86d215de..d60286f0 100644 --- a/website/content/docs/advanced-guides/migration-v2.mdx +++ b/website/content/docs/advanced-guides/migration-v2.mdx @@ -57,6 +57,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - Consume Map value and entry iterators, and all Set iterators, while their draft is active. After the producer finishes or fails, or a manual draft is finalized, an iterator cannot yield another value and throws a `TypeError`. Lazy iterator helpers follow the same lifetime; an exhausted iterator stays exhausted. Map keys are not drafted, and `keys()` continues to return a native iterator. - `apply()` copies the own symbol keys of patch values, and an own `__proto__` key, as `JSON.parse()` creates one, stays a data property. v1 dropped symbol keys there and turned an own `__proto__` key into the prototype of the copy. - `apply()` rejects a patch whose path ends in `__proto__` on an object or array, as it rejects `__proto__` earlier in a path. Coercible property keys, such as a boxed string, are checked after conversion too. With `mutable: true`, v1 assigned the value and so replaced the prototype of the object. +- In a patch path, `apply()` reads an item of a Set only at a numeric position, and throws for any other key. v1 read any property of an array of the Set's items there, which let a path through `__proto__` reach `Array.prototype`. - An object that `mark` makes draftable, such as a plain object under `markSimpleObject`, keeps an own `__proto__` key, as `JSON.parse()` creates one, as a property of its copy; v1 assigned it, which replaced the prototype of the copy. - A patch for a Map key that is an array holds the key as one path segment; v1 spread the array into the path, so applying the patch wrote to other keys. - `apply()` uses a Map key of a patch path as it is. v1 converted it to a string first, which threw for an object without a prototype. From dfb5ff5aed05bc3284bfdee75a4df466d982a0ba Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:13:07 +0800 Subject: [PATCH 03/21] fix: throw when a mutable apply() would replace the root state --- README.md | 3 +- src/apply.ts | 5 ++- src/error.ts | 5 +++ test/apply.test.ts | 40 +++++++++++++++++++ test/production.test.ts | 5 +++ .../docs/advanced-guides/migration-v2.mdx | 1 + .../content/docs/advanced-guides/pathes.mdx | 2 +- website/content/docs/api-reference/apply.mdx | 2 +- website/content/docs/extra-topics/errors.mdx | 1 + 9 files changed, 60 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 92925c8f..168a3feb 100644 --- a/README.md +++ b/README.md @@ -389,7 +389,7 @@ apply( expect(baseState).toEqual({ foo: { bar: "test2" } }); ``` -> ⚠️Note: The mutable option cannot be combined with other options. When using mutable option, apply() will return void instead of a new state. +> ⚠️Note: The mutable option cannot be combined with other options. When using mutable option, apply() will return void instead of a new state. As it changes the state in place, it throws for a patch that replaces the root state with another value, such as the patch of a recipe that returns a new state. > Patches add and remove Set elements by value, also a changed element of a Set that added or removed elements, and `apply()` copies patch values, so inverse patches cannot remove an object that `apply()` added to a Set, as in an undo after a redo. See [Sets of objects](https://mutative.js.org/docs/advanced-guides/pathes#sets-of-objects). @@ -809,6 +809,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - `apply()` copies the own symbol keys of patch values, and an own `__proto__` key, as `JSON.parse()` creates one, stays a data property. v1 dropped symbol keys there and turned an own `__proto__` key into the prototype of the copy. - `apply()` rejects a patch whose path ends in `__proto__` on an object or array, as it rejects `__proto__` earlier in a path. Coercible property keys, such as a boxed string, are checked after conversion too. With `mutable: true`, v1 assigned the value and so replaced the prototype of the object. - In a patch path, `apply()` reads an item of a Set only at a numeric position, and throws for any other key. v1 read any property of an array of the Set's items there, which let a path through `__proto__` reach `Array.prototype`. +- `apply()` with `mutable: true` throws for a patch that replaces the root state with another value, such as the patch of a recipe that returns a new state, as the state cannot be replaced in place. v1 ignored the replacement and applied the patches after it to the replacing value, so the state stayed as it was and the value of that patch changed. - An object that `mark` makes draftable, such as a plain object under `markSimpleObject`, keeps an own `__proto__` key, as `JSON.parse()` creates one, as a property of its copy; v1 assigned it, which replaced the prototype of the copy. - A patch for a Map key that is an array holds the key as one path segment; v1 spread the array into the path, so applying the patch wrote to other keys. - `apply()` uses a Map key of a patch path as it is. v1 converted it to a string first, which threw for an object without a prototype. diff --git a/src/apply.ts b/src/apply.ts index a0984495..c70c5533 100644 --- a/src/apply.ts +++ b/src/apply.ts @@ -101,6 +101,7 @@ export function apply< F extends boolean = false, A extends ApplyOptions | undefined = ApplyImmutableOptions, >(state: T, patches: Patches, applyOptions?: A): ApplyResult { + const mutable = (applyOptions as ApplyMutableOptions)?.mutable; let i: number; for (i = patches.length - 1; i >= 0; i -= 1) { const { value, op, path } = patches[i]; @@ -108,6 +109,9 @@ export function apply< (!path.length && op === Operation.Replace) || (path === '' && op === Operation.Add) ) { + // A mutable application changes the state in place and returns nothing, + // so it cannot replace the state with another value. + if (mutable && value !== state) die(ErrorCode.ReplaceMutableRoot); state = value; break; } @@ -115,7 +119,6 @@ export function apply< if (i > -1) { patches = patches.slice(i + 1); } - const mutable = (applyOptions as ApplyMutableOptions)?.mutable; // The objects that a mutable application changes in place are not drafts. const getTargetType = mutable ? getType : getPatchTargetType; const mutate = (draft: Draft | T) => { diff --git a/src/error.ts b/src/error.ts index 5e064ddd..6a634fdd 100644 --- a/src/error.ts +++ b/src/error.ts @@ -26,6 +26,7 @@ export const enum ErrorCode { OriginalOnNonDraft = 18, RawReturnWithoutValue = 19, RawReturnWithExtraArguments = 20, + ReplaceMutableRoot = 21, } type ErrorArguments = { @@ -49,6 +50,7 @@ type ErrorArguments = { [ErrorCode.OriginalOnNonDraft]: [target: any]; [ErrorCode.RawReturnWithoutValue]: []; [ErrorCode.RawReturnWithExtraArguments]: []; + [ErrorCode.ReplaceMutableRoot]: []; }; type ErrorBuilders = { @@ -104,6 +106,9 @@ const errors: ErrorBuilders = __DEV__ () => 'rawReturn() must be called with a value.', // ErrorCode.RawReturnWithExtraArguments () => 'rawReturn() must be called with one argument.', + // ErrorCode.ReplaceMutableRoot + () => + 'apply() with the mutable option changes the state in place, so a patch cannot replace the root state with another value.', ] : ([] as unknown as ErrorBuilders); diff --git a/test/apply.test.ts b/test/apply.test.ts index aae3e2d5..3c44aa5d 100644 --- a/test/apply.test.ts +++ b/test/apply.test.ts @@ -1891,6 +1891,46 @@ test('base - mutate', () => { expect(result).toBeUndefined(); }); +test('a mutable application cannot replace the root state', () => { + const [, patches] = create({ old: 1 } as any, () => ({ n: 2 }), { + enablePatches: true, + }); + for (const root of [ + patches[0], + { op: 'replace', path: '', value: { n: 2 } }, + { op: 'add', path: '', value: { n: 2 } }, + ] as Patches) { + const state = { old: 1 }; + const value = root.value; + expect(() => + apply( + state, + [root, { op: 'replace', path: ['n'], value: 3 }] as Patches, + { mutable: true } + ) + ).toThrow( + 'apply() with the mutable option changes the state in place, so a patch cannot replace the root state with another value.' + ); + expect(state).toEqual({ old: 1 }); + expect(value).toEqual({ n: 2 }); + } + // A replacement with the state itself changes nothing, as the patch of a + // recipe that returns its base state, and the patches after it apply. + const state = { n: 1 }; + const [, own] = create(state, () => state, { enablePatches: true }); + apply(state, [...own, { op: 'replace', path: ['n'], value: 2 }], { + mutable: true, + }); + expect(state).toEqual({ n: 2 }); + // Without the option, the patches replace the root state. + expect( + apply({ old: 1 } as any, [ + ...patches, + { op: 'replace', path: ['n'], value: 3 }, + ]) + ).toEqual({ n: 3 }); +}); + describe('array methods on the draft copy', () => { // Plain arrays run `shift`, `unshift`, `splice`, `reverse`, searches and // primitive sorts natively on the draft's copy: removed and moved elements diff --git a/test/production.test.ts b/test/production.test.ts index 4ed6a7c0..a4bb9181 100644 --- a/test/production.test.ts +++ b/test/production.test.ts @@ -49,6 +49,11 @@ test('patch, original() and rawReturn() errors carry their minified code', () => expect(() => original({})).toThrow(minified(18)); expect(() => (rawReturn as any)()).toThrow(minified(19)); expect(() => (rawReturn as any)({}, {})).toThrow(minified(20)); + expect(() => + apply({ a: 1 }, [{ op: 'replace', path: [], value: { b: 1 } }], { + mutable: true, + }) + ).toThrow(minified(21)); }); test('auto-freeze passes over primitives in Maps, Sets, and arrays', () => { diff --git a/website/content/docs/advanced-guides/migration-v2.mdx b/website/content/docs/advanced-guides/migration-v2.mdx index d60286f0..6c737bc9 100644 --- a/website/content/docs/advanced-guides/migration-v2.mdx +++ b/website/content/docs/advanced-guides/migration-v2.mdx @@ -58,6 +58,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - `apply()` copies the own symbol keys of patch values, and an own `__proto__` key, as `JSON.parse()` creates one, stays a data property. v1 dropped symbol keys there and turned an own `__proto__` key into the prototype of the copy. - `apply()` rejects a patch whose path ends in `__proto__` on an object or array, as it rejects `__proto__` earlier in a path. Coercible property keys, such as a boxed string, are checked after conversion too. With `mutable: true`, v1 assigned the value and so replaced the prototype of the object. - In a patch path, `apply()` reads an item of a Set only at a numeric position, and throws for any other key. v1 read any property of an array of the Set's items there, which let a path through `__proto__` reach `Array.prototype`. +- `apply()` with `mutable: true` throws for a patch that replaces the root state with another value, such as the patch of a recipe that returns a new state, as the state cannot be replaced in place. v1 ignored the replacement and applied the patches after it to the replacing value, so the state stayed as it was and the value of that patch changed. - An object that `mark` makes draftable, such as a plain object under `markSimpleObject`, keeps an own `__proto__` key, as `JSON.parse()` creates one, as a property of its copy; v1 assigned it, which replaced the prototype of the copy. - A patch for a Map key that is an array holds the key as one path segment; v1 spread the array into the path, so applying the patch wrote to other keys. - `apply()` uses a Map key of a patch path as it is. v1 converted it to a string first, which threw for an object without a prototype. diff --git a/website/content/docs/advanced-guides/pathes.mdx b/website/content/docs/advanced-guides/pathes.mdx index 2eeea013..2549934b 100644 --- a/website/content/docs/advanced-guides/pathes.mdx +++ b/website/content/docs/advanced-guides/pathes.mdx @@ -84,7 +84,7 @@ expect(inversePatches).toMatchInlineSnapshot(` - **Maps.** `set()` emits an `add` patch for a new key and a `replace` patch for an existing one, and `delete()` a `remove` patch. - **Sets.** Patches add an item by its value and remove one by the value it holds; see [Sets of objects](#sets-of-objects). - **Moved drafts.** A changed draft that leaves its key, because `sort()` or an assignment moved it or another value replaced it there, emits no patches under its old path; the patches of its parent carry its value. -- **Applying patches.** `apply()` applies `add`, `remove` and `replace` patches and accepts `-` as the index that appends to an array. It throws for other operations and for paths through `__proto__` or `constructor`. It takes the `enableAutoFreeze`, `strict` and `mark` options, or `mutable: true` to change the state in place, and it changes a draft in place. +- **Applying patches.** `apply()` applies `add`, `remove` and `replace` patches and accepts `-` as the index that appends to an array. It throws for other operations and for paths through `__proto__` or `constructor`. It takes the `enableAutoFreeze`, `strict` and `mark` options, or `mutable: true` to change the state in place, which throws for a patch that replaces the root state with another value, and it changes a draft in place. - **JSON Patch.** With `pathAsArray: false` and `arrayLengthAssignment: false`, the patches of objects and arrays are JSON Patch operations. Maps and Sets have no JSON Patch counterpart. [Comparison with Immer](/docs/extra-topics/comparison-with-immer#patches) lists where these patches differ from Immer's. diff --git a/website/content/docs/api-reference/apply.mdx b/website/content/docs/api-reference/apply.mdx index 7d408418..33eff311 100644 --- a/website/content/docs/api-reference/apply.mdx +++ b/website/content/docs/api-reference/apply.mdx @@ -84,5 +84,5 @@ apply( expect(baseState).toEqual({ foo: { bar: 'test2' } }); ``` -> ⚠️Note: The mutable option cannot be combined with other options. When using mutable option, apply() will return void instead of a new state. +> ⚠️Note: The mutable option cannot be combined with other options. When using mutable option, apply() will return void instead of a new state. As it changes the state in place, it throws for a patch that replaces the root state with another value, such as the patch of a recipe that returns a new state. diff --git a/website/content/docs/extra-topics/errors.mdx b/website/content/docs/extra-topics/errors.mdx index 12bf71c9..b5d9eed5 100644 --- a/website/content/docs/extra-topics/errors.mdx +++ b/website/content/docs/extra-topics/errors.mdx @@ -34,3 +34,4 @@ Numeric codes remain stable when an error is removed. Retired codes are reserved | 18 | `OriginalOnNonDraft` | `original()` only accepts a draft. | | 19 | `RawReturnWithoutValue` | `rawReturn()` must be called with a value. | | 20 | `RawReturnWithExtraArguments` | `rawReturn()` takes exactly one argument. | +| 21 | `ReplaceMutableRoot` | `apply()` with `mutable: true` cannot apply a patch that replaces the root state with another value. | From a7ed86c13bfc877dfa5d9667f4c35f4f5e882b4f Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:14:17 +0800 Subject: [PATCH 04/21] fix: throw in development builds when a draft becomes a Map key --- README.md | 5 +++ src/map.ts | 7 ++++ test/map-draft-keys.test.ts | 42 +++++++++++++++++++ test/production.test.ts | 10 +++++ .../docs/advanced-guides/migration-v2.mdx | 1 + website/content/docs/extra-topics/faq.mdx | 4 ++ 6 files changed, 69 insertions(+) create mode 100644 test/map-draft-keys.test.ts diff --git a/README.md b/README.md index 168a3feb..b33ddbf3 100644 --- a/README.md +++ b/README.md @@ -648,6 +648,10 @@ Yes. Deleting the entry being visited and changing the values of other entries w Yes, but they compare elements as iterating the draft returns them. An object of the base state is a draft there, so these methods do not match it with the original object, although `has()` accepts the original object. To compare a Set draft with objects of the base state, call the method on `current(draft)`, in which unchanged objects keep their identity, or on `original(draft)` for the base state, or compare ids, for example `[...draft].filter((item) => ids.has(item.id))`. This applies to `union()`, `intersection()`, `difference()`, `symmetricDifference()`, `isSubsetOf()`, `isSupersetOf()` and `isDisjointFrom()`. +- Can a draft be a Map key? + +No. Map keys are used as they are, and a draft is revoked when its producer ends, so a Map that kept a draft as a key would hold a key that throws when it is read, and so would the patches that name it. Use `original(draft)`, `current(draft)` or an id as the key. Development builds throw when a recipe passes a draft to the `set()` method of a Map draft. + - Does Mutative support shared references? Yes, Mutative supports shared references, but **each path to a shared object gets its own independent draft**. Modifications to one path do not automatically reflect in others. If you want to preserve shared references in the result, you must explicitly assign them (e.g., `draft.b = draft.a`). [Read more details](https://mutative.js.org/docs/extra-topics/shared-references). @@ -787,6 +791,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - In strict mode, `rawReturn()` of a value without drafts no longer prints contradictory warnings. - With `enablePatches: { pathAsArray: false }`, development builds throw when a patch path would hold a Map key that is not a string, or a symbol key, as a string path cannot name such a key: applying the patch writes another key, for example `'1'` instead of `1`, or fails for a change below that key. Production builds still generate such patches, as v1 did, and still throw a `TypeError` for a symbol key. Keep the default array paths for such keys. - With `enableAutoFreeze`, development builds report a circular reference only for an object that holds itself. A changed draft that holds its original, as `draft.prev = original(draft)` makes it, or that links to a shared object through another path no longer throws; production builds always froze such states. For a real cycle, the error names the path of the object that repeats, which can be one key longer than in v1. +- Development builds throw when a recipe passes a draft to the `set()` method of a Map draft, as in `draft.map.set(draft.key, value)`: Map keys are used as they are, so the next state would keep the draft as a key, revoked, and reading it would throw. Production builds still keep such a key, as v1 did. Use `original(draft)`, `current(draft)` or an id as the key. ### TypeScript diff --git a/src/map.ts b/src/map.ts index aa54cd94..49c2c6a6 100644 --- a/src/map.ts +++ b/src/map.ts @@ -54,6 +54,13 @@ export const mapHandler = { return latest(getProxyDraft(this)!).has(key); }, set(key: any, value: any) { + // Map keys are used as they are, and a draft is revoked when its producer + // ends, so a Map would keep a key that throws when it is read. + if (__DEV__ && getProxyDraft(key)) { + throw new Error( + `A draft cannot be a Map key: its producer revokes it when it ends. Use original(key), current(key) or an id as the key; see https://mutative.js.org/docs/extra-topics/faq` + ); + } const target = getProxyDraft(this)!; const source = latest(target); if (!source.has(key) || !isEqual(source.get(key), value)) { diff --git a/test/map-draft-keys.test.ts b/test/map-draft-keys.test.ts new file mode 100644 index 00000000..b96f0c30 --- /dev/null +++ b/test/map-draft-keys.test.ts @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { create, current, original } from '../src'; + +const message = + 'A draft cannot be a Map key: its producer revokes it when it ends. Use original(key), current(key) or an id as the key; see https://mutative.js.org/docs/extra-topics/faq'; + +test.each([{}, { enablePatches: true }, { enableAutoFreeze: true }])( + 'a draft cannot be a Map key (%o)', + (options) => { + expect(() => + create( + { key: { id: 1 }, map: new Map() }, + (draft) => { + draft.map.set(draft.key, 'value'); + }, + options + ) + ).toThrow(message); + } +); + +test('a draft of an outer producer cannot be a Map key', () => { + expect(() => + create({ key: { id: 1 } }, (outer) => { + create(new Map(), (draft) => { + draft.set(outer.key, 'value'); + }); + }) + ).toThrow(message); +}); + +test('the original or a snapshot of a draft can be a Map key', () => { + const base = { key: { id: 1 }, map: new Map() }; + const state = create(base, (draft) => { + draft.map.set(original(draft.key), 'original'); + draft.key.id = 2; + draft.map.set(current(draft.key), 'current'); + }); + expect(state.map.get(base.key)).toBe('original'); + expect([...state.map.keys()]).toEqual([{ id: 1 }, { id: 2 }]); + expect(state.key).toEqual({ id: 2 }); +}); diff --git a/test/production.test.ts b/test/production.test.ts index a4bb9181..c2f42b3a 100644 --- a/test/production.test.ts +++ b/test/production.test.ts @@ -56,6 +56,16 @@ test('patch, original() and rawReturn() errors carry their minified code', () => ).toThrow(minified(21)); }); +test('production builds skip the check of drafts used as Map keys', () => { + const { create } = mutative; + // Development builds throw here; production builds keep the key, as v1 did. + expect(() => + create({ key: { id: 1 }, map: new Map() }, (draft) => { + draft.map.set(draft.key, 'value'); + }) + ).not.toThrow(); +}); + test('auto-freeze passes over primitives in Maps, Sets, and arrays', () => { const { create } = mutative; const state = create( diff --git a/website/content/docs/advanced-guides/migration-v2.mdx b/website/content/docs/advanced-guides/migration-v2.mdx index 6c737bc9..326d19bc 100644 --- a/website/content/docs/advanced-guides/migration-v2.mdx +++ b/website/content/docs/advanced-guides/migration-v2.mdx @@ -36,6 +36,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - In strict mode, `rawReturn()` of a value without drafts no longer prints contradictory warnings. - With `enablePatches: { pathAsArray: false }`, development builds throw when a patch path would hold a Map key that is not a string, or a symbol key, as a string path cannot name such a key: applying the patch writes another key, for example `'1'` instead of `1`, or fails for a change below that key. Production builds still generate such patches, as v1 did, and still throw a `TypeError` for a symbol key. Keep the default array paths for such keys. - With `enableAutoFreeze`, development builds report a circular reference only for an object that holds itself. A changed draft that holds its original, as `draft.prev = original(draft)` makes it, or that links to a shared object through another path no longer throws; production builds always froze such states. For a real cycle, the error names the path of the object that repeats, which can be one key longer than in v1. +- Development builds throw when a recipe passes a draft to the `set()` method of a Map draft, as in `draft.map.set(draft.key, value)`: Map keys are used as they are, so the next state would keep the draft as a key, revoked, and reading it would throw. Production builds still keep such a key, as v1 did. Use `original(draft)`, `current(draft)` or an id as the key. ## TypeScript diff --git a/website/content/docs/extra-topics/faq.mdx b/website/content/docs/extra-topics/faq.mdx index 29a9a73c..ae092233 100644 --- a/website/content/docs/extra-topics/faq.mdx +++ b/website/content/docs/extra-topics/faq.mdx @@ -28,6 +28,10 @@ Yes. Deleting the entry being visited and changing the values of other entries w Yes, but they compare elements as iterating the draft returns them. An object of the base state is a draft there, so these methods do not match it with the original object, although `has()` accepts the original object. To compare a Set draft with objects of the base state, call the method on `current(draft)`, in which unchanged objects keep their identity, or on `original(draft)` for the base state, or compare ids, for example `[...draft].filter((item) => ids.has(item.id))`. This applies to `union()`, `intersection()`, `difference()`, `symmetricDifference()`, `isSubsetOf()`, `isSupersetOf()` and `isDisjointFrom()`. +- Can a draft be a Map key? + +No. Map keys are used as they are, and a draft is revoked when its producer ends, so a Map that kept a draft as a key would hold a key that throws when it is read, and so would the patches that name it. Use `original(draft)`, `current(draft)` or an id as the key. Development builds throw when a recipe passes a draft to the `set()` method of a Map draft. + - Does Mutative support shared references? Yes, Mutative supports shared references, but **each path to a shared object gets its own independent draft**. Modifications to one path do not automatically reflect in others. If you want to preserve shared references in the result, you must explicitly assign them (e.g., `draft.b = draft.a`). [Read more details](./shared-references.mdx). From f59fc374bc1fe1c6fb6ed81d287873f24884b0cc Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:15:04 +0800 Subject: [PATCH 05/21] fix: type curried producers whose recipe annotates its draft and returns a new state --- README.md | 1 + src/makeCreator.ts | 2 +- test/types.test.ts | 27 +++++++++++++++++++ .../docs/advanced-guides/migration-v2.mdx | 1 + 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b33ddbf3..92b31080 100644 --- a/README.md +++ b/README.md @@ -797,6 +797,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - `create()` with an explicit state type and an async recipe, such as `create(base, async (draft) => { … })`, returns `Promise`, and so do curried producers; v1 typed the result as `State`. - `apply()` accepts `enableAutoFreeze: true` and then returns `Immutable`, as `apply()` does; v1 rejected the option and typed the frozen result as mutable. With `enableAutoFreeze` typed as `boolean` or optional `true`, the result is `State | Immutable`. With `mutable` typed as `boolean`, optional `boolean`, or optional `true`, `apply()` returns `State | void`. +- A curried producer whose recipe annotates its draft and returns a new state, such as `create((draft: State) => ({ ...draft, count: 0 }))`, returns `State`, or `Promise` for an async recipe. v1 typed such a call as a manual draft, so calling the producer did not compile. ### Fixes that change results diff --git a/src/makeCreator.ts b/src/makeCreator.ts index 5c772952..95d590f5 100644 --- a/src/makeCreator.ts +++ b/src/makeCreator.ts @@ -110,7 +110,7 @@ type MakeCreator = < P extends any[] = [], F extends boolean = _F, O extends PatchesOptions = _O, - R extends void | Promise = void, + R extends void | Promise | T | Promise = void, >( mutate: (draft: Draft, ...args: P) => R, options?: ExternalOptions diff --git a/test/types.test.ts b/test/types.test.ts index 57c0ad54..2af6abf7 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -3,6 +3,7 @@ import { apply, create, makeCreator, + type Draft, type Immutable, type Patches, } from '../src'; @@ -243,6 +244,32 @@ test('a sync curried recipe can return a new state or its draft', () => { expect(returnDraft(base()).count).toBe(2); }); +test('a curried recipe with an annotated draft can return a new state', async () => { + const increment = create((draft: State) => ({ + ...draft, + count: draft.count + 1, + })); + expectTypeOf(increment).toEqualTypeOf<(base: State) => State>(); + expect(increment(base())).toEqual({ count: 2, list: [1] }); + + const add = create((draft: Draft, by: number) => ({ + ...draft, + count: draft.count + by, + })); + expectTypeOf(add).toEqualTypeOf<(base: State, by: number) => State>(); + expect(add(base(), 2).count).toBe(3); + + const reset = create(async (draft: State) => ({ ...draft, count: 0 })); + expectTypeOf(reset).toEqualTypeOf<(base: State) => Promise>(); + expect((await reset(base())).count).toBe(0); + + const mutate = create((draft: State) => { + draft.count += 1; + }); + expectTypeOf(mutate).toEqualTypeOf<(base: State) => State>(); + expect(mutate(base()).count).toBe(2); +}); + test('generic helpers keep synchronous replacement results', () => { function update(state: T, recipe: (draft: T) => T): T { return create(state, recipe); diff --git a/website/content/docs/advanced-guides/migration-v2.mdx b/website/content/docs/advanced-guides/migration-v2.mdx index 326d19bc..4738f70f 100644 --- a/website/content/docs/advanced-guides/migration-v2.mdx +++ b/website/content/docs/advanced-guides/migration-v2.mdx @@ -42,6 +42,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - `create()` with an explicit state type and an async recipe, such as `create(base, async (draft) => { … })`, returns `Promise`, and so do curried producers; v1 typed the result as `State`. - `apply()` accepts `enableAutoFreeze: true` and then returns `Immutable`, as `apply()` does; v1 rejected the option and typed the frozen result as mutable. With `enableAutoFreeze` typed as `boolean` or optional `true`, the result is `State | Immutable`. With `mutable` typed as `boolean`, optional `boolean`, or optional `true`, `apply()` returns `State | void`. +- A curried producer whose recipe annotates its draft and returns a new state, such as `create((draft: State) => ({ ...draft, count: 0 }))`, returns `State`, or `Promise` for an async recipe. v1 typed such a call as a manual draft, so calling the producer did not compile. ## Fixes that change results From a37dda5f58cf3b5b83554ffff9c0c1dbf377b419 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:16:51 +0800 Subject: [PATCH 06/21] fix: type the results of explicit-state recipes that may return a Promise --- README.md | 1 + src/makeCreator.ts | 22 +++++++++++ test/types.test.ts | 39 +++++++++++++++++++ .../docs/advanced-guides/migration-v2.mdx | 1 + 4 files changed, 63 insertions(+) diff --git a/README.md b/README.md index 92b31080..1b24d943 100644 --- a/README.md +++ b/README.md @@ -798,6 +798,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - `create()` with an explicit state type and an async recipe, such as `create(base, async (draft) => { … })`, returns `Promise`, and so do curried producers; v1 typed the result as `State`. - `apply()` accepts `enableAutoFreeze: true` and then returns `Immutable`, as `apply()` does; v1 rejected the option and typed the frozen result as mutable. With `enableAutoFreeze` typed as `boolean` or optional `true`, the result is `State | Immutable`. With `mutable` typed as `boolean`, optional `boolean`, or optional `true`, `apply()` returns `State | void`. - A curried producer whose recipe annotates its draft and returns a new state, such as `create((draft: State) => ({ ...draft, count: 0 }))`, returns `State`, or `Promise` for an async recipe. v1 typed such a call as a manual draft, so calling the producer did not compile. +- With an explicit state type, a recipe that may or may not return a Promise, such as one typed `(draft: State) => void | Promise`, gives `State | Promise`, also in curried producers and creators from `makeCreator()`. v1 typed the result as `State`. ### Fixes that change results diff --git a/src/makeCreator.ts b/src/makeCreator.ts index 95d590f5..83a99eef 100644 --- a/src/makeCreator.ts +++ b/src/makeCreator.ts @@ -40,6 +40,10 @@ type ExplicitSyncReturn = [ExplicitState] extends [ ? AsyncRecipeContext : ExplicitReturn | AsyncRecipeContext; +type ExplicitMaybeAsyncReturn = + | ExplicitReturn + | Promise>; + type ExplicitRecipe = [T] extends [never] ? never : (draft: Draft>, ...args: P) => R; @@ -64,6 +68,12 @@ type MakeCreator = < mutate: ExplicitRecipe>>, options?: ExternalOptions ): Promise, O, F>>; + // A recipe that may or may not return a Promise gives either result. + ( + base: ExplicitState, + mutate: ExplicitRecipe>, + options?: ExternalOptions + ): Result, O, F> | Promise, O, F>>; < T extends any, F extends boolean = _F, @@ -105,6 +115,18 @@ type MakeCreator = < base: ExplicitState, ...args: P ) => Promise, O, F>>; + < + T = never, + P extends any[] = [], + F extends boolean = _F, + O extends PatchesOptions = _O, + >( + mutate: ExplicitRecipe>, + options?: ExternalOptions + ): ( + base: ExplicitState, + ...args: P + ) => Result, O, F> | Promise, O, F>>; < T extends any, P extends any[] = [], diff --git a/test/types.test.ts b/test/types.test.ts index 2af6abf7..2abf5fe1 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -57,6 +57,45 @@ test('a sync recipe keeps its result type with an explicit state type', () => { expect(replaced.count).toBe(5); }); +test('a recipe that may return a Promise gives either result with an explicit state type', async () => { + const recipe = (draft: State): void | Promise => { + if (draft.count > 1) { + return Promise.resolve().then(() => { + draft.count += 1; + }); + } + draft.count += 1; + }; + const now = create(base(), recipe); + expectTypeOf(now).toEqualTypeOf>(); + expect(now).toEqual({ count: 2, list: [1] }); + const later = create({ count: 2, list: [] }, recipe); + expect(later).toBeInstanceOf(Promise); + expect(await later).toEqual({ count: 3, list: [] }); + + const produce = create(recipe); + expectTypeOf(produce).toEqualTypeOf< + (base: State) => State | Promise + >(); + expect(await produce({ count: 2, list: [] })).toEqual({ count: 3, list: [] }); + + const withPatches = create(base(), recipe, { + enablePatches: true, + }); + expectTypeOf(withPatches).toEqualTypeOf< + | [State, Patches, Patches] + | Promise<[State, Patches, Patches]> + >(); + const [state, patches] = await withPatches; + expect(apply(base(), patches)).toEqual(state); + + const frozen = makeCreator({ enableAutoFreeze: true })(base(), recipe); + expectTypeOf(frozen).toEqualTypeOf< + Immutable | Promise> + >(); + expect(Object.isFrozen(await frozen)).toBe(true); +}); + test('explicit state types contextualize literal replacement values', async () => { type Status = { status: 'idle' | 'done'; count: number }; const source: Status = { status: 'idle', count: 0 }; diff --git a/website/content/docs/advanced-guides/migration-v2.mdx b/website/content/docs/advanced-guides/migration-v2.mdx index 4738f70f..805caaae 100644 --- a/website/content/docs/advanced-guides/migration-v2.mdx +++ b/website/content/docs/advanced-guides/migration-v2.mdx @@ -43,6 +43,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - `create()` with an explicit state type and an async recipe, such as `create(base, async (draft) => { … })`, returns `Promise`, and so do curried producers; v1 typed the result as `State`. - `apply()` accepts `enableAutoFreeze: true` and then returns `Immutable`, as `apply()` does; v1 rejected the option and typed the frozen result as mutable. With `enableAutoFreeze` typed as `boolean` or optional `true`, the result is `State | Immutable`. With `mutable` typed as `boolean`, optional `boolean`, or optional `true`, `apply()` returns `State | void`. - A curried producer whose recipe annotates its draft and returns a new state, such as `create((draft: State) => ({ ...draft, count: 0 }))`, returns `State`, or `Promise` for an async recipe. v1 typed such a call as a manual draft, so calling the producer did not compile. +- With an explicit state type, a recipe that may or may not return a Promise, such as one typed `(draft: State) => void | Promise`, gives `State | Promise`, also in curried producers and creators from `makeCreator()`. v1 typed the result as `State`. ## Fixes that change results From b56333bab7e298192d110949459cb8110f8d4f97 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:17:31 +0800 Subject: [PATCH 07/21] docs: state that a Set that apply() changes can hold its elements in another order --- README.md | 2 +- website/content/docs/advanced-guides/pathes.mdx | 2 +- website/content/docs/extra-topics/comparison-with-immer.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1b24d943..4f4f32f9 100644 --- a/README.md +++ b/README.md @@ -391,7 +391,7 @@ expect(baseState).toEqual({ foo: { bar: "test2" } }); > ⚠️Note: The mutable option cannot be combined with other options. When using mutable option, apply() will return void instead of a new state. As it changes the state in place, it throws for a patch that replaces the root state with another value, such as the patch of a recipe that returns a new state. -> Patches add and remove Set elements by value, also a changed element of a Set that added or removed elements, and `apply()` copies patch values, so inverse patches cannot remove an object that `apply()` added to a Set, as in an undo after a redo. See [Sets of objects](https://mutative.js.org/docs/advanced-guides/pathes#sets-of-objects). +> Patches add and remove Set elements by value, also a changed element of a Set that added or removed elements, and `apply()` copies patch values, so inverse patches cannot remove an object that `apply()` added to a Set, as in an undo after a redo. `apply()` also appends the elements that it adds, so such a Set can hold its elements in another order, and the patches of a later recipe, which find Set elements by position, can then change another element. See [Sets of objects](https://mutative.js.org/docs/advanced-guides/pathes#sets-of-objects). ### `current()` diff --git a/website/content/docs/advanced-guides/pathes.mdx b/website/content/docs/advanced-guides/pathes.mdx index 2549934b..b13a146a 100644 --- a/website/content/docs/advanced-guides/pathes.mdx +++ b/website/content/docs/advanced-guides/pathes.mdx @@ -91,7 +91,7 @@ expect(inversePatches).toMatchInlineSnapshot(` ## Sets of objects -Patches add a Set element by its value and remove one by the value it holds, so a `remove` patch removes an object only from a Set that holds that same object. A Set whose elements changed in place, without any element added or removed, gets patches under the position of each changed element, and they replay in both directions. Once a recipe adds or removes elements of a Set, the Set's `remove` and `add` patches carry every changed element by value instead. `apply()` copies the value of each patch before applying it, so a Set that `apply()` changes holds copies of the objects that patches add, and the inverse patches cannot remove those copies: an undo after a redo leaves the object in the Set. Patches for Sets of primitives are not affected. When patches are replayed on states that `apply()` returned, as undo and redo do, keep such objects in a Map keyed by an id or in an array instead. +Patches add a Set element by its value and remove one by the value it holds, so a `remove` patch removes an object only from a Set that holds that same object. A Set whose elements changed in place, without any element added or removed, gets patches under the position of each changed element, and they replay in both directions. Once a recipe adds or removes elements of a Set, the Set's `remove` and `add` patches carry every changed element by value instead. `apply()` copies the value of each patch before applying it, so a Set that `apply()` changes holds copies of the objects that patches add, and the inverse patches cannot remove those copies: an undo after a redo leaves the object in the Set. `apply()` also appends every element that it adds, so a Set that it changes can hold its elements in another order than the state that the recipe produced: a changed element of a Set that also added or removed elements, or an element that inverse patches add back, moves to the end, and a recipe that deletes an element and adds it back moves it to the end without a patch. The positions in the patches of a later recipe then find other elements, and its changes under them apply to another element without an error. Sets of primitives keep their elements, but their order can change in the same way. When patches are replayed on states that `apply()` returned, as undo, redo and synchronized copies do, keep such objects in a Map keyed by an id or in an array instead. ## JSON patches diff --git a/website/content/docs/extra-topics/comparison-with-immer.mdx b/website/content/docs/extra-topics/comparison-with-immer.mdx index 8c6fb3b4..4f4bff94 100644 --- a/website/content/docs/extra-topics/comparison-with-immer.mdx +++ b/website/content/docs/extra-topics/comparison-with-immer.mdx @@ -66,7 +66,7 @@ Both libraries emit the same patches for most changes: the same operations, path - **Changed items of a Set.** When a recipe changes objects in a Set without adding or removing items, Mutative emits a `replace` patch under the position of each changed item, while Immer removes the old object and adds the new one. Once the recipe adds or removes items, both carry the changed items by value. - **String paths.** With `enablePatches: { pathAsArray: false }`, Mutative emits paths as JSON Pointer strings, as JSON Patch requires, while Immer's paths are always arrays. A string path cannot name a Map key that is not a string, or a symbol key. - **Applying patches.** `apply()` takes the `enableAutoFreeze`, `strict` and `mark` options, or `mutable: true` to change the state in place and return `undefined`. -- **Alike in both.** `apply()` and `applyPatches()` change a draft in place, copy patch values before applying them, accept `-` as the index that appends to an array, and reject `move`, `copy` and `test` operations and paths through `__proto__` or `constructor`. As they copy patch values, an object that patches add to a Set is a copy, which the inverse patches cannot remove, so an undo after a redo leaves it in the Set in both libraries; see [Sets of objects](/docs/advanced-guides/pathes#sets-of-objects). +- **Alike in both.** `apply()` and `applyPatches()` change a draft in place, copy patch values before applying them, accept `-` as the index that appends to an array, and reject `move`, `copy` and `test` operations and paths through `__proto__` or `constructor`. As they copy patch values, an object that patches add to a Set is a copy, which the inverse patches cannot remove, so an undo after a redo leaves it in the Set in both libraries, and both append the elements that patches add, so such a Set can hold its elements in another order; see [Sets of objects](/docs/advanced-guides/pathes#sets-of-objects). ## Immer behaviors that Mutative handles From 91accaba444392f7a4cb316e888b9ddbb8caf3e1 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:17:52 +0800 Subject: [PATCH 08/21] docs: keep drafts of one create() call out of the drafts of another --- README.md | 2 ++ website/content/docs/api-reference/create.mdx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 4f4f32f9..578b6c43 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,8 @@ On a draft, `rename()` returns the draft itself, so the outer recipe changes its Where that does not fit, make such changes in the helper's recipe, or through the outer draft before calling the helper. If the helper's result must not share objects with the base state, give `create()` a deep copy, as in `create(structuredClone(current(draft)), recipe)`, at the cost of copying the draft on each call and of new references for the values that the recipe leaves unchanged; `structuredClone` turns class instances into plain objects and throws on functions. To catch such writes, enable `enableAutoFreeze` in development, for example with `makeCreator({ enableAutoFreeze: process.env.NODE_ENV !== 'production' })` for such helpers: it freezes the helper's result together with the objects of the base state that it shares, so a write to them throws instead of changing the base state, as Immer's default auto-freeze does. +A draft belongs to the `create()` call that made it, also the draft that `create(base)` returns. Do not store it in the draft of another call, or in an object that you assign there: it is revoked when its own call ends, so the other call's result can hold a draft that throws when it is read, and a Set draft that received it may no longer find it with `has()` or `delete()`. Store `current(draft)`, the original object or the result of that call instead. + ### `apply()` Use `apply()` for applying patches to get the new state. diff --git a/website/content/docs/api-reference/create.mdx b/website/content/docs/api-reference/create.mdx index 785f7b50..8c644a30 100644 --- a/website/content/docs/api-reference/create.mdx +++ b/website/content/docs/api-reference/create.mdx @@ -108,3 +108,5 @@ function rename(item: Item, name: string): Item { On a draft, `rename()` returns the draft itself, so the outer recipe changes its result through the outer draft, and the values that it leaves unchanged keep their references. Unlike a call to `create()`, it changes the outer draft even if the caller discards the result. Where that does not fit, make such changes in the helper's recipe, or through the outer draft before calling the helper. If the helper's result must not share objects with the base state, give `create()` a deep copy, as in `create(structuredClone(current(draft)), recipe)`, at the cost of copying the draft on each call and of new references for the values that the recipe leaves unchanged; `structuredClone` turns class instances into plain objects and throws on functions. To catch such writes, enable `enableAutoFreeze` in development, for example with `makeCreator({ enableAutoFreeze: process.env.NODE_ENV !== 'production' })` for such helpers: it freezes the helper's result together with the objects of the base state that it shares, so a write to them throws instead of changing the base state, as Immer's default auto-freeze does. + +A draft belongs to the `create()` call that made it, also the draft that `create(base)` returns. Do not store it in the draft of another call, or in an object that you assign there: it is revoked when its own call ends, so the other call's result can hold a draft that throws when it is read, and a Set draft that received it may no longer find it with `has()` or `delete()`. Store `current(draft)`, the original object or the result of that call instead. From 12dd8db37b2846eea2e6f9dfbfb46342474cee34 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:18:46 +0800 Subject: [PATCH 09/21] docs: describe how Mutative copies instances that a mark makes draftable --- README.md | 1 + website/content/docs/advanced-guides/mark.mdx | 4 ++++ website/content/docs/api-reference/create.mdx | 1 + website/content/docs/extra-topics/comparison-with-immer.mdx | 2 +- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 578b6c43..a39de439 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,7 @@ The recipe can be an async function: `create()` then returns a Promise of the re > When the mark function is (target) => 'immutable', it means all the objects in the state structure are immutable. In this specific case, you can totally turn on `AutoFreeze` and `Patches`. > `mark` supports multiple marks, and the marks are executed in order, and the first mark that returns a value will be used. > When a object tree node is marked by the `mark` function as `mutable`, all of its child nodes will also not be drafted by Mutative and will retain their original values. + > An instance that `mark` makes `immutable` is copied with all its own properties, and those that are enumerable and writable are assigned, so an own property that shadows a setter of its class runs it, and one that shadows a getter or a read-only property throws; see [Copying marked instances](https://mutative.js.org/docs/advanced-guides/mark#copying-marked-instances). #### `create()` - Currying diff --git a/website/content/docs/advanced-guides/mark.mdx b/website/content/docs/advanced-guides/mark.mdx index c7ba8714..4d89db8d 100644 --- a/website/content/docs/advanced-guides/mark.mdx +++ b/website/content/docs/advanced-guides/mark.mdx @@ -55,3 +55,7 @@ expect(nextState.fooBar).not.toBe(baseState.fooBar); Set a mark to determine if the value is mutable or if an instance is an immutable, and it can also return a shallow copy function(AutoFreeze and Patches should both be disabled, Some patches operation might not be equivalent). When the mark function is (target) => 'immutable', it means all the objects in the state structure are immutable. In this specific case, you can totally turn on AutoFreeze and Patches. mark supports multiple marks, and the marks are executed in order, and the first mark that returns a value will be used. + +## Copying marked instances + +Mutative copies an instance that `mark` makes `immutable` with all its own properties, non-enumerable ones included, and reads its own accessors as values, as Immer's `setUseStrictShallowCopy('class_only')` does. To copy quickly, it assigns the own properties that are enumerable, writable and configurable instead of defining them, as Immer's default copy does. An own property that shadows a setter of its class, such as a class field over an accessor of a base class, therefore runs that setter on the copy, which can change objects that the copy shares with the base state, and one that shadows a getter without a setter or a read-only property makes the copy throw. Keep such values in accessors of the class or under other keys. diff --git a/website/content/docs/api-reference/create.mdx b/website/content/docs/api-reference/create.mdx index 8c644a30..3866908c 100644 --- a/website/content/docs/api-reference/create.mdx +++ b/website/content/docs/api-reference/create.mdx @@ -52,6 +52,7 @@ Then options is optional. > Set a mark to determine if the value is mutable or if an instance is an immutable, and it can also return a shallow copy function(`AutoFreeze` and `Patches` should both be disabled, Some patches operation might not be equivalent). > When the mark function is (target) => 'immutable', it means all the objects in the state structure are immutable. In this specific case, you can totally turn on `AutoFreeze` and `Patches`. > `mark` supports multiple marks, and the marks are executed in order, and the first mark that returns a value will be used. + > An instance that `mark` makes `immutable` is copied with all its own properties, and those that are enumerable and writable are assigned, so an own property that shadows a setter of its class runs it, and one that shadows a getter or a read-only property throws; see [Copying marked instances](/docs/advanced-guides/mark#copying-marked-instances). ## Currying diff --git a/website/content/docs/extra-topics/comparison-with-immer.mdx b/website/content/docs/extra-topics/comparison-with-immer.mdx index 4f4bff94..921592d2 100644 --- a/website/content/docs/extra-topics/comparison-with-immer.mdx +++ b/website/content/docs/extra-topics/comparison-with-immer.mdx @@ -45,7 +45,7 @@ Mutative and Immer work the same way: a recipe changes a draft, and the library - **Auto-freeze.** Mutative leaves results unfrozen unless `enableAutoFreeze` is set, while Immer freezes them by default. With freezing on, both freeze the whole result deeply, unchanged parts included, and freeze Map and Set instances so that their mutators throw. Mutative also freezes the objects that a Map uses as keys, which Immer leaves unfrozen. - **Options per call.** Each `create()` and `apply()` call takes its own options, and `makeCreator()` sets defaults, where Immer's `setAutoFreeze()` and its plugins change the module for every caller. Maps, Sets and patches work without the `enableMapSet()` and `enablePatches()` calls that Immer requires and throws without. -- **Marking.** A `mark` function decides how values are drafted: it can make class instances draftable without changing their class, where Immer requires an `[immerable]` property on it, keep values mutable so that the recipe changes them in place, or return a function that copies a value. Mutative copies a class instance that a mark makes draftable with all its own properties, non-enumerable ones included, as Immer does with `setUseStrictShallowCopy('class_only')`. Both copy only the enumerable properties of plain objects and read their getters as values. See [mark](/docs/advanced-guides/mark). +- **Marking.** A `mark` function decides how values are drafted: it can make class instances draftable without changing their class, where Immer requires an `[immerable]` property on it, keep values mutable so that the recipe changes them in place, or return a function that copies a value. Mutative copies a class instance that a mark makes draftable with all its own properties, non-enumerable ones included, as Immer does with `setUseStrictShallowCopy('class_only')`, but assigns those that are enumerable and writable, as Immer's default copy does, so an own property that shadows a setter of the class runs it; see [Copying marked instances](/docs/advanced-guides/mark#copying-marked-instances). Both copy only the enumerable properties of plain objects and read their getters as values. See [mark](/docs/advanced-guides/mark). - **Strict mode.** With `strict: true`, reading a value that the recipe could change without a draft, such as a class instance that is not marked, throws unless the read happens inside `unsafe()`. Immer has no such mode. See [strict mode](/docs/advanced-guides/strict-mode). ## Drafts From 8e137fe54b1201b1dde444ff258c667e547c8884 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:18:55 +0800 Subject: [PATCH 10/21] docs: describe the JSON Patch support of patches precisely --- README.md | 2 +- website/content/docs/intro.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a39de439..bb582f13 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Mutative's `create` includes patches, `Map`/`Set` support and the native array m - **Mutation makes immutable updates** - Immutable data structures supporting objects, arrays, Sets and Maps. - **High performance** - About 6.7x faster than Immer with each library's defaults, and faster than hand-written spreads in measured wide-object and large-array insertion workloads. - **Optional freezing state** - No freezing of immutable data by default. -- **Support for JSON Patch** - Full compliance with JSON Patch specification. +- **Support for JSON Patch** - Patches use the `add`, `remove` and `replace` operations of JSON Patch, and with `pathAsArray: false` and `arrayLengthAssignment: false`, those of objects and arrays are JSON Patch operations. - **Custom shallow copy** - Support for more types of immutable data. - **Support mark for immutable and mutable data** - Allows for non-invasive marking. - **Safer mutable data access in strict mode** - It brings more secure immutable updates. diff --git a/website/content/docs/intro.mdx b/website/content/docs/intro.mdx index 0edc052b..a384d7df 100644 --- a/website/content/docs/intro.mdx +++ b/website/content/docs/intro.mdx @@ -70,7 +70,7 @@ This is why Mutative was created. - **Mutation makes immutable updates** - Immutable data structures supporting objects, arrays, Sets and Maps. - **High performance** - About 6.7x faster than Immer out of the box, and faster than hand-written spreads when updating objects with thousands of keys or inserting at the front of large arrays. - **Optional freezing state** - No freezing of immutable data by default. -- **Support for JSON Patch** - Full compliance with JSON Patch specification. +- **Support for JSON Patch** - Patches use the `add`, `remove` and `replace` operations of JSON Patch, and with `pathAsArray: false` and `arrayLengthAssignment: false`, those of objects and arrays are JSON Patch operations. - **Custom shallow copy** - Support for more types of immutable data. - **Support mark for immutable and mutable data** - Allows for non-invasive marking. - **Safer mutable data access in strict mode** - It brings more secure immutable updates. From 080a20500efdd03828f63d0f023fc6d7458b3e98 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:19:03 +0800 Subject: [PATCH 11/21] ci: type-check the source and tests before publishing --- .github/workflows/npm-publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 395c8466..fe9dc76c 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -18,6 +18,7 @@ jobs: node-version: '22.x' registry-url: 'https://registry.npmjs.org' - run: pnpm install --frozen-lockfile + - run: pnpm type-check - run: pnpm build - run: pnpm size - run: pnpm test:package From c83cc96964e19e96f90072d746b25c9c776aa696 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:24:59 +0800 Subject: [PATCH 12/21] docs: regenerate the API reference --- docs/README.md | 10 +- docs/functions/apply.md | 266 +++++++++++++++- docs/functions/castDraft.md | 16 +- docs/functions/castImmutable.md | 16 +- docs/functions/castMutable.md | 16 +- docs/functions/create.md | 238 --------------- docs/functions/current.md | 34 ++- docs/functions/isDraft.md | 12 +- docs/functions/isDraftable.md | 20 +- docs/functions/makeCreator.md | 144 --------- docs/functions/markSimpleObject.md | 12 +- docs/functions/original.md | 16 +- docs/functions/rawReturn.md | 16 +- docs/functions/unsafe.md | 16 +- docs/interfaces/Options.md | 37 ++- docs/type-aliases/Draft.md | 12 +- docs/type-aliases/DraftedObject.md | 17 ++ docs/type-aliases/Immutable.md | 12 +- docs/type-aliases/Patch.md | 12 +- docs/type-aliases/Patches.md | 12 +- docs/type-aliases/PatchesOptions.md | 24 +- docs/variables/create.md | 456 ++++++++++++++++++++++++++++ docs/variables/makeCreator.md | 34 +++ 23 files changed, 925 insertions(+), 523 deletions(-) delete mode 100644 docs/functions/create.md delete mode 100644 docs/functions/makeCreator.md create mode 100644 docs/type-aliases/DraftedObject.md create mode 100644 docs/variables/create.md create mode 100644 docs/variables/makeCreator.md diff --git a/docs/README.md b/docs/README.md index a9949e56..a176f12e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -**mutative** • **Docs** +**mutative** *** @@ -11,22 +11,26 @@ ## Type Aliases - [Draft](type-aliases/Draft.md) +- [DraftedObject](type-aliases/DraftedObject.md) - [Immutable](type-aliases/Immutable.md) - [Patch](type-aliases/Patch.md) - [Patches](type-aliases/Patches.md) - [PatchesOptions](type-aliases/PatchesOptions.md) +## Variables + +- [create](variables/create.md) +- [makeCreator](variables/makeCreator.md) + ## Functions - [apply](functions/apply.md) - [castDraft](functions/castDraft.md) - [castImmutable](functions/castImmutable.md) - [castMutable](functions/castMutable.md) -- [create](functions/create.md) - [current](functions/current.md) - [isDraft](functions/isDraft.md) - [isDraftable](functions/isDraftable.md) -- [makeCreator](functions/makeCreator.md) - [markSimpleObject](functions/markSimpleObject.md) - [original](functions/original.md) - [rawReturn](functions/rawReturn.md) diff --git a/docs/functions/apply.md b/docs/functions/apply.md index 79ce647b..2586733d 100644 --- a/docs/functions/apply.md +++ b/docs/functions/apply.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -6,7 +6,127 @@ # Function: apply() -> **apply**\<`T`, `F`\>(`state`, `patches`, `applyOptions`?): `T` \| `F` *extends* `true` ? [`Immutable`](../type-aliases/Immutable.md)\<`T`\> : `T` +## Call Signature + +> **apply**\<`T`, `F`, `_A`\>(`state`, `patches`, `applyOptions?`): `ApplyState`\<`T`, `F`, `undefined`\> + +Defined in: [apply.ts:62](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/apply.ts#L62) + +`apply(state, patches)` to apply patches to state + +## Example + +```ts +import { create, apply } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const [state, patches] = create( + baseState, + (draft) => { + draft.foo.bar = 'str2'; + }, + { enablePatches: true } +); +expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); +expect(patches).toEqual([{ op: 'replace', path: ['foo', 'bar'], value: 'str2' }]); +expect(state).toEqual(apply(baseState, patches)); +``` + +### Type Parameters + +#### T + +`T` *extends* `object` + +#### F + +`F` *extends* `boolean` = `false` + +#### _A + +`_A` *extends* `undefined` \| `ApplyOptions`\<`boolean`\> = `ApplyImmutableOptions`\<`F`\> + +### Parameters + +#### state + +`T` + +#### patches + +[`Patches`](../type-aliases/Patches.md) + +#### applyOptions? + +`undefined` + +### Returns + +`ApplyState`\<`T`, `F`, `undefined`\> + +## Call Signature + +> **apply**\<`T`, `F`, `A`\>(`state`, `patches`, `applyOptions`): `ApplyResult`\<`T`, `F`, `A`\> + +Defined in: [apply.ts:71](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/apply.ts#L71) + +`apply(state, patches)` to apply patches to state + +## Example + +```ts +import { create, apply } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const [state, patches] = create( + baseState, + (draft) => { + draft.foo.bar = 'str2'; + }, + { enablePatches: true } +); +expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); +expect(patches).toEqual([{ op: 'replace', path: ['foo', 'bar'], value: 'str2' }]); +expect(state).toEqual(apply(baseState, patches)); +``` + +### Type Parameters + +#### T + +`T` *extends* `object` + +#### F + +`F` *extends* `boolean` = `false` + +#### A + +`A` *extends* `undefined` \| `ApplyOptions`\<`boolean`\> = `ApplyImmutableOptions`\<`F`\> + +### Parameters + +#### state + +`T` + +#### patches + +[`Patches`](../type-aliases/Patches.md) + +#### applyOptions + +`A` + +### Returns + +`ApplyResult`\<`T`, `F`, `A`\> + +## Call Signature + +> **apply**\<`T`, `F`, `A`\>(`state`, `patches`, `applyOptions?`): `ApplyResult`\<`T`, `F`, `undefined` \| `A`\> + +Defined in: [apply.ts:78](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/apply.ts#L78) `apply(state, patches)` to apply patches to state @@ -28,24 +148,144 @@ expect(patches).toEqual([{ op: 'replace', path: ['foo', 'bar'], value: 'str2' }] expect(state).toEqual(apply(baseState, patches)); ``` -## Type Parameters +### Type Parameters + +#### T + +`T` *extends* `object` + +#### F + +`F` *extends* `boolean` = `false` + +#### A + +`A` *extends* `undefined` \| `ApplyOptions`\<`boolean`\> = `ApplyImmutableOptions`\<`F`\> + +### Parameters + +#### state + +`T` + +#### patches + +[`Patches`](../type-aliases/Patches.md) + +#### applyOptions? + +`A` + +### Returns + +`ApplyResult`\<`T`, `F`, `undefined` \| `A`\> + +## Call Signature + +> **apply**\<`T`, `F`\>(`state`, `patches`, `applyOptions`): `void` + +Defined in: [apply.ts:89](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/apply.ts#L89) + +`apply(state, patches)` to apply patches to state + +## Example + +```ts +import { create, apply } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const [state, patches] = create( + baseState, + (draft) => { + draft.foo.bar = 'str2'; + }, + { enablePatches: true } +); +expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); +expect(patches).toEqual([{ op: 'replace', path: ['foo', 'bar'], value: 'str2' }]); +expect(state).toEqual(apply(baseState, patches)); +``` + +### Type Parameters + +#### T + +`T` *extends* `object` + +#### F + +`F` *extends* `boolean` = `false` + +### Parameters + +#### state + +`T` + +#### patches + +[`Patches`](../type-aliases/Patches.md) + +#### applyOptions + +##### mutable + +`true` + +### Returns + +`void` + +## Call Signature + +> **apply**\<`T`, `F`\>(`state`, `patches`, `applyOptions`): `ApplyResult`\<`T`, `F`, `undefined` \| `ApplyMutableOptions`\> + +Defined in: [apply.ts:94](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/apply.ts#L94) + +`apply(state, patches)` to apply patches to state + +## Example + +```ts +import { create, apply } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const [state, patches] = create( + baseState, + (draft) => { + draft.foo.bar = 'str2'; + }, + { enablePatches: true } +); +expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); +expect(patches).toEqual([{ op: 'replace', path: ['foo', 'bar'], value: 'str2' }]); +expect(state).toEqual(apply(baseState, patches)); +``` + +### Type Parameters + +#### T + +`T` *extends* `object` + +#### F -• **T** *extends* `object` +`F` *extends* `boolean` = `false` -• **F** *extends* `boolean` = `false` +### Parameters -## Parameters +#### state -• **state**: `T` +`T` -• **patches**: [`Patches`](../type-aliases/Patches.md) +#### patches -• **applyOptions?**: `Pick`\<`Options`\<`boolean`, `F`\>, `"mark"` \| `"strict"` \| `"enableAutoFreeze"`\> +[`Patches`](../type-aliases/Patches.md) -## Returns +#### applyOptions -`T` \| `F` *extends* `true` ? [`Immutable`](../type-aliases/Immutable.md)\<`T`\> : `T` +`undefined` | `ApplyMutableOptions` -## Defined in +### Returns -[apply.ts:26](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/apply.ts#L26) +`ApplyResult`\<`T`, `F`, `undefined` \| `ApplyMutableOptions`\> diff --git a/docs/functions/castDraft.md b/docs/functions/castDraft.md index bf1d6f55..c845d4ba 100644 --- a/docs/functions/castDraft.md +++ b/docs/functions/castDraft.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -8,20 +8,22 @@ > **castDraft**\<`T`\>(`value`): [`Draft`](../type-aliases/Draft.md)\<`T`\> +Defined in: [utils/cast.ts:6](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/cast.ts#L6) + Cast a value to an Draft type value. ## Type Parameters -• **T** +### T + +`T` ## Parameters -• **value**: `T` +### value + +`T` ## Returns [`Draft`](../type-aliases/Draft.md)\<`T`\> - -## Defined in - -[utils/cast.ts:6](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/cast.ts#L6) diff --git a/docs/functions/castImmutable.md b/docs/functions/castImmutable.md index f94f8664..a30a841d 100644 --- a/docs/functions/castImmutable.md +++ b/docs/functions/castImmutable.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -8,20 +8,22 @@ > **castImmutable**\<`T`\>(`value`): [`Immutable`](../type-aliases/Immutable.md)\<`T`\> +Defined in: [utils/cast.ts:13](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/cast.ts#L13) + Cast a value to an Immutable type value. ## Type Parameters -• **T** +### T + +`T` ## Parameters -• **value**: `T` +### value + +`T` ## Returns [`Immutable`](../type-aliases/Immutable.md)\<`T`\> - -## Defined in - -[utils/cast.ts:13](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/cast.ts#L13) diff --git a/docs/functions/castMutable.md b/docs/functions/castMutable.md index f620787a..3a748748 100644 --- a/docs/functions/castMutable.md +++ b/docs/functions/castMutable.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -8,20 +8,22 @@ > **castMutable**\<`T`\>(`draft`): `T` +Defined in: [utils/cast.ts:20](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/cast.ts#L20) + Cast a value to an Mutable type value. ## Type Parameters -• **T** +### T + +`T` ## Parameters -• **draft**: [`Draft`](../type-aliases/Draft.md)\<`T`\> +### draft + +[`Draft`](../type-aliases/Draft.md)\<`T`\> ## Returns `T` - -## Defined in - -[utils/cast.ts:20](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/cast.ts#L20) diff --git a/docs/functions/create.md b/docs/functions/create.md deleted file mode 100644 index 6fa1421c..00000000 --- a/docs/functions/create.md +++ /dev/null @@ -1,238 +0,0 @@ -[**mutative**](../README.md) • **Docs** - -*** - -[mutative](../README.md) / create - -# Function: create() - -`create(baseState, callback, options)` to create the next state - -## Example - -```ts -import { create } from '../index'; - -const baseState = { foo: { bar: 'str' }, arr: [] }; -const state = create( - baseState, - (draft) => { - draft.foo.bar = 'str2'; - }, -); - -expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); -expect(state).not.toBe(baseState); -expect(state.foo).not.toBe(baseState.foo); -expect(state.arr).toBe(baseState.arr); -``` - -## create(base, mutate, options) - -> **create**\<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options`?): `CreateResult`\<`T`, `O`, `F`, `R`\> - -`create(baseState, callback, options)` to create the next state - -## Example - -```ts -import { create } from '../index'; - -const baseState = { foo: { bar: 'str' }, arr: [] }; -const state = create( - baseState, - (draft) => { - draft.foo.bar = 'str2'; - }, -); - -expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); -expect(state).not.toBe(baseState); -expect(state.foo).not.toBe(baseState.foo); -expect(state.arr).toBe(baseState.arr); -``` - -### Type Parameters - -• **T** *extends* `unknown` - -• **F** *extends* `boolean` = `false` - -• **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` - -• **R** *extends* `unknown` = `void` - -### Parameters - -• **base**: `T` - -• **mutate** - -• **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> - -### Returns - -`CreateResult`\<`T`, `O`, `F`, `R`\> - -### Defined in - -[create.ts:25](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/create.ts#L25) - -## create(base, mutate, options) - -> **create**\<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options`?): `CreateResult`\<`T`, `O`, `F`, `R`\> - -`create(baseState, callback, options)` to create the next state - -## Example - -```ts -import { create } from '../index'; - -const baseState = { foo: { bar: 'str' }, arr: [] }; -const state = create( - baseState, - (draft) => { - draft.foo.bar = 'str2'; - }, -); - -expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); -expect(state).not.toBe(baseState); -expect(state.foo).not.toBe(baseState.foo); -expect(state.arr).toBe(baseState.arr); -``` - -### Type Parameters - -• **T** *extends* `unknown` - -• **F** *extends* `boolean` = `false` - -• **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` - -• **R** *extends* `void` \| `Promise`\<`void`\> = `void` - -### Parameters - -• **base**: `T` - -• **mutate** - -• **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> - -### Returns - -`CreateResult`\<`T`, `O`, `F`, `R`\> - -### Defined in - -[create.ts:25](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/create.ts#L25) - -## create(mutate, options) - -> **create**\<`T`, `P`, `F`, `O`, `R`\>(`mutate`, `options`?): (`base`, ...`args`) => `CreateResult`\<`T`, `O`, `F`, `R`\> - -`create(baseState, callback, options)` to create the next state - -## Example - -```ts -import { create } from '../index'; - -const baseState = { foo: { bar: 'str' }, arr: [] }; -const state = create( - baseState, - (draft) => { - draft.foo.bar = 'str2'; - }, -); - -expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); -expect(state).not.toBe(baseState); -expect(state.foo).not.toBe(baseState.foo); -expect(state.arr).toBe(baseState.arr); -``` - -### Type Parameters - -• **T** *extends* `unknown` - -• **P** *extends* `any`[] = [] - -• **F** *extends* `boolean` = `false` - -• **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` - -• **R** *extends* `void` \| `Promise`\<`void`\> = `void` - -### Parameters - -• **mutate** - -• **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> - -### Returns - -`Function` - -#### Parameters - -• **base**: `T` - -• ...**args**: `P` - -#### Returns - -`CreateResult`\<`T`, `O`, `F`, `R`\> - -### Defined in - -[create.ts:25](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/create.ts#L25) - -## create(base, options) - -> **create**\<`T`, `O`, `F`\>(`base`, `options`?): [[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>] - -`create(baseState, callback, options)` to create the next state - -## Example - -```ts -import { create } from '../index'; - -const baseState = { foo: { bar: 'str' }, arr: [] }; -const state = create( - baseState, - (draft) => { - draft.foo.bar = 'str2'; - }, -); - -expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); -expect(state).not.toBe(baseState); -expect(state.foo).not.toBe(baseState.foo); -expect(state.arr).toBe(baseState.arr); -``` - -### Type Parameters - -• **T** *extends* `unknown` - -• **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` - -• **F** *extends* `boolean` = `false` - -### Parameters - -• **base**: `T` - -• **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> - -### Returns - -[[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>] - -### Defined in - -[create.ts:25](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/create.ts#L25) diff --git a/docs/functions/current.md b/docs/functions/current.md index a676a874..ff79ecc7 100644 --- a/docs/functions/current.md +++ b/docs/functions/current.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -6,10 +6,12 @@ # Function: current() -## current(target) +## Call Signature > **current**\<`T`\>(`target`): `T` +Defined in: [current.ts:198](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/current.ts#L198) + `current(draft)` to get current state in the draft mutation function. ## Example @@ -29,31 +31,37 @@ const state = create( ### Type Parameters -• **T** *extends* `object` +#### T + +`T` *extends* `object` ### Parameters -• **target**: [`Draft`](../type-aliases/Draft.md)\<`T`\> +#### target + +[`Draft`](../type-aliases/Draft.md)\<`T`\> ### Returns `T` -### Defined in - -[current.ts:120](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/current.ts#L120) - -## current(target) +## Call Signature > **current**\<`T`\>(`target`): `T` +Defined in: [current.ts:200](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/current.ts#L200) + ### Type Parameters -• **T** *extends* `object` +#### T + +`T` *extends* `object` ### Parameters -• **target**: `T` +#### target + +`T` ### Returns @@ -62,7 +70,3 @@ const state = create( ### Deprecated You should call current only on `Draft` types. - -### Defined in - -[current.ts:122](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/current.ts#L122) diff --git a/docs/functions/isDraft.md b/docs/functions/isDraft.md index a04da94d..71d741e6 100644 --- a/docs/functions/isDraft.md +++ b/docs/functions/isDraft.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -8,16 +8,16 @@ > **isDraft**(`target`): `boolean` +Defined in: [utils/draft.ts:33](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/draft.ts#L33) + Check if the value is a draft ## Parameters -• **target**: `any` +### target + +`any` ## Returns `boolean` - -## Defined in - -[utils/draft.ts:12](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/draft.ts#L12) diff --git a/docs/functions/isDraftable.md b/docs/functions/isDraftable.md index 80487aec..1f806fdc 100644 --- a/docs/functions/isDraftable.md +++ b/docs/functions/isDraftable.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -6,22 +6,24 @@ # Function: isDraftable() -> **isDraftable**(`value`, `options`?): `boolean` +> **isDraftable**(`value`, `options?`): `boolean` + +Defined in: [utils/draft.ts:59](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/draft.ts#L59) Check if a value is draftable ## Parameters -• **value**: `any` +### value -• **options?** +`any` -• **options.mark?**: `Mark`\<`any`, `any`\> +### options? -## Returns +#### mark? -`boolean` +`Mark`\<`any`, `any`\> -## Defined in +## Returns -[utils/draft.ts:29](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/draft.ts#L29) +`boolean` diff --git a/docs/functions/makeCreator.md b/docs/functions/makeCreator.md deleted file mode 100644 index f8e92638..00000000 --- a/docs/functions/makeCreator.md +++ /dev/null @@ -1,144 +0,0 @@ -[**mutative**](../README.md) • **Docs** - -*** - -[mutative](../README.md) / makeCreator - -# Function: makeCreator() - -> **makeCreator**\<`_F`, `_O`\>(`options`?): \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options`?) => `CreateResult`\<`T`, `O`, `F`, `R`\>\<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options`?) => `CreateResult`\<`T`, `O`, `F`, `R`\>\<`T`, `P`, `F`, `O`, `R`\>(`mutate`, `options`?) => (`base`, ...`args`) => `CreateResult`\<`T`, `O`, `F`, `R`\>\<`T`, `O`, `F`\>(`base`, `options`?) => [[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>] - -`makeCreator(options)` to make a creator function. - -## Example - -```ts -import { makeCreator } from '../index'; - -const baseState = { foo: { bar: 'str' }, arr: [] }; -const create = makeCreator({ enableAutoFreeze: true }); -const state = create( - baseState, - (draft) => { - draft.foo.bar = 'str2'; - }, -); - -expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); -expect(state).not.toBe(baseState); -expect(state.foo).not.toBe(baseState.foo); -expect(state.arr).toBe(baseState.arr); -expect(Object.isFrozen(state)).toBeTruthy(); -``` - -## Type Parameters - -• **_F** *extends* `boolean` = `false` - -• **_O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` - -## Parameters - -• **options?**: [`Options`](../interfaces/Options.md)\<`_O`, `_F`\> - -## Returns - -`Function` - -### Type Parameters - -• **T** *extends* `unknown` - -• **F** *extends* `boolean` = `_F` - -• **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` - -• **R** *extends* `unknown` = `void` - -### Parameters - -• **base**: `T` - -• **mutate** - -• **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> - -### Returns - -`CreateResult`\<`T`, `O`, `F`, `R`\> - -### Type Parameters - -• **T** *extends* `unknown` - -• **F** *extends* `boolean` = `_F` - -• **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` - -• **R** *extends* `void` \| `Promise`\<`void`\> = `void` - -### Parameters - -• **base**: `T` - -• **mutate** - -• **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> - -### Returns - -`CreateResult`\<`T`, `O`, `F`, `R`\> - -### Type Parameters - -• **T** *extends* `unknown` - -• **P** *extends* `any`[] = [] - -• **F** *extends* `boolean` = `_F` - -• **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` - -• **R** *extends* `void` \| `Promise`\<`void`\> = `void` - -### Parameters - -• **mutate** - -• **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> - -### Returns - -`Function` - -#### Parameters - -• **base**: `T` - -• ...**args**: `P` - -#### Returns - -`CreateResult`\<`T`, `O`, `F`, `R`\> - -### Type Parameters - -• **T** *extends* `unknown` - -• **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` - -• **F** *extends* `boolean` = `_F` - -### Parameters - -• **base**: `T` - -• **options?**: [`Options`](../interfaces/Options.md)\<`O`, `F`\> - -### Returns - -[[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>] - -## Defined in - -[makeCreator.ts:87](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/makeCreator.ts#L87) diff --git a/docs/functions/markSimpleObject.md b/docs/functions/markSimpleObject.md index bca4a027..aacc1d63 100644 --- a/docs/functions/markSimpleObject.md +++ b/docs/functions/markSimpleObject.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -8,14 +8,14 @@ > **markSimpleObject**(`value`): `undefined` \| `"immutable"` +Defined in: [utils/marker.ts:26](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/marker.ts#L26) + ## Parameters -• **value**: `unknown` +### value + +`unknown` ## Returns `undefined` \| `"immutable"` - -## Defined in - -[utils/marker.ts:26](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/utils/marker.ts#L26) diff --git a/docs/functions/original.md b/docs/functions/original.md index 983585cf..aa3fb723 100644 --- a/docs/functions/original.md +++ b/docs/functions/original.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -8,6 +8,8 @@ > **original**\<`T`\>(`target`): `T` +Defined in: [original.ts:22](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/original.ts#L22) + `original(draft)` to get original state in the draft mutation function. ## Example @@ -27,16 +29,16 @@ const state = create( ## Type Parameters -• **T** +### T + +`T` ## Parameters -• **target**: `T` - -## Returns +### target `T` -## Defined in +## Returns -[original.ts:21](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/original.ts#L21) +`T` diff --git a/docs/functions/rawReturn.md b/docs/functions/rawReturn.md index 7deb7ced..ddd76ea9 100644 --- a/docs/functions/rawReturn.md +++ b/docs/functions/rawReturn.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -8,6 +8,8 @@ > **rawReturn**\<`T`\>(`value`): `T` +Defined in: [rawReturn.ts:22](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/rawReturn.ts#L22) + Use rawReturn() to wrap the return value to skip the draft check and thus improve performance. ## Example @@ -27,16 +29,16 @@ expect(state).toBe(baseState); ## Type Parameters -• **T** *extends* `undefined` \| `object` +### T -## Parameters +`T` *extends* `undefined` \| `object` -• **value**: `T` +## Parameters -## Returns +### value `T` -## Defined in +## Returns -[rawReturn.ts:21](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/rawReturn.ts#L21) +`T` diff --git a/docs/functions/unsafe.md b/docs/functions/unsafe.md index 1ba26366..374706f2 100644 --- a/docs/functions/unsafe.md +++ b/docs/functions/unsafe.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -8,6 +8,8 @@ > **unsafe**\<`T`\>(`callback`): `T` +Defined in: [unsafe.ts:57](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/unsafe.ts#L57) + `unsafe(callback)` to access mutable data directly in strict mode. ## Example @@ -39,16 +41,16 @@ expect(state.foobar.bar).toBe(2); ## Type Parameters -• **T** +### T + +`T` ## Parameters -• **callback** +### callback + +() => `T` ## Returns `T` - -## Defined in - -[unsafe.ts:53](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/unsafe.ts#L53) diff --git a/docs/interfaces/Options.md b/docs/interfaces/Options.md index ee648f05..66d142b5 100644 --- a/docs/interfaces/Options.md +++ b/docs/interfaces/Options.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -6,11 +6,17 @@ # Interface: Options\ +Defined in: [interface.ts:171](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L171) + ## Type Parameters -• **O** *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) +### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) -• **F** *extends* `boolean` +### F + +`F` *extends* `boolean` ## Properties @@ -18,11 +24,9 @@ > `optional` **enableAutoFreeze**: `F` -Enable autoFreeze, and return frozen state. - -#### Defined in +Defined in: [interface.ts:184](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L184) -[interface.ts:137](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L137) +Enable autoFreeze, and return frozen state. *** @@ -30,11 +34,9 @@ Enable autoFreeze, and return frozen state. > `optional` **enablePatches**: `O` -Enable patch, and return the patches and inversePatches. - -#### Defined in +Defined in: [interface.ts:180](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L180) -[interface.ts:133](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L133) +Enable patch, and return the patches and inversePatches. *** @@ -42,21 +44,18 @@ Enable patch, and return the patches and inversePatches. > `optional` **mark**: `Mark`\<`O`, `F`\> \| `Mark`\<`O`, `F`\>[] +Defined in: [interface.ts:189](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L189) + Set a mark to determine if the object is mutable or if an instance is an immutable. And it can also return a shallow copy function(AutoFreeze and Patches should both be disabled). -#### Defined in - -[interface.ts:142](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L142) - *** ### strict? > `optional` **strict**: `boolean` -In strict mode, Forbid accessing non-draftable values and forbid returning a non-draft value. - -#### Defined in +Defined in: [interface.ts:176](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L176) -[interface.ts:129](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L129) +In strict mode, Forbid accessing non-draftable values and forbid returning a non-draft value. +Development builds also warn once when a recipe leaves 1,000 or more drafts unchanged. diff --git a/docs/type-aliases/Draft.md b/docs/type-aliases/Draft.md index 927a4f25..c4d8ce0c 100644 --- a/docs/type-aliases/Draft.md +++ b/docs/type-aliases/Draft.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -6,12 +6,12 @@ # Type Alias: Draft\ -> **Draft**\<`T`\>: `T` *extends* `Primitive` \| `AtomicObject` ? `T` : `T` *extends* `IfAvailable`\<`ReadonlyMap`\\> ? `DraftedMap`\<`K`, `V`\> : `T` *extends* `IfAvailable`\<`ReadonlySet`\\> ? `DraftedSet`\<`V`\> : `T` *extends* `WeakReferences` ? `T` : `T` *extends* `object` ? `DraftedObject`\<`T`\> : `T` +> **Draft**\<`T`\> = `T` *extends* `Primitive` \| `AtomicObject` ? `T` : `T` *extends* `IfAvailable`\<`ReadonlyMap`\\> ? `DraftedMap`\<`K`, `V`\> : `T` *extends* `IfAvailable`\<`ReadonlySet`\\> ? `DraftedSet`\<`V`\> : `T` *extends* `WeakReferences` ? `T` : `T` *extends* `object` ? [`DraftedObject`](DraftedObject.md)\<`T`\> : `T` -## Type Parameters +Defined in: [interface.ts:229](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L229) -• **T** +## Type Parameters -## Defined in +### T -[interface.ts:182](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L182) +`T` diff --git a/docs/type-aliases/DraftedObject.md b/docs/type-aliases/DraftedObject.md new file mode 100644 index 00000000..f4c122b8 --- /dev/null +++ b/docs/type-aliases/DraftedObject.md @@ -0,0 +1,17 @@ +[**mutative**](../README.md) + +*** + +[mutative](../README.md) / DraftedObject + +# Type Alias: DraftedObject\ + +> **DraftedObject**\<`T`\> = `{ -readonly [K in keyof T]: Draft }` + +Defined in: [interface.ts:225](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L225) + +## Type Parameters + +### T + +`T` diff --git a/docs/type-aliases/Immutable.md b/docs/type-aliases/Immutable.md index e35687ad..ed41b409 100644 --- a/docs/type-aliases/Immutable.md +++ b/docs/type-aliases/Immutable.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -6,12 +6,12 @@ # Type Alias: Immutable\ -> **Immutable**\<`T`\>: `T` *extends* `Primitive` \| `AtomicObject` ? `T` : `T` *extends* `IfAvailable`\<`ReadonlyMap`\\> ? `ImmutableMap`\<`K`, `V`\> : `T` *extends* `IfAvailable`\<`ReadonlySet`\\> ? `ImmutableSet`\<`V`\> : `T` *extends* `WeakReferences` ? `T` : `T` *extends* `object` ? `ImmutableObject`\<`T`\> : `T` +> **Immutable**\<`T`\> = `T` *extends* `Primitive` \| `AtomicObject` ? `T` : `T` *extends* `IfAvailable`\<`ReadonlyMap`\\> ? `ImmutableMap`\<`K`, `V`\> : `T` *extends* `IfAvailable`\<`ReadonlySet`\\> ? `ImmutableSet`\<`V`\> : `T` *extends* `WeakReferences` ? `T` : `T` *extends* `object` ? `ImmutableObject`\<`T`\> : `T` -## Type Parameters +Defined in: [interface.ts:211](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L211) -• **T** +## Type Parameters -## Defined in +### T -[interface.ts:164](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L164) +`T` diff --git a/docs/type-aliases/Patch.md b/docs/type-aliases/Patch.md index 5656f241..02d6b418 100644 --- a/docs/type-aliases/Patch.md +++ b/docs/type-aliases/Patch.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -6,12 +6,12 @@ # Type Alias: Patch\ -> **Patch**\<`P`\>: `P` *extends* `object` ? `IPatch` & `object` : `P` *extends* `true` \| `object` ? `IPatch` & `object` : `IPatch` & `object` +> **Patch**\<`P`\> = `P` *extends* `object` ? `IPatch` & `object` : `P` *extends* `true` \| `object` ? `IPatch` & `object` : `IPatch` & `object` -## Type Parameters +Defined in: [interface.ts:96](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L96) -• **P** *extends* [`PatchesOptions`](PatchesOptions.md) = `any` +## Type Parameters -## Defined in +### P -[interface.ts:58](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L58) +`P` *extends* [`PatchesOptions`](PatchesOptions.md) = `any` diff --git a/docs/type-aliases/Patches.md b/docs/type-aliases/Patches.md index c58219ae..0e32c189 100644 --- a/docs/type-aliases/Patches.md +++ b/docs/type-aliases/Patches.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -6,12 +6,12 @@ # Type Alias: Patches\ -> **Patches**\<`P`\>: [`Patch`](Patch.md)\<`P`\>[] +> **Patches**\<`P`\> = [`Patch`](Patch.md)\<`P`\>[] -## Type Parameters +Defined in: [interface.ts:110](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L110) -• **P** *extends* [`PatchesOptions`](PatchesOptions.md) = `any` +## Type Parameters -## Defined in +### P -[interface.ts:72](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L72) +`P` *extends* [`PatchesOptions`](PatchesOptions.md) = `any` diff --git a/docs/type-aliases/PatchesOptions.md b/docs/type-aliases/PatchesOptions.md index a29045a2..c605a9a6 100644 --- a/docs/type-aliases/PatchesOptions.md +++ b/docs/type-aliases/PatchesOptions.md @@ -1,4 +1,4 @@ -[**mutative**](../README.md) • **Docs** +[**mutative**](../README.md) *** @@ -6,8 +6,24 @@ # Type Alias: PatchesOptions -> **PatchesOptions**: `boolean` \| `object` +> **PatchesOptions** = `boolean` \| \{ `arrayLengthAssignment?`: `boolean`; `pathAsArray?`: `boolean`; \} -## Defined in +Defined in: [interface.ts:18](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L18) -[interface.ts:18](https://github.com/unadlib/mutative/blob/7129237bc42b8475743ffff427a1f8f85e8e1e51/src/interface.ts#L18) +## Type Declaration + +`boolean` + +\{ `arrayLengthAssignment?`: `boolean`; `pathAsArray?`: `boolean`; \} + +### arrayLengthAssignment? + +> `optional` **arrayLengthAssignment**: `boolean` + +The default value is `true`. If it's `true`, the array length will be included in the patches, otherwise no include array length. + +### pathAsArray? + +> `optional` **pathAsArray**: `boolean` + +The default value is `true`. If it's `true`, the path will be an array, otherwise it is a string. diff --git a/docs/variables/create.md b/docs/variables/create.md new file mode 100644 index 00000000..d7798cd2 --- /dev/null +++ b/docs/variables/create.md @@ -0,0 +1,456 @@ +[**mutative**](../README.md) + +*** + +[mutative](../README.md) / create + +# Variable: create() + +> `const` **create**: \{\<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>; \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`\<`T`, `O`, `F`, `R`\>; \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`\<`T`, `O`, `F`, `R`\>; \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>; \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `P`, `F`, `O`, `R`\>(`mutate`, `options?`): (`base`, ...`args`) => `CreateResult`\<`T`, `O`, `F`, `R`\>; \<`T`, `O`, `F`\>(`base`, `options?`): \[[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>\]; \} + +Defined in: [create.ts:25](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/create.ts#L25) + +`create(baseState, callback, options)` to create the next state + +## Example + +```ts +import { create } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const state = create( + baseState, + (draft) => { + draft.foo.bar = 'str2'; + }, +); + +expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); +expect(state).not.toBe(baseState); +expect(state.foo).not.toBe(baseState.foo); +expect(state.arr).toBe(baseState.arr); +``` + +## Call Signature + +> \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> + +### Type Parameters + +#### T + +`T` = `never` + +#### F + +`F` *extends* `boolean` = `false` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` + +### Parameters + +#### base + +`ExplicitState`\<`T`\> + +#### mutate + +`ExplicitRecipe`\<`T`, \[\], `ExplicitSyncReturn`\<`T`\>\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> + +## Call Signature + +> \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +### Type Parameters + +#### T + +`T` = `never` + +#### F + +`F` *extends* `boolean` = `false` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` + +### Parameters + +#### base + +`ExplicitState`\<`T`\> + +#### mutate + +`ExplicitRecipe`\<`T`, \[\], `Promise`\<`ExplicitReturn`\<`T`\>\>\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +## Call Signature + +> \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +### Type Parameters + +#### T + +`T` = `never` + +#### F + +`F` *extends* `boolean` = `false` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` + +### Parameters + +#### base + +`ExplicitState`\<`T`\> + +#### mutate + +`ExplicitRecipe`\<`T`, \[\], `ExplicitMaybeAsyncReturn`\<`T`\>\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +## Call Signature + +> \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`\<`T`, `O`, `F`, `R`\> + +### Type Parameters + +#### T + +`T` *extends* `unknown` + +#### F + +`F` *extends* `boolean` = `false` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` + +#### R + +`R` *extends* `unknown` = `void` + +### Parameters + +#### base + +`T` + +#### mutate + +(`draft`) => `R` + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`CreateResult`\<`T`, `O`, `F`, `R`\> + +## Call Signature + +> \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`\<`T`, `O`, `F`, `R`\> + +### Type Parameters + +#### T + +`T` *extends* `unknown` + +#### F + +`F` *extends* `boolean` = `false` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` + +#### R + +`R` *extends* `void` \| `Promise`\<`void`\> = `void` + +### Parameters + +#### base + +`T` + +#### mutate + +(`draft`) => `R` + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`CreateResult`\<`T`, `O`, `F`, `R`\> + +## Call Signature + +> \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> + +### Type Parameters + +#### T + +`T` = `never` + +#### P + +`P` *extends* `any`[] = \[\] + +#### F + +`F` *extends* `boolean` = `false` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` + +### Parameters + +#### mutate + +`ExplicitRecipe`\<`T`, `P`, `ExplicitSyncReturn`\<`T`\>\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +> (`base`, ...`args`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> + +#### Parameters + +##### base + +`ExplicitState`\<`T`\> + +##### args + +...`P` + +#### Returns + +`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> + +## Call Signature + +> \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +### Type Parameters + +#### T + +`T` = `never` + +#### P + +`P` *extends* `any`[] = \[\] + +#### F + +`F` *extends* `boolean` = `false` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` + +### Parameters + +#### mutate + +`ExplicitRecipe`\<`T`, `P`, `Promise`\<`ExplicitReturn`\<`T`\>\>\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +> (`base`, ...`args`): `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +#### Parameters + +##### base + +`ExplicitState`\<`T`\> + +##### args + +...`P` + +#### Returns + +`Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +## Call Signature + +> \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +### Type Parameters + +#### T + +`T` = `never` + +#### P + +`P` *extends* `any`[] = \[\] + +#### F + +`F` *extends* `boolean` = `false` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` + +### Parameters + +#### mutate + +`ExplicitRecipe`\<`T`, `P`, `ExplicitMaybeAsyncReturn`\<`T`\>\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +> (`base`, ...`args`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +#### Parameters + +##### base + +`ExplicitState`\<`T`\> + +##### args + +...`P` + +#### Returns + +`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +## Call Signature + +> \<`T`, `P`, `F`, `O`, `R`\>(`mutate`, `options?`): (`base`, ...`args`) => `CreateResult`\<`T`, `O`, `F`, `R`\> + +### Type Parameters + +#### T + +`T` *extends* `unknown` + +#### P + +`P` *extends* `any`[] = \[\] + +#### F + +`F` *extends* `boolean` = `false` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` + +#### R + +`R` *extends* `unknown` = `void` + +### Parameters + +#### mutate + +(`draft`, ...`args`) => `R` + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +> (`base`, ...`args`): `CreateResult`\<`T`, `O`, `F`, `R`\> + +#### Parameters + +##### base + +`T` + +##### args + +...`P` + +#### Returns + +`CreateResult`\<`T`, `O`, `F`, `R`\> + +## Call Signature + +> \<`T`, `O`, `F`\>(`base`, `options?`): \[[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>\] + +### Type Parameters + +#### T + +`T` *extends* `unknown` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` + +#### F + +`F` *extends* `boolean` = `false` + +### Parameters + +#### base + +`T` + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +\[[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>\] diff --git a/docs/variables/makeCreator.md b/docs/variables/makeCreator.md new file mode 100644 index 00000000..d5738c3c --- /dev/null +++ b/docs/variables/makeCreator.md @@ -0,0 +1,34 @@ +[**mutative**](../README.md) + +*** + +[mutative](../README.md) / makeCreator + +# Variable: makeCreator + +> `const` **makeCreator**: `MakeCreator` + +Defined in: [makeCreator.ts:173](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/makeCreator.ts#L173) + +`makeCreator(options)` to make a creator function. + +## Example + +```ts +import { makeCreator } from '../index'; + +const baseState = { foo: { bar: 'str' }, arr: [] }; +const create = makeCreator({ enableAutoFreeze: true }); +const state = create( + baseState, + (draft) => { + draft.foo.bar = 'str2'; + }, +); + +expect(state).toEqual({ foo: { bar: 'str2' }, arr: [] }); +expect(state).not.toBe(baseState); +expect(state.foo).not.toBe(baseState.foo); +expect(state.arr).toBe(baseState.arr); +expect(Object.isFrozen(state)).toBeTruthy(); +``` From 8e7708e5b35da55e420e6e2e5968343e5b62bade Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:55:21 +0800 Subject: [PATCH 13/21] fix: keep the result types of synchronous recipes with an explicit primitive state type --- README.md | 2 +- src/makeCreator.ts | 27 ++++++++++---- test/types.test.ts | 37 +++++++++++++++++++ .../docs/advanced-guides/migration-v2.mdx | 2 +- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bb582f13..a70a53ca 100644 --- a/README.md +++ b/README.md @@ -801,7 +801,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - `create()` with an explicit state type and an async recipe, such as `create(base, async (draft) => { … })`, returns `Promise`, and so do curried producers; v1 typed the result as `State`. - `apply()` accepts `enableAutoFreeze: true` and then returns `Immutable`, as `apply()` does; v1 rejected the option and typed the frozen result as mutable. With `enableAutoFreeze` typed as `boolean` or optional `true`, the result is `State | Immutable`. With `mutable` typed as `boolean`, optional `boolean`, or optional `true`, `apply()` returns `State | void`. - A curried producer whose recipe annotates its draft and returns a new state, such as `create((draft: State) => ({ ...draft, count: 0 }))`, returns `State`, or `Promise` for an async recipe. v1 typed such a call as a manual draft, so calling the producer did not compile. -- With an explicit state type, a recipe that may or may not return a Promise, such as one typed `(draft: State) => void | Promise`, gives `State | Promise`, also in curried producers and creators from `makeCreator()`. v1 typed the result as `State`. +- With an explicit state type, a recipe that may or may not return a Promise, such as one typed `(draft: State) => void | Promise`, gives `State | Promise`, also in curried producers and creators from `makeCreator()`. v1 typed the result as `State`, as this version still does for a primitive state type, such as `number`. ### Fixes that change results diff --git a/src/makeCreator.ts b/src/makeCreator.ts index 83a99eef..890c8ad6 100644 --- a/src/makeCreator.ts +++ b/src/makeCreator.ts @@ -34,20 +34,31 @@ type AsyncRecipeContext = Promise> & { readonly __mutativeAsyncContext: never; }; -type ExplicitSyncReturn = [ExplicitState] extends [ +type IsExplicitPrimitive = [ExplicitState] extends [ string | number | bigint | boolean | symbol | null | undefined, ] - ? AsyncRecipeContext - : ExplicitReturn | AsyncRecipeContext; + ? true + : false; -type ExplicitMaybeAsyncReturn = - | ExplicitReturn - | Promise>; +type ExplicitSyncReturn = + IsExplicitPrimitive extends true + ? AsyncRecipeContext + : ExplicitReturn | AsyncRecipeContext; type ExplicitRecipe = [T] extends [never] ? never : (draft: Draft>, ...args: P) => R; +// A recipe that may or may not return a Promise. The synchronous recipes of a +// primitive state skip the first overload, which only gives async recipes +// their context, so this one would take them too: they keep the overloads +// that infer the return type, and a primitive state keeps its synchronous +// result type for such a recipe. +type ExplicitMaybeAsyncRecipe = + IsExplicitPrimitive extends true + ? never + : ExplicitRecipe | Promise>>; + type MakeCreator = < _F extends boolean = false, _O extends PatchesOptions = false, @@ -71,7 +82,7 @@ type MakeCreator = < // A recipe that may or may not return a Promise gives either result. ( base: ExplicitState, - mutate: ExplicitRecipe>, + mutate: ExplicitMaybeAsyncRecipe, options?: ExternalOptions ): Result, O, F> | Promise, O, F>>; < @@ -121,7 +132,7 @@ type MakeCreator = < F extends boolean = _F, O extends PatchesOptions = _O, >( - mutate: ExplicitRecipe>, + mutate: ExplicitMaybeAsyncRecipe, options?: ExternalOptions ): ( base: ExplicitState, diff --git a/test/types.test.ts b/test/types.test.ts index 2abf5fe1..e162a09a 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -96,6 +96,43 @@ test('a recipe that may return a Promise gives either result with an explicit st expect(Object.isFrozen(await frozen)).toBe(true); }); +test('synchronous recipes of a primitive state keep their result type', () => { + const count = create(0, (value) => value + 1); + expectTypeOf(count).toEqualTypeOf(); + expect(count).toBe(1); + + const [next, patches] = create(0, (value) => value + 1, { + enablePatches: true, + }); + expectTypeOf(next).toEqualTypeOf(); + expect(next).toBe(1); + expect(patches).toEqual([{ op: 'replace', path: [], value: 1 }]); + + const unchanged = create('a', () => {}); + expectTypeOf(unchanged).toEqualTypeOf(); + expect(unchanged).toBe('a'); + + const toggle = create((value) => !value); + expectTypeOf(toggle).toEqualTypeOf<(base: boolean) => boolean>(); + expect(toggle(true)).toBe(false); + + const add = create((value, by) => value + by); + expectTypeOf(add).toEqualTypeOf<(base: number, by: number) => number>(); + expect(add(1, 2)).toBe(3); + + const frozen = makeCreator({ enableAutoFreeze: true })( + 0, + (value) => value + 1 + ); + expectTypeOf(frozen).toEqualTypeOf(); + expect(frozen).toBe(1); + + // A recipe that may return a Promise keeps the synchronous type here. + const maybe = (value: number): number | Promise => + value > 0 ? Promise.resolve(value) : value + 1; + expectTypeOf(create(0, maybe)).toEqualTypeOf(); +}); + test('explicit state types contextualize literal replacement values', async () => { type Status = { status: 'idle' | 'done'; count: number }; const source: Status = { status: 'idle', count: 0 }; diff --git a/website/content/docs/advanced-guides/migration-v2.mdx b/website/content/docs/advanced-guides/migration-v2.mdx index 805caaae..6b7c5b09 100644 --- a/website/content/docs/advanced-guides/migration-v2.mdx +++ b/website/content/docs/advanced-guides/migration-v2.mdx @@ -43,7 +43,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - `create()` with an explicit state type and an async recipe, such as `create(base, async (draft) => { … })`, returns `Promise`, and so do curried producers; v1 typed the result as `State`. - `apply()` accepts `enableAutoFreeze: true` and then returns `Immutable`, as `apply()` does; v1 rejected the option and typed the frozen result as mutable. With `enableAutoFreeze` typed as `boolean` or optional `true`, the result is `State | Immutable`. With `mutable` typed as `boolean`, optional `boolean`, or optional `true`, `apply()` returns `State | void`. - A curried producer whose recipe annotates its draft and returns a new state, such as `create((draft: State) => ({ ...draft, count: 0 }))`, returns `State`, or `Promise` for an async recipe. v1 typed such a call as a manual draft, so calling the producer did not compile. -- With an explicit state type, a recipe that may or may not return a Promise, such as one typed `(draft: State) => void | Promise`, gives `State | Promise`, also in curried producers and creators from `makeCreator()`. v1 typed the result as `State`. +- With an explicit state type, a recipe that may or may not return a Promise, such as one typed `(draft: State) => void | Promise`, gives `State | Promise`, also in curried producers and creators from `makeCreator()`. v1 typed the result as `State`, as this version still does for a primitive state type, such as `number`. ## Fixes that change results From 8667c30c96a236a80844257728eb46826ca6f5d8 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:56:00 +0800 Subject: [PATCH 14/21] fix: accept Map keys whose properties cannot be read in the development check of draft keys --- src/map.ts | 17 +++++++++++++---- test/map-draft-keys.test.ts | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/map.ts b/src/map.ts index 49c2c6a6..14ab5954 100644 --- a/src/map.ts +++ b/src/map.ts @@ -56,10 +56,19 @@ export const mapHandler = { set(key: any, value: any) { // Map keys are used as they are, and a draft is revoked when its producer // ends, so a Map would keep a key that throws when it is read. - if (__DEV__ && getProxyDraft(key)) { - throw new Error( - `A draft cannot be a Map key: its producer revokes it when it ends. Use original(key), current(key) or an id as the key; see https://mutative.js.org/docs/extra-topics/faq` - ); + if (__DEV__) { + let isDraftKey = false; + try { + isDraftKey = !!getProxyDraft(key); + } catch { + // Map keys are compared by identity only, so a key can also be a Proxy + // that rejects reading the symbol of drafts, or a revoked one. + } + if (isDraftKey) { + throw new Error( + `A draft cannot be a Map key: its producer revokes it when it ends. Use original(key), current(key) or an id as the key; see https://mutative.js.org/docs/extra-topics/faq` + ); + } } const target = getProxyDraft(this)!; const source = latest(target); diff --git a/test/map-draft-keys.test.ts b/test/map-draft-keys.test.ts index b96f0c30..72023f03 100644 --- a/test/map-draft-keys.test.ts +++ b/test/map-draft-keys.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { create, current, original } from '../src'; +import { apply, create, current, original } from '../src'; const message = 'A draft cannot be a Map key: its producer revokes it when it ends. Use original(key), current(key) or an id as the key; see https://mutative.js.org/docs/extra-topics/faq'; @@ -40,3 +40,34 @@ test('the original or a snapshot of a draft can be a Map key', () => { expect([...state.map.keys()]).toEqual([{ id: 1 }, { id: 2 }]); expect(state.key).toEqual({ id: 2 }); }); + +test('a key whose properties cannot be read is no draft', () => { + const opaque = new Proxy( + {}, + { + get() { + throw new Error('opaque key'); + }, + } + ); + const { proxy: revoked, revoke } = Proxy.revocable({}, {}); + revoke(); + for (const key of [opaque, revoked]) { + const added = create(new Map(), (draft) => { + draft.set(key, 1); + }); + expect(added.get(key)).toBe(1); + const updated = create(added, (draft) => { + draft.set(key, 2); + }); + expect(updated.get(key)).toBe(2); + const same = create(updated, (draft) => { + draft.set(key, 2); + }); + expect(same).toBe(updated); + const applied = apply(updated, [ + { op: 'replace', path: [key as any], value: 3 }, + ]); + expect(applied.get(key)).toBe(3); + } +}); From 421150af0091e35a045ad9f5d928f8e580d86809 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:57:08 +0800 Subject: [PATCH 15/21] docs: show the signatures of makeCreator() in the API reference --- docs/functions/apply.md | 10 +- docs/functions/castDraft.md | 2 +- docs/functions/castImmutable.md | 2 +- docs/functions/castMutable.md | 2 +- docs/functions/current.md | 4 +- docs/functions/isDraft.md | 2 +- docs/functions/isDraftable.md | 2 +- docs/functions/markSimpleObject.md | 2 +- docs/functions/original.md | 2 +- docs/functions/rawReturn.md | 2 +- docs/functions/unsafe.md | 2 +- docs/interfaces/Options.md | 10 +- docs/type-aliases/Draft.md | 2 +- docs/type-aliases/DraftedObject.md | 2 +- docs/type-aliases/Immutable.md | 2 +- docs/type-aliases/Patch.md | 2 +- docs/type-aliases/Patches.md | 2 +- docs/type-aliases/PatchesOptions.md | 2 +- docs/variables/create.md | 6 +- docs/variables/makeCreator.md | 428 +++++++++++++++++++++++++++- src/makeCreator.ts | 2 +- 21 files changed, 456 insertions(+), 34 deletions(-) diff --git a/docs/functions/apply.md b/docs/functions/apply.md index 2586733d..2a4f8848 100644 --- a/docs/functions/apply.md +++ b/docs/functions/apply.md @@ -10,7 +10,7 @@ > **apply**\<`T`, `F`, `_A`\>(`state`, `patches`, `applyOptions?`): `ApplyState`\<`T`, `F`, `undefined`\> -Defined in: [apply.ts:62](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/apply.ts#L62) +Defined in: [apply.ts:62](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/apply.ts#L62) `apply(state, patches)` to apply patches to state @@ -68,7 +68,7 @@ expect(state).toEqual(apply(baseState, patches)); > **apply**\<`T`, `F`, `A`\>(`state`, `patches`, `applyOptions`): `ApplyResult`\<`T`, `F`, `A`\> -Defined in: [apply.ts:71](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/apply.ts#L71) +Defined in: [apply.ts:71](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/apply.ts#L71) `apply(state, patches)` to apply patches to state @@ -126,7 +126,7 @@ expect(state).toEqual(apply(baseState, patches)); > **apply**\<`T`, `F`, `A`\>(`state`, `patches`, `applyOptions?`): `ApplyResult`\<`T`, `F`, `undefined` \| `A`\> -Defined in: [apply.ts:78](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/apply.ts#L78) +Defined in: [apply.ts:78](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/apply.ts#L78) `apply(state, patches)` to apply patches to state @@ -184,7 +184,7 @@ expect(state).toEqual(apply(baseState, patches)); > **apply**\<`T`, `F`\>(`state`, `patches`, `applyOptions`): `void` -Defined in: [apply.ts:89](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/apply.ts#L89) +Defined in: [apply.ts:89](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/apply.ts#L89) `apply(state, patches)` to apply patches to state @@ -240,7 +240,7 @@ expect(state).toEqual(apply(baseState, patches)); > **apply**\<`T`, `F`\>(`state`, `patches`, `applyOptions`): `ApplyResult`\<`T`, `F`, `undefined` \| `ApplyMutableOptions`\> -Defined in: [apply.ts:94](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/apply.ts#L94) +Defined in: [apply.ts:94](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/apply.ts#L94) `apply(state, patches)` to apply patches to state diff --git a/docs/functions/castDraft.md b/docs/functions/castDraft.md index c845d4ba..23563d72 100644 --- a/docs/functions/castDraft.md +++ b/docs/functions/castDraft.md @@ -8,7 +8,7 @@ > **castDraft**\<`T`\>(`value`): [`Draft`](../type-aliases/Draft.md)\<`T`\> -Defined in: [utils/cast.ts:6](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/cast.ts#L6) +Defined in: [utils/cast.ts:6](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/cast.ts#L6) Cast a value to an Draft type value. diff --git a/docs/functions/castImmutable.md b/docs/functions/castImmutable.md index a30a841d..5cdbb3b2 100644 --- a/docs/functions/castImmutable.md +++ b/docs/functions/castImmutable.md @@ -8,7 +8,7 @@ > **castImmutable**\<`T`\>(`value`): [`Immutable`](../type-aliases/Immutable.md)\<`T`\> -Defined in: [utils/cast.ts:13](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/cast.ts#L13) +Defined in: [utils/cast.ts:13](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/cast.ts#L13) Cast a value to an Immutable type value. diff --git a/docs/functions/castMutable.md b/docs/functions/castMutable.md index 3a748748..4cdff0d8 100644 --- a/docs/functions/castMutable.md +++ b/docs/functions/castMutable.md @@ -8,7 +8,7 @@ > **castMutable**\<`T`\>(`draft`): `T` -Defined in: [utils/cast.ts:20](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/cast.ts#L20) +Defined in: [utils/cast.ts:20](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/cast.ts#L20) Cast a value to an Mutable type value. diff --git a/docs/functions/current.md b/docs/functions/current.md index ff79ecc7..4576b773 100644 --- a/docs/functions/current.md +++ b/docs/functions/current.md @@ -10,7 +10,7 @@ > **current**\<`T`\>(`target`): `T` -Defined in: [current.ts:198](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/current.ts#L198) +Defined in: [current.ts:198](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/current.ts#L198) `current(draft)` to get current state in the draft mutation function. @@ -49,7 +49,7 @@ const state = create( > **current**\<`T`\>(`target`): `T` -Defined in: [current.ts:200](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/current.ts#L200) +Defined in: [current.ts:200](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/current.ts#L200) ### Type Parameters diff --git a/docs/functions/isDraft.md b/docs/functions/isDraft.md index 71d741e6..609bc2f8 100644 --- a/docs/functions/isDraft.md +++ b/docs/functions/isDraft.md @@ -8,7 +8,7 @@ > **isDraft**(`target`): `boolean` -Defined in: [utils/draft.ts:33](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/draft.ts#L33) +Defined in: [utils/draft.ts:33](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/draft.ts#L33) Check if the value is a draft diff --git a/docs/functions/isDraftable.md b/docs/functions/isDraftable.md index 1f806fdc..8b0a21de 100644 --- a/docs/functions/isDraftable.md +++ b/docs/functions/isDraftable.md @@ -8,7 +8,7 @@ > **isDraftable**(`value`, `options?`): `boolean` -Defined in: [utils/draft.ts:59](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/draft.ts#L59) +Defined in: [utils/draft.ts:59](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/draft.ts#L59) Check if a value is draftable diff --git a/docs/functions/markSimpleObject.md b/docs/functions/markSimpleObject.md index aacc1d63..0829e879 100644 --- a/docs/functions/markSimpleObject.md +++ b/docs/functions/markSimpleObject.md @@ -8,7 +8,7 @@ > **markSimpleObject**(`value`): `undefined` \| `"immutable"` -Defined in: [utils/marker.ts:26](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/utils/marker.ts#L26) +Defined in: [utils/marker.ts:26](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/marker.ts#L26) ## Parameters diff --git a/docs/functions/original.md b/docs/functions/original.md index aa3fb723..8bc17f43 100644 --- a/docs/functions/original.md +++ b/docs/functions/original.md @@ -8,7 +8,7 @@ > **original**\<`T`\>(`target`): `T` -Defined in: [original.ts:22](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/original.ts#L22) +Defined in: [original.ts:22](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/original.ts#L22) `original(draft)` to get original state in the draft mutation function. diff --git a/docs/functions/rawReturn.md b/docs/functions/rawReturn.md index ddd76ea9..29000eab 100644 --- a/docs/functions/rawReturn.md +++ b/docs/functions/rawReturn.md @@ -8,7 +8,7 @@ > **rawReturn**\<`T`\>(`value`): `T` -Defined in: [rawReturn.ts:22](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/rawReturn.ts#L22) +Defined in: [rawReturn.ts:22](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/rawReturn.ts#L22) Use rawReturn() to wrap the return value to skip the draft check and thus improve performance. diff --git a/docs/functions/unsafe.md b/docs/functions/unsafe.md index 374706f2..856cd58e 100644 --- a/docs/functions/unsafe.md +++ b/docs/functions/unsafe.md @@ -8,7 +8,7 @@ > **unsafe**\<`T`\>(`callback`): `T` -Defined in: [unsafe.ts:57](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/unsafe.ts#L57) +Defined in: [unsafe.ts:57](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/unsafe.ts#L57) `unsafe(callback)` to access mutable data directly in strict mode. diff --git a/docs/interfaces/Options.md b/docs/interfaces/Options.md index 66d142b5..362124ca 100644 --- a/docs/interfaces/Options.md +++ b/docs/interfaces/Options.md @@ -6,7 +6,7 @@ # Interface: Options\ -Defined in: [interface.ts:171](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L171) +Defined in: [interface.ts:171](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L171) ## Type Parameters @@ -24,7 +24,7 @@ Defined in: [interface.ts:171](https://github.com/unadlib/mutative/blob/080a2050 > `optional` **enableAutoFreeze**: `F` -Defined in: [interface.ts:184](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L184) +Defined in: [interface.ts:184](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L184) Enable autoFreeze, and return frozen state. @@ -34,7 +34,7 @@ Enable autoFreeze, and return frozen state. > `optional` **enablePatches**: `O` -Defined in: [interface.ts:180](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L180) +Defined in: [interface.ts:180](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L180) Enable patch, and return the patches and inversePatches. @@ -44,7 +44,7 @@ Enable patch, and return the patches and inversePatches. > `optional` **mark**: `Mark`\<`O`, `F`\> \| `Mark`\<`O`, `F`\>[] -Defined in: [interface.ts:189](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L189) +Defined in: [interface.ts:189](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L189) Set a mark to determine if the object is mutable or if an instance is an immutable. And it can also return a shallow copy function(AutoFreeze and Patches should both be disabled). @@ -55,7 +55,7 @@ And it can also return a shallow copy function(AutoFreeze and Patches should bot > `optional` **strict**: `boolean` -Defined in: [interface.ts:176](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L176) +Defined in: [interface.ts:176](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L176) In strict mode, Forbid accessing non-draftable values and forbid returning a non-draft value. Development builds also warn once when a recipe leaves 1,000 or more drafts unchanged. diff --git a/docs/type-aliases/Draft.md b/docs/type-aliases/Draft.md index c4d8ce0c..4cfeaf20 100644 --- a/docs/type-aliases/Draft.md +++ b/docs/type-aliases/Draft.md @@ -8,7 +8,7 @@ > **Draft**\<`T`\> = `T` *extends* `Primitive` \| `AtomicObject` ? `T` : `T` *extends* `IfAvailable`\<`ReadonlyMap`\\> ? `DraftedMap`\<`K`, `V`\> : `T` *extends* `IfAvailable`\<`ReadonlySet`\\> ? `DraftedSet`\<`V`\> : `T` *extends* `WeakReferences` ? `T` : `T` *extends* `object` ? [`DraftedObject`](DraftedObject.md)\<`T`\> : `T` -Defined in: [interface.ts:229](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L229) +Defined in: [interface.ts:229](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L229) ## Type Parameters diff --git a/docs/type-aliases/DraftedObject.md b/docs/type-aliases/DraftedObject.md index f4c122b8..cdcb52d2 100644 --- a/docs/type-aliases/DraftedObject.md +++ b/docs/type-aliases/DraftedObject.md @@ -8,7 +8,7 @@ > **DraftedObject**\<`T`\> = `{ -readonly [K in keyof T]: Draft }` -Defined in: [interface.ts:225](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L225) +Defined in: [interface.ts:225](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L225) ## Type Parameters diff --git a/docs/type-aliases/Immutable.md b/docs/type-aliases/Immutable.md index ed41b409..0f4da060 100644 --- a/docs/type-aliases/Immutable.md +++ b/docs/type-aliases/Immutable.md @@ -8,7 +8,7 @@ > **Immutable**\<`T`\> = `T` *extends* `Primitive` \| `AtomicObject` ? `T` : `T` *extends* `IfAvailable`\<`ReadonlyMap`\\> ? `ImmutableMap`\<`K`, `V`\> : `T` *extends* `IfAvailable`\<`ReadonlySet`\\> ? `ImmutableSet`\<`V`\> : `T` *extends* `WeakReferences` ? `T` : `T` *extends* `object` ? `ImmutableObject`\<`T`\> : `T` -Defined in: [interface.ts:211](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L211) +Defined in: [interface.ts:211](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L211) ## Type Parameters diff --git a/docs/type-aliases/Patch.md b/docs/type-aliases/Patch.md index 02d6b418..f37cc04e 100644 --- a/docs/type-aliases/Patch.md +++ b/docs/type-aliases/Patch.md @@ -8,7 +8,7 @@ > **Patch**\<`P`\> = `P` *extends* `object` ? `IPatch` & `object` : `P` *extends* `true` \| `object` ? `IPatch` & `object` : `IPatch` & `object` -Defined in: [interface.ts:96](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L96) +Defined in: [interface.ts:96](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L96) ## Type Parameters diff --git a/docs/type-aliases/Patches.md b/docs/type-aliases/Patches.md index 0e32c189..ef117cca 100644 --- a/docs/type-aliases/Patches.md +++ b/docs/type-aliases/Patches.md @@ -8,7 +8,7 @@ > **Patches**\<`P`\> = [`Patch`](Patch.md)\<`P`\>[] -Defined in: [interface.ts:110](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L110) +Defined in: [interface.ts:110](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L110) ## Type Parameters diff --git a/docs/type-aliases/PatchesOptions.md b/docs/type-aliases/PatchesOptions.md index c605a9a6..d73b4977 100644 --- a/docs/type-aliases/PatchesOptions.md +++ b/docs/type-aliases/PatchesOptions.md @@ -8,7 +8,7 @@ > **PatchesOptions** = `boolean` \| \{ `arrayLengthAssignment?`: `boolean`; `pathAsArray?`: `boolean`; \} -Defined in: [interface.ts:18](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/interface.ts#L18) +Defined in: [interface.ts:18](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L18) ## Type Declaration diff --git a/docs/variables/create.md b/docs/variables/create.md index d7798cd2..1b59f380 100644 --- a/docs/variables/create.md +++ b/docs/variables/create.md @@ -8,7 +8,7 @@ > `const` **create**: \{\<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>; \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`\<`T`, `O`, `F`, `R`\>; \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`\<`T`, `O`, `F`, `R`\>; \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>; \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `P`, `F`, `O`, `R`\>(`mutate`, `options?`): (`base`, ...`args`) => `CreateResult`\<`T`, `O`, `F`, `R`\>; \<`T`, `O`, `F`\>(`base`, `options?`): \[[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>\]; \} -Defined in: [create.ts:25](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/create.ts#L25) +Defined in: [create.ts:25](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/create.ts#L25) `create(baseState, callback, options)` to create the next state @@ -129,7 +129,7 @@ expect(state.arr).toBe(baseState.arr); #### mutate -`ExplicitRecipe`\<`T`, \[\], `ExplicitMaybeAsyncReturn`\<`T`\>\> +`ExplicitMaybeAsyncRecipe`\<`T`, \[\]\> #### options? @@ -345,7 +345,7 @@ expect(state.arr).toBe(baseState.arr); #### mutate -`ExplicitRecipe`\<`T`, `P`, `ExplicitMaybeAsyncReturn`\<`T`\>\> +`ExplicitMaybeAsyncRecipe`\<`T`, `P`\> #### options? diff --git a/docs/variables/makeCreator.md b/docs/variables/makeCreator.md index d5738c3c..85dfb120 100644 --- a/docs/variables/makeCreator.md +++ b/docs/variables/makeCreator.md @@ -4,11 +4,11 @@ [mutative](../README.md) / makeCreator -# Variable: makeCreator +# Variable: makeCreator() -> `const` **makeCreator**: `MakeCreator` +> `const` **makeCreator**: \<`_F`, `_O`\>(`options?`) => \{\<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>; \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`\<`T`, `O`, `F`, `R`\>; \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`\<`T`, `O`, `F`, `R`\>; \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>; \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\>; \<`T`, `P`, `F`, `O`, `R`\>(`mutate`, `options?`): (`base`, ...`args`) => `CreateResult`\<`T`, `O`, `F`, `R`\>; \<`T`, `O`, `F`\>(`base`, `options?`): \[[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>\]; \} -Defined in: [makeCreator.ts:173](https://github.com/unadlib/mutative/blob/080a20500efdd03828f63d0f023fc6d7458b3e98/src/makeCreator.ts#L173) +Defined in: [makeCreator.ts:184](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/makeCreator.ts#L184) `makeCreator(options)` to make a creator function. @@ -32,3 +32,425 @@ expect(state.foo).not.toBe(baseState.foo); expect(state.arr).toBe(baseState.arr); expect(Object.isFrozen(state)).toBeTruthy(); ``` + +## Type Parameters + +### _F + +`_F` *extends* `boolean` = `false` + +### _O + +`_O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `false` + +## Parameters + +### options? + +[`Options`](../interfaces/Options.md)\<`_O`, `_F`\> + +## Returns + +> \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> + +### Type Parameters + +#### T + +`T` = `never` + +#### F + +`F` *extends* `boolean` = `_F` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` + +### Parameters + +#### base + +`ExplicitState`\<`T`\> + +#### mutate + +`ExplicitRecipe`\<`T`, \[\], `ExplicitSyncReturn`\<`T`\>\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> + +> \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +### Type Parameters + +#### T + +`T` = `never` + +#### F + +`F` *extends* `boolean` = `_F` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` + +### Parameters + +#### base + +`ExplicitState`\<`T`\> + +#### mutate + +`ExplicitRecipe`\<`T`, \[\], `Promise`\<`ExplicitReturn`\<`T`\>\>\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +> \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +### Type Parameters + +#### T + +`T` = `never` + +#### F + +`F` *extends* `boolean` = `_F` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` + +### Parameters + +#### base + +`ExplicitState`\<`T`\> + +#### mutate + +`ExplicitMaybeAsyncRecipe`\<`T`, \[\]\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +> \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`\<`T`, `O`, `F`, `R`\> + +### Type Parameters + +#### T + +`T` *extends* `unknown` + +#### F + +`F` *extends* `boolean` = `_F` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` + +#### R + +`R` *extends* `unknown` = `void` + +### Parameters + +#### base + +`T` + +#### mutate + +(`draft`) => `R` + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`CreateResult`\<`T`, `O`, `F`, `R`\> + +> \<`T`, `F`, `O`, `R`\>(`base`, `mutate`, `options?`): `CreateResult`\<`T`, `O`, `F`, `R`\> + +### Type Parameters + +#### T + +`T` *extends* `unknown` + +#### F + +`F` *extends* `boolean` = `_F` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` + +#### R + +`R` *extends* `void` \| `Promise`\<`void`\> = `void` + +### Parameters + +#### base + +`T` + +#### mutate + +(`draft`) => `R` + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`CreateResult`\<`T`, `O`, `F`, `R`\> + +> \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> + +### Type Parameters + +#### T + +`T` = `never` + +#### P + +`P` *extends* `any`[] = \[\] + +#### F + +`F` *extends* `boolean` = `_F` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` + +### Parameters + +#### mutate + +`ExplicitRecipe`\<`T`, `P`, `ExplicitSyncReturn`\<`T`\>\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +> (`base`, ...`args`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> + +#### Parameters + +##### base + +`ExplicitState`\<`T`\> + +##### args + +...`P` + +#### Returns + +`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> + +> \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +### Type Parameters + +#### T + +`T` = `never` + +#### P + +`P` *extends* `any`[] = \[\] + +#### F + +`F` *extends* `boolean` = `_F` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` + +### Parameters + +#### mutate + +`ExplicitRecipe`\<`T`, `P`, `Promise`\<`ExplicitReturn`\<`T`\>\>\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +> (`base`, ...`args`): `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +#### Parameters + +##### base + +`ExplicitState`\<`T`\> + +##### args + +...`P` + +#### Returns + +`Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +> \<`T`, `P`, `F`, `O`\>(`mutate`, `options?`): (`base`, ...`args`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +### Type Parameters + +#### T + +`T` = `never` + +#### P + +`P` *extends* `any`[] = \[\] + +#### F + +`F` *extends* `boolean` = `_F` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` + +### Parameters + +#### mutate + +`ExplicitMaybeAsyncRecipe`\<`T`, `P`\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +> (`base`, ...`args`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +#### Parameters + +##### base + +`ExplicitState`\<`T`\> + +##### args + +...`P` + +#### Returns + +`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> + +> \<`T`, `P`, `F`, `O`, `R`\>(`mutate`, `options?`): (`base`, ...`args`) => `CreateResult`\<`T`, `O`, `F`, `R`\> + +### Type Parameters + +#### T + +`T` *extends* `unknown` + +#### P + +`P` *extends* `any`[] = \[\] + +#### F + +`F` *extends* `boolean` = `_F` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` + +#### R + +`R` *extends* `unknown` = `void` + +### Parameters + +#### mutate + +(`draft`, ...`args`) => `R` + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +> (`base`, ...`args`): `CreateResult`\<`T`, `O`, `F`, `R`\> + +#### Parameters + +##### base + +`T` + +##### args + +...`P` + +#### Returns + +`CreateResult`\<`T`, `O`, `F`, `R`\> + +> \<`T`, `O`, `F`\>(`base`, `options?`): \[[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>\] + +### Type Parameters + +#### T + +`T` *extends* `unknown` + +#### O + +`O` *extends* [`PatchesOptions`](../type-aliases/PatchesOptions.md) = `_O` + +#### F + +`F` *extends* `boolean` = `_F` + +### Parameters + +#### base + +`T` + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +\[[`Draft`](../type-aliases/Draft.md)\<`T`\>, () => `Result`\<`T`, `O`, `F`\>\] diff --git a/src/makeCreator.ts b/src/makeCreator.ts index 890c8ad6..165285ef 100644 --- a/src/makeCreator.ts +++ b/src/makeCreator.ts @@ -59,7 +59,7 @@ type ExplicitMaybeAsyncRecipe = ? never : ExplicitRecipe | Promise>>; -type MakeCreator = < +/** @inline */ type MakeCreator = < _F extends boolean = false, _O extends PatchesOptions = false, >( From 852aab8dbf656d288e140ab92d313a9dc66e7986 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 10:58:12 +0800 Subject: [PATCH 16/21] build: refresh the size baseline for the development checks of Map keys and cycles --- scripts/build-size-baseline.json | 80 ++++++++++++++++---------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/scripts/build-size-baseline.json b/scripts/build-size-baseline.json index 070ac60f..ccc179c4 100644 --- a/scripts/build-size-baseline.json +++ b/scripts/build-size-baseline.json @@ -1,7 +1,7 @@ { "reference": { - "revision": "df6131ced5215e29e206d3f435dbe80764394714", - "description": "A moved array element is first looked up where the offset of its move puts its original index", + "revision": "421150af0091e35a045ad9f5d928f8e580d86809", + "description": "Development builds throw for a draft used as a Map key, also for keys whose properties cannot be read, and freezing no longer records the originals of changed drafts", "node": "v24.16.0", "commands": [ "pnpm install --frozen-lockfile", @@ -15,69 +15,69 @@ "esbuild": "0.24.0", "measurements": { "artifact/mutative.cjs.production.min.js": { - "raw": 27353, - "gzip": 9184, - "brotli": 8439 + "raw": 27324, + "gzip": 9178, + "brotli": 8423 }, "artifact/mutative.umd.production.min.js": { - "raw": 27510, - "gzip": 9250, - "brotli": 8489 + "raw": 27481, + "gzip": 9239, + "brotli": 8480 }, "artifact/mutative.esm.production.min.mjs": { - "raw": 27328, - "gzip": 9206, - "brotli": 8463 + "raw": 27299, + "gzip": 9200, + "brotli": 8452 }, "artifact/mutative.cjs.development.js": { - "raw": 73292, - "gzip": 17656, - "brotli": 15344 + "raw": 72847, + "gzip": 17743, + "brotli": 15413 }, "artifact/mutative.umd.development.js": { - "raw": 75572, - "gzip": 17860, - "brotli": 15504 + "raw": 75122, + "gzip": 17940, + "brotli": 15569 }, "artifact/mutative.esm.js": { - "raw": 74144, - "gzip": 17772, - "brotli": 15439 + "raw": 73682, + "gzip": 17833, + "brotli": 15458 }, "artifact/mutative.esm.mjs": { - "raw": 74145, - "gzip": 17774, - "brotli": 15422 + "raw": 73683, + "gzip": 17834, + "brotli": 15477 }, "consumer/import/create": { - "raw": 24578, - "gzip": 8598, - "brotli": 7898 + "raw": 24533, + "gzip": 8576, + "brotli": 7892 }, "consumer/import/patches": { - "raw": 26576, - "gzip": 9276, + "raw": 26549, + "gzip": 9267, "brotli": 8498 }, "consumer/import/all": { - "raw": 27361, - "gzip": 9533, - "brotli": 8742 + "raw": 27334, + "gzip": 9525, + "brotli": 8738 }, "consumer/require/create": { - "raw": 26861, - "gzip": 9517, - "brotli": 8721 + "raw": 26826, + "gzip": 9513, + "brotli": 8735 }, "consumer/require/patches": { - "raw": 26929, - "gzip": 9556, - "brotli": 8756 + "raw": 26894, + "gzip": 9553, + "brotli": 8748 }, "consumer/require/all": { - "raw": 26826, - "gzip": 9496, - "brotli": 8704 + "raw": 26791, + "gzip": 9492, + "brotli": 8700 } } } From 10293abd22db624ad5694339d8b6c87118803e17 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 12:02:30 +0800 Subject: [PATCH 17/21] fix: find a Set item of a patch path only at its index --- README.md | 2 +- src/apply.ts | 19 +++++----- test/apply.test.ts | 36 +++++++++++++++++++ .../docs/advanced-guides/migration-v2.mdx | 2 +- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a70a53ca..1f569edb 100644 --- a/README.md +++ b/README.md @@ -818,7 +818,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - Consume Map value and entry iterators, and all Set iterators, while their draft is active. After the producer finishes or fails, or a manual draft is finalized, an iterator cannot yield another value and throws a `TypeError`. Lazy iterator helpers follow the same lifetime; an exhausted iterator stays exhausted. Map keys are not drafted, and `keys()` continues to return a native iterator. - `apply()` copies the own symbol keys of patch values, and an own `__proto__` key, as `JSON.parse()` creates one, stays a data property. v1 dropped symbol keys there and turned an own `__proto__` key into the prototype of the copy. - `apply()` rejects a patch whose path ends in `__proto__` on an object or array, as it rejects `__proto__` earlier in a path. Coercible property keys, such as a boxed string, are checked after conversion too. With `mutable: true`, v1 assigned the value and so replaced the prototype of the object. -- In a patch path, `apply()` reads an item of a Set only at a numeric position, and throws for any other key. v1 read any property of an array of the Set's items there, which let a path through `__proto__` reach `Array.prototype`. +- In a patch path, `apply()` reads an item of a Set only at its index, such as `1` or `'1'`, and throws for any other key, such as `__proto__`. v1 read any property of an array of the Set's items there, which let a path through `__proto__` reach `Array.prototype`. - `apply()` with `mutable: true` throws for a patch that replaces the root state with another value, such as the patch of a recipe that returns a new state, as the state cannot be replaced in place. v1 ignored the replacement and applied the patches after it to the replacing value, so the state stayed as it was and the value of that patch changed. - An object that `mark` makes draftable, such as a plain object under `markSimpleObject`, keeps an own `__proto__` key, as `JSON.parse()` creates one, as a property of its copy; v1 assigned it, which replaced the prototype of the copy. - A patch for a Map key that is an array holds the key as one path segment; v1 spread the array into the path, so applying the patch wrote to other keys. diff --git a/src/apply.ts b/src/apply.ts index c70c5533..4bee9997 100644 --- a/src/apply.ts +++ b/src/apply.ts @@ -11,6 +11,7 @@ import { deepClone, getProxyDraft, getType, + has, isDraft, unescapePath, } from './utils'; @@ -140,15 +141,15 @@ export function apply< die(ErrorCode.ReservedPatchAttribute); } } - base = - parentType === DraftType.Map - ? base.get(key) - : parentType === DraftType.Set - ? // A Set item is found by its position, a number, so that no - // other key, such as `__proto__`, reads a property of the - // array of its items. - Array.from(base)[+key] - : base[key]; + if (parentType === DraftType.Set) { + // A Set item is found at its index in the array of the Set's items, + // so that no other key, such as `__proto__`, reads a property that + // the array inherits. No number names such a property. + base = Array.from(base); + if (typeof key !== 'number' && !has(base, key)) + die(ErrorCode.CannotApplyPatch, path); + } + base = parentType === DraftType.Map ? base.get(key) : base[key]; if (typeof base !== 'object') { die(ErrorCode.CannotApplyPatch, path); } diff --git a/test/apply.test.ts b/test/apply.test.ts index 3c44aa5d..ac0d2b5d 100644 --- a/test/apply.test.ts +++ b/test/apply.test.ts @@ -1351,6 +1351,42 @@ test('a patch cannot reach a prototype through a Set position', () => { } }); +test('a Set item of a patch path is found only at its index', () => { + // Keys that convert to a number but are no index of an item find nothing, + // as they do in an array. + const keys: unknown[] = [ + '', + ' ', + '-0', + '01', + '1.0', + '1e0', + '0x1', + '+1', + null, + false, + true, + [], + 'length', + ]; + for (const key of keys) { + for (const options of [undefined, { mutable: true }]) { + const state = { set: new Set([{ v: 1 }, { v: 2 }]) }; + const path = ['set', key, 'v']; + expect(() => + apply(state, [{ op: 'replace', path, value: 3 }] as Patches, options) + ).toThrow(`Cannot apply patch at '${path.join('/')}'.`); + expect([...state.set]).toEqual([{ v: 1 }, { v: 2 }]); + } + } + // An empty segment of a string path is no index either. + expect(() => + apply({ set: new Set([{ v: 1 }]) }, [ + { op: 'replace', path: '/set//v', value: 3 }, + ]) + ).toThrow(`Cannot apply patch at 'set//v'.`); +}); + test.each([false, true])( 'coercible terminal keys cannot set a prototype (mutable: %s)', (mutable) => { diff --git a/website/content/docs/advanced-guides/migration-v2.mdx b/website/content/docs/advanced-guides/migration-v2.mdx index 6b7c5b09..f9c32f76 100644 --- a/website/content/docs/advanced-guides/migration-v2.mdx +++ b/website/content/docs/advanced-guides/migration-v2.mdx @@ -60,7 +60,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - Consume Map value and entry iterators, and all Set iterators, while their draft is active. After the producer finishes or fails, or a manual draft is finalized, an iterator cannot yield another value and throws a `TypeError`. Lazy iterator helpers follow the same lifetime; an exhausted iterator stays exhausted. Map keys are not drafted, and `keys()` continues to return a native iterator. - `apply()` copies the own symbol keys of patch values, and an own `__proto__` key, as `JSON.parse()` creates one, stays a data property. v1 dropped symbol keys there and turned an own `__proto__` key into the prototype of the copy. - `apply()` rejects a patch whose path ends in `__proto__` on an object or array, as it rejects `__proto__` earlier in a path. Coercible property keys, such as a boxed string, are checked after conversion too. With `mutable: true`, v1 assigned the value and so replaced the prototype of the object. -- In a patch path, `apply()` reads an item of a Set only at a numeric position, and throws for any other key. v1 read any property of an array of the Set's items there, which let a path through `__proto__` reach `Array.prototype`. +- In a patch path, `apply()` reads an item of a Set only at its index, such as `1` or `'1'`, and throws for any other key, such as `__proto__`. v1 read any property of an array of the Set's items there, which let a path through `__proto__` reach `Array.prototype`. - `apply()` with `mutable: true` throws for a patch that replaces the root state with another value, such as the patch of a recipe that returns a new state, as the state cannot be replaced in place. v1 ignored the replacement and applied the patches after it to the replacing value, so the state stayed as it was and the value of that patch changed. - An object that `mark` makes draftable, such as a plain object under `markSimpleObject`, keeps an own `__proto__` key, as `JSON.parse()` creates one, as a property of its copy; v1 assigned it, which replaced the prototype of the copy. - A patch for a Map key that is an array holds the key as one path segment; v1 spread the array into the path, so applying the patch wrote to other keys. From a0b801b173104e58297d8765b74ff440d0b8b540 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 12:02:51 +0800 Subject: [PATCH 18/21] docs: name all attributes of the own properties that copies of marked instances assign --- README.md | 2 +- website/content/docs/api-reference/create.mdx | 2 +- website/content/docs/extra-topics/comparison-with-immer.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1f569edb..4d45cdd0 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ The recipe can be an async function: `create()` then returns a Promise of the re > When the mark function is (target) => 'immutable', it means all the objects in the state structure are immutable. In this specific case, you can totally turn on `AutoFreeze` and `Patches`. > `mark` supports multiple marks, and the marks are executed in order, and the first mark that returns a value will be used. > When a object tree node is marked by the `mark` function as `mutable`, all of its child nodes will also not be drafted by Mutative and will retain their original values. - > An instance that `mark` makes `immutable` is copied with all its own properties, and those that are enumerable and writable are assigned, so an own property that shadows a setter of its class runs it, and one that shadows a getter or a read-only property throws; see [Copying marked instances](https://mutative.js.org/docs/advanced-guides/mark#copying-marked-instances). + > An instance that `mark` makes `immutable` is copied with all its own properties, and those that are enumerable, writable and configurable are assigned, so an own property that shadows a setter of its class runs it, and one that shadows a getter or a read-only property throws; see [Copying marked instances](https://mutative.js.org/docs/advanced-guides/mark#copying-marked-instances). #### `create()` - Currying diff --git a/website/content/docs/api-reference/create.mdx b/website/content/docs/api-reference/create.mdx index 3866908c..eee66009 100644 --- a/website/content/docs/api-reference/create.mdx +++ b/website/content/docs/api-reference/create.mdx @@ -52,7 +52,7 @@ Then options is optional. > Set a mark to determine if the value is mutable or if an instance is an immutable, and it can also return a shallow copy function(`AutoFreeze` and `Patches` should both be disabled, Some patches operation might not be equivalent). > When the mark function is (target) => 'immutable', it means all the objects in the state structure are immutable. In this specific case, you can totally turn on `AutoFreeze` and `Patches`. > `mark` supports multiple marks, and the marks are executed in order, and the first mark that returns a value will be used. - > An instance that `mark` makes `immutable` is copied with all its own properties, and those that are enumerable and writable are assigned, so an own property that shadows a setter of its class runs it, and one that shadows a getter or a read-only property throws; see [Copying marked instances](/docs/advanced-guides/mark#copying-marked-instances). + > An instance that `mark` makes `immutable` is copied with all its own properties, and those that are enumerable, writable and configurable are assigned, so an own property that shadows a setter of its class runs it, and one that shadows a getter or a read-only property throws; see [Copying marked instances](/docs/advanced-guides/mark#copying-marked-instances). ## Currying diff --git a/website/content/docs/extra-topics/comparison-with-immer.mdx b/website/content/docs/extra-topics/comparison-with-immer.mdx index 921592d2..feeb0020 100644 --- a/website/content/docs/extra-topics/comparison-with-immer.mdx +++ b/website/content/docs/extra-topics/comparison-with-immer.mdx @@ -45,7 +45,7 @@ Mutative and Immer work the same way: a recipe changes a draft, and the library - **Auto-freeze.** Mutative leaves results unfrozen unless `enableAutoFreeze` is set, while Immer freezes them by default. With freezing on, both freeze the whole result deeply, unchanged parts included, and freeze Map and Set instances so that their mutators throw. Mutative also freezes the objects that a Map uses as keys, which Immer leaves unfrozen. - **Options per call.** Each `create()` and `apply()` call takes its own options, and `makeCreator()` sets defaults, where Immer's `setAutoFreeze()` and its plugins change the module for every caller. Maps, Sets and patches work without the `enableMapSet()` and `enablePatches()` calls that Immer requires and throws without. -- **Marking.** A `mark` function decides how values are drafted: it can make class instances draftable without changing their class, where Immer requires an `[immerable]` property on it, keep values mutable so that the recipe changes them in place, or return a function that copies a value. Mutative copies a class instance that a mark makes draftable with all its own properties, non-enumerable ones included, as Immer does with `setUseStrictShallowCopy('class_only')`, but assigns those that are enumerable and writable, as Immer's default copy does, so an own property that shadows a setter of the class runs it; see [Copying marked instances](/docs/advanced-guides/mark#copying-marked-instances). Both copy only the enumerable properties of plain objects and read their getters as values. See [mark](/docs/advanced-guides/mark). +- **Marking.** A `mark` function decides how values are drafted: it can make class instances draftable without changing their class, where Immer requires an `[immerable]` property on it, keep values mutable so that the recipe changes them in place, or return a function that copies a value. Mutative copies a class instance that a mark makes draftable with all its own properties, non-enumerable ones included, as Immer does with `setUseStrictShallowCopy('class_only')`, but assigns those that are enumerable, writable and configurable, as Immer's default copy does, so an own property that shadows a setter of the class runs it; see [Copying marked instances](/docs/advanced-guides/mark#copying-marked-instances). Both copy only the enumerable properties of plain objects and read their getters as values. See [mark](/docs/advanced-guides/mark). - **Strict mode.** With `strict: true`, reading a value that the recipe could change without a draft, such as a class instance that is not marked, throws unless the read happens inside `unsafe()`. Immer has no such mode. See [strict mode](/docs/advanced-guides/strict-mode). ## Drafts From 4322cc3fa5d0456543cc1b12a900c58ae9d4beb0 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 12:03:06 +0800 Subject: [PATCH 19/21] docs: state that reading the properties of a draft kept as a Map key throws --- README.md | 4 ++-- website/content/docs/advanced-guides/migration-v2.mdx | 2 +- website/content/docs/extra-topics/faq.mdx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4d45cdd0..ad384c3a 100644 --- a/README.md +++ b/README.md @@ -653,7 +653,7 @@ Yes, but they compare elements as iterating the draft returns them. An object of - Can a draft be a Map key? -No. Map keys are used as they are, and a draft is revoked when its producer ends, so a Map that kept a draft as a key would hold a key that throws when it is read, and so would the patches that name it. Use `original(draft)`, `current(draft)` or an id as the key. Development builds throw when a recipe passes a draft to the `set()` method of a Map draft. +No. Map keys are used as they are, and a draft is revoked when its producer ends, so a Map that kept a draft as a key, and the patches that name that key, would hold a revoked draft, whose properties throw when they are read. Use `original(draft)`, `current(draft)` or an id as the key. Development builds throw when a recipe passes a draft to the `set()` method of a Map draft. - Does Mutative support shared references? @@ -794,7 +794,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - In strict mode, `rawReturn()` of a value without drafts no longer prints contradictory warnings. - With `enablePatches: { pathAsArray: false }`, development builds throw when a patch path would hold a Map key that is not a string, or a symbol key, as a string path cannot name such a key: applying the patch writes another key, for example `'1'` instead of `1`, or fails for a change below that key. Production builds still generate such patches, as v1 did, and still throw a `TypeError` for a symbol key. Keep the default array paths for such keys. - With `enableAutoFreeze`, development builds report a circular reference only for an object that holds itself. A changed draft that holds its original, as `draft.prev = original(draft)` makes it, or that links to a shared object through another path no longer throws; production builds always froze such states. For a real cycle, the error names the path of the object that repeats, which can be one key longer than in v1. -- Development builds throw when a recipe passes a draft to the `set()` method of a Map draft, as in `draft.map.set(draft.key, value)`: Map keys are used as they are, so the next state would keep the draft as a key, revoked, and reading it would throw. Production builds still keep such a key, as v1 did. Use `original(draft)`, `current(draft)` or an id as the key. +- Development builds throw when a recipe passes a draft to the `set()` method of a Map draft, as in `draft.map.set(draft.key, value)`: Map keys are used as they are, so the next state would keep the draft as a key, revoked, and reading its properties would throw. Production builds still keep such a key, as v1 did. Use `original(draft)`, `current(draft)` or an id as the key. ### TypeScript diff --git a/website/content/docs/advanced-guides/migration-v2.mdx b/website/content/docs/advanced-guides/migration-v2.mdx index f9c32f76..20374dc4 100644 --- a/website/content/docs/advanced-guides/migration-v2.mdx +++ b/website/content/docs/advanced-guides/migration-v2.mdx @@ -36,7 +36,7 @@ Mutative v2 keeps the v1 API. The changes below, made since v1.3.0, can affect e - In strict mode, `rawReturn()` of a value without drafts no longer prints contradictory warnings. - With `enablePatches: { pathAsArray: false }`, development builds throw when a patch path would hold a Map key that is not a string, or a symbol key, as a string path cannot name such a key: applying the patch writes another key, for example `'1'` instead of `1`, or fails for a change below that key. Production builds still generate such patches, as v1 did, and still throw a `TypeError` for a symbol key. Keep the default array paths for such keys. - With `enableAutoFreeze`, development builds report a circular reference only for an object that holds itself. A changed draft that holds its original, as `draft.prev = original(draft)` makes it, or that links to a shared object through another path no longer throws; production builds always froze such states. For a real cycle, the error names the path of the object that repeats, which can be one key longer than in v1. -- Development builds throw when a recipe passes a draft to the `set()` method of a Map draft, as in `draft.map.set(draft.key, value)`: Map keys are used as they are, so the next state would keep the draft as a key, revoked, and reading it would throw. Production builds still keep such a key, as v1 did. Use `original(draft)`, `current(draft)` or an id as the key. +- Development builds throw when a recipe passes a draft to the `set()` method of a Map draft, as in `draft.map.set(draft.key, value)`: Map keys are used as they are, so the next state would keep the draft as a key, revoked, and reading its properties would throw. Production builds still keep such a key, as v1 did. Use `original(draft)`, `current(draft)` or an id as the key. ## TypeScript diff --git a/website/content/docs/extra-topics/faq.mdx b/website/content/docs/extra-topics/faq.mdx index ae092233..5730230e 100644 --- a/website/content/docs/extra-topics/faq.mdx +++ b/website/content/docs/extra-topics/faq.mdx @@ -30,7 +30,7 @@ Yes, but they compare elements as iterating the draft returns them. An object of - Can a draft be a Map key? -No. Map keys are used as they are, and a draft is revoked when its producer ends, so a Map that kept a draft as a key would hold a key that throws when it is read, and so would the patches that name it. Use `original(draft)`, `current(draft)` or an id as the key. Development builds throw when a recipe passes a draft to the `set()` method of a Map draft. +No. Map keys are used as they are, and a draft is revoked when its producer ends, so a Map that kept a draft as a key, and the patches that name that key, would hold a revoked draft, whose properties throw when they are read. Use `original(draft)`, `current(draft)` or an id as the key. Development builds throw when a recipe passes a draft to the `set()` method of a Map draft. - Does Mutative support shared references? From 6ce57edc10a89e889792cf81b6962f16c8c2e293 Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 12:04:57 +0800 Subject: [PATCH 20/21] build: refresh the size baseline for the index check of Set items in patch paths --- scripts/build-size-baseline.json | 76 ++++++++++++++++---------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/scripts/build-size-baseline.json b/scripts/build-size-baseline.json index ccc179c4..b4e52830 100644 --- a/scripts/build-size-baseline.json +++ b/scripts/build-size-baseline.json @@ -1,7 +1,7 @@ { "reference": { - "revision": "421150af0091e35a045ad9f5d928f8e580d86809", - "description": "Development builds throw for a draft used as a Map key, also for keys whose properties cannot be read, and freezing no longer records the originals of changed drafts", + "revision": "4322cc3fa5d0456543cc1b12a900c58ae9d4beb0", + "description": "Patch paths find a Set item only at its index", "node": "v24.16.0", "commands": [ "pnpm install --frozen-lockfile", @@ -15,39 +15,39 @@ "esbuild": "0.24.0", "measurements": { "artifact/mutative.cjs.production.min.js": { - "raw": 27324, - "gzip": 9178, - "brotli": 8423 + "raw": 27367, + "gzip": 9202, + "brotli": 8446 }, "artifact/mutative.umd.production.min.js": { - "raw": 27481, - "gzip": 9239, - "brotli": 8480 + "raw": 27522, + "gzip": 9258, + "brotli": 8488 }, "artifact/mutative.esm.production.min.mjs": { - "raw": 27299, - "gzip": 9200, - "brotli": 8452 + "raw": 27339, + "gzip": 9221, + "brotli": 8462 }, "artifact/mutative.cjs.development.js": { - "raw": 72847, - "gzip": 17743, - "brotli": 15413 + "raw": 72935, + "gzip": 17763, + "brotli": 15418 }, "artifact/mutative.umd.development.js": { - "raw": 75122, - "gzip": 17940, - "brotli": 15569 + "raw": 75214, + "gzip": 17961, + "brotli": 15571 }, "artifact/mutative.esm.js": { - "raw": 73682, - "gzip": 17833, - "brotli": 15458 + "raw": 73770, + "gzip": 17853, + "brotli": 15466 }, "artifact/mutative.esm.mjs": { - "raw": 73683, - "gzip": 17834, - "brotli": 15477 + "raw": 73771, + "gzip": 17854, + "brotli": 15468 }, "consumer/import/create": { "raw": 24533, @@ -55,29 +55,29 @@ "brotli": 7892 }, "consumer/import/patches": { - "raw": 26549, - "gzip": 9267, - "brotli": 8498 + "raw": 26588, + "gzip": 9280, + "brotli": 8513 }, "consumer/import/all": { - "raw": 27334, - "gzip": 9525, - "brotli": 8738 + "raw": 27373, + "gzip": 9537, + "brotli": 8752 }, "consumer/require/create": { - "raw": 26826, - "gzip": 9513, - "brotli": 8735 + "raw": 26867, + "gzip": 9518, + "brotli": 8721 }, "consumer/require/patches": { - "raw": 26894, - "gzip": 9553, - "brotli": 8748 + "raw": 26935, + "gzip": 9557, + "brotli": 8750 }, "consumer/require/all": { - "raw": 26791, - "gzip": 9492, - "brotli": 8700 + "raw": 26832, + "gzip": 9499, + "brotli": 8702 } } } From d745da6b33774b98812e96f3d14415a11ba6158b Mon Sep 17 00:00:00 2001 From: unadlib Date: Sun, 11 Oct 2026 12:56:10 +0800 Subject: [PATCH 21/21] fix: convert the key of a Set step in a patch path once --- src/apply.ts | 4 +++- test/apply.test.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/apply.ts b/src/apply.ts index 4bee9997..721af5ca 100644 --- a/src/apply.ts +++ b/src/apply.ts @@ -144,7 +144,9 @@ export function apply< if (parentType === DraftType.Set) { // A Set item is found at its index in the array of the Set's items, // so that no other key, such as `__proto__`, reads a property that - // the array inherits. No number names such a property. + // the array inherits. No number names such a property. The key is + // converted once, so that the check and the read use the same key. + key = normalizePatchKey(key); base = Array.from(base); if (typeof key !== 'number' && !has(base, key)) die(ErrorCode.CannotApplyPatch, path); diff --git a/test/apply.test.ts b/test/apply.test.ts index ac0d2b5d..94f674c5 100644 --- a/test/apply.test.ts +++ b/test/apply.test.ts @@ -1387,6 +1387,43 @@ test('a Set item of a patch path is found only at its index', () => { ).toThrow(`Cannot apply patch at 'set//v'.`); }); +test('a key of a Set step in a patch path is converted once', () => { + // A key that converts to another value on each call must not pass the check + // of an index as one key and then be read as another. + for (const second of ['__proto__', '1']) { + for (const options of [undefined, { mutable: true }]) { + let calls = 0; + const key = { + [Symbol.toPrimitive]() { + calls += 1; + return calls === 1 ? '0' : second; + }, + }; + const state = { set: new Set([{ v: 1 }, { v: 2 }]) }; + let polluted = false; + try { + const result = apply( + state, + [{ op: 'replace', path: ['set', key as any, 'probe'], value: true }], + options + ); + expect([...(result ?? state).set]).toEqual([ + { v: 1, probe: true }, + { v: 2 }, + ]); + } finally { + polluted = Object.prototype.hasOwnProperty.call( + Array.prototype, + 'probe' + ); + delete (Array.prototype as any).probe; + } + expect(polluted).toBe(false); + expect(calls).toBe(1); + } + } +}); + test.each([false, true])( 'coercible terminal keys cannot set a prototype (mutable: %s)', (mutable) => {