Compare commits

..

1489 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
Kevin Jahns
633eb9033c 13.5.12 2021-08-02 16:48:25 +02:00
Kevin Jahns
4707fc46ac fix formatting bug. fixes #319 closes #320 2021-08-02 16:43:25 +02:00
Kevin Jahns
89b4320a8e fix test in #310 2021-08-02 16:20:01 +02:00
Kevin Jahns
0ea0a35521 Merge pull request #310 from SamDuvall/fix-xml-clone
Fix XmlElement.clone and XmlFragment.clone
2021-08-02 16:12:39 +02:00
Daishi Kato
15ea4ee805 Update README.md 2021-07-25 10:08:43 +09:00
Kevin Jahns
744469d363 13.5.11 2021-06-24 17:00:48 +02:00
Kevin Jahns
311dd50f1b array.insert throws length-exceeded event - fixes #314 2021-06-24 16:50:25 +02:00
Kevin Jahns
89c5541ee6 fix merge logic edge-cases with skips 2021-06-24 15:14:49 +02:00
Kevin Jahns
28d8db86f0 Merge branch 'main' of github.com:yjs/yjs into main 2021-06-21 12:05:09 +02:00
Kevin Jahns
0c34216ed0 merge pending structs in v1 format 2021-06-21 12:04:40 +02:00
Kevin Jahns
9aa518bc14 Merge pull request #313 from YousefED/hasAttribute
implement hasAttribute
2021-06-16 02:59:00 +02:00
Kevin Jahns
27b1190a28 Merge pull request #312 from whawker/console-log-removal
remove console.log added in #309
2021-06-16 01:48:22 +02:00
Yousef El-Dardiry
f3d8db491b implement hasAttribute 2021-06-15 16:36:22 +02:00
Will Hawker
e9905602f8 remove console.log debug artifact added in #309 2021-06-11 09:30:54 +01:00
Kevin Jahns
2b8154fa16 Merge pull request #309 from whawker/map-clear
add `clear` to YMap
2021-06-09 16:07:44 +02:00
Sam Duvall
5ddb7eefed Fix YXmlElement.clone and YXmlFragment.clone 2021-06-08 21:30:08 -04:00
Will Hawker
4b35de5ad5 add clear to YMap to remove all elements 2021-06-08 21:09:44 +01:00
Kevin Jahns
097b9e8208 13.5.10 2021-06-07 19:43:44 +02:00
Kevin Jahns
5cac153a17 Fix #308 - stateVector should ignore skips and incomplete content 2021-06-07 19:41:54 +02:00
Kevin Jahns
a7e4724edd 13.5.9 2021-05-31 17:56:10 +02:00
Kevin Jahns
71d8da6513 force that transactions that apply document updates are set as non-local transatctions. Fixes #307 2021-05-31 17:54:24 +02:00
Kevin Jahns
c72ac448e9 13.5.8 2021-05-25 21:25:00 +02:00
Kevin Jahns
da21fca334 add countable check to search_marker update 2021-05-25 21:23:12 +02:00
Kevin Jahns
d80512d690 13.5.7 2021-05-25 21:21:31 +02:00
Kevin Jahns
6886881b76 fix #297 (length not updated) by updating search markers properly 2021-05-25 21:17:01 +02:00
Kevin Jahns
dc9717ecd0 13.5.6 2021-05-14 18:57:14 +02:00
Kevin Jahns
7bd764fba7 use non-explicit resolution for lib0 2021-05-14 18:53:24 +02:00
Kevin Jahns
4047890a6e Merge pull request #299 from mgielda/patch-1
Fix broken link for All Done
2021-05-14 01:08:56 +02:00
Kevin Jahns
1627e7b3f6 Merge pull request #292 from nikgraf/patch-1
add Serenity Notes to Who is using Yjs
2021-05-14 01:05:26 +02:00
Kevin Jahns
e55d3cc510 Merge pull request #294 from cindywu/patch-1
Fix typo in README
2021-05-14 01:04:53 +02:00
Kevin Jahns
55bd0b16f7 Merge pull request #295 from getflourish/patch-1
Fix a small typo
2021-05-14 01:04:11 +02:00
Kevin Jahns
ab7de51064 Merge pull request #296 from manstie/patch-1
fix readme typo
2021-05-14 01:03:39 +02:00
Michael Gielda
d4917bb567 Fix broken link for All Done
Signed-off-by: Michael Gielda <mgielda@antmicro.com>
2021-05-08 08:29:46 +02:00
Kevin Jahns
4e343ccace remove unnecessary if-conditition (reminiscent from a previous Yjs version) 2021-04-29 20:25:57 +02:00
manstie
4efd47447b fix readme typo 2021-04-26 18:09:58 +08:00
Florian Schulz
5aa1aaebb3 Fix a small typo 2021-04-22 14:25:53 +02:00
cindy
7656f897d6 Fix typo in README 2021-04-14 17:53:25 -10:00
Kevin Jahns
5244755879 13.5.5 2021-04-13 22:06:55 +02:00
Kevin Jahns
3a7a324a24 fix #291 2021-04-13 22:05:30 +02:00
Kevin Jahns
9e98fec504 Perform undo until change was applied 2021-04-13 13:04:49 +02:00
Nik Graf
b1c7022890 add Serenity Notes to Who is using Yjs 2021-04-12 20:57:16 +02:00
Kevin Jahns
c67428d715 13.5.4 2021-04-02 23:13:24 +02:00
Kevin Jahns
45a9af96af Merge pull request #289 from jhaynie/main
fix crash when the walker didn't match because n will be null
2021-04-02 23:11:48 +02:00
Jeff Haynie
249c4f9c45 switch order for type to get picked up 2021-04-01 14:59:34 -05:00
Jeff Haynie
cdc7d3ffe6 fix crash when the walker didnt match because n will be null 2021-04-01 14:54:37 -05:00
Kevin Jahns
ac6a0e7667 Merge pull request #285 from damz/patch-1
Trivial editing of README.md
2021-03-25 14:02:38 +01:00
Kevin Jahns
12881e2be7 13.5.3 2021-03-21 21:32:38 +01:00
Kevin Jahns
77958da657 unify Y.Array & Y.Text deltas so event.changes.delta is equal to event.delta 2021-03-21 21:31:28 +01:00
Kevin Jahns
8a8a60efde remove unpkg entry 2021-03-21 15:31:17 +01:00
Damien Tournoud
7a1d648e79 Trivial editing of README.md 2021-03-15 07:24:36 -07:00
Kevin Jahns
3af420e790 add package.json to exports so other packages can consume it 2021-03-15 15:08:19 +01:00
Kevin Jahns
4f2d13e3ce 13.5.2 2021-03-11 18:54:02 +01:00
Kevin Jahns
e0b76cd2f4 [UndoManager] stop tracking unrelated insertions - yjs/y-monaco#10 2021-03-11 18:52:35 +01:00
Kevin Jahns
d812636c5b Implement doc suggestions - closes #249 2021-03-03 12:20:53 +01:00
Kevin Jahns
21fee0fe96 spelling issue lib0 repetition 2021-02-28 16:09:54 +01:00
Kevin Jahns
fab14a09de 13.5.1 2021-02-20 22:21:06 +01:00
Kevin Jahns
710b4ba145 upgrade typescript to v4 2021-02-20 22:19:22 +01:00
Kevin Jahns
34091ae614 add conditional exports 2021-02-20 21:55:01 +01:00
Kevin Jahns
feb8ec1afc catch errors from sponsoring message 2021-02-20 21:45:52 +01:00
Kevin Jahns
ce9139c9f4 Merge pull request #278 from BitPhinix/patch-1
Add slate-yjs to bindings
2021-02-18 17:56:21 +01:00
Eric Meier
e2e5d0870c Add slate-yjs to bindings
Adds slate-yjs to the bindings inside in the readme.
2021-02-14 13:04:57 +01:00
Kevin Jahns
04cff60931 add performance test for updates 2021-02-08 13:46:22 +01:00
Kevin Jahns
5dfe4e8af2 add documentation for differential updates 2021-02-08 12:40:00 +01:00
Kevin Jahns
05ca0b0208 13.5.0 2021-02-08 11:50:35 +01:00
Kevin Jahns
ee7c189fdc fix formatting issue #275 #277 2021-02-08 11:45:26 +01:00
Kevin Jahns
01c08ef202 Merge branch 'QortexDevs-merge-empty-lines-despite-attributes' into main 2021-02-08 11:09:24 +01:00
Kevin Jahns
894c0d7731 resolve conflicts 2021-02-08 11:09:13 +01:00
Kevin Jahns
fdf632f03e Merge pull request #274 from yjs/differential-updates-263
Differential updates
2021-02-07 23:58:59 +01:00
Kevin Jahns
ce80cb4a0d 13.4.14 2021-02-02 15:52:37 +01:00
Kevin Jahns
ae3c4cc050 add testHelper to bundle 2021-02-02 15:50:22 +01:00
Kevin Jahns
27a78047c5 13.4.13 2021-02-02 15:12:23 +01:00
Kevin Jahns
7a128c271b add changedParentTypes to undomanager events 2021-02-02 15:09:42 +01:00
Николай Митин
263cc0856e Implemented bug test 2021-01-31 18:17:10 +03:00
Kevin Jahns
2199ac3e4e merge relativePosition updates 2021-01-30 00:12:01 +01:00
Kevin Jahns
275d52b19d implement diffUpdates with tests - #263 2021-01-29 18:18:29 +01:00
Kevin Jahns
7edbb2485f complete refactor of update mechanism to allow encoding of pending updates - #263 2021-01-28 20:28:30 +01:00
Kevin Jahns
304812fb07 concept for improved implementation of pending updates 2021-01-17 15:22:36 +01:00
Kevin Jahns
baca852733 add relevant relative positions exports 2021-01-13 01:16:21 +01:00
Kevin Jahns
7cbf204143 reduce bundle size #272 2021-01-10 15:13:19 +01:00
Kevin Jahns
c8a59118b5 13.4.12 2021-01-10 12:37:08 +01:00
Kevin Jahns
bee397f1e5 rename funding exec 2021-01-10 12:31:01 +01:00
Kevin Jahns
1e97cf8323 bump dependencies & update npm website 2021-01-10 12:27:37 +01:00
Kevin Jahns
c28ad0608e emit transaction on update call 2021-01-10 12:19:44 +01:00
Kevin Jahns
e19f16f22c Merge branch 'main' of github.com:yjs/yjs into main 2021-01-10 00:17:30 +01:00
Kevin Jahns
6f074a873d add bunch of tests for relative positions 2021-01-10 00:16:18 +01:00
Kevin Jahns
4af04d6a29 fix associative relative positions 2021-01-10 00:01:56 +01:00
Kevin Jahns
97d9714710 13.4.11 2021-01-09 15:01:52 +01:00
Kevin Jahns
ca667be68b proper updating of text-attributes 2021-01-09 14:59:56 +01:00
Kevin Jahns
8086a4f816 13.4.10 2021-01-09 14:58:21 +01:00
Kevin Jahns
186f7140b6 fix #271 - multiline text formatting issue 2021-01-09 14:55:37 +01:00
Kevin Jahns
edc1f9418f reproduce #271 2021-01-09 14:45:51 +01:00
Kevin Jahns
32b734b24d add tests 2021-01-08 23:03:44 +01:00
Kevin Jahns
656328631c first prototype of associative relative positions (left- or right-associative) 2021-01-08 23:03:16 +01:00
Kevin Jahns
dbd1b3cb59 add tests for meta decoding of updates and state vector comparison of update and ydoc approach 2020-12-30 20:21:14 +01:00
Kevin Jahns
8fadec4dcd add test for merging via Y.Doc instance (should encode pending updates as well) 2020-12-30 19:32:00 +01:00
Kevin Jahns
8013b4ef5c lint 2020-12-29 17:07:25 +01:00
Kevin Jahns
0a40b541e8 test with all encoders 2020-12-29 16:59:27 +01:00
Kevin Jahns
728bb6f1b2 13.4.9 2020-12-22 17:23:25 +01:00
Kevin Jahns
fd59696b9a change funding url format 2020-12-22 17:21:07 +01:00
Kevin Jahns
bfacd2e63a 13.4.8 2020-12-22 17:15:30 +01:00
Kevin Jahns
6bc9c220b9 add funding to package.json 2020-12-22 17:12:46 +01:00
Kevin Jahns
7c0b98bbb2 lint 2020-12-22 17:05:36 +01:00
Kevin Jahns
034463798d deprecate toJSON 2020-12-22 17:04:31 +01:00
Kevin Jahns
4c929c6808 lint & refactoring 2020-12-19 16:29:17 +01:00
Kevin Jahns
0fc213e92e Merge branch 'main' into differential-updates-263 2020-12-18 22:02:54 +01:00
Kevin Jahns
bbc688975d improve funding message 2020-12-18 21:38:21 +01:00
Kevin Jahns
ab9373c188 funding message 2020-12-18 16:17:38 +01:00
Kevin Jahns
af576788f1 Merge branch 'main' into differential-updates-263 2020-12-18 02:04:31 +01:00
Kevin Jahns
fbbf085278 add mergeUpdates tests to comparison framework 2020-12-17 21:50:39 +01:00
Kevin Jahns
d8868c47e1 test case for deletes + fix 2020-12-16 23:45:28 +01:00
Kevin Jahns
47221c26c4 test with v1 and v2 encoding 2020-12-16 23:26:38 +01:00
Kevin Jahns
ba83398374 fix tests 2020-12-16 22:58:22 +01:00
Kevin Jahns
0b23d5aeeb First working version of differential updates - #263 2020-12-16 22:53:11 +01:00
Kevin Jahns
072947c0bb implement update logging 2020-12-16 21:25:00 +01:00
Kevin Jahns
22aef63d8a add Skip struct 2020-12-16 21:08:18 +01:00
Kevin Jahns
f8341220c3 first working version that also considers holes in document updates - #263 2020-12-15 15:39:08 +01:00
Kevin Jahns
50e5964fcb Merge pull request #268 from jsilvao/main
Update README.md
2020-12-14 20:39:48 +01:00
Kevin Jahns
004a781a56 basic merge works. fixes first test #263 2020-12-13 16:24:43 +01:00
Javier Silva Ortíz
31dee48f63 Update README.md
Add a new Yjs user
2020-12-12 17:37:07 -05:00
Kevin Jahns
c8534ea6bc merging delete-sets #263 2020-12-12 22:48:10 +01:00
Kevin Jahns
1e0fd60df4 proper merge for deletesets 2020-12-12 22:40:55 +01:00
Kevin Jahns
3404d22d12 13.4.7 2020-12-12 21:41:08 +01:00
Kevin Jahns
d3b56702ad Merge pull request #267 from yjs/wishlist-259
Implements some features of wishlist #259
2020-12-12 21:38:45 +01:00
Kevin Jahns
d5e6c26420 Merge branch 'main' into wishlist-259 2020-12-12 21:36:45 +01:00
Kevin Jahns
e497f07f7a remove new pos api template 2020-12-12 21:33:14 +01:00
Kevin Jahns
510354d99f add github workflow 2020-12-12 21:22:55 +01:00
Kevin Jahns
c3342d0b34 Merge pull request #266 from yjs/circleci-project-setup
Add .circleci/config.yml
2020-12-12 21:19:41 +01:00
Kevin Jahns
45af21f31e Add .circleci/config.yml 2020-12-12 21:18:14 +01:00
Kevin Jahns
320da29b69 implement merge-logic - #263 2020-12-10 18:06:35 +01:00
Kevin Jahns
783c4d8209 write #263 append logic 2020-12-09 17:48:45 +01:00
Kevin Jahns
2c708b647d write lazy encoder & decoder - #263 2020-12-08 20:20:40 +01:00
Kevin Jahns
7a45be8c88 add merge tests for #263 2020-12-07 19:47:48 +01:00
Kevin Jahns
972d15dda5 Update Sponsors 2020-12-05 13:17:14 +01:00
Kevin Jahns
fdf2063943 13.4.6 2020-12-04 14:02:53 +01:00
Kevin Jahns
e81267d4df implement correct destroy event 2020-12-04 14:01:14 +01:00
Kevin Jahns
563c34f81a Update README.md 2020-12-01 15:50:58 +01:00
Kevin Jahns
ba713983e3 update sponsors 2020-12-01 15:41:45 +01:00
Kevin Jahns
bf2ee3680b 13.4.5 2020-11-21 19:28:56 +01:00
Kevin Jahns
b812a3dd6c Add getItem to the exports 2020-11-21 19:27:12 +01:00
Kevin Jahns
b3f5b50377 Merge branch 'wishlist-259' of github.com:yjs/yjs into wishlist-259 2020-11-16 12:40:27 +01:00
Kevin Jahns
7bcd4a828d Create new Pos API - #259 2020-11-16 12:40:18 +01:00
Kevin Jahns
cb705922b4 implement insertAfter - #259 2020-11-15 14:57:45 +01:00
Kevin Jahns
1ed58909d3 implement prev/nextSibling&firstChild & parent - #259 2020-11-14 13:33:43 +01:00
Kevin Jahns
0aca7bbefa implement attributes on Y.Text 2020-11-13 12:40:53 +01:00
Kevin Jahns
e1f0324840 call UndoManager pop-stack-item after transaction 2020-11-13 12:05:53 +01:00
Kevin Jahns
7bac783490 13.4.4 2020-11-08 13:09:49 +01:00
Kevin Jahns
1508c44f68 lint 2020-11-08 13:08:14 +01:00
Kevin Jahns
3dd843372f Merge pull request #254 from nornagon/array-from
add Y.Array.from
2020-11-08 02:01:48 +01:00
Kevin Jahns
d6be4d9391 Merge pull request #253 from lpmi-13/update_links
update http links, where possible, to https
2020-11-08 02:00:36 +01:00
Kevin Jahns
53f2344017 implement .clone, .slice, and yxml.get 2020-11-08 01:51:39 +01:00
Kevin Jahns
86f7631d1e 13.4.3 2020-11-04 00:37:24 +01:00
Kevin Jahns
3bb107504f fix superflous event happening in nested event system 2020-11-04 00:35:08 +01:00
Jeremy Rose
4c46ebfb45 add Y.Array.from 2020-11-01 10:01:04 -08:00
Adam Leskis
9d0d63ead7 update http links, where possible, to https
cattaz.io, unfortunately, is still only available over http, but I've raised an issue in the repo to enable https on github pages, which the site appears to be using.
2020-10-31 10:32:05 +00:00
Kevin Jahns
39803c1d11 13.4.2 2020-10-31 03:58:59 +01:00
Kevin Jahns
46fae57036 Merge pull request #244 from hanspagel/patch-1
fix a small typo (at it heart -> at its heart)
2020-10-31 03:51:17 +01:00
Kevin Jahns
e9cb07da55 Failsafe when splitting surrogate pairs - fixes #248 2020-10-31 02:05:33 +01:00
Kevin Jahns
114f28f48e log error when removing eventhandler that doesnt exist - implements #246 2020-10-31 00:34:19 +01:00
Kevin Jahns
a1da486c8a Merge branch 'main' of github.com:yjs/yjs into main 2020-10-29 12:40:48 +01:00
Kevin Jahns
4fb9cc2a30 fire top-level events first 2020-10-29 12:40:39 +01:00
Kevin Jahns
e2c9eb7f01 13.4.1 2020-10-10 16:53:31 +02:00
Kevin Jahns
6fd33c0720 fix permanent user-data init with new DS-decoder - fixes yjs/y-websocket#33 2020-10-10 16:48:43 +02:00
Hans Pagel
72f3ce75b2 fix a small typo (at it heart -> at its heart) 2020-09-28 23:29:09 +02:00
Kevin Jahns
fd211731cc 13.4.0 2020-09-28 19:04:58 +02:00
Kevin Jahns
8049776074 fix double undo - fixes #241 2020-09-28 19:00:13 +02:00
Kevin Jahns
32b1338d48 Merge pull request #233 from rideg/add_typing_232
Amend typing of YEvent.changes, fixes #232
2020-09-28 18:38:03 +02:00
Kevin Jahns
c2f0ca3fae Merge pull request #238 from johnrees/patch-1
Fix typo in README example
2020-09-28 18:36:35 +02:00
Kevin Jahns
dfc6b879de Merge pull request #239 from yjs/subdocs
implemented first subdocuments draft #234
2020-09-28 18:35:43 +02:00
Kevin Jahns
81f16ff0b5 Merge pull request #243 from yjs/create-doc-from-snapshot-3
Create doc from snapshot 3
2020-09-28 18:34:01 +02:00
Kevin Jahns
e1a2ccd7f6 add tests to snapshots case and fix the case of empty ranges 2020-09-28 18:32:24 +02:00
Kevin Jahns
be8cc8a20c Merge branch '159-create-doc-from-snapshot-2' of git://github.com/calibr/yjs into calibr-159-create-doc-from-snapshot-2 2020-09-28 17:57:51 +02:00
Kevin Jahns
a253cfc090 Merge pull request #235 from DeepAnchor/patch-1
Fix JSDoc annotation
2020-09-28 17:55:25 +02:00
Kevin Jahns
992c0b5e32 13.4.0-0 2020-09-10 01:57:00 +02:00
Kevin Jahns
e17d661769 implemented first subdocuments draft #234 2020-09-10 01:54:16 +02:00
calibr
fef3fc2a4a remove debug messages 2020-09-08 13:33:41 +03:00
calibr
eee695eeeb use encoding/decoding for restoring snapshots 2020-09-08 13:32:02 +03:00
John Rees
38e38a92dc Fix typo in README example 2020-09-04 11:30:01 +01:00
Kevin Jahns
dadc08597d Merge branch 'josephg-main' into main 2020-09-03 19:14:39 +02:00
Kevin Jahns
e769a2a354 Finishing up INTERNALS.md 2020-09-03 19:14:19 +02:00
Seph Gentle
0dd0a4be14 Added draft of INTERNALS.js describing how Yjs works 2020-09-03 10:02:04 +10:00
DeepAnchor
7193ae63b7 Fix JSDoc annotation 2020-08-25 13:09:34 -07:00
rideg
4d48224518 Add typing 2020-08-24 09:57:38 -07:00
Kevin Jahns
b4fc073aa5 properly annotate DeltaItem.insert - fixes #227 2020-08-08 18:29:50 +02:00
Kevin Jahns
9c0d1eb209 Merge branch '159-create-doc-from-snapshot-2' of git://github.com/calibr/yjs into calibr-159-create-doc-from-snapshot-2 2020-08-08 12:03:50 +02:00
Kevin Jahns
6a9f853d12 fix readme formatting 2020-08-08 02:43:03 +02:00
Kevin Jahns
ce3b0f3043 13.3.2 2020-08-07 19:31:29 +02:00
Kevin Jahns
94646b2f45 fix item.content undefined 2020-08-07 19:29:08 +02:00
Kevin Jahns
29c2ad4492 13.3.1 2020-08-07 17:53:00 +02:00
Kevin Jahns
637fadf38e lint markdown 2020-08-07 17:51:17 +02:00
Kevin Jahns
0c6c11d583 Merge branch 'main' of github.com:yjs/yjs into main 2020-08-07 17:47:28 +02:00
Kevin Jahns
6f9a2c9df7 implement before/afterAllTransactions 2020-08-07 17:47:20 +02:00
Kevin Jahns
7876a96163 Merge pull request #224 from ajhyndman/document-tojson
Document the  doc.toJSON method
2020-08-04 16:53:10 +02:00
calibr
ceba4b1837 restoring document to a specific state using a Snapshot, #159 2020-07-27 03:56:32 +03:00
Andrew Hyndman
22653c799c Document the doc.toJSON method 2020-07-22 21:47:55 -07:00
Kevin Jahns
68109b033f lint - fixes #223 2020-07-22 12:32:34 +02:00
Kevin Jahns
38eb2e502c stricter searchMarker filter 2020-07-16 20:44:54 +02:00
Kevin Jahns
270a69fcf6 13.3.0 2020-07-15 22:18:47 +02:00
Kevin Jahns
6e3b708599 implement search-marker prototype (limited usage for now) 2020-07-15 22:03:02 +02:00
Kevin Jahns
6e8167fe51 integration refactor with stackHead magic 2020-07-13 17:38:39 +02:00
Kevin Jahns
3449687280 micro optimizations in struct reader 2020-07-13 15:47:51 +02:00
Kevin Jahns
3406247a3e choose rencoding version at random 2020-07-12 21:11:12 +02:00
Kevin Jahns
076d550dfa export YTextEvent - fixes #213 2020-07-12 20:13:18 +02:00
Kevin Jahns
bb45816f05 remove bare for .. of iterations - fixes #220 2020-07-12 20:04:56 +02:00
Kevin Jahns
5414ac7f6e yjs implements unpkg - implements #216 2020-07-12 19:13:50 +02:00
Kevin Jahns
0b8f032364 add AbstractConnector interface - implements #215 2020-07-12 19:07:16 +02:00
Kevin Jahns
dc136ff56a Merge branch 'relm-us-ydoctojson' into main 2020-07-12 18:51:04 +02:00
Kevin Jahns
b73a720fdc merge with upstream 2020-07-12 18:50:32 +02:00
Kevin Jahns
cf420d6241 export decodeStateVector - fixes #218 2020-07-12 18:41:34 +02:00
Kevin Jahns
859e169c91 fix empty type name 2020-07-12 18:40:39 +02:00
Kevin Jahns
6c2cf0f769 Implement experimental new encoder 🚀 2020-07-12 18:25:54 +02:00
Duane Johnson
1a942aa4e0 whitespace 2020-07-12 09:38:20 -06:00
Duane Johnson
368dc6b36a Add YDoc toJSON 2020-07-12 09:36:51 -06:00
Kevin Jahns
2151c514e5 fix empty parentYKey issue 2020-07-08 17:54:41 +02:00
Kevin Jahns
bb25ce7731 Remove tsc import doc because typescript is now natively supported 2020-06-29 00:32:08 +02:00
Kevin Jahns
e31e968f0d fix node arraybuffer decoding 2020-06-20 01:48:00 +02:00
Kevin Jahns
1a494761a3 add compatibility tests 2020-06-19 21:45:10 +02:00
Kevin Jahns
b434501d11 merge upstream 2020-06-18 00:33:35 +02:00
Kevin Jahns
d1d86277b8 update sponsors ❤️ 2020-06-18 00:32:51 +02:00
Kevin Jahns
d7a11ccf4d fix gc regression issue & add another breaking condition for the integration algorithm 2020-06-18 00:31:25 +02:00
Kevin Jahns
4c48116947 Added Sponsor ❤️ 2020-06-13 14:48:11 +02:00
Kevin Jahns
6dd26d3b48 reduce number of variables and sanity checks 😵 2020-06-09 23:48:27 +02:00
Kevin Jahns
6b0154f046 improve mem usage by conditional execution of the integration part (step throught the integration if there are conflicting items) 2020-06-09 16:34:07 +02:00
Kevin Jahns
7fb63de8fc 13.2.0 2020-06-09 01:04:00 +02:00
Kevin Jahns
c4d80d133d Merge branch 'master' of github.com:yjs/yjs 2020-06-09 00:54:59 +02:00
Kevin Jahns
cebe96c001 Merge pull request #209 from relm-us/ymap-size
Add 'size' getter to Y.Map
2020-06-09 00:54:52 +02:00
Kevin Jahns
4d2369ce21 Merge branch 'master' of github.com:yjs/yjs 2020-06-09 00:53:38 +02:00
Kevin Jahns
5293ab4df1 Improve memory usage by omitting the ItemRef step and directly applying the Item 2020-06-09 00:53:05 +02:00
Duane Johnson
e53c01c6c5 Add 'size' getter to Y.Map 2020-06-07 07:44:37 -06:00
Kevin Jahns
03faa27787 Merge pull request #208 from relm-us/ymap-iterable-constructor
Add optional iterable param to Y.Map(), matching Map()
2020-06-07 12:34:08 +02:00
Duane Johnson
868dd5f0a5 Add optional iterable param to Y.Map(), matching Map() 2020-06-06 21:32:24 -06:00
Kevin Jahns
fa58ce53cd Update Sponsors ❤️ 2020-06-07 01:56:16 +02:00
Kevin Jahns
0a0098fdfb reuse item position references in Y.Text 2020-06-05 00:27:36 +02:00
Kevin Jahns
a5a48d07f6 13.1.1 2020-06-04 18:15:58 +02:00
Kevin Jahns
7b16d5c92d implement pivoting in struct search 2020-06-04 18:14:41 +02:00
Kevin Jahns
ee147c14f1 Merge branch 'master' of github.com:yjs/yjs 2020-06-04 17:07:27 +02:00
Kevin Jahns
e86d5ba25b fix ref offset issue 2020-06-04 17:07:17 +02:00
Kevin Jahns
149ca6f636 Merge pull request #205 from Kisama/ytext-newline-option
Add sanitize option
2020-06-03 19:22:29 +02:00
Cole
e4223760b0 - rollback shorter url to original and ignore max length check for specific line
- add opts sanitize for applyDelata in YText
- apply applyDelata document about YText
2020-06-03 11:18:09 +09:00
Cole
9d3dd4e082 Add setter form permit empty paragraph at the end of the content when applyDelta. 2020-06-03 11:15:03 +09:00
Cole
5a4ff33bf4 Merge branch 'master' of github.com:yjs/yjs 2020-06-03 11:12:38 +09:00
Kevin Jahns
a059fa12e9 13.1.0 2020-06-02 23:52:56 +02:00
Kevin Jahns
0628d8f1c9 fix linting 2020-06-02 23:44:13 +02:00
Kevin Jahns
19e2d51190 Merge branch 'master' of github.com:yjs/yjs 2020-06-02 23:20:54 +02:00
Kevin Jahns
60fab42b3f improve memory allocation ⇒ less "minor gc" cleanups 2020-06-02 23:20:45 +02:00
Cole
469404c6e1 move quill relate newline remove logic to y-quill 2020-06-01 19:17:54 +09:00
Kevin Jahns
c9756e5b57 add npm funding url 2020-05-31 23:24:35 +02:00
Kevin Jahns
601d24e930 Add more backers ❤️ 2020-05-30 21:20:59 +02:00
Kevin Jahns
b2c16674f2 Add sponsors to readme ❤️ 2020-05-29 15:19:43 +02:00
Kevin Jahns
13da804b5e use organization funding and issue template file 2020-05-18 23:46:32 +02:00
Kevin Jahns
c5ca7b6f8c Update issue templates 2020-05-18 23:31:10 +02:00
Kevin Jahns
f4b68c0dd4 Merge pull request #200 from Mansehej/yarray-unshift
Implement unshift function in Y-Array
2020-05-18 22:14:13 +02:00
Mansehej
4407f70052 Update ReadMe for y-array unshift 2020-05-19 01:01:23 +05:30
Mansehej
8bb52a485a Implement unshift to y-arrays 2020-05-19 01:01:23 +05:30
Kevin Jahns
9fc18d5ce0 fix lint issues 2020-05-18 18:43:16 +02:00
Kevin Jahns
ada4f400b5 Merge branch 'mohe2015-patch-1' 2020-05-18 18:04:18 +02:00
Kevin Jahns
06048b87ee rework provider combination demo 2020-05-18 18:04:04 +02:00
Kevin Jahns
05dde1db01 Merge branch 'patch-1' of git://github.com/mohe2015/yjs into mohe2015-patch-1 2020-05-18 17:41:20 +02:00
Kevin Jahns
b5b32c5b3c add relm and nimbus as users of Yjs 2020-05-18 17:09:44 +02:00
Kevin Jahns
3f0e2078de Update README.md 2020-05-14 17:01:49 +02:00
Kevin Jahns
21470bb409 Update README.md 2020-05-14 16:59:48 +02:00
Kevin Jahns
772bb87d5c 13.0.8 2020-05-13 19:29:51 +02:00
Kevin Jahns
dab172fa1d Rework UndoManager to support changes from other / multiple users 2020-05-13 19:28:30 +02:00
Kevin Jahns
a70c5112cd fix wrong type declaration in documentation. fixes #195 2020-05-11 11:10:38 +02:00
Kevin Jahns
7cb423c046 13.0.7 2020-05-11 01:46:51 +02:00
Kevin Jahns
4547b35641 cleanup formatting attributes 2020-05-11 01:45:27 +02:00
Kevin Jahns
4c87f9a021 13.0.6 2020-05-08 14:50:53 +02:00
Kevin Jahns
4b08c67e06 bump lib0 to fix critical encoding issue in safari 2020-05-08 14:49:50 +02:00
Kevin Jahns
9f5bc9ddfe change client id when duplicate content is detected 2020-05-03 16:10:58 +02:00
Moritz Hedtke
8221db795a Update README.md 2020-04-27 22:39:09 +02:00
Moritz Hedtke
68b4418956 Update README.md 2020-04-27 22:35:37 +02:00
Moritz Hedtke
fa09ebfd82 Add example of combining providers to README.md 2020-04-27 22:31:26 +02:00
Kevin Jahns
b399ffa765 add gc information to API docs 2020-04-26 13:24:18 +02:00
Kevin Jahns
180f4667c1 Readme correction: UndoManager accepts options 2020-04-17 02:02:09 +02:00
Kevin Jahns
9455373611 Merge branch 'master' of github.com:yjs/yjs 2020-04-15 20:50:29 +02:00
Kevin Jahns
aa804d89c0 update now.sh links 2020-04-15 19:52:34 +02:00
Kevin Jahns
3ef51a5d1a run test-exhaustive 2020-04-03 12:11:25 +02:00
Kevin Jahns
e61089c659 npm ci before workflow start 2020-04-03 12:09:13 +02:00
Kevin Jahns
97625cf29b fix workflow 2020-04-03 12:05:43 +02:00
Kevin Jahns
a5dc6c27aa Setup github workflow 2020-04-03 12:02:37 +02:00
Kevin Jahns
26a51bafc9 13.0.5 2020-04-02 01:05:04 +02:00
Kevin Jahns
f40e09d156 type fixes for breaking typescript@3.8.* release 2020-04-02 01:03:30 +02:00
Kevin Jahns
81650bc8f6 Merge branch 'gived-ISNIT0/187' 2020-04-01 23:44:40 +02:00
Kevin Jahns
c87caafeb6 lint & refactor PR #187 2020-04-01 23:39:27 +02:00
Kevin Jahns
195b26d90f Merge branch 'ISNIT0/187' of https://github.com/gived/yjs into gived-ISNIT0/187 2020-04-01 14:05:18 +02:00
Kevin Jahns
7e0189ca84 Merge branch 'master' of github.com:yjs/yjs 2020-04-01 14:04:45 +02:00
Kevin Jahns
192706f2a8 update readme 2020-04-01 14:04:41 +02:00
Joe Reeve
a4ce8ae07d 🐛 fix for #187 2020-03-31 16:06:28 +01:00
Kevin Jahns
e04a980af1 Merge pull request #184 from yjs/readme-cleanup
remove deadlinks
2020-03-21 21:50:43 +01:00
Nik Graf
47d40eb6b0 remove deadlinks 2020-03-21 15:51:39 +01:00
Kevin Jahns
fc4a39cc7d Merge pull request #182 from LucasGenoud/patch-1
Update lib0 to latest version
2020-02-27 18:13:22 +01:00
LucasGenoud
44e1fd9f14 Update lib0 to latest version 2020-02-27 10:51:21 +01:00
Kevin Jahns
02cc5a215f bump lib0 2020-02-19 09:49:54 -06:00
Kevin Jahns
d1e8d50c43 13.0.4 2020-02-12 10:53:56 +01:00
Kevin Jahns
18bb2d0719 fix imports in esm bundle 2020-02-12 10:52:51 +01:00
Kevin Jahns
45df311dd7 13.0.3 2020-02-12 10:38:28 +01:00
Kevin Jahns
62888b4004 bundle yjs as a module to prevent declaration issues from circular dependencies 2020-02-12 10:37:22 +01:00
Kevin Jahns
76c389dba0 13.0.2 2020-02-03 12:23:39 +01:00
Kevin Jahns
78fa98c000 add type definition for YText.length 2020-02-03 12:22:35 +01:00
Kevin Jahns
e9f9e08450 13.0.1 2020-01-27 03:43:45 +01:00
Kevin Jahns
e3c59b0aa7 more options to gc data (undomanager.clear and tryGc) 2020-01-27 03:42:32 +01:00
Kevin Jahns
705dce7838 add y-indexeddb section 2020-01-23 22:49:04 +01:00
Kevin Jahns
0fb55981ba 13.0.0 2020-01-23 21:53:02 +01:00
Kevin Jahns
89378e29ae publish stable Yjs release 🎆 2020-01-23 21:51:26 +01:00
Kevin Jahns
cce35270ec typescript typingis!!! fixes #180 2020-01-23 21:45:56 +01:00
Kevin Jahns
d78180bf97 make opts optional in PermanentUserData 2020-01-23 18:05:12 +01:00
Kevin Jahns
0ab415de3e 13.0.0-108 2020-01-23 05:01:05 +01:00
Kevin Jahns
ff3969caeb dedupe npm 2020-01-23 05:00:11 +01:00
Kevin Jahns
c82cc9f8d6 lint 2020-01-23 04:59:17 +01:00
Kevin Jahns
ef5c71bd8b PermanentUserData fixes 2020-01-23 04:58:02 +01:00
Kevin Jahns
bd6be3d23b 13.0.0-107 2020-01-22 16:45:48 +01:00
Kevin Jahns
0e6deab9c9 type toJSON returns 2020-01-22 16:44:30 +01:00
Kevin Jahns
6cd9e2be32 lint 2020-01-22 16:42:16 +01:00
Kevin Jahns
ac8dab1e88 Merge pull request #179 from garth/text-tojson
basic Y.Text toJSON returns {unformatted:string}
2020-01-22 16:19:01 +01:00
Garth Williams
38ed725c2c basic Y.Text toJSON returns unformatted string
This avoids text nodes in nested structures returning undefined when toJSON is called by a parent.
2020-01-22 13:34:13 +01:00
Kevin Jahns
a210bad25e update keywords 2020-01-19 00:43:23 +01:00
Kevin Jahns
6929a4f0f8 13.0.0-106 2020-01-14 05:16:43 +01:00
Kevin Jahns
52dacfa5f2 update package-lock 2020-01-14 05:15:36 +01:00
Kevin Jahns
27efe86f9c isParentOf 2020-01-14 05:13:51 +01:00
Kevin Jahns
882b9055c7 fix localimports path ending 2020-01-14 02:36:29 +01:00
Kevin Jahns
e089089413 fix debug resolve 2020-01-13 17:03:56 +01:00
Kevin Jahns
197932752e 13.0.0-105 2020-01-13 14:55:05 +01:00
Kevin Jahns
f0b2bdaf34 revert to classic cjs module 2020-01-13 14:54:07 +01:00
Kevin Jahns
b96362c0f1 use correct module script 2020-01-13 07:55:58 +01:00
Kevin Jahns
67f241cd7a 13.0.0-104 2020-01-13 07:48:47 +01:00
Kevin Jahns
c8af0bebf7 fix preversion script 2020-01-13 07:47:43 +01:00
Kevin Jahns
4f35e799a6 update to lib0@.2 2020-01-13 07:41:31 +01:00
Kevin Jahns
eb2a52dd26 update README with podcast links, consulting info, and y-webrtc 2019-12-11 13:26:46 +01:00
Kevin Jahns
189b1068ae 13.0.0-103 2019-12-10 20:52:20 +01:00
Kevin Jahns
7a3b60a5d7 add markdownlint-cli as dep 2019-12-10 20:51:07 +01:00
Kevin Jahns
99f06fc093 bump lib0 for improved encoding performance 2019-12-10 20:46:58 +01:00
Kevin Jahns
22917bca19 fix gc & proper options typings for Y.Doc, fixes #176 2019-12-10 17:51:49 +01:00
Kevin Jahns
7f0e25dcba permanent user store writes updates in separate transaction 2019-12-10 17:18:57 +01:00
Kevin Jahns
d90c9b1cb2 bump lib0 for faster text encoding 2019-12-10 00:26:28 +01:00
Kevin Jahns
c426055f17 spelling 2019-12-10 00:19:02 +01:00
Kevin Jahns
18c9010b63 Merge branch 'master' of github.com:y-js/yjs 2019-11-26 13:02:49 +01:00
Kevin Jahns
c3edac62ef doc typo 2019-11-26 13:02:43 +01:00
Kevin Jahns
755de18fd5 Create Funding.yml 2019-11-07 14:41:50 +01:00
Kevin Jahns
641dc25076 13.0.0-102 2019-10-25 23:47:23 +02:00
Kevin Jahns
1d58ea785f Merge branch 'master' of github.com:yjs/yjs 2019-10-25 23:45:50 +02:00
Kevin Jahns
f53dff5043 delay errors in observe callbacks to throw after cleanup is done 2019-10-25 23:44:09 +02:00
Kevin Jahns
74d1a31f49 Merge pull request #174 from boschDev/master
Fix attrs loop in yXmlText
2019-10-15 17:19:30 +02:00
Roeland Bosch
d1063ab70b Fix attrs loop in yXmlText 2019-10-15 17:07:20 +02:00
Kevin Jahns
f4c919d9ec 13.0.0-101 2019-10-08 18:33:50 +02:00
Kevin Jahns
aeb23dbaa9 follow redone items to prevent some undo-redo issues. Fixes #162 2019-10-08 18:31:56 +02:00
Kevin Jahns
6d4f0c0cdd 13.0.0-100 2019-10-08 17:40:32 +02:00
Kevin Jahns
303138f309 sanitize items before undoing. fixes #165 2019-10-08 17:36:00 +02:00
Kevin Jahns
ad373a3dce Merge pull request #172 from istvank/patch-1
Fixing Y.Map's documentation of forEach
2019-10-05 20:09:53 +02:00
István Koren
2150fa58f2 Fixing Y.Map's documentation of forEach
fixes #171 As always, it's an honor to submit a PR! 🐒 There was also a missing dot in the Y.XmlFragment title.
2019-10-05 15:14:30 +02:00
Kevin Jahns
ece4841b5c update stackItem.meta doc 2019-10-03 22:06:07 +02:00
Kevin Jahns
8103220c05 Merge branch 'master' of github.com:yjs/yjs 2019-09-30 11:10:13 +02:00
Kevin Jahns
66d500f08d YEvent: consider case that item was added & removed in the same transaction 2019-09-30 11:10:03 +02:00
Kevin Jahns
5f8e7c7ba7 Merge pull request #169 from yjs/improve-readme
update quill cursors support
2019-09-23 11:22:51 +02:00
Nik Graf
7b8eee6b25 update quill cursors support 2019-09-23 11:22:24 +02:00
Kevin Jahns
1d5947c602 13.0.0-99 2019-09-23 11:11:45 +02:00
Kevin Jahns
53e4028952 Merge pull request #168 from yjs/fix-absolute-position-calculation
fix absolute position calculation
2019-09-23 11:09:48 +02:00
Nik Graf
b38a8d99e5 fix absolute position calculation 2019-09-23 11:05:50 +02:00
Kevin Jahns
6c4971ae25 13.0.0-98 2019-09-17 18:55:04 +02:00
Kevin Jahns
d1f5ff0f59 implement PermanentUserData storage prototype 2019-09-17 18:53:59 +02:00
Kevin Jahns
1d297601e8 export .createDeleteSet functionality 2019-09-04 22:08:05 +02:00
Kevin Jahns
d9fface0be 13.0.0-97 2019-09-04 13:21:10 +02:00
Kevin Jahns
7d5db917da fix type error >= tsc@3.6 2019-09-04 13:19:25 +02:00
Kevin Jahns
6e7529723d update lib0 2019-09-04 13:15:34 +02:00
Kevin Jahns
6cb64b3707 move repository to yjs org 2019-09-04 13:08:34 +02:00
Kevin Jahns
bb1c0b809f implement snapshot & event.changes 2019-09-03 16:33:29 +02:00
Kevin Jahns
8bcff6138c Y.Text snapshot support (toDelta) 2019-08-31 22:42:18 +02:00
Kevin Jahns
e78d84ee59 md lint 2019-08-31 16:47:12 +02:00
Kevin Jahns
c23bcb66ce delta format: use flat attr comparison 2019-08-31 16:44:07 +02:00
Kevin Jahns
5fddcef3ea Update logo 2019-08-29 12:51:16 +02:00
Kevin Jahns
e1e46c6eb1 Merge branch 'master' of github.com:y-js/yjs 2019-08-27 02:17:16 +02:00
Kevin Jahns
13ad0c8464 implement Y.XmlFragment.length 2019-08-27 02:17:08 +02:00
Kevin Jahns
7700b50470 Merge pull request #161 from blackening/master
Updated documentation for Y.Array forEach
2019-08-20 23:18:46 +02:00
Kevin Jahns
fc4d6165b4 13.0.0-96 2019-08-20 22:29:56 +02:00
Kevin Jahns
251c8aaefc UndoManager configuration to filter deletes 2019-08-20 22:28:49 +02:00
Kevin Jahns
1337d38ada 13.0.0-95 2019-08-09 01:18:15 +02:00
Kevin Jahns
f5c66e41cb audit 2019-08-09 01:16:40 +02:00
Kevin Jahns
0e7da017fe Use lib0/any-encoding instead of JSON 2019-08-09 01:15:46 +02:00
blackening
f0262ffaae Updated documentation for Y.Array forEach
Reference:
https://github.com/y-js/yjs/blob/master/src/types/YArray.js#L186
https://github.com/y-js/yjs/blob/master/src/types/AbstractType.js#L239
2019-07-09 19:58:06 +08:00
Kevin Jahns
36203af88e should not rely on all deconstructing features because not all parsers support it 2019-06-29 14:47:34 +02:00
Kevin Jahns
dd2b8bc6c7 13.0.0-94 2019-06-25 11:57:50 +02:00
Kevin Jahns
463065ac21 UndoManager: keep item before item is deleted (fixes some edge cases of followRedo) 2019-06-25 11:56:41 +02:00
Kevin Jahns
d064e6e96e UndoManager accepts an array of types as scope. Implements #156 2019-06-25 02:26:18 +02:00
Kevin Jahns
b1ed2df208 proper TOC links 2019-06-25 00:10:12 +02:00
Kevin Jahns
1fe4ef135c 13.0.0-93 2019-06-24 23:06:11 +02:00
Kevin Jahns
e376b5d472 UndoManager fixes 2019-06-24 23:04:53 +02:00
Kevin Jahns
952a9b2c41 13.0.0-92 2019-06-23 13:05:30 +02:00
Kevin Jahns
03458dc641 Port Undo/Redo approach with a clean API 2019-06-23 13:04:14 +02:00
Kevin Jahns
14df5b72af fix consistency bug - ref.toStruct does not correctly create GC when offset is specified 2019-06-18 18:46:19 +02:00
Kevin Jahns
338968031b 13.0.0-91 2019-06-18 18:05:39 +02:00
Kevin Jahns
1aac245b93 New types dont fire events - fixes #155 2019-06-18 17:41:19 +02:00
Kevin Jahns
1faff323c1 13.0.0-90 2019-06-14 16:00:02 +02:00
Kevin Jahns
e7280c7ae2 allow case sensitive yxml nodes 2019-06-14 15:59:00 +02:00
Kevin Jahns
4c38619b5d 13.0.0-89 2019-06-13 10:33:35 +02:00
Kevin Jahns
b4e5c5cc1f Correctly insert embed when using YText.applyDelta 2019-06-13 10:30:39 +02:00
Kevin Jahns
b0dbd84f7f lint markdown 2019-06-13 10:28:30 +02:00
Kevin Jahns
4a990963d9 13.0.0-88 2019-06-05 18:37:21 +02:00
Kevin Jahns
7e7c9d5b11 add relevant type information 2019-06-05 14:53:00 +02:00
Kevin Jahns
775f6eed1d fix websocket example 2019-06-02 15:16:14 +02:00
Kevin Jahns
1e83b9418c 13.0.0-87 2019-05-28 14:20:44 +02:00
Kevin Jahns
ac3f672c80 Merge branch 'master' of github.com:y-js/yjs 2019-05-28 14:19:11 +02:00
Kevin Jahns
2192aa5821 Use generic Item with typed content to reduce cache misses 2019-05-28 14:18:20 +02:00
Kevin Jahns
70bb523005 Merge branch 'master' of github.com:y-js/yjs 2019-05-27 12:50:21 +02:00
Kevin Jahns
10ce6de57a import statement fix 2019-05-27 12:50:12 +02:00
Kevin Jahns
3fba4f25a5 Merge pull request #153 from calibr/124-text-embeds
process embeds in YText.toDelta
2019-05-25 13:04:10 +02:00
Kevin Jahns
66c35d8499 testing: do not stringify array values before comparing 2019-05-25 12:54:30 +02:00
Kevin Jahns
4c14157dcf 13.0.0-86 2019-05-25 12:50:05 +02:00
Kevin Jahns
ef6c382e20 fix array iterator on merged content. fixes #152 2019-05-25 12:49:08 +02:00
calibr
ee45b4fdd6 process embeds in YText.toDelta 2019-05-25 13:48:57 +03:00
Kevin Jahns
668e9e8a9b 13.0.0-85 2019-05-25 03:13:54 +02:00
Kevin Jahns
37a6d68543 implement support for boolean values. fixes #151 2019-05-25 03:12:56 +02:00
Kevin Jahns
f893198769 remove examples. fixes #149 2019-05-22 17:32:51 +02:00
Kevin Jahns
d3ee1a0ec2 Add editor support to v13 readme 2019-05-22 01:26:13 +02:00
Kevin Jahns
d6593412a2 13.0.0-84 2019-05-19 21:49:36 +02:00
Kevin Jahns
d31bf36531 use generated esm module by default 2019-05-19 21:48:09 +02:00
Kevin Jahns
a485f550db 13.0.0-83 2019-05-19 20:59:56 +02:00
Kevin Jahns
0610b16227 bump lib0 for webpack compatibility 2019-05-19 20:43:18 +02:00
Kevin Jahns
72e470c5f0 Fix ytext event.delta - items that are synced and deleted
When items are added and deleted in the same transaction, event.delta would recognize them as added (though they are actually deleted). Now it just ignores them.
2019-05-19 20:42:53 +02:00
Kevin Jahns
4d12a02e2f fix offset in state vector 2019-05-16 12:31:53 +02:00
Kevin Jahns
4a7d6f0a2d fix sorting bug that only affects older node versions (probably because old sorting algorithms are not stable) 2019-05-14 15:21:34 +02:00
Kevin Jahns
c80f446b5f README: update provider tutorial 2019-05-12 11:18:43 +02:00
Kevin Jahns
81a529d8dc update *getting started* yjs version 2019-05-07 15:43:09 +02:00
Kevin Jahns
4f0ab78914 13.0.0-82 2019-05-07 13:54:00 +02:00
Kevin Jahns
8c36f67f0b rework and document api 2019-05-07 13:44:23 +02:00
Kevin Jahns
77687d94e6 13.0.0-81 2019-04-28 17:32:05 +02:00
Kevin Jahns
4644511303 bump y-protocols dependency 2019-04-28 17:30:52 +02:00
Kevin Jahns
20005eecdb Merge deleted items more efficiently.
Previously deleted items were simply added to transaction._mergeStructs. But this inherently inefficient as it will splice the struct store for every item.

Now Yjs iterates over transaction.ds and tries to merge structs. It iterates from right to left so merging should be more efficient that before. But more work needs to be done.

For example we could set structs[i] = null and filter the structs after merging is done.
2019-04-28 17:20:35 +02:00
Kevin Jahns
c9dda245bf v13 api docs 2019-04-28 02:53:25 +02:00
Kevin Jahns
1417470156 update demos link 2019-04-27 03:44:48 +02:00
Kevin Jahns
584e5dfd40 Link to v13 docs from README 2019-04-27 03:35:44 +02:00
Kevin Jahns
805acbb9f5 13.0.0-80 2019-04-26 19:55:14 +02:00
Kevin Jahns
32c4c09072 update parent._map when splitting an item 2019-04-26 19:54:00 +02:00
Kevin Jahns
8c5a06bbf8 fix gc when item is deleted in observer call 2019-04-26 18:37:38 +02:00
Kevin Jahns
a336cc167c order observer and transaction cleanups after one another 2019-04-26 13:31:00 +02:00
Kevin Jahns
21d86cd2be Delete all children of ItemType when it is deleted 2019-04-26 12:29:28 +02:00
Kevin Jahns
1d0f9faa91 AbstractItem.mergeWith helper outsourced into separate function 2019-04-24 18:10:33 +02:00
Kevin Jahns
45237571b7 gc more efficiently 2019-04-23 20:51:32 +02:00
Kevin Jahns
bb6f6cd141 13.0.0-79 2019-04-20 00:03:30 +02:00
Kevin Jahns
729c1f16b8 fix test provider 2019-04-20 00:02:40 +02:00
Kevin Jahns
b6059704aa update dependencies 2019-04-20 00:00:09 +02:00
Kevin Jahns
fa3c92f44c change parameter order of transaction events 2019-04-19 23:36:00 +02:00
Kevin Jahns
cd82de7742 lint 2019-04-12 20:08:38 +02:00
Kevin Jahns
07a6a0044b simplify exposed APi 2019-04-12 20:04:07 +02:00
Kevin Jahns
4582832a71 rework intro 2019-04-12 14:24:31 +02:00
Kevin Jahns
07ac1d03e3 fix jsdoc 2019-04-11 23:34:56 +02:00
Kevin Jahns
cbcf1facb8 remove todo.md 2019-04-11 17:35:09 +02:00
Kevin Jahns
31ff7ac78c improve jsdoc comments 2019-04-11 13:22:59 +02:00
Kevin Jahns
ed3b31e58f jsdoc fixes 2019-04-11 00:49:07 +02:00
Kevin Jahns
759ecb21f7 rename transaction._replacedItems to transaction._mergeStructs 2019-04-11 00:31:43 +02:00
Kevin Jahns
9c29d820c8 rename AbstractRef to AbstractStructRef 2019-04-11 00:26:42 +02:00
Kevin Jahns
2ef11a5344 splitting an item must always happen inside a transaction, because we always need to check if we can merge it back 2019-04-11 00:23:08 +02:00
Kevin Jahns
9fe47e98d5 type._map points to the last element instead to enable merging of deletes in Map 2019-04-10 21:01:59 +02:00
Kevin Jahns
654510f3ff read struct refs as array 2019-04-10 18:52:22 +02:00
Kevin Jahns
52ec698635 implement some of the commented todos 2019-04-09 04:01:37 +02:00
Kevin Jahns
1b06f59d1c fixed remaining tests 2019-04-09 00:48:24 +02:00
Kevin Jahns
12bcc4d080 fix remaining random tests 2019-04-09 00:31:28 +02:00
Kevin Jahns
e1a9f314a7 fixed part of split/merge logic 2019-04-08 13:41:28 +02:00
Kevin Jahns
7a111de186 refactor read/write of structs 2019-04-07 23:08:08 +02:00
Kevin Jahns
90b3fa9dd9 fixed merging and adapted writeStructs to write end of message 2019-04-07 12:47:04 +02:00
Kevin Jahns
c635963747 Compare origin ids in item.integrate 2019-04-06 15:55:20 +02:00
Kevin Jahns
1b17b5e400 fixed 10 tests 2019-04-06 13:00:32 +02:00
Kevin Jahns
61d9d96d15 fix replace with delete 2019-04-05 21:06:43 +02:00
Kevin Jahns
7d0c048708 Items accept origins as IDs 2019-04-05 19:46:18 +02:00
Kevin Jahns
8a7416ad50 Create Structs based on offset, if necessary
implement offset parameter in Ref.toStruct
2019-04-05 12:38:02 +02:00
Kevin Jahns
e56899a02c after refactor - some tests are working again 2019-04-05 00:37:09 +02:00
Kevin Jahns
30bf3742c9 add internals file and use it to organize imports 2019-04-04 19:35:38 +02:00
Kevin Jahns
8dbd2c4696 restructure EventHandler 2019-04-04 13:50:00 +02:00
Kevin Jahns
6578727c9c fixed all type issues 2019-04-03 13:23:27 +02:00
Kevin Jahns
92ca001cdc implement getMap, getArray, getXml, .. 2019-04-03 03:08:10 +02:00
Kevin Jahns
415de1cc4c all YArray.tests type fixes 2019-04-03 02:30:44 +02:00
Kevin Jahns
e23582b1cd more type fixes and rethinking writeStructs 2019-04-02 23:08:58 +02:00
Kevin Jahns
73c28952c2 fix all types but yxmlelement 2019-03-30 11:00:54 +01:00
Kevin Jahns
1bc1e88d6a fix y-text 2019-03-30 01:08:09 +01:00
Kevin Jahns
c188f813a4 fixed YMap 2019-03-29 13:49:13 +01:00
Kevin Jahns
ff981a8697 fixed YArray 2019-03-29 01:03:02 +01:00
Kevin Jahns
d9ab593b07 prelim refactor commit 2019-03-26 01:14:15 +01:00
Kevin Jahns
293527e62b fix a few tsc errors (96 remaining) 2019-03-13 02:15:43 +01:00
Kevin Jahns
5a42a94cf4 add typescript to lint script 2019-03-13 01:49:51 +01:00
Kevin Jahns
040808300c clean up build script - no more warnings 2019-03-13 01:16:31 +01:00
Kevin Jahns
57975d409e cleanup dependencies 2019-03-13 00:22:38 +01:00
Kevin Jahns
306b2c64f3 Merge branch 'master' of https://github.com/y-js/yjs 2019-03-13 00:04:42 +01:00
Kevin Jahns
585265e9a5 refactor and remove dependency circles 2019-03-13 00:04:19 +01:00
Kevin Jahns
777ae9503a Merge pull request #142 from mtn/mtn-patch-1
Correct typo in README example
2019-03-12 03:36:51 +01:00
Kevin Jahns
4c1798e5fa fix all remaining tests (xml tests) 2019-03-12 01:42:51 +01:00
Kevin Jahns
f4d85e2a3e fix y-text tests 2019-03-12 01:22:06 +01:00
Kevin Jahns
a0f0c9c377 testing: use lib0.testing.compare to compare Maps and sets 2019-03-11 18:34:50 +01:00
Kevin Jahns
95ec2a435a fix remaining y-map tests 2019-03-11 17:52:51 +01:00
Kevin Jahns
da9836fe59 added all y-map tests 2019-03-11 12:31:37 +01:00
Kevin Jahns
3a7411f9e8 reworked some ymap tests (a few are running again) 2019-03-11 00:00:41 +01:00
Kevin Jahns
39cee7c6e7 refix array tests and switch to lib0 2019-03-10 23:26:53 +01:00
Kevin Jahns
0a5753c191 decode items before they are decoded. fixes lots of y-array tests 2019-03-07 18:57:39 +01:00
Kevin Jahns
76b7d0b651 fixed some issues in random tests 2019-03-06 13:29:16 +01:00
Kevin Jahns
99e3e95a00 added remaining y-array tests (random still failing) 2019-03-05 14:00:31 +01:00
Kevin Jahns
93ee4ee287 converted first y-array test to funlib/testing 2019-03-04 14:28:18 +01:00
Kevin Jahns
c5cc403a29 update test commands 2019-03-01 23:45:09 +01:00
Kevin Jahns
75f4a0a5f0 restructuring the project 2019-03-01 23:28:11 +01:00
Michael Noronha
591df5c00a Correct typo in README example
/bower_components -> ./bower_components
2019-02-23 17:30:05 -06:00
Kevin Jahns
f6b4819ae3 prosemirror: implement isChangeOrigin in state 2019-01-31 09:50:52 +01:00
Kevin Jahns
d483d9cc83 13.0.0-78 2019-01-29 01:38:40 +01:00
Kevin Jahns
453407b93d fix connection status and awareness info when disconnected (ws-provider) 2019-01-29 01:38:23 +01:00
Kevin Jahns
e699f92333 13.0.0-77 2019-01-29 00:56:15 +01:00
Kevin Jahns
6ff47719ef Merge branch 'master' of github.com:y-js/yjs 2019-01-29 00:55:22 +01:00
Kevin Jahns
3a0694c35c added utilities to make and recover snapshots 2019-01-29 00:54:58 +01:00
Kevin Jahns
74e5243742 Merge pull request #138 from calibr/yjs
updating YArray's iterator to iterate Types correctly
2019-01-23 11:00:37 +01:00
calibr
dcf43b9797 switch to the next item in YArray's iterator after processing a Type item 2019-01-16 03:12:58 +03:00
Kevin Jahns
77e479c03b working on snapshotting and version history 2019-01-09 23:54:36 +01:00
Kevin Jahns
ec58a99748 add clock vector to awareness protocol 2018-12-22 15:51:09 +01:00
Kevin Jahns
f1eb66655b implemented leveldb persistence for websocket server 2018-12-22 13:45:59 +01:00
Kevin Jahns
7f4ae9fe14 implemented codemirror binding with cursor support 2018-12-21 13:51:38 +01:00
Kevin Jahns
c0ba56a21f update v13 docs 2018-12-19 01:12:29 +01:00
Kevin Jahns
4063e28b5e 13.0.0-76 2018-12-11 20:19:07 +01:00
Kevin Jahns
b6f7cd7869 fix broadcast channel communication 2018-12-11 20:18:11 +01:00
Kevin Jahns
1a79e429ed 13.0.0-75 2018-12-11 19:49:50 +01:00
Kevin Jahns
04066a5678 permission protocol + reduce circular dependencies 2018-12-11 19:49:21 +01:00
Kevin Jahns
e09ef15349 13.0.0-74 2018-12-04 18:07:04 +01:00
Kevin Jahns
3d70eee959 item: increase parent length only if parentSub=null 2018-12-03 23:09:59 +01:00
Kevin Jahns
582095e5a3 improved granularity of prosemirror binding 2018-12-03 17:09:00 +01:00
Kevin Jahns
c9ea3a412e more efficient length computing 2018-11-28 13:20:14 +01:00
Kevin Jahns
a2c51c36e9 implement generic broadcastchannel and apply it to websocket provider 2018-11-27 18:29:25 +01:00
Kevin Jahns
ab3dba5b06 add source file info to examples 2018-11-27 15:24:58 +01:00
Kevin Jahns
3ddff186c2 back to .js extension 2018-11-27 14:59:24 +01:00
Kevin Jahns
9bd199a6e7 add description to each example 2018-11-27 00:57:15 +01:00
Kevin Jahns
01d0825ae6 13.0.0-73 2018-11-26 17:14:48 +01:00
Kevin Jahns
e2f98525d2 clean examples build 2018-11-26 17:14:45 +01:00
Kevin Jahns
70a0a03130 no start content in prosemirror example 2018-11-26 16:59:01 +01:00
Kevin Jahns
656d85c62e add dom example 2018-11-26 16:06:17 +01:00
Kevin Jahns
e168dd48fb proper api endpoints for examples 2018-11-26 14:54:46 +01:00
Kevin Jahns
12d43199d5 add http listener to websocket-server 2018-11-26 13:08:23 +01:00
Kevin Jahns
539fa8b21d examples use hosted server 2018-11-26 02:13:06 +01:00
Kevin Jahns
f572f94586 port support 2018-11-25 23:41:17 +01:00
Kevin Jahns
c12d00b227 mjs nodejs support 2018-11-25 22:39:50 +01:00
Kevin Jahns
e4a5f2caec jsdoc fixes 2018-11-25 05:43:18 +01:00
Kevin Jahns
9f9f465238 update logo link 2018-11-25 04:50:23 +01:00
Kevin Jahns
8450ff86d7 make npm build ready for netlify 2018-11-25 04:41:52 +01:00
Kevin Jahns
70139262c5 add rollup-cli as dependency 2018-11-25 03:36:06 +01:00
Kevin Jahns
9c0da271eb large scale refactoring 2018-11-25 03:17:00 +01:00
Kevin Jahns
ade3e1949d update cdn destination. closes #128 2018-11-20 15:03:28 +01:00
Kevin Jahns
eec63a008f 13.0.0-72 2018-11-20 03:53:55 +01:00
Kevin Jahns
52abcdd043 fix all tests 2018-11-16 12:33:41 +01:00
Kevin Jahns
f94653424a add prosemirror tests 2018-11-14 07:20:06 +01:00
Kevin Jahns
d67a794e2c 13.0.0-71 2018-11-09 01:49:59 +01:00
Kevin Jahns
60318083a6 make websocket-server a binary and add bindings and provider to npm package 2018-11-09 01:49:43 +01:00
Kevin Jahns
7607070452 13.0.0-70 2018-11-09 01:24:06 +01:00
Kevin Jahns
28fb7b6e9c remove logging in prosemirror binding 2018-11-09 01:23:16 +01:00
Kevin Jahns
aafe15757f implemented awareness protocol and added cursor support 2018-11-09 00:13:30 +01:00
Kevin Jahns
31d6ef6296 cleanup prosemirror example 2018-11-06 15:15:27 +01:00
Kevin Jahns
32b8fac37f added prosemirror binding 2018-11-06 13:44:35 +01:00
Kevin Jahns
e8060de914 13.0.0-69 2018-11-02 01:54:53 +01:00
Kevin Jahns
22b036527c further refine build process to also include lib 2018-11-02 01:54:40 +01:00
Kevin Jahns
feb1e030d7 13.0.0-68 2018-11-02 01:52:24 +01:00
Kevin Jahns
bd271e3952 update publish process 2018-11-02 01:52:20 +01:00
Kevin Jahns
df80938190 13.0.0-67 2018-11-02 00:47:09 +01:00
Kevin Jahns
67bbc0a3fe implemented websocket provider 2018-10-30 00:51:09 +01:00
Kevin Jahns
e1ece6dc66 refactoring: removed default connector and persistence, new code style, proper jsdocs, enabled typechecking 2018-10-29 21:58:21 +01:00
Kevin Jahns
fe038822a3 Merge branch 'ydb-integration' of https://github.com/y-js/yjs into ydb-integration 2018-10-22 12:23:47 +02:00
Kevin Jahns
dece14486c start refactoring 2018-10-22 12:23:35 +02:00
Kevin Jahns
2daffbc2ca implement syncstate 2018-10-17 15:14:47 +02:00
Kevin Jahns
4c01a34d09 integrate ydb client and adapt some demos 2018-10-13 14:38:29 +02:00
Kevin Jahns
3b08267daa merge experimental-connectors 2018-10-08 17:11:18 +02:00
Kevin
b98ebddb69 ydb client 2018-10-08 16:09:50 +02:00
Kevin Jahns
9d5bf50676 13.0.0-66 2018-07-17 18:50:03 +02:00
Kevin Jahns
c0972f8158 reset selection also for local transactions 2018-07-17 18:49:28 +02:00
Kevin Jahns
548125a944 13.0.0-65 2018-07-16 18:38:09 +02:00
Kevin Jahns
a7b124ca6e 13.0.0-64 2018-07-16 18:19:36 +02:00
Kevin Jahns
4022374620 dombinding: always set browser range after change 2018-07-16 18:15:24 +02:00
Kevin Jahns
860e4d7af6 13.0.0-63 2018-06-23 00:30:45 +02:00
Kevin Jahns
6376d69b58 fix undo of map update 2018-06-23 00:29:44 +02:00
Kevin Jahns
5cf6f45f19 13.0.0-62 2018-06-13 00:08:01 +02:00
Kevin Jahns
967903673b fixed undo/redo issues and implemented ability to manually flush the UndoManager 2018-06-13 00:06:38 +02:00
Kevin Jahns
2d897f1844 add function to create lots of rooms 2018-06-07 16:36:42 +02:00
Kevin Jahns
fb2f9bc493 add client-server updateCounter support to sync all persisted rooms 2018-06-04 17:35:39 +02:00
Kevin Jahns
6f9ae0c4fc save current state at the beginning in YIndexedDB 2018-06-03 22:39:22 +02:00
Kevin Jahns
9df20fac8a added YIndexedDB 2018-06-03 21:58:51 +02:00
Kevin Jahns
a1fb1a6258 add persistence decoder 2018-06-02 22:16:12 +02:00
Kevin Jahns
417d0ef3b5 save state in FilePersistence 2018-06-02 13:33:04 +02:00
Kevin Jahns
9be256231b enable gc in demo 2018-05-23 16:15:11 +02:00
Kevin Jahns
c122bdc750 BinaryEncoder writes directly to ArrayBuffer, improves memory consumption 2018-05-23 16:06:38 +02:00
Kevin Jahns
4ef36ab81c fix tests 2018-05-23 14:16:54 +02:00
Kevin Jahns
cccc0e1015 implemented experimental websockets-connector 2018-05-23 14:01:00 +02:00
Kevin Jahns
db5312443e 13.0.0-61 2018-05-18 02:02:44 +02:00
Kevin Jahns
dbda07424b fix DomBinding destroy 2018-05-18 02:01:53 +02:00
Kevin Jahns
684d38d6c8 make compatible with webpack and less sophisticated module bundlers 2018-05-13 13:07:24 +02:00
Kevin Jahns
44fa064eb2 Merge branch 'master' of github.com:y-js/yjs 2018-05-12 16:43:28 +02:00
Kevin Jahns
9b6fffd880 Add rollup dependency - closes #110 2018-05-12 16:43:05 +02:00
Kevin Jahns
e9993b2643 Merge pull request #111 from larskarbo/patch-1
(DOCS) Remove y-array duplicates
2018-05-12 16:41:14 +02:00
Kevin Jahns
762e9e8a3a do not log _start in logItemHelper. Fixes #114 2018-05-12 15:42:31 +02:00
Kevin Jahns
6ddeb788c7 Merge branch 'master' of github.com:y-js/yjs 2018-05-09 16:28:07 +02:00
Kevin Jahns
b9245f323c prefer !== undefined check instead of hasOwnProperty 2018-05-09 16:27:55 +02:00
Kevin Jahns
c0e630b635 prefer parentElement instead of parentNode 2018-05-09 14:42:24 +02:00
Kevin Jahns
e56457a0ef 13.0.0-60 2018-05-08 13:46:27 +02:00
Kevin Jahns
ca13849828 fix domBinding infinite loop 2018-05-08 13:45:51 +02:00
Kevin Jahns
92c2fbd6d3 remove debug dependency 2018-05-07 11:52:37 +02:00
Kevin Jahns
65b8921f05 13.0.0-59 2018-05-07 11:45:39 +02:00
Kevin Jahns
1ace7f4b73 fix parentNode binding in case parent doesn't fire MO 2018-05-07 11:44:22 +02:00
Kevin Jahns
6336064516 13.0.0-58 2018-05-05 15:33:53 +02:00
Kevin Jahns
49d2e42b41 fix missing dom-binding - still investigating 2018-05-05 15:33:16 +02:00
Kevin Jahns
c098e8e745 implement scroll-fixer 2018-05-05 14:47:59 +02:00
Kevin Jahns
38558a7fad 13.0.0-57 2018-05-02 18:42:56 +02:00
Kevin Jahns
bdb3782f8f add option to disable gc (compatible with older versions) 2018-05-02 18:42:18 +02:00
Kevin Jahns
bc32f7348e 13.0.0-56 2018-04-27 18:43:24 +02:00
Kevin Jahns
09a94f053e merge with master 2018-04-27 18:39:34 +02:00
Kevin Jahns
0df0079fa3 Merge branch 'master' of github.com:y-js/yjs 2018-04-27 18:33:51 +02:00
Kevin Jahns
a54d826d6d bugfixes 2018-04-27 18:33:28 +02:00
Kevin Jahns
99f92cb9a0 fix filtering 2018-04-26 16:01:17 +02:00
Kevin Jahns
e788ad1333 fix hook binding 2018-04-26 14:07:12 +02:00
Kevin Jahns
1fe37c565e hooks port to domBinding 2018-04-26 13:26:21 +02:00
Lars Karbo
ed2273e2ed Remove y-array duplicates 2018-04-24 12:59:51 -07:00
Kevin Jahns
94933a704d correctly handle gc with UndoManager and un-merge when syncing 2018-04-23 13:25:40 +02:00
Kevin Jahns
ef6eb08335 fix most gc bugs - test suite running again 2018-04-19 18:28:25 +02:00
Kevin Jahns
d915c8dd13 prelim gc 2018-04-03 13:28:24 +02:00
Kevin Jahns
32207cbca0 implement new mark deleted / gc approach 2018-03-29 16:36:34 +02:00
Kevin Jahns
135c6d31be documentation and fix tests 2018-03-29 11:58:02 +02:00
Kevin Jahns
61149b458a less duplicate code 2018-03-23 05:22:45 +01:00
Kevin Jahns
ba97bfdd9e remove fundraiser campaign 2018-03-23 04:44:26 +01:00
Kevin Jahns
689bca8602 Merge remote-tracking branch 'origin' into v13-doc 2018-03-23 04:40:08 +01:00
Kevin Jahns
6dd43cde17 cleanup docs 2018-03-23 04:39:32 +01:00
Kevin Jahns
026675b438 separate dom binding 2018-03-23 01:55:47 +01:00
Kevin Jahns
941a22b257 13.0.0-55 2018-03-14 18:52:49 -07:00
Kevin Jahns
4aa41b98a9 fix dom filtering bug 2018-03-14 18:51:48 -07:00
Kevin Jahns
acf443aacb reworking bindings 2018-03-12 03:36:37 +01:00
Kevin Jahns
aa8c934833 Add fundraiser campaign 2018-03-06 05:27:03 +01:00
Kevin Jahns
814af5a3d7 fix import locations 2018-03-06 05:22:18 +01:00
Kevin Jahns
bbc207aaa6 restructer and move to esdoc 2018-03-06 03:17:50 +01:00
Kevin Jahns
a9b610479d big documentation update - all public functions and classes are documented now 2018-03-05 03:12:04 +01:00
Kevin Jahns
079de07eff 13.0.0-54 2018-03-01 16:45:25 +01:00
Kevin Jahns
54453e87fa fix consecutive undo,redo,undo,redo.. (abc test) 2018-03-01 16:44:26 +01:00
Kevin Jahns
1b0e3659c3 undo fixes for consecutive undo-redo 2018-03-01 13:50:01 +01:00
Kevin Jahns
dc22a79ac4 properly unregister event when binding is destroyed 2018-02-27 03:52:40 +01:00
Kevin Jahns
384a4b72b0 add quill-cursors example 2018-02-26 17:24:28 +01:00
Kevin Jahns
f35c056bde fix some tests 2018-02-26 03:23:22 +01:00
Kevin Jahns
250050e83b Merge remote-tracking branch 'origin' into y-richtext-rewrite 2018-02-26 02:19:08 +01:00
Kevin Jahns
248d08be30 implement quill binding for y-text 2018-02-26 02:18:39 +01:00
Kevin Jahns
641f426339 13.0.0-53 2018-02-25 02:31:59 +01:00
Kevin Jahns
fcbca65d8f fromBinary is a transaction 2018-02-25 02:31:20 +01:00
Kevin Jahns
5f8ae0dd43 13.0.0-52 2018-02-18 19:20:00 +01:00
Kevin Jahns
de14fe0f3e fix getAttribute vs attributes.value fixes y-js/y-xml#8 2018-02-18 18:58:49 +01:00
Kevin Jahns
5e4b071693 actually use clock in undo-manager 2018-02-15 18:58:43 +01:00
Kevin Jahns
937de2c59f fix fast undo-redo bug 2018-02-15 18:28:53 +01:00
Kevin Jahns
f1f1bff901 preliminary undo-redo fixes 2018-02-15 17:58:14 +01:00
Kevin Jahns
da748a78f4 start rewriting y-richtext 2018-02-15 01:25:08 +01:00
Kevin Jahns
4855b2d590 13.0.0-51 2018-02-07 14:08:43 +01:00
Kevin Jahns
908ce31e2f Merge branch 'master' of github.com:y-js/yjs 2018-02-07 14:08:07 +01:00
Kevin Jahns
e4d4c23f0b bugfix - persist deletes when syncing 2018-02-07 14:07:57 +01:00
Kevin Jahns
fc500a8247 Merge pull request #94 from LukasDrgon/patch-3
Add CDN usage
2018-01-31 20:36:26 -08:00
Kevin Jahns
4b84541d76 13.0.0-50 2018-01-30 20:12:58 -08:00
Kevin Jahns
a3ab42c157 implemnt mutual exclude pattern directly in Persistence.js 2018-01-30 20:11:59 -08:00
Kevin Jahns
bbd3317d62 13.0.0-49 2018-01-30 15:53:33 -08:00
Kevin Jahns
5d3922cb64 fix undo-redo 2018-01-30 15:52:36 -08:00
Kevin Jahns
a81a2cd553 13.0.0-48 2018-01-29 16:41:52 -08:00
Kevin Jahns
c0d24bdba4 lint 2018-01-29 16:41:27 -08:00
Kevin Jahns
40e913e9c5 add toBinary and fromBinary to Y.utils 2018-01-29 16:39:09 -08:00
Kevin Jahns
94f6a0fd9c implement Y.*Binding approach 2018-01-29 11:55:28 -08:00
Kevin Jahns
41a88dbc43 fix examples for Yjs@13 2018-01-25 17:28:33 -07:00
Kevin Jahns
1d4f283955 13.0.0-47 2018-01-18 18:44:56 +01:00
Kevin Jahns
fc3a4c376c implement when-handler 2018-01-18 18:44:20 +01:00
Lukas Drgon
acb0affa33 Add CDN usage 2018-01-17 22:03:49 +01:00
Kevin Jahns
0b510b64a3 persistence updates + make Persistence.init async 2018-01-16 16:13:47 +01:00
Kevin Jahns
c8f0cf5556 13.0.0-46 2018-01-10 00:20:03 +01:00
Kevin Jahns
11a4271fd1 13.0.0-45 2018-01-10 00:18:50 +01:00
Kevin Jahns
c7670915c7 Merge branch 'master' of github.com:y-js/yjs 2018-01-10 00:17:34 +01:00
Kevin Jahns
eb2d596538 implement mutualExclude factory 2018-01-10 00:17:26 +01:00
Kevin Jahns
48e17ea1a7 13.0.0-44 2018-01-10 00:16:33 +01:00
Kevin Jahns
1a22fdd45e persistence improvements 2018-01-10 00:11:25 +01:00
Kevin Jahns
07cf0b3436 export AbstractPersistence 2018-01-08 17:30:30 +01:00
Kevin Jahns
5a68b9f4ad loaded event when loaded from persistence adapter 2018-01-08 02:28:46 +01:00
Kevin Jahns
445dd3e0da fix several y-xml bugs 2018-01-03 03:50:27 +01:00
Kevin Jahns
0ba97d78f8 better relative cursor positions for text editing - decrease number of generated messages for cursor 2017-12-31 16:14:02 +01:00
Kevin Jahns
fc5be5c7cc fix empty string insertion bug 2017-12-31 14:49:20 +01:00
Kevin Jahns
f2debc150c reimplement persistence approach 2017-12-24 03:18:00 +01:00
Kevin Jahns
08f37a86e3 13.0.0-43 2017-12-21 16:06:29 +01:00
Kevin Jahns
f5d17e6236 filter y-xml when domFilter is set 2017-12-21 16:05:50 +01:00
Kevin Jahns
8f3bd7170a 13.0.0-42 2017-12-19 17:39:01 +01:00
Kevin Jahns
5586334549 fix initial content in y-array 2017-12-19 17:37:04 +01:00
Kevin Jahns
24c1e4dcc8 13.0.0-41 2017-12-14 14:30:02 +01:00
Kevin Jahns
d61bbecf4e fix tree walker on YXmlFragment 2017-12-14 14:29:16 +01:00
Kevin Jahns
85492ad2e0 fix drawing example. Add drawing hook for y-xml 2017-12-13 12:49:34 +01:00
Kevin Jahns
02253f9a8d fix log outputs 2017-12-13 10:28:19 +01:00
Kevin Jahns
8105bef1af work on drawing demo 2017-12-06 19:20:52 -08:00
Kevin Jahns
4efa16e2dd 13.0.0-40 2017-12-05 21:50:34 -08:00
Kevin Jahns
ad44f59def implement new dom update algorithm 2017-12-05 21:50:00 -08:00
Kevin Jahns
9c471ea24d 13.0.0-39 2017-12-05 17:06:01 -08:00
Kevin Jahns
d9e76014f5 fix remaining cursor relocation issues 2017-12-05 17:05:12 -08:00
Kevin Jahns
4091b7d004 13.0.0-38 2017-12-05 00:53:25 -08:00
Kevin Jahns
dfc183643d support data-yjs-hook attribute for yjs hooks 2017-12-05 00:52:52 -08:00
Kevin Jahns
cf8698f2b6 13.0.0-37 2017-12-02 01:45:55 -08:00
Kevin Jahns
3595f14da7 fix insert in y-text 2017-12-02 01:45:22 -08:00
Kevin Jahns
c6e671b1d5 13.0.0-36 2017-11-30 18:39:17 -08:00
Kevin Jahns
e4c10fd6b3 handle xmlhook in mutation observer 2017-11-30 18:38:12 -08:00
Kevin Jahns
e70aa09f88 Implement YXml element hooks (based on _yjsHook property) 2017-11-29 17:16:06 -08:00
Kevin Jahns
7808b143da 13.0.0-35 2017-11-28 17:38:31 -08:00
Kevin Jahns
b35092928e fix user selection issues 2017-11-28 17:37:15 -08:00
Kevin Jahns
b7dbcf69d3 13.0.0-34 2017-11-26 23:24:41 -08:00
Kevin Jahns
377df18788 prevent updating cursor position if not necessary 2017-11-26 23:23:29 -08:00
Kevin Jahns
26a323733d 13.0.0-33 2017-11-26 14:42:59 -08:00
Kevin Jahns
d0d1015074 filter remote changes in YXml* 2017-11-26 14:42:06 -08:00
Kevin Jahns
2e3240b379 13.0.0-32 2017-11-14 21:31:11 -08:00
Kevin Jahns
2558652356 fix attribute filter (it used to filter everything) 2017-11-14 21:19:39 -08:00
Kevin Jahns
783cbd63fc 13.0.0-31 2017-11-14 20:44:12 -08:00
Kevin Jahns
41be80e751 fix y-xml server environment 2017-11-14 20:43:30 -08:00
Kevin Jahns
3d6050d8a2 13.0.0-30 2017-11-12 13:37:37 -08:00
Kevin Jahns
3d5ba7b4cc fix the case that a new transaction starts in an event listener (afterTransaction, observe, observeDeep) 2017-11-12 13:37:06 -08:00
Kevin Jahns
415b66607c fixed filtering 2017-11-10 19:04:00 -08:00
Kevin Jahns
05cd1d0575 13.0.0-29 2017-11-10 18:46:10 -08:00
Kevin Jahns
4edc22bedb remove prematurely commited dom-filter update 2017-11-10 18:45:41 -08:00
Kevin Jahns
16f84c67d5 13.0.0-28 2017-11-10 18:41:39 -08:00
Kevin Jahns
290d3c8ffe support undefined as an attribute value 2017-11-10 18:41:10 -08:00
Kevin Jahns
c51e8b46c2 13.0.0-27 2017-11-10 12:55:05 -08:00
Kevin Jahns
0cda1630d2 fix path bugs 2017-11-10 12:54:33 -08:00
Kevin Jahns
d232b883e9 13.0.0-26 2017-11-09 17:32:45 -08:00
Kevin Jahns
3a0e65403f fix undo scope 2017-11-09 17:31:58 -08:00
Kevin Jahns
224fff93ba 13.0.0-25 2017-11-08 17:31:50 -08:00
Kevin Jahns
4f55e8c655 fix event.path by using event.currentTarget 2017-11-08 17:31:12 -08:00
Kevin Jahns
a08624c04e implemented tree-walker 2017-11-08 13:40:36 -08:00
Kevin Jahns
9b00929172 13.0.0-24 2017-11-08 00:02:24 -08:00
Kevin Jahns
b94267e14a prevent mangling of Types and other useful data 2017-11-08 00:01:18 -08:00
Kevin Jahns
e696304845 13.0.0-23 2017-11-07 22:52:33 -08:00
Kevin Jahns
d503c9d640 lint 2017-11-07 22:51:46 -08:00
Kevin Jahns
e5f289506f observeDeep receives array of events 2017-11-07 22:44:43 -08:00
Kevin Jahns
c453593ee7 y-xml: hand over fake document if necessary 2017-11-07 21:06:29 -08:00
Kevin Jahns
5ed1818de5 fix selection RootID name 2017-11-07 20:47:00 -08:00
Kevin Jahns
0310500c4e add YEvent 2017-11-07 20:34:44 -08:00
Kevin Jahns
b7defc32e8 fix selecting multiple paragraphs 2017-11-07 20:28:31 -08:00
Kevin Jahns
dbdd49af23 fix relativePosition if startof is a root type 2017-11-07 20:10:01 -08:00
Kevin Jahns
b7c05ba133 fix YMap.keys() 2017-11-07 19:56:03 -08:00
Kevin Jahns
9298903bdb filter out attribute events for YXmlFragment 2017-11-07 19:31:57 -08:00
Kevin Jahns
d59e30b239 implement generic YEvent with path property 2017-11-07 19:18:41 -08:00
Kevin Jahns
d29b83a457 improve backwards-compatibility 2017-11-07 18:31:04 -08:00
Kevin Jahns
0208d83f91 implemented undo 🙌 2017-10-30 11:47:56 +01:00
Kevin Jahns
c545118637 fixed selection handler befor/after transactions 2017-10-28 23:02:48 +02:00
Kevin Jahns
c619aa33d9 fixed inserting large xml portions 2017-10-27 22:28:42 +02:00
Kevin Jahns
1dea8f394f unbind yxml immediately when deleted 2017-10-26 21:02:25 +02:00
Kevin Jahns
5cf8d20cf6 delete child if parent is deleted 2017-10-26 20:53:27 +02:00
Kevin Jahns
74f9ceab01 fixed dom filtering tests 2017-10-26 20:24:09 +02:00
Kevin Jahns
ca81cdf3be fixed xml tests 2017-10-26 19:50:43 +02:00
Kevin Jahns
96c6aa2751 fixed ds syncing bug 2017-10-26 19:12:33 +02:00
Kevin Jahns
e6b5e258fb several DS fixes (logging works now) 2017-10-26 16:22:35 +02:00
Kevin Jahns
e8170a09a7 fixed all insert tests 2017-10-26 14:40:21 +02:00
Kevin Jahns
9d1ad8cb28 fix item-splitting 2017-10-24 16:41:19 +02:00
Kevin Jahns
d859fd68fe fixed several random tests 2017-10-23 22:43:41 +02:00
Kevin Jahns
2b7d2ed1e6 implemented logTable method on data stores 2017-10-22 23:50:49 +02:00
Kevin Jahns
142a5ada60 fix some tests, implement event classes for types, and re-implement logging 2017-10-22 19:13:12 +02:00
Kevin Jahns
c92f987496 fix some tests, implement event classes for types, and re-implement logging 2017-10-22 19:12:50 +02:00
Kevin Jahns
755c9eb16e implemented xml type for new event system 2017-10-19 17:36:28 +02:00
Kevin Jahns
1311c7a0d8 fix first y-array test 2017-10-16 04:53:12 +02:00
Kevin Jahns
4eec8ecdd3 fix encoding and rb tree tests 2017-10-15 12:21:14 +02:00
Kevin Jahns
0e426f8928 fix compiling issues 2017-10-14 23:03:24 +02:00
Kevin Jahns
82015d5a37 refactor the whole damn thing 2017-10-11 03:41:54 +02:00
Kevin Jahns
d9ee67d2f3 13.0.0-22 2017-10-07 00:42:06 +02:00
Kevin Jahns
791f6c12f0 add indexeddb example 2017-10-07 00:40:34 +02:00
Kevin Jahns
23d019c244 add writeObjectToYMap and writeArrayToYArray helper utilities 2017-10-07 00:39:26 +02:00
Kevin Jahns
c8ca80d15f 13.0.0-21 2017-10-02 15:52:11 +02:00
Kevin Jahns
be282c8338 fix lint 2017-10-02 15:50:56 +02:00
Kevin Jahns
829a094c6d check for responsiveness when maxBufferSize is set 2017-10-02 15:45:23 +02:00
Kevin Jahns
725273167e 13.0.0-20 2017-09-29 22:34:18 +02:00
Kevin Jahns
581264c5e3 implement relative position helper 2017-09-29 22:33:28 +02:00
Kevin Jahns
be537c9f8c 13.0.0-19 2017-09-26 21:53:01 +02:00
Kevin Jahns
4028eee39d implemented chunked broadcast of updates 2017-09-26 21:52:07 +02:00
Kevin Jahns
0e3e561ec7 13.0.0-18 2017-09-20 11:34:03 +02:00
Kevin Jahns
7df46cb731 Merge branch 'master' of github.com:y-js/yjs 2017-09-20 11:30:24 +02:00
Kevin Jahns
40fb16ef32 catch y-* related errors 2017-09-20 11:29:13 +02:00
Kevin Jahns
ada5d36cd5 add more y-xml tests 2017-09-19 03:16:48 +02:00
Kevin Jahns
f537a43e29 implement tests for dom filter 2017-09-18 22:14:45 +02:00
Kevin Jahns
3a305fb228 13.0.0-17 2017-09-11 17:38:21 +02:00
Kevin Jahns
1afdab376d fix linting 2017-09-11 17:37:39 +02:00
Kevin Jahns
526c862071 added test case for moving nodes 2017-09-11 17:35:20 +02:00
Kevin Jahns
fdbb558ce2 persistence db fixes 2017-09-11 16:02:19 +02:00
Kevin Jahns
76ad58bb59 fix example dist script 2017-09-07 23:02:19 +02:00
Kevin Jahns
c88a813bb0 fix tests by removing y-memory include 2017-09-06 20:52:52 +02:00
Kevin Jahns
ccf6d86c98 removed generators 2017-09-06 20:10:38 +02:00
Kevin Jahns
6b5c02f1ce 13.0.0-16 2017-08-26 01:11:31 +02:00
Kevin Jahns
2be6e935a4 fix lint in xml tests 2017-08-26 01:10:50 +02:00
Kevin Jahns
0ddf3bf742 Y.Xml renamed to Y.XmlElement 2017-08-25 20:35:17 +02:00
Kevin Jahns
5f29724578 merge textarea example 2017-08-24 14:46:16 +02:00
Kevin Jahns
ab6cde07e6 Implemented Xml Struct 2017-08-24 14:44:40 +02:00
Kevin Jahns
0455eaa8ad 13.0.0-15 2017-08-14 15:53:54 +02:00
Kevin Jahns
9ed7e15d0f 13.0.0-14 2017-08-14 15:49:15 +02:00
Kevin Jahns
6e633d0bd9 lint 2017-08-14 15:41:37 +02:00
Kevin Jahns
e16195cb54 implement timeout for creating Yjs instance 2017-08-14 15:39:17 +02:00
Kevin Jahns
86c46cf0ec 13.0.0-13 2017-08-13 01:04:37 +02:00
Kevin Jahns
8770c8e934 Implement persistence layer 2017-08-13 01:03:54 +02:00
Kevin Jahns
7e12ea2db5 move array tests and map tests to yjs 2017-08-09 02:21:17 +02:00
Kevin Jahns
3ca260e0da 13.0.0-12 2017-08-04 18:07:44 +02:00
Kevin Jahns
edb5e4f719 send sync step 1 after sync step 2 is processed (for slaves) 2017-08-04 18:06:36 +02:00
Kevin Jahns
be3b8b65ce 13.0.0-11 2017-08-04 16:30:58 +02:00
Kevin Jahns
d093ef56c8 userJoined accepts auth parameter. Sync with all users at once, instead of one at a time 2017-08-04 16:27:07 +02:00
Kevin Jahns
90b2a895b8 13.0.0-10 2017-08-03 00:25:13 +02:00
Kevin Jahns
4f57c91b82 fix syncing protocol - compute messages after auth 2017-08-03 00:24:01 +02:00
Kevin Jahns
3e1d89253f fix unhandled message bug in connector 2017-08-01 17:49:37 +02:00
Kevin Jahns
03e1a3fc12 13.0.0-9 2017-08-01 16:21:38 +02:00
Kevin Jahns
5c33f41c30 fix linting 2017-08-01 16:19:25 +02:00
Kevin Jahns
65e8c29b33 remove all async-functions - making it compatible with node 6 2017-08-01 16:15:36 +02:00
Kevin Jahns
fed77d532f 13.0.0-8 2017-07-31 16:05:30 +02:00
Kevin Jahns
d129184f7b fix linting 2017-07-31 15:43:04 +02:00
Kevin Jahns
a05bb1d4f9 merge bugfix-unable-to-deliver-message branch 2017-07-31 15:40:25 +02:00
Kevin Jahns
65af4963e6 merge bugfix-multiple-clients-sync branch 2017-07-31 15:35:27 +02:00
Kevin Jahns
4dce0816a6 fix preferUntransformed sync 2017-07-31 14:41:40 +02:00
Kevin Jahns
5384bf4faf remove unneccesarry whenTransactionsFinished command 2017-07-31 14:01:34 +02:00
Kevin Jahns
454ac9ba16 remove ds.length == 0 condition for preferUntransformed 2017-07-31 03:19:47 +02:00
Kevin Jahns
e2ec53be65 implemented three-way sync for master-slave apps 2017-07-31 02:06:07 +02:00
Kevin Jahns
aa6edcfd9b add warning for message type when ArrayBuffer is expected 2017-07-31 01:13:52 +02:00
Kevin Jahns
f31ec9a8b8 fixed varUint encoding issue 2017-07-30 22:16:59 +02:00
Kevin Jahns
003fa735a0 enable y-map tests 2017-07-27 15:15:20 +02:00
Kevin Jahns
574f0c3269 fix logging message type 2017-07-27 14:49:36 +02:00
Kevin Jahns
eb4fb3a225 binary encoding bugfixes & export BinaryEncoder + BinaryDecoder 2017-07-24 15:37:04 +02:00
Kevin Jahns
c97130abc4 implement generateUserId for node & clients that dont support crypto 2017-07-22 18:37:48 +02:00
Kevin Jahns
a19cfa1465 redesigned connector protocol - enabled binary compression 2017-07-22 18:07:56 +02:00
Kevin Jahns
bb45abbb70 13.0.0-7 2017-07-22 01:16:50 +02:00
Kevin Jahns
67b47fd868 bugfix - sync step 2 also authenticates) 2017-07-22 01:15:13 +02:00
Kevin Jahns
2c18b9ffad 13.0.0-6 2017-07-21 23:56:13 +02:00
Kevin Jahns
a6b7d76544 bugfix: unable to deliver message. fixes receiving message before authentication 2017-07-21 23:55:11 +02:00
Kevin Jahns
442ea7ec70 13.0.0-5 2017-07-19 21:22:37 +02:00
Kevin Jahns
747da52c0b fix two clients syncing at the time 2017-07-19 21:19:41 +02:00
Kevin Jahns
6c37bd4463 Merge remote-tracking branch 'origin/master' into v13 2017-07-13 20:03:29 +02:00
Kevin Jahns
dd6c196135 link to the IPFS connector 2017-07-13 19:51:29 +02:00
Kevin Jahns
252bec0ad2 implemented binary encoding for all basic structs 2017-07-13 17:42:21 +02:00
Kevin Jahns
6c8876d282 remove option forwardToSyncing clients as it is no longer necessary - it was previously only used by y-webrtc 2017-07-13 00:48:14 +02:00
Kevin Jahns
3c317828d1 Use integer as userId instead of String 2017-07-13 00:37:35 +02:00
Kevin Jahns
cd3f4a72d6 13.0.0-4 2017-07-06 15:17:23 +02:00
Kevin Jahns
2c852c85c6 add node build 2017-07-06 15:16:13 +02:00
Kevin Jahns
434ec84837 13.0.0-3 2017-07-06 03:29:09 +02:00
Kevin Jahns
2b618cd83c change to correct main file 2017-07-06 03:28:06 +02:00
Kevin Jahns
f4327529b9 13.0.0-2 2017-07-05 18:41:26 +02:00
Kevin Jahns
67189f4d44 dont lint in postversion 2017-07-05 18:40:41 +02:00
Kevin Jahns
6225fb4dfd fix linting of examples 2017-07-05 18:33:16 +02:00
Kevin Jahns
a7550fe5d3 13.0.0-1 2017-07-05 18:12:35 +02:00
Kevin Jahns
9d9c84f40e fit y-memory in helper.js 2017-07-05 18:10:24 +02:00
Kevin Jahns
ae91902de3 Yjs throws "error" event in unexpected cases. fixes #72 2017-07-05 17:58:19 +02:00
Kevin Jahns
033d24eee7 use y-memory@v8 directory structure 2017-07-05 17:44:17 +02:00
Kevin Jahns
8abef69aa7 implemented named event handler 2017-07-05 17:01:21 +02:00
Kevin Jahns
7e4dedab38 always use generateUniqueUserId. fixes #74 2017-07-05 11:40:19 +02:00
Kevin Jahns
85e488bbe6 Throw proper error stack when observer function thrown an error - implements #75 2017-07-05 11:37:22 +02:00
Kevin Jahns
a6a321da10 fix textarea example to fit new directory structure 2017-07-05 11:34:28 +02:00
Kevin Jahns
008764ccdc remove dist submodule 2017-07-05 11:26:20 +02:00
Kevin Jahns
de5f4abe32 filter deleted ops only if gc is disabled 2017-07-04 04:59:07 -07:00
Kevin Jahns
382d06f6d4 reworked getOperations (decrease size of sent operations, fixe some gc issues). garbageCollectOperation now sets origin to the direct left operation, if possible 2017-07-03 23:19:11 -07:00
Kevin Jahns
66de422749 fix issues with new master-slave tests 2017-06-30 15:18:07 -07:00
Kevin Jahns
bbf5e39408 implemented client-server model (untested) 2017-06-30 14:07:14 -07:00
Kevin Jahns
c8bca15d72 13.0.0-0 2017-06-30 09:16:58 -07:00
Kevin Jahns
a64730e651 fix several sync issues. improve performance a bit by removing ds from first sync step 2017-06-29 15:04:36 -07:00
Kevin Jahns
409a9414f1 fix the "gc state" warning 2017-06-27 02:07:03 +02:00
Kevin Jahns
24facaab09 fix os comparison in compareUsers 2017-06-21 16:29:51 +02:00
Kevin Jahns
060549f2cb enable gc in random tests 2017-06-19 21:16:42 +02:00
Kevin Jahns
dfe3b0b1d1 Merge branch 'master' into v13 2017-06-19 10:48:16 +02:00
Kevin Jahns
a5506a5ded Release 12.3.1 2017-06-17 14:32:28 +02:00
Kevin Jahns
361d4a48e1 implement getPath for any type. types now initialize _parent. fix some cases where observeDeep is not fired 2017-06-17 14:31:00 +02:00
Kevin Jahns
e23154bec2 update dependencies 2017-06-16 01:04:58 +02:00
Kevin Jahns
1682d43c26 added chancejs to dependencies 2017-06-07 21:43:52 +02:00
Kevin Jahns
68c417fe6f fix gc timeout 2017-05-24 16:34:57 +02:00
Kevin Jahns
2ea163a5cf outsourced helper and test-connector 2017-05-22 13:57:16 +02:00
Kevin Jahns
020dacdad4 removed some unnecessary setTimeouts 2017-05-21 00:31:16 +02:00
Kevin Jahns
42abcc897c added examples 2017-05-19 02:22:00 +02:00
Kevin Jahns
0a321610aa use rollup for yjs 2017-05-16 18:35:30 +02:00
Kevin Jahns
edf47d3491 add disconnect / reconnect return values 2017-05-12 04:19:14 +02:00
Kevin Jahns
14ee42cad5 Release 12.3.0 2017-05-08 12:40:31 +02:00
Kevin Jahns
f990927d3e implemented Y.utils.bubbleEvent 2017-05-08 12:01:51 +02:00
Kevin Jahns
a1cef4662f Release 12.2.1 2017-05-03 18:00:59 +02:00
Kevin Jahns
2c343970c4 fixed sync issue with "preferUntransformed" 2017-05-03 18:00:20 +02:00
Kevin Jahns
74b41e03e3 Release 12.2.0 2017-05-03 16:17:13 +02:00
Kevin Jahns
b242aab955 implemented "preferUntransformed" 2017-05-03 16:13:52 +02:00
Kevin Jahns
8e4efd9bba fixed "waiting for auth" issue in the test suite 2017-04-28 11:21:26 +02:00
Kevin Jahns
47d5899058 fixed test suite. It stopped working because of previous adoptions on the gc behavior. I failed to notice it.. 2017-04-27 14:58:08 +02:00
Kevin Jahns
a126a29876 Added Monaco editor example 2017-04-18 18:24:03 +02:00
Kevin Jahns
4aa720116f add serviceworker example 2017-04-11 16:21:00 +02:00
Kevin Jahns
e29162c3fc Release 12.1.7 2017-04-10 11:22:34 +02:00
Kevin Jahns
aa40855953 Add debug package as dependency. Fixes #66 2017-04-10 10:30:28 +02:00
Kevin Jahns
b6545d62fc update CodeMirror example 2017-03-29 13:43:26 +02:00
Kevin Jahns
3425d95507 update CodeMirror example 2017-03-29 13:23:28 +02:00
Kevin Jahns
53682c17fb added CodeMirror example 2017-03-29 13:21:30 +02:00
Kevin Jahns
a492a83f0c Release 12.1.6 2017-03-20 19:16:06 +01:00
Kevin Jahns
d340e557c1 removed the Changelog from README.md. Instead use the github releases to describe the changes 2017-03-20 19:13:08 +01:00
Kevin Jahns
d5cd9d94d5 implemented logging with the debug logging utility (read the updated docs) 2017-03-20 19:11:59 +01:00
Kevin Jahns
e1a160b894 Release 12.1.5 2017-03-06 13:27:32 +01:00
Kevin Jahns
f996ac83d2 added option *generateUserId* in abstract connector 2017-03-06 13:27:04 +01:00
Kevin Jahns
922637930f Release 12.1.4 2016-12-19 10:42:05 +01:00
Kevin Jahns
ff7e9cdef2 var/let declaration - fixes #61 2016-12-19 10:41:15 +01:00
Kevin Jahns
f02641deb7 Added Yjs Polymer element to module section 2016-12-01 11:15:39 +01:00
Kevin Jahns
f97144356c Release 12.1.3 2016-11-22 13:12:36 +01:00
Kevin Jahns
a9fdd5df66 updated webworker example 2016-11-21 16:29:03 +01:00
Kevin Jahns
e90f241ae0 implemented resetAuth 2016-11-21 01:22:15 +01:00
Kevin Jahns
102bef4f92 added sw example 2016-11-14 16:29:58 +01:00
Kevin Jahns
96e9c3c166 enable empty share property 2016-11-14 16:27:08 +01:00
Kevin Jahns
1080f83990 implemented moduleName, made window statements failsafe 2016-11-14 13:01:41 +01:00
Kevin Jahns
66b6b2a568 added banner to distribution files. Fixes #56 2016-11-14 10:30:10 +01:00
Kevin Jahns
7415f27fbc Release 12.1.2 2016-11-10 17:01:58 +01:00
Kevin Jahns
c9d1f34864 reconnect only when disconnected (and reverse) 2016-11-10 12:54:01 +01:00
Kevin Jahns
34997f940b Release 12.1.1 2016-11-09 14:26:37 +01:00
Kevin Jahns
4e9e21e75e lint 2016-11-09 14:19:13 +01:00
Kevin Jahns
6c375a37c8 update dist dir 2016-11-09 14:18:06 +01:00
Kevin Jahns
cd0cddaf35 fixed several sync issues 2016-11-04 11:42:50 +01:00
Kevin Jahns
93c23ddc09 fixed some gc issues (unfinished) 2016-11-03 16:28:48 +01:00
Kevin Jahns
480dfdfb77 dont modify gc buffer if gcTimeout > 0 or disconnected 2016-11-02 20:55:24 +01:00
Kevin Jahns
dda2a1ef82 another take on sourcedir: null specifies not to load anything automatically 2016-10-31 02:58:21 +01:00
Kevin Jahns
f32ff1b613 fixed several consistency issues with y-indexeddb. Implemented support for .close() - a soft replacement for .destroy() 2016-10-31 01:17:24 +01:00
Kevin Jahns
8ab16f4ada Release 12.1.0 2016-10-29 21:45:38 +02:00
Kevin Jahns
3fdcf82bcc fixed error description for observers that throw errors 2016-10-29 16:55:16 +02:00
Kevin Jahns
6dd33f4f90 copy db, connector, and type properties before handing them over to the connector, etc. This prevents some weird errors using y-elements 2016-10-24 23:10:44 +02:00
Kevin Jahns
0521fac8d8 implemented auth utilities for yjs 2016-10-24 11:57:59 +02:00
Kevin Jahns
666ab8285c update examples 2016-10-13 17:29:24 +02:00
Kevin Jahns
675c7f6638 rephrase intro 2016-10-13 15:53:41 +02:00
Kevin Jahns
463608cb5c Release 12.0.4 2016-10-12 15:52:04 +02:00
Kevin Jahns
d1059b5d04 local sourceDir causes troubles if modules are required by a yjs instance 2016-10-12 15:51:42 +02:00
Kevin Jahns
8b24284e25 Release 12.0.3 2016-10-07 21:00:46 +02:00
Kevin Jahns
08bcdfb008 implemented es6 import utility function (Y.extend(module) === module(Y)) 2016-10-07 21:00:17 +02:00
Kevin Jahns
f93d7b1e70 Release 12.0.2 2016-10-05 01:00:00 +02:00
Kevin Jahns
4d024883bc Fixed "[0] of undefined" server issue 2016-10-04 16:12:24 +02:00
Kevin Jahns
ecd412c6f6 sourcedir is no longer a global variable 2016-10-04 14:54:52 +02:00
Kevin Jahns
b939cdd086 Release 12.0.1 2016-09-27 16:17:33 +02:00
Kevin Jahns
17803266d4 repairChecker: Yjs is now able to detect incorrect states that happen when messages get lost. When Yjs is in an incorrect state it repairs itself and syncs again 2016-09-27 16:12:35 +02:00
Kevin Jahns
f0e88d192c Release 12.0.0 2016-09-20 19:19:18 +02:00
Kevin Jahns
e66c0f8a4e update doc 2016-09-20 19:13:09 +02:00
Kevin Jahns
eba3d590cc update doc 2016-09-20 19:12:41 +02:00
Kevin Jahns
0b31e63b82 update dist 2016-09-20 18:30:52 +02:00
Kevin Jahns
d22fbca6cc ready to publish (lint) 2016-09-01 02:40:35 +02:00
Kevin Jahns
330434ee24 update npm example 2016-08-31 02:58:28 +02:00
Kevin Jahns
2f0216bf89 update help 2016-08-30 21:03:08 +02:00
Kevin Jahns
f9d0625bd2 simplify example 2016-08-30 20:58:17 +02:00
Kevin Jahns
7a9d60770a simplify documentation 2016-08-30 20:43:05 +02:00
Kevin Jahns
059f72ffe1 fix init problems with v12, update examples to be synchronous 2016-08-30 18:10:19 +02:00
Kevin Jahns
d2d74a64ab update dist & added note to changelog 2016-08-29 17:56:39 +02:00
Kevin Jahns
a1f0140069 updated documentation (changelog for v12, added leveldb, consistency) 2016-08-29 17:21:30 +02:00
Kevin Jahns
7bd8e81342 fix test verifier 2016-08-28 22:13:07 +02:00
Kevin Jahns
34f365cd8f implemented support for synchronous type creation 2016-08-26 13:53:31 +02:00
Kevin Jahns
b3ba8e7546 Merge branch 'master' of github.com:y-js/yjs into v12 2016-08-25 04:43:43 +02:00
Kevin Jahns
e1e94bcf5d made createType synchronous 2016-08-25 04:42:23 +02:00
Kevin Jahns
4a83ff8514 updated quill example 2016-08-23 11:13:09 +02:00
Kevin Jahns
4078020afd Release 11.2.5 2016-08-22 12:15:32 +02:00
Kevin Jahns
e31d5e0e1d fixed late-join sync issue & use leveldb only in node environment 2016-08-22 12:05:39 +02:00
Kevin Jahns
acbc884eb5 Release 11.2.4 2016-08-04 19:25:36 +02:00
Kevin Jahns
f9315288d0 found another bug that only happens in persistent databases (operationAdded changes an operation, so it needs to be retrieved again after calling it) 2016-08-01 20:48:55 +02:00
Kevin Jahns
3b0d0343f4 Release 11.2.2 2016-08-01 17:04:06 +02:00
Kevin Jahns
74c881bb5b Fixed bug that only occurs in persistent databases (setOperation was not called). 2016-08-01 16:52:02 +02:00
Kevin Jahns
63f8a891be update dist 2016-07-05 16:21:19 +02:00
Kevin Jahns
2083cdb6b0 several bug-fixes (for y-richtext beta) 2016-07-01 17:45:53 +02:00
Kevin Jahns
2091392031 implemented random seed generator, in order to reproduce tests 2016-06-29 17:48:56 +02:00
Kevin Jahns
3dc67e075b debugged some of the special cases of Utils.receivedOp & found some bugs 2016-06-28 16:23:02 +02:00
Kevin Jahns
81e72126ce implemented new extention for awaitOps. It fixes several consistency issues (they were previously hard to detect), and it is also pretty efficient. It still has some debugger statements, so enjoy with care 2016-06-14 21:27:42 +02:00
Kevin Jahns
e77a753708 fixed bug in array & richtext 2016-06-03 19:04:47 +02:00
Kevin Jahns
bc856a09f5 Release 11.2.1 2016-05-24 15:25:14 +02:00
Kevin Jahns
f7ae62a906 Release 11.2.0 2016-05-10 18:13:01 +02:00
Kevin Jahns
6669be104e Release 11.1.0 2016-05-07 13:20:43 +01:00
Kevin Jahns
14d59de2bd improved awaitOps 2016-05-07 13:16:48 +01:00
Kevin Jahns
483d2c78aa Use uglify-js v2.5.0 to fix #52 2016-05-06 12:57:07 +01:00
Kevin Jahns
5b835563c8 implement new method awaitedOps in favor of awaitedDeletes/awaitedInserts. This will fix some bugs when the type gets out of sync with the state of yjs 2016-05-06 12:53:10 +01:00
Kevin Jahns
996566419c some flush updates 2016-05-04 17:14:39 +02:00
Kevin Jahns
5d6a9872e2 Michael found a weird character in my code 2016-05-02 11:04:24 +02:00
Kevin Jahns
8930865a21 Release 11.0.4 2016-05-02 11:00:07 +02:00
Kevin Jahns
2897695680 update dist, more args for SmallLookupBuffer 2016-04-30 23:01:36 +01:00
Kevin Jahns
5118f02b49 Release 11.0.3 2016-04-27 12:04:39 +01:00
Kevin Jahns
a10933beef fixed awaitedInserts (critical for y-richtext) 2016-04-27 12:02:39 +01:00
Kevin Jahns
c2ffe0b697 Release 11.0.2 2016-04-26 21:28:21 +02:00
Kevin Jahns
2d1a7b067b fixed some minor problems related to compareId 2016-04-26 21:27:08 +02:00
Kevin Jahns
2675f0277c Release 11.0.1 2016-04-26 15:34:19 +02:00
Kevin Jahns
918bc334b2 Release 11.0.0 2016-04-26 15:02:44 +02:00
Kevin Jahns
accf0dbafb fixed bump script 2016-04-26 15:01:22 +02:00
Kevin Jahns
6b8ce0ab4f changed the observe functionality 2016-04-25 16:32:45 +02:00
Kevin Jahns
71bf6438e1 found some gc bugs that occur when using deletion lengths 2016-04-25 13:09:52 +02:00
Kevin Jahns
90b7b01e9a fixes #49 2016-04-22 22:09:49 +01:00
Kevin Jahns
895ec86ff6 all tests working. Fixed an older bug: When gc an op I forgot to update the state. This only affected offline editing, and was very hard to catch in the past 2016-04-22 21:27:43 +01:00
Kevin Jahns
bffd130b92 fixed first two random cases, (gc seems still to be an issue ..) 2016-04-21 18:04:46 +02:00
Kevin Jahns
feae0d51bd fixed content.length in tryExecute 2016-04-20 12:37:56 +02:00
Kevin Jahns
f46c8df605 fixed some cases that lead to inconsistencies 2016-04-20 12:03:06 +02:00
Kevin Jahns
82025c5de9 better debugging, pretty print (no undefined) in logtable 2016-04-19 16:34:14 +02:00
Kevin Jahns
153ec811e2 fixed some gc bugs, improved applyDeleteSet 2016-04-18 16:13:29 +02:00
Kevin Jahns
01031d27c3 some gc fixes 2016-04-18 10:48:48 +01:00
Kevin Jahns
c72f62ecb6 implemented support for range of deletions (unfinished) 2016-04-14 18:09:27 +02:00
Kevin Jahns
e1df1a7a12 fixed one more problem with ds & fixed ace example & insertions are combined even when created in different execution tasks 2016-04-13 16:59:54 +02:00
Kevin Jahns
a7f845f553 fixed some old todos, fixed some cases related to "content is an array" 2016-04-12 16:18:05 +02:00
Kevin Jahns
20321c8a7d fixed test connector buffer to really be parallel! 2016-04-11 16:20:27 +02:00
Kevin Jahns
f3fadd3895 test suite works again with "contentS" feature! 2016-04-11 15:07:08 +02:00
Kevin Jahns
08a79d0e7b several bug fixes regarding "content is an array" 2016-04-07 15:54:47 +02:00
Kevin Jahns
5b21104da3 del is applied after ins (type is always called when ins already deleted) 2016-04-06 15:45:30 +02:00
Kevin Jahns
ecc2aef0f8 update 2016-04-05 15:38:22 +02:00
Kevin Jahns
1c32067908 implementing new insertion struct (unfinished) 2016-04-05 10:43:57 +01:00
Kevin Jahns
fe75ed6208 typos in README.md 2016-03-24 12:00:19 +01:00
Kevin Jahns
c2404b1e98 Release 10.0.3 2016-03-23 19:55:56 +01:00
Kevin Jahns
f363e1e9fc Merge branch 'master' of https://github.com/y-js/yjs 2016-03-23 19:54:40 +01:00
Kevin Jahns
749514c074 update dist 2016-03-23 19:54:28 +01:00
Kevin Jahns
24f8616386 Release 10.0.2 2016-03-23 14:42:26 +01:00
Kevin Jahns
d4ee8af772 Release 10.0.1 2016-03-23 14:41:08 +01:00
Kevin Jahns
83a42271ad fix remaining memory leaks 2016-03-23 14:33:51 +01:00
Kevin Jahns
88971b4e69 fixed several issues of the gc. I.e. the gc sometimes did not collect the whole subtree when deleting an operation 2016-03-21 21:00:28 +01:00
Kevin Jahns
f844dcbc1e bugfixes creating structs without loading type (e.g. for y-websockets-server) 2016-03-14 19:46:57 +01:00
Kevin Jahns
c9c00b5a08 found bug: types are not called anymore 2016-03-11 01:08:26 +01:00
Kevin Jahns
d79e3102fc new approach for type definitions 2016-03-10 17:49:36 +01:00
Kevin Jahns
ba4f444f32 Implemented support for composite type, fixed insert type issues for y-array 2016-02-29 13:46:08 +01:00
Kevin Jahns
effc2fe576 Release 9.0.4 2016-02-24 12:00:50 +01:00
Kevin Jahns
f9a54626b1 update dist 2016-02-24 12:00:27 +01:00
Kevin Jahns
808a07d218 added some error messages 2016-02-24 11:40:19 +01:00
Kevin Jahns
afbe81a602 update .gitignore 2016-02-24 10:09:56 +01:00
Kevin Jahns
2883947641 remove jsconfig 2016-02-24 10:08:11 +01:00
Kevin Jahns
1c15edd332 remove another .vscode 2016-02-24 10:07:07 +01:00
Kevin Jahns
214380c3ca updated changelog 2016-02-23 16:24:07 +01:00
Kevin Jahns
ecbf03ab10 Release 9.0.3 2016-02-23 15:50:19 +01:00
Kevin Jahns
5aedddeea3 update dist 2016-02-23 15:48:15 +01:00
Kevin Jahns
babdb765c5 Release 9.0.2 2016-02-23 15:42:13 +01:00
Kevin Jahns
43b4d59f9b updated package.json, major version upgrade, added peerDependencies 2016-02-23 15:41:28 +01:00
Kevin Jahns
64a5fae838 Release 9.0.1 2016-02-23 11:58:14 +01:00
Kevin Jahns
5036053d9c added peer dependencies, upgraded version to major (new versioning scheme) 2016-02-23 11:57:44 +01:00
Kevin Jahns
0ec249d388 Release 0.8.28 2016-02-23 11:27:30 +01:00
Kevin Jahns
be68a25904 Implement vesion header. Different version headers must not sync. implements #48 2016-02-23 11:21:37 +01:00
Kevin Jahns
fc92b12e85 Release 0.8.27 2016-02-22 13:08:05 +01:00
Kevin Jahns
e35f4d19f3 Release 0.8.26 2016-02-22 13:06:15 +01:00
Kevin Jahns
6d3c4b21fb Release 0.8.25 2016-02-22 13:04:36 +01:00
Kevin Jahns
339590f49e Release 0.8.22 2016-02-22 12:38:26 +01:00
Kevin Jahns
429c1f83c1 Merge pull request #45 from istvank/master
Making documentation consistent
2016-02-18 17:33:04 +01:00
Kevin Jahns
03bab63358 Release 0.8.21 2016-02-18 16:57:32 +01:00
Kevin Jahns
06ef22b8ca sorry sorry sorry -.- fixes #47 2016-02-18 16:52:13 +01:00
Kevin Jahns
f579a436c7 Release 0.8.20 2016-02-16 16:08:54 +01:00
Kevin Jahns
da7e67d97d implemented destroy & updated disconnect 2016-02-16 15:51:12 +01:00
Kevin Jahns
bd54a43a33 Release 0.8.18 2016-02-15 15:22:55 +01:00
Kevin Jahns
68c21131d3 Remove that single \for.. in\ loop that appearantly causes troubles for istvan (why am I fixing this again?) fixes #46 2016-02-15 15:21:11 +01:00
István Koren
3826d9b592 Make it consistent with npm doc few lines below 2016-02-14 13:24:12 +01:00
István Koren
fa9ff669e4 Merge pull request #1 from y-js/master
Updating to latest yjs version
2016-02-13 13:14:43 +01:00
Kevin Jahns
bca7477ca5 Release 0.8.17 2016-02-04 23:13:51 +01:00
Kevin Jahns
b40b7e10ab Release 0.8.16 2016-02-04 23:12:53 +01:00
Kevin Jahns
d20141fec1 Release 0.8.15 2016-02-04 23:11:11 +01:00
Kevin Jahns
5f2a81d064 updated api documentation 2016-02-04 23:08:29 +01:00
Kevin Jahns
56ba55cbab Release 0.8.14 2016-02-04 15:26:32 +01:00
Kevin Jahns
7be262e9f3 fixing @Joeao bug 2016-02-04 15:26:09 +01:00
Kevin Jahns
1da76dbc20 update dist 2016-02-04 12:53:39 +01:00
Kevin Jahns
8924c3e163 Release 0.8.13 2016-02-04 12:47:09 +01:00
Kevin Jahns
608b5e3319 Release 0.8.12 2016-02-04 12:12:57 +01:00
Kevin Jahns
d532fc530f update dist 2016-02-04 12:06:06 +01:00
Kevin Jahns
a5760a45bb Release 0.8.11 2016-02-04 10:53:04 +01:00
Kevin Jahns
437955ba84 update 2016-02-03 11:47:07 +01:00
Kevin Jahns
dab72be87f update 2016-02-03 11:37:36 +01:00
Kevin Jahns
89a6ec374e update 2016-01-27 17:05:28 +01:00
Kevin Jahns
4b6352b11a typo 2016-01-27 11:34:11 +01:00
Kevin Jahns
31d2a231e3 Further reduced number of db requests 2016-01-26 15:30:19 +01:00
Kevin Jahns
6b1cf18822 Improvements on DS lookups 2016-01-26 11:29:58 +01:00
Kevin Jahns
39dc2317b7 Implemented more efficient garbage collectior
from worst case of O(n) -> O(1) - where n is the number of insertions in
a list

So this is a huge improvement, I guess :)
2016-01-23 20:09:30 +01:00
Kevin Jahns
38bf398709 Improvements that are required for offline editing 2016-01-23 01:02:01 +01:00
Kevin Jahns
364ed325b0 fixed spec 2016-01-22 14:16:16 +01:00
Kevin Jahns
1b3f5443b3 implemented small lookup buffer. This heavily improves lookups for slow databases 2016-01-22 14:09:51 +01:00
Kevin Jahns
37ac7787d0 Update garbage collect algorithm. Fixed some tests appearantly :) 2016-01-21 21:08:20 +01:00
Kevin Jahns
8e4cf83330 typos 2016-01-18 17:21:47 +01:00
Kevin Jahns
5524ab9c20 Release 0.8.9 2016-01-18 16:45:46 +01:00
Kevin Jahns
65dc716936 Release 0.8.8 2016-01-18 15:40:38 +01:00
Kevin Jahns
5b7a4482cf Release 0.8.7 2016-01-16 01:45:58 +01:00
Kevin Jahns
cfa089f7cf Release 0.8.6 2016-01-16 01:42:00 +01:00
Kevin Jahns
190442a58d update documentation 2016-01-16 01:40:26 +01:00
Kevin Jahns
0398b5260a Release 0.8.5 2016-01-15 18:09:46 +01:00
Kevin Jahns
8544c16771 Release 0.8.4 2016-01-15 17:58:08 +01:00
Kevin Jahns
a5f55359c3 improve data exchange performance 2016-01-15 17:57:06 +01:00
Kevin Jahns
102555a3b0 Release 0.8.3 2016-01-15 03:46:55 +01:00
Kevin Jahns
ece8268e44 Release 0.8.2 2016-01-15 03:10:58 +01:00
Kevin Jahns
dd279bccf7 Release 0.8.1 2016-01-15 00:03:43 +01:00
Kevin Jahns
7e046e0753 Release 0.8.0 2016-01-15 00:02:12 +01:00
Kevin Jahns
51a834d6c9 Implemente a new sync procedure that is optimal with respect to big O notation (there is probably a way to reduce it by a factor of 1/2) 2016-01-15 00:00:41 +01:00
Kevin Jahns
a33d0bf7bc Release 0.7.6 2016-01-11 15:48:10 +01:00
Kevin Jahns
fd6a28eb25 Release 0.7.5 2016-01-11 15:47:24 +01:00
Kevin Jahns
579fd52455 publish v0.7.3 2016-01-09 21:08:02 +01:00
Kevin Jahns
8cfc9d41c3 Made compatible with windows 2016-01-09 04:17:23 +01:00
Kevin Jahns
bdf290adb2 added safety to setUserId (when called twice) 2015-12-30 16:37:35 +01:00
Kevin Jahns
98d87cb26d update 2015-12-18 16:34:21 +01:00
Kevin Jahns
fbbfa9fd47 added example 2015-12-09 18:40:10 +01:00
Kevin Jahns
72bd0d9c3a update map type 2015-12-08 16:26:55 +01:00
Kevin Jahns
3dbeb2c415 Merge pull request #34 from istvank/master
Changed to semver ;)
2015-12-08 14:17:29 +01:00
István Koren
2a9fd96958 Changed to semver ;)
Two lines below it states you switch to semver, still there was 1.0... ;)
2015-12-08 12:08:12 +01:00
Kevin Jahns
9d34ccfdbc update 2015-12-03 18:05:12 +01:00
Kevin Jahns
7753994e36 fixed bugs resolving from new init style 2015-12-03 17:27:13 +01:00
Kevin Jahns
709779425c make module import safer 2015-12-02 20:04:59 +01:00
Kevin Jahns
334db3234b outsourced Y.Map type 2015-12-02 16:57:55 +01:00
Kevin Jahns
0db7fe5d46 added support for static content, added jigsaw puzzle 2015-12-02 15:58:22 +01:00
Kevin Jahns
3a55ca4f21 update 2015-12-01 19:27:14 +01:00
Kevin Jahns
8d14a9cbba starting to implement new sharedObjects idea 2015-11-30 15:56:45 +01:00
Kevin Jahns
f6c5051472 added es6 distribution & gulp task for es6 distribution 2015-11-30 15:25:55 +01:00
Kevin Jahns
eff6fb1cc5 added flow support for everything except tests 2015-11-30 15:02:34 +01:00
Kevin Jahns
0ebfae6997 added flow support for Transaction.js 2015-11-30 14:26:22 +01:00
Kevin Jahns
e9c40f9a83 added flow support for Struct.js 2015-11-30 12:47:33 +01:00
Kevin Jahns
da2762edf5 added flow support for Connector.js 2015-11-30 12:26:02 +01:00
Kevin Jahns
bd9c3813fd * starting flow integration
* found a bug in EventHelper
* reduce wait() calls
2015-11-26 00:46:02 +01:00
Kevin Jahns
940a44bb7c fix transaction wait bug 2015-11-25 16:04:01 +01:00
Kevin Jahns
aa2e7fd917 Added jsconfig.json, fixed tests for large numbers 2015-11-20 21:18:34 +01:00
Kevin Jahns
9fc55f5386 update readme 2015-11-19 18:10:31 +01:00
Kevin Jahns
8ee563f873 finally fixed the timeout hack for tests 2015-11-18 16:17:59 +01:00
Kevin Jahns
5fcfbbfe94 updated build process 2015-11-17 15:28:45 +01:00
Kevin Jahns
8870fdc495 lint 2015-11-15 02:14:06 +01:00
Kevin Jahns
58a612eaa1 added option for servers that want to propagate applied operations (aka the websockets connector) 2015-11-15 02:04:06 +01:00
Kevin Jahns
ae12b087e7 fixed module loading issue 2015-11-14 20:53:38 +01:00
Kevin Jahns
528dbc6e5a announcing new version in readme 2015-11-14 20:44:54 +01:00
Kevin Jahns
1deb453cc5 fixed the dist build process 2015-11-14 20:41:34 +01:00
Kevin Jahns
099297ebdf working on build process 2015-11-13 16:09:40 +01:00
Kevin Jahns
3faeb628fd updated dist build process 2015-11-12 20:42:58 +01:00
Kevin Jahns
d1e30c5040 updated examples and dist build 2015-11-11 17:19:22 +01:00
Kevin Jahns
fa45ce04ef prettyfied README for website 2015-11-11 00:00:15 +01:00
Kevin Jahns
2d20fd59d0 outsourced Textbind, improved automatic module loader 2015-11-09 03:03:37 +01:00
Kevin Jahns
08d07796ee added spec helper 2015-11-07 22:20:47 +01:00
Kevin Jahns
010d0d684e fixed linting 2015-11-07 22:18:28 +01:00
Kevin Jahns
6dc347642b implemented module loader for yjs 2015-11-07 22:12:48 +01:00
Kevin Jahns
138afe39dc improving.. breaking.. the gulpfile 2015-11-06 16:16:38 +01:00
Kevin Jahns
0832be2380 improved error messaging.. thats it for today 2015-11-05 17:20:27 +01:00
Kevin Jahns
8a2a184f30 Release 0.6.32 2015-11-05 17:09:01 +01:00
Kevin Jahns
4882e77fdd improved gulpfile.helper 2015-11-05 16:55:03 +01:00
Kevin Jahns
78f4f6f5b9 implemented gulpfile.helper 2015-11-05 15:53:26 +01:00
Kevin Jahns
317f7f19bb updated gulpfile to wiki 2015-11-05 00:35:11 +01:00
Kevin Jahns
00f58ba68f fixed travis 2015-11-04 17:12:59 +01:00
Kevin Jahns
029a169114 fixed serve:examples 2015-11-04 17:06:20 +01:00
Kevin Jahns
f58889a05d outsourced examples 2015-11-04 16:53:02 +01:00
Kevin Jahns
e9ac59dcf8 fixed tests, finalizing the scripts (sorry for all the commits -.-) 2015-11-04 15:01:12 +01:00
Kevin Jahns
57cf20555f Deploy 0.6.21 2015-11-04 14:39:54 +01:00
Kevin Jahns
805ed3b577 Deploy 0.6.20 2015-11-04 14:37:06 +01:00
Kevin Jahns
2a0d5c0cd7 Deploy 0.6.19 2015-11-04 14:36:01 +01:00
Kevin Jahns
13ed66c326 Deploy 0.6.18 2015-11-04 14:35:08 +01:00
Kevin Jahns
1c35198839 Deploy 0.6.17 2015-11-04 14:33:44 +01:00
Kevin Jahns
a7021b9212 Deploy 0.6.16 2015-11-04 14:32:04 +01:00
Kevin Jahns
1fa1f1a668 bumps package version 2015-11-04 14:10:01 +01:00
DadaMonad
243e62e320 bumps package version 2015-11-02 13:14:52 +00:00
DadaMonad
15e933ee5b bumps package version 2015-11-02 13:04:55 +00:00
DadaMonad
605e1052ac bumps package version 2015-11-02 13:04:08 +00:00
DadaMonad
16c00525d1 bumps package version 2015-11-02 12:57:00 +00:00
Kevin Jahns
e9da461625 update 2015-10-30 16:00:08 +01:00
Kevin Jahns
a071c07ee2 added dist submodule 2015-10-30 15:30:02 +01:00
DadaMonad
8dad4f6ed4 updated documentaiton 2015-10-25 16:15:03 +00:00
Kevin Jahns
0980609cc9 fixed bug in delete operations 2015-10-19 11:27:49 +02:00
Kevin Jahns
29f3f3f722 added offline editing demo 🌟 2015-10-18 03:07:34 +02:00
Kevin Jahns
04139d3b7e implemented indexedDB database :shipit: 2015-10-17 23:02:51 +02:00
Kevin Jahns
45814c4e00 fixed bug (o.right is already gc'd), implemented some test helpers 2015-10-17 17:16:36 +02:00
Kevin Jahns
cf365b8902 started to remove everything RBTree related from the Transaction.js 2015-10-16 12:31:03 +02:00
Kevin Jahns
aff10fa4db started refactoring the Memory db 2015-10-15 18:54:29 +02:00
Kevin Jahns
181595293f refactored database 2015-10-14 19:28:19 +02:00
Kevin Jahns
ee133ef334 refactored test suites 2015-10-14 18:10:04 +02:00
Kevin Jahns
661232f23c fixed the test suite 2015-10-14 10:27:46 +02:00
Kevin Jahns
541a93d152 refactoring the tarnsition functions 2015-10-13 21:40:36 +02:00
Kevin Jahns
d6e1cd42a2 implemented disconnect/reconnect in webrtc connector. adapted the example gc also collects child elements (needs improvements) 2015-10-13 14:50:54 +02:00
Kevin Jahns
51e20fb9c7 fixed some example issues 2015-10-12 15:59:22 +02:00
Kevin Jahns
e32aef4c9f late join works (also when activating garbage collector), added some tests to verify (havent tested for large >500 operations) 2015-10-12 15:17:12 +02:00
Kevin Jahns
9c4074e3e3 fixed late join issues when gc is turned off 2015-10-11 03:06:26 +02:00
Kevin Jahns
aadef59934 fixed DS bugs (i guess..) now handling more complicated scenarios 2015-10-09 16:09:00 +02:00
Kevin Jahns
6a13419c62 fixed several bugs in multi join/rejoin 2015-10-08 02:12:20 +02:00
Kevin Jahns
1ace3e3120 implemented observePath, fixed some inconsistencies 2015-10-06 19:45:29 +02:00
Kevin Jahns
c95dae3c33 fixed inconsistency bugs for tests<1000 2015-10-06 14:22:52 +02:00
Kevin Jahns
82e2254302 fixed some inconsistency bugs with DS 2015-10-05 14:24:11 +02:00
DadaMonad
6e9f990d5c small fixes that i made on the train 2015-10-05 09:48:32 +00:00
DadaMonad
7d4adf314d fixed some bugs from the last commit 2015-10-02 08:01:58 +00:00
Kevin Jahns
8745fd64ca code refactoring, and documentation 2015-09-29 13:59:38 +02:00
Kevin Jahns
638c575dfc fixed some consistency bugs. new method seems to work well, it still has problems though 2015-09-29 01:01:04 +02:00
Kevin Jahns
acf8d37616 added deploy gulp method 2015-09-28 23:54:56 +02:00
Kevin Jahns
ae8be1ec6b improved new sync idea (save gcs in DS) 2015-09-28 13:06:17 +02:00
Kevin Jahns
a5f76cee84 starting to extend the DS with gc functionality 2015-09-27 20:02:00 +02:00
Kevin Jahns
2013266d56 merged changes on home pc. some improvements on rejoin&sync 2015-09-27 00:58:23 +02:00
Kevin Jahns
b08aeee4fc updating some changes i forgot to commit 2015-09-26 14:42:50 +02:00
Kevin Jahns
183f30878e checking out new gc approach 2015-09-25 16:00:20 +02:00
Kevin Jahns
5e4c56af29 fixed bugs, tests are running, source is documented 2015-09-17 20:30:40 +02:00
Kevin Jahns
13bef69be4 updated gitignore 2015-09-17 02:34:43 +02:00
Kevin Jahns
b1d70ef25e added comments to most of the classes. 2015-09-17 00:21:01 +02:00
Kevin Jahns
6f3a291ef5 fixed some tests, lint, better run-scripts 2015-09-16 16:25:30 +02:00
Kevin Jahns
2a601ac6f6 fixed some bugs & linted & prettyfied gulpfile 2015-09-13 18:22:45 +02:00
Kevin Jahns
82b3e50d49 new build system 2015-09-11 18:35:32 +02:00
Kevin Jahns
4bfe484fc2 node-inspector 2015-09-10 19:41:07 +02:00
Kevin Jahns
b9e21665e2 update 2015-09-09 20:29:39 +02:00
Kevin Jahns
06e7caab2d gc implementation 2015-07-26 16:03:13 +00:00
Kevin Jahns
c8ded24842 started implementing the garbage collector 2015-07-26 03:13:13 +00:00
Kevin Jahns
dae0f71cbc fixed another test 2015-07-26 00:01:53 +00:00
Kevin Jahns
81c601c65f fixed late sync with deletions 2015-07-25 23:58:57 +00:00
Kevin Jahns
56165a3c10 late sync with insertions only work now 2015-07-25 23:26:52 +00:00
Kevin Jahns
5e0d602e12 finished & tested DeleteStore 2015-07-25 16:28:05 +00:00
Kevin Jahns
420821be31 continuing DeleteStore 2015-07-24 22:24:49 +02:00
Kevin Jahns
d1fda080d9 added some fixes and started DeleteStore implementation 2015-07-22 19:30:00 +02:00
Kevin Jahns
dd5e2adc87 update 2015-07-21 17:25:07 +02:00
Kevin Jahns
ee983ceff6 switched to *standard* coding style 2015-07-21 17:15:38 +02:00
Kevin Jahns
ee116b8ca4 fixed all the tests 2015-07-19 23:31:35 +00:00
Kevin Jahns
d4ef54358b re-implementing tests with async await. tests also check asynchronous behaviour now. 2015-07-18 23:15:20 +02:00
Kevin Jahns
ebc628adfc fixed really nasty bug, requestTransaction was called synchronously 2015-07-17 15:04:00 +02:00
Kevin Jahns
4563ccc98e fixed trailing space bug in contenteditable elements 2015-07-17 10:43:39 +02:00
Kevin Jahns
a4f7f5c987 fixed bugs that came wih the last commit 2015-07-17 10:34:43 +02:00
Kevin Jahns
4a7f09c32d last bug fixes for TextBind type (for now) 2015-07-16 06:53:47 +02:00
Kevin Jahns
f78dc52d7b added textbind example, improved & fixed syncing, RBTree handles ids correctly now, webrtc connector is quite reliable now 2015-07-16 06:15:23 +02:00
Kevin Jahns
f9f8228db6 outsourcing some code. custom types definition change 2015-07-15 22:32:36 +02:00
Kevin Jahns
60b75d1862 array & type are observeable 2015-07-15 21:24:05 +02:00
Kevin Jahns
9b3fe2f197 webrtc connector working 2015-07-14 22:39:01 +02:00
Kevin Jahns
6b153896dd delete support for Array & Map 2015-07-14 20:51:07 +02:00
Kevin Jahns
66a7d2720d split the big text suite into smaller ones 2015-07-14 11:58:43 +02:00
Kevin Jahns
d50d34dc12 created Array type that has a good time complexity for both insert and retrieval of objects 2015-07-12 03:45:12 +02:00
Kevin Jahns
8cc374cabb added eventhandler 2015-07-10 15:00:54 +02:00
Kevin Jahns
8e9e62b3d0 discontinuing todays session 2015-07-09 22:19:10 +02:00
Kevin Jahns
9b45a78e58 fixing types. 2015-07-09 15:50:59 +02:00
Kevin Jahns
f862fae473 fixed a bug 2015-07-09 01:33:46 +02:00
Kevin Jahns
0493d99d57 list and map types work now and they support delete. added random tests 2015-07-09 01:30:57 +02:00
Kevin Jahns
a1026bc365 use RBTree for in-memory storage 2015-07-08 21:25:36 +02:00
Kevin Jahns
fe4564542b implemented deletion of elements & and iteration method & lots of tests 2015-07-08 20:05:18 +02:00
Kevin Jahns
7b52111c31 fixed insertion bug in RBTree. adding does now work correctly 2015-07-07 21:17:28 +02:00
Kevin Jahns
c184cb961b implemented RBTree as a in-memory database for operations (in progress) 2015-07-07 18:11:27 +02:00
Kevin Jahns
02f2f6b0fe wrap up todays session 2015-07-06 23:39:28 +02:00
Kevin Jahns
e47dee53a3 random tests succeed on Map :) 2015-07-06 23:04:01 +02:00
Kevin Jahns
9b6183ea70 custom types work. Now I need to re-implement the test case from 0.5 2015-07-06 18:37:54 +02:00
Kevin Jahns
79ec71d559 added test case 2015-07-06 16:57:30 +02:00
Kevin Jahns
bf4d5f24a8 simple conflicts are now handled correctly 2015-07-06 16:47:49 +02:00
Kevin Jahns
9d0373b85b added not working tests 2015-07-03 14:43:08 +02:00
Kevin Jahns
f8ad9abcc0 late join should work now. Need to test more. root is passed to transaction generator 2015-06-30 17:57:19 +02:00
Kevin Jahns
b25977be06 Map type works with simple update & sync. now going to implement support for syncing existing operation buffers 2015-06-30 15:44:14 +02:00
Kevin Jahns
bffbb6ca27 basic get&set of Map properties works 2015-06-29 13:20:19 +02:00
Kevin Jahns
8f63147dbc added Map struct 2015-06-28 12:42:54 +02:00
Kevin Jahns
7a274565e5 added memory data store (actually adding it..) 2015-06-28 11:14:40 +02:00
Kevin Jahns
75793d0ced added memory data store 2015-06-28 01:42:17 +02:00
Kevin Jahns
7ec409e09f linted all files 2015-06-27 19:01:15 +02:00
Kevin Jahns
fec03dc6e1 added test connector, webrtc connector, ideas to apply operations with very low overhead 2015-06-25 18:41:00 +02:00
Kevin Jahns
3142b0f161 added some Operations, a connector, more structure. In particular I put a lot of time into the event handling 2015-06-21 14:56:41 +02:00
Kevin Jahns
042bcee482 now using one master generator, that rulez them all 2015-06-21 09:45:57 +02:00
Kevin Jahns
b3e09d001f updated whenOperationExists 2015-06-21 03:50:58 +02:00
Kevin Jahns
dcec0fe967 Implemented some operations. OperationStore executes now ops, not the Engine 2015-06-21 02:24:41 +02:00
Kevin Jahns
ae790b6947 updated OperationBuffer 2015-06-19 14:54:35 +02:00
Kevin Jahns
4b08cbe875 no more promises in requestTransaction :) 2015-06-18 15:11:22 +02:00
Kevin Jahns
01173879a0 Merge pull request #25 from y-js/origin/0.6
merging the infamous `origin/origin/0.6` branch
2015-06-18 11:47:47 +02:00
Kevin Jahns
6f99ee5c34 requestTransaction accepts Promises&Generators 2015-06-18 02:35:52 +02:00
Kevin Jahns
8d1bccbea0 added new generator approach 2015-06-17 19:16:52 +02:00
Kevin Jahns
b6c278f8e4 added indexeddb 2015-06-16 20:13:14 +02:00
Kevin Jahns
5a9f59913e changed to pre-commit 2015-06-16 19:51:14 +02:00
Kevin Jahns
bf493216a2 updated gitignore, flow working 2015-06-16 17:45:05 +02:00
Kevin Jahns
d37d0ef9af update 2015-06-16 17:18:40 +02:00
Kevin Jahns
c7a6e74dd9 updated precommit 2015-06-16 16:20:38 +02:00
Kevin Jahns
24570b791a defined specs 2015-06-16 14:41:35 +02:00
Kevin Jahns
f99853529e improved gulpfile 2015-06-16 14:36:00 +02:00
Kevin Jahns
159f37474d checking out new stuff 2015-06-15 14:53:02 +02:00
Kevin Jahns
1b63f5efde added lots of magic 2015-06-09 22:30:42 +02:00
Kevin Jahns
c3ba8173d7 added v0.6 info note 2015-06-09 18:10:11 +02:00
Kevin Jahns
7a89c1cc6d added first prototype of the new HB with indexedDB 2015-06-09 18:08:23 +02:00
Kevin Jahns
c5b47e88ac bump version numbers 2015-05-28 15:47:43 +02:00
Kevin Jahns
dc3c6a5d42 added support to use existing user ids! Fixes #23 2015-05-28 15:44:13 +02:00
Kevin Jahns
a9c2ec6ba0 Merge pull request #22 from cphyc/bugfix-21
Bugfix 21
2015-05-21 14:51:48 +02:00
Corentin STG_CADIOU
f166b9efc5 Compiled files 2015-05-21 14:39:13 +02:00
Corentin STG_CADIOU
0441b83f74 Fix error with compute_when_synced 2015-05-21 14:28:47 +02:00
Kevin Jahns
90c82a6a02 Merge pull request #20 from cphyc/Fix-error-in-package.json
Use correct path for gulp
2015-05-19 13:31:56 +02:00
cphyc
da25905b73 Use correct path for gulp 2015-05-19 10:47:36 +02:00
Kevin Jahns
3c07a938cd prepublish yjs 2015-05-07 13:34:42 +02:00
Kevin Jahns
55ccacc442 added doc 2015-05-05 16:09:34 +02:00
Kevin Jahns
946a11f03d fixing possible source of a bug in y-rt 2015-05-05 12:55:21 +02:00
Kevin Jahns
93f3a49396 fixed problem in large sendHB 2015-05-04 13:25:12 +02:00
Kevin Jahns
3eed100b8d updated compiled files 2015-05-03 19:59:41 +02:00
Kevin Jahns
eb136ae1bf update README 2015-05-03 19:57:51 +02:00
Kevin Jahns
006d0a2643 update README 2015-05-03 19:56:44 +02:00
Kevin Jahns
7959bdf5ac Merge pull request #19 from y-js/0.5
0.5
2015-05-03 19:51:50 +02:00
Kevin Jahns
6f9ee0d9ba added tutorial in README 2015-05-03 19:50:59 +02:00
Kevin Jahns
d901d5f5e4 small fixes 2015-04-30 16:29:48 +02:00
Kevin Jahns
b2c7706a2e fixed y-object polymer 2015-04-30 15:09:10 +02:00
Kevin Jahns
4d926cf841 updated examples 2015-04-30 14:15:48 +02:00
Kevin Jahns
0314a1b709 Merge pull request #18 from y-js/0.5
Added support for custom connectors. Not compatible with 0.4!!
2015-04-30 12:42:38 +02:00
Kevin Jahns
8a5b69e86c better fix for #17 2015-04-30 09:48:39 +02:00
Kevin Jahns
ce5250b9d8 updated readme 2015-04-29 01:07:12 +02:00
Kevin Jahns
f51c791490 updated readme 2015-04-29 00:57:36 +02:00
Kevin Jahns
b75305a082 updated readme 2015-04-29 00:55:11 +02:00
Kevin Jahns
8d80fd5614 updated readme 2015-04-29 00:54:27 +02:00
Kevin Jahns
bad6c913fc upd 2015-04-28 16:07:45 +02:00
Kevin Jahns
85d85540e7 updated Composition (error when doing undo) 2015-04-28 14:57:37 +02:00
Kevin Jahns
80f1cfd21b added user events 2015-04-28 11:31:43 +02:00
Kevin Jahns
729d7ed3aa fixed getNext & getPrev 2015-04-24 13:52:17 +02:00
Kevin Jahns
0a89150fab insertions must not be deleted anymore! 2015-04-23 17:40:40 +02:00
Kevin Jahns
6fc33e40bb observer will get the reference too now (ListManager) 2015-04-23 15:27:20 +02:00
Kevin Jahns
7f6592a6b7 removed phantom 2015-04-20 11:51:52 +02:00
Kevin Jahns
b9cdbcc6fa deleted debug log 2015-04-19 19:21:05 +02:00
Kevin Jahns
2a78cdba48 Operation can now save static content/Operations 2015-04-19 18:54:03 +02:00
Kevin Jahns
b02662c36e refs, complex saveOperation' and validateSavedOperations` 2015-04-17 20:11:05 +02:00
Kevin Jahns
f44f463e9d references & composition value
fixing bugs with references
composition type parses now composition value
2015-04-17 00:36:52 +02:00
Kevin Jahns
757bb118ce Insert bug detected (because of references) 2015-04-16 02:31:22 +02:00
DadaMonad
5417ffb999 non-important update 2015-04-14 17:14:43 +00:00
DadaMonad
4de979bc33 Merge branch '0.5' of https://github.com/rwth-acis/yjs into 0.5 2015-04-14 17:13:27 +00:00
DadaMonad
875f56586e added parameter to getNext 2015-04-14 17:13:16 +00:00
Kevin Jahns
249b712648 MapManager returns null, when reference does not exist 2015-04-14 01:30:35 +02:00
DadaMonad
58cefae839 updated composition 2015-04-12 13:09:10 +00:00
DadaMonad
d9c5ab5fa8 added support for getNext/getPrev on insertions 2015-04-07 10:37:50 +00:00
DadaMonad
6d99ed07f0 update 2015-04-07 03:17:03 +00:00
DadaMonad
e55ed9f2b4 composition type & support for list references 2015-04-07 00:16:12 +00:00
DadaMonad
bb0bfcc5c8 cleaned up, removed Replaceable operation, changed Operation specific events method (put it into the Manager types), created Composition type 2015-04-06 21:35:04 +00:00
DadaMonad
b24de43fe2 removed webcomponents from dependencies 2015-03-09 19:39:18 +00:00
DadaMonad
446560d9e8 added support for namespaces on types 2015-03-09 17:38:26 +00:00
DadaMonad
148e46f043 updated README 2015-03-01 15:44:31 +00:00
DadaMonad
e8f20dabd3 updated readme with new types 2015-02-27 18:32:05 +00:00
DadaMonad
96ed8b0f98 outsourced all types (except for object type) 2015-02-27 18:01:21 +00:00
DadaMonad
c663230c1b add xml examples 2015-02-26 21:39:12 +00:00
DadaMonad
0a8118367d yay - every test on xml and dom succeeds 2015-02-26 21:28:06 +00:00
DadaMonad
f932f560bd completed the xml tests - and lots of them run successfully 2015-02-26 18:28:35 +00:00
DadaMonad
f9542b90db made xml tests more expressive 2015-02-26 10:09:41 +00:00
DadaMonad
014495febd added random tests for xml test suite 2015-02-26 09:20:26 +00:00
DadaMonad
82f11c421f fixed parent issue (only one parent per Y.Xml type) 2015-02-25 23:41:57 +00:00
DadaMonad
9059618d1f Y-Xml tests pass 2015-02-25 22:50:26 +00:00
DadaMonad
9a8f8fba05 added tests for new xml type. 2015-02-24 20:34:27 +00:00
DadaMonad
3ba89edf7d support for circular structures (e.g. with JSON) 2015-02-24 16:09:42 +00:00
DadaMonad
fea6de3bf9 starting to refine the Replaceable type. Most types should be a replaceable. 2015-02-23 16:36:55 +00:00
DadaMonad
2a644f2f0c travis: build master branch only 2015-02-23 13:30:58 +00:00
DadaMonad
f189ae11b0 tests are no longer failing :) 2015-02-23 13:20:52 +00:00
DadaMonad
2e9f8f6d03 added text as a custom type, more tests are working 2015-02-23 11:41:04 +00:00
DadaMonad
860934de06 broke the dammn thing 2015-02-19 15:55:05 +00:00
DadaMonad
792440a71d devided ops/types 2015-02-19 10:41:34 +00:00
DadaMonad
1aacc0e967 remove travis 0.8 0.6 version 2015-02-18 00:44:16 +00:00
DadaMonad
d4b0c8cbbd travis build 2015-02-18 00:28:43 +00:00
DadaMonad
d6526f12fb fixed mocha stop 2015-02-18 00:01:43 +00:00
DadaMonad
d3af98cd17 travis - install coffee and bower first 2015-02-17 23:19:41 +00:00
DadaMonad
e33eb6a928 travis build bug 2015-02-17 23:14:00 +00:00
DadaMonad
d1be152983 travis button 2015-02-17 23:06:29 +00:00
DadaMonad
548a77833a added travis stuff 2015-02-17 23:00:25 +00:00
DadaMonad
5ba0a7492a refactoring text types 2015-02-17 21:10:46 +00:00
DadaMonad
c65f11b308 updated mocha 2015-02-17 19:37:15 +00:00
DadaMonad
77b83cae2a fixing double late join test fail 2015-02-17 19:23:27 +00:00
DadaMonad
f609c22be8 fixed text binding (enter, utf8 chars..) 2015-02-17 10:26:32 +00:00
DadaMonad
670854e9d8 added gitter button 2015-02-15 17:48:30 +00:00
DadaMonad
2bb7ba03cd added contrib (finally - heh?) 2015-02-15 17:17:34 +00:00
DadaMonad
686be484fc bumped version numbers 2015-02-15 15:35:09 +00:00
DadaMonad
60de3ce5b0 getting ready for 0.4 realeasy 2015-02-15 15:33:35 +00:00
DadaMonad
b6fe47efe1 changed to syncMethod 2015-02-05 16:19:55 +00:00
DadaMonad
e5f16812b3 added new y-test connector 2015-02-05 14:15:20 +00:00
DadaMonad
3eb933400a fixed doSync bug, fixed connection problems, improved p2p sync method - still
there are some cases that may lead to inconsistencies. Currently, only the master-slave method is a reliable sync method
2015-02-05 10:46:40 +00:00
DadaMonad
58a479be9b included connector type 2015-02-03 18:55:02 +00:00
DadaMonad
f835a72151 v0.3.2 2015-01-30 17:03:18 +00:00
DadaMonad
50fa81d191 fixed focus issue 2015-01-30 17:02:03 +00:00
108 changed files with 23724 additions and 43316 deletions

View File

@@ -1,3 +0,0 @@
{
"directory": "../"
}

29
.github/workflows/node.js.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build --if-present
- run: npm test

31
.github/workflows/nodejs.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 13.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run lint
- run: npm run test-extensive
env:
CI: true

10
.gitignore vendored
View File

@@ -1,6 +1,4 @@
/node_modules/
bower_components
.directory
.c9
.codio
.settings
node_modules
dist
.vscode
docs

52
.jsdoc.json Normal file
View File

@@ -0,0 +1,52 @@
{
"sourceType": "module",
"tags": {
"allowUnknownTags": true,
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["./src"],
"includePattern": ".js$"
},
"plugins": [
"plugins/markdown"
],
"templates": {
"referenceTitle": "Yjs",
"disableSort": false,
"useCollapsibles": true,
"collapse": true,
"resources": {
"yjs.dev": "Website",
"docs.yjs.dev": "Docs",
"discuss.yjs.dev": "Forum",
"https://gitter.im/Yjs/community": "Chat"
},
"logo": {
"url": "https://yjs.dev/images/logo/yjs-512x512.png",
"width": "162px",
"height": "162px",
"link": "/"
},
"tabNames": {
"api": "API",
"tutorials": "Examples"
},
"footerText": "Shared Editing",
"css": [
"./style.css"
],
"default": {
"staticFiles": {
"include": []
}
}
},
"opts": {
"destination": "./docs/",
"encoding": "utf8",
"private": false,
"recurse": true,
"template": "./node_modules/tui-jsdoc-template"
}
}

4
.markdownlint.json Normal file
View File

@@ -0,0 +1,4 @@
{
"default": true,
"no-inline-html": false
}

179
INTERNALS.md Normal file
View File

@@ -0,0 +1,179 @@
# Yjs Internals
This document roughly explains how Yjs works internally. There is a complete
walkthrough of the Yjs codebase available as a recording:
https://youtu.be/0l5XgnQ6rB4
The Yjs CRDT algorithm is described in the [YATA
paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types)
from 2016. For an algorithmic view of how it works, the paper is a reasonable
place to start. There are a handful of small improvements implemented in Yjs
which aren't described in the paper. The most notable is that items have an
`originRight` as well as an `origin` property, which improves performance when
many concurrent inserts happen after the same character.
At its heart, Yjs is a list CRDT. Everything is squeezed into a list in order to
reuse the CRDT resolution algorithm:
- Arrays are easy - they're lists of arbitrary items.
- Text is a list of characters, optionally punctuated by formatting markers and
embeds for rich text support. Several characters can be wrapped in a single
linked list `Item` (this is also known as the compound representation of
CRDTs). More information about this in [this blog
article](https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/).
- Maps are lists of entries. The last inserted entry for each key is used, and
all other duplicates for each key are flagged as deleted.
Each client is assigned a unique *clientID* property on first insert. This is a
random 53-bit integer (53 bits because that fits in the javascript safe integer
range).
## List items
Each item in a Yjs list is made up of two objects:
- An `Item` (*src/structs/Item.js*). This is used to relate the item to other
adjacent items.
- An object in the `AbstractType` hierarchy (subclasses of
*src/types/AbstractType.js* - eg `YText`). This stores the actual content in
the Yjs document.
The item and type object pair have a 1-1 mapping. The item's `content` field
references the AbstractType object and the AbstractType object's `_item` field
references the item.
Everything inserted in a Yjs document is given a unique ID, formed from a
*ID(clientID, clock)* pair (also known as a [Lamport
Timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp)). The clock counts
up from 0 with the first inserted character or item a client makes. This is
similar to automerge's operation IDs, but note that the clock is only
incremented by inserts. Deletes are handled in a very different way (see
below).
If a run of characters is inserted into a document (eg `"abc"`), the clock will
be incremented for each character (eg 3 times here). But Yjs will only add a
single `Item` into the list. This has no effect on the core CRDT algorithm, but
the optimization dramatically decreases the number of javascript objects
created during normal text editing. This optimization only applies if the
characters share the same clientID, they're inserted in order, and all
characters have either been deleted or all characters are not deleted. The item
will be split if the run is interrupted for any reason (eg a character in the
middle of the run is deleted).
When an item is created, it stores a reference to the IDs of the preceeding and
succeeding item. These are stored in the item's `origin` and `originRight`
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 this
algorithm.
### Item Storage
The items themselves are stored in two data structures and a cache:
- The items are stored in a tree of doubly-linked lists in *document order*.
Each item has `left` and `right` properties linking to its siblings in the
document. Items also have a `parent` property to reference their parent in the
document tree (null at the root). (And you can access an item's children, if
any, through `item.content`).
- All items are referenced in *insertion order* inside the struct store
(*src/utils/StructStore.js*). This references the list of items inserted by
for each client, in chronological order. This is used to find an item in the
tree with a given ID (using a binary search). It is also used to efficiently
gather the operations a peer is missing during sync (more on this below).
When a local insert happens, Yjs needs to map the insert position in the
document (eg position 1000) to an ID. With just the linked list, this would
require a slow O(n) linear scan of the list. But when editing a document, most
inserts are either at the same position as the last insert, or nearby. To
improve performance, Yjs stores a cache of the 10 most recently looked up
insert positions in the document. This is consulted and updated when a position
is looked up to improve performance in the average case. The cache is updated
using a heuristic that is still changing (currently, it is updated when a new
position significantly diverges from existing markers in the cache). Internally
this is referred to as the skip list / fast search marker.
### Deletions
Deletions in Yjs are treated very differently from insertions. Insertions are
implemented as a sequential operation based CRDT, but deletions are treated as
a simpler state based CRDT.
When an item has been deleted by any peer, at any point in history, it is
flagged as deleted on the item. (Internally Yjs uses the `info` bitfield.) Yjs
does not record metadata about a deletion:
- No data is kept on *when* an item was deleted, or which user deleted it.
- The struct store does not contain deletion records
- The clientID's clock is not incremented
If garbage collection is enabled in Yjs, when an object is deleted its content
is discarded. If a deleted object contains children (eg a field is deleted in
an object), the content is replaced with a `GC` object (*src/structs/GC.js*).
This is a very lightweight structure - it only stores the length of the removed
content.
Yjs has some special logic to share which content in a document has been
deleted:
- When a delete happens, as well as marking the item, the deleted IDs are
listed locally within the transaction. (See below for more information about
transactions.) When a transaction has been committed locally, the set of
deleted items is appended to a transaction's update message.
- A snapshot (a marked point in time in the Yjs history) is specified using
both the set of (clientID, clock) pairs *and* the set of all deleted item
IDs. The deleted set is O(n), but because deletions usually happen in runs,
this data set is usually tiny in practice. (The real world editing trace from
the B4 benchmark document contains 182k inserts and 77k deleted characters. The
deleted set size in a snapshot is only 4.5Kb).
## Transactions
All updates in Yjs happen within a *transaction*. (Defined in
*src/utils/Transaction.js*.)
The transaction collects a set of updates to the Yjs document to be applied on
remote peers atomically. Once a transaction has been committed locally, it
generates a compressed *update message* which is broadcast to synchronized
remote peers to notify them of the local change. The update message contains:
- The set of newly inserted items
- The set of items deleted within the transaction.
## Network protocol
The network protocol is not really a part of Yjs. There are a few relevant
concepts that can be used to create a custom network protocol:
* `update`: The Yjs document can be encoded to an *update* object that can be
parsed to reconstruct the document. Also every change on the document fires
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 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
their state vector (often referred to as *sync step 1*). The remote peer can
compute the missing `Item` objects using the `clocks` of the respective clients
and compute a minimal update message that reflects all missing updates (sync
step 2).
An implementation of the syncing process is in
[y-protocols](https://github.com/yjs/y-protocols).
## Snapshots
A snapshot can be used to restore an old document state. It is a `state vector`
\+ `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.
It is not recommended to restore an old document state using snapshots,
although that would certainly be possible. Instead, the old state should be
computed by iterating through the newest state and using the additional
information from the state vector.

View File

@@ -1,6 +1,8 @@
The MIT License (MIT)
Copyright (c) 2014 Kevin Jahns <kevin.jahns@rwth-aachen.de>.
Copyright (c) 2014
- Kevin Jahns <kevin.jahns@rwth-aachen.de>.
- Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

1144
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +0,0 @@
{
"name": "yjs",
"version": "0.3.1",
"homepage": "https://github.com/DadaMonad/yjs",
"authors": [
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
],
"description": "A Framework that enables Real-Time collaboration on arbitrary data structures.",
"main": [
"./y.js",
"./y-object.html",
"./build/node/y.js"
],
"keywords": [
"OT",
"collaboration",
"synchronization",
"ShareJS",
"Coweb",
"concurrency"
],
"license": "MIT",
"ignore": [
"node_modules",
"bower_components",
"test",
"extras",
"test"
],
"dependencies": {
"polymer": "Polymer/polymer#~0.5.3"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,66 +0,0 @@
var adaptConnector;
adaptConnector = function(connector, engine, HB, execution_listener) {
var applyHB, encode_state_vector, getHB, getStateVector, parse_state_vector, send_;
send_ = function(o) {
if (o.uid.creator === HB.getUserId() && (typeof o.uid.op_number !== "string")) {
return connector.broadcast(o);
}
};
if (connector.invokeSync != null) {
HB.setInvokeSyncHandler(connector.invokeSync);
}
execution_listener.push(send_);
encode_state_vector = function(v) {
var name, value, _results;
_results = [];
for (name in v) {
value = v[name];
_results.push({
user: name,
state: value
});
}
return _results;
};
parse_state_vector = function(v) {
var s, state_vector, _i, _len;
state_vector = {};
for (_i = 0, _len = v.length; _i < _len; _i++) {
s = v[_i];
state_vector[s.user] = s.state;
}
return state_vector;
};
getStateVector = function() {
return encode_state_vector(HB.getOperationCounter());
};
getHB = function(v) {
var hb, json, o, state_vector, _i, _len;
state_vector = parse_state_vector(v);
hb = HB._encode(state_vector);
for (_i = 0, _len = hb.length; _i < _len; _i++) {
o = hb[_i];
o.fromHB = "true";
}
json = {
hb: hb,
state_vector: encode_state_vector(HB.getOperationCounter())
};
return json;
};
applyHB = function(hb) {
return engine.applyOp(hb);
};
connector.getStateVector = getStateVector;
connector.getHB = getHB;
connector.applyHB = applyHB;
connector.receive_handlers.push(function(sender, op) {
if (op.uid.creator !== HB.getUserId()) {
return engine.applyOp(op);
}
});
return connector.setIsBoundToY();
};
module.exports = adaptConnector;

View File

@@ -1,113 +0,0 @@
var Engine;
if (typeof window !== "undefined" && window !== null) {
window.unprocessed_counter = 0;
}
if (typeof window !== "undefined" && window !== null) {
window.unprocessed_exec_counter = 0;
}
if (typeof window !== "undefined" && window !== null) {
window.unprocessed_types = [];
}
Engine = (function() {
function Engine(HB, types) {
this.HB = HB;
this.types = types;
this.unprocessed_ops = [];
}
Engine.prototype.parseOperation = function(json) {
var type;
type = this.types[json.type];
if ((type != null ? type.parse : void 0) != null) {
return type.parse(json);
} else {
throw new Error("You forgot to specify a parser for type " + json.type + ". The message is " + (JSON.stringify(json)) + ".");
}
};
/*
applyOpsBundle: (ops_json)->
ops = []
for o in ops_json
ops.push @parseOperation o
for o in ops
if not o.execute()
@unprocessed_ops.push o
@tryUnprocessed()
*/
Engine.prototype.applyOpsCheckDouble = function(ops_json) {
var o, _i, _len, _results;
_results = [];
for (_i = 0, _len = ops_json.length; _i < _len; _i++) {
o = ops_json[_i];
if (this.HB.getOperation(o.uid) == null) {
_results.push(this.applyOp(o));
} else {
_results.push(void 0);
}
}
return _results;
};
Engine.prototype.applyOps = function(ops_json) {
return this.applyOp(ops_json);
};
Engine.prototype.applyOp = function(op_json_array) {
var o, op_json, _i, _len;
if (op_json_array.constructor !== Array) {
op_json_array = [op_json_array];
}
for (_i = 0, _len = op_json_array.length; _i < _len; _i++) {
op_json = op_json_array[_i];
o = this.parseOperation(op_json);
if (op_json.fromHB != null) {
o.fromHB = op_json.fromHB;
}
if (this.HB.getOperation(o) != null) {
} else if (((!this.HB.isExpectedOperation(o)) && (o.fromHB == null)) || (!o.execute())) {
this.unprocessed_ops.push(o);
if (typeof window !== "undefined" && window !== null) {
window.unprocessed_types.push(o.type);
}
}
}
return this.tryUnprocessed();
};
Engine.prototype.tryUnprocessed = function() {
var old_length, op, unprocessed, _i, _len, _ref;
while (true) {
old_length = this.unprocessed_ops.length;
unprocessed = [];
_ref = this.unprocessed_ops;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
op = _ref[_i];
if (this.HB.getOperation(op) != null) {
} else if ((!this.HB.isExpectedOperation(op) && (op.fromHB == null)) || (!op.execute())) {
unprocessed.push(op);
}
}
this.unprocessed_ops = unprocessed;
if (this.unprocessed_ops.length === old_length) {
break;
}
}
if (this.unprocessed_ops.length !== 0) {
return this.HB.invokeSync();
}
};
return Engine;
})();
module.exports = Engine;

View File

@@ -1,250 +0,0 @@
var HistoryBuffer,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
HistoryBuffer = (function() {
function HistoryBuffer(user_id) {
this.user_id = user_id;
this.emptyGarbage = __bind(this.emptyGarbage, this);
this.operation_counter = {};
this.buffer = {};
this.change_listeners = [];
this.garbage = [];
this.trash = [];
this.performGarbageCollection = true;
this.garbageCollectTimeout = 30000;
this.reserved_identifier_counter = 0;
setTimeout(this.emptyGarbage, this.garbageCollectTimeout);
}
HistoryBuffer.prototype.resetUserId = function(id) {
var o, o_name, own;
own = this.buffer[this.user_id];
if (own != null) {
for (o_name in own) {
o = own[o_name];
o.uid.creator = id;
}
if (this.buffer[id] != null) {
throw new Error("You are re-assigning an old user id - this is not (yet) possible!");
}
this.buffer[id] = own;
delete this.buffer[this.user_id];
}
this.operation_counter[id] = this.operation_counter[this.user_id];
delete this.operation_counter[this.user_id];
return this.user_id = id;
};
HistoryBuffer.prototype.emptyGarbage = function() {
var o, _i, _len, _ref;
_ref = this.garbage;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
o = _ref[_i];
if (typeof o.cleanup === "function") {
o.cleanup();
}
}
this.garbage = this.trash;
this.trash = [];
if (this.garbageCollectTimeout !== -1) {
this.garbageCollectTimeoutId = setTimeout(this.emptyGarbage, this.garbageCollectTimeout);
}
return void 0;
};
HistoryBuffer.prototype.getUserId = function() {
return this.user_id;
};
HistoryBuffer.prototype.addToGarbageCollector = function() {
var o, _i, _len, _results;
if (this.performGarbageCollection) {
_results = [];
for (_i = 0, _len = arguments.length; _i < _len; _i++) {
o = arguments[_i];
if (o != null) {
_results.push(this.garbage.push(o));
} else {
_results.push(void 0);
}
}
return _results;
}
};
HistoryBuffer.prototype.stopGarbageCollection = function() {
this.performGarbageCollection = false;
this.setManualGarbageCollect();
this.garbage = [];
return this.trash = [];
};
HistoryBuffer.prototype.setManualGarbageCollect = function() {
this.garbageCollectTimeout = -1;
clearTimeout(this.garbageCollectTimeoutId);
return this.garbageCollectTimeoutId = void 0;
};
HistoryBuffer.prototype.setGarbageCollectTimeout = function(garbageCollectTimeout) {
this.garbageCollectTimeout = garbageCollectTimeout;
};
HistoryBuffer.prototype.getReservedUniqueIdentifier = function() {
return {
creator: '_',
op_number: "_" + (this.reserved_identifier_counter++),
doSync: false
};
};
HistoryBuffer.prototype.getOperationCounter = function(user_id) {
var ctn, res, user, _ref;
if (user_id == null) {
res = {};
_ref = this.operation_counter;
for (user in _ref) {
ctn = _ref[user];
res[user] = ctn;
}
return res;
} else {
return this.operation_counter[user_id];
}
};
HistoryBuffer.prototype.isExpectedOperation = function(o) {
var _base, _name;
if ((_base = this.operation_counter)[_name = o.uid.creator] == null) {
_base[_name] = 0;
}
o.uid.op_number <= this.operation_counter[o.uid.creator];
return true;
};
HistoryBuffer.prototype._encode = function(state_vector) {
var json, o, o_json, o_next, o_number, o_prev, u_name, unknown, user, _ref;
if (state_vector == null) {
state_vector = {};
}
json = [];
unknown = function(user, o_number) {
if ((user == null) || (o_number == null)) {
throw new Error("dah!");
}
return (state_vector[user] == null) || state_vector[user] <= o_number;
};
_ref = this.buffer;
for (u_name in _ref) {
user = _ref[u_name];
for (o_number in user) {
o = user[o_number];
if (o.uid.doSync && unknown(u_name, o_number)) {
o_json = o._encode();
if (o.next_cl != null) {
o_next = o.next_cl;
while ((o_next.next_cl != null) && unknown(o_next.uid.creator, o_next.uid.op_number)) {
o_next = o_next.next_cl;
}
o_json.next = o_next.getUid();
} else if (o.prev_cl != null) {
o_prev = o.prev_cl;
while ((o_prev.prev_cl != null) && unknown(o_prev.uid.creator, o_prev.uid.op_number)) {
o_prev = o_prev.prev_cl;
}
o_json.prev = o_prev.getUid();
}
json.push(o_json);
}
}
}
return json;
};
HistoryBuffer.prototype.getNextOperationIdentifier = function(user_id) {
var uid;
if (user_id == null) {
user_id = this.user_id;
}
if (this.operation_counter[user_id] == null) {
this.operation_counter[user_id] = 0;
}
uid = {
'creator': user_id,
'op_number': this.operation_counter[user_id],
'doSync': true
};
this.operation_counter[user_id]++;
return uid;
};
HistoryBuffer.prototype.getOperation = function(uid) {
var o, _ref;
if (uid.uid != null) {
uid = uid.uid;
}
o = (_ref = this.buffer[uid.creator]) != null ? _ref[uid.op_number] : void 0;
if ((uid.sub != null) && (o != null)) {
return o.retrieveSub(uid.sub);
} else {
return o;
}
};
HistoryBuffer.prototype.addOperation = function(o) {
if (this.buffer[o.uid.creator] == null) {
this.buffer[o.uid.creator] = {};
}
if (this.buffer[o.uid.creator][o.uid.op_number] != null) {
throw new Error("You must not overwrite operations!");
}
if ((o.uid.op_number.constructor !== String) && (!this.isExpectedOperation(o)) && (o.fromHB == null)) {
throw new Error("this operation was not expected!");
}
this.addToCounter(o);
this.buffer[o.uid.creator][o.uid.op_number] = o;
return o;
};
HistoryBuffer.prototype.removeOperation = function(o) {
var _ref;
return (_ref = this.buffer[o.uid.creator]) != null ? delete _ref[o.uid.op_number] : void 0;
};
HistoryBuffer.prototype.setInvokeSyncHandler = function(f) {
return this.invokeSync = f;
};
HistoryBuffer.prototype.invokeSync = function() {};
HistoryBuffer.prototype.renewStateVector = function(state_vector) {
var state, user, _results;
_results = [];
for (user in state_vector) {
state = state_vector[user];
if ((this.operation_counter[user] == null) || (this.operation_counter[user] < state_vector[user])) {
_results.push(this.operation_counter[user] = state_vector[user]);
} else {
_results.push(void 0);
}
}
return _results;
};
HistoryBuffer.prototype.addToCounter = function(o) {
if (this.operation_counter[o.uid.creator] == null) {
this.operation_counter[o.uid.creator] = 0;
}
if (typeof o.uid.op_number === 'number' && o.uid.creator !== this.getUserId()) {
if (o.uid.op_number === this.operation_counter[o.uid.creator]) {
return this.operation_counter[o.uid.creator]++;
} else {
return this.invokeSync(o.uid.creator);
}
}
};
return HistoryBuffer;
})();
module.exports = HistoryBuffer;

View File

@@ -1,487 +0,0 @@
var __slice = [].slice,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
module.exports = function(HB) {
var execution_listener, types;
types = {};
execution_listener = [];
types.Operation = (function() {
function Operation(uid) {
this.is_deleted = false;
this.garbage_collected = false;
this.event_listeners = [];
if (uid != null) {
this.uid = uid;
}
}
Operation.prototype.type = "Operation";
Operation.prototype.retrieveSub = function() {
throw new Error("sub properties are not enable on this operation type!");
};
Operation.prototype.observe = function(f) {
return this.event_listeners.push(f);
};
Operation.prototype.unobserve = function(f) {
return this.event_listeners = this.event_listeners.filter(function(g) {
return f !== g;
});
};
Operation.prototype.deleteAllObservers = function() {
return this.event_listeners = [];
};
Operation.prototype["delete"] = function() {
(new types.Delete(void 0, this)).execute();
return null;
};
Operation.prototype.callEvent = function() {
return this.forwardEvent.apply(this, [this].concat(__slice.call(arguments)));
};
Operation.prototype.forwardEvent = function() {
var args, f, op, _i, _len, _ref, _results;
op = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
_ref = this.event_listeners;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
f = _ref[_i];
_results.push(f.call.apply(f, [op].concat(__slice.call(args))));
}
return _results;
};
Operation.prototype.isDeleted = function() {
return this.is_deleted;
};
Operation.prototype.applyDelete = function(garbagecollect) {
if (garbagecollect == null) {
garbagecollect = true;
}
if (!this.garbage_collected) {
this.is_deleted = true;
if (garbagecollect) {
this.garbage_collected = true;
return HB.addToGarbageCollector(this);
}
}
};
Operation.prototype.cleanup = function() {
HB.removeOperation(this);
return this.deleteAllObservers();
};
Operation.prototype.setParent = function(parent) {
this.parent = parent;
};
Operation.prototype.getParent = function() {
return this.parent;
};
Operation.prototype.getUid = function() {
if (this.uid.noOperation == null) {
return this.uid;
} else {
return this.uid.alt;
}
};
Operation.prototype.cloneUid = function() {
var n, uid, v, _ref;
uid = {};
_ref = this.getUid();
for (n in _ref) {
v = _ref[n];
uid[n] = v;
}
return uid;
};
Operation.prototype.dontSync = function() {
return this.uid.doSync = false;
};
Operation.prototype.execute = function() {
var l, _i, _len;
this.is_executed = true;
if (this.uid == null) {
this.uid = HB.getNextOperationIdentifier();
}
if (this.uid.noOperation == null) {
HB.addOperation(this);
for (_i = 0, _len = execution_listener.length; _i < _len; _i++) {
l = execution_listener[_i];
l(this._encode());
}
}
return this;
};
Operation.prototype.saveOperation = function(name, op) {
if ((op != null ? op.execute : void 0) != null) {
return this[name] = op;
} else if (op != null) {
if (this.unchecked == null) {
this.unchecked = {};
}
return this.unchecked[name] = op;
}
};
Operation.prototype.validateSavedOperations = function() {
var name, op, op_uid, success, uninstantiated, _ref;
uninstantiated = {};
success = this;
_ref = this.unchecked;
for (name in _ref) {
op_uid = _ref[name];
op = HB.getOperation(op_uid);
if (op) {
this[name] = op;
} else {
uninstantiated[name] = op_uid;
success = false;
}
}
delete this.unchecked;
if (!success) {
this.unchecked = uninstantiated;
}
return success;
};
return Operation;
})();
types.Delete = (function(_super) {
__extends(Delete, _super);
function Delete(uid, deletes) {
this.saveOperation('deletes', deletes);
Delete.__super__.constructor.call(this, uid);
}
Delete.prototype.type = "Delete";
Delete.prototype._encode = function() {
return {
'type': "Delete",
'uid': this.getUid(),
'deletes': this.deletes.getUid()
};
};
Delete.prototype.execute = function() {
var res;
if (this.validateSavedOperations()) {
res = Delete.__super__.execute.apply(this, arguments);
if (res) {
this.deletes.applyDelete(this);
}
return res;
} else {
return false;
}
};
return Delete;
})(types.Operation);
types.Delete.parse = function(o) {
var deletes_uid, uid;
uid = o['uid'], deletes_uid = o['deletes'];
return new this(uid, deletes_uid);
};
types.Insert = (function(_super) {
__extends(Insert, _super);
function Insert(uid, prev_cl, next_cl, origin, parent) {
this.saveOperation('parent', parent);
this.saveOperation('prev_cl', prev_cl);
this.saveOperation('next_cl', next_cl);
if (origin != null) {
this.saveOperation('origin', origin);
} else {
this.saveOperation('origin', prev_cl);
}
Insert.__super__.constructor.call(this, uid);
}
Insert.prototype.type = "Insert";
Insert.prototype.applyDelete = function(o) {
var callLater, garbagecollect, _ref;
if (this.deleted_by == null) {
this.deleted_by = [];
}
callLater = false;
if ((this.parent != null) && !this.isDeleted() && (o != null)) {
callLater = true;
}
if (o != null) {
this.deleted_by.push(o);
}
garbagecollect = false;
if (this.next_cl.isDeleted()) {
garbagecollect = true;
}
Insert.__super__.applyDelete.call(this, garbagecollect);
if (callLater) {
this.callOperationSpecificDeleteEvents(o);
}
if ((_ref = this.prev_cl) != null ? _ref.isDeleted() : void 0) {
return this.prev_cl.applyDelete();
}
};
Insert.prototype.cleanup = function() {
var d, o, _i, _len, _ref;
if (this.next_cl.isDeleted()) {
_ref = this.deleted_by;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
d = _ref[_i];
d.cleanup();
}
o = this.next_cl;
while (o.type !== "Delimiter") {
if (o.origin === this) {
o.origin = this.prev_cl;
}
o = o.next_cl;
}
this.prev_cl.next_cl = this.next_cl;
this.next_cl.prev_cl = this.prev_cl;
return Insert.__super__.cleanup.apply(this, arguments);
}
};
Insert.prototype.getDistanceToOrigin = function() {
var d, o;
d = 0;
o = this.prev_cl;
while (true) {
if (this.origin === o) {
break;
}
d++;
o = o.prev_cl;
}
return d;
};
Insert.prototype.execute = function() {
var distance_to_origin, i, o;
if (!this.validateSavedOperations()) {
return false;
} else {
if (this.parent != null) {
if (this.prev_cl == null) {
this.prev_cl = this.parent.beginning;
}
if (this.origin == null) {
this.origin = this.parent.beginning;
}
if (this.next_cl == null) {
this.next_cl = this.parent.end;
}
}
if (this.prev_cl != null) {
distance_to_origin = this.getDistanceToOrigin();
o = this.prev_cl.next_cl;
i = distance_to_origin;
while (true) {
if (o !== this.next_cl) {
if (o.getDistanceToOrigin() === i) {
if (o.uid.creator < this.uid.creator) {
this.prev_cl = o;
distance_to_origin = i + 1;
} else {
}
} else if (o.getDistanceToOrigin() < i) {
if (i - distance_to_origin <= o.getDistanceToOrigin()) {
this.prev_cl = o;
distance_to_origin = i + 1;
} else {
}
} else {
break;
}
i++;
o = o.next_cl;
} else {
break;
}
}
this.next_cl = this.prev_cl.next_cl;
this.prev_cl.next_cl = this;
this.next_cl.prev_cl = this;
}
this.setParent(this.prev_cl.getParent());
Insert.__super__.execute.apply(this, arguments);
this.callOperationSpecificInsertEvents();
return this;
}
};
Insert.prototype.callOperationSpecificInsertEvents = function() {
var _ref;
return (_ref = this.parent) != null ? _ref.callEvent([
{
type: "insert",
position: this.getPosition(),
object: this.parent,
changedBy: this.uid.creator,
value: this.content
}
]) : void 0;
};
Insert.prototype.callOperationSpecificDeleteEvents = function(o) {
return this.parent.callEvent([
{
type: "delete",
position: this.getPosition(),
object: this.parent,
length: 1,
changedBy: o.uid.creator
}
]);
};
Insert.prototype.getPosition = function() {
var position, prev;
position = 0;
prev = this.prev_cl;
while (true) {
if (prev instanceof types.Delimiter) {
break;
}
if (!prev.isDeleted()) {
position++;
}
prev = prev.prev_cl;
}
return position;
};
return Insert;
})(types.Operation);
types.ImmutableObject = (function(_super) {
__extends(ImmutableObject, _super);
function ImmutableObject(uid, content) {
this.content = content;
ImmutableObject.__super__.constructor.call(this, uid);
}
ImmutableObject.prototype.type = "ImmutableObject";
ImmutableObject.prototype.val = function() {
return this.content;
};
ImmutableObject.prototype._encode = function() {
var json;
json = {
'type': this.type,
'uid': this.getUid(),
'content': this.content
};
return json;
};
return ImmutableObject;
})(types.Operation);
types.ImmutableObject.parse = function(json) {
var content, uid;
uid = json['uid'], content = json['content'];
return new this(uid, content);
};
types.Delimiter = (function(_super) {
__extends(Delimiter, _super);
function Delimiter(prev_cl, next_cl, origin) {
this.saveOperation('prev_cl', prev_cl);
this.saveOperation('next_cl', next_cl);
this.saveOperation('origin', prev_cl);
Delimiter.__super__.constructor.call(this, {
noOperation: true
});
}
Delimiter.prototype.type = "Delimiter";
Delimiter.prototype.applyDelete = function() {
var o;
Delimiter.__super__.applyDelete.call(this);
o = this.prev_cl;
while (o != null) {
o.applyDelete();
o = o.prev_cl;
}
return void 0;
};
Delimiter.prototype.cleanup = function() {
return Delimiter.__super__.cleanup.call(this);
};
Delimiter.prototype.execute = function() {
var _ref, _ref1;
if (((_ref = this.unchecked) != null ? _ref['next_cl'] : void 0) != null) {
return Delimiter.__super__.execute.apply(this, arguments);
} else if ((_ref1 = this.unchecked) != null ? _ref1['prev_cl'] : void 0) {
if (this.validateSavedOperations()) {
if (this.prev_cl.next_cl != null) {
throw new Error("Probably duplicated operations");
}
this.prev_cl.next_cl = this;
return Delimiter.__super__.execute.apply(this, arguments);
} else {
return false;
}
} else if ((this.prev_cl != null) && (this.prev_cl.next_cl == null)) {
delete this.prev_cl.unchecked.next_cl;
this.prev_cl.next_cl = this;
return Delimiter.__super__.execute.apply(this, arguments);
} else if ((this.prev_cl != null) || (this.next_cl != null) || true) {
return Delimiter.__super__.execute.apply(this, arguments);
}
};
Delimiter.prototype._encode = function() {
var _ref, _ref1;
return {
'type': this.type,
'uid': this.getUid(),
'prev': (_ref = this.prev_cl) != null ? _ref.getUid() : void 0,
'next': (_ref1 = this.next_cl) != null ? _ref1.getUid() : void 0
};
};
return Delimiter;
})(types.Operation);
types.Delimiter.parse = function(json) {
var next, prev, uid;
uid = json['uid'], prev = json['prev'], next = json['next'];
return new this(uid, prev, next);
};
return {
'types': types,
'execution_listener': execution_listener
};
};

View File

@@ -1,158 +0,0 @@
var text_types_uninitialized,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
text_types_uninitialized = require("./TextTypes");
module.exports = function(HB) {
var text_types, types;
text_types = text_types_uninitialized(HB);
types = text_types.types;
types.Object = (function(_super) {
__extends(Object, _super);
function Object() {
return Object.__super__.constructor.apply(this, arguments);
}
Object.prototype.type = "Object";
Object.prototype.applyDelete = function() {
return Object.__super__.applyDelete.call(this);
};
Object.prototype.cleanup = function() {
return Object.__super__.cleanup.call(this);
};
Object.prototype.toJson = function(transform_to_value) {
var json, name, o, that, val;
if (transform_to_value == null) {
transform_to_value = false;
}
if ((this.bound_json == null) || (Object.observe == null) || true) {
val = this.val();
json = {};
for (name in val) {
o = val[name];
if (o instanceof types.Object) {
json[name] = o.toJson(transform_to_value);
} else if (o instanceof types.Array) {
json[name] = o.toJson(transform_to_value);
} else if (transform_to_value && o instanceof types.Operation) {
json[name] = o.val();
} else {
json[name] = o;
}
}
this.bound_json = json;
if (Object.observe != null) {
that = this;
Object.observe(this.bound_json, function(events) {
var event, _i, _len, _results;
_results = [];
for (_i = 0, _len = events.length; _i < _len; _i++) {
event = events[_i];
if ((event.changedBy == null) && (event.type === "add" || (event.type = "update"))) {
_results.push(that.val(event.name, event.object[event.name]));
} else {
_results.push(void 0);
}
}
return _results;
});
this.observe(function(events) {
var event, notifier, oldVal, _i, _len, _results;
_results = [];
for (_i = 0, _len = events.length; _i < _len; _i++) {
event = events[_i];
if (event.created_ !== HB.getUserId()) {
notifier = Object.getNotifier(that.bound_json);
oldVal = that.bound_json[event.name];
if (oldVal != null) {
notifier.performChange('update', function() {
return that.bound_json[event.name] = that.val(event.name);
}, that.bound_json);
_results.push(notifier.notify({
object: that.bound_json,
type: 'update',
name: event.name,
oldValue: oldVal,
changedBy: event.changedBy
}));
} else {
notifier.performChange('add', function() {
return that.bound_json[event.name] = that.val(event.name);
}, that.bound_json);
_results.push(notifier.notify({
object: that.bound_json,
type: 'add',
name: event.name,
oldValue: oldVal,
changedBy: event.changedBy
}));
}
} else {
_results.push(void 0);
}
}
return _results;
});
}
}
return this.bound_json;
};
Object.prototype.val = function(name, content) {
var args, i, o, type, _i, _ref;
if ((name != null) && arguments.length > 1) {
if ((content != null) && (content.constructor != null)) {
type = types[content.constructor.name];
if ((type != null) && (type.create != null)) {
args = [];
for (i = _i = 1, _ref = arguments.length; 1 <= _ref ? _i < _ref : _i > _ref; i = 1 <= _ref ? ++_i : --_i) {
args.push(arguments[i]);
}
o = type.create.apply(null, args);
return Object.__super__.val.call(this, name, o);
} else {
throw new Error("The " + content.constructor.name + "-type is not (yet) supported in Y.");
}
} else {
return Object.__super__.val.call(this, name, content);
}
} else {
return Object.__super__.val.call(this, name);
}
};
Object.prototype._encode = function() {
return {
'type': this.type,
'uid': this.getUid()
};
};
return Object;
})(types.MapManager);
types.Object.parse = function(json) {
var uid;
uid = json['uid'];
return new this(uid);
};
types.Object.create = function(content, mutable) {
var json, n, o;
json = new types.Object().execute();
for (n in content) {
o = content[n];
json.val(n, o, mutable);
}
return json;
};
types.Number = {};
types.Number.create = function(content) {
return content;
};
return text_types;
};

View File

@@ -1,354 +0,0 @@
var basic_types_uninitialized,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
basic_types_uninitialized = require("./BasicTypes");
module.exports = function(HB) {
var basic_types, types;
basic_types = basic_types_uninitialized(HB);
types = basic_types.types;
types.MapManager = (function(_super) {
__extends(MapManager, _super);
function MapManager(uid) {
this.map = {};
MapManager.__super__.constructor.call(this, uid);
}
MapManager.prototype.type = "MapManager";
MapManager.prototype.applyDelete = function() {
var name, p, _ref;
_ref = this.map;
for (name in _ref) {
p = _ref[name];
p.applyDelete();
}
return MapManager.__super__.applyDelete.call(this);
};
MapManager.prototype.cleanup = function() {
return MapManager.__super__.cleanup.call(this);
};
MapManager.prototype.val = function(name, content) {
var o, prop, result, _ref;
if (arguments.length > 1) {
this.retrieveSub(name).replace(content);
return this;
} else if (name != null) {
prop = this.map[name];
if ((prop != null) && !prop.isContentDeleted()) {
return prop.val();
} else {
return void 0;
}
} else {
result = {};
_ref = this.map;
for (name in _ref) {
o = _ref[name];
if (!o.isContentDeleted()) {
result[name] = o.val();
}
}
return result;
}
};
MapManager.prototype["delete"] = function(name) {
var _ref;
if ((_ref = this.map[name]) != null) {
_ref.deleteContent();
}
return this;
};
MapManager.prototype.retrieveSub = function(property_name) {
var event_properties, event_this, map_uid, rm, rm_uid;
if (this.map[property_name] == null) {
event_properties = {
name: property_name
};
event_this = this;
map_uid = this.cloneUid();
map_uid.sub = property_name;
rm_uid = {
noOperation: true,
alt: map_uid
};
rm = new types.ReplaceManager(event_properties, event_this, rm_uid);
this.map[property_name] = rm;
rm.setParent(this, property_name);
rm.execute();
}
return this.map[property_name];
};
return MapManager;
})(types.Operation);
types.ListManager = (function(_super) {
__extends(ListManager, _super);
function ListManager(uid) {
this.beginning = new types.Delimiter(void 0, void 0);
this.end = new types.Delimiter(this.beginning, void 0);
this.beginning.next_cl = this.end;
this.beginning.execute();
this.end.execute();
ListManager.__super__.constructor.call(this, uid);
}
ListManager.prototype.type = "ListManager";
ListManager.prototype.execute = function() {
if (this.validateSavedOperations()) {
this.beginning.setParent(this);
this.end.setParent(this);
return ListManager.__super__.execute.apply(this, arguments);
} else {
return false;
}
};
ListManager.prototype.getLastOperation = function() {
return this.end.prev_cl;
};
ListManager.prototype.getFirstOperation = function() {
return this.beginning.next_cl;
};
ListManager.prototype.toArray = function() {
var o, result;
o = this.beginning.next_cl;
result = [];
while (o !== this.end) {
result.push(o);
o = o.next_cl;
}
return result;
};
ListManager.prototype.getOperationByPosition = function(position) {
var o;
o = this.beginning;
while (true) {
if (o instanceof types.Delimiter && (o.prev_cl != null)) {
o = o.prev_cl;
while (o.isDeleted() || !(o instanceof types.Delimiter)) {
o = o.prev_cl;
}
break;
}
if (position <= 0 && !o.isDeleted()) {
break;
}
o = o.next_cl;
if (!o.isDeleted()) {
position -= 1;
}
}
return o;
};
return ListManager;
})(types.Operation);
types.ReplaceManager = (function(_super) {
__extends(ReplaceManager, _super);
function ReplaceManager(event_properties, event_this, uid, beginning, end) {
this.event_properties = event_properties;
this.event_this = event_this;
if (this.event_properties['object'] == null) {
this.event_properties['object'] = this.event_this;
}
ReplaceManager.__super__.constructor.call(this, uid, beginning, end);
}
ReplaceManager.prototype.type = "ReplaceManager";
ReplaceManager.prototype.applyDelete = function() {
var o;
o = this.beginning;
while (o != null) {
o.applyDelete();
o = o.next_cl;
}
return ReplaceManager.__super__.applyDelete.call(this);
};
ReplaceManager.prototype.cleanup = function() {
return ReplaceManager.__super__.cleanup.call(this);
};
ReplaceManager.prototype.callEventDecorator = function(events) {
var event, name, prop, _i, _len, _ref;
if (!this.isDeleted()) {
for (_i = 0, _len = events.length; _i < _len; _i++) {
event = events[_i];
_ref = this.event_properties;
for (name in _ref) {
prop = _ref[name];
event[name] = prop;
}
}
this.event_this.callEvent(events);
}
return void 0;
};
ReplaceManager.prototype.replace = function(content, replaceable_uid) {
var o, relp;
o = this.getLastOperation();
relp = (new types.Replaceable(content, this, replaceable_uid, o, o.next_cl)).execute();
return void 0;
};
ReplaceManager.prototype.isContentDeleted = function() {
return this.getLastOperation().isDeleted();
};
ReplaceManager.prototype.deleteContent = function() {
(new types.Delete(void 0, this.getLastOperation().uid)).execute();
return void 0;
};
ReplaceManager.prototype.val = function() {
var o;
o = this.getLastOperation();
return typeof o.val === "function" ? o.val() : void 0;
};
ReplaceManager.prototype._encode = function() {
var json;
json = {
'type': this.type,
'uid': this.getUid(),
'beginning': this.beginning.getUid(),
'end': this.end.getUid()
};
return json;
};
return ReplaceManager;
})(types.ListManager);
types.Replaceable = (function(_super) {
__extends(Replaceable, _super);
function Replaceable(content, parent, uid, prev, next, origin, is_deleted) {
if ((content != null) && (content.creator != null)) {
this.saveOperation('content', content);
} else {
this.content = content;
}
this.saveOperation('parent', parent);
Replaceable.__super__.constructor.call(this, uid, prev, next, origin);
this.is_deleted = is_deleted;
}
Replaceable.prototype.type = "Replaceable";
Replaceable.prototype.val = function() {
return this.content;
};
Replaceable.prototype.applyDelete = function() {
var res, _base, _base1, _base2;
res = Replaceable.__super__.applyDelete.apply(this, arguments);
if (this.content != null) {
if (this.next_cl.type !== "Delimiter") {
if (typeof (_base = this.content).deleteAllObservers === "function") {
_base.deleteAllObservers();
}
}
if (typeof (_base1 = this.content).applyDelete === "function") {
_base1.applyDelete();
}
if (typeof (_base2 = this.content).dontSync === "function") {
_base2.dontSync();
}
}
this.content = null;
return res;
};
Replaceable.prototype.cleanup = function() {
return Replaceable.__super__.cleanup.apply(this, arguments);
};
Replaceable.prototype.callOperationSpecificInsertEvents = function() {
var old_value;
if (this.next_cl.type === "Delimiter" && this.prev_cl.type !== "Delimiter") {
if (!this.is_deleted) {
old_value = this.prev_cl.content;
this.parent.callEventDecorator([
{
type: "update",
changedBy: this.uid.creator,
oldValue: old_value
}
]);
}
this.prev_cl.applyDelete();
} else if (this.next_cl.type !== "Delimiter") {
this.applyDelete();
} else {
this.parent.callEventDecorator([
{
type: "add",
changedBy: this.uid.creator
}
]);
}
return void 0;
};
Replaceable.prototype.callOperationSpecificDeleteEvents = function(o) {
if (this.next_cl.type === "Delimiter") {
return this.parent.callEventDecorator([
{
type: "delete",
changedBy: o.uid.creator,
oldValue: this.content
}
]);
}
};
Replaceable.prototype._encode = function() {
var json;
json = {
'type': this.type,
'parent': this.parent.getUid(),
'prev': this.prev_cl.getUid(),
'next': this.next_cl.getUid(),
'origin': this.origin.getUid(),
'uid': this.getUid(),
'is_deleted': this.is_deleted
};
if (this.content instanceof types.Operation) {
json['content'] = this.content.getUid();
} else {
if ((this.content != null) && (this.content.creator != null)) {
throw new Error("You must not set creator here!");
}
json['content'] = this.content;
}
return json;
};
return Replaceable;
})(types.Insert);
types.Replaceable.parse = function(json) {
var content, is_deleted, next, origin, parent, prev, uid;
content = json['content'], parent = json['parent'], uid = json['uid'], prev = json['prev'], next = json['next'], origin = json['origin'], is_deleted = json['is_deleted'];
return new this(content, parent, uid, prev, next, origin, is_deleted);
};
return basic_types;
};

View File

@@ -1,558 +0,0 @@
var structured_types_uninitialized,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
structured_types_uninitialized = require("./StructuredTypes");
module.exports = function(HB) {
var parser, structured_types, types;
structured_types = structured_types_uninitialized(HB);
types = structured_types.types;
parser = structured_types.parser;
types.TextInsert = (function(_super) {
__extends(TextInsert, _super);
function TextInsert(content, uid, prev, next, origin, parent) {
if (content != null ? content.creator : void 0) {
this.saveOperation('content', content);
} else {
this.content = content;
}
TextInsert.__super__.constructor.call(this, uid, prev, next, origin, parent);
}
TextInsert.prototype.type = "TextInsert";
TextInsert.prototype.getLength = function() {
if (this.isDeleted()) {
return 0;
} else {
return this.content.length;
}
};
TextInsert.prototype.applyDelete = function() {
TextInsert.__super__.applyDelete.apply(this, arguments);
if (this.content instanceof types.Operation) {
this.content.applyDelete();
}
return this.content = null;
};
TextInsert.prototype.execute = function() {
if (!this.validateSavedOperations()) {
return false;
} else {
if (this.content instanceof types.Operation) {
this.content.insert_parent = this;
}
return TextInsert.__super__.execute.call(this);
}
};
TextInsert.prototype.val = function(current_position) {
if (this.isDeleted() || (this.content == null)) {
return "";
} else {
return this.content;
}
};
TextInsert.prototype._encode = function() {
var json, _ref;
json = {
'type': this.type,
'uid': this.getUid(),
'prev': this.prev_cl.getUid(),
'next': this.next_cl.getUid(),
'origin': this.origin.getUid(),
'parent': this.parent.getUid()
};
if (((_ref = this.content) != null ? _ref.getUid : void 0) != null) {
json['content'] = this.content.getUid();
} else {
json['content'] = this.content;
}
return json;
};
return TextInsert;
})(types.Insert);
types.TextInsert.parse = function(json) {
var content, next, origin, parent, prev, uid;
content = json['content'], uid = json['uid'], prev = json['prev'], next = json['next'], origin = json['origin'], parent = json['parent'];
return new types.TextInsert(content, uid, prev, next, origin, parent);
};
types.Array = (function(_super) {
__extends(Array, _super);
function Array() {
return Array.__super__.constructor.apply(this, arguments);
}
Array.prototype.type = "Array";
Array.prototype.applyDelete = function() {
var o;
o = this.end;
while (o != null) {
o.applyDelete();
o = o.prev_cl;
}
return Array.__super__.applyDelete.call(this);
};
Array.prototype.cleanup = function() {
return Array.__super__.cleanup.call(this);
};
Array.prototype.toJson = function(transform_to_value) {
var i, o, val, _i, _len, _results;
if (transform_to_value == null) {
transform_to_value = false;
}
val = this.val();
_results = [];
for (o = _i = 0, _len = val.length; _i < _len; o = ++_i) {
i = val[o];
if (o instanceof types.Object) {
_results.push(o.toJson(transform_to_value));
} else if (o instanceof types.Array) {
_results.push(o.toJson(transform_to_value));
} else if (transform_to_value && o instanceof types.Operation) {
_results.push(o.val());
} else {
_results.push(o);
}
}
return _results;
};
Array.prototype.val = function(pos) {
var o, result;
if (pos != null) {
o = this.getOperationByPosition(pos + 1);
if (!(o instanceof types.Delimiter)) {
return o.val();
} else {
throw new Error("this position does not exist");
}
} else {
o = this.beginning.next_cl;
result = [];
while (o !== this.end) {
if (!o.isDeleted()) {
result.push(o.val());
}
o = o.next_cl;
}
return result;
}
};
Array.prototype.push = function(content) {
return this.insertAfter(this.end.prev_cl, content);
};
Array.prototype.insertAfter = function(left, content, options) {
var c, createContent, right, tmp, _i, _len;
createContent = function(content, options) {
var type;
if ((content != null) && (content.constructor != null)) {
type = types[content.constructor.name];
if ((type != null) && (type.create != null)) {
return type.create(content, options);
} else {
throw new Error("The " + content.constructor.name + "-type is not (yet) supported in Y.");
}
} else {
return content;
}
};
right = left.next_cl;
while (right.isDeleted()) {
right = right.next_cl;
}
left = right.prev_cl;
if (content instanceof types.Operation) {
(new types.TextInsert(content, void 0, left, right)).execute();
} else {
for (_i = 0, _len = content.length; _i < _len; _i++) {
c = content[_i];
tmp = (new types.TextInsert(createContent(c, options), void 0, left, right)).execute();
left = tmp;
}
}
return this;
};
Array.prototype.insert = function(position, content, options) {
var ith;
ith = this.getOperationByPosition(position);
return this.insertAfter(ith, [content], options);
};
Array.prototype["delete"] = function(position, length) {
var d, delete_ops, i, o, _i;
o = this.getOperationByPosition(position + 1);
delete_ops = [];
for (i = _i = 0; 0 <= length ? _i < length : _i > length; i = 0 <= length ? ++_i : --_i) {
if (o instanceof types.Delimiter) {
break;
}
d = (new types.Delete(void 0, o)).execute();
o = o.next_cl;
while ((!(o instanceof types.Delimiter)) && o.isDeleted()) {
o = o.next_cl;
}
delete_ops.push(d._encode());
}
return this;
};
Array.prototype._encode = function() {
var json;
json = {
'type': this.type,
'uid': this.getUid()
};
return json;
};
return Array;
})(types.ListManager);
types.Array.parse = function(json) {
var uid;
uid = json['uid'];
return new this(uid);
};
types.Array.create = function(content, mutable) {
var ith, list;
if (mutable === "mutable") {
list = new types.Array().execute();
ith = list.getOperationByPosition(0);
list.insertAfter(ith, content);
return list;
} else if ((mutable == null) || (mutable === "immutable")) {
return content;
} else {
throw new Error("Specify either \"mutable\" or \"immutable\"!!");
}
};
types.String = (function(_super) {
__extends(String, _super);
function String(uid) {
this.textfields = [];
String.__super__.constructor.call(this, uid);
}
String.prototype.type = "String";
String.prototype.val = function() {
var c, o;
c = (function() {
var _i, _len, _ref, _results;
_ref = this.toArray();
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
o = _ref[_i];
if (o.val != null) {
_results.push(o.val());
} else {
_results.push("");
}
}
return _results;
}).call(this);
return c.join('');
};
String.prototype.toString = function() {
return this.val();
};
String.prototype.insert = function(position, content, options) {
var ith;
ith = this.getOperationByPosition(position);
return this.insertAfter(ith, content, options);
};
String.prototype.bind = function(textfield, dom_root) {
var createRange, creator_token, t, word, writeContent, writeRange, _i, _len, _ref;
if (dom_root == null) {
dom_root = window;
}
if (dom_root.getSelection == null) {
dom_root = window;
}
_ref = this.textfields;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
t = _ref[_i];
if (t === textfield) {
return;
}
}
creator_token = false;
word = this;
textfield.value = this.val();
this.textfields.push(textfield);
if ((textfield.selectionStart != null) && (textfield.setSelectionRange != null)) {
createRange = function(fix) {
var left, right;
left = textfield.selectionStart;
right = textfield.selectionEnd;
if (fix != null) {
left = fix(left);
right = fix(right);
}
return {
left: left,
right: right
};
};
writeRange = function(range) {
writeContent(word.val());
return textfield.setSelectionRange(range.left, range.right);
};
writeContent = function(content) {
return textfield.value = content;
};
} else {
createRange = function(fix) {
var clength, left, right, s;
s = dom_root.getSelection();
clength = textfield.textContent.length;
left = Math.min(s.anchorOffset, clength);
right = Math.min(s.focusOffset, clength);
if (fix != null) {
left = fix(left);
right = fix(right);
}
return {
left: left,
right: right,
isReal: true
};
};
writeRange = function(range) {
var r, s, textnode;
writeContent(word.val());
textnode = textfield.childNodes[0];
if (range.isReal && (textnode != null)) {
if (range.left < 0) {
range.left = 0;
}
range.right = Math.max(range.left, range.right);
if (range.right > textnode.length) {
range.right = textnode.length;
}
range.left = Math.min(range.left, range.right);
r = document.createRange();
r.setStart(textnode, range.left);
r.setEnd(textnode, range.right);
s = window.getSelection();
s.removeAllRanges();
return s.addRange(r);
}
};
writeContent = function(content) {
var append;
append = "";
if (content[content.length - 1] === " ") {
content = content.slice(0, content.length - 1);
append = '&nbsp;';
}
textfield.textContent = content;
return textfield.innerHTML += append;
};
}
writeContent(this.val());
this.observe(function(events) {
var event, fix, o_pos, r, _j, _len1, _results;
_results = [];
for (_j = 0, _len1 = events.length; _j < _len1; _j++) {
event = events[_j];
if (!creator_token) {
if (event.type === "insert") {
o_pos = event.position;
fix = function(cursor) {
if (cursor <= o_pos) {
return cursor;
} else {
cursor += 1;
return cursor;
}
};
r = createRange(fix);
_results.push(writeRange(r));
} else if (event.type === "delete") {
o_pos = event.position;
fix = function(cursor) {
if (cursor < o_pos) {
return cursor;
} else {
cursor -= 1;
return cursor;
}
};
r = createRange(fix);
_results.push(writeRange(r));
} else {
_results.push(void 0);
}
} else {
_results.push(void 0);
}
}
return _results;
});
textfield.onkeypress = function(event) {
var char, diff, pos, r;
if (word.is_deleted) {
textfield.onkeypress = null;
return true;
}
creator_token = true;
char = null;
if (event.key != null) {
if (event.charCode === 32) {
char = " ";
} else if (event.keyCode === 13) {
char = '\n';
} else {
char = event.key;
}
} else {
char = window.String.fromCharCode(event.keyCode);
}
if (char.length > 1) {
return true;
} else if (char.length > 0) {
r = createRange();
pos = Math.min(r.left, r.right);
diff = Math.abs(r.right - r.left);
word["delete"](pos, diff);
word.insert(pos, char);
r.left = pos + char.length;
r.right = r.left;
writeRange(r);
}
event.preventDefault();
creator_token = false;
return false;
};
textfield.onpaste = function(event) {
if (word.is_deleted) {
textfield.onpaste = null;
return true;
}
return event.preventDefault();
};
textfield.oncut = function(event) {
if (word.is_deleted) {
textfield.oncut = null;
return true;
}
return event.preventDefault();
};
return textfield.onkeydown = function(event) {
var del_length, diff, new_pos, pos, r, val;
creator_token = true;
if (word.is_deleted) {
textfield.onkeydown = null;
return true;
}
r = createRange();
pos = Math.min(r.left, r.right, word.val().length);
diff = Math.abs(r.left - r.right);
if ((event.keyCode != null) && event.keyCode === 8) {
if (diff > 0) {
word["delete"](pos, diff);
r.left = pos;
r.right = pos;
writeRange(r);
} else {
if ((event.ctrlKey != null) && event.ctrlKey) {
val = word.val();
new_pos = pos;
del_length = 0;
if (pos > 0) {
new_pos--;
del_length++;
}
while (new_pos > 0 && val[new_pos] !== " " && val[new_pos] !== '\n') {
new_pos--;
del_length++;
}
word["delete"](new_pos, pos - new_pos);
r.left = new_pos;
r.right = new_pos;
writeRange(r);
} else {
if (pos > 0) {
word["delete"](pos - 1, 1);
r.left = pos - 1;
r.right = pos - 1;
writeRange(r);
}
}
}
event.preventDefault();
creator_token = false;
return false;
} else if ((event.keyCode != null) && event.keyCode === 46) {
if (diff > 0) {
word["delete"](pos, diff);
r.left = pos;
r.right = pos;
writeRange(r);
} else {
word["delete"](pos, 1);
r.left = pos;
r.right = pos;
writeRange(r);
}
event.preventDefault();
creator_token = false;
return false;
} else {
creator_token = false;
return true;
}
};
};
String.prototype._encode = function() {
var json;
json = {
'type': this.type,
'uid': this.getUid()
};
return json;
};
return String;
})(types.Array);
types.String.parse = function(json) {
var uid;
uid = json['uid'];
return new this(uid);
};
types.String.create = function(content, mutable) {
var word;
if (mutable === "mutable") {
word = new types.String().execute();
word.insert(0, content);
return word;
} else if ((mutable == null) || (mutable === "immutable")) {
return content;
} else {
throw new Error("Specify either \"mutable\" or \"immutable\"!!");
}
};
return structured_types;
};

View File

@@ -1,92 +0,0 @@
var Y, bindToChildren;
Y = require('./y');
bindToChildren = function(that) {
var attr, i, _i, _ref;
for (i = _i = 0, _ref = that.children.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
attr = that.children.item(i);
if (attr.name != null) {
attr.val = that.val.val(attr.name);
}
}
return that.val.observe(function(events) {
var event, newVal, _j, _len, _results;
_results = [];
for (_j = 0, _len = events.length; _j < _len; _j++) {
event = events[_j];
if (event.name != null) {
_results.push((function() {
var _k, _ref1, _results1;
_results1 = [];
for (i = _k = 0, _ref1 = that.children.length; 0 <= _ref1 ? _k < _ref1 : _k > _ref1; i = 0 <= _ref1 ? ++_k : --_k) {
attr = that.children.item(i);
if ((attr.name != null) && attr.name === event.name) {
newVal = that.val.val(attr.name);
if (attr.val !== newVal) {
_results1.push(attr.val = newVal);
} else {
_results1.push(void 0);
}
} else {
_results1.push(void 0);
}
}
return _results1;
})());
} else {
_results.push(void 0);
}
}
return _results;
});
};
Polymer("y-object", {
ready: function() {
if (this.connector != null) {
this.val = new Y(this.connector);
return bindToChildren(this);
} else if (this.val != null) {
return bindToChildren(this);
}
},
valChanged: function() {
if ((this.val != null) && this.val.type === "Object") {
return bindToChildren(this);
}
},
connectorChanged: function() {
if (this.val == null) {
this.val = new Y(this.connector);
return bindToChildren(this);
}
}
});
Polymer("y-property", {
ready: function() {
if ((this.val != null) && (this.name != null)) {
if (this.val.constructor === Object) {
this.val = this.parentElement.val(this.name, this.val).val(this.name);
} else if (typeof this.val === "string") {
this.parentElement.val(this.name, this.val);
}
if (this.val.type === "Object") {
return bindToChildren(this);
}
}
},
valChanged: function() {
var _ref;
if ((this.val != null) && (this.name != null)) {
if (this.val.constructor === Object) {
return this.val = this.parentElement.val.val(this.name, this.val).val(this.name);
} else if (this.val.type === "Object") {
return bindToChildren(this);
} else if ((((_ref = this.parentElement.val) != null ? _ref.val : void 0) != null) && this.val !== this.parentElement.val.val(this.name)) {
return this.parentElement.val.val(this.name, this.val);
}
}
}
});

View File

@@ -1,54 +0,0 @@
var Engine, HistoryBuffer, adaptConnector, createY, json_types_uninitialized,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
json_types_uninitialized = require("./Types/JsonTypes");
HistoryBuffer = require("./HistoryBuffer");
Engine = require("./Engine");
adaptConnector = require("./ConnectorAdapter");
createY = function(connector) {
var HB, Y, type_manager, types, user_id;
user_id = null;
if (connector.id != null) {
user_id = connector.id;
} else {
user_id = "_temp";
connector.onUserIdSet(function(id) {
user_id = id;
return HB.resetUserId(id);
});
}
HB = new HistoryBuffer(user_id);
type_manager = json_types_uninitialized(HB);
types = type_manager.types;
Y = (function(_super) {
__extends(Y, _super);
function Y() {
this.connector = connector;
this.HB = HB;
this.types = types;
this.engine = new Engine(this.HB, type_manager.types);
adaptConnector(this.connector, this.engine, this.HB, type_manager.execution_listener);
Y.__super__.constructor.apply(this, arguments);
}
Y.prototype.getConnector = function() {
return this.connector;
};
return Y;
})(types.Object);
return new Y(HB.getReservedUniqueIdentifier()).execute();
};
module.exports = createY;
if ((typeof window !== "undefined" && window !== null) && (window.Y == null)) {
window.Y = createY;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,27 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Test Yatta!</title>
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css" />
</head>
<body>
<div id="mocha"></div>
<div id="test_dom" test_attribute="the test" class="stuffy" style="color: blue"><p id="replaceme">replace me</p><p id="removeme">remove me</p><p>This is a test object for <b>XmlFramework</b></p><span class="span_element"><p>span</p></span></div>
<script src="../../node_modules/mocha/mocha.js" class="awesome"></script>
<script>
mocha.setup('bdd');
mocha.ui('bdd');
mocha.reporter('html');
</script>
<script src="TextYatta_test.js"></script>
<script src="JsonYatta_test.js"></script>
<!--script src="XmlYatta_test_browser.js"></script-->
<script>
//mocha.checkLeaks();
//mocha.run();
window.onerror = null;
if (window.mochaPhantomJS) { mochaPhantomJS.run(); }
else { mocha.run(); }
</script>
</body>
</html>

View File

@@ -1,5 +0,0 @@
# Examples
Here you find some (hopefully) usefull examples on how to use Yatta!
Please note, that the XMPP Connector is the best supported Connector at the moment.

View File

@@ -1,15 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset=utf-8 />
<title>Y Example</title>
<script src="../../../webcomponentsjs/webcomponents.min.js"></script>
<link rel="import" href="../../../polymer/polymer.html">
<link rel="import" href="y-test.html">
</head>
<body>
<y-test></y-test>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,19 +0,0 @@
setTimeout(function(){
window.y_test = document.querySelector("y-test");
window.y_test.y.val("stuff",{otherstuff:{nostuff:"this is no stuff"}})
setTimeout(function(){
var res = y_test.y.val("stuff");
if(!(y_test.nostuff === "this is no stuff")){
console.log("Deep inherit doesn't work!")
}
window.y_stuff_property.val = {nostuff: "this is also no stuff"};
setTimeout(function(){
if(!(y_test.nostuff === "this is also no stuff")){
console.log("Element val overwrite doesn't work")
}
console.log("Everything is fine :)");
},500)
},500);
},3000)

View File

@@ -1,38 +0,0 @@
<link rel="import" href="../../y-object.html">
<link rel="import" href="../../../y-connectors/y-xmpp/y-xmpp.html">
<link rel="import" href="../../../paper-slider/paper-slider.html">
<polymer-element name="y-test" attributes="y connector stuff">
<template>
<h1 id="text" contentEditable> Check this out !</h1>
<y-xmpp id="connector" connector={{connector}} room="testy-xmpp-polymer"></y-xmpp>
<y-object connector={{connector}} val={{y}}>
<y-property name="slider" val={{slider}}>
</y-property>
<y-property name="stuff" val={{stuff}}>
<y-property id="otherstuff" name="otherstuff" val={{otherstuff}}>
</y-property>
</y-property>
</y-object>
<y-object val={{otherstuff}}>
<y-property name="nostuff" val={{nostuff}}>
</y-property>
</y-object>
<paper-slider min="0" max="200" immediateValue={{slider}}></paper-slider>
</template>
<script>
Polymer({
ready: function(){
window.y_stuff_property = this.$.otherstuff;
this.y.val("slider",50)
var that = this;
this.connector.whenSynced(function(){
if(that.y.val("text") == null){
that.y.val("text","stuff","mutable");
}
that.y.val("text").bind(that.$.text,that.shadowRoot)
})
}
})
</script>
</polymer-element>

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Y Example</title>
<script src="../../build/browser/y.js"></script>
<script src="../../../y-connectors/y-xmpp/y-xmpp.js"></script>
<script src="./index.js"></script>
</head>
<body>
<h1 contentEditable> yjs Tutorial</h1>
<p> Collaborative Json editing with <a href="https://github.com/rwth-acis/yjs/">yjs</a>
and XMPP Connector. </p>
<textarea style="width:80%;" rows=40 id="textfield"></textarea>
<p> <a href="https://github.com/rwth-acis/yjs/">yjs</a> is a Framework for Real-Time collaboration on arbitrary data types.
</p>
</body>
</html>

View File

@@ -1,26 +0,0 @@
connector = new Y.XMPP("testy-xmpp-json2");
connector.debug = true
y = new Y(connector);
window.onload = function(){
var textbox = document.getElementById("textfield");
y.observe(function(events){
for(var i=0; i<events.length; i++){
var event = events[i];
if(event.name === "textfield" && event.type !== "delete"){
y.val("textfield").bind(textbox);
y.val("headline").bind(document.querySelector("h1"))
}
}
});
connector.whenSynced(function(){
if(y.val("textfield") == null){
y.val("headline","headline", "mutable");
y.val("textfield","stuff", "mutable")
}
})
};

View File

@@ -1,122 +0,0 @@
gulp = require('gulp')
coffee = require('gulp-coffee')
concat = require('gulp-concat')
uglify = require 'gulp-uglify'
sourcemaps = require('gulp-sourcemaps')
browserify = require('gulp-browserify')
rename = require 'gulp-rename'
rimraf = require 'gulp-rimraf'
gulpif = require 'gulp-if'
ignore = require 'gulp-ignore'
git = require 'gulp-git'
debug = require 'gulp-debug'
coffeelint = require 'gulp-coffeelint'
mocha = require 'gulp-mocha'
run = require 'gulp-run'
ljs = require 'gulp-ljs'
plumber = require 'gulp-plumber'
mochaPhantomJS = require 'gulp-mocha-phantomjs'
cache = require 'gulp-cached'
coffeeify = require 'gulp-coffeeify'
gulp.task 'default', ['build_browser']
files =
lib : ['./lib/**/*.coffee']
browser : ['./lib/y.coffee','./lib/y-object.coffee']
#test : ['./test/**/*_test.coffee']
test : ['./test/Json_test.coffee', './test/Text_test.coffee']
gulp : ['./gulpfile.coffee']
examples : ['./examples/**/*.js']
other: ['./lib/**/*']
files.all = []
for name,file_list of files
if name isnt 'build'
files.all = files.all.concat file_list
gulp.task 'deploy_nodejs', ->
gulp.src files.lib
.pipe sourcemaps.init()
.pipe coffee()
.pipe sourcemaps.write './'
.pipe gulp.dest 'build/node/'
.pipe gulpif '!**/', git.add({args : "-A"})
gulp.task 'deploy', ['mocha', 'build_browser', 'deploy_nodejs', 'lint', 'phantom_test', 'codo']
gulp.task 'build_browser', ->
gulp.src files.browser, { read: false }
.pipe plumber()
.pipe browserify
transform: ['coffeeify']
extensions: ['.coffee']
debug : true
.pipe rename
extname: ".js"
.pipe gulp.dest './build/browser/'
.pipe uglify()
.pipe gulp.dest '.'
gulp.src files.test, {read: false}
.pipe plumber()
.pipe browserify
transform: ['coffeeify']
extensions: ['.coffee']
debug: true
.pipe rename
extname: ".js"
.pipe gulp.dest './build/test/'
gulp.task 'build_node', ->
gulp.src files.lib
.pipe plumber()
.pipe coffee({bare:true})
.pipe gulp.dest './build/node'
gulp.task 'build', ['build_node', 'build_browser'], ->
gulp.task 'watch', ['build_browser'], ->
gulp.watch files.all, ['build_browser']
gulp.task 'mocha', ->
gulp.src files.test, { read: false }
.pipe plumber()
.pipe mocha {reporter : 'list'}
gulp.task 'lint', ->
gulp.src files.all
.pipe ignore.include '**/*.coffee'
.pipe coffeelint {
"max_line_length":
"level": "ignore"
}
.pipe coffeelint.reporter()
gulp.task 'phantom_watch', ['phantom_test'], ->
gulp.watch files.all, ['phantom_test']
gulp.task 'literate', ->
gulp.src files.examples
.pipe ljs { code : true }
.pipe rename
basename : "README"
extname : ".md"
.pipe gulp.dest 'examples/'
.pipe gulpif '!**/', git.add({args : "-A"})
gulp.task 'codo', [], ()->
command = './node_modules/codo/bin/codo -o "./doc" --name "yjs" --readme "README.md" --undocumented false --private true --title "yjs API" ./lib - LICENSE.txt '
run(command).exec()
gulp.task 'phantom_test', ['build_browser'], ()->
gulp.src 'build/test/index.html'
.pipe mochaPhantomJS()
gulp.task 'clean', ->
gulp.src ['./build/{browser,test,node}/**/*.{js,map}','./doc/'], { read: false }
.pipe rimraf()
gulp.task 'default', ['clean','build'], ->

View File

@@ -1,55 +0,0 @@
#
# @param {Engine} engine The transformation engine
# @param {HistoryBuffer} HB
# @param {Array<Function>} execution_listener You must ensure that whenever an operation is executed, every function in this Array is called.
#
adaptConnector = (connector, engine, HB, execution_listener)->
send_ = (o)->
if o.uid.creator is HB.getUserId() and (typeof o.uid.op_number isnt "string")
connector.broadcast o
if connector.invokeSync?
HB.setInvokeSyncHandler connector.invokeSync
execution_listener.push send_
# For the XMPPConnector: lets send it as an array
# therefore, we have to restructure it later
encode_state_vector = (v)->
for name,value of v
user: name
state: value
parse_state_vector = (v)->
state_vector = {}
for s in v
state_vector[s.user] = s.state
state_vector
getStateVector = ()->
encode_state_vector HB.getOperationCounter()
getHB = (v)->
state_vector = parse_state_vector v
hb = HB._encode state_vector
for o in hb
o.fromHB = "true" # execute immediately
json =
hb: hb
state_vector: encode_state_vector HB.getOperationCounter()
json
applyHB = (hb)->
engine.applyOp hb
connector.getStateVector = getStateVector
connector.getHB = getHB
connector.applyHB = applyHB
connector.receive_handlers.push (sender, op)->
if op.uid.creator isnt HB.getUserId()
engine.applyOp op
connector.setIsBoundToY()
module.exports = adaptConnector

View File

@@ -1,112 +0,0 @@
window?.unprocessed_counter = 0 # del this
window?.unprocessed_exec_counter = 0 # TODO
window?.unprocessed_types = []
#
# @nodoc
# The Engine handles how and in which order to execute operations and add operations to the HistoryBuffer.
#
class Engine
#
# @param {HistoryBuffer} HB
# @param {Object} types list of available types
#
constructor: (@HB, @types)->
@unprocessed_ops = []
#
# Parses an operatio from the json format. It uses the specified parser in your OperationType module.
#
parseOperation: (json)->
type = @types[json.type]
if type?.parse?
type.parse json
else
throw new Error "You forgot to specify a parser for type #{json.type}. The message is #{JSON.stringify json}."
#
# Apply a set of operations. E.g. the operations you received from another users HB._encode().
# @note You must not use this method when you already have ops in your HB!
###
applyOpsBundle: (ops_json)->
ops = []
for o in ops_json
ops.push @parseOperation o
for o in ops
if not o.execute()
@unprocessed_ops.push o
@tryUnprocessed()
###
#
# Same as applyOps but operations that are already in the HB are not applied.
# @see Engine.applyOps
#
applyOpsCheckDouble: (ops_json)->
for o in ops_json
if not @HB.getOperation(o.uid)?
@applyOp o
#
# Apply a set of operations. (Helper for using applyOp on Arrays)
# @see Engine.applyOp
applyOps: (ops_json)->
@applyOp ops_json
#
# Apply an operation that you received from another peer.
# TODO: make this more efficient!!
# - operations may only executed in order by creator, order them in object of arrays (key by creator)
# - you can probably make something like dependencies (creator1 waits for creator2)
applyOp: (op_json_array)->
if op_json_array.constructor isnt Array
op_json_array = [op_json_array]
for op_json in op_json_array
# $parse_and_execute will return false if $o_json was parsed and executed, otherwise the parsed operadion
o = @parseOperation op_json
if op_json.fromHB?
o.fromHB = op_json.fromHB
# @HB.addOperation o
if @HB.getOperation(o)?
# nop
else if ((not @HB.isExpectedOperation(o)) and (not o.fromHB?)) or (not o.execute())
@unprocessed_ops.push o
window?.unprocessed_types.push o.type # TODO: delete this
@tryUnprocessed()
#
# Call this method when you applied a new operation.
# It checks if operations that were previously not executable are now executable.
#
tryUnprocessed: ()->
while true
old_length = @unprocessed_ops.length
unprocessed = []
for op in @unprocessed_ops
if @HB.getOperation(op)?
# nop
else if (not @HB.isExpectedOperation(op) and (not op.fromHB?)) or (not op.execute())
unprocessed.push op
@unprocessed_ops = unprocessed
if @unprocessed_ops.length is old_length
break
if @unprocessed_ops.length isnt 0
@HB.invokeSync()
module.exports = Engine

View File

@@ -1,220 +0,0 @@
#
# @nodoc
# An object that holds all applied operations.
#
# @note The HistoryBuffer is commonly abbreviated to HB.
#
class HistoryBuffer
#
# Creates an empty HB.
# @param {Object} user_id Creator of the HB.
#
constructor: (@user_id)->
@operation_counter = {}
@buffer = {}
@change_listeners = []
@garbage = [] # Will be cleaned on next call of garbageCollector
@trash = [] # Is deleted. Wait until it is not used anymore.
@performGarbageCollection = true
@garbageCollectTimeout = 30000
@reserved_identifier_counter = 0
setTimeout @emptyGarbage, @garbageCollectTimeout
resetUserId: (id)->
own = @buffer[@user_id]
if own?
for o_name,o of own
o.uid.creator = id
if @buffer[id]?
throw new Error "You are re-assigning an old user id - this is not (yet) possible!"
@buffer[id] = own
delete @buffer[@user_id]
@operation_counter[id] = @operation_counter[@user_id]
delete @operation_counter[@user_id]
@user_id = id
emptyGarbage: ()=>
for o in @garbage
#if @getOperationCounter(o.uid.creator) > o.uid.op_number
o.cleanup?()
@garbage = @trash
@trash = []
if @garbageCollectTimeout isnt -1
@garbageCollectTimeoutId = setTimeout @emptyGarbage, @garbageCollectTimeout
undefined
#
# Get the user id with wich the History Buffer was initialized.
#
getUserId: ()->
@user_id
addToGarbageCollector: ()->
if @performGarbageCollection
for o in arguments
if o?
@garbage.push o
stopGarbageCollection: ()->
@performGarbageCollection = false
@setManualGarbageCollect()
@garbage = []
@trash = []
setManualGarbageCollect: ()->
@garbageCollectTimeout = -1
clearTimeout @garbageCollectTimeoutId
@garbageCollectTimeoutId = undefined
setGarbageCollectTimeout: (@garbageCollectTimeout)->
#
# I propose to use it in your Framework, to create something like a root element.
# An operation with this identifier is not propagated to other clients.
# This is why everybode must create the same operation with this uid.
#
getReservedUniqueIdentifier: ()->
{
creator : '_'
op_number : "_#{@reserved_identifier_counter++}"
doSync: false
}
#
# Get the operation counter that describes the current state of the document.
#
getOperationCounter: (user_id)->
if not user_id?
res = {}
for user,ctn of @operation_counter
res[user] = ctn
res
else
@operation_counter[user_id]
isExpectedOperation: (o)->
@operation_counter[o.uid.creator] ?= 0
o.uid.op_number <= @operation_counter[o.uid.creator]
true #TODO: !! this could break stuff. But I dunno why
#
# Encode this operation in such a way that it can be parsed by remote peers.
# TODO: Make this more efficient!
_encode: (state_vector={})->
json = []
unknown = (user, o_number)->
if (not user?) or (not o_number?)
throw new Error "dah!"
not state_vector[user]? or state_vector[user] <= o_number
for u_name,user of @buffer
# TODO next, if @state_vector[user] <= state_vector[user]
for o_number,o of user
if o.uid.doSync and unknown(u_name, o_number)
# its necessary to send it, and not known in state_vector
o_json = o._encode()
if o.next_cl? # applies for all ops but the most right delimiter!
# search for the next _known_ operation. (When state_vector is {} then this is the Delimiter)
o_next = o.next_cl
while o_next.next_cl? and unknown(o_next.uid.creator, o_next.uid.op_number)
o_next = o_next.next_cl
o_json.next = o_next.getUid()
else if o.prev_cl? # most right delimiter only!
# same as the above with prev.
o_prev = o.prev_cl
while o_prev.prev_cl? and unknown(o_prev.uid.creator, o_prev.uid.op_number)
o_prev = o_prev.prev_cl
o_json.prev = o_prev.getUid()
json.push o_json
json
#
# Get the number of operations that were created by a user.
# Accordingly you will get the next operation number that is expected from that user.
# This will increment the operation counter.
#
getNextOperationIdentifier: (user_id)->
if not user_id?
user_id = @user_id
if not @operation_counter[user_id]?
@operation_counter[user_id] = 0
uid =
'creator' : user_id
'op_number' : @operation_counter[user_id]
'doSync' : true
@operation_counter[user_id]++
uid
#
# Retrieve an operation from a unique id.
#
# when uid has a "sub" property, the value of it will be applied
# on the operations retrieveSub method (which must! be defined)
#
getOperation: (uid)->
if uid.uid?
uid = uid.uid
o = @buffer[uid.creator]?[uid.op_number]
if uid.sub? and o?
o.retrieveSub uid.sub
else
o
#
# Add an operation to the HB. Note that this will not link it against
# other operations (it wont executed)
#
addOperation: (o)->
if not @buffer[o.uid.creator]?
@buffer[o.uid.creator] = {}
if @buffer[o.uid.creator][o.uid.op_number]?
throw new Error "You must not overwrite operations!"
if (o.uid.op_number.constructor isnt String) and (not @isExpectedOperation(o)) and (not o.fromHB?) # you already do this in the engine, so delete it here!
throw new Error "this operation was not expected!"
@addToCounter(o)
@buffer[o.uid.creator][o.uid.op_number] = o
o
removeOperation: (o)->
delete @buffer[o.uid.creator]?[o.uid.op_number]
# When the HB determines inconsistencies, then the invokeSync
# handler wil be called, which should somehow invoke the sync with another collaborator.
# The parameter of the sync handler is the user_id with wich an inconsistency was determined
setInvokeSyncHandler: (f)->
@invokeSync = f
# empty per default # TODO: do i need this?
invokeSync: ()->
# after you received the HB of another user (in the sync process),
# you renew your own state_vector to the state_vector of the other user
renewStateVector: (state_vector)->
for user,state of state_vector
if (not @operation_counter[user]?) or (@operation_counter[user] < state_vector[user])
@operation_counter[user] = state_vector[user]
#
# Increment the operation_counter that defines the current state of the Engine.
#
addToCounter: (o)->
if not @operation_counter[o.uid.creator]?
@operation_counter[o.uid.creator] = 0
if typeof o.uid.op_number is 'number' and o.uid.creator isnt @getUserId()
# TODO: check if operations are send in order
if o.uid.op_number is @operation_counter[o.uid.creator]
@operation_counter[o.uid.creator]++
else
@invokeSync o.uid.creator
#if @operation_counter[o.uid.creator] isnt (o.uid.op_number + 1)
#console.log (@operation_counter[o.uid.creator] - (o.uid.op_number + 1))
#console.log o
#throw new Error "You don't receive operations in the proper order. Try counting like this 0,1,2,3,4,.. ;)"
module.exports = HistoryBuffer

View File

@@ -1,555 +0,0 @@
module.exports = (HB)->
# @see Engine.parse
types = {}
execution_listener = []
#
# @private
# @abstract
# @nodoc
# A generic interface to operations.
#
# An operation has the following methods:
# * _encode: encodes an operation (needed only if instance of this operation is sent).
# * execute: execute the effects of this operations. Good examples are Insert-type and AddName-type
# * val: in the case that the operation holds a value
#
# Furthermore an encodable operation has a parser. We extend the parser object in order to parse encoded operations.
#
class types.Operation
#
# @param {Object} uid A unique identifier.
# If uid is undefined, a new uid will be created before at the end of the execution sequence
#
constructor: (uid)->
@is_deleted = false
@garbage_collected = false
@event_listeners = [] # TODO: rename to observers or sth like that
if uid?
@uid = uid
type: "Operation"
retrieveSub: ()->
throw new Error "sub properties are not enable on this operation type!"
#
# Add an event listener. It depends on the operation which events are supported.
# @param {Function} f f is executed in case the event fires.
#
observe: (f)->
@event_listeners.push f
#
# Deletes function from the observer list
# @see Operation.observe
#
# @overload unobserve(event, f)
# @param f {Function} The function that you want to delete
unobserve: (f)->
@event_listeners = @event_listeners.filter (g)->
f isnt g
#
# Deletes all subscribed event listeners.
# This should be called, e.g. after this has been replaced.
# (Then only one replace event should fire. )
# This is also called in the cleanup method.
deleteAllObservers: ()->
@event_listeners = []
delete: ()->
(new types.Delete undefined, @).execute()
null
#
# Fire an event.
# TODO: Do something with timeouts. You don't want this to fire for every operation (e.g. insert).
# TODO: do you need callEvent+forwardEvent? Only one suffices probably
callEvent: ()->
@forwardEvent @, arguments...
#
# Fire an event and specify in which context the listener is called (set 'this').
# TODO: do you need this ?
forwardEvent: (op, args...)->
for f in @event_listeners
f.call op, args...
isDeleted: ()->
@is_deleted
applyDelete: (garbagecollect = true)->
if not @garbage_collected
#console.log "applyDelete: #{@type}"
@is_deleted = true
if garbagecollect
@garbage_collected = true
HB.addToGarbageCollector @
cleanup: ()->
#console.log "cleanup: #{@type}"
HB.removeOperation @
@deleteAllObservers()
#
# Set the parent of this operation.
#
setParent: (@parent)->
#
# Get the parent of this operation.
#
getParent: ()->
@parent
#
# Computes a unique identifier (uid) that identifies this operation.
#
getUid: ()->
if not @uid.noOperation?
@uid
else
@uid.alt # could be (safely) undefined
cloneUid: ()->
uid = {}
for n,v of @getUid()
uid[n] = v
uid
dontSync: ()->
@uid.doSync = false
#
# @private
# If not already done, set the uid
# Add this to the HB
# Notify the all the listeners.
#
execute: ()->
@is_executed = true
if not @uid?
# When this operation was created without a uid, then set it here.
# There is only one other place, where this can be done - before an Insertion
# is executed (because we need the creator_id)
@uid = HB.getNextOperationIdentifier()
if not @uid.noOperation?
HB.addOperation @
for l in execution_listener
l @_encode()
@
#
# @private
# Operations may depend on other operations (linked lists, etc.).
# The saveOperation and validateSavedOperations methods provide
# an easy way to refer to these operations via an uid or object reference.
#
# For example: We can create a new Delete operation that deletes the operation $o like this
# - var d = new Delete(uid, $o); or
# - var d = new Delete(uid, $o.getUid());
# Either way we want to access $o via d.deletes. In the second case validateSavedOperations must be called first.
#
# @overload saveOperation(name, op_uid)
# @param {String} name The name of the operation. After validating (with validateSavedOperations) the instantiated operation will be accessible via this[name].
# @param {Object} op_uid A uid that refers to an operation
# @overload saveOperation(name, op)
# @param {String} name The name of the operation. After calling this function op is accessible via this[name].
# @param {Operation} op An Operation object
#
saveOperation: (name, op)->
#
# Every instance of $Operation must have an $execute function.
# We use duck-typing to check if op is instantiated since there
# could exist multiple classes of $Operation
#
if op?.execute?
# is instantiated
@[name] = op
else if op?
# not initialized. Do it when calling $validateSavedOperations()
@unchecked ?= {}
@unchecked[name] = op
#
# @private
# After calling this function all not instantiated operations will be accessible.
# @see Operation.saveOperation
#
# @return [Boolean] Whether it was possible to instantiate all operations.
#
validateSavedOperations: ()->
uninstantiated = {}
success = @
for name, op_uid of @unchecked
op = HB.getOperation op_uid
if op
@[name] = op
else
uninstantiated[name] = op_uid
success = false
delete @unchecked
if not success
@unchecked = uninstantiated
success
#
# @nodoc
# A simple Delete-type operation that deletes an operation.
#
class types.Delete extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} deletes UID or reference of the operation that this to be deleted.
#
constructor: (uid, deletes)->
@saveOperation 'deletes', deletes
super uid
type: "Delete"
#
# @private
# Convert all relevant information of this operation to the json-format.
# This result can be sent to other clients.
#
_encode: ()->
{
'type': "Delete"
'uid': @getUid()
'deletes': @deletes.getUid()
}
#
# @private
# Apply the deletion.
#
execute: ()->
if @validateSavedOperations()
res = super
if res
@deletes.applyDelete @
res
else
false
#
# Define how to parse Delete operations.
#
types.Delete.parse = (o)->
{
'uid' : uid
'deletes': deletes_uid
} = o
new this(uid, deletes_uid)
#
# @nodoc
# A simple insert-type operation.
#
# An insert operation is always positioned between two other insert operations.
# Internally this is realized as associative lists, whereby each insert operation has a predecessor and a successor.
# For the sake of efficiency we maintain two lists:
# - The short-list (abbrev. sl) maintains only the operations that are not deleted
# - The complete-list (abbrev. cl) maintains all operations
#
class types.Insert extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl)
# @param {Operation} next_cl The successor of this operation in the complete-list (cl)
#
constructor: (uid, prev_cl, next_cl, origin, parent)->
@saveOperation 'parent', parent
@saveOperation 'prev_cl', prev_cl
@saveOperation 'next_cl', next_cl
if origin?
@saveOperation 'origin', origin
else
@saveOperation 'origin', prev_cl
super uid
type: "Insert"
#
# set content to null and other stuff
# @private
#
applyDelete: (o)->
@deleted_by ?= []
callLater = false
if @parent? and not @isDeleted() and o? # o? : if not o?, then the delimiter deleted this Insertion. Furthermore, it would be wrong to call it. TODO: make this more expressive and save
# call iff wasn't deleted earlyer
callLater = true
if o?
@deleted_by.push o
garbagecollect = false
if @next_cl.isDeleted()
garbagecollect = true
super garbagecollect
if callLater
@callOperationSpecificDeleteEvents(o)
if @prev_cl?.isDeleted()
# garbage collect prev_cl
@prev_cl.applyDelete()
cleanup: ()->
if @next_cl.isDeleted()
# delete all ops that delete this insertion
for d in @deleted_by
d.cleanup()
# throw new Error "right is not deleted. inconsistency!, wrararar"
# change origin references to the right
o = @next_cl
while o.type isnt "Delimiter"
if o.origin is @
o.origin = @prev_cl
o = o.next_cl
# reconnect left/right
@prev_cl.next_cl = @next_cl
@next_cl.prev_cl = @prev_cl
super
# else
# Someone inserted something in the meantime.
# Remember: this can only be garbage collected when next_cl is deleted
#
# @private
# The amount of positions that $this operation was moved to the right.
#
getDistanceToOrigin: ()->
d = 0
o = @prev_cl
while true
if @origin is o
break
d++
o = o.prev_cl
d
#
# @private
# Include this operation in the associative lists.
execute: ()->
if not @validateSavedOperations()
return false
else
if @parent?
if not @prev_cl?
@prev_cl = @parent.beginning
if not @origin?
@origin = @parent.beginning
if not @next_cl?
@next_cl = @parent.end
if @prev_cl?
distance_to_origin = @getDistanceToOrigin() # most cases: 0
o = @prev_cl.next_cl
i = distance_to_origin # loop counter
# $this has to find a unique position between origin and the next known character
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
# o2,o3 and o4 origin is 1 (the position of o2)
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
# therefore $this would be always to the right of o3
# case 2: $origin < $o.origin
# if current $this insert_position > $o origin: $this ins
# else $insert_position will not change
# (maybe we encounter case 1 later, then this will be to the right of $o)
# case 3: $origin > $o.origin
# $this insert_position is to the left of $o (forever!)
while true
if o isnt @next_cl
# $o happened concurrently
if o.getDistanceToOrigin() is i
# case 1
if o.uid.creator < @uid.creator
@prev_cl = o
distance_to_origin = i + 1
else
# nop
else if o.getDistanceToOrigin() < i
# case 2
if i - distance_to_origin <= o.getDistanceToOrigin()
@prev_cl = o
distance_to_origin = i + 1
else
#nop
else
# case 3
break
i++
o = o.next_cl
else
# $this knows that $o exists,
break
# now reconnect everything
@next_cl = @prev_cl.next_cl
@prev_cl.next_cl = @
@next_cl.prev_cl = @
@setParent @prev_cl.getParent() # do Insertions always have a parent?
super # notify the execution_listeners
@callOperationSpecificInsertEvents()
@
callOperationSpecificInsertEvents: ()->
@parent?.callEvent [
type: "insert"
position: @getPosition()
object: @parent
changedBy: @uid.creator
value: @content
]
callOperationSpecificDeleteEvents: (o)->
@parent.callEvent [
type: "delete"
position: @getPosition()
object: @parent # TODO: You can combine getPosition + getParent in a more efficient manner! (only left Delimiter will hold @parent)
length: 1
changedBy: o.uid.creator
]
#
# Compute the position of this operation.
#
getPosition: ()->
position = 0
prev = @prev_cl
while true
if prev instanceof types.Delimiter
break
if not prev.isDeleted()
position++
prev = prev.prev_cl
position
#
# @nodoc
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
#
class types.ImmutableObject extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} content
#
constructor: (uid, @content)->
super uid
type: "ImmutableObject"
#
# @return [String] The content of this operation.
#
val : ()->
@content
#
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json = {
'type': @type
'uid' : @getUid()
'content' : @content
}
json
types.ImmutableObject.parse = (json)->
{
'uid' : uid
'content' : content
} = json
new this(uid, content)
#
# @nodoc
# A delimiter is placed at the end and at the beginning of the associative lists.
# This is necessary in order to have a beginning and an end even if the content
# of the Engine is empty.
#
class types.Delimiter extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl)
# @param {Operation} next_cl The successor of this operation in the complete-list (cl)
#
constructor: (prev_cl, next_cl, origin)->
@saveOperation 'prev_cl', prev_cl
@saveOperation 'next_cl', next_cl
@saveOperation 'origin', prev_cl
super {noOperation: true}
type: "Delimiter"
applyDelete: ()->
super()
o = @prev_cl
while o?
o.applyDelete()
o = o.prev_cl
undefined
cleanup: ()->
super()
#
# @private
#
execute: ()->
if @unchecked?['next_cl']?
super
else if @unchecked?['prev_cl']
if @validateSavedOperations()
if @prev_cl.next_cl?
throw new Error "Probably duplicated operations"
@prev_cl.next_cl = @
super
else
false
else if @prev_cl? and not @prev_cl.next_cl?
delete @prev_cl.unchecked.next_cl
@prev_cl.next_cl = @
super
else if @prev_cl? or @next_cl? or true # TODO: are you sure? This can happen right?
super
#else
# throw new Error "Delimiter is unsufficient defined!"
#
# @private
#
_encode: ()->
{
'type' : @type
'uid' : @getUid()
'prev' : @prev_cl?.getUid()
'next' : @next_cl?.getUid()
}
types.Delimiter.parse = (json)->
{
'uid' : uid
'prev' : prev
'next' : next
} = json
new this(uid, prev, next)
# This is what this module exports after initializing it with the HistoryBuffer
{
'types' : types
'execution_listener' : execution_listener
}

View File

@@ -1,148 +0,0 @@
text_types_uninitialized = require "./TextTypes"
module.exports = (HB)->
text_types = text_types_uninitialized HB
types = text_types.types
#
# Manages Object-like values.
#
class types.Object extends types.MapManager
#
# Identifies this class.
# Use it to check whether this is a json-type or something else.
#
# @example
# var x = y.val('unknown')
# if (x.type === "Object") {
# console.log JSON.stringify(x.toJson())
# }
#
type: "Object"
applyDelete: ()->
super()
cleanup: ()->
super()
#
# Transform this to a Json. If your browser supports Object.observe it will be transformed automatically when a change arrives.
# Otherwise you will loose all the sharing-abilities (the new object will be a deep clone)!
# @return {Json}
#
# TODO: at the moment you don't consider changing of properties.
# E.g.: let x = {a:[]}. Then x.a.push 1 wouldn't change anything
#
toJson: (transform_to_value = false)->
if not @bound_json? or not Object.observe? or true # TODO: currently, you are not watching mutable strings for changes, and, therefore, the @bound_json is not updated. TODO TODO wuawuawua easy
val = @val()
json = {}
for name, o of val
if o instanceof types.Object
json[name] = o.toJson(transform_to_value)
else if o instanceof types.Array
json[name] = o.toJson(transform_to_value)
else if transform_to_value and o instanceof types.Operation
json[name] = o.val()
else
json[name] = o
@bound_json = json
if Object.observe?
that = @
Object.observe @bound_json, (events)->
for event in events
if not event.changedBy? and (event.type is "add" or event.type = "update")
# this event is not created by Y.
that.val(event.name, event.object[event.name])
@observe (events)->
for event in events
if event.created_ isnt HB.getUserId()
notifier = Object.getNotifier(that.bound_json)
oldVal = that.bound_json[event.name]
if oldVal?
notifier.performChange 'update', ()->
that.bound_json[event.name] = that.val(event.name)
, that.bound_json
notifier.notify
object: that.bound_json
type: 'update'
name: event.name
oldValue: oldVal
changedBy: event.changedBy
else
notifier.performChange 'add', ()->
that.bound_json[event.name] = that.val(event.name)
, that.bound_json
notifier.notify
object: that.bound_json
type: 'add'
name: event.name
oldValue: oldVal
changedBy:event.changedBy
@bound_json
#
# @overload val()
# Get this as a Json object.
# @return [Json]
#
# @overload val(name)
# Get value of a property.
# @param {String} name Name of the object property.
# @return [Object Type||String|Object] Depending on the value of the property. If mutable it will return a Operation-type object, if immutable it will return String/Object.
#
# @overload val(name, content)
# Set a new property.
# @param {String} name Name of the object property.
# @param {Object|String} content Content of the object property.
# @return [Object Type] This object. (supports chaining)
#
val: (name, content)->
if name? and arguments.length > 1
if content? and content.constructor?
type = types[content.constructor.name]
if type? and type.create?
args = []
for i in [1...arguments.length]
args.push arguments[i]
o = type.create.apply null, args
super name, o
else
throw new Error "The #{content.constructor.name}-type is not (yet) supported in Y."
else
super name, content
else # is this even necessary ? I have to define every type anyway.. (see Number type below)
super name
#
# @private
#
_encode: ()->
{
'type' : @type
'uid' : @getUid()
}
types.Object.parse = (json)->
{
'uid' : uid
} = json
new this(uid)
types.Object.create = (content, mutable)->
json = new types.Object().execute()
for n,o of content
json.val n, o, mutable
json
types.Number = {}
types.Number.create = (content)->
content
text_types

View File

@@ -1,357 +0,0 @@
basic_types_uninitialized = require "./BasicTypes"
module.exports = (HB)->
basic_types = basic_types_uninitialized HB
types = basic_types.types
#
# @nodoc
# Manages map like objects. E.g. Json-Type and XML attributes.
#
class types.MapManager extends types.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (uid)->
@map = {}
super uid
type: "MapManager"
applyDelete: ()->
for name,p of @map
p.applyDelete()
super()
cleanup: ()->
super()
#
# @see JsonTypes.val
#
val: (name, content)->
if arguments.length > 1
@retrieveSub(name).replace content
@
else if name?
prop = @map[name]
if prop? and not prop.isContentDeleted()
prop.val()
else
undefined
else
result = {}
for name,o of @map
if not o.isContentDeleted()
result[name] = o.val()
result
delete: (name)->
@map[name]?.deleteContent()
@
retrieveSub: (property_name)->
if not @map[property_name]?
event_properties =
name: property_name
event_this = @
map_uid = @cloneUid()
map_uid.sub = property_name
rm_uid =
noOperation: true
alt: map_uid
rm = new types.ReplaceManager event_properties, event_this, rm_uid # this operation shall not be saved in the HB
@map[property_name] = rm
rm.setParent @, property_name
rm.execute()
@map[property_name]
#
# @nodoc
# Manages a list of Insert-type operations.
#
class types.ListManager extends types.Operation
#
# A ListManager maintains a non-empty list that has a beginning and an end (both Delimiters!)
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Delimiter} beginning Reference or Object.
# @param {Delimiter} end Reference or Object.
constructor: (uid)->
@beginning = new types.Delimiter undefined, undefined
@end = new types.Delimiter @beginning, undefined
@beginning.next_cl = @end
@beginning.execute()
@end.execute()
super uid
type: "ListManager"
#
# @private
# @see Operation.execute
#
execute: ()->
if @validateSavedOperations()
@beginning.setParent @
@end.setParent @
super
else
false
# Get the element previous to the delemiter at the end
getLastOperation: ()->
@end.prev_cl
# similar to the above
getFirstOperation: ()->
@beginning.next_cl
# Transforms the the list to an array
# Doesn't return left-right delimiter.
toArray: ()->
o = @beginning.next_cl
result = []
while o isnt @end
result.push o
o = o.next_cl
result
#
# Retrieves the x-th not deleted element.
# e.g. "abc" : the 1th character is "a"
# the 0th character is the left Delimiter
#
getOperationByPosition: (position)->
o = @beginning
while true
# find the i-th op
if o instanceof types.Delimiter and o.prev_cl?
# the user or you gave a position parameter that is to big
# for the current array. Therefore we reach a Delimiter.
# Then, we'll just return the last character.
o = o.prev_cl
while o.isDeleted() or not (o instanceof types.Delimiter)
o = o.prev_cl
break
if position <= 0 and not o.isDeleted()
break
o = o.next_cl
if not o.isDeleted()
position -= 1
o
#
# @nodoc
# Adds support for replace. The ReplaceManager manages Replaceable operations.
# Each Replaceable holds a value that is now replaceable.
#
# The TextType-type has implemented support for replace
# @see TextType
#
class types.ReplaceManager extends types.ListManager
#
# @param {Object} event_properties Decorates the event that is thrown by the RM
# @param {Object} event_this The object on which the event shall be executed
# @param {Operation} initial_content Initialize this with a Replaceable that holds the initial_content.
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Delimiter} beginning Reference or Object.
# @param {Delimiter} end Reference or Object.
constructor: (@event_properties, @event_this, uid, beginning, end)->
if not @event_properties['object']?
@event_properties['object'] = @event_this
super uid, beginning, end
type: "ReplaceManager"
applyDelete: ()->
o = @beginning
while o?
o.applyDelete()
o = o.next_cl
super()
cleanup: ()->
super()
#
# This doesn't throw the same events as the ListManager. Therefore, the
# Replaceables also not throw the same events.
# So, ReplaceManager and ListManager both implement
# these functions that are called when an Insertion is executed (at the end).
#
#
callEventDecorator: (events)->
if not @isDeleted()
for event in events
for name,prop of @event_properties
event[name] = prop
@event_this.callEvent events
undefined
#
# Replace the existing word with a new word.
#
# @param content {Operation} The new value of this ReplaceManager.
# @param replaceable_uid {UID} Optional: Unique id of the Replaceable that is created
#
replace: (content, replaceable_uid)->
o = @getLastOperation()
relp = (new types.Replaceable content, @, replaceable_uid, o, o.next_cl).execute()
# TODO: delete repl (for debugging)
undefined
isContentDeleted: ()->
@getLastOperation().isDeleted()
deleteContent: ()->
(new types.Delete undefined, @getLastOperation().uid).execute()
undefined
#
# Get the value of this
# @return {String}
#
val: ()->
o = @getLastOperation()
#if o instanceof types.Delimiter
# throw new Error "Replace Manager doesn't contain anything."
o.val?() # ? - for the case that (currently) the RM does not contain anything (then o is a Delimiter)
#
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json =
{
'type': @type
'uid' : @getUid()
'beginning' : @beginning.getUid()
'end' : @end.getUid()
}
json
#
# @nodoc
# The ReplaceManager manages Replaceables.
# @see ReplaceManager
#
class types.Replaceable extends types.Insert
#
# @param {Operation} content The value that this Replaceable holds.
# @param {ReplaceManager} parent Used to replace this Replaceable with another one.
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (content, parent, uid, prev, next, origin, is_deleted)->
# see encode to see, why we are doing it this way
if content? and content.creator?
@saveOperation 'content', content
else
@content = content
@saveOperation 'parent', parent
super uid, prev, next, origin # Parent is already saved by Replaceable
@is_deleted = is_deleted
type: "Replaceable"
#
# Return the content that this operation holds.
#
val: ()->
@content
applyDelete: ()->
res = super
if @content?
if @next_cl.type isnt "Delimiter"
@content.deleteAllObservers?()
@content.applyDelete?()
@content.dontSync?()
@content = null
res
cleanup: ()->
super
#
# This is called, when the Insert-type was successfully executed.
# TODO: consider doing this in a more consistent manner. This could also be
# done with execute. But currently, there are no specital Insert-types for ListManager.
#
callOperationSpecificInsertEvents: ()->
if @next_cl.type is "Delimiter" and @prev_cl.type isnt "Delimiter"
# this replaces another Replaceable
if not @is_deleted # When this is received from the HB, this could already be deleted!
old_value = @prev_cl.content
@parent.callEventDecorator [
type: "update"
changedBy: @uid.creator
oldValue: old_value
]
@prev_cl.applyDelete()
else if @next_cl.type isnt "Delimiter"
# This won't be recognized by the user, because another
# concurrent operation is set as the current value of the RM
@applyDelete()
else # prev _and_ next are Delimiters. This is the first created Replaceable in the RM
@parent.callEventDecorator [
type: "add"
changedBy: @uid.creator
]
undefined
callOperationSpecificDeleteEvents: (o)->
if @next_cl.type is "Delimiter"
@parent.callEventDecorator [
type: "delete"
changedBy: o.uid.creator
oldValue: @content
]
#
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json =
{
'type': @type
'parent' : @parent.getUid()
'prev': @prev_cl.getUid()
'next': @next_cl.getUid()
'origin' : @origin.getUid()
'uid' : @getUid()
'is_deleted': @is_deleted
}
if @content instanceof types.Operation
json['content'] = @content.getUid()
else
# This could be a security concern.
# Throw error if the users wants to trick us
if @content? and @content.creator?
throw new Error "You must not set creator here!"
json['content'] = @content
json
types.Replaceable.parse = (json)->
{
'content' : content
'parent' : parent
'uid' : uid
'prev': prev
'next': next
'origin' : origin
'is_deleted': is_deleted
} = json
new this(content, parent, uid, prev, next, origin, is_deleted)
basic_types

View File

@@ -1,522 +0,0 @@
structured_types_uninitialized = require "./StructuredTypes"
module.exports = (HB)->
structured_types = structured_types_uninitialized HB
types = structured_types.types
parser = structured_types.parser
#
# @nodoc
# Extends the basic Insert type to an operation that holds a text value
#
class types.TextInsert extends types.Insert
#
# @param {String} content The content of this Insert-type Operation. Usually you restrict the length of content to size 1
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (content, uid, prev, next, origin, parent)->
if content?.creator
@saveOperation 'content', content
else
@content = content
super uid, prev, next, origin, parent
type: "TextInsert"
#
# Retrieve the effective length of the $content of this operation.
#
getLength: ()->
if @isDeleted()
0
else
@content.length
applyDelete: ()->
super # no braces indeed!
if @content instanceof types.Operation
@content.applyDelete()
@content = null
execute: ()->
if not @validateSavedOperations()
return false
else
if @content instanceof types.Operation
@content.insert_parent = @
super()
#
# The result will be concatenated with the results from the other insert operations
# in order to retrieve the content of the engine.
# @see HistoryBuffer.toExecutedArray
#
val: (current_position)->
if @isDeleted() or not @content?
""
else
@content
#
# Convert all relevant information of this operation to the json-format.
# This result can be send to other clients.
#
_encode: ()->
json =
{
'type': @type
'uid' : @getUid()
'prev': @prev_cl.getUid()
'next': @next_cl.getUid()
'origin': @origin.getUid()
'parent': @parent.getUid()
}
if @content?.getUid?
json['content'] = @content.getUid()
else
json['content'] = @content
json
types.TextInsert.parse = (json)->
{
'content' : content
'uid' : uid
'prev': prev
'next': next
'origin' : origin
'parent' : parent
} = json
new types.TextInsert content, uid, prev, next, origin, parent
class types.Array extends types.ListManager
type: "Array"
applyDelete: ()->
o = @end
while o?
o.applyDelete()
o = o.prev_cl
super()
cleanup: ()->
super()
toJson: (transform_to_value = false)->
val = @val()
for i, o in val
if o instanceof types.Object
o.toJson(transform_to_value)
else if o instanceof types.Array
o.toJson(transform_to_value)
else if transform_to_value and o instanceof types.Operation
o.val()
else
o
val: (pos)->
if pos?
o = @getOperationByPosition(pos+1)
if not (o instanceof types.Delimiter)
o.val()
else
throw new Error "this position does not exist"
else
o = @beginning.next_cl
result = []
while o isnt @end
if not o.isDeleted()
result.push o.val()
o = o.next_cl
result
push: (content)->
@insertAfter @end.prev_cl, content
insertAfter: (left, content, options)->
createContent = (content, options)->
if content? and content.constructor?
type = types[content.constructor.name]
if type? and type.create?
type.create content, options
else
throw new Error "The #{content.constructor.name}-type is not (yet) supported in Y."
else
content
right = left.next_cl
while right.isDeleted()
right = right.next_cl # find the first character to the right, that is not deleted. In the case that position is 0, its the Delimiter.
left = right.prev_cl
if content instanceof types.Operation
(new types.TextInsert content, undefined, left, right).execute()
else
for c in content
tmp = (new types.TextInsert createContent(c, options), undefined, left, right).execute()
left = tmp
@
#
# Inserts a string into the word.
#
# @return {Array Type} This String object.
#
insert: (position, content, options)->
ith = @getOperationByPosition position
# the (i-1)th character. e.g. "abc" the 1th character is "a"
# the 0th character is the left Delimiter
@insertAfter ith, [content], options
#
# Deletes a part of the word.
#
# @return {Array Type} This String object
#
delete: (position, length)->
o = @getOperationByPosition(position+1) # position 0 in this case is the deletion of the first character
delete_ops = []
for i in [0...length]
if o instanceof types.Delimiter
break
d = (new types.Delete undefined, o).execute()
o = o.next_cl
while (not (o instanceof types.Delimiter)) and o.isDeleted()
o = o.next_cl
delete_ops.push d._encode()
@
#
# @private
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json = {
'type': @type
'uid' : @getUid()
}
json
types.Array.parse = (json)->
{
'uid' : uid
} = json
new this(uid)
types.Array.create = (content, mutable)->
if (mutable is "mutable")
list = new types.Array().execute()
ith = list.getOperationByPosition 0
list.insertAfter ith, content
list
else if (not mutable?) or (mutable is "immutable")
content
else
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
#
# Handles a String-like data structures with support for insert/delete at a word-position.
# @note Currently, only Text is supported!
#
class types.String extends types.Array
#
# @private
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (uid)->
@textfields = []
super uid
#
# Identifies this class.
# Use it to check whether this is a word-type or something else.
#
# @example
# var x = y.val('unknown')
# if (x.type === "String") {
# console.log JSON.stringify(x.toJson())
# }
#
type: "String"
#
# Get the String-representation of this word.
# @return {String} The String-representation of this object.
#
val: ()->
c = for o in @toArray()
if o.val?
o.val()
else
""
c.join('')
#
# Same as String.val
# @see String.val
#
toString: ()->
@val()
#
# Inserts a string into the word.
#
# @return {Array Type} This String object.
#
insert: (position, content, options)->
ith = @getOperationByPosition position
# the (i-1)th character. e.g. "abc" the 1th character is "a"
# the 0th character is the left Delimiter
@insertAfter ith, content, options
#
# Bind this String to a textfield or input field.
#
# @example
# var textbox = document.getElementById("textfield");
# y.bind(textbox);
#
bind: (textfield, dom_root)->
dom_root ?= window
if (not dom_root.getSelection?)
dom_root = window
# don't duplicate!
for t in @textfields
if t is textfield
return
creator_token = false;
word = @
textfield.value = @val()
@textfields.push textfield
if textfield.selectionStart? and textfield.setSelectionRange?
createRange = (fix)->
left = textfield.selectionStart
right = textfield.selectionEnd
if fix?
left = fix left
right = fix right
{
left: left
right: right
}
writeRange = (range)->
writeContent word.val()
textfield.setSelectionRange range.left, range.right
writeContent = (content)->
textfield.value = content
else
createRange = (fix)->
s = dom_root.getSelection()
clength = textfield.textContent.length
left = Math.min s.anchorOffset, clength
right = Math.min s.focusOffset, clength
if fix?
left = fix left
right = fix right
{
left: left
right: right
isReal: true
}
writeRange = (range)->
writeContent word.val()
textnode = textfield.childNodes[0]
if range.isReal and textnode?
if range.left < 0
range.left = 0
range.right = Math.max range.left, range.right
if range.right > textnode.length
range.right = textnode.length
range.left = Math.min range.left, range.right
r = document.createRange()
r.setStart(textnode, range.left)
r.setEnd(textnode, range.right)
s = window.getSelection()
s.removeAllRanges()
s.addRange(r)
writeContent = (content)->
append = ""
if content[content.length - 1] is " "
content = content.slice(0,content.length-1)
append = '&nbsp;'
textfield.textContent = content
textfield.innerHTML += append
writeContent this.val()
@observe (events)->
for event in events
if not creator_token
if event.type is "insert"
o_pos = event.position
fix = (cursor)->
if cursor <= o_pos
cursor
else
cursor += 1
cursor
r = createRange fix
writeRange r
else if event.type is "delete"
o_pos = event.position
fix = (cursor)->
if cursor < o_pos
cursor
else
cursor -= 1
cursor
r = createRange fix
writeRange r
# consume all text-insert changes.
textfield.onkeypress = (event)->
if word.is_deleted
# if word is deleted, do not do anything ever again
textfield.onkeypress = null
return true
creator_token = true
char = null
if event.key?
if event.charCode is 32
char = " "
else if event.keyCode is 13
char = '\n'
else
char = event.key
else
char = window.String.fromCharCode event.keyCode
if char.length > 1
return true
else if char.length > 0
r = createRange()
pos = Math.min r.left, r.right
diff = Math.abs(r.right - r.left)
word.delete pos, diff
word.insert pos, char
r.left = pos + char.length
r.right = r.left
writeRange r
event.preventDefault()
creator_token = false
false
textfield.onpaste = (event)->
if word.is_deleted
# if word is deleted, do not do anything ever again
textfield.onpaste = null
return true
event.preventDefault()
textfield.oncut = (event)->
if word.is_deleted
# if word is deleted, do not do anything ever again
textfield.oncut = null
return true
event.preventDefault()
#
# consume deletes. Note that
# chrome: won't consume deletions on keypress event.
# keyCode is deprecated. BUT: I don't see another way.
# since event.key is not implemented in the current version of chrome.
# Every browser supports keyCode. Let's stick with it for now..
#
textfield.onkeydown = (event)->
creator_token = true
if word.is_deleted
# if word is deleted, do not do anything ever again
textfield.onkeydown = null
return true
r = createRange()
pos = Math.min(r.left, r.right, word.val().length)
diff = Math.abs(r.left - r.right)
if event.keyCode? and event.keyCode is 8 # Backspace
if diff > 0
word.delete pos, diff
r.left = pos
r.right = pos
writeRange r
else
if event.ctrlKey? and event.ctrlKey
val = word.val()
new_pos = pos
del_length = 0
if pos > 0
new_pos--
del_length++
while new_pos > 0 and val[new_pos] isnt " " and val[new_pos] isnt '\n'
new_pos--
del_length++
word.delete new_pos, (pos-new_pos)
r.left = new_pos
r.right = new_pos
writeRange r
else
if pos > 0
word.delete (pos-1), 1
r.left = pos-1
r.right = pos-1
writeRange r
event.preventDefault()
creator_token = false
return false
else if event.keyCode? and event.keyCode is 46 # Delete
if diff > 0
word.delete pos, diff
r.left = pos
r.right = pos
writeRange r
else
word.delete pos, 1
r.left = pos
r.right = pos
writeRange r
event.preventDefault()
creator_token = false
return false
else
creator_token = false
true
#
# @private
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json = {
'type': @type
'uid' : @getUid()
}
json
types.String.parse = (json)->
{
'uid' : uid
} = json
new this(uid)
types.String.create = (content, mutable)->
if (mutable is "mutable")
word = new types.String().execute()
word.insert 0, content
word
else if (not mutable?) or (mutable is "immutable")
content
else
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
structured_types

View File

@@ -1,367 +0,0 @@
###
json_types_uninitialized = require "./JsonTypes"
# some dom implementations may call another dom.method that simulates the behavior of another.
# For example xml.insertChild(dom) , wich inserts an element at the end, and xml.insertAfter(dom,null) wich does the same
# But Y's proxy may be called only once!
proxy_token = false
dont_proxy = (f)->
proxy_token = true
try
f()
catch e
proxy_token = false
throw new Error e
proxy_token = false
_proxy = (f_name, f)->
old_f = @[f_name]
if old_f?
@[f_name] = ()->
if not proxy_token and not @_y?.isDeleted()
that = this
args = arguments
dont_proxy ()->
f.apply that, args
old_f.apply that, args
else
old_f.apply this, arguments
#else
# @[f_name] = f
Element?.prototype._proxy = _proxy
module.exports = (HB)->
json_types = json_types_uninitialized HB
types = json_types.types
parser = json_types.parser
#
# Manages XML types
# Not supported:
# * Attribute nodes
# * Real replace of child elements (to much overhead). Currently, the new element is inserted after the 'replaced' element, and then it is deleted.
# * Namespaces (*NS)
# * Browser specific methods (webkit-* operations)
class XmlType extends types.Insert
constructor: (uid, @tagname, attributes, elements, @xml)->
### In case you make this instanceof Insert again
if prev? and (not next?) and prev.type?
# adjust what you actually mean. you want to insert after prev, then
# next is not defined. but we only insert after non-deleted elements.
# This is also handled in TextInsert.
while prev.isDeleted()
prev = prev.prev_cl
next = prev.next_cl
###
super(uid)
if @xml?._y?
d = new types.Delete undefined, @xml._y
HB.addOperation(d).execute()
@xml._y = null
if attributes? and elements?
@saveOperation 'attributes', attributes
@saveOperation 'elements', elements
else if (not attributes?) and (not elements?)
@attributes = new types.JsonType()
@attributes.setMutableDefault 'immutable'
HB.addOperation(@attributes).execute()
@elements = new types.WordType()
@elements.parent = @
HB.addOperation(@elements).execute()
else
throw new Error "Either define attribute and elements both, or none of them"
if @xml?
@tagname = @xml.tagName
for i in [0...@xml.attributes.length]
attr = xml.attributes[i]
@attributes.val(attr.name, attr.value)
for n in @xml.childNodes
if n.nodeType is n.TEXT_NODE
word = new TextNodeType(undefined, n)
HB.addOperation(word).execute()
@elements.push word
else if n.nodeType is n.ELEMENT_NODE
element = new XmlType undefined, undefined, undefined, undefined, n
HB.addOperation(element).execute()
@elements.push element
else
throw new Error "I don't know Node-type #{n.nodeType}!!"
@setXmlProxy()
undefined
#
# Identifies this class.
# Use it in order to check whether this is an xml-type or something else.
#
type: "XmlType"
applyDelete: (op)->
if @insert_parent? and not @insert_parent.isDeleted()
@insert_parent.applyDelete op
else
@attributes.applyDelete()
@elements.applyDelete()
super
cleanup: ()->
super()
setXmlProxy: ()->
@xml._y = @
that = @
@elements.on 'insert', (event, op)->
if op.creator isnt HB.getUserId() and this is that.elements
newNode = op.content.val()
right = op.next_cl
while right? and right.isDeleted()
right = right.next_cl
rightNode = null
if right.type isnt 'Delimiter'
rightNode = right.val().val()
dont_proxy ()->
that.xml.insertBefore newNode, rightNode
@elements.on 'delete', (event, op)->
del_op = op.deleted_by[0]
if del_op? and del_op.creator isnt HB.getUserId() and this is that.elements
deleted = op.content.val()
dont_proxy ()->
that.xml.removeChild deleted
@attributes.on ['add', 'update'], (event, property_name, op)->
if op.creator isnt HB.getUserId() and this is that.attributes
dont_proxy ()->
newval = op.val().val()
if newval?
that.xml.setAttribute(property_name, op.val().val())
else
that.xml.removeAttribute(property_name)
## Here are all methods that proxy the behavior of the xml
# you want to find a specific child element. Since they are carried by an Insert-Type, you want to find that Insert-Operation.
# @param child {DomElement} Dom element.
# @return {InsertType} This carries the XmlType that represents the DomElement (child). false if i couldn't find it.
#
findNode = (child)->
if not child?
throw new Error "you must specify a parameter!"
child = child._y
elem = that.elements.beginning.next_cl
while elem.type isnt 'Delimiter' and elem.content isnt child
elem = elem.next_cl
if elem.type is 'Delimiter'
false
else
elem
insertBefore = (insertedNode_s, adjacentNode)->
next = null
if adjacentNode?
next = findNode adjacentNode
prev = null
if next
prev = next.prev_cl
else
prev = @_y.elements.end.prev_cl
while prev.isDeleted()
prev = prev.prev_cl
inserted_nodes = null
if insertedNode_s.nodeType is insertedNode_s.DOCUMENT_FRAGMENT_NODE
child = insertedNode_s.lastChild
while child?
element = new XmlType undefined, undefined, undefined, undefined, child
HB.addOperation(element).execute()
that.elements.insertAfter prev, element
child = child.previousSibling
else
element = new XmlType undefined, undefined, undefined, undefined, insertedNode_s
HB.addOperation(element).execute()
that.elements.insertAfter prev, element
@xml._proxy 'insertBefore', insertBefore
@xml._proxy 'appendChild', insertBefore
@xml._proxy 'removeAttribute', (name)->
that.attributes.val(name, undefined)
@xml._proxy 'setAttribute', (name, value)->
that.attributes.val name, value
renewClassList = (newclass)->
dont_do_it = false
if newclass?
for elem in this
if newclass is elem
dont_do_it = true
value = Array.prototype.join.call this, " "
if newclass? and not dont_do_it
value += " "+newclass
that.attributes.val('class', value )
_proxy.call @xml.classList, 'add', renewClassList
_proxy.call @xml.classList, 'remove', renewClassList
@xml.__defineSetter__ 'className', (val)->
@setAttribute('class', val)
@xml.__defineGetter__ 'className', ()->
that.attributes.val('class')
@xml.__defineSetter__ 'textContent', (val)->
# remove all nodes
elem = that.xml.firstChild
while elem?
remove = elem
elem = elem.nextSibling
that.xml.removeChild remove
# insert word content
if val isnt ""
text_node = document.createTextNode val
that.xml.appendChild text_node
removeChild = (node)->
elem = findNode node
if not elem
throw new Error "You are only allowed to delete existing (direct) child elements!"
d = new types.Delete undefined, elem
HB.addOperation(d).execute()
node._y = null
@xml._proxy 'removeChild', removeChild
@xml._proxy 'replaceChild', (insertedNode, replacedNode)->
insertBefore.call this, insertedNode, replacedNode
removeChild.call this, replacedNode
val: (enforce = false)->
if document?
if (not @xml?) or enforce
@xml = document.createElement @tagname
attr = @attributes.val()
for attr_name, value of attr
if value?
a = document.createAttribute attr_name
a.value = value
@xml.setAttributeNode a
e = @elements.beginning.next_cl
while e.type isnt "Delimiter"
n = e.content
if not e.isDeleted() and e.content? # TODO: how can this happen? Probably because listeners
if n.type is "XmlType"
@xml.appendChild n.val(enforce)
else if n.type is "TextNodeType"
text_node = n.val()
@xml.appendChild text_node
else
throw new Error "Internal structure cannot be transformed to dom"
e = e.next_cl
@setXmlProxy()
@xml
execute: ()->
super()
###
if not @validateSavedOperations()
return false
else
return true
###
#
# Get the parent of this JsonType.
# @return {XmlType}
#
getParent: ()->
@parent
#
# @private
#
# Convert all relevant information of this operation to the json-format.
# This result can be send to other clients.
#
_encode: ()->
json =
{
'type' : @type
'attributes' : @attributes.getUid()
'elements' : @elements.getUid()
'tagname' : @tagname
'uid' : @getUid()
}
json
parser['XmlType'] = (json)->
{
'uid' : uid
'attributes' : attributes
'elements' : elements
'tagname' : tagname
} = json
new XmlType uid, tagname, attributes, elements, undefined
#
# @nodoc
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
#
class TextNodeType extends types.ImmutableObject
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
# @param {Object} content
#
constructor: (uid, content)->
if content._y?
d = new types.Delete undefined, content._y
HB.addOperation(d).execute()
content._y = null
content._y = @
super uid, content
applyDelete: (op)->
if @insert_parent? and not @insert_parent.isDeleted()
@insert_parent.applyDelete op
else
super
type: "TextNodeType"
#
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: ()->
json = {
'type': @type
'uid' : @getUid()
'content' : @content.textContent
}
json
parser['TextNodeType'] = (json)->
{
'uid' : uid
'content' : content
} = json
textnode = document.createTextNode content
new TextNodeType uid, textnode
types['XmlType'] = XmlType
json_types
###

View File

@@ -1,59 +0,0 @@
Y = require './y'
bindToChildren = (that)->
for i in [0...that.children.length]
attr = that.children.item(i)
if attr.name?
attr.val = that.val.val(attr.name)
that.val.observe (events)->
for event in events
if event.name?
for i in [0...that.children.length]
attr = that.children.item(i)
if attr.name? and attr.name is event.name
newVal = that.val.val(attr.name)
if attr.val isnt newVal
attr.val = newVal
Polymer "y-object",
ready: ()->
if @connector?
@val = new Y @connector
bindToChildren @
else if @val?
bindToChildren @
valChanged: ()->
if @val? and @val.type is "Object"
bindToChildren @
connectorChanged: ()->
if (not @val?)
@val = new Y @connector
bindToChildren @
Polymer "y-property",
ready: ()->
if @val? and @name?
if @val.constructor is Object
@val = @parentElement.val(@name,@val).val(@name)
# TODO: please use instanceof instead of .type,
# since it is more safe (consider someone putting a custom Object type here)
else if typeof @val is "string"
@parentElement.val(@name,@val)
if @val.type is "Object"
bindToChildren @
valChanged: ()->
if @val? and @name?
if @val.constructor is Object
@val = @parentElement.val.val(@name,@val).val(@name)
# TODO: please use instanceof instead of .type,
# since it is more safe (consider someone putting a custom Object type here)
else if @val.type is "Object"
bindToChildren @
else if @parentElement.val?.val? and @val isnt @parentElement.val.val(@name)
@parentElement.val.val @name, @val

View File

@@ -1,48 +0,0 @@
json_types_uninitialized = require "./Types/JsonTypes"
HistoryBuffer = require "./HistoryBuffer"
Engine = require "./Engine"
adaptConnector = require "./ConnectorAdapter"
createY = (connector)->
user_id = null
if connector.id?
user_id = connector.id # TODO: change to getUniqueId()
else
user_id = "_temp"
connector.onUserIdSet (id)->
user_id = id
HB.resetUserId id
HB = new HistoryBuffer user_id
type_manager = json_types_uninitialized HB
types = type_manager.types
#
# Framework for Json data-structures.
# Known values that are supported:
# * String
# * Integer
# * Array
#
class Y extends types.Object
#
# @param {String} user_id Unique id of the peer.
# @param {Connector} Connector the connector class.
#
constructor: ()->
@connector = connector
@HB = HB
@types = types
@engine = new Engine @HB, type_manager.types
adaptConnector @connector, @engine, @HB, type_manager.execution_listener
super
getConnector: ()->
@connector
return new Y(HB.getReservedUniqueIdentifier()).execute()
module.exports = createY
if window? and not window.Y?
window.Y = createY

6987
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +1,93 @@
{
"name": "yjs",
"version": "0.3.1",
"description": "A Framework that enables Real-Time Collaboration on arbitrary data structures.",
"main": "./build/node/y.js",
"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 ❤",
"url": "https://github.com/sponsors/dmonad"
},
"scripts": {
"test": "./node_modules/.bin/gulp test"
"test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
"dist": "rm -rf dist && rollup -c && tsc",
"watch": "rollup -wc",
"lint": "markdownlint README.md && standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
"serve-docs": "npm run docs && http-server ./docs/",
"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"
},
"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": [
"dist/yjs.*",
"dist/src",
"src",
"tests/testHelper.js",
"dist/testHelper.mjs",
"sponsor-y.js"
],
"dictionaries": {
"test": "tests"
},
"standard": {
"ignore": [
"/dist",
"/node_modules",
"/docs"
]
},
"repository": {
"type": "git",
"url": "https://github.com/rwth-acis/yjs"
"url": "https://github.com/yjs/yjs.git"
},
"keywords": [
"OT",
"collaboration",
"Yata",
"synchronization",
"ShareJS",
"Coweb",
"concurrency"
"Yjs",
"CRDT",
"offline",
"offline-first",
"shared-editing",
"concurrency",
"collaboration"
],
"author": "Kevin Jahns",
"email": "kevin.jahns@rwth-aachen.de",
"email": "kevin.jahns@protonmail.com",
"license": "MIT",
"bugs": {
"url": "https://github.com/rwth-acis/yjs/issues"
"url": "https://github.com/yjs/yjs/issues"
},
"homepage": "https://dadamonad.github.io/yjs/",
"homepage": "https://docs.yjs.dev",
"dependencies": {
"lib0": "^0.2.49"
},
"devDependencies": {
"codo": "^2.0.9",
"underscore": "^1.6.0",
"chai": "^1.9.1",
"codo": "^2.0.9",
"coffee-errors": "~0.8.6",
"coffee-script": "^1.7.1",
"coffeeify": "^0.6.0",
"gulp": "^3.8.7",
"gulp-browserify": "^0.5.0",
"gulp-cached": "^1.0.1",
"gulp-coffee": "^2.1.1",
"gulp-coffeeify": "^0.1.2",
"gulp-coffeelint": "^0.3.3",
"gulp-concat": "^2.3.4",
"gulp-copy": "0.0.2",
"gulp-debug": "^1.0.0",
"gulp-git": "^0.5.0",
"gulp-if": "^1.2.4",
"gulp-ignore": "^1.2.0",
"gulp-ljs": "^0.1.1",
"gulp-mocha": "^0.5.2",
"gulp-mocha-phantomjs": "^0.5.0",
"gulp-plumber": "^0.6.6",
"gulp-rename": "^1.2.0",
"gulp-rimraf": "^0.1.0",
"gulp-run": "^1.6.3",
"gulp-sourcemaps": "^1.1.1",
"gulp-uglify": "^0.3.1",
"gulp-watch": "^3.0.0",
"jquery": "^2.1.1",
"mocha": "^1.21.4",
"sinon": "^1.10.2",
"sinon-chai": "^2.5.0"
"@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.7",
"markdownlint-cli": "^0.23.2",
"rollup": "^2.60.0",
"standard": "^16.0.4",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^4.4.4",
"y-protocols": "^1.0.5"
}
}

111
rollup.config.js Normal file
View File

@@ -0,0 +1,111 @@
import nodeResolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
const localImports = process.env.LOCALIMPORTS
const customModules = new Set([
'y-websocket',
'y-codemirror',
'y-ace',
'y-textarea',
'y-quill',
'y-dom',
'y-prosemirror'
])
/**
* @type {Set<any>}
*/
const customLibModules = new Set([
'lib0',
'y-protocols'
])
const debugResolve = {
resolveId (importee) {
if (importee === 'yjs') {
return `${process.cwd()}/src/index.js`
}
if (localImports) {
if (customModules.has(importee.split('/')[0])) {
return `${process.cwd()}/../${importee}/src/${importee}.js`
}
if (customLibModules.has(importee.split('/')[0])) {
return `${process.cwd()}/../${importee}`
}
}
return null
}
}
export default [{
input: './src/index.js',
output: {
name: 'Y',
file: 'dist/yjs.cjs',
format: 'cjs',
sourcemap: true,
paths: path => {
if (/^lib0\//.test(path)) {
return `lib0/dist/${path.slice(5)}.cjs`
}
return path
}
},
external: id => /^lib0\//.test(id)
}, {
input: './src/index.js',
output: {
name: 'Y',
file: 'dist/yjs.mjs',
format: 'esm',
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: {
name: 'test',
file: 'dist/tests.js',
format: 'iife',
sourcemap: true
},
plugins: [
debugResolve,
nodeResolve({
mainFields: ['module', 'browser', 'main']
}),
commonjs()
]
}, {
input: './tests/index.js',
output: {
name: 'test',
file: 'dist/tests.cjs',
format: 'cjs',
sourcemap: true
},
plugins: [
debugResolve,
nodeResolve({
mainFields: ['module', 'main']
}),
commonjs()
],
external: ['isomorphic.js']
}]

122
src/index.js Normal file
View File

@@ -0,0 +1,122 @@
/** eslint-env browser */
export {
Doc,
Transaction,
YArray as Array,
YMap as Map,
YText as Text,
YXmlText as XmlText,
YXmlHook as XmlHook,
YXmlElement as XmlElement,
YXmlFragment as XmlFragment,
YXmlEvent,
YMapEvent,
YArrayEvent,
YTextEvent,
YEvent,
Item,
AbstractStruct,
GC,
ContentBinary,
ContentDeleted,
ContentEmbed,
ContentFormat,
ContentJSON,
ContentAny,
ContentString,
ContentType,
AbstractType,
getTypeChildren,
createRelativePositionFromTypeIndex,
createRelativePositionFromJSON,
createAbsolutePositionFromRelativePosition,
compareRelativePositions,
AbsolutePosition,
RelativePosition,
ID,
createID,
compareIDs,
getState,
Snapshot,
createSnapshot,
createDeleteSet,
createDeleteSetFromStructStore,
cleanupYTextFormatting,
snapshot,
emptySnapshot,
findRootTypeKey,
findIndexSS,
getItem,
typeListToArraySnapshot,
typeMapGetSnapshot,
createDocFromSnapshot,
iterateDeletedStructs,
applyUpdate,
applyUpdateV2,
readUpdate,
readUpdateV2,
encodeStateAsUpdate,
encodeStateAsUpdateV2,
encodeStateVector,
UndoManager,
decodeSnapshot,
encodeSnapshot,
decodeSnapshotV2,
encodeSnapshotV2,
decodeStateVector,
logUpdate,
logUpdateV2,
decodeUpdate,
decodeUpdateV2,
relativePositionToJSON,
isDeleted,
isParentOf,
equalSnapshots,
PermanentUserData, // @TODO experimental
tryGc,
transact,
AbstractConnector,
logType,
mergeUpdates,
mergeUpdatesV2,
parseUpdateMeta,
parseUpdateMetaV2,
encodeStateVectorFromUpdate,
encodeStateVectorFromUpdateV2,
encodeRelativePosition,
decodeRelativePosition,
diffUpdate,
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

43
src/internals.js Normal file
View File

@@ -0,0 +1,43 @@
export * from './utils/AbstractConnector.js'
export * from './utils/DeleteSet.js'
export * from './utils/Doc.js'
export * from './utils/UpdateDecoder.js'
export * from './utils/UpdateEncoder.js'
export * from './utils/encoding.js'
export * from './utils/EventHandler.js'
export * from './utils/ID.js'
export * from './utils/isParentOf.js'
export * from './utils/logging.js'
export * from './utils/PermanentUserData.js'
export * from './utils/RelativePosition.js'
export * from './utils/Snapshot.js'
export * from './utils/StructStore.js'
export * from './utils/Transaction.js'
export * from './utils/UndoManager.js'
export * from './utils/updates.js'
export * from './utils/YEvent.js'
export * from './types/AbstractType.js'
export * from './types/YArray.js'
export * from './types/YMap.js'
export * from './types/YText.js'
export * from './types/YXmlFragment.js'
export * from './types/YXmlElement.js'
export * from './types/YXmlEvent.js'
export * from './types/YXmlHook.js'
export * from './types/YXmlText.js'
export * from './structs/AbstractStruct.js'
export * from './structs/GC.js'
export * from './structs/ContentBinary.js'
export * from './structs/ContentDeleted.js'
export * from './structs/ContentDoc.js'
export * from './structs/ContentEmbed.js'
export * from './structs/ContentFormat.js'
export * from './structs/ContentJSON.js'
export * from './structs/ContentAny.js'
export * from './structs/ContentString.js'
export * from './structs/ContentType.js'
export * from './structs/Item.js'
export * from './structs/Skip.js'

View File

@@ -0,0 +1,52 @@
import {
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error'
export class AbstractStruct {
/**
* @param {ID} id
* @param {number} length
*/
constructor (id, length) {
this.id = id
this.length = length
}
/**
* @type {boolean}
*/
get deleted () {
throw error.methodUnimplemented()
}
/**
* Merge this struct with the item to the right.
* This method is already assuming that `this.id.clock + this.length === this.id.clock`.
* Also this method does *not* remove right from StructStore!
* @param {AbstractStruct} right
* @return {boolean} wether this merged with right
*/
mergeWith (right) {
return false
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
* @param {number} offset
* @param {number} encodingRef
*/
write (encoder, offset, encodingRef) {
throw error.methodUnimplemented()
}
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction, offset) {
throw error.methodUnimplemented()
}
}

108
src/structs/ContentAny.js Normal file
View File

@@ -0,0 +1,108 @@
import {
UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore // eslint-disable-line
} from '../internals.js'
export class ContentAny {
/**
* @param {Array<any>} arr
*/
constructor (arr) {
/**
* @type {Array<any>}
*/
this.arr = arr
}
/**
* @return {number}
*/
getLength () {
return this.arr.length
}
/**
* @return {Array<any>}
*/
getContent () {
return this.arr
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentAny}
*/
copy () {
return new ContentAny(this.arr)
}
/**
* @param {number} offset
* @return {ContentAny}
*/
splice (offset) {
const right = new ContentAny(this.arr.slice(offset))
this.arr = this.arr.slice(0, offset)
return right
}
/**
* @param {ContentAny} right
* @return {boolean}
*/
mergeWith (right) {
this.arr = this.arr.concat(right.arr)
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
const len = this.arr.length
encoder.writeLen(len - offset)
for (let i = offset; i < len; i++) {
const c = this.arr[i]
encoder.writeAny(c)
}
}
/**
* @return {number}
*/
getRef () {
return 8
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentAny}
*/
export const readContentAny = decoder => {
const len = decoder.readLen()
const cs = []
for (let i = 0; i < len; i++) {
cs.push(decoder.readAny())
}
return new ContentAny(cs)
}

View File

@@ -0,0 +1,92 @@
import {
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error'
export class ContentBinary {
/**
* @param {Uint8Array} content
*/
constructor (content) {
this.content = content
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.content]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentBinary}
*/
copy () {
return new ContentBinary(this.content)
}
/**
* @param {number} offset
* @return {ContentBinary}
*/
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {ContentBinary} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeBuf(this.content)
}
/**
* @return {number}
*/
getRef () {
return 3
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
* @return {ContentBinary}
*/
export const readContentBinary = decoder => new ContentBinary(decoder.readBuf())

View File

@@ -0,0 +1,101 @@
import {
addToDeleteSet,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
} from '../internals.js'
export class ContentDeleted {
/**
* @param {number} len
*/
constructor (len) {
this.len = len
}
/**
* @return {number}
*/
getLength () {
return this.len
}
/**
* @return {Array<any>}
*/
getContent () {
return []
}
/**
* @return {boolean}
*/
isCountable () {
return false
}
/**
* @return {ContentDeleted}
*/
copy () {
return new ContentDeleted(this.len)
}
/**
* @param {number} offset
* @return {ContentDeleted}
*/
splice (offset) {
const right = new ContentDeleted(this.len - offset)
this.len = offset
return right
}
/**
* @param {ContentDeleted} right
* @return {boolean}
*/
mergeWith (right) {
this.len += right.len
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
addToDeleteSet(transaction.deleteSet, item.id.client, item.id.clock, this.len)
item.markDeleted()
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeLen(this.len - offset)
}
/**
* @return {number}
*/
getRef () {
return 1
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
* @return {ContentDeleted}
*/
export const readContentDeleted = decoder => new ContentDeleted(decoder.readLen())

141
src/structs/ContentDoc.js Normal file
View File

@@ -0,0 +1,141 @@
import {
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error'
/**
* @param {string} guid
* @param {Object<string, any>} opts
*/
const createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false })
/**
* @private
*/
export class ContentDoc {
/**
* @param {Doc} doc
*/
constructor (doc) {
if (doc._item) {
console.error('This document was already integrated as a sub-document. You should create a second instance instead with the same guid.')
}
/**
* @type {Doc}
*/
this.doc = doc
/**
* @type {any}
*/
const opts = {}
this.opts = opts
if (!doc.gc) {
opts.gc = false
}
if (doc.autoLoad) {
opts.autoLoad = true
}
if (doc.meta !== null) {
opts.meta = doc.meta
}
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.doc]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentDoc}
*/
copy () {
return new ContentDoc(createDocFromOpts(this.doc.guid, this.opts))
}
/**
* @param {number} offset
* @return {ContentDoc}
*/
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {ContentDoc} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
// this needs to be reflected in doc.destroy as well
this.doc._item = item
transaction.subdocsAdded.add(this.doc)
if (this.doc.shouldLoad) {
transaction.subdocsLoaded.add(this.doc)
}
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {
if (transaction.subdocsAdded.has(this.doc)) {
transaction.subdocsAdded.delete(this.doc)
} else {
transaction.subdocsRemoved.add(this.doc)
}
}
/**
* @param {StructStore} store
*/
gc (store) { }
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeString(this.doc.guid)
encoder.writeAny(this.opts)
}
/**
* @return {number}
*/
getRef () {
return 9
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentDoc}
*/
export const readContentDoc = decoder => new ContentDoc(createDocFromOpts(decoder.readString(), decoder.readAny()))

View File

@@ -0,0 +1,98 @@
import {
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error'
/**
* @private
*/
export class ContentEmbed {
/**
* @param {Object} embed
*/
constructor (embed) {
this.embed = embed
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.embed]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentEmbed}
*/
copy () {
return new ContentEmbed(this.embed)
}
/**
* @param {number} offset
* @return {ContentEmbed}
*/
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {ContentEmbed} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeJSON(this.embed)
}
/**
* @return {number}
*/
getRef () {
return 5
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentEmbed}
*/
export const readContentEmbed = decoder => new ContentEmbed(decoder.readJSON())

View File

@@ -0,0 +1,103 @@
import {
AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error'
/**
* @private
*/
export class ContentFormat {
/**
* @param {string} key
* @param {Object} value
*/
constructor (key, value) {
this.key = key
this.value = value
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return []
}
/**
* @return {boolean}
*/
isCountable () {
return false
}
/**
* @return {ContentFormat}
*/
copy () {
return new ContentFormat(this.key, this.value)
}
/**
* @param {number} offset
* @return {ContentFormat}
*/
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {ContentFormat} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
// @todo searchmarker are currently unsupported for rich text documents
/** @type {AbstractType<any>} */ (item.parent)._searchMarker = null
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeKey(this.key)
encoder.writeJSON(this.value)
}
/**
* @return {number}
*/
getRef () {
return 6
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentFormat}
*/
export const readContentFormat = decoder => new ContentFormat(decoder.readKey(), decoder.readJSON())

118
src/structs/ContentJSON.js Normal file
View File

@@ -0,0 +1,118 @@
import {
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line
} from '../internals.js'
/**
* @private
*/
export class ContentJSON {
/**
* @param {Array<any>} arr
*/
constructor (arr) {
/**
* @type {Array<any>}
*/
this.arr = arr
}
/**
* @return {number}
*/
getLength () {
return this.arr.length
}
/**
* @return {Array<any>}
*/
getContent () {
return this.arr
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentJSON}
*/
copy () {
return new ContentJSON(this.arr)
}
/**
* @param {number} offset
* @return {ContentJSON}
*/
splice (offset) {
const right = new ContentJSON(this.arr.slice(offset))
this.arr = this.arr.slice(0, offset)
return right
}
/**
* @param {ContentJSON} right
* @return {boolean}
*/
mergeWith (right) {
this.arr = this.arr.concat(right.arr)
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
const len = this.arr.length
encoder.writeLen(len - offset)
for (let i = offset; i < len; i++) {
const c = this.arr[i]
encoder.writeString(c === undefined ? 'undefined' : JSON.stringify(c))
}
}
/**
* @return {number}
*/
getRef () {
return 2
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentJSON}
*/
export const readContentJSON = decoder => {
const len = decoder.readLen()
const cs = []
for (let i = 0; i < len; i++) {
const c = decoder.readString()
if (c === 'undefined') {
cs.push(undefined)
} else {
cs.push(JSON.parse(c))
}
}
return new ContentJSON(cs)
}

View File

@@ -0,0 +1,112 @@
import {
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line
} from '../internals.js'
/**
* @private
*/
export class ContentString {
/**
* @param {string} str
*/
constructor (str) {
/**
* @type {string}
*/
this.str = str
}
/**
* @return {number}
*/
getLength () {
return this.str.length
}
/**
* @return {Array<any>}
*/
getContent () {
return this.str.split('')
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentString}
*/
copy () {
return new ContentString(this.str)
}
/**
* @param {number} offset
* @return {ContentString}
*/
splice (offset) {
const right = new ContentString(this.str.slice(offset))
this.str = this.str.slice(0, offset)
// Prevent encoding invalid documents because of splitting of surrogate pairs: https://github.com/yjs/yjs/issues/248
const firstCharCode = this.str.charCodeAt(offset - 1)
if (firstCharCode >= 0xD800 && firstCharCode <= 0xDBFF) {
// Last character of the left split is the start of a surrogate utf16/ucs2 pair.
// We don't support splitting of surrogate pairs because this may lead to invalid documents.
// Replace the invalid character with a unicode replacement character (<28> / U+FFFD)
this.str = this.str.slice(0, offset - 1) + '<27>'
// replace right as well
right.str = '<27>' + right.str.slice(1)
}
return right
}
/**
* @param {ContentString} right
* @return {boolean}
*/
mergeWith (right) {
this.str += right.str
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeString(offset === 0 ? this.str : this.str.slice(offset))
}
/**
* @return {number}
*/
getRef () {
return 4
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentString}
*/
export const readContentString = decoder => new ContentString(decoder.readString())

172
src/structs/ContentType.js Normal file
View File

@@ -0,0 +1,172 @@
import {
readYArray,
readYMap,
readYText,
readYXmlElement,
readYXmlFragment,
readYXmlHook,
readYXmlText,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error'
/**
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractType<any>>}
* @private
*/
export const typeRefs = [
readYArray,
readYMap,
readYText,
readYXmlElement,
readYXmlFragment,
readYXmlHook,
readYXmlText
]
export const YArrayRefID = 0
export const YMapRefID = 1
export const YTextRefID = 2
export const YXmlElementRefID = 3
export const YXmlFragmentRefID = 4
export const YXmlHookRefID = 5
export const YXmlTextRefID = 6
/**
* @private
*/
export class ContentType {
/**
* @param {AbstractType<any>} type
*/
constructor (type) {
/**
* @type {AbstractType<any>}
*/
this.type = type
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.type]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentType}
*/
copy () {
return new ContentType(this.type._copy())
}
/**
* @param {number} offset
* @return {ContentType}
*/
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {ContentType} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
this.type._integrate(transaction.doc, item)
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {
let item = this.type._start
while (item !== null) {
if (!item.deleted) {
item.delete(transaction)
} else {
// 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
transaction._mergeStructs.push(item)
}
item = item.right
}
this.type._map.forEach(item => {
if (!item.deleted) {
item.delete(transaction)
} else {
// same as above
transaction._mergeStructs.push(item)
}
})
transaction.changed.delete(this.type)
}
/**
* @param {StructStore} store
*/
gc (store) {
let item = this.type._start
while (item !== null) {
item.gc(store, true)
item = item.right
}
this.type._start = null
this.type._map.forEach(/** @param {Item | null} item */ (item) => {
while (item !== null) {
item.gc(store, true)
item = item.left
}
})
this.type._map = new Map()
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
this.type._write(encoder)
}
/**
* @return {number}
*/
getRef () {
return 7
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentType}
*/
export const readContentType = decoder => new ContentType(typeRefs[decoder.readTypeRef()](decoder))

61
src/structs/GC.js Normal file
View File

@@ -0,0 +1,61 @@
import {
AbstractStruct,
addStruct,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js'
export const structGCRefNumber = 0
/**
* @private
*/
export class GC extends AbstractStruct {
get deleted () {
return true
}
delete () {}
/**
* @param {GC} right
* @return {boolean}
*/
mergeWith (right) {
if (this.constructor !== right.constructor) {
return false
}
this.length += right.length
return true
}
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction, offset) {
if (offset > 0) {
this.id.clock += offset
this.length -= offset
}
addStruct(transaction.doc.store, this)
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeInfo(structGCRefNumber)
encoder.writeLen(this.length - offset)
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @return {null | number}
*/
getMissing (transaction, store) {
return null
}
}

810
src/structs/Item.js Normal file
View File

@@ -0,0 +1,810 @@
import {
GC,
getState,
AbstractStruct,
replaceStruct,
addStruct,
addToDeleteSet,
findRootTypeKey,
compareIDs,
getItem,
getItemCleanEnd,
getItemCleanStart,
readContentDeleted,
readContentBinary,
readContentJSON,
readContentAny,
readContentString,
readContentEmbed,
readContentDoc,
createID,
readContentFormat,
readContentType,
addChangedTypeToTransaction,
isDeleted,
DeleteSet, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error'
import * as binary from 'lib0/binary'
/**
* @todo This should return several items
*
* @param {StructStore} store
* @param {ID} id
* @return {{item:Item, diff:number}}
*/
export const followRedone = (store, id) => {
/**
* @type {ID|null}
*/
let nextID = id
let diff = 0
let item
do {
if (diff > 0) {
nextID = createID(nextID.client, nextID.clock + diff)
}
item = getItem(store, nextID)
diff = nextID.clock - item.id.clock
nextID = item.redone
} while (nextID !== null && item instanceof Item)
return {
item, diff
}
}
/**
* Make sure that neither item nor any of its parents is ever deleted.
*
* This property does not persist when storing it into a database or when
* sending it to other peers
*
* @param {Item|null} item
* @param {boolean} keep
*/
export const keepItem = (item, keep) => {
while (item !== null && item.keep !== keep) {
item.keep = keep
item = /** @type {AbstractType<any>} */ (item.parent)._item
}
}
/**
* Split leftItem into two items
* @param {Transaction} transaction
* @param {Item} leftItem
* @param {number} diff
* @return {Item}
*
* @function
* @private
*/
export const splitItem = (transaction, leftItem, diff) => {
// create rightItem
const { client, clock } = leftItem.id
const rightItem = new Item(
createID(client, clock + diff),
leftItem,
createID(client, clock + diff - 1),
leftItem.right,
leftItem.rightOrigin,
leftItem.parent,
leftItem.parentSub,
leftItem.content.splice(diff)
)
if (leftItem.deleted) {
rightItem.markDeleted()
}
if (leftItem.keep) {
rightItem.keep = true
}
if (leftItem.redone !== null) {
rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff)
}
// update left (do not set leftItem.rightOrigin as it will lead to problems when syncing)
leftItem.right = rightItem
// update right
if (rightItem.right !== null) {
rightItem.right.left = rightItem
}
// right is more specific.
transaction._mergeStructs.push(rightItem)
// update parent._map
if (rightItem.parentSub !== null && rightItem.right === null) {
/** @type {AbstractType<any>} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem)
}
leftItem.length = diff
return rightItem
}
/**
* Redoes the effect of this operation.
*
* @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, itemsToDelete, ignoreRemoteMapChanges) => {
const doc = transaction.doc
const store = doc.store
const ownClientID = doc.clientID
const redone = item.redone
if (redone !== null) {
return getItemCleanStart(transaction, redone)
}
let parentItem = /** @type {AbstractType<any>} */ (item.parent)._item
/**
* @type {Item|null}
*/
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
// find next cloned_redo items
while (left !== null) {
/**
* @type {Item|null}
*/
let leftTrace = left
// trace redone until parent matches
while (leftTrace !== null && /** @type {AbstractType<any>} */ (leftTrace.parent)._item !== parentItem) {
leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
}
if (leftTrace !== null && /** @type {AbstractType<any>} */ (leftTrace.parent)._item === parentItem) {
left = leftTrace
break
}
left = left.left
}
while (right !== null) {
/**
* @type {Item|null}
*/
let rightTrace = right
// trace redone until parent matches
while (rightTrace !== null && /** @type {AbstractType<any>} */ (rightTrace.parent)._item !== parentItem) {
rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
}
if (rightTrace !== null && /** @type {AbstractType<any>} */ (rightTrace.parent)._item === parentItem) {
right = rightTrace
break
}
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)
const redoneItem = new Item(
nextId,
left, left && left.lastId,
right, right && right.id,
parentType,
item.parentSub,
item.content.copy()
)
item.redone = nextId
keepItem(redoneItem, true)
redoneItem.integrate(transaction, 0)
return redoneItem
}
/**
* Abstract class that represents any content.
*/
export class Item extends AbstractStruct {
/**
* @param {ID} id
* @param {Item | null} left
* @param {ID | null} origin
* @param {Item | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it.
* @param {string | null} parentSub
* @param {AbstractContent} content
*/
constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) {
super(id, content.getLength())
/**
* The item that was originally to the left of this item.
* @type {ID | null}
*/
this.origin = origin
/**
* The item that is currently to the left of this item.
* @type {Item | null}
*/
this.left = left
/**
* The item that is currently to the right of this item.
* @type {Item | null}
*/
this.right = right
/**
* The item that was originally to the right of this item.
* @type {ID | null}
*/
this.rightOrigin = rightOrigin
/**
* @type {AbstractType<any>|ID|null}
*/
this.parent = parent
/**
* If the parent refers to this item with some kind of key (e.g. YMap, the
* key is specified here. The key is then used to refer to the list in which
* to insert this item. If `parentSub = null` type._start is the list in
* which to insert to. Otherwise it is `parent._map`.
* @type {String | null}
*/
this.parentSub = parentSub
/**
* If this type's effect is redone this type refers to the type that undid
* this operation.
* @type {ID | null}
*/
this.redone = null
/**
* @type {AbstractContent}
*/
this.content = content
/**
* bit1: keep
* bit2: countable
* bit3: deleted
* bit4: mark - mark node as fast-search-marker
* @type {number} byte
*/
this.info = this.content.isCountable() ? binary.BIT2 : 0
}
/**
* This is used to mark the item as an indexed fast-search marker
*
* @type {boolean}
*/
set marker (isMarked) {
if (((this.info & binary.BIT4) > 0) !== isMarked) {
this.info ^= binary.BIT4
}
}
get marker () {
return (this.info & binary.BIT4) > 0
}
/**
* If true, do not garbage collect this Item.
*/
get keep () {
return (this.info & binary.BIT1) > 0
}
set keep (doKeep) {
if (this.keep !== doKeep) {
this.info ^= binary.BIT1
}
}
get countable () {
return (this.info & binary.BIT2) > 0
}
/**
* Whether this item was deleted or not.
* @type {Boolean}
*/
get deleted () {
return (this.info & binary.BIT3) > 0
}
set deleted (doDelete) {
if (this.deleted !== doDelete) {
this.info ^= binary.BIT3
}
}
markDeleted () {
this.info |= binary.BIT3
}
/**
* Return the creator clientID of the missing op or define missing items and return null.
*
* @param {Transaction} transaction
* @param {StructStore} store
* @return {null | number}
*/
getMissing (transaction, store) {
if (this.origin && this.origin.client !== this.id.client && this.origin.clock >= getState(store, this.origin.client)) {
return this.origin.client
}
if (this.rightOrigin && this.rightOrigin.client !== this.id.client && this.rightOrigin.clock >= getState(store, this.rightOrigin.client)) {
return this.rightOrigin.client
}
if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) {
return this.parent.client
}
// We have all missing ids, now find the items
if (this.origin) {
this.left = getItemCleanEnd(transaction, store, this.origin)
this.origin = this.left.lastId
}
if (this.rightOrigin) {
this.right = getItemCleanStart(transaction, this.rightOrigin)
this.rightOrigin = this.right.id
}
if ((this.left && this.left.constructor === GC) || (this.right && this.right.constructor === GC)) {
this.parent = null
}
// only set parent if this shouldn't be garbage collected
if (!this.parent) {
if (this.left && this.left.constructor === Item) {
this.parent = this.left.parent
this.parentSub = this.left.parentSub
}
if (this.right && this.right.constructor === Item) {
this.parent = this.right.parent
this.parentSub = this.right.parentSub
}
} else if (this.parent.constructor === ID) {
const parentItem = getItem(store, this.parent)
if (parentItem.constructor === GC) {
this.parent = null
} else {
this.parent = /** @type {ContentType} */ (parentItem.content).type
}
}
return null
}
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction, offset) {
if (offset > 0) {
this.id.clock += offset
this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1))
this.origin = this.left.lastId
this.content = this.content.splice(offset)
this.length -= offset
}
if (this.parent) {
if ((!this.left && (!this.right || this.right.left !== null)) || (this.left && this.left.right !== this.right)) {
/**
* @type {Item|null}
*/
let left = this.left
/**
* @type {Item|null}
*/
let o
// set o to the first conflicting item
if (left !== null) {
o = left.right
} else if (this.parentSub !== null) {
o = /** @type {AbstractType<any>} */ (this.parent)._map.get(this.parentSub) || null
while (o !== null && o.left !== null) {
o = o.left
}
} else {
o = /** @type {AbstractType<any>} */ (this.parent)._start
}
// TODO: use something like DeleteSet here (a tree implementation would be best)
// @todo use global set definitions
/**
* @type {Set<Item>}
*/
const conflictingItems = new Set()
/**
* @type {Set<Item>}
*/
const itemsBeforeOrigin = new Set()
// Let c in conflictingItems, b in itemsBeforeOrigin
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
// Note that conflictingItems is a subset of itemsBeforeOrigin
while (o !== null && o !== this.right) {
itemsBeforeOrigin.add(o)
conflictingItems.add(o)
if (compareIDs(this.origin, o.origin)) {
// case 1
if (o.id.client < this.id.client) {
left = o
conflictingItems.clear()
} else if (compareIDs(this.rightOrigin, o.rightOrigin)) {
// this and o are conflicting and point to the same integration points. The id decides which item comes first.
// Since this is to the left of o, we can break here
break
} // else, o might be integrated before an item that this conflicts with. If so, we will find it in the next iterations
} else if (o.origin !== null && itemsBeforeOrigin.has(getItem(transaction.doc.store, o.origin))) { // use getItem instead of getItemCleanEnd because we don't want / need to split items.
// case 2
if (!conflictingItems.has(getItem(transaction.doc.store, o.origin))) {
left = o
conflictingItems.clear()
}
} else {
break
}
o = o.right
}
this.left = left
}
// reconnect left/right + update parent map/start if necessary
if (this.left !== null) {
const right = this.left.right
this.right = right
this.left.right = this
} else {
let r
if (this.parentSub !== null) {
r = /** @type {AbstractType<any>} */ (this.parent)._map.get(this.parentSub) || null
while (r !== null && r.left !== null) {
r = r.left
}
} else {
r = /** @type {AbstractType<any>} */ (this.parent)._start
;/** @type {AbstractType<any>} */ (this.parent)._start = this
}
this.right = r
}
if (this.right !== null) {
this.right.left = this
} else if (this.parentSub !== null) {
// set as current parent value if right === null and this is parentSub
/** @type {AbstractType<any>} */ (this.parent)._map.set(this.parentSub, this)
if (this.left !== null) {
// this is the current attribute value of parent. delete right
this.left.delete(transaction)
}
}
// adjust length of parent
if (this.parentSub === null && this.countable && !this.deleted) {
/** @type {AbstractType<any>} */ (this.parent)._length += this.length
}
addStruct(transaction.doc.store, this)
this.content.integrate(transaction, this)
// add parent to transaction.changed
addChangedTypeToTransaction(transaction, /** @type {AbstractType<any>} */ (this.parent), this.parentSub)
if ((/** @type {AbstractType<any>} */ (this.parent)._item !== null && /** @type {AbstractType<any>} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) {
// delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction)
}
} else {
// parent is not defined. Integrate GC struct instead
new GC(this.id, this.length).integrate(transaction, 0)
}
}
/**
* Returns the next non-deleted item
*/
get next () {
let n = this.right
while (n !== null && n.deleted) {
n = n.right
}
return n
}
/**
* Returns the previous non-deleted item
*/
get prev () {
let n = this.left
while (n !== null && n.deleted) {
n = n.left
}
return n
}
/**
* Computes the last content address of this Item.
*/
get lastId () {
// allocating ids is pretty costly because of the amount of ids created, so we try to reuse whenever possible
return this.length === 1 ? this.id : createID(this.id.client, this.id.clock + this.length - 1)
}
/**
* Try to merge two items
*
* @param {Item} right
* @return {boolean}
*/
mergeWith (right) {
if (
this.constructor === right.constructor &&
compareIDs(right.origin, this.lastId) &&
this.right === right &&
compareIDs(this.rightOrigin, right.rightOrigin) &&
this.id.client === right.id.client &&
this.id.clock + this.length === right.id.clock &&
this.deleted === right.deleted &&
this.redone === null &&
right.redone === null &&
this.content.constructor === right.content.constructor &&
this.content.mergeWith(right.content)
) {
const searchMarker = /** @type {AbstractType<any>} */ (this.parent)._searchMarker
if (searchMarker) {
searchMarker.forEach(marker => {
if (marker.p === right) {
// right is going to be "forgotten" so we need to update the marker
marker.p = this
// adjust marker index
if (!this.deleted && this.countable) {
marker.index -= this.length
}
}
})
}
if (right.keep) {
this.keep = true
}
this.right = right.right
if (this.right !== null) {
this.right.left = this
}
this.length += right.length
return true
}
return false
}
/**
* Mark this Item as deleted.
*
* @param {Transaction} transaction
*/
delete (transaction) {
if (!this.deleted) {
const parent = /** @type {AbstractType<any>} */ (this.parent)
// adjust the length of parent
if (this.countable && this.parentSub === null) {
parent._length -= this.length
}
this.markDeleted()
addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length)
addChangedTypeToTransaction(transaction, parent, this.parentSub)
this.content.delete(transaction)
}
}
/**
* @param {StructStore} store
* @param {boolean} parentGCd
*/
gc (store, parentGCd) {
if (!this.deleted) {
throw error.unexpectedCase()
}
this.content.gc(store)
if (parentGCd) {
replaceStruct(store, this, new GC(this.id, this.length))
} else {
this.content = new ContentDeleted(this.length)
}
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
*
* This is called when this Item is sent to a remote peer.
*
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
* @param {number} offset
*/
write (encoder, offset) {
const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin
const rightOrigin = this.rightOrigin
const parentSub = this.parentSub
const info = (this.content.getRef() & binary.BITS5) |
(origin === null ? 0 : binary.BIT8) | // origin is defined
(rightOrigin === null ? 0 : binary.BIT7) | // right origin is defined
(parentSub === null ? 0 : binary.BIT6) // parentSub is non-null
encoder.writeInfo(info)
if (origin !== null) {
encoder.writeLeftID(origin)
}
if (rightOrigin !== null) {
encoder.writeRightID(rightOrigin)
}
if (origin === null && rightOrigin === null) {
const parent = /** @type {AbstractType<any>} */ (this.parent)
if (parent._item !== undefined) {
const parentItem = parent._item
if (parentItem === null) {
// parent type on y._map
// find the correct key
const ykey = findRootTypeKey(parent)
encoder.writeParentInfo(true) // write parentYKey
encoder.writeString(ykey)
} else {
encoder.writeParentInfo(false) // write parent id
encoder.writeLeftID(parentItem.id)
}
} else if (parent.constructor === String) { // this edge case was added by differential updates
encoder.writeParentInfo(true) // write parentYKey
encoder.writeString(parent)
} else if (parent.constructor === ID) {
encoder.writeParentInfo(false) // write parent id
encoder.writeLeftID(parent)
} else {
error.unexpectedCase()
}
if (parentSub !== null) {
encoder.writeString(parentSub)
}
}
this.content.write(encoder, offset)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @param {number} info
*/
export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder)
/**
* A lookup map for reading Item content.
*
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractContent>}
*/
export const contentRefs = [
() => { error.unexpectedCase() }, // GC is not ItemContent
readContentDeleted, // 1
readContentJSON, // 2
readContentBinary, // 3
readContentString, // 4
readContentEmbed, // 5
readContentFormat, // 6
readContentType, // 7
readContentAny, // 8
readContentDoc, // 9
() => { error.unexpectedCase() } // 10 - Skip is not ItemContent
]
/**
* Do not implement this class!
*/
export class AbstractContent {
/**
* @return {number}
*/
getLength () {
throw error.methodUnimplemented()
}
/**
* @return {Array<any>}
*/
getContent () {
throw error.methodUnimplemented()
}
/**
* Should return false if this Item is some kind of meta information
* (e.g. format information).
*
* * Whether this Item should be addressable via `yarray.get(i)`
* * Whether this Item should be counted when computing yarray.length
*
* @return {boolean}
*/
isCountable () {
throw error.methodUnimplemented()
}
/**
* @return {AbstractContent}
*/
copy () {
throw error.methodUnimplemented()
}
/**
* @param {number} offset
* @return {AbstractContent}
*/
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {AbstractContent} right
* @return {boolean}
*/
mergeWith (right) {
throw error.methodUnimplemented()
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
throw error.methodUnimplemented()
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {
throw error.methodUnimplemented()
}
/**
* @param {StructStore} store
*/
gc (store) {
throw error.methodUnimplemented()
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
throw error.methodUnimplemented()
}
/**
* @return {number}
*/
getRef () {
throw error.methodUnimplemented()
}
}

60
src/structs/Skip.js Normal file
View File

@@ -0,0 +1,60 @@
import {
AbstractStruct,
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error'
import * as encoding from 'lib0/encoding'
export const structSkipRefNumber = 10
/**
* @private
*/
export class Skip extends AbstractStruct {
get deleted () {
return true
}
delete () {}
/**
* @param {Skip} right
* @return {boolean}
*/
mergeWith (right) {
if (this.constructor !== right.constructor) {
return false
}
this.length += right.length
return true
}
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction, offset) {
// skip structs cannot be integrated
error.unexpectedCase()
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeInfo(structSkipRefNumber)
// write as VarUint because Skips can't make use of predictable length-encoding
encoding.writeVarUint(encoder.restEncoder, this.length - offset)
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @return {null | number}
*/
getMissing (transaction, store) {
return null
}
}

935
src/types/AbstractType.js Normal file
View File

@@ -0,0 +1,935 @@
import {
removeEventHandlerListener,
callEventHandlerListeners,
addEventHandlerListener,
createEventHandler,
getState,
isVisible,
ContentType,
createID,
ContentAny,
ContentBinary,
getItemCleanStart,
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
} from '../internals.js'
import * as map from 'lib0/map'
import * as iterator from 'lib0/iterator'
import * as error from 'lib0/error'
import * as math from 'lib0/math'
const maxSearchMarker = 80
/**
* A unique timestamp that identifies each marker.
*
* Time is relative,.. this is more like an ever-increasing clock.
*
* @type {number}
*/
let globalSearchMarkerTimestamp = 0
export class ArraySearchMarker {
/**
* @param {Item} p
* @param {number} index
*/
constructor (p, index) {
p.marker = true
this.p = p
this.index = index
this.timestamp = globalSearchMarkerTimestamp++
}
}
/**
* @param {ArraySearchMarker} marker
*/
const refreshMarkerTimestamp = marker => { marker.timestamp = globalSearchMarkerTimestamp++ }
/**
* This is rather complex so this function is the only thing that should overwrite a marker
*
* @param {ArraySearchMarker} marker
* @param {Item} p
* @param {number} index
*/
const overwriteMarker = (marker, p, index) => {
marker.p.marker = false
marker.p = p
p.marker = true
marker.index = index
marker.timestamp = globalSearchMarkerTimestamp++
}
/**
* @param {Array<ArraySearchMarker>} searchMarker
* @param {Item} p
* @param {number} index
*/
const markPosition = (searchMarker, p, index) => {
if (searchMarker.length >= maxSearchMarker) {
// override oldest marker (we don't want to create more objects)
const marker = searchMarker.reduce((a, b) => a.timestamp < b.timestamp ? a : b)
overwriteMarker(marker, p, index)
return marker
} else {
// create new marker
const pm = new ArraySearchMarker(p, index)
searchMarker.push(pm)
return pm
}
}
/**
* Search marker help us to find positions in the associative array faster.
*
* They speed up the process of finding a position without much bookkeeping.
*
* A maximum of `maxSearchMarker` objects are created.
*
* This function always returns a refreshed marker (updated timestamp)
*
* @param {AbstractType<any>} yarray
* @param {number} index
*/
export const findMarker = (yarray, index) => {
if (yarray._start === null || index === 0 || yarray._searchMarker === null) {
return null
}
const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b)
let p = yarray._start
let pindex = 0
if (marker !== null) {
p = marker.p
pindex = marker.index
refreshMarkerTimestamp(marker) // we used it, we might need to use it again
}
// iterate to right if possible
while (p.right !== null && pindex < index) {
if (!p.deleted && p.countable) {
if (index < pindex + p.length) {
break
}
pindex += p.length
}
p = p.right
}
// iterate to left if necessary (might be that pindex > index)
while (p.left !== null && pindex > index) {
p = p.left
if (!p.deleted && p.countable) {
pindex -= p.length
}
}
// we want to make sure that p can't be merged with left, because that would screw up everything
// in that cas just return what we have (it is most likely the best marker anyway)
// iterate to left until p can't be merged with left
while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) {
p = p.left
if (!p.deleted && p.countable) {
pindex -= p.length
}
}
// @todo remove!
// assure position
// {
// let start = yarray._start
// let pos = 0
// while (start !== p) {
// if (!start.deleted && start.countable) {
// pos += start.length
// }
// start = /** @type {Item} */ (start.right)
// }
// if (pos !== pindex) {
// debugger
// throw new Error('Gotcha position fail!')
// }
// }
// if (marker) {
// if (window.lengthes == null) {
// window.lengthes = []
// window.getLengthes = () => window.lengthes.sort((a, b) => a - b)
// }
// window.lengthes.push(marker.index - pindex)
// console.log('distance', marker.index - pindex, 'len', p && p.parent.length)
// }
if (marker !== null && math.abs(marker.index - pindex) < /** @type {YText|YArray<any>} */ (p.parent).length / maxSearchMarker) {
// adjust existing marker
overwriteMarker(marker, p, pindex)
return marker
} else {
// create new marker
return markPosition(yarray._searchMarker, p, pindex)
}
}
/**
* Update markers when a change happened.
*
* This should be called before doing a deletion!
*
* @param {Array<ArraySearchMarker>} searchMarker
* @param {number} index
* @param {number} len If insertion, len is positive. If deletion, len is negative.
*/
export const updateMarkerChanges = (searchMarker, index, len) => {
for (let i = searchMarker.length - 1; i >= 0; i--) {
const m = searchMarker[i]
if (len > 0) {
/**
* @type {Item|null}
*/
let p = m.p
p.marker = false
// Ideally we just want to do a simple position comparison, but this will only work if
// search markers don't point to deleted items for formats.
// Iterate marker to prev undeleted countable position so we know what to do when updating a position
while (p && (p.deleted || !p.countable)) {
p = p.left
if (p && !p.deleted && p.countable) {
// adjust position. the loop should break now
m.index -= p.length
}
}
if (p === null || p.marker === true) {
// remove search marker if updated position is null or if position is already marked
searchMarker.splice(i, 1)
continue
}
m.p = p
p.marker = true
}
if (index < m.index || (len > 0 && index === m.index)) { // a simple index <= m.index check would actually suffice
m.index = math.max(index, m.index + len)
}
}
}
/**
* Accumulate all (list) children of a type and return them as an Array.
*
* @param {AbstractType<any>} t
* @return {Array<Item>}
*/
export const getTypeChildren = t => {
let s = t._start
const arr = []
while (s) {
arr.push(s)
s = s.right
}
return arr
}
/**
* Call event listeners with an event. This will also add an event to all
* parents (for `.observeDeep` handlers).
*
* @template EventType
* @param {AbstractType<EventType>} type
* @param {Transaction} transaction
* @param {EventType} event
*/
export const callTypeObservers = (type, transaction, event) => {
const changedType = type
const changedParentTypes = transaction.changedParentTypes
while (true) {
// @ts-ignore
map.setIfUndefined(changedParentTypes, type, () => []).push(event)
if (type._item === null) {
break
}
type = /** @type {AbstractType<any>} */ (type._item.parent)
}
callEventHandlerListeners(changedType._eH, event, transaction)
}
/**
* @template EventType
* Abstract Yjs Type class
*/
export class AbstractType {
constructor () {
/**
* @type {Item|null}
*/
this._item = null
/**
* @type {Map<string,Item>}
*/
this._map = new Map()
/**
* @type {Item|null}
*/
this._start = null
/**
* @type {Doc|null}
*/
this.doc = null
this._length = 0
/**
* Event handlers
* @type {EventHandler<EventType,Transaction>}
*/
this._eH = createEventHandler()
/**
* Deep event handlers
* @type {EventHandler<Array<YEvent<any>>,Transaction>}
*/
this._dEH = createEventHandler()
/**
* @type {null | Array<ArraySearchMarker>}
*/
this._searchMarker = null
}
/**
* @return {AbstractType<any>|null}
*/
get parent () {
return this._item ? /** @type {AbstractType<any>} */ (this._item.parent) : null
}
/**
* Integrate this type into the Yjs instance.
*
* * Save this struct in the os
* * This type is sent to other client
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item|null} item
*/
_integrate (y, item) {
this.doc = y
this._item = item
}
/**
* @return {AbstractType<EventType>}
*/
_copy () {
throw error.methodUnimplemented()
}
/**
* @return {AbstractType<EventType>}
*/
clone () {
throw error.methodUnimplemented()
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) { }
/**
* The first non-deleted item
*/
get _first () {
let n = this._start
while (n !== null && n.deleted) {
n = n.right
}
return n
}
/**
* Creates YEvent and calls all type observers.
* Must be implemented by each type.
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
if (!transaction.local && this._searchMarker) {
this._searchMarker.length = 0
}
}
/**
* Observe all events that are created on this type.
*
* @param {function(EventType, Transaction):void} f Observer function
*/
observe (f) {
addEventHandlerListener(this._eH, f)
}
/**
* Observe all events that are created by this type and its children.
*
* @param {function(Array<YEvent<any>>,Transaction):void} f Observer function
*/
observeDeep (f) {
addEventHandlerListener(this._dEH, f)
}
/**
* Unregister an observer function.
*
* @param {function(EventType,Transaction):void} f Observer function
*/
unobserve (f) {
removeEventHandlerListener(this._eH, f)
}
/**
* Unregister an observer function.
*
* @param {function(Array<YEvent<any>>,Transaction):void} f Observer function
*/
unobserveDeep (f) {
removeEventHandlerListener(this._dEH, f)
}
/**
* @abstract
* @return {any}
*/
toJSON () {}
}
/**
* @param {AbstractType<any>} type
* @param {number} start
* @param {number} end
* @return {Array<any>}
*
* @private
* @function
*/
export const typeListSlice = (type, start, end) => {
if (start < 0) {
start = type._length + start
}
if (end < 0) {
end = type._length + end
}
let len = end - start
const cs = []
let n = type._start
while (n !== null && len > 0) {
if (n.countable && !n.deleted) {
const c = n.content.getContent()
if (c.length <= start) {
start -= c.length
} else {
for (let i = start; i < c.length && len > 0; i++) {
cs.push(c[i])
len--
}
start = 0
}
}
n = n.right
}
return cs
}
/**
* @param {AbstractType<any>} type
* @return {Array<any>}
*
* @private
* @function
*/
export const typeListToArray = type => {
const cs = []
let n = type._start
while (n !== null) {
if (n.countable && !n.deleted) {
const c = n.content.getContent()
for (let i = 0; i < c.length; i++) {
cs.push(c[i])
}
}
n = n.right
}
return cs
}
/**
* @param {AbstractType<any>} type
* @param {Snapshot} snapshot
* @return {Array<any>}
*
* @private
* @function
*/
export const typeListToArraySnapshot = (type, snapshot) => {
const cs = []
let n = type._start
while (n !== null) {
if (n.countable && isVisible(n, snapshot)) {
const c = n.content.getContent()
for (let i = 0; i < c.length; i++) {
cs.push(c[i])
}
}
n = n.right
}
return cs
}
/**
* Executes a provided function on once on overy element of this YArray.
*
* @param {AbstractType<any>} type
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
*
* @private
* @function
*/
export const typeListForEach = (type, f) => {
let index = 0
let n = type._start
while (n !== null) {
if (n.countable && !n.deleted) {
const c = n.content.getContent()
for (let i = 0; i < c.length; i++) {
f(c[i], index++, type)
}
}
n = n.right
}
}
/**
* @template C,R
* @param {AbstractType<any>} type
* @param {function(C,number,AbstractType<any>):R} f
* @return {Array<R>}
*
* @private
* @function
*/
export const typeListMap = (type, f) => {
/**
* @type {Array<any>}
*/
const result = []
typeListForEach(type, (c, i) => {
result.push(f(c, i, type))
})
return result
}
/**
* @param {AbstractType<any>} type
* @return {IterableIterator<any>}
*
* @private
* @function
*/
export const typeListCreateIterator = type => {
let n = type._start
/**
* @type {Array<any>|null}
*/
let currentContent = null
let currentContentIndex = 0
return {
[Symbol.iterator] () {
return this
},
next: () => {
// find some content
if (currentContent === null) {
while (n !== null && n.deleted) {
n = n.right
}
// check if we reached the end, no need to check currentContent, because it does not exist
if (n === null) {
return {
done: true,
value: undefined
}
}
// we found n, so we can set currentContent
currentContent = n.content.getContent()
currentContentIndex = 0
n = n.right // we used the content of n, now iterate to next
}
const value = currentContent[currentContentIndex++]
// check if we need to empty currentContent
if (currentContent.length <= currentContentIndex) {
currentContent = null
}
return {
done: false,
value
}
}
}
}
/**
* Executes a provided function on once on overy element of this YArray.
* Operates on a snapshotted state of the document.
*
* @param {AbstractType<any>} type
* @param {function(any,number,AbstractType<any>):void} f A function to execute on every element of this YArray.
* @param {Snapshot} snapshot
*
* @private
* @function
*/
export const typeListForEachSnapshot = (type, f, snapshot) => {
let index = 0
let n = type._start
while (n !== null) {
if (n.countable && isVisible(n, snapshot)) {
const c = n.content.getContent()
for (let i = 0; i < c.length; i++) {
f(c[i], index++, type)
}
}
n = n.right
}
}
/**
* @param {AbstractType<any>} type
* @param {number} index
* @return {any}
*
* @private
* @function
*/
export const typeListGet = (type, index) => {
const marker = findMarker(type, index)
let n = type._start
if (marker !== null) {
n = marker.p
index -= marker.index
}
for (; n !== null; n = n.right) {
if (!n.deleted && n.countable) {
if (index < n.length) {
return n.content.getContent()[index]
}
index -= n.length
}
}
}
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {Item?} referenceItem
* @param {Array<Object<string,any>|Array<any>|boolean|number|null|string|Uint8Array>} content
*
* @private
* @function
*/
export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => {
let left = referenceItem
const doc = transaction.doc
const ownClientId = doc.clientID
const store = doc.store
const right = referenceItem === null ? parent._start : referenceItem.right
/**
* @type {Array<Object|Array<any>|number|null>}
*/
let jsonContent = []
const packJsonContent = () => {
if (jsonContent.length > 0) {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent))
left.integrate(transaction, 0)
jsonContent = []
}
}
content.forEach(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)
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()
}
const lengthExceeded = error.create('Length exceeded!')
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {number} index
* @param {Array<Object<string,any>|Array<any>|number|null|string|Uint8Array>} content
*
* @private
* @function
*/
export const typeListInsertGenerics = (transaction, parent, index, content) => {
if (index > parent._length) {
throw lengthExceeded
}
if (index === 0) {
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, index, content.length)
}
return typeListInsertGenericsAfter(transaction, parent, null, content)
}
const startIndex = index
const marker = findMarker(parent, index)
let n = parent._start
if (marker !== null) {
n = marker.p
index -= marker.index
// we need to iterate one to the left so that the algorithm works
if (index === 0) {
// @todo refactor this as it actually doesn't consider formats
n = n.prev // important! get the left undeleted item so that we can actually decrease index
index += (n && n.countable && !n.deleted) ? n.length : 0
}
}
for (; n !== null; n = n.right) {
if (!n.deleted && n.countable) {
if (index <= n.length) {
if (index < n.length) {
// insert in-between
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
}
break
}
index -= n.length
}
}
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, startIndex, content.length)
}
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
* @param {number} index
* @param {number} length
*
* @private
* @function
*/
export const typeListDelete = (transaction, parent, index, length) => {
if (length === 0) { return }
const startIndex = index
const startLength = length
const marker = findMarker(parent, index)
let n = parent._start
if (marker !== null) {
n = marker.p
index -= marker.index
}
// compute the first item to be deleted
for (; n !== null && index > 0; n = n.right) {
if (!n.deleted && n.countable) {
if (index < n.length) {
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
}
index -= n.length
}
}
// delete all items until done
while (length > 0 && n !== null) {
if (!n.deleted) {
if (length < n.length) {
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length))
}
n.delete(transaction)
length -= n.length
}
n = n.right
}
if (length > 0) {
throw lengthExceeded
}
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */)
}
}
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {string} key
*
* @private
* @function
*/
export const typeMapDelete = (transaction, parent, key) => {
const c = parent._map.get(key)
if (c !== undefined) {
c.delete(transaction)
}
}
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {string} key
* @param {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} value
*
* @private
* @function
*/
export const typeMapSet = (transaction, parent, key, value) => {
const left = parent._map.get(key) || null
const doc = transaction.doc
const ownClientId = doc.clientID
let content
if (value == null) {
content = new ContentAny([value])
} else {
switch (value.constructor) {
case Number:
case Object:
case Boolean:
case Array:
case String:
content = new ContentAny([value])
break
case Uint8Array:
content = new ContentBinary(/** @type {Uint8Array} */ (value))
break
case Doc:
content = new ContentDoc(/** @type {Doc} */ (value))
break
default:
if (value instanceof AbstractType) {
content = new ContentType(value)
} else {
throw new Error('Unexpected content type')
}
}
}
new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0)
}
/**
* @param {AbstractType<any>} parent
* @param {string} key
* @return {Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
*
* @private
* @function
*/
export const typeMapGet = (parent, key) => {
const val = parent._map.get(key)
return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined
}
/**
* @param {AbstractType<any>} parent
* @return {Object<string,Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
*
* @private
* @function
*/
export const typeMapGetAll = (parent) => {
/**
* @type {Object<string,any>}
*/
const res = {}
parent._map.forEach((value, key) => {
if (!value.deleted) {
res[key] = value.content.getContent()[value.length - 1]
}
})
return res
}
/**
* @param {AbstractType<any>} parent
* @param {string} key
* @return {boolean}
*
* @private
* @function
*/
export const typeMapHas = (parent, key) => {
const val = parent._map.get(key)
return val !== undefined && !val.deleted
}
/**
* @param {AbstractType<any>} parent
* @param {string} key
* @param {Snapshot} snapshot
* @return {Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
*
* @private
* @function
*/
export const typeMapGetSnapshot = (parent, key, snapshot) => {
let v = parent._map.get(key) || null
while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) {
v = v.left
}
return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined
}
/**
* @param {Map<string,Item>} map
* @return {IterableIterator<Array<any>>}
*
* @private
* @function
*/
export const createMapIterator = map => iterator.iteratorFilter(map.entries(), /** @param {any} entry */ entry => !entry[1].deleted)

267
src/types/YArray.js Normal file
View File

@@ -0,0 +1,267 @@
/**
* @module YArray
*/
import {
YEvent,
AbstractType,
typeListGet,
typeListToArray,
typeListForEach,
typeListCreateIterator,
typeListInsertGenerics,
typeListPushGenerics,
typeListDelete,
typeListMap,
YArrayRefID,
callTypeObservers,
transact,
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
} from '../internals.js'
import { typeListSlice } from './AbstractType.js'
/**
* Event that describes the changes on a YArray
* @template T
* @extends YEvent<YArray<T>>
*/
export class YArrayEvent extends YEvent {
/**
* @param {YArray<T>} yarray The changed type
* @param {Transaction} transaction The transaction object
*/
constructor (yarray, transaction) {
super(yarray, transaction)
this._transaction = transaction
}
}
/**
* A shared Array implementation.
* @template T
* @extends AbstractType<YArrayEvent<T>>
* @implements {Iterable<T>}
*/
export class YArray extends AbstractType {
constructor () {
super()
/**
* @type {Array<any>?}
* @private
*/
this._prelimContent = []
/**
* @type {Array<ArraySearchMarker>}
*/
this._searchMarker = []
}
/**
* Construct a new YArray containing the specified items.
* @template T
* @param {Array<T>} items
* @return {YArray<T>}
*/
static from (items) {
const a = new YArray()
a.push(items)
return a
}
/**
* Integrate this type into the Yjs instance.
*
* * Save this struct in the os
* * This type is sent to other client
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item} item
*/
_integrate (y, item) {
super._integrate(y, item)
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
this._prelimContent = null
}
_copy () {
return new YArray()
}
/**
* @return {YArray<T>}
*/
clone () {
const arr = new YArray()
arr.insert(0, this.toArray().map(el =>
el instanceof AbstractType ? el.clone() : el
))
return arr
}
get length () {
return this._prelimContent === null ? this._length : this._prelimContent.length
}
/**
* Creates YArrayEvent and calls observers.
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
super._callObserver(transaction, parentSubs)
callTypeObservers(this, transaction, new YArrayEvent(this, transaction))
}
/**
* Inserts new content at an index.
*
* Important: This function expects an array of content. Not just a content
* object. The reason for this "weirdness" is that inserting several elements
* is very efficient when it is done as a single operation.
*
* @example
* // Insert character 'a' at position 0
* yarray.insert(0, ['a'])
* // Insert numbers 1, 2 at position 1
* yarray.insert(1, [1, 2])
*
* @param {number} index The index to insert content at.
* @param {Array<T>} content The array of content
*/
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, content)
})
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
}
}
/**
* Appends content to this YArray.
*
* @param {Array<T>} content Array of content to append.
*
* @todo Use the following implementation in all types.
*/
push (content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListPushGenerics(transaction, this, content)
})
} else {
/** @type {Array<any>} */ (this._prelimContent).push(...content)
}
}
/**
* Preppends content to this YArray.
*
* @param {Array<T>} content Array of content to preppend.
*/
unshift (content) {
this.insert(0, content)
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} length The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListDelete(transaction, this, index, length)
})
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, length)
}
}
/**
* Returns the i-th element from a YArray.
*
* @param {number} index The index of the element to return from the YArray
* @return {T}
*/
get (index) {
return typeListGet(this, index)
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<T>}
*/
toArray () {
return typeListToArray(this)
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @param {number} [start]
* @param {number} [end]
* @return {Array<T>}
*/
slice (start = 0, end = this.length) {
return typeListSlice(this, start, end)
}
/**
* Transforms this Shared Type to a JSON object.
*
* @return {Array<any>}
*/
toJSON () {
return this.map(c => c instanceof AbstractType ? c.toJSON() : c)
}
/**
* Returns an Array with the result of calling a provided function on every
* element of this YArray.
*
* @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
*/
map (f) {
return typeListMap(this, /** @type {any} */ (f))
}
/**
* Executes a provided function on once on overy element of this YArray.
*
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
typeListForEach(this, f)
}
/**
* @return {IterableIterator<T>}
*/
[Symbol.iterator] () {
return typeListCreateIterator(this)
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YArrayRefID)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
*
* @private
* @function
*/
export const readYArray = decoder => new YArray()

267
src/types/YMap.js Normal file
View File

@@ -0,0 +1,267 @@
/**
* @module YMap
*/
import {
YEvent,
AbstractType,
typeMapDelete,
typeMapSet,
typeMapGet,
typeMapHas,
createMapIterator,
YMapRefID,
callTypeObservers,
transact,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
} from '../internals.js'
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 {
/**
* @param {YMap<T>} ymap The YArray that changed.
* @param {Transaction} transaction
* @param {Set<any>} subs The keys that changed.
*/
constructor (ymap, transaction, subs) {
super(ymap, transaction)
this.keysChanged = subs
}
}
/**
* @template MapType
* A shared Map implementation.
*
* @extends AbstractType<YMapEvent<MapType>>
* @implements {Iterable<MapType>}
*/
export class YMap extends AbstractType {
/**
*
* @param {Iterable<readonly [string, any]>=} entries - an optional iterable to initialize the YMap
*/
constructor (entries) {
super()
/**
* @type {Map<string,any>?}
* @private
*/
this._prelimContent = null
if (entries === undefined) {
this._prelimContent = new Map()
} else {
this._prelimContent = new Map(entries)
}
}
/**
* Integrate this type into the Yjs instance.
*
* * Save this struct in the os
* * This type is sent to other client
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item} item
*/
_integrate (y, item) {
super._integrate(y, item)
;/** @type {Map<string, any>} */ (this._prelimContent).forEach((value, key) => {
this.set(key, value)
})
this._prelimContent = null
}
_copy () {
return new YMap()
}
/**
* @return {YMap<MapType>}
*/
clone () {
const map = new YMap()
this.forEach((value, key) => {
map.set(key, value instanceof AbstractType ? value.clone() : value)
})
return map
}
/**
* Creates YMapEvent and calls observers.
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs))
}
/**
* Transforms this Shared Type to a JSON object.
*
* @return {Object<string,any>}
*/
toJSON () {
/**
* @type {Object<string,MapType>}
*/
const map = {}
this._map.forEach((item, key) => {
if (!item.deleted) {
const v = item.content.getContent()[item.length - 1]
map[key] = v instanceof AbstractType ? v.toJSON() : v
}
})
return map
}
/**
* Returns the size of the YMap (count of key/value pairs)
*
* @return {number}
*/
get size () {
return [...createMapIterator(this._map)].length
}
/**
* Returns the keys for each element in the YMap Type.
*
* @return {IterableIterator<string>}
*/
keys () {
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[0])
}
/**
* Returns the values for each element in the YMap Type.
*
* @return {IterableIterator<any>}
*/
values () {
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1])
}
/**
* Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<any>}
*/
entries () {
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => [v[0], v[1].content.getContent()[v[1].length - 1]])
}
/**
* Executes a provided function on once on every key-value pair.
*
* @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
this._map.forEach((item, key) => {
if (!item.deleted) {
f(item.content.getContent()[item.length - 1], key, this)
}
})
}
/**
* Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<any>}
*/
[Symbol.iterator] () {
return this.entries()
}
/**
* Remove a specified element from this YMap.
*
* @param {string} key The key of the element to remove.
*/
delete (key) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapDelete(transaction, this, key)
})
} else {
/** @type {Map<string, any>} */ (this._prelimContent).delete(key)
}
}
/**
* 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 {MapType} value The value of the element to add
*/
set (key, value) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapSet(transaction, this, key, value)
})
} else {
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
}
return value
}
/**
* Returns a specified element from this YMap.
*
* @param {string} key
* @return {MapType|undefined}
*/
get (key) {
return /** @type {any} */ (typeMapGet(this, key))
}
/**
* Returns a boolean indicating whether the specified key exists or not.
*
* @param {string} key The key to test.
* @return {boolean}
*/
has (key) {
return typeMapHas(this, key)
}
/**
* Removes all elements from this YMap.
*/
clear () {
if (this.doc !== null) {
transact(this.doc, transaction => {
this.forEach(function (value, key, map) {
typeMapDelete(transaction, map, key)
})
})
} else {
/** @type {Map<string, any>} */ (this._prelimContent).clear()
}
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YMapRefID)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
*
* @private
* @function
*/
export const readYMap = decoder => new YMap()

1256
src/types/YText.js Normal file

File diff suppressed because it is too large Load Diff

237
src/types/YXmlElement.js Normal file
View File

@@ -0,0 +1,237 @@
import {
YXmlFragment,
transact,
typeMapDelete,
typeMapHas,
typeMapSet,
typeMapGet,
typeMapGetAll,
typeListForEach,
YXmlElementRefID,
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Snapshot, Doc, Item // eslint-disable-line
} from '../internals.js'
/**
* An YXmlElement imitates the behavior of a
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
*
* * An YXmlElement has attributes (key value pairs)
* * An YXmlElement has childElements that must inherit from YXmlElement
*/
export class YXmlElement extends YXmlFragment {
constructor (nodeName = 'UNDEFINED') {
super()
this.nodeName = nodeName
/**
* @type {Map<string, any>|null}
*/
this._prelimAttrs = new Map()
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get nextSibling () {
const n = this._item ? this._item.next : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get prevSibling () {
const n = this._item ? this._item.prev : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* Integrate this type into the Yjs instance.
*
* * Save this struct in the os
* * This type is sent to other client
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item} item
*/
_integrate (y, item) {
super._integrate(y, item)
;(/** @type {Map<string, any>} */ (this._prelimAttrs)).forEach((value, key) => {
this.setAttribute(key, value)
})
this._prelimAttrs = null
}
/**
* Creates an Item with the same effect as this Item (without position effect)
*
* @return {YXmlElement}
*/
_copy () {
return new YXmlElement(this.nodeName)
}
/**
* @return {YXmlElement}
*/
clone () {
const el = new YXmlElement(this.nodeName)
const attrs = this.getAttributes()
for (const key in attrs) {
el.setAttribute(key, attrs[key])
}
// @ts-ignore
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
return el
}
/**
* Returns the XML serialization of this YXmlElement.
* The attributes are ordered by attribute-name, so you can easily use this
* method to compare YXmlElements
*
* @return {string} The string representation of this type.
*
* @public
*/
toString () {
const attrs = this.getAttributes()
const stringBuilder = []
const keys = []
for (const key in attrs) {
keys.push(key)
}
keys.sort()
const keysLen = keys.length
for (let i = 0; i < keysLen; i++) {
const key = keys[i]
stringBuilder.push(key + '="' + attrs[key] + '"')
}
const nodeName = this.nodeName.toLocaleLowerCase()
const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
}
/**
* Removes an attribute from this YXmlElement.
*
* @param {String} attributeName The attribute name that is to be removed.
*
* @public
*/
removeAttribute (attributeName) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapDelete(transaction, this, attributeName)
})
} else {
/** @type {Map<string,any>} */ (this._prelimAttrs).delete(attributeName)
}
}
/**
* Sets or updates an attribute.
*
* @param {String} attributeName The attribute name that is to be set.
* @param {String} attributeValue The attribute value that is to be set.
*
* @public
*/
setAttribute (attributeName, attributeValue) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapSet(transaction, this, attributeName, attributeValue)
})
} else {
/** @type {Map<string, any>} */ (this._prelimAttrs).set(attributeName, attributeValue)
}
}
/**
* Returns an attribute value that belongs to the attribute name.
*
* @param {String} attributeName The attribute name that identifies the
* queried value.
* @return {String} The queried attribute value.
*
* @public
*/
getAttribute (attributeName) {
return /** @type {any} */ (typeMapGet(this, attributeName))
}
/**
* Returns whether an attribute exists
*
* @param {String} attributeName The attribute name to check for existence.
* @return {boolean} whether the attribute exists.
*
* @public
*/
hasAttribute (attributeName) {
return /** @type {any} */ (typeMapHas(this, attributeName))
}
/**
* Returns all attribute name/value pairs in a JSON Object.
*
* @return {Object<string, any>} A JSON Object that describes the attributes.
*
* @public
*/
getAttributes () {
return typeMapGetAll(this)
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDOM (_document = document, hooks = {}, binding) {
const dom = _document.createElement(this.nodeName)
const attrs = this.getAttributes()
for (const key in attrs) {
dom.setAttribute(key, attrs[key])
}
typeListForEach(this, yxml => {
dom.appendChild(yxml.toDOM(_document, hooks, binding))
})
if (binding !== undefined) {
binding._createAssociation(dom, this)
}
return dom
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
*
* This is called when this Item is sent to a remote peer.
*
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
*/
_write (encoder) {
encoder.writeTypeRef(YXmlElementRefID)
encoder.writeKey(this.nodeName)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {YXmlElement}
*
* @function
*/
export const readYXmlElement = decoder => new YXmlElement(decoder.readKey())

40
src/types/YXmlEvent.js Normal file
View File

@@ -0,0 +1,40 @@
import {
YEvent,
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
} 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 {
/**
* @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created.
* @param {Set<string|null>} subs The set of changed attributes. `null` is included if the
* child list changed.
* @param {Transaction} transaction The transaction instance with wich the
* change was created.
*/
constructor (target, subs, transaction) {
super(target, transaction)
/**
* Whether the children changed.
* @type {Boolean}
* @private
*/
this.childListChanged = false
/**
* Set of all changed attributes.
* @type {Set<string>}
*/
this.attributesChanged = new Set()
subs.forEach((sub) => {
if (sub === null) {
this.childListChanged = true
} else {
this.attributesChanged.add(sub)
}
})
}
}

436
src/types/YXmlFragment.js Normal file
View File

@@ -0,0 +1,436 @@
/**
* @module YXml
*/
import {
YXmlEvent,
YXmlElement,
AbstractType,
typeListMap,
typeListForEach,
typeListInsertGenerics,
typeListInsertGenericsAfter,
typeListDelete,
typeListToArray,
YXmlFragmentRefID,
callTypeObservers,
transact,
typeListGet,
typeListSlice,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error'
/**
* Define the elements to which a set of CSS queries apply.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
*
* @example
* query = '.classSelector'
* query = 'nodeSelector'
* query = '#idSelector'
*
* @typedef {string} CSS_Selector
*/
/**
* Dom filter function.
*
* @callback domFilter
* @param {string} nodeName The nodeName of the element
* @param {Map} attributes The map of attributes.
* @return {boolean} Whether to include the Dom node in the YXmlElement.
*/
/**
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
* position within them.
*
* Can be created with {@link YXmlFragment#createTreeWalker}
*
* @public
* @implements {Iterable<YXmlElement|YXmlText|YXmlElement|YXmlHook>}
*/
export class YXmlTreeWalker {
/**
* @param {YXmlFragment | YXmlElement} root
* @param {function(AbstractType<any>):boolean} [f]
*/
constructor (root, f = () => true) {
this._filter = f
this._root = root
/**
* @type {Item}
*/
this._currentNode = /** @type {Item} */ (root._start)
this._firstCall = true
}
[Symbol.iterator] () {
return this
}
/**
* Get the next node.
*
* @return {IteratorResult<YXmlElement|YXmlText|YXmlHook>} The next node.
*
* @public
*/
next () {
/**
* @type {Item|null}
*/
let n = this._currentNode
let type = n && n.content && /** @type {any} */ (n.content).type
if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item
do {
type = /** @type {any} */ (n.content).type
if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) {
// walk down in the tree
n = type._start
} else {
// walk right or up in the tree
while (n !== null) {
if (n.right !== null) {
n = n.right
break
} else if (n.parent === this._root) {
n = null
} else {
n = /** @type {AbstractType<any>} */ (n.parent)._item
}
}
}
} while (n !== null && (n.deleted || !this._filter(/** @type {ContentType} */ (n.content).type)))
}
this._firstCall = false
if (n === null) {
// @ts-ignore
return { value: undefined, done: true }
}
this._currentNode = n
return { value: /** @type {any} */ (n.content).type, done: false }
}
}
/**
* Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
* A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
* nodeName and it does not have attributes. Though it can be bound to a DOM
* element - in this case the attributes and the nodeName are not shared.
*
* @public
* @extends AbstractType<YXmlEvent>
*/
export class YXmlFragment extends AbstractType {
constructor () {
super()
/**
* @type {Array<any>|null}
*/
this._prelimContent = []
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get firstChild () {
const first = this._first
return first ? first.content.getContent()[0] : null
}
/**
* Integrate this type into the Yjs instance.
*
* * Save this struct in the os
* * This type is sent to other client
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item} item
*/
_integrate (y, item) {
super._integrate(y, item)
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
this._prelimContent = null
}
_copy () {
return new YXmlFragment()
}
/**
* @return {YXmlFragment}
*/
clone () {
const el = new YXmlFragment()
// @ts-ignore
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
return el
}
get length () {
return this._prelimContent === null ? this._length : this._prelimContent.length
}
/**
* Create a subtree of childNodes.
*
* @example
* const walker = elem.createTreeWalker(dom => dom.nodeName === 'div')
* for (let node in walker) {
* // `node` is a div node
* nop(node)
* }
*
* @param {function(AbstractType<any>):boolean} filter Function that is called on each child element and
* returns a Boolean indicating whether the child
* is to be included in the subtree.
* @return {YXmlTreeWalker} A subtree and a position within it.
*
* @public
*/
createTreeWalker (filter) {
return new YXmlTreeWalker(this, filter)
}
/**
* Returns the first YXmlElement that matches the query.
* Similar to DOM's {@link querySelector}.
*
* Query support:
* - tagname
* TODO:
* - id
* - attribute
*
* @param {CSS_Selector} query The query on the children.
* @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null.
*
* @public
*/
querySelector (query) {
query = query.toUpperCase()
// @ts-ignore
const iterator = new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query)
const next = iterator.next()
if (next.done) {
return null
} else {
return next.value
}
}
/**
* Returns all YXmlElements that match the query.
* Similar to Dom's {@link querySelectorAll}.
*
* @todo Does not yet support all queries. Currently only query by tagName.
*
* @param {CSS_Selector} query The query on the children
* @return {Array<YXmlElement|YXmlText|YXmlHook|null>} The elements that match this query.
*
* @public
*/
querySelectorAll (query) {
query = query.toUpperCase()
// @ts-ignore
return Array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
}
/**
* Creates YXmlEvent and calls observers.
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction))
}
/**
* Get the string representation of all the children of this YXmlFragment.
*
* @return {string} The string representation of all children.
*/
toString () {
return typeListMap(this, xml => xml.toString()).join('')
}
/**
* @return {string}
*/
toJSON () {
return this.toString()
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDOM (_document = document, hooks = {}, binding) {
const fragment = _document.createDocumentFragment()
if (binding !== undefined) {
binding._createAssociation(fragment, this)
}
typeListForEach(this, xmlType => {
fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null)
})
return fragment
}
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {number} index The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, content)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, 0, ...content)
}
}
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insertAfter (ref, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
const refItem = (ref && ref instanceof AbstractType) ? ref._item : ref
typeListInsertGenericsAfter(transaction, this, refItem, content)
})
} else {
const pc = /** @type {Array<any>} */ (this._prelimContent)
const index = ref === null ? 0 : pc.findIndex(el => el === ref) + 1
if (index === 0 && ref !== null) {
throw error.create('Reference item not found')
}
pc.splice(index, 0, ...content)
}
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListDelete(transaction, this, index, length)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, length)
}
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<YXmlElement|YXmlText|YXmlHook>}
*/
toArray () {
return typeListToArray(this)
}
/**
* Appends content to this YArray.
*
* @param {Array<YXmlElement|YXmlText>} content Array of content to append.
*/
push (content) {
this.insert(this.length, content)
}
/**
* Preppends content to this YArray.
*
* @param {Array<YXmlElement|YXmlText>} content Array of content to preppend.
*/
unshift (content) {
this.insert(0, content)
}
/**
* Returns the i-th element from a YArray.
*
* @param {number} index The index of the element to return from the YArray
* @return {YXmlElement|YXmlText}
*/
get (index) {
return typeListGet(this, index)
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @param {number} [start]
* @param {number} [end]
* @return {Array<YXmlElement|YXmlText>}
*/
slice (start = 0, end = this.length) {
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.
*
* This is called when this Item is sent to a remote peer.
*
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
*/
_write (encoder) {
encoder.writeTypeRef(YXmlFragmentRefID)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {YXmlFragment}
*
* @private
* @function
*/
export const readYXmlFragment = decoder => new YXmlFragment()

95
src/types/YXmlHook.js Normal file
View File

@@ -0,0 +1,95 @@
import {
YMap,
YXmlHookRefID,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
} from '../internals.js'
/**
* You can manage binding to a custom type with YXmlHook.
*
* @extends {YMap<any>}
*/
export class YXmlHook extends YMap {
/**
* @param {string} hookName nodeName of the Dom Node.
*/
constructor (hookName) {
super()
/**
* @type {string}
*/
this.hookName = hookName
}
/**
* Creates an Item with the same effect as this Item (without position effect)
*/
_copy () {
return new YXmlHook(this.hookName)
}
/**
* @return {YXmlHook}
*/
clone () {
const el = new YXmlHook(this.hookName)
this.forEach((value, key) => {
el.set(key, value)
})
return el
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object.<string, any>} [hooks] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDOM (_document = document, hooks = {}, binding) {
const hook = hooks[this.hookName]
let dom
if (hook !== undefined) {
dom = hook.createDom(this)
} else {
dom = document.createElement(this.hookName)
}
dom.setAttribute('data-yjs-hook', this.hookName)
if (binding !== undefined) {
binding._createAssociation(dom, this)
}
return dom
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
*
* This is called when this Item is sent to a remote peer.
*
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
*/
_write (encoder) {
encoder.writeTypeRef(YXmlHookRefID)
encoder.writeKey(this.hookName)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {YXmlHook}
*
* @private
* @function
*/
export const readYXmlHook = decoder =>
new YXmlHook(decoder.readKey())

121
src/types/YXmlText.js Normal file
View File

@@ -0,0 +1,121 @@
import {
YText,
YXmlTextRefID,
ContentType, YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, // eslint-disable-line
} from '../internals.js'
/**
* Represents text in a Dom Element. In the future this type will also handle
* simple formatting information like bold and italic.
*/
export class YXmlText extends YText {
/**
* @type {YXmlElement|YXmlText|null}
*/
get nextSibling () {
const n = this._item ? this._item.next : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get prevSibling () {
const n = this._item ? this._item.prev : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
_copy () {
return new YXmlText()
}
/**
* @return {YXmlText}
*/
clone () {
const text = new YXmlText()
text.applyDelta(this.toDelta())
return text
}
/**
* Creates a Dom Element that mirrors this YXmlText.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<string, any>} [hooks] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDOM (_document = document, hooks, binding) {
const dom = _document.createTextNode(this.toString())
if (binding !== undefined) {
binding._createAssociation(dom, this)
}
return dom
}
toString () {
// @ts-ignore
return this.toDelta().map(delta => {
const nestedNodes = []
for (const nodeName in delta.attributes) {
const attrs = []
for (const key in delta.attributes[nodeName]) {
attrs.push({ key, value: delta.attributes[nodeName][key] })
}
// sort attributes to get a unique order
attrs.sort((a, b) => a.key < b.key ? -1 : 1)
nestedNodes.push({ nodeName, attrs })
}
// sort node order to get a unique order
nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1)
// now convert to dom string
let str = ''
for (let i = 0; i < nestedNodes.length; i++) {
const node = nestedNodes[i]
str += `<${node.nodeName}`
for (let j = 0; j < node.attrs.length; j++) {
const attr = node.attrs[j]
str += ` ${attr.key}="${attr.value}"`
}
str += '>'
}
str += delta.insert
for (let i = nestedNodes.length - 1; i >= 0; i--) {
str += `</${nestedNodes[i].nodeName}>`
}
return str
}).join('')
}
/**
* @return {string}
*/
toJSON () {
return this.toString()
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YXmlTextRefID)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {YXmlText}
*
* @private
* @function
*/
export const readYXmlText = decoder => new YXmlText()

View File

@@ -0,0 +1,26 @@
import { Observable } from 'lib0/observable'
import {
Doc // eslint-disable-line
} from '../internals.js'
/**
* This is an abstract interface that all Connectors should implement to keep them interchangeable.
*
* @note This interface is experimental and it is not advised to actually inherit this class.
* It just serves as typing information.
*
* @extends {Observable<any>}
*/
export class AbstractConnector extends Observable {
/**
* @param {Doc} ydoc
* @param {any} awareness
*/
constructor (ydoc, awareness) {
super()
this.doc = ydoc
this.awareness = awareness
}
}

326
src/utils/DeleteSet.js Normal file
View File

@@ -0,0 +1,326 @@
import {
findIndexSS,
getState,
splitItem,
iterateStructs,
UpdateEncoderV2,
DSDecoderV1, DSEncoderV1, DSDecoderV2, DSEncoderV2, Item, GC, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js'
import * as array from 'lib0/array'
import * as math from 'lib0/math'
import * as map from 'lib0/map'
import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding'
export class DeleteItem {
/**
* @param {number} clock
* @param {number} len
*/
constructor (clock, len) {
/**
* @type {number}
*/
this.clock = clock
/**
* @type {number}
*/
this.len = len
}
}
/**
* We no longer maintain a DeleteStore. DeleteSet is a temporary object that is created when needed.
* - When created in a transaction, it must only be accessed after sorting, and merging
* - This DeleteSet is send to other clients
* - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore
* - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged.
*/
export class DeleteSet {
constructor () {
/**
* @type {Map<number,Array<DeleteItem>>}
*/
this.clients = new Map()
}
}
/**
* Iterate over all structs that the DeleteSet gc's.
*
* @param {Transaction} transaction
* @param {DeleteSet} ds
* @param {function(GC|Item):void} f
*
* @function
*/
export const iterateDeletedStructs = (transaction, ds, f) =>
ds.clients.forEach((deletes, clientid) => {
const structs = /** @type {Array<GC|Item>} */ (transaction.doc.store.clients.get(clientid))
for (let i = 0; i < deletes.length; i++) {
const del = deletes[i]
iterateStructs(transaction, structs, del.clock, del.len, f)
}
})
/**
* @param {Array<DeleteItem>} dis
* @param {number} clock
* @return {number|null}
*
* @private
* @function
*/
export const findIndexDS = (dis, clock) => {
let left = 0
let right = dis.length - 1
while (left <= right) {
const midindex = math.floor((left + right) / 2)
const mid = dis[midindex]
const midclock = mid.clock
if (midclock <= clock) {
if (clock < midclock + mid.len) {
return midindex
}
left = midindex + 1
} else {
right = midindex - 1
}
}
return null
}
/**
* @param {DeleteSet} ds
* @param {ID} id
* @return {boolean}
*
* @private
* @function
*/
export const isDeleted = (ds, id) => {
const dis = ds.clients.get(id.client)
return dis !== undefined && findIndexDS(dis, id.clock) !== null
}
/**
* @param {DeleteSet} ds
*
* @private
* @function
*/
export const sortAndMergeDeleteSet = ds => {
ds.clients.forEach(dels => {
dels.sort((a, b) => a.clock - b.clock)
// merge items without filtering or splicing the array
// i is the current pointer
// j refers to the current insert position for the pointed item
// try to merge dels[i] into dels[j-1] or set dels[j]=dels[i]
let i, j
for (i = 1, j = 1; i < dels.length; i++) {
const left = dels[j - 1]
const right = dels[i]
if (left.clock + left.len >= right.clock) {
left.len = math.max(left.len, right.clock + right.len - left.clock)
} else {
if (j < i) {
dels[j] = right
}
j++
}
}
dels.length = j
})
}
/**
* @param {Array<DeleteSet>} dss
* @return {DeleteSet} A fresh DeleteSet
*/
export const mergeDeleteSets = dss => {
const merged = new DeleteSet()
for (let dssI = 0; dssI < dss.length; dssI++) {
dss[dssI].clients.forEach((delsLeft, client) => {
if (!merged.clients.has(client)) {
// Write all missing keys from current ds and all following.
// If merged already contains `client` current ds has already been added.
/**
* @type {Array<DeleteItem>}
*/
const dels = delsLeft.slice()
for (let i = dssI + 1; i < dss.length; i++) {
array.appendTo(dels, dss[i].clients.get(client) || [])
}
merged.clients.set(client, dels)
}
})
}
sortAndMergeDeleteSet(merged)
return merged
}
/**
* @param {DeleteSet} ds
* @param {number} client
* @param {number} clock
* @param {number} length
*
* @private
* @function
*/
export const addToDeleteSet = (ds, client, clock, length) => {
map.setIfUndefined(ds.clients, client, () => []).push(new DeleteItem(clock, length))
}
export const createDeleteSet = () => new DeleteSet()
/**
* @param {StructStore} ss
* @return {DeleteSet} Merged and sorted DeleteSet
*
* @private
* @function
*/
export const createDeleteSetFromStructStore = ss => {
const ds = createDeleteSet()
ss.clients.forEach((structs, client) => {
/**
* @type {Array<DeleteItem>}
*/
const dsitems = []
for (let i = 0; i < structs.length; i++) {
const struct = structs[i]
if (struct.deleted) {
const clock = struct.id.clock
let len = struct.length
if (i + 1 < structs.length) {
for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) {
len += next.length
}
}
dsitems.push(new DeleteItem(clock, len))
}
}
if (dsitems.length > 0) {
ds.clients.set(client, dsitems)
}
})
return ds
}
/**
* @param {DSEncoderV1 | DSEncoderV2} encoder
* @param {DeleteSet} ds
*
* @private
* @function
*/
export const writeDeleteSet = (encoder, ds) => {
encoding.writeVarUint(encoder.restEncoder, ds.clients.size)
ds.clients.forEach((dsitems, client) => {
encoder.resetDsCurVal()
encoding.writeVarUint(encoder.restEncoder, client)
const len = dsitems.length
encoding.writeVarUint(encoder.restEncoder, len)
for (let i = 0; i < len; i++) {
const item = dsitems[i]
encoder.writeDsClock(item.clock)
encoder.writeDsLen(item.len)
}
})
}
/**
* @param {DSDecoderV1 | DSDecoderV2} decoder
* @return {DeleteSet}
*
* @private
* @function
*/
export const readDeleteSet = decoder => {
const ds = new DeleteSet()
const numClients = decoding.readVarUint(decoder.restDecoder)
for (let i = 0; i < numClients; i++) {
decoder.resetDsCurVal()
const client = decoding.readVarUint(decoder.restDecoder)
const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
if (numberOfDeletes > 0) {
const dsField = map.setIfUndefined(ds.clients, client, () => [])
for (let i = 0; i < numberOfDeletes; i++) {
dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen()))
}
}
}
return ds
}
/**
* @todo YDecoder also contains references to String and other Decoders. Would make sense to exchange YDecoder.toUint8Array for YDecoder.DsToUint8Array()..
*/
/**
* @param {DSDecoderV1 | DSDecoderV2} decoder
* @param {Transaction} transaction
* @param {StructStore} store
* @return {Uint8Array|null} Returns a v2 update containing all deletes that couldn't be applied yet; or null if all deletes were applied successfully.
*
* @private
* @function
*/
export const readAndApplyDeleteSet = (decoder, transaction, store) => {
const unappliedDS = new DeleteSet()
const numClients = decoding.readVarUint(decoder.restDecoder)
for (let i = 0; i < numClients; i++) {
decoder.resetDsCurVal()
const client = decoding.readVarUint(decoder.restDecoder)
const numberOfDeletes = decoding.readVarUint(decoder.restDecoder)
const structs = store.clients.get(client) || []
const state = getState(store, client)
for (let i = 0; i < numberOfDeletes; i++) {
const clock = decoder.readDsClock()
const clockEnd = clock + decoder.readDsLen()
if (clock < state) {
if (state < clockEnd) {
addToDeleteSet(unappliedDS, client, state, clockEnd - state)
}
let index = findIndexSS(structs, clock)
/**
* We can ignore the case of GC and Delete structs, because we are going to skip them
* @type {Item}
*/
// @ts-ignore
let struct = structs[index]
// split the first item if necessary
if (!struct.deleted && struct.id.clock < clock) {
structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
index++ // increase we now want to use the next struct
}
while (index < structs.length) {
// @ts-ignore
struct = structs[index++]
if (struct.id.clock < clockEnd) {
if (!struct.deleted) {
if (clockEnd < struct.id.clock + struct.length) {
structs.splice(index, 0, splitItem(transaction, struct, clockEnd - struct.id.clock))
}
struct.delete(transaction)
}
} else {
break
}
}
} else {
addToDeleteSet(unappliedDS, client, clock, clockEnd - clock)
}
}
}
if (unappliedDS.clients.size > 0) {
const ds = new UpdateEncoderV2()
encoding.writeVarUint(ds.restEncoder, 0) // encode 0 structs
writeDeleteSet(ds, unappliedDS)
return ds.toUint8Array()
}
return null
}

290
src/utils/Doc.js Normal file
View File

@@ -0,0 +1,290 @@
/**
* @module Y
*/
import {
StructStore,
AbstractType,
YArray,
YText,
YMap,
YXmlFragment,
transact,
ContentDoc, Item, Transaction, YEvent // eslint-disable-line
} from '../internals.js'
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
/**
* @typedef {Object} DocOpts
* @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()
*/
/**
* A Yjs instance handles the state of shared data.
* @extends Observable<string>
*/
export class Doc extends Observable {
/**
* @param {DocOpts} [opts] configuration
*/
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<any>>>}
*/
this.share = new Map()
this.store = new StructStore()
/**
* @type {Transaction | null}
*/
this._transaction = null
/**
* @type {Array<Transaction>}
*/
this._transactionCleanups = []
/**
* @type {Set<Doc>}
*/
this.subdocs = new Set()
/**
* If this document is a subdocument - a document integrated into another document - then _item is defined.
* @type {Item?}
*/
this._item = null
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)
})
})
}
/**
* Notify the parent document that you request to load data into this subdocument (if it is a subdocument).
*
* `load()` might be used in the future to request any provider to load the most current data.
*
* It is safe to call `load()` multiple times.
*/
load () {
const item = this._item
if (item !== null && !this.shouldLoad) {
transact(/** @type {any} */ (item.parent).doc, transaction => {
transaction.subdocsLoaded.add(this)
}, null, true)
}
this.shouldLoad = true
}
getSubdocs () {
return this.subdocs
}
getSubdocGuids () {
return new Set(Array.from(this.subdocs).map(doc => doc.guid))
}
/**
* Changes that happen inside of a transaction are bundled. This means that
* the observer fires _after_ the transaction is finished and that all changes
* that happened inside of the transaction are sent as one message to the
* other peers.
*
* @param {function(Transaction):void} f The function that should be executed as a transaction
* @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin
*
* @public
*/
transact (f, origin = null) {
transact(this, f, origin)
}
/**
* Define a shared data type.
*
* Multiple calls of `y.get(name, TypeConstructor)` yield the same result
* and do not overwrite each other. I.e.
* `y.define(name, Y.Array) === y.define(name, Y.Array)`
*
* After this method is called, the type is also available on `y.share.get(name)`.
*
* *Best Practices:*
* Define all types right after the Yjs instance is created and store them in a separate object.
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
*
* @example
* const y = new Y(..)
* const appState = {
* document: y.getText('document')
* comments: y.getArray('comments')
* }
*
* @param {string} name
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
*
* @public
*/
get (name, TypeConstructor = AbstractType) {
const type = map.setIfUndefined(this.share, name, () => {
// @ts-ignore
const t = new TypeConstructor()
t._integrate(this, null)
return t
})
const Constr = type.constructor
if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) {
if (Constr === AbstractType) {
// @ts-ignore
const t = new TypeConstructor()
t._map = type._map
type._map.forEach(/** @param {Item?} n */ n => {
for (; n !== null; n = n.left) {
// @ts-ignore
n.parent = t
}
})
t._start = type._start
for (let n = t._start; n !== null; n = n.right) {
n.parent = t
}
t._length = type._length
this.share.set(name, t)
t._integrate(this, null)
return t
} else {
throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
}
}
return type
}
/**
* @template T
* @param {string} [name]
* @return {YArray<T>}
*
* @public
*/
getArray (name = '') {
// @ts-ignore
return this.get(name, YArray)
}
/**
* @param {string} [name]
* @return {YText}
*
* @public
*/
getText (name = '') {
// @ts-ignore
return this.get(name, YText)
}
/**
* @template T
* @param {string} [name]
* @return {YMap<T>}
*
* @public
*/
getMap (name = '') {
// @ts-ignore
return this.get(name, YMap)
}
/**
* @param {string} [name]
* @return {YXmlFragment}
*
* @public
*/
getXmlFragment (name = '') {
// @ts-ignore
return this.get(name, YXmlFragment)
}
/**
* Converts the entire document into a js object, recursively traversing each yjs type
* Doesn't log types that have not been defined (using ydoc.getType(..)).
*
* @deprecated Do not use this method and rather call toJSON directly on the shared types.
*
* @return {Object<string, any>}
*/
toJSON () {
/**
* @type {Object<string, any>}
*/
const doc = {}
this.share.forEach((value, key) => {
doc[key] = value.toJSON()
})
return doc
}
/**
* Emit `destroy` event and unregister all event handlers.
*/
destroy () {
array.from(this.subdocs).forEach(subdoc => subdoc.destroy())
const item = this._item
if (item !== null) {
this._item = null
const content = /** @type {ContentDoc} */ (item.content)
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(doc)
}
transaction.subdocsRemoved.add(this)
}, null, true)
}
this.emit('destroyed', [true])
this.emit('destroy', [this])
super.destroy()
}
/**
* @param {string} eventName
* @param {function(...any):any} f
*/
on (eventName, f) {
super.on(eventName, f)
}
/**
* @param {string} eventName
* @param {function} f
*/
off (eventName, f) {
super.off(eventName, f)
}
}

87
src/utils/EventHandler.js Normal file
View File

@@ -0,0 +1,87 @@
import * as f from 'lib0/function'
/**
* General event handler implementation.
*
* @template ARG0, ARG1
*
* @private
*/
export class EventHandler {
constructor () {
/**
* @type {Array<function(ARG0, ARG1):void>}
*/
this.l = []
}
}
/**
* @template ARG0,ARG1
* @returns {EventHandler<ARG0,ARG1>}
*
* @private
* @function
*/
export const createEventHandler = () => new EventHandler()
/**
* Adds an event listener that is called when
* {@link EventHandler#callEventListeners} is called.
*
* @template ARG0,ARG1
* @param {EventHandler<ARG0,ARG1>} eventHandler
* @param {function(ARG0,ARG1):void} f The event handler.
*
* @private
* @function
*/
export const addEventHandlerListener = (eventHandler, f) =>
eventHandler.l.push(f)
/**
* Removes an event listener.
*
* @template ARG0,ARG1
* @param {EventHandler<ARG0,ARG1>} eventHandler
* @param {function(ARG0,ARG1):void} f The event handler that was added with
* {@link EventHandler#addEventListener}
*
* @private
* @function
*/
export const removeEventHandlerListener = (eventHandler, f) => {
const l = eventHandler.l
const len = l.length
eventHandler.l = l.filter(g => f !== g)
if (len === eventHandler.l.length) {
console.error('[yjs] Tried to remove event handler that doesn\'t exist.')
}
}
/**
* Removes all event listeners.
* @template ARG0,ARG1
* @param {EventHandler<ARG0,ARG1>} eventHandler
*
* @private
* @function
*/
export const removeAllEventHandlerListeners = eventHandler => {
eventHandler.l.length = 0
}
/**
* Call all event listeners that were added via
* {@link EventHandler#addEventListener}.
*
* @template ARG0,ARG1
* @param {EventHandler<ARG0,ARG1>} eventHandler
* @param {ARG0} arg0
* @param {ARG1} arg1
*
* @private
* @function
*/
export const callEventHandlerListeners = (eventHandler, arg0, arg1) =>
f.callAll(eventHandler.l, [arg0, arg1])

90
src/utils/ID.js Normal file
View File

@@ -0,0 +1,90 @@
import { AbstractType } from '../internals.js' // eslint-disable-line
import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding'
import * as error from 'lib0/error'
export class ID {
/**
* @param {number} client client id
* @param {number} clock unique per client id, continuous number
*/
constructor (client, clock) {
/**
* Client id
* @type {number}
*/
this.client = client
/**
* unique per client id, continuous number
* @type {number}
*/
this.clock = clock
}
}
/**
* @param {ID | null} a
* @param {ID | null} b
* @return {boolean}
*
* @function
*/
export const compareIDs = (a, b) => a === b || (a !== null && b !== null && a.client === b.client && a.clock === b.clock)
/**
* @param {number} client
* @param {number} clock
*
* @private
* @function
*/
export const createID = (client, clock) => new ID(client, clock)
/**
* @param {encoding.Encoder} encoder
* @param {ID} id
*
* @private
* @function
*/
export const writeID = (encoder, id) => {
encoding.writeVarUint(encoder, id.client)
encoding.writeVarUint(encoder, id.clock)
}
/**
* Read ID.
* * If first varUint read is 0xFFFFFF a RootID is returned.
* * Otherwise an ID is returned
*
* @param {decoding.Decoder} decoder
* @return {ID}
*
* @private
* @function
*/
export const readID = decoder =>
createID(decoding.readVarUint(decoder), decoding.readVarUint(decoder))
/**
* The top types are mapped from y.share.get(keyname) => type.
* `type` does not store any information about the `keyname`.
* This function finds the correct `keyname` for `type` and throws otherwise.
*
* @param {AbstractType<any>} type
* @return {string}
*
* @private
* @function
*/
export const findRootTypeKey = type => {
// @ts-ignore _y must be defined, otherwise unexpected case
for (const [key, value] of type.doc.share.entries()) {
if (value === type) {
return key
}
}
throw error.unexpectedCase()
}

View File

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

View File

@@ -0,0 +1,330 @@
import {
writeID,
readID,
compareIDs,
getState,
findRootTypeKey,
Item,
createID,
ContentType,
followRedone,
ID, Doc, AbstractType // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding'
import * as error from 'lib0/error'
/**
* A relative position is based on the Yjs model and is not affected by document changes.
* E.g. If you place a relative position before a certain character, it will always point to this character.
* If you place a relative position at the end of a type, it will always point to the end of the type.
*
* A numeric position is often unsuited for user selections, because it does not change when content is inserted
* before or after.
*
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the relative position.
*
* One of the properties must be defined.
*
* @example
* // Current cursor position is at position 10
* const relativePosition = createRelativePositionFromIndex(yText, 10)
* // modify yText
* yText.insert(0, 'abc')
* yText.delete(3, 10)
* // Compute the cursor position
* const absolutePosition = createAbsolutePositionFromRelativePosition(y, relativePosition)
* absolutePosition.type === yText // => true
* console.log('cursor location is ' + absolutePosition.index) // => cursor location is 3
*
*/
export class RelativePosition {
/**
* @param {ID|null} type
* @param {string|null} tname
* @param {ID|null} item
* @param {number} assoc
*/
constructor (type, tname, item, assoc = 0) {
/**
* @type {ID|null}
*/
this.type = type
/**
* @type {string|null}
*/
this.tname = tname
/**
* @type {ID | null}
*/
this.item = item
/**
* A relative position is associated to a specific character. By default
* assoc >= 0, the relative position is associated to the character
* after the meant position.
* I.e. position 1 in 'ab' is associated to character 'b'.
*
* If assoc < 0, then the relative position is associated to the caharacter
* before the meant position.
*
* @type {number}
*/
this.assoc = assoc
}
}
/**
* @param {RelativePosition} rpos
* @return {any}
*/
export const relativePositionToJSON = rpos => {
const json = {}
if (rpos.type) {
json.type = rpos.type
}
if (rpos.tname) {
json.tname = rpos.tname
}
if (rpos.item) {
json.item = rpos.item
}
if (rpos.assoc != null) {
json.assoc = rpos.assoc
}
return json
}
/**
* @param {any} json
* @return {RelativePosition}
*
* @function
*/
export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock), json.assoc == null ? 0 : json.assoc)
export class AbsolutePosition {
/**
* @param {AbstractType<any>} type
* @param {number} index
* @param {number} [assoc]
*/
constructor (type, index, assoc = 0) {
/**
* @type {AbstractType<any>}
*/
this.type = type
/**
* @type {number}
*/
this.index = index
this.assoc = assoc
}
}
/**
* @param {AbstractType<any>} type
* @param {number} index
* @param {number} [assoc]
*
* @function
*/
export const createAbsolutePosition = (type, index, assoc = 0) => new AbsolutePosition(type, index, assoc)
/**
* @param {AbstractType<any>} type
* @param {ID|null} item
* @param {number} [assoc]
*
* @function
*/
export const createRelativePosition = (type, item, assoc) => {
let typeid = null
let tname = null
if (type._item === null) {
tname = findRootTypeKey(type)
} else {
typeid = createID(type._item.id.client, type._item.id.clock)
}
return new RelativePosition(typeid, tname, item, assoc)
}
/**
* Create a relativePosition based on a absolute position.
*
* @param {AbstractType<any>} type The base type (e.g. YText or YArray).
* @param {number} index The absolute position.
* @param {number} [assoc]
* @return {RelativePosition}
*
* @function
*/
export const createRelativePositionFromTypeIndex = (type, index, assoc = 0) => {
let t = type._start
if (assoc < 0) {
// associated to the left character or the beginning of a type, increment index if possible.
if (index === 0) {
return createRelativePosition(type, null, assoc)
}
index--
}
while (t !== null) {
if (!t.deleted && t.countable) {
if (t.length > index) {
// case 1: found position somewhere in the linked list
return createRelativePosition(type, createID(t.id.client, t.id.clock + index), assoc)
}
index -= t.length
}
if (t.right === null && assoc < 0) {
// left-associated position, return last available id
return createRelativePosition(type, t.lastId, assoc)
}
t = t.right
}
return createRelativePosition(type, null, assoc)
}
/**
* @param {encoding.Encoder} encoder
* @param {RelativePosition} rpos
*
* @function
*/
export const writeRelativePosition = (encoder, rpos) => {
const { type, tname, item, assoc } = rpos
if (item !== null) {
encoding.writeVarUint(encoder, 0)
writeID(encoder, item)
} else if (tname !== null) {
// case 2: found position at the end of the list and type is stored in y.share
encoding.writeUint8(encoder, 1)
encoding.writeVarString(encoder, tname)
} else if (type !== null) {
// case 3: found position at the end of the list and type is attached to an item
encoding.writeUint8(encoder, 2)
writeID(encoder, type)
} else {
throw error.unexpectedCase()
}
encoding.writeVarInt(encoder, assoc)
return encoder
}
/**
* @param {RelativePosition} rpos
* @return {Uint8Array}
*/
export const encodeRelativePosition = rpos => {
const encoder = encoding.createEncoder()
writeRelativePosition(encoder, rpos)
return encoding.toUint8Array(encoder)
}
/**
* @param {decoding.Decoder} decoder
* @return {RelativePosition}
*
* @function
*/
export const readRelativePosition = decoder => {
let type = null
let tname = null
let itemID = null
switch (decoding.readVarUint(decoder)) {
case 0:
// case 1: found position somewhere in the linked list
itemID = readID(decoder)
break
case 1:
// case 2: found position at the end of the list and type is stored in y.share
tname = decoding.readVarString(decoder)
break
case 2: {
// case 3: found position at the end of the list and type is attached to an item
type = readID(decoder)
}
}
const assoc = decoding.hasContent(decoder) ? decoding.readVarInt(decoder) : 0
return new RelativePosition(type, tname, itemID, assoc)
}
/**
* @param {Uint8Array} uint8Array
* @return {RelativePosition}
*/
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
/**
* @param {RelativePosition} rpos
* @param {Doc} doc
* @return {AbsolutePosition|null}
*
* @function
*/
export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
const store = doc.store
const rightID = rpos.item
const typeID = rpos.type
const tname = rpos.tname
const assoc = rpos.assoc
let type = null
let index = 0
if (rightID !== null) {
if (getState(store, rightID.client) <= rightID.clock) {
return null
}
const res = followRedone(store, rightID)
const right = res.item
if (!(right instanceof Item)) {
return null
}
type = /** @type {AbstractType<any>} */ (right.parent)
if (type._item === null || !type._item.deleted) {
index = (right.deleted || !right.countable) ? 0 : (res.diff + (assoc >= 0 ? 0 : 1)) // adjust position based on left association if necessary
let n = right.left
while (n !== null) {
if (!n.deleted && n.countable) {
index += n.length
}
n = n.left
}
}
} else {
if (tname !== null) {
type = doc.get(tname)
} else if (typeID !== null) {
if (getState(store, typeID.client) <= typeID.clock) {
// type does not exist yet
return null
}
const { item } = followRedone(store, typeID)
if (item instanceof Item && item.content instanceof ContentType) {
type = item.content.type
} else {
// struct is garbage collected
return null
}
} else {
throw error.unexpectedCase()
}
if (assoc >= 0) {
index = type._length
} else {
index = 0
}
}
return createAbsolutePosition(type, index, rpos.assoc)
}
/**
* @param {RelativePosition|null} a
* @param {RelativePosition|null} b
* @return {boolean}
*
* @function
*/
export const compareRelativePositions = (a, b) => a === b || (
a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type) && a.assoc === b.assoc
)

201
src/utils/Snapshot.js Normal file
View File

@@ -0,0 +1,201 @@
import {
isDeleted,
createDeleteSetFromStructStore,
getStateVector,
getItemCleanStart,
iterateDeletedStructs,
writeDeleteSet,
writeStateVector,
readDeleteSet,
readStateVector,
createDeleteSet,
createID,
getState,
findIndexSS,
UpdateEncoderV2,
applyUpdateV2,
DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line
} from '../internals.js'
import * as map from 'lib0/map'
import * as set from 'lib0/set'
import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding'
export class Snapshot {
/**
* @param {DeleteSet} ds
* @param {Map<number,number>} sv state map
*/
constructor (ds, sv) {
/**
* @type {DeleteSet}
*/
this.ds = ds
/**
* State Map
* @type {Map<number,number>}
*/
this.sv = sv
}
}
/**
* @param {Snapshot} snap1
* @param {Snapshot} snap2
* @return {boolean}
*/
export const equalSnapshots = (snap1, snap2) => {
const ds1 = snap1.ds.clients
const ds2 = snap2.ds.clients
const sv1 = snap1.sv
const sv2 = snap2.sv
if (sv1.size !== sv2.size || ds1.size !== ds2.size) {
return false
}
for (const [key, value] of sv1.entries()) {
if (sv2.get(key) !== value) {
return false
}
}
for (const [client, dsitems1] of ds1.entries()) {
const dsitems2 = ds2.get(client) || []
if (dsitems1.length !== dsitems2.length) {
return false
}
for (let i = 0; i < dsitems1.length; i++) {
const dsitem1 = dsitems1[i]
const dsitem2 = dsitems2[i]
if (dsitem1.clock !== dsitem2.clock || dsitem1.len !== dsitem2.len) {
return false
}
}
}
return true
}
/**
* @param {Snapshot} snapshot
* @param {DSEncoderV1 | DSEncoderV2} [encoder]
* @return {Uint8Array}
*/
export const encodeSnapshotV2 = (snapshot, encoder = new DSEncoderV2()) => {
writeDeleteSet(encoder, snapshot.ds)
writeStateVector(encoder, snapshot.sv)
return encoder.toUint8Array()
}
/**
* @param {Snapshot} snapshot
* @return {Uint8Array}
*/
export const encodeSnapshot = snapshot => encodeSnapshotV2(snapshot, new DSEncoderV1())
/**
* @param {Uint8Array} buf
* @param {DSDecoderV1 | DSDecoderV2} [decoder]
* @return {Snapshot}
*/
export const decodeSnapshotV2 = (buf, decoder = new DSDecoderV2(decoding.createDecoder(buf))) => {
return new Snapshot(readDeleteSet(decoder), readStateVector(decoder))
}
/**
* @param {Uint8Array} buf
* @return {Snapshot}
*/
export const decodeSnapshot = buf => decodeSnapshotV2(buf, new DSDecoderV1(decoding.createDecoder(buf)))
/**
* @param {DeleteSet} ds
* @param {Map<number,number>} sm
* @return {Snapshot}
*/
export const createSnapshot = (ds, sm) => new Snapshot(ds, sm)
export const emptySnapshot = createSnapshot(createDeleteSet(), new Map())
/**
* @param {Doc} doc
* @return {Snapshot}
*/
export const snapshot = doc => createSnapshot(createDeleteSetFromStructStore(doc.store), getStateVector(doc.store))
/**
* @param {Item} item
* @param {Snapshot|undefined} snapshot
*
* @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)
/**
* @param {Transaction} transaction
* @param {Snapshot} snapshot
*/
export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
const meta = map.setIfUndefined(transaction.meta, splitSnapshotAffectedStructs, set.create)
const store = transaction.doc.store
// check if we already split for this snapshot
if (!meta.has(snapshot)) {
snapshot.sv.forEach((clock, client) => {
if (clock < getState(store, client)) {
getItemCleanStart(transaction, createID(client, clock))
}
})
iterateDeletedStructs(transaction, snapshot.ds, item => {})
meta.add(snapshot)
}
}
/**
* @param {Doc} originDoc
* @param {Snapshot} snapshot
* @param {Doc} [newDoc] Optionally, you may define the Yjs document that receives the data from originDoc
* @return {Doc}
*/
export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) => {
if (originDoc.gc) {
// we should not try to restore a GC-ed document, because some of the restored items might have their content deleted
throw new Error('originDoc must not be garbage collected')
}
const { sv, ds } = snapshot
const encoder = new UpdateEncoderV2()
originDoc.transact(transaction => {
let size = 0
sv.forEach(clock => {
if (clock > 0) {
size++
}
})
encoding.writeVarUint(encoder.restEncoder, size)
// splitting the structs before writing them to the encoder
for (const [client, clock] of sv) {
if (clock === 0) {
continue
}
if (clock < getState(originDoc.store, client)) {
getItemCleanStart(transaction, createID(client, clock))
}
const structs = originDoc.store.clients.get(client) || []
const lastStructIndex = findIndexSS(structs, clock - 1)
// write # encoded structs
encoding.writeVarUint(encoder.restEncoder, lastStructIndex + 1)
encoder.writeClient(client)
// first clock written is 0
encoding.writeVarUint(encoder.restEncoder, 0)
for (let i = 0; i <= lastStructIndex; i++) {
structs[i].write(encoder, 0)
}
}
writeDeleteSet(encoder, ds)
})
applyUpdateV2(newDoc, encoder.toUint8Array(), 'snapshot')
return newDoc
}

262
src/utils/StructStore.js Normal file
View File

@@ -0,0 +1,262 @@
import {
GC,
splitItem,
Transaction, ID, Item, DSDecoderV2 // eslint-disable-line
} from '../internals.js'
import * as math from 'lib0/math'
import * as error from 'lib0/error'
export class StructStore {
constructor () {
/**
* @type {Map<number,Array<GC|Item>>}
*/
this.clients = new Map()
/**
* @type {null | { missing: Map<number, number>, update: Uint8Array }}
*/
this.pendingStructs = null
/**
* @type {null | Uint8Array}
*/
this.pendingDs = null
}
}
/**
* Return the states as a Map<client,clock>.
* Note that clock refers to the next expected clock id.
*
* @param {StructStore} store
* @return {Map<number,number>}
*
* @public
* @function
*/
export const getStateVector = store => {
const sm = new Map()
store.clients.forEach((structs, client) => {
const struct = structs[structs.length - 1]
sm.set(client, struct.id.clock + struct.length)
})
return sm
}
/**
* @param {StructStore} store
* @param {number} client
* @return {number}
*
* @public
* @function
*/
export const getState = (store, client) => {
const structs = store.clients.get(client)
if (structs === undefined) {
return 0
}
const lastStruct = structs[structs.length - 1]
return lastStruct.id.clock + lastStruct.length
}
/**
* @param {StructStore} store
*
* @private
* @function
*/
export const integretyCheck = store => {
store.clients.forEach(structs => {
for (let i = 1; i < structs.length; i++) {
const l = structs[i - 1]
const r = structs[i]
if (l.id.clock + l.length !== r.id.clock) {
throw new Error('StructStore failed integrety check')
}
}
})
}
/**
* @param {StructStore} store
* @param {GC|Item} struct
*
* @private
* @function
*/
export const addStruct = (store, struct) => {
let structs = store.clients.get(struct.id.client)
if (structs === undefined) {
structs = []
store.clients.set(struct.id.client, structs)
} else {
const lastStruct = structs[structs.length - 1]
if (lastStruct.id.clock + lastStruct.length !== struct.id.clock) {
throw error.unexpectedCase()
}
}
structs.push(struct)
}
/**
* Perform a binary search on a sorted array
* @param {Array<Item|GC>} structs
* @param {number} clock
* @return {number}
*
* @private
* @function
*/
export const findIndexSS = (structs, clock) => {
let left = 0
let right = structs.length - 1
let mid = structs[right]
let midclock = mid.id.clock
if (midclock === clock) {
return right
}
// @todo does it even make sense to pivot the search?
// If a good split misses, it might actually increase the time to find the correct item.
// Currently, the only advantage is that search with pivoting might find the item on the first try.
let midindex = math.floor((clock / (midclock + mid.length - 1)) * right) // pivoting the search
while (left <= right) {
mid = structs[midindex]
midclock = mid.id.clock
if (midclock <= clock) {
if (clock < midclock + mid.length) {
return midindex
}
left = midindex + 1
} else {
right = midindex - 1
}
midindex = math.floor((left + right) / 2)
}
// Always check state before looking for a struct in StructStore
// Therefore the case of not finding a struct is unexpected
throw error.unexpectedCase()
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {StructStore} store
* @param {ID} id
* @return {GC|Item}
*
* @private
* @function
*/
export const find = (store, id) => {
/**
* @type {Array<GC|Item>}
*/
// @ts-ignore
const structs = store.clients.get(id.client)
return structs[findIndexSS(structs, id.clock)]
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
* @private
* @function
*/
export const getItem = /** @type {function(StructStore,ID):Item} */ (find)
/**
* @param {Transaction} transaction
* @param {Array<Item|GC>} structs
* @param {number} clock
*/
export const findIndexCleanStart = (transaction, structs, clock) => {
const index = findIndexSS(structs, clock)
const struct = structs[index]
if (struct.id.clock < clock && struct instanceof Item) {
structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
return index + 1
}
return index
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {Transaction} transaction
* @param {ID} id
* @return {Item}
*
* @private
* @function
*/
export const getItemCleanStart = (transaction, id) => {
const structs = /** @type {Array<Item>} */ (transaction.doc.store.clients.get(id.client))
return structs[findIndexCleanStart(transaction, structs, id.clock)]
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {Transaction} transaction
* @param {StructStore} store
* @param {ID} id
* @return {Item}
*
* @private
* @function
*/
export const getItemCleanEnd = (transaction, store, id) => {
/**
* @type {Array<Item>}
*/
// @ts-ignore
const structs = store.clients.get(id.client)
const index = findIndexSS(structs, id.clock)
const struct = structs[index]
if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) {
structs.splice(index + 1, 0, splitItem(transaction, struct, id.clock - struct.id.clock + 1))
}
return struct
}
/**
* Replace `item` with `newitem` in store
* @param {StructStore} store
* @param {GC|Item} struct
* @param {GC|Item} newStruct
*
* @private
* @function
*/
export const replaceStruct = (store, struct, newStruct) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(struct.id.client))
structs[findIndexSS(structs, struct.id.clock)] = newStruct
}
/**
* Iterate over a range of structs
*
* @param {Transaction} transaction
* @param {Array<Item|GC>} structs
* @param {number} clockStart Inclusive start
* @param {number} len
* @param {function(GC|Item):void} f
*
* @function
*/
export const iterateStructs = (transaction, structs, clockStart, len, f) => {
if (len === 0) {
return
}
const clockEnd = clockStart + len
let index = findIndexCleanStart(transaction, structs, clockStart)
let struct
do {
struct = structs[index++]
if (clockEnd < struct.id.clock + struct.length) {
findIndexCleanStart(transaction, structs, clockEnd)
}
f(struct)
} while (index < structs.length && structs[index].id.clock < clockEnd)
}

416
src/utils/Transaction.js Normal file
View File

@@ -0,0 +1,416 @@
import {
getState,
writeStructsFromTransaction,
writeDeleteSet,
DeleteSet,
sortAndMergeDeleteSet,
getStateVector,
findIndexSS,
callEventHandlerListeners,
Item,
generateNewClientId,
createID,
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
} from '../internals.js'
import * as map from 'lib0/map'
import * as math from 'lib0/math'
import * as set from 'lib0/set'
import * as logging from 'lib0/logging'
import { callAll } from 'lib0/function'
/**
* A transaction is created for every change on the Yjs model. It is possible
* to bundle changes on the Yjs model in a single transaction to
* minimize the number on messages sent and the number of observer calls.
* If possible the user of this library should bundle as many changes as
* possible. Here is an example to illustrate the advantages of bundling:
*
* @example
* const map = y.define('map', YMap)
* // Log content when change is triggered
* map.observe(() => {
* console.log('change triggered')
* })
* // Each change on the map type triggers a log message:
* map.set('a', 0) // => "change triggered"
* map.set('b', 0) // => "change triggered"
* // When put in a transaction, it will trigger the log after the transaction:
* y.transact(() => {
* map.set('a', 1)
* map.set('b', 1)
* }) // => "change triggered"
*
* @public
*/
export class Transaction {
/**
* @param {Doc} doc
* @param {any} origin
* @param {boolean} local
*/
constructor (doc, origin, local) {
/**
* The Yjs instance.
* @type {Doc}
*/
this.doc = doc
/**
* Describes the set of deleted items by ids
* @type {DeleteSet}
*/
this.deleteSet = new DeleteSet()
/**
* Holds the state before the transaction started.
* @type {Map<Number,Number>}
*/
this.beforeState = getStateVector(doc.store)
/**
* Holds the state after the transaction.
* @type {Map<Number,Number>}
*/
this.afterState = new Map()
/**
* 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<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<any>>,Array<YEvent<any>>>}
*/
this.changedParentTypes = new Map()
/**
* @type {Array<AbstractStruct>}
*/
this._mergeStructs = []
/**
* @type {any}
*/
this.origin = origin
/**
* Stores meta information on the transaction
* @type {Map<any,any>}
*/
this.meta = new Map()
/**
* Whether this change originates from this doc.
* @type {boolean}
*/
this.local = local
/**
* @type {Set<Doc>}
*/
this.subdocsAdded = new Set()
/**
* @type {Set<Doc>}
*/
this.subdocsRemoved = new Set()
/**
* @type {Set<Doc>}
*/
this.subdocsLoaded = new Set()
}
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {Transaction} transaction
* @return {boolean} Whether data was written.
*/
export const writeUpdateMessageFromTransaction = (encoder, transaction) => {
if (transaction.deleteSet.clients.size === 0 && !map.any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) {
return false
}
sortAndMergeDeleteSet(transaction.deleteSet)
writeStructsFromTransaction(encoder, transaction)
writeDeleteSet(encoder, transaction.deleteSet)
return true
}
/**
* @param {Transaction} transaction
*
* @private
* @function
*/
export const nextID = transaction => {
const y = transaction.doc
return createID(y.clientID, getState(y.store, y.clientID))
}
/**
* If `type.parent` was added in current transaction, `type` technically
* did not change, it was just added and we should not fire events for `type`.
*
* @param {Transaction} transaction
* @param {AbstractType<YEvent<any>>} type
* @param {string|null} parentSub
*/
export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
const item = type._item
if (item === null || (item.id.clock < (transaction.beforeState.get(item.id.client) || 0) && !item.deleted)) {
map.setIfUndefined(transaction.changed, type, set.create).add(parentSub)
}
}
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
*/
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1]
const right = structs[pos]
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1)
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
/** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
}
}
}
}
/**
* @param {DeleteSet} ds
* @param {StructStore} store
* @param {function(Item):boolean} gcFilter
*/
const tryGcDeleteSet = (ds, store, gcFilter) => {
for (const [client, deleteItems] of ds.clients.entries()) {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
const endDeleteItemClock = deleteItem.clock + deleteItem.len
for (
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
si < structs.length && struct.id.clock < endDeleteItemClock;
struct = structs[++si]
) {
const struct = structs[si]
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break
}
if (struct instanceof Item && struct.deleted && !struct.keep && gcFilter(struct)) {
struct.gc(store, false)
}
}
}
}
}
/**
* @param {DeleteSet} ds
* @param {StructStore} store
*/
const tryMergeDeleteSet = (ds, store) => {
// try to merge deleted / gc'd items
// merge from right to left for better efficiecy and so we don't miss any merge targets
ds.clients.forEach((deleteItems, client) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di]
// start with merging the item next to the last deleted item
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
for (
let si = mostRightIndexToCheck, struct = structs[si];
si > 0 && struct.id.clock >= deleteItem.clock;
struct = structs[--si]
) {
tryToMergeWithLeft(structs, si)
}
}
})
}
/**
* @param {DeleteSet} ds
* @param {StructStore} store
* @param {function(Item):boolean} gcFilter
*/
export const tryGc = (ds, store, gcFilter) => {
tryGcDeleteSet(ds, store, gcFilter)
tryMergeDeleteSet(ds, store)
}
/**
* @param {Array<Transaction>} transactionCleanups
* @param {number} i
*/
const cleanupTransactions = (transactionCleanups, i) => {
if (i < transactionCleanups.length) {
const transaction = transactionCleanups[i]
const doc = transaction.doc
const store = doc.store
const ds = transaction.deleteSet
const mergeStructs = transaction._mergeStructs
try {
sortAndMergeDeleteSet(ds)
transaction.afterState = getStateVector(transaction.doc.store)
doc.emit('beforeObserverCalls', [transaction, doc])
/**
* An array of event callbacks.
*
* Each callback is called even if the other ones throw errors.
*
* @type {Array<function():void>}
*/
const fs = []
// observe events on changed types
transaction.changed.forEach((subs, itemtype) =>
fs.push(() => {
if (itemtype._item === null || !itemtype._item.deleted) {
itemtype._callObserver(transaction, subs)
}
})
)
fs.push(() => {
// deep observe events
transaction.changedParentTypes.forEach((events, type) =>
fs.push(() => {
// We need to think about the possibility that the user transforms the
// Y.Doc in the event.
if (type._item === null || !type._item.deleted) {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
event.currentTarget = type
})
// sort events by path length so that top-level events are fired first.
events
.sort((event1, event2) => event1.path.length - event2.path.length)
// We don't need to check for events.length
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction)
}
})
)
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
})
callAll(fs, [])
} finally {
// Replace deleted items with ItemDeleted / GC.
// This is where content is actually remove from the Yjs Doc.
if (doc.gc) {
tryGcDeleteSet(ds, store, doc.gcFilter)
}
tryMergeDeleteSet(ds, store)
// on all affected store.clients props, try to merge
transaction.afterState.forEach((clock, client) => {
const beforeClock = transaction.beforeState.get(client) || 0
if (beforeClock !== clock) {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
// we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i)
}
}
})
// try to merge mergeStructs
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
// but at the moment DS does not handle duplicates
for (let i = 0; i < mergeStructs.length; i++) {
const { client, clock } = mergeStructs[i].id
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
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])
if (doc._observers.has('update')) {
const encoder = new UpdateEncoderV1()
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
if (hasContent) {
doc.emit('update', [encoder.toUint8Array(), transaction.origin, doc, transaction])
}
}
if (doc._observers.has('updateV2')) {
const encoder = new UpdateEncoderV2()
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
if (hasContent) {
doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction])
}
}
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 = []
doc.emit('afterAllTransactions', [doc, transactionCleanups])
} else {
cleanupTransactions(transactionCleanups, i + 1)
}
}
}
}
/**
* Implements the functionality of `y.transact(()=>{..})`
*
* @param {Doc} doc
* @param {function(Transaction):void} f
* @param {any} [origin=true]
*
* @function
*/
export const transact = (doc, f, origin = null, local = true) => {
const transactionCleanups = doc._transactionCleanups
let initialCall = false
if (doc._transaction === null) {
initialCall = true
doc._transaction = new Transaction(doc, origin, local)
transactionCleanups.push(doc._transaction)
if (transactionCleanups.length === 1) {
doc.emit('beforeAllTransactions', [doc])
}
doc.emit('beforeTransaction', [doc._transaction, doc])
}
try {
f(doc._transaction)
} finally {
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)
}
}
}
}

380
src/utils/UndoManager.js Normal file
View File

@@ -0,0 +1,380 @@
import {
mergeDeleteSets,
iterateDeletedStructs,
keepItem,
transact,
createID,
redoItem,
isParentOf,
followRedone,
getItemCleanStart,
isDeleted,
addToDeleteSet,
Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line
} from '../internals.js'
import * as time from 'lib0/time'
import * as array from 'lib0/array'
import { Observable } from 'lib0/observable'
class StackItem {
/**
* @param {DeleteSet} deletions
* @param {DeleteSet} insertions
*/
constructor (deletions, insertions) {
this.insertions = insertions
this.deletions = deletions
/**
* Use this to save and restore metadata like selection range
*/
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
* @param {Array<StackItem>} stack
* @param {string} eventType
* @return {StackItem?}
*/
const popStackItem = (undoManager, stack, eventType) => {
/**
* Whether a change happened
* @type {StackItem?}
*/
let result = null
/**
* Keep a reference to the transaction so we can fire the event with the changedParentTypes
* @type {any}
*/
let _tr = null
const doc = undoManager.doc
const scope = undoManager.scope
transact(doc, transaction => {
while (stack.length > 0 && result === null) {
const store = doc.store
const stackItem = /** @type {StackItem} */ (stack.pop())
/**
* @type {Set<Item>}
*/
const itemsToRedo = new Set()
/**
* @type {Array<Item>}
*/
const itemsToDelete = []
let performedChange = false
iterateDeletedStructs(transaction, stackItem.insertions, struct => {
if (struct instanceof Item) {
if (struct.redone !== null) {
let { item, diff } = followRedone(store, struct.id)
if (diff > 0) {
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
}
struct = item
}
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct)
}
}
})
iterateDeletedStructs(transaction, stackItem.deletions, struct => {
if (
struct instanceof Item &&
scope.some(type => isParentOf(type, struct)) &&
// Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
!isDeleted(stackItem.insertions, struct.id)
) {
itemsToRedo.add(struct)
}
})
itemsToRedo.forEach(struct => {
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.
for (let i = itemsToDelete.length - 1; i >= 0; i--) {
const item = itemsToDelete[i]
if (undoManager.deleteFilter(item)) {
item.delete(transaction)
performedChange = true
}
}
result = performedChange ? stackItem : null
}
transaction.changed.forEach((subProps, type) => {
// destroy search marker if necessary
if (subProps.has(null) && type._searchMarker) {
type._searchMarker.length = 0
}
})
_tr = transaction
}, undoManager)
if (result != null) {
const changedParentTypes = _tr.changedParentTypes
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType, changedParentTypes }, undoManager])
}
return result
}
/**
* @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 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.
*/
/**
* Fires 'stack-item-added' event when a stack item was added to either the undo- or
* the redo-stack. You may store additional stack information via the
* metadata property on `event.stackItem.meta` (it is a `Map` of metadata properties).
* Fires 'stack-item-popped' event when a stack item was popped from either the
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
*
* @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,
captureTransaction = tr => true,
deleteFilter = () => true,
trackedOrigins = new Set([null]),
ignoreRemoteMapChanges = false,
doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope.doc)
} = {}) {
super()
/**
* @type {Array<AbstractType<any>>}
*/
this.scope = []
this.addToScope(typeScope)
this.deleteFilter = deleteFilter
trackedOrigins.add(this)
this.trackedOrigins = trackedOrigins
this.captureTransaction = captureTransaction
/**
* @type {Array<StackItem>}
*/
this.undoStack = []
/**
* @type {Array<StackItem>}
*/
this.redoStack = []
/**
* Whether the client is currently undoing (calling UndoManager.undo)
*
* @type {boolean}
*/
this.undoing = false
this.redoing = false
this.doc = doc
this.lastChange = 0
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
this.captureTimeout = captureTimeout
/**
* @param {Transaction} transaction
*/
this.afterTransactionHandler = transaction => {
// Only track certain transactions
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
const redoing = this.redoing
const stack = undoing ? this.redoStack : this.undoStack
if (undoing) {
this.stopCapturing() // next undo should not be appended to last stack item
} else if (!redoing) {
// neither undoing nor redoing: delete redoStack
this.clear(false, true)
}
const insertions = new DeleteSet()
transaction.afterState.forEach((endClock, client) => {
const startClock = transaction.beforeState.get(client) || 0
const len = endClock - startClock
if (len > 0) {
addToDeleteSet(insertions, client, startClock, len)
}
})
const now = time.getUnixTime()
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])
lastOp.insertions = mergeDeleteSets([lastOp.insertions, insertions])
} else {
// create a new stack op
stack.push(new StackItem(transaction.deleteSet, insertions))
didAdd = true
}
if (!undoing && !redoing) {
this.lastChange = now
}
// make sure that deleted structs are not gc'd
iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
keepItem(item, true)
}
})
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()
})
}
/**
* @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)
}
})
}
/**
* @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 }])
})
}
}
/**
* UndoManager merges Undo-StackItem if they are created within time-gap
* smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next
* StackItem won't be merged.
*
*
* @example
* // without stopCapturing
* ytext.insert(0, 'a')
* ytext.insert(1, 'b')
* um.undo()
* ytext.toString() // => '' (note that 'ab' was removed)
* // with stopCapturing
* ytext.insert(0, 'a')
* um.stopCapturing()
* ytext.insert(0, 'b')
* um.undo()
* ytext.toString() // => 'a' (note that only 'b' was removed)
*
*/
stopCapturing () {
this.lastChange = 0
}
/**
* Undo last changes on type.
*
* @return {StackItem?} Returns StackItem if a change was applied
*/
undo () {
this.undoing = true
let res
try {
res = popStackItem(this, this.undoStack, 'undo')
} finally {
this.undoing = false
}
return res
}
/**
* Redo last undo operation.
*
* @return {StackItem?} Returns StackItem if a change was applied
*/
redo () {
this.redoing = true
let res
try {
res = popStackItem(this, this.redoStack, 'redo')
} finally {
this.redoing = false
}
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()
}
}

281
src/utils/UpdateDecoder.js Normal file
View File

@@ -0,0 +1,281 @@
import * as buffer from 'lib0/buffer'
import * as decoding from 'lib0/decoding'
import {
ID, createID
} from '../internals.js'
export class DSDecoderV1 {
/**
* @param {decoding.Decoder} decoder
*/
constructor (decoder) {
this.restDecoder = decoder
}
resetDsCurVal () {
// nop
}
/**
* @return {number}
*/
readDsClock () {
return decoding.readVarUint(this.restDecoder)
}
/**
* @return {number}
*/
readDsLen () {
return decoding.readVarUint(this.restDecoder)
}
}
export class UpdateDecoderV1 extends DSDecoderV1 {
/**
* @return {ID}
*/
readLeftID () {
return createID(decoding.readVarUint(this.restDecoder), decoding.readVarUint(this.restDecoder))
}
/**
* @return {ID}
*/
readRightID () {
return createID(decoding.readVarUint(this.restDecoder), decoding.readVarUint(this.restDecoder))
}
/**
* Read the next client id.
* Use this in favor of readID whenever possible to reduce the number of objects created.
*/
readClient () {
return decoding.readVarUint(this.restDecoder)
}
/**
* @return {number} info An unsigned 8-bit integer
*/
readInfo () {
return decoding.readUint8(this.restDecoder)
}
/**
* @return {string}
*/
readString () {
return decoding.readVarString(this.restDecoder)
}
/**
* @return {boolean} isKey
*/
readParentInfo () {
return decoding.readVarUint(this.restDecoder) === 1
}
/**
* @return {number} info An unsigned 8-bit integer
*/
readTypeRef () {
return decoding.readVarUint(this.restDecoder)
}
/**
* Write len of a struct - well suited for Opt RLE encoder.
*
* @return {number} len
*/
readLen () {
return decoding.readVarUint(this.restDecoder)
}
/**
* @return {any}
*/
readAny () {
return decoding.readAny(this.restDecoder)
}
/**
* @return {Uint8Array}
*/
readBuf () {
return buffer.copyUint8Array(decoding.readVarUint8Array(this.restDecoder))
}
/**
* Legacy implementation uses JSON parse. We use any-decoding in v2.
*
* @return {any}
*/
readJSON () {
return JSON.parse(decoding.readVarString(this.restDecoder))
}
/**
* @return {string}
*/
readKey () {
return decoding.readVarString(this.restDecoder)
}
}
export class DSDecoderV2 {
/**
* @param {decoding.Decoder} decoder
*/
constructor (decoder) {
/**
* @private
*/
this.dsCurrVal = 0
this.restDecoder = decoder
}
resetDsCurVal () {
this.dsCurrVal = 0
}
/**
* @return {number}
*/
readDsClock () {
this.dsCurrVal += decoding.readVarUint(this.restDecoder)
return this.dsCurrVal
}
/**
* @return {number}
*/
readDsLen () {
const diff = decoding.readVarUint(this.restDecoder) + 1
this.dsCurrVal += diff
return diff
}
}
export class UpdateDecoderV2 extends DSDecoderV2 {
/**
* @param {decoding.Decoder} decoder
*/
constructor (decoder) {
super(decoder)
/**
* List of cached keys. If the keys[id] does not exist, we read a new key
* from stringEncoder and push it to keys.
*
* @type {Array<string>}
*/
this.keys = []
decoding.readVarUint(decoder) // read feature flag - currently unused
this.keyClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder))
this.clientDecoder = new decoding.UintOptRleDecoder(decoding.readVarUint8Array(decoder))
this.leftClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder))
this.rightClockDecoder = new decoding.IntDiffOptRleDecoder(decoding.readVarUint8Array(decoder))
this.infoDecoder = new decoding.RleDecoder(decoding.readVarUint8Array(decoder), decoding.readUint8)
this.stringDecoder = new decoding.StringDecoder(decoding.readVarUint8Array(decoder))
this.parentInfoDecoder = new decoding.RleDecoder(decoding.readVarUint8Array(decoder), decoding.readUint8)
this.typeRefDecoder = new decoding.UintOptRleDecoder(decoding.readVarUint8Array(decoder))
this.lenDecoder = new decoding.UintOptRleDecoder(decoding.readVarUint8Array(decoder))
}
/**
* @return {ID}
*/
readLeftID () {
return new ID(this.clientDecoder.read(), this.leftClockDecoder.read())
}
/**
* @return {ID}
*/
readRightID () {
return new ID(this.clientDecoder.read(), this.rightClockDecoder.read())
}
/**
* Read the next client id.
* Use this in favor of readID whenever possible to reduce the number of objects created.
*/
readClient () {
return this.clientDecoder.read()
}
/**
* @return {number} info An unsigned 8-bit integer
*/
readInfo () {
return /** @type {number} */ (this.infoDecoder.read())
}
/**
* @return {string}
*/
readString () {
return this.stringDecoder.read()
}
/**
* @return {boolean}
*/
readParentInfo () {
return this.parentInfoDecoder.read() === 1
}
/**
* @return {number} An unsigned 8-bit integer
*/
readTypeRef () {
return this.typeRefDecoder.read()
}
/**
* Write len of a struct - well suited for Opt RLE encoder.
*
* @return {number}
*/
readLen () {
return this.lenDecoder.read()
}
/**
* @return {any}
*/
readAny () {
return decoding.readAny(this.restDecoder)
}
/**
* @return {Uint8Array}
*/
readBuf () {
return decoding.readVarUint8Array(this.restDecoder)
}
/**
* This is mainly here for legacy purposes.
*
* Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder.
*
* @return {any}
*/
readJSON () {
return decoding.readAny(this.restDecoder)
}
/**
* @return {string}
*/
readKey () {
const keyClock = this.keyClockDecoder.read()
if (keyClock < this.keys.length) {
return this.keys[keyClock]
} else {
const key = this.stringDecoder.read()
this.keys.push(key)
return key
}
}
}

321
src/utils/UpdateEncoder.js Normal file
View File

@@ -0,0 +1,321 @@
import * as error from 'lib0/error'
import * as encoding from 'lib0/encoding'
import {
ID // eslint-disable-line
} from '../internals.js'
export class DSEncoderV1 {
constructor () {
this.restEncoder = encoding.createEncoder()
}
toUint8Array () {
return encoding.toUint8Array(this.restEncoder)
}
resetDsCurVal () {
// nop
}
/**
* @param {number} clock
*/
writeDsClock (clock) {
encoding.writeVarUint(this.restEncoder, clock)
}
/**
* @param {number} len
*/
writeDsLen (len) {
encoding.writeVarUint(this.restEncoder, len)
}
}
export class UpdateEncoderV1 extends DSEncoderV1 {
/**
* @param {ID} id
*/
writeLeftID (id) {
encoding.writeVarUint(this.restEncoder, id.client)
encoding.writeVarUint(this.restEncoder, id.clock)
}
/**
* @param {ID} id
*/
writeRightID (id) {
encoding.writeVarUint(this.restEncoder, id.client)
encoding.writeVarUint(this.restEncoder, id.clock)
}
/**
* Use writeClient and writeClock instead of writeID if possible.
* @param {number} client
*/
writeClient (client) {
encoding.writeVarUint(this.restEncoder, client)
}
/**
* @param {number} info An unsigned 8-bit integer
*/
writeInfo (info) {
encoding.writeUint8(this.restEncoder, info)
}
/**
* @param {string} s
*/
writeString (s) {
encoding.writeVarString(this.restEncoder, s)
}
/**
* @param {boolean} isYKey
*/
writeParentInfo (isYKey) {
encoding.writeVarUint(this.restEncoder, isYKey ? 1 : 0)
}
/**
* @param {number} info An unsigned 8-bit integer
*/
writeTypeRef (info) {
encoding.writeVarUint(this.restEncoder, info)
}
/**
* Write len of a struct - well suited for Opt RLE encoder.
*
* @param {number} len
*/
writeLen (len) {
encoding.writeVarUint(this.restEncoder, len)
}
/**
* @param {any} any
*/
writeAny (any) {
encoding.writeAny(this.restEncoder, any)
}
/**
* @param {Uint8Array} buf
*/
writeBuf (buf) {
encoding.writeVarUint8Array(this.restEncoder, buf)
}
/**
* @param {any} embed
*/
writeJSON (embed) {
encoding.writeVarString(this.restEncoder, JSON.stringify(embed))
}
/**
* @param {string} key
*/
writeKey (key) {
encoding.writeVarString(this.restEncoder, key)
}
}
export class DSEncoderV2 {
constructor () {
this.restEncoder = encoding.createEncoder() // encodes all the rest / non-optimized
this.dsCurrVal = 0
}
toUint8Array () {
return encoding.toUint8Array(this.restEncoder)
}
resetDsCurVal () {
this.dsCurrVal = 0
}
/**
* @param {number} clock
*/
writeDsClock (clock) {
const diff = clock - this.dsCurrVal
this.dsCurrVal = clock
encoding.writeVarUint(this.restEncoder, diff)
}
/**
* @param {number} len
*/
writeDsLen (len) {
if (len === 0) {
error.unexpectedCase()
}
encoding.writeVarUint(this.restEncoder, len - 1)
this.dsCurrVal += len
}
}
export class UpdateEncoderV2 extends DSEncoderV2 {
constructor () {
super()
/**
* @type {Map<string,number>}
*/
this.keyMap = new Map()
/**
* Refers to the next uniqe key-identifier to me used.
* See writeKey method for more information.
*
* @type {number}
*/
this.keyClock = 0
this.keyClockEncoder = new encoding.IntDiffOptRleEncoder()
this.clientEncoder = new encoding.UintOptRleEncoder()
this.leftClockEncoder = new encoding.IntDiffOptRleEncoder()
this.rightClockEncoder = new encoding.IntDiffOptRleEncoder()
this.infoEncoder = new encoding.RleEncoder(encoding.writeUint8)
this.stringEncoder = new encoding.StringEncoder()
this.parentInfoEncoder = new encoding.RleEncoder(encoding.writeUint8)
this.typeRefEncoder = new encoding.UintOptRleEncoder()
this.lenEncoder = new encoding.UintOptRleEncoder()
}
toUint8Array () {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, 0) // this is a feature flag that we might use in the future
encoding.writeVarUint8Array(encoder, this.keyClockEncoder.toUint8Array())
encoding.writeVarUint8Array(encoder, this.clientEncoder.toUint8Array())
encoding.writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array())
encoding.writeVarUint8Array(encoder, this.rightClockEncoder.toUint8Array())
encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.infoEncoder))
encoding.writeVarUint8Array(encoder, this.stringEncoder.toUint8Array())
encoding.writeVarUint8Array(encoder, encoding.toUint8Array(this.parentInfoEncoder))
encoding.writeVarUint8Array(encoder, this.typeRefEncoder.toUint8Array())
encoding.writeVarUint8Array(encoder, this.lenEncoder.toUint8Array())
// @note The rest encoder is appended! (note the missing var)
encoding.writeUint8Array(encoder, encoding.toUint8Array(this.restEncoder))
return encoding.toUint8Array(encoder)
}
/**
* @param {ID} id
*/
writeLeftID (id) {
this.clientEncoder.write(id.client)
this.leftClockEncoder.write(id.clock)
}
/**
* @param {ID} id
*/
writeRightID (id) {
this.clientEncoder.write(id.client)
this.rightClockEncoder.write(id.clock)
}
/**
* @param {number} client
*/
writeClient (client) {
this.clientEncoder.write(client)
}
/**
* @param {number} info An unsigned 8-bit integer
*/
writeInfo (info) {
this.infoEncoder.write(info)
}
/**
* @param {string} s
*/
writeString (s) {
this.stringEncoder.write(s)
}
/**
* @param {boolean} isYKey
*/
writeParentInfo (isYKey) {
this.parentInfoEncoder.write(isYKey ? 1 : 0)
}
/**
* @param {number} info An unsigned 8-bit integer
*/
writeTypeRef (info) {
this.typeRefEncoder.write(info)
}
/**
* Write len of a struct - well suited for Opt RLE encoder.
*
* @param {number} len
*/
writeLen (len) {
this.lenEncoder.write(len)
}
/**
* @param {any} any
*/
writeAny (any) {
encoding.writeAny(this.restEncoder, any)
}
/**
* @param {Uint8Array} buf
*/
writeBuf (buf) {
encoding.writeVarUint8Array(this.restEncoder, buf)
}
/**
* This is mainly here for legacy purposes.
*
* Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder.
*
* @param {any} embed
*/
writeJSON (embed) {
encoding.writeAny(this.restEncoder, embed)
}
/**
* Property keys are often reused. For example, in y-prosemirror the key `bold` might
* occur very often. For a 3d application, the key `position` might occur very often.
*
* We cache these keys in a Map and refer to them via a unique number.
*
* @param {string} key
*/
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(clock)
}
}
}

256
src/utils/YEvent.js Normal file
View File

@@ -0,0 +1,256 @@
import {
isDeleted,
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
} from '../internals.js'
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 {T} target The changed type.
* @param {Transaction} transaction
*/
constructor (target, transaction) {
/**
* The type on which this event was created on.
* @type {T}
*/
this.target = target
/**
* The current target on which the observe callback is called.
* @type {AbstractType<any>}
*/
this.currentTarget = target
/**
* The transaction that triggered this event.
* @type {Transaction}
*/
this.transaction = transaction
/**
* @type {Object|null}
*/
this._changes = null
/**
* @type {null | Map<string, { action: 'add' | 'update' | 'delete', oldValue: any, newValue: any }>}
*/
this._keys = null
/**
* @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
*/
this._delta = null
}
/**
* Computes the path from `y` to the changed type.
*
* @todo v14 should standardize on path: Array<{parent, index}> because that is easier to work with.
*
* The following property holds:
* @example
* let type = y
* event.path.forEach(dir => {
* type = type.get(dir)
* })
* type === event.target // => true
*/
get path () {
// @ts-ignore _item is defined because target is integrated
return getPathTo(this.currentTarget, this.target)
}
/**
* Check if a struct is deleted by this event.
*
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
*
* @param {AbstractStruct} struct
* @return {boolean}
*/
deletes (struct) {
return isDeleted(this.transaction.deleteSet, struct.id)
}
/**
* @type {Map<string, { action: 'add' | 'update' | 'delete', oldValue: any, newValue: any }>}
*/
get keys () {
if (this._keys === null) {
const keys = new Map()
const target = this.target
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
changed.forEach(key => {
if (key !== null) {
const item = /** @type {Item} */ (target._map.get(key))
/**
* @type {'delete' | 'add' | 'update'}
*/
let action
let oldValue
if (this.adds(item)) {
let prev = item.left
while (prev !== null && this.adds(prev)) {
prev = prev.left
}
if (this.deletes(item)) {
if (prev !== null && this.deletes(prev)) {
action = 'delete'
oldValue = array.last(prev.content.getContent())
} else {
return
}
} else {
if (prev !== null && this.deletes(prev)) {
action = 'update'
oldValue = array.last(prev.content.getContent())
} else {
action = 'add'
oldValue = undefined
}
}
} else {
if (this.deletes(item)) {
action = 'delete'
oldValue = array.last(/** @type {Item} */ item.content.getContent())
} else {
return // nop
}
}
keys.set(key, { action, oldValue })
}
})
this._keys = keys
}
return this._keys
}
/**
* @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
*/
get delta () {
return this.changes.delta
}
/**
* Check if a struct is added by this event.
*
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
*
* @param {AbstractStruct} struct
* @return {boolean}
*/
adds (struct) {
return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0)
}
/**
* @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}>}}
*/
get changes () {
let changes = this._changes
if (changes === null) {
const target = this.target
const added = set.create()
const deleted = set.create()
/**
* @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>}
*/
const delta = []
changes = {
added,
deleted,
delta,
keys: this.keys
}
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
if (changed.has(null)) {
/**
* @type {any}
*/
let lastOp = null
const packOp = () => {
if (lastOp) {
delta.push(lastOp)
}
}
for (let item = target._start; item !== null; item = item.right) {
if (item.deleted) {
if (this.deletes(item) && !this.adds(item)) {
if (lastOp === null || lastOp.delete === undefined) {
packOp()
lastOp = { delete: 0 }
}
lastOp.delete += item.length
deleted.add(item)
} // else nop
} else {
if (this.adds(item)) {
if (lastOp === null || lastOp.insert === undefined) {
packOp()
lastOp = { insert: [] }
}
lastOp.insert = lastOp.insert.concat(item.content.getContent())
added.add(item)
} else {
if (lastOp === null || lastOp.retain === undefined) {
packOp()
lastOp = { retain: 0 }
}
lastOp.retain += item.length
}
}
}
if (lastOp !== null && lastOp.retain === undefined) {
packOp()
}
}
this._changes = changes
}
return /** @type {any} */ (changes)
}
}
/**
* Compute the path from this type to the specified target.
*
* @example
* // `child` should be accessible via `type.get(path[0]).get(path[1])..`
* const path = type.getPathTo(child)
* // assuming `type instanceof YArray`
* console.log(path) // might look like => [2, 'key1']
* child === type.get(path[0]).get(path[1])
*
* @param {AbstractType<any>} parent
* @param {AbstractType<any>} child target
* @return {Array<string|number>} Path to the target
*
* @private
* @function
*/
const getPathTo = (parent, child) => {
const path = []
while (child._item !== null && child !== parent) {
if (child._item.parentSub !== null) {
// parent is map-ish
path.unshift(child._item.parentSub)
} else {
// parent is array-ish
let i = 0
let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
while (c !== child._item && c !== null) {
if (!c.deleted) {
i++
}
c = c.right
}
path.unshift(i)
}
child = /** @type {AbstractType<any>} */ (child._item.parent)
}
return path
}

645
src/utils/encoding.js Normal file
View File

@@ -0,0 +1,645 @@
/**
* @module encoding
*/
/*
* We use the first five bits in the info flag for determining the type of the struct.
*
* 0: GC
* 1: Item with Deleted content
* 2: Item with JSON content
* 3: Item with Binary content
* 4: Item with String content
* 5: Item with Embed content (for richtext content)
* 6: Item with Format content (a formatting marker for richtext content)
* 7: Item with Type
*/
import {
findIndexSS,
getState,
createID,
getStateVector,
readAndApplyDeleteSet,
writeDeleteSet,
createDeleteSetFromStructStore,
transact,
readItemContent,
UpdateDecoderV1,
UpdateDecoderV2,
UpdateEncoderV1,
UpdateEncoderV2,
DSEncoderV2,
DSDecoderV1,
DSEncoderV1,
mergeUpdates,
mergeUpdatesV2,
Skip,
diffUpdateV2,
convertUpdateFormatV2ToV1,
DSDecoderV2, Doc, Transaction, GC, Item, StructStore // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding'
import * as binary from 'lib0/binary'
import * as map from 'lib0/map'
import * as math from 'lib0/math'
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {Array<GC|Item>} structs All structs by `client`
* @param {number} client
* @param {number} clock write structs starting with `ID(client,clock)`
*
* @function
*/
const writeStructs = (encoder, structs, client, clock) => {
// write first id
clock = math.max(clock, structs[0].id.clock) // make sure the first id exists
const startNewStructs = findIndexSS(structs, clock)
// write # encoded structs
encoding.writeVarUint(encoder.restEncoder, structs.length - startNewStructs)
encoder.writeClient(client)
encoding.writeVarUint(encoder.restEncoder, clock)
const firstStruct = structs[startNewStructs]
// write first struct with an offset
firstStruct.write(encoder, clock - firstStruct.id.clock)
for (let i = startNewStructs + 1; i < structs.length; i++) {
structs[i].write(encoder, 0)
}
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {StructStore} store
* @param {Map<number,number>} _sm
*
* @private
* @function
*/
export const writeClientsStructs = (encoder, store, _sm) => {
// we filter all valid _sm entries into sm
const sm = new Map()
_sm.forEach((clock, client) => {
// only write if new structs are available
if (getState(store, client) > clock) {
sm.set(client, clock)
}
})
getStateVector(store).forEach((clock, client) => {
if (!_sm.has(client)) {
sm.set(client, 0)
}
})
// write # states that were updated
encoding.writeVarUint(encoder.restEncoder, sm.size)
// Write items with higher client ids first
// This heavily improves the conflict algorithm.
Array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
// @ts-ignore
writeStructs(encoder, store.clients.get(client), client, clock)
})
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder The decoder object to read data from.
* @param {Doc} doc
* @return {Map<number, { i: number, refs: Array<Item | GC> }>}
*
* @private
* @function
*/
export const readClientsStructRefs = (decoder, doc) => {
/**
* @type {Map<number, { i: number, refs: Array<Item | GC> }>}
*/
const clientRefs = map.create()
const numOfStateUpdates = decoding.readVarUint(decoder.restDecoder)
for (let i = 0; i < numOfStateUpdates; i++) {
const numberOfStructs = decoding.readVarUint(decoder.restDecoder)
/**
* @type {Array<GC|Item>}
*/
const refs = new Array(numberOfStructs)
const client = decoder.readClient()
let clock = decoding.readVarUint(decoder.restDecoder)
// const start = performance.now()
clientRefs.set(client, { i: 0, refs })
for (let i = 0; i < numberOfStructs; i++) {
const info = decoder.readInfo()
switch (binary.BITS5 & info) {
case 0: { // GC
const len = decoder.readLen()
refs[i] = new GC(createID(client, clock), len)
clock += len
break
}
case 10: { // Skip Struct (nothing to apply)
// @todo we could reduce the amount of checks by adding Skip struct to clientRefs so we know that something is missing.
const len = decoding.readVarUint(decoder.restDecoder)
refs[i] = new Skip(createID(client, clock), len)
clock += len
break
}
default: { // Item with content
/**
* The optimized implementation doesn't use any variables because inlining variables is faster.
* Below a non-optimized version is shown that implements the basic algorithm with
* a few comments
*/
const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
// and we read the next string as parentYKey.
// It indicates how we store/retrieve parent from `y.share`
// @type {string|null}
const struct = new Item(
createID(client, clock),
null, // leftd
(info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
null, // right
(info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
cantCopyParentInfo ? (decoder.readParentInfo() ? doc.get(decoder.readString()) : decoder.readLeftID()) : null, // parent
cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
readItemContent(decoder, info) // item content
)
/* A non-optimized implementation of the above algorithm:
// The item that was originally to the left of this item.
const origin = (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null
// The item that was originally to the right of this item.
const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null
const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
const hasParentYKey = cantCopyParentInfo ? decoder.readParentInfo() : false
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
// and we read the next string as parentYKey.
// It indicates how we store/retrieve parent from `y.share`
// @type {string|null}
const parentYKey = cantCopyParentInfo && hasParentYKey ? decoder.readString() : null
const struct = new Item(
createID(client, clock),
null, // leftd
origin, // origin
null, // right
rightOrigin, // right origin
cantCopyParentInfo && !hasParentYKey ? decoder.readLeftID() : (parentYKey !== null ? doc.get(parentYKey) : null), // parent
cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
readItemContent(decoder, info) // item content
)
*/
refs[i] = struct
clock += struct.length
}
}
}
// console.log('time to read: ', performance.now() - start) // @todo remove
}
return clientRefs
}
/**
* Resume computing structs generated by struct readers.
*
* While there is something to do, we integrate structs in this order
* 1. top element on stack, if stack is not empty
* 2. next element from current struct reader (if empty, use next struct reader)
*
* If struct causally depends on another struct (ref.missing), we put next reader of
* `ref.id.client` on top of stack.
*
* At some point we find a struct that has no causal dependencies,
* then we start emptying the stack.
*
* It is not possible to have circles: i.e. struct1 (from client1) depends on struct2 (from client2)
* depends on struct3 (from client1). Therefore the max stack size is eqaul to `structReaders.length`.
*
* This method is implemented in a way so that we can resume computation if this update
* causally depends on another update.
*
* @param {Transaction} transaction
* @param {StructStore} store
* @param {Map<number, { i: number, refs: (GC | Item)[] }>} clientsStructRefs
* @return { null | { update: Uint8Array, missing: Map<number,number> } }
*
* @private
* @function
*/
const integrateStructs = (transaction, store, clientsStructRefs) => {
/**
* @type {Array<Item | GC>}
*/
const stack = []
// sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user.
let clientsStructRefsIds = Array.from(clientsStructRefs.keys()).sort((a, b) => a - b)
if (clientsStructRefsIds.length === 0) {
return null
}
const getNextStructTarget = () => {
if (clientsStructRefsIds.length === 0) {
return null
}
let nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]))
while (nextStructsTarget.refs.length === nextStructsTarget.i) {
clientsStructRefsIds.pop()
if (clientsStructRefsIds.length > 0) {
nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]))
} else {
return null
}
}
return nextStructsTarget
}
let curStructsTarget = getNextStructTarget()
if (curStructsTarget === null && stack.length === 0) {
return null
}
/**
* @type {StructStore}
*/
const restStructs = new StructStore()
const missingSV = new Map()
/**
* @param {number} client
* @param {number} clock
*/
const updateMissingSv = (client, clock) => {
const mclock = missingSV.get(client)
if (mclock == null || mclock > clock) {
missingSV.set(client, clock)
}
}
/**
* @type {GC|Item}
*/
let stackHead = /** @type {any} */ (curStructsTarget).refs[/** @type {any} */ (curStructsTarget).i++]
// caching the state because it is used very often
const state = new Map()
const addStackToRestSS = () => {
for (const item of stack) {
const client = item.id.client
const unapplicableItems = clientsStructRefs.get(client)
if (unapplicableItems) {
// decrement because we weren't able to apply previous operation
unapplicableItems.i--
restStructs.clients.set(client, unapplicableItems.refs.slice(unapplicableItems.i))
clientsStructRefs.delete(client)
unapplicableItems.i = 0
unapplicableItems.refs = []
} else {
// item was the last item on clientsStructRefs and the field was already cleared. Add item to restStructs and continue
restStructs.clients.set(client, [item])
}
// remove client from clientsStructRefsIds to prevent users from applying the same update again
clientsStructRefsIds = clientsStructRefsIds.filter(c => c !== client)
}
stack.length = 0
}
// iterate over all struct readers until we are done
while (true) {
if (stackHead.constructor !== Skip) {
const localClock = map.setIfUndefined(state, stackHead.id.client, () => getState(store, stackHead.id.client))
const offset = localClock - stackHead.id.clock
if (offset < 0) {
// update from the same client is missing
stack.push(stackHead)
updateMissingSv(stackHead.id.client, stackHead.id.clock - 1)
// hid a dead wall, add all items from stack to restSS
addStackToRestSS()
} else {
const missing = stackHead.getMissing(transaction, store)
if (missing !== null) {
stack.push(stackHead)
// get the struct reader that has the missing struct
/**
* @type {{ refs: Array<GC|Item>, i: number }}
*/
const structRefs = clientsStructRefs.get(/** @type {number} */ (missing)) || { refs: [], i: 0 }
if (structRefs.refs.length === structRefs.i) {
// This update message causally depends on another update message that doesn't exist yet
updateMissingSv(/** @type {number} */ (missing), getState(store, missing))
addStackToRestSS()
} else {
stackHead = structRefs.refs[structRefs.i++]
continue
}
} else if (offset === 0 || offset < stackHead.length) {
// all fine, apply the stackhead
stackHead.integrate(transaction, offset)
state.set(stackHead.id.client, stackHead.id.clock + stackHead.length)
}
}
}
// iterate to next stackHead
if (stack.length > 0) {
stackHead = /** @type {GC|Item} */ (stack.pop())
} else if (curStructsTarget !== null && curStructsTarget.i < curStructsTarget.refs.length) {
stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++])
} else {
curStructsTarget = getNextStructTarget()
if (curStructsTarget === null) {
// we are done!
break
} else {
stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++])
}
}
}
if (restStructs.clients.size > 0) {
const encoder = new UpdateEncoderV2()
writeClientsStructs(encoder, restStructs, new Map())
// write empty deleteset
// writeDeleteSet(encoder, new DeleteSet())
encoding.writeVarUint(encoder.restEncoder, 0) // => no need for an extra function call, just write 0 deletes
return { missing: missingSV, update: encoder.toUint8Array() }
}
return null
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {Transaction} transaction
*
* @private
* @function
*/
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState)
/**
* Read and apply a document update.
*
* This function has the same effect as `applyUpdate` but accepts an decoder.
*
* @param {decoding.Decoder} decoder
* @param {Doc} ydoc
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
* @param {UpdateDecoderV1 | UpdateDecoderV2} [structDecoder]
*
* @function
*/
export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) =>
transact(ydoc, transaction => {
// force that transaction.local is set to non-local
transaction.local = false
let retry = false
const doc = transaction.doc
const store = doc.store
// let start = performance.now()
const ss = readClientsStructRefs(structDecoder, doc)
// console.log('time to read structs: ', performance.now() - start) // @todo remove
// start = performance.now()
// console.log('time to merge: ', performance.now() - start) // @todo remove
// start = performance.now()
const restStructs = integrateStructs(transaction, store, ss)
const pending = store.pendingStructs
if (pending) {
// check if we can apply something
for (const [client, clock] of pending.missing) {
if (clock < getState(store, client)) {
retry = true
break
}
}
if (restStructs) {
// merge restStructs into store.pending
for (const [client, clock] of restStructs.missing) {
const mclock = pending.missing.get(client)
if (mclock == null || mclock > clock) {
pending.missing.set(client, clock)
}
}
pending.update = mergeUpdatesV2([pending.update, restStructs.update])
}
} else {
store.pendingStructs = restStructs
}
// console.log('time to integrate: ', performance.now() - start) // @todo remove
// start = performance.now()
const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store)
if (store.pendingDs) {
// @todo we could make a lower-bound state-vector check as we do above
const pendingDSUpdate = new UpdateDecoderV2(decoding.createDecoder(store.pendingDs))
decoding.readVarUint(pendingDSUpdate.restDecoder) // read 0 structs, because we only encode deletes in pendingdsupdate
const dsRest2 = readAndApplyDeleteSet(pendingDSUpdate, transaction, store)
if (dsRest && dsRest2) {
// case 1: ds1 != null && ds2 != null
store.pendingDs = mergeUpdatesV2([dsRest, dsRest2])
} else {
// case 2: ds1 != null
// case 3: ds2 != null
// case 4: ds1 == null && ds2 == null
store.pendingDs = dsRest || dsRest2
}
} else {
// Either dsRest == null && pendingDs == null OR dsRest != null
store.pendingDs = dsRest
}
// console.log('time to cleanup: ', performance.now() - start) // @todo remove
// start = performance.now()
// console.log('time to resume delete readers: ', performance.now() - start) // @todo remove
// start = performance.now()
if (retry) {
const update = /** @type {{update: Uint8Array}} */ (store.pendingStructs).update
store.pendingStructs = null
applyUpdateV2(transaction.doc, update)
}
}, transactionOrigin, false)
/**
* Read and apply a document update.
*
* This function has the same effect as `applyUpdate` but accepts an decoder.
*
* @param {decoding.Decoder} decoder
* @param {Doc} ydoc
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
*
* @function
*/
export const readUpdate = (decoder, ydoc, transactionOrigin) => readUpdateV2(decoder, ydoc, transactionOrigin, new UpdateDecoderV1(decoder))
/**
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
*
* This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder.
*
* @param {Doc} ydoc
* @param {Uint8Array} update
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
*
* @function
*/
export const applyUpdateV2 = (ydoc, update, transactionOrigin, YDecoder = UpdateDecoderV2) => {
const decoder = decoding.createDecoder(update)
readUpdateV2(decoder, ydoc, transactionOrigin, new YDecoder(decoder))
}
/**
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
*
* This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder.
*
* @param {Doc} ydoc
* @param {Uint8Array} update
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
*
* @function
*/
export const applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, UpdateDecoderV1)
/**
* Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will
* only write the operations that are missing.
*
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {Doc} doc
* @param {Map<number,number>} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs
*
* @function
*/
export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map()) => {
writeClientsStructs(encoder, doc.store, targetStateVector)
writeDeleteSet(encoder, createDeleteSetFromStructStore(doc.store))
}
/**
* Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will
* only write the operations that are missing.
*
* Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder
*
* @param {Doc} doc
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
* @param {UpdateEncoderV1 | UpdateEncoderV2} [encoder]
* @return {Uint8Array}
*
* @function
*/
export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => {
const targetStateVector = decodeStateVector(encodedTargetStateVector)
writeStateAsUpdate(encoder, doc, targetStateVector)
const updates = [encoder.toUint8Array()]
// also add the pending updates (if there are any)
if (doc.store.pendingDs) {
updates.push(doc.store.pendingDs)
}
if (doc.store.pendingStructs) {
updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector))
}
if (updates.length > 1) {
if (encoder.constructor === UpdateEncoderV1) {
return mergeUpdates(updates.map((update, i) => i === 0 ? update : convertUpdateFormatV2ToV1(update)))
} else if (encoder.constructor === UpdateEncoderV2) {
return mergeUpdatesV2(updates)
}
}
return updates[0]
}
/**
* Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will
* only write the operations that are missing.
*
* Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder
*
* @param {Doc} doc
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
* @return {Uint8Array}
*
* @function
*/
export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => encodeStateAsUpdateV2(doc, encodedTargetStateVector, new UpdateEncoderV1())
/**
* Read state vector from Decoder and return as Map
*
* @param {DSDecoderV1 | DSDecoderV2} decoder
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
*
* @function
*/
export const readStateVector = decoder => {
const ss = new Map()
const ssLength = decoding.readVarUint(decoder.restDecoder)
for (let i = 0; i < ssLength; i++) {
const client = decoding.readVarUint(decoder.restDecoder)
const clock = decoding.readVarUint(decoder.restDecoder)
ss.set(client, clock)
}
return ss
}
/**
* Read decodedState and return State as Map.
*
* @param {Uint8Array} decodedState
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
*
* @function
*/
// export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoderV2(decoding.createDecoder(decodedState)))
/**
* Read decodedState and return State as Map.
*
* @param {Uint8Array} decodedState
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
*
* @function
*/
export const decodeStateVector = decodedState => readStateVector(new DSDecoderV1(decoding.createDecoder(decodedState)))
/**
* @param {DSEncoderV1 | DSEncoderV2} encoder
* @param {Map<number,number>} sv
* @function
*/
export const writeStateVector = (encoder, sv) => {
encoding.writeVarUint(encoder.restEncoder, sv.size)
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)
})
return encoder
}
/**
* @param {DSEncoderV1 | DSEncoderV2} encoder
* @param {Doc} doc
*
* @function
*/
export const writeDocumentStateVector = (encoder, doc) => writeStateVector(encoder, getStateVector(doc.store))
/**
* Encode State as Uint8Array.
*
* @param {Doc|Map<number,number>} doc
* @param {DSEncoderV1 | DSEncoderV2} [encoder]
* @return {Uint8Array}
*
* @function
*/
export const encodeStateVectorV2 = (doc, encoder = new DSEncoderV2()) => {
if (doc instanceof Map) {
writeStateVector(encoder, doc)
} else {
writeDocumentStateVector(encoder, doc)
}
return encoder.toUint8Array()
}
/**
* Encode State as Uint8Array.
*
* @param {Doc|Map<number,number>} doc
* @return {Uint8Array}
*
* @function
*/
export const encodeStateVector = doc => encodeStateVectorV2(doc, new DSEncoderV1())

22
src/utils/isParentOf.js Normal file
View File

@@ -0,0 +1,22 @@
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
/**
* Check if `parent` is a parent of `child`.
*
* @param {AbstractType<any>} parent
* @param {Item|null} child
* @return {Boolean} Whether `parent` is a parent of `child`.
*
* @private
* @function
*/
export const isParentOf = (parent, child) => {
while (child !== null) {
if (child.parent === parent) {
return true
}
child = /** @type {AbstractType<any>} */ (child.parent)._item
}
return false
}

22
src/utils/logging.js Normal file
View File

@@ -0,0 +1,22 @@
import {
AbstractType // eslint-disable-line
} from '../internals.js'
/**
* Convenient helper to log type information.
*
* Do not use in productive systems as the output can be immense!
*
* @param {AbstractType<any>} type
*/
export const logType = type => {
const res = []
let n = type._start
while (n) {
res.push(n)
n = n.right
}
console.log('Children: ', res)
console.log('Children content: ', res.filter(m => !m.deleted).map(m => m.content))
}

581
src/utils/updates.js Normal file
View File

@@ -0,0 +1,581 @@
import * as binary from 'lib0/binary'
import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding'
import * as logging from 'lib0/logging'
import * as math from 'lib0/math'
import {
createID,
readItemContent,
readDeleteSet,
writeDeleteSet,
Skip,
mergeDeleteSets,
DSEncoderV1,
DSEncoderV2,
decodeStateVector,
Item, GC, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
} from '../internals.js'
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
*/
function * lazyStructReaderGenerator (decoder) {
const numOfStateUpdates = decoding.readVarUint(decoder.restDecoder)
for (let i = 0; i < numOfStateUpdates; i++) {
const numberOfStructs = decoding.readVarUint(decoder.restDecoder)
const client = decoder.readClient()
let clock = decoding.readVarUint(decoder.restDecoder)
for (let i = 0; i < numberOfStructs; i++) {
const info = decoder.readInfo()
// @todo use switch instead of ifs
if (info === 10) {
const len = decoding.readVarUint(decoder.restDecoder)
yield new Skip(createID(client, clock), len)
clock += len
} else if ((binary.BITS5 & info) !== 0) {
const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
// and we read the next string as parentYKey.
// It indicates how we store/retrieve parent from `y.share`
// @type {string|null}
const struct = new Item(
createID(client, clock),
null, // left
(info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
null, // right
(info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
// @ts-ignore Force writing a string here.
cantCopyParentInfo ? (decoder.readParentInfo() ? decoder.readString() : decoder.readLeftID()) : null, // parent
cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
readItemContent(decoder, info) // item content
)
yield struct
clock += struct.length
} else {
const len = decoder.readLen()
yield new GC(createID(client, clock), len)
clock += len
}
}
}
}
export class LazyStructReader {
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @param {boolean} filterSkips
*/
constructor (decoder, filterSkips) {
this.gen = lazyStructReaderGenerator(decoder)
/**
* @type {null | Item | Skip | GC}
*/
this.curr = null
this.done = false
this.filterSkips = filterSkips
this.next()
}
/**
* @return {Item | GC | Skip |null}
*/
next () {
// ignore "Skip" structs
do {
this.curr = this.gen.next().value || null
} while (this.filterSkips && this.curr !== null && this.curr.constructor === Skip)
return this.curr
}
}
/**
* @param {Uint8Array} update
*
*/
export const logUpdate = update => logUpdateV2(update, UpdateDecoderV1)
/**
* @param {Uint8Array} update
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} [YDecoder]
*
*/
export const logUpdateV2 = (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)
}
logging.print('Structs: ', structs)
const ds = readDeleteSet(updateDecoder)
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
*/
constructor (encoder) {
this.currClient = 0
this.startClock = 0
this.written = 0
this.encoder = encoder
/**
* We want to write operations lazily, but also we need to know beforehand how many operations we want to write for each client.
*
* This kind of meta-information (#clients, #structs-per-client-written) is written to the restEncoder.
*
* We fragment the restEncoder and store a slice of it per-client until we know how many clients there are.
* When we flush (toUint8Array) we write the restEncoder using the fragments and the meta-information.
*
* @type {Array<{ written: number, restEncoder: Uint8Array }>}
*/
this.clientStructs = []
}
}
/**
* @param {Array<Uint8Array>} updates
* @return {Uint8Array}
*/
export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1, UpdateEncoderV1)
/**
* @param {Uint8Array} update
* @param {typeof DSEncoderV1 | typeof DSEncoderV2} YEncoder
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} YDecoder
* @return {Uint8Array}
*/
export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YDecoder = UpdateDecoderV2) => {
const encoder = new YEncoder()
const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), false)
let curr = updateDecoder.curr
if (curr !== null) {
let size = 0
let currClient = curr.id.client
let stopCounting = curr.id.clock !== 0 // must start at 0
let currClock = stopCounting ? 0 : curr.id.clock + curr.length
for (; curr !== null; curr = updateDecoder.next()) {
if (currClient !== curr.id.client) {
if (currClock !== 0) {
size++
// We found a new client
// write what we have to the encoder
encoding.writeVarUint(encoder.restEncoder, currClient)
encoding.writeVarUint(encoder.restEncoder, currClock)
}
currClient = curr.id.client
currClock = 0
stopCounting = curr.id.clock !== 0
}
// we ignore skips
if (curr.constructor === Skip) {
stopCounting = true
}
if (!stopCounting) {
currClock = curr.id.clock + curr.length
}
}
// write what we have
if (currClock !== 0) {
size++
encoding.writeVarUint(encoder.restEncoder, currClient)
encoding.writeVarUint(encoder.restEncoder, currClock)
}
// prepend the size of the state vector
const enc = encoding.createEncoder()
encoding.writeVarUint(enc, size)
encoding.writeBinaryEncoder(enc, encoder.restEncoder)
encoder.restEncoder = enc
return encoder.toUint8Array()
} else {
encoding.writeVarUint(encoder.restEncoder, 0)
return encoder.toUint8Array()
}
}
/**
* @param {Uint8Array} update
* @return {Uint8Array}
*/
export const encodeStateVectorFromUpdate = update => encodeStateVectorFromUpdateV2(update, DSEncoderV1, UpdateDecoderV1)
/**
* @param {Uint8Array} update
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} YDecoder
* @return {{ from: Map<number,number>, to: Map<number,number> }}
*/
export const parseUpdateMetaV2 = (update, YDecoder = UpdateDecoderV2) => {
/**
* @type {Map<number, number>}
*/
const from = new Map()
/**
* @type {Map<number, number>}
*/
const to = new Map()
const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), false)
let curr = updateDecoder.curr
if (curr !== null) {
let currClient = curr.id.client
let currClock = curr.id.clock
// write the beginning to `from`
from.set(currClient, currClock)
for (; curr !== null; curr = updateDecoder.next()) {
if (currClient !== curr.id.client) {
// We found a new client
// write the end to `to`
to.set(currClient, currClock)
// write the beginning to `from`
from.set(curr.id.client, curr.id.clock)
// update currClient
currClient = curr.id.client
}
currClock = curr.id.clock + curr.length
}
// write the end to `to`
to.set(currClient, currClock)
}
return { from, to }
}
/**
* @param {Uint8Array} update
* @return {{ from: Map<number,number>, to: Map<number,number> }}
*/
export const parseUpdateMeta = update => parseUpdateMetaV2(update, UpdateDecoderV1)
/**
* This method is intended to slice any kind of struct and retrieve the right part.
* It does not handle side-effects, so it should only be used by the lazy-encoder.
*
* @param {Item | GC | Skip} left
* @param {number} diff
* @return {Item | GC}
*/
const sliceStruct = (left, diff) => {
if (left.constructor === GC) {
const { client, clock } = left.id
return new GC(createID(client, clock + diff), left.length - diff)
} else if (left.constructor === Skip) {
const { client, clock } = left.id
return new Skip(createID(client, clock + diff), left.length - diff)
} else {
const leftItem = /** @type {Item} */ (left)
const { client, clock } = leftItem.id
return new Item(
createID(client, clock + diff),
null,
createID(client, clock + diff - 1),
null,
leftItem.rightOrigin,
leftItem.parent,
leftItem.parentSub,
leftItem.content.splice(diff)
)
}
}
/**
*
* This function works similarly to `readUpdateV2`.
*
* @param {Array<Uint8Array>} updates
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
* @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))
/**
* @todo we don't need offset because we always slice before
* @type {null | { struct: Item | GC | Skip, offset: number }}
*/
let currWrite = null
const updateEncoder = new YEncoder()
// write structs lazily
const lazyStructEncoder = new LazyStructWriter(updateEncoder)
// Note: We need to ensure that all lazyStructDecoders are fully consumed
// Note: Should merge document updates whenever possible - even from different updates
// Note: Should handle that some operations cannot be applied yet ()
while (true) {
// Write higher clients first ⇒ sort by clientID & clock and remove decoders without content
lazyStructDecoders = lazyStructDecoders.filter(dec => dec.curr !== null)
lazyStructDecoders.sort(
/** @type {function(any,any):number} */ (dec1, dec2) => {
if (dec1.curr.id.client === dec2.curr.id.client) {
const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock
if (clockDiff === 0) {
// @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
}
} else {
return dec2.curr.id.client - dec1.curr.id.client
}
}
)
if (lazyStructDecoders.length === 0) {
break
}
const currDecoder = lazyStructDecoders[0]
// write from currDecoder until the next operation is from another client or if filler-struct
// then we need to reorder the decoders and find the next operation to write
const firstClient = /** @type {Item | GC} */ (currDecoder.curr).id.client
if (currWrite !== null) {
let curr = /** @type {Item | GC | null} */ (currDecoder.curr)
let iterated = false
// iterate until we find something that we haven't written already
// remember: first the high client-ids are written
while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) {
curr = currDecoder.next()
iterated = true
}
if (
curr === null || // current decoder is empty
curr.id.client !== firstClient || // check whether there is another decoder that has has updates from `firstClient`
(iterated && curr.id.clock > currWrite.struct.id.clock + currWrite.struct.length) // the above while loop was used and we are potentially missing updates
) {
continue
}
if (firstClient !== currWrite.struct.id.client) {
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
currWrite = { struct: curr, offset: 0 }
currDecoder.next()
} else {
if (currWrite.struct.id.clock + currWrite.struct.length < curr.id.clock) {
// @todo write currStruct & set currStruct = Skip(clock = currStruct.id.clock + currStruct.length, length = curr.id.clock - self.clock)
if (currWrite.struct.constructor === Skip) {
// extend existing skip
currWrite.struct.length = curr.id.clock + curr.length - currWrite.struct.id.clock
} else {
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
const diff = curr.id.clock - currWrite.struct.id.clock - currWrite.struct.length
/**
* @type {Skip}
*/
const struct = new Skip(createID(firstClient, currWrite.struct.id.clock + currWrite.struct.length), diff)
currWrite = { struct, offset: 0 }
}
} else { // if (currWrite.struct.id.clock + currWrite.struct.length >= curr.id.clock) {
const diff = currWrite.struct.id.clock + currWrite.struct.length - curr.id.clock
if (diff > 0) {
if (currWrite.struct.constructor === Skip) {
// prefer to slice Skip because the other struct might contain more information
currWrite.struct.length -= diff
} else {
curr = sliceStruct(curr, diff)
}
}
if (!currWrite.struct.mergeWith(/** @type {any} */ (curr))) {
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
currWrite = { struct: curr, offset: 0 }
currDecoder.next()
}
}
}
} else {
currWrite = { struct: /** @type {Item | GC} */ (currDecoder.curr), offset: 0 }
currDecoder.next()
}
for (
let next = currDecoder.curr;
next !== null && next.id.client === firstClient && next.id.clock === currWrite.struct.id.clock + currWrite.struct.length && next.constructor !== Skip;
next = currDecoder.next()
) {
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
currWrite = { struct: next, offset: 0 }
}
}
if (currWrite !== null) {
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset)
currWrite = null
}
finishLazyStructWriting(lazyStructEncoder)
const dss = updateDecoders.map(decoder => readDeleteSet(decoder))
const ds = mergeDeleteSets(dss)
writeDeleteSet(updateEncoder, ds)
return updateEncoder.toUint8Array()
}
/**
* @param {Uint8Array} update
* @param {Uint8Array} sv
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
*/
export const diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
const state = decodeStateVector(sv)
const encoder = new YEncoder()
const lazyStructWriter = new LazyStructWriter(encoder)
const decoder = new YDecoder(decoding.createDecoder(update))
const reader = new LazyStructReader(decoder, false)
while (reader.curr) {
const curr = reader.curr
const currClient = curr.id.client
const svClock = state.get(currClient) || 0
if (reader.curr.constructor === Skip) {
// the first written struct shouldn't be a skip
reader.next()
continue
}
if (curr.id.clock + curr.length > svClock) {
writeStructToLazyStructWriter(lazyStructWriter, curr, math.max(svClock - curr.id.clock, 0))
reader.next()
while (reader.curr && reader.curr.id.client === currClient) {
writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0)
reader.next()
}
} else {
// read until something new comes up
while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) {
reader.next()
}
}
}
finishLazyStructWriting(lazyStructWriter)
// write ds
const ds = readDeleteSet(decoder)
writeDeleteSet(encoder, ds)
return encoder.toUint8Array()
}
/**
* @param {Uint8Array} update
* @param {Uint8Array} sv
*/
export const diffUpdate = (update, sv) => diffUpdateV2(update, sv, UpdateDecoderV1, UpdateEncoderV1)
/**
* @param {LazyStructWriter} lazyWriter
*/
const flushLazyStructWriter = lazyWriter => {
if (lazyWriter.written > 0) {
lazyWriter.clientStructs.push({ written: lazyWriter.written, restEncoder: encoding.toUint8Array(lazyWriter.encoder.restEncoder) })
lazyWriter.encoder.restEncoder = encoding.createEncoder()
lazyWriter.written = 0
}
}
/**
* @param {LazyStructWriter} lazyWriter
* @param {Item | GC} struct
* @param {number} offset
*/
const writeStructToLazyStructWriter = (lazyWriter, struct, offset) => {
// flush curr if we start another client
if (lazyWriter.written > 0 && lazyWriter.currClient !== struct.id.client) {
flushLazyStructWriter(lazyWriter)
}
if (lazyWriter.written === 0) {
lazyWriter.currClient = struct.id.client
// write next client
lazyWriter.encoder.writeClient(struct.id.client)
// write startClock
encoding.writeVarUint(lazyWriter.encoder.restEncoder, struct.id.clock + offset)
}
struct.write(lazyWriter.encoder, offset)
lazyWriter.written++
}
/**
* Call this function when we collected all parts and want to
* put all the parts together. After calling this method,
* you can continue using the UpdateEncoder.
*
* @param {LazyStructWriter} lazyWriter
*/
const finishLazyStructWriting = (lazyWriter) => {
flushLazyStructWriter(lazyWriter)
// this is a fresh encoder because we called flushCurr
const restEncoder = lazyWriter.encoder.restEncoder
/**
* Now we put all the fragments together.
* This works similarly to `writeClientsStructs`
*/
// write # states that were updated - i.e. the clients
encoding.writeVarUint(restEncoder, lazyWriter.clientStructs.length)
for (let i = 0; i < lazyWriter.clientStructs.length; i++) {
const partStructs = lazyWriter.clientStructs[i]
/**
* Works similarly to `writeStructs`
*/
// write # encoded structs
encoding.writeVarUint(restEncoder, partStructs.written)
// write the rest of the fragment
encoding.writeUint8Array(restEncoder, partStructs.restEncoder)
}
}
/**
* @param {Uint8Array} update
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
*/
export const convertUpdateFormat = (update, YDecoder, YEncoder) => {
const updateDecoder = new YDecoder(decoding.createDecoder(update))
const lazyDecoder = new LazyStructReader(updateDecoder, false)
const updateEncoder = new YEncoder()
const lazyWriter = new LazyStructWriter(updateEncoder)
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
writeStructToLazyStructWriter(lazyWriter, curr, 0)
}
finishLazyStructWriting(lazyWriter)
const ds = readDeleteSet(updateDecoder)
writeDeleteSet(updateEncoder, ds)
return updateEncoder.toUint8Array()
}
/**
* @param {Uint8Array} update
*/
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, UpdateDecoderV1, UpdateEncoderV2)
/**
* @param {Uint8Array} update
*/
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, UpdateDecoderV2, UpdateEncoderV1)

9
test.html Normal file
View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Testing Yjs</title>
</head>
<body>
<script type="module" src="./dist/tests.js"></script>
</body>
</html>

View File

@@ -1,274 +0,0 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
sinon = require('sinon')
sinonChai = require('sinon-chai')
_ = require("underscore")
chai.use(sinonChai)
Connector = require "../../y-connectors/lib/y-test/y-test.coffee"
Y = require "../lib/y.coffee"
Test = require "./TestSuite"
class JsonTest extends Test
makeNewUser: (userId)->
conn = new Connector userId
super new Y conn
type: "JsonTest"
getRandomRoot: (user_num, root)->
root ?= @users[user_num]
types = @users[user_num].types
if _.random(0,1) is 1 # take root
root
else # take child
elems = null
if root.type is "Object"
elems =
for oname,val of root.val()
val
else if root.type is "Array"
elems = root.val()
else
return root
elems = elems.filter (elem)->
(elem.type is "Array") or (elem.type is "Object")
if elems.length is 0
root
else
p = elems[_.random(0, elems.length-1)]
@getRandomRoot user_num, p
getContent: (user_num)->
@users[user_num].toJson(true)
getGeneratingFunctions: (user_num)->
types = @users[user_num].types
super(user_num).concat [
f : (y)=> # SET PROPERTY
l = y.val().length
y.val(_.random(0, l-1), @getRandomText(), 'immutable')
null
types : [types.Array]
, f : (y)=> # Delete Array Element
list = y.val()
if list.length > 0
key = list[_random(0,list.length-1)]
y.delete(key)
types: [types.Array]
, f : (y)=> # insert TEXT mutable
l = y.val().length
y.val(_.random(0, l-1), @getRamdomObject())
types: [types.Array]
, f : (y)=> # insert string
l = y.val().length
y.val(_.random(0, l-1), @getRandomText(), 'immutable')
null
types : [types.Array]
, f : (y)=> # Delete Object Property
list = for name, o of y.val()
name
if list.length > 0
key = list[_random(0,list.length-1)]
y.delete(key)
types: [types.Object]
, f : (y)=> # SET Object Property
y.val(@getRandomKey(), @getRandomObject())
types: [types.Object]
,
f : (y)=> # SET PROPERTY TEXT
y.val(@getRandomKey(), @getRandomText(), 'mutable')
types: [types.Object]
]
describe "JsonFramework", ->
beforeEach (done)->
@timeout 50000
@yTest = new JsonTest()
@users = @yTest.users
@test_user = @yTest.makeNewUser "test_user"
done()
it "can handle many engines, many operations, concurrently (random)", ->
console.log "" # TODO
@yTest.run()
it "has a working test suite", ->
@yTest.compareAll()
it "handles double-late-join", ->
test = new JsonTest("double")
test.run()
@yTest.run()
u1 = test.users[0]
u2 = @yTest.users[1]
ops1 = u1.HB._encode()
ops2 = u2.HB._encode()
u1.HB.renewStateVector u2.HB.getOperationCounter()
u2.HB.renewStateVector u1.HB.getOperationCounter()
u1.engine.applyOps ops2
u2.engine.applyOps ops1
expect(test.getContent(0)).to.deep.equal(@yTest.getContent(1))
it "can handle creaton of complex json (1)", ->
@yTest.users[0].val('a', 'q', "mutable")
@yTest.users[1].val('a', 't', "mutable")
@yTest.compareAll()
q = @yTest.users[2].val('a')
q.insert(0,'A')
@yTest.compareAll()
expect(@yTest.getSomeUser().val("a").val()).to.equal("At")
it "can handle creaton of complex json (2)", ->
@yTest.getSomeUser().val('x', {'a':'b'})
@yTest.getSomeUser().val('a', {'a':{q:"dtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt"}}, "mutable")
@yTest.getSomeUser().val('b', {'a':{}})
@yTest.getSomeUser().val('c', {'a':'c'})
@yTest.getSomeUser().val('c', {'a':'b'})
@yTest.compareAll()
q = @yTest.getSomeUser().val("a").val("a").val("q")
q.insert(0,'A')
@yTest.compareAll()
expect(@yTest.getSomeUser().val("a").val("a").val("q").val()).to.equal("Adtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")
it "can handle creaton of complex json (3)", ->
@yTest.users[0].val('l', [1,2,3], "mutable")
@yTest.users[1].val('l', [4,5,6], "mutable")
@yTest.compareAll()
@yTest.users[2].val('l').insert(0,'A')
w = @yTest.users[1].val('l').insert(0,'B', "mutable").val(0)
w.insert 1, "C"
expect(w.val()).to.equal("BC")
@yTest.compareAll()
it "handles immutables and primitive data types", ->
@yTest.getSomeUser().val('string', "text", "immutable")
@yTest.getSomeUser().val('number', 4, "immutable")
@yTest.getSomeUser().val('object', {q:"rr"}, "immutable")
@yTest.getSomeUser().val('null', null)
@yTest.compareAll()
expect(@yTest.getSomeUser().val('string')).to.equal "text"
expect(@yTest.getSomeUser().val('number')).to.equal 4
expect(@yTest.getSomeUser().val('object').val('q')).to.equal "rr"
expect(@yTest.getSomeUser().val('null') is null).to.be.ok
it "handles immutables and primitive data types (2)", ->
@yTest.users[0].val('string', "text", "immutable")
@yTest.users[1].val('number', 4, "immutable")
@yTest.users[2].val('object', {q:"rr"}, "immutable")
@yTest.users[0].val('null', null)
@yTest.compareAll()
expect(@yTest.getSomeUser().val('string')).to.equal "text"
expect(@yTest.getSomeUser().val('number')).to.equal 4
expect(@yTest.getSomeUser().val('object').val('q')).to.equal "rr"
expect(@yTest.getSomeUser().val('null') is null).to.be.ok
it "Observers work on JSON Types (add type observers, local and foreign)", ->
u = @yTest.users[0]
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("add")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('0')
expect(change.name).to.equal("newStuff")
last_task = "observer1"
u.observe observer1
u.val("newStuff","someStuff","mutable")
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("add")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('1')
expect(change.name).to.equal("moreStuff")
last_task = "observer2"
u.observe observer2
v = @yTest.users[1]
v.val("moreStuff","someMoreStuff")
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2
it "Observers work on JSON Types (update type observers, local and foreign)", ->
u = @yTest.users[0].val("newStuff","oldStuff","mutable").val("moreStuff","moreOldStuff","mutable")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("update")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('0')
expect(change.name).to.equal("newStuff")
expect(change.oldValue.val()).to.equal("oldStuff")
last_task = "observer1"
u.observe observer1
u.val("newStuff","someStuff")
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("update")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('1')
expect(change.name).to.equal("moreStuff")
expect(change.oldValue.val()).to.equal("moreOldStuff")
last_task = "observer2"
u.observe observer2
v = @yTest.users[1]
v.val("moreStuff","someMoreStuff")
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2
it "Observers work on JSON Types (delete type observers, local and foreign)", ->
u = @yTest.users[0].val("newStuff","oldStuff","mutable").val("moreStuff","moreOldStuff","mutable")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("delete")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('0')
expect(change.name).to.equal("newStuff")
expect(change.oldValue.val()).to.equal("oldStuff")
last_task = "observer1"
u.observe observer1
u.delete("newStuff")
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("delete")
expect(change.object).to.equal(u)
expect(change.changedBy).to.equal('1')
expect(change.name).to.equal("moreStuff")
expect(change.oldValue.val()).to.equal("moreOldStuff")
last_task = "observer2"
u.observe observer2
v = @yTest.users[1]
v.delete("moreStuff")
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2

View File

@@ -1,208 +0,0 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
sinon = require('sinon')
sinonChai = require('sinon-chai')
_ = require("underscore")
chai.use(sinonChai)
Connector = require "../../y-connectors/lib/y-test/y-test.coffee"
module.exports = class Test
constructor: (@name_suffix = "")->
@number_of_test_cases_multiplier = 1
@repeat_this = 3 * @number_of_test_cases_multiplier
@doSomething_amount = 123 * @number_of_test_cases_multiplier
@number_of_engines = 5 + @number_of_test_cases_multiplier - 1
@time = 0 # denotes to the time when run was started
@ops = 0 # number of operations (used with @time)
@time_now = 0 # current time
@debug = false
@reinitialize()
reinitialize: ()->
@users = []
for i in [0...@number_of_engines]
u = @makeNewUser (i+@name_suffix)
for user in @users
u.getConnector().join(user.getConnector()) # TODO: change the test-connector to make this more convenient
@users.push u
@initUsers?(@users[0])
@flushAll()
# is called by implementing class
makeNewUser: (user)->
user.HB.setManualGarbageCollect()
user
getSomeUser: ()->
i = _.random 0, (@users.length-1)
@users[i]
getRandomText: (chars, min_length = 0)->
chars ?= "abcdefghijklmnopqrstuvwxyz"
length = _.random min_length, 10
#length = 1
nextchar = chars[(_.random 0, (chars.length-1))]
text = ""
_(length).times ()-> text += nextchar
text
getRandomObject: ()->
result = {}
key1 = @getRandomKey()
key2 = @getRandomKey()
val1 = @getRandomText()
val2 = null
if _.random(0,1) is 1
val2 = @getRandomObject()
else
val2 = @getRandomText()
result[key1] = val1
result[key2] = val2
result
getRandomKey: ()->
@getRandomText [1,2,'x','y'], 1 # only 4 keys
getGeneratingFunctions: (user_num)=>
types = @users[user_num].types
[
f : (y)=> # INSERT TEXT
y
pos = _.random 0, (y.val().length-1)
y.insert pos, @getRandomText()
null
types: [types.String]
,
f : (y)-> # DELETE TEXT
if y.val().length > 0
pos = _.random 0, (y.val().length-1) # TODO: put here also arbitrary number (test behaviour in error cases)
length = _.random 0, (y.val().length - pos)
ops1 = y.delete pos, length
undefined
types : [types.String]
]
getRandomRoot: (user_num)->
throw new Error "overwrite me!"
getContent: (user_num)->
throw new Error "overwrite me!"
generateRandomOp: (user_num)=>
y = @getRandomRoot(user_num)
choices = @getGeneratingFunctions(user_num).filter (gf)->
_.some gf.types, (type)->
y instanceof type
if choices.length is 0
console.dir(y)
throw new Error "You forgot to specify a test generation methot for this Operation! (#{y.type})"
i = _.random 0, (choices.length-1)
choices[i].f y
applyRandomOp: (user_num)=>
user = @users[user_num]
user.getConnector().flushOneRandom()
doSomething: ()->
user_num = _.random (@number_of_engines-1)
choices = [@applyRandomOp, @generateRandomOp]
choice = _.random (choices.length-1)
choices[choice](user_num)
flushAll: (final)->
# TODO:!!
final = false
if @users.length <= 1 or not final
for user,user_number in @users
user.getConnector().flushAll()
else
for user,user_number in @users[1..]
user.getConnector().flushAll()
ops = @users[1].getHistoryBuffer()._encode @users[0].HB.getOperationCounter()
@users[0].engine.applyOpsCheckDouble ops
compareAll: (test_number)->
@flushAll(true)
@time += (new Date()).getTime() - @time_now
number_of_created_operations = 0
for i in [0...(@users.length)]
number_of_created_operations += @users[i].getConnector().getOpsInExecutionOrder().length
@ops += number_of_created_operations*@users.length
ops_per_msek = Math.floor(@ops/@time)
if test_number? # and @debug
console.log "#{test_number}/#{@repeat_this}: #{number_of_created_operations} were created and applied on (#{@users.length}) users ops in a different order." + " Over all we consumed #{@ops} operations in #{@time/1000} seconds (#{ops_per_msek} ops/msek)."
for i in [0...(@users.length-1)]
if @debug
if not _.isEqual @getContent(i), @getContent(i+1)
printOpsInExecutionOrder = (otnumber, otherotnumber)=>
ops = _.filter @users[otnumber].getConnector().getOpsInExecutionOrder(), (o)->
typeof o.uid.op_name isnt 'string' and o.uid.creator isnt '_'
for s,j in ops
console.log "op#{j} = " + (JSON.stringify s)
console.log ""
s = "ops = ["
for o,j in ops
if j isnt 0
s += ", "
s += "op#{j}"
s += "]"
console.log s
console.log "@test_user.engine.applyOps ops"
console.log "expect(@test_user.val('name').val()).to.equal(\"#{@users[otherotnumber].val('name').val()}\")"
ops
console.log ""
console.log "Found an OT Puzzle!"
console.log "OT states:"
for u,j in @users
console.log "OT#{j}: "+u.val('name').val()
console.log "\nOT execution order (#{i},#{i+1}):"
printOpsInExecutionOrder i, i+1
console.log ""
ops = printOpsInExecutionOrder i+1, i
console.log ""
expect(@getContent(i)).to.deep.equal(@getContent(i+1))
run: ()->
if @debug
console.log ''
for times in [1..@repeat_this]
@time_now = (new Date).getTime()
for i in [1..Math.floor(@doSomething_amount/2)]
@doSomething()
@flushAll(false)
for u in @users
u.HB.emptyGarbage()
for i in [1..Math.floor(@doSomething_amount/2)]
@doSomething()
@compareAll(times)
@testHBencoding()
if times isnt @repeat_this
@reinitialize()
testHBencoding: ()->
# in case of JsonFramework, every user will create its JSON first! therefore, the testusers id must be small than all the others (see InsertType)
@users[@users.length] = @makeNewUser (-1) # this does not want to join with anymody
@users[@users.length-1].HB.renewStateVector @users[0].HB.getOperationCounter()
@users[@users.length-1].engine.applyOps @users[0].HB._encode()
#if @getContent(@users.length-1) isnt @getContent(0)
# console.log "testHBencoding:"
# console.log "Unprocessed ops first: #{@users[0].engine.unprocessed_ops.length}"
# console.log "Unprocessed ops last: #{@users[@users.length-1].engine.unprocessed_ops.length}"
expect(@getContent(@users.length-1)).to.deep.equal(@getContent(0))

View File

@@ -1,119 +0,0 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
sinon = require('sinon')
sinonChai = require('sinon-chai')
_ = require("underscore")
chai.use(sinonChai)
Y = require "../lib/y"
Connector = require "../../y-connectors/lib/y-test/y-test.coffee"
Test = require "./TestSuite"
class TextTest extends Test
type: "TextTest"
makeNewUser: (userId)->
conn = new Connector userId
new Y conn
initUsers: (u)->
u.val("TextTest","","mutable")
getRandomRoot: (user_num)->
@users[user_num].val("TextTest")
getContent: (user_num)->
@users[user_num].val("TextTest").val()
describe "TextFramework", ->
beforeEach (done)->
@timeout 50000
@yTest = new TextTest()
done()
it "simple multi-char insert", ->
u = @yTest.users[0].val("TextTest")
u.insert 0, "abc"
u = @yTest.users[1].val("TextTest")
u.insert 0, "xyz"
@yTest.compareAll()
u.delete 0, 1
@yTest.compareAll()
expect(u.val()).to.equal("bcxyz")
it "Observers work on shared Text (insert type observers, local and foreign)", ->
u = @yTest.users[0].val("TextTest","my awesome Text","mutable").val("TextTest")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("insert")
expect(change.object).to.equal(u)
expect(change.value).to.equal("a")
expect(change.position).to.equal(1)
expect(change.changedBy).to.equal('0')
last_task = "observer1"
u.observe observer1
u.insert 1, "a"
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("insert")
expect(change.object).to.equal(u)
expect(change.value).to.equal("x")
expect(change.position).to.equal(0)
expect(change.changedBy).to.equal('1')
last_task = "observer2"
u.observe observer2
v = @yTest.users[1].val("TextTest")
v.insert 0, "x"
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2
it "Observers work on shared Text (delete type observers, local and foreign)", ->
u = @yTest.users[0].val("TextTest","my awesome Text","mutable").val("TextTest")
@yTest.flushAll()
last_task = null
observer1 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("delete")
expect(change.object).to.equal(u)
expect(change.position).to.equal(1)
expect(change.length).to.equal(1)
expect(change.changedBy).to.equal('0')
last_task = "observer1"
u.observe observer1
u.delete 1, 1
expect(last_task).to.equal("observer1")
u.unobserve observer1
observer2 = (changes)->
expect(changes.length).to.equal(1)
change = changes[0]
expect(change.type).to.equal("delete")
expect(change.object).to.equal(u)
expect(change.position).to.equal(0)
expect(change.length).to.equal(1)
expect(change.changedBy).to.equal('1')
last_task = "observer2"
u.observe observer2
v = @yTest.users[1].val("TextTest")
v.delete 0, 1
@yTest.flushAll()
expect(last_task).to.equal("observer2")
u.unobserve observer2
it "can handle many engines, many operations, concurrently (random)", ->
console.log("testiy deleted this TODO:dtrn")
@yTest.run()

View File

@@ -1,196 +0,0 @@
chai = require('chai')
expect = chai.expect
should = chai.should()
sinon = require('sinon')
sinonChai = require('sinon-chai')
_ = require("underscore")
$ = require("jquery")
document?.$ = $ # for browser
require 'coffee-errors'
chai.use(sinonChai)
Y = require "../lib/index"
Connector = require "../../Yatta-Connectors/lib/test-connector/test-connector.coffee"
Test = require "./TestSuite"
class XmlTest extends Test
type: "XmlTest"
makeNewUser: (user, conn)->
super new Y.XmlFramework user, conn
getRandomRoot: (user_num)->
@users[user_num].getSharedObject()
getContent: (user_num)->
@users[user_num].val()
describe "XmlFramework", ->
beforeEach (done)->
@timeout 50000
@yTest = new XmlTest()
###
@users = @yTest.users
###
test_users = []
connector = (new Connector 0, test_users)
@test_user = @yTest.makeNewUser 0, connector
test_users.push @test_user
# test_user_listen listens to the actions of test_user. He will update his dom when he receives from test_user.
@test_user_listen = @yTest.makeNewUser 2, connector
test_users.push @test_user_listen
@test_user2 = @yTest.makeNewUser 1, (Connector_uninitialized [])
$("#test_dom").replaceWith('<div id="test_dom" test_attribute="the test" class="stuffy" style="color: blue"><p id="replaceme">replace me</p><p id="removeme">remove me</p><p>This is a test object for <b>XmlFramework</b></p><span class="span_element"><p>span</p></span></div>')
@$dom = $("#test_dom")
@dom = @$dom[0]
@test_user.val(@dom)
@test_user_listen.getConnector().flushAll()
@test_user_listen_dom = @test_user_listen.val()
@check = ()=>
dom_ = @dom.outerHTML
# now test if other collaborators can parse the HB and result in the same content
hb = @test_user.HB._encode()
@test_user2.engine.applyOps(hb)
dom2 = @test_user2.val()
expect(dom_).to.equal(dom2.outerHTML)
@test_user_listen.getConnector().flushAll()
expect(dom_).to.equal(@test_user_listen_dom.outerHTML)
done()
it "can transform to a new real Dom element", ->
dom_ = @test_user.val(true)
expect(dom_ isnt @dom).to.be.true
it "supports dom.insertBefore", ->
newdom = $("<p>dtrn</p>")[0]
newdom2 = $("<p>dtrn2</p>")[0]
n = $("#removeme")[0]
@dom.insertBefore(newdom, null)
@dom.insertBefore(newdom2, n)
@check()
it "supports dom.appendChild", ->
newdom = $("<p>dtrn</p>")[0]
@dom.appendChild(newdom)
@check()
it "supports dom.setAttribute", ->
@dom.setAttribute("test_attribute", "newVal")
@check()
it "supports dom.removeAttribute", ->
@dom.removeAttribute("test_attribute")
@check()
it "supports dom.removeChild", ->
@dom.removeChild($("#removeme")[0])
expect($("#removeme").length).to.equal(0)
@check()
it "supports dom.replaceChild", ->
newdom = $("<p>replaced</p>")[0]
replace = $("#replaceme")[0]
@dom.replaceChild(newdom,replace)
expect($("#replaceme").length).to.equal(0)
@check()
it "supports dom.classList.add", ->
@dom.classList.add "classy"
@check()
it "supports dom.textcontent", -> #TODO!!!!
@dom.classList.add "classy"
@check()
it "supports jquery.addClass", ->
@$dom.addClass("testy")
@check()
it "supports jquery.after", ->
d = $("#test_dom p")
d.after("<div class=\"inserted_after\">after</div>")
@check()
it "supports jquery.append", ->
d = $("#test_dom p")
d.after("<b>appended</b>")
@check()
it "supports jquery.appendTo", ->
$("<b>appendedTo</b>").appendTo("#test_dom p")
$("p").appendTo("#test_dom")
@check()
it "supports jquery.before", ->
d = $("#test_dom p")
d.before("<div>before</div>")
@check()
it "supports jquery.detach", ->
d = $(".inserted_after")
d.detach()
@check()
it "supports jquery.empty", ->
d = $("#test_dom p")
d.empty()
@check()
it "supports jquery.insertAfter", ->
$("<p>after span</p>").insertAfter(".span_element")
@check()
it "supports jquery.insertBefore", ->
$("<p>before span</p>").insertBefore(".span_element")
@check()
it "supports jquery.prepend", ->
d = $("#test_dom div")
d.prepend("<p>prepended</p>")
@check()
it "supports jquery.prependTo", ->
$("<p atone=false attwo=\"dtrn\" class=\"attr_node sudo su\">prepended to</p>").prependTo("#test_dom div")
@check()
it "supports jquery.remove", ->
d = $("#test_dom b")
d.remove()
@check()
it "supports jquery.removeAttr", ->
d = $(".attr_node")
d.removeAttr("attwo")
@check()
it "supports jquery.removeClass", ->
d = $(".attr_node")
d.removeClass("sudo")
@check()
it "supports jquery.attr", ->
d = $(".attr_node")
d.attr("atone", true)
@check()
it "supports jquery.replaceAll", ->
$("<span>New span content </span>").replaceAll("#test_dom div")
@check()
it "supports jquery.replaceWith", ->
d = $("#test_dom span")
d.replaceWith("<div>me is div again </div>")
@check()

File diff suppressed because one or more lines are too long

271
tests/doc.tests.js Normal file
View File

@@ -0,0 +1,271 @@
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.
*
* @param {t.TestCase} tc
*/
export const testClientIdDuplicateChange = tc => {
const doc1 = new Y.Doc()
doc1.clientID = 0
const doc2 = new Y.Doc()
doc2.clientID = 0
t.assert(doc2.clientID === doc1.clientID)
doc1.getArray('a').insert(0, [1, 2])
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))
t.assert(doc2.clientID !== doc1.clientID)
}
/**
* @param {t.TestCase} tc
*/
export const testGetTypeEmptyId = tc => {
const doc1 = new Y.Doc()
doc1.getText('').insert(0, 'h')
doc1.getText().insert(1, 'i')
const doc2 = new Y.Doc()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))
t.assert(doc2.getText().toString() === 'hi')
t.assert(doc2.getText('').toString() === 'hi')
}
/**
* @param {t.TestCase} tc
*/
export const testToJSON = tc => {
const doc = new Y.Doc()
t.compare(doc.toJSON(), {}, 'doc.toJSON yields empty object')
const arr = doc.getArray('array')
arr.push(['test1'])
const map = doc.getMap('map')
map.set('k1', 'v1')
const map2 = new Y.Map()
map.set('k2', map2)
map2.set('m2k1', 'm2v1')
t.compare(doc.toJSON(), {
array: ['test1'],
map: {
k1: 'v1',
k2: {
m2k1: 'm2v1'
}
}
}, 'doc.toJSON has array and recursive map')
}
/**
* @param {t.TestCase} tc
*/
export const testSubdoc = tc => {
const doc = new Y.Doc()
doc.load() // doesn't do anything
{
/**
* @type {Array<any>|null}
*/
let event = /** @type {any} */ (null)
doc.on('subdocs', subdocs => {
event = [Array.from(subdocs.added).map(x => x.guid), Array.from(subdocs.removed).map(x => x.guid), Array.from(subdocs.loaded).map(x => x.guid)]
})
const subdocs = doc.getMap('mysubdocs')
const docA = new Y.Doc({ guid: 'a' })
docA.load()
subdocs.set('a', docA)
t.compare(event, [['a'], [], ['a']])
event = null
subdocs.get('a').load()
t.assert(event === null)
event = null
subdocs.get('a').destroy()
t.compare(event, [['a'], ['a'], []])
subdocs.get('a').load()
t.compare(event, [[], [], ['a']])
subdocs.set('b', new Y.Doc({ guid: 'a', shouldLoad: false }))
t.compare(event, [['a'], [], []])
subdocs.get('b').load()
t.compare(event, [[], [], ['a']])
const docC = new Y.Doc({ guid: 'c' })
docC.load()
subdocs.set('c', docC)
t.compare(event, [['c'], [], ['c']])
t.compare(Array.from(doc.getSubdocGuids()), ['a', 'c'])
}
const doc2 = new Y.Doc()
{
t.compare(Array.from(doc2.getSubdocs()), [])
/**
* @type {Array<any>|null}
*/
let event = /** @type {any} */ (null)
doc2.on('subdocs', subdocs => {
event = [Array.from(subdocs.added).map(d => d.guid), Array.from(subdocs.removed).map(d => d.guid), Array.from(subdocs.loaded).map(d => d.guid)]
})
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
t.compare(event, [['a', 'a', 'c'], [], []])
doc2.getMap('mysubdocs').get('a').load()
t.compare(event, [[], [], ['a']])
t.compare(Array.from(doc2.getSubdocGuids()), ['a', 'c'])
doc2.getMap('mysubdocs').delete('a')
t.compare(event, [[], ['a'], []])
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)
}

108
tests/encoding.tests.js Normal file
View File

@@ -0,0 +1,108 @@
import * as t from 'lib0/testing'
import * as promise from 'lib0/promise'
import {
contentRefs,
readContentBinary,
readContentDeleted,
readContentString,
readContentJSON,
readContentEmbed,
readContentType,
readContentFormat,
readContentAny,
readContentDoc,
Doc,
PermanentUserData,
encodeStateAsUpdate,
applyUpdate
} from '../src/internals.js'
import * as Y from '../src/index.js'
/**
* @param {t.TestCase} tc
*/
export const testStructReferences = tc => {
t.assert(contentRefs.length === 11)
t.assert(contentRefs[1] === readContentDeleted)
t.assert(contentRefs[2] === readContentJSON) // TODO: deprecate content json?
t.assert(contentRefs[3] === readContentBinary)
t.assert(contentRefs[4] === readContentString)
t.assert(contentRefs[5] === readContentEmbed)
t.assert(contentRefs[6] === readContentFormat)
t.assert(contentRefs[7] === readContentType)
t.assert(contentRefs[8] === readContentAny)
t.assert(contentRefs[9] === readContentDoc)
// contentRefs[10] is reserved for Skip structs
}
/**
* There is some custom encoding/decoding happening in PermanentUserData.
* This is why it landed here.
*
* @param {t.TestCase} tc
*/
export const testPermanentUserData = async tc => {
const ydoc1 = new Doc()
const ydoc2 = new Doc()
const pd1 = new PermanentUserData(ydoc1)
const pd2 = new PermanentUserData(ydoc2)
pd1.setUserMapping(ydoc1, ydoc1.clientID, 'user a')
pd2.setUserMapping(ydoc2, ydoc2.clientID, 'user b')
ydoc1.getText().insert(0, 'xhi')
ydoc1.getText().delete(0, 1)
ydoc2.getText().insert(0, 'hxxi')
ydoc2.getText().delete(1, 2)
await promise.wait(10)
applyUpdate(ydoc2, encodeStateAsUpdate(ydoc1))
applyUpdate(ydoc1, encodeStateAsUpdate(ydoc2))
// now sync a third doc with same name as doc1 and then create PermanentUserData
const ydoc3 = new Doc()
applyUpdate(ydoc3, encodeStateAsUpdate(ydoc1))
const pd3 = new PermanentUserData(ydoc3)
pd3.setUserMapping(ydoc3, ydoc3.clientID, 'user a')
}
/**
* Reported here: https://github.com/yjs/yjs/issues/308
* @param {t.TestCase} tc
*/
export const testDiffStateVectorOfUpdateIsEmpty = tc => {
const ydoc = new Y.Doc()
/**
* @type {any}
*/
let sv = null
ydoc.getText().insert(0, 'a')
ydoc.on('update', update => {
sv = Y.encodeStateVectorFromUpdate(update)
})
// should produce an update with an empty state vector (because previous ops are missing)
ydoc.getText().insert(0, 'a')
t.assert(sv !== null && sv.byteLength === 1 && sv[0] === 0)
}
/**
* Reported here: https://github.com/yjs/yjs/issues/308
* @param {t.TestCase} tc
*/
export const testDiffStateVectorOfUpdateIgnoresSkips = tc => {
const ydoc = new Y.Doc()
/**
* @type {Array<Uint8Array>}
*/
const updates = []
ydoc.on('update', update => {
updates.push(update)
})
ydoc.getText().insert(0, 'a')
ydoc.getText().insert(0, 'b')
ydoc.getText().insert(0, 'c')
const update13 = Y.mergeUpdates([updates[0], updates[2]])
const sv = Y.encodeStateVectorFromUpdate(update13)
const state = Y.decodeStateVector(sv)
t.assert(state.get(ydoc.clientID) === 1)
t.assert(state.size === 1)
}

28
tests/index.js Normal file
View File

@@ -0,0 +1,28 @@
import * as map from './y-map.tests.js'
import * as array from './y-array.tests.js'
import * as text from './y-text.tests.js'
import * as xml from './y-xml.tests.js'
import * as encoding from './encoding.tests.js'
import * as undoredo from './undo-redo.tests.js'
import * as compatibility from './compatibility.tests.js'
import * as doc from './doc.tests.js'
import * as snapshot from './snapshot.tests.js'
import * as updates from './updates.tests.js'
import * as relativePositions from './relativePositions.tests.js'
import { runTests } from 'lib0/testing'
import { isBrowser, isNode } from 'lib0/environment'
import * as log from 'lib0/logging'
if (isBrowser) {
log.createVConsole(document.body)
}
runTests({
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions
}).then(success => {
/* istanbul ignore next */
if (isNode) {
process.exit(success ? 0 : 1)
}
})

View File

@@ -0,0 +1,104 @@
import * as Y from '../src/index.js'
import * as t from 'lib0/testing'
/**
* @param {Y.Text} ytext
*/
const checkRelativePositions = ytext => {
// test if all positions are encoded and restored correctly
for (let i = 0; i < ytext.length; i++) {
// for all types of associations..
for (let assoc = -1; assoc < 2; assoc++) {
const rpos = Y.createRelativePositionFromTypeIndex(ytext, i, assoc)
const encodedRpos = Y.encodeRelativePosition(rpos)
const decodedRpos = Y.decodeRelativePosition(encodedRpos)
const absPos = /** @type {Y.AbsolutePosition} */ (Y.createAbsolutePositionFromRelativePosition(decodedRpos, /** @type {Y.Doc} */ (ytext.doc)))
t.assert(absPos.index === i)
t.assert(absPos.assoc === assoc)
}
}
}
/**
* @param {t.TestCase} tc
*/
export const testRelativePositionCase1 = tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
ytext.insert(0, '1')
ytext.insert(0, 'abc')
ytext.insert(0, 'z')
ytext.insert(0, 'y')
ytext.insert(0, 'x')
checkRelativePositions(ytext)
}
/**
* @param {t.TestCase} tc
*/
export const testRelativePositionCase2 = tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
ytext.insert(0, 'abc')
checkRelativePositions(ytext)
}
/**
* @param {t.TestCase} tc
*/
export const testRelativePositionCase3 = tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
ytext.insert(0, 'abc')
ytext.insert(0, '1')
ytext.insert(0, 'xyz')
checkRelativePositions(ytext)
}
/**
* @param {t.TestCase} tc
*/
export const testRelativePositionCase4 = tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
ytext.insert(0, '1')
checkRelativePositions(ytext)
}
/**
* @param {t.TestCase} tc
*/
export const testRelativePositionCase5 = tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
ytext.insert(0, '2')
ytext.insert(0, '1')
checkRelativePositions(ytext)
}
/**
* @param {t.TestCase} tc
*/
export const testRelativePositionCase6 = tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
checkRelativePositions(ytext)
}
/**
* @param {t.TestCase} tc
*/
export const testRelativePositionAssociationDifference = tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
ytext.insert(0, '2')
ytext.insert(0, '1')
const rposRight = Y.createRelativePositionFromTypeIndex(ytext, 1, 0)
const rposLeft = Y.createRelativePositionFromTypeIndex(ytext, 1, -1)
ytext.insert(1, 'x')
const posRight = Y.createAbsolutePositionFromRelativePosition(rposRight, ydoc)
const posLeft = Y.createAbsolutePositionFromRelativePosition(rposLeft, ydoc)
t.assert(posRight != null && posRight.index === 2)
t.assert(posLeft != null && posLeft.index === 1)
}

171
tests/snapshot.tests.js Normal file
View File

@@ -0,0 +1,171 @@
import * as Y from '../src/index.js'
import * as t from 'lib0/testing'
import { init } from './testHelper.js'
/**
* @param {t.TestCase} tc
*/
export const testBasicRestoreSnapshot = tc => {
const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, ['hello'])
const snap = Y.snapshot(doc)
doc.getArray('array').insert(1, ['world'])
const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['hello'])
t.compare(doc.getArray('array').toArray(), ['hello', 'world'])
}
/**
* @param {t.TestCase} tc
*/
export const testEmptyRestoreSnapshot = tc => {
const doc = new Y.Doc({ gc: false })
const snap = Y.snapshot(doc)
snap.sv.set(9999, 0)
doc.getArray().insert(0, ['world'])
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 = Y.snapshot(doc)
const docRestored2 = Y.createDocFromSnapshot(doc, snap2)
t.compare(docRestored2.getArray().toArray(), ['world'])
}
/**
* @param {t.TestCase} tc
*/
export const testRestoreSnapshotWithSubType = tc => {
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 = Y.snapshot(doc)
subMap.set('key2', 'value2')
const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toJSON(), [{
key1: 'value1'
}])
t.compare(doc.getArray('array').toJSON(), [{
key1: 'value1',
key2: 'value2'
}])
}
/**
* @param {t.TestCase} tc
*/
export const testRestoreDeletedItem1 = tc => {
const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, ['item1', 'item2'])
const snap = Y.snapshot(doc)
doc.getArray('array').delete(0)
const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item1', 'item2'])
t.compare(doc.getArray('array').toArray(), ['item2'])
}
/**
* @param {t.TestCase} tc
*/
export const testRestoreLeftItem = tc => {
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 = Y.snapshot(doc)
doc.getArray('array').delete(1)
const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item0', 'item1'])
t.compare(doc.getArray('array').toArray(), ['item0'])
}
/**
* @param {t.TestCase} tc
*/
export const testDeletedItemsBase = tc => {
const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, ['item1'])
doc.getArray('array').delete(0)
const snap = Y.snapshot(doc)
doc.getArray('array').insert(0, ['item0'])
const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), [])
t.compare(doc.getArray('array').toArray(), ['item0'])
}
/**
* @param {t.TestCase} tc
*/
export const testDeletedItems2 = tc => {
const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, ['item1', 'item2', 'item3'])
doc.getArray('array').delete(1)
const snap = Y.snapshot(doc)
doc.getArray('array').insert(0, ['item0'])
const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item1', 'item3'])
t.compare(doc.getArray('array').toArray(), ['item0', 'item1', 'item3'])
}
/**
* @param {t.TestCase} tc
*/
export const testDependentChanges = tc => {
const { array0, array1, testConnector } = init(tc, { users: 2 })
if (!array0.doc) {
throw new Error('no document 0')
}
if (!array1.doc) {
throw new Error('no document 1')
}
/**
* @type {Y.Doc}
*/
const doc0 = array0.doc
/**
* @type {Y.Doc}
*/
const doc1 = array1.doc
doc0.gc = false
doc1.gc = false
array0.insert(0, ['user1item1'])
testConnector.syncAll()
array1.insert(1, ['user2item1'])
testConnector.syncAll()
const snap = Y.snapshot(array0.doc)
array0.insert(2, ['user1item2'])
testConnector.syncAll()
array1.insert(3, ['user2item2'])
testConnector.syncAll()
const docRestored0 = Y.createDocFromSnapshot(array0.doc, snap)
t.compare(docRestored0.getArray('array').toArray(), ['user1item1', 'user2item1'])
const docRestored1 = Y.createDocFromSnapshot(array1.doc, snap)
t.compare(docRestored1.getArray('array').toArray(), ['user1item1', 'user2item1'])
}

473
tests/testHelper.js Normal file
View File

@@ -0,0 +1,473 @@
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'
import * as object from 'lib0/object'
import * as map from 'lib0/map'
import * as Y from '../src/index.js'
export * from '../src/index.js'
if (typeof window !== 'undefined') {
// @ts-ignore
window.Y = Y // eslint-disable-line
}
/**
* @param {TestYInstance} y // publish message created by `y` to all other online clients
* @param {Uint8Array} m
*/
const broadcastMessage = (y, m) => {
if (y.tc.onlineConns.has(y)) {
y.tc.onlineConns.forEach(remoteYInstance => {
if (remoteYInstance !== y) {
remoteYInstance._receive(m, y)
}
})
}
}
export let useV2 = false
export const encV1 = {
encodeStateAsUpdate: Y.encodeStateAsUpdate,
mergeUpdates: Y.mergeUpdates,
applyUpdate: Y.applyUpdate,
logUpdate: Y.logUpdate,
updateEventName: 'update',
diffUpdate: Y.diffUpdate
}
export const encV2 = {
encodeStateAsUpdate: Y.encodeStateAsUpdateV2,
mergeUpdates: Y.mergeUpdatesV2,
applyUpdate: Y.applyUpdateV2,
logUpdate: Y.logUpdateV2,
updateEventName: 'updateV2',
diffUpdate: Y.diffUpdateV2
}
export let enc = encV1
const useV1Encoding = () => {
useV2 = false
enc = encV1
}
const useV2Encoding = () => {
console.error('sync protocol doesnt support v2 protocol yet, fallback to v1 encoding') // @Todo
useV2 = false
enc = encV1
}
export class TestYInstance extends Y.Doc {
/**
* @param {TestConnector} testConnector
* @param {number} clientID
*/
constructor (testConnector, clientID) {
super()
this.userID = clientID // overwriting clientID
/**
* @type {TestConnector}
*/
this.tc = testConnector
/**
* @type {Map<TestYInstance, Array<Uint8Array>>}
*/
this.receiving = new Map()
testConnector.allConns.add(this)
/**
* The list of received updates.
* We are going to merge them later using Y.mergeUpdates and check if the resulting document is correct.
* @type {Array<Uint8Array>}
*/
this.updates = []
// set up observe on local model
this.on(enc.updateEventName, /** @param {Uint8Array} update @param {any} origin */ (update, origin) => {
if (origin !== testConnector) {
const encoder = encoding.createEncoder()
syncProtocol.writeUpdate(encoder, update)
broadcastMessage(this, encoding.toUint8Array(encoder))
}
this.updates.push(update)
})
this.connect()
}
/**
* Disconnect from TestConnector.
*/
disconnect () {
this.receiving = new Map()
this.tc.onlineConns.delete(this)
}
/**
* Append yourself to the list of known Y instances in testconnector.
* Also initiate sync with all clients.
*/
connect () {
if (!this.tc.onlineConns.has(this)) {
this.tc.onlineConns.add(this)
const encoder = encoding.createEncoder()
syncProtocol.writeSyncStep1(encoder, this)
// publish SyncStep1
broadcastMessage(this, encoding.toUint8Array(encoder))
this.tc.onlineConns.forEach(remoteYInstance => {
if (remoteYInstance !== this) {
// remote instance sends instance to this instance
const encoder = encoding.createEncoder()
syncProtocol.writeSyncStep1(encoder, remoteYInstance)
this._receive(encoding.toUint8Array(encoder), remoteYInstance)
}
})
}
}
/**
* Receive a message from another client. This message is only appended to the list of receiving messages.
* TestConnector decides when this client actually reads this message.
*
* @param {Uint8Array} message
* @param {TestYInstance} remoteClient
*/
_receive (message, remoteClient) {
map.setIfUndefined(this.receiving, remoteClient, () => []).push(message)
}
}
/**
* Keeps track of TestYInstances.
*
* The TestYInstances add/remove themselves from the list of connections maiained in this object.
* I think it makes sense. Deal with it.
*/
export class TestConnector {
/**
* @param {prng.PRNG} gen
*/
constructor (gen) {
/**
* @type {Set<TestYInstance>}
*/
this.allConns = new Set()
/**
* @type {Set<TestYInstance>}
*/
this.onlineConns = new Set()
/**
* @type {prng.PRNG}
*/
this.prng = gen
}
/**
* Create a new Y instance and add it to the list of connections
* @param {number} clientID
*/
createY (clientID) {
return new TestYInstance(this, clientID)
}
/**
* Choose random connection and flush a random message from a random sender.
*
* If this function was unable to flush a message, because there are no more messages to flush, it returns false. true otherwise.
* @return {boolean}
*/
flushRandomMessage () {
const gen = this.prng
const conns = Array.from(this.onlineConns).filter(conn => conn.receiving.size > 0)
if (conns.length > 0) {
const receiver = prng.oneOf(gen, conns)
const [sender, messages] = prng.oneOf(gen, Array.from(receiver.receiving))
const m = messages.shift()
if (messages.length === 0) {
receiver.receiving.delete(sender)
}
if (m === undefined) {
return this.flushRandomMessage()
}
const encoder = encoding.createEncoder()
// console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver))
// do not publish data created when this function is executed (could be ss2 or update message)
syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver, receiver.tc)
if (encoding.length(encoder) > 0) {
// send reply message
sender._receive(encoding.toUint8Array(encoder), receiver)
}
return true
}
return false
}
/**
* @return {boolean} True iff this function actually flushed something
*/
flushAllMessages () {
let didSomething = false
while (this.flushRandomMessage()) {
didSomething = true
}
return didSomething
}
reconnectAll () {
this.allConns.forEach(conn => conn.connect())
}
disconnectAll () {
this.allConns.forEach(conn => conn.disconnect())
}
syncAll () {
this.reconnectAll()
this.flushAllMessages()
}
/**
* @return {boolean} Whether it was possible to disconnect a randon connection.
*/
disconnectRandom () {
if (this.onlineConns.size === 0) {
return false
}
prng.oneOf(this.prng, Array.from(this.onlineConns)).disconnect()
return true
}
/**
* @return {boolean} Whether it was possible to reconnect a random connection.
*/
reconnectRandom () {
/**
* @type {Array<TestYInstance>}
*/
const reconnectable = []
this.allConns.forEach(conn => {
if (!this.onlineConns.has(conn)) {
reconnectable.push(conn)
}
})
if (reconnectable.length === 0) {
return false
}
prng.oneOf(this.prng, reconnectable).connect()
return true
}
}
/**
* @template T
* @param {t.TestCase} tc
* @param {{users?:number}} conf
* @param {InitTestObjectCallback<T>} [initTestObject]
* @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) => {
/**
* @type {Object<string,any>}
*/
const result = {
users: []
}
const gen = tc.prng
// choose an encoding approach at random
if (prng.bool(gen)) {
useV2Encoding()
} else {
useV1Encoding()
}
const testConnector = new TestConnector(gen)
result.testConnector = testConnector
for (let i = 0; i < users; i++) {
const y = testConnector.createY(i)
y.clientID = i
result.users.push(y)
result['array' + i] = y.getArray('array')
result['map' + i] = y.getMap('map')
result['xml' + i] = y.get('xml', Y.XmlElement)
result['text' + i] = y.getText('text')
}
testConnector.syncAll()
result.testObjects = result.users.map(initTestObject || (() => null))
useV1Encoding()
return /** @type {any} */ (result)
}
/**
* 1. reconnect and flush all
* 2. user 0 gc
* 3. get type content
* 4. disconnect & reconnect all (so gc is propagated)
* 5. compare os, ds, ss
*
* @param {Array<TestYInstance>} users
*/
export const compare = users => {
users.forEach(u => u.connect())
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 => {
const ydoc = new Y.Doc()
enc.applyUpdate(ydoc, enc.mergeUpdates(user.updates))
return ydoc
})
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.XmlElement).toString())
const userTextValues = users.map(u => u.getText('text').toDelta())
for (const u of users) {
t.assert(u.store.pendingDs === null)
t.assert(u.store.pendingStructs === null)
}
// Test Array iterator
t.compare(users[0].getArray('array').toArray(), Array.from(users[0].getArray('array')))
// Test Map iterator
const ymapkeys = Array.from(users[0].getMap('map').keys())
t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length)
ymapkeys.forEach(key => t.assert(object.hasProperty(userMapValues[0], key)))
/**
* @type {Object<string,any>}
*/
const mapRes = {}
for (const [k, v] of users[0].getMap('map')) {
mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v
}
t.compare(userMapValues[0], mapRes)
// Compare all users
for (let i = 0; i < users.length - 1; i++) {
t.compare(userArrayValues[i].length, users[i].getArray('array').length)
t.compare(userArrayValues[i], userArrayValues[i + 1])
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], '', (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)
}
users.map(u => u.destroy())
}
/**
* @param {Y.Item?} a
* @param {Y.Item?} b
* @return {boolean}
*/
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
/**
* @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)
for (const [client, structs1] of ss1.clients) {
const structs2 = /** @type {Array<Y.AbstractStruct>} */ (ss2.clients.get(client))
t.assert(structs2 !== undefined && structs1.length === structs2.length)
for (let i = 0; i < structs1.length; i++) {
const s1 = structs1[i]
const s2 = structs2[i]
// checks for abstract struct
if (
s1.constructor !== s2.constructor ||
!Y.compareIDs(s1.id, s2.id) ||
s1.deleted !== s2.deleted ||
// @ts-ignore
s1.length !== s2.length
) {
t.fail('Structs dont match')
}
if (s1 instanceof Y.Item) {
if (
!(s2 instanceof Y.Item) ||
!((s1.left === null && s2.left === null) || (s1.left !== null && s2.left !== null && Y.compareIDs(s1.left.lastId, s2.left.lastId))) ||
!compareItemIDs(s1.right, s2.right) ||
!Y.compareIDs(s1.origin, s2.origin) ||
!Y.compareIDs(s1.rightOrigin, s2.rightOrigin) ||
s1.parentSub !== s2.parentSub
) {
return t.fail('Items dont match')
}
// make sure that items are connected correctly
t.assert(s1.left === null || s1.left.right === s1)
t.assert(s1.right === null || s1.right.left === s1)
t.assert(s2.left === null || s2.left.right === s2)
t.assert(s2.right === null || s2.right.left === s2)
}
}
}
}
/**
* @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<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]
const di2 = deleteItems2[i]
if (di1.clock !== di2.clock || di1.len !== di2.len) {
t.fail('DeleteSets dont match')
}
}
})
}
/**
* @template T
* @callback InitTestObjectCallback
* @param {TestYInstance} y
* @return {T}
*/
/**
* @template T
* @param {t.TestCase} tc
* @param {Array<function(Y.Doc,prng.PRNG,T):void>} mods
* @param {number} iterations
* @param {InitTestObjectCallback<T>} [initTestObject]
*/
export const applyRandomTests = (tc, mods, iterations, initTestObject) => {
const gen = tc.prng
const result = init(tc, { users: 5 }, initTestObject)
const { testConnector, users } = result
for (let i = 0; i < iterations; i++) {
if (prng.int32(gen, 0, 100) <= 2) {
// 2% chance to disconnect/reconnect a random user
if (prng.bool(gen)) {
testConnector.disconnectRandom()
} else {
testConnector.reconnectRandom()
}
} else if (prng.int32(gen, 0, 100) <= 1) {
// 1% chance to flush all
testConnector.flushAllMessages()
} else if (prng.int32(gen, 0, 100) <= 50) {
// 50% chance to flush a random message
testConnector.flushRandomMessage()
}
const user = prng.int32(gen, 0, users.length - 1)
const test = prng.oneOf(gen, mods)
test(users[user], gen, result.testObjects[user])
}
compare(users)
return result
}

646
tests/undo-redo.tests.js Normal file
View File

@@ -0,0 +1,646 @@
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
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 Y.UndoManager(text0)
// items that are added & deleted in the same transaction won't be undo
text0.insert(0, 'test')
text0.delete(0, 4)
undoManager.undo()
t.assert(text0.toString() === '')
// follow redone items
text0.insert(0, 'a')
undoManager.stopCapturing()
text0.delete(0, 1)
undoManager.stopCapturing()
undoManager.undo()
t.assert(text0.toString() === 'a')
undoManager.undo()
t.assert(text0.toString() === '')
text0.insert(0, 'abc')
text1.insert(0, 'xyz')
testConnector.syncAll()
undoManager.undo()
t.assert(text0.toString() === 'xyz')
undoManager.redo()
t.assert(text0.toString() === 'abcxyz')
testConnector.syncAll()
text1.delete(0, 1)
testConnector.syncAll()
undoManager.undo()
t.assert(text0.toString() === 'xyz')
undoManager.redo()
t.assert(text0.toString() === 'bcxyz')
// test marks
text0.format(1, 3, { bold: true })
t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }])
undoManager.undo()
t.compare(text0.toDelta(), [{ insert: 'bcxyz' }])
undoManager.redo()
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
*/
export const testDoubleUndo = tc => {
const doc = new Y.Doc()
const text = doc.getText()
text.insert(0, '1221')
const manager = new Y.UndoManager(text)
text.insert(2, '3')
text.insert(3, '3')
manager.undo()
manager.undo()
text.insert(2, '3')
t.compareStrings(text.toString(), '12321')
}
/**
* @param {t.TestCase} tc
*/
export const testUndoMap = tc => {
const { testConnector, map0, map1 } = init(tc, { users: 2 })
map0.set('a', 0)
const undoManager = new Y.UndoManager(map0)
map0.set('a', 1)
undoManager.undo()
t.assert(map0.get('a') === 0)
undoManager.redo()
t.assert(map0.get('a') === 1)
// testing sub-types and if it can restore a whole type
const subType = new Y.Map()
map0.set('a', subType)
subType.set('x', 42)
t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
undoManager.undo()
t.assert(map0.get('a') === 1)
undoManager.redo()
t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
testConnector.syncAll()
// if content is overwritten by another user, undo operations should be skipped
map1.set('a', 44)
testConnector.syncAll()
undoManager.undo()
t.assert(map0.get('a') === 44)
undoManager.redo()
t.assert(map0.get('a') === 44)
// test setting value multiple times
map0.set('b', 'initial')
undoManager.stopCapturing()
map0.set('b', 'val1')
map0.set('b', 'val2')
undoManager.stopCapturing()
undoManager.undo()
t.assert(map0.get('b') === 'initial')
}
/**
* @param {t.TestCase} tc
*/
export const testUndoArray = tc => {
const { testConnector, array0, array1 } = init(tc, { users: 3 })
const undoManager = new Y.UndoManager(array0)
array0.insert(0, [1, 2, 3])
array1.insert(0, [4, 5, 6])
testConnector.syncAll()
t.compare(array0.toArray(), [1, 2, 3, 4, 5, 6])
undoManager.undo()
t.compare(array0.toArray(), [4, 5, 6])
undoManager.redo()
t.compare(array0.toArray(), [1, 2, 3, 4, 5, 6])
testConnector.syncAll()
array1.delete(0, 1) // user1 deletes [1]
testConnector.syncAll()
undoManager.undo()
t.compare(array0.toArray(), [4, 5, 6])
undoManager.redo()
t.compare(array0.toArray(), [2, 3, 4, 5, 6])
array0.delete(0, 5)
// test nested structure
const ymap = new Y.Map()
array0.insert(0, [ymap])
t.compare(array0.toJSON(), [{}])
undoManager.stopCapturing()
ymap.set('a', 1)
t.compare(array0.toJSON(), [{ a: 1 }])
undoManager.undo()
t.compare(array0.toJSON(), [{}])
undoManager.undo()
t.compare(array0.toJSON(), [2, 3, 4, 5, 6])
undoManager.redo()
t.compare(array0.toJSON(), [{}])
undoManager.redo()
t.compare(array0.toJSON(), [{ a: 1 }])
testConnector.syncAll()
array1.get(0).set('b', 2)
testConnector.syncAll()
t.compare(array0.toJSON(), [{ a: 1, b: 2 }])
undoManager.undo()
t.compare(array0.toJSON(), [{ b: 2 }])
undoManager.undo()
t.compare(array0.toJSON(), [2, 3, 4, 5, 6])
undoManager.redo()
t.compare(array0.toJSON(), [{ b: 2 }])
undoManager.redo()
t.compare(array0.toJSON(), [{ a: 1, b: 2 }])
}
/**
* @param {t.TestCase} tc
*/
export const testUndoXml = tc => {
const { xml0 } = init(tc, { users: 3 })
const undoManager = new Y.UndoManager(xml0)
const child = new Y.XmlElement('p')
xml0.insert(0, [child])
const textchild = new Y.XmlText('content')
child.insert(0, [textchild])
t.assert(xml0.toString() === '<undefined><p>content</p></undefined>')
// format textchild and revert that change
undoManager.stopCapturing()
textchild.format(3, 4, { bold: {} })
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
undoManager.undo()
t.assert(xml0.toString() === '<undefined><p>content</p></undefined>')
undoManager.redo()
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
xml0.delete(0, 1)
t.assert(xml0.toString() === '<undefined></undefined>')
undoManager.undo()
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
}
/**
* @param {t.TestCase} tc
*/
export const testUndoEvents = tc => {
const { text0 } = init(tc, { users: 3 })
const undoManager = new Y.UndoManager(text0)
let counter = 0
let receivedMetadata = -1
undoManager.on('stack-item-added', /** @param {any} event */ event => {
t.assert(event.type != null)
t.assert(event.changedParentTypes != null && event.changedParentTypes.has(text0))
event.stackItem.meta.set('test', counter++)
})
undoManager.on('stack-item-popped', /** @param {any} event */ event => {
t.assert(event.type != null)
t.assert(event.changedParentTypes != null && event.changedParentTypes.has(text0))
receivedMetadata = event.stackItem.meta.get('test')
})
text0.insert(0, 'abc')
undoManager.undo()
t.assert(receivedMetadata === 0)
undoManager.redo()
t.assert(receivedMetadata === 1)
}
/**
* @param {t.TestCase} tc
*/
export const testTrackClass = tc => {
const { users, text0 } = init(tc, { users: 3 })
// only track origins that are numbers
const undoManager = new Y.UndoManager(text0, { trackedOrigins: new Set([Number]) })
users[0].transact(() => {
text0.insert(0, 'abc')
}, 42)
t.assert(text0.toString() === 'abc')
undoManager.undo()
t.assert(text0.toString() === '')
}
/**
* @param {t.TestCase} tc
*/
export const testTypeScope = tc => {
const { array0 } = init(tc, { users: 3 })
// only track origins that are numbers
const text0 = new Y.Text()
const text1 = new Y.Text()
array0.insert(0, [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)
t.assert(text1.toString() === 'abc')
undoManager.undo()
t.assert(text1.toString() === 'abc')
undoManagerBoth.undo()
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 {Y.Array<any>}
*/
const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0)
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()
array0.insert(0, [map0, map1])
undoManager.undo()
t.assert(array0.length === 1)
array0.get(0)
t.assert(Array.from(array0.get(0).keys()).length === 1)
}
/**
* This issue has been reported in https://discuss.yjs.dev/t/undomanager-with-external-updates/454/6
* @param {t.TestCase} tc
*/
export const testUndoUntilChangePerformed = tc => {
const doc = new Y.Doc()
const doc2 = new Y.Doc()
doc.on('update', update => Y.applyUpdate(doc2, update))
doc2.on('update', update => Y.applyUpdate(doc, update))
const yArray = doc.getArray('array')
const yArray2 = doc2.getArray('array')
const yMap = new Y.Map()
yMap.set('hello', 'world')
yArray.push([yMap])
const yMap2 = new Y.Map()
yMap2.set('key', 'value')
yArray.push([yMap2])
const undoManager = new Y.UndoManager([yArray], { trackedOrigins: new Set([doc.clientID]) })
const undoManager2 = new Y.UndoManager([doc2.get('array')], { trackedOrigins: new Set([doc2.clientID]) })
Y.transact(doc, () => yMap2.set('key', 'value modified'), doc.clientID)
undoManager.stopCapturing()
Y.transact(doc, () => yMap.set('hello', 'world modified'), doc.clientID)
Y.transact(doc2, () => yArray2.delete(0), doc2.clientID)
undoManager2.undo()
undoManager.undo()
t.compareStrings(yMap2.get('key'), 'value')
}
/**
* This issue has been reported in https://github.com/yjs/yjs/issues/317
* @param {t.TestCase} tc
*/
export const testUndoNestedUndoIssue = tc => {
const doc = new Y.Doc({ gc: false })
const design = doc.getMap()
const undoManager = new Y.UndoManager(design, { captureTimeout: 0 })
/**
* @type {Y.Map<any>}
*/
const text = new Y.Map()
const blocks1 = new Y.Array()
const blocks1block = new Y.Map()
doc.transact(() => {
blocks1block.set('text', 'Type Something')
blocks1.push([blocks1block])
text.set('blocks', blocks1block)
design.set('text', text)
})
const blocks2 = new Y.Array()
const blocks2block = new Y.Map()
doc.transact(() => {
blocks2block.set('text', 'Something')
blocks2.push([blocks2block])
text.set('blocks', blocks2block)
})
const blocks3 = new Y.Array()
const blocks3block = new Y.Map()
doc.transact(() => {
blocks3block.set('text', 'Something Else')
blocks3.push([blocks3block])
text.set('blocks', blocks3block)
})
t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } })
undoManager.undo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } })
undoManager.undo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } })
undoManager.undo()
t.compare(design.toJSON(), { })
undoManager.redo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } })
undoManager.redo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } })
undoManager.redo()
t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } })
}
/**
* This issue has been reported in https://github.com/yjs/yjs/issues/355
*
* @param {t.TestCase} tc
*/
export const testConsecutiveRedoBug = tc => {
const doc = new Y.Doc()
const yRoot = doc.getMap()
const undoMgr = new Y.UndoManager(yRoot)
let yPoint = new Y.Map()
yPoint.set('x', 0)
yPoint.set('y', 0)
yRoot.set('a', yPoint)
undoMgr.stopCapturing()
yPoint.set('x', 100)
yPoint.set('y', 100)
undoMgr.stopCapturing()
yPoint.set('x', 200)
yPoint.set('y', 200)
undoMgr.stopCapturing()
yPoint.set('x', 300)
yPoint.set('y', 300)
undoMgr.stopCapturing()
t.compare(yPoint.toJSON(), { x: 300, y: 300 })
undoMgr.undo() // x=200, y=200
t.compare(yPoint.toJSON(), { x: 200, y: 200 })
undoMgr.undo() // x=100, y=100
t.compare(yPoint.toJSON(), { x: 100, y: 100 })
undoMgr.undo() // x=0, y=0
t.compare(yPoint.toJSON(), { x: 0, y: 0 })
undoMgr.undo() // nil
t.compare(yRoot.get('a'), undefined)
undoMgr.redo() // x=0, y=0
yPoint = yRoot.get('a')
t.compare(yPoint.toJSON(), { x: 0, y: 0 })
undoMgr.redo() // x=100, y=100
t.compare(yPoint.toJSON(), { x: 100, y: 100 })
undoMgr.redo() // x=200, y=200
t.compare(yPoint.toJSON(), { x: 200, y: 200 })
undoMgr.redo() // expected x=300, y=300, actually nil
t.compare(yPoint.toJSON(), { x: 300, y: 300 })
}
/**
* This issue has been reported in https://github.com/yjs/yjs/issues/304
*
* @param {t.TestCase} tc
*/
export const testUndoXmlBug = tc => {
const origin = 'origin'
const doc = new Y.Doc()
const fragment = doc.getXmlFragment('t')
const undoManager = new Y.UndoManager(fragment, {
captureTimeout: 0,
trackedOrigins: new Set([origin])
})
// create element
doc.transact(() => {
const e = new Y.XmlElement('test-node')
e.setAttribute('a', '100')
e.setAttribute('b', '0')
fragment.insert(fragment.length, [e])
}, origin)
// change one attribute
doc.transact(() => {
const e = fragment.get(0)
e.setAttribute('a', '200')
}, origin)
// change both attributes
doc.transact(() => {
const e = fragment.get(0)
e.setAttribute('a', '180')
e.setAttribute('b', '50')
}, origin)
undoManager.undo()
undoManager.undo()
undoManager.undo()
undoManager.redo()
undoManager.redo()
undoManager.redo()
t.compare(fragment.toString(), '<test-node a="180" b="50"></test-node>')
}
/**
* This issue has been reported in https://github.com/yjs/yjs/issues/343
*
* @param {t.TestCase} tc
*/
export const testUndoBlockBug = tc => {
const doc = new Y.Doc({ gc: false })
const design = doc.getMap()
const undoManager = new Y.UndoManager(design, { captureTimeout: 0 })
const text = new Y.Map()
const blocks1 = new Y.Array()
const blocks1block = new Y.Map()
doc.transact(() => {
blocks1block.set('text', '1')
blocks1.push([blocks1block])
text.set('blocks', blocks1block)
design.set('text', text)
})
const blocks2 = new Y.Array()
const blocks2block = new Y.Map()
doc.transact(() => {
blocks2block.set('text', '2')
blocks2.push([blocks2block])
text.set('blocks', blocks2block)
})
const blocks3 = new Y.Array()
const blocks3block = new Y.Map()
doc.transact(() => {
blocks3block.set('text', '3')
blocks3.push([blocks3block])
text.set('blocks', blocks3block)
})
const blocks4 = new Y.Array()
const blocks4block = new Y.Map()
doc.transact(() => {
blocks4block.set('text', '4')
blocks4.push([blocks4block])
text.set('blocks', blocks4block)
})
// {"text":{"blocks":{"text":"4"}}}
undoManager.undo() // {"text":{"blocks":{"3"}}}
undoManager.undo() // {"text":{"blocks":{"text":"2"}}}
undoManager.undo() // {"text":{"blocks":{"text":"1"}}}
undoManager.undo() // {}
undoManager.redo() // {"text":{"blocks":{"text":"1"}}}
undoManager.redo() // {"text":{"blocks":{"text":"2"}}}
undoManager.redo() // {"text":{"blocks":{"text":"3"}}}
undoManager.redo() // {"text":{}}
t.compare(design.toJSON(), { text: { blocks: { text: '4' } } })
}
/**
* 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>')
}

306
tests/updates.tests.js Normal file
View File

@@ -0,0 +1,306 @@
import * as t from 'lib0/testing'
import { init, compare } from './testHelper.js' // eslint-disable-line
import * as Y from '../src/index.js'
import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js'
import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding'
/**
* @typedef {Object} Enc
* @property {function(Array<Uint8Array>):Uint8Array} Enc.mergeUpdates
* @property {function(Y.Doc):Uint8Array} Enc.encodeStateAsUpdate
* @property {function(Y.Doc, Uint8Array):void} Enc.applyUpdate
* @property {function(Uint8Array):void} Enc.logUpdate
* @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta
* @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector
* @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
* @property {string} Enc.updateEventName
* @property {string} Enc.description
* @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
*/
/**
* @type {Enc}
*/
const encV1 = {
mergeUpdates: Y.mergeUpdates,
encodeStateAsUpdate: Y.encodeStateAsUpdate,
applyUpdate: Y.applyUpdate,
logUpdate: Y.logUpdate,
parseUpdateMeta: Y.parseUpdateMeta,
encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdate,
encodeStateVector: Y.encodeStateVector,
updateEventName: 'update',
description: 'V1',
diffUpdate: Y.diffUpdate
}
/**
* @type {Enc}
*/
const encV2 = {
mergeUpdates: Y.mergeUpdatesV2,
encodeStateAsUpdate: Y.encodeStateAsUpdateV2,
applyUpdate: Y.applyUpdateV2,
logUpdate: Y.logUpdateV2,
parseUpdateMeta: Y.parseUpdateMetaV2,
encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdateV2,
encodeStateVector: Y.encodeStateVector,
updateEventName: 'updateV2',
description: 'V2',
diffUpdate: Y.diffUpdateV2
}
/**
* @type {Enc}
*/
const encDoc = {
mergeUpdates: (updates) => {
const ydoc = new Y.Doc({ gc: false })
updates.forEach(update => {
Y.applyUpdateV2(ydoc, update)
})
return Y.encodeStateAsUpdateV2(ydoc)
},
encodeStateAsUpdate: Y.encodeStateAsUpdateV2,
applyUpdate: Y.applyUpdateV2,
logUpdate: Y.logUpdateV2,
parseUpdateMeta: Y.parseUpdateMetaV2,
encodeStateVectorFromUpdate: Y.encodeStateVectorFromUpdateV2,
encodeStateVector: Y.encodeStateVector,
updateEventName: 'updateV2',
description: 'Merge via Y.Doc',
/**
* @param {Uint8Array} update
* @param {Uint8Array} sv
*/
diffUpdate: (update, sv) => {
const ydoc = new Y.Doc({ gc: false })
Y.applyUpdateV2(ydoc, update)
return Y.encodeStateAsUpdateV2(ydoc, sv)
}
}
const encoders = [encV1, encV2, encDoc]
/**
* @param {Array<Y.Doc>} users
* @param {Enc} enc
*/
const fromUpdates = (users, enc) => {
const updates = users.map(user =>
enc.encodeStateAsUpdate(user)
)
const ydoc = new Y.Doc()
enc.applyUpdate(ydoc, enc.mergeUpdates(updates))
return ydoc
}
/**
* @param {t.TestCase} tc
*/
export const testMergeUpdates = tc => {
const { users, array0, array1 } = init(tc, { users: 3 })
array0.insert(0, [1])
array1.insert(0, [2])
compare(users)
encoders.forEach(enc => {
const merged = fromUpdates(users, enc)
t.compareArrays(array0.toArray(), merged.getArray('array').toArray())
})
}
/**
* @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
* @param {Enc} enc
* @param {boolean} hasDeletes
*/
const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
const cases = []
// Case 1: Simple case, simply merge everything
cases.push(enc.mergeUpdates(updates))
// Case 2: Overlapping updates
cases.push(enc.mergeUpdates([
enc.mergeUpdates(updates.slice(2)),
enc.mergeUpdates(updates.slice(0, 2))
]))
// Case 3: Overlapping updates
cases.push(enc.mergeUpdates([
enc.mergeUpdates(updates.slice(2)),
enc.mergeUpdates(updates.slice(1, 3)),
updates[0]
]))
// Case 4: Separated updates (containing skips)
cases.push(enc.mergeUpdates([
enc.mergeUpdates([updates[0], updates[2]]),
enc.mergeUpdates([updates[1], updates[3]]),
enc.mergeUpdates(updates.slice(4))
]))
// Case 5: overlapping with many duplicates
cases.push(enc.mergeUpdates(cases))
// const targetState = enc.encodeStateAsUpdate(ydoc)
// t.info('Target State: ')
// enc.logUpdate(targetState)
cases.forEach((mergedUpdates, i) => {
// t.info('State Case $' + i + ':')
// enc.logUpdate(updates)
const merged = new Y.Doc({ gc: false })
enc.applyUpdate(merged, mergedUpdates)
t.compareArrays(merged.getArray().toArray(), ydoc.getArray().toArray())
t.compare(enc.encodeStateVector(merged), enc.encodeStateVectorFromUpdate(mergedUpdates))
if (enc.updateEventName !== 'update') { // @todo should this also work on legacy updates?
for (let j = 1; j < updates.length; j++) {
const partMerged = enc.mergeUpdates(updates.slice(j))
const partMeta = enc.parseUpdateMeta(partMerged)
const targetSV = Y.encodeStateVectorFromUpdateV2(Y.mergeUpdatesV2(updates.slice(0, j)))
const diffed = enc.diffUpdate(mergedUpdates, targetSV)
const diffedMeta = enc.parseUpdateMeta(diffed)
t.compare(partMeta, diffedMeta)
{
// We can'd do the following
// - t.compare(diffed, mergedDeletes)
// because diffed contains the set of all deletes.
// So we add all deletes from `diffed` to `partDeletes` and compare then
const decoder = decoding.createDecoder(diffed)
const updateDecoder = new UpdateDecoderV2(decoder)
readClientsStructRefs(updateDecoder, new Y.Doc())
const ds = readDeleteSet(updateDecoder)
const updateEncoder = new UpdateEncoderV2()
encoding.writeVarUint(updateEncoder.restEncoder, 0) // 0 structs
writeDeleteSet(updateEncoder, ds)
const deletesUpdate = updateEncoder.toUint8Array()
const mergedDeletes = Y.mergeUpdatesV2([deletesUpdate, partMerged])
if (!hasDeletes || enc !== encDoc) {
// deletes will almost definitely lead to different encoders because of the mergeStruct feature that is present in encDoc
t.compare(diffed, mergedDeletes)
}
}
}
}
const meta = enc.parseUpdateMeta(mergedUpdates)
meta.from.forEach((clock, client) => t.assert(clock === 0))
meta.to.forEach((clock, client) => {
const structs = /** @type {Array<Y.Item>} */ (merged.store.clients.get(client))
const lastStruct = structs[structs.length - 1]
t.assert(lastStruct.id.clock + lastStruct.length === clock)
})
})
}
/**
* @param {t.TestCase} tc
*/
export const testMergeUpdates1 = tc => {
encoders.forEach((enc, i) => {
t.info(`Using encoder: ${enc.description}`)
const ydoc = new Y.Doc({ gc: false })
const updates = /** @type {Array<Uint8Array>} */ ([])
ydoc.on(enc.updateEventName, update => { updates.push(update) })
const array = ydoc.getArray()
array.insert(0, [1])
array.insert(0, [2])
array.insert(0, [3])
array.insert(0, [4])
checkUpdateCases(ydoc, updates, enc, false)
})
}
/**
* @param {t.TestCase} tc
*/
export const testMergeUpdates2 = tc => {
encoders.forEach((enc, i) => {
t.info(`Using encoder: ${enc.description}`)
const ydoc = new Y.Doc({ gc: false })
const updates = /** @type {Array<Uint8Array>} */ ([])
ydoc.on(enc.updateEventName, update => { updates.push(update) })
const array = ydoc.getArray()
array.insert(0, [1, 2])
array.delete(1, 1)
array.insert(0, [3, 4])
array.delete(1, 2)
checkUpdateCases(ydoc, updates, enc, true)
})
}
/**
* @param {t.TestCase} tc
*/
export const testMergePendingUpdates = tc => {
const yDoc = new Y.Doc()
/**
* @type {Array<Uint8Array>}
*/
const serverUpdates = []
yDoc.on('update', (update, origin, c) => {
serverUpdates.splice(serverUpdates.length, 0, update)
})
const yText = yDoc.getText('textBlock')
yText.applyDelta([{ insert: 'r' }])
yText.applyDelta([{ insert: 'o' }])
yText.applyDelta([{ insert: 'n' }])
yText.applyDelta([{ insert: 'e' }])
yText.applyDelta([{ insert: 'n' }])
const yDoc1 = new Y.Doc()
Y.applyUpdate(yDoc1, serverUpdates[0])
const update1 = Y.encodeStateAsUpdate(yDoc1)
const yDoc2 = new Y.Doc()
Y.applyUpdate(yDoc2, update1)
Y.applyUpdate(yDoc2, serverUpdates[1])
const update2 = Y.encodeStateAsUpdate(yDoc2)
const yDoc3 = new Y.Doc()
Y.applyUpdate(yDoc3, update2)
Y.applyUpdate(yDoc3, serverUpdates[3])
const update3 = Y.encodeStateAsUpdate(yDoc3)
const yDoc4 = new Y.Doc()
Y.applyUpdate(yDoc4, update3)
Y.applyUpdate(yDoc4, serverUpdates[2])
const update4 = Y.encodeStateAsUpdate(yDoc4)
const yDoc5 = new Y.Doc()
Y.applyUpdate(yDoc5, update4)
Y.applyUpdate(yDoc5, serverUpdates[4])
// @ts-ignore
const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
const yText5 = yDoc5.getText('textBlock')
t.compareStrings(yText5.toString(), 'nenor')
}

Some files were not shown because too many files have changed in this diff Show More