MCPcopy Index your code
hub / github.com/unadlib/mutative

github.com/unadlib/mutative @v1.3.0 sqlite

repository ↗ · DeepWiki ↗ · release v1.3.0 ↗
384 symbols 1,244 edges 117 files 0 documented · 0% 4 cross-repo links
README

Mutative

Mutative Logo

Node CI Coverage Status npm NPM Downloads license

Mutative - A JavaScript library for efficient immutable updates, 2-6x faster than naive handcrafted reducer, and more than 10x faster than Immer.

Why is Mutative faster than the spread operation(naive handcrafted reducer)?

The spread operation has performance pitfalls, which can be detailed in the following article:

And Mutative optimization focus on shallow copy optimization, more complete lazy drafts, finalization process optimization, and more.

Motivation

Writing immutable updates by hand is usually difficult, prone to errors, and cumbersome. Immer helps us write simpler immutable updates with "mutative" logic.

But its performance issue causes a runtime performance overhead. Immer must have auto-freeze enabled by default(Performance will be worse if auto-freeze is disabled), such immutable state with Immer is not common. In scenarios such as cross-processing, remote data transfer, etc., these immutable data must be constantly frozen.

There are more parts that could be improved, such as better type inference, non-intrusive markup, support for more types of immutability, Safer immutability, more edge cases, and so on.

This is why Mutative was created.

Mutative vs Naive Handcrafted Reducer Performance

Mutative vs Reducer benchmark by object:

  • Naive handcrafted reducer
// baseState type: Record<string, { value: number }>
const state = {
  ...baseState,
  key0: {
    ...baseState.key0,
    value: i,
  },
};
  • Mutative
const state = create(baseState, (draft) => {
  draft.key0.value = i;
});

Mutative vs Reducer benchmark by object

Measure(seconds) to update the 1K-100K items object, lower is better(view source).

Mutative is up to 2x faster than naive handcrafted reducer for updating immutable objects.

Mutative vs Reducer benchmark by array:

  • Naive handcrafted reducer
// baseState type: { value: number }[]

// slower 6x than Mutative
const state = [
  { ...baseState[0], value: i },
  ...baseState.slice(1, baseState.length),
];

// slower 2.5x than Mutative
// const state = baseState.map((item, index) =>
//   index === 0 ? { ...item, value: i } : item
// );

// same performance as Mutative
// const state = [...baseState];
// state[0] = { ...baseState[0], value: i };

The actual difference depends on which spread operation syntax you use.

  • Mutative
const state = create(baseState, (draft) => {
  draft[0].value = i;
});

Mutative vs Reducer benchmark by array

Measure(seconds) to update the 1K-100K items array, lower is better(view source).

Mutative is up to 6x faster than naive handcrafted reducer for updating immutable arrays.

Mutative vs Immer Performance

Mutative passed all of Immer's test cases.

Measure(ops/sec) to update 50K arrays and 1K objects, bigger is better(view source). [Mutative v1.3.0 vs Immer v10.1.3]

Benchmark

Naive handcrafted reducer - No Freeze x 4,777 ops/sec ±1.06% (94 runs sampled)
Mutative - No Freeze x 6,783 ops/sec ±0.71% (96 runs sampled)
Immer - No Freeze x 5.72 ops/sec ±0.39% (19 runs sampled)

Mutative - Freeze x 1,069 ops/sec ±0.75% (97 runs sampled)
Immer - Freeze x 392 ops/sec ±0.66% (92 runs sampled)

Mutative - Patches and No Freeze x 1,006 ops/sec ±1.73% (95 runs sampled)
Immer - Patches and No Freeze x 5.73 ops/sec ±0.16% (19 runs sampled)

Mutative - Patches and Freeze x 548 ops/sec ±1.06% (94 runs sampled)
Immer - Patches and Freeze x 287 ops/sec ±0.84% (93 runs sampled)

The fastest method is Mutative - No Freeze

Run yarn benchmark to measure performance.

OS: macOS 14.7, CPU: Apple M1 Max, Node.js: v22.11.0

Immer relies on auto-freeze to be enabled, if auto-freeze is disabled, Immer will have a huge performance drop and Mutative will have a huge performance lead, especially with large data structures it will have a performance lead of more than 50x.

