@coaction/yjs 2.0.0 → 3.0.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 CHANGED
@@ -1,15 +1,15 @@
1
1
  # @coaction/yjs
2
2
 
3
- ![Node CI](https://github.com/coactionjs/coaction/workflows/Node%20CI/badge.svg)
4
- [![npm](https://img.shields.io/npm/v/@coaction/yjs.svg)](https://www.npmjs.com/package/@coaction/yjs)
5
- ![license](https://img.shields.io/npm/l/@coaction/yjs)
3
+ ![Node CI](https://github.com/coactionjs/coaction/workflows/Node%20CI/badge.svg) [![npm](https://img.shields.io/npm/v/@coaction/yjs.svg)](https://www.npmjs.com/package/@coaction/yjs) ![license](https://img.shields.io/npm/l/@coaction/yjs)
4
+
5
+ [English documentation](https://coactionjs.github.io/coaction/en/docs/) · [中文文档](https://coactionjs.github.io/coaction/zh/docs/)
6
6
 
7
7
  A Coaction integration tool for Yjs.
8
8
 
9
9
  ## Installation
10
10
 
11
11
  ```sh
12
- npm install coaction @coaction/yjs
12
+ pnpm add coaction @coaction/yjs
13
13
  ```
14
14
 
15
15
  ## Quick Start
@@ -112,6 +112,9 @@ Middleware wrapper around `bindYjs`. It also wires cleanup into `store.destroy()
112
112
  - Keep the synced state plain and serializable.
113
113
  - Methods and getters are Coaction runtime behavior and are not the synchronization payload.
114
114
  - Prefer a plain object at the root of your store state.
115
+ - Remote snapshots and delete operations obey Coaction's fixed schema rules:
116
+ unknown root keys are rejected, while known single-store root keys may be
117
+ absent and then read as `undefined` from the public state object.
115
118
 
116
119
  ## Compatibility and Limits
117
120
 
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { Middleware, Store } from "coaction";
1
+ import { Middleware, Store } from "coaction/adapter";
2
2
  import * as Y from "yjs";
3
3
  export * from "yjs";
4
4
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Middleware, Store } from "coaction";
1
+ import { Middleware, Store } from "coaction/adapter";
2
2
  import * as Y from "yjs";
3
3
  export * from "yjs";
4
4
 
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- let coaction = require("coaction");
24
+ let coaction_adapter = require("coaction/adapter");
25
25
  let yjs = require("yjs");
26
26
  yjs = __toESM(yjs);
27
27
  //#region packages/coaction-yjs/src/shared.ts
@@ -145,6 +145,24 @@ function toArrayIndex(segment) {
145
145
  if (typeof segment === "string" && String(index) !== segment) return;
146
146
  return index;
147
147
  }
148
+ function assertCanSetPathSegment(target, key, path) {
149
+ if (Object.prototype.hasOwnProperty.call(target, key)) return;
150
+ if (Object.isExtensible(target)) return;
151
+ throw new coaction_adapter.StateSchemaError(`Unknown state key '${path.map((segment) => String(segment)).join(".")}' cannot be added after store initialization. Coaction state schema is fixed.`);
152
+ }
153
+ function clearObjectKey(target, key) {
154
+ if (!Object.prototype.hasOwnProperty.call(target, key)) return;
155
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
156
+ if (descriptor?.configurable) {
157
+ delete target[key];
158
+ return;
159
+ }
160
+ if (descriptor && "set" in descriptor && descriptor.set) {
161
+ target[key] = void 0;
162
+ return;
163
+ }
164
+ if (descriptor && "writable" in descriptor && descriptor.writable) target[key] = void 0;
165
+ }
148
166
  function compactOperations(operations) {
149
167
  const deduplicated = /* @__PURE__ */ new Map();
150
168
  for (const operation of operations) {
@@ -182,12 +200,17 @@ function setAtPath(target, path, value) {
182
200
  const targetKey = typeof segmentIndex === "undefined" ? segment : segmentIndex;
183
201
  const nextValue = current[targetKey];
184
202
  const needsArray = typeof nextSegment === "number" || Array.isArray(nextValue) && typeof toArrayIndex(nextSegment) !== "undefined";
185
- if (typeof nextValue !== "object" || nextValue === null || (needsArray ? !Array.isArray(nextValue) : Array.isArray(nextValue))) current[targetKey] = typeof nextSegment === "number" ? [] : {};
203
+ if (typeof nextValue !== "object" || nextValue === null || (needsArray ? !Array.isArray(nextValue) : Array.isArray(nextValue))) {
204
+ assertCanSetPathSegment(current, targetKey, path.slice(0, index + 1));
205
+ current[targetKey] = typeof nextSegment === "number" ? [] : {};
206
+ }
186
207
  current = current[targetKey];
187
208
  }
188
209
  const leaf = path[path.length - 1];
189
210
  const leafIndex = Array.isArray(current) ? toArrayIndex(leaf) : void 0;
190
- current[typeof leafIndex === "undefined" ? leaf : leafIndex] = cloneForStore(value);
211
+ const leafKey = typeof leafIndex === "undefined" ? leaf : leafIndex;
212
+ assertCanSetPathSegment(current, leafKey, path);
213
+ current[leafKey] = cloneForStore(value);
191
214
  }
192
215
  function deleteAtPath(target, path) {
193
216
  if (path.length === 0) return;
@@ -205,7 +228,7 @@ function deleteAtPath(target, path) {
205
228
  if (typeof leafIndex !== "undefined" && leafIndex >= 0 && leafIndex < current.length) current.splice(leafIndex, 1);
206
229
  return;
207
230
  }
208
- delete current[leaf];
231
+ clearObjectKey(current, leaf);
209
232
  }
210
233
  function collectRemoteOperations(events, stateMap) {
211
234
  const operations = [];
@@ -414,53 +437,6 @@ const assertYjsSerializableState = (state, path = []) => {
414
437
  const assertRemoteOperationsSerializable = (operations) => {
415
438
  for (const operation of operations) if (operation.type === "set") assertYjsSerializableState(operation.value, operation.path);
416
439
  };
417
- const createRootReplacementPatches = (currentState, nextState) => {
418
- const patches = [];
419
- const inversePatches = [];
420
- const nextKeys = new Set(getOwnEnumerableKeys(nextState));
421
- for (const key of getOwnEnumerableKeys(currentState)) {
422
- if (typeof key === "symbol" || nextKeys.has(key)) continue;
423
- patches.push({
424
- op: "remove",
425
- path: [key]
426
- });
427
- inversePatches.push({
428
- op: "add",
429
- path: [key],
430
- value: currentState[key]
431
- });
432
- }
433
- for (const key of nextKeys) {
434
- if (typeof key === "symbol") continue;
435
- if (!Object.prototype.hasOwnProperty.call(currentState, key)) {
436
- patches.push({
437
- op: "add",
438
- path: [key],
439
- value: nextState[key]
440
- });
441
- inversePatches.push({
442
- op: "remove",
443
- path: [key]
444
- });
445
- continue;
446
- }
447
- if (Object.is(currentState[key], nextState[key])) continue;
448
- patches.push({
449
- op: "replace",
450
- path: [key],
451
- value: nextState[key]
452
- });
453
- inversePatches.push({
454
- op: "replace",
455
- path: [key],
456
- value: currentState[key]
457
- });
458
- }
459
- return {
460
- patches,
461
- inversePatches
462
- };
463
- };
464
440
  const __unsafeTestOnly__ = {
465
441
  getYValueAtPath: (root, path) => getYValueAtPath(root, path),
466
442
  setAtPath: (target, path, value) => {
@@ -494,31 +470,22 @@ const bindYjs = (store, options = {}) => {
494
470
  syncingFromYjs = false;
495
471
  }
496
472
  };
473
+ const applyReplacementState = (next) => {
474
+ if (store.share === "main") {
475
+ store.setState(next, () => {
476
+ return (0, coaction_adapter.applyRootReplacementWithPatches)(store, next);
477
+ });
478
+ return;
479
+ }
480
+ store.setState(null);
481
+ store.apply(next);
482
+ };
497
483
  const applyRemoteState = (state) => {
498
484
  assertYjsSerializableState(state);
499
485
  const next = sanitizePlainValue(state);
500
486
  syncingFromYjs = true;
501
487
  try {
502
- if (store.share === "main") store.setState(next, () => {
503
- const { patches, inversePatches } = createRootReplacementPatches(store.getPureState(), next);
504
- const finalPatches = store.patch ? store.patch({
505
- patches,
506
- inversePatches
507
- }) : {
508
- patches,
509
- inversePatches
510
- };
511
- if (finalPatches.patches.length) store.apply(store.getPureState(), finalPatches.patches);
512
- return [
513
- store.getPureState(),
514
- finalPatches.patches,
515
- finalPatches.inversePatches
516
- ];
517
- });
518
- else {
519
- store.setState(null);
520
- store.apply(next);
521
- }
488
+ applyReplacementState(next);
522
489
  const pureState = clone(store.getPureState());
523
490
  lastSyncedState = isPlainObject(pureState) ? pureState : {};
524
491
  } finally {
@@ -530,6 +497,16 @@ const bindYjs = (store, options = {}) => {
530
497
  assertRemoteOperationsSerializable(operations);
531
498
  syncingFromYjs = true;
532
499
  try {
500
+ if (operations.some((operation) => operation.type === "delete" && operation.path.length === 1)) {
501
+ const next = clone(store.getPureState());
502
+ if (!isPlainObject(next)) return;
503
+ for (const operation of operations) if (operation.type === "set") setAtPath(next, operation.path, operation.value);
504
+ else deleteAtPath(next, operation.path);
505
+ applyReplacementState(next);
506
+ const pureState = clone(store.getPureState());
507
+ lastSyncedState = isPlainObject(pureState) ? pureState : {};
508
+ return;
509
+ }
533
510
  store.setState((draft) => {
534
511
  const mutableDraft = draft;
535
512
  for (const operation of operations) if (operation.type === "set") setAtPath(mutableDraft, operation.path, operation.value);
@@ -566,7 +543,7 @@ const bindYjs = (store, options = {}) => {
566
543
  setTimeout(scheduleFlushFromYjs, 0);
567
544
  return;
568
545
  }
569
- if (isYjsSerializableStateError(error)) {
546
+ if (isYjsSerializableStateError(error) || (0, coaction_adapter.isStateSchemaError)(error)) {
570
547
  restoreRootState();
571
548
  return;
572
549
  }
@@ -584,7 +561,7 @@ const bindYjs = (store, options = {}) => {
584
561
  setTimeout(scheduleFlushFromYjs, 0);
585
562
  return;
586
563
  }
587
- if (isYjsSerializableStateError(error)) {
564
+ if (isYjsSerializableStateError(error) || (0, coaction_adapter.isStateSchemaError)(error)) {
588
565
  restoreRootState();
589
566
  return;
590
567
  }
@@ -657,7 +634,7 @@ const bindYjs = (store, options = {}) => {
657
634
  try {
658
635
  applyRemoteState(state);
659
636
  } catch (error) {
660
- if (isYjsSerializableStateError(error)) {
637
+ if (isYjsSerializableStateError(error) || (0, coaction_adapter.isStateSchemaError)(error)) {
661
638
  restoreRootState();
662
639
  return;
663
640
  }
@@ -731,7 +708,7 @@ const bindYjs = (store, options = {}) => {
731
708
  };
732
709
  const yjs$1 = (options = {}) => (store) => {
733
710
  let binding;
734
- const cancelBinding = (0, coaction.onStoreReady)(store, () => {
711
+ const cancelBinding = (0, coaction_adapter.onStoreReady)(store, () => {
735
712
  binding = bindYjs(store, options);
736
713
  });
737
714
  const baseDestroy = store.destroy;
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { onStoreReady } from "coaction";
1
+ import { StateSchemaError, applyRootReplacementWithPatches, isStateSchemaError, onStoreReady } from "coaction/adapter";
2
2
  import * as Y from "yjs";
3
3
  export * from "yjs";
4
4
  //#endregion
@@ -123,6 +123,24 @@ function toArrayIndex(segment) {
123
123
  if (typeof segment === "string" && String(index) !== segment) return;
124
124
  return index;
125
125
  }
126
+ function assertCanSetPathSegment(target, key, path) {
127
+ if (Object.prototype.hasOwnProperty.call(target, key)) return;
128
+ if (Object.isExtensible(target)) return;
129
+ throw new StateSchemaError(`Unknown state key '${path.map((segment) => String(segment)).join(".")}' cannot be added after store initialization. Coaction state schema is fixed.`);
130
+ }
131
+ function clearObjectKey(target, key) {
132
+ if (!Object.prototype.hasOwnProperty.call(target, key)) return;
133
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
134
+ if (descriptor?.configurable) {
135
+ delete target[key];
136
+ return;
137
+ }
138
+ if (descriptor && "set" in descriptor && descriptor.set) {
139
+ target[key] = void 0;
140
+ return;
141
+ }
142
+ if (descriptor && "writable" in descriptor && descriptor.writable) target[key] = void 0;
143
+ }
126
144
  function compactOperations(operations) {
127
145
  const deduplicated = /* @__PURE__ */ new Map();
128
146
  for (const operation of operations) {
@@ -160,12 +178,17 @@ function setAtPath(target, path, value) {
160
178
  const targetKey = typeof segmentIndex === "undefined" ? segment : segmentIndex;
161
179
  const nextValue = current[targetKey];
162
180
  const needsArray = typeof nextSegment === "number" || Array.isArray(nextValue) && typeof toArrayIndex(nextSegment) !== "undefined";
163
- if (typeof nextValue !== "object" || nextValue === null || (needsArray ? !Array.isArray(nextValue) : Array.isArray(nextValue))) current[targetKey] = typeof nextSegment === "number" ? [] : {};
181
+ if (typeof nextValue !== "object" || nextValue === null || (needsArray ? !Array.isArray(nextValue) : Array.isArray(nextValue))) {
182
+ assertCanSetPathSegment(current, targetKey, path.slice(0, index + 1));
183
+ current[targetKey] = typeof nextSegment === "number" ? [] : {};
184
+ }
164
185
  current = current[targetKey];
165
186
  }
166
187
  const leaf = path[path.length - 1];
167
188
  const leafIndex = Array.isArray(current) ? toArrayIndex(leaf) : void 0;
168
- current[typeof leafIndex === "undefined" ? leaf : leafIndex] = cloneForStore(value);
189
+ const leafKey = typeof leafIndex === "undefined" ? leaf : leafIndex;
190
+ assertCanSetPathSegment(current, leafKey, path);
191
+ current[leafKey] = cloneForStore(value);
169
192
  }
170
193
  function deleteAtPath(target, path) {
171
194
  if (path.length === 0) return;
@@ -183,7 +206,7 @@ function deleteAtPath(target, path) {
183
206
  if (typeof leafIndex !== "undefined" && leafIndex >= 0 && leafIndex < current.length) current.splice(leafIndex, 1);
184
207
  return;
185
208
  }
186
- delete current[leaf];
209
+ clearObjectKey(current, leaf);
187
210
  }
188
211
  function collectRemoteOperations(events, stateMap) {
189
212
  const operations = [];
@@ -392,53 +415,6 @@ const assertYjsSerializableState = (state, path = []) => {
392
415
  const assertRemoteOperationsSerializable = (operations) => {
393
416
  for (const operation of operations) if (operation.type === "set") assertYjsSerializableState(operation.value, operation.path);
394
417
  };
395
- const createRootReplacementPatches = (currentState, nextState) => {
396
- const patches = [];
397
- const inversePatches = [];
398
- const nextKeys = new Set(getOwnEnumerableKeys(nextState));
399
- for (const key of getOwnEnumerableKeys(currentState)) {
400
- if (typeof key === "symbol" || nextKeys.has(key)) continue;
401
- patches.push({
402
- op: "remove",
403
- path: [key]
404
- });
405
- inversePatches.push({
406
- op: "add",
407
- path: [key],
408
- value: currentState[key]
409
- });
410
- }
411
- for (const key of nextKeys) {
412
- if (typeof key === "symbol") continue;
413
- if (!Object.prototype.hasOwnProperty.call(currentState, key)) {
414
- patches.push({
415
- op: "add",
416
- path: [key],
417
- value: nextState[key]
418
- });
419
- inversePatches.push({
420
- op: "remove",
421
- path: [key]
422
- });
423
- continue;
424
- }
425
- if (Object.is(currentState[key], nextState[key])) continue;
426
- patches.push({
427
- op: "replace",
428
- path: [key],
429
- value: nextState[key]
430
- });
431
- inversePatches.push({
432
- op: "replace",
433
- path: [key],
434
- value: currentState[key]
435
- });
436
- }
437
- return {
438
- patches,
439
- inversePatches
440
- };
441
- };
442
418
  const __unsafeTestOnly__ = {
443
419
  getYValueAtPath: (root, path) => getYValueAtPath(root, path),
444
420
  setAtPath: (target, path, value) => {
@@ -472,31 +448,22 @@ const bindYjs = (store, options = {}) => {
472
448
  syncingFromYjs = false;
473
449
  }
474
450
  };
451
+ const applyReplacementState = (next) => {
452
+ if (store.share === "main") {
453
+ store.setState(next, () => {
454
+ return applyRootReplacementWithPatches(store, next);
455
+ });
456
+ return;
457
+ }
458
+ store.setState(null);
459
+ store.apply(next);
460
+ };
475
461
  const applyRemoteState = (state) => {
476
462
  assertYjsSerializableState(state);
477
463
  const next = sanitizePlainValue(state);
478
464
  syncingFromYjs = true;
479
465
  try {
480
- if (store.share === "main") store.setState(next, () => {
481
- const { patches, inversePatches } = createRootReplacementPatches(store.getPureState(), next);
482
- const finalPatches = store.patch ? store.patch({
483
- patches,
484
- inversePatches
485
- }) : {
486
- patches,
487
- inversePatches
488
- };
489
- if (finalPatches.patches.length) store.apply(store.getPureState(), finalPatches.patches);
490
- return [
491
- store.getPureState(),
492
- finalPatches.patches,
493
- finalPatches.inversePatches
494
- ];
495
- });
496
- else {
497
- store.setState(null);
498
- store.apply(next);
499
- }
466
+ applyReplacementState(next);
500
467
  const pureState = clone(store.getPureState());
501
468
  lastSyncedState = isPlainObject(pureState) ? pureState : {};
502
469
  } finally {
@@ -508,6 +475,16 @@ const bindYjs = (store, options = {}) => {
508
475
  assertRemoteOperationsSerializable(operations);
509
476
  syncingFromYjs = true;
510
477
  try {
478
+ if (operations.some((operation) => operation.type === "delete" && operation.path.length === 1)) {
479
+ const next = clone(store.getPureState());
480
+ if (!isPlainObject(next)) return;
481
+ for (const operation of operations) if (operation.type === "set") setAtPath(next, operation.path, operation.value);
482
+ else deleteAtPath(next, operation.path);
483
+ applyReplacementState(next);
484
+ const pureState = clone(store.getPureState());
485
+ lastSyncedState = isPlainObject(pureState) ? pureState : {};
486
+ return;
487
+ }
511
488
  store.setState((draft) => {
512
489
  const mutableDraft = draft;
513
490
  for (const operation of operations) if (operation.type === "set") setAtPath(mutableDraft, operation.path, operation.value);
@@ -544,7 +521,7 @@ const bindYjs = (store, options = {}) => {
544
521
  setTimeout(scheduleFlushFromYjs, 0);
545
522
  return;
546
523
  }
547
- if (isYjsSerializableStateError(error)) {
524
+ if (isYjsSerializableStateError(error) || isStateSchemaError(error)) {
548
525
  restoreRootState();
549
526
  return;
550
527
  }
@@ -562,7 +539,7 @@ const bindYjs = (store, options = {}) => {
562
539
  setTimeout(scheduleFlushFromYjs, 0);
563
540
  return;
564
541
  }
565
- if (isYjsSerializableStateError(error)) {
542
+ if (isYjsSerializableStateError(error) || isStateSchemaError(error)) {
566
543
  restoreRootState();
567
544
  return;
568
545
  }
@@ -635,7 +612,7 @@ const bindYjs = (store, options = {}) => {
635
612
  try {
636
613
  applyRemoteState(state);
637
614
  } catch (error) {
638
- if (isYjsSerializableStateError(error)) {
615
+ if (isYjsSerializableStateError(error) || isStateSchemaError(error)) {
639
616
  restoreRootState();
640
617
  return;
641
618
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coaction/yjs",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "A Coaction integration tool for Yjs",
5
5
  "keywords": [
6
6
  "coaction",
@@ -28,9 +28,14 @@
28
28
  "types": "dist/index.d.ts",
29
29
  "exports": {
30
30
  ".": {
31
- "types": "./dist/index.d.ts",
32
- "import": "./dist/index.mjs",
33
- "require": "./dist/index.js",
31
+ "import": {
32
+ "types": "./dist/index.d.mts",
33
+ "default": "./dist/index.mjs"
34
+ },
35
+ "require": {
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.js"
38
+ },
34
39
  "default": "./dist/index.mjs"
35
40
  },
36
41
  "./package.json": "./package.json"
@@ -41,10 +46,10 @@
41
46
  },
42
47
  "devDependencies": {
43
48
  "yjs": "^13.6.27",
44
- "coaction": "2.0.0"
49
+ "coaction": "3.0.0"
45
50
  },
46
51
  "peerDependencies": {
47
- "coaction": "^2.0.0",
52
+ "coaction": "^3.0.0",
48
53
  "yjs": "^13.6.0"
49
54
  },
50
55
  "authors": [