Compare commits

..

32 Commits

Author SHA1 Message Date
Kevin Jahns
189b1068ae 13.0.0-103 2019-12-10 20:52:20 +01:00
Kevin Jahns
7a3b60a5d7 add markdownlint-cli as dep 2019-12-10 20:51:07 +01:00
Kevin Jahns
99f06fc093 bump lib0 for improved encoding performance 2019-12-10 20:46:58 +01:00
Kevin Jahns
22917bca19 fix gc & proper options typings for Y.Doc, fixes #176 2019-12-10 17:51:49 +01:00
Kevin Jahns
7f0e25dcba permanent user store writes updates in separate transaction 2019-12-10 17:18:57 +01:00
Kevin Jahns
d90c9b1cb2 bump lib0 for faster text encoding 2019-12-10 00:26:28 +01:00
Kevin Jahns
c426055f17 spelling 2019-12-10 00:19:02 +01:00
Kevin Jahns
18c9010b63 Merge branch 'master' of github.com:y-js/yjs 2019-11-26 13:02:49 +01:00
Kevin Jahns
c3edac62ef doc typo 2019-11-26 13:02:43 +01:00
Kevin Jahns
755de18fd5 Create Funding.yml 2019-11-07 14:41:50 +01:00
Kevin Jahns
641dc25076 13.0.0-102 2019-10-25 23:47:23 +02:00
Kevin Jahns
1d58ea785f Merge branch 'master' of github.com:yjs/yjs 2019-10-25 23:45:50 +02:00
Kevin Jahns
f53dff5043 delay errors in observe callbacks to throw after cleanup is done 2019-10-25 23:44:09 +02:00
Kevin Jahns
74d1a31f49 Merge pull request #174 from boschDev/master
Fix attrs loop in yXmlText
2019-10-15 17:19:30 +02:00
Roeland Bosch
d1063ab70b Fix attrs loop in yXmlText 2019-10-15 17:07:20 +02:00
Kevin Jahns
f4c919d9ec 13.0.0-101 2019-10-08 18:33:50 +02:00
Kevin Jahns
aeb23dbaa9 follow redone items to prevent some undo-redo issues. Fixes #162 2019-10-08 18:31:56 +02:00
Kevin Jahns
6d4f0c0cdd 13.0.0-100 2019-10-08 17:40:32 +02:00
Kevin Jahns
303138f309 sanitize items before undoing. fixes #165 2019-10-08 17:36:00 +02:00
Kevin Jahns
ad373a3dce Merge pull request #172 from istvank/patch-1
Fixing Y.Map's documentation of forEach
2019-10-05 20:09:53 +02:00
István Koren
2150fa58f2 Fixing Y.Map's documentation of forEach
fixes #171 As always, it's an honor to submit a PR! 🐒 There was also a missing dot in the Y.XmlFragment title.
2019-10-05 15:14:30 +02:00
Kevin Jahns
ece4841b5c update stackItem.meta doc 2019-10-03 22:06:07 +02:00
Kevin Jahns
8103220c05 Merge branch 'master' of github.com:yjs/yjs 2019-09-30 11:10:13 +02:00
Kevin Jahns
66d500f08d YEvent: consider case that item was added & removed in the same transaction 2019-09-30 11:10:03 +02:00
Kevin Jahns
5f8e7c7ba7 Merge pull request #169 from yjs/improve-readme
update quill cursors support
2019-09-23 11:22:51 +02:00
Nik Graf
7b8eee6b25 update quill cursors support 2019-09-23 11:22:24 +02:00
Kevin Jahns
1d5947c602 13.0.0-99 2019-09-23 11:11:45 +02:00
Kevin Jahns
53e4028952 Merge pull request #168 from yjs/fix-absolute-position-calculation
fix absolute position calculation
2019-09-23 11:09:48 +02:00
Nik Graf
b38a8d99e5 fix absolute position calculation 2019-09-23 11:05:50 +02:00
Kevin Jahns
6c4971ae25 13.0.0-98 2019-09-17 18:55:04 +02:00
Kevin Jahns
d1f5ff0f59 implement PermanentUserData storage prototype 2019-09-17 18:53:59 +02:00
Kevin Jahns
1d297601e8 export .createDeleteSet functionality 2019-09-04 22:08:05 +02:00
25 changed files with 674 additions and 254 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: dmonad
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -55,7 +55,7 @@ are implemented in separate modules.
| Name | Cursors | Binding | Demo | | Name | Cursors | Binding | Demo |
|---|:-:|---|---| |---|:-:|---|---|
| [ProseMirror](https://prosemirror.net/)                                                   | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) | | [ProseMirror](https://prosemirror.net/)                                                   | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) |
| [Quill](https://quilljs.com/) | | [y-quill](http://github.com/yjs/y-quill) | [demo](https://yjs-demos.now.sh/quill/) | | [Quill](https://quilljs.com/) | | [y-quill](http://github.com/yjs/y-quill) | [demo](https://yjs-demos.now.sh/quill/) |
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) | | [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) |
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://yjs-demos.now.sh/monaco/) | | [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://yjs-demos.now.sh/monaco/) |
| [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/yjs/y-ace) | [demo](https://yjs-demos.now.sh/ace/) | | [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/yjs/y-ace) | [demo](https://yjs-demos.now.sh/ace/) |
@@ -80,7 +80,7 @@ leveldb database.
<dd> <dd>
[WIP] Creates a connected graph of webrtc connections with a high [WIP] Creates a connected graph of webrtc connections with a high
<a href="https://en.wikipedia.org/wiki/Strength_of_a_graph">strength</a>. It <a href="https://en.wikipedia.org/wiki/Strength_of_a_graph">strength</a>. It
requires a signalling server that connects a client to the first peer. But after requires a signaling server that connects a client to the first peer. But after
that the network manages itself. It is well suited for large and small networks. that the network manages itself. It is well suited for large and small networks.
</dd> </dd>
<dt><a href="http://github.com/yjs/y-dat">y-dat</a></dt> <dt><a href="http://github.com/yjs/y-dat">y-dat</a></dt>
@@ -235,7 +235,8 @@ or any of its children.
Copies the <code>[key,value]</code> pairs of this YMap to a new Object.It Copies the <code>[key,value]</code> pairs of this YMap to a new Object.It
transforms all child types to JSON using their <code>toJSON</code> method. transforms all child types to JSON using their <code>toJSON</code> method.
</dd> </dd>
<b><code>forEach(function(key:string,value:object|boolean|Array|string|number|Uint8Array|Y.Type))</code></b> <b><code>forEach(function(value:object|boolean|Array|string|number|Uint8Array|Y.Type,
key:string, map: Y.Map))</code></b>
<dd> <dd>
Execute the provided function once for every key-value pair. Execute the provided function once for every key-value pair.
</dd> </dd>
@@ -343,7 +344,7 @@ or any of its children.
</details> </details>
<details> <details>
<summary><b>YXmlFragment</b></summary> <summary><b>Y.XmlFragment</b></summary>
<br> <br>
<p> <p>
A container that holds an Array of Y.XmlElements. A container that holds an Array of Y.XmlElements.

177
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-97", "version": "13.0.0-103",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -735,6 +735,12 @@
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true "dev": true
}, },
"deep-extend": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
"integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==",
"dev": true
},
"deep-is": { "deep-is": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@@ -1490,8 +1496,7 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@@ -1512,14 +1517,12 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -1534,20 +1537,17 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@@ -1664,8 +1664,7 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@@ -1677,7 +1676,6 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@@ -1692,7 +1690,6 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@@ -1700,14 +1697,12 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@@ -1726,7 +1721,6 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@@ -1807,8 +1801,7 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@@ -1820,7 +1813,6 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@@ -1906,8 +1898,7 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@@ -1943,7 +1934,6 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@@ -1963,7 +1953,6 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@@ -2007,14 +1996,12 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
} }
} }
}, },
@@ -2089,6 +2076,12 @@
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
"dev": true "dev": true
}, },
"graceful-readlink": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
"dev": true
},
"has": { "has": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -2252,6 +2245,12 @@
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true "dev": true
}, },
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true
},
"inquirer": { "inquirer": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
@@ -2596,9 +2595,9 @@
} }
}, },
"lib0": { "lib0": {
"version": "0.1.0", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.1.0.tgz", "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.1.7.tgz",
"integrity": "sha512-pkpnv2IJEOb6iwpcJ6BVQu9GkZ9VINKeQ/0BcArHpozqaGQYWe+ychf2p9wHKToHUnivPoGZZ7rFqrxNXjqFBg==" "integrity": "sha512-ooa/CXJ76VMY3ZaE0lYNYkLp/OND1jobHHY6QUtSDxne+xb3+Gl6f8WPUXdBkgnPEv59s/+jCdN4HOumdI5rSg=="
}, },
"linkify-it": { "linkify-it": {
"version": "2.2.0", "version": "2.2.0",
@@ -2676,6 +2675,12 @@
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=",
"dev": true "dev": true
}, },
"lodash.differencewith": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.differencewith/-/lodash.differencewith-4.5.0.tgz",
"integrity": "sha1-uvr7yRi1UVTheRdqALsK76rIVLc=",
"dev": true
},
"lodash.filter": { "lodash.filter": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz",
@@ -2789,6 +2794,78 @@
"integrity": "sha512-n8zCGjxA3T+Mx1pG8HEgbJbkB8JFUuRkeTZQuIM8iPY6oQ8sWOPRZJDFC9a/pNg2QkHEjjGkhBEl/RSyzaDZ3A==", "integrity": "sha512-n8zCGjxA3T+Mx1pG8HEgbJbkB8JFUuRkeTZQuIM8iPY6oQ8sWOPRZJDFC9a/pNg2QkHEjjGkhBEl/RSyzaDZ3A==",
"dev": true "dev": true
}, },
"markdownlint": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.17.2.tgz",
"integrity": "sha512-vsxopn0qEdm0P2XI3S9sVA+jvjKjR8lHZ+0FKlusth+1UK9tI29mRFkKeZPERmbWsMehJcogfMieBUkMgNEFkQ==",
"dev": true,
"requires": {
"markdown-it": "10.0.0"
},
"dependencies": {
"entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==",
"dev": true
},
"markdown-it": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"entities": "~2.0.0",
"linkify-it": "^2.0.0",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
}
}
}
},
"markdownlint-cli": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.19.0.tgz",
"integrity": "sha512-5hUEBAmbCVJflws6841HJ0KTZdgGWYaPThoXPI6Wjn1VkoiYWsprNH0r3PvPmyGXtvbHJ7/7eGPde2a6cx8t0w==",
"dev": true,
"requires": {
"commander": "~2.9.0",
"deep-extend": "~0.5.1",
"get-stdin": "~5.0.1",
"glob": "~7.1.2",
"js-yaml": "^3.13.1",
"lodash.differencewith": "~4.5.0",
"lodash.flatten": "~4.4.0",
"markdownlint": "~0.17.1",
"markdownlint-rule-helpers": "~0.5.0",
"minimatch": "~3.0.4",
"rc": "~1.2.7"
},
"dependencies": {
"commander": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
"integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=",
"dev": true,
"requires": {
"graceful-readlink": ">= 1.0.0"
}
},
"get-stdin": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz",
"integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=",
"dev": true
}
}
},
"markdownlint-rule-helpers": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.5.0.tgz",
"integrity": "sha512-6bJAV4TjUoDDnqxfb6EKTuZlpYI6vn4kerid7WTrZaEjsWuYDeYDAN+r4o+vbUYFZfJkiBU7NPBqPd4QJ1CZzQ==",
"dev": true
},
"marked": { "marked": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
@@ -3296,6 +3373,38 @@
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"dev": true "dev": true
}, },
"rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"requires": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"dependencies": {
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true
}
}
},
"react-is": { "react-is": {
"version": "16.8.6", "version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-97", "version": "13.0.0-103",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.js", "main": "./dist/yjs.js",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",
@@ -51,12 +51,13 @@
}, },
"homepage": "https://yjs.dev", "homepage": "https://yjs.dev",
"dependencies": { "dependencies": {
"lib0": "^0.1.0" "lib0": "^0.1.7"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^3.6.1", "concurrently": "^3.6.1",
"jsdoc": "^3.6.3", "jsdoc": "^3.6.3",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"markdownlint-cli": "^0.19.0",
"rollup": "^1.20.3", "rollup": "^1.20.3",
"rollup-cli": "^1.0.9", "rollup-cli": "^1.0.9",
"rollup-plugin-node-resolve": "^4.2.4", "rollup-plugin-node-resolve": "^4.2.4",

View File

@@ -38,6 +38,8 @@ export {
getState, getState,
Snapshot, Snapshot,
createSnapshot, createSnapshot,
createDeleteSet,
createDeleteSetFromStructStore,
snapshot, snapshot,
emptySnapshot, emptySnapshot,
findRootTypeKey, findRootTypeKey,
@@ -51,5 +53,6 @@ export {
decodeSnapshot, decodeSnapshot,
encodeSnapshot, encodeSnapshot,
isDeleted, isDeleted,
equalSnapshots equalSnapshots,
PermanentUserData // @TODO experimental
} from './internals.js' } from './internals.js'

View File

@@ -1,13 +1,16 @@
export * from './utils/DeleteSet.js' export * from './utils/DeleteSet.js'
export * from './utils/Doc.js'
export * from './utils/encoding.js'
export * from './utils/EventHandler.js' export * from './utils/EventHandler.js'
export * from './utils/ID.js' export * from './utils/ID.js'
export * from './utils/isParentOf.js' export * from './utils/isParentOf.js'
export * from './utils/PermanentUserData.js'
export * from './utils/RelativePosition.js' export * from './utils/RelativePosition.js'
export * from './utils/Snapshot.js' export * from './utils/Snapshot.js'
export * from './utils/StructStore.js' export * from './utils/StructStore.js'
export * from './utils/Transaction.js' export * from './utils/Transaction.js'
export * from './utils/UndoManager.js' export * from './utils/UndoManager.js'
export * from './utils/Doc.js'
export * from './utils/YEvent.js' export * from './utils/YEvent.js'
export * from './types/AbstractType.js' export * from './types/AbstractType.js'
@@ -31,5 +34,3 @@ export * from './structs/ContentAny.js'
export * from './structs/ContentString.js' export * from './structs/ContentString.js'
export * from './structs/ContentType.js' export * from './structs/ContentType.js'
export * from './structs/Item.js' export * from './structs/Item.js'
export * from './utils/encoding.js'

View File

@@ -35,6 +35,8 @@ import * as set from 'lib0/set.js'
import * as binary from 'lib0/binary.js' import * as binary from 'lib0/binary.js'
/** /**
* @todo This should return several items
*
* @param {StructStore} store * @param {StructStore} store
* @param {ID} id * @param {ID} id
* @return {{item:Item, diff:number}} * @return {{item:Item, diff:number}}
@@ -53,7 +55,7 @@ export const followRedone = (store, id) => {
item = getItem(store, nextID) item = getItem(store, nextID)
diff = nextID.clock - item.id.clock diff = nextID.clock - item.id.clock
nextID = item.redone nextID = item.redone
} while (nextID !== null) } while (nextID !== null && item instanceof Item)
return { return {
item, diff item, diff
} }
@@ -135,7 +137,7 @@ export const splitItem = (transaction, leftItem, diff) => {
*/ */
export const redoItem = (transaction, item, redoitems) => { export const redoItem = (transaction, item, redoitems) => {
if (item.redone !== null) { if (item.redone !== null) {
return getItemCleanStart(transaction, transaction.doc.store, item.redone) return getItemCleanStart(transaction, item.redone)
} }
let parentItem = item.parent._item let parentItem = item.parent._item
/** /**
@@ -175,7 +177,7 @@ export const redoItem = (transaction, item, redoitems) => {
} }
if (parentItem !== null && parentItem.redone !== null) { if (parentItem !== null && parentItem.redone !== null) {
while (parentItem.redone !== null) { while (parentItem.redone !== null) {
parentItem = getItemCleanStart(transaction, transaction.doc.store, parentItem.redone) parentItem = getItemCleanStart(transaction, parentItem.redone)
} }
// find next cloned_redo items // find next cloned_redo items
while (left !== null) { while (left !== null) {
@@ -185,7 +187,7 @@ export const redoItem = (transaction, item, redoitems) => {
let leftTrace = left let leftTrace = left
// trace redone until parent matches // trace redone until parent matches
while (leftTrace !== null && leftTrace.parent._item !== parentItem) { while (leftTrace !== null && leftTrace.parent._item !== parentItem) {
leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, transaction.doc.store, leftTrace.redone) leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
} }
if (leftTrace !== null && leftTrace.parent._item === parentItem) { if (leftTrace !== null && leftTrace.parent._item === parentItem) {
left = leftTrace left = leftTrace
@@ -200,7 +202,7 @@ export const redoItem = (transaction, item, redoitems) => {
let rightTrace = right let rightTrace = right
// trace redone until parent matches // trace redone until parent matches
while (rightTrace !== null && rightTrace.parent._item !== parentItem) { while (rightTrace !== null && rightTrace.parent._item !== parentItem) {
rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, transaction.doc.store, rightTrace.redone) rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
} }
if (rightTrace !== null && rightTrace.parent._item === parentItem) { if (rightTrace !== null && rightTrace.parent._item === parentItem) {
right = rightTrace right = rightTrace
@@ -726,7 +728,7 @@ export class ItemRef extends AbstractStructRef {
} }
const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left) const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
const right = this.right === null ? null : getItemCleanStart(transaction, store, this.right) const right = this.right === null ? null : getItemCleanStart(transaction, this.right)
let parent = null let parent = null
let parentSub = this.parentSub let parentSub = this.parentSub
if (this.parent !== null) { if (this.parent !== null) {

View File

@@ -30,7 +30,7 @@ import * as encoding from 'lib0/encoding.js' // eslint-disable-line
* @param {EventType} event * @param {EventType} event
*/ */
export const callTypeObservers = (type, transaction, event) => { export const callTypeObservers = (type, transaction, event) => {
callEventHandlerListeners(type._eH, event, transaction) const changedType = type
const changedParentTypes = transaction.changedParentTypes const changedParentTypes = transaction.changedParentTypes
while (true) { while (true) {
// @ts-ignore // @ts-ignore
@@ -40,6 +40,7 @@ export const callTypeObservers = (type, transaction, event) => {
} }
type = type._item.parent type = type._item.parent
} }
callEventHandlerListeners(changedType._eH, event, transaction)
} }
/** /**
@@ -428,7 +429,7 @@ export const typeListInsertGenerics = (transaction, parent, index, content) => {
if (index <= n.length) { if (index <= n.length) {
if (index < n.length) { if (index < n.length) {
// insert in-between // insert in-between
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + index)) getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
} }
break break
} }
@@ -454,7 +455,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
for (; n !== null && index > 0; n = n.right) { for (; n !== null && index > 0; n = n.right) {
if (!n.deleted && n.countable) { if (!n.deleted && n.countable) {
if (index < n.length) { if (index < n.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + index)) getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
} }
index -= n.length index -= n.length
} }
@@ -463,7 +464,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
while (length > 0 && n !== null) { while (length > 0 && n !== null) {
if (!n.deleted) { if (!n.deleted) {
if (length < n.length) { if (length < n.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + length)) getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length))
} }
n.delete(transaction) n.delete(transaction)
length -= n.length length -= n.length

View File

@@ -17,7 +17,7 @@ import {
ContentFormat, ContentFormat,
ContentString, ContentString,
splitSnapshotAffectedStructs, splitSnapshotAffectedStructs,
Doc, Item, Snapshot, StructStore, Transaction // eslint-disable-line ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line import * as decoding from 'lib0/decoding.js' // eslint-disable-line
@@ -68,7 +68,6 @@ export class ItemInsertionResult extends ItemListPosition {
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {Item|null} left * @param {Item|null} left
* @param {Item|null} right * @param {Item|null} right
@@ -78,7 +77,7 @@ export class ItemInsertionResult extends ItemListPosition {
* @private * @private
* @function * @function
*/ */
const findNextPosition = (transaction, store, currentAttributes, left, right, count) => { const findNextPosition = (transaction, currentAttributes, left, right, count) => {
while (right !== null && count > 0) { while (right !== null && count > 0) {
switch (right.content.constructor) { switch (right.content.constructor) {
case ContentEmbed: case ContentEmbed:
@@ -86,7 +85,7 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
if (!right.deleted) { if (!right.deleted) {
if (count < right.length) { if (count < right.length) {
// split right // split right
getItemCleanStart(transaction, store, createID(right.id.client, right.id.clock + count)) getItemCleanStart(transaction, createID(right.id.client, right.id.clock + count))
} }
count -= right.length count -= right.length
} }
@@ -105,7 +104,6 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {number} index * @param {number} index
* @return {ItemTextListPosition} * @return {ItemTextListPosition}
@@ -113,11 +111,11 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
* @private * @private
* @function * @function
*/ */
const findPosition = (transaction, store, parent, index) => { const findPosition = (transaction, parent, index) => {
let currentAttributes = new Map() let currentAttributes = new Map()
let left = null let left = null
let right = parent._start let right = parent._start
return findNextPosition(transaction, store, currentAttributes, left, right, index) return findNextPosition(transaction, currentAttributes, left, right, index)
} }
/** /**
@@ -299,7 +297,7 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
case ContentEmbed: case ContentEmbed:
case ContentString: case ContentString:
if (length < right.length) { if (length < right.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length)) getItemCleanStart(transaction, createID(right.id.client, right.id.clock + length))
} }
length -= right.length length -= right.length
break break
@@ -343,7 +341,7 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
case ContentEmbed: case ContentEmbed:
case ContentString: case ContentString:
if (length < right.length) { if (length < right.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length)) getItemCleanStart(transaction, createID(right.id.client, right.id.clock + length))
} }
length -= right.length length -= right.length
right.delete(transaction) right.delete(transaction)
@@ -714,11 +712,12 @@ export class YText extends AbstractType {
* *
* @param {Snapshot} [snapshot] * @param {Snapshot} [snapshot]
* @param {Snapshot} [prevSnapshot] * @param {Snapshot} [prevSnapshot]
* @param {function('removed' | 'added', ID):any} [computeYChange]
* @return {any} The Delta representation of this type. * @return {any} The Delta representation of this type.
* *
* @public * @public
*/ */
toDelta (snapshot, prevSnapshot) { toDelta (snapshot, prevSnapshot, computeYChange) {
/** /**
* @type{Array<any>} * @type{Array<any>}
*/ */
@@ -767,12 +766,12 @@ export class YText extends AbstractType {
if (snapshot !== undefined && !isVisible(n, snapshot)) { if (snapshot !== undefined && !isVisible(n, snapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') { if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
packStr() packStr()
currentAttributes.set('ychange', { user: n.id.client, state: 'removed' }) currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
} }
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) { } else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') { if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') {
packStr() packStr()
currentAttributes.set('ychange', { user: n.id.client, state: 'added' }) currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
} }
} else if (cur !== undefined) { } else if (cur !== undefined) {
packStr() packStr()
@@ -818,7 +817,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
if (!attributes) { if (!attributes) {
attributes = {} attributes = {}
currentAttributes.forEach((v, k) => { attributes[k] = v }) currentAttributes.forEach((v, k) => { attributes[k] = v })
@@ -847,7 +846,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
insertText(transaction, this, left, right, currentAttributes, embed, attributes) insertText(transaction, this, left, right, currentAttributes, embed, attributes)
}) })
} else { } else {
@@ -870,7 +869,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
deleteText(transaction, left, right, currentAttributes, length) deleteText(transaction, left, right, currentAttributes, length)
}) })
} else { } else {
@@ -892,7 +891,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
let { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) let { left, right, currentAttributes } = findPosition(transaction, this, index)
if (right === null) { if (right === null) {
return return
} }

View File

@@ -56,7 +56,7 @@ export class YXmlText extends YText {
const node = nestedNodes[i] const node = nestedNodes[i]
str += `<${node.nodeName}` str += `<${node.nodeName}`
for (let j = 0; j < node.attrs.length; j++) { for (let j = 0; j < node.attrs.length; j++) {
const attr = node.attrs[i] const attr = node.attrs[j]
str += ` ${attr.key}="${attr.value}"` str += ` ${attr.key}="${attr.value}"`
} }
str += '>' str += '>'

View File

@@ -8,6 +8,7 @@ import {
Item, GC, StructStore, Transaction, ID // eslint-disable-line Item, GC, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as array from 'lib0/array.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@@ -52,14 +53,13 @@ export class DeleteSet {
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {DeleteSet} ds * @param {DeleteSet} ds
* @param {StructStore} store
* @param {function(GC|Item):void} f * @param {function(GC|Item):void} f
* *
* @function * @function
*/ */
export const iterateDeletedStructs = (transaction, ds, store, f) => export const iterateDeletedStructs = (transaction, ds, f) =>
ds.clients.forEach((deletes, clientid) => { ds.clients.forEach((deletes, clientid) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(clientid)) const structs = /** @type {Array<GC|Item>} */ (transaction.doc.store.clients.get(clientid))
for (let i = 0; i < deletes.length; i++) { for (let i = 0; i < deletes.length; i++) {
const del = deletes[i] const del = deletes[i]
iterateStructs(transaction, structs, del.clock, del.len, f) iterateStructs(transaction, structs, del.clock, del.len, f)
@@ -137,22 +137,27 @@ export const sortAndMergeDeleteSet = ds => {
} }
/** /**
* @param {DeleteSet} ds1 * @param {Array<DeleteSet>} dss
* @param {DeleteSet} ds2
* @return {DeleteSet} A fresh DeleteSet * @return {DeleteSet} A fresh DeleteSet
*/ */
export const mergeDeleteSets = (ds1, ds2) => { export const mergeDeleteSets = dss => {
const merged = new DeleteSet() const merged = new DeleteSet()
// Write all keys from ds1 to merged. If ds2 has the same key, combine the sets. for (let dssI = 0; dssI < dss.length; dssI++) {
ds1.clients.forEach((dels1, client) => dss[dssI].clients.forEach((delsLeft, client) => {
merged.clients.set(client, dels1.concat(ds2.clients.get(client) || [])) if (!merged.clients.has(client)) {
) // Write all missing keys from current ds and all following.
// Write all missing keys from ds2 to merged. // If merged already contains `client` current ds has already been added.
ds2.clients.forEach((dels2, client) => { /**
if (!merged.clients.has(client)) { * @type {Array<DeleteItem>}
merged.clients.set(client, dels2) */
} const dels = delsLeft.slice()
}) for (let i = dssI + 1; i < dss.length; i++) {
array.appendTo(dels, dss[i].clients.get(client) || [])
}
merged.clients.set(client, dels)
}
})
}
sortAndMergeDeleteSet(merged) sortAndMergeDeleteSet(merged)
return merged return merged
} }

View File

@@ -23,11 +23,12 @@ import * as map from 'lib0/map.js'
*/ */
export class Doc extends Observable { export class Doc extends Observable {
/** /**
* @param {Object|undefined} conf configuration * @param {Object} conf configuration
* @param {boolean} [conf.gc] Disable garbage collection (default: gc=true)
*/ */
constructor (conf = {}) { constructor ({ gc = true } = {}) {
super() super()
this.gc = conf.gc || true this.gc = gc
this.clientID = random.uint32() this.clientID = random.uint32()
/** /**
* @type {Map<string, AbstractType<YEvent>>} * @type {Map<string, AbstractType<YEvent>>}

View File

@@ -0,0 +1,138 @@
import {
YArray,
YMap,
readDeleteSet,
writeDeleteSet,
createDeleteSet,
ID, DeleteSet, YArrayEvent, Transaction, Doc // eslint-disable-line
} from '../internals.js'
import * as decoding from 'lib0/decoding.js'
import * as encoding from 'lib0/encoding.js'
import { mergeDeleteSets, isDeleted } from './DeleteSet.js'
export class PermanentUserData {
/**
* @param {Doc} doc
* @param {string} key
*/
constructor (doc, key = 'users') {
const users = doc.getMap(key)
/**
* @type {Map<string,DeleteSet>}
*/
const dss = new Map()
this.yusers = users
this.doc = doc
/**
* Maps from clientid to userDescription
*
* @type {Map<number,string>}
*/
this.clients = new Map()
this.dss = dss
/**
* @param {YMap<any>} user
* @param {string} userDescription
*/
const initUser = (user, userDescription) => {
/**
* @type {YArray<Uint8Array>}
*/
const ds = user.get('ds')
const ids = user.get('ids')
const addClientId = /** @param {number} clientid */ clientid => this.clients.set(clientid, userDescription)
ds.observe(/** @param {YArrayEvent<any>} event */ event => {
event.changes.added.forEach(item => {
item.content.getContent().forEach(encodedDs => {
if (encodedDs instanceof Uint8Array) {
this.dss.set(userDescription, mergeDeleteSets([this.dss.get(userDescription) || createDeleteSet(), readDeleteSet(decoding.createDecoder(encodedDs))]))
}
})
})
})
this.dss.set(userDescription, mergeDeleteSets(ds.map(encodedDs => readDeleteSet(decoding.createDecoder(encodedDs)))))
ids.observe(/** @param {YArrayEvent<any>} event */ event =>
event.changes.added.forEach(item => item.content.getContent().forEach(addClientId))
)
ids.forEach(addClientId)
}
// observe users
users.observe(event => {
event.keysChanged.forEach(userDescription =>
initUser(users.get(userDescription), userDescription)
)
})
// add intial data
users.forEach(initUser)
}
/**
* @param {Doc} doc
* @param {number} clientid
* @param {string} userDescription
*/
setUserMapping (doc, clientid, userDescription) {
const users = this.yusers
let user = users.get(userDescription)
if (!user) {
user = new YMap()
user.set('ids', new YArray())
user.set('ds', new YArray())
users.set(userDescription, user)
}
user.get('ids').push([clientid])
users.observe(event => {
setTimeout(() => {
const userOverwrite = users.get(userDescription)
if (userOverwrite !== user) {
// user was overwritten, port all data over to the next user object
// @todo Experiment with Y.Sets here
user = userOverwrite
// @todo iterate over old type
this.clients.forEach((_userDescription, clientid) => {
if (userDescription === _userDescription) {
user.get('ids').push([clientid])
}
})
const encoder = encoding.createEncoder()
const ds = this.dss.get(userDescription)
if (ds) {
writeDeleteSet(encoder, ds)
user.get('ds').push([encoding.toUint8Array(encoder)])
}
}
}, 0)
})
doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
setTimeout(() => {
const yds = user.get('ds')
const ds = transaction.deleteSet
if (transaction.local && ds.clients.size > 0) {
const encoder = encoding.createEncoder()
writeDeleteSet(encoder, ds)
yds.push([encoding.toUint8Array(encoder)])
}
})
})
}
/**
* @param {number} clientid
* @return {any}
*/
getUserByClientId (clientid) {
return this.clients.get(clientid) || null
}
/**
* @param {ID} id
* @return {string | null}
*/
getUserByDeletedId (id) {
for (const [userDescription, ds] of this.dss) {
if (isDeleted(ds, id)) {
return userDescription
}
}
return null
}
}

View File

@@ -228,7 +228,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
return null return null
} }
type = right.parent type = right.parent
if (type._item !== null && !type._item.deleted) { if (type._item === null || !type._item.deleted) {
index = right.deleted || !right.countable ? 0 : res.diff index = right.deleted || !right.countable ? 0 : res.diff
let n = right.left let n = right.left
while (n !== null) { while (n !== null) {

View File

@@ -131,10 +131,10 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
if (!meta.has(snapshot)) { if (!meta.has(snapshot)) {
snapshot.sv.forEach((clock, client) => { snapshot.sv.forEach((clock, client) => {
if (clock < getState(store, client)) { if (clock < getState(store, client)) {
getItemCleanStart(transaction, store, createID(client, clock)) getItemCleanStart(transaction, createID(client, clock))
} }
}) })
iterateDeletedStructs(transaction, snapshot.ds, store, item => {}) iterateDeletedStructs(transaction, snapshot.ds, item => {})
meta.add(snapshot) meta.add(snapshot)
} }
} }

View File

@@ -197,16 +197,15 @@ export const findIndexCleanStart = (transaction, structs, clock) => {
* Expects that id is actually in store. This function throws or is an infinite loop otherwise. * Expects that id is actually in store. This function throws or is an infinite loop otherwise.
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store
* @param {ID} id * @param {ID} id
* @return {Item} * @return {Item}
* *
* @private * @private
* @function * @function
*/ */
export const getItemCleanStart = (transaction, store, id) => { export const getItemCleanStart = (transaction, id) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(id.client)) const structs = /** @type {Array<Item>} */ (transaction.doc.store.clients.get(id.client))
return /** @type {Item} */ (structs[findIndexCleanStart(transaction, structs, id.clock)]) return structs[findIndexCleanStart(transaction, structs, id.clock)]
} }
/** /**

View File

@@ -17,6 +17,7 @@ import * as encoding from 'lib0/encoding.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
import * as set from 'lib0/set.js' import * as set from 'lib0/set.js'
import { callAll } from 'lib0/function.js'
/** /**
* A transaction is created for every change on the Yjs model. It is possible * A transaction is created for every change on the Yjs model. It is possible
@@ -46,8 +47,9 @@ export class Transaction {
/** /**
* @param {Doc} doc * @param {Doc} doc
* @param {any} origin * @param {any} origin
* @param {boolean} local
*/ */
constructor (doc, origin) { constructor (doc, origin, local) {
/** /**
* The Yjs instance. * The Yjs instance.
* @type {Doc} * @type {Doc}
@@ -71,7 +73,7 @@ export class Transaction {
/** /**
* All types that were directly modified (property added or child * All types that were directly modified (property added or child
* inserted/deleted). New types are not included in this Set. * inserted/deleted). New types are not included in this Set.
* Maps from type to parentSubs (`item._parentSub = null` for YArray) * Maps from type to parentSubs (`item.parentSub = null` for YArray)
* @type {Map<AbstractType<YEvent>,Set<String|null>>} * @type {Map<AbstractType<YEvent>,Set<String|null>>}
*/ */
this.changed = new Map() this.changed = new Map()
@@ -95,6 +97,11 @@ export class Transaction {
* @type {Map<any,any>} * @type {Map<any,any>}
*/ */
this.meta = new Map() this.meta = new Map()
/**
* Whether this change originates from this doc.
* @type {boolean}
*/
this.local = local
} }
} }
@@ -138,22 +145,180 @@ export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
} }
} }
/**
* @param {Array<Transaction>} transactionCleanups
* @param {number} i
*/
const cleanupTransactions = (transactionCleanups, i) => {
if (i < transactionCleanups.length) {
const transaction = transactionCleanups[i]
const doc = transaction.doc
const store = doc.store
const ds = transaction.deleteSet
try {
sortAndMergeDeleteSet(ds)
transaction.afterState = getStateVector(transaction.doc.store)
doc._transaction = null
doc.emit('beforeObserverCalls', [transaction, doc])
/**
* An array of event callbacks.
*
* Each callback is called even if the other ones throw errors.
*
* @type {Array<function():void>}
*/
const fs = []
// observe events on changed types
transaction.changed.forEach((subs, itemtype) =>
fs.push(() => {
if (itemtype._item === null || !itemtype._item.deleted) {
itemtype._callObserver(transaction, subs)
}
})
)
fs.push(() => {
// deep observe events
transaction.changedParentTypes.forEach((events, type) =>
fs.push(() => {
// We need to think about the possibility that the user transforms the
// Y.Doc in the event.
if (type._item === null || !type._item.deleted) {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
event.currentTarget = type
})
// We don't need to check for events.length
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction)
}
})
)
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
})
callAll(fs, [])
} finally {
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
*/
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1]
const right = structs[pos]
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1)
if (right instanceof Item && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
right.parent._map.set(right.parentSub, /** @type {Item} */ (left))
}
}
}
}
// Replace deleted items with ItemDeleted / GC.
// This is where content is actually remove from the Yjs Doc.
if (doc.gc) {
for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
const endDeleteItemClock = deleteItem.clock + deleteItem.len
for (
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
si < structs.length && struct.id.clock < endDeleteItemClock;
struct = structs[++si]
) {
const struct = structs[si]
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break
}
if (struct instanceof Item && struct.deleted && !struct.keep) {
struct.gc(store, false)
}
}
}
}
}
// try to merge deleted / gc'd items
// merge from right to left for better efficiecy and so we don't miss any merge targets
for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
// start with merging the item next to the last deleted item
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
for (
let si = mostRightIndexToCheck, struct = structs[si];
si > 0 && struct.id.clock >= deleteItem.clock;
struct = structs[--si]
) {
tryToMergeWithLeft(structs, si)
}
}
}
// on all affected store.clients props, try to merge
for (const [client, clock] of transaction.afterState) {
const beforeClock = transaction.beforeState.get(client) || 0
if (beforeClock !== clock) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
// we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i)
}
}
}
// try to merge mergeStructs
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
// but at the moment DS does not handle duplicates
for (const mid of transaction._mergeStructs) {
const client = mid.client
const clock = mid.clock
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
// @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc])
if (doc._observers.has('update')) {
const updateMessage = computeUpdateMessageFromTransaction(transaction)
if (updateMessage !== null) {
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
}
}
if (transactionCleanups.length <= i + 1) {
doc._transactionCleanups = []
} else {
cleanupTransactions(transactionCleanups, i + 1)
}
}
}
}
/** /**
* Implements the functionality of `y.transact(()=>{..})` * Implements the functionality of `y.transact(()=>{..})`
* *
* @param {Doc} doc * @param {Doc} doc
* @param {function(Transaction):void} f * @param {function(Transaction):void} f
* @param {any} [origin] * @param {any} [origin=true]
* *
* @private * @private
* @function * @function
*/ */
export const transact = (doc, f, origin = null) => { export const transact = (doc, f, origin = null, local = true) => {
const transactionCleanups = doc._transactionCleanups const transactionCleanups = doc._transactionCleanups
let initialCall = false let initialCall = false
if (doc._transaction === null) { if (doc._transaction === null) {
initialCall = true initialCall = true
doc._transaction = new Transaction(doc, origin) doc._transaction = new Transaction(doc, origin, local)
transactionCleanups.push(doc._transaction) transactionCleanups.push(doc._transaction)
doc.emit('beforeTransaction', [doc._transaction, doc]) doc.emit('beforeTransaction', [doc._transaction, doc])
} }
@@ -163,134 +328,13 @@ export const transact = (doc, f, origin = null) => {
if (initialCall && transactionCleanups[0] === doc._transaction) { if (initialCall && transactionCleanups[0] === doc._transaction) {
// The first transaction ended, now process observer calls. // The first transaction ended, now process observer calls.
// Observer call may create new transactions for which we need to call the observers and do cleanup. // Observer call may create new transactions for which we need to call the observers and do cleanup.
// We don't want to nest these calls, so we execute these calls one after another // We don't want to nest these calls, so we execute these calls one after
for (let i = 0; i < transactionCleanups.length; i++) { // another.
const transaction = transactionCleanups[i] // Also we need to ensure that all cleanups are called, even if the
const store = transaction.doc.store // observes throw errors.
const ds = transaction.deleteSet // This file is full of hacky try {} finally {} blocks to ensure that an
sortAndMergeDeleteSet(ds) // event can throw errors and also that the cleanup is called.
transaction.afterState = getStateVector(transaction.doc.store) cleanupTransactions(transactionCleanups, 0)
doc._transaction = null
doc.emit('beforeObserverCalls', [transaction, doc])
// emit change events on changed types
transaction.changed.forEach((subs, itemtype) => {
if (itemtype._item === null || !itemtype._item.deleted) {
itemtype._callObserver(transaction, subs)
}
})
transaction.changedParentTypes.forEach((events, type) => {
// We need to think about the possibility that the user transforms the
// Y.Doc in the event.
if (type._item === null || !type._item.deleted) {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
event.currentTarget = type
})
// We don't need to check for events.length
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction)
}
})
doc.emit('afterTransaction', [transaction, doc])
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
*/
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1]
const right = structs[pos]
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1)
if (right instanceof Item && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
right.parent._map.set(right.parentSub, /** @type {Item} */ (left))
}
}
}
}
// Replace deleted items with ItemDeleted / GC.
// This is where content is actually remove from the Yjs Doc.
if (doc.gc) {
for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
const endDeleteItemClock = deleteItem.clock + deleteItem.len
for (
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
si < structs.length && struct.id.clock < endDeleteItemClock;
struct = structs[++si]
) {
const struct = structs[si]
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break
}
if (struct instanceof Item && struct.deleted && !struct.keep) {
struct.gc(store, false)
}
}
}
}
}
// try to merge deleted / gc'd items
// merge from right to left for better efficiecy and so we don't miss any merge targets
for (const [client, deleteItems] of ds.clients) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
// start with merging the item next to the last deleted item
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
for (
let si = mostRightIndexToCheck, struct = structs[si];
si > 0 && struct.id.clock >= deleteItem.clock;
struct = structs[--si]
) {
tryToMergeWithLeft(structs, si)
}
}
}
// on all affected store.clients props, try to merge
for (const [client, clock] of transaction.afterState) {
const beforeClock = transaction.beforeState.get(client) || 0
if (beforeClock !== clock) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
// we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i)
}
}
}
// try to merge mergeStructs
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
// but at the moment DS does not handle duplicates
for (const mid of transaction._mergeStructs) {
const client = mid.client
const clock = mid.clock
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
// @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc])
if (doc._observers.has('update')) {
const updateMessage = computeUpdateMessageFromTransaction(transaction)
if (updateMessage !== null) {
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
}
}
}
doc._transactionCleanups = []
} }
} }
} }

