{"id":24564851,"url":"https://github.com/beynar/syncrostate","last_synced_at":"2025-04-10T02:29:22.656Z","repository":{"id":271551621,"uuid":"878823649","full_name":"beynar/syncrostate","owner":"beynar","description":null,"archived":false,"fork":false,"pushed_at":"2025-02-21T10:33:19.000Z","size":1200,"stargazers_count":83,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-02T21:12:51.772Z","etag":null,"topics":["crdt","svelte","sveltehack","sveltekit","yjs"],"latest_commit_sha":null,"homepage":"https://syncrostate.pages.dev","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/beynar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-10-26T07:51:13.000Z","updated_at":"2025-04-02T06:47:51.000Z","dependencies_parsed_at":"2025-02-21T11:29:54.951Z","dependency_job_id":null,"html_url":"https://github.com/beynar/syncrostate","commit_stats":null,"previous_names":["beynar/syncrostate"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beynar%2Fsyncrostate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beynar%2Fsyncrostate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beynar%2Fsyncrostate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beynar%2Fsyncrostate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/beynar","download_url":"https://codeload.github.com/beynar/syncrostate/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248144018,"owners_count":21054865,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crdt","svelte","sveltehack","sveltekit","yjs"],"created_at":"2025-01-23T11:29:38.926Z","updated_at":"2025-04-10T02:29:22.631Z","avatar_url":"https://github.com/beynar.png","language":"HTML","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n[![npm version](https://badge.fury.io/js/syncrostate.svg)](https://badge.fury.io/js/syncrostate)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)\n[![Svelte v5](https://img.shields.io/badge/Svelte-v5-FF3E00.svg)](https://svelte.dev)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/)\n[![Bundle size](https://deno.bundlejs.com/badge?q=syncrostate@latest\u0026config=%7B%22esbuild%22:%7B%22external%22:%5B%22svelte%22,%22clsx%22%5D%7D%7D)](https://deno.bundlejs.com/badge?q=syncrostate@latest\u0026config=%7B%22esbuild%22:%7B%22external%22:%5B%22svelte%22,%22clsx%22%5D%7D%7D)\n\n\u003c/div\u003e\n\n# SyncroState\n\nSyncroState 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.\n\nInspired by [Syncedstore](https://github.com/yousefed/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.\n\n\u003e ⚠️ The demo uses a public Liveblocks API key which may become rate-limited. I recommend using your own API key for thorough testing.\n\n## Features\n\n- 🚀 **Powered by Yjs** - Industry-leading CRDT for conflict-free real-time collaboration\n- 🔒 **Type-Safe** - Full TypeScript support with rich type inference and schema validation\n- 💫 **Svelte DX** - Works like regular Svelte state with fine-grained reactivity and simple mutations\n- 🎯 **Rich Data Types** - Support for primitives, arrays, objects, dates, enums, and sets.\n- 🔌 **Provider Agnostic** - Works with Liveblocks, PartyKit, or any Yjs provider\n- 📚 **Local Persistence ready** - Support for y-indexeddb for offline use\n- ↩️ **Undo/Redo** - Built-in support for state history\n- 🎮 **Bindable** - Use `bind:value` like you would with any Svelte state\n- 🎨 **Optional \u0026 Nullable** - Flexible schema definition with optional and nullable fields\n\n## Installation\n\n```bash\n# Using pnpm\npnpm add syncrostate\n\n# Using bun\nbun add syncrostate\n\n# Using npm\nnpm install syncrostate\n```\n\n## Quick Start\n\nSyncroState uses a schema-first approach to define your collaborative state. Here's a simple example:\n\nThe schema defines both the structure and the types of your state. Every field is automatically:\n\n- Type-safe with TypeScript\n- Reactive with Svelte\n- Synchronized across clients\n- Validated against the schema\n\nOnce created, you can use the state like a regular Svelte state: mutate it, bind it, use mutative methods, etc.\n\n```svelte\n\u003cscript\u003e\n\timport { syncroState, y } from 'syncrostate';\n\timport { LiveblocksYjsProvider } from '@liveblocks/yjs';\n\timport { createClient } from '@liveblocks/client';\n\n\tconst document = syncroState({\n\t\t// Optional but required for remote sync: Connect and sync to a yjs provider\n\t\t// If omitted, state will be local-only and in memory.\n\t\tsync: ({ doc, synced }) =\u003e {\n\t\t\tconst client = createClient({\n\t\t\t\tpublicApiKey: 'your-api-key'\n\t\t\t});\n\t\t\tconst { room } = client.enterRoom('room-id');\n\t\t\tconst provider = new LiveblocksYjsProvider(room, doc);\n\t\t\tprovider.on('synced', () =\u003e {\n\t\t\t\tsynced();\n\t\t\t});\n\t\t},\n\n\t\t// Define your state schema. It must be an object\n\t\tschema: {\n\t\t\t// Primitive values\n\t\t\tname: y.string(),\n\t\t\tage: y.number(),\n\t\t\tisOnline: y.boolean(),\n\t\t\tlastSeen: y.date(),\n\t\t\ttheme: y.enum(['light', 'dark']),\n\n\t\t\t// Nested objects\n\t\t\tpreferences: y.object({\n\t\t\t\ttheme: y.enum(['light', 'dark']),\n\t\t\t\tnotifications: y.boolean()\n\t\t\t}),\n\n\t\t\t// Arrays of any type\n\t\t\ttodos: y.array(y.object({\n\t\t\t\ttitle: y.string(),\n\t\t\t\tcompleted: y.boolean()\n\t\t\t})),\n\n\t\t\t// Sets of any primitive type\n\t\t\tcolors: y.set(y.string())\n\t\t}\n\t});\n\u003c/script\u003e\n\n\u003c!-- Use it like regular Svelte state --\u003e\n\u003cinput bind:value={document.name} /\u003e\n\u003cbutton onclick={() =\u003e document.todos.push({ title: 'New todo', completed: false })}\u003e\n\tAdd Todo\n\u003c/button\u003e\n```\n\n## How it works\n\nSyncroState 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:\n\n### Local State Management\n\n1. **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`.\n2. **Mutation Trapping**: These proxies intercept all state mutations (assignments, mutative operations, object modifications, reassignments). This allows SyncroState to:\n\n   - Validate changes against the schema\n   - Update the local Svelte state immediately for responsive UI updates\n   - Forward changes to the underlying Yjs document\n\n### Synchronization Layer\n\n1. **Yjs Integration**: The state is backed by Yjs types in the following way:\n\n   - Primitive values (numbers, booleans, dates, enums) are stored using Y.Text\n   - Arrays are stored using Y.Array\n   - Objects are stored using Y.Map\n\n   When you modify the state:\n\n   - The change is wrapped in a Yjs transaction\n   - For primitives, the value is serialized and stored in the Y.Text\n   - For collections, the corresponding Y.Array or Y.Map is updated.\n   - Yjs handles conflict resolution and ensures eventual consistency\n\n2. **Remote Updates**: When changes come in from other clients:\n\n   - Yjs observer callbacks are triggered\n   - The proxies update their internal Svelte state\n   - Svelte's reactivity system automatically updates any UI components using that state\n\n### Type Safety and Validation\n\n- The schema you define isn't just for TypeScript types - it creates specialized proxies that understand how to handle each type of data\n- Nested objects and arrays create nested proxy structures that maintain reactivity at every level\n- All mutations are validated against the schema before being applied\n\nThis architecture ensures that:\n\n- Local changes feel instant and responsive\n- All clients converge to the same state\n- The state remains type-safe and valid\n- You get Svelte's fine-grained reactivity for optimal performance\n\n## Things to notice\n\n### Adding persistence and remote provider\n\nTo add a persistence provider like y-indexeddb and use a remote provider like Liveblocks or y-websocket you will do something like this:\n\n```ts\nimport { IndexeddbPersistence } from \"y-indexeddb\";\nimport { createClient } from \"@liveblocks/client\";\nimport { LiveblocksYjsProvider } from \"@liveblocks/yjs\";\n\nconst document = syncroState({\n  sync: ({ doc, synced }) =\u003e {\n    const docName = \"your-doc-name\";\n    const localProvider = new IndexeddbPersistence(docName, doc);\n    const remoteClient = createClient({\n      publicApiKey: \"your-api-key\"\n    });\n    const { room } = remoteClient.enterRoom(docName);\n    localProvider.on(\"synced\", () =\u003e {\n      const remoteProvider = new LiveblocksYjsProvider(room, doc);\n      remoteProvider.on(\"synced\", () =\u003e {\n        synced();\n      });\n    });\n  }\n  // ... your schema\n});\n```\n\n### Waiting for the state to be synced\n\nWhen you are using a remote provider, you might want to wait for the state to be synced before doing something.\nThe 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.\n\n```svelte\n{#if document.getState?.().synced}\n\t\u003cdiv\u003eMy name is {document.name}\u003c/div\u003e\n{/if}\n```\n\n### Editing multiple object properties at once\n\nIf you want to edit multiple object properties at once it's preferable to reassign the entire object.\nThis way, syncrostate can apply the changes inside a single transaction and avoid partial updates.\nOnly the properties that are being changed will trigger reactivity and remote updates.\n\n```js\n// Instead of this\nstate.user.name = \"John\";\nstate.user.age = 30;\n\n// Do this\nstate.user = {\n  ...state.user,\n  name: \"John\",\n  age: 30\n};\n```\n\n### Accessing the underlying Yjs document and shared types\n\nEvery syncrostate object or array has three additional methods: `getState`, `getYTypes` and `getYTypes`.\n\n- `getState` returns the state `type State` of the syncronisation.\n- `getYTypes` returns the underlying YObject or YArray.\n- `getYTypes` returns the YJS types children of the YObject or YArray.\n\n```ts\ntype State {\n  synced: boolean;\n  awareness: Awareness;\n  doc: Y.Doc;\n  undoManager: Y.UndoManager;\n  transaction: (fn: () =\u003e void) =\u003e void;\n  transactionKey: any;\n  undo: () =\u003e void;\n  redo: () =\u003e void;\n}\n```\n\n### Undo/Redo\n\nSyncroState uses Yjs's undo/redo system to provide undo/redo functionality. These methods are available through the `getState` method.\n\n## Roadmap\n\n- [ ] Add support for Set and Map types\n- [ ] Find a way to make syncrostate schema optional\n- [ ] Add support for recursive types\n- [ ] Add support for nested documents\n- [ ] Add a simple way to manage awareness sharing\n\n## License\n\nSyncroState is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeynar%2Fsyncrostate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeynar%2Fsyncrostate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeynar%2Fsyncrostate/lists"}