Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

A library for building Yjs collaborative web applications with Mutative

License

NotificationsYou must be signed in to change notification settings

mutativejs/mutative-yjs

Repository files navigation

Node CInpmlicense

A library for building Yjs collaborative web applications with Mutative.

Mutative is a high-performance immutable data structure library for JavaScript.Y.js is a CRDT library with mutation-based API.mutative-yjs allows manipulating Y.js data types with the API provided by Mutative.

Features

  • 🔄Bidirectional Sync: Seamlessly sync between Yjs CRDT types and plain JavaScript objects
  • 🎯Immutable Updates: Use Mutative's intuitive draft-based API for state updates
  • 📦Type Safe: Full TypeScript support with type inference
  • 🚀Performance: Efficient patch-based updates with structural sharing
  • 🔌Flexible: Customizable patch application for advanced use cases
  • 📡Reactive: Built-in subscription system for state changes
  • Explicit Transactions: Updates to Y.js are batched in transactions, you control the boundary
  • 🪶Lightweight: Simple, small codebase with no magic or vendor lock-in
  • 🎨Non-intrusive: Always opt-in by nature (snapshots are just plain objects)

Why mutative-yjs?

Do:

// any operation supported by mutativebinder.update((state)=>{state.nested[0].key={id:123,p1:'a',p2:['a','b','c'],};});

Instead of:

Y.transact(state.doc,()=>{constval=newY.Map();val.set('id',123);val.set('p1','a');constarr=newY.Array();arr.push(['a','b','c']);val.set('p2',arr);state.get('nested').get(0).set('key',val);});

Installation

npm install mutative-yjs mutative yjs# oryarn add mutative-yjs mutative yjs# orpnpm add mutative-yjs mutative yjs

Quick Start

import*asYfrom'yjs';import{bind}from'mutative-yjs';// Create a Yjs documentconstdoc=newY.Doc();constyMap=doc.getMap('data');// Bind the Yjs data structureconstbinder=bind<{count:number;items:string[]}>(yMap);// Initialize with databinder.update((state)=>{state.count=0;state.items=['apple','banana'];});// Update state using Mutative's draft APIbinder.update((state)=>{state.count++;state.items.push('orange');});// Get current snapshotconsole.log(binder.get());// { count: 1, items: ['apple', 'banana', 'orange'] }// Subscribe to changesconstunsubscribe=binder.subscribe((snapshot)=>{console.log('State updated:',snapshot);});// Changes from Yjs are automatically reflectedyMap.set('count',5);console.log(binder.get().count);// 5// Clean upunsubscribe();binder.unbind();
  1. import { bind } from 'mutative-yjs'.
  2. Create a binder:const binder = bind(doc.getMap("state")).
  3. Add subscription to the snapshot:binder.subscribe(listener).
    1. Mutations in Y.js data types will trigger snapshot subscriptions.
    2. Callingupdate(...) (similar tocreate(...) in Mutative) will update their corresponding Y.js types and also trigger snapshot subscriptions.
  4. Callbinder.get() to get the latest snapshot.
  5. (Optionally) callbinder.unbind() to release the observer.

Y.Map binds to plain object{},Y.Array binds to plain array[], and any level of nestedY.Map/Y.Array binds to nested plain JSON object/array respectively.

Y.XmlElement &Y.Text have no equivalent to JSON data types, so they are not supported by default. If you want to use them, please use the Y.js top-level type (e.g.doc.getText("xxx")) directly, or seeCustomize binding & schema section below.

Demo

Demo Playground

API Reference

bind(source, options?)

Binds a Yjs data type to create a binder instance.

Parameters:

  • source:Y.Map<any> | Y.Array<any> - The Yjs data type to bind
  • options?:Options<S> - Optional configuration

Returns:Binder<S> - A binder instance with methods to interact with the bound data

Example:

constdoc=newY.Doc();constyMap=doc.getMap('myData');constbinder=bind<MyDataType>(yMap);

createBinder(source, initialState, options?)

Creates a binder with initial state in one call. This is a convenience function that combinesbind() and initialization.

Parameters:

  • source:Y.Map<any> | Y.Array<any> - The Yjs data type to bind
  • initialState:S - The initial state to set
  • options?:Options<S> - Optional configuration

Returns:Binder<S> - A binder instance with the initial state applied

Example:

constdoc=newY.Doc();constyMap=doc.getMap('myData');constbinder=createBinder(yMap,{count:0,items:[]});

Binder API

binder.get()

Returns the current snapshot of the data.

constsnapshot=binder.get();

binder.update(fn)

Updates the state using a Mutative draft function. Changes are applied to both the snapshot and the underlying Yjs data structure.

