Giter Site home page Giter Site logo

joebobmiles / zustand-middleware-yjs Goto Github PK

View Code? Open in Web Editor NEW
96.0 3.0 6.0 795 KB

Zustand middleware that enables sharing of state between clients via Yjs.

License: MIT License

TypeScript 99.90% Shell 0.10%
peer-to-peer p2p yjs middleware zustand distributed state-mangement local-first offline-first crdt

zustand-middleware-yjs's Introduction

Yjs Middleware for Zustand

One of the difficult things about using Yjs is that it's not easily integrated with modern state management libraries in React. This middleware for Zustand solves that problem by allowing a Zustand store to be turned into a CRDT, with the store's state replicated to all peers.

This differs from the other Yjs and Zustand solution, zustand-yjs by allowing any Zustand store be turned into a CRDT. This contrasts with zustand-yjs's solution, which uses a Zustand store to collect shared types and access them through special hooks.

Because this solution is simply a middleware, it can also work anywhere Zustand can be used. The vanilla Zustand create() function handles middleware exactly the same as the React version. And not only that, but it can be composed with other middleware, such as Immer or Redux!

Example

import React from "react";
import { render } from "react-dom";

import * as Y from "yjs";
import create from "zustand";
import yjs from "zustand-middleware-yjs";

// Create a Y Doc to place our store in.
const ydoc = new Y.Doc();

// Create the Zustand store.
const useSharedStore = create(
  // Wrap the store creator with the Yjs middleware.
  yjs(
    // Provide the Y Doc and the name of the shared type that will be used
    // to hold the store.
    ydoc, "shared",
          
    // Create the store as you would normally.
    (set) =>
      ({
        count: 0,
        increment: () =>
          set(
            (state) =>
            ({
              count: state.count + 1,
            })
          ),
      })
  )
);

// Use the shared store like you normally would any other Zustand store.
const App = () =>
{
  const { count, increment } = useSharedStore((state) =>
    ({
      count: state.count,
      increment: state.increment
    }));

  return (
    <>
      <p>count: {count}</p>
      <button onClick={() => increment()}>+</button>
    </>
  );
};

render(
  <App />,
  document.getElementById("app-root")
);

Caveats

  1. The Yjs awareness protocol is not supported. At the moment, it is unclear if the library is able to support Yjs protocols. This means that, for now, support for the awareness protocol is not planned.
    • This does not mean you cannot use awareness in your projects - see the sister project y-react for an example of using awareness without the middleware.

License

This library is licensed under the MIT license:

Copyright © 2021 Joseph R Miles

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

zustand-middleware-yjs's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

zustand-middleware-yjs's Issues

'Array' is not exported from 'yjs' (imported as 'Y').

Hi there,

I am getting the following error when executing the example code from the Readme.md. My setup is quite simple:

In a node:14 docker container

$ npx create-react-app testapp
$ cd testapp
$ yarn add zustand zustand-middleware-yjs yjs
$ yarn start

( I just copy&pasted the example code to the src/index.js before yarn start)

Failed to compile.

./node_modules/zustand-middleware-yjs/dist/yjs.mjs
Attempted import error: 'Array' is not exported from 'yjs' (imported as 'Y').

Can you help me with a hint whats going wrong?

A simple example?

Hello,
I'm sorry to bother you for nothing. In principle, your middleware seems perfect, but after several attempts, it's impossible to get it to work with WebRTC (piesocket in my example).

I can't see what's wrong, even if I turn the code upside down, so I made a basic code to create my "store", do you see anything shocking?

Namely that the setter works correctly without the middleware, I have other middleware that also works perfectly. I achieve a functional (basic) result by playing with the "storage" of "persist" to send messages directly, and intercept messages, so PieSocket is not the problem either (and the connection is indicated as being functional).

In short, I don't see :

import { create } from "zustand";
import * as Y from "yjs";
import { WebrtcProvider } from "y-webrtc";
import yjs from "zustand-middleware-yjs";

const ydoc = new Y.Doc();
const api_key = 'XXXX';
const docName = "doc_name";
const roomId = 1;
const wsUrl = `ws://free.blr2.piesocket.com/v3/${roomId}?api_key=${api_key}`;

new WebrtcProvider(docName, ydoc, {
    signaling: [ wsUrl ],
});

const createWithMiddleware = (storeConfig) => {
    return create(yjs(ydoc, "shared", storeConfig));
}

export const useAppConfigStore = createWithMiddleware(
    (set) => ({
        appConfig: {
            test: 'is a test',
        },
        setAppConfig: (newConfig) => {
            set(state => ({
                appConfig: {
                    ...state.appConfig,
                    ...newConfig
                }
            }))
        },
    })
);

