Compare commits

...

500 Commits

Author SHA1 Message Date
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
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
207 changed files with 16842 additions and 19875 deletions

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

View File

@@ -1,10 +0,0 @@
{
"source": "./src",
"destination": "./docs",
"plugins": [{
"name": "esdoc-standard-plugin",
"option": {
"accessor": {"access": ["public"], "autoPrivate": true}
}
}]
}

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

8
.gitignore vendored
View File

@@ -1,8 +1,4 @@
node_modules
bower_components
docs
/y.*
/examples/*/index.dist.*
dist
.vscode
.yjsPersisted
build
docs

52
.jsdoc.json Normal file
View File

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

4
.markdownlint.json Normal file
View File

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

179
INTERNALS.md Normal file
View File

@@ -0,0 +1,179 @@
# Yjs Internals
This document roughly explains how Yjs works internally. There is a complete
walkthrough of the Yjs codebase available as a recording:
https://youtu.be/0l5XgnQ6rB4
The Yjs CRDT algorithm is described in the [YATA
paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types)
from 2016. For an algorithmic view of how it works, the paper is a reasonable
place to start. There are a handful of small improvements implemented in Yjs
which aren't described in the paper. The most notable is that items have an
`originRight` as well as an `origin` property, which improves performance when
many concurrent inserts happen after the same character.
At its heart, Yjs is a list CRDT. Everything is squeezed into a list in order to
reuse the CRDT resolution algorithm:
- Arrays are easy - they're lists of arbitrary items.
- Text is a list of characters, optionally punctuated by formatting markers and
embeds for rich text support. Several characters can be wrapped in a single
linked list `Item` (this is also known as the compound representation of
CRDTs). More information about this in [this blog
article](https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/).
- Maps are lists of entries. The last inserted entry for each key is used, and
all other duplicates for each key are flagged as deleted.
Each client is assigned a unique *clientID* property on first insert. This is a
random 53-bit integer (53 bits because that fits in the javascript safe integer
range).
## List items
Each item in a Yjs list is made up of two objects:
- An `Item` (*src/structs/Item.js*). This is used to relate the item to other
adjacent items.
- An object in the `AbstractType` heirachy (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.

1263
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,67 +0,0 @@
/**
* Type that maps from Yjs type to Target type.
* Used to implement double bindings.
*
* @template Y
* @template T
*/
export default class BindMapping {
/**
*/
constructor () {
/**
* @type Map<Y, T>
*/
this.yt = new Map()
/**
* @type Map<T, Y>
*/
this.ty = new Map()
}
/**
* Map y to t. Removes all existing bindings from y and t
* @param {Y} y
* @param {T} t
*/
bind (y, t) {
const existingT = this.yt.get(y)
if (existingT !== undefined) {
this.ty.delete(existingT)
}
const existingY = this.ty.get(t)
if (existingY !== undefined) {
this.yt.delete(existingY)
}
this.yt.set(y, t)
this.ty.set(t, y)
}
/**
* @param {Y} y
* @return {boolean}
*/
hasY (y) {
return this.yt.has(y)
}
/**
* @param {T} t
* @return {boolean}
*/
hasT (t) {
return this.ty.has(t)
}
/**
* @param {Y} y
* @return {T}
*/
getY (y) {
return this.yt.get(y)
}
/**
* @param {T} t
* @return {Y}
*/
getT (t) {
return this.ty.get(t)
}
}

View File

@@ -1,47 +0,0 @@
import { createMutex } from '../../lib/mutex.js'
/**
* Abstract class for bindings.
*
* A binding handles data binding from a Yjs type to a data object. For example,
* you can bind a Quill editor instance to a YText instance with the `QuillBinding` class.
*
* It is expected that a concrete implementation accepts two parameters
* (type and binding target).
*
* @example
* const quill = new Quill(document.createElement('div'))
* const type = y.define('quill', Y.Text)
* const binding = new Y.QuillBinding(quill, type)
*
*/
export default class Binding {
/**
* @param {YType} type Yjs type.
* @param {any} target Binding Target.
*/
constructor (type, target) {
/**
* The Yjs type that is bound to `target`
* @type {YType}
*/
this.type = type
/**
* The target that `type` is bound to.
* @type {*}
*/
this.target = target
/**
* @private
*/
this._mutualExclude = createMutex()
}
/**
* Remove all data observers (both from the type and the target).
*/
destroy () {
this.type = null
this.target = null
}
}

View File

@@ -1,211 +0,0 @@
/* global MutationObserver, getSelection */
import { fromRelativePosition } from '../../Util/relativePosition.js'
import Binding from '../Binding.js'
import { createAssociation, removeAssociation } from './util.js'
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
import { defaultFilter, applyFilterOnType } from './filter.js'
import typeObserver from './typeObserver.js'
import domObserver from './domObserver.js'
/**
* @typedef {import('./filter.js').DomFilter} DomFilter
*/
/**
* A binding that binds the children of a YXmlFragment to a DOM element.
*
* This binding is automatically destroyed when its parent is deleted.
*
* @example
* const div = document.createElement('div')
* const type = y.define('xml', Y.XmlFragment)
* const binding = new Y.QuillBinding(type, div)
*
*/
export default class DomBinding extends Binding {
/**
* @param {YXmlFragment} type The bind source. This is the ultimate source of
* truth.
* @param {Element} target The bind target. Mirrors the target.
* @param {Object} [opts] Optional configurations
* @param {DomFilter} [opts.filter=defaultFilter] The filter function to use.
*/
constructor (type, target, opts = {}) {
// Binding handles textType as this.type and domTextarea as this.target
super(type, target)
this.opts = opts
opts.document = opts.document || document
opts.hooks = opts.hooks || {}
this.scrollingElement = opts.scrollingElement || null
/**
* Maps each DOM element to the type that it is associated with.
* @type {Map}
*/
this.domToType = new Map()
/**
* Maps each YXml type to the DOM element that it is associated with.
* @type {Map}
*/
this.typeToDom = new Map()
/**
* Defines which DOM attributes and elements to filter out.
* Also filters remote changes.
* @type {DomFilter}
*/
this.filter = opts.filter || defaultFilter
// set initial value
target.innerHTML = ''
type.forEach(child => {
target.insertBefore(child.toDom(opts.document, opts.hooks, this), null)
})
this._typeObserver = typeObserver.bind(this)
this._domObserver = mutations => {
domObserver.call(this, mutations, opts.document)
}
type.observeDeep(this._typeObserver)
this._mutationObserver = new MutationObserver(this._domObserver)
this._mutationObserver.observe(target, {
childList: true,
attributes: true,
characterData: true,
subtree: true
})
this._currentSel = null
this._selectionchange = () => {
this._currentSel = getCurrentRelativeSelection(this)
}
document.addEventListener('selectionchange', this._selectionchange)
const y = type._y
this.y = y
// Force flush dom changes before Type changes are applied (they might
// modify the dom)
this._beforeTransactionHandler = (y, transaction, remote) => {
this._domObserver(this._mutationObserver.takeRecords())
this._mutualExclude(() => {
beforeTransactionSelectionFixer(this, remote)
})
}
y.on('beforeTransaction', this._beforeTransactionHandler)
this._afterTransactionHandler = (y, transaction, remote) => {
this._mutualExclude(() => {
afterTransactionSelectionFixer(this, remote)
})
// remove associations
// TODO: this could be done more efficiently
// e.g. Always delete using the following approach, or removeAssociation
// in dom/type-observer..
transaction.deletedStructs.forEach(type => {
const dom = this.typeToDom.get(type)
if (dom !== undefined) {
removeAssociation(this, dom, type)
}
})
}
y.on('afterTransaction', this._afterTransactionHandler)
// Before calling observers, apply dom filter to all changed and new types.
this._beforeObserverCallsHandler = (y, transaction) => {
// Apply dom filter to new and changed types
transaction.changedTypes.forEach((subs, type) => {
// Only check attributes. New types are filtered below.
if ((subs.size > 1 || (subs.size === 1 && subs.has(null) === false))) {
applyFilterOnType(y, this, type)
}
})
transaction.newTypes.forEach(type => {
applyFilterOnType(y, this, type)
})
}
y.on('beforeObserverCalls', this._beforeObserverCallsHandler)
createAssociation(this, target, type)
}
/**
* NOTE: currently does not apply filter to existing elements!
* @param {DomFilter} filter The filter function to use from now on.
*/
setFilter (filter) {
this.filter = filter
// TODO: apply filter to all elements
}
_getUndoStackInfo () {
return this.getSelection()
}
_restoreUndoStackInfo (info) {
this.restoreSelection(info)
}
getSelection () {
return this._currentSel
}
restoreSelection (selection) {
if (selection !== null) {
const { to, from } = selection
/**
* There is little information on the difference between anchor/focus and base/extent.
* MDN doesn't even mention base/extent anymore.. though you still have to call
* setBaseAndExtent to change the selection..
* I can observe that base/extend refer to notes higher up in the xml hierachy.
* Espesially for undo/redo this is preferred. If this becomes a problem in the future,
* we should probably go back to anchor/focus.
*/
const browserSelection = getSelection()
let { baseNode, baseOffset, extentNode, extentOffset } = browserSelection
if (from !== null) {
let sel = fromRelativePosition(this.y, from)
if (sel !== null) {
let node = this.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== baseNode || offset !== baseOffset) {
baseNode = node
baseOffset = offset
}
}
}
if (to !== null) {
let sel = fromRelativePosition(this.y, to)
if (sel !== null) {
let node = this.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== extentNode || offset !== extentOffset) {
extentNode = node
extentOffset = offset
}
}
}
browserSelection.setBaseAndExtent(
baseNode,
baseOffset,
extentNode,
extentOffset
)
}
}
/**
* Remove all properties that are handled by this class.
*/
destroy () {
this.domToType = null
this.typeToDom = null
this.type.unobserveDeep(this._typeObserver)
this._mutationObserver.disconnect()
const y = this.type._y
y.off('beforeTransaction', this._beforeTransactionHandler)
y.off('beforeObserverCalls', this._beforeObserverCallsHandler)
y.off('afterTransaction', this._afterTransactionHandler)
document.removeEventListener('selectionchange', this._selectionchange)
super.destroy()
}
}
/**
* A filter defines which elements and attributes to share.
* Return null if the node should be filtered. Otherwise return the Map of
* accepted attributes.
*
* @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction
*/

View File

@@ -1,144 +0,0 @@
import YXmlHook from '../../Types/YXml/YXmlHook.js'
import {
iterateUntilUndeleted,
removeAssociation,
insertNodeHelper } from './util.js'
import diff from '../../../lib/simpleDiff.js'
import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
/**
* 1. Check if any of the nodes was deleted
* 2. Iterate over the children.
* 2.1 If a node exists that is not yet bound to a type, insert a new node
* 2.2 If _contents.length < dom.childNodes.length, fill the
* rest of _content with childNodes
* 2.3 If a node was moved, delete it and
* recreate a new yxml element that is bound to that node.
* You can detect that a node was moved because expectedId
* !== actualId in the list
* @private
*/
function applyChangesFromDom (binding, dom, yxml, _document) {
if (yxml == null || yxml === false || yxml.constructor === YXmlHook) {
return
}
const y = yxml._y
const knownChildren = new Set()
for (let i = dom.childNodes.length - 1; i >= 0; i--) {
const type = binding.domToType.get(dom.childNodes[i])
if (type !== undefined && type !== false) {
knownChildren.add(type)
}
}
// 1. Check if any of the nodes was deleted
yxml.forEach(function (childType) {
if (knownChildren.has(childType) === false) {
childType._delete(y)
removeAssociation(binding, binding.typeToDom.get(childType), childType)
}
})
// 2. iterate
const childNodes = dom.childNodes
const len = childNodes.length
let prevExpectedType = null
let expectedType = iterateUntilUndeleted(yxml._start)
for (let domCnt = 0; domCnt < len; domCnt++) {
const childNode = childNodes[domCnt]
const childType = binding.domToType.get(childNode)
if (childType !== undefined) {
if (childType === false) {
// should be ignored or is going to be deleted
continue
}
if (expectedType !== null) {
if (expectedType !== childType) {
// 2.3 Not expected node
if (childType._parent !== yxml) {
// child was moved from another parent
// childType is going to be deleted by its previous parent
removeAssociation(binding, childNode, childType)
} else {
// child was moved to a different position.
removeAssociation(binding, childNode, childType)
childType._delete(y)
}
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
} else {
// Found expected node. Continue.
prevExpectedType = expectedType
expectedType = iterateUntilUndeleted(expectedType._right)
}
} else {
// 2.2 Fill _content with child nodes
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
}
} else {
// 2.1 A new node was found
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
}
}
}
/**
* @private
*/
export default function domObserver (mutations, _document) {
this._mutualExclude(() => {
this.type._y.transact(() => {
let diffChildren = new Set()
mutations.forEach(mutation => {
const dom = mutation.target
const yxml = this.domToType.get(dom)
if (yxml === undefined) { // In case yxml is undefined, we double check if we forgot to bind the dom
let parent = dom
let yParent
do {
parent = parent.parentElement
yParent = this.domToType.get(parent)
} while (yParent === undefined && parent !== null)
if (yParent !== false && yParent !== undefined && yParent.constructor !== YXmlHook) {
diffChildren.add(parent)
}
return
} else if (yxml === false || yxml.constructor === YXmlHook) {
// dom element is filtered / a dom hook
return
}
switch (mutation.type) {
case 'characterData':
var change = diff(yxml.toString(), dom.nodeValue)
yxml.delete(change.pos, change.remove)
yxml.insert(change.pos, change.insert)
break
case 'attributes':
if (yxml.constructor === YXmlFragment) {
break
}
let name = mutation.attributeName
let val = dom.getAttribute(name)
// check if filter accepts attribute
let attributes = new Map()
attributes.set(name, val)
if (yxml.constructor !== YXmlFragment && this.filter(dom.nodeName, attributes).size > 0) {
if (yxml.getAttribute(name) !== val) {
if (val == null) {
yxml.removeAttribute(name)
} else {
yxml.setAttribute(name, val)
}
}
}
break
case 'childList':
diffChildren.add(mutation.target)
break
}
})
for (let dom of diffChildren) {
const yxml = this.domToType.get(dom)
applyChangesFromDom(this, dom, yxml, _document)
}
})
})
}

View File

@@ -1,66 +0,0 @@
/* eslint-env browser */
import YXmlText from '../../src/Types/YXml/YXmlText.js'
import YXmlHook from '../../src/Types/YXml/YXmlHook.js'
import YXmlElement from '../../src/Types/YXml/YXmlElement.js'
import { createAssociation, domsToTypes } from './util.js'
import { filterDomAttributes, defaultFilter } from './filter.js'
/**
* @typedef {import('./filter.js').DomFilter} DomFilter
* @typedef {import('./DomBinding.js').default} DomBinding
*/
/**
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
*
* @param {Element|Text} element The DOM Element
* @param {?Document} _document Optional. Provide the global document object
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
* @param {DomFilter} [filter=defaultFilter] Optional. Dom element filter
* @param {?DomBinding} binding Warning: This property is for internal use only!
* @return {YXmlElement | YXmlText | false}
*/
export default function domToType (element, _document = document, hooks = {}, filter = defaultFilter, binding) {
/**
* @type {any}
*/
let type = null
if (element instanceof Element) {
let hookName = null
let hook
// configure `hookName !== undefined` if element is a hook.
if (element.hasAttribute('data-yjs-hook')) {
hookName = element.getAttribute('data-yjs-hook')
hook = hooks[hookName]
if (hook === undefined) {
console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`)
element.removeAttribute('data-yjs-hook')
hookName = null
}
}
if (hookName === null) {
// Not a hook
const attrs = filterDomAttributes(element, filter)
if (attrs === null) {
type = false
} else {
type = new YXmlElement(element.nodeName)
attrs.forEach((val, key) => {
type.setAttribute(key, val)
})
type.insert(0, domsToTypes(element.childNodes, document, hooks, filter, binding))
}
} else {
// Is a hook
type = new YXmlHook(hookName)
hook.fillType(element, type)
}
} else if (element instanceof Text) {
type = new YXmlText()
type.insert(0, element.nodeValue)
} else {
throw new Error('Can\'t transform this node type to a YXml type!')
}
createAssociation(binding, element, type)
return type
}

View File

@@ -1,67 +0,0 @@
import isParentOf from '../../src/Util/isParentOf.js'
/**
* @callback DomFilter
* @param {string} nodeName
* @param {Map<string, string>} attrs
* @return {Map | null}
*/
/**
* Default filter method (does nothing).
*
* @param {String} nodeName The nodeName of the element
* @param {Map} attrs Map of key-value pairs that are attributes of the node.
* @return {Map | null} The allowed attributes or null, if the element should be
* filtered.
*/
export function defaultFilter (nodeName, attrs) {
// TODO: implement basic filter that filters out dangerous properties!
return attrs
}
/**
*
*/
export function filterDomAttributes (dom, filter) {
const attrs = new Map()
for (let i = dom.attributes.length - 1; i >= 0; i--) {
const attr = dom.attributes[i]
attrs.set(attr.name, attr.value)
}
return filter(dom.nodeName, attrs)
}
/**
* Applies a filter on a type.
*
* @param {Y} y The Yjs instance.
* @param {DomBinding} binding The DOM binding instance that has the dom filter.
* @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
*
* @private
*/
export function applyFilterOnType (y, binding, type) {
if (isParentOf(binding.type, type)) {
const nodeName = type.nodeName
let attributes = new Map()
if (type.getAttributes !== undefined) {
let attrs = type.getAttributes()
for (let key in attrs) {
attributes.set(key, attrs[key])
}
}
const filteredAttributes = binding.filter(nodeName, new Map(attributes))
if (filteredAttributes === null) {
type._delete(y)
} else {
// iterate original attributes
attributes.forEach((value, key) => {
// delete all attributes that are not in filteredAttributes
if (filteredAttributes.has(key) === false) {
type.removeAttribute(key)
}
})
}
}
}

View File

@@ -1,35 +0,0 @@
/* globals getSelection */
import { getRelativePosition } from '../../src/Util/relativePosition.js'
let relativeSelection = null
function _getCurrentRelativeSelection (domBinding) {
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
const baseNodeType = domBinding.domToType.get(baseNode)
const extentNodeType = domBinding.domToType.get(extentNode)
if (baseNodeType !== undefined && extentNodeType !== undefined) {
return {
from: getRelativePosition(baseNodeType, baseOffset),
to: getRelativePosition(extentNodeType, extentOffset)
}
}
return null
}
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
export function beforeTransactionSelectionFixer (domBinding) {
relativeSelection = getCurrentRelativeSelection(domBinding)
}
/**
* Reset the browser range after every transaction.
* This prevents any collapsing issues with the local selection.
* @private
*/
export function afterTransactionSelectionFixer (domBinding) {
if (relativeSelection !== null) {
domBinding.restoreSelection(relativeSelection)
}
}

View File

@@ -1,106 +0,0 @@
/* eslint-env browser */
/* global getSelection */
import YXmlText from '../../src/Types/YXml/YXmlText.js'
import YXmlHook from '../../src/Types/YXml/YXmlHook.js'
import { removeDomChildrenUntilElementFound } from './util.js'
function findScrollReference (scrollingElement) {
if (scrollingElement !== null) {
let anchor = getSelection().anchorNode
if (anchor == null) {
let children = scrollingElement.children // only iterate through non-text nodes
for (let i = 0; i < children.length; i++) {
const elem = children[i]
const rect = elem.getBoundingClientRect()
if (rect.top >= 0) {
return { elem, top: rect.top }
}
}
} else {
/**
* @type {Element}
*/
let elem = anchor.parentElement
if (anchor instanceof Element) {
elem = anchor
}
return {
elem,
top: elem.getBoundingClientRect().top
}
}
}
return null
}
function fixScroll (scrollingElement, ref) {
if (ref !== null) {
const { elem, top } = ref
const currentTop = elem.getBoundingClientRect().top
const newScroll = scrollingElement.scrollTop + currentTop - top
if (newScroll >= 0) {
scrollingElement.scrollTop = newScroll
}
}
}
/**
* @private
*/
export default function typeObserver (events) {
this._mutualExclude(() => {
const scrollRef = findScrollReference(this.scrollingElement)
events.forEach(event => {
const yxml = event.target
const dom = this.typeToDom.get(yxml)
if (dom !== undefined && dom !== false) {
if (yxml.constructor === YXmlText) {
dom.nodeValue = yxml.toString()
} else if (event.attributesChanged !== undefined) {
// update attributes
event.attributesChanged.forEach(attributeName => {
const value = yxml.getAttribute(attributeName)
if (value === undefined) {
dom.removeAttribute(attributeName)
} else {
dom.setAttribute(attributeName, value)
}
})
/*
* TODO: instead of hard-checking the types, it would be best to
* specify the type's features. E.g.
* - _yxmlHasAttributes
* - _yxmlHasChildren
* Furthermore, the features shouldn't be encoded in the types,
* only in the attributes (above)
*/
if (event.childListChanged && yxml.constructor !== YXmlHook) {
let currentChild = dom.firstChild
yxml.forEach(childType => {
const childNode = this.typeToDom.get(childType)
switch (childNode) {
case undefined:
// Does not exist. Create it.
const node = childType.toDom(this.opts.document, this.opts.hooks, this)
dom.insertBefore(node, currentChild)
break
case false:
// nop
break
default:
// Is already attached to the dom.
// Find it and remove all dom nodes in-between.
removeDomChildrenUntilElementFound(dom, currentChild, childNode)
currentChild = childNode.nextSibling
break
}
})
removeDomChildrenUntilElementFound(dom, currentChild, null)
}
}
}
})
fixScroll(this.scrollingElement, scrollRef)
})
}

View File

@@ -1,131 +0,0 @@
import domToType from './domToType.js'
/**
* @typedef {import('../../src/Types/YXml/YXmlText.js').default} YXmlText
* @typedef {import('../../src/Types/YXml/YXmlElement.js').default} YXmlElement
* @typedef {import('../../src/Types/YXml/YXmlHook.js').default} YXmlHook
* @typedef {import('./DomBinding.js').default} DomBinding
*/
/**
* Iterates items until an undeleted item is found.
*
* @private
*/
export function iterateUntilUndeleted (item) {
while (item !== null && item._deleted) {
item = item._right
}
return item
}
/**
* Removes an association (the information that a DOM element belongs to a
* type).
*
* @param {DomBinding} domBinding The binding object
* @param {Element} dom The dom that is to be associated with type
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
*
*/
export function removeAssociation (domBinding, dom, type) {
domBinding.domToType.delete(dom)
domBinding.typeToDom.delete(type)
}
/**
* Creates an association (the information that a DOM element belongs to a
* type).
*
* @param {DomBinding} domBinding The binding object
* @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type
* @param {YXmlElement|YXmlHook|YXmlText} type The type that is to be associated with dom
*
*/
export function createAssociation (domBinding, dom, type) {
if (domBinding !== undefined) {
domBinding.domToType.set(dom, type)
domBinding.typeToDom.set(type, dom)
}
}
/**
* If oldDom is associated with a type, associate newDom with the type and
* forget about oldDom. If oldDom is not associated with any type, nothing happens.
*
* @param {DomBinding} domBinding The binding object
* @param {Element} oldDom The existing dom
* @param {Element} newDom The new dom object
*/
export function switchAssociation (domBinding, oldDom, newDom) {
if (domBinding !== undefined) {
const type = domBinding.domToType.get(oldDom)
if (type !== undefined) {
removeAssociation(domBinding, oldDom, type)
createAssociation(domBinding, newDom, type)
}
}
}
/**
* Insert Dom Elements after one of the children of this YXmlFragment.
* The Dom elements will be bound to a new YXmlElement and inserted at the
* specified position.
*
* @param {YXmlElement} type The type in which to insert DOM elements.
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
* inserted after this node. Set null to insert at
* the beginning.
* @param {Array<Element>} doms The Dom elements to insert.
* @param {?Document} _document Optional. Provide the global document object.
* @param {DomBinding} binding The dom binding
* @return {Array<YXmlElement>} The YxmlElements that are inserted.
*
* @private
*/
export function insertDomElementsAfter (type, prev, doms, _document, binding) {
const types = domsToTypes(doms, _document, binding.opts.hooks, binding.filter, binding)
return type.insertAfter(prev, types)
}
export function domsToTypes (doms, _document, hooks, filter, binding) {
const types = []
for (let dom of doms) {
const t = domToType(dom, _document, hooks, filter, binding)
if (t !== false) {
types.push(t)
}
}
return types
}
/**
* @private
*/
export function insertNodeHelper (yxml, prevExpectedNode, child, _document, binding) {
let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding)
if (insertedNodes.length > 0) {
return insertedNodes[0]
} else {
return prevExpectedNode
}
}
/**
* Remove children until `elem` is found.
*
* @param {Element} parent The parent of `elem` and `currentChild`.
* @param {Element} currentChild Start removing elements with `currentChild`. If
* `currentChild` is `elem` it won't be removed.
* @param {Element|null} elem The elemnt to look for.
*
* @private
*/
export function removeDomChildrenUntilElementFound (parent, currentChild, elem) {
while (currentChild !== elem) {
const del = currentChild
currentChild = currentChild.nextSibling
parent.removeChild(del)
}
}

View File

@@ -1,269 +0,0 @@
import BindMapping from '../BindMapping.js'
import * as PModel from 'prosemirror-model'
import * as Y from '../../src/index.js'
import { createMutex } from '../../lib/mutex.js'
import { Plugin, PluginKey } from 'prosemirror-state'
import { Decoration, DecorationSet } from 'prosemirror-view'
/**
* @typedef {import('prosemirror-view').EditorView} EditorView
* @typedef {import('prosemirror-state').EditorState} EditorState
* @typedef {BindMapping<Y.Text | Y.XmlElement, PModel.Node>} ProsemirrorMapping
*/
export const prosemirrorPluginKey = new PluginKey('yjs')
/**
* This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
*
* This plugin also keeps references to the type and the shared document so other plugins can access it.
* @param {Y.XmlFragment} yXmlFragment
*/
export const prosemirrorPlugin = yXmlFragment => {
const pluginState = {
type: yXmlFragment,
y: yXmlFragment._y,
binding: null
}
const plugin = new Plugin({
key: prosemirrorPluginKey,
state: {
init: (initargs, state) => {
return pluginState
},
apply: (tr, pluginState) => {
return pluginState
}
},
view: view => {
const binding = new ProsemirrorBinding(yXmlFragment, view)
pluginState.binding = binding
return {
update: () => {
binding._prosemirrorChanged()
},
destroy: () => {
binding.destroy()
}
}
}
})
return plugin
}
export const cursorPluginKey = new PluginKey('yjs-cursor')
export const cursorPlugin = new Plugin({
key: cursorPluginKey,
props: {
decorations: state => {
const y = prosemirrorPluginKey.getState(state).y
const awareness = y.getAwarenessInfo()
const decorations = []
awareness.forEach((state, userID) => {
if (state.cursor != null) {
const username = `User: ${userID}`
decorations.push(Decoration.widget(state.cursor.from, () => {
const cursor = document.createElement('span')
cursor.classList.add('ProseMirror-yjs-cursor')
const user = document.createElement('div')
user.insertBefore(document.createTextNode(username), null)
cursor.insertBefore(user, null)
return cursor
}, { key: username }))
decorations.push(Decoration.inline(state.cursor.from, state.cursor.to, { style: 'background-color: #ffa50070' }))
}
})
return DecorationSet.create(state.doc, decorations)
}
},
view: view => {
const y = prosemirrorPluginKey.getState(view.state).y
const awarenessListener = () => {
view.updateState(view.state)
}
y.on('awareness', awarenessListener)
return {
update: () => {
const y = prosemirrorPluginKey.getState(view.state).y
const from = view.state.selection.from
const to = view.state.selection.to
const current = y.getLocalAwarenessInfo()
if (current.cursor == null || current.cursor.to !== to || current.cursor.from !== from) {
y.setAwarenessField('cursor', {
from, to
})
}
},
destroy: () => {
const y = prosemirrorPluginKey.getState(view.state).y
y.setAwarenessField('cursor', null)
y.off('awareness', awarenessListener)
}
}
}
})
export default class ProsemirrorBinding {
/**
* @param {Y.XmlFragment} yXmlFragment The bind source
* @param {EditorView} prosemirrorView The target binding
*/
constructor (yXmlFragment, prosemirrorView) {
this.type = yXmlFragment
this.prosemirrorView = prosemirrorView
this.mux = createMutex()
/**
* @type {ProsemirrorMapping}
*/
this.mapping = new BindMapping()
this._observeFunction = this._typeChanged.bind(this)
yXmlFragment.observeDeep(this._observeFunction)
}
_typeChanged (events) {
if (events.length === 0) {
return
}
this.mux(() => {
events.forEach(event => {
// recompute node for each parent
// except main node, compute main node in the end
let target = event.target
if (target !== this.type) {
do {
if (target.constructor === Y.XmlElement) {
createNodeFromYElement(target, this.prosemirrorView.state.schema, this.mapping)
}
target = target._parent
} while (target._parent !== this.type)
}
})
const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(t, this.prosemirrorView.state.schema, this.mapping))
const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
this.prosemirrorView.updateState(this.prosemirrorView.state.apply(tr))
})
}
_prosemirrorChanged () {
this.mux(() => {
updateYFragment(this.type, this.prosemirrorView.state, this.mapping)
})
}
destroy () {
this.type.unobserveDeep(this._observeFunction)
}
}
/**
* @param {Y.XmlElement} el
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @return {PModel.Node}
*/
export const createNodeIfNotExists = (el, schema, mapping) => {
const node = mapping.getY(el)
if (node === undefined) {
return createNodeFromYElement(el, schema, mapping)
}
return node
}
/**
* @param {Y.XmlElement} el
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @return {PModel.Node}
*/
export const createNodeFromYElement = (el, schema, mapping) => {
const children = []
el.toArray().forEach(type => {
if (type.constructor === Y.XmlElement) {
children.push(createNodeIfNotExists(type, schema, mapping))
} else {
children.concat(createTextNodesFromYText(type, schema, mapping)).forEach(textchild => children.push(textchild))
}
})
const node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(), el.toArray().map(t => createNodeIfNotExists(t, schema, mapping)))
mapping.bind(el, node)
return node
}
/**
* @param {Y.Text} text
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @return {Array<PModel.Node>}
*/
export const createTextNodesFromYText = (text, schema, mapping) => {
const nodes = []
const deltas = text.toDelta()
for (let i = 0; i < deltas.length; i++) {
const delta = deltas[i]
const marks = []
for (let markName in delta.attributes) {
marks.push(schema.mark(markName, delta.attributes[markName]))
}
nodes.push(schema.text(delta.insert, marks))
}
if (nodes.length > 0) {
mapping.bind(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
}
return nodes
}
/**
* @param {PModel.Node} node
* @param {ProsemirrorMapping} mapping
* @return {Y.XmlElement | Y.Text}
*/
export const createTypeFromNode = (node, mapping) => {
let type
if (node.isText) {
type = new Y.Text()
const attrs = {}
node.marks.forEach(mark => { attrs[mark.type.name] = mark.attrs })
type.insert(0, node.text, attrs)
} else {
type = new Y.XmlElement(node.type.name)
for (let key in node.attrs) {
type.setAttribute(key, node.attrs[key])
}
type.insert(0, node.content.content.map(node => createTypeFromNode(node, mapping)))
}
mapping.bind(type, node)
return type
}
/**
* @param {Y.XmlFragment} yDomFragment
* @param {EditorState} state
* @param {BindMapping} mapping
*/
const updateYFragment = (yDomFragment, state, mapping) => {
const pChildCnt = state.doc.content.childCount
const yChildren = yDomFragment.toArray()
const yChildCnt = yChildren.length
const minCnt = pChildCnt < yChildCnt ? pChildCnt : yChildCnt
let left = 0
let right = 0
// find number of matching elements from left
for (;left < minCnt; left++) {
if (state.doc.content.child(left) !== mapping.getY(yChildren[left])) {
break
}
}
// find number of matching elements from right
for (;right < minCnt; right++) {
if (state.doc.content.child(pChildCnt - right - 1) !== mapping.getY(yChildren[yChildCnt - right - 1])) {
break
}
}
if (left + right > pChildCnt) {
// nothing changed
return
}
yDomFragment._y.transact(() => {
// now update y to match editor state
yDomFragment.delete(left, yChildCnt - left - right)
yDomFragment.insert(left, state.doc.content.content.slice(left, pChildCnt - right).map(node => createTypeFromNode(node, mapping)))
})
}

View File

@@ -1,53 +0,0 @@
import Binding from '../Binding.js'
function typeObserver (event) {
const quill = this.target
// Force flush Quill changes.
quill.update('yjs')
this._mutualExclude(function () {
// Apply computed delta.
quill.updateContents(event.delta, 'yjs')
// Force flush Quill changes. Ignore applied changes.
quill.update('yjs')
})
}
function quillObserver (delta) {
this._mutualExclude(() => {
this.type.applyDelta(delta.ops)
})
}
/**
* A Binding that binds a YText type to a Quill editor.
*
* @example
* const quill = new Quill(document.createElement('div'))
* const type = y.define('quill', Y.Text)
* const binding = new Y.QuillBinding(quill, type)
* // Now modifications on the DOM will be reflected in the Type, and the other
* // way around!
*/
export default class QuillBinding extends Binding {
/**
* @param {YText} textType
* @param {Quill} quill
*/
constructor (textType, quill) {
// Binding handles textType as this.type and quill as this.target.
super(textType, quill)
// Set initial value.
quill.setContents(textType.toDelta(), 'yjs')
// Observers are handled by this class.
this._typeObserver = typeObserver.bind(this)
this._quillObserver = quillObserver.bind(this)
textType.observe(this._typeObserver)
quill.on('text-change', this._quillObserver)
}
destroy () {
// Remove everything that is handled by this class.
this.type.unobserve(this._typeObserver)
this.target.off('text-change', this._quillObserver)
super.destroy()
}
}

View File

@@ -1,56 +0,0 @@
import Binding from '../Binding.js'
import simpleDiff from '../../lib/simpleDiff.js'
import { getRelativePosition, fromRelativePosition } from '../../src/Util/relativePosition.js'
function typeObserver () {
this._mutualExclude(() => {
const textarea = this.target
const textType = this.type
const relativeStart = getRelativePosition(textType, textarea.selectionStart)
const relativeEnd = getRelativePosition(textType, textarea.selectionEnd)
textarea.value = textType.toString()
const start = fromRelativePosition(textType._y, relativeStart)
const end = fromRelativePosition(textType._y, relativeEnd)
textarea.setSelectionRange(start, end)
})
}
function domObserver () {
this._mutualExclude(() => {
let diff = simpleDiff(this.type.toString(), this.target.value)
this.type.delete(diff.pos, diff.remove)
this.type.insert(diff.pos, diff.insert)
})
}
/**
* A binding that binds a YText to a dom textarea.
*
* This binding is automatically destroyed when its parent is deleted.
*
* @example
* const textare = document.createElement('textarea')
* const type = y.define('textarea', Y.Text)
* const binding = new Y.QuillBinding(type, textarea)
*
*/
export default class TextareaBinding extends Binding {
constructor (textType, domTextarea) {
// Binding handles textType as this.type and domTextarea as this.target
super(textType, domTextarea)
// set initial value
domTextarea.value = textType.toString()
// Observers are handled by this class
this._typeObserver = typeObserver.bind(this)
this._domObserver = domObserver.bind(this)
textType.observe(this._typeObserver)
domTextarea.addEventListener('input', this._domObserver)
}
destroy () {
// Remove everything that is handled by this class
this.type.unobserve(this._typeObserver)
this.target.unobserve(this._domObserver)
super.destroy()
}
}

View File

@@ -1,33 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style type="text/css" media="screen">
#aceContainer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.inserted {
position:absolute;
z-index:20;
background-color: #FFC107;
}
.deleted {
position:absolute;
z-index:20;
background-color: #FFC107;
}
</style>
</head>
<body>
<div id="aceContainer"></div>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/ace-builds/src/ace.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,17 +0,0 @@
/* global Y, ace */
let y = new Y('ace-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yAce = y
// bind the textarea to a shared text element
var editor = ace.edit('aceContainer')
editor.setTheme('ace/theme/chrome')
editor.getSession().setMode('ace/mode/javascript')
y.define('ace', Y.Text).bindAce(editor)

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<style>
#chat p span {
color: blue;
}
</style>
<div id="chat"></div>
<form id="chatform">
<input name="username" type="text" style="width:15%;">
<input name="message" type="text" style="width:60%;">
<input type="submit" value="Send">
</form>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,65 +0,0 @@
/* global Y */
let y = new Y('chat-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yChat = y
let chatprotocol = y.define('chatprotocol', Y.Array)
let chatcontainer = document.querySelector('#chat')
// This functions inserts a message at the specified position in the DOM
function appendMessage (message, position) {
var p = document.createElement('p')
var uname = document.createElement('span')
uname.appendChild(document.createTextNode(message.username + ': '))
p.appendChild(uname)
p.appendChild(document.createTextNode(message.message))
chatcontainer.insertBefore(p, chatcontainer.children[position] || null)
}
// This function makes sure that only 7 messages exist in the chat history.
// The rest is deleted
function cleanupChat () {
if (chatprotocol.length > 7) {
chatprotocol.delete(0, chatprotocol.length - 7)
}
}
cleanupChat()
// Insert the initial content
chatprotocol.toArray().forEach(appendMessage)
// whenever content changes, make sure to reflect the changes in the DOM
chatprotocol.observe(function (event) {
// concurrent insertions may result in a history > 7, so cleanup here
cleanupChat()
chatcontainer.innerHTML = ''
chatprotocol.toArray().forEach(appendMessage)
})
document.querySelector('#chatform').onsubmit = function (event) {
// the form is submitted
var message = {
username: this.querySelector('[name=username]').value,
message: this.querySelector('[name=message]').value
}
if (message.username.length > 0 && message.message.length > 0) {
if (chatprotocol.length > 6) {
// If we are goint to insert the 8th element, make sure to delete first.
chatprotocol.delete(0)
}
// Here we insert a message in the shared chat type.
// This will call the observe function (see line 40)
// and reflect the change in the DOM
chatprotocol.push([message])
this.querySelector('[name=message]').value = ''
}
// Do not send this form!
event.preventDefault()
return false
}

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="codeMirrorContainer"></div>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
<style>
.CodeMirror {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
</style>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,16 +0,0 @@
/* global Y, CodeMirror */
let y = new Y('codemirror-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yCodeMirror = y
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
mode: 'javascript',
lineNumbers: true
})
y.define('codemirror', Y.Text).bindCodeMirror(editor)

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<style>
path {
fill: none;
stroke: blue;
stroke-width: 1px;
stroke-linejoin: round;
stroke-linecap: round;
}
</style>
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/d3/d3.min.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,74 +0,0 @@
/* globals Y, d3 */
let y = new Y('drawing-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yDrawing = y
var drawing = y.define('drawing', Y.Array)
var renderPath = d3.svg.line()
.x(function (d) { return d[0] })
.y(function (d) { return d[1] })
.interpolate('basic')
var svg = d3.select('#drawingCanvas')
.call(d3.behavior.drag()
.on('dragstart', dragstart)
.on('drag', drag)
.on('dragend', dragend))
// create line from a shared array object and update the line when the array changes
function drawLine (yarray) {
var line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
yarray.observe(function (event) {
line.remove()
line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
})
}
// call drawLine every time an array is appended
drawing.observe(function (event) {
event.removedElements.forEach(function () {
// if one is deleted, all will be deleted!!
svg.selectAll('path').remove()
})
event.addedElements.forEach(function (path) {
drawLine(path)
})
})
// draw all existing content
for (var i = 0; i < drawing.length; i++) {
drawLine(drawing.get(i))
}
// clear canvas on request
document.querySelector('#clearDrawingCanvas').onclick = function () {
drawing.delete(0, drawing.length)
}
var sharedLine = null
function dragstart () {
drawing.insert(drawing.length, [Y.Array])
sharedLine = drawing.get(drawing.length - 1)
}
// After one dragged event is recognized, we ignore them for 33ms.
var ignoreDrag = null
function drag () {
if (sharedLine != null && ignoreDrag == null) {
ignoreDrag = window.setTimeout(function () {
ignoreDrag = null
}, 10)
sharedLine.push([d3.mouse(this)])
}
}
function dragend () {
sharedLine = null
window.clearTimeout(ignoreDrag)
ignoreDrag = null
}

View File

@@ -1,36 +0,0 @@
<!DOCTYPE html>
<html>
</head>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/d3/d3.min.js"></script>
<script src="./index.js"></script>
<style>
magic-drawing .drawingCanvas path {
fill: none;
stroke: blue;
stroke-width: 2px;
stroke-linejoin: round;
stroke-linecap: round;
}
magic-drawing .drawingCanvas {
width: 500px;
height: 500px;
cursor: default;
padding:1px;
border:1px solid #021a40;
}
magic-drawing .clearDrawingButton {
position: absolute;
top: 0;
left: 0;
}
magic-drawing {
position: relative;
display: block;
}
</style>
</head>
<body contenteditable="true">
</body>
</html>

View File

@@ -1,134 +0,0 @@
/* global Y, d3 */
const hooks = {
'magic-drawing': {
fillType: function (dom, type) {
initDrawingBindings(type, dom)
},
createDom: function (type) {
const dom = document.createElement('magic-drawing')
initDrawingBindings(type, dom)
return dom
}
}
}
window.onload = function () {
window.domBinding = new Y.DomBinding(window.yXmlType, document.body, { hooks })
}
window.addMagicDrawing = function addMagicDrawing () {
let mt = document.createElement('magic-drawing')
mt.setAttribute('data-yjs-hook', 'magic-drawing')
document.body.append(mt)
}
var renderPath = d3.svg.line()
.x(function (d) { return d[0] })
.y(function (d) { return d[1] })
.interpolate('basic')
function initDrawingBindings (type, dom) {
dom.contentEditable = 'false'
dom.setAttribute('data-yjs-hook', 'magic-drawing')
var drawing = type.get('drawing')
if (drawing === undefined) {
drawing = type.set('drawing', new Y.Array())
}
var canvas = dom.querySelector('.drawingCanvas')
if (canvas == null) {
canvas = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
canvas.setAttribute('class', 'drawingCanvas')
canvas.setAttribute('viewbox', '0 0 100 100')
dom.insertBefore(canvas, null)
}
var clearDrawingButton = dom.querySelector('.clearDrawingButton')
if (clearDrawingButton == null) {
clearDrawingButton = document.createElement('button')
clearDrawingButton.setAttribute('type', 'button')
clearDrawingButton.setAttribute('class', 'clearDrawingButton')
clearDrawingButton.innerText = 'Clear Drawing'
dom.insertBefore(clearDrawingButton, null)
}
var svg = d3.select(canvas)
.call(d3.behavior.drag()
.on('dragstart', dragstart)
.on('drag', drag)
.on('dragend', dragend))
// create line from a shared array object and update the line when the array changes
function drawLine (yarray, svg) {
var line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
yarray.observe(function (event) {
line.remove()
line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
})
}
// call drawLine every time an array is appended
drawing.observe(function (event) {
event.removedElements.forEach(function () {
// if one is deleted, all will be deleted!!
svg.selectAll('path').remove()
})
event.addedElements.forEach(function (path) {
drawLine(path, svg)
})
})
// draw all existing content
for (var i = 0; i < drawing.length; i++) {
drawLine(drawing.get(i), svg)
}
// clear canvas on request
clearDrawingButton.onclick = function () {
drawing.delete(0, drawing.length)
}
var sharedLine = null
function dragstart () {
drawing.insert(drawing.length, [Y.Array])
sharedLine = drawing.get(drawing.length - 1)
}
// After one dragged event is recognized, we ignore them for 33ms.
var ignoreDrag = null
function drag () {
if (sharedLine != null && ignoreDrag == null) {
ignoreDrag = window.setTimeout(function () {
ignoreDrag = null
}, 10)
sharedLine.push([d3.mouse(this)])
}
}
function dragend () {
sharedLine = null
window.clearTimeout(ignoreDrag)
ignoreDrag = null
}
}
let y = new Y('html-editor-drawing-hook-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yXml = y
window.yXmlType = y.define('xml', Y.XmlFragment)
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
captureTimeout: 500
})
document.onkeydown = function interceptUndoRedo (e) {
if (e.keyCode === 90 && e.metaKey) {
if (!e.shiftKey) {
window.undoManager.undo()
} else {
window.undoManager.redo()
}
e.preventDefault()
}
}

View File

@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
</head>
<script src="./index.js" type="module"></script>
</head>
<body>
<label for="room">Room: </label>
<input type="text" id="room" name="room">
<div id="content" contenteditable style="position:absolute;top:35px;left:0;right:0;bottom:0;outline: 0px solid transparent;"></div>
</body>
</html>

View File

@@ -1,77 +0,0 @@
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.js'
import Y from '../../src/Y.js'
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
import UndoManager from '../../src/Util/UndoManager.js'
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js'
import YXmlText from '../../src/Types/YXml/YXmlText.js'
import YXmlElement from '../../src/Types/YXml/YXmlElement.js'
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.js'
const connector = new YWebsocketsConnector()
const persistence = new YIndexdDBPersistence()
const roomInput = document.querySelector('#room')
let currentRoomName = null
let y = null
let domBinding = null
function setRoomName (roomName) {
if (currentRoomName !== roomName) {
console.log(`change room: "${roomName}"`)
roomInput.value = roomName
currentRoomName = roomName
location.hash = '#' + roomName
if (y !== null) {
domBinding.destroy()
}
const room = connector._rooms.get(roomName)
if (room !== undefined) {
y = room.y
} else {
y = new Y(roomName, null, null, { gc: true })
persistence.connectY(roomName, y).then(() => {
// connect after persisted content was applied to y
// If we don't wait for persistence, the other peer will send all data, waisting
// network bandwidth..
connector.connectY(roomName, y)
})
window.y = y
}
window.y = y
window.yXmlType = y.define('xml', YXmlFragment)
domBinding = new DomBinding(window.yXmlType, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
}
}
window.setRoomName = setRoomName
window.createRooms = function (i = 0) {
setInterval(function () {
setRoomName(i + '')
i++
const nodes = []
for (let j = 0; j < 100; j++) {
const node = new YXmlElement('p')
node.insert(0, [new YXmlText(`This is the ${i}th paragraph of room ${i}`)])
nodes.push(node)
}
y.share.xml.insert(0, nodes)
}, 100)
}
connector.syncPersistence(persistence)
window.connector = connector
window.persistence = persistence
window.onload = function () {
setRoomName((location.hash || '#default').slice(1))
roomInput.addEventListener('input', e => {
const roomName = e.target.value
setRoomName(roomName)
})
}

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="codeMirrorContainer"></div>
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
<style>
.CodeMirror {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
</style>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src='../../../y-indexeddb/y-indexeddb.js'></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,19 +0,0 @@
/* global Y, CodeMirror */
const persistence = new Y.IndexedDB()
const connector = {
connector: {
name: 'websockets-client',
room: 'codemirror-example'
}
}
const y = new Y('codemirror-example', connector, persistence)
window.yCodeMirror = y
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
mode: 'javascript',
lineNumbers: true
})
y.define('codemirror', Y.Text).bindCodeMirror(editor)

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
.draggable {
cursor: move;
}
</style>
</head>
<body>
<svg id="puzzle-example" width="100%" viewBox="0 0 800 800">
<g>
<path d="M 311.76636,154.23389 C 312.14136,171.85693 318.14087,184.97998 336.13843,184.23047 C 354.13647,183.48047 351.88647,180.48096 354.88599,178.98096 C 357.8855,177.48096 368.38452,170.35693 380.00806,169.98193 C 424.61841,168.54297 419.78296,223.6001 382.25757,223.6001 C 377.75806,223.6001 363.51001,219.10107 356.38599,211.97656 C 349.26196,204.85254 310.64185,207.10254 314.76636,236.34863 C 316.34888,247.5708 324.08374,267.90723 324.84595,286.23486 C 325.29321,296.99414 323.17603,307.00635 321.58911,315.6377 C 360.11353,305.4585 367.73462,304.30518 404.00513,312.83936 C 410.37915,314.33887 436.62573,310.21436 421.25269,290.3418 C 405.87964,270.46924 406.25464,248.34717 417.12817,240.84814 C 428.00171,233.34912 446.74976,228.84961 457.99829,234.09912 C 469.24683,239.34814 484.61987,255.84619 475.24585,271.59424 C 465.87231,287.34229 452.74878,290.7168 456.49829,303.84033 C 460.2478,316.96387 479.74536,320.33838 500.74292,321.83789 C 509.70142,322.47803 527.97192,323.28467 542.10864,320.12939 C 549.91821,318.38672 556.92212,315.89502 562.46753,313.56396 C 561.40796,277.80664 560.84888,245.71729 560.3606,241.97314 C 558.85278,230.41455 542.49536,217.28564 525.86499,223.2251 C 520.61548,225.1001 519.86548,231.84912 505.24243,232.59912 C 444.92798,235.69238 462.06958,143.26709 525.86499,180.48096 C 539.52759,188.45068 575.19409,190.7583 570.10913,156.85889 C 567.85962,141.86035 553.98608,102.86523 553.98608,102.86523 C 553.98608,102.86523 477.23755,111.82227 451.99878,91.991699 C 441.50024,83.74292 444.87476,69.494629 449.37427,61.245605 C 453.87378,52.996582 465.12231,46.622559 464.74731,36.123779 C 463.02563,-12.086426 392.96704,-10.902832 396.5061,36.873535 C 397.25562,46.997314 406.62964,52.621582 410.75415,60.495605 C 420.00757,78.161377 405.50024,96.073486 384.50757,99.490723 C 377.36206,100.65381 349.17505,102.65332 320.39429,102.23486 C 319.677,102.22461 318.95923,102.21143 318.24194,102.19775 C 315.08423,120.9751 311.55688,144.39697 311.76636,154.23389 z " style="fill:#f2c569;stroke:#000000" id="path2502"/>
<path d="M 500.74292,321.83789 C 479.74536,320.33838 460.2478,316.96387 456.49829,303.84033 C 452.74878,290.7168 465.87231,287.34229 475.24585,271.59424 C 484.61987,255.84619 469.24683,239.34814 457.99829,234.09912 C 446.74976,228.84961 428.00171,233.34912 417.12817,240.84814 C 406.25464,248.34717 405.87964,270.46924 421.25269,290.3418 C 436.62573,310.21436 410.37915,314.33887 404.00513,312.83936 C 367.73462,304.30518 360.11353,305.4585 321.58911,315.6377 C 320.56372,321.21484 319.75854,326.2207 320.01538,330.46191 C 320.76538,342.83545 329.3894,385.95508 327.8894,392.7041 C 326.3894,399.45312 313.64136,418.20117 297.89331,407.32715 C 282.14526,396.45361 276.52075,393.4541 265.27222,394.5791 C 254.02368,395.70361 239.77563,402.07812 239.77563,419.32568 C 239.77563,436.57373 250.27417,449.69727 268.64673,447.82227 C 287.36353,445.9126 317.92163,423.11035 325.63989,452.69678 C 330.1394,469.94434 330.51392,487.19238 330.1394,498.44092 C 329.95825,503.87646 326.09985,518.06592 322.16089,531.28125 C 353.2854,532.73682 386.47095,531.26611 394.2561,529.93701 C 430.30933,523.78174 429.31909,496.09766 412.62866,477.44385 C 406.25464,470.31934 401.75513,455.32129 405.87964,444.82275 C 414.07056,423.97314 458.8064,422.17773 473.37134,438.82324 C 483.86987,450.82178 475.99585,477.44385 468.49683,482.69287 C 453.52222,493.17529 457.22485,516.83008 473.37134,528.06201 C 504.79126,549.91943 572.35913,535.56152 572.35913,535.56152 C 572.35913,535.56152 567.85962,498.06592 567.48462,471.81934 C 567.10962,445.57275 589.60669,450.07227 593.3562,450.07227 C 597.10571,450.07227 604.22974,455.32129 609.47925,459.4458 C 614.72876,463.57031 618.85327,469.94434 630.85181,470.69434 C 677.43726,473.60596 674.58813,420.7373 631.97632,413.32666 C 623.35229,411.82666 614.72876,416.32617 603.10522,424.57519 C 591.48169,432.82422 577.23315,425.32519 570.10913,417.45117 C 566.07788,412.99561 563.8479,360.16406 562.46753,313.56396 C 556.92212,315.89502 549.91821,318.38672 542.10864,320.12939 C 527.97192,323.28467 509.70142,322.47803 500.74292,321.83789 z " style="fill:#f3f3d6;stroke:#000000" id="path2504"/>
<path d="M 240.52563,141.86035 C 257.60327,159.6499 243.94507,188.68799 214.65356,190.22949 C 185.09448,191.78516 164.66675,157.17822 190.28589,136.61621 C 200.49585,128.42139 198.05786,114.12158 179.78296,106.98975 C 154.4187,97.091553 90.54419,107.73975 90.54419,107.73975 C 90.54419,107.73975 100.88794,135.11328 101.41772,168.48242 C 101.79272,192.104 68.796875,189.47949 63.172607,186.85498 C 57.54834,184.23047 45.924805,173.73145 37.675781,173.73145 C -14.411865,173.73145 -10.013184,245.84375 39.925537,232.22412 C 48.174316,229.97461 56.42334,220.97559 68.796875,222.47559 C 81.17041,223.9751 87.544434,232.59912 87.544434,246.09766 C 87.544434,252.51709 87.0354,281.24268 86.340576,312.87012 C 119.15894,313.67676 160.60962,314.46582 170.03442,313.58887 C 186.15698,312.08936 195.90601,301.59033 188.40698,293.3418 C 180.90796,285.09277 156.16089,256.59619 179.03296,239.34814 C 201.90503,222.10059 235.65112,231.84912 239.77563,247.22217 C 243.90015,262.59521 240.52563,273.46924 234.90112,279.09326 C 229.27661,284.71777 210.52905,298.96582 221.40259,308.71484 C 232.27661,318.46338 263.77222,330.83691 302.39282,320.71338 C 309.58862,318.82715 315.92114,317.13525 321.58911,315.6377 C 323.17603,307.00635 325.29321,296.99414 324.84595,286.23486 C 324.08374,267.90723 316.34888,247.5708 314.76636,236.34863 C 310.64185,207.10254 349.26196,204.85254 356.38599,211.97656 C 363.51001,219.10107 377.75806,223.6001 382.25757,223.6001 C 419.78296,223.6001 424.61841,168.54297 380.00806,169.98193 C 368.38452,170.35693 357.8855,177.48096 354.88599,178.98096 C 351.88647,180.48096 354.13647,183.48047 336.13843,184.23047 C 318.14087,184.97998 312.14136,171.85693 311.76636,154.23389 C 311.55688,144.39697 315.08423,120.9751 318.24194,102.19775 C 290.37524,101.67725 262.46069,98.968262 254.39868,97.991211 C 233.38013,95.443359 217.17456,117.53662 240.52563,141.86035 z " style="fill:#bebcdb;stroke:#000000" id="path2506"/>
<path d="M 325.63989,452.69678 C 317.92163,423.11035 287.36353,445.9126 268.64673,447.82227 C 250.27417,449.69727 239.77563,436.57373 239.77563,419.32568 C 239.77563,402.07812 254.02368,395.70361 265.27222,394.5791 C 276.52075,393.4541 282.14526,396.45361 297.89331,407.32715 C 313.64136,418.20117 326.3894,399.45313 327.8894,392.7041 C 329.3894,385.95508 320.76538,342.83545 320.01538,330.46191 C 319.75855,326.2207 320.56372,321.21484 321.58911,315.6377 C 315.92114,317.13525 309.58862,318.82715 302.39282,320.71338 C 263.77222,330.83691 232.27661,318.46338 221.40259,308.71484 C 210.52905,298.96582 229.27661,284.71777 234.90112,279.09326 C 240.52563,273.46924 243.90015,262.59521 239.77563,247.22217 C 235.65112,231.84912 201.90503,222.10059 179.03296,239.34814 C 156.16089,256.59619 180.90796,285.09277 188.40698,293.3418 C 195.90601,301.59033 186.15698,312.08936 170.03442,313.58887 C 160.60962,314.46582 119.15894,313.67676 86.340576,312.87012 C 85.573975,347.74561 84.581299,386.15088 83.794922,402.07812 C 82.295166,432.44922 109.29175,422.32568 115.66577,420.82568 C 122.04028,419.32568 126.16479,409.57715 143.03735,408.45215 C 185.9231,405.59326 186.09985,466.69629 144.16235,467.69482 C 128.41431,468.06982 113.79126,451.19678 108.16675,447.44727 C 102.54272,443.69775 87.919433,442.94775 83.794922,457.9458 C 82.01709,464.41113 78.118652,481.65137 78.098144,496.18994 C 78.071045,515.38037 82.295166,531.81201 82.295166,531.81201 C 82.295166,531.81201 105.54224,526.5625 149.41187,526.5625 C 193.28149,526.5625 199.65552,547.93506 194.78101,558.80859 C 189.90649,569.68213 181.28296,568.93213 179.40796,583.18066 C 172.7063,634.11133 253.34106,631.08203 249.14917,584.68018 C 247.96948,571.62354 237.16528,571.66699 232.27661,557.68359 C 222.17944,528.80273 244.64966,523.56299 257.39819,524.68799 C 263.59351,525.23437 290.95679,529.73389 320.75757,531.21582 C 321.22437,531.23877 321.69312,531.25928 322.16089,531.28125 C 326.09985,518.06592 329.95825,503.87646 330.1394,498.44092 C 330.51392,487.19238 330.1394,469.94434 325.63989,452.69678 z " style="fill:#d3ea9d;stroke:#000000" id="path2508"/>
</g>
</svg>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../bower_components/d3/d3.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,67 +0,0 @@
/* global Y, d3 */
let y = new Y('jigsaw-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
let jigsaw = y.define('jigsaw', Y.Map)
window.yJigsaw = y
var origin // mouse start position - translation of piece
var drag = d3.behavior.drag()
.on('dragstart', function (params) {
// get the translation of the element
var translation = d3
.select(this)
.attr('transform')
.slice(10, -1)
.split(',')
.map(Number)
// mouse coordinates
var mouse = d3.mouse(this.parentNode)
origin = {
x: mouse[0] - translation[0],
y: mouse[1] - translation[1]
}
})
.on('drag', function () {
var mouse = d3.mouse(this.parentNode)
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
var y = mouse[1] - origin.y
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
})
.on('dragend', function (piece, i) {
// save the current translation of the puzzle piece
var mouse = d3.mouse(this.parentNode)
var x = mouse[0] - origin.x
var y = mouse[1] - origin.y
jigsaw.set(piece, {x: x, y: y})
})
var data = ['piece1', 'piece2', 'piece3', 'piece4']
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
pieces
.classed('draggable', true)
.attr('transform', function (piece) {
var translation = piece.get('translation') || {x: 0, y: 0}
return 'translate(' + translation.x + ',' + translation.y + ')'
}).call(drag)
data.forEach(function (piece) {
jigsaw.observe(function () {
// whenever a property of a piece changes, update the translation of the pieces
pieces
.transition()
.attr('transform', function (piece) {
var translation = piece.get(piece)
if (translation == null || typeof translation.x !== 'number' || typeof translation.y !== 'number') {
translation = { x: 0, y: 0 }
}
return 'translate(' + translation.x + ',' + translation.y + ')'
})
})
})

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="monacoContainer"></div>
<style>
#monacoContainer {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
</style>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,22 +0,0 @@
/* global Y, monaco */
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
let y = new Y('monaco-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
require(['vs/editor/editor.main'], function () {
window.yMonaco = y
// Create Monaco editor
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
language: 'javascript'
})
// Bind to y.share.monaco
y.define('monaco', Y.Text).bindMonaco(editor)
})

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html>
</head>
<script src="./index.js" type="module"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class="sidebar">
<h3 id="createNoteButton">+ Create Note</h3>
<div class="notelist"></div>
</div>
<div class="main">
<h1 id="headline"></h1>
<div id="editor" contenteditable="true"></div>
</div>
</body>
</html>

View File

@@ -1,132 +0,0 @@
/* eslint-env browser */
import { createYdbClient } from '../../YdbClient/index.js'
import Y from '../../src/Y.dist.js'
import * as ydb from '../../YdbClient/YdbClient.js'
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
const uuidv4 = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
})
createYdbClient('ws://localhost:8899/ws').then(ydbclient => {
const y = ydbclient.getY('notelist')
let ynotelist = y.define('notelist', Y.Array)
window.ynotelist = ynotelist
const domNoteList = document.querySelector('.notelist')
// utils
const addEventListener = (element, eventname, f) => element.addEventListener(eventname, f)
// create note button
const createNoteButton = event => {
ynotelist.insert(0, [{
guid: uuidv4(),
title: 'Note #' + ynotelist.length
}])
}
addEventListener(document.querySelector('#createNoteButton'), 'click', createNoteButton)
window.createNote = createNoteButton
window.createNotes = n => {
y.transact(() => {
for (let i = 0; i < n; i++) {
createNoteButton()
}
})
}
// clear note list function
window.clearNotes = () => ynotelist.delete(0, ynotelist.length)
// update editor and editor title
let domBinding = null
const updateEditor = () => {
domNoteList.querySelectorAll('a').forEach(a => a.classList.remove('selected'))
const domNote = document.querySelector('.notelist').querySelector(`[href="${location.hash}"]`)
if (domNote !== null) {
domNote.classList.add('selected')
const note = ynotelist.toArray().find(note => note.guid === location.hash.slice(1))
if (note !== undefined) {
const ydoc = ydbclient.getY(note.guid)
const ycontent = ydoc.define('content', Y.XmlFragment)
if (domBinding !== null) {
domBinding.destroy()
}
domBinding = new DomBinding(ycontent, document.querySelector('#editor'))
document.querySelector('#headline').innerText = note.title
document.querySelector('#editor').focus()
}
}
}
// listen to url-hash changes
addEventListener(window, 'hashchange', updateEditor)
updateEditor()
const styleSyncedState = (div, noteSyncedState) => {
let classes = []
if (noteSyncedState.persisted) {
classes.push('persisted')
} else {
if (noteSyncedState.upsynced) {
classes.push('upsynced')
} else {
classes.push('noupsynced')
}
if (noteSyncedState.downsynced) {
classes.push('downsynced')
} else {
classes.push('nodownsynced')
}
}
div.setAttribute('class', classes.join(' '))
}
ydbclient.on('syncstate', event => event.updated.forEach((state, room) => {
const a = document.querySelector(`[href="#${room}"]`)
if (a !== null) {
styleSyncedState(a.firstChild, state)
}
}))
// render note list
const renderNoteList = (elementList, insertRef = domNoteList.firstChild) => {
const fragment = document.createDocumentFragment()
const addNow = elementList.splice(0, 100)
addNow.forEach(note => {
const a = document.createElement('a')
const div = document.createElement('div')
a.insertBefore(div, null)
a.setAttribute('href', '#' + note.guid)
div.innerText = note.title
styleSyncedState(div, ydbclient.getRoomState(note.guid))
fragment.insertBefore(a, null)
})
if (domBinding == null) {
updateEditor()
}
domNoteList.insertBefore(fragment, insertRef)
if (elementList.length > 0) {
setTimeout(() => renderNoteList(elementList, insertRef), 100)
}
}
{
const notelist = ynotelist.toArray()
if (notelist.length > 0) {
renderNoteList(notelist)
ydb.subscribeRooms(ydbclient, notelist.map(note => note.guid))
}
}
ynotelist.observe(event => {
const addedNotes = []
event.addedElements.forEach(itemJson => itemJson._content.forEach(json => addedNotes.push(json)))
renderNoteList(addedNotes.slice().reverse()) // renderNoteList modifies addedNotes, so first make a copy of it
setTimeout(() => {
ydb.subscribeRooms(ydbclient, addedNotes.map(note => note.guid))
}, 200)
if (domBinding === null) {
updateEditor()
}
})
})

View File

@@ -1,100 +0,0 @@
.sidebar {
height: 100%; /* Full-height: remove this if you want "auto" height */
width: 180px; /* Set the width of the sidebar */
position: fixed; /* Fixed Sidebar (stay in place on scroll) */
z-index: 1; /* Stay on top */
top: 0; /* Stay at the top */
left: 0;
background-color: #111; /* Black */
overflow-x: hidden; /* Disable horizontal scroll */
padding-top: 20px;
color: #50abff;
}
#createNoteButton {
padding-left: .5em;
padding-top: .5em;
padding-bottom: .7em;
margin: 0;
cursor: pointer;
}
.notelist > a {
padding: 6px 8px 6px 16px;
text-decoration: none;
font-size: 13px;
color: #818181;
display: block;
}
.notelist > a.selected {
border-style: outset;
}
.notelist > a > div {
position: relative;
display: inline;
}
/* When you mouse over the navigation links, change their color */
.sidebar a:hover {
color: #f1f1f1;
}
/* Style page content */
.main {
margin-left: 180px; /* Same as the width of the sidebar */
padding: 0px 10px;
}
/* On smaller screens, where height is less than 450px, change the style of the sidebar (less padding and a smaller font size) */
@media screen and (max-height: 450px) {
.sidebar {padding-top: 15px;}
.sidebar a {font-size: 18px;}
}
#editor {
min-height: 400px;
}
[contenteditable]:focus {
outline: 0px solid transparent;
}
.persisted::before {
content: "✔";
color: green;
position: absolute;
right: -14px;
top: 0px;
}
.upsynced::before {
content: "↑";
color: green;
position: absolute;
right: -14px;
top: 0px;
}
.noupsynced::before {
content: "↑";
color: red;
position: absolute;
right: -14px;
top: 0px;
}
.downsynced::after {
content: "↓";
color: green;
position: absolute;
right: -22px;
top: 0px;
}
.nodownsynced::after {
content: "↓";
color: red;
position: absolute;
right: -22px;
top: 0px;
}

View File

@@ -1,73 +0,0 @@
/* eslint-env browser */
import {Plugin} from 'prosemirror-state'
import {Decoration, DecorationSet} from 'prosemirror-view'
import {schema} from 'prosemirror-schema-basic'
const findPlaceholder = (state, id) => {
let decos = PlaceholderPlugin.getState(state)
let found = decos.find(null, null, spec => spec.id === id)
return found.length ? found[0].from : null
}
export const startImageUpload = (view, file) => {
// A fresh object to act as the ID for this upload
let id = {}
// Replace the selection with a placeholder
let tr = view.state.tr
if (!tr.selection.empty) tr.deleteSelection()
tr.setMeta(PlaceholderPlugin, {add: {id, pos: tr.selection.from}})
view.dispatch(tr)
uploadFile(file).then(url => {
let pos = findPlaceholder(view.state, id)
// If the content around the placeholder has been deleted, drop
// the image
if (pos == null) return
// Otherwise, insert it at the placeholder's position, and remove
// the placeholder
view.dispatch(view.state.tr
.replaceWith(pos, pos, schema.nodes.image.create({src: url}))
.setMeta(PlaceholderPlugin, {remove: {id}}))
}, () => {
// On failure, just clean up the placeholder
view.dispatch(tr.setMeta(PlaceholderPlugin, {remove: {id}}))
})
}
// This is just a dummy that loads the file and creates a data URL.
// You could swap it out with a function that does an actual upload
// and returns a regular URL for the uploaded file.
function uploadFile (file) {
let reader = new FileReader()
return new Promise((resolve, reject) => {
reader.onload = () => resolve(reader.result)
reader.onerror = () => reject(reader.error)
// Some extra delay to make the asynchronicity visible
setTimeout(() => reader.readAsDataURL(file), 1500)
})
}
export const PlaceholderPlugin = new Plugin({
state: {
init () { return DecorationSet.empty },
apply (tr, set) {
// Adjust decoration positions to changes made by the transaction
set = set.map(tr.mapping, tr.doc)
// See if the transaction adds or removes any placeholders
let action = tr.getMeta(this)
if (action && action.add) {
let widget = document.createElement('placeholder')
let deco = Decoration.widget(action.add.pos, widget, {id: action.add.id})
set = set.add(tr.doc, [deco])
} else if (action && action.remove) {
set = set.remove(set.find(null, null, spec => spec.id === action.remove.id))
}
return set
}
},
props: {
decorations (state) { return this.getState(state) }
}
})

View File

@@ -1,21 +0,0 @@
# Prosemirror Example
### Run basic websockets server
```sh
node /provider/websocket/server.js
```
### Bundle Prosemirror Example
This example requires external modules and needs to be bundled before shipping it to the browser.
```sh
cd /examples/prosemirror/
# bundle prosemirror example
npx rollup -wc
# serve example
npx serve .
```

View File

@@ -1,51 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script type="module" src="./index.dist.js"></script>
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
<style>
placeholder {
display: inline;
border: 1px solid #ccc;
color: #ccc;
}
placeholder:after {
content: "☁";
font-size: 200%;
line-height: 0.1;
font-weight: bold;
}
.ProseMirror img { max-width: 100px }
.ProseMirror-yjs-cursor {
position: absolute;
border-left: black;
border-left-style: solid;
border-left-width: 2px;
border-color: orange;
height: 1em;
}
.ProseMirror-yjs-cursor > div {
position: relative;
top: -1.05em;
font-size: 13px;
background-color: rgb(250, 129, 0);
font-family: serif;
font-style: normal;
font-weight: normal;
line-height: normal;
user-select: none;
color: white;
padding-left: 2px;
padding-right: 2px;
}
</style>
</head>
<body>
<div id="editor" style="margin-bottom: 23px"></div>
<div style="display: none" id="content">
<h3>Hello User</h3>
<p>type something ...</p>
</div>
<div>Insert image: <input type=file id=image-upload></div>
</body>
</html>

View File

@@ -1,45 +0,0 @@
/* eslint-env browser */
import * as Y from '../../src/index.js'
import { prosemirrorPlugin, cursorPlugin } from '../../bindings/ProsemirrorBinding/ProsemirrorBinding.js'
import WebsocketProvider from '../../provider/websocket/WebSocketProvider.js'
import {EditorState} from 'prosemirror-state'
import {EditorView} from 'prosemirror-view'
import {Schema, DOMParser, Mark, Fragment, Node, Slice} from 'prosemirror-model'
import {schema} from 'prosemirror-schema-basic'
import {exampleSetup} from 'prosemirror-example-setup'
import { PlaceholderPlugin, startImageUpload } from './PlaceholderPlugin.js'
const provider = new WebsocketProvider('ws://localhost:1234/')
const ydocument = provider.get('prosemirror')
/**
* @type {any}
*/
const type = ydocument.define('prosemirror', Y.XmlFragment)
const view = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
plugins: exampleSetup({schema}).concat([PlaceholderPlugin, prosemirrorPlugin(type), cursorPlugin])
})
})
window.provider = provider
window.ydocument = ydocument
window.type = type
window.view = view
window.EditorState = EditorState
window.EditorView = EditorView
window.Mark = Mark
window.Fragment = Fragment
window.Node = Node
window.Schema = Schema
window.Slice = Slice
document.querySelector('#image-upload').addEventListener('change', e => {
if (view.state.selection.$from.parent.inlineContent && e.target.files.length) {
startImageUpload(view, e.target.files[0])
}
view.focus()
})

View File

@@ -1,20 +0,0 @@
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
export default {
input: './index.js',
output: {
name: 'index',
file: 'index.dist.js',
format: 'umd',
sourcemap: true
},
plugins: [
nodeResolve({
main: true,
module: true,
browser: true
}),
commonjs()
]
}

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!-- Main quill library -->
<script src="../../node_modules/quill/dist/quill.min.js"></script>
<link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet">
<!-- Quill cursors module -->
<script src="../../node_modules/quill-cursors/dist/quill-cursors.min.js"></script>
<link href="../../node_modules/quill-cursors/dist/quill-cursors.css" rel="stylesheet">
<!-- Yjs Library and connector -->
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
</head>
<body>
<div id="quill-container">
<div id="quill">
</div>
</div>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,78 +0,0 @@
/* global Y, Quill, QuillCursors */
Quill.register('modules/cursors', QuillCursors)
let y = new Y('quill-0', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
let users = y.define('users', Y.Array)
let myUserInfo = new Y.Map()
myUserInfo.set('name', 'dada')
myUserInfo.set('color', 'red')
users.push([myUserInfo])
let quill = new Quill('#quill-container', {
modules: {
toolbar: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }]
],
cursors: {
hideDelay: 500
}
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
})
let cursors = quill.getModule('cursors')
function drawCursors () {
cursors.clearCursors()
users.map((user, userId) => {
if (user !== myUserInfo) {
let relativeRange = user.get('range')
let lastUpdated = new Date(user.get('last updated'))
if (lastUpdated != null && new Date() - lastUpdated < 20000 && relativeRange != null) {
let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset
let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset
let range = { index: start, length: end - start }
cursors.setCursor(userId + '', range, user.get('name'), user.get('color'))
}
}
})
}
users.observeDeep(drawCursors)
drawCursors()
quill.on('selection-change', function (range) {
if (range != null) {
myUserInfo.set('range', {
start: Y.utils.getRelativePosition(yText, range.index),
end: Y.utils.getRelativePosition(yText, range.index + range.length)
})
} else {
myUserInfo.delete('range')
}
myUserInfo.set('last updated', new Date().toString())
})
let yText = y.define('quill', Y.Text)
let quillBinding = new Y.QuillBinding(yText, quill)
window.quillBinding = quillBinding
window.yText = yText
window.y = y
window.quill = quill
window.users = users
window.cursors = cursors

View File

@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!-- Main Quill library -->
<script src="../../node_modules/quill/dist/quill.min.js"></script>
<link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet">
<!-- Yjs Library and connector -->
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
</head>
<body>
<div id="quill-container">
<div id="quill">
</div>
</div>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,33 +0,0 @@
/* global Y, Quill */
let y = new Y('quill-cursors-0', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
let quill = new Quill('#quill-container', {
modules: {
toolbar: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }]
]
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
})
let yText = y.define('quill', Y.Text)
let quillBinding = new Y.QuillBinding(yText, quill)
window.quillBinding = quillBinding
window.yText = yText
window.y = y
window.quill = quill

View File

@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!-- quill does not include dist files! We are using the hosted version instead -->
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
<link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
<style>
#quill-container {
border: 1px solid gray;
box-shadow: 0px 0px 10px gray;
}
</style>
</head>
<body>
<div id="quill-container">
<div id="quill">
</div>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
<!-- quill does not include dist files! We are using the hosted version instead (see above)
<script src="../bower_components/quill/dist/quill.js"></script>
-->
<script src="../bower_components/yjs/y.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,49 +0,0 @@
/* global Y, Quill */
// register yjs service worker
if ('serviceWorker' in navigator) {
// Register service worker
// it is important to copy yjs-sw-template to the root directory!
navigator.serviceWorker.register('./yjs-sw-template.js').then(function (reg) {
console.log('Yjs service worker registration succeeded. Scope is ' + reg.scope)
}).catch(function (err) {
console.error('Yjs service worker registration failed with error ' + err)
})
}
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'serviceworker',
room: 'ServiceWorkerExample2'
},
sourceDir: '/bower_components',
share: {
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
}
}).then(function (y) {
window.yServiceWorker = y
// create quill element
window.quill = new Quill('#quill', {
modules: {
formula: true,
syntax: true,
toolbar: [
[{ size: ['small', false, 'large', 'huge'] }],
['bold', 'italic', 'underline'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }]
]
},
theme: 'snow'
})
// bind quill to richtext type
y.share.richtext.bind(window.quill)
})

View File

@@ -1,22 +0,0 @@
/* eslint-env worker */
// copy and modify this file
self.DBConfig = {
name: 'indexeddb'
}
self.ConnectorConfig = {
name: 'websockets-client',
// url: '..',
options: {
jsonp: false
}
}
importScripts(
'/bower_components/yjs/y.js',
'/bower_components/y-memory/y-memory.js',
'/bower_components/y-indexeddb/y-indexeddb.js',
'/bower_components/y-websockets-client/y-websockets-client.js',
'/bower_components/y-serviceworker/yjs-sw-include.js'
)

View File

@@ -1,7 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
<script type="module" src="./index.js"></script>
</body>
</html>

View File

@@ -1,13 +0,0 @@
/* eslint-env browser */
import * as Y from '../../src/index.js'
import WebsocketProvider from '../../provider/websocket/WebSocketProvider.js'
const provider = new WebsocketProvider('ws://localhost:1234/')
const ydocument = provider.get('textarea')
const type = ydocument.define('textarea', Y.Text)
const textarea = document.querySelector('textarea')
const binding = new Y.TextareaBinding(type, textarea)
window.textareaExample = {
provider, ydocument, type, textarea, binding
}

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html>
</head>
<!-- jquery is not required for YXml. It is just here for convenience, and to test batch operations. -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="./index.js"></script>
</head>
<body>
<h1> Shared DOM Example </h1>
<p> Use native DOM function or jQuery to manipulate the shared DOM (window.sharedDom). </p>
<div class="command">
<button type="button">Execute</button>
<input type="text" value='$(sharedDom).append("<h3>Appended headline</h3>")' size="40"/>
</div>
<div class="command">
<button type="button">Execute</button>
<input type="text" value='$(sharedDom).attr("align","right")' size="40"/>
</div>
<div class="command">
<button type="button">Execute</button>
<input type="text" value='$(sharedDom).attr("style","color:blue;")' size="40"/>
</div>
<script>
/* global $ */
var commands = document.querySelectorAll('.command')
Array.prototype.forEach.call(commands, function (command) {
var execute = function () {
// eslint-disable-next-line no-eval
eval(command.querySelector('input').value)
}
command.querySelector('button').onclick = execute
$(command.querySelector('input')).keyup(function (e) {
if (e.keyCode === 13) {
execute()
}
})
})
</script>
</body>
</html>

View File

@@ -1,13 +0,0 @@
/* global Y */
let y = new Y('xml-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.yXml = y
// bind xml type to a dom, and put it in body
window.sharedDom = y.define('xml', Y.XmlElement).toDom()
document.body.appendChild(window.sharedDom)

View File

@@ -1,113 +0,0 @@
/**
* Handles named events.
*/
export default class NamedEventHandler {
constructor () {
this._eventListener = new Map()
this._stateListener = new Map()
}
/**
* @private
* Returns all listeners that listen to a specified name.
*
* @param {String} name The query event name.
*/
_getListener (name) {
let listeners = this._eventListener.get(name)
if (listeners === undefined) {
listeners = {
once: new Set(),
on: new Set()
}
this._eventListener.set(name, listeners)
}
return listeners
}
/**
* Adds a named event listener. The listener is removed after it has been
* called once.
*
* @param {String} name The event name to listen to.
* @param {Function} f The function that is executed when the event is fired.
*/
once (name, f) {
let listeners = this._getListener(name)
listeners.once.add(f)
}
/**
* Adds a named event listener.
*
* @param {String} name The event name to listen to.
* @param {Function} f The function that is executed when the event is fired.
*/
on (name, f) {
let listeners = this._getListener(name)
listeners.on.add(f)
}
/**
* @private
* Init the saved state for an event name.
*/
_initStateListener (name) {
let state = this._stateListener.get(name)
if (state === undefined) {
state = {}
state.promise = new Promise(function (resolve) {
state.resolve = resolve
})
this._stateListener.set(name, state)
}
return state
}
/**
* Returns a Promise that is resolved when the event name is called.
* The Promise is immediately resolved when the event name was called in the
* past.
*/
when (name) {
return this._initStateListener(name).promise
}
/**
* Remove an event listener that was registered with either
* {@link EventHandler#on} or {@link EventHandler#once}.
*/
off (name, f) {
if (name == null || f == null) {
throw new Error('You must specify event name and function!')
}
const listener = this._eventListener.get(name)
if (listener !== undefined) {
listener.on.delete(f)
listener.once.delete(f)
}
}
/**
* Emit a named event. All registered event listeners that listen to the
* specified name will receive the event.
*
* @param {String} name The event name.
* @param {Array} args The arguments that are applied to the event listener.
*/
emit (name, ...args) {
this._initStateListener(name).resolve()
const listener = this._eventListener.get(name)
if (listener !== undefined) {
listener.on.forEach(f => f.apply(null, args))
listener.once.forEach(f => f.apply(null, args))
listener.once = new Set()
} else if (name === 'error') {
console.error(args[0])
}
}
destroy () {
this._eventListener = null
}
}

View File

@@ -1,465 +0,0 @@
function rotate (tree, parent, newParent, n) {
if (parent === null) {
tree.root = newParent
newParent._parent = null
} else if (parent.left === n) {
parent.left = newParent
} else if (parent.right === n) {
parent.right = newParent
} else {
throw new Error('The elements are wrongly connected!')
}
}
class N {
// A created node is always red!
constructor (val) {
this.val = val
this.color = true
this._left = null
this._right = null
this._parent = null
}
isRed () { return this.color }
isBlack () { return !this.color }
redden () { this.color = true; return this }
blacken () { this.color = false; return this }
get grandparent () {
return this.parent.parent
}
get parent () {
return this._parent
}
get sibling () {
return (this === this.parent.left)
? this.parent.right : this.parent.left
}
get left () {
return this._left
}
get right () {
return this._right
}
set left (n) {
if (n !== null) {
n._parent = this
}
this._left = n
}
set right (n) {
if (n !== null) {
n._parent = this
}
this._right = n
}
rotateLeft (tree) {
const parent = this.parent
const newParent = this.right
const newRight = this.right.left
newParent.left = this
this.right = newRight
rotate(tree, parent, newParent, this)
}
next () {
if (this.right !== null) {
// search the most left node in the right tree
var o = this.right
while (o.left !== null) {
o = o.left
}
return o
} else {
var p = this
while (p.parent !== null && p !== p.parent.left) {
p = p.parent
}
return p.parent
}
}
prev () {
if (this.left !== null) {
// search the most right node in the left tree
var o = this.left
while (o.right !== null) {
o = o.right
}
return o
} else {
var p = this
while (p.parent !== null && p !== p.parent.right) {
p = p.parent
}
return p.parent
}
}
rotateRight (tree) {
const parent = this.parent
const newParent = this.left
const newLeft = this.left.right
newParent.right = this
this.left = newLeft
rotate(tree, parent, newParent, this)
}
getUncle () {
// we can assume that grandparent exists when this is called!
if (this.parent === this.parent.parent.left) {
return this.parent.parent.right
} else {
return this.parent.parent.left
}
}
}
/*
* This is a Red Black Tree implementation
*/
export default class Tree {
constructor () {
this.root = null
this.length = 0
}
findNext (id) {
var nextID = id.clone()
nextID.clock += 1
return this.findWithLowerBound(nextID)
}
findPrev (id) {
let prevID = id.clone()
prevID.clock -= 1
return this.findWithUpperBound(prevID)
}
findNodeWithLowerBound (from) {
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if (from === null || (from.lessThan(o.val._id) && o.left !== null)) {
// o is included in the bound
// try to find an element that is closer to the bound
o = o.left
} else if (from !== null && o.val._id.lessThan(from)) {
// o is not within the bound, maybe one of the right elements is..
if (o.right !== null) {
o = o.right
} else {
// there is no right element. Search for the next bigger element,
// this should be within the bounds
return o.next()
}
} else {
return o
}
}
}
}
findNodeWithUpperBound (to) {
if (to === void 0) {
throw new Error('You must define from!')
}
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if ((to === null || o.val._id.lessThan(to)) && o.right !== null) {
// o is included in the bound
// try to find an element that is closer to the bound
o = o.right
} else if (to !== null && to.lessThan(o.val._id)) {
// o is not within the bound, maybe one of the left elements is..
if (o.left !== null) {
o = o.left
} else {
// there is no left element. Search for the prev smaller element,
// this should be within the bounds
return o.prev()
}
} else {
return o
}
}
}
}
findSmallestNode () {
var o = this.root
while (o != null && o.left != null) {
o = o.left
}
return o
}
findWithLowerBound (from) {
var n = this.findNodeWithLowerBound(from)
return n == null ? null : n.val
}
findWithUpperBound (to) {
var n = this.findNodeWithUpperBound(to)
return n == null ? null : n.val
}
iterate (from, to, f) {
var o
if (from === null) {
o = this.findSmallestNode()
} else {
o = this.findNodeWithLowerBound(from)
}
while (
o !== null &&
(
to === null || // eslint-disable-line no-unmodified-loop-condition
o.val._id.lessThan(to) ||
o.val._id.equals(to)
)
) {
f(o.val)
o = o.next()
}
}
find (id) {
let n = this.findNode(id)
if (n !== null) {
return n.val
} else {
return null
}
}
findNode (id) {
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if (o === null) {
return null
}
if (id.lessThan(o.val._id)) {
o = o.left
} else if (o.val._id.lessThan(id)) {
o = o.right
} else {
return o
}
}
}
}
delete (id) {
var d = this.findNode(id)
if (d == null) {
// throw new Error('Element does not exist!')
return
}
this.length--
if (d.left !== null && d.right !== null) {
// switch d with the greates element in the left subtree.
// o should have at most one child.
var o = d.left
// find
while (o.right !== null) {
o = o.right
}
// switch
d.val = o.val
d = o
}
// d has at most one child
// let n be the node that replaces d
var isFakeChild
var child = d.left || d.right
if (child === null) {
isFakeChild = true
child = new N(null)
child.blacken()
d.right = child
} else {
isFakeChild = false
}
if (d.parent === null) {
if (!isFakeChild) {
this.root = child
child.blacken()
child._parent = null
} else {
this.root = null
}
return
} else if (d.parent.left === d) {
d.parent.left = child
} else if (d.parent.right === d) {
d.parent.right = child
} else {
throw new Error('Impossible!')
}
if (d.isBlack()) {
if (child.isRed()) {
child.blacken()
} else {
this._fixDelete(child)
}
}
this.root.blacken()
if (isFakeChild) {
if (child.parent.left === child) {
child.parent.left = null
} else if (child.parent.right === child) {
child.parent.right = null
} else {
throw new Error('Impossible #3')
}
}
}
_fixDelete (n) {
function isBlack (node) {
return node !== null ? node.isBlack() : true
}
function isRed (node) {
return node !== null ? node.isRed() : false
}
if (n.parent === null) {
// this can only be called after the first iteration of fixDelete.
return
}
// d was already replaced by the child
// d is not the root
// d and child are black
var sibling = n.sibling
if (isRed(sibling)) {
// make sibling the grandfather
n.parent.redden()
sibling.blacken()
if (n === n.parent.left) {
n.parent.rotateLeft(this)
} else if (n === n.parent.right) {
n.parent.rotateRight(this)
} else {
throw new Error('Impossible #2')
}
sibling = n.sibling
}
// parent, sibling, and children of n are black
if (n.parent.isBlack() &&
sibling.isBlack() &&
isBlack(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
this._fixDelete(n.parent)
} else if (n.parent.isRed() &&
sibling.isBlack() &&
isBlack(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
n.parent.blacken()
} else {
if (n === n.parent.left &&
sibling.isBlack() &&
isRed(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
sibling.left.blacken()
sibling.rotateRight(this)
sibling = n.sibling
} else if (n === n.parent.right &&
sibling.isBlack() &&
isRed(sibling.right) &&
isBlack(sibling.left)
) {
sibling.redden()
sibling.right.blacken()
sibling.rotateLeft(this)
sibling = n.sibling
}
sibling.color = n.parent.color
n.parent.blacken()
if (n === n.parent.left) {
sibling.right.blacken()
n.parent.rotateLeft(this)
} else {
sibling.left.blacken()
n.parent.rotateRight(this)
}
}
}
put (v) {
var node = new N(v)
if (this.root !== null) {
var p = this.root // p abbrev. parent
while (true) {
if (node.val._id.lessThan(p.val._id)) {
if (p.left === null) {
p.left = node
break
} else {
p = p.left
}
} else if (p.val._id.lessThan(node.val._id)) {
if (p.right === null) {
p.right = node
break
} else {
p = p.right
}
} else {
p.val = node.val
return p
}
}
this._fixInsert(node)
} else {
this.root = node
}
this.length++
this.root.blacken()
return node
}
_fixInsert (n) {
if (n.parent === null) {
n.blacken()
return
} else if (n.parent.isBlack()) {
return
}
var uncle = n.getUncle()
if (uncle !== null && uncle.isRed()) {
// Note: parent: red, uncle: red
n.parent.blacken()
uncle.blacken()
n.grandparent.redden()
this._fixInsert(n.grandparent)
} else {
// Note: parent: red, uncle: black or null
// Now we transform the tree in such a way that
// either of these holds:
// 1) grandparent.left.isRed
// and grandparent.left.left.isRed
// 2) grandparent.right.isRed
// and grandparent.right.right.isRed
if (n === n.parent.right && n.parent === n.grandparent.left) {
n.parent.rotateLeft(this)
// Since we rotated and want to use the previous
// cases, we need to set n in such a way that
// n.parent.isRed again
n = n.left
} else if (n === n.parent.left && n.parent === n.grandparent.right) {
n.parent.rotateRight(this)
// see above
n = n.right
}
// Case 1) or 2) hold from here on.
// Now traverse grandparent, make parent a black node
// on the highest level which holds two red nodes.
n.parent.blacken()
n.grandparent.redden()
if (n === n.parent.left) {
// Case 1
n.grandparent.rotateRight(this)
} else {
// Case 2
n.grandparent.rotateLeft(this)
}
}
}
}

View File

@@ -1,7 +0,0 @@
export const BITS32 = 0xFFFFFFFF
export const BITS21 = (1 << 21) - 1
export const BITS16 = (1 << 16) - 1
export const BIT26 = 1 << 26
export const BIT32 = 1 << 32

View File

@@ -1,168 +0,0 @@
/* global Buffer */
import * as globals from './globals.js'
/**
* A Decoder handles the decoding of an ArrayBuffer.
*/
export class Decoder {
/**
* @param {ArrayBuffer} buffer Binary data to decode
*/
constructor (buffer) {
this.arr = new Uint8Array(buffer)
this.pos = 0
}
}
/**
* @param {ArrayBuffer} buffer
* @return {Decoder}
*/
export const createDecoder = buffer => new Decoder(buffer)
export const hasContent = decoder => decoder.pos !== decoder.arr.length
/**
* Clone a decoder instance.
* Optionally set a new position parameter.
* @param {Decoder} decoder The decoder instance
* @return {Decoder} A clone of `decoder`
*/
export const clone = (decoder, newPos = decoder.pos) => {
let _decoder = createDecoder(decoder.arr.buffer)
_decoder.pos = newPos
return _decoder
}
/**
* Read `len` bytes as an ArrayBuffer.
* @param {Decoder} decoder The decoder instance
* @param {number} len The length of bytes to read
* @return {ArrayBuffer}
*/
export const readArrayBuffer = (decoder, len) => {
const arrayBuffer = globals.createUint8ArrayFromLen(len)
const view = globals.createUint8ArrayFromBuffer(decoder.arr.buffer, decoder.pos, len)
arrayBuffer.set(view)
decoder.pos += len
return arrayBuffer.buffer
}
/**
* Read variable length payload as ArrayBuffer
* @param {Decoder} decoder
* @return {ArrayBuffer}
*/
export const readPayload = decoder => readArrayBuffer(decoder, readVarUint(decoder))
/**
* Read the rest of the content as an ArrayBuffer
* @param {Decoder} decoder
* @return {ArrayBuffer}
*/
export const readTail = decoder => readArrayBuffer(decoder, decoder.arr.length - decoder.pos)
/**
* Skip one byte, jump to the next position.
* @param {Decoder} decoder The decoder instance
* @return {number} The next position
*/
export const skip8 = decoder => decoder.pos++
/**
* Read one byte as unsigned integer.
* @param {Decoder} decoder The decoder instance
* @return {number} Unsigned 8-bit integer
*/
export const readUint8 = decoder => decoder.arr[decoder.pos++]
/**
* Read 4 bytes as unsigned integer.
*
* @param {Decoder} decoder
* @return {number} An unsigned integer.
*/
export const readUint32 = decoder => {
let uint =
decoder.arr[decoder.pos] +
(decoder.arr[decoder.pos + 1] << 8) +
(decoder.arr[decoder.pos + 2] << 16) +
(decoder.arr[decoder.pos + 3] << 24)
decoder.pos += 4
return uint
}
/**
* Look ahead without incrementing position.
* to the next byte and read it as unsigned integer.
*
* @param {Decoder} decoder
* @return {number} An unsigned integer.
*/
export const peekUint8 = decoder => decoder.arr[decoder.pos]
/**
* Read unsigned integer (32bit) with variable length.
* 1/8th of the storage is used as encoding overhead.
* * numbers < 2^7 is stored in one bytlength
* * numbers < 2^14 is stored in two bylength
*
* @param {Decoder} decoder
* @return {number} An unsigned integer.length
*/
export const readVarUint = decoder => {
let num = 0
let len = 0
while (true) {
let r = decoder.arr[decoder.pos++]
num = num | ((r & 0b1111111) << len)
len += 7
if (r < 1 << 7) {
return num >>> 0 // return unsigned number!
}
if (len > 35) {
throw new Error('Integer out of range!')
}
}
}
/**
* Read string of variable length
* * varUint is used to store the length of the string
*
* Transforming utf8 to a string is pretty expensive. The code performs 10x better
* when String.fromCodePoint is fed with all characters as arguments.
* But most environments have a maximum number of arguments per functions.
* For effiency reasons we apply a maximum of 10000 characters at once.
*
* @param {Decoder} decoder
* @return {String} The read String.
*/
export const readVarString = decoder => {
let remainingLen = readVarUint(decoder)
let encodedString = ''
while (remainingLen > 0) {
const nextLen = remainingLen < 10000 ? remainingLen : 10000
const bytes = new Array(nextLen)
for (let i = 0; i < nextLen; i++) {
bytes[i] = decoder.arr[decoder.pos++]
}
encodedString += String.fromCodePoint.apply(null, bytes)
remainingLen -= nextLen
}
return decodeURIComponent(escape(encodedString))
}
/**
* Look ahead and read varString without incrementing position
* @param {Decoder} decoder
* @return {string}
*/
export const peekVarString = decoder => {
let pos = decoder.pos
let s = readVarString(decoder)
decoder.pos = pos
return s
}

View File

@@ -1,218 +0,0 @@
import * as globals from './globals.js'
const bits7 = 0b1111111
const bits8 = 0b11111111
/**
* A BinaryEncoder handles the encoding to an ArrayBuffer.
*/
export class Encoder {
constructor () {
this.cpos = 0
this.cbuf = globals.createUint8ArrayFromLen(1000)
this.bufs = []
}
}
export const createEncoder = () => new Encoder()
/**
* The current length of the encoded data.
*/
export const length = encoder => {
let len = encoder.cpos
for (let i = 0; i < encoder.bufs.length; i++) {
len += encoder.bufs[i].length
}
return len
}
/**
* Transform to ArrayBuffer. TODO: rename to .toArrayBuffer
* @param {Encoder} encoder
* @return {ArrayBuffer} The created ArrayBuffer.
*/
export const toBuffer = encoder => {
const uint8arr = globals.createUint8ArrayFromLen(length(encoder))
let curPos = 0
for (let i = 0; i < encoder.bufs.length; i++) {
let d = encoder.bufs[i]
uint8arr.set(d, curPos)
curPos += d.length
}
uint8arr.set(globals.createUint8ArrayFromBuffer(encoder.cbuf.buffer, 0, encoder.cpos), curPos)
return uint8arr.buffer
}
/**
* Write one byte to the encoder.
*
* @param {Encoder} encoder
* @param {number} num The byte that is to be encoded.
*/
export const write = (encoder, num) => {
if (encoder.cpos === encoder.cbuf.length) {
encoder.bufs.push(encoder.cbuf)
encoder.cbuf = globals.createUint8ArrayFromLen(encoder.cbuf.length * 2)
encoder.cpos = 0
}
encoder.cbuf[encoder.cpos++] = num
}
/**
* Write one byte at a specific position.
* Position must already be written (i.e. encoder.length > pos)
*
* @param {Encoder} encoder
* @param {number} pos Position to which to write data
* @param {number} num Unsigned 8-bit integer
*/
export const set = (encoder, pos, num) => {
let buffer = null
// iterate all buffers and adjust position
for (let i = 0; i < encoder.bufs.length && buffer === null; i++) {
const b = encoder.bufs[i]
if (pos < b.length) {
buffer = b // found buffer
} else {
pos -= b.length
}
}
if (buffer === null) {
// use current buffer
buffer = encoder.cbuf
}
buffer[pos] = num
}
/**
* Write one byte as an unsigned integer.
*
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
export const writeUint8 = (encoder, num) => write(encoder, num & bits8)
/**
* Write one byte as an unsigned Integer at a specific location.
*
* @param {Encoder} encoder
* @param {number} pos The location where the data will be written.
* @param {number} num The number that is to be encoded.
*/
export const setUint8 = (encoder, pos, num) => set(encoder, pos, num & bits8)
/**
* Write two bytes as an unsigned integer.
*
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
export const writeUint16 = (encoder, num) => {
write(encoder, num & bits8)
write(encoder, (num >>> 8) & bits8)
}
/**
* Write two bytes as an unsigned integer at a specific location.
*
* @param {Encoder} encoder
* @param {number} pos The location where the data will be written.
* @param {number} num The number that is to be encoded.
*/
export const setUint16 = (encoder, pos, num) => {
set(encoder, pos, num & bits8)
set(encoder, pos + 1, (num >>> 8) & bits8)
}
/**
* Write two bytes as an unsigned integer
*
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
export const writeUint32 = (encoder, num) => {
for (let i = 0; i < 4; i++) {
write(encoder, num & bits8)
num >>>= 8
}
}
/**
* Write two bytes as an unsigned integer at a specific location.
*
* @param {Encoder} encoder
* @param {number} pos The location where the data will be written.
* @param {number} num The number that is to be encoded.
*/
export const setUint32 = (encoder, pos, num) => {
for (let i = 0; i < 4; i++) {
set(encoder, pos + i, num & bits8)
num >>>= 8
}
}
/**
* Write a variable length unsigned integer.
*
* Encodes integers in the range from [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
*
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
export const writeVarUint = (encoder, num) => {
while (num >= 0b10000000) {
write(encoder, 0b10000000 | (bits7 & num))
num >>>= 7
}
write(encoder, bits7 & num)
}
/**
* Write a variable length string.
*
* @param {Encoder} encoder
* @param {String} str The string that is to be encoded.
*/
export const writeVarString = (encoder, str) => {
const encodedString = unescape(encodeURIComponent(str))
const len = encodedString.length
writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
write(encoder, encodedString.codePointAt(i))
}
}
/**
* Write the content of another Encoder.
*
* TODO: can be improved!
*
* @param {Encoder} encoder The enUint8Arr
* @param {Encoder} append The BinaryEncoder to be written.
*/
export const writeBinaryEncoder = (encoder, append) => writeArrayBuffer(encoder, toBuffer(append))
/**
* Append an arrayBuffer to the encoder.
*
* @param {Encoder} encoder
* @param {ArrayBuffer} arrayBuffer
*/
export const writeArrayBuffer = (encoder, arrayBuffer) => {
const prevBufferLen = encoder.cbuf.length
// TODO: Append to cbuf if possible
encoder.bufs.push(globals.createUint8ArrayFromBuffer(encoder.cbuf.buffer, 0, encoder.cpos))
encoder.bufs.push(globals.createUint8ArrayFromArrayBuffer(arrayBuffer))
encoder.cbuf = globals.createUint8ArrayFromLen(prevBufferLen)
encoder.cpos = 0
}
/**
* @param {Encoder} encoder
* @param {ArrayBuffer} arrayBuffer
*/
export const writePayload = (encoder, arrayBuffer) => {
writeVarUint(encoder, arrayBuffer.byteLength)
writeArrayBuffer(encoder, arrayBuffer)
}

View File

@@ -1,49 +0,0 @@
import * as encoding from './encoding.js'
/**
* Check if binary encoding is compatible with golang binary encoding - binary.PutVarUint.
*
* Result: is compatible up to 32 bit: [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
*/
let err = null
try {
const tests = [
{ in: 0, out: [0] },
{ in: 1, out: [1] },
{ in: 128, out: [128, 1] },
{ in: 200, out: [200, 1] },
{ in: 32, out: [32] },
{ in: 500, out: [244, 3] },
{ in: 256, out: [128, 2] },
{ in: 700, out: [188, 5] },
{ in: 1024, out: [128, 8] },
{ in: 1025, out: [129, 8] },
{ in: 4048, out: [208, 31] },
{ in: 5050, out: [186, 39] },
{ in: 1000000, out: [192, 132, 61] },
{ in: 34951959, out: [151, 166, 213, 16] },
{ in: 2147483646, out: [254, 255, 255, 255, 7] },
{ in: 2147483647, out: [255, 255, 255, 255, 7] },
{ in: 2147483648, out: [128, 128, 128, 128, 8] },
{ in: 2147483700, out: [180, 128, 128, 128, 8] },
{ in: 4294967294, out: [254, 255, 255, 255, 15] },
{ in: 4294967295, out: [255, 255, 255, 255, 15] }
]
tests.forEach(test => {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, test.in)
const buffer = new Uint8Array(encoding.toBuffer(encoder))
if (buffer.byteLength !== test.out.length) {
throw new Error('Length don\'t match!')
}
for (let j = 0; j < buffer.length; j++) {
if (buffer[j] !== test[1][j]) {
throw new Error('values don\'t match!')
}
}
})
} catch (error) {
err = error
} finally {
console.log('YDB Client: Encoding varUint compatiblity test: ', err || 'success!')
}

View File

@@ -1,63 +0,0 @@
/* eslint-env browser */
export const Uint8Array_ = Uint8Array
/**
* @param {Array<number>} arr
* @return {ArrayBuffer}
*/
export const createArrayBufferFromArray = arr => new Uint8Array_(arr).buffer
export const createUint8ArrayFromLen = len => new Uint8Array_(len)
/**
* Create Uint8Array with initial content from buffer
*/
export const createUint8ArrayFromBuffer = (buffer, byteOffset, length) => new Uint8Array_(buffer, byteOffset, length)
/**
* Create Uint8Array with initial content from buffer
*/
export const createUint8ArrayFromArrayBuffer = arraybuffer => new Uint8Array_(arraybuffer)
export const createArrayFromArrayBuffer = arraybuffer => Array.from(createUint8ArrayFromArrayBuffer(arraybuffer))
export const createPromise = f => new Promise(f)
export const createMap = () => new Map()
export const createSet = () => new Set()
/**
* `Promise.all` wait for all promises in the array to resolve and return the result
* @param {Array<Promise<any>>} arrp
* @return {any}
*/
export const pall = arrp => Promise.all(arrp)
export const preject = reason => Promise.reject(reason)
export const presolve = res => Promise.resolve(res)
export const until = (timeout, check) => createPromise((resolve, reject) => {
const hasTimeout = timeout > 0
const untilInterval = () => {
if (check()) {
clearInterval(intervalHandle)
resolve()
} else if (hasTimeout) {
timeout -= 10
if (timeout < 0) {
clearInterval(intervalHandle)
reject(error('Timeout'))
}
}
}
const intervalHandle = setInterval(untilInterval, 10)
})
export const error = description => new Error(description)
export const max = (a, b) => a > b ? a : b
/**
* @param {number} t Time to wait
* @return {Promise} Promise that is resolved after t ms
*/
export const wait = t => createPromise(r => setTimeout(r, t))

View File

@@ -1,159 +0,0 @@
/* eslint-env browser */
import * as globals from './globals.js'
/*
* IDB Request to Promise transformer
*/
export const rtop = request => globals.createPromise((resolve, reject) => {
request.onerror = event => reject(new Error(event.target.error))
request.onblocked = () => location.reload()
request.onsuccess = event => resolve(event.target.result)
})
/**
* @return {Promise<IDBDatabase>}
*/
export const openDB = (name, initDB) => globals.createPromise((resolve, reject) => {
let request = indexedDB.open(name)
/**
* @param {any} event
*/
request.onupgradeneeded = event => initDB(event.target.result)
/**
* @param {any} event
*/
request.onerror = event => reject(new Error(event.target.error))
request.onblocked = () => location.reload()
/**
* @param {any} event
*/
request.onsuccess = event => {
const db = event.target.result
db.onversionchange = () => { db.close() }
addEventListener('unload', () => db.close())
resolve(db)
}
})
export const deleteDB = name => rtop(indexedDB.deleteDatabase(name))
export const createStores = (db, definitions) => definitions.forEach(d =>
db.createObjectStore.apply(db, d)
)
/**
* @param {IDBObjectStore} store
* @param {String | number | ArrayBuffer | Date | Array } key
* @return {Promise<ArrayBuffer>}
*/
export const get = (store, key) =>
rtop(store.get(key))
/**
* @param {IDBObjectStore} store
* @param {String | number | ArrayBuffer | Date | IDBKeyRange | Array } key
*/
export const del = (store, key) =>
rtop(store.delete(key))
/**
* @param {IDBObjectStore} store
* @param {String | number | ArrayBuffer | Date | boolean} item
* @param {String | number | ArrayBuffer | Date | Array} [key]
*/
export const put = (store, item, key) =>
rtop(store.put(item, key))
/**
* @param {IDBObjectStore} store
* @param {String | number | ArrayBuffer | Date | boolean} item
* @param {String | number | ArrayBuffer | Date | Array} [key]
* @return {Promise<ArrayBuffer>}
*/
export const add = (store, item, key) =>
rtop(store.add(item, key))
/**
* @param {IDBObjectStore} store
* @param {String | number | ArrayBuffer | Date} item
* @return {Promise<number>}
*/
export const addAutoKey = (store, item) =>
rtop(store.add(item))
/**
* @param {IDBObjectStore} store
* @param {IDBKeyRange} [range]
*/
export const getAll = (store, range) =>
rtop(store.getAll(range))
/**
* @param {IDBObjectStore} store
* @param {IDBKeyRange} [range]
*/
export const getAllKeys = (store, range) =>
rtop(store.getAllKeys(range))
/**
* @typedef KeyValuePair
* @type {Object}
* @property {any} k key
* @property {any} v Value
*/
/**
* @param {IDBObjectStore} store
* @param {IDBKeyRange} [range]
* @return {Promise<Array<KeyValuePair>>}
*/
export const getAllKeysValues = (store, range) =>
globals.pall([getAllKeys(store, range), getAll(store, range)]).then(([ks, vs]) => ks.map((k, i) => ({ k, v: vs[i] })))
/**
* Iterate on keys and values
* @param {IDBObjectStore} store
* @param {IDBKeyRange?} keyrange
* @param {function(any, any)} f Return true in order to continue the cursor
*/
export const iterate = (store, keyrange, f) => globals.createPromise((resolve, reject) => {
const request = store.openCursor(keyrange)
request.onerror = reject
/**
* @param {any} event
*/
request.onsuccess = event => {
const cursor = event.target.result
if (cursor === null) {
return resolve()
}
f(cursor.value, cursor.key)
cursor.continue()
}
})
/**
* Iterate on the keys (no values)
* @param {IDBObjectStore} store
* @param {IDBKeyRange} keyrange
* @param {function(IDBCursor)} f Call `idbcursor.continue()` to iterate further
*/
export const iterateKeys = (store, keyrange, f) => {
/**
* @param {any} event
*/
store.openKeyCursor(keyrange).onsuccess = event => f(event.target.result)
}
/**
* Open store from transaction
* @param {IDBTransaction} t
* @param {String} store
* @returns {IDBObjectStore}
*/
export const getStore = (t, store) => t.objectStore(store)
export const createIDBKeyRangeBound = (lower, upper, lowerOpen, upperOpen) => IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen)
export const createIDBKeyRangeUpperBound = (upper, upperOpen) => IDBKeyRange.upperBound(upper, upperOpen)
export const createIDBKeyRangeLowerBound = (lower, lowerOpen) => IDBKeyRange.lowerBound(lower, lowerOpen)

View File

@@ -1,34 +0,0 @@
import * as test from './test.js'
import * as idb from './idb.js'
import * as logging from './logging.js'
const initTestDB = db => idb.createStores(db, [['test']])
const testDBName = 'idb-test'
const createTransaction = db => db.transaction(['test'], 'readwrite')
/**
* @param {IDBTransaction} t
* @return {IDBObjectStore}
*/
const getStore = t => idb.getStore(t, 'test')
idb.deleteDB(testDBName).then(() => idb.openDB(testDBName, initTestDB)).then(db => {
test.run('idb iteration', async testname => {
const t = createTransaction(db)
await idb.put(getStore(t), 0, ['t', 0])
await idb.put(getStore(t), 1, ['t', 1])
const valsGetAll = await idb.getAll(getStore(t))
if (valsGetAll.length !== 2) {
logging.fail('getAll does not return two values')
}
const valsIterate = []
const keyrange = idb.createIDBKeyRangeBound(['t', 0], ['t', 1], false, false)
await idb.put(getStore(t), 2, ['t', 2])
await idb.iterate(getStore(t), keyrange, (val, key) => {
valsIterate.push(val)
})
if (valsIterate.length !== 2) {
logging.fail('iterate does not return two values')
}
})
})

View File

@@ -1,23 +0,0 @@
import * as globals from './globals.js'
let date = new Date().getTime()
const writeDate = () => {
const oldDate = date
date = new Date().getTime()
return date - oldDate
}
export const print = (...args) => console.log(...args)
export const log = m => print(`%cydb-client: %c${m} %c+${writeDate()}ms`, 'color: blue;', '', 'color: blue')
export const fail = m => {
throw new Error(m)
}
/**
* @param {ArrayBuffer} buffer
* @return {string}
*/
export const arrayBufferToString = buffer => JSON.stringify(Array.from(globals.createUint8ArrayFromBuffer(buffer)))

View File

@@ -1,2 +0,0 @@
export const floor = Math.floor

View File

@@ -1,32 +0,0 @@
/**
* Creates a mutual exclude function with the following property:
*
* @example
* const mutex = createMutex()
* mutex(function () {
* // This function is immediately executed
* mutex(function () {
* // This function is never executed, as it is called with the same
* // mutex function
* })
* })
*
* @return {Function} A mutual exclude function
* @public
*/
export const createMutex = () => {
let token = true
return (f, g) => {
if (token) {
token = false
try {
f()
} finally {
token = true
}
} else if (g !== undefined) {
g()
}
}
}

View File

@@ -1,2 +0,0 @@
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER

View File

@@ -1,66 +0,0 @@
const N = 624
const M = 397
function twist (u, v) {
return ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0))
}
function nextState (state) {
let p = 0
let j
for (j = N - M + 1; --j; p++) {
state[p] = state[p + M] ^ twist(state[p], state[p + 1])
}
for (j = M; --j; p++) {
state[p] = state[p + M - N] ^ twist(state[p], state[p + 1])
}
state[p] = state[p + M - N] ^ twist(state[p], state[0])
}
/**
* This is a port of Shawn Cokus's implementation of the original Mersenne Twister algorithm (http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/MT2002/CODES/MTARCOK/mt19937ar-cok.c).
* MT has a very high period of 2^19937. Though the authors of xorshift describe that a high period is not
* very relevant (http://vigna.di.unimi.it/xorshift/). It is four times slower than xoroshiro128plus and
* needs to recompute its state after generating 624 numbers.
*
* @example
* const gen = new Mt19937(new Date().getTime())
* console.log(gen.next())
*
* @public
*/
export default class Mt19937 {
/**
* @param {Number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
*/
constructor (seed) {
this.seed = seed
const state = new Uint32Array(N)
state[0] = seed
for (let i = 1; i < N; i++) {
state[i] = (Math.imul(1812433253, (state[i - 1] ^ (state[i - 1] >>> 30))) + i) & 0xFFFFFFFF
}
this._state = state
this._i = 0
nextState(this._state)
}
/**
* Generate a random signed integer.
*
* @return {Number} A 32 bit signed integer.
*/
next () {
if (this._i === N) {
// need to compute a new state
nextState(this._state)
this._i = 0
}
let y = this._state[this._i++]
y ^= (y >>> 11)
y ^= (y << 7) & 0x9d2c5680
y ^= (y << 15) & 0xefc60000
y ^= (y >>> 18)
return y
}
}

View File

@@ -1,48 +0,0 @@
import Mt19937 from './Mt19937.js'
import Xoroshiro128plus from './Xoroshiro128plus.js'
import Xorshift32 from './Xorshift32.js'
import * as time from '../../time.js'
const DIAMETER = 300
const NUMBERS = 10000
function runPRNG (name, Gen) {
console.log('== ' + name + ' ==')
const gen = new Gen(1234)
let head = 0
let tails = 0
const date = time.getUnixTime()
const canvas = document.createElement('canvas')
canvas.height = DIAMETER
canvas.width = DIAMETER
const ctx = canvas.getContext('2d')
const vals = new Set()
ctx.fillStyle = 'blue'
for (let i = 0; i < NUMBERS; i++) {
const n = gen.next() & 0xFFFFFF
const x = (gen.next() >>> 0) % DIAMETER
const y = (gen.next() >>> 0) % DIAMETER
ctx.fillRect(x, y, 1, 2)
if ((n & 1) === 1) {
head++
} else {
tails++
}
if (vals.has(n)) {
console.warn(`The generator generated a duplicate`)
}
vals.add(n)
}
console.log('time: ', time.getUnixTime() - date)
console.log('head:', head, 'tails:', tails)
console.log('%c ', `font-size: 200px; background: url(${canvas.toDataURL()}) no-repeat;`)
const h1 = document.createElement('h1')
h1.insertBefore(document.createTextNode(name), null)
document.body.insertBefore(h1, null)
document.body.appendChild(canvas)
}
runPRNG('mt19937', Mt19937)
runPRNG('xoroshiro128plus', Xoroshiro128plus)
runPRNG('xorshift32', Xorshift32)

View File

@@ -1,5 +0,0 @@
# Pseudo Random Number Generators (PRNG)
Given a seed a PRNG generates a sequence of numbers that cannot be reasonably predicted. Two PRNGs must generate the same random sequence of numbers if given the same seed.
TODO: explain what POINT is

View File

@@ -1,98 +0,0 @@
import Xorshift32 from './Xorshift32.js'
/**
* This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures.
*
* This implementation follows the idea of the original xoroshiro128plus implementation,
* but is optimized for the JavaScript runtime. I.e.
* * The operations are performed on 32bit integers (the original implementation works with 64bit values).
* * The initial 128bit state is computed based on a 32bit seed and Xorshift32.
* * This implementation returns two 32bit values based on the 64bit value that is computed by xoroshiro128plus.
* Caution: The last addition step works slightly different than in the original implementation - the add carry of the
* first 32bit addition is not carried over to the last 32bit.
*
* [Reference implementation](http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c)
*/
export default class Xoroshiro128plus {
constructor (seed) {
this.seed = seed
// This is a variant of Xoroshiro128plus to fill the initial state
const xorshift32 = new Xorshift32(seed)
this.state = new Uint32Array(4)
for (let i = 0; i < 4; i++) {
this.state[i] = xorshift32.next()
}
this._fresh = true
}
next () {
const state = this.state
if (this._fresh) {
this._fresh = false
return (state[0] + state[2]) & 0xFFFFFFFF
} else {
this._fresh = true
const s0 = state[0]
const s1 = state[1]
const s2 = state[2] ^ s0
const s3 = state[3] ^ s1
// function js_rotl (x, k) {
// k = k - 32
// const x1 = x[0]
// const x2 = x[1]
// x[0] = x2 << k | x1 >>> (32 - k)
// x[1] = x1 << k | x2 >>> (32 - k)
// }
// rotl(s0, 55) // k = 23 = 55 - 32; j = 9 = 32 - 23
state[0] = (s1 << 23 | s0 >>> 9) ^ s2 ^ (s2 << 14 | s3 >>> 18)
state[1] = (s0 << 23 | s1 >>> 9) ^ s3 ^ (s3 << 14)
// rol(s1, 36) // k = 4 = 36 - 32; j = 23 = 32 - 9
state[2] = s3 << 4 | s2 >>> 28
state[3] = s2 << 4 | s3 >>> 28
return (state[1] + state[3]) & 0xFFFFFFFF
}
}
}
/*
// reference implementation
#include <stdint.h>
#include <stdio.h>
uint64_t s[2];
static inline uint64_t rotl(const uint64_t x, int k) {
return (x << k) | (x >> (64 - k));
}
uint64_t next(void) {
const uint64_t s0 = s[0];
uint64_t s1 = s[1];
s1 ^= s0;
s[0] = rotl(s0, 55) ^ s1 ^ (s1 << 14); // a, b
s[1] = rotl(s1, 36); // c
return (s[0] + s[1]) & 0xFFFFFFFF;
}
int main(void)
{
int i;
s[0] = 1111 | (1337ul << 32);
s[1] = 1234 | (9999ul << 32);
printf("1000 outputs of genrand_int31()\n");
for (i=0; i<100; i++) {
printf("%10lu ", i);
printf("%10lu ", next());
printf("- %10lu ", s[0] >> 32);
printf("%10lu ", (s[0] << 32) >> 32);
printf("%10lu ", s[1] >> 32);
printf("%10lu ", (s[1] << 32) >> 32);
printf("\n");
// if (i%5==4) printf("\n");
}
return 0;
}
*/

View File

@@ -1,26 +0,0 @@
/**
* Xorshift32 is a very simple but elegang PRNG with a period of `2^32-1`.
*/
export default class Xorshift32 {
/**
* @param {number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
*/
constructor (seed) {
this.seed = seed
this._state = seed
}
/**
* Generate a random signed integer.
*
* @return {Number} A 32 bit signed integer.
*/
next () {
let x = this._state
x ^= x << 13
x ^= x >> 17
x ^= x << 5
this._state = x
return x
}
}

View File

@@ -1,131 +0,0 @@
import * as binary from '../binary.js'
import { fromCharCode, fromCodePoint } from '../string.js'
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.js'
import * as math from '../math.js'
import DefaultPRNG from './PRNG/Xoroshiro128plus.js'
/**
* Description of the function
* @callback generatorNext
* @return {number} A 32bit integer
*/
/**
* A random type generator.
*
* @typedef {Object} PRNG
* @property {generatorNext} next Generate new number
*/
/**
* Create a Xoroshiro128plus Pseudo-Random-Number-Generator.
* This is the fastest full-period generator passing BigCrush without systematic failures.
* But there are more PRNGs available in ./PRNG/.
*
* @param {number} seed A positive 32bit integer. Do not use negative numbers.
* @return {PRNG}
*/
export const createPRNG = seed => new DefaultPRNG(Math.floor(seed < 1 ? seed * binary.BITS32 : seed))
/**
* Generates a single random bool.
*
* @param {PRNG} gen A random number generator.
* @return {Boolean} A random boolean
*/
export const bool = gen => (gen.next() & 2) === 2 // brackets are non-optional!
/**
* Generates a random integer with 53 bit resolution.
*
* @param {PRNG} gen A random number generator.
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
* @return {Number} A random integer on [min, max]
*/
export const int53 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => math.floor(real53(gen) * (max + 1 - min) + min)
/**
* Generates a random integer with 32 bit resolution.
*
* @param {PRNG} gen A random number generator.
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
* @return {Number} A random integer on [min, max]
*/
export const int32 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => min + ((gen.next() >>> 0) % (max + 1 - min))
/**
* Generates a random real on [0, 1) with 32 bit resolution.
*
* @param {PRNG} gen A random number generator.
* @return {Number} A random real number on [0, 1).
*/
export const real32 = gen => (gen.next() >>> 0) / binary.BITS32
/**
* Generates a random real on [0, 1) with 53 bit resolution.
*
* @param {PRNG} gen A random number generator.
* @return {Number} A random real number on [0, 1).
*/
export const real53 = gen => (((gen.next() >>> 5) * binary.BIT26) + (gen.next() >>> 6)) / MAX_SAFE_INTEGER
/**
* Generates a random character from char code 32 - 126. I.e. Characters, Numbers, special characters, and Space:
*
* (Space)!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}~
*/
export const char = gen => fromCharCode(int32(gen, 32, 126))
/**
* @param {PRNG} gen
* @return {string} A single letter (a-z)
*/
export const letter = gen => fromCharCode(int32(gen, 97, 122))
/**
* @param {PRNG} gen
* @return {string} A random word without spaces consisting of letters (a-z)
*/
export const word = gen => {
const len = int32(gen, 0, 20)
let str = ''
for (let i = 0; i < len; i++) {
str += letter(gen)
}
return str
}
/**
* TODO: this function produces invalid runes. Does not cover all of utf16!!
*/
export const utf16Rune = gen => {
const codepoint = int32(gen, 0, 256)
return fromCodePoint(codepoint)
}
/**
* @param {PRNG} gen
* @param {number} [maxlen = 20]
*/
export const utf16String = (gen, maxlen = 20) => {
const len = int32(gen, 0, maxlen)
let str = ''
for (let i = 0; i < len; i++) {
str += utf16Rune(gen)
}
return str
}
/**
* Returns one element of a given array.
*
* @param {PRNG} gen A random number generator.
* @param {Array<T>} array Non empty Array of possible values.
* @return {T} One of the values of the supplied Array.
* @template T
*/
export const oneOf = (gen, array) => array[int32(gen, 0, array.length - 1)]

View File

@@ -1,110 +0,0 @@
/**
*TODO: enable tests
import * as rt from '../rich-text/formatters.mjs'
import { test } from '../test/test.mjs'
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.mjs'
import Xorshift32 from './PRNG/Xorshift32.mjs'
import MT19937 from './PRNG/Mt19937.mjs'
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.mjs'
import { MAX_SAFE_INTEGER } from '../number/constants.mjs'
import { BIT32 } from '../binary/constants.mjs'
function init (Gen) {
return {
gen: new Gen(1234)
}
}
const PRNGs = [
{ name: 'Xoroshiro128plus', Gen: Xoroshiro128plus },
{ name: 'Xorshift32', Gen: Xorshift32 },
{ name: 'MT19937', Gen: MT19937 }
]
const ITERATONS = 1000000
for (const PRNG of PRNGs) {
const prefix = rt.orange`${PRNG.name}:`
test(rt.plain`${prefix} generateBool`, function generateBoolTest (t) {
const { gen } = init(PRNG.Gen)
let head = 0
let tail = 0
let b
let i
for (i = 0; i < ITERATONS; i++) {
b = generateBool(gen)
if (b) {
head++
} else {
tail++
}
}
t.log(`Generated ${head} heads and ${tail} tails.`)
t.assert(tail >= Math.floor(ITERATONS * 0.49), 'Generated enough tails.')
t.assert(head >= Math.floor(ITERATONS * 0.49), 'Generated enough heads.')
})
test(rt.plain`${prefix} generateInt integers average correctly`, function averageIntTest (t) {
const { gen } = init(PRNG.Gen)
let count = 0
let i
for (i = 0; i < ITERATONS; i++) {
count += generateInt(gen, 0, 100)
}
const average = count / ITERATONS
const expectedAverage = 100 / 2
t.log(`Average is: ${average}. Expected average is ${expectedAverage}.`)
t.assert(Math.abs(average - expectedAverage) <= 1, 'Expected average is at most 1 off.')
})
test(rt.plain`${prefix} generateInt32 generates integer with 32 bits`, function generateLargeIntegers (t) {
const { gen } = init(PRNG.Gen)
let num = 0
let i
let newNum
for (i = 0; i < ITERATONS; i++) {
newNum = generateInt32(gen, 0, MAX_SAFE_INTEGER)
if (newNum > num) {
num = newNum
}
}
t.log(`Largest number generated is ${num} (0b${num.toString(2)})`)
t.assert(num > (BIT32 >>> 0), 'Largest number is 32 bits long.')
})
test(rt.plain`${prefix} generateReal has 53 bit resolution`, function real53bitResolution (t) {
const { gen } = init(PRNG.Gen)
let num = 0
let i
let newNum
for (i = 0; i < ITERATONS; i++) {
newNum = generateReal(gen) * MAX_SAFE_INTEGER
if (newNum > num) {
num = newNum
}
}
t.log(`Largest number generated is ${num}.`)
t.assert((MAX_SAFE_INTEGER - num) / MAX_SAFE_INTEGER < 0.01, 'Largest number is close to MAX_SAFE_INTEGER (at most 1% off).')
})
test(rt.plain`${prefix} generateChar generates all described characters`, function real53bitResolution (t) {
const { gen } = init(PRNG.Gen)
const charSet = new Set()
const chars = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}~"'
let i
let char
for (i = chars.length - 1; i >= 0; i--) {
charSet.add(chars[i])
}
for (i = 0; i < ITERATONS; i++) {
char = generateChar(gen)
charSet.delete(char)
}
t.log(`Charactes missing: ${charSet.size} - generating all of "${chars}"`)
t.assert(charSet.size === 0, 'Generated all documented characters.')
})
}
*/

View File

@@ -1,47 +0,0 @@
/**
* A SimpleDiff describes a change on a String.
*
* @example
* console.log(a) // the old value
* console.log(b) // the updated value
* // Apply changes of diff (pseudocode)
* a.remove(diff.pos, diff.remove) // Remove `diff.remove` characters
* a.insert(diff.pos, diff.insert) // Insert `diff.insert`
* a === b // values match
*
* @typedef {Object} SimpleDiff
* @property {Number} pos The index where changes were applied
* @property {Number} remove The number of characters to delete starting
* at `index`.
* @property {String} insert The new text to insert at `index` after applying
* `delete`
*/
/**
* Create a diff between two strings. This diff implementation is highly
* efficient, but not very sophisticated.
*
* @public
* @param {String} a The old version of the string
* @param {String} b The updated version of the string
* @return {SimpleDiff} The diff description.
*/
export default function simpleDiff (a, b) {
let left = 0 // number of same characters counting from left
let right = 0 // number of same characters counting from right
while (left < a.length && left < b.length && a[left] === b[left]) {
left++
}
if (left !== a.length || left !== b.length) {
// Only check right if a !== b
while (right + left < a.length && right + left < b.length && a[a.length - right - 1] === b[b.length - right - 1]) {
right++
}
}
return {
pos: left, // TODO: rename to index (also in type above)
remove: a.length - left - right,
insert: b.slice(left, b.length - right)
}
}

View File

@@ -1,2 +0,0 @@
export const fromCharCode = String.fromCharCode
export const fromCodePoint = String.fromCodePoint

View File

@@ -1,33 +0,0 @@
import * as logging from './logging.js'
import simpleDiff from './simpleDiff.js'
export const run = async (name, f) => {
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
const start = new Date()
try {
await f(name)
} catch (e) {
logging.print(`%cFailure:%c ${name} in %c${new Date().getTime() - start.getTime()}ms`, 'color:red;font-weight:bold', '', 'color:grey')
throw e
}
logging.print(`%cSuccess:%c ${name} in %c${new Date().getTime() - start.getTime()}ms`, 'color:green;font-weight:bold', '', 'color:grey')
}
export const compareArrays = (as, bs) => {
if (as.length !== bs.length) {
return false
}
for (let i = 0; i < as.length; i++) {
if (as[i] !== bs[i]) {
return false
}
}
return true
}
export const compareStrings = (a, b) => {
if (a !== b) {
const diff = simpleDiff(a, b)
logging.print(`%c${a.slice(0, diff.pos)}%c${a.slice(diff.pos, diff.remove)}%c${diff.insert}%c${a.slice(diff.pos + diff.remove)}`, 'color:grey', 'color:red', 'color:green', 'color:grey')
}
}

View File

@@ -1,3 +0,0 @@
export const getDate = () => new Date()
export const getUnixTime = () => getDate().getTime()

7419
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +1,88 @@
{
"name": "yjs",
"version": "13.0.0-71",
"description": "A framework for real-time p2p shared editing on any data",
"main": "./build/node/index.js",
"module": "./src/index.js",
"version": "13.5.5",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",
"types": "./dist/src/index.d.ts",
"sideEffects": false,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
},
"scripts": {
"test": "npm run lint",
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
"lint": "standard src/**/*.js test/**/*.js tests-lib/**/*.js",
"docs": "esdoc",
"serve-docs": "npm run docs && serve ./docs/",
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
"postversion": "npm run dist"
"test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
"dist": "rm -rf dist && rollup -c && tsc",
"watch": "rollup -wc",
"lint": "markdownlint README.md && standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
"serve-docs": "npm run docs && http-server ./docs/",
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repetition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs",
"debug": "concurrently 'http-server -o test.html' 'npm run watch'",
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs",
"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",
"./package.json": "./package.json"
},
"files": [
"src/*",
".esdoc.json",
"docs/*",
"build/*",
"lib/*",
"provider/*",
"bindings/*"
"dist/yjs.*",
"dist/src",
"src",
"tests/testHelper.js",
"sponsor-y.js"
],
"bin": {
"y-websockets": "provider/websocket/server.js"
"dictionaries": {
"test": "tests"
},
"standard": {
"ignore": [
"/y.js",
"/y.js.map"
"/dist",
"/node_modules",
"/docs"
]
},
"repository": {
"type": "git",
"url": "https://github.com/y-js/yjs.git"
"url": "https://github.com/yjs/yjs.git"
},
"keywords": [
"Yjs",
"OT",
"Collaboration",
"Synchronization",
"ShareJS",
"Coweb",
"Concurrency"
"CRDT",
"offline",
"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/y-js/yjs/issues"
},
"homepage": "http://y-js.org",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-plugin-external-helpers": "^6.22.0",
"babel-plugin-transform-regenerator": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-latest": "^6.24.1",
"concurrently": "^3.6.1",
"cutest": "^0.1.9",
"esdoc": "^1.1.0",
"esdoc-standard-plugin": "^1.0.0",
"prosemirror-example-setup": "^1.0.1",
"prosemirror-schema-basic": "^1.0.0",
"prosemirror-state": "^1.2.2",
"prosemirror-view": "^1.6.5",
"quill": "^1.3.6",
"quill-cursors": "^1.0.3",
"rollup": "^0.58.2",
"rollup-plugin-babel": "^2.7.1",
"rollup-plugin-commonjs": "^8.4.1",
"rollup-plugin-inject": "^2.2.0",
"rollup-plugin-multi-entry": "^2.0.2",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-uglify": "^1.0.2",
"rollup-regenerator-runtime": "^6.23.1",
"rollup-watch": "^3.2.2",
"standard": "^11.0.1"
"url": "https://github.com/yjs/yjs/issues"
},
"homepage": "https://docs.yjs.dev",
"dependencies": {
"ws": "^6.1.0"
"lib0": "^0.2.38"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.2.0",
"concurrently": "^3.6.1",
"http-server": "^0.12.3",
"jsdoc": "^3.6.6",
"markdownlint-cli": "^0.23.2",
"rollup": "^2.39.0",
"standard": "^14.3.4",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^4.1.5",
"y-protocols": "^1.0.4"
}
}

View File

@@ -1,121 +0,0 @@
/* eslint-env browser */
import * as Y from '../../src/index.js'
export * from '../../src/index.js'
const messageSync = 0
const messageAwareness = 1
const reconnectTimeout = 100
const setupWS = (doc, url) => {
const websocket = new WebSocket(url)
websocket.binaryType = 'arraybuffer'
doc.ws = websocket
websocket.onmessage = event => {
const decoder = Y.createDecoder(event.data)
const encoder = Y.createEncoder()
const messageType = Y.readVarUint(decoder)
switch (messageType) {
case messageSync:
Y.writeVarUint(encoder, messageSync)
doc.mux(() =>
Y.readSyncMessage(decoder, encoder, doc)
)
break
case messageAwareness:
Y.readAwarenessMessage(decoder, doc)
break
}
if (Y.length(encoder) > 1) {
websocket.send(Y.toBuffer(encoder))
}
}
websocket.onclose = () => {
doc.ws = null
doc.wsconnected = false
doc.emit('status', {
status: 'connected'
})
setTimeout(setupWS, reconnectTimeout, doc, url)
}
websocket.onopen = () => {
doc.wsconnected = true
doc.emit('status', {
status: 'disconnected'
})
// always send sync step 1 when connected
const encoder = Y.createEncoder()
Y.writeVarUint(encoder, messageSync)
Y.writeSyncStep1(encoder, doc)
websocket.send(Y.toBuffer(encoder))
// force send stored awareness info
doc.setAwarenessField(null, null)
}
}
const broadcastUpdate = (y, transaction) => {
if (y.wsconnected && transaction.encodedStructsLen > 0) {
y.mux(() => {
const encoder = Y.createEncoder()
Y.writeVarUint(encoder, messageSync)
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
y.ws.send(Y.toBuffer(encoder))
})
}
}
class WebsocketsSharedDocument extends Y.Y {
constructor (url) {
super()
this.wsconnected = false
this.mux = Y.createMutex()
this.ws = null
this._localAwarenessState = {}
this.awareness = new Map()
setupWS(this, url)
this.on('afterTransaction', broadcastUpdate)
}
getLocalAwarenessInfo () {
return this._localAwarenessState
}
getAwarenessInfo () {
return this.awareness
}
setAwarenessField (field, value) {
if (field !== null) {
this._localAwarenessState[field] = value
}
if (this.wsconnected) {
const encoder = Y.createEncoder()
Y.writeVarUint(encoder, messageAwareness)
Y.writeUsersStateChange(encoder, [{ userID: this.userID, state: this._localAwarenessState }])
this.ws.send(Y.toBuffer(encoder))
}
}
}
export default class WebsocketProvider {
constructor (url) {
// ensure that url is always ends with /
while (url[url.length - 1] === '/') {
url = url.slice(0, url.length - 1)
}
this.url = url + '/'
/**
* @type {Map<string, WebsocketsSharedDocument>}
*/
this.docs = new Map()
}
/**
* @param {string} name
* @return {WebsocketsSharedDocument}
*/
get (name) {
let doc = this.docs.get(name)
if (doc === undefined) {
doc = new WebsocketsSharedDocument(this.url + name)
}
return doc
}
}

View File

@@ -1,101 +0,0 @@
const Y = require('../../build/node/index.js')
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 1234 })
const docs = new Map()
const messageSync = 0
const messageAwareness = 1
const afterTransaction = (doc, transaction) => {
if (transaction.encodedStructsLen > 0) {
const encoder = Y.createEncoder()
Y.writeVarUint(encoder, messageSync)
Y.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
const message = Y.toBuffer(encoder)
doc.conns.forEach((_, conn) => conn.send(message))
}
}
class WSSharedDoc extends Y.Y {
constructor () {
super()
this.mux = Y.createMutex()
/**
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
* @type {Map<Object, Set<number>>}
*/
this.conns = new Map()
this.awareness = new Map()
this.on('afterTransaction', afterTransaction)
}
}
const messageListener = (conn, doc, message) => {
const encoder = Y.createEncoder()
const decoder = Y.createDecoder(message)
const messageType = Y.readVarUint(decoder)
switch (messageType) {
case messageSync:
Y.writeVarUint(encoder, messageSync)
Y.readSyncMessage(decoder, encoder, doc)
if (Y.length(encoder) > 1) {
conn.send(Y.toBuffer(encoder))
}
break
case messageAwareness: {
Y.writeVarUint(encoder, messageAwareness)
const updates = Y.forwardAwarenessMessage(decoder, encoder)
updates.forEach(update => {
doc.awareness.set(update.userID, update.state)
doc.conns.get(conn).add(update.userID)
})
const buff = Y.toBuffer(encoder)
doc.conns.forEach((_, c) => {
c.send(buff)
})
break
}
}
}
const setupConnection = (conn, req) => {
conn.binaryType = 'arraybuffer'
// get doc, create if it does not exist yet
let doc = docs.get(req.url.slice(1))
if (doc === undefined) {
doc = new WSSharedDoc()
docs.set(req.url.slice(1), doc)
}
doc.conns.set(conn, new Set())
// listen and reply to events
conn.on('message', message => messageListener(conn, doc, message))
conn.on('close', () => {
const controlledIds = doc.conns.get(conn)
doc.conns.delete(conn)
const encoder = Y.createEncoder()
Y.writeVarUint(encoder, messageAwareness)
Y.writeUsersStateChange(encoder, Array.from(controlledIds).map(userID => {
doc.awareness.delete(userID)
return { userID, state: null }
}))
const buf = Y.toBuffer(encoder)
doc.conns.forEach((_, conn) => conn.send(buf))
})
// send sync step 1
const encoder = Y.createEncoder()
Y.writeVarUint(encoder, messageSync)
Y.writeSyncStep1(encoder, doc)
conn.send(Y.toBuffer(encoder))
if (doc.awareness.size > 0) {
const encoder = Y.createEncoder()
const userStates = []
doc.awareness.forEach((state, userID) => {
userStates.push({ state, userID })
})
Y.writeVarUint(encoder, messageAwareness)
Y.writeUsersStateChange(encoder, userStates)
conn.send(Y.toBuffer(encoder))
}
}
wss.on('connection', setupConnection)

View File

@@ -1,31 +0,0 @@
import babel from 'rollup-plugin-babel'
import uglify from 'rollup-plugin-uglify'
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
var pkg = require('./package.json')
export default {
input: 'src/index.js',
name: 'Y',
sourcemap: true,
output: {
file: 'build/umd/index.js',
format: 'umd'
},
plugins: [
nodeResolve({
main: true,
module: true,
browser: true
}),
commonjs(),
babel(),
],
banner: `
/**
* ${pkg.name} - ${pkg.description}
* @version v${pkg.version}
* @license ${pkg.license}
*/
`
}

94
rollup.config.js Normal file
View File

@@ -0,0 +1,94 @@
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, -3)}.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/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']
}]

View File

@@ -1,18 +0,0 @@
const pkg = require('./package.json')
export default {
input: 'src/index.js',
output: {
name: 'Y',
file: 'build/node/index.js',
format: 'cjs',
sourcemap: true,
banner: `
/**
* ${pkg.name} - ${pkg.description}
* @version v${pkg.version}
* @license ${pkg.license}
*/
`
}
}

View File

@@ -1,22 +0,0 @@
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import multiEntry from 'rollup-plugin-multi-entry'
export default {
input: 'test/index.js',
name: 'y-tests',
sourcemap: true,
output: {
file: 'y.test.js',
format: 'umd'
},
plugins: [
multiEntry(),
nodeResolve({
main: true,
module: true,
browser: true
}),
commonjs()
]
}

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

View File

@@ -1,2 +0,0 @@
export default class AbstractPersistence {}

View File

@@ -1,72 +0,0 @@
import fs from 'fs'
import path from 'path'
import * as encoding from '../../lib/encoding.js'
import * as decoding from '../../lib/decoding.js'
import { createMutualExclude } from '../../lib/mutualExclude.js'
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
function createFilePath (persistence, roomName) {
// TODO: filename checking!
return path.join(persistence.dir, roomName)
}
export default class FilePersistence {
constructor (dir) {
this.dir = dir
this._mutex = createMutualExclude()
}
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
// TODO: implement
// nop
}
saveUpdate (room, y, encodedStructs) {
return new Promise((resolve, reject) => {
this._mutex(() => {
const filePath = createFilePath(this, room)
const updateMessage = encoding.createEncoder()
encodeUpdate(y, encodedStructs, updateMessage)
fs.appendFile(filePath, Buffer.from(encoding.toBuffer(updateMessage)), (err) => {
if (err !== null) {
reject(err)
} else {
resolve()
}
})
}, resolve)
})
}
saveState (roomName, y) {
return new Promise((resolve, reject) => {
const encoder = encoding.createEncoder()
encodeStructsDS(y, encoder)
const filePath = createFilePath(this, roomName)
fs.writeFile(filePath, Buffer.from(encoding.toBuffer(encoder)), (err) => {
if (err !== null) {
reject(err)
} else {
resolve()
}
})
})
}
readState (roomName, y) {
// Check if the file exists in the current directory.
return new Promise((resolve, reject) => {
const filePath = path.join(this.dir, roomName)
fs.readFile(filePath, (err, data) => {
if (err !== null) {
resolve()
// reject(err)
} else {
this._mutex(() => {
console.info(`unpacking data (${data.length})`)
console.time('unpacking')
decodePersisted(y, decoding.createDecoder(data.buffer))
console.timeEnd('unpacking')
})
resolve()
}
})
})
}
}

View File

@@ -1,281 +0,0 @@
/* global indexedDB, location, BroadcastChannel */
import Y from '../Y.js'
import { createMutualExclude } from '../../lib/mutualExclude.js'
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
import * as decoding from '../../lib/decoding.js'
import * as encoding from '../../lib/encoding.js'
/*
* Request to Promise transformer
*/
function rtop (request) {
return new Promise(function (resolve, reject) {
request.onerror = function (event) {
reject(new Error(event.target.error))
}
request.onblocked = function () {
location.reload()
}
request.onsuccess = function (event) {
resolve(event.target.result)
}
})
}
function openDB (room) {
return new Promise(function (resolve, reject) {
let request = indexedDB.open(room)
request.onupgradeneeded = function (event) {
const db = event.target.result
if (db.objectStoreNames.contains('updates')) {
db.deleteObjectStore('updates')
}
db.createObjectStore('updates', {autoIncrement: true})
}
request.onerror = function (event) {
reject(new Error(event.target.error))
}
request.onblocked = function () {
location.reload()
}
request.onsuccess = function (event) {
const db = event.target.result
db.onversionchange = function () { db.close() }
resolve(db)
}
})
}
function persist (room) {
let t = room.db.transaction(['updates'], 'readwrite')
let updatesStore = t.objectStore('updates')
return rtop(updatesStore.getAll())
.then(updates => {
// apply all previous updates before deleting them
room.mutex(() => {
updates.forEach(update => {
decodePersisted(y, new BinaryDecoder(update))
})
})
const encoder = new BinaryEncoder()
encodeStructsDS(y, encoder)
// delete all pending updates
rtop(updatesStore.clear()).then(() => {
// write current model
updatesStore.put(encoder.createBuffer())
})
})
}
function saveUpdate (room, updateBuffer) {
const db = room.db
if (db !== null) {
const t = db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
const updatePut = rtop(updatesStore.put(updateBuffer))
rtop(updatesStore.count()).then(cnt => {
if (cnt >= PREFERRED_TRIM_SIZE) {
persist(room)
}
})
return updatePut
}
}
function registerRoomInPersistence (documentsDB, roomName) {
return documentsDB.then(
db => Promise.all([
db,
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
])
).then(
([db, doc]) => {
if (doc === undefined) {
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
}
}
)
}
const PREFERRED_TRIM_SIZE = 400
export default class IndexedDBPersistence {
constructor () {
this._rooms = new Map()
this._documentsDB = new Promise(function (resolve, reject) {
let request = indexedDB.open('_yjs_documents')
request.onupgradeneeded = function (event) {
const db = event.target.result
if (db.objectStoreNames.contains('documents')) {
db.deleteObjectStore('documents')
}
db.createObjectStore('documents', { keyPath: "roomName" })
}
request.onerror = function (event) {
reject(new Error(event.target.error))
}
request.onblocked = function () {
location.reload()
}
request.onsuccess = function (event) {
const db = event.target.result
db.onversionchange = function () { db.close() }
resolve(db)
}
})
addEventListener('unload', () => {
// close everything when page unloads
this._rooms.forEach(room => {
if (room.db !== null) {
room.db.close()
} else {
room.dbPromise.then(db => db.close())
}
})
this._documentsDB.then(db => db.close())
})
}
getAllDocuments () {
return this._documentsDB.then(
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
)
}
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
this._documentsDB.then(
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
)
}
_createYInstance (roomName) {
const room = this._rooms.get(roomName)
if (room !== undefined) {
return room.y
}
const y = new Y()
return openDB(roomName).then(
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
).then(
updates =>
y.transact(() => {
updates.forEach(update => {
decodePersisted(y, new BinaryDecoder(update))
})
}, true)
).then(() => Promise.resolve(y))
}
_persistStructsDS (roomName, structsDS) {
const encoder = new BinaryEncoder()
encoder.writeVarUint(PERSIST_STRUCTS_DS)
encoder.writeArrayBuffer(structsDS)
return openDB(roomName).then(db => {
const t = db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
return rtop(updatesStore.put(encoder.createBuffer()))
})
}
_persistStructs (roomName, structs) {
const encoder = new BinaryEncoder()
encoder.writeVarUint(PERSIST_UPDATE)
encoder.writeArrayBuffer(structs)
return openDB(roomName).then(db => {
const t = db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
return rtop(updatesStore.put(encoder.createBuffer()))
})
}
connectY (roomName, y) {
if (this._rooms.has(roomName)) {
throw new Error('A Y instance is already bound to this room!')
}
let room = {
db: null,
dbPromise: null,
channel: null,
mutex: createMutualExclude(),
y
}
if (typeof BroadcastChannel !== 'undefined') {
room.channel = new BroadcastChannel('__yjs__' + roomName)
room.channel.addEventListener('message', e => {
room.mutex(function () {
decodePersisted(y, new BinaryDecoder(e.data))
})
})
}
y.on('destroyed', () => {
this.disconnectY(roomName, y)
})
y.on('afterTransaction', (y, transaction) => {
room.mutex(() => {
if (transaction.encodedStructsLen > 0) {
const encoder = new BinaryEncoder()
const update = new BinaryEncoder()
encodeUpdate(y, transaction.encodedStructs, update)
const updateBuffer = update.createBuffer()
if (room.channel !== null) {
room.channel.postMessage(updateBuffer)
}
if (transaction.encodedStructsLen > 0) {
if (room.db !== null) {
saveUpdate(room, updateBuffer)
}
}
}
})
})
// register document in documentsDB
this._documentsDB.then(
db =>
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
.then(
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
)
)
// open room db and read existing data
return room.dbPromise = openDB(roomName)
.then(db => {
room.db = db
const t = room.db.transaction(['updates'], 'readwrite')
const updatesStore = t.objectStore('updates')
// write current state as update
const encoder = new BinaryEncoder()
encodeStructsDS(y, encoder)
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
// read persisted state
return rtop(updatesStore.getAll()).then(updates => {
room.mutex(() => {
y.transact(() => {
updates.forEach(update => {
decodePersisted(y, new BinaryDecoder(update))
})
}, true)
})
})
})
})
}
disconnectY (roomName) {
const {
db, channel
} = this._rooms.get(roomName)
db.close()
if (channel !== null) {
channel.close()
}
this._rooms.delete(roomName)
}
/**
* Remove all persisted data that belongs to a room.
* Automatically destroys all Yjs all Yjs instances that persist to
* the room. If `destroyYjsInstances = false` the persistence functionality
* will be removed from the Yjs instances.
*/
removePersistedData (roomName, destroyYjsInstances = true) {
this.disconnectY(roomName)
return rtop(indexedDB.deleteDatabase(roomName))
}
}

View File

@@ -1,51 +0,0 @@
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js'
import { writeStructs } from '../MessageHandler/syncStep1.js'
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js'
export const PERSIST_UPDATE = 0
/**
* Write an update to an encoder.
*
* @param {Yjs} y A Yjs instance
* @param {BinaryEncoder} updateEncoder I.e. transaction.encodedStructs
*/
export function encodeUpdate (y, updateEncoder, encoder) {
encoder.writeVarUint(PERSIST_UPDATE)
encoder.writeBinaryEncoder(updateEncoder)
}
export const PERSIST_STRUCTS_DS = 1
/**
* Write the current Yjs data model to an encoder.
*
* @param {Yjs} y A Yjs instance
* @param {BinaryEncoder} encoder An encoder to write to
*/
export function encodeStructsDS (y, encoder) {
encoder.writeVarUint(PERSIST_STRUCTS_DS)
writeStructs(y, encoder, new Map())
writeDeleteSet(y, encoder)
}
/**
* Feed the Yjs instance with the persisted state
* @param {Yjs} y A Yjs instance.
* @param {BinaryDecoder} decoder A Decoder instance that holds the file content.
*/
export function decodePersisted (y, decoder) {
y.transact(() => {
while (decoder.hasContent()) {
const contentType = decoder.readVarUint()
switch (contentType) {
case PERSIST_UPDATE:
integrateRemoteStructs(decoder, y)
break
case PERSIST_STRUCTS_DS:
integrateRemoteStructs(decoder, y)
readDeleteSet(y, decoder)
break
}
}
}, true)
}

View File

@@ -1,90 +0,0 @@
import Tree from '../../lib/Tree.js'
import * as ID from '../Util/ID.js'
class DSNode {
constructor (id, len, gc) {
this._id = id
this.len = len
this.gc = gc
}
clone () {
return new DSNode(this._id, this.len, this.gc)
}
}
export default class DeleteStore extends Tree {
logTable () {
const deletes = []
this.iterate(null, null, function (n) {
deletes.push({
user: n._id.user,
clock: n._id.clock,
len: n.len,
gc: n.gc
})
})
console.table(deletes)
}
isDeleted (id) {
var n = this.findWithUpperBound(id)
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
}
mark (id, length, gc) {
if (length === 0) return
// Step 1. Unmark range
const leftD = this.findWithUpperBound(ID.createID(id.user, id.clock - 1))
// Resize left DSNode if necessary
if (leftD !== null && leftD._id.user === id.user) {
if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) {
// node is overlapping. need to resize
if (id.clock + length < leftD._id.clock + leftD.len) {
// overlaps new mark range and some more
// create another DSNode to the right of new mark
this.put(new DSNode(ID.createID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc))
}
// resize left DSNode
leftD.len = id.clock - leftD._id.clock
} // Otherwise there is no overlapping
}
// Resize right DSNode if necessary
const upper = ID.createID(id.user, id.clock + length - 1)
const rightD = this.findWithUpperBound(upper)
if (rightD !== null && rightD._id.user === id.user) {
if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node
const d = id.clock + length - rightD._id.clock
rightD._id = ID.createID(rightD._id.user, rightD._id.clock + d)
rightD.len -= d
}
}
// Now we only have to delete all inner marks
const deleteNodeIds = []
this.iterate(id, upper, m => {
deleteNodeIds.push(m._id)
})
for (let i = deleteNodeIds.length - 1; i >= 0; i--) {
this.delete(deleteNodeIds[i])
}
let newMark = new DSNode(id, length, gc)
// Step 2. Check if we can extend left or right
if (leftD !== null && leftD._id.user === id.user && leftD._id.clock + leftD.len === id.clock && leftD.gc === gc) {
// We can extend left
leftD.len += length
newMark = leftD
}
const rightNext = this.find(ID.createID(id.user, id.clock + length))
if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) {
// We can merge newMark and rightNext
newMark.len += rightNext.len
this.delete(rightNext._id)
}
if (leftD !== newMark) {
// only put if we didn't extend left
this.put(newMark)
}
}
// TODO: exchange markDeleted for mark()
markDeleted (id, length) {
this.mark(id, length, false)
}
}

View File

@@ -1,94 +0,0 @@
import Tree from '../../lib/Tree.js'
import * as ID from '../Util/ID.js'
import { getStruct } from '../Util/structReferences.js'
import { stringifyID, stringifyItemID } from '../protocols/syncProtocol.js'
import GC from '../Struct/GC.js'
export default class OperationStore extends Tree {
constructor (y) {
super()
this.y = y
}
logTable () {
const items = []
this.iterate(null, null, function (item) {
if (item.constructor === GC) {
items.push({
id: stringifyItemID(item),
content: item._length,
deleted: 'GC'
})
} else {
items.push({
id: stringifyItemID(item),
origin: item._origin === null ? '()' : stringifyID(item._origin._lastId),
left: item._left === null ? '()' : stringifyID(item._left._lastId),
right: stringifyItemID(item._right),
right_origin: stringifyItemID(item._right_origin),
parent: stringifyItemID(item._parent),
parentSub: item._parentSub,
deleted: item._deleted,
content: JSON.stringify(item._content)
})
}
})
console.table(items)
}
get (id) {
let struct = this.find(id)
if (struct === null && id instanceof ID.RootID) {
const Constr = getStruct(id.type)
const y = this.y
struct = new Constr()
struct._id = id
struct._parent = y
y.transact(() => {
struct._integrate(y)
})
this.put(struct)
}
return struct
}
// Use getItem for structs with _length > 1
getItem (id) {
var item = this.findWithUpperBound(id)
if (item === null) {
return null
}
const itemID = item._id
if (id.user === itemID.user && id.clock < itemID.clock + item._length) {
return item
} else {
return null
}
}
// Return an insertion such that id is the first element of content
// This function manipulates an item, if necessary
getItemCleanStart (id) {
var ins = this.getItem(id)
if (ins === null || ins._length === 1) {
return ins
}
const insID = ins._id
if (insID.clock === id.clock) {
return ins
} else {
return ins._splitAt(this.y, id.clock - insID.clock)
}
}
// Return an insertion such that id is the last element of content
// This function manipulates an operation, if necessary
getItemCleanEnd (id) {
var ins = this.getItem(id)
if (ins === null || ins._length === 1) {
return ins
}
const insID = ins._id
if (insID.clock + ins._length - 1 === id.clock) {
return ins
} else {
ins._splitAt(this.y, id.clock - insID.clock + 1)
return ins
}
}
}

View File

@@ -1,51 +0,0 @@
import * as ID from '../Util/ID.js'
/**
* @typedef {Map<number, number>} StateSet
*/
export default class StateStore {
constructor (y) {
this.y = y
this.state = new Map()
}
logTable () {
const entries = []
for (let [user, state] of this.state) {
entries.push({
user, state
})
}
console.table(entries)
}
getNextID (len) {
const user = this.y.userID
const state = this.getState(user)
this.setState(user, state + len)
return ID.createID(user, state)
}
updateRemoteState (struct) {
let user = struct._id.user
let userState = this.state.get(user)
while (struct !== null && struct._id.clock === userState) {
userState += struct._length
struct = this.y.os.get(ID.createID(user, userState))
}
this.state.set(user, userState)
}
getState (user) {
let state = this.state.get(user)
if (state == null) {
return 0
}
return state
}
setState (user, state) {
// TODO: modify missingi structs here
const beforeState = this.y._transaction.beforeState
if (!beforeState.has(user)) {
beforeState.set(user, this.getState(user))
}
this.state.set(user, state)
}
}

View File

@@ -1,130 +0,0 @@
import { getStructReference } from '../Util/structReferences.js'
import * as ID from '../Util/ID.js'
import { stringifyID } from '../protocols/syncProtocol.js'
import { writeStructToTransaction } from '../Util/Transaction.js'
import * as decoding from '../../lib/decoding.js'
import * as encoding from '../../lib/encoding.js'
/**
* @private
* Delete all items in an ID-range.
* Does not create delete operations!
* TODO: implement getItemCleanStartNode for better performance (only one lookup).
*/
export function deleteItemRange (y, user, clock, range, gcChildren) {
let item = y.os.getItemCleanStart(ID.createID(user, clock))
if (item !== null) {
if (!item._deleted) {
item._splitAt(y, range)
item._delete(y, false, true)
}
let itemLen = item._length
range -= itemLen
clock += itemLen
if (range > 0) {
let node = y.os.findNode(ID.createID(user, clock))
while (node !== null && node.val !== null && range > 0 && node.val._id.equals(ID.createID(user, clock))) {
const nodeVal = node.val
if (!nodeVal._deleted) {
nodeVal._splitAt(y, range)
nodeVal._delete(y, false, gcChildren)
}
const nodeLen = nodeVal._length
range -= nodeLen
clock += nodeLen
node = node.next()
}
}
}
}
/**
* @private
* A Delete change is not a real Item, but it provides the same interface as an
* Item. The only difference is that it will not be saved in the ItemStore
* (OperationStore), but instead it is safed in the DeleteStore.
*/
export default class Delete {
constructor () {
/**
* @type {ID.ID}
*/
this._targetID = null
/**
* @type {import('./Item.js').default}
*/
this._target = null
this._length = null
}
/**
* @private
* Read the next Item in a Decoder and fill this Item with the read data.
*
* This is called when data is received from a remote peer.
*
* @param {import('../Y.js').default} y The Yjs instance that this Item belongs to.
* @param {decoding.Decoder} decoder The decoder object to read data from.
*/
_fromBinary (y, decoder) {
// TODO: set target, and add it to missing if not found
// There is an edge case in p2p networks!
/**
* @type {any}
*/
const targetID = ID.decode(decoder)
this._targetID = targetID
this._length = decoding.readVarUint(decoder)
if (y.os.getItem(targetID) === null) {
return [targetID]
} else {
return []
}
}
/**
* @private
* 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 {encoding.Encoder} encoder The encoder to write data to.
*/
_toBinary (encoder) {
encoding.writeUint8(encoder, getStructReference(this.constructor))
this._targetID.encode(encoder)
encoding.writeVarUint(encoder, this._length)
}
/**
* @private
* Integrates this Item into the shared structure.
*
* This method actually applies the change to the Yjs instance. In the case of
* Delete it marks the delete target as deleted.
*
* * If created remotely (a remote user deleted something),
* this Delete is applied to all structs in id-range.
* * If created lokally (e.g. when y-array deletes a range of elements),
* this struct is broadcasted only (it is already executed)
*/
_integrate (y, locallyCreated = false) {
if (!locallyCreated) {
// from remote
const id = this._targetID
deleteItemRange(y, id.user, id.clock, this._length, false)
}
writeStructToTransaction(y._transaction, this)
}
/**
* Transform this YXml Type to a readable format.
* Useful for logging as all Items and Delete implement this method.
*
* @private
*/
_logString () {
return `Delete - target: ${stringifyID(this._targetID)}, len: ${this._length}`
}
}

View File

@@ -1,97 +0,0 @@
import { getStructReference } from '../Util/structReferences.js'
import * as ID from '../Util/ID.js'
import { writeStructToTransaction } from '../Util/Transaction.js'
import * as decoding from '../../lib/decoding.js'
import * as encoding from '../../lib/encoding.js'
// TODO should have the same base class as Item
export default class GC {
constructor () {
/**
* @type {ID.ID}
*/
this._id = null
this._length = 0
}
get _deleted () {
return true
}
_integrate (y) {
const id = this._id
const userState = y.ss.getState(id.user)
if (id.clock === userState) {
y.ss.setState(id.user, id.clock + this._length)
}
y.ds.mark(this._id, this._length, true)
let n = y.os.put(this)
const prev = n.prev().val
if (prev !== null && prev.constructor === GC && prev._id.user === n.val._id.user && prev._id.clock + prev._length === n.val._id.clock) {
// TODO: do merging for all items!
prev._length += n.val._length
y.os.delete(n.val._id)
n = prev
}
if (n.val) {
n = n.val
}
const next = y.os.findNext(n._id)
if (next !== null && next.constructor === GC && next._id.user === n._id.user && next._id.clock === n._id.clock + n._length) {
n._length += next._length
y.os.delete(next._id)
}
if (id.user !== ID.RootFakeUserID) {
writeStructToTransaction(y._transaction, 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 {encoding.Encoder} encoder The encoder to write data to.
* @private
*/
_toBinary (encoder) {
encoding.writeUint8(encoder, getStructReference(this.constructor))
this._id.encode(encoder)
encoding.writeVarUint(encoder, this._length)
}
/**
* Read the next Item in a Decoder and fill this Item with the read data.
*
* This is called when data is received from a remote peer.
*
* @param {import('../Y.js').default} y The Yjs instance that this Item belongs to.
* @param {decoding.Decoder} decoder The decoder object to read data from.
* @private
*/
_fromBinary (y, decoder) {
/**
* @type {any}
*/
const id = ID.decode(decoder)
this._id = id
this._length = decoding.readVarUint(decoder)
const missing = []
if (y.ss.getState(id.user) < id.clock) {
missing.push(ID.createID(id.user, id.clock - 1))
}
return missing
}
_splitAt () {
return this
}
_clonePartial (diff) {
const gc = new GC()
gc._id = ID.createID(this._id.user, this._id.clock + diff)
gc._length = this._length - diff
return gc
}
}

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