SyncroState brings Svelte 5 reactivity to the multiplayer level. Built on top of Yjs, it provides a reactive and type-safe way to build multiplayer experiences.
Inspired by Syncedstore, SyncroState modernizes collaborative state management by leveraging new Svelte 5's reactivity system. It provides a natural way to work with synchronized state that feels just like a regular Svelte $state.
Note to Svelte Hackathon Organizers
This project is my submission for the Svelte Hackathon. Thank you for organizing this amazing event!
For testing: You can run the test suite in the
/package
folder using thetest:unit
command or run the sveltekit app inside the/package
folder using thedev
command or run the demo in the/demo
folder using thedev
command.â ī¸ The demo uses a public Liveblocks API key which may become rate-limited. I recommend using your own API key for thorough testing.
bind:value
like you would with any Svelte state# Using pnpm
pnpm add syncrostate
# Using bun
bun add syncrostate
# Using npm
npm install syncrostate
SyncroState uses a schema-first approach to define your collaborative state. Here's a simple example:
The schema defines both the structure and the types of your state. Every field is automatically:
Once created, you can use the state like a regular Svelte state: mutate it, bind it, use mutative methods, etc.
<script>
import { syncroState, y } from 'syncrostate';
import { LiveblocksYjsProvider } from '@liveblocks/yjs';
import { createClient } from '@liveblocks/client';
const document = syncroState({
// Optional but required for remote sync: Connect and sync to a yjs provider
// If omitted, state will be local-only and in memory.
sync: ({ doc, synced }) => {
const client = createClient({
publicApiKey: 'your-api-key'
});
const { room } = client.enterRoom('room-id');
const provider = new LiveblocksYjsProvider(room, doc);
provider.on('synced', () => {
synced();
});
},
// Define your state schema. It must be an object
schema: {
// Primitive values
name: y.string(),
age: y.number(),
isOnline: y.boolean(),
lastSeen: y.date(),
theme: y.enum(['light', 'dark']),
// Nested objects
preferences: y.object({
theme: y.enum(['light', 'dark']),
notifications: y.boolean()
}),
// Arrays of any type
todos: y.array(y.object({
title: y.string(),
completed: y.boolean()
})),
// Sets of any primitive type
colors: y.set(y.string())
}
});
</script>
<!-- Use it like regular Svelte state -->
<input bind:value={document.name} />
<button onclick={() => document.todos.push({ title: 'New todo', completed: false })}>
Add Todo
</button>
SyncroState combines the power of Svelte's reactivity system with Yjs's CRDT capabilities to create a seamless real-time collaborative state management solution. Here's how it works under the hood:
Proxy-based State Tree: When you create a state using syncroState()
, it builds a tree of proxy objects that mirror your schema structure. Each property (primitive or nested) is wrapped in a specialized proxy that leverages Svelte's reactivity through $state
or specialized proxy like SvelteDate
or SvelteSet
and soon SvelteMap
.
Mutation Trapping: These proxies intercept all state mutations (assignments, mutative operations, object modifications, reassignments). This allows SyncroState to:
Yjs Integration: The state is backed by Yjs types in the following way:
When you modify the state:
Remote Updates: When changes come in from other clients:
This architecture ensures that:
To add a persistence provider like y-indexeddb and use a remote provider like Liveblocks or y-websocket you will do something like this:
import { IndexeddbPersistence } from "y-indexeddb";
import { createClient } from "@liveblocks/client";
import { LiveblocksYjsProvider } from "@liveblocks/yjs";
const document = syncroState({
sync: ({ doc, synced }) => {
const docName = "your-doc-name";
const localProvider = new IndexeddbPersistence(docName, doc);
const remoteClient = createClient({
publicApiKey: "your-api-key"
});
const { room } = remoteClient.enterRoom(docName);
localProvider.on("synced", () => {
const remoteProvider = new LiveblocksYjsProvider(room, doc);
remoteProvider.on("synced", () => {
synced();
});
});
}
// ... your schema
});
When you are using a remote provider, you might want to wait for the state to be synced before doing something.
The syncrostate object has a getState()
methods that return the state of the syncronisation from which you can get the synced
property to check if the state is synced.
{#if document.getState?.().synced}
<div>My name is {document.name}</div>
{/if}
If you want to edit multiple object properties at once it's preferable to reassign the entire object. This way, syncrostate can apply the changes inside a single transaction and avoid partial updates. Only the properties that are being changed will trigger reactivity and remote updates.
// Instead of this
state.user.name = "John";
state.user.age = 30;
// Do this
state.user = {
...state.user,
name: "John",
age: 30
};
Every syncrostate object or array has three additional methods: getState
, getYTypes
and getYTypes
.
getState
returns the state type State
of the syncronisation.getYTypes
returns the underlying YObject or YArray.getYTypes
returns the YJS types children of the YObject or YArray.type State {
synced: boolean;
awareness: Awareness;
doc: Y.Doc;
undoManager: Y.UndoManager;
transaction: (fn: () => void) => void;
transactionKey: any;
undo: () => void;
redo: () => void;
}
SyncroState uses Yjs's undo/redo system to provide undo/redo functionality. These methods are available through the getState
method.
SyncroState is licensed under the MIT License. See the LICENSE file for details.