Compare commits

..

134 Commits

Author SHA1 Message Date
Kevin Jahns
1bfa6dfb74 13.6.18 2024-06-18 16:59:36 +02:00
Kevin Jahns
2e5abad773 fix #645 yjs/y-utility#8 2024-06-18 16:51:57 +02:00
Kevin Jahns
3f1746f3a9 add lexical editor 2024-06-17 20:29:14 +02:00
Kevin Jahns
34b06b6cf9 13.6.17 2024-06-17 15:15:04 +02:00
Kevin Jahns
d4dac558c0 fix creating relative position from json when type name is the empty string 2024-06-17 15:13:07 +02:00
Kevin Jahns
a47a48b891 Merge pull request #649 from szv/readme-fixes
Readme fixes
2024-06-15 20:12:15 +02:00
Sebastian Szvetecz
2e79d0369e Fixed markdown link for ellipsus.org in README.md 2024-06-12 01:12:48 +02:00
Sebastian Szvetecz
88506f6d78 Fixed star icon in README.md 2024-06-12 01:12:13 +02:00
Kevin Jahns
fbd088ee78 13.6.16 2024-06-10 12:21:06 +02:00
Kevin Jahns
0678ed1eb5 fix event.path in observeDeep - closes #457 2024-06-10 12:18:16 +02:00
Kevin Jahns
0973e0acd4 Merge pull request #648 from ellipsus-writes/export-merge-ds
Export mergeDeleteSets
2024-06-07 17:20:20 +02:00
Fuad Saud
6932696795 Export mergeDeleteSets
Useful for comparing snapshots.
2024-06-06 13:38:08 +02:00
Kevin Jahns
a4303f914d Merge pull request #646 from MaxNoetzold/patch-1
Add y-postgresql to provider list in README
2024-06-04 13:47:02 +02:00
Max Nötzold
03593aeeb1 fix linting errors 2024-05-21 16:03:31 +02:00
Max Nötzold
d67a951104 add y-postgresql info to readme 2024-05-21 15:30:24 +02:00
Kevin Jahns
72205a688f Merge pull request #644 from i12345/patch-1
Update INTERNALS.md
2024-05-15 21:00:32 +02:00
i12345
edad668dbd Update INTERNALS.md
explained 53 bit JS numbers
2024-05-15 13:43:59 -05:00
Kevin Jahns
c264b1c291 Merge pull request #640 from malte-j/patch-1
Add y-op-sqlite to Readme
2024-05-11 12:27:35 +02:00
Kevin Jahns
cdd8e4f5fc Merge pull request #643 from jasonbw/patch-1
Fix y-websocket `server.cjs` path
2024-05-09 13:43:05 +02:00
Jason Wang
06e71f651d Fix y-websocket server path
The commmit c3d14cf07d renamed `server.js` to `server.cjs`; mirror that change here.
2024-05-09 05:55:26 -05:00
Malte Janßen
54594a2d75 Add y-op-sqlite to readme 2024-04-30 16:29:06 +02:00
Kevin Jahns
13772bf891 Merge pull request #603 from kevboh/patch-1
add screen.garden as user
2024-04-28 21:39:28 +02:00
Kevin Jahns
0896ed42b2 Merge branch 'main' into patch-1 2024-04-28 21:38:49 +02:00
Kevin Jahns
656b7e7f6a add more users 2024-04-28 21:16:58 +02:00
Kevin Jahns
0511b66346 Merge pull request #635 from synix/fix/unused-transaction
remove unused _transaction in YArrayEvent
2024-04-28 21:08:58 +02:00
Kevin Jahns
91b718cde0 13.6.15 2024-04-27 00:50:32 +02:00
Kevin Jahns
d56221b66a Merge pull request #637 from synix/fix/readme-lint
fix: markdownlint readme error
2024-04-27 00:46:42 +02:00
Kevin Jahns
ce43124ad0 [relative-positions] add option to configure whether to follow redon insertions - #638 2024-04-27 00:24:49 +02:00
synix
0af69cf6d6 fix: markdownlint readme error 2024-04-26 13:15:06 +08:00
synix
3df335cb4c update slice() function's doc 2024-04-26 12:03:28 +08:00
synix
387be70ae9 make slice() function's doc more accurate 2024-04-26 11:49:52 +08:00
Kevin Jahns
927c2369aa Merge pull request #636 from fxsalazar/patch-1
Add Hocuspocus as a backend provider
2024-04-26 00:42:32 +02:00
Felix Salazar
8270373c9f Add Hocuspocus as a backend provider 2024-04-25 19:34:05 +02:00
synix
43815d8292 fix lint error 2024-04-25 11:33:36 +08:00
synix
f0dc53f53f fix minor typos 2024-04-25 11:17:49 +08:00
synix
25ae9f3236 remove unused _transaction in YArray 2024-04-25 11:03:17 +08:00
Kevin Jahns
5e712e39b1 add ourboard as user 2024-04-24 16:03:21 +02:00
Kevin Jahns
4ffd23fd0b typo 2024-04-17 20:41:42 +02:00
Kevin Jahns
05d974cee1 Merge pull request #630 from sakihet/fix-typo
fix typo
2024-04-15 18:54:02 +02:00
saki
f1532771b7 fix typo 2024-04-16 01:15:02 +09:00
Kevin Jahns
aee9e14d09 Merge pull request #629 from synix/fix/outdated-y-instance
remove outdated Y instance in comments
2024-04-13 19:54:23 +02:00
synix
f5aa852054 remove outdated Y instance in comments 2024-04-13 21:26:44 +08:00
Kevin Jahns
b990ad9f86 Merge pull request #627 from synix/fix/INTERNALS
update search marker count in INTERNALS.md
2024-04-12 13:22:02 +02:00
synix
43e17802a6 fix: update search marker count in INTERNALS.md 2024-04-11 11:21:14 +08:00
Kevin Jahns
01c3668a0b Merge pull request #626 from satyajeetjadhav/main
Update Readme who-is-using (thinkdeli.com)
2024-04-09 18:06:25 +02:00
Satyajeet Jadhav
52b906898f Update Readme who-is-using (thinkdeli.com) 2024-04-09 16:39:18 +05:30
Kevin Jahns
d119459fad add huly as a user 2024-04-03 15:22:58 +02:00
Kevin Jahns
d730abe594 add synthesia as a user 2024-03-24 21:00:23 +01:00
Kevin Jahns
ca24f1ee76 added more sponsors 2024-03-23 14:29:23 +01:00
Kevin Jahns
dc45a8d3cf [readme] Added AppMaster to "Who is Using" 2024-03-23 12:44:01 +01:00
Kevin Jahns
2062f52a90 add reference to y-redis 2024-03-15 01:42:16 +01:00
Kevin Jahns
6e674ff5f7 add y-webxdc - related to yjs/docs#55 2024-03-14 21:09:34 +01:00
Kevin Jahns
2fba694cd4 Add documentation & clarification to clone method #622 2024-03-14 20:33:34 +01:00
Kevin Jahns
b235c57d76 add tinybase 2024-03-12 16:22:12 +01:00
Kevin Jahns
6beab79eb4 add tests for falsy formatting attributes - #619 2024-03-01 11:39:31 +01:00
Kevin Jahns
1e69d650b8 13.6.14 2024-03-01 11:31:21 +01:00
Kevin Jahns
133cfc9cdc allow falsy values in formatting attributes 2024-03-01 11:29:14 +01:00
Kevin Jahns
83db6c814c Merge pull request #619 from jul13579/allow-falsy-attribute-values
Allow falsy attribute values
2024-03-01 11:23:04 +01:00
Julian Lehrhuber
cdbb55818d Allow falsy attribute values 2024-03-01 10:37:51 +01:00
Kevin Jahns
90675be3ab 13.6.13 2024-02-29 17:37:25 +01:00
Kevin Jahns
541306b254 migrate to ObservableV2 2024-02-29 17:08:57 +01:00
Kevin Jahns
53173a9ea7 Merge branch 'mylesj-feat/undomanager-doingstackitem' 2024-02-29 14:47:00 +01:00
Kevin Jahns
29fa60ccf9 [Undo] add UndoManager.currStackItem 2024-02-29 14:46:43 +01:00
Myles J
917261a1ce Facilitate referencing UndoManager StackItem inside Type observers 2024-02-29 00:50:36 +00:00
Kevin Jahns
a9dc72fcc0 Merge pull request #612 from MentalGear/patch-1
docs: fix typo
2024-02-25 10:31:20 +01:00
Kevin Jahns
90a90ab010 add y-fire to provider list #189 2024-02-20 19:52:58 +01:00
MentalGear
009f6ab551 docs: fix typo 2024-02-17 20:38:53 +01:00
Kevin Jahns
a8582442e3 13.6.12 2024-02-09 23:38:50 +01:00
Kevin Jahns
f54ea625e2 Merge branch 'raineorshine-getXmlElement' 2024-02-09 23:31:29 +01:00
Kevin Jahns
ce06b2abec update deps 2024-02-09 23:31:07 +01:00
Kevin Jahns
e1bce03ed8 better typings for ydoc.get 2024-02-09 23:27:24 +01:00
Raine Revere
16d9638bc8 Add ydoc.getXmlElement 2024-02-05 13:35:32 +00:00
Kevin Jahns
415a645874 13.6.11 2024-01-21 11:30:14 +01:00
Kevin Jahns
1cb52dc863 fix Y.Text formatting issue - closes #606 2024-01-21 11:27:12 +01:00
Kevin Jahns
7a8ca6eaa5 add linear as a user of Yjs 2024-01-15 14:11:28 +01:00
Kevin Jahns
e348255bb1 Merge pull request #604 from lukasz-jazwa/proffesional-support-section
Updated readme.md with Professional Support section
2024-01-10 17:27:03 +01:00
Kevin Jahns
79c095d4dc Merge pull request #605 from xaviergonz/patch-1
Update README.md with mobx-keystone binding
2023-12-28 15:31:00 +01:00
Javier Gonzalez
0241fd3c40 Update README.md with mobx-keystone binding 2023-12-23 11:16:57 +01:00
lukasz jazwa
cf78ce12b2 Updated readme.md with Professional Support section 2023-12-14 21:55:53 +01:00
Kevin Jahns
77bd74127d Update who-is-using (Cargo.site) 2023-12-11 16:37:23 +01:00
Kevin Barrett
221cb81dbf add screen.garden as user 2023-11-30 16:51:23 -05:00
Kevin Jahns
fe36ffd122 add AWS Sagemaker, JupyterLab, JupyterCAD as users 2023-11-28 16:22:37 +01:00
Kevin Jahns
28ccd5e0dd add providers (also mention some y-crdt based providers) 2023-11-21 19:55:29 +01:00
Kevin Jahns
1d4f2e5435 13.6.10 2023-11-21 12:29:49 +01:00
Kevin Jahns
2c0daeb071 implement snapshot API for yxml.getAttributes. implements #543 2023-11-21 12:24:21 +01:00
Kevin Jahns
013b2b6886 13.6.9 2023-11-20 12:56:27 +01:00
Kevin Jahns
c2e7076400 add iterator type checks 2023-11-20 12:53:58 +01:00
Kevin Jahns
289ff16f66 Merge pull request #590 from haines/map-iterator-types
Fix typing of `Y.Map` iterators
2023-11-20 12:40:42 +01:00
Kevin Jahns
9f8c55885f update github workflow 2023-11-20 12:46:12 +01:00
Kevin Jahns
5861876e6f Merge pull request #587 from SamyPesse/patch-1
Only emit "load" when sync is set to true
2023-11-20 12:36:25 +01:00
Kevin Jahns
37236fa31f update license. closes #471 2023-11-20 12:39:51 +01:00
Kevin Jahns
b32f5434f1 Merge pull request #557 from tonny008/patch-1
fix comment in doc
2023-11-20 12:28:12 +01:00
Kevin Jahns
61e84c5f99 Merge pull request #579 from jsejcksn/fix/docs/readme
docs(readme): fix typo
2023-11-20 12:27:12 +01:00
Kevin Jahns
b531438369 Merge pull request #595 from himself65/expose-content-doc
feat: expose some types
2023-11-20 12:22:46 +01:00
Kevin Jahns
ac49dbcbd8 Merge pull request #583 from nikgraf/improve-docs
add documentation on V2 events
2023-11-14 20:30:04 +01:00
Kevin Jahns
da8ca5168e Merge pull request #596 from siddug/patch-1
Adding www.btw.so to the platforms who use Yjs
2023-11-11 11:16:19 +01:00
Siddhartha Gunti
a3d69bba72 Adding www.btw.so to the platforms who use Yjs
Repo here: https://github.com/btw-so/btw
2023-11-07 18:54:22 +05:30
Alex Yang
e5f286cf89 feat: expose some types 2023-11-06 16:51:23 -06:00
Kevin Jahns
c14a8d70f1 Merge pull request #593 from himself65/remove-unused-check
fix: remove unused if-statement check
2023-11-02 20:23:06 +01:00
Alex Yang
f52569b8fa fix: remove unused if-statement check 2023-10-30 19:41:09 -05:00
Andrew Haines
25bef2308f Fix typing of Y.Map iterators
Signed-off-by: Andrew Haines <andrew@haines.org.nz>
2023-10-25 15:00:48 +01:00
Samy Pessé
e7572d61c6 Only emit "load" when sync is set to true 2023-10-22 11:03:25 +02:00
Nik Graf
e6afc51b84 add documentation on V2 events 2023-10-07 20:56:02 +02:00
Jesse Jackson
171d801e0a docs(readme): fix typo
Update link text for Swift bindings: from "yrb" to "yswift"
2023-10-05 00:41:48 -05:00
Kevin Jahns
9a7b659919 bump lib0 2023-09-23 16:59:32 +02:00
Kevin Jahns
e0a9c0d9bb Merge pull request #577 from EricHasegawa/patch-1
Correct typos in INTERNALS.md
2023-09-19 13:15:58 +02:00
Eric Hasegawa
3a758f89a1 Correct typos in INTERNALS.md 2023-09-18 13:30:15 -07:00
Kevin Jahns
b5051e91ac Merge pull request #533 from booxood/fix-docs-example
docs: fix "Example: Syncing clients without loading the Y.Doc" code
2023-09-18 19:32:01 +02:00
Kevin Jahns
2fe8907ab0 13.6.8 2023-09-18 10:22:28 +02:00
Kevin Jahns
29270b5f3e fix "can't read origin of undefined" - fixes #417 2023-09-18 09:55:50 +02:00
Kevin Jahns
a099e98bd6 create error on call - fixes #569 2023-09-07 13:41:35 +02:00
Kevin Jahns
1b0da31d00 Merge pull request #567 from stevenfabre/main
Added Liveblocks Yjs to the list of providers
2023-09-06 17:01:29 +02:00
Kevin Jahns
a1fda219e4 lint readme 2023-09-02 17:39:49 +02:00
Kevin Jahns
09687221ac Merge pull request #568 from himself65/docs-update
docs: add `@toeverything/y-indexeddb`
2023-09-01 13:01:57 +02:00
Alex Yang
4d7a366f6e docs: add @toeverything/y-indexeddb 2023-08-31 17:21:36 -05:00
Steven Fabre
0b30413f6e Liveblocks in Yjs README
Signed-off-by: Steven Fabre <hello@stevenfabre.com>
2023-08-31 05:34:05 -04:00
Kevin Jahns
eeae74decf sponsors update 2023-08-29 16:34:26 +02:00
Kevin Jahns
5ac498d62e Merge pull request #566 from IllumiDesk/docs/add-illumidesk-ref
Adds IllumiDesk as a reference to the using list
2023-08-29 16:33:32 +02:00
Greg Werner
9a9a1ffeeb Adds IllumiDesk as a reference to the using list
Signed-off-by: Greg Werner <werner.greg@gmail.com>
2023-08-28 14:45:43 -04:00
Kevin Jahns
1ed12434a1 Merge pull request #556 from AdventureBeard/patch-1
Update README.md
2023-08-28 12:26:31 +02:00
Kevin Jahns
7a4975ee85 Merge branch 'main' into patch-1 2023-08-28 12:26:18 +02:00
Kevin Jahns
92ee76ad6e Merge pull request #564 from akshaykmr/readme-update-who-is-using
add oorja.io to who is using yjs
2023-08-24 14:01:32 +02:00
Kevin Jahns
97c09a6cca fix #509 2023-08-24 13:52:38 +02:00
Kevin Jahns
2e3ba0f81f Merge pull request #565 from himself65/himself65-patch-1
Update README.md
2023-08-23 16:46:17 +02:00
Alex Yang
61abf3a1db Update README.md 2023-08-23 09:25:18 -05:00
Akshay Kumar
bd867cb161 add oorja to who is using yjs 2023-08-23 19:31:58 +05:30
Kevin Jahns
87b7d3e951 add Yjs-compatible ports to documentation 2023-08-23 15:42:48 +02:00
GQ
7bdf94167a fix comment 2023-07-31 15:05:33 +08:00
Braden
03b9a806e8 Update README.md 2023-07-23 14:40:03 -05:00
Kevin Jahns
5ee6992d1f 13.6.7 2023-07-17 14:43:07 +02:00
Kevin Jahns
dd31040656 Merge pull request #553 from yjs/simpler-bulk-changes-fix-542
bulk-merging structs - replaces #542, fixes #541
2023-07-17 14:35:18 +02:00
Kevin Jahns
c77dedb68d bulk-merging structs - replaces #542, fixes #541 2023-07-17 14:29:54 +02:00
Kevin Jahns
90f2a06b5e throw error when event changes are computed after a transaction 2023-06-27 13:20:53 +02:00
Liucw
1ce1751432 docs: fix "Example: Syncing clients without loading the Y.Doc" code 2023-05-17 14:52:25 +08:00
53 changed files with 1419 additions and 608 deletions

