npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

yjs-redux

v0.15.0

Published

[![npm shield](https://img.shields.io/npm/v/yjs-redux)](https://www.npmjs.com/package/yjs-redux)

Downloads

30

Readme

Redux Binding for Yjs

npm shield

Connects a redux store to yjs (https://docs.yjs.dev/) with support for all kind of JavaScript objects, including custom classes. Allows you to use the synchronization features of Yjs with the data management capabilities of redux. It works with any redux store, whether you use redux toolkit or not, and even supports initial values. Only transmits the changed values and keeps the number of changes in redux low.

Install

npm i yjs-redux

General Usage

/* eslint-disable @typescript-eslint/no-explicit-any */
import * as Y from 'yjs';
import { WebrtcProvider } from 'y-webrtc';
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { createBinder, SyncOptions } from 'yjs-redux';

const ydoc = new Y.Doc();

new WebrtcProvider('demo-room', ydoc);

const options: SyncOptions = {
    typeResolvers: {},
    valueResolvers: {},
    syncAlways: true // Also sync arrays and objects.
};

// Call this for every slice you want to synchronize.
const binder = createYjsReduxBinder(options);

binder.connectSlice(doc);

export const store = configureStore({
    reducer: binder.enhanceReducer(combineReducers({
        ... YOUR REDUCERS
    })),
    middleware: [binder.middleware()]
});

export type RootState = ReturnType<typeof store.getState>;

Features

Reduces changes

This implementation keeps the changes low and only synchronizes the minimum amount of changes as they have happened in the redx store. Changes are only applied when the object has been changed. This guarantees best performance for react equality checks and for writing selectors.

Custom Type system

By default we distinguish between custom types, that have the following properties:

  • __typeName: Unique type name within your redux store.
  • __instanceId: Globally unique ID for an object and updated versions of the same object.

Only when the type name is present values are either mapped to Y.Array or Y.Map instances. This guarantees that values that should be treated as atomar values are not updated partially. Consider an object position with an x and y property. If we would map this property to a map and two users would move an object simultanously, we could end up with an position that is a combination of both updates.

If you do not want to map arrays and objects to yjs types and only custom types, you can set the syncAlways property to false.

Insert and Removal detection

Inserts to an array are automatically detected. This keeps the number of changes low and improves the network performance and the reduces the number of updates in your components or selectors.

Support for custom value types

If you have implemented custom value types like vectors, colors and so on as classes, you can implement the ValueResolver interface to support the serialization of these values.

import { ValueResolver } from 'yjs-redux';

class Color {
    public readonly __typeName = Color.TYPE_NAME;

    public static readonly TYPE_NAME = 'Color';

    public constructor(
        public readonly value: string
    ) {
    }
}

class ColorValueResolver implements ValueResolver<Color> {
    public static readonly INSTANCE = new ColorValueResolver();

    private constructor() {
    }

    public fromYjs(source: SourceObject): Color {
        return new Color(source['value'] as string);
    }

    public fromValue(source: Color): Readonly<{ [key: string]: unknown; }> {
        return { value: source.value };
    }
}

const options: SyncOptions = {
    valueResolvers: {
        [Color.TYPE_NAME]: ColorValueResolver.INSTANCE
    }
};

Support for custom classes

Classes that are not handled as value types and can be updated partially can also be mapped to yjs if you implement one of the following interfaces:

  • ArrayTypeResolver: If your type should be mapped to Y.Array instance.
  • ObjectTypeResolver: If you type should be mapped to Y.Map instance.

For example we can implement custom immutable class like this:

import { ObjectDiff, ObjectTypeResolver, SourceObject } from 'yjs-redux';

class ImmutableArray<T> {
    public readonly __typeName = ImmutableArray.TYPE_NAME;

    public static readonly TYPE_NAME = 'ImmutableArray';

    constructor(
        public readonly __instanceId: string,
        public readonly __generation: number,
        public readonly items: ReadonlyArray<T>,
    ) {
    }
}

class ImmutableArrayResolver<T> implements ArrayTypeResolver<ImmutableArray<T>> {
    public readonly sourceType = 'Array';

    public static readonly INSTANCE = new ImmutableArrayResolver();

    private constructor() {
    }

    public create(source: SourceArray): ImmutableArray<T> {
        return new ImmutableArray<T>(idGenerator(), 0, source as T[],);
    }

    public syncToYjs(value: ImmutableArray<T>): SourceArray {
        return value.items;
    }

    public syncToObject(existing: ImmutableArray<T>, diffs: ArrayDiff[]): ImmutableArray<T> {
        const newItems = [...existing.items];

        for (const diff of diffs) {
            if (diff.type === 'Set') {
                newItems[diff.index] = diff.value as T;
            } else if (diff.type === 'Insert') {
                newItems.splice(diff.index, 0, diff.value as T);
            } else {
                newItems.splice(diff.index, 1);
            }
        }

        return new ImmutableArray<T>(existing.__instanceId, existing.__generation + 1, newItems);
    }
}

Contributing

Contributions are very welcome. Just checkout the code and run:

npm i
npm run test
// OR
npm run dev

How it works

Sync from Redux to Yjs

To synchronize from redux to yjs we compare the current and the previous state and compare the value. Because of the immutable nature of redux you can usually skip large parts of the state tree, that have not been changed. Then we basically do one of the following operations:

  • Set a map value.
  • Remove an map key.
  • Push an item to an array.
  • Remove an item from an array.
  • Create new yjs structures when new state is created.

Whenever we create a yjs we also define where this object. We create a bidirectional mappign with the following properties.

  • yjs[__source] = state to define the synchronization source from a yjs type. We can use that to resolve the state object from a yjs instance.
  • state[__target] = yjs to define the synchronization yjs type for a state object. We can use that to resolve the yjs type from a state object.

Sync from Yjs

We use the events from synchronize from yjs to states. Because of the immutable nature of redux, we always have to update all parents if we update one of the ancestores. Therefore we use the following flow:

  • From and event target (the yjs type) we resolve the source state object.
  • We mark the state object as invalid and attach the event to the state object.
  • We loop to the root object using the parent property of the yjs type to also navigate to the state root and also mark all items as invalid, that need to be changed.

Lets assume we have the following state and that we receive and update for the paragraph. This would create the following metadata.

root [__invalid: true]
   pages [__invalid: true]
      page [__invalid: true]
         paragraphs [__invalid: true]
            paragraph [__invalid: true, event]
      page
         paragraphs
            paragraph
   images
      image

Now we have to loop from root to the children and update all state objects using a depth first search.