Skip to main content

Loro Binding Example

In this example we will be synchronizing two separate root stores via the mobx-keystone-loro package (documentation here). This package ensures a Loro document is kept in sync with a mobx-keystone store and vice-versa. This is useful for example when you when you want to have multiple clients that keep in sync with each other using a peer-to-peer connection, an intermediate server, etc. Another nice feature of CRDTs is that they are conflict-free, so you don't have to worry about conflicts when two clients edit the same data at the same time, even if they were offline while doing such changes. Due to this all updates become optimistic updates and perceived performance is great since it does not require a server confirming the operation.

Note: This example uses BroadcastChannel to share state between the two instances in the same browser.

Code

appInstance.tsx

examples/loroBinding/appInstance.tsx
import { LoroDoc } from "loro-crdt"
import { observable } from "mobx"
import { getSnapshot, registerRootStore } from "mobx-keystone"
import { applyJsonObjectToLoroMap, bindLoroToMobxKeystone } from "mobx-keystone-loro"
import { observer } from "mobx-react"
import { useEffect, useState } from "react"
import { TodoListView } from "../todoList/app"
import { createDefaultTodoList, TodoList } from "../todoList/store"

function getInitialState() {
const doc = new LoroDoc()
const loroRootStore = doc.getMap("rootStore")

const todoListSnapshot = getSnapshot(createDefaultTodoList())
applyJsonObjectToLoroMap(loroRootStore, todoListSnapshot)
doc.commit()

const update = doc.export({ mode: "snapshot" })

return update
}

// we get the initial state from the server, which is a Loro update
const rootStoreLoroUpdate = getInitialState()

function initAppInstance() {
// hydrate into a Loro document
const doc = new LoroDoc()
doc.import(rootStoreLoroUpdate)

// and create a binding into a mobx-keystone object rootStore
const { boundObject: rootStore } = bindLoroToMobxKeystone({
mobxKeystoneType: TodoList,
loroDoc: doc,
loroObject: doc.getMap("rootStore"),
})

// although not strictly required, it is always a good idea to register your root stores
// as such, since this allows the model hook `onAttachedToRootStore` to work and other goodies
registerRootStore(rootStore)

// expose the connection status as an observable
const status = observable({
connected: true,
setConnected(value: boolean) {
this.connected = value
},
})

// Simple sync via BroadcastChannel
const channel = new BroadcastChannel("mobx-keystone-loro-binding-demo")

let lastSentVersion = doc.version()

const unsubLocalUpdates = doc.subscribeLocalUpdates((update) => {
if (status.connected) {
channel.postMessage({ update, fromPeer: doc.peerIdStr })
lastSentVersion = doc.version()
}
})

channel.onmessage = (event) => {
if (!status.connected) return

const { update, fromPeer, isSyncRequest } = event.data
if (fromPeer === doc.peerIdStr) return // ignore own messages if any

try {
doc.import(update)
lastSentVersion = doc.version()

if (isSyncRequest) {
// if it is a sync request, send back only the changes they are missing
channel.postMessage({
update: doc.export({ mode: "update", from: event.data.version }),
fromPeer: doc.peerIdStr,
isSyncRequest: false,
})
}
} catch (e) {
console.error("Import failed", e)
}
}

const toggleConnection = () => {
const newConnected = !status.connected
status.setConnected(newConnected)
if (newConnected) {
// when reconnecting, send our current state to others and ask for theirs
channel.postMessage({
update: doc.export({ mode: "update", from: lastSentVersion }),
version: doc.version(),
fromPeer: doc.peerIdStr,
isSyncRequest: true,
})
// checkpoint the version we just sent
lastSentVersion = doc.version()
}
}

return { rootStore, status, toggleConnection, dispose: () => unsubLocalUpdates() }
}

export const AppInstance = observer(() => {
const [{ rootStore, status, toggleConnection, dispose }] = useState(() => initAppInstance())

useEffect(() => {
return () => {
dispose()
}
}, [dispose])

return (
<>
<TodoListView list={rootStore} />

<br />

<div>{status.connected ? "Online (sync enabled)" : "Offline (sync disabled)"}</div>
<button
type="button"
onClick={() => {
toggleConnection()
}}
style={{ width: "fit-content" }}
>
{status.connected ? "Disconnect" : "Connect"}
</button>
</>
)
})

app.tsx

examples/loroBinding/app.tsx
import React from "react"
import { AppInstance } from "./appInstance"

let iframeResizerChildInited = false

function initIframeResizerChild() {
if (!iframeResizerChildInited) {
iframeResizerChildInited = true
void import("@iframe-resizer/child")
}
}

export const App = () => {
initIframeResizerChild()

return (
<div style={{ display: "flex" }}>
<div style={{ flex: "0 0 50%", padding: "10px", borderRight: "1px solid #ccc" }}>
<AppInstance />
</div>
<div style={{ flex: "0 0 50%", padding: "10px" }}>
<AppInstance />
</div>
</div>
)
}