So if you are using Immer, you will have to enable auto-freeze for performance. Mutative is disabled auto-freeze by default. With the default configuration of both, we can see the 17x performance gap between Mutative (6,783 ops/sec) and Immer (392 ops/sec).

Overall, Mutative has a huge performance lead over Immer in more performance testing scenarios. Run yarn performance to get all the performance results locally.

More Performance Testing Scenarios, Mutative is up to 2.5X-82.9 faster than Immer:

Mutative vs Immer - All benchmark results by average multiplier

view source.

Features and Benefits

  • Mutation makes immutable updates - Immutable data structures supporting objects, arrays, Sets and Maps.
  • High performance - 10x faster than immer by default, even faster than naive handcrafted reducer.
  • Optional freezing state - No freezing of immutable data by default.
  • Support for JSON Patch - Full compliance with JSON Patch specification.
  • 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.
  • Support for reducer - Support reducer function and any other immutable state library.

Difference between Mutative and Immer

Mutative Immer
Custom shallow copy
Strict mode
No data freeze by default
Non-invasive marking
Complete freeze data
Non-global config
async draft function
Fully compatible with JSON Patch spec
new Set methods(Mutative v1.1.0+)

Mutative has fewer bugs such as accidental draft escapes than Immer, view details.

Installation

Yarn

yarn add mutative

NPM

npm install mutative

CDN

  • Unpkg: <script src="https://unpkg.com/mutative"></script>
  • JSDelivr: <script src="https://cdn.jsdelivr.net/npm/mutative"></script>

Usage

import { create } from 'mutative';

const baseState = {
  foo: 'bar',
  list: [{ text: 'coding' }],
};

const state = create(baseState, (draft) => {
  draft.list.push({ text: 'learning' });
});

expect(state).not.toBe(baseState);
expect(state.list).not.toBe(baseState.list);

create(baseState, (draft) => void, options?: Options): newState

The first argument of create() is the base state. Mutative drafts it and passes it to the arguments of the draft function, and performs the draft mutation until the draft function finishes, then Mutative will finalize it and produce the new state.

Use create() for more advanced features by setting options.

APIs

create()

Use create() for draft mutation to get a new state, which also supports currying.

import { create } from 'mutative';

const baseState = {
  foo: 'bar',
  list: [{ text: 'todo' }],
};

const state = create(baseState, (draft) => {
  draft.foo = 'foobar';
  draft.list.push({ text: 'learning' });
});

In this basic example, the changes to the draft are 'mutative' within the draft callback, and create() is finally executed with a new immutable state.

create(state, fn, options)

Then options is optional.

  • strict - boolean, the default is false.

Forbid accessing non-draftable values in strict mode(unless using unsafe()).

When strict mode is enabled, mutable data can only be accessed using unsafe().

It is recommended to enable strict in development mode and disable strict in production mode. This will ensure safe explicit returns and also keep good performance in the production build. If the value that does not mix any current draft or is undefined is returned, then use rawReturn().

If you'd like to enable strict mode by default in a development build and turn it off for production, you can use strict: process.env.NODE_ENV !== 'production'.

  • enablePatches - boolean | { pathAsArray?: boolean; arrayLengthAssignment?: boolean; }, the default is false.

Enable patch, and return the patches/inversePatches.

If you need to set the shape of the generated patch in more detail, then you can set pathAsArray and arrayLengthAssignmentpathAsArray default value is true, if it's true, the path will be an array, otherwise it is a string; arrayLengthAssignment default value is true, if it's true, the array length will be included in the patches, otherwise no include array length(NOTE: If arrayLengthAssignment is false, it is fully compatible with JSON Patch spec, but it may have additional performance loss), view related discussions.

  • enableAutoFreeze - boolean, the default is false.

Enable autoFreeze, and return frozen state, and enable circular reference checking only in development mode.

  • mark - (target) => ('mutable'|'immutable'|function) | (target) => ('mutable'|'immutable'|function)[]

    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. 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.

create() - Currying

  • create draft
const [draft, finalize] = create(baseState);
draft.foobar.bar = 'baz';
const state = finalize();

Support set options such as const [draft, finalize] = create(baseState, { enableAutoFreeze: true });

  • create producer
const produce = create((draft) => {
  draft.foobar.bar = 'baz';
});
const state = produce(baseState);

Also support set options such as const produce = create((draft) => {}, { enableAutoFreeze: true });

apply()

Use apply() for applying patches to get the new state.

import { create, apply } from 'mutative';

const baseState = {
  foo: 'bar',
  list: [{ text: 'todo' }],
};

const [state, patches, inversePatches] = create(
  baseState,
  (draft) => {
    draft.foo = 'foobar';
    draft.list.push({ text: 'learning' });
  },
  {
    enablePatches: true,
  }
);

const nextState = apply(baseState, patches);
expect(nextState).toEqual(state);
const prevState = apply(state, inversePatches);
expect(prevState).toEqual(baseState);

apply(state, patches, options)

The options parameter is optional and supports two types of configurations:

  1. Immutable options (similar to create options but without enablePatches):

  2. strict - boolean, forbid accessing non-draftable values in strict mode

  3. enableAutoFreeze - boolean, enable autoFreeze and return frozen state
  4. mark - mark function to determine if a value is mutable/immutable
const baseState = { foo: { bar: 'test' } };

// This will create a new state.
const result = apply(baseState, [
  {
    op: 'replace',
    path: ['foo', 'bar'],
    value: 'test2',
  },
]);
expect(baseState).not.toEqual({ foo: { bar: 'test2' } });
expect(result).toEqual({ foo: { bar: 'test2' } });
  1. Mutable option(Mutative v1.2.0+):
  2. mutable - boolean, if true the state will be mutated directly instead of creating a new state

Example with muta

Extension points exported contracts — how you extend this code

Item (Interface)
(no doc)
test/original.test.ts
Item (Interface)
(no doc)
test/current.test.ts
BaseState (Interface)
(no doc)
test/performance/set-map.ts
State (Interface)
(no doc)
test/immer/__tests__/redux.ts
Finalities (Interface)
(no doc)
src/interface.ts
Vec2 (Interface)
(no doc)
test/performance/read-draft/mockPhysics.ts
Action (Interface)
(no doc)
test/immer/__tests__/redux.ts
ProxyDraft (Interface)
(no doc)
src/interface.ts

Core symbols most depended-on inside this repo

produce
called by 450
test/immer/src/immer.ts
assert
called by 131
test/immer/src/immer.ts
measure
called by 110
test/__immer_performance_tests__/measure.ts
apply
called by 98
src/apply.ts
setAutoFreeze
called by 69
test/immer/src/immer.ts
isDraft
called by 69
src/utils/draft.ts
current
called by 63
src/current.ts
produceWithPatches
called by 51
test/immer/src/immer.ts

Shape

Function 243
Class 84
Method 39
Interface 17
Enum 1

Languages

TypeScript100%

Modules by API surface

test/immer/__tests__/base.ts35 symbols
test/index.test.ts27 symbols
test/immer/src/immer.ts21 symbols
test/create.test.ts18 symbols
src/set.ts18 symbols
src/utils/draft.ts15 symbols
test/apply.test.ts11 symbols
src/map.ts11 symbols
src/draft.ts11 symbols
test/immer/__tests__/draft.ts9 symbols
test/immer-non-support.test.ts9 symbols
test/immer/__tests__/patch.ts8 symbols

Dependencies from manifests, versioned

@docusaurus/core3.3.2 · 1×
@docusaurus/module-type-aliases3.2.1 · 1×
@docusaurus/preset-classic3.3.2 · 1×
@docusaurus/remark-plugin-npm2yarn3.3.2 · 1×
@docusaurus/theme-live-codeblock3.3.2 · 1×
@docusaurus/theme-mermaid3.3.2 · 1×
@docusaurus/types3.2.1 · 1×
@mdx-js/react3.0.0 · 1×
@rollup/plugin-commonjs28.0.6 · 1×
@rollup/plugin-node-resolve16.0.1 · 1×
@rollup/plugin-replace6.0.2 · 1×
@rollup/plugin-terser0.4.4 · 1×

For agents

$ claude mcp add mutative \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact