Compare commits

...

1321 Commits

Author SHA1 Message Date
Kevin Jahns
b56debef00 14.0.0-1 2023-06-28 15:27:24 +02:00
Kevin Jahns
5b16071380 fix #481 - calculate path correctly when parents are moved 2023-06-28 15:25:59 +02:00
Kevin Jahns
7ced59c847 fix #485 - forEach index 2023-06-27 12:21:40 +02:00
Kevin Jahns
3c98d97369 remove toDom methods 2022-08-23 16:57:29 +02:00
Kevin Jahns
56d747faea 14.0.0-0 2022-08-18 18:17:12 +02:00
Kevin Jahns
a3b97d941b rename to ListCursor 2022-07-19 14:49:46 +02:00
Kevin Jahns
efcfe4b483 extend move info field by three bits for future usage 2022-07-19 11:07:56 +02:00
Kevin Jahns
4de3c004a8 remove todo comment 2022-07-11 18:42:29 +02:00
Kevin Jahns
100e436e2c cleanup 2022-07-11 18:36:42 +02:00
Kevin Jahns
3b31764b6e Fixed all tests - full support for collapsed move deletions 2022-07-09 16:57:56 +02:00
Kevin Jahns
19723670c4 fix several issues of supporting deleted move ops 2022-07-08 21:36:36 +02:00
Kevin Jahns
0ce40596d1 remove old move-ranges if collapsed 2022-07-06 18:57:12 +02:00
Kevin Jahns
4078e115c1 optimize encoding of move ops 2022-07-04 16:44:01 +02:00
Kevin Jahns
ab5061cd47 normalize ranges 2022-07-04 16:05:44 +02:00
Kevin Jahns
44499cb9fe fix move-range tests of moved-moved items 2022-06-16 20:23:34 +02:00
Kevin Jahns
b63d22e7db lint 2022-05-18 10:50:36 +02:00
Kevin Jahns
bf05061cc7 fix all tests for range-moves of length 1 2022-05-18 10:44:12 +02:00
Kevin Jahns
7e9319f82e filter empty ranges when calculating minMoveRanges 2022-05-15 21:04:02 +02:00
Kevin Jahns
2e9a7df603 use uint8 type encoding for relative-positions 2022-05-10 15:31:01 +02:00
Kevin Jahns
1f99e8203a fix a bunch of issues with range-move approach 2022-05-07 16:14:18 +02:00
Kevin Jahns
69b7f4bfb9 implement solid move-range approach - tests not running 2022-05-05 13:03:59 +02:00
Kevin Jahns
b2b7b8c280 tmp commit 2022-04-11 15:54:10 +02:00
Kevin Jahns
a0c9235a36 fix test-move logic 2022-04-04 16:35:50 +02:00
Kevin Jahns
e8ecc8f74b fix circlic move-loops 2022-04-04 15:35:23 +02:00
Kevin Jahns
b32f88cd40 fix all remaining bugs for single-item moves (mainly event bugs) 2022-04-04 13:10:43 +02:00
Kevin Jahns
51c095ec52 fix search marker issues - splitting of items with stored rel search markers 2022-03-31 08:35:24 +02:00
Kevin Jahns
285dc79a6b fix edge case when moving backwards from move operation 2022-03-30 10:07:55 +02:00
Kevin Jahns
f65d1b8475 fix ListIterator backwards iteration within moved ranges 2022-03-29 16:55:22 +02:00
Kevin Jahns
c4b28aceec fix prevMove bug 2022-03-26 11:03:28 +01:00
Kevin Jahns
cc93f346ce 13.6.0-2 2021-12-08 16:12:11 +01:00
Kevin Jahns
d3dcd24ef4 fix various tests 2021-12-08 16:10:49 +01:00
Kevin Jahns
6fc4fbd466 13.6.0-1 2021-12-07 13:54:22 +01:00
Kevin Jahns
53e2c83f86 add meta property to AbstractType 2021-12-07 13:53:28 +01:00
Kevin Jahns
24bca2af43 13.6.0-0 2021-12-07 12:42:32 +01:00
Kevin Jahns
b75682022e skip tests in preversion (should be handled by np) 2021-12-07 12:41:40 +01:00
Kevin Jahns
3d31ba8759 adding more sanity checkss to yarray.tests 2021-12-07 12:37:03 +01:00
Kevin Jahns
bd47efe0ee fix all tests 2021-12-06 22:21:55 +01:00
Kevin Jahns
f5781f8366 update searchmarkers after insert correctly 2021-12-06 22:07:46 +01:00
Kevin Jahns
6230abb78c make sure that markers are correct without reinit 2021-12-06 21:22:18 +01:00
Kevin Jahns
4356d70ed0 reinit search marker after transaction 2021-12-06 21:00:20 +01:00
Kevin Jahns
0948229422 handle nested moves 2021-12-06 15:07:43 +01:00
Kevin Jahns
fc5e36158f made simple one-time move work 2021-12-06 15:07:43 +01:00
Kevin Jahns
d314c3e1a6 fixed ListIterator.reachedEnd edge case 2021-12-06 15:07:43 +01:00
Kevin Jahns
2a33507c00 fixed pos.rel cases 2021-12-06 15:07:43 +01:00
Kevin Jahns
40c3be1732 fix backwards edge case 2021-12-06 15:07:43 +01:00
Kevin Jahns
4a8ebc31f7 fix listiterator.map returning undefined as the last element 2021-12-06 15:07:43 +01:00
Kevin Jahns
6df152c4ec proper iteration through arrays (for mappings, toJSON, ..) 2021-12-06 15:07:43 +01:00
Kevin Jahns
fc38f3b848 formatting bug 2021-12-06 15:07:43 +01:00
Kevin Jahns
a057bf1cf0 fix disconnect issue 2021-12-06 15:07:43 +01:00
Kevin Jahns
8b82c573c4 fix basic inserd & delete bug 2021-12-06 15:07:43 +01:00
Kevin Jahns
a77221ffd2 fix toJSON value 2021-12-06 15:07:42 +01:00
Kevin Jahns
b9ccbb2dc7 created new abstraction for search marker 2021-12-06 15:06:17 +01:00
Kevin Jahns
a723c32557 use new ListPosition abstraction in Y.Array .slice and .get 2021-12-06 15:06:17 +01:00
Kevin Jahns
56ab251e79 make moved a separate prop on item 2021-12-06 15:06:17 +01:00
Kevin Jahns
53a7b286b8 Move content and list iteration abstraction 2021-12-06 15:06:13 +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
114 changed files with 24179 additions and 116676 deletions

View File

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

7
.circleci/config.yml Normal file
View File

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

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
}

View File

@@ -1,11 +0,0 @@
language: node_js
before_install:
- "npm install -g bower coffee-script"
- "bower install"
node_js:
- "0.12"
- "0.11"
- "0.10"
branches:
only:
- master

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 the 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 know state of each user (a set of
tubles `(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`. I 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

1082
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +0,0 @@
{
"name": "yjs",
"version": "0.5.2",
"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"
],
"devDependencies": {
"y-test" : "y-test#~0.4.0"
}
}

View File

@@ -1,11 +0,0 @@
# Directories
### build/browser
You find the browserified (not minified) version of yjs here. This is nice for debugging, since it also includes sourcemaps. For production, however, you should use the version that you find in the main directory.
### build/node
Yjs for nodejs is located here. You can only use the submodules, or require 'y' in your node project. Also works with browserify.
### build/test
Start build/test/index.html' in your browser, to perform testing Yjs.

View File

@@ -1,7 +0,0 @@
<polymer-element name="y-object" hidden attributes="val connector y">
</polymer-element>
<polymer-element name="y-property" hidden attributes="val name y">
</polymer-element>
<script src="./y-object.js"></script>

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,71 +0,0 @@
var ConnectorClass, adaptConnector;
ConnectorClass = require("./ConnectorClass");
adaptConnector = function(connector, engine, HB, execution_listener) {
var applyHB, encode_state_vector, f, getHB, getStateVector, name, parse_state_vector, send_;
for (name in ConnectorClass) {
f = ConnectorClass[name];
connector[name] = f;
}
connector.setIsBoundToY();
send_ = function(o) {
if ((o.uid.creator === HB.getUserId()) && (typeof o.uid.op_number !== "string") && (HB.getUserId() !== "_temp")) {
return connector.broadcast(o);
}
};
if (connector.invokeSync != null) {
HB.setInvokeSyncHandler(connector.invokeSync);
}
execution_listener.push(send_);
encode_state_vector = function(v) {
var results, value;
results = [];
for (name in v) {
value = v[name];
results.push({
user: name,
state: value
});
}
return results;
};
parse_state_vector = function(v) {
var i, len, s, state_vector;
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, state_vector;
state_vector = parse_state_vector(v);
hb = HB._encode(state_vector);
json = {
hb: hb,
state_vector: encode_state_vector(HB.getOperationCounter())
};
return json;
};
applyHB = function(hb, fromHB) {
return engine.applyOp(hb, fromHB);
};
connector.getStateVector = getStateVector;
connector.getHB = getHB;
connector.applyHB = applyHB;
if (connector.receive_handlers == null) {
connector.receive_handlers = [];
}
return connector.receive_handlers.push(function(sender, op) {
if (op.uid.creator !== HB.getUserId()) {
return engine.applyOp(op);
}
});
};
module.exports = adaptConnector;

View File

@@ -1,415 +0,0 @@
module.exports = {
init: function(options) {
var req;
req = (function(_this) {
return function(name, choices) {
if (options[name] != null) {
if ((choices == null) || choices.some(function(c) {
return c === options[name];
})) {
return _this[name] = options[name];
} else {
throw new Error("You can set the '" + name + "' option to one of the following choices: " + JSON.encode(choices));
}
} else {
throw new Error("You must specify " + name + ", when initializing the Connector!");
}
};
})(this);
req("syncMethod", ["syncAll", "master-slave"]);
req("role", ["master", "slave"]);
req("user_id");
if (typeof this.on_user_id_set === "function") {
this.on_user_id_set(this.user_id);
}
if (options.perform_send_again != null) {
this.perform_send_again = options.perform_send_again;
} else {
this.perform_send_again = true;
}
if (this.role === "master") {
this.syncMethod = "syncAll";
}
this.is_synced = false;
this.connections = {};
if (this.receive_handlers == null) {
this.receive_handlers = [];
}
this.connections = {};
this.current_sync_target = null;
this.sent_hb_to_all_users = false;
return this.is_initialized = true;
},
onUserEvent: function(f) {
if (this.connections_listeners == null) {
this.connections_listeners = [];
}
return this.connections_listeners.push(f);
},
isRoleMaster: function() {
return this.role === "master";
},
isRoleSlave: function() {
return this.role === "slave";
},
findNewSyncTarget: function() {
var c, ref, user;
this.current_sync_target = null;
if (this.syncMethod === "syncAll") {
ref = this.connections;
for (user in ref) {
c = ref[user];
if (!c.is_synced) {
this.performSync(user);
break;
}
}
}
if (this.current_sync_target == null) {
this.setStateSynced();
}
return null;
},
userLeft: function(user) {
var f, i, len, ref, results;
delete this.connections[user];
this.findNewSyncTarget();
if (this.connections_listeners != null) {
ref = this.connections_listeners;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
f = ref[i];
results.push(f({
action: "userLeft",
user: user
}));
}
return results;
}
},
userJoined: function(user, role) {
var base, f, i, len, ref, results;
if (role == null) {
throw new Error("Internal: You must specify the role of the joined user! E.g. userJoined('uid:3939','slave')");
}
if ((base = this.connections)[user] == null) {
base[user] = {};
}
this.connections[user].is_synced = false;
if ((!this.is_synced) || this.syncMethod === "syncAll") {
if (this.syncMethod === "syncAll") {
this.performSync(user);
} else if (role === "master") {
this.performSyncWithMaster(user);
}
}
if (this.connections_listeners != null) {
ref = this.connections_listeners;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
f = ref[i];
results.push(f({
action: "userJoined",
user: user,
role: role
}));
}
return results;
}
},
whenSynced: function(args) {
if (args.constructor === Function) {
args = [args];
}
if (this.is_synced) {
return args[0].apply(this, args.slice(1));
} else {
if (this.compute_when_synced == null) {
this.compute_when_synced = [];
}
return this.compute_when_synced.push(args);
}
},
onReceive: function(f) {
return this.receive_handlers.push(f);
},
/*
* Broadcast a message to all connected peers.
* @param message {Object} The message to broadcast.
#
broadcast: (message)->
throw new Error "You must implement broadcast!"
#
* Send a message to a peer, or set of peers
#
send: (peer_s, message)->
throw new Error "You must implement send!"
*/
performSync: function(user) {
var _hb, hb, i, len, o;
if (this.current_sync_target == null) {
this.current_sync_target = user;
this.send(user, {
sync_step: "getHB",
send_again: "true",
data: this.getStateVector()
});
if (!this.sent_hb_to_all_users) {
this.sent_hb_to_all_users = true;
hb = this.getHB([]).hb;
_hb = [];
for (i = 0, len = hb.length; i < len; i++) {
o = hb[i];
_hb.push(o);
if (_hb.length > 10) {
this.broadcast({
sync_step: "applyHB_",
data: _hb
});
_hb = [];
}
}
return this.broadcast({
sync_step: "applyHB",
data: _hb
});
}
}
},
performSyncWithMaster: function(user) {
var _hb, hb, i, len, o;
this.current_sync_target = user;
this.send(user, {
sync_step: "getHB",
send_again: "true",
data: this.getStateVector()
});
hb = this.getHB([]).hb;
_hb = [];
for (i = 0, len = hb.length; i < len; i++) {
o = hb[i];
_hb.push(o);
if (_hb.length > 10) {
this.broadcast({
sync_step: "applyHB_",
data: _hb
});
_hb = [];
}
}
return this.broadcast({
sync_step: "applyHB",
data: _hb
});
},
setStateSynced: function() {
var args, el, f, i, len, ref;
if (!this.is_synced) {
this.is_synced = true;
if (this.compute_when_synced != null) {
ref = this.compute_when_synced;
for (i = 0, len = ref.length; i < len; i++) {
el = ref[i];
f = el[0];
args = el.slice(1);
f.apply(args);
}
delete this.compute_when_synced;
}
return null;
}
},
whenReceivedStateVector: function(f) {
if (this.when_received_state_vector_listeners == null) {
this.when_received_state_vector_listeners = [];
}
return this.when_received_state_vector_listeners.push(f);
},
receiveMessage: function(sender, res) {
var _hb, data, f, hb, i, j, k, len, len1, len2, o, ref, ref1, results, sendApplyHB, send_again;
if (res.sync_step == null) {
ref = this.receive_handlers;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
f = ref[i];
results.push(f(sender, res));
}
return results;
} else {
if (sender === this.user_id) {
return;
}
if (res.sync_step === "getHB") {
if (this.when_received_state_vector_listeners != null) {
ref1 = this.when_received_state_vector_listeners;
for (j = 0, len1 = ref1.length; j < len1; j++) {
f = ref1[j];
f.call(this, res.data);
}
}
delete this.when_received_state_vector_listeners;
data = this.getHB(res.data);
hb = data.hb;
_hb = [];
if (this.is_synced) {
sendApplyHB = (function(_this) {
return function(m) {
return _this.send(sender, m);
};
})(this);
} else {
sendApplyHB = (function(_this) {
return function(m) {
return _this.broadcast(m);
};
})(this);
}
for (k = 0, len2 = hb.length; k < len2; k++) {
o = hb[k];
_hb.push(o);
if (_hb.length > 10) {
sendApplyHB({
sync_step: "applyHB_",
data: _hb
});
_hb = [];
}
}
sendApplyHB({
sync_step: "applyHB",
data: _hb
});
if ((res.send_again != null) && this.perform_send_again) {
send_again = (function(_this) {
return function(sv) {
return function() {
var l, len3;
hb = _this.getHB(sv).hb;
for (l = 0, len3 = hb.length; l < len3; l++) {
o = hb[l];
_hb.push(o);
if (_hb.length > 10) {
_this.send(sender, {
sync_step: "applyHB_",
data: _hb
});
_hb = [];
}
}
return _this.send(sender, {
sync_step: "applyHB",
data: _hb,
sent_again: "true"
});
};
};
})(this)(data.state_vector);
return setTimeout(send_again, 3000);
}
} else if (res.sync_step === "applyHB") {
this.applyHB(res.data, sender === this.current_sync_target);
if ((this.syncMethod === "syncAll" || (res.sent_again != null)) && (!this.is_synced) && ((this.current_sync_target === sender) || (this.current_sync_target == null))) {
this.connections[sender].is_synced = true;
return this.findNewSyncTarget();
}
} else if (res.sync_step === "applyHB_") {
return this.applyHB(res.data, sender === this.current_sync_target);
}
}
},
parseMessageFromXml: function(m) {
var parse_array, parse_object;
parse_array = function(node) {
var i, len, n, ref, results;
ref = node.children;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
n = ref[i];
if (n.getAttribute("isArray") === "true") {
results.push(parse_array(n));
} else {
results.push(parse_object(n));
}
}
return results;
};
parse_object = function(node) {
var i, int, json, len, n, name, ref, ref1, value;
json = {};
ref = node.attrs;
for (name in ref) {
value = ref[name];
int = parseInt(value);
if (isNaN(int) || ("" + int) !== value) {
json[name] = value;
} else {
json[name] = int;
}
}
ref1 = node.children;
for (i = 0, len = ref1.length; i < len; i++) {
n = ref1[i];
name = n.name;
if (n.getAttribute("isArray") === "true") {
json[name] = parse_array(n);
} else {
json[name] = parse_object(n);
}
}
return json;
};
return parse_object(m);
},
encodeMessageToXml: function(m, json) {
var encode_array, encode_object;
encode_object = function(m, json) {
var name, value;
for (name in json) {
value = json[name];
if (value == null) {
} else if (value.constructor === Object) {
encode_object(m.c(name), value);
} else if (value.constructor === Array) {
encode_array(m.c(name), value);
} else {
m.setAttribute(name, value);
}
}
return m;
};
encode_array = function(m, array) {
var e, i, len;
m.setAttribute("isArray", "true");
for (i = 0, len = array.length; i < len; i++) {
e = array[i];
if (e.constructor === Object) {
encode_object(m.c("array-element"), e);
} else {
encode_array(m.c("array-element"), e);
}
}
return m;
};
if (json.constructor === Object) {
return encode_object(m.c("y", {
xmlns: "http://y.ninja/connector-stanza"
}), json);
} else if (json.constructor === Array) {
return encode_array(m.c("y", {
xmlns: "http://y.ninja/connector-stanza"
}), json);
} else {
throw new Error("I can't encode this json!");
}
},
setIsBoundToY: function() {
if (typeof this.on_bound_to_y === "function") {
this.on_bound_to_y();
}
delete this.when_bound_to_y;
return this.is_bound_to_y = true;
}
};

View File

@@ -1,120 +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 i, len, o, 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, fromHB) {
var i, len, o, op_json;
if (fromHB == null) {
fromHB = false;
}
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];
if (fromHB) {
op_json.fromHB = "true";
}
o = this.parseOperation(op_json);
o.parsed_from_json = 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 i, len, old_length, op, ref, unprocessed;
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,255 +0,0 @@
var HistoryBuffer,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
HistoryBuffer = (function() {
function HistoryBuffer(user_id1) {
this.user_id = user_id1;
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.setUserId = function(user_id1, state_vector) {
var base, buff, counter_diff, name, o, o_name, ref;
this.user_id = user_id1;
if ((base = this.buffer)[name = this.user_id] == null) {
base[name] = [];
}
buff = this.buffer[this.user_id];
counter_diff = state_vector[this.user_id] || 0;
if (this.buffer._temp != null) {
ref = this.buffer._temp;
for (o_name in ref) {
o = ref[o_name];
o.uid.creator = this.user_id;
o.uid.op_number += counter_diff;
buff[o.uid.op_number] = o;
}
}
this.operation_counter[this.user_id] = (this.operation_counter._temp || 0) + counter_diff;
delete this.operation_counter._temp;
return delete this.buffer._temp;
};
HistoryBuffer.prototype.emptyGarbage = function() {
var i, len, o, 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 i, len, o, 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++)
};
};
HistoryBuffer.prototype.getOperationCounter = function(user_id) {
var ctn, ref, res, user;
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, ref, u_name, unknown, user;
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];
if (u_name === "_") {
continue;
}
for (o_number in user) {
o = user[o_number];
if ((o.uid.noOperation == null) && 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]
};
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 results, state, user;
results = [];
for (user in state_vector) {
state = state_vector[user];
if (((this.operation_counter[user] == null) || (this.operation_counter[user] < state_vector[user])) && (state_vector[user] != null)) {
results.push(this.operation_counter[user] = state_vector[user]);
} else {
results.push(void 0);
}
}
return results;
};
HistoryBuffer.prototype.addToCounter = function(o) {
var base, name;
if ((base = this.operation_counter)[name = o.uid.creator] == null) {
base[name] = 0;
}
if (o.uid.op_number === this.operation_counter[o.uid.creator]) {
this.operation_counter[o.uid.creator]++;
}
while (this.buffer[o.uid.creator][this.operation_counter[o.uid.creator]] != null) {
this.operation_counter[o.uid.creator]++;
}
return void 0;
};
return HistoryBuffer;
})();
module.exports = HistoryBuffer;

View File

@@ -1,91 +0,0 @@
var YObject;
YObject = (function() {
function YObject(_object) {
var name, ref, val;
this._object = _object != null ? _object : {};
if (this._object.constructor === Object) {
ref = this._object;
for (name in ref) {
val = ref[name];
if (val.constructor === Object) {
this._object[name] = new YObject(val);
}
}
} else {
throw new Error("Y.Object accepts Json Objects only");
}
}
YObject.prototype._name = "Object";
YObject.prototype._getModel = function(types, ops) {
var n, o, ref;
if (this._model == null) {
this._model = new ops.MapManager(this).execute();
ref = this._object;
for (n in ref) {
o = ref[n];
this._model.val(n, o);
}
}
delete this._object;
return this._model;
};
YObject.prototype._setModel = function(_model) {
this._model = _model;
return delete this._object;
};
YObject.prototype.observe = function(f) {
this._model.observe(f);
return this;
};
YObject.prototype.unobserve = function(f) {
this._model.unobserve(f);
return this;
};
YObject.prototype.val = function(name, content) {
var n, ref, res, v;
if (this._model != null) {
return this._model.val.apply(this._model, arguments);
} else {
if (content != null) {
return this._object[name] = content;
} else if (name != null) {
return this._object[name];
} else {
res = {};
ref = this._object;
for (n in ref) {
v = ref[n];
res[n] = v;
}
return res;
}
}
};
YObject.prototype["delete"] = function(name) {
this._model["delete"](name);
return this;
};
return YObject;
})();
if (typeof window !== "undefined" && window !== null) {
if (window.Y != null) {
window.Y.Object = YObject;
} else {
throw new Error("You must first import Y!");
}
}
if (typeof module !== "undefined" && module !== null) {
module.exports = YObject;
}

View File

@@ -1,670 +0,0 @@
var slice = [].slice,
extend = 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; },
hasProp = {}.hasOwnProperty;
module.exports = function() {
var execution_listener, ops;
ops = {};
execution_listener = [];
ops.Operation = (function() {
function Operation(custom_type, uid, content, content_operations) {
var name, op;
if (custom_type != null) {
this.custom_type = custom_type;
}
this.is_deleted = false;
this.garbage_collected = false;
this.event_listeners = [];
if (uid != null) {
this.uid = uid;
}
if (content === void 0) {
} else if ((content != null) && (content.creator != null)) {
this.saveOperation('content', content);
} else {
this.content = content;
}
if (content_operations != null) {
this.content_operations = {};
for (name in content_operations) {
op = content_operations[name];
this.saveOperation(name, op, 'content_operations');
}
}
}
Operation.prototype.type = "Operation";
Operation.prototype.getContent = function(name) {
var content, n, ref, ref1, v;
if (this.content != null) {
if (this.content.getCustomType != null) {
return this.content.getCustomType();
} else if (this.content.constructor === Object) {
if (name != null) {
if (this.content[name] != null) {
return this.content[name];
} else {
return this.content_operations[name].getCustomType();
}
} else {
content = {};
ref = this.content;
for (n in ref) {
v = ref[n];
content[n] = v;
}
if (this.content_operations != null) {
ref1 = this.content_operations;
for (n in ref1) {
v = ref1[n];
v = v.getCustomType();
content[n] = v;
}
}
return content;
}
} else {
return this.content;
}
} else {
return this.content;
}
};
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 ops.Delete(void 0, this)).execute();
return null;
};
Operation.prototype.callEvent = function() {
var callon;
if (this.custom_type != null) {
callon = this.getCustomType();
} else {
callon = this;
}
return this.forwardEvent.apply(this, [callon].concat(slice.call(arguments)));
};
Operation.prototype.forwardEvent = function() {
var args, f, j, len, op, ref, results;
op = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
ref = this.event_listeners;
results = [];
for (j = 0, len = ref.length; j < len; j++) {
f = ref[j];
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 this.HB.addToGarbageCollector(this);
}
}
};
Operation.prototype.cleanup = function() {
this.HB.removeOperation(this);
return this.deleteAllObservers();
};
Operation.prototype.setParent = function(parent1) {
this.parent = parent1;
};
Operation.prototype.getParent = function() {
return this.parent;
};
Operation.prototype.getUid = function() {
var map_uid;
if (this.uid.noOperation == null) {
return this.uid;
} else {
if (this.uid.alt != null) {
map_uid = this.uid.alt.cloneUid();
map_uid.sub = this.uid.sub;
return map_uid;
} else {
return void 0;
}
}
};
Operation.prototype.cloneUid = function() {
var n, ref, uid, v;
uid = {};
ref = this.getUid();
for (n in ref) {
v = ref[n];
uid[n] = v;
}
return uid;
};
Operation.prototype.execute = function() {
var j, l, len;
if (this.validateSavedOperations()) {
this.is_executed = true;
if (this.uid == null) {
this.uid = this.HB.getNextOperationIdentifier();
}
if (this.uid.noOperation == null) {
this.HB.addOperation(this);
for (j = 0, len = execution_listener.length; j < len; j++) {
l = execution_listener[j];
l(this._encode());
}
}
return this;
} else {
return false;
}
};
Operation.prototype.saveOperation = function(name, op, base) {
var base1, dest, j, last_path, len, path, paths;
if (base == null) {
base = "this";
}
if ((op != null) && (op._getModel != null)) {
op = op._getModel(this.custom_types, this.operations);
}
if (op == null) {
} else if ((op.execute != null) || !((op.op_number != null) && (op.creator != null))) {
if (base === "this") {
return this[name] = op;
} else {
dest = this[base];
paths = name.split("/");
last_path = paths.pop();
for (j = 0, len = paths.length; j < len; j++) {
path = paths[j];
dest = dest[path];
}
return dest[last_path] = op;
}
} else {
if (this.unchecked == null) {
this.unchecked = {};
}
if ((base1 = this.unchecked)[base] == null) {
base1[base] = {};
}
return this.unchecked[base][name] = op;
}
};
Operation.prototype.validateSavedOperations = function() {
var base, base_name, dest, j, last_path, len, name, op, op_uid, path, paths, ref, success, uninstantiated;
uninstantiated = {};
success = true;
ref = this.unchecked;
for (base_name in ref) {
base = ref[base_name];
for (name in base) {
op_uid = base[name];
op = this.HB.getOperation(op_uid);
if (op) {
if (base_name === "this") {
this[name] = op;
} else {
dest = this[base_name];
paths = name.split("/");
last_path = paths.pop();
for (j = 0, len = paths.length; j < len; j++) {
path = paths[j];
dest = dest[path];
}
dest[last_path] = op;
}
} else {
if (uninstantiated[base_name] == null) {
uninstantiated[base_name] = {};
}
uninstantiated[base_name][name] = op_uid;
success = false;
}
}
}
if (!success) {
this.unchecked = uninstantiated;
return false;
} else {
delete this.unchecked;
return this;
}
};
Operation.prototype.getCustomType = function() {
var Type, j, len, ref, t;
if (this.custom_type == null) {
return this;
} else {
if (this.custom_type.constructor === String) {
Type = this.custom_types;
ref = this.custom_type.split(".");
for (j = 0, len = ref.length; j < len; j++) {
t = ref[j];
Type = Type[t];
}
this.custom_type = new Type();
this.custom_type._setModel(this);
}
return this.custom_type;
}
};
Operation.prototype._encode = function(json) {
var n, o, operations, ref, ref1;
if (json == null) {
json = {};
}
json.type = this.type;
json.uid = this.getUid();
if (this.custom_type != null) {
if (this.custom_type.constructor === String) {
json.custom_type = this.custom_type;
} else {
json.custom_type = this.custom_type._name;
}
}
if (((ref = this.content) != null ? ref.getUid : void 0) != null) {
json.content = this.content.getUid();
} else {
json.content = this.content;
}
if (this.content_operations != null) {
operations = {};
ref1 = this.content_operations;
for (n in ref1) {
o = ref1[n];
if (o._getModel != null) {
o = o._getModel(this.custom_types, this.operations);
}
operations[n] = o.getUid();
}
json.content_operations = operations;
}
return json;
};
return Operation;
})();
ops.Delete = (function(superClass) {
extend(Delete, superClass);
function Delete(custom_type, uid, deletes) {
this.saveOperation('deletes', deletes);
Delete.__super__.constructor.call(this, custom_type, 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;
})(ops.Operation);
ops.Delete.parse = function(o) {
var deletes_uid, uid;
uid = o['uid'], deletes_uid = o['deletes'];
return new this(null, uid, deletes_uid);
};
ops.Insert = (function(superClass) {
extend(Insert, superClass);
function Insert(custom_type, content, content_operations, parent, uid, prev_cl, next_cl, origin) {
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, custom_type, uid, content, content_operations);
}
Insert.prototype.type = "Insert";
Insert.prototype.val = function() {
return this.getContent();
};
Insert.prototype.getNext = function(i) {
var n;
if (i == null) {
i = 1;
}
n = this;
while (i > 0 && (n.next_cl != null)) {
n = n.next_cl;
if (!n.is_deleted) {
i--;
}
}
if (n.is_deleted) {
null;
}
return n;
};
Insert.prototype.getPrev = function(i) {
var n;
if (i == null) {
i = 1;
}
n = this;
while (i > 0 && (n.prev_cl != null)) {
n = n.prev_cl;
if (!n.is_deleted) {
i--;
}
}
if (n.is_deleted) {
return null;
} else {
return n;
}
};
Insert.prototype.applyDelete = function(o) {
var callLater, garbagecollect;
if (this.deleted_by == null) {
this.deleted_by = [];
}
callLater = false;
if ((this.parent != null) && !this.is_deleted && (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.parent.callOperationSpecificDeleteEvents(this, o);
}
if ((this.prev_cl != null) && this.prev_cl.isDeleted()) {
return this.prev_cl.applyDelete();
}
};
Insert.prototype.cleanup = function() {
var d, j, len, o, ref;
if (this.next_cl.isDeleted()) {
ref = this.deleted_by;
for (j = 0, len = ref.length; j < len; j++) {
d = ref[j];
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;
if (this.content instanceof ops.Operation && !(this.content instanceof ops.Insert)) {
this.content.referenced_by--;
if (this.content.referenced_by <= 0 && !this.content.is_deleted) {
this.content.applyDelete();
}
}
delete this.content;
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 base1, distance_to_origin, i, o;
if (!this.validateSavedOperations()) {
return false;
} else {
if (this.content instanceof ops.Operation) {
this.content.insert_parent = this;
if ((base1 = this.content).referenced_by == null) {
base1.referenced_by = 0;
}
this.content.referenced_by++;
}
if (this.parent != null) {
if (this.prev_cl == null) {
this.prev_cl = this.parent.beginning;
}
if (this.origin == null) {
this.origin = this.prev_cl;
} else if (this.origin === "Delimiter") {
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.parent.callOperationSpecificInsertEvents(this);
return this;
}
};
Insert.prototype.getPosition = function() {
var position, prev;
position = 0;
prev = this.prev_cl;
while (true) {
if (prev instanceof ops.Delimiter) {
break;
}
if (!prev.isDeleted()) {
position++;
}
prev = prev.prev_cl;
}
return position;
};
Insert.prototype._encode = function(json) {
if (json == null) {
json = {};
}
json.prev = this.prev_cl.getUid();
json.next = this.next_cl.getUid();
if (this.origin.type === "Delimiter") {
json.origin = "Delimiter";
} else if (this.origin !== this.prev_cl) {
json.origin = this.origin.getUid();
}
json.parent = this.parent.getUid();
return Insert.__super__._encode.call(this, json);
};
return Insert;
})(ops.Operation);
ops.Insert.parse = function(json) {
var content, content_operations, next, origin, parent, prev, uid;
content = json['content'], content_operations = json['content_operations'], uid = json['uid'], prev = json['prev'], next = json['next'], origin = json['origin'], parent = json['parent'];
return new this(null, content, content_operations, parent, uid, prev, next, origin);
};
ops.Delimiter = (function(superClass) {
extend(Delimiter, superClass);
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, null, {
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;
})(ops.Operation);
ops.Delimiter.parse = function(json) {
var next, prev, uid;
uid = json['uid'], prev = json['prev'], next = json['next'];
return new this(uid, prev, next);
};
return {
'operations': ops,
'execution_listener': execution_listener
};
};

View File

@@ -1,579 +0,0 @@
var basic_ops_uninitialized,
extend = 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; },
hasProp = {}.hasOwnProperty;
basic_ops_uninitialized = require("./Basic");
module.exports = function() {
var basic_ops, ops;
basic_ops = basic_ops_uninitialized();
ops = basic_ops.operations;
ops.MapManager = (function(superClass) {
extend(MapManager, superClass);
function MapManager(custom_type, uid, content, content_operations) {
this._map = {};
MapManager.__super__.constructor.call(this, custom_type, uid, content, content_operations);
}
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.map = function(f) {
var n, ref, v;
ref = this._map;
for (n in ref) {
v = ref[n];
f(n, v);
}
return void 0;
};
MapManager.prototype.val = function(name, content) {
var o, prop, ref, rep, res, result;
if (arguments.length > 1) {
if ((content != null) && (content._getModel != null)) {
rep = content._getModel(this.custom_types, this.operations);
} else {
rep = content;
}
this.retrieveSub(name).replace(rep);
return this.getCustomType();
} else if (name != null) {
prop = this._map[name];
if ((prop != null) && !prop.isContentDeleted()) {
res = prop.val();
if (res instanceof ops.Operation) {
return res.getCustomType();
} else {
return res;
}
} 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, rm, rm_uid;
if (this._map[property_name] == null) {
event_properties = {
name: property_name
};
event_this = this;
rm_uid = {
noOperation: true,
sub: property_name,
alt: this
};
rm = new ops.ReplaceManager(null, 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;
})(ops.Operation);
ops.MapManager.parse = function(json) {
var content, content_operations, custom_type, uid;
uid = json['uid'], custom_type = json['custom_type'], content = json['content'], content_operations = json['content_operations'];
return new this(custom_type, uid, content, content_operations);
};
ops.ListManager = (function(superClass) {
extend(ListManager, superClass);
function ListManager(custom_type, uid, content, content_operations) {
this.beginning = new ops.Delimiter(void 0, void 0);
this.end = new ops.Delimiter(this.beginning, void 0);
this.beginning.next_cl = this.end;
this.beginning.execute();
this.end.execute();
ListManager.__super__.constructor.call(this, custom_type, uid, content, content_operations);
}
ListManager.prototype.type = "ListManager";
ListManager.prototype.applyDelete = function() {
var o;
o = this.beginning;
while (o != null) {
o.applyDelete();
o = o.next_cl;
}
return ListManager.__super__.applyDelete.call(this);
};
ListManager.prototype.cleanup = function() {
return ListManager.__super__.cleanup.call(this);
};
ListManager.prototype.toJson = function(transform_to_value) {
var i, j, len, o, results, val;
if (transform_to_value == null) {
transform_to_value = false;
}
val = this.val();
results = [];
for (o = j = 0, len = val.length; j < len; o = ++j) {
i = val[o];
if (o instanceof ops.Object) {
results.push(o.toJson(transform_to_value));
} else if (o instanceof ops.ListManager) {
results.push(o.toJson(transform_to_value));
} else if (transform_to_value && o instanceof ops.Operation) {
results.push(o.val());
} else {
results.push(o);
}
}
return results;
};
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) {
if (!o.is_deleted) {
result.push(o.val());
}
o = o.next_cl;
}
return result;
};
ListManager.prototype.map = function(f) {
var o, result;
o = this.beginning.next_cl;
result = [];
while (o !== this.end) {
if (!o.is_deleted) {
result.push(f(o));
}
o = o.next_cl;
}
return result;
};
ListManager.prototype.fold = function(init, f) {
var o;
o = this.beginning.next_cl;
while (o !== this.end) {
if (!o.is_deleted) {
init = f(init, o);
}
o = o.next_cl;
}
return init;
};
ListManager.prototype.val = function(pos) {
var o;
if (pos != null) {
o = this.getOperationByPosition(pos + 1);
if (!(o instanceof ops.Delimiter)) {
return o.val();
} else {
throw new Error("this position does not exist");
}
} else {
return this.toArray();
}
};
ListManager.prototype.ref = function(pos) {
var o;
if (pos != null) {
o = this.getOperationByPosition(pos + 1);
if (!(o instanceof ops.Delimiter)) {
return o;
} else {
return null;
}
} else {
throw new Error("you must specify a position parameter");
}
};
ListManager.prototype.getOperationByPosition = function(position) {
var o;
o = this.beginning;
while (true) {
if (o instanceof ops.Delimiter && (o.prev_cl != null)) {
o = o.prev_cl;
while (o.isDeleted() && (o.prev_cl != null)) {
o = o.prev_cl;
}
break;
}
if (position <= 0 && !o.isDeleted()) {
break;
}
o = o.next_cl;
if (!o.isDeleted()) {
position -= 1;
}
}
return o;
};
ListManager.prototype.push = function(content) {
return this.insertAfter(this.end.prev_cl, [content]);
};
ListManager.prototype.insertAfter = function(left, contents) {
var c, j, len, right, tmp;
right = left.next_cl;
while (right.isDeleted()) {
right = right.next_cl;
}
left = right.prev_cl;
if (contents instanceof ops.Operation) {
(new ops.Insert(null, content, null, void 0, void 0, left, right)).execute();
} else {
for (j = 0, len = contents.length; j < len; j++) {
c = contents[j];
if ((c != null) && (c._name != null) && (c._getModel != null)) {
c = c._getModel(this.custom_types, this.operations);
}
tmp = (new ops.Insert(null, c, null, void 0, void 0, left, right)).execute();
left = tmp;
}
}
return this;
};
ListManager.prototype.insert = function(position, contents) {
var ith;
ith = this.getOperationByPosition(position);
return this.insertAfter(ith, contents);
};
ListManager.prototype["delete"] = function(position, length) {
var d, delete_ops, i, j, o, ref;
if (length == null) {
length = 1;
}
o = this.getOperationByPosition(position + 1);
delete_ops = [];
for (i = j = 0, ref = length; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) {
if (o instanceof ops.Delimiter) {
break;
}
d = (new ops.Delete(null, void 0, o)).execute();
o = o.next_cl;
while ((!(o instanceof ops.Delimiter)) && o.isDeleted()) {
o = o.next_cl;
}
delete_ops.push(d._encode());
}
return this;
};
ListManager.prototype.callOperationSpecificInsertEvents = function(op) {
var getContentType;
getContentType = function(content) {
if (content instanceof ops.Operation) {
return content.getCustomType();
} else {
return content;
}
};
return this.callEvent([
{
type: "insert",
reference: op,
position: op.getPosition(),
object: this.getCustomType(),
changedBy: op.uid.creator,
value: getContentType(op.val())
}
]);
};
ListManager.prototype.callOperationSpecificDeleteEvents = function(op, del_op) {
return this.callEvent([
{
type: "delete",
reference: op,
position: op.getPosition(),
object: this.getCustomType(),
length: 1,
changedBy: del_op.uid.creator,
oldValue: op.val()
}
]);
};
return ListManager;
})(ops.Operation);
ops.ListManager.parse = function(json) {
var content, content_operations, custom_type, uid;
uid = json['uid'], custom_type = json['custom_type'], content = json['content'], content_operations = json['content_operations'];
return new this(custom_type, uid, content, content_operations);
};
ops.Composition = (function(superClass) {
extend(Composition, superClass);
function Composition(custom_type, _composition_value, composition_value_operations, uid, tmp_composition_ref) {
var n, o;
this._composition_value = _composition_value;
Composition.__super__.constructor.call(this, custom_type, uid);
if (tmp_composition_ref != null) {
this.tmp_composition_ref = tmp_composition_ref;
} else {
this.composition_ref = this.end.prev_cl;
}
if (composition_value_operations != null) {
this.composition_value_operations = {};
for (n in composition_value_operations) {
o = composition_value_operations[n];
this.saveOperation(n, o, '_composition_value');
}
}
}
Composition.prototype.type = "Composition";
Composition.prototype.execute = function() {
var composition_ref;
if (this.validateSavedOperations()) {
this.getCustomType()._setCompositionValue(this._composition_value);
delete this._composition_value;
if (this.tmp_composition_ref) {
composition_ref = this.HB.getOperation(this.tmp_composition_ref);
if (composition_ref != null) {
delete this.tmp_composition_ref;
this.composition_ref = composition_ref;
}
}
return Composition.__super__.execute.apply(this, arguments);
} else {
return false;
}
};
Composition.prototype.callOperationSpecificInsertEvents = function(op) {
var o;
if (this.tmp_composition_ref != null) {
if (op.uid.creator === this.tmp_composition_ref.creator && op.uid.op_number === this.tmp_composition_ref.op_number) {
this.composition_ref = op;
delete this.tmp_composition_ref;
op = op.next_cl;
if (op === this.end) {
return;
}
} else {
return;
}
}
o = this.end.prev_cl;
while (o !== op) {
this.getCustomType()._unapply(o.undo_delta);
o = o.prev_cl;
}
while (o !== this.end) {
o.undo_delta = this.getCustomType()._apply(o.val());
o = o.next_cl;
}
this.composition_ref = this.end.prev_cl;
return this.callEvent([
{
type: "update",
changedBy: op.uid.creator,
newValue: this.val()
}
]);
};
Composition.prototype.callOperationSpecificDeleteEvents = function(op, del_op) {};
Composition.prototype.applyDelta = function(delta, operations) {
(new ops.Insert(null, delta, operations, this, null, this.end.prev_cl, this.end)).execute();
return void 0;
};
Composition.prototype._encode = function(json) {
var custom, n, o, ref;
if (json == null) {
json = {};
}
custom = this.getCustomType()._getCompositionValue();
json.composition_value = custom.composition_value;
if (custom.composition_value_operations != null) {
json.composition_value_operations = {};
ref = custom.composition_value_operations;
for (n in ref) {
o = ref[n];
json.composition_value_operations[n] = o.getUid();
}
}
if (this.composition_ref != null) {
json.composition_ref = this.composition_ref.getUid();
} else {
json.composition_ref = this.tmp_composition_ref;
}
return Composition.__super__._encode.call(this, json);
};
return Composition;
})(ops.ListManager);
ops.Composition.parse = function(json) {
var composition_ref, composition_value, composition_value_operations, custom_type, uid;
uid = json['uid'], custom_type = json['custom_type'], composition_value = json['composition_value'], composition_value_operations = json['composition_value_operations'], composition_ref = json['composition_ref'];
return new this(custom_type, composition_value, composition_value_operations, uid, composition_ref);
};
ops.ReplaceManager = (function(superClass) {
extend(ReplaceManager, superClass);
function ReplaceManager(custom_type, event_properties1, event_this1, uid) {
this.event_properties = event_properties1;
this.event_this = event_this1;
if (this.event_properties['object'] == null) {
this.event_properties['object'] = this.event_this.getCustomType();
}
ReplaceManager.__super__.constructor.call(this, custom_type, uid);
}
ReplaceManager.prototype.type = "ReplaceManager";
ReplaceManager.prototype.callEventDecorator = function(events) {
var event, j, len, name, prop, ref;
if (!this.isDeleted()) {
for (j = 0, len = events.length; j < len; j++) {
event = events[j];
ref = this.event_properties;
for (name in ref) {
prop = ref[name];
event[name] = prop;
}
}
this.event_this.callEvent(events);
}
return void 0;
};
ReplaceManager.prototype.callOperationSpecificInsertEvents = function(op) {
var old_value;
if (op.next_cl.type === "Delimiter" && op.prev_cl.type !== "Delimiter") {
if (!op.is_deleted) {
old_value = op.prev_cl.val();
this.callEventDecorator([
{
type: "update",
changedBy: op.uid.creator,
oldValue: old_value
}
]);
}
op.prev_cl.applyDelete();
} else if (op.next_cl.type !== "Delimiter") {
op.applyDelete();
} else {
this.callEventDecorator([
{
type: "add",
changedBy: op.uid.creator
}
]);
}
return void 0;
};
ReplaceManager.prototype.callOperationSpecificDeleteEvents = function(op, del_op) {
if (op.next_cl.type === "Delimiter") {
return this.callEventDecorator([
{
type: "delete",
changedBy: del_op.uid.creator,
oldValue: op.val()
}
]);
}
};
ReplaceManager.prototype.replace = function(content, replaceable_uid) {
var o, relp;
o = this.getLastOperation();
relp = (new ops.Insert(null, content, null, this, replaceable_uid, o, o.next_cl)).execute();
return void 0;
};
ReplaceManager.prototype.isContentDeleted = function() {
return this.getLastOperation().isDeleted();
};
ReplaceManager.prototype.deleteContent = function() {
var last_op;
last_op = this.getLastOperation();
if ((!last_op.isDeleted()) && last_op.type !== "Delimiter") {
(new ops.Delete(null, 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;
};
return ReplaceManager;
})(ops.ListManager);
return basic_ops;
};

View File

@@ -1,90 +0,0 @@
var bindToChildren;
bindToChildren = function(that) {
var attr, i, j, ref;
for (i = j = 0, ref = that.children.length; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) {
attr = that.children.item(i);
if (attr.name != null) {
attr.val = that.val.val(attr.name);
}
}
return that.val.observe(function(events) {
var event, k, len, newVal, results;
results = [];
for (k = 0, len = events.length; k < len; k++) {
event = events[k];
if (event.name != null) {
results.push((function() {
var l, ref1, results1;
results1 = [];
for (i = l = 0, ref1 = that.children.length; 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) {
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._name === "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, new Y.Object(this.val)).val(this.name);
} else if (typeof this.val === "string") {
this.parentElement.val(this.name, this.val);
}
if (this.val._name === "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, new Y.Object(this.val)).val(this.name);
} else if (this.val._name === "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,45 +0,0 @@
var Engine, HistoryBuffer, adaptConnector, createY, structured_ops_uninitialized;
structured_ops_uninitialized = require("./Operations/Structured");
HistoryBuffer = require("./HistoryBuffer");
Engine = require("./Engine");
adaptConnector = require("./ConnectorAdapter");
createY = function(connector) {
var HB, ct, engine, model, ops, ops_manager, user_id;
if (connector.user_id != null) {
user_id = connector.user_id;
} else {
user_id = "_temp";
connector.when_received_state_vector_listeners = [
function(state_vector) {
return HB.setUserId(this.user_id, state_vector);
}
];
}
HB = new HistoryBuffer(user_id);
ops_manager = structured_ops_uninitialized(HB, this.constructor);
ops = ops_manager.operations;
engine = new Engine(HB, ops);
adaptConnector(connector, engine, HB, ops_manager.execution_listener);
ops.Operation.prototype.HB = HB;
ops.Operation.prototype.operations = ops;
ops.Operation.prototype.engine = engine;
ops.Operation.prototype.connector = connector;
ops.Operation.prototype.custom_types = this.constructor;
ct = new createY.Object();
model = new ops.MapManager(ct, HB.getReservedUniqueIdentifier()).execute();
ct._setModel(model);
return ct;
};
module.exports = createY;
if (typeof window !== "undefined" && window !== null) {
window.Y = createY;
}
createY.Object = require("./ObjectType");

View File

@@ -1,27 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Test Yjs!</title>
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css" />
</head>
<body>
<div id="mocha"></div>
<script src="../../node_modules/mocha/mocha.js" class="awesome"></script>
<script>
mocha.setup('bdd');
mocha.ui('bdd');
mocha.reporter('html');
</script>
<script src="object-test.js"></script>
<script src="xml-test.js"></script>
<script src="list-test.js"></script>
<script src="text-test.js"></script>
<script>
//mocha.checkLeaks();
//mocha.run();
window.onerror = null;
if (window.mochaPhantomJS) { mochaPhantomJS.run(); }
else { mocha.run(); }
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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,5 +0,0 @@
# Examples
Here you find some (hopefully) usefull examples on how to use Yjs!
Feel free to use the code of the examples in your own project. They include basic examples how to use Yjs.

View File

@@ -1,14 +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>
</body>
</html>

View File

@@ -1,57 +0,0 @@
<script src="../../build/browser/y.js"></script>
<script src="../../../y-text/build/browser/y-text.js"></script>
<link rel="import" href="../../build/browser/y-object.html">
<link rel="import" href="../../../y-xmpp/build/browser/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" syncMode="syncAll" debug="true"></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",new Y.Text("stuff"));
}
that.y.val("text").bind(that.$.text,that.shadowRoot)
})
// Everything is initialized. Lets test stuff!
window.y_test = this;
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);
}
})
</script>
</polymer-element>

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Y Example</title>
<script src="../../build/browser/y.js"></script>
<script src="../../../y-text/build/browser/y-text.js"></script>
<script src="../../../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/y-js/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().join("testy-xmpp-json3", {syncMode: "syncAll"});
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", new Y.Text("headline"));
y.val("textfield",new Y.Text("stuff"))
}
})
};

View File

@@ -1,114 +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'
cache = require 'gulp-cached'
coffeeify = require 'gulp-coffeeify'
exit = require 'gulp-exit'
gulp.task 'default', ['build_browser']
files =
lib : ['./lib/**/*.coffee']
browser : ['./lib/y.coffee','./lib/y-object.coffee']
test : ['./test/**/*test.coffee', '../y-*/test/*test.coffee']
#test : ['./test/Json_test.coffee', './test/Text_test.coffee']
gulp : ['./gulpfile.coffee']
examples : ['./examples/**/*.js']
other: ['./lib/**/*', './test/*']
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', '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"
dirname: "./"
.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'], ->
gulp.watch files.all, ['build']
gulp.task 'mocha', ->
gulp.src files.test, { read: false }
.pipe mocha {reporter : 'list'}
.pipe exit()
gulp.task 'lint', ->
gulp.src files.all
.pipe ignore.include '**/*.coffee'
.pipe coffeelint {
"max_line_length":
"level": "ignore"
}
.pipe coffeelint.reporter()
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 'clean', ->
gulp.src ['./build/{browser,test,node}/**/*.{js,map}','./doc/'], { read: false }
.pipe rimraf()
gulp.task 'default', ['clean','build'], ->

View File

@@ -1,61 +0,0 @@
ConnectorClass = require "./ConnectorClass"
#
# @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)->
for name, f of ConnectorClass
connector[name] = f
connector.setIsBoundToY()
send_ = (o)->
if (o.uid.creator is HB.getUserId()) and
(typeof o.uid.op_number isnt "string") and # TODO: i don't think that we need this anymore..
(HB.getUserId() isnt "_temp")
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
json =
hb: hb
state_vector: encode_state_vector HB.getOperationCounter()
json
applyHB = (hb, fromHB)->
engine.applyOp hb, fromHB
connector.getStateVector = getStateVector
connector.getHB = getHB
connector.applyHB = applyHB
connector.receive_handlers ?= []
connector.receive_handlers.push (sender, op)->
if op.uid.creator isnt HB.getUserId()
engine.applyOp op
module.exports = adaptConnector

View File

@@ -1,355 +0,0 @@
module.exports =
#
# @params new Connector(options)
# @param options.syncMethod {String} is either "syncAll" or "master-slave".
# @param options.role {String} The role of this client
# (slave or master (only used when syncMethod is master-slave))
# @param options.perform_send_again {Boolean} Whetehr to whether to resend the HB after some time period. This reduces sync errors, but has some overhead (optional)
#
init: (options)->
req = (name, choices)=>
if options[name]?
if (not choices?) or choices.some((c)->c is options[name])
@[name] = options[name]
else
throw new Error "You can set the '"+name+"' option to one of the following choices: "+JSON.encode(choices)
else
throw new Error "You must specify "+name+", when initializing the Connector!"
req "syncMethod", ["syncAll", "master-slave"]
req "role", ["master", "slave"]
req "user_id"
@on_user_id_set?(@user_id)
# whether to resend the HB after some time period. This reduces sync errors.
# But this is not necessary in the test-connector
if options.perform_send_again?
@perform_send_again = options.perform_send_again
else
@perform_send_again = true
# A Master should sync with everyone! TODO: really? - for now its safer this way!
if @role is "master"
@syncMethod = "syncAll"
# is set to true when this is synced with all other connections
@is_synced = false
# Peerjs Connections: key: conn-id, value: object
@connections = {}
# List of functions that shall process incoming data
@receive_handlers ?= []
# whether this instance is bound to any y instance
@connections = {}
@current_sync_target = null
@sent_hb_to_all_users = false
@is_initialized = true
onUserEvent: (f)->
@connections_listeners ?= []
@connections_listeners.push f
isRoleMaster: ->
@role is "master"
isRoleSlave: ->
@role is "slave"
findNewSyncTarget: ()->
@current_sync_target = null
if @syncMethod is "syncAll"
for user, c of @connections
if not c.is_synced
@performSync user
break
if not @current_sync_target?
@setStateSynced()
null
userLeft: (user)->
delete @connections[user]
@findNewSyncTarget()
if @connections_listeners?
for f in @connections_listeners
f {
action: "userLeft"
user: user
}
userJoined: (user, role)->
if not role?
throw new Error "Internal: You must specify the role of the joined user! E.g. userJoined('uid:3939','slave')"
# a user joined the room
@connections[user] ?= {}
@connections[user].is_synced = false
if (not @is_synced) or @syncMethod is "syncAll"
if @syncMethod is "syncAll"
@performSync user
else if role is "master"
# TODO: What if there are two masters? Prevent sending everything two times!
@performSyncWithMaster user
if @connections_listeners?
for f in @connections_listeners
f {
action: "userJoined"
user: user
role: role
}
#
# Execute a function _when_ we are connected. If not connected, wait until connected.
# @param f {Function} Will be executed on the Connector context.
#
whenSynced: (args)->
if args.constructor is Function
args = [args]
if @is_synced
args[0].apply this, args[1..]
else
@compute_when_synced ?= []
@compute_when_synced.push args
#
# Execute an function when a message is received.
# @param f {Function} Will be executed on the PeerJs-Connector context. f will be called with (sender_id, broadcast {true|false}, message).
#
onReceive: (f)->
@receive_handlers.push f
###
# Broadcast a message to all connected peers.
# @param message {Object} The message to broadcast.
#
broadcast: (message)->
throw new Error "You must implement broadcast!"
#
# Send a message to a peer, or set of peers
#
send: (peer_s, message)->
throw new Error "You must implement send!"
###
#
# perform a sync with a specific user.
#
performSync: (user)->
if not @current_sync_target?
@current_sync_target = user
@send user,
sync_step: "getHB"
send_again: "true"
data: @getStateVector()
if not @sent_hb_to_all_users
@sent_hb_to_all_users = true
hb = @getHB([]).hb
_hb = []
for o in hb
_hb.push o
if _hb.length > 10
@broadcast
sync_step: "applyHB_"
data: _hb
_hb = []
@broadcast
sync_step: "applyHB"
data: _hb
#
# When a master node joined the room, perform this sync with him. It will ask the master for the HB,
# and will broadcast his own HB
#
performSyncWithMaster: (user)->
@current_sync_target = user
@send user,
sync_step: "getHB"
send_again: "true"
data: @getStateVector()
hb = @getHB([]).hb
_hb = []
for o in hb
_hb.push o
if _hb.length > 10
@broadcast
sync_step: "applyHB_"
data: _hb
_hb = []
@broadcast
sync_step: "applyHB"
data: _hb
#
# You are sure that all clients are synced, call this function.
#
setStateSynced: ()->
if not @is_synced
@is_synced = true
if @compute_when_synced?
for el in @compute_when_synced
f = el[0]
args = el[1..]
f.apply(args)
delete @compute_when_synced
null
# executed when the a state_vector is received. listener will be called only once!
whenReceivedStateVector: (f)->
@when_received_state_vector_listeners ?= []
@when_received_state_vector_listeners.push f
#
# You received a raw message, and you know that it is intended for to Yjs. Then call this function.
#
receiveMessage: (sender, res)->
if not res.sync_step?
for f in @receive_handlers
f sender, res
else
if sender is @user_id
return
if res.sync_step is "getHB"
# call listeners
if @when_received_state_vector_listeners?
for f in @when_received_state_vector_listeners
f.call this, res.data
delete @when_received_state_vector_listeners
data = @getHB(res.data)
hb = data.hb
_hb = []
# always broadcast, when not synced.
# This reduces errors, when the clients goes offline prematurely.
# When this client only syncs to one other clients, but looses connectors,
# before syncing to the other clients, the online clients have different states.
# Since we do not want to perform regular syncs, this is a good alternative
if @is_synced
sendApplyHB = (m)=>
@send sender, m
else
sendApplyHB = (m)=>
@broadcast m
for o in hb
_hb.push o
if _hb.length > 10
sendApplyHB
sync_step: "applyHB_"
data: _hb
_hb = []
sendApplyHB
sync_step : "applyHB"
data: _hb
if res.send_again? and @perform_send_again
send_again = do (sv = data.state_vector)=>
()=>
hb = @getHB(sv).hb
for o in hb
_hb.push o
if _hb.length > 10
@send sender,
sync_step: "applyHB_"
data: _hb
_hb = []
@send sender,
sync_step: "applyHB",
data: _hb
sent_again: "true"
setTimeout send_again, 3000
else if res.sync_step is "applyHB"
@applyHB(res.data, sender is @current_sync_target)
if (@syncMethod is "syncAll" or res.sent_again?) and (not @is_synced) and ((@current_sync_target is sender) or (not @current_sync_target?))
@connections[sender].is_synced = true
@findNewSyncTarget()
else if res.sync_step is "applyHB_"
@applyHB(res.data, sender is @current_sync_target)
# Currently, the HB encodes operations as JSON. For the moment I want to keep it
# that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
# too much overhead. Y is very likely to get changed a lot in the future
#
# Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
# we encode the JSON as XML.
#
# When the HB support encoding as XML, the format should look pretty much like this.
# does not support primitive values as array elements
# expects an ltx (less than xml) object
parseMessageFromXml: (m)->
parse_array = (node)->
for n in node.children
if n.getAttribute("isArray") is "true"
parse_array n
else
parse_object n
parse_object = (node)->
json = {}
for name, value of node.attrs
int = parseInt(value)
if isNaN(int) or (""+int) isnt value
json[name] = value
else
json[name] = int
for n in node.children
name = n.name
if n.getAttribute("isArray") is "true"
json[name] = parse_array n
else
json[name] = parse_object n
json
parse_object m
# encode message in xml
# we use string because Strophe only accepts an "xml-string"..
# So {a:4,b:{c:5}} will look like
# <y a="4">
# <b c="5"></b>
# </y>
# m - ltx element
# json - guess it ;)
#
encodeMessageToXml: (m, json)->
# attributes is optional
encode_object = (m, json)->
for name,value of json
if not value?
# nop
else if value.constructor is Object
encode_object m.c(name), value
else if value.constructor is Array
encode_array m.c(name), value
else
m.setAttribute(name,value)
m
encode_array = (m, array)->
m.setAttribute("isArray","true")
for e in array
if e.constructor is Object
encode_object m.c("array-element"), e
else
encode_array m.c("array-element"), e
m
if json.constructor is Object
encode_object m.c("y",{xmlns:"http://y.ninja/connector-stanza"}), json
else if json.constructor is Array
encode_array m.c("y",{xmlns:"http://y.ninja/connector-stanza"}), json
else
throw new Error "I can't encode this json!"
setIsBoundToY: ()->
@on_bound_to_y?()
delete @when_bound_to_y
@is_bound_to_y = true

View File

@@ -1,115 +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, fromHB = false)->
if op_json_array.constructor isnt Array
op_json_array = [op_json_array]
for op_json in op_json_array
if fromHB
op_json.fromHB = "true" # execute immediately, if
# $parse_and_execute will return false if $o_json was parsed and executed, otherwise the parsed operadion
o = @parseOperation op_json
o.parsed_from_json = 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,227 +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
# At the beginning (when the user id was not assigned yet),
# the operations are added to buffer._temp. When you finally get your user id,
# the operations are copies from buffer._temp to buffer[id]. Furthermore, when buffer[id] does already contain operations
# (because of a previous session), the uid.op_numbers of the operations have to be reassigned.
# This is what this function does. It adds them to buffer[id],
# and assigns them the correct uid.op_number and uid.creator
setUserId: (@user_id, state_vector)->
@buffer[@user_id] ?= []
buff = @buffer[@user_id]
# we assumed that we started with counter = 0.
# when we receive tha state_vector, and actually have
# counter = 10. Then we have to add 10 to every op_counter
counter_diff = state_vector[@user_id] or 0
if @buffer._temp?
for o_name,o of @buffer._temp
o.uid.creator = @user_id
o.uid.op_number += counter_diff
buff[o.uid.op_number] = o
@operation_counter[@user_id] = (@operation_counter._temp or 0) + counter_diff
delete @operation_counter._temp
delete @buffer._temp
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++}"
}
#
# 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]
if u_name is "_"
continue
for o_number,o of user
if (not o.uid.noOperation?) 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]
@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])) and state_vector[user]?
@operation_counter[user] = state_vector[user]
#
# Increment the operation_counter that defines the current state of the Engine.
#
addToCounter: (o)->
@operation_counter[o.uid.creator] ?= 0
# TODO: check if operations are send in order
if o.uid.op_number is @operation_counter[o.uid.creator]
@operation_counter[o.uid.creator]++
while @buffer[o.uid.creator][@operation_counter[o.uid.creator]]?
@operation_counter[o.uid.creator]++
undefined
module.exports = HistoryBuffer

View File

@@ -1,74 +0,0 @@
class YObject
constructor: (@_object = {})->
if @_object.constructor is Object
for name, val of @_object
if val.constructor is Object
@_object[name] = new YObject(val)
else
throw new Error "Y.Object accepts Json Objects only"
_name: "Object"
_getModel: (types, ops)->
if not @_model?
@_model = new ops.MapManager(@).execute()
for n,o of @_object
@_model.val n, o
delete @_object
@_model
_setModel: (@_model)->
delete @_object
observe: (f)->
@_model.observe f
@
unobserve: (f)->
@_model.unobserve f
@
#
# @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 [*] Depends on the value of the property.
#
# @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 @_model?
@_model.val.apply @_model, arguments
else
if content?
@_object[name] = content
else if name?
@_object[name]
else
res = {}
for n,v of @_object
res[n] = v
res
delete: (name)->
@_model.delete(name)
@
if window?
if window.Y?
window.Y.Object = YObject
else
throw new Error "You must first import Y!"
if module?
module.exports = YObject

View File

@@ -1,678 +0,0 @@
module.exports = ()->
# @see Engine.parse
ops = {}
execution_listener = []
#
# @private
# @abstract
# @nodoc
# A generic interface to ops.
#
# 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 ops.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: (custom_type, uid, content, content_operations)->
if custom_type?
@custom_type = custom_type
@is_deleted = false
@garbage_collected = false
@event_listeners = [] # TODO: rename to observers or sth like that
if uid?
@uid = uid
# see encode to see, why we are doing it this way
if content is undefined
# nop
else if content? and content.creator?
@saveOperation 'content', content
else
@content = content
if content_operations?
@content_operations = {}
for name, op of content_operations
@saveOperation name, op, 'content_operations'
type: "Operation"
getContent: (name)->
if @content?
if @content.getCustomType?
@content.getCustomType()
else if @content.constructor is Object
if name?
if @content[name]?
@content[name]
else
@content_operations[name].getCustomType()
else
content = {}
for n,v of @content
content[n] = v
if @content_operations?
for n,v of @content_operations
v = v.getCustomType()
content[n] = v
content
else
@content
else
@content
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 ops.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: ()->
if @custom_type?
callon = @getCustomType()
else
callon = @
@forwardEvent callon, 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
if @uid.alt? # could be (safely) undefined
map_uid = @uid.alt.cloneUid()
map_uid.sub = @uid.sub
map_uid
else
undefined
cloneUid: ()->
uid = {}
for n,v of @getUid()
uid[n] = v
uid
#
# @private
# If not already done, set the uid
# Add this to the HB
# Notify the all the listeners.
#
execute: ()->
if @validateSavedOperations()
@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()
@
else
false
#
# @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, base = "this")->
if op? and op._getModel?
op = op._getModel(@custom_types, @operations)
#
# 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 not op?
# nop
else if op.execute? or not (op.op_number? and op.creator?)
# is instantiated, or op is string. Currently "Delimiter" is saved as string
# (in combination with @parent you can retrieve the delimiter..)
if base is "this"
@[name] = op
else
dest = @[base]
paths = name.split("/")
last_path = paths.pop()
for path in paths
dest = dest[path]
dest[last_path] = op
else
# not initialized. Do it when calling $validateSavedOperations()
@unchecked ?= {}
@unchecked[base] ?= {}
@unchecked[base][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 = true
for base_name, base of @unchecked
for name, op_uid of base
op = @HB.getOperation op_uid
if op
if base_name is "this"
@[name] = op
else
dest = @[base_name]
paths = name.split("/")
last_path = paths.pop()
for path in paths
dest = dest[path]
dest[last_path] = op
else
uninstantiated[base_name] ?= {}
uninstantiated[base_name][name] = op_uid
success = false
if not success
@unchecked = uninstantiated
return false
else
delete @unchecked
return @
getCustomType: ()->
if not @custom_type?
# throw new Error "This operation was not initialized with a custom type"
@
else
if @custom_type.constructor is String
# has not been initialized yet (only the name is specified)
Type = @custom_types
for t in @custom_type.split(".")
Type = Type[t]
@custom_type = new Type()
@custom_type._setModel @
@custom_type
#
# @private
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: (json = {})->
json.type = @type
json.uid = @getUid()
if @custom_type?
if @custom_type.constructor is String
json.custom_type = @custom_type
else
json.custom_type = @custom_type._name
if @content?.getUid?
json.content = @content.getUid()
else
json.content = @content
if @content_operations?
operations = {}
for n,o of @content_operations
if o._getModel?
o = o._getModel(@custom_types, @operations)
operations[n] = o.getUid()
json.content_operations = operations
json
#
# @nodoc
# A simple Delete-type operation that deletes an operation.
#
class ops.Delete extends ops.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: (custom_type, uid, deletes)->
@saveOperation 'deletes', deletes
super custom_type, 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.
#
ops.Delete.parse = (o)->
{
'uid' : uid
'deletes': deletes_uid
} = o
new this(null, 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 (unimplemented, good idea?)
# - The complete-list (abbrev. cl) maintains all operations
#
class ops.Insert extends ops.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: (custom_type, content, content_operations, parent, uid, prev_cl, next_cl, origin)->
@saveOperation 'parent', parent
@saveOperation 'prev_cl', prev_cl
@saveOperation 'next_cl', next_cl
if origin?
@saveOperation 'origin', origin
else
@saveOperation 'origin', prev_cl
super custom_type, uid, content, content_operations
type: "Insert"
val: ()->
@getContent()
getNext: (i=1)->
n = @
while i > 0 and n.next_cl?
n = n.next_cl
if not n.is_deleted
i--
if n.is_deleted
null
n
getPrev: (i=1)->
n = @
while i > 0 and n.prev_cl?
n = n.prev_cl
if not n.is_deleted
i--
if n.is_deleted
null
else
n
#
# set content to null and other stuff
# @private
#
applyDelete: (o)->
@deleted_by ?= []
callLater = false
if @parent? and not @is_deleted 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
@parent.callOperationSpecificDeleteEvents(this, o)
if @prev_cl? and @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
# delete content
# - we must not do this in applyDelete, because this would lead to inconsistencies
# (e.g. the following operation order must be invertible :
# Insert refers to content, then the content is deleted)
# Therefore, we have to do this in the cleanup
# * NODE: We never delete Insertions!
if @content instanceof ops.Operation and not (@content instanceof ops.Insert)
@content.referenced_by--
if @content.referenced_by <= 0 and not @content.is_deleted
@content.applyDelete()
delete @content
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 @content instanceof ops.Operation
@content.insert_parent = @ # TODO: this is probably not necessary and only nice for debugging
@content.referenced_by ?= 0
@content.referenced_by++
if @parent?
if not @prev_cl?
@prev_cl = @parent.beginning
if not @origin?
@origin = @prev_cl
else if @origin is "Delimiter"
@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
@parent.callOperationSpecificInsertEvents(this)
@
#
# Compute the position of this operation.
#
getPosition: ()->
position = 0
prev = @prev_cl
while true
if prev instanceof ops.Delimiter
break
if not prev.isDeleted()
position++
prev = prev.prev_cl
position
#
# Convert all relevant information of this operation to the json-format.
# This result can be send to other clients.
#
_encode: (json = {})->
json.prev = @prev_cl.getUid()
json.next = @next_cl.getUid()
if @origin.type is "Delimiter"
json.origin = "Delimiter"
else if @origin isnt @prev_cl
json.origin = @origin.getUid()
# if not (json.prev? and json.next?)
json.parent = @parent.getUid()
super json
ops.Insert.parse = (json)->
{
'content' : content
'content_operations' : content_operations
'uid' : uid
'prev': prev
'next': next
'origin' : origin
'parent' : parent
} = json
new this null, content, content_operations, parent, uid, prev, next, origin
#
# @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 ops.Delimiter extends ops.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 null, {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()
}
ops.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
{
'operations' : ops
'execution_listener' : execution_listener
}

View File

@@ -1,533 +0,0 @@
basic_ops_uninitialized = require "./Basic"
module.exports = ()->
basic_ops = basic_ops_uninitialized()
ops = basic_ops.operations
#
# @nodoc
# Manages map like objects. E.g. Json-Type and XML attributes.
#
class ops.MapManager extends ops.Operation
#
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
#
constructor: (custom_type, uid, content, content_operations)->
@_map = {}
super custom_type, uid, content, content_operations
type: "MapManager"
applyDelete: ()->
for name,p of @_map
p.applyDelete()
super()
cleanup: ()->
super()
map: (f)->
for n,v of @_map
f(n,v)
undefined
#
# @see JsonOperations.val
#
val: (name, content)->
if arguments.length > 1
if content? and content._getModel?
rep = content._getModel(@custom_types, @operations)
else
rep = content
@retrieveSub(name).replace rep
@getCustomType()
else if name?
prop = @_map[name]
if prop? and not prop.isContentDeleted()
res = prop.val()
if res instanceof ops.Operation
res.getCustomType()
else
res
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 = @
rm_uid =
noOperation: true
sub: property_name
alt: @
rm = new ops.ReplaceManager null, 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]
ops.MapManager.parse = (json)->
{
'uid' : uid
'custom_type' : custom_type
'content' : content
'content_operations' : content_operations
} = json
new this(custom_type, uid, content, content_operations)
#
# @nodoc
# Manages a list of Insert-type operations.
#
class ops.ListManager extends ops.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: (custom_type, uid, content, content_operations)->
@beginning = new ops.Delimiter undefined, undefined
@end = new ops.Delimiter @beginning, undefined
@beginning.next_cl = @end
@beginning.execute()
@end.execute()
super custom_type, uid, content, content_operations
type: "ListManager"
applyDelete: ()->
o = @beginning
while o?
o.applyDelete()
o = o.next_cl
super()
cleanup: ()->
super()
toJson: (transform_to_value = false)->
val = @val()
for i, o in val
if o instanceof ops.Object
o.toJson(transform_to_value)
else if o instanceof ops.ListManager
o.toJson(transform_to_value)
else if transform_to_value and o instanceof ops.Operation
o.val()
else
o
#
# @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
if not o.is_deleted
result.push o.val()
o = o.next_cl
result
map: (f)->
o = @beginning.next_cl
result = []
while o isnt @end
if not o.is_deleted
result.push f(o)
o = o.next_cl
result
fold: (init, f)->
o = @beginning.next_cl
while o isnt @end
if not o.is_deleted
init = f(init, o)
o = o.next_cl
init
val: (pos)->
if pos?
o = @getOperationByPosition(pos+1)
if not (o instanceof ops.Delimiter)
o.val()
else
throw new Error "this position does not exist"
else
@toArray()
ref: (pos)->
if pos?
o = @getOperationByPosition(pos+1)
if not (o instanceof ops.Delimiter)
o
else
null
# throw new Error "this position does not exist"
else
throw new Error "you must specify a position parameter"
#
# 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 ops.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() and o.prev_cl?
o = o.prev_cl
break
if position <= 0 and not o.isDeleted()
break
o = o.next_cl
if not o.isDeleted()
position -= 1
o
push: (content)->
@insertAfter @end.prev_cl, [content]
insertAfter: (left, contents)->
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
# TODO: always expect an array as content. Then you can combine this with the other option (else)
if contents instanceof ops.Operation
(new ops.Insert null, content, null, undefined, undefined, left, right).execute()
else
for c in contents
if c? and c._name? and c._getModel?
c = c._getModel(@custom_types, @operations)
tmp = (new ops.Insert null, c, null, undefined, undefined, left, right).execute()
left = tmp
@
#
# Inserts an array of content into this list.
# @Note: This expects an array as content!
#
# @return {ListManager Type} This String object.
#
insert: (position, contents)->
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, contents
#
# Deletes a part of the word.
#
# @return {ListManager Type} This String object
#
delete: (position, length = 1)->
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 ops.Delimiter
break
d = (new ops.Delete null, undefined, o).execute()
o = o.next_cl
while (not (o instanceof ops.Delimiter)) and o.isDeleted()
o = o.next_cl
delete_ops.push d._encode()
@
callOperationSpecificInsertEvents: (op)->
getContentType = (content)->
if content instanceof ops.Operation
content.getCustomType()
else
content
@callEvent [
type: "insert"
reference: op
position: op.getPosition()
object: @getCustomType()
changedBy: op.uid.creator
value: getContentType op.val()
]
callOperationSpecificDeleteEvents: (op, del_op)->
@callEvent [
type: "delete"
reference: op
position: op.getPosition()
object: @getCustomType() # TODO: You can combine getPosition + getParent in a more efficient manner! (only left Delimiter will hold @parent)
length: 1
changedBy: del_op.uid.creator
oldValue: op.val()
]
ops.ListManager.parse = (json)->
{
'uid' : uid
'custom_type': custom_type
'content' : content
'content_operations' : content_operations
} = json
new this(custom_type, uid, content, content_operations)
class ops.Composition extends ops.ListManager
constructor: (custom_type, @_composition_value, composition_value_operations, uid, tmp_composition_ref)->
# we can't use @seveOperation 'composition_ref', tmp_composition_ref here,
# because then there is a "loop" (insertion refers to parent, refers to insertion..)
# This is why we have to check in @callOperationSpecificInsertEvents until we find it
super custom_type, uid
if tmp_composition_ref?
@tmp_composition_ref = tmp_composition_ref
else
@composition_ref = @end.prev_cl
if composition_value_operations?
@composition_value_operations = {}
for n,o of composition_value_operations
@saveOperation n, o, '_composition_value'
type: "Composition"
#
# @private
# @see Operation.execute
#
execute: ()->
if @validateSavedOperations()
@getCustomType()._setCompositionValue @_composition_value
delete @_composition_value
# check if tmp_composition_ref already exists
if @tmp_composition_ref
composition_ref = @HB.getOperation @tmp_composition_ref
if composition_ref?
delete @tmp_composition_ref
@composition_ref = composition_ref
super
else
false
#
# This is called, when the Insert-operation was successfully executed.
#
callOperationSpecificInsertEvents: (op)->
if @tmp_composition_ref?
if op.uid.creator is @tmp_composition_ref.creator and op.uid.op_number is @tmp_composition_ref.op_number
@composition_ref = op
delete @tmp_composition_ref
op = op.next_cl
if op is @end
return
else
return
o = @end.prev_cl
while o isnt op
@getCustomType()._unapply o.undo_delta
o = o.prev_cl
while o isnt @end
o.undo_delta = @getCustomType()._apply o.val()
o = o.next_cl
@composition_ref = @end.prev_cl
@callEvent [
type: "update"
changedBy: op.uid.creator
newValue: @val()
]
callOperationSpecificDeleteEvents: (op, del_op)->
return
#
# Create a new Delta
# - inserts new Content at the end of the list
# - updates the composition_value
# - updates the composition_ref
#
# @param delta The delta that is applied to the composition_value
#
applyDelta: (delta, operations)->
(new ops.Insert null, delta, operations, @, null, @end.prev_cl, @end).execute()
undefined
#
# Encode this operation in such a way that it can be parsed by remote peers.
#
_encode: (json = {})->
custom = @getCustomType()._getCompositionValue()
json.composition_value = custom.composition_value
if custom.composition_value_operations?
json.composition_value_operations = {}
for n,o of custom.composition_value_operations
json.composition_value_operations[n] = o.getUid()
if @composition_ref?
json.composition_ref = @composition_ref.getUid()
else
json.composition_ref = @tmp_composition_ref
super json
ops.Composition.parse = (json)->
{
'uid' : uid
'custom_type': custom_type
'composition_value' : composition_value
'composition_value_operations' : composition_value_operations
'composition_ref' : composition_ref
} = json
new this(custom_type, composition_value, composition_value_operations, uid, composition_ref)
#
# @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 ops.ReplaceManager extends ops.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: (custom_type, @event_properties, @event_this, uid)->
if not @event_properties['object']?
@event_properties['object'] = @event_this.getCustomType()
super custom_type, uid
type: "ReplaceManager"
#
# 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
#
# 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-ops for ListManager.
#
callOperationSpecificInsertEvents: (op)->
if op.next_cl.type is "Delimiter" and op.prev_cl.type isnt "Delimiter"
# this replaces another Replaceable
if not op.is_deleted # When this is received from the HB, this could already be deleted!
old_value = op.prev_cl.val()
@callEventDecorator [
type: "update"
changedBy: op.uid.creator
oldValue: old_value
]
op.prev_cl.applyDelete()
else if op.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
op.applyDelete()
else # prev _and_ next are Delimiters. This is the first created Replaceable in the RM
@callEventDecorator [
type: "add"
changedBy: op.uid.creator
]
undefined
callOperationSpecificDeleteEvents: (op, del_op)->
if op.next_cl.type is "Delimiter"
@callEventDecorator [
type: "delete"
changedBy: del_op.uid.creator
oldValue: op.val()
]
#
# 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 ops.Insert null, content, null, @, replaceable_uid, o, o.next_cl).execute()
# TODO: delete repl (for debugging)
undefined
isContentDeleted: ()->
@getLastOperation().isDeleted()
deleteContent: ()->
last_op = @getLastOperation()
if (not last_op.isDeleted()) and last_op.type isnt "Delimiter"
(new ops.Delete null, undefined, @getLastOperation().uid).execute()
undefined
#
# Get the value of this
# @return {String}
#
val: ()->
o = @getLastOperation()
#if o instanceof ops.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)
basic_ops

View File

@@ -1,55 +0,0 @@
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._name 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,new Y.Object(@val)).val(@name)
# TODO: please use instanceof instead of ._name,
# 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._name is "Object"
bindToChildren @
valChanged: ()->
if @val? and @name?
if @val.constructor is Object
@val = @parentElement.val.val(@name, new Y.Object(@val)).val(@name)
# TODO: please use instanceof instead of ._name,
# since it is more safe (consider someone putting a custom Object type here)
else if @val._name is "Object"
bindToChildren @
else if @parentElement.val?.val? and @val isnt @parentElement.val.val(@name)
@parentElement.val.val @name, @val

View File

@@ -1,38 +0,0 @@
structured_ops_uninitialized = require "./Operations/Structured"
HistoryBuffer = require "./HistoryBuffer"
Engine = require "./Engine"
adaptConnector = require "./ConnectorAdapter"
createY = (connector)->
if connector.user_id?
user_id = connector.user_id # TODO: change to getUniqueId()
else
user_id = "_temp"
connector.when_received_state_vector_listeners = [(state_vector)->
HB.setUserId this.user_id, state_vector
]
HB = new HistoryBuffer user_id
ops_manager = structured_ops_uninitialized HB, this.constructor
ops = ops_manager.operations
engine = new Engine HB, ops
adaptConnector connector, engine, HB, ops_manager.execution_listener
ops.Operation.prototype.HB = HB
ops.Operation.prototype.operations = ops
ops.Operation.prototype.engine = engine
ops.Operation.prototype.connector = connector
ops.Operation.prototype.custom_types = this.constructor
ct = new createY.Object()
model = new ops.MapManager(ct, HB.getReservedUniqueIdentifier()).execute()
ct._setModel model
ct
module.exports = createY
if window?
window.Y = createY
createY.Object = require "./ObjectType"

6916
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,65 +1,92 @@
{
"name": "yjs",
"version": "0.5.2",
"description": "A Framework that enables Real-Time Collaboration on arbitrary data structures.",
"main": "./build/node/y.js",
"version": "14.0.0-1",
"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": {
"prepublish": "./node_modules/gulp/bin/gulp.js build_node",
"test": "./node_modules/gulp/bin/gulp.js mocha"
"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 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs",
"debug": "concurrently 'http-server -o test.html' 'npm run watch'",
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs",
"postinstall": "node ./sponsor-y.js"
},
"exports": {
".": {
"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",
"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.43"
},
"devDependencies": {
"chai": "^2.2.0",
"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-exit": "0.0.2",
"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-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",
"underscore": "^1.6.0",
"mocha": "^2.1.0",
"sinon": "^1.12.2",
"sinon-chai": "^2.7.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']
}]

12
sponsor-y.js Normal file
View File

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

110
src/index.js Normal file
View File

@@ -0,0 +1,110 @@
/** 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,
typeMapGetSnapshot,
createDocFromSnapshot,
iterateDeletedStructs,
applyUpdate,
applyUpdateV2,
readUpdate,
readUpdateV2,
encodeStateAsUpdate,
encodeStateAsUpdateV2,
encodeStateVector,
UndoManager,
decodeSnapshot,
encodeSnapshot,
decodeSnapshotV2,
encodeSnapshotV2,
decodeStateVector,
logUpdate,
logUpdateV2,
relativePositionToJSON,
isDeleted,
isParentOf,
equalSnapshots,
PermanentUserData, // @TODO experimental
tryGc,
transact,
AbstractConnector,
logType,
mergeUpdates,
mergeUpdatesV2,
parseUpdateMeta,
parseUpdateMetaV2,
encodeStateVectorFromUpdate,
encodeStateVectorFromUpdateV2,
encodeRelativePosition,
decodeRelativePosition,
diffUpdate,
diffUpdateV2
} from './internals.js'
const glo = /** @type {any} */ (typeof window !== 'undefined'
? window
: typeof global !== 'undefined' ? global : {})
const importIdentifier = '__ $YJS$ __'
if (glo[importIdentifier] === true) {
/**
* Dear reader of this warning message. Please take this seriously.
*
* If you see this message, please make sure that you only import one version of Yjs. In many cases,
* your package manager installs two versions of Yjs that are used by different packages within your project.
* Another reason for this message is that some parts of your project use the commonjs version of Yjs
* and others use the EcmaScript version of Yjs.
*
* This often leads to issues that are hard to debug. We often need to perform constructor checks,
* e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to
* do the constructor checks anymore - which might break the CRDT algorithm.
*/
console.warn('Yjs was already imported. Importing different versions of Yjs often leads to issues.')
}
glo[importIdentifier] = true

45
src/internals.js Normal file
View File

@@ -0,0 +1,45 @@
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/ListCursor.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/ContentMove.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.readString(), 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)
}

296
src/structs/ContentMove.js Normal file
View File

@@ -0,0 +1,296 @@
import * as error from 'lib0/error'
import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding'
import * as math from 'lib0/math'
import {
writeID,
readID,
ID, AbstractType, ContentType, RelativePosition, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore, getItem, getItemCleanStart, getItemCleanEnd, // eslint-disable-line
addsStruct
} from '../internals.js'
/**
* @param {ContentMove | { start: RelativePosition, end: RelativePosition }} moved
* @param {Transaction} tr
* @param {boolean} split
* @return {{ start: Item, end: Item }} $start (inclusive) is the beginning and $end (inclusive) is the end of the moved area
*/
export const getMovedCoords = (moved, tr, split) => {
const store = tr.doc.store
const startItem = moved.start.item
const endItem = moved.end.item
let start // this (inclusive) is the beginning of the moved area
let end // this (exclusive) is the first item after start that is not part of the moved area
if (startItem) {
if (moved.start.assoc < 0) {
// We know that the items have already been split, hence getItem suffices.
start = split ? getItemCleanEnd(tr, startItem) : getItem(store, startItem)
start = start.right
} else {
start = split ? getItemCleanStart(tr, startItem) : getItem(store, startItem)
}
} else if (moved.start.tname != null) {
start = tr.doc.get(moved.start.tname)._start
} else if (moved.start.type) {
start = /** @type {ContentType} */ (getItem(store, moved.start.type).content).type._start
} else {
error.unexpectedCase()
}
if (endItem) {
if (moved.end.assoc < 0) {
end = split ? getItemCleanEnd(tr, endItem) : getItem(store, endItem)
end = end.right
} else {
end = split ? getItemCleanStart(tr, endItem) : getItem(store, endItem)
}
} else {
error.unexpectedCase()
}
return { start: /** @type {Item} */ (start), end: /** @type {Item} */ (end) }
}
/**
* @param {Transaction} tr
* @param {ContentMove} moved
* @param {Item} movedItem
* @param {Set<Item>} trackedMovedItems
* @return {boolean} true if there is a loop
*/
export const findMoveLoop = (tr, moved, movedItem, trackedMovedItems) => {
if (trackedMovedItems.has(movedItem)) {
return true
}
trackedMovedItems.add(movedItem)
/**
* @type {{ start: Item | null, end: Item | null }}
*/
let { start, end } = getMovedCoords(moved, tr, false)
while (start !== end && start != null) {
if (
!start.deleted &&
start.moved === movedItem &&
start.content.constructor === ContentMove &&
findMoveLoop(tr, start.content, start, trackedMovedItems)
) {
return true
}
start = start.right
}
return false
}
/**
* @private
*/
export class ContentMove {
/**
* @param {RelativePosition} start
* @param {RelativePosition} end
* @param {number} priority if we want to move content that is already moved, we need to assign a higher priority to this move operation.
*/
constructor (start, end, priority) {
this.start = start
this.end = end
this.priority = priority
/**
* We store which Items+ContentMove we override. Once we delete
* this ContentMove, we need to re-integrate the overridden items.
*
* This representation can be improved if we ever run into memory issues because of too many overrides.
* Ideally, we should probably just re-iterate the document and re-integrate all moved items.
* This is fast enough and reduces memory footprint significantly.
*
* @type {Set<Item>}
*/
this.overrides = new Set()
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [null]
}
/**
* @return {boolean}
*/
isCountable () {
return false
}
/**
* @return {ContentMove}
*/
copy () {
return new ContentMove(this.start, this.end, this.priority)
}
/**
* @param {number} offset
* @return {ContentMove}
*/
splice (offset) {
return this
}
/**
* @param {ContentMove} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
const sm = /** @type {AbstractType<any>} */ (item.parent)._searchMarker
if (sm) sm.length = 0
const movedCoords = getMovedCoords(this, transaction, true)
/**
* @type {{ start: Item | null, end: item | null }}
*/
let { start, end } = movedCoords
let maxPriority = 0
// If this ContentMove was created locally, we set prio = -1. This indicates
// that we want to set prio to the current prio-maximum of the moved range.
const adaptPriority = this.priority < 0
while (start !== end && start != null) {
const prevMove = start.moved // this is the same as prevMove
const nextPrio = prevMove ? /** @type {ContentMove} */ (prevMove.content).priority : -1
if (adaptPriority || nextPrio < this.priority || (prevMove != null && nextPrio === this.priority && (prevMove.id.client < item.id.client || (prevMove.id.client === item.id.client && prevMove.id.clock < item.id.clock)))) {
if (prevMove !== null) {
if (/** @type {ContentMove} */ (prevMove.content).isCollapsed()) {
prevMove.deleteAsCleanup(transaction, adaptPriority)
}
this.overrides.add(prevMove)
if (start !== movedCoords.start) {
// only add this to mergeStructs if this is not the first item
transaction._mergeStructs.push(start)
}
}
maxPriority = math.max(maxPriority, nextPrio)
// was already moved
if (prevMove && !transaction.prevMoved.has(start) && !addsStruct(transaction, prevMove)) {
// only override prevMoved if the prevMoved item is not new
// we need to know which item previously moved an item
transaction.prevMoved.set(start, prevMove)
}
start.moved = item
if (!start.deleted && start.content.constructor === ContentMove && findMoveLoop(transaction, start.content, start, new Set([item]))) {
item.deleteAsCleanup(transaction, adaptPriority)
return
}
} else if (prevMove != null) {
/** @type {ContentMove} */ (prevMove.content).overrides.add(item)
}
start = start.right
}
if (adaptPriority) {
this.priority = maxPriority + 1
}
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
delete (transaction, item) {
/**
* @type {{ start: Item | null, end: Item | null }}
*/
let { start, end } = getMovedCoords(this, transaction, false)
while (start !== end && start != null) {
if (start.moved === item) {
const prevMoved = transaction.prevMoved.get(start)
if (addsStruct(transaction, item)) {
if (prevMoved === item) {
// Edge case: Item has been moved by this move op and it has been created & deleted in the same transaction (hence no effect that should be emitted by the change computation)
transaction.prevMoved.delete(start)
}
} else if (prevMoved == null) { // && !addsStruct(tr, item)
// Normal case: item has been moved by this move and it has not been created & deleted in the same transaction
transaction.prevMoved.set(start, item)
}
start.moved = null
}
start = start.right
}
/**
* @param {Item} reIntegrateItem
*/
const reIntegrate = reIntegrateItem => {
const content = /** @type {ContentMove} */ (reIntegrateItem.content)
// content is not yet transformed to a ContentDeleted
if (content.getRef() === 11) {
if (reIntegrateItem.deleted) {
// potentially we can integrate the items that reIntegrateItem overrides
content.overrides.forEach(reIntegrate)
} else {
content.integrate(transaction, reIntegrateItem)
}
}
}
this.overrides.forEach(reIntegrate)
}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
const isCollapsed = this.isCollapsed()
encoding.writeVarUint(encoder.restEncoder, (isCollapsed ? 1 : 0) | (this.start.assoc >= 0 ? 2 : 0) | (this.end.assoc >= 0 ? 4 : 0) | this.priority << 6)
writeID(encoder.restEncoder, /** @type {ID} */ (this.start.item))
if (!isCollapsed) {
writeID(encoder.restEncoder, /** @type {ID} */ (this.end.item))
}
}
/**
* @return {number}
*/
getRef () {
return 11
}
isCollapsed () {
return this.start.item === this.end.item && this.start.item !== null
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentMove}
*/
export const readContentMove = decoder => {
const info = decoding.readVarUint(decoder.restDecoder)
const isCollapsed = (info & 1) === 1
const startAssoc = (info & 2) === 2 ? 0 : -1
const endAssoc = (info & 4) === 4 ? 0 : -1
// @TODO use BIT3 & BIT4 to indicate the case `null` is the start/end
// BIT5 is reserved for future extensions
const priority = info >>> 6
const startId = readID(decoder.restDecoder)
const start = new RelativePosition(null, null, startId, startAssoc)
const end = new RelativePosition(null, null, isCollapsed ? startId : readID(decoder.restDecoder), endAssoc)
return new ContentMove(start, end, priority)
}

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<YEvent>} 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 {
// Whis 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
}
}

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

@@ -0,0 +1,870 @@
import {
GC,
getState,
AbstractStruct,
replaceStruct,
addStruct,
addToDeleteSet,
findRootTypeKey,
compareIDs,
getItem,
getItemCleanEnd,
getItemCleanStart,
readContentDeleted,
readContentBinary,
readContentJSON,
readContentAny,
readContentString,
readContentEmbed,
readContentDoc,
createID,
readContentFormat,
readContentType,
readContentMove,
addChangedTypeToTransaction,
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'
import { ContentMove } from './ContentMove.js'
/**
* @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
if (leftItem.moved) {
rightItem.moved = leftItem.moved
const m = transaction.prevMoved.get(leftItem)
if (m) {
transaction.prevMoved.set(rightItem, m)
}
}
return rightItem
}
/**
* Redoes the effect of this operation.
*
* @param {Transaction} transaction The Yjs instance.
* @param {Item} item
* @param {Set<Item>} redoitems
* @param {Array<Item>} itemsToDelete
*
* @return {Item|null}
*
* @private
*/
export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
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
/**
* @type {Item|null}
*/
let right
if (item.parentSub === null) {
// Is an array item. Insert at the old position
left = item.left
right = item
} else {
// Is a map item. Insert as current value
left = item
while (left.right !== null) {
left = left.right
if (left.id.client !== ownClientID) {
// It is not possible to redo this item because it conflicts with a
// change from another client
return null
}
}
if (left.right !== null) {
left = /** @type {Item} */ (/** @type {AbstractType<any>} */ (item.parent)._map.get(item.parentSub))
}
right = null
}
// make sure that parent is redone
if (parentItem !== null && parentItem.deleted === true && parentItem.redone === null) {
// try to undo parent if it will be undone anyway
if (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete) === null) {
return null
}
}
if (parentItem !== null && parentItem.redone !== null) {
while (parentItem.redone !== null) {
parentItem = getItemCleanStart(transaction, parentItem.redone)
}
// find next cloned_redo items
while (left !== null) {
/**
* @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
}
// 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 && left.right !== right && itemsToDelete.findIndex(d => d === /** @type {Item} */ (left).right) >= 0) {
left = left.right
}
}
const nextClock = getState(store, ownClientID)
const nextId = createID(ownClientID, nextClock)
const redoneItem = new Item(
nextId,
left, left && left.lastId,
right, right && right.id,
parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type,
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 reundone this type refers to the type-id that undid
* this operation.
*
* @type {ID | null}
*/
this.redone = null
/**
* This property is reused by the moved prop. In this case this property refers to an Item.
*
* @type {Item | null}
*/
this.moved = 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
}
if (this.content.constructor === ContentMove) {
const c = /** @type {ContentMove} */ (this.content)
const start = c.start.item
const end = c.isCollapsed() ? null : c.end.item
if (start && start.clock >= getState(store, start.client)) {
return start.client
}
if (end && end.clock >= getState(store, end.client)) {
return end.client
}
}
// We have all missing ids, now find the items
if (this.origin) {
this.left = getItemCleanEnd(transaction, 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, 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
}
// check if this item is in a moved range
if ((this.left && this.left.moved) || (this.right && this.right.moved)) {
const leftMoved = this.left && this.left.moved && /** @type {ContentMove} */ (this.left.moved.content)
const rightMoved = this.right && this.right.moved && /** @type {ContentMove} */ (this.right.moved.content)
if (leftMoved === rightMoved) {
this.moved = /** @type {Item} */ (this.left).moved
} else if (
(leftMoved != null && !leftMoved.isCollapsed()) ||
(rightMoved != null && !rightMoved.isCollapsed())
) {
// We know that this item is on the edge of a moved range.
// @todo Instead, we could check to which moved-range this item belongs
// This approach (reintegration) is pretty expensive in some scenarios
leftMoved && leftMoved.integrate(transaction, /** @type {any} */ (this.left).moved)
rightMoved && rightMoved.integrate(transaction, /** @type {any} */ (this.right).moved)
}
}
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.moved === right.moved &&
this.content.constructor === right.content.constructor &&
this.content.mergeWith(right.content)
) {
if (right.marker) {
// Right will be "forgotten", so we delete all
// search markers that reference right.
const searchMarker = /** @type {AbstractType<any>} */ (this.parent)._searchMarker
if (searchMarker) {
for (let i = searchMarker.length - 1; i >= 0; i--) {
if (searchMarker[i].nextItem === right) {
// @todo do something more efficient than splicing..
searchMarker.splice(i, 1)
}
}
}
}
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, this)
}
}
/**
* Similar to `this.delete(tr)`, but additionally ensures
* that the deleted range is broadcasted using a different
* origin/source in a separate update event, so that
* the providers don't filter this message.
*
* @param {Transaction} transaction
* @param {boolean} isLocal
*/
deleteAsCleanup (transaction, isLocal) {
this.delete(transaction)
if (!isLocal) {
addToDeleteSet(transaction.cleanupDeletions, this.id.client, this.id.clock, this.length)
}
}
/**
* @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
readContentMove // 11
]
/**
* 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
* @param {Item} item
*/
delete (transaction, item) {
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
}
}

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

@@ -0,0 +1,563 @@
import {
removeEventHandlerListener,
callEventHandlerListeners,
addEventHandlerListener,
createEventHandler,
getState,
isVisible,
ContentType,
createID,
ContentAny,
ContentBinary,
ListCursor,
ContentDoc, 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 = 300
const freshSearchMarkerDistance = 30
/**
* 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.
*
* @template T
* @param {Transaction} tr
* @param {AbstractType<any>} yarray
* @param {number} index
* @param {function(ListCursor):T} f
* @return T
*/
export const useSearchMarker = (tr, yarray, index, f) => {
const searchMarker = yarray._searchMarker
if (searchMarker === null || yarray._start === null || index < freshSearchMarkerDistance) {
return f(new ListCursor(yarray).forward(tr, index, true))
}
if (searchMarker.length === 0) {
const sm = new ListCursor(yarray).forward(tr, index, true)
searchMarker.push(sm)
if (sm.nextItem) sm.nextItem.marker = true
}
const sm = searchMarker.reduce(
(a, b, arrayIndex) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b
)
const newIsCheaper = math.abs(sm.index - index) >= index
const createFreshMarker = searchMarker.length < maxSearchMarker && (math.abs(sm.index - index) > freshSearchMarkerDistance || newIsCheaper)
const fsm = createFreshMarker ? (newIsCheaper ? new ListCursor(yarray) : sm.clone()) : sm
const prevItem = /** @type {Item} */ (sm.nextItem)
if (createFreshMarker) {
searchMarker.push(fsm)
}
const diff = fsm.index - index
if (diff > 0) {
fsm.backward(tr, diff)
} else {
fsm.forward(tr, -diff, true)
}
const result = f(fsm)
if (fsm.reachedEnd) {
fsm.reachedEnd = false
const nextItem = /** @type {Item} */ (fsm.nextItem)
if (nextItem.countable && !nextItem.deleted) {
fsm.index -= nextItem.length
}
fsm.rel = 0
}
fsm.index -= fsm.rel
fsm.rel = 0
if (!createFreshMarker) {
// reused old marker and we moved to a different position
prevItem.marker = false
}
const fsmItem = fsm.nextItem
if (fsmItem) {
if (fsmItem.marker) {
// already marked, forget current iterator
searchMarker.splice(searchMarker.findIndex(m => m === fsm), 1)
} else {
fsmItem.marker = true
}
}
return result
}
/**
* Update markers when a change happened.
*
* This should be called before doing a deletion!
*
* @param {Array<ListCursor>} searchMarker
* @param {number} index
* @param {number} len If insertion, len is positive. If deletion, len is negative.
* @param {ListCursor|null} origSearchMarker Do not update this searchmarker because it is the one we used to manipulate. @todo !=null for improved perf in ytext
*/
export const updateMarkerChanges = (searchMarker, index, len, origSearchMarker) => {
for (let i = searchMarker.length - 1; i >= 0; i--) {
const marker = searchMarker[i]
if (marker !== origSearchMarker) {
if (len > 0 && index === marker.index) {
// inserting at a marked position deletes the marked position because we can't do a simple transformation
// (we don't know whether to insert directly before or directly after the position)
searchMarker.splice(i, 1)
if (marker.nextItem) marker.nextItem.marker = false
continue
}
if (index < marker.index) { // a simple index <= m.index check would actually suffice
marker.index = math.max(index, marker.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>,Transaction>}
*/
this._dEH = createEventHandler()
/**
* @type {null | Array<ListCursor>}
*/
this._searchMarker = null
/**
* You can store custom stuff here.
* This might be useful to associate your application state to this shared type.
*
* @type {Map<any, any>}
*/
this.meta = new Map()
}
/**
* @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>,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>,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 {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()
}
/**
* @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)

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

@@ -0,0 +1,341 @@
/**
* @module YArray
*/
import {
YEvent,
AbstractType,
YArrayRefID,
callTypeObservers,
transact,
ListCursor,
useSearchMarker,
createRelativePositionFromTypeIndex,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, // eslint-disable-line
getMinimalListViewRanges
} from '../internals.js'
/**
* Event that describes the changes on a YArray
* @template 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<ListCursor>}
*/
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 (content.length > 0) {
if (this.doc !== null) {
transact(this.doc, transaction => {
useSearchMarker(transaction, this, index, walker =>
walker.insertArrayValue(transaction, content)
)
})
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
}
}
}
/**
* Move a single item from $index to $target.
*
* If the original item is to the left of $target, then the index of the item will decrement.
*
* ```js
* yarray.insert(0, [1, 2, 3])
* yarray.move(0, 3) // move "1" to index 3
* yarray.toArray() // => [2, 3, 1]
* yarray.move(2, 0) // move "1" to index 0
* yarray.toArray() // => [1, 2, 3]
* ```
*
* @param {number} index
* @param {number} target
*/
move (index, target) {
if (index === target || index + 1 === target || index >= this.length) {
// It doesn't make sense to move a range into the same range (it's basically a no-op).
return
}
if (this.doc !== null) {
transact(this.doc, transaction => {
const start = createRelativePositionFromTypeIndex(this, index, 1)
const end = start.clone()
end.assoc = -1
useSearchMarker(transaction, this, target, walker => {
walker.insertMove(transaction, [{ start, end }])
})
})
} else {
const content = /** @type {Array<any>} */ (this._prelimContent).splice(index, 1)
;/** @type {Array<any>} */ (this._prelimContent).splice(target, 0, ...content)
}
}
/**
* @experimental
*
* @param {number} startIndex Inclusive move-start
* @param {number} endIndex Inclusive move-end
* @param {number} target
* @param {number} assocStart >=0 if start should be associated with the right character. See relative-position assoc parameter.
* @param {number} assocEnd >= 0 if end should be associated with the right character.
*/
moveRange (startIndex, endIndex, target, assocStart = 1, assocEnd = -1) {
if (
(startIndex <= target && target <= endIndex) || // It doesn't make sense to move a range into the same range (it's basically a no-op).
endIndex - startIndex < 0 // require length of >= 0
) {
return
}
if (this.doc !== null) {
transact(this.doc, transaction => {
const ranges = useSearchMarker(transaction, this, startIndex, walker =>
getMinimalListViewRanges(transaction, walker, endIndex - startIndex + 1)
)
useSearchMarker(transaction, this, target, walker => {
walker.insertMove(transaction, ranges)
})
})
} else {
const content = /** @type {Array<any>} */ (this._prelimContent).splice(startIndex, endIndex - startIndex + 1)
;/** @type {Array<any>} */ (this._prelimContent).splice(target, 0, ...content)
}
}
/**
* Appends content to this YArray.
*
* @param {Array<T>} content Array of content to append.
*/
push (content) {
this.insert(this.length, 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 => {
useSearchMarker(transaction, this, index, walker =>
walker.delete(transaction, 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 transact(/** @type {Doc} */ (this.doc), transaction =>
useSearchMarker(transaction, this, index, walker =>
walker.slice(transaction, 1)[0]
)
)
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<T>}
*/
toArray () {
return transact(/** @type {Doc} */ (this.doc), tr =>
new ListCursor(this).slice(tr, this.length)
)
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @param {number} [start]
* @param {number} [end]
* @return {Array<T>}
*/
slice (start = 0, end = this.length) {
return transact(/** @type {Doc} */ (this.doc), transaction =>
useSearchMarker(transaction, this, start, walker =>
walker.slice(transaction, end < 0 ? this.length + end - start : end - start)
)
)
}
/**
* 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 transact(/** @type {Doc} */ (this.doc), tr =>
new ListCursor(this).map(tr, 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) {
return transact(/** @type {Doc} */ (this.doc), tr =>
new ListCursor(this).forEach(tr, f)
)
}
/**
* @return {IterableIterator<T>}
*/
[Symbol.iterator] () {
// @todo, this could be optimized using a real iterator
return this.toArray().values()
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YArrayRefID)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
*
* @private
* @function
*/
export const readYArray = decoder => new YArray()

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

@@ -0,0 +1,269 @@
/**
* @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
* 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) {
/**
* @type {Object<string,MapType>}
*/
const map = {}
this._map.forEach((item, key) => {
if (!item.deleted) {
f(item.content.getContent()[item.length - 1], key, this)
}
})
return map
}
/**
* @return {IterableIterator<MapType>}
*/
[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()

1250
src/types/YText.js Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,207 @@
import {
YXmlFragment,
transact,
typeMapDelete,
typeMapHas,
typeMapSet,
typeMapGet,
typeMapGetAll,
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.
*
* @param {Snapshot} [snapshot]
* @return {Object<string, any>} A JSON Object that describes the attributes.
*
* @public
*/
getAttributes (snapshot) {
return typeMapGetAll(this)
}
/**
* 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())

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

@@ -0,0 +1,39 @@
import {
YEvent,
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
} from '../internals.js'
/**
* 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)
}
})
}
}

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

@@ -0,0 +1,409 @@
/**
* @module YXml
*/
import {
YXmlEvent,
YXmlElement,
AbstractType,
typeListInsertGenericsAfter,
typeListToArray,
YXmlFragmentRefID,
callTypeObservers,
transact,
typeListSlice,
useSearchMarker,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot, // eslint-disable-line
ListCursor
} 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 () {
if (this.doc != null) {
return transact(this.doc, tr => new ListCursor(this).map(tr, xml => xml.toString()).join(''))
}
return ''
}
/**
* @return {string}
*/
toJSON () {
return this.toString()
}
/**
* 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) {
return transact(this.doc, transaction =>
useSearchMarker(transaction, this, index, walker =>
walker.insertArrayValue(transaction, 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(/** @type {Doc} */ (this.doc), transaction =>
useSearchMarker(transaction, this, index, walker =>
walker.delete(transaction, 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 transact(/** @type {Doc} */ (this.doc), transaction =>
useSearchMarker(transaction, this, index, walker =>
walker.slice(transaction, 1)[0]
)
)
}
/**
* 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)
}
/**
* 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()

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

@@ -0,0 +1,65 @@
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
}
/**
* 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())

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

@@ -0,0 +1,98 @@
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
}
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>>}
*/
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()
}

740
src/utils/ListCursor.js Normal file
View File

@@ -0,0 +1,740 @@
import * as error from 'lib0/error'
import {
getItemCleanStart,
createID,
getMovedCoords,
updateMarkerChanges,
getState,
ContentAny,
ContentBinary,
ContentType,
ContentDoc,
Doc,
compareIDs,
createRelativePosition,
RelativePosition, ID, AbstractContent, ContentMove, Transaction, Item, AbstractType // eslint-disable-line
} from '../internals.js'
import { compareRelativePositions } from './RelativePosition.js'
import * as array from 'lib0/array'
const lengthExceeded = error.create('Length exceeded!')
/**
* We keep the moved-stack across several transactions. Local or remote changes can invalidate
* "moved coords" on the moved-stack.
*
* The reason for this is that if assoc < 0, then getMovedCoords will return the target.right item.
* While the computed item is on the stack, it is possible that a user inserts something between target
* and the item on the stack. Then we expect that the newly inserted item is supposed to be on the new
* computed item.
*
* @param {Transaction} tr
* @param {ListCursor} li
*/
const popMovedStack = (tr, li) => {
let { start, end, move } = li.movedStack.pop() || { start: null, end: null, move: null }
if (move) {
const moveContent = /** @type {ContentMove} */ (move.content)
if (
(
moveContent.start.assoc < 0 && (
(start === null && moveContent.start.item !== null) ||
(start !== null && !compareIDs(/** @type {Item} */ (start.left).lastId, moveContent.start.item))
)
) || (
moveContent.end.assoc < 0 && (
(end === null && moveContent.end.item !== null) ||
(end !== null && !compareIDs(/** @type {Item} */ (end.left).lastId, moveContent.end.item))
)
)
) {
const coords = getMovedCoords(moveContent, tr, false)
start = coords.start
end = coords.end
}
}
li.currMove = move
li.currMoveStart = start
li.currMoveEnd = end
li.reachedEnd = false
}
/**
* Structure that helps to iterate through list-like structures. This is a useful abstraction that keeps track of move operations.
*/
export class ListCursor {
/**
* @param {AbstractType<any>} type
*/
constructor (type) {
this.type = type
/**
* Current index-position
*/
this.index = 0
/**
* Relative position to the current item (if item.content.length > 1)
*/
this.rel = 0
/**
* This refers to the current right item, unless reachedEnd is true. Then it refers to the left item.
*
* @public
* @type {Item | null}
*/
this.nextItem = type._start
this.reachedEnd = type._start === null
/**
* @type {Item | null}
*/
this.currMove = null
/**
* @type {Item | null}
*/
this.currMoveStart = null
/**
* @type {Item | null}
*/
this.currMoveEnd = null
/**
* @type {Array<{ start: Item | null, end: Item | null, move: Item }>}
*/
this.movedStack = []
}
clone () {
const iter = new ListCursor(this.type)
iter.index = this.index
iter.rel = this.rel
iter.nextItem = this.nextItem
iter.reachedEnd = this.reachedEnd
iter.currMove = this.currMove
iter.currMoveStart = this.currMoveStart
iter.currMoveEnd = this.currMoveEnd
iter.movedStack = this.movedStack.slice()
return iter
}
/**
* @type {Item | null}
*/
get left () {
if (this.reachedEnd) {
return this.nextItem
} else {
return this.nextItem && this.nextItem.left
}
}
/**
* @type {Item | null}
*/
get right () {
if (this.reachedEnd) {
return null
} else {
return this.nextItem
}
}
/**
* @param {Transaction} tr
* @param {number} index
*/
moveTo (tr, index) {
const diff = index - this.index
if (diff > 0) {
this.forward(tr, diff, true)
} else if (diff < 0) {
this.backward(tr, -diff)
}
}
/**
* When using skipUncountables=false within a "useSearchMarker" call, it is recommended
* to move the marker to the end. @todo do this after each useSearchMarkerCall
*
* @param {Transaction} tr
* @param {number} len
* @param {boolean} skipUncountables Iterate as much as possible iterating over uncountables until we find the next item.
*/
forward (tr, len, skipUncountables) {
if (len === 0 && this.nextItem == null) {
return this
}
if (this.index + len > this.type._length || this.nextItem == null) {
throw lengthExceeded
}
let item = /** @type {Item} */ (this.nextItem)
this.index += len
// eslint-disable-next-line no-unmodified-loop-condition
while ((!this.reachedEnd || this.currMove !== null) && (len > 0 || (skipUncountables && len === 0 && item && (!item.countable || item.deleted || item === this.currMoveEnd || (this.reachedEnd && this.currMoveEnd === null) || item.moved !== this.currMove)))) {
if (item === this.currMoveEnd || (this.currMoveEnd === null && this.reachedEnd && this.currMove)) {
item = /** @type {Item} */ (this.currMove) // we iterate to the right after the current condition
popMovedStack(tr, this)
} else if (item === null) {
error.unexpectedCase() // should never happen
} else if (item.countable && !item.deleted && item.moved === this.currMove && len > 0) {
len -= item.length
if (len < 0) {
this.rel = item.length + len
len = 0
break
}
} else if (item.content.constructor === ContentMove && item.moved === this.currMove) {
if (this.currMove) {
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
}
const { start, end } = getMovedCoords(item.content, tr, false)
this.currMove = item
this.currMoveStart = start
this.currMoveEnd = end
item = start
continue
}
if (this.reachedEnd) {
throw error.unexpectedCase
}
if (item.right) {
item = item.right
} else {
this.reachedEnd = true
}
}
this.index -= len
this.nextItem = item
return this
}
/**
* We prefer to insert content outside of a moved range.
* Try to escape the moved range by walking to the left over deleted items.
*
* @param {Transaction} tr
*/
reduceMoveDepth (tr) {
let nextItem = this.nextItem
if (nextItem !== null) {
while (this.currMove) {
if (nextItem === this.currMoveStart) {
nextItem = /** @type {Item} */ (this.currMove) // we iterate to the left after the current condition
popMovedStack(tr, this)
continue
}
// check if we can iterate to the left while stepping over deleted items until we find an item === this.currMoveStart
/**
* @type {Item} nextItem
*/
let item = nextItem
while (item.deleted && item.moved === this.currMove && item !== this.currMoveStart) {
item = /** @type {Item} */ (item.left) // this must exist otherwise we miscalculated the move
}
if (item === this.currMoveStart) {
// we only want to iterate over deleted items if we can escape a move
nextItem = item
} else {
break
}
}
this.nextItem = nextItem
}
}
/**
* @param {Transaction} tr
* @param {number} len
* @return {ListCursor}
*/
backward (tr, len) {
if (this.index - len < 0) {
throw lengthExceeded
}
this.index -= len
if (this.reachedEnd) {
const nextItem = /** @type {Item} */ (this.nextItem)
this.rel = nextItem.countable && !nextItem.deleted ? nextItem.length : 0
this.reachedEnd = false
}
if (this.rel >= len) {
this.rel -= len
return this
}
let item = this.nextItem
if (item && item.content.constructor === ContentMove) {
item = item.left
} else {
len += ((item && item.countable && !item.deleted && item.moved === this.currMove) ? item.length : 0) - this.rel
}
this.rel = 0
while (item && len > 0) {
if (item.countable && !item.deleted && item.moved === this.currMove) {
len -= item.length
if (len < 0) {
this.rel = -len
len = 0
}
if (len === 0) {
break
}
} else if (item.content.constructor === ContentMove && item.moved === this.currMove) {
if (this.currMove) {
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
}
const { start, end } = getMovedCoords(item.content, tr, false)
this.currMove = item
this.currMoveStart = start
this.currMoveEnd = end
item = start
continue
}
if (item === this.currMoveStart) {
item = /** @type {Item} */ (this.currMove) // we iterate to the left after the current condition
popMovedStack(tr, this)
}
item = item.left
}
this.nextItem = item
return this
}
/**
* @template {{length: number}} T
* @param {Transaction} tr
* @param {number} len
* @param {T} value the initial content
* @param {function(AbstractContent, number, number):T} slice
* @param {function(T, T): T} concat
*/
_slice (tr, len, value, slice, concat) {
if (this.index + len > this.type._length) {
throw lengthExceeded
}
this.index += len
/**
* We store nextItem in a variable because this version cannot be null.
*/
let nextItem = /** @type {Item} */ (this.nextItem)
while (len > 0 && !this.reachedEnd) {
while (nextItem.countable && !this.reachedEnd && len > 0 && nextItem !== this.currMoveEnd) {
if (!nextItem.deleted && nextItem.moved === this.currMove) {
const slicedContent = slice(nextItem.content, this.rel, len)
len -= slicedContent.length
value = concat(value, slicedContent)
if (this.rel + slicedContent.length === nextItem.length) {
this.rel = 0
} else {
this.rel += slicedContent.length
continue // do not iterate to item.right
}
}
if (nextItem.right) {
nextItem = nextItem.right
} else {
this.reachedEnd = true
}
}
if ((!this.reachedEnd || this.currMove !== null) && len > 0) {
// always set nextItem before any method call
this.nextItem = nextItem
this.forward(tr, 0, true)
nextItem = this.nextItem
}
}
this.nextItem = nextItem
if (len < 0) {
this.index -= len
}
return value
}
/**
* @param {Transaction} tr
* @param {number} len
*/
delete (tr, len) {
const startLength = len
const sm = this.type._searchMarker
let item = this.nextItem
if (this.index + len > this.type._length) {
throw lengthExceeded
}
while (len > 0) {
while (item && !item.deleted && item.countable && !this.reachedEnd && len > 0 && item.moved === this.currMove && item !== this.currMoveEnd) {
if (this.rel > 0) {
item = getItemCleanStart(tr, createID(item.id.client, item.id.clock + this.rel))
this.rel = 0
}
if (len < item.length) {
getItemCleanStart(tr, createID(item.id.client, item.id.clock + len))
}
len -= item.length
item.delete(tr)
if (item.right) {
item = item.right
} else {
this.reachedEnd = true
}
}
if (len > 0) {
this.nextItem = item
this.forward(tr, 0, true)
item = this.nextItem
}
}
this.nextItem = item
if (sm) {
updateMarkerChanges(sm, this.index, -startLength + len, this)
}
}
/**
* @param {Transaction} tr
*/
_splitRel (tr) {
if (this.rel > 0) {
/**
* @type {ID}
*/
const itemid = /** @type {Item} */ (this.nextItem).id
this.nextItem = getItemCleanStart(tr, createID(itemid.client, itemid.clock + this.rel))
this.rel = 0
}
}
/**
* Important: you must update markers after calling this method!
*
* @param {Transaction} tr
* @param {Array<AbstractContent>} content
*/
insertContents (tr, content) {
this.reduceMoveDepth(tr)
this._splitRel(tr)
const parent = this.type
const store = tr.doc.store
const ownClientId = tr.doc.clientID
/**
* @type {Item | null}
*/
const right = this.right
/**
* @type {Item | null}
*/
let left = this.left
content.forEach(c => {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, c)
left.integrate(tr, 0)
})
if (right === null) {
this.nextItem = left
this.reachedEnd = true
} else {
this.nextItem = right
}
}
/**
* @param {Transaction} tr
* @param {Array<{ start: RelativePosition, end: RelativePosition }>} ranges
*/
insertMove (tr, ranges) {
this.insertContents(tr, ranges.map(range => new ContentMove(range.start, range.end, -1)))
// @todo is there a better alrogirthm to update searchmarkers? We could simply remove the markers that are in the updated range.
// Also note that searchmarkers are updated in insertContents as well.
const sm = this.type._searchMarker
if (sm) sm.length = 0 // @todo instead, iterate through sm and delete all marked properties on items
}
/**
* @param {Transaction} tr
* @param {Array<Object<string,any>|Array<any>|boolean|number|null|string|Uint8Array>} values
*/
insertArrayValue (tr, values) {
this._splitRel(tr)
const sm = this.type._searchMarker
/**
* @type {Array<AbstractContent>}
*/
const contents = []
/**
* @type {Array<Object|Array<any>|number|null>}
*/
let jsonContent = []
const packJsonContent = () => {
if (jsonContent.length > 0) {
contents.push(new ContentAny(jsonContent))
jsonContent = []
}
}
values.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:
contents.push(new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
break
case Doc:
contents.push(new ContentDoc(/** @type {Doc} */ (c)))
break
default:
if (c instanceof AbstractType) {
contents.push(new ContentType(c))
} else {
throw new Error('Unexpected content type in insert operation')
}
}
}
}
})
packJsonContent()
this.insertContents(tr, contents)
this.index += values.length
if (sm) {
updateMarkerChanges(sm, this.index - values.length, values.length, this)
}
}
/**
* @param {Transaction} tr
* @param {number} len
*/
slice (tr, len) {
return this._slice(tr, len, [], sliceArrayContent, concatArrayContent)
}
/**
* @param {Transaction} tr
* @param {function(any, number, any):void} f
*/
forEach (tr, f) {
for (const val of this.values(tr)) {
// decrease index because retrieving value will increase index
f(val, this.index - 1, this.type)
}
}
/**
* @template T
* @param {Transaction} tr
* @param {function(any, number, any):T} f
* @return {Array<T>}
*/
map (tr, f) {
const arr = new Array(this.type._length - this.index)
let i = 0
for (const val of this.values(tr)) {
arr[i++] = f(val, this.index - 1, this.type)
}
return arr
}
/**
* @param {Transaction} tr
*/
values (tr) {
return {
[Symbol.iterator] () {
return this
},
next: () => {
if (this.reachedEnd || this.index === this.type._length) {
return { done: true }
}
const [value] = this.slice(tr, 1)
return {
done: false,
value: value
}
}
}
}
}
/**
* @param {AbstractContent} itemcontent
* @param {number} start
* @param {number} len
*/
const sliceArrayContent = (itemcontent, start, len) => {
const content = itemcontent.getContent()
return content.length <= len && start === 0 ? content : content.slice(start, start + len)
}
/**
* @param {Array<any>} content
* @param {Array<any>} added
*/
const concatArrayContent = (content, added) => {
content.push(...added)
return content
}
/**
* Move-ranges must not cross each other.
*
* This function computes the minimal amount of ranges to move a range of content to
* a different place.
*
* Algorithm:
* * Store the current stack in $preStack and $preItem = walker.nextItem
* * Iterate forward $len items.
* * The current stack is stored is $afterStack and $
* * Delete the stack-items that both of them have in common
*
* @param {Transaction} tr
* @param {ListCursor} walker
* @param {number} len
* @return {Array<{ start: RelativePosition, end: RelativePosition }>}
*/
export const getMinimalListViewRanges = (tr, walker, len) => {
if (len === 0) return []
if (walker.index + len > walker.type._length) {
throw lengthExceeded
}
// stepping outside the current move-range as much as possible
walker.reduceMoveDepth(tr)
/**
* @type {Array<{ start: RelativePosition, end: RelativePosition, move: Item | null }>}
*/
const ranges = []
// store relevant information for the beginning, before we iterate forward
/**
* @type {Array<Item>}
*/
const preStack = walker.movedStack.map(si => si.move)
const preMove = walker.currMove
const preItem = /** @type {Item} */ (walker.nextItem)
const preRel = walker.rel
walker.forward(tr, len, false)
// store the same information for the end, after we iterate forward
/**
* @type {Array<Item>}
*/
const afterStack = walker.movedStack.map(si => si.move)
const afterMove = walker.currMove
/**
const nextIsCurrMoveStart = walker.nextItem === walker.currMoveStart
const afterItem = /** @type {Item} / (nextIsCurrMoveStart
? walker.currMove
: (walker.rel > 0 || walker.reachedEnd)
? walker.nextItem
: /** @type {Item} / (walker.nextItem).left
) */
const afterItem = /** @type {Item} */ (
(walker.rel > 0 || walker.reachedEnd)
? walker.nextItem
: /** @type {Item} */ (walker.nextItem).left
)
/**
* afterRel is always > 0
*/
const afterRel = walker.rel > 0
? walker.rel
: afterItem.length
walker.forward(tr, 0, false) // @todo remove once this is done is useSearchMarker
let start = createRelativePosition(walker.type, createID(preItem.id.client, preItem.id.clock + preRel), 0)
let end = createRelativePosition(
walker.type,
createID(afterItem.id.client, afterItem.id.clock + afterRel - 1),
-1
)
if (preMove) {
preStack.push(preMove)
}
if (afterMove) {
afterStack.push(afterMove)
}
// remove common stack-items
while (preStack.length > 0 && preStack[0] === afterStack[0]) {
preStack.shift()
afterStack.shift()
}
const topLevelMove = preStack.length > 0 ? preStack[0].moved : (afterStack.length > 0 ? afterStack[0].moved : null)
// remove stack-items that are useless for our computation (that wouldn't produce meaningful ranges)
// @todo
while (preStack.length > 0) {
const move = /** @type {Item} */ (preStack.pop())
ranges.push({
start,
end: /** @type {ContentMove} */ (move.content).end,
move
})
start = createRelativePosition(walker.type, createID(move.id.client, move.id.clock), -1)
}
const middleMove = { start, end, move: topLevelMove }
ranges.push(middleMove)
while (afterStack.length > 0) {
const move = /** @type {Item} */ (afterStack.pop())
ranges.push({
start: /** @type {ContentMove} */ (move.content).start,
end,
move
})
end = createRelativePosition(walker.type, createID(move.id.client, move.id.clock), 0)
}
// Update end of the center move operation
// Move ranges must be applied in order
middleMove.end = end
const normalizedRanges = array.flatten(ranges.map(range => {
// A subset of a range could be moved by another move with a higher priority.
// If that is the case, we need to ignore those moved items.
const { start, end } = getMovedCoords(range, tr, false)
const move = range.move
const ranges = []
/**
* @type {RelativePosition | null}
*/
let rangeStart = range.start
/**
* @type {Item}
*/
let item = start
while (item !== end) {
if (item.moved !== move && rangeStart != null) {
ranges.push({ start: rangeStart, end: createRelativePosition(walker.type, createID(item.id.client, item.id.clock), 0) })
rangeStart = null
}
if (item.moved === move && rangeStart === null) {
// @todo It might be better to set this to item.left, with assoc -1
rangeStart = createRelativePosition(walker.type, createID(item.id.client, item.id.clock), 0)
}
item = /** @type {Item} */ (item.right)
}
if (rangeStart != null) {
ranges.push({
start: rangeStart,
end: range.end
})
}
return ranges
}))
// filter out unnecessary ranges
return normalizedRanges.filter(range => !compareRelativePositions(range.start, range.end))
}

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,331 @@
import {
writeID,
readID,
compareIDs,
getState,
findRootTypeKey,
Item,
createID,
ContentType,
followRedone,
transact,
useSearchMarker,
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
}
clone () {
return new RelativePosition(this.type, this.tname, this.item, this.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) => {
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--
}
return transact(/** @type {Doc} */ (type.doc), tr =>
useSearchMarker(tr, type, index, walker => {
if (walker.reachedEnd) {
const item = assoc < 0 ? /** @type {Item} */ (walker.nextItem).lastId : null
return createRelativePosition(type, item, assoc)
} else {
const id = /** @type {Item} */ (walker.nextItem).id
return createRelativePosition(type, createID(id.client, id.clock + walker.rel), assoc)
}
})
)
}
/**
* @param {encoding.Encoder} encoder
* @param {RelativePosition} rpos
*
* @function
*/
export const writeRelativePosition = (encoder, rpos) => {
const { type, tname, item, assoc } = rpos
if (item !== null) {
encoding.writeUint8(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.readUint8(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
}

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

@@ -0,0 +1,261 @@
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 {ID} id
* @return {Item}
*
* @private
* @function
*/
export const getItemCleanEnd = (transaction, id) => {
/**
* @type {Array<Item>}
*/
// @ts-ignore
const structs = transaction.doc.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)
}

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

@@ -0,0 +1,464 @@
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 encoding from 'lib0/encoding'
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()
/**
* These deletes were used to cleanup the document and
* should be broadcasted again using a different transaction-origin.
*
* @type {DeleteSet}
*/
this.cleanupDeletions = 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>,Set<String|null>>}
*/
this.changed = new Map()
/**
* Stores the events for the types that observe also child elements.
* It is mainly used by `observeDeep`.
* @type {Map<AbstractType<YEvent>,Array<YEvent>>}
*/
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()
/**
* We store the reference that last moved an item.
* This is needed to compute the delta when multiple ContentMove move
* the same item.
*
* @type {Map<Item, Item>}
*/
this.prevMoved = new Map()
}
}
/**
* @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 {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {Transaction} transaction
*/
export const writeCleanupMessageFromTransaction = (encoder, transaction) => {
const ds = transaction.cleanupDeletions
sortAndMergeDeleteSet(ds)
// write structs: 0 structs were created
encoding.writeVarUint(encoder.restEncoder, 0)
writeDeleteSet(encoder, ds)
}
/**
* @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>} 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._transaction = null
doc.emit('beforeObserverCalls', [transaction, doc])
/**
* An array of event callbacks.
*
* Each callback is called even if the other ones throw errors.
*
* @type {Array<function():void>}
*/
const fs = []
// observe events on changed types
transaction.changed.forEach((subs, itemtype) =>
fs.push(() => {
if (itemtype._item === null || !itemtype._item.deleted) {
itemtype._callObserver(transaction, subs)
}
})
)
fs.push(() => {
// deep observe events
transaction.changedParentTypes.forEach((events, type) =>
fs.push(() => {
// We need to think about the possibility that the user transforms the
// Y.Doc in the event.
if (type._item === null || !type._item.deleted) {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
event.currentTarget = type
})
// 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])
const needsCleanupEvent = transaction.cleanupDeletions.clients.size > 0
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 (needsCleanupEvent) {
const encoder = new UpdateEncoderV1()
writeCleanupMessageFromTransaction(encoder, transaction)
doc.emit('update', [encoder.toUint8Array(), 'cleanup', 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])
if (needsCleanupEvent) {
const encoder = new UpdateEncoderV2()
writeCleanupMessageFromTransaction(encoder, transaction)
doc.emit('updateV2', [encoder.toUint8Array(), 'cleanup', 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(()=>{..})`
*
* @template T
*
* @param {Doc} doc
* @param {function(Transaction):T} f
* @param {any} [origin=true]
* @return {T}
*
* @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])
}
let res
try {
res = f(doc._transaction)
} finally {
if (initialCall && transactionCleanups[0] === doc._transaction) {
// The first transaction ended, now process observer calls.
// Observer call may create new transactions for which we need to call the observers and do cleanup.
// We don't want to nest these calls, so we execute these calls one after
// another.
// Also we need to ensure that all cleanups are called, even if the
// observes throw errors.
// This file is full of hacky try {} finally {} blocks to ensure that an
// event can throw errors and also that the cleanup is called.
cleanupTransactions(transactionCleanups, 0)
}
}
return res
}
/**
* @param {Transaction} tr
* @param {AbstractStruct} struct
*/
export const addsStruct = (tr, struct) =>
struct.id.clock >= (tr.beforeState.get(struct.id.client) || 0)

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

@@ -0,0 +1,285 @@
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 { 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 {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, itemsToDelete) !== 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(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes
* it is necessary to filter whan 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])]
*/
/**
* 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'>}
*/
export class UndoManager extends Observable {
/**
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
* @param {UndoManagerOptions} options
*/
constructor (typeScope, { captureTimeout = 500, deleteFilter = () => true, trackedOrigins = new Set([null]) } = {}) {
super()
this.scope = typeScope instanceof Array ? typeScope : [typeScope]
this.deleteFilter = deleteFilter
trackedOrigins.add(this)
this.trackedOrigins = trackedOrigins
/**
* @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 = /** @type {Doc} */ (this.scope[0].doc)
this.lastChange = 0
this.doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
// Only track certain transactions
if (!this.scope.some(type => transaction.changedParentTypes.has(type)) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))) {
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.redoStack = []
}
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()
if (now - this.lastChange < 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))
}
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)
}
})
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this])
})
}
clear () {
this.doc.transact(transaction => {
/**
* @param {StackItem} stackItem
*/
const clearItem = stackItem => {
iterateDeletedStructs(transaction, stackItem.deletions, item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
keepItem(item, false)
}
})
}
this.undoStack.forEach(clearItem)
this.redoStack.forEach(clearItem)
})
this.undoStack = []
this.redoStack = []
}
/**
* 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
}
}

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
}
}
}

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

@@ -0,0 +1,307 @@
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) {
this.keyClockEncoder.write(this.keyClock++)
this.stringEncoder.write(key)
} else {
this.keyClockEncoder.write(this.keyClock++)
}
}
}

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

@@ -0,0 +1,324 @@
import {
isDeleted,
getMovedCoords,
ContentMove, Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
} from '../internals.js'
import * as set from 'lib0/set'
import * as array from 'lib0/array'
import { addsStruct } from './Transaction.js'
import { ListCursor } from './ListCursor.js'
/**
* YEvent describes the changes on a YType.
*/
export class YEvent {
/**
* @param {AbstractType<any>} target The changed type.
* @param {Transaction} transaction
*/
constructor (target, transaction) {
/**
* The type on which this event was created on.
* @type {AbstractType<any>}
*/
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, this.transaction)
}
/**
* 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.
*
* @todo this can be removed in the next release (prefer function)
*
* @param {AbstractStruct} struct
* @return {boolean}
*/
adds (struct) {
return addsStruct(this.transaction, struct)
}
/**
* @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) {
this.transaction.doc.transact(tr => {
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 {Array<{ end: Item | null, move: Item | null, isNew: boolean, isDeleted: boolean }>}
*/
const movedStack = []
/**
* @type {Item | null}
*/
let currMove = null
/**
* @type {boolean}
*/
let currMoveIsNew = false
/**
* @type {boolean}
*/
let currMoveIsDeleted = false
/**
* @type {Item | null}
*/
let currMoveEnd = null
/**
* @type {any}
*/
let lastOp = null
const packOp = () => {
if (lastOp) {
delta.push(lastOp)
}
}
/**
* @param {Item} item
*/
const isMovedByNew = item => {
let moved = item.moved
while (moved != null) {
if (this.adds(moved)) {
return true
}
moved = moved.moved
}
return false
}
for (let item = target._start; ;) {
if (item === currMoveEnd && currMove) {
item = currMove
const { end, move, isNew, isDeleted } = movedStack.pop() || { end: null, move: null, isNew: false, isDeleted: false }
currMoveIsNew = isNew
currMoveIsDeleted = isDeleted
currMoveEnd = end
currMove = move
} else if (item === null) {
break
} else if (item.content.constructor === ContentMove) {
if (item.moved === currMove && (!item.deleted || (this.deletes(item) && !this.adds(item)))) {
movedStack.push({ end: currMoveEnd, move: currMove, isNew: currMoveIsNew, isDeleted: currMoveIsDeleted })
const { start, end } = getMovedCoords(item.content, tr, true) // We must split items for move-ranges, for single moves no splitting suffices
currMove = item
currMoveEnd = end
currMoveIsNew = this.adds(item) || currMoveIsNew
currMoveIsDeleted = item.deleted || currMoveIsDeleted
item = start
continue // do not move to item.right
}
} else if (item.moved !== currMove) {
if (!currMoveIsNew && item.countable && (!item.deleted || this.deletes(item)) && !this.adds(item) && (item.moved === null || isMovedByNew(item) || currMoveIsDeleted) && (this.transaction.prevMoved.get(item) || null) === currMove) {
if (lastOp === null || lastOp.delete === undefined) {
packOp()
lastOp = { delete: 0 }
}
lastOp.delete += item.length
}
} else if (item.deleted) {
if (!currMoveIsNew && this.deletes(item) && !this.adds(item) && !this.transaction.prevMoved.has(item)) {
if (lastOp === null || lastOp.delete === undefined) {
packOp()
lastOp = { delete: 0 }
}
lastOp.delete += item.length
deleted.add(item)
}
} else {
if (currMoveIsNew || this.adds(item) || this.transaction.prevMoved.has(item)) {
if (lastOp === null || lastOp.insert === undefined) {
packOp()
lastOp = { insert: [] }
}
// @todo push items instead (or splice..)
lastOp.insert = lastOp.insert.concat(item.content.getContent())
if (!currMoveIsNew) {
added.add(item)
}
} else {
if (lastOp === null || lastOp.retain === undefined) {
packOp()
lastOp = { retain: 0 }
}
lastOp.retain += item.length
}
}
item = /** @type {Item} */ (item).right
}
if (lastOp !== null && lastOp.retain == null) {
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
* @param {Transaction} tr
* @return {Array<string|number>} Path to the target
*
* @private
* @function
*/
const getPathTo = (parent, child, tr) => {
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
const c = new ListCursor(/** @type {AbstractType<any>} */ (child._item.parent))
while (c.nextItem != null && !c.reachedEnd && c.nextItem !== child._item) {
c.forward(tr, (c.nextItem.countable && !c.nextItem.deleted) ? c.nextItem.length : 0, true)
}
path.unshift(c.index)
}
child = /** @type {AbstractType<any>} */ (child._item.parent)
}
return path
}

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

@@ -0,0 +1,633 @@
/**
* @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
}
}
}
}
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)
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
}
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
}
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))
}

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

@@ -0,0 +1,558 @@
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)
}
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) {
// @todo this incurs an exponential overhead. We could instead only sort the item that changed.
// 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,219 +0,0 @@
chai = require('chai')
expect = chai.expect
sinon = require('sinon')
sinonChai = require('sinon-chai')
_ = require("underscore")
chai.use(sinonChai)
Connector = require "../../y-test/lib/y-test.coffee"
Y = null # need global reference!
module.exports = class Test
constructor: (@name_suffix = "", Yjs)->
Y = Yjs
@number_of_test_cases_multiplier = 1
@repeat_this = 1 * @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
@max_depth = 3
@debug = false
@reinitialize()
for gf in @getGeneratingFunctions(0)
if not (gf.types? and gf.f?)
throw new Error "Generating Functions are not initialized properly!"
for t in gf.types
if not t?
throw new Error "You havent includedt this type in Y (do require 'y-whatever')"
reinitialize: ()->
@users = []
for i in [0...@number_of_engines]
u = @makeNewUser (i+@name_suffix)
for user in @users
u._model.connector.join(user._model.connector) # 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._model.HB.stopGarbageCollection()
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]._model.operations
[]
getRandomRoot: (user_num)->
throw new Error "implement me!"
compare: (o1, o2, depth = (@max_depth+1))->
if o1 is o2 or depth <= 0
true
else if o1._name? and o1._name isnt o2._name
throw new Error "different types"
else if o1._model?
@compare o1._model, o2._model, depth
else if o1.type is "MapManager"
for name, val of o1.val()
@compare(val, o2.val(name), depth-1)
else if o1.type is "ListManager"
for val,i in o1.val()
@compare(val, o2.val(i), depth-1)
else if o1.constructor is Array and o2.constructor is Array
if o1.length isnt o2.length
throw new Error "The Arrays do not have the same size!"
for o,i in o1
@compare o, o2[i], (depth-1)
else if o1 isnt o2
throw new Error "different values"
else
throw new Error "I don't know what to do .. "
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._model.connector.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._model.connector.flushAll()
else
for user,user_number in @users[1..]
user._model.connector.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]._model.connector.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]._model.connector.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(@compare(@users[i], @users[i+1])).to.not.be.undefined
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._model.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]._model.HB.renewStateVector @users[0]._model.HB.getOperationCounter()
@users[@users.length-1]._model.engine.applyOps @users[0]._model.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(@compare(@users[@users.length-1], @users[0])).to.not.be.undefined

View File

@@ -1,324 +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.coffee"
Y.Test = require "../../y-test/lib/y-test.coffee"
Y.Text = require "../../y-text/lib/y-text"
Y.List = require "../../y-list/lib/y-list"
TestSuite = require "./TestSuite"
class ObjectTest extends TestSuite
constructor: (suffix)->
super suffix, Y
makeNewUser: (userId)->
conn = new Y.Test userId
super new Y conn
type: "ObjectTest"
getRandomRoot: (user_num, root, depth = @max_depth)->
root ?= @users[user_num]
if depth is 0 or _.random(0,1) is 1 # take root
root
else # take child
depth--
elems = null
if root._name is "Object"
elems =
for oname,val of root.val()
val
else if root._name is "Array"
elems = root.val()
else
return root
elems = elems.filter (elem)->
elem? and ((elem._name is "Array") or (elem._name is "Object"))
if elems.length is 0
root
else
p = elems[_.random(0, elems.length-1)]
@getRandomRoot user_num, p, depth
getGeneratingFunctions: (user_num)->
super(user_num).concat [
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: [Y.Object]
,
f : (y)=> # SET Object Property
y.val(@getRandomKey(), new Y.Object(@getRandomObject()))
types: [Y.Object]
,
f : (y)=> # SET PROPERTY TEXT
y.val(@getRandomKey(), new Y.Text(@getRandomText()))
types: [Y.Object]
,
f : (y)=> # SET PROPERTY List
y.val(@getRandomKey(), new Y.List(@getRandomText().split("")))
types: [Y.Object]
,
f : (y)=> # Delete Array Element
list = y.val()
if list.length > 0
i = _.random(0,list.length-1)
y.delete(i)
types: [Y.List]
,
f : (y)=> # insert Object mutable
l = y.val().length
y.insert(_.random(0, l-1), new Y.Object(@getRandomObject()))
types: [Y.List]
,
f : (y)=> # insert Text mutable
l = y.val().length
y.insert(_.random(0, l-1), new Y.Text(@getRandomText()))
types : [Y.List]
,
f : (y)=> # insert List mutable
l = y.val().length
y.insert(_.random(0, l-1), new Y.List(@getRandomText().split("")))
types : [Y.List]
,
f : (y)=> # insert Number (primitive object)
l = y.val().length
y.insert(_.random(0,l-1), _.random(0,42))
types : [Y.List]
,
f : (y)=> # SET Object Property (circular)
y.val(@getRandomKey(), @getRandomRoot user_num)
types: [Y.Object]
,
f : (y)=> # insert Object mutable (circular)
l = y.val().length
y.insert(_.random(0, l-1), @getRandomRoot user_num)
types: [Y.List]
,
f : (y)=> # INSERT TEXT
y
pos = _.random 0, (y.val().length-1)
y.insert pos, @getRandomText()
null
types: [Y.Text]
,
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 : [Y.Text]
]
module.exports = ObjectTest
describe "Object Test", ->
@timeout 500000
beforeEach (done)->
@yTest = new ObjectTest()
@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 ObjectTest("double")
test.run()
@yTest.run()
u1 = test.users[0]
u2 = @yTest.users[1]
ops1 = u1._model.HB._encode()
ops2 = u2._model.HB._encode()
u1._model.engine.applyOp ops2, true
u2._model.engine.applyOp ops1, true
expect(@yTest.compare(u1, u2)).to.not.be.undefined
it "can handle creaton of complex json (1)", ->
@yTest.users[0].val('a', new Y.Text('q'))
@yTest.users[1].val('a', new Y.Text('t'))
@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', new Y.Object({'a':'b'}))
@yTest.getSomeUser().val('a', new Y.Object({'a':{q: new Y.Text("dtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")}}))
@yTest.getSomeUser().val('b', new Y.Object({'a':{}}))
@yTest.getSomeUser().val('c', new Y.Object({'a':'c'}))
@yTest.getSomeUser().val('c', new Y.Object({'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', new Y.List([1,2,3]))
@yTest.users[1].val('l', new Y.List([4,5,6]))
@yTest.compareAll()
@yTest.users[2].val('l').insert(0,'A')
w = @yTest.users[1].val('l').insert(0,new Y.Text('B')).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")
@yTest.getSomeUser().val('number', 4)
@yTest.getSomeUser().val('object', new Y.Object({q:"rr"}))
@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")
@yTest.users[1].val('number', 4)
@yTest.users[2].val('object', new Y.Object({q:"rr"}))
@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",new Y.Text("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("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", new Y.Text("oldStuff")).val("moreStuff",new Y.Text("moreOldStuff"))
@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",new Y.Text("oldStuff")).val("moreStuff",new Y.Text("moreOldStuff"))
@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
it "can handle circular JSON", ->
u = @yTest.users[0]
u.val("me", u)
@yTest.compareAll()
u.val("stuff", new Y.Object({x: true}))
u.val("same_stuff", u.val("stuff"))
u.val("same_stuff").val("x", 5)
expect(u.val("same_stuff").val("x")).to.equal(5)
@yTest.compareAll()
u.val("stuff").val("y", u.val("stuff"))
@yTest.compareAll()

File diff suppressed because one or more lines are too long

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

@@ -0,0 +1,247 @@
import * as Y from '../src/index.js'
import * as t from 'lib0/testing'
/**
* Client id should be changed when an instance receives updates from another client using the same client id.
*
* @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'])
t.compare(arr.toJSON(), ['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)
}

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

@@ -0,0 +1,111 @@
import * as t from 'lib0/testing'
import * as promise from 'lib0/promise'
import {
contentRefs,
readContentBinary,
readContentDeleted,
readContentString,
readContentJSON,
readContentEmbed,
readContentType,
readContentFormat,
readContentAny,
readContentDoc,
readContentMove,
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 === 12)
// contentRefs[0] is reserved for GC
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
t.assert(contentRefs[11] === readContentMove)
}
/**
* 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 {null | Uint8Array}
*/
let sv = /** @type {any} */ (null)
ydoc.getText().insert(0, 'a')
ydoc.on('update', update => {
sv = Y.encodeStateVectorFromUpdate(update)
})
// should produce an update with an empty state vector (because previous ops are missing)
ydoc.getText().insert(0, 'a')
t.assert(sv !== null && sv.byteLength === 1 && sv[0] === 0)
}
/**
* Reported here: https://github.com/yjs/yjs/issues/308
* @param {t.TestCase} tc
*/
export const testDiffStateVectorOfUpdateIgnoresSkips = tc => {
const ydoc = new Y.Doc()
/**
* @type {Array<Uint8Array>}
*/
const updates = []
ydoc.on('update', update => {
updates.push(update)
})
ydoc.getText().insert(0, 'a')
ydoc.getText().insert(0, 'b')
ydoc.getText().insert(0, 'c')
const update13 = Y.mergeUpdates([updates[0], updates[2]])
const sv = Y.encodeStateVectorFromUpdate(update13)
const state = Y.decodeStateVector(sv)
t.assert(state.get(ydoc.clientID) === 1)
t.assert(state.size === 1)
}

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