@editneo/sync 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @editneo/sync
|
|
2
|
+
|
|
3
|
+
The synchronization layer for EditNeo. This package provides a `SyncManager` class that bridges the Zustand editor store with [Yjs](https://yjs.dev/) CRDTs, giving you offline persistence through IndexedDB and real-time multi-user collaboration through WebSockets — with automatic conflict resolution.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @editneo/sync @editneo/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
The `SyncManager` creates a Yjs document and maps the editor's block structure into two Yjs shared types:
|
|
14
|
+
|
|
15
|
+
- A `Y.Map` for individual blocks (keyed by block ID)
|
|
16
|
+
- A `Y.Array` for the ordered list of root block IDs
|
|
17
|
+
|
|
18
|
+
Changes flow in both directions:
|
|
19
|
+
|
|
20
|
+
1. **Local to remote:** When the user edits a block through the store, your code calls `syncBlock()` or `syncRoot()` to push the change into the Yjs document. The Yjs providers then propagate it to IndexedDB and to other connected clients.
|
|
21
|
+
|
|
22
|
+
2. **Remote to local:** When a change arrives from another client (or from IndexedDB on page load), the `SyncManager`'s observers detect the Yjs mutation and update the Zustand store accordingly.
|
|
23
|
+
|
|
24
|
+
Because Yjs is a CRDT, conflicting edits from multiple users are merged automatically without a central server making decisions.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Basic (offline only)
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { SyncManager } from "@editneo/sync";
|
|
32
|
+
|
|
33
|
+
const sync = new SyncManager("my-document");
|
|
34
|
+
// Data is now persisted to IndexedDB under the key "editneo-document-my-document"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### With real-time collaboration
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const sync = new SyncManager("my-document", {
|
|
41
|
+
url: "wss://your-yjs-server.com",
|
|
42
|
+
room: "my-document",
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The `url` should point to a [y-websocket](https://github.com/yjs/y-websocket) server. The `room` determines which document the client joins — clients in the same room share the same Yjs document.
|
|
47
|
+
|
|
48
|
+
### Syncing local changes
|
|
49
|
+
|
|
50
|
+
After the store modifies a block, push the change to Yjs:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { useEditorStore } from "@editneo/core";
|
|
54
|
+
|
|
55
|
+
// After updating a block in the store
|
|
56
|
+
const updatedBlock = useEditorStore.getState().blocks["block-123"];
|
|
57
|
+
sync.syncBlock(updatedBlock);
|
|
58
|
+
|
|
59
|
+
// After reordering root blocks
|
|
60
|
+
const rootBlocks = useEditorStore.getState().rootBlocks;
|
|
61
|
+
sync.syncRoot(rootBlocks);
|
|
62
|
+
|
|
63
|
+
// After deleting a block
|
|
64
|
+
sync.deleteBlock("block-123");
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
These methods include basic deduplication: they compare the incoming data against what Yjs currently holds and skip the write if nothing changed. This prevents trivial feedback loops where a Yjs observer fires a store update, which then tries to write back to Yjs.
|
|
68
|
+
|
|
69
|
+
### Cursor awareness
|
|
70
|
+
|
|
71
|
+
When a WebSocket provider is active, you can access the [Yjs Awareness](https://docs.yjs.dev/getting-started/adding-awareness) instance to share cursor positions between users:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const awareness = sync.awareness;
|
|
75
|
+
|
|
76
|
+
if (awareness) {
|
|
77
|
+
// Set local cursor state
|
|
78
|
+
awareness.setLocalStateField("cursor", {
|
|
79
|
+
blockId: "block-abc",
|
|
80
|
+
offset: 12,
|
|
81
|
+
name: "Alice",
|
|
82
|
+
color: "#3b82f6",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Listen for remote cursor changes
|
|
86
|
+
awareness.on("change", () => {
|
|
87
|
+
const states = awareness.getStates();
|
|
88
|
+
// states is a Map<clientID, { cursor: { blockId, offset, name, color } }>
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The `CursorOverlay` component from `@editneo/react` consumes this awareness data automatically.
|
|
94
|
+
|
|
95
|
+
### Cleanup
|
|
96
|
+
|
|
97
|
+
When the editor unmounts or the document changes, destroy the sync manager to close connections and free resources:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
sync.destroy();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This destroys the IndexedDB provider, the WebSocket provider (if any), and the underlying Yjs document.
|
|
104
|
+
|
|
105
|
+
## API Reference
|
|
106
|
+
|
|
107
|
+
### `new SyncManager(docId, syncConfig?)`
|
|
108
|
+
|
|
109
|
+
| Parameter | Type | Description |
|
|
110
|
+
| ------------ | ------------------------------- | --------------------------------------------------------------------- |
|
|
111
|
+
| `docId` | `string` | Unique document identifier. Used to namespace the IndexedDB database. |
|
|
112
|
+
| `syncConfig` | `{ url: string; room: string }` | Optional. WebSocket server URL and room name for real-time sync. |
|
|
113
|
+
|
|
114
|
+
### Instance Properties
|
|
115
|
+
|
|
116
|
+
| Property | Type | Description |
|
|
117
|
+
| ------------ | -------------------------------- | -------------------------------------------------- |
|
|
118
|
+
| `doc` | `Y.Doc` | The underlying Yjs document |
|
|
119
|
+
| `yBlocks` | `Y.Map<any>` | Yjs map of all blocks |
|
|
120
|
+
| `yRoot` | `Y.Array<string>` | Yjs array of root block IDs |
|
|
121
|
+
| `provider` | `IndexeddbPersistence` | The IndexedDB persistence provider |
|
|
122
|
+
| `wsProvider` | `WebsocketProvider \| undefined` | The WebSocket provider, if configured |
|
|
123
|
+
| `awareness` | `Awareness \| undefined` | The awareness instance from the WebSocket provider |
|
|
124
|
+
|
|
125
|
+
### Instance Methods
|
|
126
|
+
|
|
127
|
+
| Method | Signature | Description |
|
|
128
|
+
| ------------- | -------------------------------- | ----------------------------------------------- |
|
|
129
|
+
| `syncBlock` | `(block: NeoBlock) => void` | Pushes a block update to Yjs (with dedup check) |
|
|
130
|
+
| `syncRoot` | `(rootBlocks: string[]) => void` | Replaces the root block ordering in Yjs |
|
|
131
|
+
| `deleteBlock` | `(id: string) => void` | Removes a block from the Yjs map |
|
|
132
|
+
| `destroy` | `() => void` | Tears down all providers and the Yjs document |
|
|
133
|
+
|
|
134
|
+
## Running a WebSocket Server
|
|
135
|
+
|
|
136
|
+
The simplest way to run a Yjs WebSocket server for development:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npx y-websocket
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This starts a server on `ws://localhost:1234`. Point your `syncConfig.url` there:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
new SyncManager("doc", { url: "ws://localhost:1234", room: "doc" });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
For production, see the [y-websocket documentation](https://github.com/yjs/y-websocket) for deployment options including authentication, scaling, and persistence.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|