Storage

pluv.io supports conflict-free replicated data-types (CRDT) storage with yjs and loro. This enables modifying shared data between multiple users with strong eventual consistency.

Specify CRDT on PluvIO

To get started with storage for pluv.io, first specify which CRDT you intend to use on createIO.

import { yjs } from "@pluv/crdt-yjs";
import { loro } from "@pluv/crdt-loro";
import { platformPluv } from "@pluv/platform-pluv";

export const io = createIO(
    platformPluv({
        // ...
        crdt: yjs,
    })
);

export const io = createIO(
    platformPluv({
        // ...
        crdt: loro,
    })
);

Set initialStorage

Then set an initialStorage config on your createClient config. The examples below will use Yjs.

// frontend/io
import { createClient, infer } from "@pluv/client";
import { yjs } from "@pluv/crdt-yjs";
import { createBundle } from "@pluv/react";
import type { ioServer } from "./backend/io";

const types = infer((i) => ({ io: i<typeof ioServer> }));
const client = createClient({
    // Set the initial storage value, and type here
    // Don't worry, we can set a different default value in the room component
    initialStorage: yjs.doc(() => ({
        messages: yjs.array<string>([]),
    })),
    types,
    // ...
});

export const {
    PluvRoomProvider,
    useStorage,
    useTransact,
} = createBundle(client);

Setup PluvRoomProvider

Then, setup PluvRoomProvider with your new initialStorage if it is different than your default from createClient.

Note: This only affects rooms that have not loaded an initial storage via getInitialStorage.

import { yjs } from "@pluv/crdt-yjs";
import type { FC } from "react";
import { PluvRoomProvider } from "./frontend/io";

export const MyPage: FC = () => {
    return (
        <PluvRoomProvider
            room="my-room-id"
            initialStorage={() => ({
                messages: yjs.array(),
            })}
        >
            <MyRoom />
        </PluvRoomProvider>
    );
};

Use CRDT native types

We can then use yjs shared-types or loro container types to leverage shared CRDTs between connected clients using useStorage.

// Yjs example
import { useCallback } from "react";
import type { Array as YArray } from "yjs";
import { useStorage } from "./frontend/io";

// "messages" is a key from the root properties of `initialStorage`.
// sharedType is a shared-type from Yjs
const [messages, sharedType] = useStorage("messages");
//               ^? const sharedType: YArray<string> | null

const addMessage = useCallback((message: string) => {
    sharedType?.push([message]);
}, [sharedType]);

messages?.map((message, i) => <div key={i}>{message}</div>);

Loro usage

When using loro, the loro document has to be committed for value subscriptions to register changes. So to do this, wrap your mutations in the transact function from useTransact.

// Loro example
import { useCallback } from "react";
import { useStorage, useTransact } from "./frontend/io";

// "messages" is a key from the root properties of `initialStorage`.
const [messages] = useStorage("messages");
const transact = useTransact();

const addMessage = useCallback((message: string) => {
    transact((tx) => {
        // tx.messages is a container-type from loro
        tx.messages.insert(tx.messages.length, message);
    });
}, [container]);

messages?.map((message, i) => <div key={i}>{message}</div>);