Compare commits

..

188 Commits

Author SHA1 Message Date
Kevin Jahns
31b4ab8d0c 13.5.44 2023-01-01 18:25:03 +01:00
Kevin Jahns
ab978b2003 add exports.module field - #438 2023-01-01 18:23:21 +01:00
Kevin Jahns
afc6728c9e 13.5.43 2022-11-30 12:28:28 +01:00
Kevin Jahns
0ef5bd42fe lint readme 2022-11-30 12:26:55 +01:00
Kevin Jahns
3ece681758 allow transactions within event handlers having different origins 2022-11-30 12:09:19 +01:00
Kevin Jahns
cac9407185 remove snapshot param in yxml.getAttributes 2022-11-30 11:34:34 +01:00
Kevin Jahns
7ea8ffebae Merge pull request #482 from joakim/patch-2
Add missing word
2022-11-08 16:30:12 +01:00
Joakim
d7751c16fd Add missing word 2022-11-05 21:15:33 +01:00
Kevin Jahns
a64c51ec06 Merge pull request #478 from ViktorQvarfordt/patch-1
Add Sana to -Who Is Using Yjs-
2022-10-24 13:10:31 +02:00
Viktor Qvarfordt
7405057037 Add Sana to -Who Is Using Yjs- 2022-10-23 21:34:43 +02:00
Kevin Jahns
6208b82872 13.5.42 2022-10-18 16:52:38 +02:00
Kevin Jahns
12a9134b09 lint 2022-10-18 16:51:07 +02:00
Kevin Jahns
7395229086 Port test from @PatrickShaw #432. Allow infinite captureTimeout in UndoManager #431. Closes #432 2022-10-18 16:45:30 +02:00
Kevin Jahns
8fb73edd97 Merge pull request #453 from Cargo/main
Allow updating captureTimeout on UndoManager instances
2022-10-03 11:58:03 +02:00
Kevin Jahns
f1ad5686c1 Add Hyperquery to -Who Is Using Yjs- 2022-10-02 17:10:14 +02:00
Kevin Jahns
ed9236bdc7 Merge pull request #464 from MaxNoetzold/main
Add y-mongodb-provider to provider list in README
2022-09-26 22:08:43 +02:00
Kevin Jahns
5405fd2d7c Merge pull request #465 from neo/patch-1
Remove unused return in `forEach` of `YMap`
2022-09-20 13:07:23 +02:00
Wenchen Li
12667f6b66 Remove unused return in forEach of YMap
If the idea is to keep the API as close to the JS Map as possible, maybe we should consider not returning here.

Ref: https://github.com/microsoft/TypeScript/blob/v4.8.3/lib/lib.es2015.collection.d.ts#L28-L31
2022-09-19 18:19:41 -04:00
MaxNoetzold
3d7ef7e28b add y-mongodb-provider to provider list in README 2022-09-18 14:17:52 +02:00
Kevin Jahns
56267e0a7d Merge pull request #455 from scenaristeur/patch-1
Update INTERNALS.md
2022-09-16 23:26:04 +02:00
Kevin Jahns
da71f6fa45 Merge pull request #463 from doodlewind/patch-1
add AFFiNE to sponsor list
2022-09-14 17:43:04 +02:00
Kevin Jahns
588788fbef fix snapshot diff calculation naming bug 2022-09-14 00:37:06 +02:00
Yifeng Wang
fb9df6efe2 add AFFiNE to sponsor list 2022-09-13 21:31:10 +08:00
Kevin Jahns
a69ecb0287 Merge pull request #460 from regischen/fix-typo
fix typo
2022-08-25 12:03:42 +02:00
regischen
923fc6e06e fix typo 2022-08-25 17:35:24 +08:00
David
0fdfd93e4b Update INTERNALS.md
Typo
2022-08-14 10:10:14 +02:00
Aart Rost
e0e5f8d2ea Allow updating captureTimeout on UndoManager instances
Used to pause the undoManager by toggling the timeout with `yUndoManager.captureTimeout = Number.MAX_VALUE`
2022-08-10 14:07:40 -07:00
Kevin Jahns
daf034cf75 Merge pull request #452 from aryzing/patch-1
Update README.md
2022-08-08 10:57:13 +02:00
Eduard Bardají Puig
2157ebb4d0 Update README.md
Add missing `.getText()` method to docs.
2022-08-07 20:52:16 +02:00
Kevin Jahns
97ef4ae1e0 13.5.41 2022-07-28 14:14:27 +02:00
Kevin Jahns
df2d59e2fb UndoManager: fix special deletion case. closes #447 closes #443 2022-07-28 14:12:21 +02:00
Kevin Jahns
7a61c90261 13.5.40 2022-07-22 14:24:44 +02:00
Kevin Jahns
6fa8778fc7 add doc parameter to UndoManager 2022-07-22 14:22:46 +02:00
Kevin Jahns
1bc9308566 improve already-imported message further 2022-06-16 12:57:04 +02:00
Kevin Jahns
a5e0448a92 Merge pull request #434 from PatrickShaw/globalThis-add
Prefer globalThis over window and global
2022-06-15 12:04:09 +02:00
Kevin Jahns
c0c2b3347b 13.5.39 2022-06-15 10:49:48 +02:00
Kevin Jahns
6258ba1ce9 Merge pull request #435 from yogas/export_UpdateEncoderV1
export UpdateEncoderV1
2022-06-15 10:47:04 +02:00
Vladimir Shapovalov
5a7ee74f68 export UpdateEncoderV1 2022-06-14 13:16:35 +07:00
Kevin Jahns
29fb4a0aab improve double-import error message 2022-06-13 10:27:04 +02:00
Patrick Shaw
8937494bdd Added globalThis 2022-06-01 13:36:32 +10:00
Kevin Jahns
4504196d5c add gitter & discord link 2022-05-19 22:15:10 +02:00
Kevin Jahns
0c8d29bfff Merge pull request #424 from KentoMoriwaki/patch-1
Add support for esm in node.js
2022-05-17 14:38:25 +02:00
Kento Moriwaki
43384e4148 Add support for esm in node.js 2022-05-17 21:18:04 +09:00
Kevin Jahns
a2b62b0a58 13.5.38 2022-05-14 18:11:57 +02:00
Kevin Jahns
6febf51b1a fix captureTransaction 2022-05-14 18:10:19 +02:00
Kevin Jahns
5a4816a1b2 13.5.37 2022-05-14 14:25:28 +02:00
Kevin Jahns
4ad8af9a80 Add option to UndoManager to filter transactions 2022-05-14 14:23:47 +02:00
Kevin Jahns
fc25136b25 13.5.36 2022-05-09 12:55:19 +02:00
Kevin Jahns
ece1fe5426 minimize changes when formatting text - #422 2022-05-09 12:53:26 +02:00
Kevin Jahns
40196ae0a3 add slidebeamer as user 2022-04-27 16:33:00 +02:00
Kevin Jahns
bdefe0526d 13.5.35 2022-04-23 15:15:47 +02:00
Kevin Jahns
dbbb86adc7 bump dependencies 2022-04-23 15:14:05 +02:00
Kevin Jahns
1c9c97ffe6 add yxml.forEach method - closes #421 2022-04-23 15:13:26 +02:00
Kevin Jahns
14c14de21e simplify testhelper 2022-04-20 18:01:33 +02:00
Kevin Jahns
71fad52854 fix typing information YMap.iterator - fixes #420 2022-04-20 13:09:56 +02:00
Kevin Jahns
3935ba1faa update sponsors message 2022-04-18 19:12:41 +02:00
Kevin Jahns
4aacb487d2 complete documentation about relative positions 2022-04-17 16:03:20 +02:00
Kevin Jahns
5f56baa23e remove circle-ci 2022-04-06 23:31:49 +02:00
Kevin Jahns
8d809ebacb Adding Skiff as a user 2022-04-05 09:54:45 +02:00
Kevin Jahns
92624afbff 13.5.34 2022-04-04 16:23:21 +02:00
Kevin Jahns
1e8efd5104 bump lib0 for compatibility with y-crdt 2022-04-04 16:21:41 +02:00
Kevin Jahns
7b680f1bda 13.5.33 2022-03-30 14:55:10 +02:00
Kevin Jahns
806bf3f6dd lint 2022-03-30 14:53:14 +02:00
Kevin Jahns
42fe19daf1 require updated lib0 dependency 2022-03-30 14:51:21 +02:00
Kevin Jahns
7d3de7fa07 add blocksurvey.io as a user 2022-03-28 10:50:48 +02:00
Kevin Jahns
63c1cb4eb9 bump dependencies 2022-03-28 10:38:54 +02:00
Kevin Jahns
be1449a7af 13.5.32 2022-03-26 10:31:13 +01:00
Kevin Jahns
a22b3cdbc1 add option to UndoManager to ignore remote map changes. implements #390 2022-03-26 10:29:19 +01:00
Kevin Jahns
e9a0dc4ed2 add destroy logic 2022-03-25 11:08:30 +01:00
Kevin Jahns
b0b276d964 13.5.31 2022-03-25 11:01:57 +01:00
Kevin Jahns
d3e117702c add method to add & remove tracked origins 2022-03-25 11:00:07 +01:00
Kevin Jahns
ff5067e149 13.5.30 2022-03-22 09:47:10 +01:00
Kevin Jahns
f80e39a477 Merge pull request #408 from rileyjshaw/upgrade-lib0
Upgrade lib0 from v0.2.43 to v0.2.47
2022-03-22 09:43:41 +01:00
Riley Shaw
f70198333a Upgrade lib0 from v0.2.43 to v0.2.47
After upgrading to the latest version (13.5.29), I started to see the
following error:

```
ERROR in ./node_modules/yjs/dist/yjs.mjs 4048:13-26
export 'isArray' (imported as 'array') was not found in 'lib0/array' (possible exports: appendTo, copy, create, equalFlat, every, flatten, from, last, some)
```

It looks like `array.isArray` was added to `lib0` in [v0.2.47](https://github.com/dmonad/lib0/releases/tag/v0.2.47).
Updating the version in `package.json` fixed the error in my project.
2022-03-21 19:16:23 -04:00
Kevin Jahns
3c31b22a92 13.5.29 2022-03-21 00:00:00 +01:00
Kevin Jahns
6b8cef29e2 address #398 2022-03-20 23:58:14 +01:00
Kevin Jahns
4a06492fb1 add stack-item-updated event to Y.UndoManager. implements #407 2022-03-20 22:49:23 +01:00
Kevin Jahns
46fbce0de8 more utility around Y.UndoManager 2022-03-20 22:41:33 +01:00
Kevin Jahns
239703fe5c Merge pull request #401 from dylans/patch-1
Add Living Spec to readme
2022-03-19 13:22:10 +01:00
Kevin Jahns
5e907e3281 Merge pull request #404 from alderzhang/alder/issue-403
fix decodeUpdateV2 bug
2022-03-15 13:53:12 +01:00
alderzhang
6aea35246b fix decodeUpdateV2 bug 2022-03-15 15:22:51 +08:00
Dylan Schiemann
5058189a46 Add Living Spec to readme
We've sponsored via GitHub and OpenCollective for a while and have made a few contributions. We'd like to be added to the list of users if appropriate. Thanks.
2022-03-13 04:23:17 -07:00
Kevin Jahns
4db3439bb1 Merge pull request #397 from sep2/patch-1
add immer-yjs to Bindings
2022-03-09 22:06:56 +01:00
Felix
aa5463b06d add immer-yjs to Bindings 2022-03-07 11:45:50 +08:00
Kevin Jahns
afe8e52840 13.5.28 2022-03-02 13:27:33 +01:00
Kevin Jahns
d0f9c4a27f lint 2022-03-02 13:25:56 +01:00
Kevin Jahns
a5ffdce342 bump dependencies 2022-03-02 13:23:18 +01:00
Kevin Jahns
67d27dfca2 update readme 2022-03-02 13:19:00 +01:00
Kevin Jahns
9f1548204a Merge pull request #376 from fson/yevent-target-type
Add more accurate typing for YEvent.target
2022-03-02 13:18:00 +01:00
Kevin Jahns
46e108f345 Merge pull request #388 from sanalabs/uk/decode-methods
Add decode methods
2022-03-01 15:16:59 +01:00
Kevin Jahns
bda622f523 Merge pull request #391 from Flamenco/patch-1
Fix typo
2022-03-01 15:15:58 +01:00
Kevin Jahns
fef9e39d91 Merge pull request #393 from dkuhnert/issue-392
cleanup redundant text attributes when delete attributes
2022-02-24 22:07:49 +01:00
Kevin Jahns
5751a12c11 add PRSM to projects 2022-02-24 14:45:17 +01:00
dkuhnert
fddb620d41 cleanup redundant text attributes when delete attributes
fixes #392
2022-02-23 18:20:26 +01:00
dkuhnert
abf3fab1b6 cleanup redundant text attributes when delete attributes
fixes #392
2022-02-23 14:53:31 +01:00
Flamenco
69e2375dc5 Fix typo 2022-02-18 08:15:45 -05:00
Kevin Jahns
058a50285c add dynaboard to the list of projects 2022-02-16 21:15:19 +01:00
Ulf Karlsson
8678ef62d6 Remove curr.info 2022-02-07 20:00:10 +01:00
Ulf Karlsson
db53b6c720 Add decode methods 2022-02-07 17:15:06 +01:00
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
Ville Immonen
81a36a2762 add more accurate typing for YEvent.target 2022-01-15 14:22:17 +02: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
Daishi Kato
15ea4ee805 Update README.md 2021-07-25 10:08:43 +09:00
37 changed files with 6604 additions and 1543 deletions

View File

@@ -1,7 +0,0 @@
version: 2.1
orbs:
node: circleci/node@3.0.0
workflows:
node-tests:
jobs:
- node/test

View File

@@ -66,7 +66,7 @@ fields, respectively. These are used when peers concurrently insert at the same
location in a document. Though quite rare in practice, Yjs needs to make sure
the list items always resolve to the same order on all peers. The actual logic
is relatively simple - its only a couple dozen lines of code and it lives in
the `Item#integrate()` method. The YATA paper has much more detail on the this
the `Item#integrate()` method. The YATA paper has much more detail on this
algorithm.
### Item Storage
@@ -152,8 +152,8 @@ concepts that can be used to create a custom network protocol:
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 delete set.
* `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
* `state vector`: A state vector defines the known state of each user (a set of
tuples `(client, clock)`). This object is also efficiently encoded as a
Uint8Array.
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
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 >
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.

226
README.md
View File

@@ -15,60 +15,49 @@ suited for even large documents.
* Demos: [https://github.com/yjs/yjs-demos](https://github.com/yjs/yjs-demos)
* Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev)
* Chat: [Gitter](https://gitter.im/Yjs/community) | [Discord](https://discord.gg/T3nqMT6qbM)
* Benchmark Yjs vs. Automerge:
[https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks)
* Podcast [**"Yjs Deep Dive into real time collaborative editing solutions":**](https://www.tag1consulting.com/blog/deep-dive-real-time-collaborative-editing-solutions-tagteamtalk-001-0)
* Podcast [**"Google Docs-style editing in Gutenberg with the YJS framework":**](https://publishpress.com/blog/yjs/)
:construction_worker_woman: If you are looking for professional (paid) support to
build collaborative or distributed applications ping us at
<yjs@tag1consulting.com>. Otherwise you can find help on our
[discussion board](https://discuss.yjs.dev).
:construction_worker_woman: If you are looking for professional support, please
consider supporting this project via a "support contract" on
[GitHub Sponsors](https://github.com/sponsors/dmonad). I will attend your issues
quicker and we can discuss questions and problems in regular video conferences.
Otherwise you can find help on our community [discussion board](https://discuss.yjs.dev).
## Sponsors
## Sponsorship
I'm currently looking for sponsors that allow me to be less dependent on
contracting work. These awesome backers already fund further development of
Yjs:
[![davidhq](https://github.com/davidhq.png?size=60)](https://github.com/davidhq)
[![Ifiok Jr.](https://github.com/ifiokjr.png?size=60)](https://github.com/ifiokjr)
[![Burke Libbey](https://github.com/burke.png?size=60)](https://github.com/burke)
[![Beni Cherniavsky-Paskin](https://github.com/cben.png?size=60)](https://github.com/cben)
[![Tom Moor](https://github.com/tommoor.png?size=60)](https://github.com/tommoor)
[![Michael Meyers](https://github.com/michaelemeyers.png?size=60)](https://github.com/michaelemeyers)
[![Cristiano Benjamin](https://github.com/csbenjamin.png?size=60)](https://github.com/csbenjamin)
[![Braden](https://github.com/AdventureBeard.png?size=60)](https://github.com/AdventureBeard)
[![nimbuswebinc](https://nimbusweb.me/new-style-img/note-icon.svg)](https://github.com/nimbuswebinc)
[![JourneyApps](https://github.com/journeyapps.png?size=60)](https://github.com/journeyapps)
[![Adam Brunnmeier](https://github.com/adabru.png?size=60)](https://github.com/adabru)
[![Nathanael Anderson](https://github.com/NathanaelA.png?size=60)](https://github.com/NathanaelA)
[<img src="https://room.sh/img/icons/android-chrome-192x192.png" height="60px" />](https://room.sh/)
Sponsorship also comes with special perks! [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=d42f2d)](https://github.com/sponsors/dmonad)
Please contribute to the project financially - especially if your company relies
on Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=d42f2d)](https://github.com/sponsors/dmonad)
## Who is using Yjs
* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted
collaborative notes app.
* [Sana](https://sanalabs.com/) A learning platform with collaborative text
editing powered by Yjs.
* [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source
knowledge base. 🏅
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2:
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
community. :star2:
* [Input](https://input.com/) A collaborative note taking app. :star2:
community. :star:
* [Room.sh](https://room.sh/) A meeting application with integrated
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 Web.
* [JoeDocs](https://joedocs.com/) An open collaborative wiki.
* [Pluxbox RadioManager](https://pluxbox.com/) A web-based app to
collaboratively organize radio broadcasts.
* [Cattaz](http://cattaz.io/) A wiki that can run custom applications in the
wiki pages.
Nimbus Web. :star:
* [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to
collaboratively organize radio broadcasts. :star:
* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted
collaborative notes app.
* [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation. *[(source)](https://github.com/micrology/prsm)*
* [Alldone](https://alldone.app/) A next-gen project management and
collaboration platform.
* [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate.
* [Slidebeamer](https://slidebeamer.com/) Presentation app.
* [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys.
* [Skiff](https://skiff.org/) Private, decentralized workspace.
* [Hyperquery](https://hyperquery.ai/) A collaborative data workspace for
sharing analyses, documentation, spreadsheets, and dashboards.
## Table of Contents
@@ -100,6 +89,9 @@ 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) |
| [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) |
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
| [immer](https://github.com/immerjs/immer) | | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) |
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |
### Providers
@@ -128,6 +120,12 @@ leveldb database.
Efficiently persists document updates to the browsers indexeddb database.
The document is immediately available and only diffs need to be synced through the
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>
<dt><a href="https://github.com/yjs/y-dat">y-dat</a></dt>
<dd>
@@ -135,6 +133,20 @@ network provider.
<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
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>
<dt><a href="https://github.com/MaxNoetzold/y-mongodb-provider">y-mongodb-provider</a></dt>
<dd>
Adds persistent storage to a server with MongoDB. Can be used with the
y-websocket provider.
</dd>
</dl>
@@ -249,36 +261,36 @@ necessary.
<dl>
<b><code>parent:Y.AbstractType|null</code></b>
<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>
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
position 0.
</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>
<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>
<b><code>delete(index:number, length:number)</code></b>
<dd></dd>
<b><code>get(index:number)</code></b>
<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>
<b><code>length:number</code></b>
<dd></dd>
<b>
<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))
</code>
</b>
<dd></dd>
<b><code>map(function(T, number, YArray):M):Array&lt;M&gt;</code></b>
<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>
<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>
Copies the content of this YArray to a new Array. It transforms all child types
to JSON using their <code>toJSON</code> method.
@@ -324,9 +336,9 @@ or any of its children.
<dd></dd>
<b><code>size: number</code></b>
<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>
<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>
<b><code>delete(key:string)</code></b>
<dd></dd>
@@ -338,12 +350,12 @@ or any of its children.
<dd>Removes all elements from this YMap.</dd>
<b><code>clone():Y.Map</code></b>
<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>
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.
</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>
<dd>
Execute the provided function once for every key-value pair.
@@ -628,6 +640,8 @@ type. Doesn't log types that have not been defined (using
<dd>Define a shared Y.Array type. Is equivalent to <code>y.get(string, Y.Array)</code>.</dd>
<b><code>getMap(string):Y.Map</code></b>
<dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd>
<b><code>getText(string):Y.Text</code></b>
<dd>Define a shared Y.Text type. Is equivalent to <code>y.get(string, Y.Text)</code>.</dd>
<b><code>getXmlFragment(string):Y.XmlFragment</code></b>
<dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd>
<b><code>on(string, function)</code></b>
@@ -713,7 +727,7 @@ Y.applyUpdate(ydoc1, diff2)
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
document to memory. Yjs exposes an API to compute the differences directly on the
@@ -737,6 +751,17 @@ currentState1 = Y.mergeUpdates([currentState1, diff2])
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>
<b><code>Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])</code></b>
<dd>
@@ -769,10 +794,33 @@ Encode the missing differences to another update message. This function works
similarly to <code>Y.encodeStateAsUpdate(ydoc, stateVector)</code> but works
on updates instead.
</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>
### Relative Positions
When working with collaborative documents, we often need to work with positions.
Positions may represent cursor locations, selection ranges, or even assign a
comment to a range of text. Normal index-positions (expressed as integers) are
not convenient to use because the index-range is invalidated as soon as a remote
change manipulates the document. Relative positions give you a powerful API to
express positions.
A relative position is fixated to an element in the shared document and is not
affected by remote changes. I.e. given the document `"a|c"`, the relative
position is attached to `c`. When a remote user modifies the document by
inserting a character before the cursor, the cursor will stay attached to the
character `c`. `insert(1, 'x')("a|c") = "ax|c"`. When the relative position is
set to the end of the document, it will stay attached to the end of the
document.
#### Example: Transform to RelativePosition and back
```js
@@ -807,14 +855,35 @@ pos.index === 2 // => true
```
<dl>
<b><code>Y.createRelativePositionFromTypeIndex(Uint8Array|Y.Type, number)</code></b>
<dd></dd>
<b><code>Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc)</code></b>
<dd></dd>
<b><code>Y.encodeRelativePosition(RelativePosition):Uint8Array</code></b>
<dd></dd>
<b><code>
Y.createRelativePositionFromTypeIndex(type:Uint8Array|Y.Type, index: number
[, assoc=0])
</code></b>
<dd>
Create a relative position fixated to the i-th element in any sequence-like
shared type (if <code>assoc >= 0</code>). By default, the position associates
with the character that comes after the specified index position. If
<code>assoc < 0</code>, then the relative position associates with the character
before the specified index position.
</dd>
<b><code>
Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc):
{ type: Y.AbstractType, index: number, assoc: number } | null
</code></b>
<dd>
Create an absolute position from a relative position. If the relative position
cannot be referenced, or the type is deleted, then the result is null.
</dd>
<b><code>
Y.encodeRelativePosition(RelativePosition):Uint8Array
</code></b>
<dd>
Encode a relative position to an Uint8Array. Binary data is the preferred
encoding format for document updates. If you prefer JSON encoding, you can
simply JSON.stringify / JSON.parse the relative position instead.
</dd>
<b><code>Y.decodeRelativePosition(Uint8Array):RelativePosition</code></b>
<dd></dd>
<dd>Decode a binary-encoded relative position to a RelativePositon object.</dd>
</dl>
### Y.UndoManager
@@ -855,6 +924,16 @@ undo- or the redo-stack.
</dd>
<b>
<code>
on('stack-item-updated', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo'
| 'redo' })
</code>
</b>
<dd>
Register an event that is called when an existing <code>StackItem</code> is updated.
This happens when two changes happen within a "captureInterval".
</dd>
<b>
<code>
on('stack-item-popped', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo'
| 'redo' })
</code>
@@ -863,6 +942,14 @@ on('stack-item-popped', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo'
Register an event that is called when a <code>StackItem</code> is popped from
the undo- or the redo-stack.
</dd>
<b>
<code>
on('stack-cleared', { undoStackCleared: boolean, redoStackCleared: boolean })
</code>
</b>
<dd>
Register an event that is called when the undo- and/or the redo-stack is cleared.
</dd>
</dl>
#### Example: Stop Capturing
@@ -955,7 +1042,7 @@ undoManager.on('stack-item-popped', event => {
*Conflict-free replicated data types* (CRDT) for collaborative editing are an
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
content), while CRDTs use mathematical models that usually do not involve index
transformations, like linked lists. OT is currently the de-facto standard for
@@ -974,21 +1061,22 @@ More information about the specific implementation is available in
[INTERNALS.md](./INTERNALS.md) and in
[this walkthrough of the Yjs codebase](https://youtu.be/0l5XgnQ6rB4).
CRDTs that suitable for shared text editing suffer from the fact that they only grow
in size. There are CRDTs that do not grow in size, but they do not have the
characteristics that are benificial for shared text editing (like intention
preservation). Yjs implements many improvements to the original algorithm that
diminish the trade-off that the document only grows in size. We can't garbage
collect deleted structs (tombstones) while ensuring a unique order of the
structs. But we can 1. merge preceeding structs into a single struct to reduce
the amount of meta information, 2. we can delete content from the struct if it
is deleted, and 3. we can garbage collect tombstones if we don't care about the
order of the structs anymore (e.g. if the parent was deleted).
CRDTs that are suitable for shared text editing suffer from the fact that they
only grow in size. There are CRDTs that do not grow in size, but they do not
have the characteristics that are benificial for shared text editing (like
intention preservation). Yjs implements many improvements to the original
algorithm that diminish the trade-off that the document only grows in size. We
can't garbage collect deleted structs (tombstones) while ensuring a unique
order of the structs. But we can 1. merge preceeding structs into a single
struct to reduce the amount of meta information, 2. we can delete content from
the struct if it is deleted, and 3. we can garbage collect tombstones if we
don't care about the order of the structs anymore (e.g. if the parent was
deleted).
**Examples:**
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'},
{id: {client, clock: 1}, content: 'b'}`) and then merged into a single
struct: `[{id: {client, clock: 0}, content: 'ab'}]`.

6321
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
{
"name": "yjs",
"version": "13.5.12",
"version": "13.5.44",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",
"types": "./dist/src/index.d.ts",
"type": "module",
"sideEffects": false,
"funding": {
"type": "GitHub Sponsors ❤",
@@ -21,16 +22,18 @@
"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'",
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs",
"postinstall": "node ./sponsor-y.js"
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs"
},
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"module": "./dist/yjs.mjs",
"import": "./dist/yjs.mjs",
"require": "./dist/yjs.cjs"
},
"./src/index.js": "./src/index.js",
"./tests/testHelper.js": "./tests/testHelper.js",
"./testHelper": "./dist/testHelper.mjs",
"./package.json": "./package.json"
},
"files": [
@@ -38,6 +41,7 @@
"dist/src",
"src",
"tests/testHelper.js",
"dist/testHelper.mjs",
"sponsor-y.js"
],
"dictionaries": {
@@ -71,19 +75,19 @@
},
"homepage": "https://docs.yjs.dev",
"dependencies": {
"lib0": "^0.2.41"
"lib0": "^0.2.49"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.2.1",
"concurrently": "^3.6.1",
"http-server": "^0.12.3",
"jsdoc": "^3.6.6",
"jsdoc": "^3.6.7",
"markdownlint-cli": "^0.23.2",
"rollup": "^2.47.0",
"standard": "^14.3.4",
"rollup": "^2.60.0",
"standard": "^16.0.4",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^4.2.4",
"y-protocols": "^1.0.4"
"typescript": "^4.4.4",
"y-protocols": "^1.0.5"
}
}

View File

@@ -60,6 +60,23 @@ export default [{
sourcemap: true
},
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',
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 {
Doc,
@@ -26,12 +27,13 @@ export {
ContentString,
ContentType,
AbstractType,
RelativePosition,
getTypeChildren,
createRelativePositionFromTypeIndex,
createRelativePositionFromJSON,
createAbsolutePositionFromRelativePosition,
compareRelativePositions,
AbsolutePosition,
RelativePosition,
ID,
createID,
compareIDs,
@@ -40,9 +42,11 @@ export {
createSnapshot,
createDeleteSet,
createDeleteSetFromStructStore,
cleanupYTextFormatting,
snapshot,
emptySnapshot,
findRootTypeKey,
findIndexSS,
getItem,
typeListToArraySnapshot,
typeMapGetSnapshot,
@@ -63,6 +67,8 @@ export {
decodeStateVector,
logUpdate,
logUpdateV2,
decodeUpdate,
decodeUpdateV2,
relativePositionToJSON,
isDeleted,
isParentOf,
@@ -81,5 +87,36 @@ export {
encodeRelativePosition,
decodeRelativePosition,
diffUpdate,
diffUpdateV2
diffUpdateV2,
convertUpdateFormatV1ToV2,
convertUpdateFormatV2ToV1,
UpdateEncoderV1
} from './internals.js'
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
? globalThis
: typeof window !== 'undefined'
? window
// @ts-ignore
: typeof global !== 'undefined' ? global : {})
const importIdentifier = '__ $YJS$ __'
if (glo[importIdentifier] === true) {
/**
* Dear reader of this message. Please take this seriously.
*
* If you see this message, 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.
*
* https://github.com/yjs/yjs/issues/438
*/
console.error('Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438')
}
glo[importIdentifier] = true

View File

@@ -5,6 +5,12 @@ import {
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
*/
@@ -61,7 +67,7 @@ export class ContentDoc {
* @return {ContentDoc}
*/
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
* @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

@@ -100,4 +100,4 @@ export class ContentFormat {
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentFormat}
*/
export const readContentFormat = decoder => new ContentFormat(decoder.readString(), decoder.readJSON())
export const readContentFormat = decoder => new ContentFormat(decoder.readKey(), decoder.readJSON())

View File

@@ -39,7 +39,7 @@ export const YXmlTextRefID = 6
*/
export class ContentType {
/**
* @param {AbstractType<YEvent>} type
* @param {AbstractType<any>} type
*/
constructor (type) {
/**
@@ -109,7 +109,7 @@ export class ContentType {
if (!item.deleted) {
item.delete(transaction)
} 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,
// 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

View File

@@ -22,7 +22,8 @@ import {
readContentFormat,
readContentType,
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'
import * as error from 'lib0/error'
@@ -125,12 +126,14 @@ export const splitItem = (transaction, leftItem, diff) => {
* @param {Transaction} transaction The Yjs instance.
* @param {Item} item
* @param {Set<Item>} redoitems
* @param {DeleteSet} itemsToDelete
* @param {boolean} ignoreRemoteMapChanges
*
* @return {Item|null}
*
* @private
*/
export const redoItem = (transaction, item, redoitems) => {
export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges) => {
const doc = transaction.doc
const store = doc.store
const ownClientID = doc.clientID
@@ -142,42 +145,27 @@ export const redoItem = (transaction, item, redoitems) => {
/**
* @type {Item|null}
*/
let left
let left = null
/**
* @type {Item|null}
*/
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, ignoreRemoteMapChanges) === 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) {
// Is an array item. Insert at the old position
left = item.left
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
while (left !== null) {
/**
@@ -209,6 +197,28 @@ export const redoItem = (transaction, item, redoitems) => {
}
right = right.right
}
} else {
right = null
if (item.right && !ignoreRemoteMapChanges) {
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)
}
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 nextId = createID(ownClientID, nextClock)
@@ -216,7 +226,7 @@ export const redoItem = (transaction, item, redoitems) => {
nextId,
left, left && left.lastId,
right, right && right.id,
parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type,
parentType,
item.parentSub,
item.content.copy()
)
@@ -275,7 +285,7 @@ export class Item extends AbstractStruct {
*/
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.
* @type {ID | null}
*/

View File

@@ -278,7 +278,7 @@ export class AbstractType {
this._eH = createEventHandler()
/**
* Deep event handlers
* @type {EventHandler<Array<YEvent>,Transaction>}
* @type {EventHandler<Array<YEvent<any>>,Transaction>}
*/
this._dEH = createEventHandler()
/**
@@ -364,7 +364,7 @@ export class AbstractType {
/**
* Observe all events that are created by this type and its children.
*
* @param {function(Array<YEvent>,Transaction):void} f Observer function
* @param {function(Array<YEvent<any>>,Transaction):void} f Observer function
*/
observeDeep (f) {
addEventHandlerListener(this._dEH, f)
@@ -382,7 +382,7 @@ export class AbstractType {
/**
* Unregister an observer function.
*
* @param {function(Array<YEvent>,Transaction):void} f Observer function
* @param {function(Array<YEvent<any>>,Transaction):void} f Observer function
*/
unobserveDeep (f) {
removeEventHandlerListener(this._dEH, f)
@@ -623,7 +623,7 @@ export const typeListGet = (type, index) => {
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @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
* @function
@@ -635,7 +635,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
const store = doc.store
const right = referenceItem === null ? parent._start : referenceItem.right
/**
* @type {Array<Object|Array<any>|number>}
* @type {Array<Object|Array<any>|number|null>}
*/
let jsonContent = []
const packJsonContent = () => {
@@ -646,34 +646,38 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
}
}
content.forEach(c => {
switch (c.constructor) {
case Number:
case Object:
case Boolean:
case Array:
case String:
jsonContent.push(c)
break
default:
packJsonContent()
switch (c.constructor) {
case Uint8Array:
case ArrayBuffer:
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.integrate(transaction, 0)
break
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))
if (c === null) {
jsonContent.push(c)
} else {
switch (c.constructor) {
case Number:
case Object:
case Boolean:
case Array:
case String:
jsonContent.push(c)
break
default:
packJsonContent()
switch (c.constructor) {
case Uint8Array:
case ArrayBuffer:
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.integrate(transaction, 0)
} else {
throw new Error('Unexpected content type in insert operation')
}
}
break
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()
@@ -685,7 +689,7 @@ const lengthExceeded = error.create('Length exceeded!')
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @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
* @function
@@ -731,6 +735,29 @@ export const typeListInsertGenerics = (transaction, parent, index, 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 {AbstractType<any>} parent
@@ -797,7 +824,7 @@ export const typeMapDelete = (transaction, parent, key) => {
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @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
* @function
@@ -838,7 +865,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
/**
* @param {AbstractType<any>} parent
* @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
* @function
@@ -850,7 +877,7 @@ export const typeMapGet = (parent, key) => {
/**
* @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
* @function
@@ -885,7 +912,7 @@ export const typeMapHas = (parent, key) => {
* @param {AbstractType<any>} parent
* @param {string} key
* @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
* @function

View File

@@ -10,6 +10,7 @@ import {
typeListForEach,
typeListCreateIterator,
typeListInsertGenerics,
typeListPushGenerics,
typeListDelete,
typeListMap,
YArrayRefID,
@@ -22,6 +23,7 @@ import { typeListSlice } from './AbstractType.js'
/**
* Event that describes the changes on a YArray
* @template T
* @extends YEvent<YArray<T>>
*/
export class YArrayEvent extends YEvent {
/**
@@ -142,9 +144,17 @@ export class YArray extends AbstractType {
* Appends content to this YArray.
*
* @param {Array<T>} content Array of content to append.
*
* @todo Use the following implementation in all types.
*/
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 +225,7 @@ export class YArray extends AbstractType {
* Returns an Array with the result of calling a provided function on every
* 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
* @return {Array<M>} A new array with each element being the result of the
* callback function

View File

@@ -21,6 +21,7 @@ import * as iterator from 'lib0/iterator'
/**
* @template T
* @extends YEvent<YMap<T>>
* Event that describes the changes on a YMap.
*/
export class YMapEvent extends YEvent {
@@ -36,11 +37,11 @@ export class YMapEvent extends YEvent {
}
/**
* @template T number|string|Object|Array|Uint8Array
* @template MapType
* A shared Map implementation.
*
* @extends AbstractType<YMapEvent<T>>
* @implements {Iterable<T>}
* @extends AbstractType<YMapEvent<MapType>>
* @implements {Iterable<MapType>}
*/
export class YMap extends AbstractType {
/**
@@ -85,7 +86,7 @@ export class YMap extends AbstractType {
}
/**
* @return {YMap<T>}
* @return {YMap<MapType>}
*/
clone () {
const map = new YMap()
@@ -108,11 +109,11 @@ export class YMap extends AbstractType {
/**
* Transforms this Shared Type to a JSON object.
*
* @return {Object<string,T>}
* @return {Object<string,any>}
*/
toJSON () {
/**
* @type {Object<string,T>}
* @type {Object<string,MapType>}
*/
const map = {}
this._map.forEach((item, key) => {
@@ -163,23 +164,20 @@ export class YMap extends AbstractType {
/**
* 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) {
/**
* @type {Object<string,T>}
*/
const map = {}
this._map.forEach((item, key) => {
if (!item.deleted) {
f(item.content.getContent()[item.length - 1], key, this)
}
})
return map
}
/**
* @return {IterableIterator<T>}
* Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<any>}
*/
[Symbol.iterator] () {
return this.entries()
@@ -204,7 +202,7 @@ export class YMap extends AbstractType {
* 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 {T} value The value of the element to add
* @param {MapType} value The value of the element to add
*/
set (key, value) {
if (this.doc !== null) {
@@ -221,7 +219,7 @@ export class YMap extends AbstractType {
* Returns a specified element from this YMap.
*
* @param {string} key
* @return {T|undefined}
* @return {MapType|undefined}
*/
get (key) {
return /** @type {any} */ (typeMapGet(this, key))

View File

@@ -26,6 +26,7 @@ import {
typeMapGet,
typeMapGetAll,
updateMarkerChanges,
ContentType,
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
} from '../internals.js'
@@ -62,17 +63,16 @@ export class ItemTextListPosition {
error.unexpectedCase()
}
switch (this.right.content.constructor) {
case ContentEmbed:
case ContentString:
if (!this.right.deleted) {
this.index += this.right.length
}
break
case ContentFormat:
if (!this.right.deleted) {
updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content))
}
break
default:
if (!this.right.deleted) {
this.index += this.right.length
}
break
}
this.left = this.right
this.right = this.right.right
@@ -91,8 +91,12 @@ export class ItemTextListPosition {
const findNextPosition = (transaction, pos, count) => {
while (pos.right !== null && count > 0) {
switch (pos.right.content.constructor) {
case ContentEmbed:
case ContentString:
case ContentFormat:
if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
}
break
default:
if (!pos.right.deleted) {
if (count < pos.right.length) {
// split right
@@ -102,11 +106,6 @@ const findNextPosition = (transaction, pos, count) => {
count -= pos.right.length
}
break
case ContentFormat:
if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
}
break
}
pos.left = pos.right
pos.right = pos.right.right
@@ -245,7 +244,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {ItemTextListPosition} currPos
* @param {string|object} text
* @param {string|object|AbstractType<any>} text
* @param {Object<string,any>} attributes
*
* @private
@@ -262,7 +261,7 @@ const insertText = (transaction, parent, currPos, text, attributes) => {
minimizeAttributeChanges(currPos, attributes)
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
// 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
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
@@ -292,7 +291,17 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
// iterate until first non-format or null is found
// delete all formats with attributes[format.key] != null
while (length > 0 && currPos.right !== null) {
// also check the attributes after the first non-format as we do not want to insert redundant negated attributes there
// eslint-disable-next-line no-labels
iterationLoop: while (
currPos.right !== null &&
(length > 0 ||
(
negatedAttributes.size > 0 &&
(currPos.right.deleted || currPos.right.content.constructor === ContentFormat)
)
)
) {
if (!currPos.right.deleted) {
switch (currPos.right.content.constructor) {
case ContentFormat: {
@@ -302,14 +311,20 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
if (equalAttrs(attr, value)) {
negatedAttributes.delete(key)
} else {
if (length === 0) {
// no need to further extend negatedAttributes
// eslint-disable-next-line no-labels
break iterationLoop
}
negatedAttributes.set(key, value)
}
currPos.right.delete(transaction)
} else {
currPos.currentAttributes.set(key, value)
}
break
}
case ContentEmbed:
case ContentString:
default:
if (length < currPos.right.length) {
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length))
}
@@ -340,22 +355,28 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
*
* @param {Transaction} transaction
* @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>} endAttributes This attribute is modified!
* @param {Map<string,any>} currAttributes
* @return {number} The amount of formatting Items deleted.
*
* @function
*/
const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttributes) => {
while (end && end.content.constructor !== ContentString && end.content.constructor !== ContentEmbed) {
const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
let end = curr
const endAttributes = map.copy(currAttributes)
while (end && (!end.countable || end.deleted)) {
if (!end.deleted && end.content.constructor === ContentFormat) {
updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content))
}
end = end.right
}
let cleanups = 0
let reachedEndOfCurr = false
while (start !== end) {
if (curr === start) {
reachedEndOfCurr = true
}
if (!start.deleted) {
const content = start.content
switch (content.constructor) {
@@ -365,6 +386,9 @@ const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttri
// Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction)
cleanups++
if (!reachedEndOfCurr && (currAttributes.get(key) || null) === value && (startAttributes.get(key) || null) !== value) {
currAttributes.delete(key)
}
}
break
}
@@ -381,12 +405,12 @@ const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttri
*/
const cleanupContextlessFormattingGap = (transaction, item) => {
// 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
}
const attrs = new Set()
// 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) {
const key = /** @type {ContentFormat} */ (item.content).key
if (attrs.has(key)) {
@@ -424,8 +448,7 @@ export const cleanupYTextFormatting = type => {
case ContentFormat:
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content))
break
case ContentEmbed:
case ContentString:
default:
res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes)
startAttributes = map.copy(currentAttributes)
start = end
@@ -454,6 +477,7 @@ const deleteText = (transaction, currPos, length) => {
while (length > 0 && currPos.right !== null) {
if (currPos.right.deleted === false) {
switch (currPos.right.content.constructor) {
case ContentType:
case ContentEmbed:
case ContentString:
if (length < currPos.right.length) {
@@ -467,7 +491,7 @@ const deleteText = (transaction, currPos, length) => {
currPos.forward()
}
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)
if (parent._searchMarker) {
@@ -504,6 +528,7 @@ const deleteText = (transaction, currPos, length) => {
*/
/**
* @extends YEvent<YText>
* Event that describes the changes on a YText type.
*/
export class YTextEvent extends YEvent {
@@ -540,7 +565,7 @@ export class YTextEvent extends YEvent {
get changes () {
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 = {
keys: this.keys,
@@ -557,7 +582,7 @@ export class YTextEvent extends YEvent {
* Compute the changes in the delta format.
* 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
*/
@@ -565,7 +590,7 @@ export class YTextEvent extends YEvent {
if (this._delta === null) {
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 = []
transact(y, transaction => {
@@ -626,12 +651,13 @@ export class YTextEvent extends YEvent {
}
while (item !== null) {
switch (item.content.constructor) {
case ContentType:
case ContentEmbed:
if (this.adds(item)) {
if (!this.deletes(item)) {
addOp()
action = 'insert'
insert = /** @type {ContentEmbed} */ (item.content).embed
insert = item.content.getContent()[0]
addOp()
}
} else if (this.deletes(item)) {
@@ -685,7 +711,7 @@ export class YTextEvent extends YEvent {
} else {
attributes[key] = value
}
} else {
} else if (value !== null) {
item.delete(transaction)
}
}
@@ -707,11 +733,11 @@ export class YTextEvent extends YEvent {
addOp()
}
if (value === null) {
attributes[key] = value
} else {
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)
}
}
@@ -992,12 +1018,12 @@ export class YText extends AbstractType {
case ContentString: {
const cur = currentAttributes.get('ychange')
if (snapshot !== undefined && !isVisible(n, snapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'removed') {
packStr()
currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
}
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') {
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'added') {
packStr()
currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
}
@@ -1008,13 +1034,14 @@ export class YText extends AbstractType {
str += /** @type {ContentString} */ (n.content).str
break
}
case ContentType:
case ContentEmbed: {
packStr()
/**
* @type {Object<string,any>}
*/
const op = {
insert: /** @type {ContentEmbed} */ (n.content).embed
insert: n.content.getContent()[0]
}
if (currentAttributes.size > 0) {
const attrs = /** @type {Object<string,any>} */ ({})
@@ -1037,7 +1064,7 @@ export class YText extends AbstractType {
n = n.right
}
packStr()
}, splitSnapshotAffectedStructs)
}, 'cleanup')
return ops
}
@@ -1075,16 +1102,13 @@ export class YText extends AbstractType {
* Inserts an embed at a index.
*
* @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
* embed
*
* @public
*/
insertEmbed (index, embed, attributes = {}) {
if (embed.constructor !== Object) {
throw new Error('Embed must be an Object')
}
const y = this.doc
if (y !== null) {
transact(y, transaction => {

View File

@@ -176,12 +176,11 @@ export class YXmlElement extends YXmlFragment {
/**
* Returns all attribute name/value pairs in a JSON Object.
*
* @param {Snapshot} [snapshot]
* @return {Object<string, any>} A JSON Object that describes the attributes.
*
* @public
*/
getAttributes (snapshot) {
getAttributes () {
return typeMapGetAll(this)
}

View File

@@ -5,6 +5,7 @@ import {
} from '../internals.js'
/**
* @extends YEvent<YXmlElement|YXmlText|YXmlFragment>
* An Event that describes changes on a YXml Element or Yxml Fragment
*/
export class YXmlEvent extends YEvent {

View File

@@ -404,6 +404,15 @@ export class YXmlFragment extends AbstractType {
return typeListSlice(this, start, end)
}
/**
* Executes a provided function on once on overy child element.
*
* @param {function(YXmlElement|YXmlText,number, typeof this):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
typeListForEach(this, f)
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.

View File

@@ -17,6 +17,7 @@ import { Observable } from 'lib0/observable'
import * as random from 'lib0/random'
import * as map from 'lib0/map'
import * as array from 'lib0/array'
import * as promise from 'lib0/promise'
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 {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 | 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 {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,14 +40,15 @@ export class Doc extends Observable {
/**
* @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()
this.gc = gc
this.gcFilter = gcFilter
this.clientID = generateNewClientId()
this.guid = guid
this.collectionid = collectionid
/**
* @type {Map<string, AbstractType<YEvent>>}
* @type {Map<string, AbstractType<YEvent<any>>>}
*/
this.share = new Map()
this.store = new StructStore()
@@ -65,9 +69,16 @@ export class Doc extends Observable {
* @type {Item?}
*/
this._item = null
this.shouldLoad = autoLoad
this.shouldLoad = shouldLoad
this.autoLoad = autoLoad
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]
* @return {YMap<any>}
* @return {YMap<T>}
*
* @public
*/
@@ -245,16 +257,12 @@ export class Doc extends Observable {
if (item !== null) {
this._item = null
const content = /** @type {ContentDoc} */ (item.content)
if (item.deleted) {
// @ts-ignore
content.doc = null
} else {
content.doc = new Doc({ guid: this.guid, ...content.opts })
content.doc._item = item
}
content.doc = new Doc({ guid: this.guid, ...content.opts, shouldLoad: false })
content.doc._item = item
transact(/** @type {any} */ (item).parent.doc, transaction => {
const doc = content.doc
if (!item.deleted) {
transaction.subdocsAdded.add(content.doc)
transaction.subdocsAdded.add(doc)
}
transaction.subdocsRemoved.add(this)
}, null, true)

View File

@@ -129,9 +129,9 @@ export const snapshot = doc => createSnapshot(createDeleteSetFromStructStore(doc
* @protected
* @function
*/
export const isVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : (
snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
)
export const isVisible = (item, snapshot) => snapshot === undefined
? !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

View File

@@ -75,13 +75,13 @@ export class Transaction {
* All types that were directly modified (property added or child
* inserted/deleted). New types are not included in this Set.
* Maps from type to parentSubs (`item.parentSub = null` for YArray)
* @type {Map<AbstractType<YEvent>,Set<String|null>>}
* @type {Map<AbstractType<YEvent<any>>,Set<String|null>>}
*/
this.changed = new Map()
/**
* Stores the events for the types that observe also child elements.
* It is mainly used by `observeDeep`.
* @type {Map<AbstractType<YEvent>,Array<YEvent>>}
* @type {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>}
*/
this.changedParentTypes = new Map()
/**
@@ -148,7 +148,7 @@ export const nextID = transaction => {
* did not change, it was just added and we should not fire events for `type`.
*
* @param {Transaction} transaction
* @param {AbstractType<YEvent>} type
* @param {AbstractType<YEvent<any>>} type
* @param {string|null} parentSub
*/
export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
@@ -251,7 +251,6 @@ const cleanupTransactions = (transactionCleanups, i) => {
try {
sortAndMergeDeleteSet(ds)
transaction.afterState = getStateVector(transaction.doc.store)
doc._transaction = null
doc.emit('beforeObserverCalls', [transaction, doc])
/**
* An array of event callbacks.
@@ -331,8 +330,8 @@ const cleanupTransactions = (transactionCleanups, i) => {
}
}
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.')
doc.clientID = generateNewClientId()
}
// @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc])
@@ -350,11 +349,19 @@ const cleanupTransactions = (transactionCleanups, i) => {
doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction])
}
}
transaction.subdocsAdded.forEach(subdoc => doc.subdocs.add(subdoc))
transaction.subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc))
doc.emit('subdocs', [{ loaded: transaction.subdocsLoaded, added: transaction.subdocsAdded, removed: transaction.subdocsRemoved }])
transaction.subdocsRemoved.forEach(subdoc => subdoc.destroy())
const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction
if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) {
subdocsAdded.forEach(subdoc => {
subdoc.clientID = doc.clientID
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) {
doc._transactionCleanups = []
@@ -390,16 +397,20 @@ export const transact = (doc, f, origin = null, local = true) => {
try {
f(doc._transaction)
} finally {
if (initialCall && transactionCleanups[0] === doc._transaction) {
// The first transaction ended, now process observer calls.
// Observer call may create new transactions for which we need to call the observers and do cleanup.
// We don't want to nest these calls, so we execute these calls one after
// another.
// Also we need to ensure that all cleanups are called, even if the
// observes throw errors.
// This file is full of hacky try {} finally {} blocks to ensure that an
// event can throw errors and also that the cleanup is called.
cleanupTransactions(transactionCleanups, 0)
if (initialCall) {
const finishCleanup = doc._transaction === transactionCleanups[0]
doc._transaction = null
if (finishCleanup) {
// The first transaction ended, now process observer calls.
// Observer call may create new transactions for which we need to call the observers and do cleanup.
// We don't want to nest these calls, so we execute these calls one after
// another.
// Also we need to ensure that all cleanups are called, even if the
// observes throw errors.
// This file is full of hacky try {} finally {} blocks to ensure that an
// event can throw errors and also that the cleanup is called.
cleanupTransactions(transactionCleanups, 0)
}
}
}
}

View File

@@ -14,6 +14,7 @@ import {
} from '../internals.js'
import * as time from 'lib0/time'
import * as array from 'lib0/array'
import { Observable } from 'lib0/observable'
class StackItem {
@@ -30,6 +31,18 @@ class StackItem {
this.meta = new Map()
}
}
/**
* @param {Transaction} tr
* @param {UndoManager} um
* @param {StackItem} stackItem
*/
const clearUndoManagerStackItem = (tr, um, stackItem) => {
iterateDeletedStructs(tr, stackItem.deletions, item => {
if (item instanceof Item && um.scope.some(type => isParentOf(type, item))) {
keepItem(item, false)
}
})
}
/**
* @param {UndoManager} undoManager
@@ -88,7 +101,7 @@ const popStackItem = (undoManager, stack, eventType) => {
}
})
itemsToRedo.forEach(struct => {
performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges) !== null || performedChange
})
// We want to delete in reverse order so that children are deleted before
// parents, so we have more information available when items are filtered.
@@ -119,11 +132,14 @@ const popStackItem = (undoManager, stack, eventType) => {
/**
* @typedef {Object} UndoManagerOptions
* @property {number} [UndoManagerOptions.captureTimeout=500]
* @property {function(Transaction):boolean} [UndoManagerOptions.captureTransaction] Do not capture changes of a Transaction if result false.
* @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes
* it is necessary to filter whan an Undo/Redo operation can delete. If this
* it is necessary to filter what an Undo/Redo operation can delete. If this
* filter returns false, the type/item won't be deleted even it is in the
* undo/redo scope.
* @property {Set<any>} [UndoManagerOptions.trackedOrigins=new Set([null])]
* @property {boolean} [ignoreRemoteMapChanges] Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..).
* @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty.
*/
/**
@@ -133,19 +149,31 @@ const popStackItem = (undoManager, stack, eventType) => {
* Fires 'stack-item-popped' event when a stack item was popped from either the
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
*
* @extends {Observable<'stack-item-added'|'stack-item-popped'>}
* @extends {Observable<'stack-item-added'|'stack-item-popped'|'stack-cleared'|'stack-item-updated'>}
*/
export class UndoManager extends Observable {
/**
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
* @param {UndoManagerOptions} options
*/
constructor (typeScope, { captureTimeout = 500, deleteFilter = () => true, trackedOrigins = new Set([null]) } = {}) {
constructor (typeScope, {
captureTimeout = 500,
captureTransaction = tr => true,
deleteFilter = () => true,
trackedOrigins = new Set([null]),
ignoreRemoteMapChanges = false,
doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope.doc)
} = {}) {
super()
this.scope = typeScope instanceof Array ? typeScope : [typeScope]
/**
* @type {Array<AbstractType<any>>}
*/
this.scope = []
this.addToScope(typeScope)
this.deleteFilter = deleteFilter
trackedOrigins.add(this)
this.trackedOrigins = trackedOrigins
this.captureTransaction = captureTransaction
/**
* @type {Array<StackItem>}
*/
@@ -161,11 +189,20 @@ export class UndoManager extends Observable {
*/
this.undoing = false
this.redoing = false
this.doc = /** @type {Doc} */ (this.scope[0].doc)
this.doc = doc
this.lastChange = 0
this.doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
this.captureTimeout = captureTimeout
/**
* @param {Transaction} transaction
*/
this.afterTransactionHandler = transaction => {
// Only track certain transactions
if (!this.scope.some(type => transaction.changedParentTypes.has(type)) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))) {
if (
!this.captureTransaction(transaction) ||
!this.scope.some(type => transaction.changedParentTypes.has(type)) ||
(!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))
) {
return
}
const undoing = this.undoing
@@ -175,7 +212,7 @@ export class UndoManager extends Observable {
this.stopCapturing() // next undo should not be appended to last stack item
} else if (!redoing) {
// neither undoing nor redoing: delete redoStack
this.redoStack = []
this.clear(false, true)
}
const insertions = new DeleteSet()
transaction.afterState.forEach((endClock, client) => {
@@ -186,7 +223,8 @@ export class UndoManager extends Observable {
}
})
const now = time.getUnixTime()
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
let didAdd = false
if (this.lastChange > 0 && now - this.lastChange < this.captureTimeout && stack.length > 0 && !undoing && !redoing) {
// append change to last stack op
const lastOp = stack[stack.length - 1]
lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet])
@@ -194,6 +232,7 @@ export class UndoManager extends Observable {
} else {
// create a new stack op
stack.push(new StackItem(transaction.deleteSet, insertions))
didAdd = true
}
if (!undoing && !redoing) {
this.lastChange = now
@@ -204,27 +243,59 @@ export class UndoManager extends Observable {
keepItem(item, true)
}
})
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this])
const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]
if (didAdd) {
this.emit('stack-item-added', changeEvent)
} else {
this.emit('stack-item-updated', changeEvent)
}
}
this.doc.on('afterTransaction', this.afterTransactionHandler)
this.doc.on('destroy', () => {
this.destroy()
})
}
clear () {
this.doc.transact(transaction => {
/**
* @param {StackItem} stackItem
*/
const clearItem = stackItem => {
iterateDeletedStructs(transaction, stackItem.deletions, item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
keepItem(item, false)
}
})
/**
* @param {Array<AbstractType<any>> | AbstractType<any>} ytypes
*/
addToScope (ytypes) {
ytypes = array.isArray(ytypes) ? ytypes : [ytypes]
ytypes.forEach(ytype => {
if (this.scope.every(yt => yt !== ytype)) {
this.scope.push(ytype)
}
this.undoStack.forEach(clearItem)
this.redoStack.forEach(clearItem)
})
this.undoStack = []
this.redoStack = []
}
/**
* @param {any} origin
*/
addTrackedOrigin (origin) {
this.trackedOrigins.add(origin)
}
/**
* @param {any} origin
*/
removeTrackedOrigin (origin) {
this.trackedOrigins.delete(origin)
}
clear (clearUndoStack = true, clearRedoStack = true) {
if ((clearUndoStack && this.canUndo()) || (clearRedoStack && this.canRedo())) {
this.doc.transact(tr => {
if (clearUndoStack) {
this.undoStack.forEach(item => clearUndoManagerStackItem(tr, this, item))
this.undoStack = []
}
if (clearRedoStack) {
this.redoStack.forEach(item => clearUndoManagerStackItem(tr, this, item))
this.redoStack = []
}
this.emit('stack-cleared', [{ undoStackCleared: clearUndoStack, redoStackCleared: clearRedoStack }])
})
}
}
/**
@@ -282,4 +353,28 @@ export class UndoManager extends Observable {
}
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
}
destroy () {
this.trackedOrigins.delete(this)
this.doc.off('afterTransaction', this.afterTransactionHandler)
super.destroy()
}
}

View File

@@ -298,10 +298,24 @@ export class UpdateEncoderV2 extends DSEncoderV2 {
writeKey (key) {
const clock = this.keyMap.get(key)
if (clock === undefined) {
/**
* @todo uncomment to introduce this feature finally
*
* Background. The ContentFormat object was always encoded using writeKey, but the decoder used to use readString.
* Furthermore, I forgot to set the keyclock. So everything was working fine.
*
* However, this feature here is basically useless as it is not being used (it actually only consumes extra memory).
*
* I don't know yet how to reintroduce this feature..
*
* Older clients won't be able to read updates when we reintroduce this feature. So this should probably be done using a flag.
*
*/
// this.keyMap.set(key, this.keyClock)
this.keyClockEncoder.write(this.keyClock++)
this.stringEncoder.write(key)
} else {
this.keyClockEncoder.write(this.keyClock++)
this.keyClockEncoder.write(clock)
}
}
}

View File

@@ -8,17 +8,18 @@ import * as set from 'lib0/set'
import * as array from 'lib0/array'
/**
* @template {AbstractType<any>} T
* YEvent describes the changes on a YType.
*/
export class YEvent {
/**
* @param {AbstractType<any>} target The changed type.
* @param {T} target The changed type.
* @param {Transaction} transaction
*/
constructor (target, transaction) {
/**
* The type on which this event was created on.
* @type {AbstractType<any>}
* @type {T}
*/
this.target = target
/**
@@ -40,7 +41,7 @@ export class YEvent {
*/
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
}
@@ -129,7 +130,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 () {
return this.changes.delta

View File

@@ -601,7 +601,7 @@ export const decodeStateVector = decodedState => readStateVector(new DSDecoderV1
*/
export const writeStateVector = (encoder, sv) => {
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, clock)
})

View File

@@ -112,6 +112,30 @@ export const logUpdateV2 = (update, YDecoder = UpdateDecoderV2) => {
logging.print('DeleteSet: ', ds)
}
/**
* @param {Uint8Array} update
*
*/
export const decodeUpdate = (update) => decodeUpdateV2(update, UpdateDecoderV1)
/**
* @param {Uint8Array} update
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} [YDecoder]
*
*/
export const decodeUpdateV2 = (update, YDecoder = UpdateDecoderV2) => {
const structs = []
const updateDecoder = new YDecoder(decoding.createDecoder(update))
const lazyDecoder = new LazyStructReader(updateDecoder, false)
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
structs.push(curr)
}
return {
structs,
ds: readDeleteSet(updateDecoder)
}
}
export class LazyStructWriter {
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
@@ -287,6 +311,9 @@ const sliceStruct = (left, diff) => {
* @return {Uint8Array}
*/
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)))
let lazyStructDecoders = updateDecoders.map(decoder => new LazyStructReader(decoder, true))
@@ -312,9 +339,10 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U
if (dec1.curr.id.client === dec2.curr.id.client) {
const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock
if (clockDiff === 0) {
return dec1.curr.constructor === dec2.curr.constructor ? 0 : (
dec1.curr.constructor === Skip ? 1 : -1
)
// @todo remove references to skip since the structDecoders must filter Skips.
return dec1.curr.constructor === dec2.curr.constructor
? 0
: dec1.curr.constructor === Skip ? 1 : -1 // we are filtering skips anyway.
} else {
return clockDiff
}

View File

@@ -2,6 +2,31 @@
import * as Y from '../src/index.js'
import * as t from 'lib0/testing'
/**
* @param {t.TestCase} _tc
*/
export const testOriginInTransaction = _tc => {
const doc = new Y.Doc()
const ytext = doc.getText()
/**
* @type {Array<string>}
*/
const origins = []
doc.on('afterTransaction', (tr) => {
origins.push(tr.origin)
if (origins.length <= 1) {
ytext.toDelta()
doc.transact(() => {
ytext.insert(0, 'a')
}, 'nested')
}
})
doc.transact(() => {
ytext.insert(0, '0')
}, 'first')
t.compareArrays(origins, ['first', 'cleanup', 'nested'])
}
/**
* Client id should be changed when an instance receives updates from another client using the same client id.
*
@@ -88,7 +113,7 @@ export const testSubdoc = tc => {
subdocs.get('a').load()
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'], [], []])
subdocs.get('b').load()
t.compare(event, [[], [], ['a']])
@@ -124,3 +149,123 @@ export const testSubdoc = tc => {
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

@@ -72,9 +72,9 @@ export const testPermanentUserData = async tc => {
export const testDiffStateVectorOfUpdateIsEmpty = tc => {
const ydoc = new Y.Doc()
/**
* @type {null | Uint8Array}
* @type {any}
*/
let sv = /* any */ (null)
let sv = null
ydoc.getText().insert(0, 'a')
ydoc.on('update', update => {
sv = Y.encodeStateVectorFromUpdate(update)

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

View File

@@ -3,10 +3,11 @@ import * as t from 'lib0/testing'
import * as prng from 'lib0/prng'
import * as encoding from 'lib0/encoding'
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'
import * as Y from '../src/internals.js'
export * from '../src/internals.js'
import * as map from 'lib0/map'
import * as Y from '../src/index.js'
export * from '../src/index.js'
if (typeof window !== 'undefined') {
// @ts-ignore
@@ -89,8 +90,8 @@ export class TestYInstance extends Y.Doc {
const encoder = encoding.createEncoder()
syncProtocol.writeUpdate(encoder, update)
broadcastMessage(this, encoding.toUint8Array(encoder))
this.updates.push(update)
}
this.updates.push(update)
})
this.connect()
}
@@ -133,12 +134,7 @@ export class TestYInstance extends Y.Doc {
* @param {TestYInstance} remoteClient
*/
_receive (message, remoteClient) {
let messages = this.receiving.get(remoteClient)
if (messages === undefined) {
messages = []
this.receiving.set(remoteClient, messages)
}
messages.push(message)
map.setIfUndefined(this.receiving, remoteClient, () => []).push(message)
}
}
@@ -202,17 +198,6 @@ export class TestConnector {
// send reply message
sender._receive(encoding.toUint8Array(encoder), receiver)
}
{
// If update message, add the received message to the list of received messages
const decoder = decoding.createDecoder(m)
const messageType = decoding.readVarUint(decoder)
switch (messageType) {
case syncProtocol.messageYjsUpdate:
case syncProtocol.messageYjsSyncStep2:
receiver.updates.push(decoding.readVarUint8Array(decoder))
break
}
}
return true
}
return false
@@ -279,7 +264,7 @@ export class TestConnector {
* @param {t.TestCase} tc
* @param {{users?:number}} conf
* @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) => {
/**
@@ -304,7 +289,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
result.users.push(y)
result['array' + i] = y.getArray('array')
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')
}
testConnector.syncAll()
@@ -324,7 +309,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
*/
export const compare = users => {
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"
// This ensures that mergeUpdates works correctly
const mergedDocs = users.map(user => {
@@ -335,7 +320,7 @@ export const compare = users => {
users.push(.../** @type {any} */(mergedDocs))
const userArrayValues = users.map(u => u.getArray('array').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())
for (const u of users) {
t.assert(u.store.pendingDs === null)
@@ -362,8 +347,15 @@ export const compare = users => {
t.compare(userMapValues[i], userMapValues[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], userTextValues[i + 1])
t.compare(Y.getStateVector(users[i].store), Y.getStateVector(users[i + 1].store))
t.compare(userTextValues[i], userTextValues[i + 1], '', (constructor, a, b) => {
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))
compareStructStores(users[i].store, users[i + 1].store)
}
@@ -378,8 +370,8 @@ export const compare = users => {
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
/**
* @param {Y.StructStore} ss1
* @param {Y.StructStore} ss2
* @param {import('../src/internals').StructStore} ss1
* @param {import('../src/internals').StructStore} ss2
*/
export const compareStructStores = (ss1, ss2) => {
t.assert(ss1.clients.size === ss2.clients.size)
@@ -421,13 +413,13 @@ export const compareStructStores = (ss1, ss2) => {
}
/**
* @param {Y.DeleteSet} ds1
* @param {Y.DeleteSet} ds2
* @param {import('../src/internals').DeleteSet} ds1
* @param {import('../src/internals').DeleteSet} ds2
*/
export const compareDS = (ds1, ds2) => {
t.assert(ds1.clients.size === ds2.clients.size)
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)
for (let i = 0; i < deleteItems1.length; i++) {
const di1 = deleteItems1[i]

View File

@@ -1,18 +1,27 @@
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 t from 'lib0/testing'
/**
* @param {t.TestCase} tc
*/
export const testInfiniteCaptureTimeout = tc => {
const { array0 } = init(tc, { users: 3 })
const undoManager = new Y.UndoManager(array0, { captureTimeout: Number.MAX_VALUE })
array0.push([1, 2, 3])
undoManager.stopCapturing()
array0.push([4, 5, 6])
undoManager.undo()
t.compare(array0.toArray(), [1, 2, 3])
}
/**
* @param {t.TestCase} tc
*/
export const testUndoText = tc => {
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
text0.insert(0, 'test')
@@ -53,6 +62,20 @@ export const testUndoText = tc => {
t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }])
}
/**
* Test case to fix #241
* @param {t.TestCase} tc
*/
export const testEmptyTypeScope = tc => {
const ydoc = new Y.Doc()
const um = new Y.UndoManager([], { doc: ydoc })
const yarray = ydoc.getArray()
um.addToScope(yarray)
yarray.insert(0, [1])
um.undo()
t.assert(yarray.length === 0)
}
/**
* Test case to fix #241
* @param {t.TestCase} tc
@@ -81,7 +104,7 @@ export const testDoubleUndo = tc => {
export const testUndoMap = tc => {
const { testConnector, map0, map1 } = init(tc, { users: 2 })
map0.set('a', 0)
const undoManager = new UndoManager(map0)
const undoManager = new Y.UndoManager(map0)
map0.set('a', 1)
undoManager.undo()
t.assert(map0.get('a') === 0)
@@ -120,7 +143,7 @@ export const testUndoMap = tc => {
*/
export const testUndoArray = tc => {
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])
array1.insert(0, [4, 5, 6])
testConnector.syncAll()
@@ -171,7 +194,7 @@ export const testUndoArray = tc => {
*/
export const testUndoXml = tc => {
const { xml0 } = init(tc, { users: 3 })
const undoManager = new UndoManager(xml0)
const undoManager = new Y.UndoManager(xml0)
const child = new Y.XmlElement('p')
xml0.insert(0, [child])
const textchild = new Y.XmlText('content')
@@ -196,7 +219,7 @@ export const testUndoXml = tc => {
*/
export const testUndoEvents = tc => {
const { text0 } = init(tc, { users: 3 })
const undoManager = new UndoManager(text0)
const undoManager = new Y.UndoManager(text0)
let counter = 0
let receivedMetadata = -1
undoManager.on('stack-item-added', /** @param {any} event */ event => {
@@ -222,7 +245,7 @@ export const testUndoEvents = tc => {
export const testTrackClass = tc => {
const { users, text0 } = init(tc, { users: 3 })
// 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(() => {
text0.insert(0, 'abc')
}, 42)
@@ -240,8 +263,8 @@ export const testTypeScope = tc => {
const text0 = new Y.Text()
const text1 = new Y.Text()
array0.insert(0, [text0, text1])
const undoManager = new UndoManager(text0)
const undoManagerBoth = new UndoManager([text0, text1])
const undoManager = new Y.UndoManager(text0)
const undoManagerBoth = new Y.UndoManager([text0, text1])
text1.insert(0, 'abc')
t.assert(undoManager.undoStack.length === 0)
t.assert(undoManagerBoth.undoStack.length === 1)
@@ -252,15 +275,35 @@ export const testTypeScope = tc => {
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
*/
export const testUndoDeleteFilter = tc => {
/**
* @type {Array<Y.Map<any>>}
* @type {Y.Array<any>}
*/
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()
map0.set('hi', 1)
const map1 = new Y.Map()
@@ -301,3 +344,303 @@ export const testUndoUntilChangePerformed = tc => {
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' } } })
}
/**
* Undo text formatting delete should not corrupt peer state.
*
* @see https://github.com/yjs/yjs/issues/392
* @param {t.TestCase} tc
*/
export const testUndoDeleteTextFormat = tc => {
const doc = new Y.Doc()
const text = doc.getText()
text.insert(0, 'Attack ships on fire off the shoulder of Orion.')
const doc2 = new Y.Doc()
const text2 = doc2.getText()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
const undoManager = new Y.UndoManager(text)
text.format(13, 7, { bold: true })
undoManager.stopCapturing()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
text.format(16, 4, { bold: null })
undoManager.stopCapturing()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
undoManager.undo()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
const expect = [
{ insert: 'Attack ships ' },
{
insert: 'on fire',
attributes: { bold: true }
},
{ insert: ' off the shoulder of Orion.' }
]
t.compare(text.toDelta(), expect)
t.compare(text2.toDelta(), expect)
}
/**
* Undo text formatting delete should not corrupt peer state.
*
* @see https://github.com/yjs/yjs/issues/392
* @param {t.TestCase} tc
*/
export const testBehaviorOfIgnoreremotemapchangesProperty = tc => {
const doc = new Y.Doc()
const doc2 = new Y.Doc()
doc.on('update', update => Y.applyUpdate(doc2, update, doc))
doc2.on('update', update => Y.applyUpdate(doc, update, doc2))
const map1 = doc.getMap()
const map2 = doc2.getMap()
const um1 = new Y.UndoManager(map1, { ignoreRemoteMapChanges: true })
map1.set('x', 1)
map2.set('x', 2)
map1.set('x', 3)
map2.set('x', 4)
um1.undo()
t.assert(map1.get('x') === 2)
t.assert(map2.get('x') === 2)
}
/**
* Special deletion case.
*
* @see https://github.com/yjs/yjs/issues/447
* @param {t.TestCase} tc
*/
export const testSpecialDeletionCase = tc => {
const origin = 'undoable'
const doc = new Y.Doc()
const fragment = doc.getXmlFragment()
const undoManager = new Y.UndoManager(fragment, { trackedOrigins: new Set([origin]) })
doc.transact(() => {
const e = new Y.XmlElement('test')
e.setAttribute('a', '1')
e.setAttribute('b', '2')
fragment.insert(0, [e])
})
t.compareStrings(fragment.toString(), '<test a="1" b="2"></test>')
doc.transact(() => {
// change attribute "b" and delete test-node
const e = fragment.get(0)
e.setAttribute('b', '3')
fragment.delete(0)
}, origin)
t.compareStrings(fragment.toString(), '')
undoManager.undo()
t.compareStrings(fragment.toString(), '<test a="1" b="2"></test>')
}

View File

@@ -112,6 +112,24 @@ export const testMergeUpdates = tc => {
})
}
/**
* @param {t.TestCase} tc
*/
export const testKeyEncoding = tc => {
const { users, text0, text1 } = init(tc, { users: 2 })
text0.insert(0, 'a', { italic: true })
text0.insert(0, 'b')
text0.insert(0, 'c', { italic: true })
const update = Y.encodeStateAsUpdateV2(users[0])
Y.applyUpdateV2(users[1], update)
t.compare(text1.toDelta(), [{ insert: 'c', attributes: { italic: true } }, { insert: 'b' }, { insert: 'a', attributes: { italic: true } }])
compare(users)
}
/**
* @param {Y.Doc} ydoc
* @param {Array<Uint8Array>} updates - expecting at least 4 updates

View File

@@ -488,6 +488,11 @@ const arrayTransactions = [
map.set('someprop', 43)
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) {
const yarray = user.getArray('array')
const length = yarray.length
@@ -496,7 +501,7 @@ const arrayTransactions = [
let delLength = prng.int32(gen, 1, math.min(2, length - somePos))
if (prng.bool(gen)) {
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)
delLength = prng.int32(gen, 0, math.min(2, type.length - somePos))
type.delete(somePos, delLength)

View File

@@ -42,6 +42,7 @@ export const testBasicMapTests = tc => {
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
users[2].disconnect()
map0.set('null', null)
map0.set('number', 1)
map0.set('string', 'hello Y')
map0.set('object', { key: { key2: 'value' } })
@@ -54,26 +55,29 @@ export const testBasicMapTests = tc => {
array.insert(0, [0])
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('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('boolean1') === true, 'client 0 computed the change (boolean)')
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.size === 6, 'client 0 map has correct size')
t.assert(map0.size === 7, 'client 0 map has correct size')
users[2].connect()
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('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('boolean1') === true, 'client 1 computed the change (boolean)')
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.size === 6, 'client 1 map has correct size')
t.assert(map1.size === 7, 'client 1 map has correct size')
// 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('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)')

View File

@@ -6,6 +6,10 @@ import * as math from 'lib0/math'
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 => {
@@ -14,12 +18,17 @@ export const testDeltaAfterConcurrentFormatting = tc => {
testConnector.flushAllMessages()
text0.format(0, 3, { bold: true })
text1.format(2, 2, { bold: true })
let delta = null
/**
* @type {any}
*/
const deltas = []
text1.observe(event => {
delta = event.delta
if (event.delta.length > 0) {
deltas.push(event.delta)
}
})
testConnector.flushAllMessages()
t.compare(delta, [])
t.compare(deltas, [[{ retain: 3, attributes: { bold: true } }, { retain: 2, attributes: { bold: null } }]])
}
/**
@@ -138,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
*/
@@ -151,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
*/
@@ -265,6 +319,41 @@ export const testFormattingRemovedInMidText = tc => {
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
*/
@@ -493,6 +582,60 @@ export const testSearchMarkerBug1 = tc => {
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())
}
/**
* Delete formatting should not leave redundant formatting items.
*
* @param {t.TestCase} tc
*/
export const testDeleteFormatting = tc => {
const doc = new Y.Doc()
const text = doc.getText()
text.insert(0, 'Attack ships on fire off the shoulder of Orion.')
const doc2 = new Y.Doc()
const text2 = doc2.getText()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
text.format(13, 7, { bold: true })
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
text.format(16, 4, { bold: null })
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
const expected = [
{ insert: 'Attack ships ' },
{ insert: 'on ', attributes: { bold: true } },
{ insert: 'fire off the shoulder of Orion.' }
]
t.compare(text.toDelta(), expected)
t.compare(text2.toDelta(), expected)
}
// RANDOM TESTS
let charCounter = 0
@@ -628,7 +771,11 @@ const qChanges = [
(y, gen) => { // insert embed
const ytext = y.getText('text')
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
@@ -675,8 +822,12 @@ const qChanges = [
*/
const checkResult = result => {
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)
}
// Uncomment this to find formatting-cleanup issues