View File

@@ -16,16 +16,16 @@ jobs:
strategy:
matrix:
node-version: [16.x, 18.x]
node-version: [16.x, 20.x]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run lint
- run: npm run test-extensive
- run: npm run test
env:
CI: true

View File

@@ -26,7 +26,7 @@ article](https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/).
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).
range \[JavaScript uses IEEE 754 floats\]).
## List items
@@ -88,7 +88,7 @@ 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
improve performance, Yjs stores a cache of the 80 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
@@ -149,8 +149,8 @@ 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
an incremental document update that allows clients to sync with each other.
The update object is a Uint8Array that efficiently encodes `Item` objects and
the delete set.
* `state vector`: A state vector defines the known state of each user (a set of
tuples `(client, clock)`). This object is also efficiently encoded as a

View File

@@ -1,7 +1,7 @@
The MIT License (MIT)
Copyright (c) 2014
- Kevin Jahns <kevin.jahns@rwth-aachen.de>.
Copyright (c) 2023
- Kevin Jahns <kevin.jahns@protonmail.com>.
- Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany
Permission is hereby granted, free of charge, to any person obtaining a copy

193
README.md
View File

@@ -32,13 +32,34 @@ Otherwise you can find help on our community [discussion board](https://discuss.
Please contribute to the project financially - especially if your company relies
on Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=d42f2d)](https://github.com/sponsors/dmonad)
## Professional Support
* [Support Contract with the Maintainer](https://github.com/sponsors/dmonad) -
By contributing financially to the open-source Yjs project, you can receive
professional support directly from the author. This includes the opportunity for
weekly video calls to discuss your specific challenges.
* [Synergy Codes](https://synergycodes.com/yjs-services/) - Specializing in
consulting and developing real-time collaborative editing solutions for visual
apps, Synergy Codes focuses on interactive diagrams, complex graphs, charts, and
various data visualization types. Their expertise empowers developers to build
engaging and interactive visual experiences leveraging the power of Yjs. See
their work in action at [Visual Collaboration
Showcase](https://yjs-diagram.synergy.codes/).
## Who is using Yjs
* [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source
knowledge base. 🏅
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2:
* [Sana](https://sanalabs.com/) A learning platform with collaborative text
editing powered by Yjs.
knowledge base. :star2:
* [Huly](https://huly.io/) - Open Source All-in-One Project Management Platform :star2:
* [Cargo](https://cargo.site/) Site builder for designers and artists :star2:
* [Gitbook](https://gitbook.com) Knowledge management for technical teams :star2:
* [Evernote](https://evernote.com) Note-taking app :star2:
* [Lessonspace](https://thelessonspace.com) Enterprise platform for virtual
classrooms and online training :star2:
* [Ellipsus](ellipsus.com) - Collaborative writing app for storytelling etc.
Supports versioning, change attribution, and "blame". A solution for the whole
publishing process (also selling) :star:
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star:
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
community. :star:
* [Room.sh](https://room.sh/) A meeting application with integrated
@@ -47,25 +68,59 @@ on Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%2
Nimbus Web. :star:
* [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to
collaboratively organize radio broadcasts. :star:
* [modyfi](https://www.modyfi.com) - Modyfi is the design platform built for
multidisciplinary designers. Design, generate, animate, and more — without
switching between apps. :star:
* [Sana](https://sanalabs.com/) A learning platform with collaborative text
editing powered by Yjs.
* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted
collaborative notes app.
* [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation. *[(source)](https://github.com/micrology/prsm)*
* [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation.
*[(source)](https://github.com/micrology/prsm)*
* [Alldone](https://alldone.app/) A next-gen project management and
collaboration platform.
* [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate.
* [Slidebeamer](https://slidebeamer.com/) Presentation app.
* [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys.
* [Skiff](https://skiff.org/) Private, decentralized workspace.
* [JupyterLab](https://jupyter.org/) Collaborative computational Notebooks
* [JupyterCad](https://jupytercad.readthedocs.io/en/latest/) Extension to
JupyterLab that enables collaborative editing of 3d FreeCAD Models.
* [Hyperquery](https://hyperquery.ai/) A collaborative data workspace for
sharing analyses, documentation, spreadsheets, and dashboards.
* [Nosgestesclimat](https://nosgestesclimat.fr/groupe) The french carbon
footprint calculator has a group P2P mode based on yjs
* [oorja.io](https://oorja.io) Online meeting spaces extensible with
collaborative apps, end-to-end encrypted.
* [LegendKeeper](https://legendkeeper.com) Collaborative campaign planner and
worldbuilding app for tabletop RPGs.
* [IllumiDesk](https://illumidesk.com/) Build courses and content with A.I.
* [btw](https://www.btw.so) Open-source Medium alternative
* [AWS SageMaker](https://aws.amazon.com/sagemaker/) Tools for building Machine
Learning Models
* [linear](https://linear.app) Streamline issues, projects, and product roadmaps.
* [btw](https://www.btw.so) - Personal website builder
* [AWS SageMaker](https://aws.amazon.com/sagemaker/) - Machine Learning Service
* [Arkiter](https://www.arkiter.com/) - Live interview software
* [Appflowy](https://www.appflowy.io/) - They use Yrs
* [Multi.app](https://multi.app) - Multiplayer app sharing: Point, draw and edit
in shared apps as if they're on your computer. They are using Yrs.
* [AppMaster](https://appmaster.io) A No-Code platform for creating
production-ready applications with source code generation.
* [Synthesia](https://www.synthesia.io) - Collaborative Video Editor
* [thinkdeli](https://thinkdeli.com) - A fast and simple notes app powered by AI
* [ourboard](https://github.com/raimohanska/ourboard) - A collaborative whiteboard
applicaiton
* [Ellie.ai](https://ellie.ai) - Data Product Design and Collaboration
* [GoPeer](https://gopeer.org/) - Collaborative tutoring
* [screen.garden](https://screen.garden) Collaborative backend for PKM apps.
## Table of Contents
* [Overview](#Overview)
* [Bindings](#Bindings)
* [Providers](#Providers)
* [Ports](#Ports)
* [Getting Started](#Getting-Started)
* [API](#API)
* [Shared Types](#Shared-Types)
@@ -92,9 +147,11 @@ are implemented in separate modules.
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
| [BlockSuite](https://github.com/toeverything/blocksuite) | ✔ | (native) | [demo](https://blocksuite-toeverything.vercel.app/?init) |
| [Lexical](https://lexical.dev/) | ✔ | (native) | [demo](https://lexical.dev/docs/collaboration/react#see-it-in-action) |
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
| [immer](https://github.com/immerjs/immer) | | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) |
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |
| [mobx-keystone](https://mobx-keystone.js.org/) | | [mobx-keystone-yjs](https://github.com/xaviergonz/mobx-keystone/tree/master/packages/mobx-keystone-yjs) | [demo](https://mobx-keystone.js.org/examples/yjs-binding) |
### Providers
@@ -103,7 +160,20 @@ and storing shared data for offline usage is quite a hassle. **Providers**
manage all that for you and are the perfect starting point for your
collaborative app.
> This list of providers is incomplete. Please open PRs to add your providers to
> this list!
#### Connection Providers
<dl>
<dt><a href="https://github.com/yjs/y-websocket">y-websocket</a></dt>
<dd>
A module that contains a simple websocket backend and a websocket client that
connects to that backend. <a href="https://github.com/yjs/y-redis/"><b>y-redis</b></a>,
<b>y-sweet</b>, <b>ypy-websocket</b> and <a href="https://tiptap.dev/docs/hocuspocus/introduction">
<b>Hocuspocus</b></a> (see below) are alternative
backends to y-websocket.
</dd>
<dt><a href="https://github.com/yjs/y-webrtc">y-webrtc</a></dt>
<dd>
Propagates document updates peer-to-peer using WebRTC. The peers exchange
@@ -112,17 +182,26 @@ are available. Communication over the signaling servers can be encrypted by
providing a shared secret, keeping the connection information and the shared
document private.
</dd>
<dt><a href="https://github.com/yjs/y-websocket">y-websocket</a></dt>
<dt><a href="https://github.com/liveblocks/liveblocks">@liveblocks/yjs</a></dt>
<dd>
A module that contains a simple websocket backend and a websocket client that
connects to that backend. The backend can be extended to persist updates in a
leveldb database.
<a href="https://liveblocks.io/document/yjs">Liveblocks Yjs</a> provides a fully
hosted WebSocket infrastructure and persisted data store for Yjs
documents. No configuration or maintenance is required. It also features
Yjs webhook events, REST API to read and update Yjs documents, and a
browser DevTools extension.
</dd>
<dt><a href="https://github.com/yjs/y-indexeddb">y-indexeddb</a></dt>
<dt><a href="https://github.com/drifting-in-space/y-sweet">y-sweet</a></dt>
<dd>
Efficiently persists document updates to the browsers indexeddb database.
The document is immediately available and only diffs need to be synced through the
network provider.
A standalone yjs server with persistence to S3 or filesystem. They offer a
<a href="https://y-sweet.cloud">cloud service</a> as well.
</dd>
<dt><a href="https://github.com/ueberdosis/hocuspocus">Hocuspocus</a></dt>
<dd>
A standalone extensible yjs server with sqlite persistence, webhooks, auth and more.
</dd>
<dt><a href="https://docs.partykit.io/reference/y-partykit-api/">PartyKit</a></dt>
<dd>
Cloud service for building multiplayer apps.
</dd>
<dt><a href="https://github.com/marcopolo/y-libp2p">y-libp2p</a></dt>
<dd>
@@ -145,14 +224,79 @@ Use Matrix as transport and storage of Yjs updates, so you can focus building
your client app and Matrix can provide powerful features like Authentication,
Authorization, Federation, hosting (self-hosting or SaaS) and even End-to-End
Encryption (E2EE).
</dd>
</dd>
<dt><a href="https://github.com/y-crdt/yrb-actioncable">yrb-actioncable</a></dt>
<dd>
An ActionCable companion for Yjs clients. There is a fitting
<a href="https://github.com/y-crdt/yrb-redis">redis extension</a> as well.
</dd>
<dt><a href="https://github.com/y-crdt/ypy-websocket">ypy-websocket</a></dt>
<dd>
Websocket backend, written in Python.
</dd>
<dt><a href="https://tinybase.org/">Tinybase</a></dt>
<dd>
The reactive data store for local-first apps. They support multiple CRDTs and
different network technologies.
</dd>
<dt><a href="https://codeberg.org/webxdc/y-webxdc">y-webxdc</a></dt>
<dd>
Provider for sharing data in <a href="https://webxdc.org">webxdc chat apps</a>.
</dd>
</dl>
#### Persistence Providers
<dl>
<dt><a href="https://github.com/yjs/y-indexeddb">y-indexeddb</a></dt>
<dd>
Efficiently persists document updates to the browsers indexeddb database.
The document is immediately available and only diffs need to be synced through the
network provider.
</dd>
<dt><a href="https://github.com/MaxNoetzold/y-mongodb-provider">y-mongodb-provider</a></dt>
<dd>
Adds persistent storage to a server with MongoDB. Can be used with the
y-websocket provider.
</dd>
</dd>
<dt><a href="https://github.com/toeverything/AFFiNE/tree/master/packages/y-indexeddb">
@toeverything/y-indexeddb</a></dt>
<dd>
Like y-indexeddb, but with sub-documents support and fully TypeScript.
</dd>
<dt><a href="https://github.com/podraven/y-fire">y-fire</a></dt>
<dd>
A database and connection provider for Yjs based on Firestore.
</dd>
<dt><a href="https://github.com/malte-j/y-op-sqlite">y-op-sqlite</a></dt>
<dd>
Persist YJS updates in your React Native app using
<a href="https://github.com/OP-Engineering/op-sqlite">op-sqlite</a>
, the fastest SQLite library for React Native.
</dd>
<dt><a href="https://github.com/MaxNoetzold/y-postgresql">y-postgresql</a></dt>
<dd>
Provides persistent storage for a web server using PostgreSQL and
is easily compatible with y-websocket.
</dd>
</dl>
# Ports
There are several Yjs-compatible ports to other programming languages.
* [y-octo](https://github.com/toeverything/y-octo) - Rust implementation by
[AFFiNE](https://affine.pro)
* [y-crdt](https://github.com/y-crdt/y-crdt) - Rust implementation with multiple
language bindings to other languages
* [yrs](https://github.com/y-crdt/y-crdt/tree/main/yrs) - Rust interface
* [ypy](https://github.com/y-crdt/ypy) - Python binding
* [yrb](https://github.com/y-crdt/yrb) - Ruby binding
* [yswift](https://github.com/y-crdt/yswift) - Swift binding
* [yffi](https://github.com/y-crdt/y-crdt/tree/main/yffi) - C-FFI
* [ywasm](https://github.com/y-crdt/y-crdt/tree/main/ywasm) - WASM binding
* [ycs](https://github.com/yjs/ycs) - .Net compatible C# implementation.
## Getting Started
Install Yjs and a provider with your favorite package manager:
@@ -164,7 +308,7 @@ npm i yjs y-websocket
Start the y-websocket server:
```sh
PORT=1234 node ./node_modules/y-websocket/bin/server.js
PORT=1234 node ./node_modules/y-websocket/bin/server.cjs
```
### Example: Observe types
@@ -648,6 +792,8 @@ type. Doesn't log types that have not been defined (using
<dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd>
<b><code>getText(string):Y.Text</code></b>
<dd>Define a shared Y.Text type. Is equivalent to <code>y.get(string, Y.Text)</code>.</dd>
<b><code>getXmlElement(string, string):Y.XmlElement</code></b>
<dd>Define a shared Y.XmlElement type. Is equivalent to <code>y.get(string, Y.XmlElement)</code>.</dd>
<b><code>getXmlFragment(string):Y.XmlFragment</code></b>
<dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd>
<b><code>on(string, function)</code></b>
@@ -662,7 +808,8 @@ type. Doesn't log types that have not been defined (using
<b><code>on('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void)</code></b>
<dd>
Listen to document updates. Document updates must be transmitted to all other
peers. You can apply document updates in any order and multiple times.
peers. You can apply document updates in any order and multiple times. Use `updateV2`
to receive V2 events.
</dd>
<b><code>on('beforeTransaction', function(Y.Transaction, Y.Doc):void)</code></b>
<dd>Emitted before each transaction.</dd>
@@ -754,7 +901,7 @@ const diff2 = Y.diffUpdate(currentState2, stateVector1)
// sync clients
currentState1 = Y.mergeUpdates([currentState1, diff2])
currentState1 = Y.mergeUpdates([currentState1, diff1])
currentState2 = Y.mergeUpdates([currentState2, diff1])
```
#### Obfuscating Updates
@@ -784,11 +931,13 @@ ydoc2.getText().toString() // => "00000000000"
#### Using V2 update format
Yjs implements two update formats. By default you are using the V1 update format.
You can opt-in into the V2 update format wich provides much better compression.
You can opt-in into the V2 update format which provides much better compression.
It is not yet used by all providers. However, you can already use it if
you are building your own provider. All below functions are available with the
suffix "V2". E.g. `Y.applyUpdate``Y.applyUpdateV2`. We also support conversion
functions between both formats: `Y.convertUpdateFormatV1ToV2` & `Y.convertUpdateFormatV2ToV1`.
suffix "V2". E.g. `Y.applyUpdate``Y.applyUpdateV2`. Also when listening to updates
you need to specifically need listen for V2 events e.g. `yDoc.on('updateV2', …)`.
We also support conversion functions between both formats:
`Y.convertUpdateFormatV1ToV2` & `Y.convertUpdateFormatV2ToV1`.
#### Update API
@@ -1033,7 +1182,7 @@ doc.transact(() => {
ytext.insert(0, 'abc')
}, 41)
undoManager.undo()
ytext.toString() // => '' (not tracked because 41 is not an instance of
ytext.toString() // => 'abc' (not tracked because 41 is not an instance of
// `trackedTransactionorigins`)
ytext.delete(0, 3) // revert change

1133
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.6.6",
"version": "13.6.18",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",
@@ -12,9 +12,10 @@
"url": "https://github.com/sponsors/dmonad"
},
"scripts": {
"clean": "rm -rf dist docs",
"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",
"dist": "npm run clean && 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",
@@ -75,7 +76,7 @@
},
"homepage": "https://docs.yjs.dev",
"dependencies": {
"lib0": "^0.2.74"
"lib0": "^0.2.86"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.1",

View File

@@ -18,8 +18,10 @@ export {
Item,
AbstractStruct,
GC,
Skip,
ContentBinary,
ContentDeleted,
ContentDoc,
ContentEmbed,
ContentFormat,
ContentJSON,
@@ -50,6 +52,7 @@ export {
getItem,
typeListToArraySnapshot,
typeMapGetSnapshot,
typeMapGetAllSnapshot,
createDocFromSnapshot,
iterateDeletedStructs,
applyUpdate,
@@ -93,7 +96,11 @@ export {
obfuscateUpdate,
obfuscateUpdateV2,
UpdateEncoderV1,
UpdateEncoderV2,
UpdateDecoderV1,
UpdateDecoderV2,
equalDeleteSets,
mergeDeleteSets,
snapshotContainsUpdate
} from './internals.js'

View File

@@ -1,4 +1,3 @@
export * from './utils/AbstractConnector.js'
export * from './utils/DeleteSet.js'
export * from './utils/Doc.js'

View File

@@ -1,4 +1,3 @@
import {
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
} from '../internals.js'

View File

@@ -1,4 +1,3 @@
import {
addToDeleteSet,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line

View File

@@ -1,4 +1,3 @@
import {
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
} from '../internals.js'

View File

@@ -1,4 +1,3 @@
import {
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
} from '../internals.js'

View File

@@ -1,4 +1,3 @@
import {
YText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
} from '../internals.js'

View File

@@ -1,4 +1,3 @@
import {
readYArray,
readYMap,
@@ -108,7 +107,7 @@ export class ContentType {
while (item !== null) {
if (!item.deleted) {
item.delete(transaction)
} else {
} else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) {
// This will be gc'd later and we want to merge it if possible
// We try to merge all deleted items after each transaction,
// but we have no knowledge about that this needs to be merged
@@ -120,7 +119,7 @@ export class ContentType {
this.type._map.forEach(item => {
if (!item.deleted) {
item.delete(transaction)
} else {
} else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) {
// same as above
transaction._mergeStructs.push(item)
}

View File

@@ -1,4 +1,3 @@
import {
AbstractStruct,
addStruct,

View File

@@ -1,4 +1,3 @@
import {
GC,
getState,
@@ -389,9 +388,8 @@ export class Item extends AbstractStruct {
}
if ((this.left && this.left.constructor === GC) || (this.right && this.right.constructor === GC)) {
this.parent = null
}
// only set parent if this shouldn't be garbage collected
if (!this.parent) {
} else if (!this.parent) {
// only set parent if this shouldn't be garbage collected
if (this.left && this.left.constructor === Item) {
this.parent = this.left.parent
this.parentSub = this.left.parentSub

View File

@@ -1,4 +1,3 @@
import {
AbstractStruct,
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line

View File

@@ -1,4 +1,3 @@
import {
removeEventHandlerListener,
callEventHandlerListeners,
@@ -317,6 +316,10 @@ export class AbstractType {
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {AbstractType<EventType>}
*/
clone () {
@@ -478,7 +481,7 @@ export const typeListToArraySnapshot = (type, snapshot) => {
}
/**
* Executes a provided function on once on overy element of this YArray.
* Executes a provided function on once on every element of this YArray.
*
* @param {AbstractType<any>} type
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
@@ -570,7 +573,7 @@ export const typeListCreateIterator = type => {
}
/**
* Executes a provided function on once on overy element of this YArray.
* Executes a provided function on once on every element of this YArray.
* Operates on a snapshotted state of the document.
*
* @param {AbstractType<any>} type
@@ -683,7 +686,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
packJsonContent()
}
const lengthExceeded = error.create('Length exceeded!')
const lengthExceeded = () => error.create('Length exceeded!')
/**
* @param {Transaction} transaction
@@ -696,7 +699,7 @@ const lengthExceeded = error.create('Length exceeded!')
*/
export const typeListInsertGenerics = (transaction, parent, index, content) => {
if (index > parent._length) {
throw lengthExceeded
throw lengthExceeded()
}
if (index === 0) {
if (parent._searchMarker) {
@@ -798,7 +801,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
n = n.right
}
if (length > 0) {
throw lengthExceeded
throw lengthExceeded()
}
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */)
@@ -925,6 +928,34 @@ export const typeMapGetSnapshot = (parent, key, snapshot) => {
return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined
}
/**
* @param {AbstractType<any>} parent
* @param {Snapshot} snapshot
* @return {Object<string,Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
*
* @private
* @function
*/
export const typeMapGetAllSnapshot = (parent, snapshot) => {
/**
* @type {Object<string,any>}
*/
const res = {}
parent._map.forEach((value, key) => {
/**
* @type {Item|null}
*/
let v = value
while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) {
v = v.left
}
if (v !== null && isVisible(v, snapshot)) {
res[key] = v.content.getContent()[v.length - 1]
}
})
return res
}
/**
* @param {Map<string,Item>} map
* @return {IterableIterator<Array<any>>}

View File

@@ -25,16 +25,7 @@ import { typeListSlice } from './AbstractType.js'
* @template T
* @extends YEvent<YArray<T>>
*/
export class YArrayEvent extends YEvent {
/**
* @param {YArray<T>} yarray The changed type
* @param {Transaction} transaction The transaction object
*/
constructor (yarray, transaction) {
super(yarray, transaction)
this._transaction = transaction
}
}
export class YArrayEvent extends YEvent {}
/**
* A shared Array implementation.
@@ -95,6 +86,10 @@ export class YArray extends AbstractType {
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YArray<T>}
*/
clone () {
@@ -167,9 +162,9 @@ export class YArray extends AbstractType {
}
/**
* Preppends content to this YArray.
* Prepends content to this YArray.
*
* @param {Array<T>} content Array of content to preppend.
* @param {Array<T>} content Array of content to prepend.
*/
unshift (content) {
this.insert(0, content)
@@ -211,7 +206,8 @@ export class YArray extends AbstractType {
}
/**
* Transforms this YArray to a JavaScript Array.
* Returns a portion of this YArray into a JavaScript Array selected
* from start to end (end not included).
*
* @param {number} [start]
* @param {number} [end]
@@ -244,7 +240,7 @@ export class YArray extends AbstractType {
}
/**
* Executes a provided function once on overy element of this YArray.
* Executes a provided function once on every element of this YArray.
*
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
*/

View File

@@ -1,4 +1,3 @@
/**
* @module YMap
*/
@@ -41,7 +40,7 @@ export class YMapEvent extends YEvent {
* A shared Map implementation.
*
* @extends AbstractType<YMapEvent<MapType>>
* @implements {Iterable<MapType>}
* @implements {Iterable<[string, MapType]>}
*/
export class YMap extends AbstractType {
/**
@@ -89,6 +88,10 @@ export class YMap extends AbstractType {
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YMap<MapType>}
*/
clone () {
@@ -152,7 +155,7 @@ export class YMap extends AbstractType {
/**
* Returns the values for each element in the YMap Type.
*
* @return {IterableIterator<any>}
* @return {IterableIterator<MapType>}
*/
values () {
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1])
@@ -161,10 +164,10 @@ export class YMap extends AbstractType {
/**
* Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<any>}
* @return {IterableIterator<[string, MapType]>}
*/
entries () {
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => [v[0], v[1].content.getContent()[v[1].length - 1]])
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => /** @type {any} */ ([v[0], v[1].content.getContent()[v[1].length - 1]]))
}
/**
@@ -183,7 +186,7 @@ export class YMap extends AbstractType {
/**
* Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<any>}
* @return {IterableIterator<[string, MapType]>}
*/
[Symbol.iterator] () {
return this.entries()

View File

@@ -1,4 +1,3 @@
/**
* @module YText
*/
@@ -118,14 +117,15 @@ const findNextPosition = (transaction, pos, count) => {
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {number} index
* @param {boolean} useSearchMarker
* @return {ItemTextListPosition}
*
* @private
* @function
*/
const findPosition = (transaction, parent, index) => {
const findPosition = (transaction, parent, index, useSearchMarker) => {
const currentAttributes = new Map()
const marker = findMarker(parent, index)
const marker = useSearchMarker ? findMarker(parent, index) : null
if (marker) {
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes)
return findNextPosition(transaction, pos, index - marker.index)
@@ -201,7 +201,7 @@ const minimizeAttributeChanges = (currPos, attributes) => {
while (true) {
if (currPos.right === null) {
break
} else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] || null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
} else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
//
} else {
break
@@ -227,7 +227,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
// insert format-start items
for (const key in attributes) {
const val = attributes[key]
const currentVal = currPos.currentAttributes.get(key) || null
const currentVal = currPos.currentAttributes.get(key) ?? null
if (!equalAttrs(currentVal, val)) {
// save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal)
@@ -389,12 +389,12 @@ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAtt
switch (content.constructor) {
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (content)
const startAttrValue = startAttributes.get(key) || null
const startAttrValue = startAttributes.get(key) ?? null
if (endFormats.get(key) !== content || startAttrValue === value) {
// Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction)
cleanups++
if (!reachedCurr && (currAttributes.get(key) || null) === value && startAttrValue !== value) {
if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
if (startAttrValue === null) {
currAttributes.delete(key)
} else {
@@ -769,12 +769,12 @@ export class YTextEvent extends YEvent {
const { key, value } = /** @type {ContentFormat} */ (item.content)
if (this.adds(item)) {
if (!this.deletes(item)) {
const curVal = currentAttributes.get(key) || null
const curVal = currentAttributes.get(key) ?? null
if (!equalAttrs(curVal, value)) {
if (action === 'retain') {
addOp()
}
if (equalAttrs(value, (oldAttributes.get(key) || null))) {
if (equalAttrs(value, (oldAttributes.get(key) ?? null))) {
delete attributes[key]
} else {
attributes[key] = value
@@ -785,7 +785,7 @@ export class YTextEvent extends YEvent {
}
} else if (this.deletes(item)) {
oldAttributes.set(key, value)
const curVal = currentAttributes.get(key) || null
const curVal = currentAttributes.get(key) ?? null
if (!equalAttrs(curVal, value)) {
if (action === 'retain') {
addOp()
@@ -897,6 +897,10 @@ export class YText extends AbstractType {
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YText}
*/
clone () {
@@ -1120,7 +1124,7 @@ export class YText extends AbstractType {
const y = this.doc
if (y !== null) {
transact(y, transaction => {
const pos = findPosition(transaction, this, index)
const pos = findPosition(transaction, this, index, !attributes)
if (!attributes) {
attributes = {}
// @ts-ignore
@@ -1138,20 +1142,20 @@ export class YText extends AbstractType {
*
* @param {number} index The index to insert the embed at.
* @param {Object | AbstractType<any>} embed The Object that represents the embed.
* @param {TextAttributes} attributes Attribute information to apply on the
* @param {TextAttributes} [attributes] Attribute information to apply on the
* embed
*
* @public
*/
insertEmbed (index, embed, attributes = {}) {
insertEmbed (index, embed, attributes) {
const y = this.doc
if (y !== null) {
transact(y, transaction => {
const pos = findPosition(transaction, this, index)
insertText(transaction, this, pos, embed, attributes)
const pos = findPosition(transaction, this, index, !attributes)
insertText(transaction, this, pos, embed, attributes || {})
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes))
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes || {}))
}
}
@@ -1170,7 +1174,7 @@ export class YText extends AbstractType {
const y = this.doc
if (y !== null) {
transact(y, transaction => {
deleteText(transaction, findPosition(transaction, this, index), length)
deleteText(transaction, findPosition(transaction, this, index, true), length)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
@@ -1194,7 +1198,7 @@ export class YText extends AbstractType {
const y = this.doc
if (y !== null) {
transact(y, transaction => {
const pos = findPosition(transaction, this, index)
const pos = findPosition(transaction, this, index, false)
if (pos.right === null) {
return
}

View File

@@ -8,9 +8,10 @@ import {
typeMapSet,
typeMapGet,
typeMapGetAll,
typeMapGetAllSnapshot,
typeListForEach,
YXmlElementRefID,
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
} from '../internals.js'
/**
@@ -19,7 +20,7 @@ import {
/**
* An YXmlElement imitates the behavior of a
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
* https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element
*
* * An YXmlElement has attributes (key value pairs)
* * An YXmlElement has childElements that must inherit from YXmlElement
@@ -80,6 +81,10 @@ export class YXmlElement extends YXmlFragment {
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YXmlElement<KV>}
*/
clone () {
@@ -192,12 +197,13 @@ export class YXmlElement extends YXmlFragment {
/**
* Returns all attribute name/value pairs in a JSON Object.
*
* @param {Snapshot} [snapshot]
* @return {{ [Key in Extract<keyof KV,string>]?: KV[Key]}} A JSON Object that describes the attributes.
*
* @public
*/
getAttributes () {
return /** @type {any} */ (typeMapGetAll(this))
getAttributes (snapshot) {
return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this))
}
/**

View File

@@ -1,4 +1,3 @@
import {
YEvent,
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line

View File

@@ -163,6 +163,10 @@ export class YXmlFragment extends AbstractType {
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YXmlFragment}
*/
clone () {
@@ -376,9 +380,9 @@ export class YXmlFragment extends AbstractType {
}
/**
* Preppends content to this YArray.
* Prepends content to this YArray.
*
* @param {Array<YXmlElement|YXmlText>} content Array of content to preppend.
* @param {Array<YXmlElement|YXmlText>} content Array of content to prepend.
*/
unshift (content) {
this.insert(0, content)
@@ -395,7 +399,8 @@ export class YXmlFragment extends AbstractType {
}
/**
* Transforms this YArray to a JavaScript Array.
* Returns a portion of this YXmlFragment into a JavaScript Array selected
* from start to end (end not included).
*
* @param {number} [start]
* @param {number} [end]
@@ -406,7 +411,7 @@ export class YXmlFragment extends AbstractType {
}
/**
* Executes a provided function on once on overy child element.
* Executes a provided function on once on every child element.
*
* @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray.
*/

View File

@@ -1,4 +1,3 @@
import {
YMap,
YXmlHookRefID,
@@ -30,6 +29,10 @@ export class YXmlHook extends YMap {
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YXmlHook}
*/
clone () {

View File

@@ -1,4 +1,3 @@
import {
YText,
YXmlTextRefID,
@@ -31,6 +30,10 @@ export class YXmlText extends YText {
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YXmlText}
*/
clone () {

View File

@@ -1,5 +1,4 @@
import { Observable } from 'lib0/observable'
import { ObservableV2 } from 'lib0/observable'
import {
Doc // eslint-disable-line
@@ -11,9 +10,9 @@ import {
* @note This interface is experimental and it is not advised to actually inherit this class.
* It just serves as typing information.
*
* @extends {Observable<any>}
* @extends {ObservableV2<any>}
*/
export class AbstractConnector extends Observable {
export class AbstractConnector extends ObservableV2 {
/**
* @param {Doc} ydoc
* @param {any} awareness

View File

@@ -1,4 +1,3 @@
import {
findIndexSS,
getState,

View File

@@ -8,12 +8,13 @@ import {
YArray,
YText,
YMap,
YXmlElement,
YXmlFragment,
transact,
ContentDoc, Item, Transaction, YEvent // eslint-disable-line
} from '../internals.js'
import { Observable } from 'lib0/observable'
import { ObservableV2 } from 'lib0/observable'
import * as random from 'lib0/random'
import * as map from 'lib0/map'
import * as array from 'lib0/array'
@@ -33,10 +34,26 @@ export const generateNewClientId = random.uint32
*/
/**
* A Yjs instance handles the state of shared data.
* @extends Observable<string>
* @typedef {Object} DocEvents
* @property {function(Doc):void} DocEvents.destroy
* @property {function(Doc):void} DocEvents.load
* @property {function(boolean, Doc):void} DocEvents.sync
* @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.update
* @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.updateV2
* @property {function(Doc):void} DocEvents.beforeAllTransactions
* @property {function(Transaction, Doc):void} DocEvents.beforeTransaction
* @property {function(Transaction, Doc):void} DocEvents.beforeObserverCalls
* @property {function(Transaction, Doc):void} DocEvents.afterTransaction
* @property {function(Transaction, Doc):void} DocEvents.afterTransactionCleanup
* @property {function(Doc, Array<Transaction>):void} DocEvents.afterAllTransactions
* @property {function({ loaded: Set<Doc>, added: Set<Doc>, removed: Set<Doc> }, Doc, Transaction):void} DocEvents.subdocs
*/
export class Doc extends Observable {
/**
* A Yjs instance handles the state of shared data.
* @extends ObservableV2<DocEvents>
*/
export class Doc extends ObservableV2 {
/**
* @param {DocOpts} opts configuration
*/
@@ -113,8 +130,8 @@ export class Doc extends Observable {
this.whenSynced = provideSyncedPromise()
}
this.isSynced = isSynced === undefined || isSynced === true
if (!this.isLoaded) {
this.emit('load', [])
if (this.isSynced && !this.isLoaded) {
this.emit('load', [this])
}
})
/**
@@ -170,30 +187,31 @@ export class Doc extends Observable {
/**
* Define a shared data type.
*
* Multiple calls of `y.get(name, TypeConstructor)` yield the same result
* Multiple calls of `ydoc.get(name, TypeConstructor)` yield the same result
* and do not overwrite each other. I.e.
* `y.define(name, Y.Array) === y.define(name, Y.Array)`
* `ydoc.get(name, Y.Array) === ydoc.get(name, Y.Array)`
*
* After this method is called, the type is also available on `y.share.get(name)`.
* After this method is called, the type is also available on `ydoc.share.get(name)`.
*
* *Best Practices:*
* Define all types right after the Yjs instance is created and store them in a separate object.
* Define all types right after the Y.Doc instance is created and store them in a separate object.
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
*
* @template {typeof AbstractType<any>} Type
* @example
* const y = new Y(..)
* const ydoc = new Y.Doc(..)
* const appState = {
* document: y.getText('document')
* comments: y.getArray('comments')
* document: ydoc.getText('document')
* comments: ydoc.getArray('comments')
* }
*
* @param {string} name
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
* @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
* @return {InstanceType<Type>} The created type. Constructed with TypeConstructor
*
* @public
*/
get (name, TypeConstructor = AbstractType) {
get (name, TypeConstructor = /** @type {any} */ (AbstractType)) {
const type = map.setIfUndefined(this.share, name, () => {
// @ts-ignore
const t = new TypeConstructor()
@@ -219,12 +237,12 @@ export class Doc extends Observable {
t._length = type._length
this.share.set(name, t)
t._integrate(this, null)
return t
return /** @type {InstanceType<Type>} */ (t)
} else {
throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
}
}
return type
return /** @type {InstanceType<Type>} */ (type)
}
/**
@@ -235,8 +253,7 @@ export class Doc extends Observable {
* @public
*/
getArray (name = '') {
// @ts-ignore
return this.get(name, YArray)
return /** @type {YArray<T>} */ (this.get(name, YArray))
}
/**
@@ -246,7 +263,6 @@ export class Doc extends Observable {
* @public
*/
getText (name = '') {
// @ts-ignore
return this.get(name, YText)
}
@@ -258,8 +274,17 @@ export class Doc extends Observable {
* @public
*/
getMap (name = '') {
// @ts-ignore
return this.get(name, YMap)
return /** @type {YMap<T>} */ (this.get(name, YMap))
}
/**
* @param {string} [name]
* @return {YXmlElement}
*
* @public
*/
getXmlElement (name = '') {
return /** @type {YXmlElement<{[key:string]:string}>} */ (this.get(name, YXmlElement))
}
/**
@@ -269,7 +294,6 @@ export class Doc extends Observable {
* @public
*/
getXmlFragment (name = '') {
// @ts-ignore
return this.get(name, YXmlFragment)
}
@@ -313,24 +337,9 @@ export class Doc extends Observable {
transaction.subdocsRemoved.add(this)
}, null, true)
}
this.emit('destroyed', [true])
// @ts-ignore
this.emit('destroyed', [true]) // DEPRECATED!
this.emit('destroy', [this])
super.destroy()
}
/**
* @param {string} eventName
* @param {function(...any):any} f
*/
on (eventName, f) {
super.on(eventName, f)
}
/**
* @param {string} eventName
* @param {function} f
*/
off (eventName, f) {
super.off(eventName, f)
}
}

View File

@@ -1,4 +1,3 @@
import { AbstractType } from '../internals.js' // eslint-disable-line
import * as decoding from 'lib0/decoding'

View File

@@ -1,4 +1,3 @@
import {
YArray,
YMap,

View File

@@ -1,4 +1,3 @@
import {
writeID,
readID,
@@ -9,6 +8,7 @@ import {
createID,
ContentType,
followRedone,
getItem,
ID, Doc, AbstractType // eslint-disable-line
} from '../internals.js'
@@ -102,7 +102,7 @@ export const relativePositionToJSON = rpos => {
*
* @function
*/
export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock), json.assoc == null ? 0 : json.assoc)
export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname ?? null, json.item == null ? null : createID(json.item.client, json.item.clock), json.assoc == null ? 0 : json.assoc)
export class AbsolutePosition {
/**
@@ -257,13 +257,24 @@ export const readRelativePosition = decoder => {
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
/**
* Transform a relative position to an absolute position.
*
* If you want to share the relative position with other users, you should set
* `followUndoneDeletions` to false to get consistent results across all clients.
*
* When calculating the absolute position, we try to follow the "undone deletions". This yields
* better results for the user who performed undo. However, only the user who performed the undo
* will get the better results, the other users don't know which operations recreated a deleted
* range of content. There is more information in this ticket: https://github.com/yjs/yjs/issues/638
*
* @param {RelativePosition} rpos
* @param {Doc} doc
* @param {boolean} followUndoneDeletions - whether to follow undone deletions - see https://github.com/yjs/yjs/issues/638
* @return {AbsolutePosition|null}
*
* @function
*/
export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndoneDeletions = true) => {
const store = doc.store
const rightID = rpos.item
const typeID = rpos.type
@@ -275,7 +286,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
if (getState(store, rightID.client) <= rightID.clock) {
return null
}
const res = followRedone(store, rightID)
const res = followUndoneDeletions ? followRedone(store, rightID) : { item: getItem(store, rightID), diff: 0 }
const right = res.item
if (!(right instanceof Item)) {
return null
@@ -299,7 +310,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
// type does not exist yet
return null
}
const { item } = followRedone(store, typeID)
const { item } = followUndoneDeletions ? followRedone(store, typeID) : { item: getItem(store, typeID) }
if (item instanceof Item && item.content instanceof ContentType) {
type = item.content.type
} else {

View File

@@ -1,4 +1,3 @@
import {
isDeleted,
createDeleteSetFromStructStore,

View File

@@ -1,4 +1,3 @@
import {
GC,
splitItem,

View File

@@ -1,4 +1,3 @@
import {
getState,
writeStructsFromTransaction,
@@ -29,7 +28,8 @@ import { callAll } from 'lib0/function'
* possible. Here is an example to illustrate the advantages of bundling:
*
* @example
* const map = y.define('map', YMap)
* const ydoc = new Y.Doc()
* const map = ydoc.getMap('map')
* // Log content when change is triggered
* map.observe(() => {
* console.log('change triggered')
@@ -38,7 +38,7 @@ import { callAll } from 'lib0/function'
* map.set('a', 0) // => "change triggered"
* map.set('b', 0) // => "change triggered"
* // When put in a transaction, it will trigger the log after the transaction:
* y.transact(() => {
* ydoc.transact(() => {
* map.set('a', 1)
* map.set('b', 1)
* }) // => "change triggered"
@@ -166,18 +166,29 @@ export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
* @return {number} # of merged structs
*/
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1]
const right = structs[pos]
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1)
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
/** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
const tryToMergeWithLefts = (structs, pos) => {
let right = structs[pos]
let left = structs[pos - 1]
let i = pos
for (; i > 0; right = left, left = structs[--i - 1]) {
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
/** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
}
continue
}
}
break
}
const merged = pos - i
if (merged) {
// remove all merged structs from the array
structs.splice(pos + 1 - merged, merged)
}
return merged
}
/**
@@ -214,7 +225,7 @@ const tryGcDeleteSet = (ds, store, gcFilter) => {
*/
const tryMergeDeleteSet = (ds, store) => {
// try to merge deleted / gc'd items
// merge from right to left for better efficiecy and so we don't miss any merge targets
// merge from right to left for better efficiency and so we don't miss any merge targets
ds.clients.forEach((deleteItems, client) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) {
@@ -224,9 +235,9 @@ const tryMergeDeleteSet = (ds, store) => {
for (
let si = mostRightIndexToCheck, struct = structs[si];
si > 0 && struct.id.clock >= deleteItem.clock;
struct = structs[--si]
struct = structs[si]
) {
tryToMergeWithLeft(structs, si)
si -= 1 + tryToMergeWithLefts(structs, si)
}
}
})
@@ -318,23 +329,25 @@ const cleanupTransactions = (transactionCleanups, i) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
// we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i)
for (let i = structs.length - 1; i >= firstChangePos;) {
i -= 1 + tryToMergeWithLefts(structs, i)
}
}
})
// try to merge mergeStructs
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
// but at the moment DS does not handle duplicates
for (let i = 0; i < mergeStructs.length; i++) {
for (let i = mergeStructs.length - 1; i >= 0; i--) {
const { client, clock } = mergeStructs[i].id
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
if (tryToMergeWithLefts(structs, replacedStructPos + 1) > 1) {
continue // no need to perform next check, both are already merged
}
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
tryToMergeWithLefts(structs, replacedStructPos)
}
}
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {

View File

@@ -10,12 +10,13 @@ import {
getItemCleanStart,
isDeleted,
addToDeleteSet,
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
YEvent, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
} from '../internals.js'
import * as time from 'lib0/time'
import * as array from 'lib0/array'
import { Observable } from 'lib0/observable'
import * as logging from 'lib0/logging'
import { ObservableV2 } from 'lib0/observable'
export class StackItem {
/**
@@ -47,15 +48,10 @@ const clearUndoManagerStackItem = (tr, um, stackItem) => {
/**
* @param {UndoManager} undoManager
* @param {Array<StackItem>} stack
* @param {string} eventType
* @param {'undo'|'redo'} eventType
* @return {StackItem?}
*/
const popStackItem = (undoManager, stack, eventType) => {
/**
* Whether a change happened
* @type {StackItem?}
*/
let result = null
/**
* Keep a reference to the transaction so we can fire the event with the changedParentTypes
* @type {any}
@@ -64,7 +60,7 @@ const popStackItem = (undoManager, stack, eventType) => {
const doc = undoManager.doc
const scope = undoManager.scope
transact(doc, transaction => {
while (stack.length > 0 && result === null) {
while (stack.length > 0 && undoManager.currStackItem === null) {
const store = doc.store
const stackItem = /** @type {StackItem} */ (stack.pop())
/**
@@ -112,7 +108,7 @@ const popStackItem = (undoManager, stack, eventType) => {
performedChange = true
}
}
result = performedChange ? stackItem : null
undoManager.currStackItem = performedChange ? stackItem : null
}
transaction.changed.forEach((subProps, type) => {
// destroy search marker if necessary
@@ -122,11 +118,13 @@ const popStackItem = (undoManager, stack, eventType) => {
})
_tr = transaction
}, undoManager)
if (result != null) {
const res = undoManager.currStackItem
if (res != null) {
const changedParentTypes = _tr.changedParentTypes
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType, changedParentTypes }, undoManager])
undoManager.emit('stack-item-popped', [{ stackItem: res, type: eventType, changedParentTypes, origin: undoManager }, undoManager])
undoManager.currStackItem = null
}
return result
return res
}
/**
@@ -142,6 +140,14 @@ const popStackItem = (undoManager, stack, eventType) => {
* @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty.
*/
/**
* @typedef {Object} StackItemEvent
* @property {StackItem} StackItemEvent.stackItem
* @property {any} StackItemEvent.origin
* @property {'undo'|'redo'} StackItemEvent.type
* @property {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>} StackItemEvent.changedParentTypes
*/
/**
* Fires 'stack-item-added' event when a stack item was added to either the undo- or
* the redo-stack. You may store additional stack information via the
@@ -149,9 +155,9 @@ const popStackItem = (undoManager, stack, eventType) => {
* Fires 'stack-item-popped' event when a stack item was popped from either the
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
*
* @extends {Observable<'stack-item-added'|'stack-item-popped'|'stack-cleared'|'stack-item-updated'>}
* @extends {ObservableV2<{'stack-item-added':function(StackItemEvent, UndoManager):void, 'stack-item-popped': function(StackItemEvent, UndoManager):void, 'stack-cleared': function({ undoStackCleared: boolean, redoStackCleared: boolean }):void, 'stack-item-updated': function(StackItemEvent, UndoManager):void }>}
*/
export class UndoManager extends Observable {
export class UndoManager extends ObservableV2 {
/**
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
* @param {UndoManagerOptions} options
@@ -169,6 +175,7 @@ export class UndoManager extends Observable {
* @type {Array<AbstractType<any>>}
*/
this.scope = []
this.doc = doc
this.addToScope(typeScope)
this.deleteFilter = deleteFilter
trackedOrigins.add(this)
@@ -189,7 +196,12 @@ export class UndoManager extends Observable {
*/
this.undoing = false
this.redoing = false
this.doc = doc
/**
* The currently popped stack item if UndoManager.undoing or UndoManager.redoing
*
* @type {StackItem|null}
*/
this.currStackItem = null
this.lastChange = 0
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
this.captureTimeout = captureTimeout
@@ -243,6 +255,9 @@ export class UndoManager extends Observable {
keepItem(item, true)
}
})
/**
* @type {[StackItemEvent, UndoManager]}
*/
const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]
if (didAdd) {
this.emit('stack-item-added', changeEvent)
@@ -263,6 +278,7 @@ export class UndoManager extends Observable {
ytypes = array.isArray(ytypes) ? ytypes : [ytypes]
ytypes.forEach(ytype => {
if (this.scope.every(yt => yt !== ytype)) {
if (ytype.doc !== this.doc) logging.warn('[yjs#509] Not same Y.Doc') // use MultiDocUndoManager instead. also see https://github.com/yjs/yjs/issues/509
this.scope.push(ytype)
}
})

View File

@@ -1,4 +1,3 @@
import * as error from 'lib0/error'
import * as encoding from 'lib0/encoding'

View File

@@ -1,4 +1,3 @@
import {
isDeleted,
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
@@ -6,6 +5,9 @@ import {
import * as set from 'lib0/set'
import * as array from 'lib0/array'
import * as error from 'lib0/error'
const errorComputeChanges = 'You must not compute changes after the event-handler fired.'
/**
* @template {AbstractType<any>} T
@@ -84,6 +86,9 @@ export class YEvent {
*/
get keys () {
if (this._keys === null) {
if (this.transaction.doc._transactionCleanups.length === 0) {
throw error.create(errorComputeChanges)
}
const keys = new Map()
const target = this.target
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
@@ -167,6 +172,9 @@ export class YEvent {
get changes () {
let changes = this._changes
if (changes === null) {
if (this.transaction.doc._transactionCleanups.length === 0) {
throw error.create(errorComputeChanges)
}
const target = this.target
const added = set.create()
const deleted = set.create()
@@ -256,8 +264,8 @@ const getPathTo = (parent, child) => {
let i = 0
let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
while (c !== child._item && c !== null) {
if (!c.deleted) {
i++
if (!c.deleted && c.countable) {
i += c.length
}
c = c.right
}

View File

@@ -1,4 +1,3 @@
/**
* @module encoding
*/
@@ -155,7 +154,7 @@ export const readClientsStructRefs = (decoder, doc) => {
// @type {string|null}
const struct = new Item(
createID(client, clock),
null, // leftd
null, // left
(info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
null, // right
(info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
@@ -179,7 +178,7 @@ export const readClientsStructRefs = (decoder, doc) => {
const struct = new Item(
createID(client, clock),
null, // leftd
null, // left
origin, // origin
null, // right
rightOrigin, // right origin
@@ -251,7 +250,7 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
return nextStructsTarget
}
let curStructsTarget = getNextStructTarget()
if (curStructsTarget === null && stack.length === 0) {
if (curStructsTarget === null) {
return null
}
@@ -371,7 +370,7 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
/**
* Read and apply a document update.
*
* This function has the same effect as `applyUpdate` but accepts an decoder.
* This function has the same effect as `applyUpdate` but accepts a decoder.
*
* @param {decoding.Decoder} decoder
* @param {Doc} ydoc
@@ -452,7 +451,7 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n
/**
* Read and apply a document update.
*
* This function has the same effect as `applyUpdate` but accepts an decoder.
* This function has the same effect as `applyUpdate` but accepts a decoder.
*
* @param {decoding.Decoder} decoder
* @param {Doc} ydoc

View File

@@ -1,4 +1,3 @@
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
/**

View File

@@ -1,4 +1,3 @@
import {
AbstractType // eslint-disable-line
} from '../internals.js'

View File

@@ -1,4 +1,3 @@
import * as binary from 'lib0/binary'
import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding'

View File

@@ -1,4 +1,3 @@
/**
* Testing if encoding/decoding compatibility and integration compatiblity is given.
* We expect that the document always looks the same, even if we upgrade the integration algorithm, or add additional encoding approaches.

View File

@@ -1,4 +1,3 @@
import * as Y from '../src/index.js'
import * as t from 'lib0/testing'

View File

@@ -1,4 +1,3 @@
import * as Y from '../src/index.js'
import * as t from 'lib0/testing'
@@ -102,3 +101,25 @@ export const testRelativePositionAssociationDifference = tc => {
t.assert(posRight != null && posRight.index === 2)
t.assert(posLeft != null && posLeft.index === 1)
}
/**
* @param {t.TestCase} tc
*/
export const testRelativePositionWithUndo = tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
ytext.insert(0, 'hello world')
const rpos = Y.createRelativePositionFromTypeIndex(ytext, 1)
const um = new Y.UndoManager(ytext)
ytext.delete(0, 6)
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc)?.index === 0)
um.undo()
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc)?.index === 1)
const posWithoutFollow = Y.createAbsolutePositionFromRelativePosition(rpos, ydoc, false)
console.log({ posWithoutFollow })
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc, false)?.index === 6)
const ydocClone = new Y.Doc()
Y.applyUpdate(ydocClone, Y.encodeStateAsUpdate(ydoc))
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydocClone)?.index === 6)
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydocClone, false)?.index === 6)
}

View File

@@ -14,6 +14,21 @@ export const testBasic = _tc => {
t.assert(restored.getText().toString() === 'world!')
}
/**
* @param {t.TestCase} _tc
*/
export const testBasicXmlAttributes = _tc => {
const ydoc = new Y.Doc({ gc: false })
const yxml = ydoc.getMap().set('el', new Y.XmlElement('div'))
const snapshot1 = Y.snapshot(ydoc)
yxml.setAttribute('a', '1')
const snapshot2 = Y.snapshot(ydoc)
yxml.setAttribute('a', '2')
t.compare(yxml.getAttributes(), { a: '2' })
t.compare(yxml.getAttributes(snapshot2), { a: '1' })
t.compare(yxml.getAttributes(snapshot1), {})
}
/**
* @param {t.TestCase} _tc
*/

View File

@@ -1,4 +1,3 @@
import * as t from 'lib0/testing'
import * as prng from 'lib0/prng'
import * as encoding from 'lib0/encoding'
@@ -35,7 +34,7 @@ export const encV1 = {
mergeUpdates: Y.mergeUpdates,
applyUpdate: Y.applyUpdate,
logUpdate: Y.logUpdate,
updateEventName: 'update',
updateEventName: /** @type {'update'} */ ('update'),
diffUpdate: Y.diffUpdate
}
@@ -44,7 +43,7 @@ export const encV2 = {
mergeUpdates: Y.mergeUpdatesV2,
applyUpdate: Y.applyUpdateV2,
logUpdate: Y.logUpdateV2,
updateEventName: 'updateV2',
updateEventName: /** @type {'updateV2'} */ ('updateV2'),
diffUpdate: Y.diffUpdateV2
}

View File

@@ -3,6 +3,46 @@ import { init } from './testHelper.js' // eslint-disable-line
import * as Y from '../src/index.js'
import * as t from 'lib0/testing'
export const testInconsistentFormat = () => {
/**
* @param {Y.Doc} ydoc
*/
const testYjsMerge = ydoc => {
const content = /** @type {Y.XmlText} */ (ydoc.get('text', Y.XmlText))
content.format(0, 6, { bold: null })
content.format(6, 4, { type: 'text' })
t.compare(content.toDelta(), [
{
attributes: { type: 'text' },
insert: 'Merge Test'
},
{
attributes: { type: 'text', italic: true },
insert: ' After'
}
])
}
const initializeYDoc = () => {
const yDoc = new Y.Doc({ gc: false })
const content = /** @type {Y.XmlText} */ (yDoc.get('text', Y.XmlText))
content.insert(0, ' After', { type: 'text', italic: true })
content.insert(0, 'Test', { type: 'text' })
content.insert(0, 'Merge ', { type: 'text', bold: true })
return yDoc
}
{
const yDoc = initializeYDoc()
testYjsMerge(yDoc)
}
{
const initialYDoc = initializeYDoc()
const yDoc = new Y.Doc({ gc: false })
Y.applyUpdate(yDoc, Y.encodeStateAsUpdate(initialYDoc))
testYjsMerge(yDoc)
}
}
/**
* @param {t.TestCase} tc
*/
@@ -675,3 +715,33 @@ export const testUndoDeleteInMap = (tc) => {
undoManager.undo()
t.compare(map0.toJSON(), { a: 'a' })
}
/**
* It should expose the StackItem being processed if undoing
*
* @param {t.TestCase} _tc
*/
export const testUndoDoingStackItem = async (_tc) => {
const doc = new Y.Doc()
const text = doc.getText('text')
const undoManager = new Y.UndoManager([text])
undoManager.on('stack-item-added', /** @param {any} event */ event => {
event.stackItem.meta.set('str', '42')
})
let metaUndo = /** @type {any} */ (null)
let metaRedo = /** @type {any} */ (null)
text.observe((event) => {
const /** @type {Y.UndoManager} */ origin = event.transaction.origin
if (origin === undoManager && origin.undoing) {
metaUndo = origin.currStackItem?.meta.get('str')
} else if (origin === undoManager && origin.redoing) {
metaRedo = origin.currStackItem?.meta.get('str')
}
})
text.insert(0, 'abc')
undoManager.undo()
undoManager.redo()
t.compare(metaUndo, '42', 'currStackItem is accessible while undoing')
t.compare(metaRedo, '42', 'currStackItem is accessible while redoing')
t.compare(undoManager.currStackItem, null, 'currStackItem is null after observe/transaction')
}

View File

@@ -15,7 +15,7 @@ import * as object from 'lib0/object'
* @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta
* @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector
* @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
* @property {string} Enc.updateEventName
* @property {'update'|'updateV2'} Enc.updateEventName
* @property {string} Enc.description
* @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
*/
@@ -169,7 +169,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
// t.info('Target State: ')
// enc.logUpdate(targetState)
cases.forEach((mergedUpdates, i) => {
cases.forEach((mergedUpdates) => {
// t.info('State Case $' + i + ':')
// enc.logUpdate(updates)
const merged = new Y.Doc({ gc: false })
@@ -218,10 +218,10 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testMergeUpdates1 = tc => {
encoders.forEach((enc, i) => {
export const testMergeUpdates1 = _tc => {
encoders.forEach((enc) => {
t.info(`Using encoder: ${enc.description}`)
const ydoc = new Y.Doc({ gc: false })
const updates = /** @type {Array<Uint8Array>} */ ([])
@@ -299,16 +299,16 @@ export const testMergePendingUpdates = tc => {
Y.applyUpdate(yDoc5, update4)
Y.applyUpdate(yDoc5, serverUpdates[4])
// @ts-ignore
const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
const _update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
const yText5 = yDoc5.getText('textBlock')
t.compareStrings(yText5.toString(), 'nenor')
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testObfuscateUpdates = tc => {
export const testObfuscateUpdates = _tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText('text')
const ymap = ydoc.getMap('map')

View File

@@ -330,6 +330,29 @@ export const testObserveDeepEventOrder = tc => {
}
}
/**
* Correct index when computing event.path in observeDeep - https://github.com/yjs/yjs/issues/457
*
* @param {t.TestCase} _tc
*/
export const testObservedeepIndexes = _tc => {
const doc = new Y.Doc()
const map = doc.getMap()
// Create a field with the array as value
map.set('my-array', new Y.Array())
// Fill the array with some strings and our Map
map.get('my-array').push(['a', 'b', 'c', new Y.Map()])
/**
* @type {Array<any>}
*/
let eventPath = []
map.observeDeep((events) => { eventPath = events[0].path })
// set a value on the map inside of our array
map.get('my-array').get(3).set('hello', 'world')
console.log(eventPath)
t.compare(eventPath, ['my-array', 3])
}
/**
* @param {t.TestCase} tc
*/

View File

@@ -8,6 +8,54 @@ import * as Y from '../src/index.js'
import * as t from 'lib0/testing'
import * as prng from 'lib0/prng'
/**
* @param {t.TestCase} _tc
*/
export const testIterators = _tc => {
const ydoc = new Y.Doc()
/**
* @type {Y.Map<number>}
*/
const ymap = ydoc.getMap()
// we are only checking if the type assumptions are correct
/**
* @type {Array<number>}
*/
const vals = Array.from(ymap.values())
/**
* @type {Array<[string,number]>}
*/
const entries = Array.from(ymap.entries())
/**
* @type {Array<string>}
*/
const keys = Array.from(ymap.keys())
console.log(vals, entries, keys)
}
/**
* Computing event changes after transaction should result in an error. See yjs#539
*
* @param {t.TestCase} _tc
*/
export const testMapEventError = _tc => {
const doc = new Y.Doc()
const ymap = doc.getMap()
/**
* @type {any}
*/
let event = null
ymap.observe((e) => {
event = e
})
t.fails(() => {
t.info(event.keys)
})
t.fails(() => {
t.info(event.keys)
})
}
/**
* @param {t.TestCase} tc
*/

View File

@@ -1746,6 +1746,27 @@ export const testBasicFormat = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testFalsyFormats = tc => {
const { users, text0 } = init(tc, { users: 2 })
let delta
text0.observe(event => {
delta = event.delta
})
text0.insert(0, 'abcde', { falsy: false })
t.compare(text0.toDelta(), [{ insert: 'abcde', attributes: { falsy: false } }])
t.compare(delta, [{ insert: 'abcde', attributes: { falsy: false } }])
text0.format(1, 3, { falsy: true })
t.compare(text0.toDelta(), [{ insert: 'a', attributes: { falsy: false } }, { insert: 'bcd', attributes: { falsy: true } }, { insert: 'e', attributes: { falsy: false } }])
t.compare(delta, [{ retain: 1 }, { retain: 3, attributes: { falsy: true } }])
text0.format(2, 1, { falsy: false })
t.compare(text0.toDelta(), [{ insert: 'a', attributes: { falsy: false } }, { insert: 'b', attributes: { falsy: true } }, { insert: 'c', attributes: { falsy: false } }, { insert: 'd', attributes: { falsy: true } }, { insert: 'e', attributes: { falsy: false } }])
t.compare(delta, [{ retain: 2 }, { retain: 1, attributes: { falsy: false } }])
compare(users)
}
/**
* @param {t.TestCase} _tc
*/

View File

@@ -189,7 +189,6 @@ export const testClone = _tc => {
const third = new Y.XmlElement('p')
yxml.push([first, second, third])
t.compareArrays(yxml.toArray(), [first, second, third])
const cloneYxml = yxml.clone()
ydoc.getArray('copyarr').insert(0, [cloneYxml])
t.assert(cloneYxml.length === 3)
@@ -210,3 +209,15 @@ export const testFormattingBug = _tc => {
yxml.applyDelta(delta)
t.compare(yxml.toDelta(), delta)
}
/**
* @param {t.TestCase} _tc
*/
export const testElement = _tc => {
const ydoc = new Y.Doc()
const yxmlel = ydoc.getXmlElement()
const text1 = new Y.XmlText('text1')
const text2 = new Y.XmlText('text2')
yxmlel.insert(0, [text1, text2])
t.compareArrays(yxmlel.toArray(), [text1, text2])
}