diff --git a/README.md b/README.md index ad384c3a..ff1ce11e 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,8 @@ Mutative ships patches, `Map`/`Set` support and native array methods built in; I | Bundle | Immer | Mutative | | ------------------------------------------- | -----: | -------: | -| `produce` / `create` only | 3.6 kB | 7.8 kB | -| With patches, `Map`/`Set` and array methods | 6.4 kB | 8.4 kB | +| `produce` / `create` only | 3.6 kB | 7.9 kB | +| With patches, `Map`/`Set` and array methods | 6.4 kB | 8.5 kB | Mutative's `create` includes patches, `Map`/`Set` support and the native array methods even when a recipe does not use them: they are part of `create`, not separate imports, so bundlers cannot drop them. The difference buys the draft fast paths and the native array methods measured in the [performance summary](./perf-testing/reports/SUMMARY.md), which also records the artifact sizes of each measured source. See the [array methods FAQ](#faqs) for the supported fast paths and their contract, and the [Immer regression cases](./test/immer-array-methods.md) for the behavior of its array-method plugin. @@ -242,6 +242,8 @@ The recipe can be an async function: `create()` then returns a Promise of the re > A string path is a JSON Pointer and holds only strings, so it cannot name a Map key that is not a string, or a symbol key: with `pathAsArray: false`, the patch of `draft.set(1, 'b')` would set the key `'1'`. Keep the default array paths for such keys; development builds throw an error when a string path would hold one. + > TypeScript types the result from the type of `enablePatches` in the options passed to `create()` or `makeCreator()`: `true` or an object gives `[state, patches, inversePatches]`, and `boolean` gives either. Options typed `{ enablePatches: true }` that may be `undefined`, such as an optional parameter of a wrapper, or typed `{ enablePatches?: true }` still give the tuple, although `create()` returns the state alone when patches are off; type the flag as `boolean` there. With an explicit state type, TypeScript infers no other type argument, so pass `boolean` as the type argument for `enablePatches` as well, as in `create(base, recipe, options)` or `create(recipe, options)`. + - enableAutoFreeze - `boolean`, the default is false. > Enable autoFreeze, and return frozen state, and enable circular reference checking only in `development` mode. @@ -394,6 +396,8 @@ 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. +> `apply()` throws for a patch path through the `__proto__` or `constructor` of an object or array, or through the `prototype` of a function, so that patches cannot change the prototypes of objects; Map keys of these names are ordinary keys. It reads the other path segments as properties, as Immer does, and does not guard against prototypes that other code has already changed. + > 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()` @@ -801,7 +805,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`, as this version still does for a primitive state type, such as `number`. +- 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 for a primitive state type, such as `number`, and in curried producers and creators from `makeCreator()`. v1 typed the result as `State`. ### Fixes that change results diff --git a/docs/functions/apply.md b/docs/functions/apply.md index 2a4f8848..ca92da5d 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/apply.ts#L62) +Defined in: [apply.ts:63](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/apply.ts#L63) `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/8667c30c96a236a80844257728eb46826ca6f5d8/src/apply.ts#L71) +Defined in: [apply.ts:72](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/apply.ts#L72) `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/8667c30c96a236a80844257728eb46826ca6f5d8/src/apply.ts#L78) +Defined in: [apply.ts:79](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/apply.ts#L79) `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/8667c30c96a236a80844257728eb46826ca6f5d8/src/apply.ts#L89) +Defined in: [apply.ts:90](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/apply.ts#L90) `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/8667c30c96a236a80844257728eb46826ca6f5d8/src/apply.ts#L94) +Defined in: [apply.ts:95](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/apply.ts#L95) `apply(state, patches)` to apply patches to state diff --git a/docs/functions/castDraft.md b/docs/functions/castDraft.md index 23563d72..d47ff341 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/cast.ts#L6) +Defined in: [utils/cast.ts:6](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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 5cdbb3b2..1fa5a2c4 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/cast.ts#L13) +Defined in: [utils/cast.ts:13](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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 4cdff0d8..f67bd5ea 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/cast.ts#L20) +Defined in: [utils/cast.ts:20](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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 4576b773..9e2a5dc6 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/current.ts#L198) +Defined in: [current.ts:198](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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/8667c30c96a236a80844257728eb46826ca6f5d8/src/current.ts#L200) +Defined in: [current.ts:200](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/current.ts#L200) ### Type Parameters diff --git a/docs/functions/isDraft.md b/docs/functions/isDraft.md index 609bc2f8..85244467 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/draft.ts#L33) +Defined in: [utils/draft.ts:33](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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 8b0a21de..b86ce06e 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/draft.ts#L59) +Defined in: [utils/draft.ts:59](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/utils/draft.ts#L59) Check if a value is draftable diff --git a/docs/functions/markSimpleObject.md b/docs/functions/markSimpleObject.md index 0829e879..e0a98ad2 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/utils/marker.ts#L26) +Defined in: [utils/marker.ts:26](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/utils/marker.ts#L26) ## Parameters diff --git a/docs/functions/original.md b/docs/functions/original.md index 8bc17f43..23b09202 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/original.ts#L22) +Defined in: [original.ts:22](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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 29000eab..3b553837 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/rawReturn.ts#L22) +Defined in: [rawReturn.ts:22](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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 856cd58e..744abe38 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/unsafe.ts#L57) +Defined in: [unsafe.ts:57](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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 362124ca..6f991125 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L171) +Defined in: [interface.ts:171](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/interface.ts#L171) ## Type Parameters @@ -24,7 +24,7 @@ Defined in: [interface.ts:171](https://github.com/unadlib/mutative/blob/8667c30c > `optional` **enableAutoFreeze**: `F` -Defined in: [interface.ts:184](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L184) +Defined in: [interface.ts:184](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L180) +Defined in: [interface.ts:180](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L189) +Defined in: [interface.ts:189](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L176) +Defined in: [interface.ts:176](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/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 4cfeaf20..8ee6a403 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L229) +Defined in: [interface.ts:229](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/interface.ts#L229) ## Type Parameters diff --git a/docs/type-aliases/DraftedObject.md b/docs/type-aliases/DraftedObject.md index cdcb52d2..1a902141 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L225) +Defined in: [interface.ts:225](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/interface.ts#L225) ## Type Parameters diff --git a/docs/type-aliases/Immutable.md b/docs/type-aliases/Immutable.md index 0f4da060..b661bfd5 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L211) +Defined in: [interface.ts:211](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/interface.ts#L211) ## Type Parameters diff --git a/docs/type-aliases/Patch.md b/docs/type-aliases/Patch.md index f37cc04e..822a8251 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L96) +Defined in: [interface.ts:96](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/interface.ts#L96) ## Type Parameters diff --git a/docs/type-aliases/Patches.md b/docs/type-aliases/Patches.md index ef117cca..c773da83 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L110) +Defined in: [interface.ts:110](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/interface.ts#L110) ## Type Parameters diff --git a/docs/type-aliases/PatchesOptions.md b/docs/type-aliases/PatchesOptions.md index d73b4977..2a10b555 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/8667c30c96a236a80844257728eb46826ca6f5d8/src/interface.ts#L18) +Defined in: [interface.ts:18](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/interface.ts#L18) ## Type Declaration diff --git a/docs/variables/create.md b/docs/variables/create.md index 1b59f380..303e5a67 100644 --- a/docs/variables/create.md +++ b/docs/variables/create.md @@ -6,9 +6,9 @@ # 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`\>\]; \} +> `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`\>; \<`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`\>; \<`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/8667c30c96a236a80844257728eb46826ca6f5d8/src/create.ts#L25) +Defined in: [create.ts:25](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/create.ts#L25) `create(baseState, callback, options)` to create the next state @@ -105,6 +105,42 @@ 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 + +`ExplicitPrimitiveRecipe`\<`T`, \[\]\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`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 @@ -129,7 +165,7 @@ expect(state.arr).toBe(baseState.arr); #### mutate -`ExplicitMaybeAsyncRecipe`\<`T`, \[\]\> +`ExplicitRecipe`\<`T`, \[\], `ExplicitReturn`\<`T`\> \| `Promise`\<`ExplicitReturn`\<`T`\>\>\> #### options? @@ -321,6 +357,56 @@ expect(state.arr).toBe(baseState.arr); ## 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 + +`ExplicitPrimitiveRecipe`\<`T`, `P`\> + +#### 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`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> ### Type Parameters @@ -345,7 +431,7 @@ expect(state.arr).toBe(baseState.arr); #### mutate -`ExplicitMaybeAsyncRecipe`\<`T`, `P`\> +`ExplicitRecipe`\<`T`, `P`, `ExplicitReturn`\<`T`\> \| `Promise`\<`ExplicitReturn`\<`T`\>\>\> #### options? diff --git a/docs/variables/makeCreator.md b/docs/variables/makeCreator.md index 85dfb120..bf3ddddd 100644 --- a/docs/variables/makeCreator.md +++ b/docs/variables/makeCreator.md @@ -6,9 +6,9 @@ # Variable: 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`\>\]; \} +> `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`\>; \<`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`\>; \<`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:184](https://github.com/unadlib/mutative/blob/8667c30c96a236a80844257728eb46826ca6f5d8/src/makeCreator.ts#L184) +Defined in: [makeCreator.ts:205](https://github.com/unadlib/mutative/blob/c2e96282fb77355d4c5f83d86c6d201dee734d45/src/makeCreator.ts#L205) `makeCreator(options)` to make a creator function. @@ -119,6 +119,40 @@ expect(Object.isFrozen(state)).toBeTruthy(); `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> +> \<`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 + +`ExplicitPrimitiveRecipe`\<`T`, \[\]\> + +#### options? + +[`Options`](../interfaces/Options.md)\<`O`, `F`\> + +### Returns + +`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> + > \<`T`, `F`, `O`\>(`base`, `mutate`, `options?`): `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> ### Type Parameters @@ -143,7 +177,7 @@ expect(Object.isFrozen(state)).toBeTruthy(); #### mutate -`ExplicitMaybeAsyncRecipe`\<`T`, \[\]\> +`ExplicitRecipe`\<`T`, \[\], `ExplicitReturn`\<`T`\> \| `Promise`\<`ExplicitReturn`\<`T`\>\>\> #### options? @@ -325,6 +359,54 @@ expect(Object.isFrozen(state)).toBeTruthy(); `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> +> \<`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 + +`ExplicitPrimitiveRecipe`\<`T`, `P`\> + +#### 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`) => `Result`\<`ExplicitState`\<`T`\>, `O`, `F`\> \| `Promise`\<`Result`\<`ExplicitState`\<`T`\>, `O`, `F`\>\> ### Type Parameters @@ -349,7 +431,7 @@ expect(Object.isFrozen(state)).toBeTruthy(); #### mutate -`ExplicitMaybeAsyncRecipe`\<`T`, `P`\> +`ExplicitRecipe`\<`T`, `P`, `ExplicitReturn`\<`T`\> \| `Promise`\<`ExplicitReturn`\<`T`\>\>\> #### options? diff --git a/package.json b/package.json index 7a97c76b..b5bb36a6 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "name": "ESM, all exports", "path": "dist/mutative.esm.mjs", "import": "*", - "limit": "8.5 KB" + "limit": "8.6 KB" } ], "repository": { diff --git a/perf-testing/reports/SUMMARY.md b/perf-testing/reports/SUMMARY.md index 1eba3518..03c96986 100644 --- a/perf-testing/reports/SUMMARY.md +++ b/perf-testing/reports/SUMMARY.md @@ -255,6 +255,31 @@ Over the 12 cells of `shift-and-update`, the PR took 0.82 of the time of `main` The source before PR #183, which built the map for the first lookup, took 632 µs and 659 µs for the 8 and 32 edits of 10,000 frozen rows. A prototype of the same lookup, with an edit after `shift()`, eight edits after `shift()`, an edit after `splice()` and an edit after `reverse()` of 10,000 frozen and unfrozen rows, took 0.11–1.00 of the time of `main` in Chromium 148, 0.63–1.05 in Firefox 150, whose `lastIndexOf` has no such slow path, and 0.97–1.07 in WebKit 26.4, whose frozen arrays are slow to scan in either direction; these are single runs in one page per recipe and build. The production CJS artifact grew from 8,367 to 8,439 Brotli bytes. +Following a repeated element forward copy by copy costs one search per later copy, so the moved rows of an array that holds a few objects many times, as a grid of shared cells does, took far longer than before PR #200: `shift()` and eight reads near the start of 10,000 rows of two objects in turn took 581 µs against 10.3 µs. [PR #202](https://github.com/unadlib/mutative/pull/202) follows 16 later copies one after another, then starts each forward search twice as far ahead as the one before, and searches backward only from the start of the first search that finds no copy, instead of from the end of the array; the rows after the last copy are scanned forward, which V8 runs on its fast path for frozen arrays too. Production CJS builds of `9f93f16`, before PR #200, of `main` at `09ab87b` and of the PR, outside the suite, each in processes of its own, three rounds in rotated order, µs per update of `shift()` and eight reads near the start; a frozen input is a frozen base state, and a frozen result adds auto-freeze: + +| Rows | Layout | Frozen | `9f93f16` | `main` | PR #202 | +| ---: | --- | --- | ---: | ---: | ---: | +| 10,000 | no repeated object | no | 138 | 27.9 | 27.9 | +| 10,000 | no repeated object | input and result | 2,049 | 198 | 199 | +| 10,000 | two objects in turn | no | 10.3 | 581 | 13.6 | +| 10,000 | two objects in turn | input and result | 182 | 745 | 187 | +| 100,000 | two objects in turn | no | 142 | 5,840 | 145 | +| 100,000 | two objects in turn | input and result | 1,905 | 7,539 | 1,897 | +| 10,000 | 16 objects in turn | no | 10.5 | 93.5 | 14.3 | +| 100,000 | 16 objects in turn | no | 141 | 973 | 145 | +| 10,000 | each object twice | input and result | 1,120 | 191 | 191 | +| 10,000 | two objects in turn in the first 20 rows | input and result | 2,060 | 201 | 199 | +| 10,000 | two objects in turn in the first 100 rows | no | 137 | 33.2 | 30.9 | +| 10,000 | two objects in turn in the first 100 rows | input | 1,897 | 62.7 | 64.4 | +| 10,000 | two objects in turn in the first 100 rows | input and result | 2,041 | 205 | 206 | +| 100,000 | two objects in turn in the first 100 rows | input | 19,181 | 617 | 623 | +| 100,000 | two objects in turn in the first 100 rows | input and result | 20,793 | 2,091 | 2,086 | +| 10,000 | two objects in turn in the first half | no | 74.6 | 303 | 58.0 | +| 10,000 | two objects in turn in the first half | input | 985 | 330 | 655 | +| 10,000 | two objects in turn in the first half | input and result | 1,128 | 472 | 797 | + +The backward search covers at most the last doubled step, which a long run of copies makes long: with the first half of the rows repeated, the PR took twice the time of `main` on frozen input, and less than before PR #200. Arrays of objects repeated throughout took 3–4 µs more than before PR #200, whose single backward search found their last copies at once. A first version of the PR searched from the end after 16 copies, as before PR #200, and took 1,895 µs for the first 100 rows of 10,000 on frozen input. At 10,000 rows with auto-freeze off, the PR took at most about 1 µs more than that first version in every layout in Chromium 148, Firefox 150 and WebKit 26.4, and 44 µs instead of 1,553 µs in Chromium for the first 100 rows on frozen input. Two other searches were measured: halving the range with forward searches was faster in V8 but up to 3.7 times slower than the first version in WebKit on frozen input, and a backward loop in JavaScript became up to 7.5 times slower once its lookups had seen arrays of several kinds. Only the first eight lookups of an array in an update search; later ones read the map of original indices, so these searches run at most eight times per array and update. The production CJS artifact grew from 8,449 to 8,472 Brotli bytes. + ### Set reads, Map iteration, moved rows and unchanged collections PRs #183 and #184 measured four of their improvements with micro-benchmarks only, so [PR #198](https://github.com/unadlib/mutative/pull/198) added a scenario for each. Microseconds per scenario, medians of three runs, patches off; `shift-and-update` was measured again for PR #200: @@ -398,12 +423,12 @@ The source changes since `2ab58fc` outside the cells that PRs #195 and #196 meas - Returned values: without auto-freeze, the candidate searches every object of a returned value for drafts, as Immer does: `return-replace` took 2.61 ms at 10,000 rows, against 5.73 ms for Immer without auto-freeze. Immer's default auto-freeze skips frozen values (0.79 µs), and so do production builds of the candidate with auto-freeze (0.53 µs). Wrapping the value in `rawReturn()` skips the search (0.26 µs). - A Set draft copies the Set on its first change and rebuilds it in order once an item that the recipe read through an iterator changed: `set-update` took 533 µs at 10,000 items, against 406 µs for the hand-written reducer, which copies the Set once. With patches, a Set that added or deleted items compares every original item with its copy and every item of the copy with the original: `set-add` took 444 µs with patches at 10,000 items, against 30.9 µs without. Copying the Map dominates `map-update` at 10,000 entries in every library, about 525 µs. -- After `reverse()`, or after moves whose last offset does not lead back to an element, the candidate finds a moved row's original index by searching the original array from its end with `lastIndexOf`, which V8 runs about 15 times as slowly on frozen, sealed and non-extensible arrays: editing the middle row of 10,000 frozen rows took 310 µs after `reverse()`, against 188 µs after `shift()`, measured outside the suite. +- After `reverse()`, or after moves whose last offset does not lead back to an element, the candidate finds a moved row's original index by searching the original array from its end with `lastIndexOf`, which V8 runs about 15 times as slowly on frozen, sealed and non-extensible arrays: editing the middle row of 10,000 frozen rows took 310 µs after `reverse()`, against 188 µs after `shift()`, measured outside the suite. An element with more than 16 later copies is searched for backward over at most the last doubled step of its forward searches, which a long run of copies makes long: `shift()` and eight reads of 10,000 rows whose first half holds two objects in turn took 655 µs on frozen input, against 330 µs when the lookups followed every copy forward. - With auto-freeze, Immer updated a class instance with 1,000 fields faster: 237 µs against 415 µs. Without freezing the candidate is faster, 99.7 µs against 205 µs, so the difference lies in freezing the candidate's copy, which it makes property by property with descriptors where Immer 11 uses `Object.assign`. - Even in processes of their own, some cells varied between processes of the same build by up to about 1.6 times, for example `update-reuse` with freeze and patches on (170–240 µs) and Immer's `map-read` at 10,000 rows (5.1–8.0 ms); `class-wide-update` without freezing at 1,000 rows took either about 95 µs or about 200 µs in both Mutative builds. The freeze-on upstream workloads at 100 rows vary most among the candidate's cells, which is why `add` and `update-largeObject1` with freeze on are slower than Mutative 1.3.0 in this batch. Medians of three processes absorb one outlier, not two; compare such cells through geometric means rather than one by one. - Isolated processes measure each library without the other libraries and scenarios, not inside a real application, whose own code also changes V8's state; wide-object timings in particular depend on it, by up to about 70 times. - Freeze-on inputs are already frozen, so freezing cold external data is excluded. Allocation is sampled, and small retained deltas are noisy. All measurements come from one machine and Node.js; browser engines may differ: in WebKit 26.4, `Object.freeze` of a 10,000-element array took about 2.4 ms and every scan of a frozen array was slow, so an update of 10,000 frozen rows took several milliseconds there with every build. -- Bundle size: as the README measures it with esbuild 0.24.0, importing only `create` bundles to 7.8 kB with Brotli against 3.6 kB for Immer's `produce`, and with patches, Map and Set support and array methods to 8.4 kB against 6.4 kB for Immer with its plugins. `size-limit` measures the production CJS artifact at 8.56 kB against its 8.6 kB cap (8,367 Brotli bytes), and the ESM consumers of `create` and of all exports at 7.5 kB and 8.41 kB against 7.5 kB and 8.5 kB. +- Bundle size: as the README measures it with esbuild 0.24.0, importing only `create` bundles to 7.9 kB with Brotli against 3.6 kB for Immer's `produce`, and with patches, Map and Set support and array methods to 8.5 kB against 6.4 kB for Immer with its plugins. `size-limit` measures the production CJS artifact at 8.64 kB against its 8.7 kB cap (8,472 Brotli bytes), and the ESM consumers of `create` and of all exports at 7.58 kB and 8.52 kB against 7.7 kB and 8.6 kB. ## Native array method contract @@ -510,6 +535,7 @@ MUTATIVE_PERF_BUNDLE="perf-testing/dist/$NAME/immutability-benchmarks.mjs" node Earlier versions of this summary, in Git history, describe each round in detail. Cell counts compare the candidate with the named comparator: faster, within 5%, and slower. +- 2026-10-11, PR #202: a lookup of a moved array element follows 16 later copies of a repeated element one after another, then searches forward with doubling steps and backward only from the first step past its last copy, instead of once per later copy since PR #200. Against `main` at `09ab87b`, outside the suite, `shift()` and eight reads near the start of 10,000 rows of two objects in turn took 13.6 µs instead of 581 µs (10.3 µs before PR #200), and of 100,000 rows 145 µs instead of 5,840 µs; arrays without repeated objects, with few copies of each, or with copies in their first 100 rows kept their time, and 10,000 rows whose first half holds two objects in turn took 655 µs instead of 330 µs on frozen input. The production CJS artifact grew from 8,449 to 8,472 Brotli bytes. - 2026-10-10, PR #200: an array draft looks up the original index of a moved element first where the offset of the last move puts it, and searches forward from there, instead of searching the original array from its end, which V8 runs about 15 times as slowly on frozen arrays. Against `main` at `9f93f16`, Mutative alone, `shift-and-update` took 0.82 of the time over its 12 cells and 0.56–0.77 without patches at 1,000 and 10,000 rows, and eight edits of moved rows of 10,000 frozen rows 208 µs instead of 2,050 µs. With its cells measured again: 596 / 16 / 2 of 614 against Immer (geometric mean 3.59), 154 / 0 / 3 of 157 with each library's defaults (6.66), and 538 / 73 / 3 against 1.3.0 (4.29). The production CJS artifact grew from 8,367 to 8,439 Brotli bytes. - 2026-10-09, PR #198: the scenarios `set-read`, `map-forEach`, `shift-and-update` and `map-set-beside` cover improvements of PRs #183 and #184 that only micro-benchmarks had measured, and CI budgets them. Measured with every library in processes of its own at 100, 1,000 and 10,000 rows, their cells join the results: 595 / 16 / 3 of 614 against Immer (geometric mean 3.59), 153 / 1 / 3 of 157 with each library's defaults (6.66), 543 / 70 / 1 against 1.3.0 (4.28), and 18 / 3 / 129 against the hand-written reducer. Built from the commit before the improvement it covers, each scenario took at least 2.3 times as long in one or more modes, and `set-read` 62–65 times as long in every mode. - 2026-10-09, PR #196: `apply()` reads the type of each draft on a patch path from its state instead of asking its proxy twice per step. Against `main` at `4ca6345`, Mutative alone, the PR took 0.68 of the time over 14 patch application cells, and `apply-update-10pct` 0.49–0.61 of it. The patch application scenarios, measured again with every library in processes of its own, replace the earlier cells: 551 / 12 / 3 of 566 against Immer (geometric mean 3.66), 142 / 0 / 3 of 145 with each library's defaults (6.76), and 495 / 70 / 1 against 1.3.0 (3.88). The production CJS artifact grew from 8,334 to 8,367 Brotli bytes. diff --git a/scripts/build-size-baseline.json b/scripts/build-size-baseline.json index b4e52830..c2a1e45d 100644 --- a/scripts/build-size-baseline.json +++ b/scripts/build-size-baseline.json @@ -1,7 +1,7 @@ { "reference": { - "revision": "4322cc3fa5d0456543cc1b12a900c58ae9d4beb0", - "description": "Patch paths find a Set item only at its index", + "revision": "22e7114d920186a0b752dade2ac3bd3b7a74fca5", + "description": "A repeated moved array element is searched for with doubling steps and backward from the first step past its last copy", "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": 27367, - "gzip": 9202, - "brotli": 8446 + "raw": 27437, + "gzip": 9226, + "brotli": 8472 }, "artifact/mutative.umd.production.min.js": { - "raw": 27522, - "gzip": 9258, - "brotli": 8488 + "raw": 27592, + "gzip": 9292, + "brotli": 8515 }, "artifact/mutative.esm.production.min.mjs": { - "raw": 27339, - "gzip": 9221, - "brotli": 8462 + "raw": 27409, + "gzip": 9252, + "brotli": 8492 }, "artifact/mutative.cjs.development.js": { - "raw": 72935, - "gzip": 17763, - "brotli": 15418 + "raw": 73119, + "gzip": 17810, + "brotli": 15473 }, "artifact/mutative.umd.development.js": { - "raw": 75214, - "gzip": 17961, - "brotli": 15571 + "raw": 75403, + "gzip": 18014, + "brotli": 15646 }, "artifact/mutative.esm.js": { - "raw": 73770, - "gzip": 17853, - "brotli": 15466 + "raw": 73954, + "gzip": 17904, + "brotli": 15526 }, "artifact/mutative.esm.mjs": { - "raw": 73771, - "gzip": 17854, - "brotli": 15468 + "raw": 73955, + "gzip": 17906, + "brotli": 15527 }, "consumer/import/create": { - "raw": 24533, - "gzip": 8576, - "brotli": 7892 + "raw": 24594, + "gzip": 8609, + "brotli": 7924 }, "consumer/import/patches": { - "raw": 26588, - "gzip": 9280, - "brotli": 8513 + "raw": 26657, + "gzip": 9313, + "brotli": 8550 }, "consumer/import/all": { - "raw": 27373, - "gzip": 9537, - "brotli": 8752 + "raw": 27442, + "gzip": 9576, + "brotli": 8778 }, "consumer/require/create": { - "raw": 26867, - "gzip": 9518, - "brotli": 8721 + "raw": 26936, + "gzip": 9556, + "brotli": 8749 }, "consumer/require/patches": { - "raw": 26935, - "gzip": 9557, - "brotli": 8750 + "raw": 27004, + "gzip": 9591, + "brotli": 8792 }, "consumer/require/all": { - "raw": 26832, - "gzip": 9499, - "brotli": 8702 + "raw": 26901, + "gzip": 9534, + "brotli": 8777 } } } diff --git a/src/array.ts b/src/array.ts index ede7f549..0f6e4dc6 100644 --- a/src/array.ts +++ b/src/array.ts @@ -167,15 +167,22 @@ function baseIndexOf(target: ProxyDraft, value: any, index: number) { found = arrayProto.lastIndexOf.call(original, value); return found === -1 ? undefined : found; } - // A later copy of the element is found by searching forward, which V8 - // runs on its fast path for frozen arrays too, unlike `lastIndexOf`. + // Later copies of the element are found by searching forward, which V8 + // runs on its fast path for frozen arrays too, unlike `lastIndexOf`. After + // 16 copies, each search starts twice as far ahead as the one before; the + // last copy then lies before the start of the first search that finds + // none, and is searched for backward from there, not from the end. + let step = 1; for ( - let next; - (next = arrayProto.indexOf.call(original, value, found + 1)) !== -1; + let next, copies = 0; + (next = arrayProto.indexOf.call(original, value, found + step)) !== -1; ) { found = next; + if (++copies > 16) step *= 2; } - return found; + return step > 1 + ? arrayProto.lastIndexOf.call(original, value, found + step - 1) + : found; } return baseIndices(target).get(value); } diff --git a/src/makeCreator.ts b/src/makeCreator.ts index 165285ef..0b595999 100644 --- a/src/makeCreator.ts +++ b/src/makeCreator.ts @@ -49,15 +49,13 @@ 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 = +// The first overload only gives the async recipes of a primitive state their +// context, so its synchronous recipes take this one, which comes before the +// overload of a recipe that may or may not return a Promise. +type ExplicitPrimitiveRecipe = IsExplicitPrimitive extends true - ? never - : ExplicitRecipe | Promise>>; + ? ExplicitRecipe> + : never; /** @inline */ type MakeCreator = < _F extends boolean = false, @@ -79,10 +77,20 @@ type ExplicitMaybeAsyncRecipe = mutate: ExplicitRecipe>>, options?: ExternalOptions ): Promise, O, F>>; + // A synchronous recipe of a primitive state. + ( + base: ExplicitState, + mutate: ExplicitPrimitiveRecipe, + options?: ExternalOptions + ): Result, O, F>; // A recipe that may or may not return a Promise gives either result. ( base: ExplicitState, - mutate: ExplicitMaybeAsyncRecipe, + mutate: ExplicitRecipe< + T, + [], + ExplicitReturn | Promise> + >, options?: ExternalOptions ): Result, O, F> | Promise, O, F>>; < @@ -132,7 +140,20 @@ type ExplicitMaybeAsyncRecipe = F extends boolean = _F, O extends PatchesOptions = _O, >( - mutate: ExplicitMaybeAsyncRecipe, + mutate: ExplicitPrimitiveRecipe, + options?: ExternalOptions + ): (base: ExplicitState, ...args: P) => Result, O, F>; + < + T = never, + P extends any[] = [], + F extends boolean = _F, + O extends PatchesOptions = _O, + >( + mutate: ExplicitRecipe< + T, + P, + ExplicitReturn | Promise> + >, options?: ExternalOptions ): ( base: ExplicitState, diff --git a/test/array-moved-elements.test.ts b/test/array-moved-elements.test.ts index 1254025c..f911f82e 100644 --- a/test/array-moved-elements.test.ts +++ b/test/array-moved-elements.test.ts @@ -19,42 +19,64 @@ const moves: Record void> = { native ? list.reverse() : Array.prototype.reverse.call(list), }; -describe.each( - Object.keys(moves).flatMap((name) => [ - [name, false], - [name, true], - ]) -)('%s, then edits of moved elements, frozen: %s', (name, frozen) => { - test.each([1, 8, 9, 40])('%i edits', (edits) => { +// A repeated element is drafted under its last original index. +const layouts: Record { id: number; value: number }[]> = { + 'one repeated element': () => { const list = rows(40); - // A repeated element is drafted under its last original index. list[30] = list[5]; - const base = { list }; - if (frozen) { - list.forEach((item) => Object.freeze(item)); - Object.freeze(list); - Object.freeze(base); - } - const before = JSON.stringify(base); - const run = (native: boolean) => - create( - base, - (draft) => { - moves[name as string](draft.list, native); - for (let index = 0; index < edits; index += 1) { - draft.list[(index * 7) % draft.list.length].value += 1; - } - }, - { enablePatches: true, enableAutoFreeze: frozen as boolean } - ); - const [state, patches, inversePatches] = run(true); - const [expected] = run(false); - expect(state).toEqual(expected); - expect(apply(base, patches)).toEqual(state); - expect(apply(state, inversePatches)).toEqual(base); - expect(JSON.stringify(base)).toBe(before); - }); -}); + return list; + }, + 'two alternating elements': () => { + const [a, b] = rows(2); + return Array.from({ length: 40 }, (_, index) => (index % 2 ? b : a)); + }, + 'two alternating elements, then unique ones': () => { + const [a, b] = rows(2); + return rows(60).map((item, index) => + index < 40 ? (index % 2 ? b : a) : item + ); + }, +}; + +describe.each( + Object.keys(moves).flatMap((name) => + Object.keys(layouts).flatMap((layout) => [ + [name, layout, false], + [name, layout, true], + ]) + ) +)( + '%s of %s, then edits of moved elements, frozen: %s', + (name, layout, frozen) => { + test.each([1, 8, 9, 40])('%i edits', (edits) => { + const list = layouts[layout as string](); + const base = { list }; + if (frozen) { + list.forEach((item) => Object.freeze(item)); + Object.freeze(list); + Object.freeze(base); + } + const before = JSON.stringify(base); + const run = (native: boolean) => + create( + base, + (draft) => { + moves[name as string](draft.list, native); + for (let index = 0; index < edits; index += 1) { + draft.list[(index * 7) % draft.list.length].value += 1; + } + }, + { enablePatches: true, enableAutoFreeze: frozen as boolean } + ); + const [state, patches, inversePatches] = run(true); + const [expected] = run(false); + expect(state).toEqual(expected); + expect(apply(base, patches)).toEqual(state); + expect(apply(state, inversePatches)).toEqual(base); + expect(JSON.stringify(base)).toBe(before); + }); + } +); test('an edit after a move does not index the original array', () => { const base = { list: rows(1000) }; @@ -93,3 +115,61 @@ test.each([ } } ); + +test('a moved element repeated many times is not searched for once per copy', () => { + const [a, b] = rows(2); + const list = Array.from({ length: 1000 }, (_, index) => (index % 2 ? b : a)); + const base = { list }; + const indexOf = vi.spyOn(Array.prototype, 'indexOf'); + const lastIndexOf = vi.spyOn(Array.prototype, 'lastIndexOf'); + const searches = (spy: typeof indexOf | typeof lastIndexOf) => + spy.mock.contexts.filter((context) => context === list).length; + try { + const state = create(base, (draft) => { + draft.list.shift(); + for (let index = 0; index < 8; index += 1) { + draft.list[index].value = index + 1; + } + }); + expect(state.list.slice(0, 9).map((item) => item.value)).toEqual([ + 1, 2, 3, 4, 5, 6, 7, 8, 0, + ]); + // Each of the eight reads searches forward 17 times one copy after + // another, then about once per doubling of its step up to 1,000, instead + // of once per later copy, up to 499 of them, then backward once. + expect(searches(indexOf)).toBeLessThanOrEqual(8 * (17 + 11)); + expect(searches(lastIndexOf)).toBe(8); + } finally { + indexOf.mockRestore(); + lastIndexOf.mockRestore(); + } +}); + +test('a moved element repeated at the start is not searched for from the end', () => { + const [a, b] = rows(2); + const list = rows(1000).map((item, index) => + index < 100 ? (index % 2 ? b : a) : item + ); + const base = { list }; + const lastIndexOf = vi.spyOn(Array.prototype, 'lastIndexOf'); + try { + const state = create(base, (draft) => { + draft.list.shift(); + for (let index = 0; index < 8; index += 1) { + draft.list[index].value = index + 1; + } + }); + expect(state.list.slice(0, 9).map((item) => item.value)).toEqual([ + 1, 2, 3, 4, 5, 6, 7, 8, 0, + ]); + // Each backward search starts within twice the length of the repeated + // rows instead of at the end of the array. + const starts = lastIndexOf.mock.calls + .filter((_, call) => lastIndexOf.mock.contexts[call] === list) + .map(([, from]) => from); + expect(starts).toHaveLength(8); + for (const from of starts) expect(from).toBeLessThan(200); + } finally { + lastIndexOf.mockRestore(); + } +}); diff --git a/test/types.test.ts b/test/types.test.ts index e162a09a..f73cd72f 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -126,11 +126,38 @@ test('synchronous recipes of a primitive state keep their result type', () => { ); expectTypeOf(frozen).toEqualTypeOf(); expect(frozen).toBe(1); +}); - // A recipe that may return a Promise keeps the synchronous type here. +test('a recipe that may return a Promise gives either result for a primitive state', async () => { const maybe = (value: number): number | Promise => - value > 0 ? Promise.resolve(value) : value + 1; - expectTypeOf(create(0, maybe)).toEqualTypeOf(); + value > 0 ? Promise.resolve(value + 1) : value + 1; + const now = create(0, maybe); + expectTypeOf(now).toEqualTypeOf>(); + expect(now).toBe(1); + const later = create(1, maybe); + expect(later).toBeInstanceOf(Promise); + expect(await later).toBe(2); + + const produce = create(maybe); + expectTypeOf(produce).toEqualTypeOf< + (base: number) => number | Promise + >(); + expect(await produce(1)).toBe(2); + + const withPatches = create(1, maybe, { + enablePatches: true, + }); + expectTypeOf(withPatches).toEqualTypeOf< + | [number, Patches, Patches] + | Promise<[number, Patches, Patches]> + >(); + const [next, patches] = await withPatches; + expect(next).toBe(2); + expect(patches).toEqual([{ op: 'replace', path: [], value: 2 }]); + + const frozen = makeCreator({ enableAutoFreeze: true })(0, maybe); + expectTypeOf(frozen).toEqualTypeOf>(); + expect(frozen).toBe(1); }); test('explicit state types contextualize literal replacement values', async () => { diff --git a/website/content/docs/advanced-guides/migration-v2.mdx b/website/content/docs/advanced-guides/migration-v2.mdx index 20374dc4..18d276b6 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`, as this version still does for a primitive state type, such as `number`. +- 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 for a primitive state type, such as `number`, and in curried producers and creators from `makeCreator()`. v1 typed the result as `State`. ## Fixes that change results diff --git a/website/content/docs/api-reference/apply.mdx b/website/content/docs/api-reference/apply.mdx index 33eff311..9798496c 100644 --- a/website/content/docs/api-reference/apply.mdx +++ b/website/content/docs/api-reference/apply.mdx @@ -86,3 +86,5 @@ 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. +> `apply()` throws for a patch path through the `__proto__` or `constructor` of an object or array, or through the `prototype` of a function, so that patches cannot change the prototypes of objects; Map keys of these names are ordinary keys. It reads the other path segments as properties, as Immer does, and does not guard against prototypes that other code has already changed. + diff --git a/website/content/docs/api-reference/create.mdx b/website/content/docs/api-reference/create.mdx index eee66009..c54af80b 100644 --- a/website/content/docs/api-reference/create.mdx +++ b/website/content/docs/api-reference/create.mdx @@ -44,6 +44,8 @@ Then options is optional. > A string path is a JSON Pointer and holds only strings, so it cannot name a Map key that is not a string, or a symbol key: with `pathAsArray: false`, the patch of `draft.set(1, 'b')` would set the key `'1'`. Keep the default array paths for such keys; development builds throw an error when a string path would hold one. + > TypeScript types the result from the type of `enablePatches` in the options passed to `create()` or `makeCreator()`: `true` or an object gives `[state, patches, inversePatches]`, and `boolean` gives either. Options typed `{ enablePatches: true }` that may be `undefined`, such as an optional parameter of a wrapper, or typed `{ enablePatches?: true }` still give the tuple, although `create()` returns the state alone when patches are off; type the flag as `boolean` there. With an explicit state type, TypeScript infers no other type argument, so pass `boolean` as the type argument for `enablePatches` as well, as in `create(base, recipe, options)` or `create(recipe, options)`. + - [enableAutoFreeze](/docs/advanced-guides/auto-freeze) - `boolean`, the default is false. > Enable autoFreeze, and return frozen state, and enable circular reference checking only in `development` mode. diff --git a/website/content/docs/getting-started/installation.mdx b/website/content/docs/getting-started/installation.mdx index cdd54ca3..b6977b19 100644 --- a/website/content/docs/getting-started/installation.mdx +++ b/website/content/docs/getting-started/installation.mdx @@ -10,7 +10,7 @@ Mutative is a npm package that can be used in Node.js and browsers, it is also c npm install mutative ``` -A production bundle of `create`, `apply`, `current` and `original` is about 8.4 kB after Brotli compression, including patches, `Map`/`Set` support and native array methods. +A production bundle of `create`, `apply`, `current` and `original` is about 8.5 kB after Brotli compression, including patches, `Map`/`Set` support and native array methods. ## Using Mutative from CDN