Thank you very much in advance

zustand-middleware-yjs not working with other zustand middlewares

Hi,
I have been trying to use multiple middleware in a single store (zustand-middleware-yjs being one of them). My implementation looks something like in the picture. Even though the data/state in the syncStore is changed, the zustand data/state living on my other device connecting to the same room("somethingRoom") is not updating.

Screen Shot 2022-05-05 at 9 13 00 AM

Screen Shot 2022-05-05 at 9 16 44 AM

State resets when new peer joins

Noticed this issue when working on the counter project. When a new peer joins the shared session, the shared state is overwritten by the new user's local state. This was something I figured out in YJSON, but forgot about in the mess that became setting up the Rollup bundling and getting the example counter project working.

[BUG] types found but incorrect when using moduleResolution: "Bundler"

Describe the bug

typings for zustand-middleware-yjs cannot be imported, see the error message


⚠ Error (TS7016)  | 

Could not find a declaration file for module 
"zustand-middleware-yjs"
 . /project/node_modules/.pnpm/[email protected]_@[email protected][email protected]/node_modules/zustand-middleware-yjs/dist/yjs.mjs implicitly has an 
any
 type.

  | There are types at /project/node_modules/zustand-middleware-yjs/dist/index.d.ts', but this result could not be resolved when respecting package.json "exports". The zustand-middleware-yjs' library may need to update its package.json or typings. -- | --


To Reproduce
Steps to reproduce the behavior:

  • configure your tsconfig with compilerOptions.moduleResolution: "Bundler"
  • import zustand-middleware-yjs

Expected behavior
typings are expected to be imported correctly like when I set moduleResolution to node

Versions (please complete the following information):
yjs v1.3.1

solution seems to be to change how the exports are referenced in package.json, see microsoft/TypeScript#52363 (comment)

Jest chokes on ESM format

Just began integration into Repeated Pleasant Games and found that apparently Jest does not like ESM module imports that go untransformed. We either need to figure out how to distribute CommonJS and ESM or use CommonJS without breaking things like Vite.

Yjs Middleware Does Not Work with Immer Middleware

I am trying to use this to sync a react flow store but when yjs syncs react doesn't refresh the page from zustand contents.

I tried to add immer but still not working

here is my store:

import { WebsocketProvider } from 'y-websocket'
import * as Y from "yjs";
import { immer } from 'zustand/middleware/immer';
import { useStore } from 'zustand';
import yjs from "zustand-middleware-yjs";
import { createStore } from 'zustand/vanilla';

// Create a Y Doc to place our store in.
const ydoc = new Y.Doc();
const docName = "xxx";

if (typeof window != "undefined") {
  const provider = new WebsocketProvider('wss://y-websocket-server.xxx.co:5900',docName, ydoc)

  provider.on('synced', () => {
      console.log('content from the database is loaded')
  })
}

const store = createStore(
  immer(
    yjs(
      ydoc, 
      docName, 
      (set) => 
      ({
        nodes: [],
        edges: [],
        addNode: (node: any) => {
          set((state: any) => {
            state.nodes.push(node)
          })
        },
        removeNode: (id: any) => {
          set((state: any) => {
            state.nodes = state.nodes.filter((node: any) => node.id !== id)
          })
        },
        addEdge: (edge: any) => {
          set((state: any) => {
            state.edges.push(edge)
          })
        },
        removeEdge: (id: any) => {
          set((state: any) => {
            state.edges = state.edges.filter((edge: any) => edge.id !== id)
          })
        }
      })
    )
  )
);

export const usePanelStore = (selector: any) => useStore(store, selector)

TypeError is thrown when inserting consecutive fields into arrays