Parameters:

  • fn:(draft: S) => void - A function that receives a draft state to mutate
binder.update((state)=>{state.user.name='John';state.items.push({id:1,title:'New Item'});});

binder.subscribe(fn, options?)

Subscribes to state changes. The callback is invoked when:

  1. update() is called
  2. The underlying Yjs data is modified

Parameters:

  • fn:(snapshot: S) => void - Callback function that receives the new snapshot
  • options?:SubscribeOptions - Optional subscription configuration
    • immediate?: boolean - If true, calls the listener immediately with current snapshot

Returns:UnsubscribeFn - A function to unsubscribe

// Basic subscriptionconstunsubscribe=binder.subscribe((snapshot)=>{console.log('State changed:',snapshot);});// Subscribe with immediate executionbinder.subscribe((snapshot)=>{console.log('Current state:',snapshot);},{immediate:true});// Later...unsubscribe();

binder.unbind()

Releases the binder and removes the Yjs observer. Call this when you're done with the binder.

binder.unbind();

Advanced Usage

Structural Sharing

Like Mutative,mutative-yjs provides efficient structural sharing. Unchanged parts of the state maintain the same reference, which is especially beneficial for React re-renders:

constsnapshot1=binder.get();binder.update((state)=>{state.todos[0].done=true;});constsnapshot2=binder.get();// changed properties have new referencessnapshot1.todos!==snapshot2.todos;snapshot1.todos[0]!==snapshot2.todos[0];// unchanged properties keep the same referencesnapshot1.todos[1]===snapshot2.todos[1];snapshot1.todos[2]===snapshot2.todos[2];

Custom Patch Application

You can customize how Mutative patches are applied to Yjs data structures:

constbinder=bind<MyDataType>(yMap,{applyPatch:(target,patch,defaultApplyPatch)=>{// Inspect or modify the patch before applyingconsole.log('Applying patch:',patch);// You can conditionally apply patches based on the pathif(patch.path[0]==='protected'){// Skip protected fieldsreturn;}// Delegate to default behaviordefaultApplyPatch(target,patch);// Or implement custom logic// ...},});

Patches Options

Configure how Mutative generates patches:

constbinder=bind<MyDataType>(yMap,{patchesOptions:{pathAsArray:true,arrayLengthAssignment:true,},});

Refer toMutative patches documentation for more details about patches options.

Working with Y.Array

The library works with bothY.Map andY.Array:

constdoc=newY.Doc();constyArray=doc.getArray('items');typeItem={id:string;name:string};constbinder=bind<Item[]>(yArray);binder.update((items)=>{items.push({id:'1',name:'First Item'});items.push({id:'2',name:'Second Item'});});// Array operations work as expectedbinder.update((items)=>{items[0].name='Updated Name';items.splice(1,1);// Remove second item});

Collaborative Editing Example

import*asYfrom'yjs';import{WebsocketProvider}from'y-websocket';import{bind}from'mutative-yjs';// Create document and connect to serverconstdoc=newY.Doc();constprovider=newWebsocketProvider('ws://localhost:1234','room-name',doc);constyMap=doc.getMap('shared-data');constbinder=bind<AppState>(yMap);// Subscribe to remote changesbinder.subscribe((snapshot)=>{// Update UI with new staterenderApp(snapshot);});// Make local changesfunctionhandleUserAction(){binder.update((state)=>{state.todos.push({id:generateId(),text:'New todo',completed:false,});});}

Integration with React

UseuseSyncExternalStoreWithSelector for optimal React integration with selective subscriptions:

import{bind}from'mutative-yjs';import{useSyncExternalStoreWithSelector}from'use-sync-external-store/with-selector';import*asYfrom'yjs';// define state shapeinterfaceState{todos:Array<{id:string;text:string;done:boolean}>;user:{name:string;email:string};}constdoc=newY.Doc();// define storeconstbinder=bind<State>(doc.getMap('data'));// define a helper hookfunctionuseMutativeYjs<Selection>(selector:(state:State)=>Selection){constselection=useSyncExternalStoreWithSelector(binder.subscribe,binder.get,binder.get,selector);return[selection,binder.update]asconst;}// optionally set initial databinder.update((state)=>{state.todos=[];state.user={name:'Guest',email:''};});// use in componentfunctionTodoList(){const[todos,update]=useMutativeYjs((s)=>s.todos);constaddTodo=(text:string)=>{update((state)=>{state.todos.push({id:Math.random().toString(),        text,done:false,});});};consttoggleTodo=(id:string)=>{update((state)=>{consttodo=state.todos.find((t)=>t.id===id);if(todo)todo.done=!todo.done;});};// will only rerender when 'todos' array changesreturn(<div>{todos.map((todo)=>(<divkey={todo.id}onClick={()=>toggleTodo(todo.id)}>{todo.text}{todo.done ?'✓' :'○'}</div>))}</div>);}// when donebinder.unbind();

Integration with Other Frameworks

Contributions welcome! Please submit sample code via PR for Vue, Svelte, Angular, or other frameworks.

Utility Functions

applyJsonArray(dest, source)

Applies a plain JavaScript array to a Y.Array.

import{applyJsonArray}from'mutative-yjs';import*asYfrom'yjs';constyArray=newY.Array();applyJsonArray(yArray,[1,2,3,{nested:'object'}]);

applyJsonObject(dest, source)

Applies a plain JavaScript object to a Y.Map.

import{applyJsonObject}from'mutative-yjs';import*asYfrom'yjs';constyMap=newY.Map();applyJsonObject(yMap,{key1:'value1',key2:{nested:'value'},});

Type Definitions

typeJSONPrimitive=string|number|boolean|null;typeJSONValue=JSONPrimitive|JSONObject|JSONArray;typeJSONObject={[member:string]:JSONValue};interfaceJSONArrayextendsArray<JSONValue>{}typeSnapshot=JSONObject|JSONArray;typeUpdateFn<SextendsSnapshot>=(draft:S)=>void;typeListenerFn<SextendsSnapshot>=(snapshot:S)=>void;typeUnsubscribeFn=()=>void;interfaceBinder<SextendsSnapshot>{unbind:()=>void;get:()=>S;update:(fn:UpdateFn<S>)=>void;subscribe:(fn:ListenerFn<S>)=>UnsubscribeFn;}interfaceOptions<SextendsSnapshot>{applyPatch?:(target:Y.Map<any>|Y.Array<any>,patch:Patch,applyPatch:(target:Y.Map<any>|Y.Array<any>,patch:Patch)=>void)=>void;patchesOptions?:|true|{pathAsArray?:boolean;arrayLengthAssignment?:boolean;};}

How It Works

mutative-yjs creates a bridge between Yjs's CRDT data structures and Mutative's immutable update patterns:

  1. Initialization: When you bind a Yjs data type, it creates an initial snapshot
  2. Updates: When you callupdate(), Mutative generates patches describing the changes
  3. Patch Application: Patches are applied to the Yjs data structure, triggering sync
  4. Event Handling: When Yjs data changes (locally or remotely), events are converted back to snapshot updates
  5. Structural Sharing: Only modified parts of the snapshot are recreated, maintaining referential equality for unchanged data

Performance Tips

  • Batch Updates: Multiple changes in a singleupdate() call are more efficient than multiple separate calls
  • Structural Sharing: Unchanged parts of the state maintain referential equality, making React re-renders efficient
  • Transactions: Updates are wrapped in Yjs transactions automatically for optimal performance
  • Unsubscribe: Always callunbind() when done to prevent memory leaks

Collaboration Semantics

mutative-yjs implements smart collaboration semantics to preserve changes from multiple collaborators:

Array Element Replacement

When replacing array elements with objects, the library performsincremental updates instead of delete+insert:

// If both old and new values are objectsbinder.update((state)=>{state.items[0]={ ...state.items[0],name:'Updated'};});// → Updates properties in-place, preserving other collaborators' changes

This prevents the "lost update" problem discussed inimmer-yjs#1.

Circular Update Protection

The library uses transaction origins to prevent circular updates:

constbinder=bind(yMap);binder.subscribe((snapshot)=>{// Safe: won't cause infinite loopif(snapshot.count<10){binder.update((state)=>{state.count++;});}});

Circular Reference Detection

The library detects and rejects circular object references:

constcircular:any={a:1};circular.self=circular;binder.update((state)=>{state.data=circular;// ❌ Throws: "Circular reference detected"});

Examples

Check out thetest file for comprehensive examples including:

  • Basic binding and updates
  • Array operations (splice, push, etc.)
  • Nested object updates
  • Subscription handling
  • Custom patch application
  • Collaborative scenarios

Compatibility

  • Mutative: >= 1.0.0
  • Yjs: >= 13.0.0
  • TypeScript: >= 4.5
  • Node.js: >= 14

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Related Projects

  • Mutative - Efficient immutable updates with a mutable API
  • Yjs - A CRDT framework for building collaborative applications

Acknowledgments

This library bridges two powerful tools:

  • Yjs for CRDT-based conflict-free collaborative editing
  • Mutative for ergonomic and performant immutable state updates

Credits

immer-yjs is inspired byhttps://github.com/sep2/immer-yjs.

License

mutative-yjs isMIT licensed.

About

A library for building Yjs collaborative web applications with Mutative

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp