@coaction/yjs 1.0.1 → 1.1.0
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 +105 -4
- package/dist/index.d.mts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +477 -14
- package/dist/index.mjs +477 -14
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,13 +8,11 @@ A Coaction integration tool for Yjs.
|
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
|
-
You can install it via npm, yarn or pnpm.
|
|
12
|
-
|
|
13
11
|
```sh
|
|
14
12
|
npm install coaction @coaction/yjs
|
|
15
13
|
```
|
|
16
14
|
|
|
17
|
-
##
|
|
15
|
+
## Quick Start
|
|
18
16
|
|
|
19
17
|
```ts
|
|
20
18
|
import { create } from 'coaction';
|
|
@@ -35,6 +33,109 @@ const store = create(
|
|
|
35
33
|
);
|
|
36
34
|
```
|
|
37
35
|
|
|
36
|
+
## Sync Model
|
|
37
|
+
|
|
38
|
+
- The binding stores state under `doc.getMap(key).get('state')`.
|
|
39
|
+
- `state` is persisted as nested `Y.Map` / `Y.Array` structures.
|
|
40
|
+
- Local Coaction updates are synced to Yjs.
|
|
41
|
+
- Remote Yjs updates are replayed back into Coaction.
|
|
42
|
+
- Writes are diff-based against the last synced local snapshot to reduce overwrite risk in concurrent edits.
|
|
43
|
+
|
|
44
|
+
## Conflict Semantics
|
|
45
|
+
|
|
46
|
+
- Concurrent updates to different fields can converge through Yjs merge behavior.
|
|
47
|
+
- Concurrent updates to the same field follow Yjs conflict semantics (effectively last-writer-wins for scalar fields).
|
|
48
|
+
- If you need commutative semantics (for example counters), use CRDT-native field modeling in Yjs for that field.
|
|
49
|
+
|
|
50
|
+
## Provider Integration
|
|
51
|
+
|
|
52
|
+
`@coaction/yjs` only binds a Coaction store to a `Y.Doc`. Network transport is provided by your Yjs provider (for example `y-websocket`, `y-webrtc`, or a custom provider).
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { create } from 'coaction';
|
|
56
|
+
import { bindYjs } from '@coaction/yjs';
|
|
57
|
+
import { Doc } from 'yjs';
|
|
58
|
+
import { WebsocketProvider } from 'y-websocket';
|
|
59
|
+
|
|
60
|
+
const doc = new Doc();
|
|
61
|
+
const provider = new WebsocketProvider('wss://your-server', 'room-id', doc);
|
|
62
|
+
|
|
63
|
+
const store = create((set) => ({
|
|
64
|
+
count: 0,
|
|
65
|
+
increment() {
|
|
66
|
+
set((draft) => {
|
|
67
|
+
draft.count += 1;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const binding = bindYjs(store, {
|
|
73
|
+
doc,
|
|
74
|
+
key: 'counter'
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// later
|
|
78
|
+
binding.destroy();
|
|
79
|
+
provider.destroy();
|
|
80
|
+
store.destroy();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## API
|
|
84
|
+
|
|
85
|
+
### `bindYjs(store, options?)`
|
|
86
|
+
|
|
87
|
+
Binds a Coaction store to Yjs and returns a binding object.
|
|
88
|
+
|
|
89
|
+
- `options.doc?: Y.Doc`
|
|
90
|
+
- Reuse an existing doc for multi-peer collaboration.
|
|
91
|
+
- If omitted, an internal `Y.Doc` is created.
|
|
92
|
+
- `options.key?: string`
|
|
93
|
+
- Root map key for this store namespace.
|
|
94
|
+
- Default: `coaction:${store.name}`.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
|
|
98
|
+
- `doc: Y.Doc`
|
|
99
|
+
- `map: Y.Map<any>`
|
|
100
|
+
- `syncNow(): void`
|
|
101
|
+
- Pushes the current local state to Yjs immediately.
|
|
102
|
+
- `destroy(): void`
|
|
103
|
+
- Unsubscribes observers and releases resources.
|
|
104
|
+
- Destroys `doc` only when it was internally created.
|
|
105
|
+
|
|
106
|
+
### `yjs(options?)`
|
|
107
|
+
|
|
108
|
+
Middleware wrapper around `bindYjs`. It also wires cleanup into `store.destroy()`.
|
|
109
|
+
|
|
110
|
+
## State Requirements
|
|
111
|
+
|
|
112
|
+
- Keep the synced state plain and serializable.
|
|
113
|
+
- Methods and getters are Coaction runtime behavior and are not the synchronization payload.
|
|
114
|
+
- Prefer a plain object at the root of your store state.
|
|
115
|
+
|
|
116
|
+
## Compatibility and Limits
|
|
117
|
+
|
|
118
|
+
- Not supported in `store.share === 'client'` mode.
|
|
119
|
+
- Very large or highly volatile state trees can produce heavy update traffic.
|
|
120
|
+
- Cleanup is required: always call `binding.destroy()` (or `store.destroy()` when using middleware).
|
|
121
|
+
|
|
122
|
+
## Migration Notes
|
|
123
|
+
|
|
124
|
+
- Current storage shape uses `Y.Map`/`Y.Array` for nested state.
|
|
125
|
+
- If old data stored `state` as a plain object, the binding migrates it to Yjs structures automatically.
|
|
126
|
+
- If you read Yjs state directly, do not assume `map.get('state').count`; `state` is usually a `Y.Map`.
|
|
127
|
+
|
|
128
|
+
## Troubleshooting
|
|
129
|
+
|
|
130
|
+
- `setState cannot be called within the updater`
|
|
131
|
+
- Indicates re-entrant updates in your app flow. Avoid nested synchronous state loops.
|
|
132
|
+
- State not syncing across peers
|
|
133
|
+
- Verify all peers use the same room/doc provider and same `key`.
|
|
134
|
+
- Unexpected stale updates
|
|
135
|
+
- Ensure `destroy()` is called for old bindings to avoid duplicate observers.
|
|
136
|
+
- High update volume
|
|
137
|
+
- Reduce write frequency (debounce/throttle app-level updates if needed).
|
|
138
|
+
|
|
38
139
|
## Documentation
|
|
39
140
|
|
|
40
|
-
You can find the documentation [here](https://github.com/unadlib/coaction).
|
|
141
|
+
You can find the project documentation [here](https://github.com/unadlib/coaction).
|
package/dist/index.d.mts
CHANGED
|
@@ -11,8 +11,20 @@ type YjsBinding<T extends object> = {
|
|
|
11
11
|
map: Y.Map<any>;
|
|
12
12
|
syncNow: () => void;
|
|
13
13
|
destroy: () => void;
|
|
14
|
+
__unsafeTestOnly__?: {
|
|
15
|
+
applyRemoteOperations: (operations: Array<{
|
|
16
|
+
type: 'set' | 'delete';
|
|
17
|
+
path: Array<string | number>;
|
|
18
|
+
value?: unknown;
|
|
19
|
+
}>) => void;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
declare const __unsafeTestOnly__: {
|
|
23
|
+
getYValueAtPath: (root: Y.Map<unknown>, path: Array<string | number>) => unknown;
|
|
24
|
+
setAtPath: (target: any, path: Array<string | number>, value: unknown) => void;
|
|
25
|
+
deleteAtPath: (target: any, path: Array<string | number>) => void;
|
|
14
26
|
};
|
|
15
27
|
declare const bindYjs: <T extends object>(store: Store<T>, options?: YjsBindingOptions) => YjsBinding<T>;
|
|
16
28
|
declare const yjs: <T extends object>(options?: YjsBindingOptions) => Middleware<T>;
|
|
17
29
|
|
|
18
|
-
export { type YjsBinding, type YjsBindingOptions, bindYjs, yjs };
|
|
30
|
+
export { type YjsBinding, type YjsBindingOptions, __unsafeTestOnly__, bindYjs, yjs };
|
package/dist/index.d.ts
CHANGED
|
@@ -11,8 +11,20 @@ type YjsBinding<T extends object> = {
|
|
|
11
11
|
map: Y.Map<any>;
|
|
12
12
|
syncNow: () => void;
|
|
13
13
|
destroy: () => void;
|
|
14
|
+
__unsafeTestOnly__?: {
|
|
15
|
+
applyRemoteOperations: (operations: Array<{
|
|
16
|
+
type: 'set' | 'delete';
|
|
17
|
+
path: Array<string | number>;
|
|
18
|
+
value?: unknown;
|
|
19
|
+
}>) => void;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
declare const __unsafeTestOnly__: {
|
|
23
|
+
getYValueAtPath: (root: Y.Map<unknown>, path: Array<string | number>) => unknown;
|
|
24
|
+
setAtPath: (target: any, path: Array<string | number>, value: unknown) => void;
|
|
25
|
+
deleteAtPath: (target: any, path: Array<string | number>) => void;
|
|
14
26
|
};
|
|
15
27
|
declare const bindYjs: <T extends object>(store: Store<T>, options?: YjsBindingOptions) => YjsBinding<T>;
|
|
16
28
|
declare const yjs: <T extends object>(options?: YjsBindingOptions) => Middleware<T>;
|
|
17
29
|
|
|
18
|
-
export { type YjsBinding, type YjsBindingOptions, bindYjs, yjs };
|
|
30
|
+
export { type YjsBinding, type YjsBindingOptions, __unsafeTestOnly__, bindYjs, yjs };
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
// index.ts
|
|
32
32
|
var index_exports = {};
|
|
33
33
|
__export(index_exports, {
|
|
34
|
+
__unsafeTestOnly__: () => __unsafeTestOnly__,
|
|
34
35
|
bindYjs: () => bindYjs,
|
|
35
36
|
yjs: () => yjs
|
|
36
37
|
});
|
|
@@ -39,18 +40,313 @@ module.exports = __toCommonJS(index_exports);
|
|
|
39
40
|
// src/index.ts
|
|
40
41
|
var src_exports = {};
|
|
41
42
|
__export(src_exports, {
|
|
43
|
+
__unsafeTestOnly__: () => __unsafeTestOnly__,
|
|
42
44
|
bindYjs: () => bindYjs,
|
|
43
45
|
yjs: () => yjs
|
|
44
46
|
});
|
|
45
47
|
var Y = __toESM(require("yjs"));
|
|
46
48
|
__reExport(src_exports, require("yjs"));
|
|
47
|
-
var
|
|
49
|
+
var STATE_KEY = "state";
|
|
48
50
|
function clone(value) {
|
|
49
51
|
if (typeof structuredClone === "function") {
|
|
50
52
|
return structuredClone(value);
|
|
51
53
|
}
|
|
52
54
|
return JSON.parse(JSON.stringify(value));
|
|
53
55
|
}
|
|
56
|
+
function isPlainObject(value) {
|
|
57
|
+
if (typeof value !== "object" || value === null) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const prototype = Object.getPrototypeOf(value);
|
|
61
|
+
return prototype === Object.prototype || prototype === null;
|
|
62
|
+
}
|
|
63
|
+
function toPlainObject(value) {
|
|
64
|
+
const next = {};
|
|
65
|
+
value.forEach((item, key) => {
|
|
66
|
+
next[key] = toPlainValue(item);
|
|
67
|
+
});
|
|
68
|
+
return next;
|
|
69
|
+
}
|
|
70
|
+
function toPlainArray(value) {
|
|
71
|
+
return value.toArray().map((item) => toPlainValue(item));
|
|
72
|
+
}
|
|
73
|
+
function toPlainValue(value) {
|
|
74
|
+
if (value instanceof Y.Map) {
|
|
75
|
+
return toPlainObject(value);
|
|
76
|
+
}
|
|
77
|
+
if (value instanceof Y.Array) {
|
|
78
|
+
return toPlainArray(value);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function createYMap(value) {
|
|
83
|
+
const next = new Y.Map();
|
|
84
|
+
for (const [key, item] of Object.entries(value)) {
|
|
85
|
+
next.set(key, toYValue(item));
|
|
86
|
+
}
|
|
87
|
+
return next;
|
|
88
|
+
}
|
|
89
|
+
function createYArray(value) {
|
|
90
|
+
const next = new Y.Array();
|
|
91
|
+
if (value.length > 0) {
|
|
92
|
+
next.insert(
|
|
93
|
+
0,
|
|
94
|
+
value.map((item) => toYValue(item))
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return next;
|
|
98
|
+
}
|
|
99
|
+
function toYValue(value) {
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return createYArray(value);
|
|
102
|
+
}
|
|
103
|
+
if (isPlainObject(value)) {
|
|
104
|
+
return createYMap(value);
|
|
105
|
+
}
|
|
106
|
+
if (typeof value === "object" && value !== null) {
|
|
107
|
+
return clone(value);
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
function isEqualValue(left, right) {
|
|
112
|
+
if (Object.is(left, right)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
116
|
+
if (left.length !== right.length) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
120
|
+
if (!isEqualValue(left[index], right[index])) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
127
|
+
const leftKeys = Object.keys(left);
|
|
128
|
+
const rightKeys = Object.keys(right);
|
|
129
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
for (const key of leftKeys) {
|
|
133
|
+
if (!Object.prototype.hasOwnProperty.call(right, key)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (!isEqualValue(left[key], right[key])) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
function syncObjectToYMap(target, previous, source) {
|
|
145
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(source)]);
|
|
146
|
+
for (const key of keys) {
|
|
147
|
+
const hasPrevious = Object.prototype.hasOwnProperty.call(previous, key);
|
|
148
|
+
const hasNext = Object.prototype.hasOwnProperty.call(source, key);
|
|
149
|
+
if (!hasNext) {
|
|
150
|
+
target.delete(key);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const previousValue = hasPrevious ? previous[key] : void 0;
|
|
154
|
+
const nextValue = source[key];
|
|
155
|
+
if (hasPrevious && isEqualValue(previousValue, nextValue)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const current = target.get(key);
|
|
159
|
+
if (Array.isArray(nextValue)) {
|
|
160
|
+
if (Array.isArray(previousValue) && current instanceof Y.Array) {
|
|
161
|
+
syncArrayToYArray(current, previousValue, nextValue);
|
|
162
|
+
} else {
|
|
163
|
+
target.set(key, createYArray(nextValue));
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (isPlainObject(nextValue)) {
|
|
168
|
+
if (isPlainObject(previousValue) && current instanceof Y.Map) {
|
|
169
|
+
syncObjectToYMap(current, previousValue, nextValue);
|
|
170
|
+
} else {
|
|
171
|
+
target.set(key, createYMap(nextValue));
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const normalized = typeof nextValue === "object" && nextValue !== null ? clone(nextValue) : nextValue;
|
|
176
|
+
if (!Object.is(current, normalized)) {
|
|
177
|
+
target.set(key, normalized);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function syncArrayToYArray(target, previous, source) {
|
|
182
|
+
if (target.length > source.length) {
|
|
183
|
+
target.delete(source.length, target.length - source.length);
|
|
184
|
+
}
|
|
185
|
+
const maxLength = Math.max(previous.length, source.length);
|
|
186
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
187
|
+
const hasPrevious = index < previous.length;
|
|
188
|
+
const hasNext = index < source.length;
|
|
189
|
+
if (!hasNext) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const previousValue = hasPrevious ? previous[index] : void 0;
|
|
193
|
+
const nextValue = source[index];
|
|
194
|
+
if (hasPrevious && isEqualValue(previousValue, nextValue)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (index >= target.length) {
|
|
198
|
+
target.insert(index, [toYValue(nextValue)]);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const current = target.get(index);
|
|
202
|
+
if (Array.isArray(nextValue)) {
|
|
203
|
+
if (Array.isArray(previousValue) && current instanceof Y.Array) {
|
|
204
|
+
syncArrayToYArray(current, previousValue, nextValue);
|
|
205
|
+
} else {
|
|
206
|
+
target.delete(index, 1);
|
|
207
|
+
target.insert(index, [createYArray(nextValue)]);
|
|
208
|
+
}
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (isPlainObject(nextValue)) {
|
|
212
|
+
if (isPlainObject(previousValue) && current instanceof Y.Map) {
|
|
213
|
+
syncObjectToYMap(current, previousValue, nextValue);
|
|
214
|
+
} else {
|
|
215
|
+
target.delete(index, 1);
|
|
216
|
+
target.insert(index, [createYMap(nextValue)]);
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const normalized = typeof nextValue === "object" && nextValue !== null ? clone(nextValue) : nextValue;
|
|
221
|
+
if (!Object.is(current, normalized)) {
|
|
222
|
+
target.delete(index, 1);
|
|
223
|
+
target.insert(index, [normalized]);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function isSetStateReentryError(error) {
|
|
228
|
+
return error instanceof Error && error.message === "setState cannot be called within the updater";
|
|
229
|
+
}
|
|
230
|
+
function cloneForStore(value) {
|
|
231
|
+
if (typeof value === "object" && value !== null) {
|
|
232
|
+
return clone(value);
|
|
233
|
+
}
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
function toPathKey(path) {
|
|
237
|
+
return path.map((segment) => `${typeof segment}:${String(segment)}`).join("|");
|
|
238
|
+
}
|
|
239
|
+
function compactOperations(operations) {
|
|
240
|
+
const deduplicated = /* @__PURE__ */ new Map();
|
|
241
|
+
for (const operation of operations) {
|
|
242
|
+
const key = toPathKey(operation.path);
|
|
243
|
+
if (deduplicated.has(key)) {
|
|
244
|
+
deduplicated.delete(key);
|
|
245
|
+
}
|
|
246
|
+
deduplicated.set(key, operation);
|
|
247
|
+
}
|
|
248
|
+
return Array.from(deduplicated.values()).sort(
|
|
249
|
+
(left, right) => left.path.length - right.path.length
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
function getYValueAtPath(root, path) {
|
|
253
|
+
let current = root;
|
|
254
|
+
for (const segment of path) {
|
|
255
|
+
if (current instanceof Y.Map) {
|
|
256
|
+
current = current.get(String(segment));
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (current instanceof Y.Array) {
|
|
260
|
+
const index = typeof segment === "number" ? segment : Number.parseInt(segment, 10);
|
|
261
|
+
if (!Number.isInteger(index)) {
|
|
262
|
+
return void 0;
|
|
263
|
+
}
|
|
264
|
+
current = current.get(index);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
return current;
|
|
270
|
+
}
|
|
271
|
+
function setAtPath(target, path, value) {
|
|
272
|
+
if (path.length === 0) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
let current = target;
|
|
276
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
277
|
+
const segment = path[index];
|
|
278
|
+
const nextSegment = path[index + 1];
|
|
279
|
+
const nextValue = current[segment];
|
|
280
|
+
if (typeof nextValue !== "object" || nextValue === null) {
|
|
281
|
+
current[segment] = typeof nextSegment === "number" ? [] : {};
|
|
282
|
+
}
|
|
283
|
+
current = current[segment];
|
|
284
|
+
}
|
|
285
|
+
const leaf = path[path.length - 1];
|
|
286
|
+
current[leaf] = cloneForStore(value);
|
|
287
|
+
}
|
|
288
|
+
function deleteAtPath(target, path) {
|
|
289
|
+
if (path.length === 0) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
let current = target;
|
|
293
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
294
|
+
current = current[path[index]];
|
|
295
|
+
if (typeof current !== "object" || current === null) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const leaf = path[path.length - 1];
|
|
300
|
+
if (Array.isArray(current) && typeof leaf === "number") {
|
|
301
|
+
if (leaf >= 0 && leaf < current.length) {
|
|
302
|
+
current.splice(leaf, 1);
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
delete current[leaf];
|
|
307
|
+
}
|
|
308
|
+
function collectRemoteOperations(events, stateMap) {
|
|
309
|
+
const operations = [];
|
|
310
|
+
for (const event of events) {
|
|
311
|
+
if (event instanceof Y.YMapEvent) {
|
|
312
|
+
for (const changedKey of event.keysChanged) {
|
|
313
|
+
const path = [...event.path, changedKey];
|
|
314
|
+
const keyChange = event.changes.keys.get(changedKey);
|
|
315
|
+
if (keyChange?.action === "delete") {
|
|
316
|
+
operations.push({
|
|
317
|
+
type: "delete",
|
|
318
|
+
path
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
operations.push({
|
|
323
|
+
type: "set",
|
|
324
|
+
path,
|
|
325
|
+
value: toPlainValue(getYValueAtPath(stateMap, path))
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (event instanceof Y.YArrayEvent) {
|
|
331
|
+
const path = [...event.path];
|
|
332
|
+
operations.push({
|
|
333
|
+
type: "set",
|
|
334
|
+
path,
|
|
335
|
+
value: toPlainValue(getYValueAtPath(stateMap, path))
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return operations;
|
|
340
|
+
}
|
|
341
|
+
var __unsafeTestOnly__ = {
|
|
342
|
+
getYValueAtPath: (root, path) => getYValueAtPath(root, path),
|
|
343
|
+
setAtPath: (target, path, value) => {
|
|
344
|
+
setAtPath(target, path, value);
|
|
345
|
+
},
|
|
346
|
+
deleteAtPath: (target, path) => {
|
|
347
|
+
deleteAtPath(target, path);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
54
350
|
var bindYjs = (store, options = {}) => {
|
|
55
351
|
if (store.share === "client") {
|
|
56
352
|
throw new Error("Yjs binding is not supported in client store mode.");
|
|
@@ -58,48 +354,214 @@ var bindYjs = (store, options = {}) => {
|
|
|
58
354
|
const doc = options.doc ?? new Y.Doc();
|
|
59
355
|
const key = options.key ?? `coaction:${store.name}`;
|
|
60
356
|
const map = doc.getMap(key);
|
|
357
|
+
const localOrigin = /* @__PURE__ */ Symbol(`coaction-yjs:${store.name}`);
|
|
358
|
+
let destroyed = false;
|
|
359
|
+
let syncingFromYjs = false;
|
|
360
|
+
let lastSyncedState = (() => {
|
|
361
|
+
const pureState = clone(store.getPureState());
|
|
362
|
+
return isPlainObject(pureState) ? pureState : {};
|
|
363
|
+
})();
|
|
364
|
+
let flushScheduled = false;
|
|
365
|
+
let pendingSnapshot = null;
|
|
366
|
+
let pendingOperations = [];
|
|
367
|
+
const applyRemoteState = (state) => {
|
|
368
|
+
const next = clone(state);
|
|
369
|
+
syncingFromYjs = true;
|
|
370
|
+
try {
|
|
371
|
+
store.setState(next);
|
|
372
|
+
const pureState = clone(store.getPureState());
|
|
373
|
+
lastSyncedState = isPlainObject(pureState) ? pureState : {};
|
|
374
|
+
} finally {
|
|
375
|
+
syncingFromYjs = false;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const applyRemoteOperations = (operations) => {
|
|
379
|
+
if (operations.length === 0) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
syncingFromYjs = true;
|
|
383
|
+
try {
|
|
384
|
+
store.setState((draft) => {
|
|
385
|
+
const mutableDraft = draft;
|
|
386
|
+
for (const operation of operations) {
|
|
387
|
+
if (operation.type === "set") {
|
|
388
|
+
setAtPath(mutableDraft, operation.path, operation.value);
|
|
389
|
+
} else {
|
|
390
|
+
deleteAtPath(mutableDraft, operation.path);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
const pureState = clone(store.getPureState());
|
|
395
|
+
lastSyncedState = isPlainObject(pureState) ? pureState : {};
|
|
396
|
+
} finally {
|
|
397
|
+
syncingFromYjs = false;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
const getStateMap = () => {
|
|
401
|
+
const state = map.get(STATE_KEY);
|
|
402
|
+
if (state instanceof Y.Map) {
|
|
403
|
+
return state;
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
};
|
|
407
|
+
const scheduleFlushFromYjs = () => {
|
|
408
|
+
if (destroyed || flushScheduled) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
flushScheduled = true;
|
|
412
|
+
queueMicrotask(flushFromYjs);
|
|
413
|
+
};
|
|
414
|
+
const flushFromYjs = () => {
|
|
415
|
+
flushScheduled = false;
|
|
416
|
+
if (destroyed) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (pendingSnapshot) {
|
|
420
|
+
const snapshot = pendingSnapshot;
|
|
421
|
+
pendingSnapshot = null;
|
|
422
|
+
pendingOperations = [];
|
|
423
|
+
try {
|
|
424
|
+
applyRemoteState(snapshot);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (isSetStateReentryError(error)) {
|
|
427
|
+
pendingSnapshot = snapshot;
|
|
428
|
+
setTimeout(scheduleFlushFromYjs, 0);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (pendingOperations.length === 0) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const operations = compactOperations(pendingOperations);
|
|
438
|
+
pendingOperations = [];
|
|
439
|
+
try {
|
|
440
|
+
applyRemoteOperations(operations);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (isSetStateReentryError(error)) {
|
|
443
|
+
pendingOperations = [...operations, ...pendingOperations];
|
|
444
|
+
setTimeout(scheduleFlushFromYjs, 0);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
const enqueueSnapshot = (snapshot) => {
|
|
451
|
+
pendingSnapshot = snapshot;
|
|
452
|
+
pendingOperations = [];
|
|
453
|
+
scheduleFlushFromYjs();
|
|
454
|
+
};
|
|
455
|
+
const enqueueOperations = (operations) => {
|
|
456
|
+
if (operations.length === 0) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (!pendingSnapshot) {
|
|
460
|
+
pendingOperations.push(...operations);
|
|
461
|
+
}
|
|
462
|
+
scheduleFlushFromYjs();
|
|
463
|
+
};
|
|
61
464
|
const syncNow = () => {
|
|
465
|
+
if (destroyed || syncingFromYjs) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
62
468
|
const pureState = clone(store.getPureState());
|
|
469
|
+
if (!isPlainObject(pureState)) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
63
472
|
doc.transact(() => {
|
|
64
|
-
|
|
65
|
-
},
|
|
473
|
+
syncObjectToYMap(stateMap, lastSyncedState, pureState);
|
|
474
|
+
}, localOrigin);
|
|
475
|
+
lastSyncedState = pureState;
|
|
66
476
|
};
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
if (typeof incoming !== "object" || incoming === null) {
|
|
477
|
+
const stateObserver = (events, transaction) => {
|
|
478
|
+
if (transaction.origin === localOrigin) {
|
|
70
479
|
return;
|
|
71
480
|
}
|
|
72
|
-
|
|
481
|
+
enqueueOperations(collectRemoteOperations(events, stateMap));
|
|
73
482
|
};
|
|
74
|
-
|
|
75
|
-
|
|
483
|
+
let stateMap;
|
|
484
|
+
const existingStateMap = getStateMap();
|
|
485
|
+
if (existingStateMap) {
|
|
486
|
+
stateMap = existingStateMap;
|
|
487
|
+
applyRemoteState(toPlainObject(stateMap));
|
|
76
488
|
} else {
|
|
77
|
-
|
|
489
|
+
const currentState = map.get(STATE_KEY);
|
|
490
|
+
if (isPlainObject(currentState)) {
|
|
491
|
+
stateMap = createYMap(currentState);
|
|
492
|
+
doc.transact(() => {
|
|
493
|
+
map.set(STATE_KEY, stateMap);
|
|
494
|
+
}, localOrigin);
|
|
495
|
+
applyRemoteState(currentState);
|
|
496
|
+
} else {
|
|
497
|
+
const pureState = clone(store.getPureState());
|
|
498
|
+
stateMap = createYMap(isPlainObject(pureState) ? pureState : {});
|
|
499
|
+
doc.transact(() => {
|
|
500
|
+
map.set(STATE_KEY, stateMap);
|
|
501
|
+
}, localOrigin);
|
|
502
|
+
}
|
|
78
503
|
}
|
|
504
|
+
stateMap.observeDeep(stateObserver);
|
|
79
505
|
const observer = (event) => {
|
|
80
|
-
if (event.transaction.origin ===
|
|
506
|
+
if (event.transaction.origin === localOrigin) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (!event.keysChanged.has(STATE_KEY)) {
|
|
81
510
|
return;
|
|
82
511
|
}
|
|
83
|
-
|
|
84
|
-
|
|
512
|
+
const nextStateMap = getStateMap();
|
|
513
|
+
if (nextStateMap) {
|
|
514
|
+
if (stateMap !== nextStateMap) {
|
|
515
|
+
stateMap.unobserveDeep(stateObserver);
|
|
516
|
+
stateMap = nextStateMap;
|
|
517
|
+
stateMap.observeDeep(stateObserver);
|
|
518
|
+
}
|
|
519
|
+
enqueueSnapshot(toPlainObject(nextStateMap));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const currentState = map.get(STATE_KEY);
|
|
523
|
+
if (isPlainObject(currentState)) {
|
|
524
|
+
const migrated = createYMap(currentState);
|
|
525
|
+
doc.transact(() => {
|
|
526
|
+
map.set(STATE_KEY, migrated);
|
|
527
|
+
}, localOrigin);
|
|
528
|
+
if (stateMap !== migrated) {
|
|
529
|
+
stateMap.unobserveDeep(stateObserver);
|
|
530
|
+
stateMap = migrated;
|
|
531
|
+
stateMap.observeDeep(stateObserver);
|
|
532
|
+
}
|
|
533
|
+
enqueueSnapshot(currentState);
|
|
85
534
|
}
|
|
86
535
|
};
|
|
87
536
|
map.observe(observer);
|
|
88
537
|
const unsubscribe = store.subscribe(() => {
|
|
89
538
|
syncNow();
|
|
90
539
|
});
|
|
91
|
-
|
|
540
|
+
const binding = {
|
|
92
541
|
doc,
|
|
93
542
|
map,
|
|
94
543
|
syncNow,
|
|
95
544
|
destroy: () => {
|
|
545
|
+
if (destroyed) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
destroyed = true;
|
|
96
549
|
unsubscribe();
|
|
97
550
|
map.unobserve(observer);
|
|
551
|
+
stateMap.unobserveDeep(stateObserver);
|
|
98
552
|
if (!options.doc) {
|
|
99
553
|
doc.destroy();
|
|
100
554
|
}
|
|
101
555
|
}
|
|
102
556
|
};
|
|
557
|
+
if (process.env.NODE_ENV === "test") {
|
|
558
|
+
binding.__unsafeTestOnly__ = {
|
|
559
|
+
applyRemoteOperations: (operations) => {
|
|
560
|
+
applyRemoteOperations(operations);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
return binding;
|
|
103
565
|
};
|
|
104
566
|
var yjs = (options = {}) => (store) => {
|
|
105
567
|
const binding = bindYjs(store, options);
|
|
@@ -115,6 +577,7 @@ var yjs = (options = {}) => (store) => {
|
|
|
115
577
|
__reExport(index_exports, src_exports, module.exports);
|
|
116
578
|
// Annotate the CommonJS export names for ESM import in node:
|
|
117
579
|
0 && (module.exports = {
|
|
580
|
+
__unsafeTestOnly__,
|
|
118
581
|
bindYjs,
|
|
119
582
|
yjs
|
|
120
583
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -19,6 +19,7 @@ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "defau
|
|
|
19
19
|
// index.ts
|
|
20
20
|
var index_exports = {};
|
|
21
21
|
__export(index_exports, {
|
|
22
|
+
__unsafeTestOnly__: () => __unsafeTestOnly__,
|
|
22
23
|
bindYjs: () => bindYjs,
|
|
23
24
|
yjs: () => yjs
|
|
24
25
|
});
|
|
@@ -26,19 +27,314 @@ __export(index_exports, {
|
|
|
26
27
|
// src/index.ts
|
|
27
28
|
var src_exports = {};
|
|
28
29
|
__export(src_exports, {
|
|
30
|
+
__unsafeTestOnly__: () => __unsafeTestOnly__,
|
|
29
31
|
bindYjs: () => bindYjs,
|
|
30
32
|
yjs: () => yjs
|
|
31
33
|
});
|
|
32
34
|
__reExport(src_exports, yjs_star);
|
|
33
35
|
import * as Y from "yjs";
|
|
34
36
|
import * as yjs_star from "yjs";
|
|
35
|
-
var
|
|
37
|
+
var STATE_KEY = "state";
|
|
36
38
|
function clone(value) {
|
|
37
39
|
if (typeof structuredClone === "function") {
|
|
38
40
|
return structuredClone(value);
|
|
39
41
|
}
|
|
40
42
|
return JSON.parse(JSON.stringify(value));
|
|
41
43
|
}
|
|
44
|
+
function isPlainObject(value) {
|
|
45
|
+
if (typeof value !== "object" || value === null) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const prototype = Object.getPrototypeOf(value);
|
|
49
|
+
return prototype === Object.prototype || prototype === null;
|
|
50
|
+
}
|
|
51
|
+
function toPlainObject(value) {
|
|
52
|
+
const next = {};
|
|
53
|
+
value.forEach((item, key) => {
|
|
54
|
+
next[key] = toPlainValue(item);
|
|
55
|
+
});
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
function toPlainArray(value) {
|
|
59
|
+
return value.toArray().map((item) => toPlainValue(item));
|
|
60
|
+
}
|
|
61
|
+
function toPlainValue(value) {
|
|
62
|
+
if (value instanceof Y.Map) {
|
|
63
|
+
return toPlainObject(value);
|
|
64
|
+
}
|
|
65
|
+
if (value instanceof Y.Array) {
|
|
66
|
+
return toPlainArray(value);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function createYMap(value) {
|
|
71
|
+
const next = new Y.Map();
|
|
72
|
+
for (const [key, item] of Object.entries(value)) {
|
|
73
|
+
next.set(key, toYValue(item));
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
function createYArray(value) {
|
|
78
|
+
const next = new Y.Array();
|
|
79
|
+
if (value.length > 0) {
|
|
80
|
+
next.insert(
|
|
81
|
+
0,
|
|
82
|
+
value.map((item) => toYValue(item))
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
function toYValue(value) {
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
return createYArray(value);
|
|
90
|
+
}
|
|
91
|
+
if (isPlainObject(value)) {
|
|
92
|
+
return createYMap(value);
|
|
93
|
+
}
|
|
94
|
+
if (typeof value === "object" && value !== null) {
|
|
95
|
+
return clone(value);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function isEqualValue(left, right) {
|
|
100
|
+
if (Object.is(left, right)) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
104
|
+
if (left.length !== right.length) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
108
|
+
if (!isEqualValue(left[index], right[index])) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
115
|
+
const leftKeys = Object.keys(left);
|
|
116
|
+
const rightKeys = Object.keys(right);
|
|
117
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
for (const key of leftKeys) {
|
|
121
|
+
if (!Object.prototype.hasOwnProperty.call(right, key)) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (!isEqualValue(left[key], right[key])) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
function syncObjectToYMap(target, previous, source) {
|
|
133
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(source)]);
|
|
134
|
+
for (const key of keys) {
|
|
135
|
+
const hasPrevious = Object.prototype.hasOwnProperty.call(previous, key);
|
|
136
|
+
const hasNext = Object.prototype.hasOwnProperty.call(source, key);
|
|
137
|
+
if (!hasNext) {
|
|
138
|
+
target.delete(key);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const previousValue = hasPrevious ? previous[key] : void 0;
|
|
142
|
+
const nextValue = source[key];
|
|
143
|
+
if (hasPrevious && isEqualValue(previousValue, nextValue)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const current = target.get(key);
|
|
147
|
+
if (Array.isArray(nextValue)) {
|
|
148
|
+
if (Array.isArray(previousValue) && current instanceof Y.Array) {
|
|
149
|
+
syncArrayToYArray(current, previousValue, nextValue);
|
|
150
|
+
} else {
|
|
151
|
+
target.set(key, createYArray(nextValue));
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (isPlainObject(nextValue)) {
|
|
156
|
+
if (isPlainObject(previousValue) && current instanceof Y.Map) {
|
|
157
|
+
syncObjectToYMap(current, previousValue, nextValue);
|
|
158
|
+
} else {
|
|
159
|
+
target.set(key, createYMap(nextValue));
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const normalized = typeof nextValue === "object" && nextValue !== null ? clone(nextValue) : nextValue;
|
|
164
|
+
if (!Object.is(current, normalized)) {
|
|
165
|
+
target.set(key, normalized);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function syncArrayToYArray(target, previous, source) {
|
|
170
|
+
if (target.length > source.length) {
|
|
171
|
+
target.delete(source.length, target.length - source.length);
|
|
172
|
+
}
|
|
173
|
+
const maxLength = Math.max(previous.length, source.length);
|
|
174
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
175
|
+
const hasPrevious = index < previous.length;
|
|
176
|
+
const hasNext = index < source.length;
|
|
177
|
+
if (!hasNext) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const previousValue = hasPrevious ? previous[index] : void 0;
|
|
181
|
+
const nextValue = source[index];
|
|
182
|
+
if (hasPrevious && isEqualValue(previousValue, nextValue)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (index >= target.length) {
|
|
186
|
+
target.insert(index, [toYValue(nextValue)]);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const current = target.get(index);
|
|
190
|
+
if (Array.isArray(nextValue)) {
|
|
191
|
+
if (Array.isArray(previousValue) && current instanceof Y.Array) {
|
|
192
|
+
syncArrayToYArray(current, previousValue, nextValue);
|
|
193
|
+
} else {
|
|
194
|
+
target.delete(index, 1);
|
|
195
|
+
target.insert(index, [createYArray(nextValue)]);
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (isPlainObject(nextValue)) {
|
|
200
|
+
if (isPlainObject(previousValue) && current instanceof Y.Map) {
|
|
201
|
+
syncObjectToYMap(current, previousValue, nextValue);
|
|
202
|
+
} else {
|
|
203
|
+
target.delete(index, 1);
|
|
204
|
+
target.insert(index, [createYMap(nextValue)]);
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const normalized = typeof nextValue === "object" && nextValue !== null ? clone(nextValue) : nextValue;
|
|
209
|
+
if (!Object.is(current, normalized)) {
|
|
210
|
+
target.delete(index, 1);
|
|
211
|
+
target.insert(index, [normalized]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function isSetStateReentryError(error) {
|
|
216
|
+
return error instanceof Error && error.message === "setState cannot be called within the updater";
|
|
217
|
+
}
|
|
218
|
+
function cloneForStore(value) {
|
|
219
|
+
if (typeof value === "object" && value !== null) {
|
|
220
|
+
return clone(value);
|
|
221
|
+
}
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
function toPathKey(path) {
|
|
225
|
+
return path.map((segment) => `${typeof segment}:${String(segment)}`).join("|");
|
|
226
|
+
}
|
|
227
|
+
function compactOperations(operations) {
|
|
228
|
+
const deduplicated = /* @__PURE__ */ new Map();
|
|
229
|
+
for (const operation of operations) {
|
|
230
|
+
const key = toPathKey(operation.path);
|
|
231
|
+
if (deduplicated.has(key)) {
|
|
232
|
+
deduplicated.delete(key);
|
|
233
|
+
}
|
|
234
|
+
deduplicated.set(key, operation);
|
|
235
|
+
}
|
|
236
|
+
return Array.from(deduplicated.values()).sort(
|
|
237
|
+
(left, right) => left.path.length - right.path.length
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
function getYValueAtPath(root, path) {
|
|
241
|
+
let current = root;
|
|
242
|
+
for (const segment of path) {
|
|
243
|
+
if (current instanceof Y.Map) {
|
|
244
|
+
current = current.get(String(segment));
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (current instanceof Y.Array) {
|
|
248
|
+
const index = typeof segment === "number" ? segment : Number.parseInt(segment, 10);
|
|
249
|
+
if (!Number.isInteger(index)) {
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
current = current.get(index);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
return void 0;
|
|
256
|
+
}
|
|
257
|
+
return current;
|
|
258
|
+
}
|
|
259
|
+
function setAtPath(target, path, value) {
|
|
260
|
+
if (path.length === 0) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
let current = target;
|
|
264
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
265
|
+
const segment = path[index];
|
|
266
|
+
const nextSegment = path[index + 1];
|
|
267
|
+
const nextValue = current[segment];
|
|
268
|
+
if (typeof nextValue !== "object" || nextValue === null) {
|
|
269
|
+
current[segment] = typeof nextSegment === "number" ? [] : {};
|
|
270
|
+
}
|
|
271
|
+
current = current[segment];
|
|
272
|
+
}
|
|
273
|
+
const leaf = path[path.length - 1];
|
|
274
|
+
current[leaf] = cloneForStore(value);
|
|
275
|
+
}
|
|
276
|
+
function deleteAtPath(target, path) {
|
|
277
|
+
if (path.length === 0) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
let current = target;
|
|
281
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
282
|
+
current = current[path[index]];
|
|
283
|
+
if (typeof current !== "object" || current === null) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const leaf = path[path.length - 1];
|
|
288
|
+
if (Array.isArray(current) && typeof leaf === "number") {
|
|
289
|
+
if (leaf >= 0 && leaf < current.length) {
|
|
290
|
+
current.splice(leaf, 1);
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
delete current[leaf];
|
|
295
|
+
}
|
|
296
|
+
function collectRemoteOperations(events, stateMap) {
|
|
297
|
+
const operations = [];
|
|
298
|
+
for (const event of events) {
|
|
299
|
+
if (event instanceof Y.YMapEvent) {
|
|
300
|
+
for (const changedKey of event.keysChanged) {
|
|
301
|
+
const path = [...event.path, changedKey];
|
|
302
|
+
const keyChange = event.changes.keys.get(changedKey);
|
|
303
|
+
if (keyChange?.action === "delete") {
|
|
304
|
+
operations.push({
|
|
305
|
+
type: "delete",
|
|
306
|
+
path
|
|
307
|
+
});
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
operations.push({
|
|
311
|
+
type: "set",
|
|
312
|
+
path,
|
|
313
|
+
value: toPlainValue(getYValueAtPath(stateMap, path))
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (event instanceof Y.YArrayEvent) {
|
|
319
|
+
const path = [...event.path];
|
|
320
|
+
operations.push({
|
|
321
|
+
type: "set",
|
|
322
|
+
path,
|
|
323
|
+
value: toPlainValue(getYValueAtPath(stateMap, path))
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return operations;
|
|
328
|
+
}
|
|
329
|
+
var __unsafeTestOnly__ = {
|
|
330
|
+
getYValueAtPath: (root, path) => getYValueAtPath(root, path),
|
|
331
|
+
setAtPath: (target, path, value) => {
|
|
332
|
+
setAtPath(target, path, value);
|
|
333
|
+
},
|
|
334
|
+
deleteAtPath: (target, path) => {
|
|
335
|
+
deleteAtPath(target, path);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
42
338
|
var bindYjs = (store, options = {}) => {
|
|
43
339
|
if (store.share === "client") {
|
|
44
340
|
throw new Error("Yjs binding is not supported in client store mode.");
|
|
@@ -46,48 +342,214 @@ var bindYjs = (store, options = {}) => {
|
|
|
46
342
|
const doc = options.doc ?? new Y.Doc();
|
|
47
343
|
const key = options.key ?? `coaction:${store.name}`;
|
|
48
344
|
const map = doc.getMap(key);
|
|
345
|
+
const localOrigin = /* @__PURE__ */ Symbol(`coaction-yjs:${store.name}`);
|
|
346
|
+
let destroyed = false;
|
|
347
|
+
let syncingFromYjs = false;
|
|
348
|
+
let lastSyncedState = (() => {
|
|
349
|
+
const pureState = clone(store.getPureState());
|
|
350
|
+
return isPlainObject(pureState) ? pureState : {};
|
|
351
|
+
})();
|
|
352
|
+
let flushScheduled = false;
|
|
353
|
+
let pendingSnapshot = null;
|
|
354
|
+
let pendingOperations = [];
|
|
355
|
+
const applyRemoteState = (state) => {
|
|
356
|
+
const next = clone(state);
|
|
357
|
+
syncingFromYjs = true;
|
|
358
|
+
try {
|
|
359
|
+
store.setState(next);
|
|
360
|
+
const pureState = clone(store.getPureState());
|
|
361
|
+
lastSyncedState = isPlainObject(pureState) ? pureState : {};
|
|
362
|
+
} finally {
|
|
363
|
+
syncingFromYjs = false;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const applyRemoteOperations = (operations) => {
|
|
367
|
+
if (operations.length === 0) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
syncingFromYjs = true;
|
|
371
|
+
try {
|
|
372
|
+
store.setState((draft) => {
|
|
373
|
+
const mutableDraft = draft;
|
|
374
|
+
for (const operation of operations) {
|
|
375
|
+
if (operation.type === "set") {
|
|
376
|
+
setAtPath(mutableDraft, operation.path, operation.value);
|
|
377
|
+
} else {
|
|
378
|
+
deleteAtPath(mutableDraft, operation.path);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
const pureState = clone(store.getPureState());
|
|
383
|
+
lastSyncedState = isPlainObject(pureState) ? pureState : {};
|
|
384
|
+
} finally {
|
|
385
|
+
syncingFromYjs = false;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
const getStateMap = () => {
|
|
389
|
+
const state = map.get(STATE_KEY);
|
|
390
|
+
if (state instanceof Y.Map) {
|
|
391
|
+
return state;
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
};
|
|
395
|
+
const scheduleFlushFromYjs = () => {
|
|
396
|
+
if (destroyed || flushScheduled) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
flushScheduled = true;
|
|
400
|
+
queueMicrotask(flushFromYjs);
|
|
401
|
+
};
|
|
402
|
+
const flushFromYjs = () => {
|
|
403
|
+
flushScheduled = false;
|
|
404
|
+
if (destroyed) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (pendingSnapshot) {
|
|
408
|
+
const snapshot = pendingSnapshot;
|
|
409
|
+
pendingSnapshot = null;
|
|
410
|
+
pendingOperations = [];
|
|
411
|
+
try {
|
|
412
|
+
applyRemoteState(snapshot);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (isSetStateReentryError(error)) {
|
|
415
|
+
pendingSnapshot = snapshot;
|
|
416
|
+
setTimeout(scheduleFlushFromYjs, 0);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (pendingOperations.length === 0) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const operations = compactOperations(pendingOperations);
|
|
426
|
+
pendingOperations = [];
|
|
427
|
+
try {
|
|
428
|
+
applyRemoteOperations(operations);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
if (isSetStateReentryError(error)) {
|
|
431
|
+
pendingOperations = [...operations, ...pendingOperations];
|
|
432
|
+
setTimeout(scheduleFlushFromYjs, 0);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const enqueueSnapshot = (snapshot) => {
|
|
439
|
+
pendingSnapshot = snapshot;
|
|
440
|
+
pendingOperations = [];
|
|
441
|
+
scheduleFlushFromYjs();
|
|
442
|
+
};
|
|
443
|
+
const enqueueOperations = (operations) => {
|
|
444
|
+
if (operations.length === 0) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!pendingSnapshot) {
|
|
448
|
+
pendingOperations.push(...operations);
|
|
449
|
+
}
|
|
450
|
+
scheduleFlushFromYjs();
|
|
451
|
+
};
|
|
49
452
|
const syncNow = () => {
|
|
453
|
+
if (destroyed || syncingFromYjs) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
50
456
|
const pureState = clone(store.getPureState());
|
|
457
|
+
if (!isPlainObject(pureState)) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
51
460
|
doc.transact(() => {
|
|
52
|
-
|
|
53
|
-
},
|
|
461
|
+
syncObjectToYMap(stateMap, lastSyncedState, pureState);
|
|
462
|
+
}, localOrigin);
|
|
463
|
+
lastSyncedState = pureState;
|
|
54
464
|
};
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
if (typeof incoming !== "object" || incoming === null) {
|
|
465
|
+
const stateObserver = (events, transaction) => {
|
|
466
|
+
if (transaction.origin === localOrigin) {
|
|
58
467
|
return;
|
|
59
468
|
}
|
|
60
|
-
|
|
469
|
+
enqueueOperations(collectRemoteOperations(events, stateMap));
|
|
61
470
|
};
|
|
62
|
-
|
|
63
|
-
|
|
471
|
+
let stateMap;
|
|
472
|
+
const existingStateMap = getStateMap();
|
|
473
|
+
if (existingStateMap) {
|
|
474
|
+
stateMap = existingStateMap;
|
|
475
|
+
applyRemoteState(toPlainObject(stateMap));
|
|
64
476
|
} else {
|
|
65
|
-
|
|
477
|
+
const currentState = map.get(STATE_KEY);
|
|
478
|
+
if (isPlainObject(currentState)) {
|
|
479
|
+
stateMap = createYMap(currentState);
|
|
480
|
+
doc.transact(() => {
|
|
481
|
+
map.set(STATE_KEY, stateMap);
|
|
482
|
+
}, localOrigin);
|
|
483
|
+
applyRemoteState(currentState);
|
|
484
|
+
} else {
|
|
485
|
+
const pureState = clone(store.getPureState());
|
|
486
|
+
stateMap = createYMap(isPlainObject(pureState) ? pureState : {});
|
|
487
|
+
doc.transact(() => {
|
|
488
|
+
map.set(STATE_KEY, stateMap);
|
|
489
|
+
}, localOrigin);
|
|
490
|
+
}
|
|
66
491
|
}
|
|
492
|
+
stateMap.observeDeep(stateObserver);
|
|
67
493
|
const observer = (event) => {
|
|
68
|
-
if (event.transaction.origin ===
|
|
494
|
+
if (event.transaction.origin === localOrigin) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (!event.keysChanged.has(STATE_KEY)) {
|
|
69
498
|
return;
|
|
70
499
|
}
|
|
71
|
-
|
|
72
|
-
|
|
500
|
+
const nextStateMap = getStateMap();
|
|
501
|
+
if (nextStateMap) {
|
|
502
|
+
if (stateMap !== nextStateMap) {
|
|
503
|
+
stateMap.unobserveDeep(stateObserver);
|
|
504
|
+
stateMap = nextStateMap;
|
|
505
|
+
stateMap.observeDeep(stateObserver);
|
|
506
|
+
}
|
|
507
|
+
enqueueSnapshot(toPlainObject(nextStateMap));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const currentState = map.get(STATE_KEY);
|
|
511
|
+
if (isPlainObject(currentState)) {
|
|
512
|
+
const migrated = createYMap(currentState);
|
|
513
|
+
doc.transact(() => {
|
|
514
|
+
map.set(STATE_KEY, migrated);
|
|
515
|
+
}, localOrigin);
|
|
516
|
+
if (stateMap !== migrated) {
|
|
517
|
+
stateMap.unobserveDeep(stateObserver);
|
|
518
|
+
stateMap = migrated;
|
|
519
|
+
stateMap.observeDeep(stateObserver);
|
|
520
|
+
}
|
|
521
|
+
enqueueSnapshot(currentState);
|
|
73
522
|
}
|
|
74
523
|
};
|
|
75
524
|
map.observe(observer);
|
|
76
525
|
const unsubscribe = store.subscribe(() => {
|
|
77
526
|
syncNow();
|
|
78
527
|
});
|
|
79
|
-
|
|
528
|
+
const binding = {
|
|
80
529
|
doc,
|
|
81
530
|
map,
|
|
82
531
|
syncNow,
|
|
83
532
|
destroy: () => {
|
|
533
|
+
if (destroyed) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
destroyed = true;
|
|
84
537
|
unsubscribe();
|
|
85
538
|
map.unobserve(observer);
|
|
539
|
+
stateMap.unobserveDeep(stateObserver);
|
|
86
540
|
if (!options.doc) {
|
|
87
541
|
doc.destroy();
|
|
88
542
|
}
|
|
89
543
|
}
|
|
90
544
|
};
|
|
545
|
+
if (process.env.NODE_ENV === "test") {
|
|
546
|
+
binding.__unsafeTestOnly__ = {
|
|
547
|
+
applyRemoteOperations: (operations) => {
|
|
548
|
+
applyRemoteOperations(operations);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return binding;
|
|
91
553
|
};
|
|
92
554
|
var yjs = (options = {}) => (store) => {
|
|
93
555
|
const binding = bindYjs(store, options);
|
|
@@ -102,6 +564,7 @@ var yjs = (options = {}) => (store) => {
|
|
|
102
564
|
// index.ts
|
|
103
565
|
__reExport(index_exports, src_exports);
|
|
104
566
|
export {
|
|
567
|
+
__unsafeTestOnly__,
|
|
105
568
|
bindYjs,
|
|
106
569
|
yjs
|
|
107
570
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coaction/yjs",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A Coaction integration tool for Yjs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"state",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"url": "https://github.com/unadlib/coaction/issues"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"coaction": "^1.0
|
|
41
|
+
"coaction": "^1.1.0",
|
|
42
42
|
"yjs": "^13.6.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependenciesMeta": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"coaction": "^1.0
|
|
53
|
+
"coaction": "^1.1.0",
|
|
54
54
|
"yjs": "^13.6.27"
|
|
55
55
|
},
|
|
56
56
|
"publishConfig": {
|