Compare commits

..

128 Commits

Author SHA1 Message Date
Kevin Jahns
3f34777201 13.5.27 2022-02-04 12:42:59 +01:00
Kevin Jahns
24eddb2d75 fix concurrent formatting / cleanup bug 2022-02-04 12:41:13 +01:00
Kevin Jahns
8ce107bd17 Merge branch 'ja2nicholl-update-attributes-on-delete' 2022-02-04 11:27:07 +01:00
Kevin Jahns
2d1e3fde43 fixed edge formatting case 2022-02-04 11:26:32 +01:00
Kevin Jahns
04009f0d42 Merge branch 'update-attributes-on-delete' of git://github.com/ja2nicholl/yjs into ja2nicholl-update-attributes-on-delete 2022-02-04 10:39:41 +01:00
Kevin Jahns
d69d93f812 13.5.26 2022-02-03 21:38:48 +01:00
Kevin Jahns
931a37a331 only fire stack-item-added when and item was actually added. closes #368 2022-02-03 21:36:54 +01:00
Kevin Jahns
0ec2753313 13.5.25 2022-02-03 21:27:16 +01:00
Kevin Jahns
8fd1f3405a lint 2022-02-03 21:25:29 +01:00
Kevin Jahns
f577a8e3cf Merge pull request #366 from holtwick/main
Add canUndo/canRedo to UndoManager. Fixes #365
2022-02-03 21:21:33 +01:00
Jeremy Nicholl
84e95f11cb Fix formatting 2022-02-03 15:19:57 -05:00
Kevin Jahns
f08682ddfd Merge branch 'main' of github.com:yjs/yjs 2022-02-03 21:15:18 +01:00
Kevin Jahns
c20d72b886 Merge pull request #370 from doodlewind/typo
fix minor typos
2022-02-03 21:14:02 +01:00
Kevin Jahns
c9414f51a7 Merge pull request #387 from YousefED/patch-3
Update README.md (syncedstore svelte)
2022-02-03 21:12:19 +01:00
Kevin Jahns
0fee9dfff4 remove sponsoring message 2022-02-03 21:11:40 +01:00
Kevin Jahns
4cfa49d601 reproduce and fix issues #355 #343 #304 and closes #367 2022-02-03 21:10:24 +01:00
Yousef
b6562f3e80 Update README.md 2022-02-01 16:54:06 +01:00
Jeremy Nicholl
164b38f0cd Avoid copying attribute map when deleting
Calling cleanupFormattingGap should not make a copy of the
attributes because it needs to be able to update them.
2022-01-31 14:49:16 -05:00
Kevin Jahns
99326f67b8 Closes #377 2022-01-25 12:31:22 +01:00
Kevin Jahns
1c360f9f59 Merge pull request #383 from boschDev/main
Update Pluxbox RadioManager location
2022-01-25 12:27:55 +01:00
Roeland Bosch
8f0d7cdfc2 Update Pluxbox RadioManager location 2022-01-24 16:55:02 +01:00
Kevin Jahns
b281277c67 Merge pull request #380 from carloslfu/patch-1
fix typo in README
2022-01-21 00:52:33 +01:00
Carlos Galarza
532d5fccb2 fix typo in README 2022-01-20 17:32:02 -05:00
Kevin Jahns
8f421a0f42 Merge pull request #378 from YousefED/patch-2
Update README.md (matrix-crdt)
2022-01-18 19:08:55 +01:00
Yousef
8fec835338 Update README.md 2022-01-18 18:41:06 +01:00
Kevin Jahns
6403bc2bb5 13.5.24 2022-01-11 21:55:31 +01:00
Kevin Jahns
20e1234af2 implement performant push in arrays 2022-01-11 21:53:20 +01:00
Kevin Jahns
3aebb8db83 correct typings for a test 2022-01-10 22:04:28 +01:00
Yifeng Wang
51bb732606 fix minor typos 2022-01-05 01:20:57 +08:00
Dirk Holtwick
f857345451 Add canUndo/canRedo to UndoManager. Fixes #365 2021-12-18 18:26:32 +01:00
Kevin Jahns
645f05b0bb 13.5.23 2021-12-15 18:48:10 +01:00
Kevin Jahns
1cf709093c export V1 ⇔ V2 update format conversion. Closes #363 2021-12-15 18:45:08 +01:00
Kevin Jahns
9569d3e297 Merge pull request #360 from MarcoPolo/patch-1
Add a new provider, y-libp2p
2021-12-12 21:37:25 +01:00
Marco Munizaga
507edccdf8 Add a new provider, y-libp2p 2021-12-10 15:29:49 -08:00
Kevin Jahns
9914f48a52 Merge pull request #359 from YousefED/patch-1
Add SyncedStore to readme
2021-12-09 11:33:22 +01:00
Yousef
d57629b36d Add SyncedStore to readme
Not sure what the preferred way to share it would be, for now I added it to the Bindings :)
2021-12-08 14:35:12 +01:00
Kevin Jahns
294ba351b6 Merge branch 'main' of github.com:yjs/yjs 2021-11-27 11:38:00 +01:00
Kevin Jahns
610e532868 add test case for nested Y.Text as embeds. 2021-11-27 11:37:11 +01:00
Kevin Jahns
f73fb4796b Merge pull request #351 from haved/remove-cattaz-link
Remove broken link to terminated cattaz website
2021-11-27 10:22:17 +01:00
HKrogstie
32d391d7ab remove broken link to terminated cattaz
Someone else bought the domain and redirects it to a crummy google results reskin with ads.

According to the FujitsuLaboratories/cattaz repo, the website has been terminated.
2021-11-26 19:09:45 +01:00
Kevin Jahns
28e1b19e57 add loading event logic 2021-11-24 23:15:55 +01:00
Kevin Jahns
e90d9de5ed 13.5.22 2021-11-19 13:48:52 +01:00
Kevin Jahns
9a7250f192 fix undoing of content containing subdocs 2021-11-19 13:47:10 +01:00
Kevin Jahns
4154b12f14 handle local/remote autoload edge cases 2021-11-19 13:27:14 +01:00
Kevin Jahns
9df5016667 13.5.21 2021-11-15 14:00:04 +01:00
Kevin Jahns
1becaccdd9 bump lib0 dependency for bugfix dmonad/lib0#34 2021-11-15 13:58:22 +01:00
Kevin Jahns
ea4e9a0007 change order of logging statement for debugging 2021-11-14 13:10:52 +01:00
Kevin Jahns
a4e48d1ddf 13.5.20 2021-11-09 16:53:35 +01:00
Kevin Jahns
0a39a92b33 export testHelper 2021-11-09 16:51:54 +01:00
Kevin Jahns
bd819243eb 13.5.19 2021-11-09 16:39:58 +01:00
Kevin Jahns
2ec19defcb export testHelper esm properly 2021-11-06 15:55:59 +01:00
Kevin Jahns
336f7b1b1d 13.5.18 2021-11-06 14:39:55 +01:00
Kevin Jahns
8abf5b85ff fix #344 - formatting attribute assign bug 2021-11-06 14:35:04 +01:00
Kevin Jahns
320e8cbe18 add transaction to subdocs event 2021-11-02 23:24:28 +01:00
Kevin Jahns
49150f4adb add ydoc as argument in subdocs event 2021-10-29 22:04:59 +02:00
Kevin Jahns
e22fed7af3 13.5.17 2021-10-29 21:55:55 +02:00
Kevin Jahns
c91945228f inherid collectionid 2021-10-29 21:53:21 +02:00
Kevin Jahns
3586d91925 fire subdocs event only when something changed 2021-10-29 17:49:30 +02:00
Kevin Jahns
f915ebda1b 13.5.16 2021-10-15 19:18:51 +02:00
Kevin Jahns
a9b92b9099 13.5.15 2021-10-15 19:17:08 +02:00
Kevin Jahns
cbddf6ef90 add warning when Yjs was already imported 2021-10-15 19:10:11 +02:00
Kevin Jahns
491cd422c4 13.5.14 2021-10-14 16:21:02 +02:00
Kevin Jahns
4b88e2aac5 update dependencies 2021-10-14 16:19:24 +02:00
Kevin Jahns
e33c67fc72 bump standard linter 2021-10-14 16:18:50 +02:00
Kevin Jahns
085dda4cbd fix formatting test case #326 2021-10-14 16:09:23 +02:00
Kevin Jahns
f382846874 Merge pull request #326 from raedle/main
[tests] Encode/decode doc with attribute changes
2021-10-14 15:13:32 +02:00
Kevin Jahns
9afc5cf615 Merge pull request #331 from thomaswelter/main
Add support for null values in Y.Map and Y.Array
2021-10-14 15:08:20 +02:00
Kevin Jahns
ca0fb4b15d Merge pull request #332 from yjs/types-as-embeds
Allow types as Y.Text embeds
2021-10-14 15:04:26 +02:00
Kevin Jahns
d369a771a9 Merge pull request #335 from benmerckx/fix/array_map_dts
Remove T from JSDoc template, fixes #334
2021-10-14 15:00:38 +02:00
Kevin Jahns
995fbfa4cc Proper follow redones in nested redos - fixes #317 2021-10-14 14:59:26 +02:00
Kevin Jahns
7486ea7148 Merge pull request #339 from ViktorQvarfordt/patch-1
Fix type annotation of getMap
2021-10-12 22:26:54 +02:00
Viktor Qvarfordt
2c80a955da Fix type annotation of getMap
Make `getMap<T>()` take a generic type parameter just like `getArray<T>()`.
2021-10-10 11:05:18 +02:00
Kevin Jahns
233872493b Merge pull request #338 from nikgraf/patch-2
escape the plus to not interpret it as a list item
2021-10-08 17:53:58 +02:00
Nik Graf
64d164a904 escape the plus to not interpret it as a list item 2021-10-08 17:09:20 +02:00
Kevin Jahns
a08e54c2fc 13.5.13 2021-10-07 11:34:29 +02:00
Kevin Jahns
2b377cd46d export findIndexSS 2021-10-07 11:31:40 +02:00
Ben Merckx
b4b8927550 Remove T from JSDoc template, fixes #334 2021-09-28 12:36:01 +02:00
Kevin Jahns
b2761b50f2 more complex embed test 2021-09-25 11:58:39 +02:00
Kevin Jahns
28a9ce962d import from internals 2021-09-25 11:53:16 +02:00
Kevin Jahns
0ec67170d3 allow types as Y.Text embeds 2021-09-25 11:51:08 +02:00
Kevin Jahns
df9bfbe778 Merge branch 'main' of github.com:yjs/yjs 2021-09-23 22:14:42 +02:00
Kevin Jahns
f1ab417570 mergeUpdates on array with single entry just returns the first entry 2021-09-23 22:14:29 +02:00
Thomas Welter
4922eeac56 Add support for null values in Y.Map and Y.Array 2021-09-21 14:23:58 +02:00
Roman Rädle
57d6c6f831 [tests] Encode/decode doc with attribute changes
Encode a document with a text and decode it into a new document. Then, test if the same change to both documents results in the same text deltas.
2021-08-21 20:18:23 -07:00
Kevin Jahns
371f2b6d55 Merge pull request #318 from dai-shi/patch-1
Add valtio-yjs binding in README
2021-08-15 16:59:57 +02:00
Kevin Jahns
85a7ad148f Merge pull request #323 from vivaxy/bugfix/readme
Update an algorithm example
2021-08-09 15:05:33 +02:00
vivaxy
7ec1b3a19e docs: update an algorithm example 2021-08-09 19:33:45 +08:00
Kevin Jahns
633eb9033c 13.5.12 2021-08-02 16:48:25 +02:00
Kevin Jahns
4707fc46ac fix formatting bug. fixes #319 closes #320 2021-08-02 16:43:25 +02:00
Kevin Jahns
89b4320a8e fix test in #310 2021-08-02 16:20:01 +02:00
Kevin Jahns
0ea0a35521 Merge pull request #310 from SamDuvall/fix-xml-clone
Fix XmlElement.clone and XmlFragment.clone
2021-08-02 16:12:39 +02:00
Daishi Kato
15ea4ee805 Update README.md 2021-07-25 10:08:43 +09:00
Kevin Jahns
744469d363 13.5.11 2021-06-24 17:00:48 +02:00
Kevin Jahns
311dd50f1b array.insert throws length-exceeded event - fixes #314 2021-06-24 16:50:25 +02:00
Kevin Jahns
89c5541ee6 fix merge logic edge-cases with skips 2021-06-24 15:14:49 +02:00
Kevin Jahns
28d8db86f0 Merge branch 'main' of github.com:yjs/yjs into main 2021-06-21 12:05:09 +02:00
Kevin Jahns
0c34216ed0 merge pending structs in v1 format 2021-06-21 12:04:40 +02:00
Kevin Jahns
9aa518bc14 Merge pull request #313 from YousefED/hasAttribute
implement hasAttribute
2021-06-16 02:59:00 +02:00
Kevin Jahns
27b1190a28 Merge pull request #312 from whawker/console-log-removal
remove console.log added in #309
2021-06-16 01:48:22 +02:00
Yousef El-Dardiry
f3d8db491b implement hasAttribute 2021-06-15 16:36:22 +02:00
Will Hawker
e9905602f8 remove console.log debug artifact added in #309 2021-06-11 09:30:54 +01:00
Kevin Jahns
2b8154fa16 Merge pull request #309 from whawker/map-clear
add `clear` to YMap
2021-06-09 16:07:44 +02:00
Sam Duvall
5ddb7eefed Fix YXmlElement.clone and YXmlFragment.clone 2021-06-08 21:30:08 -04:00
Will Hawker
4b35de5ad5 add clear to YMap to remove all elements 2021-06-08 21:09:44 +01:00
Kevin Jahns
097b9e8208 13.5.10 2021-06-07 19:43:44 +02:00
Kevin Jahns
5cac153a17 Fix #308 - stateVector should ignore skips and incomplete content 2021-06-07 19:41:54 +02:00
Kevin Jahns
a7e4724edd 13.5.9 2021-05-31 17:56:10 +02:00
Kevin Jahns
71d8da6513 force that transactions that apply document updates are set as non-local transatctions. Fixes #307 2021-05-31 17:54:24 +02:00
Kevin Jahns
c72ac448e9 13.5.8 2021-05-25 21:25:00 +02:00
Kevin Jahns
da21fca334 add countable check to search_marker update 2021-05-25 21:23:12 +02:00
Kevin Jahns
d80512d690 13.5.7 2021-05-25 21:21:31 +02:00
Kevin Jahns
6886881b76 fix #297 (length not updated) by updating search markers properly 2021-05-25 21:17:01 +02:00
Kevin Jahns
dc9717ecd0 13.5.6 2021-05-14 18:57:14 +02:00
Kevin Jahns
7bd764fba7 use non-explicit resolution for lib0 2021-05-14 18:53:24 +02:00
Kevin Jahns
4047890a6e Merge pull request #299 from mgielda/patch-1
Fix broken link for All Done
2021-05-14 01:08:56 +02:00
Kevin Jahns
1627e7b3f6 Merge pull request #292 from nikgraf/patch-1
add Serenity Notes to Who is using Yjs
2021-05-14 01:05:26 +02:00
Kevin Jahns
e55d3cc510 Merge pull request #294 from cindywu/patch-1
Fix typo in README
2021-05-14 01:04:53 +02:00
Kevin Jahns
55bd0b16f7 Merge pull request #295 from getflourish/patch-1
Fix a small typo
2021-05-14 01:04:11 +02:00
Kevin Jahns
ab7de51064 Merge pull request #296 from manstie/patch-1
fix readme typo
2021-05-14 01:03:39 +02:00
Michael Gielda
d4917bb567 Fix broken link for All Done
Signed-off-by: Michael Gielda <mgielda@antmicro.com>
2021-05-08 08:29:46 +02:00
Kevin Jahns
4e343ccace remove unnecessary if-conditition (reminiscent from a previous Yjs version) 2021-04-29 20:25:57 +02:00
manstie
4efd47447b fix readme typo 2021-04-26 18:09:58 +08:00
Florian Schulz
5aa1aaebb3 Fix a small typo 2021-04-22 14:25:53 +02:00
cindy
7656f897d6 Fix typo in README 2021-04-14 17:53:25 -10:00
Kevin Jahns
5244755879 13.5.5 2021-04-13 22:06:55 +02:00
Kevin Jahns
3a7a324a24 fix #291 2021-04-13 22:05:30 +02:00
Kevin Jahns
9e98fec504 Perform undo until change was applied 2021-04-13 13:04:49 +02:00
Nik Graf
b1c7022890 add Serenity Notes to Who is using Yjs 2021-04-12 20:57:16 +02:00
50 changed files with 2432 additions and 1278 deletions

View File

@@ -34,7 +34,7 @@ Each item in a Yjs list is made up of two objects:
- An `Item` (*src/structs/Item.js*). This is used to relate the item to other - An `Item` (*src/structs/Item.js*). This is used to relate the item to other
adjacent items. adjacent items.
- An object in the `AbstractType` heirachy (subclasses of - An object in the `AbstractType` hierarchy (subclasses of
*src/types/AbstractType.js* - eg `YText`). This stores the actual content in *src/types/AbstractType.js* - eg `YText`). This stores the actual content in
the Yjs document. the Yjs document.
@@ -153,7 +153,7 @@ an incremental document updates that allows clients to sync with each other.
The update object is an Uint8Array that efficiently encodes `Item` objects and The update object is an Uint8Array that efficiently encodes `Item` objects and
the delete set. the delete set.
* `state vector`: A state vector defines the know state of each user (a set of * `state vector`: A state vector defines the know state of each user (a set of
tubles `(client, clock)`). This object is also efficiently encoded as a tuples `(client, clock)`). This object is also efficiently encoded as a
Uint8Array. Uint8Array.
The client can ask a remote client for missing document updates by sending The client can ask a remote client for missing document updates by sending
@@ -168,7 +168,7 @@ An implementation of the syncing process is in
## Snapshots ## Snapshots
A snapshot can be used to restore an old document state. It is a `state vector` A snapshot can be used to restore an old document state. It is a `state vector`
+ `delete set`. I client can restore an old document state by iterating through \+ `delete set`. A client can restore an old document state by iterating through
the sequence CRDT and ignoring all Items that have an `id.clock > the sequence CRDT and ignoring all Items that have an `id.clock >
stateVector[id.client].clock`. Instead of using `item.deleted` the client will stateVector[id.client].clock`. Instead of using `item.deleted` the client will
use the delete set to find out if an item was deleted or not. use the delete set to find out if an item was deleted or not.

View File

@@ -49,23 +49,19 @@ Sponsorship also comes with special perks! [![Become a Sponsor](https://img.shie
## Who is using Yjs ## Who is using Yjs
* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted
collaborative notes app.
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and * [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
community. :star2: community. :star2:
* [Input](https://input.com/) A collaborative note taking app. :star2: * [Input](https://input.com/) A collaborative note taking app. :star2:
* [Room.sh](https://room.sh/) A meeting application with integrated * [Room.sh](https://room.sh/) A meeting application with integrated
collaborative drawing, editing, and coding tools. :star: collaborative drawing, editing, and coding tools. :star:
* [https://coronavirustechhandbook.com/](https://coronavirustechhandbook.com/)
A collaborative wiki that is edited by thousands of different people to work
on a rapid and sophisticated response to the coronavirus outbreak and
subsequent impacts. :star:
* [Nimbus Note](https://nimbusweb.me/note.php) A note-taking app designed by * [Nimbus Note](https://nimbusweb.me/note.php) A note-taking app designed by
Nimbus Web. Nimbus Web.
* [JoeDocs](https://joedocs.com/) An open collaborative wiki. * [JoeDocs](https://joedocs.com/) An open collaborative wiki.
* [Pluxbox RadioManager](https://pluxbox.com/) A web-based app to * [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to
collaboratively organize radio broadcasts. collaboratively organize radio broadcasts.
* [Cattaz](http://cattaz.io/) A wiki that can run custom applications in the * [Alldone](https://alldone.app/) A next-gen project management and
wiki pages.
* [Alldone](https://alldoneapp.com/) A next-gen project management and
collaboration platform. collaboration platform.
## Table of Contents ## Table of Contents
@@ -98,6 +94,8 @@ are implemented in separate modules.
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) | | [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) | | [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) | | [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |
### Providers ### Providers
@@ -126,13 +124,28 @@ leveldb database.
Efficiently persists document updates to the browsers indexeddb database. Efficiently persists document updates to the browsers indexeddb database.
The document is immediately available and only diffs need to be synced through the The document is immediately available and only diffs need to be synced through the
network provider. network provider.
</dd>
<dt><a href="https://github.com/marcopolo/y-libp2p">y-libp2p</a></dt>
<dd>
Uses <a href="https://libp2p.io/">libp2p</a> to propagate updates via
<a href="https://github.com/libp2p/specs/tree/master/pubsub/gossipsub">GossipSub</a>.
Also includes a peer-sync mechanism to catch up on missed updates.
</dd> </dd>
<dt><a href="https://github.com/yjs/y-dat">y-dat</a></dt> <dt><a href="https://github.com/yjs/y-dat">y-dat</a></dt>
<dd> <dd>
[WIP] Write document updates effinciently to the dat network using [WIP] Write document updates efficiently to the dat network using
<a href="https://github.com/kappa-db/multifeed">multifeed</a>. Each client has <a href="https://github.com/kappa-db/multifeed">multifeed</a>. Each client has
an append-only log of CRDT local updates (hypercore). Multifeed manages and sync an append-only log of CRDT local updates (hypercore). Multifeed manages and sync
hypercores and y-dat listens to changes and applies them to the Yjs document. hypercores and y-dat listens to changes and applies them to the Yjs document.
</dd>
<dt><a href="https://github.com/yousefED/matrix-crdt">Matrix-CRDT</a></dt>
<dd>
Use <a href="https://www.matrix.org">Matrix</a> as an off-the-shelf backend for
Yjs by using the <a href="https://github.com/yousefED/matrix-crdt">MatrixProvider</a>.
Use Matrix as transport and storage of Yjs updates, so you can focus building
your client app and Matrix can provide powerful features like Authentication,
Authorization, Federation, hosting (self-hosting or SaaS) and even End-to-End
Encryption (E2EE).
</dd> </dd>
</dl> </dl>
@@ -247,36 +260,36 @@ necessary.
<dl> <dl>
<b><code>parent:Y.AbstractType|null</code></b> <b><code>parent:Y.AbstractType|null</code></b>
<dd></dd> <dd></dd>
<b><code>insert(index:number, content:Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b> <b><code>insert(index:number, content:Array&lt;object|boolean|Array|string|number|null|Uint8Array|Y.Type&gt;)</code></b>
<dd> <dd>
Insert content at <var>index</var>. Note that content is an array of elements. Insert content at <var>index</var>. Note that content is an array of elements.
I.e. <code>array.insert(0, [1])</code> splices the list and inserts 1 at I.e. <code>array.insert(0, [1])</code> splices the list and inserts 1 at
position 0. position 0.
</dd> </dd>
<b><code>push(Array&lt;Object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b> <b><code>push(Array&lt;Object|boolean|Array|string|number|null|Uint8Array|Y.Type&gt;)</code></b>
<dd></dd> <dd></dd>
<b><code>unshift(Array&lt;Object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b> <b><code>unshift(Array&lt;Object|boolean|Array|string|number|null|Uint8Array|Y.Type&gt;)</code></b>
<dd></dd> <dd></dd>
<b><code>delete(index:number, length:number)</code></b> <b><code>delete(index:number, length:number)</code></b>
<dd></dd> <dd></dd>
<b><code>get(index:number)</code></b> <b><code>get(index:number)</code></b>
<dd></dd> <dd></dd>
<b><code>slice(start:number, end:number):Array&lt;Object|boolean|Array|string|number|Uint8Array|Y.Type&gt;</code></b> <b><code>slice(start:number, end:number):Array&lt;Object|boolean|Array|string|number|null|Uint8Array|Y.Type&gt;</code></b>
<dd>Retrieve a range of content</dd> <dd>Retrieve a range of content</dd>
<b><code>length:number</code></b> <b><code>length:number</code></b>
<dd></dd> <dd></dd>
<b> <b>
<code> <code>
forEach(function(value:object|boolean|Array|string|number|Uint8Array|Y.Type, forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type,
index:number, array: Y.Array)) index:number, array: Y.Array))
</code> </code>
</b> </b>
<dd></dd> <dd></dd>
<b><code>map(function(T, number, YArray):M):Array&lt;M&gt;</code></b> <b><code>map(function(T, number, YArray):M):Array&lt;M&gt;</code></b>
<dd></dd> <dd></dd>
<b><code>toArray():Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;</code></b> <b><code>toArray():Array&lt;object|boolean|Array|string|number|null|Uint8Array|Y.Type&gt;</code></b>
<dd>Copies the content of this YArray to a new Array.</dd> <dd>Copies the content of this YArray to a new Array.</dd>
<b><code>toJSON():Array&lt;Object|boolean|Array|string|number&gt;</code></b> <b><code>toJSON():Array&lt;Object|boolean|Array|string|number|null&gt;</code></b>
<dd> <dd>
Copies the content of this YArray to a new Array. It transforms all child types Copies the content of this YArray to a new Array. It transforms all child types
to JSON using their <code>toJSON</code> method. to JSON using their <code>toJSON</code> method.
@@ -322,9 +335,9 @@ or any of its children.
<dd></dd> <dd></dd>
<b><code>size: number</code></b> <b><code>size: number</code></b>
<dd>Total number of key/value pairs.</dd> <dd>Total number of key/value pairs.</dd>
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b> <b><code>get(key:string):object|boolean|string|number|null|Uint8Array|Y.Type</code></b>
<dd></dd> <dd></dd>
<b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b> <b><code>set(key:string, value:object|boolean|string|number|null|Uint8Array|Y.Type)</code></b>
<dd></dd> <dd></dd>
<b><code>delete(key:string)</code></b> <b><code>delete(key:string)</code></b>
<dd></dd> <dd></dd>
@@ -332,14 +345,16 @@ or any of its children.
<dd></dd> <dd></dd>
<b><code>get(index:number)</code></b> <b><code>get(index:number)</code></b>
<dd></dd> <dd></dd>
<b><code>clear()</code></b>
<dd>Removes all elements from this YMap.</dd>
<b><code>clone():Y.Map</code></b> <b><code>clone():Y.Map</code></b>
<dd>Clone this type into a fresh Yjs type.</dd> <dd>Clone this type into a fresh Yjs type.</dd>
<b><code>toJSON():Object&lt;string, Object|boolean|Array|string|number|Uint8Array&gt;</code></b> <b><code>toJSON():Object&lt;string, Object|boolean|Array|string|number|null|Uint8Array&gt;</code></b>
<dd> <dd>
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(value:object|boolean|Array|string|number|Uint8Array|Y.Type, <b><code>forEach(function(value:object|boolean|Array|string|number|null|Uint8Array|Y.Type,
key:string, map: Y.Map))</code></b> 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.
@@ -709,7 +724,7 @@ Y.applyUpdate(ydoc1, diff2)
Y.applyUpdate(ydoc2, diff1) Y.applyUpdate(ydoc2, diff1)
``` ```
### Example: Syncing clients without loading the Y.Doc #### Example: Syncing clients without loading the Y.Doc
It is possible to sync clients and compute delta updates without loading the Yjs It is possible to sync clients and compute delta updates without loading the Yjs
document to memory. Yjs exposes an API to compute the differences directly on the document to memory. Yjs exposes an API to compute the differences directly on the
@@ -733,6 +748,17 @@ currentState1 = Y.mergeUpdates([currentState1, diff2])
currentState1 = Y.mergeUpdates([currentState1, diff1]) currentState1 = Y.mergeUpdates([currentState1, diff1])
``` ```
#### Using V2 update format
Yjs implements two update formats. By default you are using the V1 update format.
You can opt-in into the V2 update format wich provides much better compression.
It is not yet used by all providers. However, you can already use it if
you are building your own provider. All below functions are available with the
suffix "V2". E.g. `Y.applyUpdate``Y.applyUpdateV2`. We also support conversion
functions between both formats: `Y.convertUpdateFormatV1ToV2` & `Y.convertUpdateFormatV2ToV1`.
#### Update API
<dl> <dl>
<b><code>Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])</code></b> <b><code>Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])</code></b>
<dd> <dd>
@@ -765,6 +791,14 @@ Encode the missing differences to another update message. This function works
similarly to <code>Y.encodeStateAsUpdate(ydoc, stateVector)</code> but works similarly to <code>Y.encodeStateAsUpdate(ydoc, stateVector)</code> but works
on updates instead. on updates instead.
</dd> </dd>
<b><code>convertUpdateFormatV1ToV2</code></b>
<dd>
Convert V1 update format to the V2 update format.
</dd>
<b><code>convertUpdateFormatV2ToV1</code></b>
<dd>
Convert V2 update format to the V1 update format.
</dd>
</dl> </dl>
### Relative Positions ### Relative Positions
@@ -815,7 +849,7 @@ pos.index === 2 // => true
### Y.UndoManager ### Y.UndoManager
Yjs ships with an Undo/Redo manager for selective undo/redo of of changes on a Yjs ships with an Undo/Redo manager for selective undo/redo of changes on a
Yjs type. The changes can be optionally scoped to transaction origins. Yjs type. The changes can be optionally scoped to transaction origins.
```js ```js
@@ -951,7 +985,7 @@ undoManager.on('stack-item-popped', event => {
*Conflict-free replicated data types* (CRDT) for collaborative editing are an *Conflict-free replicated data types* (CRDT) for collaborative editing are an
alternative approach to *operational transformation* (OT). A very simple alternative approach to *operational transformation* (OT). A very simple
differenciation between the two approaches is that OT attempts to transform differentiation between the two approaches is that OT attempts to transform
index positions to ensure convergence (all clients end up with the same index positions to ensure convergence (all clients end up with the same
content), while CRDTs use mathematical models that usually do not involve index content), while CRDTs use mathematical models that usually do not involve index
transformations, like linked lists. OT is currently the de-facto standard for transformations, like linked lists. OT is currently the de-facto standard for
@@ -984,7 +1018,7 @@ order of the structs anymore (e.g. if the parent was deleted).
**Examples:** **Examples:**
1. If a user inserts elements in sequence, the struct will be merged into a 1. If a user inserts elements in sequence, the struct will be merged into a
single struct. E.g. `array.insert(0, ['a']), array.insert(0, ['b']);` is single struct. E.g. `text.insert(0, 'a'), text.insert(1, 'b');` is
first represented as two structs (`[{id: {client, clock: 0}, content: 'a'}, first represented as two structs (`[{id: {client, clock: 0}, content: 'a'},
{id: {client, clock: 1}, content: 'b'}`) and then merged into a single {id: {client, clock: 1}, content: 'b'}`) and then merged into a single
struct: `[{id: {client, clock: 0}, content: 'ab'}]`. struct: `[{id: {client, clock: 0}, content: 'ab'}]`.

1796
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.5.4", "version": "13.5.27",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.cjs", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",
"types": "./dist/src/index.d.ts", "types": "./dist/src/index.d.ts",
"type": "module",
"sideEffects": false, "sideEffects": false,
"funding": { "funding": {
"type": "GitHub Sponsors ❤", "type": "GitHub Sponsors ❤",
@@ -21,8 +22,7 @@
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repetition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs", "preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repetition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs",
"debug": "concurrently 'http-server -o test.html' 'npm run watch'", "debug": "concurrently 'http-server -o test.html' 'npm run watch'",
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs", "trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs", "trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs"
"postinstall": "node ./sponsor-y.js"
}, },
"exports": { "exports": {
".": { ".": {
@@ -31,6 +31,7 @@
}, },
"./src/index.js": "./src/index.js", "./src/index.js": "./src/index.js",
"./tests/testHelper.js": "./tests/testHelper.js", "./tests/testHelper.js": "./tests/testHelper.js",
"./testHelper": "./dist/testHelper.mjs",
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"files": [ "files": [
@@ -38,6 +39,7 @@
"dist/src", "dist/src",
"src", "src",
"tests/testHelper.js", "tests/testHelper.js",
"dist/testHelper.mjs",
"sponsor-y.js" "sponsor-y.js"
], ],
"dictionaries": { "dictionaries": {
@@ -58,7 +60,8 @@
"Yjs", "Yjs",
"CRDT", "CRDT",
"offline", "offline",
"shared editing", "offline-first",
"shared-editing",
"concurrency", "concurrency",
"collaboration" "collaboration"
], ],
@@ -70,19 +73,19 @@
}, },
"homepage": "https://docs.yjs.dev", "homepage": "https://docs.yjs.dev",
"dependencies": { "dependencies": {
"lib0": "^0.2.38" "lib0": "^0.2.43"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.2.0", "@rollup/plugin-node-resolve": "^11.2.1",
"concurrently": "^3.6.1", "concurrently": "^3.6.1",
"http-server": "^0.12.3", "http-server": "^0.12.3",
"jsdoc": "^3.6.6", "jsdoc": "^3.6.7",
"markdownlint-cli": "^0.23.2", "markdownlint-cli": "^0.23.2",
"rollup": "^2.39.0", "rollup": "^2.60.0",
"standard": "^14.3.4", "standard": "^16.0.4",
"tui-jsdoc-template": "^1.2.2", "tui-jsdoc-template": "^1.2.2",
"typescript": "^4.1.5", "typescript": "^4.4.4",
"y-protocols": "^1.0.4" "y-protocols": "^1.0.5"
} }
} }

View File

@@ -45,7 +45,7 @@ export default [{
sourcemap: true, sourcemap: true,
paths: path => { paths: path => {
if (/^lib0\//.test(path)) { if (/^lib0\//.test(path)) {
return `lib0/dist/${path.slice(5, -3)}.cjs` return `lib0/dist/${path.slice(5)}.cjs`
} }
return path return path
} }
@@ -60,6 +60,23 @@ export default [{
sourcemap: true sourcemap: true
}, },
external: id => /^lib0\//.test(id) external: id => /^lib0\//.test(id)
}, {
input: './tests/testHelper.js',
output: {
name: 'Y',
file: 'dist/testHelper.mjs',
format: 'esm',
sourcemap: true
},
external: id => /^lib0\//.test(id) || id === 'yjs',
plugins: [{
resolveId (importee) {
if (importee === '../src/index.js') {
return 'yjs'
}
return null
}
}]
}, { }, {
input: './tests/index.js', input: './tests/index.js',
output: { output: {

View File

@@ -1,12 +0,0 @@
try {
const log = require('lib0/dist/logging.cjs')
log.print()
log.print(log.BOLD, log.GREEN, log.BOLD, 'Thank you for using Yjs ', log.RED, '❤\n')
log.print(
log.GREY,
'The project has grown considerably in the past year. Too much for me to maintain\nin my spare time. Several companies built their products with Yjs.\nYet, this project receives very little funding. Yjs is far from done. I want to\ncreate more awesome extensions and work on the growing number of open issues.\n', log.BOLD, 'Dear user, the future of this project entirely depends on you.\n')
log.print(log.BLUE, log.BOLD, 'Please start funding the project now: https://github.com/sponsors/dmonad \n')
log.print(log.GREY, '(This message will be removed when I achieved my funding goal)\n\n')
} catch (e) { }

View File

@@ -1,3 +1,4 @@
/** eslint-env browser */
export { export {
Doc, Doc,
@@ -26,12 +27,13 @@ export {
ContentString, ContentString,
ContentType, ContentType,
AbstractType, AbstractType,
RelativePosition,
getTypeChildren, getTypeChildren,
createRelativePositionFromTypeIndex, createRelativePositionFromTypeIndex,
createRelativePositionFromJSON, createRelativePositionFromJSON,
createAbsolutePositionFromRelativePosition, createAbsolutePositionFromRelativePosition,
compareRelativePositions, compareRelativePositions,
AbsolutePosition,
RelativePosition,
ID, ID,
createID, createID,
compareIDs, compareIDs,
@@ -40,9 +42,11 @@ export {
createSnapshot, createSnapshot,
createDeleteSet, createDeleteSet,
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
cleanupYTextFormatting,
snapshot, snapshot,
emptySnapshot, emptySnapshot,
findRootTypeKey, findRootTypeKey,
findIndexSS,
getItem, getItem,
typeListToArraySnapshot, typeListToArraySnapshot,
typeMapGetSnapshot, typeMapGetSnapshot,
@@ -81,5 +85,31 @@ export {
encodeRelativePosition, encodeRelativePosition,
decodeRelativePosition, decodeRelativePosition,
diffUpdate, diffUpdate,
diffUpdateV2 diffUpdateV2,
convertUpdateFormatV1ToV2,
convertUpdateFormatV2ToV1
} from './internals.js' } from './internals.js'
const glo = /** @type {any} */ (typeof window !== 'undefined'
? window
// @ts-ignore
: typeof global !== 'undefined' ? global : {})
const importIdentifier = '__ $YJS$ __'
if (glo[importIdentifier] === true) {
/**
* Dear reader of this warning message. Please take this seriously.
*
* If you see this message, please make sure that you only import one version of Yjs. In many cases,
* your package manager installs two versions of Yjs that are used by different packages within your project.
* Another reason for this message is that some parts of your project use the commonjs version of Yjs
* and others use the EcmaScript version of Yjs.
*
* This often leads to issues that are hard to debug. We often need to perform constructor checks,
* e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to
* do the constructor checks anymore - which might break the CRDT algorithm.
*/
console.warn('Yjs was already imported. Importing different versions of Yjs often leads to issues.')
}
glo[importIdentifier] = true

View File

@@ -3,7 +3,7 @@ import {
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
export class AbstractStruct { export class AbstractStruct {
/** /**

View File

@@ -2,7 +2,7 @@ import {
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
export class ContentBinary { export class ContentBinary {
/** /**

View File

@@ -3,7 +3,13 @@ import {
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
/**
* @param {string} guid
* @param {Object<string, any>} opts
*/
const createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false })
/** /**
* @private * @private
@@ -61,7 +67,7 @@ export class ContentDoc {
* @return {ContentDoc} * @return {ContentDoc}
*/ */
copy () { copy () {
return new ContentDoc(this.doc) return new ContentDoc(createDocFromOpts(this.doc.guid, this.opts))
} }
/** /**
@@ -132,4 +138,4 @@ export class ContentDoc {
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentDoc} * @return {ContentDoc}
*/ */
export const readContentDoc = decoder => new ContentDoc(new Doc({ guid: decoder.readString(), ...decoder.readAny() })) export const readContentDoc = decoder => new ContentDoc(createDocFromOpts(decoder.readString(), decoder.readAny()))

View File

@@ -3,7 +3,7 @@ import {
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
/** /**
* @private * @private

View File

@@ -3,7 +3,7 @@ import {
AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
/** /**
* @private * @private

View File

@@ -10,7 +10,7 @@ import {
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
/** /**
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractType<any>>} * @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractType<any>>}
@@ -109,7 +109,7 @@ export class ContentType {
if (!item.deleted) { if (!item.deleted) {
item.delete(transaction) item.delete(transaction)
} else { } else {
// Whis will be gc'd later and we want to merge it if possible // This will be gc'd later and we want to merge it if possible
// We try to merge all deleted items after each transaction, // We try to merge all deleted items after each transaction,
// but we have no knowledge about that this needs to be merged // but we have no knowledge about that this needs to be merged
// since it is not in transaction.ds. Hence we add it to transaction._mergeStructs // since it is not in transaction.ds. Hence we add it to transaction._mergeStructs

View File

@@ -22,11 +22,12 @@ import {
readContentFormat, readContentFormat,
readContentType, readContentType,
addChangedTypeToTransaction, addChangedTypeToTransaction,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line isDeleted,
DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
import * as binary from 'lib0/binary.js' import * as binary from 'lib0/binary'
/** /**
* @todo This should return several items * @todo This should return several items
@@ -125,12 +126,13 @@ export const splitItem = (transaction, leftItem, diff) => {
* @param {Transaction} transaction The Yjs instance. * @param {Transaction} transaction The Yjs instance.
* @param {Item} item * @param {Item} item
* @param {Set<Item>} redoitems * @param {Set<Item>} redoitems
* @param {DeleteSet} itemsToDelete
* *
* @return {Item|null} * @return {Item|null}
* *
* @private * @private
*/ */
export const redoItem = (transaction, item, redoitems) => { export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
const doc = transaction.doc const doc = transaction.doc
const store = doc.store const store = doc.store
const ownClientID = doc.clientID const ownClientID = doc.clientID
@@ -142,42 +144,27 @@ export const redoItem = (transaction, item, redoitems) => {
/** /**
* @type {Item|null} * @type {Item|null}
*/ */
let left let left = null
/** /**
* @type {Item|null} * @type {Item|null}
*/ */
let right let right
// make sure that parent is redone
if (parentItem !== null && parentItem.deleted === true) {
// try to undo parent if it will be undone anyway
if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete) === null)) {
return null
}
while (parentItem.redone !== null) {
parentItem = getItemCleanStart(transaction, parentItem.redone)
}
}
const parentType = parentItem === null ? /** @type {AbstractType<any>} */ (item.parent) : /** @type {ContentType} */ (parentItem.content).type
if (item.parentSub === null) { if (item.parentSub === null) {
// Is an array item. Insert at the old position // Is an array item. Insert at the old position
left = item.left left = item.left
right = item right = item
} else {
// Is a map item. Insert as current value
left = item
while (left.right !== null) {
left = left.right
if (left.id.client !== ownClientID) {
// It is not possible to redo this item because it conflicts with a
// change from another client
return null
}
}
if (left.right !== null) {
left = /** @type {Item} */ (/** @type {AbstractType<any>} */ (item.parent)._map.get(item.parentSub))
}
right = null
}
// make sure that parent is redone
if (parentItem !== null && parentItem.deleted === true && parentItem.redone === null) {
// try to undo parent if it will be undone anyway
if (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems) === null) {
return null
}
}
if (parentItem !== null && parentItem.redone !== null) {
while (parentItem.redone !== null) {
parentItem = getItemCleanStart(transaction, parentItem.redone)
}
// find next cloned_redo items // find next cloned_redo items
while (left !== null) { while (left !== null) {
/** /**
@@ -209,6 +196,33 @@ export const redoItem = (transaction, item, redoitems) => {
} }
right = right.right right = right.right
} }
} else {
right = null
if (item.right) {
left = item
// Iterate right while right is in itemsToDelete
// If it is intended to delete right while item is redone, we can expect that item should replace right.
while (left !== null && left.right !== null && isDeleted(itemsToDelete, left.right.id)) {
left = left.right
}
// follow redone
// trace redone until parent matches
while (left !== null && left.redone !== null) {
left = getItemCleanStart(transaction, left.redone)
}
// check wether we were allowed to follow right (indicating that originally this op was replaced by another item)
if (left === null || /** @type {AbstractType<any>} */ (left.parent)._item !== parentItem) {
// invalid parent; should never happen
return null
}
if (left && left.right !== null) {
// It is not possible to redo this item because it conflicts with a
// change from another client
return null
}
} else {
left = parentType._map.get(item.parentSub) || null
}
} }
const nextClock = getState(store, ownClientID) const nextClock = getState(store, ownClientID)
const nextId = createID(ownClientID, nextClock) const nextId = createID(ownClientID, nextClock)
@@ -216,7 +230,7 @@ export const redoItem = (transaction, item, redoitems) => {
nextId, nextId,
left, left && left.lastId, left, left && left.lastId,
right, right && right.id, right, right && right.id,
parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type, parentType,
item.parentSub, item.parentSub,
item.content.copy() item.content.copy()
) )
@@ -275,7 +289,7 @@ export class Item extends AbstractStruct {
*/ */
this.parentSub = parentSub this.parentSub = parentSub
/** /**
* If this type's effect is reundone this type refers to the type that undid * If this type's effect is redone this type refers to the type that undid
* this operation. * this operation.
* @type {ID | null} * @type {ID | null}
*/ */
@@ -566,6 +580,19 @@ export class Item extends AbstractStruct {
this.content.constructor === right.content.constructor && this.content.constructor === right.content.constructor &&
this.content.mergeWith(right.content) this.content.mergeWith(right.content)
) { ) {
const searchMarker = /** @type {AbstractType<any>} */ (this.parent)._searchMarker
if (searchMarker) {
searchMarker.forEach(marker => {
if (marker.p === right) {
// right is going to be "forgotten" so we need to update the marker
marker.p = this
// adjust marker index
if (!this.deleted && this.countable) {
marker.index -= this.length
}
}
})
}
if (right.keep) { if (right.keep) {
this.keep = true this.keep = true
} }

View File

@@ -3,8 +3,8 @@ import {
AbstractStruct, AbstractStruct,
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding'
export const structSkipRefNumber = 10 export const structSkipRefNumber = 10

View File

@@ -14,10 +14,10 @@ import {
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map'
import * as iterator from 'lib0/iterator.js' import * as iterator from 'lib0/iterator'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
import * as math from 'lib0/math.js' import * as math from 'lib0/math'
const maxSearchMarker = 80 const maxSearchMarker = 80
@@ -623,7 +623,7 @@ export const typeListGet = (type, index) => {
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {Item?} referenceItem * @param {Item?} referenceItem
* @param {Array<Object<string,any>|Array<any>|boolean|number|string|Uint8Array>} content * @param {Array<Object<string,any>|Array<any>|boolean|number|null|string|Uint8Array>} content
* *
* @private * @private
* @function * @function
@@ -635,7 +635,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
const store = doc.store const store = doc.store
const right = referenceItem === null ? parent._start : referenceItem.right const right = referenceItem === null ? parent._start : referenceItem.right
/** /**
* @type {Array<Object|Array<any>|number>} * @type {Array<Object|Array<any>|number|null>}
*/ */
let jsonContent = [] let jsonContent = []
const packJsonContent = () => { const packJsonContent = () => {
@@ -646,49 +646,58 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
} }
} }
content.forEach(c => { content.forEach(c => {
switch (c.constructor) { if (c === null) {
case Number: jsonContent.push(c)
case Object: } else {
case Boolean: switch (c.constructor) {
case Array: case Number:
case String: case Object:
jsonContent.push(c) case Boolean:
break case Array:
default: case String:
packJsonContent() jsonContent.push(c)
switch (c.constructor) { break
case Uint8Array: default:
case ArrayBuffer: packJsonContent()
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c)))) switch (c.constructor) {
left.integrate(transaction, 0) case Uint8Array:
break case ArrayBuffer:
case Doc: left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c)))
left.integrate(transaction, 0)
break
default:
if (c instanceof AbstractType) {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c))
left.integrate(transaction, 0) left.integrate(transaction, 0)
} else { break
throw new Error('Unexpected content type in insert operation') case Doc:
} left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c)))
} left.integrate(transaction, 0)
break
default:
if (c instanceof AbstractType) {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c))
left.integrate(transaction, 0)
} else {
throw new Error('Unexpected content type in insert operation')
}
}
}
} }
}) })
packJsonContent() packJsonContent()
} }
const lengthExceeded = error.create('Length exceeded!')
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {number} index * @param {number} index
* @param {Array<Object<string,any>|Array<any>|number|string|Uint8Array>} content * @param {Array<Object<string,any>|Array<any>|number|null|string|Uint8Array>} content
* *
* @private * @private
* @function * @function
*/ */
export const typeListInsertGenerics = (transaction, parent, index, content) => { export const typeListInsertGenerics = (transaction, parent, index, content) => {
if (index > parent._length) {
throw lengthExceeded
}
if (index === 0) { if (index === 0) {
if (parent._searchMarker) { if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, index, content.length) updateMarkerChanges(parent._searchMarker, index, content.length)
@@ -726,6 +735,29 @@ export const typeListInsertGenerics = (transaction, parent, index, content) => {
return typeListInsertGenericsAfter(transaction, parent, n, content) return typeListInsertGenericsAfter(transaction, parent, n, content)
} }
/**
* Pushing content is special as we generally want to push after the last item. So we don't have to update
* the serach marker.
*
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {Array<Object<string,any>|Array<any>|number|null|string|Uint8Array>} content
*
* @private
* @function
*/
export const typeListPushGenerics = (transaction, parent, content) => {
// Use the marker with the highest index and iterate to the right.
const marker = (parent._searchMarker || []).reduce((maxMarker, currMarker) => currMarker.index > maxMarker.index ? currMarker : maxMarker, { index: 0, p: parent._start })
let n = marker.p
if (n) {
while (n.right) {
n = n.right
}
}
return typeListInsertGenericsAfter(transaction, parent, n, content)
}
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
@@ -766,7 +798,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
n = n.right n = n.right
} }
if (length > 0) { if (length > 0) {
throw error.create('array length exceeded') throw lengthExceeded
} }
if (parent._searchMarker) { if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */) updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */)
@@ -792,7 +824,7 @@ export const typeMapDelete = (transaction, parent, key) => {
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {string} key * @param {string} key
* @param {Object|number|Array<any>|string|Uint8Array|AbstractType<any>} value * @param {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} value
* *
* @private * @private
* @function * @function
@@ -833,7 +865,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
/** /**
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {string} key * @param {string} key
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined} * @return {Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
* *
* @private * @private
* @function * @function
@@ -845,7 +877,7 @@ export const typeMapGet = (parent, key) => {
/** /**
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @return {Object<string,Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined>} * @return {Object<string,Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
* *
* @private * @private
* @function * @function
@@ -880,7 +912,7 @@ export const typeMapHas = (parent, key) => {
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {string} key * @param {string} key
* @param {Snapshot} snapshot * @param {Snapshot} snapshot
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined} * @return {Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
* *
* @private * @private
* @function * @function

View File

@@ -10,6 +10,7 @@ import {
typeListForEach, typeListForEach,
typeListCreateIterator, typeListCreateIterator,
typeListInsertGenerics, typeListInsertGenerics,
typeListPushGenerics,
typeListDelete, typeListDelete,
typeListMap, typeListMap,
YArrayRefID, YArrayRefID,
@@ -142,9 +143,17 @@ export class YArray extends AbstractType {
* Appends content to this YArray. * Appends content to this YArray.
* *
* @param {Array<T>} content Array of content to append. * @param {Array<T>} content Array of content to append.
*
* @todo Use the following implementation in all types.
*/ */
push (content) { push (content) {
this.insert(this.length, content) if (this.doc !== null) {
transact(this.doc, transaction => {
typeListPushGenerics(transaction, this, content)
})
} else {
/** @type {Array<any>} */ (this._prelimContent).push(...content)
}
} }
/** /**
@@ -215,7 +224,7 @@ export class YArray extends AbstractType {
* Returns an Array with the result of calling a provided function on every * Returns an Array with the result of calling a provided function on every
* element of this YArray. * element of this YArray.
* *
* @template T,M * @template M
* @param {function(T,number,YArray<T>):M} f Function that produces an element of the new Array * @param {function(T,number,YArray<T>):M} f Function that produces an element of the new Array
* @return {Array<M>} A new array with each element being the result of the * @return {Array<M>} A new array with each element being the result of the
* callback function * callback function

View File

@@ -17,7 +17,7 @@ import {
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as iterator from 'lib0/iterator.js' import * as iterator from 'lib0/iterator'
/** /**
* @template T * @template T
@@ -36,11 +36,11 @@ export class YMapEvent extends YEvent {
} }
/** /**
* @template T number|string|Object|Array|Uint8Array * @template MapType
* A shared Map implementation. * A shared Map implementation.
* *
* @extends AbstractType<YMapEvent<T>> * @extends AbstractType<YMapEvent<MapType>>
* @implements {Iterable<T>} * @implements {Iterable<MapType>}
*/ */
export class YMap extends AbstractType { export class YMap extends AbstractType {
/** /**
@@ -85,7 +85,7 @@ export class YMap extends AbstractType {
} }
/** /**
* @return {YMap<T>} * @return {YMap<MapType>}
*/ */
clone () { clone () {
const map = new YMap() const map = new YMap()
@@ -108,11 +108,11 @@ export class YMap extends AbstractType {
/** /**
* Transforms this Shared Type to a JSON object. * Transforms this Shared Type to a JSON object.
* *
* @return {Object<string,T>} * @return {Object<string,any>}
*/ */
toJSON () { toJSON () {
/** /**
* @type {Object<string,T>} * @type {Object<string,MapType>}
*/ */
const map = {} const map = {}
this._map.forEach((item, key) => { this._map.forEach((item, key) => {
@@ -163,11 +163,11 @@ export class YMap extends AbstractType {
/** /**
* Executes a provided function on once on every key-value pair. * Executes a provided function on once on every key-value pair.
* *
* @param {function(T,string,YMap<T>):void} f A function to execute on every element of this YArray. * @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
*/ */
forEach (f) { forEach (f) {
/** /**
* @type {Object<string,T>} * @type {Object<string,MapType>}
*/ */
const map = {} const map = {}
this._map.forEach((item, key) => { this._map.forEach((item, key) => {
@@ -179,7 +179,7 @@ export class YMap extends AbstractType {
} }
/** /**
* @return {IterableIterator<T>} * @return {IterableIterator<MapType>}
*/ */
[Symbol.iterator] () { [Symbol.iterator] () {
return this.entries() return this.entries()
@@ -204,7 +204,7 @@ export class YMap extends AbstractType {
* Adds or updates an element with a specified key and value. * Adds or updates an element with a specified key and value.
* *
* @param {string} key The key of the element to add to this YMap * @param {string} key The key of the element to add to this YMap
* @param {T} value The value of the element to add * @param {MapType} value The value of the element to add
*/ */
set (key, value) { set (key, value) {
if (this.doc !== null) { if (this.doc !== null) {
@@ -221,7 +221,7 @@ export class YMap extends AbstractType {
* Returns a specified element from this YMap. * Returns a specified element from this YMap.
* *
* @param {string} key * @param {string} key
* @return {T|undefined} * @return {MapType|undefined}
*/ */
get (key) { get (key) {
return /** @type {any} */ (typeMapGet(this, key)) return /** @type {any} */ (typeMapGet(this, key))
@@ -237,6 +237,21 @@ export class YMap extends AbstractType {
return typeMapHas(this, key) return typeMapHas(this, key)
} }
/**
* Removes all elements from this YMap.
*/
clear () {
if (this.doc !== null) {
transact(this.doc, transaction => {
this.forEach(function (value, key, map) {
typeMapDelete(transaction, map, key)
})
})
} else {
/** @type {Map<string, any>} */ (this._prelimContent).clear()
}
}
/** /**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/ */

View File

@@ -26,12 +26,13 @@ import {
typeMapGet, typeMapGet,
typeMapGetAll, typeMapGetAll,
updateMarkerChanges, updateMarkerChanges,
ContentType,
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as object from 'lib0/object.js' import * as object from 'lib0/object'
import * as map from 'lib0/map.js' import * as map from 'lib0/map'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
/** /**
* @param {any} a * @param {any} a
@@ -62,17 +63,16 @@ export class ItemTextListPosition {
error.unexpectedCase() error.unexpectedCase()
} }
switch (this.right.content.constructor) { switch (this.right.content.constructor) {
case ContentEmbed:
case ContentString:
if (!this.right.deleted) {
this.index += this.right.length
}
break
case ContentFormat: case ContentFormat:
if (!this.right.deleted) { if (!this.right.deleted) {
updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content)) updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content))
} }
break break
default:
if (!this.right.deleted) {
this.index += this.right.length
}
break
} }
this.left = this.right this.left = this.right
this.right = this.right.right this.right = this.right.right
@@ -91,8 +91,12 @@ export class ItemTextListPosition {
const findNextPosition = (transaction, pos, count) => { const findNextPosition = (transaction, pos, count) => {
while (pos.right !== null && count > 0) { while (pos.right !== null && count > 0) {
switch (pos.right.content.constructor) { switch (pos.right.content.constructor) {
case ContentEmbed: case ContentFormat:
case ContentString: if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
}
break
default:
if (!pos.right.deleted) { if (!pos.right.deleted) {
if (count < pos.right.length) { if (count < pos.right.length) {
// split right // split right
@@ -102,11 +106,6 @@ const findNextPosition = (transaction, pos, count) => {
count -= pos.right.length count -= pos.right.length
} }
break break
case ContentFormat:
if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
}
break
} }
pos.left = pos.right pos.left = pos.right
pos.right = pos.right.right pos.right = pos.right.right
@@ -164,12 +163,13 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes
} }
const doc = transaction.doc const doc = transaction.doc
const ownClientId = doc.clientID const ownClientId = doc.clientID
let nextFormat = currPos.left
const right = currPos.right
negatedAttributes.forEach((val, key) => { negatedAttributes.forEach((val, key) => {
nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), nextFormat, nextFormat && nextFormat.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)) const left = currPos.left
const right = currPos.right
const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
nextFormat.integrate(transaction, 0) nextFormat.integrate(transaction, 0)
currPos.right = nextFormat currPos.right = nextFormat
currPos.forward()
}) })
} }
@@ -244,7 +244,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {ItemTextListPosition} currPos * @param {ItemTextListPosition} currPos
* @param {string|object} text * @param {string|object|AbstractType<any>} text
* @param {Object<string,any>} attributes * @param {Object<string,any>} attributes
* *
* @private * @private
@@ -261,7 +261,7 @@ const insertText = (transaction, parent, currPos, text, attributes) => {
minimizeAttributeChanges(currPos, attributes) minimizeAttributeChanges(currPos, attributes)
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes) const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
// insert content // insert content
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text) const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : (text instanceof AbstractType ? new ContentType(text) : new ContentEmbed(text))
let { left, right, index } = currPos let { left, right, index } = currPos
if (parent._searchMarker) { if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength()) updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
@@ -307,8 +307,7 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
} }
break break
} }
case ContentEmbed: default:
case ContentString:
if (length < currPos.right.length) { if (length < currPos.right.length) {
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length)) getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length))
} }
@@ -339,22 +338,28 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} start * @param {Item} start
* @param {Item|null} end exclusive end, automatically iterates to the next Content Item * @param {Item|null} curr exclusive end, automatically iterates to the next Content Item
* @param {Map<string,any>} startAttributes * @param {Map<string,any>} startAttributes
* @param {Map<string,any>} endAttributes This attribute is modified! * @param {Map<string,any>} currAttributes
* @return {number} The amount of formatting Items deleted. * @return {number} The amount of formatting Items deleted.
* *
* @function * @function
*/ */
const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttributes) => { const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
while (end && end.content.constructor !== ContentString && end.content.constructor !== ContentEmbed) { let end = curr
const endAttributes = map.copy(currAttributes)
while (end && (!end.countable || end.deleted)) {
if (!end.deleted && end.content.constructor === ContentFormat) { if (!end.deleted && end.content.constructor === ContentFormat) {
updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content)) updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content))
} }
end = end.right end = end.right
} }
let cleanups = 0 let cleanups = 0
let reachedEndOfCurr = false
while (start !== end) { while (start !== end) {
if (curr === start) {
reachedEndOfCurr = true
}
if (!start.deleted) { if (!start.deleted) {
const content = start.content const content = start.content
switch (content.constructor) { switch (content.constructor) {
@@ -364,6 +369,9 @@ const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttri
// Either this format is overwritten or it is not necessary because the attribute already existed. // Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction) start.delete(transaction)
cleanups++ cleanups++
if (!reachedEndOfCurr && (currAttributes.get(key) || null) === value && (startAttributes.get(key) || null) !== value) {
currAttributes.delete(key)
}
} }
break break
} }
@@ -380,12 +388,12 @@ const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttri
*/ */
const cleanupContextlessFormattingGap = (transaction, item) => { const cleanupContextlessFormattingGap = (transaction, item) => {
// iterate until item.right is null or content // iterate until item.right is null or content
while (item && item.right && (item.right.deleted || (item.right.content.constructor !== ContentString && item.right.content.constructor !== ContentEmbed))) { while (item && item.right && (item.right.deleted || !item.right.countable)) {
item = item.right item = item.right
} }
const attrs = new Set() const attrs = new Set()
// iterate back until a content item is found // iterate back until a content item is found
while (item && (item.deleted || (item.content.constructor !== ContentString && item.content.constructor !== ContentEmbed))) { while (item && (item.deleted || !item.countable)) {
if (!item.deleted && item.content.constructor === ContentFormat) { if (!item.deleted && item.content.constructor === ContentFormat) {
const key = /** @type {ContentFormat} */ (item.content).key const key = /** @type {ContentFormat} */ (item.content).key
if (attrs.has(key)) { if (attrs.has(key)) {
@@ -423,8 +431,7 @@ export const cleanupYTextFormatting = type => {
case ContentFormat: case ContentFormat:
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content)) updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content))
break break
case ContentEmbed: default:
case ContentString:
res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes) res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes)
startAttributes = map.copy(currentAttributes) startAttributes = map.copy(currentAttributes)
start = end start = end
@@ -453,6 +460,7 @@ const deleteText = (transaction, currPos, length) => {
while (length > 0 && currPos.right !== null) { while (length > 0 && currPos.right !== null) {
if (currPos.right.deleted === false) { if (currPos.right.deleted === false) {
switch (currPos.right.content.constructor) { switch (currPos.right.content.constructor) {
case ContentType:
case ContentEmbed: case ContentEmbed:
case ContentString: case ContentString:
if (length < currPos.right.length) { if (length < currPos.right.length) {
@@ -466,7 +474,7 @@ const deleteText = (transaction, currPos, length) => {
currPos.forward() currPos.forward()
} }
if (start) { if (start) {
cleanupFormattingGap(transaction, start, currPos.right, startAttrs, map.copy(currPos.currentAttributes)) cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes)
} }
const parent = /** @type {AbstractType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent) const parent = /** @type {AbstractType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent)
if (parent._searchMarker) { if (parent._searchMarker) {
@@ -539,7 +547,7 @@ export class YTextEvent extends YEvent {
get changes () { get changes () {
if (this._changes === null) { if (this._changes === null) {
/** /**
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}} * @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string|AbstractType<any>|object, delete?:number, retain?:number}>}}
*/ */
const changes = { const changes = {
keys: this.keys, keys: this.keys,
@@ -556,7 +564,7 @@ export class YTextEvent extends YEvent {
* Compute the changes in the delta format. * Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
* *
* @type {Array<{insert?:string, delete?:number, retain?:number, attributes?: Object<string,any>}>} * @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>}
* *
* @public * @public
*/ */
@@ -564,7 +572,7 @@ export class YTextEvent extends YEvent {
if (this._delta === null) { if (this._delta === null) {
const y = /** @type {Doc} */ (this.target.doc) const y = /** @type {Doc} */ (this.target.doc)
/** /**
* @type {Array<{insert?:string, delete?:number, retain?:number, attributes?: Object<string,any>}>} * @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>}
*/ */
const delta = [] const delta = []
transact(y, transaction => { transact(y, transaction => {
@@ -625,12 +633,13 @@ export class YTextEvent extends YEvent {
} }
while (item !== null) { while (item !== null) {
switch (item.content.constructor) { switch (item.content.constructor) {
case ContentType:
case ContentEmbed: case ContentEmbed:
if (this.adds(item)) { if (this.adds(item)) {
if (!this.deletes(item)) { if (!this.deletes(item)) {
addOp() addOp()
action = 'insert' action = 'insert'
insert = /** @type {ContentEmbed} */ (item.content).embed insert = item.content.getContent()[0]
addOp() addOp()
} }
} else if (this.deletes(item)) { } else if (this.deletes(item)) {
@@ -684,7 +693,7 @@ export class YTextEvent extends YEvent {
} else { } else {
attributes[key] = value attributes[key] = value
} }
} else { } else if (value !== null) {
item.delete(transaction) item.delete(transaction)
} }
} }
@@ -706,11 +715,11 @@ export class YTextEvent extends YEvent {
addOp() addOp()
} }
if (value === null) { if (value === null) {
attributes[key] = value
} else {
delete attributes[key] delete attributes[key]
} else {
attributes[key] = value
} }
} else { } else if (attr !== null) { // this will be cleaned up automatically by the contextless cleanup function
item.delete(transaction) item.delete(transaction)
} }
} }
@@ -815,6 +824,7 @@ export class YText extends AbstractType {
super._callObserver(transaction, parentSubs) super._callObserver(transaction, parentSubs)
const event = new YTextEvent(this, transaction, parentSubs) const event = new YTextEvent(this, transaction, parentSubs)
const doc = transaction.doc const doc = transaction.doc
callTypeObservers(this, transaction, event)
// If a remote change happened, we try to cleanup potential formatting duplicates. // If a remote change happened, we try to cleanup potential formatting duplicates.
if (!transaction.local) { if (!transaction.local) {
// check if another formatting item was inserted // check if another formatting item was inserted
@@ -863,7 +873,6 @@ export class YText extends AbstractType {
} }
}) })
} }
callTypeObservers(this, transaction, event)
} }
/** /**
@@ -1007,13 +1016,14 @@ export class YText extends AbstractType {
str += /** @type {ContentString} */ (n.content).str str += /** @type {ContentString} */ (n.content).str
break break
} }
case ContentType:
case ContentEmbed: { case ContentEmbed: {
packStr() packStr()
/** /**
* @type {Object<string,any>} * @type {Object<string,any>}
*/ */
const op = { const op = {
insert: /** @type {ContentEmbed} */ (n.content).embed insert: n.content.getContent()[0]
} }
if (currentAttributes.size > 0) { if (currentAttributes.size > 0) {
const attrs = /** @type {Object<string,any>} */ ({}) const attrs = /** @type {Object<string,any>} */ ({})
@@ -1074,16 +1084,13 @@ export class YText extends AbstractType {
* Inserts an embed at a index. * Inserts an embed at a index.
* *
* @param {number} index The index to insert the embed at. * @param {number} index The index to insert the embed at.
* @param {Object} embed The Object that represents the embed. * @param {Object | AbstractType<any>} embed The Object that represents the embed.
* @param {TextAttributes} attributes Attribute information to apply on the * @param {TextAttributes} attributes Attribute information to apply on the
* embed * embed
* *
* @public * @public
*/ */
insertEmbed (index, embed, attributes = {}) { insertEmbed (index, embed, attributes = {}) {
if (embed.constructor !== Object) {
throw new Error('Embed must be an Object')
}
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {

View File

@@ -3,6 +3,7 @@ import {
YXmlFragment, YXmlFragment,
transact, transact,
typeMapDelete, typeMapDelete,
typeMapHas,
typeMapSet, typeMapSet,
typeMapGet, typeMapGet,
typeMapGetAll, typeMapGetAll,
@@ -81,7 +82,7 @@ export class YXmlElement extends YXmlFragment {
el.setAttribute(key, attrs[key]) el.setAttribute(key, attrs[key])
} }
// @ts-ignore // @ts-ignore
el.insert(0, el.toArray().map(item => item instanceof AbstractType ? item.clone() : item)) el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
return el return el
} }
@@ -160,6 +161,18 @@ export class YXmlElement extends YXmlFragment {
return /** @type {any} */ (typeMapGet(this, attributeName)) return /** @type {any} */ (typeMapGet(this, attributeName))
} }
/**
* Returns whether an attribute exists
*
* @param {String} attributeName The attribute name to check for existence.
* @return {boolean} whether the attribute exists.
*
* @public
*/
hasAttribute (attributeName) {
return /** @type {any} */ (typeMapHas(this, attributeName))
}
/** /**
* Returns all attribute name/value pairs in a JSON Object. * Returns all attribute name/value pairs in a JSON Object.
* *

View File

@@ -20,7 +20,7 @@ import {
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
/** /**
* Define the elements to which a set of CSS queries apply. * Define the elements to which a set of CSS queries apply.
@@ -167,7 +167,7 @@ export class YXmlFragment extends AbstractType {
clone () { clone () {
const el = new YXmlFragment() const el = new YXmlFragment()
// @ts-ignore // @ts-ignore
el.insert(0, el.toArray().map(item => item instanceof AbstractType ? item.clone() : item)) el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
return el return el
} }

View File

@@ -1,5 +1,5 @@
import { Observable } from 'lib0/observable.js' import { Observable } from 'lib0/observable'
import { import {
Doc // eslint-disable-line Doc // eslint-disable-line

View File

@@ -8,11 +8,11 @@ import {
DSDecoderV1, DSEncoderV1, DSDecoderV2, DSEncoderV2, Item, GC, StructStore, Transaction, ID // eslint-disable-line DSDecoderV1, DSEncoderV1, DSDecoderV2, DSEncoderV2, Item, GC, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as array from 'lib0/array.js' import * as array from 'lib0/array'
import * as math from 'lib0/math.js' import * as math from 'lib0/math'
import * as map from 'lib0/map.js' import * as map from 'lib0/map'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding'
export class DeleteItem { export class DeleteItem {
/** /**
@@ -196,7 +196,7 @@ export const createDeleteSetFromStructStore = ss => {
const clock = struct.id.clock const clock = struct.id.clock
let len = struct.length let len = struct.length
if (i + 1 < structs.length) { if (i + 1 < structs.length) {
for (let next = structs[i + 1]; i + 1 < structs.length && next.id.clock === clock + len && next.deleted; next = structs[++i + 1]) { for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) {
len += next.length len += next.length
} }
} }

View File

@@ -13,10 +13,11 @@ import {
ContentDoc, Item, Transaction, YEvent // eslint-disable-line ContentDoc, Item, Transaction, YEvent // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import { Observable } from 'lib0/observable.js' import { Observable } from 'lib0/observable'
import * as random from 'lib0/random.js' import * as random from 'lib0/random'
import * as map from 'lib0/map.js' import * as map from 'lib0/map'
import * as array from 'lib0/array.js' import * as array from 'lib0/array'
import * as promise from 'lib0/promise'
export const generateNewClientId = random.uint32 export const generateNewClientId = random.uint32
@@ -25,8 +26,10 @@ export const generateNewClientId = random.uint32
* @property {boolean} [DocOpts.gc=true] Disable garbage collection (default: gc=true) * @property {boolean} [DocOpts.gc=true] Disable garbage collection (default: gc=true)
* @property {function(Item):boolean} [DocOpts.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item. * @property {function(Item):boolean} [DocOpts.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item.
* @property {string} [DocOpts.guid] Define a globally unique identifier for this document * @property {string} [DocOpts.guid] Define a globally unique identifier for this document
* @property {string | null} [DocOpts.collectionid] Associate this document with a collection. This only plays a role if your provider has a concept of collection.
* @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well. * @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well.
* @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically. * @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically.
* @property {boolean} [DocOpts.shouldLoad] Whether the document should be synced by the provider now. This is toggled to true when you call ydoc.load()
*/ */
/** /**
@@ -37,12 +40,13 @@ export class Doc extends Observable {
/** /**
* @param {DocOpts} [opts] configuration * @param {DocOpts} [opts] configuration
*/ */
constructor ({ guid = random.uuidv4(), gc = true, gcFilter = () => true, meta = null, autoLoad = false } = {}) { constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) {
super() super()
this.gc = gc this.gc = gc
this.gcFilter = gcFilter this.gcFilter = gcFilter
this.clientID = generateNewClientId() this.clientID = generateNewClientId()
this.guid = guid this.guid = guid
this.collectionid = collectionid
/** /**
* @type {Map<string, AbstractType<YEvent>>} * @type {Map<string, AbstractType<YEvent>>}
*/ */
@@ -65,9 +69,16 @@ export class Doc extends Observable {
* @type {Item?} * @type {Item?}
*/ */
this._item = null this._item = null
this.shouldLoad = autoLoad this.shouldLoad = shouldLoad
this.autoLoad = autoLoad this.autoLoad = autoLoad
this.meta = meta this.meta = meta
this.isLoaded = false
this.whenLoaded = promise.create(resolve => {
this.on('load', () => {
this.isLoaded = true
resolve(this)
})
})
} }
/** /**
@@ -194,8 +205,9 @@ export class Doc extends Observable {
} }
/** /**
* @template T
* @param {string} [name] * @param {string} [name]
* @return {YMap<any>} * @return {YMap<T>}
* *
* @public * @public
*/ */
@@ -245,16 +257,12 @@ export class Doc extends Observable {
if (item !== null) { if (item !== null) {
this._item = null this._item = null
const content = /** @type {ContentDoc} */ (item.content) const content = /** @type {ContentDoc} */ (item.content)
if (item.deleted) { content.doc = new Doc({ guid: this.guid, ...content.opts, shouldLoad: false })
// @ts-ignore content.doc._item = item
content.doc = null
} else {
content.doc = new Doc({ guid: this.guid, ...content.opts })
content.doc._item = item
}
transact(/** @type {any} */ (item).parent.doc, transaction => { transact(/** @type {any} */ (item).parent.doc, transaction => {
const doc = content.doc
if (!item.deleted) { if (!item.deleted) {
transaction.subdocsAdded.add(content.doc) transaction.subdocsAdded.add(doc)
} }
transaction.subdocsRemoved.add(this) transaction.subdocsRemoved.add(this)
}, null, true) }, null, true)

View File

@@ -1,4 +1,4 @@
import * as f from 'lib0/function.js' import * as f from 'lib0/function'
/** /**
* General event handler implementation. * General event handler implementation.

View File

@@ -1,9 +1,9 @@
import { AbstractType } from '../internals.js' // eslint-disable-line import { AbstractType } from '../internals.js' // eslint-disable-line
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
export class ID { export class ID {
/** /**

View File

@@ -8,7 +8,7 @@ import {
DSEncoderV1, DSDecoderV1, ID, DeleteSet, YArrayEvent, Transaction, Doc // eslint-disable-line DSEncoderV1, DSDecoderV1, ID, DeleteSet, YArrayEvent, Transaction, Doc // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding'
import { mergeDeleteSets, isDeleted } from './DeleteSet.js' import { mergeDeleteSets, isDeleted } from './DeleteSet.js'

View File

@@ -12,9 +12,9 @@ import {
ID, Doc, AbstractType // eslint-disable-line ID, Doc, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
/** /**
* A relative position is based on the Yjs model and is not affected by document changes. * A relative position is based on the Yjs model and is not affected by document changes.

View File

@@ -18,10 +18,10 @@ import {
DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map'
import * as set from 'lib0/set.js' import * as set from 'lib0/set'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding'
export class Snapshot { export class Snapshot {
/** /**
@@ -129,9 +129,9 @@ export const snapshot = doc => createSnapshot(createDeleteSetFromStructStore(doc
* @protected * @protected
* @function * @function
*/ */
export const isVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : ( export const isVisible = (item, snapshot) => snapshot === undefined
snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id) ? !item.deleted
) : snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction

View File

@@ -5,8 +5,8 @@ import {
Transaction, ID, Item, DSDecoderV2 // eslint-disable-line Transaction, ID, Item, DSDecoderV2 // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math'
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
export class StructStore { export class StructStore {
constructor () { constructor () {

View File

@@ -11,14 +11,14 @@ import {
Item, Item,
generateNewClientId, generateNewClientId,
createID, createID,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map'
import * as math from 'lib0/math.js' import * as math from 'lib0/math'
import * as set from 'lib0/set.js' import * as set from 'lib0/set'
import * as logging from 'lib0/logging.js' import * as logging from 'lib0/logging'
import { callAll } from 'lib0/function.js' import { callAll } from 'lib0/function'
/** /**
* 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
@@ -331,8 +331,8 @@ const cleanupTransactions = (transactionCleanups, i) => {
} }
} }
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) { if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
doc.clientID = generateNewClientId()
logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.') logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.')
doc.clientID = generateNewClientId()
} }
// @todo Merge all the transactions into one and provide send the data as a single update message // @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc]) doc.emit('afterTransactionCleanup', [transaction, doc])
@@ -350,11 +350,19 @@ const cleanupTransactions = (transactionCleanups, i) => {
doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction]) doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction])
} }
} }
transaction.subdocsAdded.forEach(subdoc => doc.subdocs.add(subdoc)) const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction
transaction.subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc)) if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) {
subdocsAdded.forEach(subdoc => {
doc.emit('subdocs', [{ loaded: transaction.subdocsLoaded, added: transaction.subdocsAdded, removed: transaction.subdocsRemoved }]) subdoc.clientID = doc.clientID
transaction.subdocsRemoved.forEach(subdoc => subdoc.destroy()) if (subdoc.collectionid == null) {
subdoc.collectionid = doc.collectionid
}
doc.subdocs.add(subdoc)
})
subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc))
doc.emit('subdocs', [{ loaded: subdocsLoaded, added: subdocsAdded, removed: subdocsRemoved }, doc, transaction])
subdocsRemoved.forEach(subdoc => subdoc.destroy())
}
if (transactionCleanups.length <= i + 1) { if (transactionCleanups.length <= i + 1) {
doc._transactionCleanups = [] doc._transactionCleanups = []

View File

@@ -13,8 +13,8 @@ import {
Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as time from 'lib0/time.js' import * as time from 'lib0/time'
import { Observable } from 'lib0/observable.js' import { Observable } from 'lib0/observable'
class StackItem { class StackItem {
/** /**
@@ -88,7 +88,7 @@ const popStackItem = (undoManager, stack, eventType) => {
} }
}) })
itemsToRedo.forEach(struct => { itemsToRedo.forEach(struct => {
performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions) !== null || performedChange
}) })
// 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
// parents, so we have more information available when items are filtered. // parents, so we have more information available when items are filtered.
@@ -99,7 +99,7 @@ const popStackItem = (undoManager, stack, eventType) => {
performedChange = true performedChange = true
} }
} }
result = stackItem result = performedChange ? stackItem : null
} }
transaction.changed.forEach((subProps, type) => { transaction.changed.forEach((subProps, type) => {
// destroy search marker if necessary // destroy search marker if necessary
@@ -186,6 +186,7 @@ export class UndoManager extends Observable {
} }
}) })
const now = time.getUnixTime() const now = time.getUnixTime()
let didAdd = false
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]
@@ -194,6 +195,7 @@ export class UndoManager extends Observable {
} else { } else {
// create a new stack op // create a new stack op
stack.push(new StackItem(transaction.deleteSet, insertions)) stack.push(new StackItem(transaction.deleteSet, insertions))
didAdd = true
} }
if (!undoing && !redoing) { if (!undoing && !redoing) {
this.lastChange = now this.lastChange = now
@@ -204,7 +206,9 @@ export class UndoManager extends Observable {
keepItem(item, true) keepItem(item, true)
} }
}) })
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]) if (didAdd) {
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this])
}
}) })
} }
@@ -282,4 +286,22 @@ export class UndoManager extends Observable {
} }
return res return res
} }
/**
* Are undo steps available?
*
* @return {boolean} `true` if undo is possible
*/
canUndo () {
return this.undoStack.length > 0
}
/**
* Are redo steps available?
*
* @return {boolean} `true` if redo is possible
*/
canRedo () {
return this.redoStack.length > 0
}
} }

View File

@@ -1,5 +1,5 @@
import * as buffer from 'lib0/buffer.js' import * as buffer from 'lib0/buffer'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding'
import { import {
ID, createID ID, createID
} from '../internals.js' } from '../internals.js'

View File

@@ -1,6 +1,6 @@
import * as error from 'lib0/error.js' import * as error from 'lib0/error'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding'
import { import {
ID // eslint-disable-line ID // eslint-disable-line

View File

@@ -4,8 +4,8 @@ import {
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as set from 'lib0/set.js' import * as set from 'lib0/set'
import * as array from 'lib0/array.js' import * as array from 'lib0/array'
/** /**
* YEvent describes the changes on a YType. * YEvent describes the changes on a YType.
@@ -40,7 +40,7 @@ export class YEvent {
*/ */
this._keys = null this._keys = null
/** /**
* @type {null | Array<{ insert?: string | Array<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>} * @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
*/ */
this._delta = null this._delta = null
} }
@@ -129,7 +129,7 @@ export class YEvent {
} }
/** /**
* @type {Array<{insert?: string | Array<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>} * @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
*/ */
get delta () { get delta () {
return this.changes.delta return this.changes.delta

View File

@@ -32,17 +32,19 @@ import {
DSEncoderV2, DSEncoderV2,
DSDecoderV1, DSDecoderV1,
DSEncoderV1, DSEncoderV1,
mergeUpdates,
mergeUpdatesV2, mergeUpdatesV2,
Skip, Skip,
diffUpdateV2, diffUpdateV2,
convertUpdateFormatV2ToV1,
DSDecoderV2, Doc, Transaction, GC, Item, StructStore // eslint-disable-line DSDecoderV2, Doc, Transaction, GC, Item, StructStore // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding'
import * as binary from 'lib0/binary.js' import * as binary from 'lib0/binary'
import * as map from 'lib0/map.js' import * as map from 'lib0/map'
import * as math from 'lib0/math.js' import * as math from 'lib0/math'
/** /**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
@@ -380,6 +382,8 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
*/ */
export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) => export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) =>
transact(ydoc, transaction => { transact(ydoc, transaction => {
// force that transaction.local is set to non-local
transaction.local = false
let retry = false let retry = false
const doc = transaction.doc const doc = transaction.doc
const store = doc.store const store = doc.store
@@ -521,15 +525,16 @@ export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8A
writeStateAsUpdate(encoder, doc, targetStateVector) writeStateAsUpdate(encoder, doc, targetStateVector)
const updates = [encoder.toUint8Array()] const updates = [encoder.toUint8Array()]
// also add the pending updates (if there are any) // also add the pending updates (if there are any)
// @todo support diffirent encoders if (doc.store.pendingDs) {
if (encoder.constructor === UpdateEncoderV2) { updates.push(doc.store.pendingDs)
if (doc.store.pendingDs) { }
updates.push(doc.store.pendingDs) if (doc.store.pendingStructs) {
} updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector))
if (doc.store.pendingStructs) { }
updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector)) if (updates.length > 1) {
} if (encoder.constructor === UpdateEncoderV1) {
if (updates.length > 1) { return mergeUpdates(updates.map((update, i) => i === 0 ? update : convertUpdateFormatV2ToV1(update)))
} else if (encoder.constructor === UpdateEncoderV2) {
return mergeUpdatesV2(updates) return mergeUpdatesV2(updates)
} }
} }
@@ -596,7 +601,7 @@ export const decodeStateVector = decodedState => readStateVector(new DSDecoderV1
*/ */
export const writeStateVector = (encoder, sv) => { export const writeStateVector = (encoder, sv) => {
encoding.writeVarUint(encoder.restEncoder, sv.size) encoding.writeVarUint(encoder.restEncoder, sv.size)
sv.forEach((clock, client) => { Array.from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping
encoding.writeVarUint(encoder.restEncoder, clock) encoding.writeVarUint(encoder.restEncoder, clock)
}) })

View File

@@ -1,9 +1,9 @@
import * as binary from 'lib0/binary.js' import * as binary from 'lib0/binary'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding'
import * as logging from 'lib0/logging.js' import * as logging from 'lib0/logging'
import * as math from 'lib0/math.js' import * as math from 'lib0/math'
import { import {
createID, createID,
readItemContent, readItemContent,
@@ -149,23 +149,27 @@ export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1,
*/ */
export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YDecoder = UpdateDecoderV2) => { export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YDecoder = UpdateDecoderV2) => {
const encoder = new YEncoder() const encoder = new YEncoder()
const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), true) const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), false)
let curr = updateDecoder.curr let curr = updateDecoder.curr
if (curr !== null) { if (curr !== null) {
let size = 1 let size = 0
let currClient = curr.id.client let currClient = curr.id.client
let currClock = curr.id.clock let stopCounting = curr.id.clock !== 0 // must start at 0
let stopCounting = false let currClock = stopCounting ? 0 : curr.id.clock + curr.length
for (; curr !== null; curr = updateDecoder.next()) { for (; curr !== null; curr = updateDecoder.next()) {
if (currClient !== curr.id.client) { if (currClient !== curr.id.client) {
size++ if (currClock !== 0) {
// We found a new client size++
// write what we have to the encoder // We found a new client
encoding.writeVarUint(encoder.restEncoder, currClient) // write what we have to the encoder
encoding.writeVarUint(encoder.restEncoder, currClock) encoding.writeVarUint(encoder.restEncoder, currClient)
encoding.writeVarUint(encoder.restEncoder, currClock)
}
currClient = curr.id.client currClient = curr.id.client
stopCounting = false currClock = 0
stopCounting = curr.id.clock !== 0
} }
// we ignore skips
if (curr.constructor === Skip) { if (curr.constructor === Skip) {
stopCounting = true stopCounting = true
} }
@@ -174,8 +178,11 @@ export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YD
} }
} }
// write what we have // write what we have
encoding.writeVarUint(encoder.restEncoder, currClient) if (currClock !== 0) {
encoding.writeVarUint(encoder.restEncoder, currClock) size++
encoding.writeVarUint(encoder.restEncoder, currClient)
encoding.writeVarUint(encoder.restEncoder, currClock)
}
// prepend the size of the state vector // prepend the size of the state vector
const enc = encoding.createEncoder() const enc = encoding.createEncoder()
encoding.writeVarUint(enc, size) encoding.writeVarUint(enc, size)
@@ -280,6 +287,9 @@ const sliceStruct = (left, diff) => {
* @return {Uint8Array} * @return {Uint8Array}
*/ */
export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
if (updates.length === 1) {
return updates[0]
}
const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update))) const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update)))
let lazyStructDecoders = updateDecoders.map(decoder => new LazyStructReader(decoder, true)) let lazyStructDecoders = updateDecoders.map(decoder => new LazyStructReader(decoder, true))
@@ -305,9 +315,10 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U
if (dec1.curr.id.client === dec2.curr.id.client) { if (dec1.curr.id.client === dec2.curr.id.client) {
const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock
if (clockDiff === 0) { if (clockDiff === 0) {
return dec1.curr.constructor === dec2.curr.constructor ? 0 : ( // @todo remove references to skip since the structDecoders must filter Skips.
dec1.curr.constructor === Skip ? 1 : -1 return dec1.curr.constructor === dec2.curr.constructor
) ? 0
: dec1.curr.constructor === Skip ? 1 : -1 // we are filtering skips anyway.
} else { } else {
return clockDiff return clockDiff
} }
@@ -326,13 +337,19 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U
if (currWrite !== null) { if (currWrite !== null) {
let curr = /** @type {Item | GC | null} */ (currDecoder.curr) let curr = /** @type {Item | GC | null} */ (currDecoder.curr)
let iterated = false
// iterate until we find something that we haven't written already // iterate until we find something that we haven't written already
// remember: first the high client-ids are written // remember: first the high client-ids are written
while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) { while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) {
curr = currDecoder.next() curr = currDecoder.next()
iterated = true
} }
if (curr === null || curr.id.client !== firstClient) { if (
curr === null || // current decoder is empty
curr.id.client !== firstClient || // check whether there is another decoder that has has updates from `firstClient`
(iterated && curr.id.clock > currWrite.struct.id.clock + currWrite.struct.length) // the above while loop was used and we are potentially missing updates
) {
continue continue
} }
@@ -508,3 +525,33 @@ const finishLazyStructWriting = (lazyWriter) => {
encoding.writeUint8Array(restEncoder, partStructs.restEncoder) encoding.writeUint8Array(restEncoder, partStructs.restEncoder)
} }
} }
/**
* @param {Uint8Array} update
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
*/
export const convertUpdateFormat = (update, YDecoder, YEncoder) => {
const updateDecoder = new YDecoder(decoding.createDecoder(update))
const lazyDecoder = new LazyStructReader(updateDecoder, false)
const updateEncoder = new YEncoder()
const lazyWriter = new LazyStructWriter(updateEncoder)
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
writeStructToLazyStructWriter(lazyWriter, curr, 0)
}
finishLazyStructWriting(lazyWriter)
const ds = readDeleteSet(updateDecoder)
writeDeleteSet(updateEncoder, ds)
return updateEncoder.toUint8Array()
}
/**
* @param {Uint8Array} update
*/
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, UpdateDecoderV1, UpdateEncoderV2)
/**
* @param {Uint8Array} update
*/
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, UpdateDecoderV2, UpdateEncoderV1)

View File

@@ -7,8 +7,8 @@
*/ */
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'
import * as buffer from 'lib0/buffer.js' import * as buffer from 'lib0/buffer'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc

View File

@@ -1,6 +1,6 @@
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'
/** /**
* Client id should be changed when an instance receives updates from another client using the same client id. * Client id should be changed when an instance receives updates from another client using the same client id.
@@ -88,7 +88,7 @@ export const testSubdoc = tc => {
subdocs.get('a').load() subdocs.get('a').load()
t.compare(event, [[], [], ['a']]) t.compare(event, [[], [], ['a']])
subdocs.set('b', new Y.Doc({ guid: 'a' })) subdocs.set('b', new Y.Doc({ guid: 'a', shouldLoad: false }))
t.compare(event, [['a'], [], []]) t.compare(event, [['a'], [], []])
subdocs.get('b').load() subdocs.get('b').load()
t.compare(event, [[], [], ['a']]) t.compare(event, [[], [], ['a']])
@@ -124,3 +124,123 @@ export const testSubdoc = tc => {
t.compare(Array.from(doc2.getSubdocGuids()), ['a', 'c']) t.compare(Array.from(doc2.getSubdocGuids()), ['a', 'c'])
} }
} }
/**
* @param {t.TestCase} tc
*/
export const testSubdocLoadEdgeCases = tc => {
const ydoc = new Y.Doc()
const yarray = ydoc.getArray()
const subdoc1 = new Y.Doc()
/**
* @type {any}
*/
let lastEvent = null
ydoc.on('subdocs', event => {
lastEvent = event
})
yarray.insert(0, [subdoc1])
t.assert(subdoc1.shouldLoad)
t.assert(subdoc1.autoLoad === false)
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc1))
t.assert(lastEvent !== null && lastEvent.added.has(subdoc1))
// destroy and check whether lastEvent adds it again to added (it shouldn't)
subdoc1.destroy()
const subdoc2 = yarray.get(0)
t.assert(subdoc1 !== subdoc2)
t.assert(lastEvent !== null && lastEvent.added.has(subdoc2))
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc2))
// load
subdoc2.load()
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc2))
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc2))
// apply from remote
const ydoc2 = new Y.Doc()
ydoc2.on('subdocs', event => {
lastEvent = event
})
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc))
const subdoc3 = ydoc2.getArray().get(0)
t.assert(subdoc3.shouldLoad === false)
t.assert(subdoc3.autoLoad === false)
t.assert(lastEvent !== null && lastEvent.added.has(subdoc3))
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc3))
// load
subdoc3.load()
t.assert(subdoc3.shouldLoad)
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc3))
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc3))
}
/**
* @param {t.TestCase} tc
*/
export const testSubdocLoadEdgeCasesAutoload = tc => {
const ydoc = new Y.Doc()
const yarray = ydoc.getArray()
const subdoc1 = new Y.Doc({ autoLoad: true })
/**
* @type {any}
*/
let lastEvent = null
ydoc.on('subdocs', event => {
lastEvent = event
})
yarray.insert(0, [subdoc1])
t.assert(subdoc1.shouldLoad)
t.assert(subdoc1.autoLoad)
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc1))
t.assert(lastEvent !== null && lastEvent.added.has(subdoc1))
// destroy and check whether lastEvent adds it again to added (it shouldn't)
subdoc1.destroy()
const subdoc2 = yarray.get(0)
t.assert(subdoc1 !== subdoc2)
t.assert(lastEvent !== null && lastEvent.added.has(subdoc2))
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc2))
// load
subdoc2.load()
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc2))
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc2))
// apply from remote
const ydoc2 = new Y.Doc()
ydoc2.on('subdocs', event => {
lastEvent = event
})
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc))
const subdoc3 = ydoc2.getArray().get(0)
t.assert(subdoc1.shouldLoad)
t.assert(subdoc1.autoLoad)
t.assert(lastEvent !== null && lastEvent.added.has(subdoc3))
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc3))
}
/**
* @param {t.TestCase} tc
*/
export const testSubdocsUndo = tc => {
const ydoc = new Y.Doc()
const elems = ydoc.getXmlFragment()
const undoManager = new Y.UndoManager(elems)
const subdoc = new Y.Doc()
// @ts-ignore
elems.insert(0, [subdoc])
undoManager.undo()
undoManager.redo()
t.assert(elems.length === 1)
}
/**
* @param {t.TestCase} tc
*/
export const testLoadDocs = async tc => {
const ydoc = new Y.Doc()
t.assert(ydoc.isLoaded === false)
let loadedEvent = false
ydoc.on('load', () => {
loadedEvent = true
})
ydoc.emit('load', [ydoc])
await ydoc.whenLoaded
t.assert(loadedEvent)
t.assert(ydoc.isLoaded)
}

View File

@@ -1,5 +1,5 @@
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing'
import * as promise from 'lib0/promise.js' import * as promise from 'lib0/promise'
import { import {
contentRefs, contentRefs,
@@ -18,6 +18,8 @@ import {
applyUpdate applyUpdate
} from '../src/internals.js' } from '../src/internals.js'
import * as Y from '../src/index.js'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -62,3 +64,45 @@ export const testPermanentUserData = async tc => {
const pd3 = new PermanentUserData(ydoc3) const pd3 = new PermanentUserData(ydoc3)
pd3.setUserMapping(ydoc3, ydoc3.clientID, 'user a') pd3.setUserMapping(ydoc3, ydoc3.clientID, 'user a')
} }
/**
* Reported here: https://github.com/yjs/yjs/issues/308
* @param {t.TestCase} tc
*/
export const testDiffStateVectorOfUpdateIsEmpty = tc => {
const ydoc = new Y.Doc()
/**
* @type {null | Uint8Array}
*/
let sv = /* any */ (null)
ydoc.getText().insert(0, 'a')
ydoc.on('update', update => {
sv = Y.encodeStateVectorFromUpdate(update)
})
// should produce an update with an empty state vector (because previous ops are missing)
ydoc.getText().insert(0, 'a')
t.assert(sv !== null && sv.byteLength === 1 && sv[0] === 0)
}
/**
* Reported here: https://github.com/yjs/yjs/issues/308
* @param {t.TestCase} tc
*/
export const testDiffStateVectorOfUpdateIgnoresSkips = tc => {
const ydoc = new Y.Doc()
/**
* @type {Array<Uint8Array>}
*/
const updates = []
ydoc.on('update', update => {
updates.push(update)
})
ydoc.getText().insert(0, 'a')
ydoc.getText().insert(0, 'b')
ydoc.getText().insert(0, 'c')
const update13 = Y.mergeUpdates([updates[0], updates[2]])
const sv = Y.encodeStateVectorFromUpdate(update13)
const state = Y.decodeStateVector(sv)
t.assert(state.get(ydoc.clientID) === 1)
t.assert(state.size === 1)
}

View File

@@ -11,9 +11,9 @@ import * as snapshot from './snapshot.tests.js'
import * as updates from './updates.tests.js' import * as updates from './updates.tests.js'
import * as relativePositions from './relativePositions.tests.js' import * as relativePositions from './relativePositions.tests.js'
import { runTests } from 'lib0/testing.js' import { runTests } from 'lib0/testing'
import { isBrowser, isNode } from 'lib0/environment.js' import { isBrowser, isNode } from 'lib0/environment'
import * as log from 'lib0/logging.js' import * as log from 'lib0/logging'
if (isBrowser) { if (isBrowser) {
log.createVConsole(document.body) log.createVConsole(document.body)

View File

@@ -1,9 +1,9 @@
import * as Y from '../src/internals' import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing'
/** /**
* @param {Y.YText} ytext * @param {Y.Text} ytext
*/ */
const checkRelativePositions = ytext => { const checkRelativePositions = ytext => {
// test if all positions are encoded and restored correctly // test if all positions are encoded and restored correctly

View File

@@ -1,17 +1,17 @@
import { createDocFromSnapshot, Doc, snapshot, YMap } from '../src/internals' import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing'
import { init } from './testHelper' import { init } from './testHelper.js'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testBasicRestoreSnapshot = tc => { export const testBasicRestoreSnapshot = tc => {
const doc = new Doc({ gc: false }) const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, ['hello']) doc.getArray('array').insert(0, ['hello'])
const snap = snapshot(doc) const snap = Y.snapshot(doc)
doc.getArray('array').insert(1, ['world']) doc.getArray('array').insert(1, ['world'])
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['hello']) t.compare(docRestored.getArray('array').toArray(), ['hello'])
t.compare(doc.getArray('array').toArray(), ['hello', 'world']) t.compare(doc.getArray('array').toArray(), ['hello', 'world'])
@@ -21,19 +21,19 @@ export const testBasicRestoreSnapshot = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testEmptyRestoreSnapshot = tc => { export const testEmptyRestoreSnapshot = tc => {
const doc = new Doc({ gc: false }) const doc = new Y.Doc({ gc: false })
const snap = snapshot(doc) const snap = Y.snapshot(doc)
snap.sv.set(9999, 0) snap.sv.set(9999, 0)
doc.getArray().insert(0, ['world']) doc.getArray().insert(0, ['world'])
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray().toArray(), []) t.compare(docRestored.getArray().toArray(), [])
t.compare(doc.getArray().toArray(), ['world']) t.compare(doc.getArray().toArray(), ['world'])
// now this snapshot reflects the latest state. It shoult still work. // now this snapshot reflects the latest state. It shoult still work.
const snap2 = snapshot(doc) const snap2 = Y.snapshot(doc)
const docRestored2 = createDocFromSnapshot(doc, snap2) const docRestored2 = Y.createDocFromSnapshot(doc, snap2)
t.compare(docRestored2.getArray().toArray(), ['world']) t.compare(docRestored2.getArray().toArray(), ['world'])
} }
@@ -41,15 +41,15 @@ export const testEmptyRestoreSnapshot = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testRestoreSnapshotWithSubType = tc => { export const testRestoreSnapshotWithSubType = tc => {
const doc = new Doc({ gc: false }) const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, [new YMap()]) doc.getArray('array').insert(0, [new Y.Map()])
const subMap = doc.getArray('array').get(0) const subMap = doc.getArray('array').get(0)
subMap.set('key1', 'value1') subMap.set('key1', 'value1')
const snap = snapshot(doc) const snap = Y.snapshot(doc)
subMap.set('key2', 'value2') subMap.set('key2', 'value2')
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toJSON(), [{ t.compare(docRestored.getArray('array').toJSON(), [{
key1: 'value1' key1: 'value1'
@@ -64,13 +64,13 @@ export const testRestoreSnapshotWithSubType = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testRestoreDeletedItem1 = tc => { export const testRestoreDeletedItem1 = tc => {
const doc = new Doc({ gc: false }) const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, ['item1', 'item2']) doc.getArray('array').insert(0, ['item1', 'item2'])
const snap = snapshot(doc) const snap = Y.snapshot(doc)
doc.getArray('array').delete(0) doc.getArray('array').delete(0)
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item1', 'item2']) t.compare(docRestored.getArray('array').toArray(), ['item1', 'item2'])
t.compare(doc.getArray('array').toArray(), ['item2']) t.compare(doc.getArray('array').toArray(), ['item2'])
@@ -80,15 +80,15 @@ export const testRestoreDeletedItem1 = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testRestoreLeftItem = tc => { export const testRestoreLeftItem = tc => {
const doc = new Doc({ gc: false }) const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, ['item1']) doc.getArray('array').insert(0, ['item1'])
doc.getMap('map').set('test', 1) doc.getMap('map').set('test', 1)
doc.getArray('array').insert(0, ['item0']) doc.getArray('array').insert(0, ['item0'])
const snap = snapshot(doc) const snap = Y.snapshot(doc)
doc.getArray('array').delete(1) doc.getArray('array').delete(1)
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item0', 'item1']) t.compare(docRestored.getArray('array').toArray(), ['item0', 'item1'])
t.compare(doc.getArray('array').toArray(), ['item0']) t.compare(doc.getArray('array').toArray(), ['item0'])
@@ -98,13 +98,13 @@ export const testRestoreLeftItem = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testDeletedItemsBase = tc => { export const testDeletedItemsBase = tc => {
const doc = new Doc({ gc: false }) const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, ['item1']) doc.getArray('array').insert(0, ['item1'])
doc.getArray('array').delete(0) doc.getArray('array').delete(0)
const snap = snapshot(doc) const snap = Y.snapshot(doc)
doc.getArray('array').insert(0, ['item0']) doc.getArray('array').insert(0, ['item0'])
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), []) t.compare(docRestored.getArray('array').toArray(), [])
t.compare(doc.getArray('array').toArray(), ['item0']) t.compare(doc.getArray('array').toArray(), ['item0'])
@@ -114,13 +114,13 @@ export const testDeletedItemsBase = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testDeletedItems2 = tc => { export const testDeletedItems2 = tc => {
const doc = new Doc({ gc: false }) const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, ['item1', 'item2', 'item3']) doc.getArray('array').insert(0, ['item1', 'item2', 'item3'])
doc.getArray('array').delete(1) doc.getArray('array').delete(1)
const snap = snapshot(doc) const snap = Y.snapshot(doc)
doc.getArray('array').insert(0, ['item0']) doc.getArray('array').insert(0, ['item0'])
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item1', 'item3']) t.compare(docRestored.getArray('array').toArray(), ['item1', 'item3'])
t.compare(doc.getArray('array').toArray(), ['item0', 'item1', 'item3']) t.compare(doc.getArray('array').toArray(), ['item0', 'item1', 'item3'])
@@ -140,11 +140,11 @@ export const testDependentChanges = tc => {
} }
/** /**
* @type Doc * @type {Y.Doc}
*/ */
const doc0 = array0.doc const doc0 = array0.doc
/** /**
* @type Doc * @type {Y.Doc}
*/ */
const doc1 = array1.doc const doc1 = array1.doc
@@ -156,16 +156,16 @@ export const testDependentChanges = tc => {
array1.insert(1, ['user2item1']) array1.insert(1, ['user2item1'])
testConnector.syncAll() testConnector.syncAll()
const snap = snapshot(array0.doc) const snap = Y.snapshot(array0.doc)
array0.insert(2, ['user1item2']) array0.insert(2, ['user1item2'])
testConnector.syncAll() testConnector.syncAll()
array1.insert(3, ['user2item2']) array1.insert(3, ['user2item2'])
testConnector.syncAll() testConnector.syncAll()
const docRestored0 = createDocFromSnapshot(array0.doc, snap) const docRestored0 = Y.createDocFromSnapshot(array0.doc, snap)
t.compare(docRestored0.getArray('array').toArray(), ['user1item1', 'user2item1']) t.compare(docRestored0.getArray('array').toArray(), ['user1item1', 'user2item1'])
const docRestored1 = createDocFromSnapshot(array1.doc, snap) const docRestored1 = Y.createDocFromSnapshot(array1.doc, snap)
t.compare(docRestored1.getArray('array').toArray(), ['user1item1', 'user2item1']) t.compare(docRestored1.getArray('array').toArray(), ['user1item1', 'user2item1'])
} }

View File

@@ -1,12 +1,12 @@
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing'
import * as prng from 'lib0/prng.js' import * as prng from 'lib0/prng'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding'
import * as syncProtocol from 'y-protocols/sync.js' import * as syncProtocol from 'y-protocols/sync'
import * as object from 'lib0/object.js' import * as object from 'lib0/object'
import * as Y from '../src/internals.js' import * as Y from '../src/index.js'
export * from '../src/internals.js' export * from '../src/index.js'
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// @ts-ignore // @ts-ignore
@@ -279,7 +279,7 @@ export class TestConnector {
* @param {t.TestCase} tc * @param {t.TestCase} tc
* @param {{users?:number}} conf * @param {{users?:number}} conf
* @param {InitTestObjectCallback<T>} [initTestObject] * @param {InitTestObjectCallback<T>} [initTestObject]
* @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.YArray<any>,array1:Y.YArray<any>,array2:Y.YArray<any>,map0:Y.YMap<any>,map1:Y.YMap<any>,map2:Y.YMap<any>,map3:Y.YMap<any>,text0:Y.YText,text1:Y.YText,text2:Y.YText,xml0:Y.YXmlElement,xml1:Y.YXmlElement,xml2:Y.YXmlElement}} * @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Array<any>,array1:Y.Array<any>,array2:Y.Array<any>,map0:Y.Map<any>,map1:Y.Map<any>,map2:Y.Map<any>,map3:Y.Map<any>,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}}
*/ */
export const init = (tc, { users = 5 } = {}, initTestObject) => { export const init = (tc, { users = 5 } = {}, initTestObject) => {
/** /**
@@ -304,7 +304,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
result.users.push(y) result.users.push(y)
result['array' + i] = y.getArray('array') result['array' + i] = y.getArray('array')
result['map' + i] = y.getMap('map') result['map' + i] = y.getMap('map')
result['xml' + i] = y.get('xml', Y.YXmlElement) result['xml' + i] = y.get('xml', Y.XmlElement)
result['text' + i] = y.getText('text') result['text' + i] = y.getText('text')
} }
testConnector.syncAll() testConnector.syncAll()
@@ -324,7 +324,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
*/ */
export const compare = users => { export const compare = users => {
users.forEach(u => u.connect()) users.forEach(u => u.connect())
while (users[0].tc.flushAllMessages()) {} while (users[0].tc.flushAllMessages()) {} // eslint-disable-line
// For each document, merge all received document updates with Y.mergeUpdates and create a new document which will be added to the list of "users" // For each document, merge all received document updates with Y.mergeUpdates and create a new document which will be added to the list of "users"
// This ensures that mergeUpdates works correctly // This ensures that mergeUpdates works correctly
const mergedDocs = users.map(user => { const mergedDocs = users.map(user => {
@@ -335,7 +335,7 @@ export const compare = users => {
users.push(.../** @type {any} */(mergedDocs)) users.push(.../** @type {any} */(mergedDocs))
const userArrayValues = users.map(u => u.getArray('array').toJSON()) const userArrayValues = users.map(u => u.getArray('array').toJSON())
const userMapValues = users.map(u => u.getMap('map').toJSON()) const userMapValues = users.map(u => u.getMap('map').toJSON())
const userXmlValues = users.map(u => u.get('xml', Y.YXmlElement).toString()) const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString())
const userTextValues = users.map(u => u.getText('text').toDelta()) const userTextValues = users.map(u => u.getText('text').toDelta())
for (const u of users) { for (const u of users) {
t.assert(u.store.pendingDs === null) t.assert(u.store.pendingDs === null)
@@ -362,8 +362,15 @@ export const compare = users => {
t.compare(userMapValues[i], userMapValues[i + 1]) t.compare(userMapValues[i], userMapValues[i + 1])
t.compare(userXmlValues[i], userXmlValues[i + 1]) t.compare(userXmlValues[i], userXmlValues[i + 1])
t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length) t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length)
t.compare(userTextValues[i], userTextValues[i + 1]) t.compare(userTextValues[i], userTextValues[i + 1], '', (constructor, a, b) => {
t.compare(Y.getStateVector(users[i].store), Y.getStateVector(users[i + 1].store)) if (a instanceof Y.AbstractType) {
t.compare(a.toJSON(), b.toJSON())
} else if (a !== b) {
t.fail('Deltas dont match')
}
return true
})
t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1]))
compareDS(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store)) compareDS(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
compareStructStores(users[i].store, users[i + 1].store) compareStructStores(users[i].store, users[i + 1].store)
} }
@@ -378,8 +385,8 @@ export const compare = users => {
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id)) export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
/** /**
* @param {Y.StructStore} ss1 * @param {import('../src/internals').StructStore} ss1
* @param {Y.StructStore} ss2 * @param {import('../src/internals').StructStore} ss2
*/ */
export const compareStructStores = (ss1, ss2) => { export const compareStructStores = (ss1, ss2) => {
t.assert(ss1.clients.size === ss2.clients.size) t.assert(ss1.clients.size === ss2.clients.size)
@@ -421,13 +428,13 @@ export const compareStructStores = (ss1, ss2) => {
} }
/** /**
* @param {Y.DeleteSet} ds1 * @param {import('../src/internals').DeleteSet} ds1
* @param {Y.DeleteSet} ds2 * @param {import('../src/internals').DeleteSet} ds2
*/ */
export const compareDS = (ds1, ds2) => { export const compareDS = (ds1, ds2) => {
t.assert(ds1.clients.size === ds2.clients.size) t.assert(ds1.clients.size === ds2.clients.size)
ds1.clients.forEach((deleteItems1, client) => { ds1.clients.forEach((deleteItems1, client) => {
const deleteItems2 = /** @type {Array<Y.DeleteItem>} */ (ds2.clients.get(client)) const deleteItems2 = /** @type {Array<import('../src/internals').DeleteItem>} */ (ds2.clients.get(client))
t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length) t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
for (let i = 0; i < deleteItems1.length; i++) { for (let i = 0; i < deleteItems1.length; i++) {
const di1 = deleteItems1[i] const di1 = deleteItems1[i]

View File

@@ -1,18 +1,14 @@
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
import {
UndoManager
} from '../src/internals.js'
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'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
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 Y.UndoManager(text0)
// items that are added & deleted in the same transaction won't be undo // items that are added & deleted in the same transaction won't be undo
text0.insert(0, 'test') text0.insert(0, 'test')
@@ -81,7 +77,7 @@ export const testDoubleUndo = tc => {
export const testUndoMap = tc => { export const testUndoMap = tc => {
const { testConnector, map0, map1 } = init(tc, { users: 2 }) const { testConnector, map0, map1 } = init(tc, { users: 2 })
map0.set('a', 0) map0.set('a', 0)
const undoManager = new UndoManager(map0) const undoManager = new Y.UndoManager(map0)
map0.set('a', 1) map0.set('a', 1)
undoManager.undo() undoManager.undo()
t.assert(map0.get('a') === 0) t.assert(map0.get('a') === 0)
@@ -120,7 +116,7 @@ export const testUndoMap = tc => {
*/ */
export const testUndoArray = tc => { export const testUndoArray = tc => {
const { testConnector, array0, array1 } = init(tc, { users: 3 }) const { testConnector, array0, array1 } = init(tc, { users: 3 })
const undoManager = new UndoManager(array0) const undoManager = new Y.UndoManager(array0)
array0.insert(0, [1, 2, 3]) array0.insert(0, [1, 2, 3])
array1.insert(0, [4, 5, 6]) array1.insert(0, [4, 5, 6])
testConnector.syncAll() testConnector.syncAll()
@@ -171,7 +167,7 @@ export const testUndoArray = tc => {
*/ */
export const testUndoXml = tc => { export const testUndoXml = tc => {
const { xml0 } = init(tc, { users: 3 }) const { xml0 } = init(tc, { users: 3 })
const undoManager = new UndoManager(xml0) const undoManager = new Y.UndoManager(xml0)
const child = new Y.XmlElement('p') const child = new Y.XmlElement('p')
xml0.insert(0, [child]) xml0.insert(0, [child])
const textchild = new Y.XmlText('content') const textchild = new Y.XmlText('content')
@@ -196,7 +192,7 @@ export const testUndoXml = tc => {
*/ */
export const testUndoEvents = tc => { export const testUndoEvents = tc => {
const { text0 } = init(tc, { users: 3 }) const { text0 } = init(tc, { users: 3 })
const undoManager = new UndoManager(text0) const undoManager = new Y.UndoManager(text0)
let counter = 0 let counter = 0
let receivedMetadata = -1 let receivedMetadata = -1
undoManager.on('stack-item-added', /** @param {any} event */ event => { undoManager.on('stack-item-added', /** @param {any} event */ event => {
@@ -222,7 +218,7 @@ export const testUndoEvents = tc => {
export const testTrackClass = tc => { export const testTrackClass = tc => {
const { users, text0 } = init(tc, { users: 3 }) const { users, text0 } = init(tc, { users: 3 })
// only track origins that are numbers // only track origins that are numbers
const undoManager = new UndoManager(text0, { trackedOrigins: new Set([Number]) }) const undoManager = new Y.UndoManager(text0, { trackedOrigins: new Set([Number]) })
users[0].transact(() => { users[0].transact(() => {
text0.insert(0, 'abc') text0.insert(0, 'abc')
}, 42) }, 42)
@@ -240,8 +236,8 @@ export const testTypeScope = tc => {
const text0 = new Y.Text() const text0 = new Y.Text()
const text1 = new Y.Text() const text1 = new Y.Text()
array0.insert(0, [text0, text1]) array0.insert(0, [text0, text1])
const undoManager = new UndoManager(text0) const undoManager = new Y.UndoManager(text0)
const undoManagerBoth = new UndoManager([text0, text1]) const undoManagerBoth = new Y.UndoManager([text0, text1])
text1.insert(0, 'abc') text1.insert(0, 'abc')
t.assert(undoManager.undoStack.length === 0) t.assert(undoManager.undoStack.length === 0)
t.assert(undoManagerBoth.undoStack.length === 1) t.assert(undoManagerBoth.undoStack.length === 1)
@@ -252,15 +248,35 @@ export const testTypeScope = tc => {
t.assert(text1.toString() === '') t.assert(text1.toString() === '')
} }
/**
* @param {t.TestCase} tc
*/
export const testUndoInEmbed = tc => {
const { text0 } = init(tc, { users: 3 })
const undoManager = new Y.UndoManager(text0)
const nestedText = new Y.Text('initial text')
undoManager.stopCapturing()
text0.insertEmbed(0, nestedText, { bold: true })
t.assert(nestedText.toString() === 'initial text')
undoManager.stopCapturing()
nestedText.delete(0, nestedText.length)
nestedText.insert(0, 'other text')
t.assert(nestedText.toString() === 'other text')
undoManager.undo()
t.assert(nestedText.toString() === 'initial text')
undoManager.undo()
t.assert(text0.length === 0)
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testUndoDeleteFilter = tc => { export const testUndoDeleteFilter = tc => {
/** /**
* @type {Array<Y.Map<any>>} * @type {Y.Array<any>}
*/ */
const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0) const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0)
const undoManager = new UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) }) const undoManager = new Y.UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) })
const map0 = new Y.Map() const map0 = new Y.Map()
map0.set('hi', 1) map0.set('hi', 1)
const map1 = new Y.Map() const map1 = new Y.Map()
@@ -270,3 +286,244 @@ export const testUndoDeleteFilter = tc => {
array0.get(0) array0.get(0)
t.assert(Array.from(array0.get(0).keys()).length === 1) t.assert(Array.from(array0.get(0).keys()).length === 1)
} }
/**
* This issue has been reported in https://discuss.yjs.dev/t/undomanager-with-external-updates/454/6
* @param {t.TestCase} tc
*/
export const testUndoUntilChangePerformed = tc => {
const doc = new Y.Doc()
const doc2 = new Y.Doc()
doc.on('update', update => Y.applyUpdate(doc2, update))
doc2.on('update', update => Y.applyUpdate(doc, update))
const yArray = doc.getArray('array')
const yArray2 = doc2.getArray('array')
const yMap = new Y.Map()
yMap.set('hello', 'world')
yArray.push([yMap])
const yMap2 = new Y.Map()
yMap2.set('key', 'value')
yArray.push([yMap2])
const undoManager = new Y.UndoManager([yArray], { trackedOrigins: new Set([doc.clientID]) })
const undoManager2 = new Y.UndoManager([doc2.get('array')], { trackedOrigins: new Set([doc2.clientID]) })
Y.transact(doc, () => yMap2.set('key', 'value modified'), doc.clientID)
undoManager.stopCapturing()
Y.transact(doc, () => yMap.set('hello', 'world modified'), doc.clientID)
Y.transact(doc2, () => yArray2.delete(0), doc2.clientID)
undoManager2.undo()
undoManager.undo()
t.compareStrings(yMap2.get('key'), 'value')
}
/**
* This issue has been reported in https://github.com/yjs/yjs/issues/317
* @param {t.TestCase} tc
*/
export const testUndoNestedUndoIssue = tc => {
const doc = new Y.Doc({ gc: false })
const design = doc.getMap()
const undoManager = new Y.UndoManager(design, { captureTimeout: 0 })
/**
* @type {Y.Map<any>}
*/
const text = new Y.Map()
const blocks1 = new Y.Array()
const blocks1block = new Y.Map()
doc.transact(() => {
blocks1block.set('text', 'Type Something')
blocks1.push([blocks1block])
text.set('blocks', blocks1block)
design.set('text', text)
})
const blocks2 = new Y.Array()
const blocks2block = new Y.Map()
doc.transact(() => {
blocks2block.set('text', 'Something')
blocks2.push([blocks2block])
text.set('blocks', blocks2block)
})
const blocks3 = new Y.Array()
const blocks3block = new Y.Map()
doc.transact(() => {
blocks3block.set('text', 'Something Else')
blocks3.push([blocks3block])
text.set('blocks', blocks3block)
})
t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } })
undoManager.undo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } })
undoManager.undo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } })
undoManager.undo()
t.compare(design.toJSON(), { })
undoManager.redo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } })
undoManager.redo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } })
undoManager.redo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } })
}
/**
* This issue has been reported in https://github.com/yjs/yjs/issues/355
*
* @param {t.TestCase} tc
*/
export const testConsecutiveRedoBug = tc => {
const doc = new Y.Doc()
const yRoot = doc.getMap()
const undoMgr = new Y.UndoManager(yRoot)
let yPoint = new Y.Map()
yPoint.set('x', 0)
yPoint.set('y', 0)
yRoot.set('a', yPoint)
undoMgr.stopCapturing()
yPoint.set('x', 100)
yPoint.set('y', 100)
undoMgr.stopCapturing()
yPoint.set('x', 200)
yPoint.set('y', 200)
undoMgr.stopCapturing()
yPoint.set('x', 300)
yPoint.set('y', 300)
undoMgr.stopCapturing()
t.compare(yPoint.toJSON(), { x: 300, y: 300 })
undoMgr.undo() // x=200, y=200
t.compare(yPoint.toJSON(), { x: 200, y: 200 })
undoMgr.undo() // x=100, y=100
t.compare(yPoint.toJSON(), { x: 100, y: 100 })
undoMgr.undo() // x=0, y=0
t.compare(yPoint.toJSON(), { x: 0, y: 0 })
undoMgr.undo() // nil
t.compare(yRoot.get('a'), undefined)
undoMgr.redo() // x=0, y=0
yPoint = yRoot.get('a')
t.compare(yPoint.toJSON(), { x: 0, y: 0 })
undoMgr.redo() // x=100, y=100
t.compare(yPoint.toJSON(), { x: 100, y: 100 })
undoMgr.redo() // x=200, y=200
t.compare(yPoint.toJSON(), { x: 200, y: 200 })
undoMgr.redo() // expected x=300, y=300, actually nil
t.compare(yPoint.toJSON(), { x: 300, y: 300 })
}
/**
* This issue has been reported in https://github.com/yjs/yjs/issues/304
*
* @param {t.TestCase} tc
*/
export const testUndoXmlBug = tc => {
const origin = 'origin'
const doc = new Y.Doc()
const fragment = doc.getXmlFragment('t')
const undoManager = new Y.UndoManager(fragment, {
captureTimeout: 0,
trackedOrigins: new Set([origin])
})
// create element
doc.transact(() => {
const e = new Y.XmlElement('test-node')
e.setAttribute('a', '100')
e.setAttribute('b', '0')
fragment.insert(fragment.length, [e])
}, origin)
// change one attribute
doc.transact(() => {
const e = fragment.get(0)
e.setAttribute('a', '200')
}, origin)
// change both attributes
doc.transact(() => {
const e = fragment.get(0)
e.setAttribute('a', '180')
e.setAttribute('b', '50')
}, origin)
undoManager.undo()
undoManager.undo()
undoManager.undo()
undoManager.redo()
undoManager.redo()
undoManager.redo()
t.compare(fragment.toString(), '<test-node a="180" b="50"></test-node>')
}
/**
* This issue has been reported in https://github.com/yjs/yjs/issues/343
*
* @param {t.TestCase} tc
*/
export const testUndoBlockBug = tc => {
const doc = new Y.Doc({ gc: false })
const design = doc.getMap()
const undoManager = new Y.UndoManager(design, { captureTimeout: 0 })
const text = new Y.Map()
const blocks1 = new Y.Array()
const blocks1block = new Y.Map()
doc.transact(() => {
blocks1block.set('text', '1')
blocks1.push([blocks1block])
text.set('blocks', blocks1block)
design.set('text', text)
})
const blocks2 = new Y.Array()
const blocks2block = new Y.Map()
doc.transact(() => {
blocks2block.set('text', '2')
blocks2.push([blocks2block])
text.set('blocks', blocks2block)
})
const blocks3 = new Y.Array()
const blocks3block = new Y.Map()
doc.transact(() => {
blocks3block.set('text', '3')
blocks3.push([blocks3block])
text.set('blocks', blocks3block)
})
const blocks4 = new Y.Array()
const blocks4block = new Y.Map()
doc.transact(() => {
blocks4block.set('text', '4')
blocks4.push([blocks4block])
text.set('blocks', blocks4block)
})
// {"text":{"blocks":{"text":"4"}}}
undoManager.undo() // {"text":{"blocks":{"3"}}}
undoManager.undo() // {"text":{"blocks":{"text":"2"}}}
undoManager.undo() // {"text":{"blocks":{"text":"1"}}}
undoManager.undo() // {}
undoManager.redo() // {"text":{"blocks":{"text":"1"}}}
undoManager.redo() // {"text":{"blocks":{"text":"2"}}}
undoManager.redo() // {"text":{"blocks":{"text":"3"}}}
undoManager.redo() // {"text":{}}
t.compare(design.toJSON(), { text: { blocks: { text: '4' } } })
}

View File

@@ -1,9 +1,9 @@
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing'
import { init, compare } from './testHelper.js' // eslint-disable-line import { init, compare } from './testHelper.js' // eslint-disable-line
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js' import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding'
/** /**
* @typedef {Object} Enc * @typedef {Object} Enc
@@ -166,9 +166,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
const targetSV = Y.encodeStateVectorFromUpdateV2(Y.mergeUpdatesV2(updates.slice(0, j))) const targetSV = Y.encodeStateVectorFromUpdateV2(Y.mergeUpdatesV2(updates.slice(0, j)))
const diffed = enc.diffUpdate(mergedUpdates, targetSV) const diffed = enc.diffUpdate(mergedUpdates, targetSV)
const diffedMeta = enc.parseUpdateMeta(diffed) const diffedMeta = enc.parseUpdateMeta(diffed)
const decDiffedSV = Y.decodeStateVector(enc.encodeStateVectorFromUpdate(diffed))
t.compare(partMeta, diffedMeta) t.compare(partMeta, diffedMeta)
t.compare(decDiffedSV, partMeta.to)
{ {
// We can'd do the following // We can'd do the following
// - t.compare(diffed, mergedDeletes) // - t.compare(diffed, mergedDeletes)
@@ -242,5 +240,49 @@ export const testMergeUpdates2 = tc => {
} }
/** /**
* @todo be able to apply Skip structs to Yjs docs * @param {t.TestCase} tc
*/ */
export const testMergePendingUpdates = tc => {
const yDoc = new Y.Doc()
/**
* @type {Array<Uint8Array>}
*/
const serverUpdates = []
yDoc.on('update', (update, origin, c) => {
serverUpdates.splice(serverUpdates.length, 0, update)
})
const yText = yDoc.getText('textBlock')
yText.applyDelta([{ insert: 'r' }])
yText.applyDelta([{ insert: 'o' }])
yText.applyDelta([{ insert: 'n' }])
yText.applyDelta([{ insert: 'e' }])
yText.applyDelta([{ insert: 'n' }])
const yDoc1 = new Y.Doc()
Y.applyUpdate(yDoc1, serverUpdates[0])
const update1 = Y.encodeStateAsUpdate(yDoc1)
const yDoc2 = new Y.Doc()
Y.applyUpdate(yDoc2, update1)
Y.applyUpdate(yDoc2, serverUpdates[1])
const update2 = Y.encodeStateAsUpdate(yDoc2)
const yDoc3 = new Y.Doc()
Y.applyUpdate(yDoc3, update2)
Y.applyUpdate(yDoc3, serverUpdates[3])
const update3 = Y.encodeStateAsUpdate(yDoc3)
const yDoc4 = new Y.Doc()
Y.applyUpdate(yDoc4, update3)
Y.applyUpdate(yDoc4, serverUpdates[2])
const update4 = Y.encodeStateAsUpdate(yDoc4)
const yDoc5 = new Y.Doc()
Y.applyUpdate(yDoc5, update4)
Y.applyUpdate(yDoc5, serverUpdates[4])
// @ts-ignore
const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
const yText5 = yDoc5.getText('textBlock')
t.compareStrings(yText5.toString(), 'nenor')
}

View File

@@ -1,9 +1,9 @@
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
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'
import * as prng from 'lib0/prng.js' import * as prng from 'lib0/prng'
import * as math from 'lib0/math.js' import * as math from 'lib0/math'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
@@ -32,6 +32,78 @@ export const testSlice = tc => {
t.compareArrays(arr.slice(0, 2), [0, 1]) t.compareArrays(arr.slice(0, 2), [0, 1])
} }
/**
* Debugging yjs#297 - a critical bug connected to the search-marker approach
*
* @param {t.TestCase} tc
*/
export const testLengthIssue = tc => {
const doc1 = new Y.Doc()
const arr = doc1.getArray('array')
arr.push([0, 1, 2, 3])
arr.delete(0)
arr.insert(0, [0])
t.assert(arr.length === arr.toArray().length)
doc1.transact(() => {
arr.delete(1)
t.assert(arr.length === arr.toArray().length)
arr.insert(1, [1])
t.assert(arr.length === arr.toArray().length)
arr.delete(2)
t.assert(arr.length === arr.toArray().length)
arr.insert(2, [2])
t.assert(arr.length === arr.toArray().length)
})
t.assert(arr.length === arr.toArray().length)
arr.delete(1)
t.assert(arr.length === arr.toArray().length)
arr.insert(1, [1])
t.assert(arr.length === arr.toArray().length)
}
/**
* Debugging yjs#314
*
* @param {t.TestCase} tc
*/
export const testLengthIssue2 = tc => {
const doc = new Y.Doc()
const next = doc.getArray()
doc.transact(() => {
next.insert(0, ['group2'])
})
doc.transact(() => {
next.insert(1, ['rectangle3'])
})
doc.transact(() => {
next.delete(0)
next.insert(0, ['rectangle3'])
})
next.delete(1)
doc.transact(() => {
next.insert(1, ['ellipse4'])
})
doc.transact(() => {
next.insert(2, ['ellipse3'])
})
doc.transact(() => {
next.insert(3, ['ellipse2'])
})
doc.transact(() => {
doc.transact(() => {
t.fails(() => {
next.insert(5, ['rectangle2'])
})
next.insert(4, ['rectangle2'])
})
doc.transact(() => {
// this should not throw an error message
next.delete(4)
})
})
console.log(next.toArray())
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -416,6 +488,11 @@ const arrayTransactions = [
map.set('someprop', 43) map.set('someprop', 43)
map.set('someprop', 44) map.set('someprop', 44)
}, },
function insertTypeNull (user, gen) {
const yarray = user.getArray('array')
const pos = prng.int32(gen, 0, yarray.length)
yarray.insert(pos, [null])
},
function _delete (user, gen) { function _delete (user, gen) {
const yarray = user.getArray('array') const yarray = user.getArray('array')
const length = yarray.length const length = yarray.length
@@ -424,7 +501,7 @@ const arrayTransactions = [
let delLength = prng.int32(gen, 1, math.min(2, length - somePos)) let delLength = prng.int32(gen, 1, math.min(2, length - somePos))
if (prng.bool(gen)) { if (prng.bool(gen)) {
const type = yarray.get(somePos) const type = yarray.get(somePos)
if (type.length > 0) { if (type instanceof Y.Array && type.length > 0) {
somePos = prng.int32(gen, 0, type.length - 1) somePos = prng.int32(gen, 0, type.length - 1)
delLength = prng.int32(gen, 0, math.min(2, type.length - somePos)) delLength = prng.int32(gen, 0, math.min(2, type.length - somePos))
type.delete(somePos, delLength) type.delete(somePos, delLength)

View File

@@ -5,8 +5,8 @@ import {
} from '../src/internals.js' } from '../src/internals.js'
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'
import * as prng from 'lib0/prng.js' import * as prng from 'lib0/prng'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
@@ -42,6 +42,7 @@ export const testBasicMapTests = tc => {
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 }) const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
users[2].disconnect() users[2].disconnect()
map0.set('null', null)
map0.set('number', 1) map0.set('number', 1)
map0.set('string', 'hello Y') map0.set('string', 'hello Y')
map0.set('object', { key: { key2: 'value' } }) map0.set('object', { key: { key2: 'value' } })
@@ -54,26 +55,29 @@ export const testBasicMapTests = tc => {
array.insert(0, [0]) array.insert(0, [0])
array.insert(0, [-1]) array.insert(0, [-1])
t.assert(map0.get('null') === null, 'client 0 computed the change (null)')
t.assert(map0.get('number') === 1, 'client 0 computed the change (number)') t.assert(map0.get('number') === 1, 'client 0 computed the change (number)')
t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)') t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)')
t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)') t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)')
t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)') t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)')
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)') t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)') t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
t.assert(map0.size === 6, 'client 0 map has correct size') t.assert(map0.size === 7, 'client 0 map has correct size')
users[2].connect() users[2].connect()
testConnector.flushAllMessages() testConnector.flushAllMessages()
t.assert(map1.get('null') === null, 'client 1 received the update (null)')
t.assert(map1.get('number') === 1, 'client 1 received the update (number)') t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)') t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)') t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)')
t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)') t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)')
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)') t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)') t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
t.assert(map1.size === 6, 'client 1 map has correct size') t.assert(map1.size === 7, 'client 1 map has correct size')
// compare disconnected user // compare disconnected user
t.assert(map2.get('null') === null, 'client 2 received the update (null) - was disconnected')
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected') t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected') t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)') t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)')
@@ -189,6 +193,49 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
compare(users) compare(users)
} }
/**
* @param {t.TestCase} tc
*/
export const testSetAndClearOfMapProperties = tc => {
const { testConnector, users, map0 } = init(tc, { users: 1 })
map0.set('stuff', 'c0')
map0.set('otherstuff', 'c1')
map0.clear()
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.assert(u.get('stuff') === undefined)
t.assert(u.get('otherstuff') === undefined)
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
}
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testSetAndClearOfMapPropertiesWithConflicts = tc => {
const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
map1.set('stuff', 'c2')
map2.set('stuff', 'c3')
testConnector.flushAllMessages()
map0.set('otherstuff', 'c0')
map1.set('otherstuff', 'c1')
map2.set('otherstuff', 'c2')
map3.set('otherstuff', 'c3')
map3.clear()
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.assert(u.get('stuff') === undefined)
t.assert(u.get('otherstuff') === undefined)
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
}
compare(users)
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -335,6 +382,30 @@ export const testThrowsAddAndUpdateAndDeleteEvents = tc => {
compare(users) compare(users)
} }
/**
* @param {t.TestCase} tc
*/
export const testThrowsDeleteEventsOnClear = tc => {
const { users, map0 } = init(tc, { users: 2 })
/**
* @type {Object<string,any>}
*/
let event = {}
map0.observe(e => {
event = e // just put it on event, should be thrown synchronously anyway
})
// set values
map0.set('stuff', 4)
map0.set('otherstuff', new Y.Array())
// clear
map0.clear()
compareEvent(event, {
keysChanged: new Set(['stuff', 'otherstuff']),
target: map0
})
compare(users)
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */

View File

@@ -1,10 +1,36 @@
import * as Y from './testHelper.js' import * as Y from './testHelper.js'
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing'
import * as prng from 'lib0/prng.js' import * as prng from 'lib0/prng'
import * as math from 'lib0/math.js' import * as math from 'lib0/math'
const { init, compare } = Y const { init, compare } = Y
/**
* In this test we are mainly interested in the cleanup behavior and whether the resulting delta makes sense.
* It is fine if the resulting delta is not minimal. But applying the delta to a rich-text editor should result in a
* synced document.
*
* @param {t.TestCase} tc
*/
export const testDeltaAfterConcurrentFormatting = tc => {
const { text0, text1, testConnector } = init(tc, { users: 2 })
text0.insert(0, 'abcde')
testConnector.flushAllMessages()
text0.format(0, 3, { bold: true })
text1.format(2, 2, { bold: true })
/**
* @type {any}
*/
const deltas = []
text1.observe(event => {
if (event.delta.length > 0) {
deltas.push(event.delta)
}
})
testConnector.flushAllMessages()
t.compare(deltas, [[{ retain: 3, attributes: { bold: true } }, { retain: 2, attributes: { bold: null } }]])
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -121,6 +147,28 @@ export const testNotMergeEmptyLinesFormat = tc => {
]) ])
} }
/**
* @param {t.TestCase} tc
*/
export const testPreserveAttributesThroughDelete = tc => {
const ydoc = new Y.Doc()
const testText = ydoc.getText('test')
testText.applyDelta([
{ insert: 'Text' },
{ insert: '\n', attributes: { title: true } },
{ insert: '\n' }
])
testText.applyDelta([
{ retain: 4 },
{ delete: 1 },
{ retain: 1, attributes: { title: true } }
])
t.compare(testText.toDelta(), [
{ insert: 'Text' },
{ insert: '\n', attributes: { title: true } }
])
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -134,6 +182,29 @@ export const testGetDeltaWithEmbeds = tc => {
}]) }])
} }
/**
* @param {t.TestCase} tc
*/
export const testTypesAsEmbed = tc => {
const { text0, text1, testConnector } = init(tc, { users: 2 })
text0.applyDelta([{
insert: new Y.Map([['key', 'val']])
}])
t.compare(text0.toDelta()[0].insert.toJSON(), { key: 'val' })
let firedEvent = false
text1.observe(event => {
const d = event.delta
t.assert(d.length === 1)
t.compare(d.map(x => /** @type {Y.AbstractType<any>} */ (x.insert).toJSON()), [{ key: 'val' }])
firedEvent = true
})
testConnector.flushAllMessages()
const delta = text1.toDelta()
t.assert(delta.length === 1)
t.compare(delta[0].insert.toJSON(), { key: 'val' })
t.assert(firedEvent, 'fired the event observer containing a Type-Embed')
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -248,6 +319,41 @@ export const testFormattingRemovedInMidText = tc => {
t.assert(Y.getTypeChildren(text0).length === 3) t.assert(Y.getTypeChildren(text0).length === 3)
} }
/**
* Reported in https://github.com/yjs/yjs/issues/344
*
* @param {t.TestCase} tc
*/
export const testFormattingDeltaUnnecessaryAttributeChange = tc => {
const { text0, text1, testConnector } = init(tc, { users: 2 })
text0.insert(0, '\n', {
PARAGRAPH_STYLES: 'normal',
LIST_STYLES: 'bullet'
})
text0.insert(1, 'abc', {
PARAGRAPH_STYLES: 'normal'
})
testConnector.flushAllMessages()
/**
* @type {Array<any>}
*/
const deltas = []
text0.observe(event => {
deltas.push(event.delta)
})
text1.observe(event => {
deltas.push(event.delta)
})
text1.format(0, 1, { LIST_STYLES: 'number' })
testConnector.flushAllMessages()
const filteredDeltas = deltas.filter(d => d.length > 0)
t.assert(filteredDeltas.length === 2)
t.compare(filteredDeltas[0], [
{ retain: 1, attributes: { LIST_STYLES: 'number' } }
])
t.compare(filteredDeltas[0], filteredDeltas[1])
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -438,6 +544,69 @@ export const testSplitSurrogateCharacter = tc => {
} }
} }
/**
* Search marker bug https://github.com/yjs/yjs/issues/307
*
* @param {t.TestCase} tc
*/
export const testSearchMarkerBug1 = tc => {
const { users, text0, text1, testConnector } = init(tc, { users: 2 })
users[0].on('update', update => {
users[0].transact(() => {
Y.applyUpdate(users[0], update)
})
})
users[0].on('update', update => {
users[1].transact(() => {
Y.applyUpdate(users[1], update)
})
})
text0.insert(0, 'a_a')
testConnector.flushAllMessages()
text0.insert(2, 's')
testConnector.flushAllMessages()
text1.insert(3, 'd')
testConnector.flushAllMessages()
text0.delete(0, 5)
testConnector.flushAllMessages()
text0.insert(0, 'a_a')
testConnector.flushAllMessages()
text0.insert(2, 's')
testConnector.flushAllMessages()
text1.insert(3, 'd')
testConnector.flushAllMessages()
t.compareStrings(text0.toString(), text1.toString())
t.compareStrings(text0.toString(), 'a_sda')
compare(users)
}
/**
* Reported in https://github.com/yjs/yjs/pull/32
*
* @param {t.TestCase} tc
*/
export const testFormattingBug = async tc => {
const ydoc1 = new Y.Doc()
const ydoc2 = new Y.Doc()
const text1 = ydoc1.getText()
text1.insert(0, '\n\n\n')
text1.format(0, 3, { url: 'http://example.com' })
ydoc1.getText().format(1, 1, { url: 'http://docs.yjs.dev' })
ydoc2.getText().format(1, 1, { url: 'http://docs.yjs.dev' })
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc1))
const text2 = ydoc2.getText()
const expectedResult = [
{ insert: '\n', attributes: { url: 'http://example.com' } },
{ insert: '\n', attributes: { url: 'http://docs.yjs.dev' } },
{ insert: '\n', attributes: { url: 'http://example.com' } }
]
t.compare(text1.toDelta(), expectedResult)
t.compare(text1.toDelta(), text2.toDelta())
console.log(text1.toDelta())
}
// RANDOM TESTS // RANDOM TESTS
let charCounter = 0 let charCounter = 0
@@ -573,7 +742,11 @@ const qChanges = [
(y, gen) => { // insert embed (y, gen) => { // insert embed
const ytext = y.getText('text') const ytext = y.getText('text')
const insertPos = prng.int32(gen, 0, ytext.length) const insertPos = prng.int32(gen, 0, ytext.length)
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' }) if (prng.bool(gen)) {
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
} else {
ytext.insertEmbed(insertPos, new Y.Map([[prng.word(gen), prng.word(gen)]]))
}
}, },
/** /**
* @param {Y.Doc} y * @param {Y.Doc} y
@@ -620,8 +793,12 @@ const qChanges = [
*/ */
const checkResult = result => { const checkResult = result => {
for (let i = 1; i < result.testObjects.length; i++) { for (let i = 1; i < result.testObjects.length; i++) {
const p1 = result.users[i].getText('text').toDelta() /**
const p2 = result.users[i].getText('text').toDelta() * @param {any} d
*/
const typeToObject = d => d.insert instanceof Y.AbstractType ? d.insert.toJSON() : d
const p1 = result.users[i].getText('text').toDelta().map(typeToObject)
const p2 = result.users[i].getText('text').toDelta().map(typeToObject)
t.compare(p1, p2) t.compare(p1, p2)
} }
// Uncomment this to find formatting-cleanup issues // Uncomment this to find formatting-cleanup issues

View File

@@ -1,7 +1,7 @@
import { init, compare } from './testHelper.js' import { init, compare } from './testHelper.js'
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'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
@@ -15,6 +15,23 @@ export const testSetProperty = tc => {
compare(users) compare(users)
} }
/**
* @param {t.TestCase} tc
*/
export const testHasProperty = tc => {
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
xml0.setAttribute('height', '10')
t.assert(xml0.hasAttribute('height'), 'Simple set+has works')
testConnector.flushAllMessages()
t.assert(xml1.hasAttribute('height'), 'Simple set+has works (remote)')
xml0.removeAttribute('height')
t.assert(!xml0.hasAttribute('height'), 'Simple set+remove+has works')
testConnector.flushAllMessages()
t.assert(!xml1.hasAttribute('height'), 'Simple set+remove+has works (remote)')
compare(users)
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -133,3 +150,36 @@ export const testInsertafter = tc => {
el.insertAfter(deepsecond1, [new Y.XmlText()]) el.insertAfter(deepsecond1, [new Y.XmlText()])
}) })
} }
/**
* @param {t.TestCase} tc
*/
export const testClone = tc => {
const ydoc = new Y.Doc()
const yxml = ydoc.getXmlFragment()
const first = new Y.XmlText('text')
const second = new Y.XmlElement('p')
const third = new Y.XmlElement('p')
yxml.push([first, second, third])
t.compareArrays(yxml.toArray(), [first, second, third])
const cloneYxml = yxml.clone()
ydoc.getArray('copyarr').insert(0, [cloneYxml])
t.assert(cloneYxml.length === 3)
t.compare(cloneYxml.toJSON(), yxml.toJSON())
}
/**
* @param {t.TestCase} tc
*/
export const testFormattingBug = tc => {
const ydoc = new Y.Doc()
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
const delta = [
{ insert: 'A', attributes: { em: {}, strong: {} } },
{ insert: 'B', attributes: { em: {} } },
{ insert: 'C', attributes: { em: {}, strong: {} } }
]
yxml.applyDelta(delta)
t.compare(yxml.toDelta(), delta)
}