@editneo/sync 0.1.1 → 0.1.3
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 +56 -50
- package/dist/index.d.ts +35 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +165 -36
- package/dist/index.js.map +1 -1
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -15,23 +15,28 @@ The `SyncManager` creates a Yjs document and maps the editor's block structure i
|
|
|
15
15
|
- A `Y.Map` for individual blocks (keyed by block ID)
|
|
16
16
|
- A `Y.Array` for the ordered list of root block IDs
|
|
17
17
|
|
|
18
|
-
Changes flow
|
|
18
|
+
Changes flow **bidirectionally** and automatically:
|
|
19
19
|
|
|
20
|
-
1. **
|
|
20
|
+
1. **Store → Yjs:** When you mutate the editor store, the `SyncManager` detects the change via Zustand's `subscribe()` and pushes it to Yjs. Only changed blocks are synced, and deletions are tracked. The Yjs providers then propagate changes to IndexedDB and to other connected clients.
|
|
21
21
|
|
|
22
|
-
2. **
|
|
22
|
+
2. **Yjs → Store:** 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.
|
|
23
|
+
|
|
24
|
+
An `isSyncing` flag prevents infinite loops between the two directions. All updates are wrapped in Yjs transactions for atomicity.
|
|
23
25
|
|
|
24
26
|
Because Yjs is a CRDT, conflicting edits from multiple users are merged automatically without a central server making decisions.
|
|
25
27
|
|
|
26
28
|
## Usage
|
|
27
29
|
|
|
28
|
-
###
|
|
30
|
+
### Standalone (offline only)
|
|
29
31
|
|
|
30
32
|
```typescript
|
|
31
33
|
import { SyncManager } from "@editneo/sync";
|
|
34
|
+
import { createEditorStore } from "@editneo/core";
|
|
32
35
|
|
|
36
|
+
const store = createEditorStore();
|
|
33
37
|
const sync = new SyncManager("my-document");
|
|
34
|
-
|
|
38
|
+
sync.bindStore(store);
|
|
39
|
+
// Data is now persisted to IndexedDB under "editneo-document-my-document"
|
|
35
40
|
```
|
|
36
41
|
|
|
37
42
|
### With real-time collaboration
|
|
@@ -41,66 +46,67 @@ const sync = new SyncManager("my-document", {
|
|
|
41
46
|
url: "wss://your-yjs-server.com",
|
|
42
47
|
room: "my-document",
|
|
43
48
|
});
|
|
49
|
+
sync.bindStore(store);
|
|
44
50
|
```
|
|
45
51
|
|
|
46
52
|
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
53
|
|
|
48
|
-
###
|
|
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);
|
|
54
|
+
### With `<NeoEditor />` (recommended)
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
const rootBlocks = useEditorStore.getState().rootBlocks;
|
|
61
|
-
sync.syncRoot(rootBlocks);
|
|
56
|
+
When used with `@editneo/react`, the `NeoEditor` component creates and binds the `SyncManager` automatically when you pass `syncConfig`:
|
|
62
57
|
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
```tsx
|
|
59
|
+
<NeoEditor
|
|
60
|
+
id="shared-doc"
|
|
61
|
+
syncConfig={{ url: "wss://your-server.com", room: "shared-doc" }}
|
|
62
|
+
>
|
|
63
|
+
<CursorOverlay />
|
|
64
|
+
</NeoEditor>
|
|
65
65
|
```
|
|
66
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
67
|
### Cursor awareness
|
|
70
68
|
|
|
71
|
-
|
|
69
|
+
Share cursor positions and user info between collaborators:
|
|
72
70
|
|
|
73
71
|
```typescript
|
|
74
|
-
|
|
72
|
+
// Set your user info
|
|
73
|
+
sync.setUser({
|
|
74
|
+
name: "Alice",
|
|
75
|
+
color: "#3b82f6",
|
|
76
|
+
avatar: "https://example.com/alice.jpg", // optional
|
|
77
|
+
});
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
awareness.on("change", () => {
|
|
87
|
-
const states = awareness.getStates();
|
|
88
|
-
// states is a Map<clientID, { cursor: { blockId, offset, name, color } }>
|
|
89
|
-
});
|
|
90
|
-
}
|
|
79
|
+
// Update cursor position
|
|
80
|
+
sync.setCursor("block-abc", 12); // blockId, character index
|
|
81
|
+
sync.setCursor(null); // Clear cursor (e.g. on blur)
|
|
82
|
+
|
|
83
|
+
// Listen for remote cursor changes
|
|
84
|
+
const awareness = sync.awareness;
|
|
85
|
+
awareness?.on("change", () => {
|
|
86
|
+
const states = awareness.getStates();
|
|
87
|
+
// Map<clientID, { user: { name, color }, cursor: { blockId, index } }>
|
|
88
|
+
});
|
|
91
89
|
```
|
|
92
90
|
|
|
93
91
|
The `CursorOverlay` component from `@editneo/react` consumes this awareness data automatically.
|
|
94
92
|
|
|
93
|
+
### Error handling
|
|
94
|
+
|
|
95
|
+
The `SyncManager` listens for connection events and logs status changes:
|
|
96
|
+
|
|
97
|
+
- `status` — connection state changes (connecting, connected, disconnected)
|
|
98
|
+
- `connection-error` — WebSocket errors
|
|
99
|
+
- `connection-close` — connection closed (auto-reconnect is handled by y-websocket)
|
|
100
|
+
|
|
95
101
|
### Cleanup
|
|
96
102
|
|
|
97
|
-
When the editor unmounts or the document changes, destroy the sync manager
|
|
103
|
+
When the editor unmounts or the document changes, destroy the sync manager:
|
|
98
104
|
|
|
99
105
|
```typescript
|
|
100
106
|
sync.destroy();
|
|
101
107
|
```
|
|
102
108
|
|
|
103
|
-
This destroys the IndexedDB provider, the WebSocket provider (if any), and the underlying Yjs document.
|
|
109
|
+
This unsubscribes from the store, destroys the IndexedDB provider, the WebSocket provider (if any), and the underlying Yjs document.
|
|
104
110
|
|
|
105
111
|
## API Reference
|
|
106
112
|
|
|
@@ -111,6 +117,15 @@ This destroys the IndexedDB provider, the WebSocket provider (if any), and the u
|
|
|
111
117
|
| `docId` | `string` | Unique document identifier. Used to namespace the IndexedDB database. |
|
|
112
118
|
| `syncConfig` | `{ url: string; room: string }` | Optional. WebSocket server URL and room name for real-time sync. |
|
|
113
119
|
|
|
120
|
+
### Instance Methods
|
|
121
|
+
|
|
122
|
+
| Method | Signature | Description |
|
|
123
|
+
| ----------- | ------------------------------------------------------------------ | -------------------------------------------------- |
|
|
124
|
+
| `bindStore` | `(store: EditorStoreInstance) => void` | Binds the sync manager to an editor store instance |
|
|
125
|
+
| `setUser` | `(user: { name: string; color: string; avatar?: string }) => void` | Sets local user awareness info |
|
|
126
|
+
| `setCursor` | `(blockId: string \| null, index?: number) => void` | Updates local cursor position in awareness |
|
|
127
|
+
| `destroy` | `() => void` | Tears down all providers and the Yjs document |
|
|
128
|
+
|
|
114
129
|
### Instance Properties
|
|
115
130
|
|
|
116
131
|
| Property | Type | Description |
|
|
@@ -122,15 +137,6 @@ This destroys the IndexedDB provider, the WebSocket provider (if any), and the u
|
|
|
122
137
|
| `wsProvider` | `WebsocketProvider \| undefined` | The WebSocket provider, if configured |
|
|
123
138
|
| `awareness` | `Awareness \| undefined` | The awareness instance from the WebSocket provider |
|
|
124
139
|
|
|
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
140
|
## Running a WebSocket Server
|
|
135
141
|
|
|
136
142
|
The simplest way to run a Yjs WebSocket server for development:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
1
|
import * as Y from 'yjs';
|
|
2
2
|
import { IndexeddbPersistence } from 'y-indexeddb';
|
|
3
3
|
import { WebsocketProvider } from 'y-websocket';
|
|
4
|
-
import {
|
|
4
|
+
import { EditorStoreInstance } from '@editneo/core';
|
|
5
|
+
export interface SyncConfig {
|
|
6
|
+
url: string;
|
|
7
|
+
room: string;
|
|
8
|
+
}
|
|
9
|
+
export type SyncStatus = 'connecting' | 'connected' | 'disconnected';
|
|
5
10
|
export declare class SyncManager {
|
|
6
11
|
doc: Y.Doc;
|
|
7
12
|
yBlocks: Y.Map<any>;
|
|
8
13
|
yRoot: Y.Array<string>;
|
|
9
14
|
provider: IndexeddbPersistence;
|
|
10
15
|
wsProvider?: WebsocketProvider;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
private store?;
|
|
17
|
+
private unsubscribeStore?;
|
|
18
|
+
private isSyncing;
|
|
19
|
+
private _status;
|
|
20
|
+
private _statusListeners;
|
|
21
|
+
constructor(docId?: string, syncConfig?: SyncConfig);
|
|
22
|
+
/**
|
|
23
|
+
* Bind this SyncManager to a specific editor store instance.
|
|
24
|
+
* - Yjs → Store: changes from remote peers update this store.
|
|
25
|
+
* - Store → Yjs: local store mutations propagate to Yjs (#29).
|
|
26
|
+
*/
|
|
27
|
+
bindStore(store: EditorStoreInstance): void;
|
|
28
|
+
/**
|
|
29
|
+
* (#31) Apply minimal inserts/deletes to a Y.Array instead of replacing everything.
|
|
30
|
+
* Uses a simple forward diff — handles most common operations (single insert, delete, move)
|
|
31
|
+
* with targeted Y.Array mutations to preserve CRDT history.
|
|
32
|
+
*/
|
|
33
|
+
private updateYArraySurgically;
|
|
34
|
+
private setupObservers;
|
|
35
|
+
setUser(user: {
|
|
36
|
+
name: string;
|
|
37
|
+
color: string;
|
|
38
|
+
avatar?: string;
|
|
39
|
+
}): void;
|
|
40
|
+
setCursor(blockId: string | null, index?: number): void;
|
|
19
41
|
destroy(): void;
|
|
20
42
|
get awareness(): import("y-protocols/awareness").Awareness | undefined;
|
|
43
|
+
/** Get the current sync connection status. */
|
|
44
|
+
getStatus(): SyncStatus;
|
|
45
|
+
/** Register a listener for connection status changes. Returns unsubscribe function. */
|
|
46
|
+
onStatusChange(listener: (status: SyncStatus) => void): () => void;
|
|
21
47
|
}
|
|
22
48
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAY,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAE9D,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,CAAC;AAErE,qBAAa,WAAW;IACtB,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACX,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpB,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACvB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,OAAO,CAAC,KAAK,CAAC,CAAsB;IACpC,OAAO,CAAC,gBAAgB,CAAC,CAAa;IACtC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,gBAAgB,CAA2C;gBAEvD,KAAK,GAAE,MAAkB,EAAE,UAAU,CAAC,EAAE,UAAU;IAqC9D;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE,mBAAmB;IA4CpC;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAkD9B,OAAO,CAAC,cAAc;IA0Bf,OAAO,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAM9D,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,MAAU;IAQ1D,OAAO;IAUP,IAAI,SAAS,0DAEZ;IAED,8CAA8C;IAC9C,SAAS,IAAI,UAAU;IAIvB,uFAAuF;IACvF,cAAc,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;CAMnE"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import * as Y from 'yjs';
|
|
2
2
|
import { IndexeddbPersistence } from 'y-indexeddb';
|
|
3
3
|
import { WebsocketProvider } from 'y-websocket';
|
|
4
|
-
import { useEditorStore } from '@editneo/core';
|
|
5
4
|
export class SyncManager {
|
|
6
5
|
constructor(docId = 'default', syncConfig) {
|
|
6
|
+
this.isSyncing = false; // (#32) Transaction flag to prevent loops
|
|
7
|
+
this._status = 'disconnected';
|
|
8
|
+
this._statusListeners = [];
|
|
7
9
|
this.doc = new Y.Doc();
|
|
8
10
|
this.yBlocks = this.doc.getMap('blocks');
|
|
9
11
|
this.yRoot = this.doc.getArray('rootBlocks');
|
|
@@ -12,56 +14,172 @@ export class SyncManager {
|
|
|
12
14
|
// WebSocket Sync
|
|
13
15
|
if (syncConfig) {
|
|
14
16
|
this.wsProvider = new WebsocketProvider(syncConfig.url, syncConfig.room, this.doc);
|
|
17
|
+
// (#30) Connection status and error handling
|
|
18
|
+
this._status = 'connecting';
|
|
15
19
|
this.wsProvider.on('status', (event) => {
|
|
16
|
-
|
|
20
|
+
const newStatus = event.status === 'connected' ? 'connected' : 'disconnected';
|
|
21
|
+
this._status = newStatus;
|
|
22
|
+
this._statusListeners.forEach(fn => fn(newStatus));
|
|
23
|
+
console.log('[EditNeo Sync] Status:', event.status);
|
|
24
|
+
});
|
|
25
|
+
this.wsProvider.on('connection-error', (event) => {
|
|
26
|
+
this._status = 'disconnected';
|
|
27
|
+
this._statusListeners.forEach(fn => fn('disconnected'));
|
|
28
|
+
console.warn('[EditNeo Sync] Connection error:', event);
|
|
29
|
+
});
|
|
30
|
+
this.wsProvider.on('connection-close', (_event) => {
|
|
31
|
+
this._status = 'connecting'; // will auto-reconnect
|
|
32
|
+
this._statusListeners.forEach(fn => fn('connecting'));
|
|
33
|
+
console.log('[EditNeo Sync] Connection closed, will auto-reconnect');
|
|
17
34
|
});
|
|
18
35
|
}
|
|
19
36
|
this.setupObservers();
|
|
20
37
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
this.
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Bind this SyncManager to a specific editor store instance.
|
|
40
|
+
* - Yjs → Store: changes from remote peers update this store.
|
|
41
|
+
* - Store → Yjs: local store mutations propagate to Yjs (#29).
|
|
42
|
+
*/
|
|
43
|
+
bindStore(store) {
|
|
44
|
+
// Clean up previous binding
|
|
45
|
+
if (this.unsubscribeStore) {
|
|
46
|
+
this.unsubscribeStore();
|
|
47
|
+
}
|
|
48
|
+
this.store = store;
|
|
49
|
+
// (#29) Subscribe to store changes → push to Yjs
|
|
50
|
+
this.unsubscribeStore = store.subscribe((state, prevState) => {
|
|
51
|
+
if (this.isSyncing)
|
|
52
|
+
return; // (#32) Prevent bounce-back
|
|
53
|
+
this.isSyncing = true;
|
|
54
|
+
try {
|
|
55
|
+
this.doc.transact(() => {
|
|
56
|
+
// Sync blocks
|
|
57
|
+
const currentBlockIds = new Set(Object.keys(state.blocks));
|
|
58
|
+
const prevBlockIds = new Set(Object.keys(prevState.blocks));
|
|
59
|
+
// Update/add new blocks
|
|
60
|
+
for (const id of currentBlockIds) {
|
|
61
|
+
if (state.blocks[id] !== prevState.blocks[id]) {
|
|
62
|
+
this.yBlocks.set(id, state.blocks[id]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Delete removed blocks
|
|
66
|
+
for (const id of prevBlockIds) {
|
|
67
|
+
if (!currentBlockIds.has(id) && this.yBlocks.has(id)) {
|
|
68
|
+
this.yBlocks.delete(id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// (#31) Surgical Y.Array updates for rootBlocks
|
|
72
|
+
if (state.rootBlocks !== prevState.rootBlocks) {
|
|
73
|
+
this.updateYArraySurgically(this.yRoot, prevState.rootBlocks, state.rootBlocks);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
this.isSyncing = false;
|
|
79
|
+
}
|
|
31
80
|
});
|
|
32
|
-
// Subscribe to store changes to push to Yjs?
|
|
33
|
-
// Doing it inside store via middleware is better to avoid loops.
|
|
34
|
-
// OR expose a method here that the store calls.
|
|
35
81
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (JSON.stringify(
|
|
45
|
-
|
|
46
|
-
|
|
82
|
+
/**
|
|
83
|
+
* (#31) Apply minimal inserts/deletes to a Y.Array instead of replacing everything.
|
|
84
|
+
* Uses a simple forward diff — handles most common operations (single insert, delete, move)
|
|
85
|
+
* with targeted Y.Array mutations to preserve CRDT history.
|
|
86
|
+
*/
|
|
87
|
+
updateYArraySurgically(yArray, _oldArr, newArr) {
|
|
88
|
+
const currentY = yArray.toJSON();
|
|
89
|
+
// If they already match, skip
|
|
90
|
+
if (JSON.stringify(currentY) === JSON.stringify(newArr))
|
|
91
|
+
return;
|
|
92
|
+
// Build a simple LCS-based diff
|
|
93
|
+
const m = currentY.length;
|
|
94
|
+
const n = newArr.length;
|
|
95
|
+
// LCS table
|
|
96
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
97
|
+
for (let i = 1; i <= m; i++) {
|
|
98
|
+
for (let j = 1; j <= n; j++) {
|
|
99
|
+
dp[i][j] = currentY[i - 1] === newArr[j - 1]
|
|
100
|
+
? dp[i - 1][j - 1] + 1
|
|
101
|
+
: Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Backtrack to find operations
|
|
105
|
+
const ops = [];
|
|
106
|
+
let i = m, j = n;
|
|
107
|
+
while (i > 0 || j > 0) {
|
|
108
|
+
if (i > 0 && j > 0 && currentY[i - 1] === newArr[j - 1]) {
|
|
109
|
+
ops.unshift({ type: 'keep', value: currentY[i - 1] });
|
|
110
|
+
i--;
|
|
111
|
+
j--;
|
|
112
|
+
}
|
|
113
|
+
else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
114
|
+
ops.unshift({ type: 'insert', value: newArr[j - 1] });
|
|
115
|
+
j--;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
ops.unshift({ type: 'delete', value: currentY[i - 1] });
|
|
119
|
+
i--;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Apply operations to Y.Array with positional tracking
|
|
123
|
+
let pos = 0;
|
|
124
|
+
for (const op of ops) {
|
|
125
|
+
if (op.type === 'keep') {
|
|
126
|
+
pos++;
|
|
127
|
+
}
|
|
128
|
+
else if (op.type === 'delete') {
|
|
129
|
+
yArray.delete(pos, 1);
|
|
130
|
+
}
|
|
131
|
+
else if (op.type === 'insert') {
|
|
132
|
+
yArray.insert(pos, [op.value]);
|
|
133
|
+
pos++;
|
|
134
|
+
}
|
|
47
135
|
}
|
|
48
136
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
137
|
+
setupObservers() {
|
|
138
|
+
// Yjs → Store (remote changes)
|
|
139
|
+
this.yBlocks.observe(() => {
|
|
140
|
+
if (!this.store || this.isSyncing)
|
|
141
|
+
return;
|
|
142
|
+
this.isSyncing = true;
|
|
143
|
+
try {
|
|
144
|
+
const newBlocks = this.yBlocks.toJSON();
|
|
145
|
+
this.store.setState({ blocks: newBlocks });
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
this.isSyncing = false;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
this.yRoot.observe(() => {
|
|
152
|
+
if (!this.store || this.isSyncing)
|
|
153
|
+
return;
|
|
154
|
+
this.isSyncing = true;
|
|
155
|
+
try {
|
|
156
|
+
const newRoot = this.yRoot.toJSON();
|
|
157
|
+
this.store.setState({ rootBlocks: newRoot });
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
this.isSyncing = false;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// (#33) Expose setUser for awareness
|
|
165
|
+
setUser(user) {
|
|
166
|
+
var _a;
|
|
167
|
+
if ((_a = this.wsProvider) === null || _a === void 0 ? void 0 : _a.awareness) {
|
|
168
|
+
this.wsProvider.awareness.setLocalStateField('user', user);
|
|
56
169
|
}
|
|
57
170
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
171
|
+
setCursor(blockId, index = 0) {
|
|
172
|
+
var _a;
|
|
173
|
+
if ((_a = this.wsProvider) === null || _a === void 0 ? void 0 : _a.awareness) {
|
|
174
|
+
this.wsProvider.awareness.setLocalStateField('cursor', blockId ? { blockId, index } : null);
|
|
61
175
|
}
|
|
62
176
|
}
|
|
63
177
|
destroy() {
|
|
64
178
|
var _a;
|
|
179
|
+
if (this.unsubscribeStore) {
|
|
180
|
+
this.unsubscribeStore();
|
|
181
|
+
this.unsubscribeStore = undefined;
|
|
182
|
+
}
|
|
65
183
|
this.provider.destroy();
|
|
66
184
|
(_a = this.wsProvider) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
67
185
|
this.doc.destroy();
|
|
@@ -70,5 +188,16 @@ export class SyncManager {
|
|
|
70
188
|
var _a;
|
|
71
189
|
return (_a = this.wsProvider) === null || _a === void 0 ? void 0 : _a.awareness;
|
|
72
190
|
}
|
|
191
|
+
/** Get the current sync connection status. */
|
|
192
|
+
getStatus() {
|
|
193
|
+
return this._status;
|
|
194
|
+
}
|
|
195
|
+
/** Register a listener for connection status changes. Returns unsubscribe function. */
|
|
196
|
+
onStatusChange(listener) {
|
|
197
|
+
this._statusListeners.push(listener);
|
|
198
|
+
return () => {
|
|
199
|
+
this._statusListeners = this._statusListeners.filter(fn => fn !== listener);
|
|
200
|
+
};
|
|
201
|
+
}
|
|
73
202
|
}
|
|
74
203
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editneo/sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Yjs-based CRDT sync manager for EditNeo with offline and WebSocket support",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
7
15
|
"files": ["dist"],
|
|
8
16
|
"scripts": {
|
|
9
17
|
"build": "tsc",
|
|
@@ -16,7 +24,7 @@
|
|
|
16
24
|
"yjs": "^13.6.10",
|
|
17
25
|
"y-indexeddb": "^9.0.12",
|
|
18
26
|
"y-websocket": "^1.5.0",
|
|
19
|
-
"@editneo/core": "
|
|
27
|
+
"@editneo/core": "^0.1.2"
|
|
20
28
|
},
|
|
21
29
|
"devDependencies": {
|
|
22
30
|
"typescript": "^5.3.3",
|