View File

@@ -9,6 +9,7 @@ import {
createID, createID,
followRedone, followRedone,
getItemCleanStart, getItemCleanStart,
getState,
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@@ -49,35 +50,53 @@ const popStackItem = (undoManager, stack, eventType) => {
transact(doc, transaction => { transact(doc, transaction => {
while (stack.length > 0 && result === null) { while (stack.length > 0 && result === null) {
const store = doc.store const store = doc.store
const clientID = doc.clientID
const stackItem = /** @type {StackItem} */ (stack.pop()) const stackItem = /** @type {StackItem} */ (stack.pop())
const stackStartClock = stackItem.start
const stackEndClock = stackItem.start + stackItem.len
const itemsToRedo = new Set() const itemsToRedo = new Set()
// @todo iterateStructs should not need the structs parameter
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(clientID))
let performedChange = false let performedChange = false
iterateDeletedStructs(transaction, stackItem.ds, store, struct => { if (stackStartClock !== stackEndClock) {
if (struct instanceof Item && scope.some(type => isParentOf(type, struct))) { // make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
getItemCleanStart(transaction, createID(clientID, stackStartClock))
if (stackEndClock < getState(doc.store, clientID)) {
getItemCleanStart(transaction, createID(clientID, stackEndClock))
}
}
iterateDeletedStructs(transaction, stackItem.ds, struct => {
if (
struct instanceof Item &&
scope.some(type => isParentOf(type, struct)) &&
// Never redo structs in [stackItem.start, stackItem.start + stackItem.end) because they were created and deleted in the same capture interval.
!(struct.id.client === clientID && struct.id.clock >= stackStartClock && struct.id.clock < stackEndClock)
) {
itemsToRedo.add(struct) itemsToRedo.add(struct)
} }
}) })
itemsToRedo.forEach(item => { itemsToRedo.forEach(struct => {
performedChange = redoItem(transaction, item, itemsToRedo) !== null || performedChange performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
}) })
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(doc.clientID))
/** /**
* @type {Array<Item>} * @type {Array<Item>}
*/ */
const itemsToDelete = [] const itemsToDelete = []
iterateStructs(transaction, structs, stackItem.start, stackItem.len, struct => { iterateStructs(transaction, structs, stackStartClock, stackItem.len, struct => {
if (struct instanceof Item && !struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) { if (struct instanceof Item) {
if (struct.redone !== null) { if (struct.redone !== null) {
let { item, diff } = followRedone(store, struct.id) let { item, diff } = followRedone(store, struct.id)
if (diff > 0) { if (diff > 0) {
item = getItemCleanStart(transaction, store, createID(item.id.client, item.id.clock + diff)) item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
} }
if (item.length > stackItem.len) { if (item.length > stackItem.len) {
getItemCleanStart(transaction, store, createID(item.id.client, item.id.clock + stackItem.len)) getItemCleanStart(transaction, createID(item.id.client, stackEndClock))
} }
struct = item struct = item
} }
itemsToDelete.push(struct) if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct)
}
} }
}) })
// We want to delete in reverse order so that children are deleted before // We want to delete in reverse order so that children are deleted before
@@ -111,9 +130,9 @@ const popStackItem = (undoManager, stack, eventType) => {
/** /**
* Fires 'stack-item-added' event when a stack item was added to either the undo- or * Fires 'stack-item-added' event when a stack item was added to either the undo- or
* the redo-stack. You may store additional stack information via the * the redo-stack. You may store additional stack information via the
* metadata property on `event.stackItem.metadata` (it is a `Map` of metadata properties). * metadata property on `event.stackItem.meta` (it is a `Map` of metadata properties).
* Fires 'stack-item-popped' event when a stack item was popped from either the * Fires 'stack-item-popped' event when a stack item was popped from either the
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.metadata`. * undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
* *
* @extends {Observable<'stack-item-added'|'stack-item-popped'>} * @extends {Observable<'stack-item-added'|'stack-item-popped'>}
*/ */
@@ -168,7 +187,7 @@ export class UndoManager extends Observable {
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) { if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
// append change to last stack op // append change to last stack op
const lastOp = stack[stack.length - 1] const lastOp = stack[stack.length - 1]
lastOp.ds = mergeDeleteSets(lastOp.ds, transaction.deleteSet) lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet])
lastOp.len = afterState - lastOp.start lastOp.len = afterState - lastOp.start
} else { } else {
// create a new stack op // create a new stack op
@@ -178,7 +197,7 @@ export class UndoManager extends Observable {
this.lastChange = now this.lastChange = now
} }
// make sure that deleted structs are not gc'd // make sure that deleted structs are not gc'd
iterateDeletedStructs(transaction, transaction.deleteSet, transaction.doc.store, /** @param {Item|GC} item */ item => { iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) { if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
keepItem(item) keepItem(item)
} }

View File

@@ -56,6 +56,8 @@ export class YEvent {
/** /**
* Check if a struct is deleted by this event. * Check if a struct is deleted by this event.
* *
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
*
* @param {AbstractStruct} struct * @param {AbstractStruct} struct
* @return {boolean} * @return {boolean}
*/ */
@@ -66,6 +68,8 @@ export class YEvent {
/** /**
* Check if a struct is added by this event. * Check if a struct is added by this event.
* *
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
*
* @param {AbstractStruct} struct * @param {AbstractStruct} struct
* @return {boolean} * @return {boolean}
*/ */
@@ -106,7 +110,7 @@ export class YEvent {
} }
for (let item = target._start; item !== null; item = item.right) { for (let item = target._start; item !== null; item = item.right) {
if (item.deleted) { if (item.deleted) {
if (this.deletes(item)) { if (this.deletes(item) && !this.adds(item)) {
if (lastOp === null || lastOp.delete === undefined) { if (lastOp === null || lastOp.delete === undefined) {
packOp() packOp()
lastOp = { delete: 0 } lastOp = { delete: 0 }

View File

@@ -26,6 +26,7 @@ import {
readAndApplyDeleteSet, readAndApplyDeleteSet,
writeDeleteSet, writeDeleteSet,
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
transact,
Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@@ -299,10 +300,10 @@ export const readStructs = (decoder, transaction, store) => {
* @function * @function
*/ */
export const readUpdate = (decoder, ydoc, transactionOrigin) => export const readUpdate = (decoder, ydoc, transactionOrigin) =>
ydoc.transact(transaction => { transact(ydoc, transaction => {
readStructs(decoder, transaction, ydoc.store) readStructs(decoder, transaction, ydoc.store)
readAndApplyDeleteSet(decoder, transaction, ydoc.store) readAndApplyDeleteSet(decoder, transaction, ydoc.store)
}, transactionOrigin) }, transactionOrigin, false)
/** /**
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`. * Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.

View File

@@ -13,6 +13,23 @@ import * as t from 'lib0/testing.js'
export const testUndoText = tc => { export const testUndoText = tc => {
const { testConnector, text0, text1 } = init(tc, { users: 3 }) const { testConnector, text0, text1 } = init(tc, { users: 3 })
const undoManager = new UndoManager(text0) const undoManager = new UndoManager(text0)
// items that are added & deleted in the same transaction won't be undo
text0.insert(0, 'test')
text0.delete(0, 4)
undoManager.undo()
t.assert(text0.toString() === '')
// follow redone items
text0.insert(0, 'a')
undoManager.stopCapturing()
text0.delete(0, 1)
undoManager.stopCapturing()
undoManager.undo()
t.assert(text0.toString() === 'a')
undoManager.undo()
t.assert(text0.toString() === '')
text0.insert(0, 'abc') text0.insert(0, 'abc')
text1.insert(0, 'xyz') text1.insert(0, 'xyz')
testConnector.syncAll() testConnector.syncAll()
@@ -65,6 +82,15 @@ export const testUndoMap = tc => {
t.assert(map0.get('a') === 44) t.assert(map0.get('a') === 44)
undoManager.redo() undoManager.redo()
t.assert(map0.get('a') === 44) t.assert(map0.get('a') === 44)
// test setting value multiple times
map0.set('b', 'initial')
undoManager.stopCapturing()
map0.set('b', 'val1')
map0.set('b', 'val2')
undoManager.stopCapturing()
undoManager.undo()
t.assert(map0.get('b') === 'initial')
} }
/** /**

View File

@@ -3,6 +3,7 @@ import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing.js'
import * as prng from 'lib0/prng.js' import * as prng from 'lib0/prng.js'
import * as math from 'lib0/math.js'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
@@ -206,7 +207,7 @@ export const testChangeEvent = tc => {
const newArr = new Y.Array() const newArr = new Y.Array()
array0.insert(0, [newArr, 4, 'dtrn']) array0.insert(0, [newArr, 4, 'dtrn'])
t.assert(changes !== null && changes.added.size === 2 && changes.deleted.size === 0) t.assert(changes !== null && changes.added.size === 2 && changes.deleted.size === 0)
t.compare(changes.delta, [{insert: [newArr, 4, 'dtrn']}]) t.compare(changes.delta, [{ insert: [newArr, 4, 'dtrn'] }])
changes = null changes = null
array0.delete(0, 2) array0.delete(0, 2)
t.assert(changes !== null && changes.added.size === 0 && changes.deleted.size === 2) t.assert(changes !== null && changes.added.size === 0 && changes.deleted.size === 2)
@@ -362,12 +363,12 @@ const arrayTransactions = [
var length = yarray.length var length = yarray.length
if (length > 0) { if (length > 0) {
var somePos = prng.int31(gen, 0, length - 1) var somePos = prng.int31(gen, 0, length - 1)
var delLength = prng.int31(gen, 1, Math.min(2, length - somePos)) var delLength = prng.int31(gen, 1, math.min(2, length - somePos))
if (prng.bool(gen)) { if (prng.bool(gen)) {
var type = yarray.get(somePos) var type = yarray.get(somePos)
if (type.length > 0) { if (type.length > 0) {
somePos = prng.int31(gen, 0, type.length - 1) somePos = prng.int31(gen, 0, type.length - 1)
delLength = prng.int31(gen, 0, Math.min(2, type.length - somePos)) delLength = prng.int31(gen, 0, math.min(2, type.length - somePos))
type.delete(somePos, delLength) type.delete(somePos, delLength)
} }
} else { } else {

View File

@@ -340,6 +340,56 @@ export const testChangeEvent = tc => {
compare(users) compare(users)
} }
/**
* @param {t.TestCase} tc
*/
export const testYmapEventExceptionsShouldCompleteTransaction = tc => {
const doc = new Y.Doc()
const map = doc.getMap('map')
let updateCalled = false
let throwingObserverCalled = false
let throwingDeepObserverCalled = false
doc.on('update', () => {
updateCalled = true
})
const throwingObserver = () => {
throwingObserverCalled = true
throw new Error('Failure')
}
const throwingDeepObserver = () => {
throwingDeepObserverCalled = true
throw new Error('Failure')
}
map.observe(throwingObserver)
map.observeDeep(throwingDeepObserver)
t.fails(() => {
map.set('y', '2')
})
t.assert(updateCalled)
t.assert(throwingObserverCalled)
t.assert(throwingDeepObserverCalled)
// check if it works again
updateCalled = false
throwingObserverCalled = false
throwingDeepObserverCalled = false
t.fails(() => {
map.set('z', '3')
})
t.assert(updateCalled)
t.assert(throwingObserverCalled)
t.assert(throwingDeepObserverCalled)
t.assert(map.get('z') === '3')
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */

View File

@@ -81,10 +81,10 @@ export const testBasicFormat = tc => {
export const testGetDeltaWithEmbeds = tc => { export const testGetDeltaWithEmbeds = tc => {
const { text0 } = init(tc, { users: 1 }) const { text0 } = init(tc, { users: 1 })
text0.applyDelta([{ text0.applyDelta([{
insert: {linebreak: 's'} insert: { linebreak: 's' }
}]) }])
t.compare(text0.toDelta(), [{ t.compare(text0.toDelta(), [{
insert: {linebreak: 's'} insert: { linebreak: 's' }
}]) }])
} }
@@ -127,7 +127,7 @@ export const testSnapshot = tc => {
delete v.attributes.ychange.user delete v.attributes.ychange.user
} }
}) })
t.compare(state2Diff, [{insert: 'a'}, {insert: 'x', attributes: {ychange: { state: 'added' }}}, {insert: 'b', attributes: {ychange: { state: 'removed' }}}, { insert: 'cd' }]) t.compare(state2Diff, [{ insert: 'a' }, { insert: 'x', attributes: { ychange: { type: 'added' } } }, { insert: 'b', attributes: { ychange: { type: 'removed' } } }, { insert: 'cd' }])
} }
/** /**

View File

@@ -38,7 +38,10 @@
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */ "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"paths": { "paths": {
"yjs": ["./src/index.js"] "yjs": ["./src/index.js"],
"lib0/*": ["node_modules/lib0/*"],
"lib0/set.js": ["node_modules/lib0/set.js"],
"lib0/function.js": ["node_modules/lib0/function.js"]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */ // "typeRoots": [], /* List of folders to include type definitions from. */