Noticed in the RPG code that we're getting a TypeError from Yjs:

    TypeError: Cannot read property 'forEach' of null

      26 |       addActor:
      27 |         (actor: Actor) =>
    > 28 |           set((state) => ({ actors: [ ...state.actors, actor ] })),
         |           ^
      29 |       removeActor:
      30 |         (actorId: string) =>
      31 |           set(

      at YMap._integrate (../node_modules/yjs/src/types/YMap.js:77:60)
      at ContentType.integrate (../node_modules/yjs/src/structs/ContentType.js:100:15)
      at Item.integrate (../node_modules/yjs/src/structs/Item.js:506:20)
      at ../node_modules/yjs/src/types/AbstractType.js:672:20
          at Array.forEach (<anonymous>)
      at typeListInsertGenericsAfter (../node_modules/yjs/src/types/AbstractType.js:648:11)
      at typeListInsertGenerics (../node_modules/yjs/src/types/AbstractType.js:701:12)
      at ../node_modules/yjs/src/types/YArray.js:134:9
      at transact (../node_modules/yjs/src/utils/Transaction.js:391:5)
      at YArray.insert (../node_modules/yjs/src/types/YArray.js:133:7)
      at ../node_modules/zustand-middleware-yjs/dist/yjs.cjs:253:40
          at Array.forEach (<anonymous>)
      at patchSharedType (../node_modules/zustand-middleware-yjs/dist/yjs.cjs:228:13)
      at ../node_modules/zustand-middleware-yjs/dist/yjs.cjs:270:21
          at Array.forEach (<anonymous>)
      at patchSharedType (../node_modules/zustand-middleware-yjs/dist/yjs.cjs:228:13)
      at ../node_modules/zustand-middleware-yjs/dist/yjs.cjs:351:13
      at Object.addActor (store/shared.ts:28:11)
      at components/control-panel/ControlPanel.spec.tsx:104:33

I've replicated the bug with the following test:

type Store =
{
  users: { name: string, status: "online" | "offline" }[],
  addUser: (name: string, status: "online" | "offline") => void,
};

const doc = new Y.Doc();

const api =
  create<Store>(yjs(
    doc,
    "hello",
    (set) =>
      ({
        "users": [],
        "addUser": (name, status) =>
          set((state) =>
            ({
              "users": [
                ...state.users,
                {
                  "name": name,
                  "status": status,
                }
              ],
            })),
      })
  ));

expect(api.getState().users).toEqual([]);

expect(() =>
{
  api.getState().addUser("alice", "offline");
  api.getState().addUser("bob", "offline");
}).not.toThrow();

I'm unsure if this is a bug in Yjs or a bug in the middleware.

Experiment with smaller Zustand state updates

The middleware asks Zustand to replace the state with a new state object. This feels inefficient, but I need to test to see which is faster: full replacement or using partial updates.

Hello from valtio-yjs

Hi, just wanted to say hello.
I'm trying a project valtio-yjs, but it's still very far from MVP. There might be something I could learn from your project.

I'm also interested in @YousefED 's work. (My hope was/is I could just use it without me creating a new one.)
YousefED/SyncedStore#5

Upgrade the zustand version from "^3.5.5" to "4.0.0"

Hi, I am just wondering if it is possible to upgrade the zustand version from "^3.5.5" to "4.0.0"? The project my team is working on requires zustand 4.0.0, and it would be great if we can have this library use zustand 4.0.0 too!

Just to provide more context to the error I am getting: I am trying to use multiple zustand middlewares, zustand-middleware-yjs being one of them, in the code state below; however, I am getting an error shown in the picture. I am suspecting that the error I am getting has to do with the difference in zustand versions between the middlewares, but I am not too sure. All the other middlewares are using zustand 4.0.0

const useStore = create((yjs(doc,"shared",subscribeWithSelector(computed(immer(store),computedSlice)))));
Screen Shot 2022-04-27 at 10 49 52 AM

Provide some way to indiciate "loading" state

It would be useful to have some way to indicate that state is being loaded from peers. This would allow users to show loading indicators or other "please wait, something's inbound" notifications instead of new state just popping in.

In particular, we want this for when new users connect to a shared store for the first time.

[BUG] Uncaught TypeError: sharedType.toJSON is not a function

Describe the bug

I'm try to intergrate into our product, but I found a problem:

Uncaught TypeError: sharedType.toJSON is not a function
    at patchSharedType (yjs.mjs:293:1)
    at yjs.mjs:341:1
    at Array.forEach (<anonymous>)
    at patchSharedType (yjs.mjs:294:1)
    at yjs.mjs:344:1
    at Array.forEach (<anonymous>)
    at patchSharedType (yjs.mjs:294:1)
    at yjs.mjs:341:1
    at Array.forEach (<anonymous>)
    at patchSharedType (yjs.mjs:294:1)
    at yjs.mjs:341:1
    at Array.forEach (<anonymous>)
    at patchSharedType (yjs.mjs:294:1)
    at yjs.mjs:464:1
    at transact (yjs.mjs:3292:1)
    at Doc.transact (yjs.mjs:505:1)
    at yjs.mjs:463:1
image

here is my data snapshot:

image

Screenshots

Versions (please complete the following information):

  • Browser [e.g. chrome, safari] Chrome
  • Version [e.g. 22] 1.3.0

Additional context
Add any other context about the problem here.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.