Compare commits

...

52 Commits

Author SHA1 Message Date
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 Barrett
221cb81dbf add screen.garden as user 2023-11-30 16:51:23 -05:00
21 changed files with 211 additions and 57 deletions

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 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 random 53-bit integer (53 bits because that fits in the javascript safe integer
range). range \[JavaScript uses IEEE 754 floats\]).
## List items ## 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 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 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 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 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 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 using a heuristic that is still changing (currently, it is updated when a new

View File

@@ -49,12 +49,17 @@ Showcase](https://yjs-diagram.synergy.codes/).
## Who is using Yjs ## Who is using Yjs
* [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source * [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source
knowledge base. 🏅 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: * [Cargo](https://cargo.site/) Site builder for designers and artists :star2:
* [Gitbook](https://gitbook.com) Knowledge management for technical teams :star2: * [Gitbook](https://gitbook.com) Knowledge management for technical teams :star2:
* [Evernote](https://evernote.com) Note-taking app :star2: * [Evernote](https://evernote.com) Note-taking app :star2:
* [Lessonspace](https://thelessonspace.com) Enterprise platform for virtual * [Lessonspace](https://thelessonspace.com) Enterprise platform for virtual
classrooms and online training :star2: 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: * [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star:
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and * [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
community. :star: community. :star:
@@ -64,11 +69,15 @@ Showcase](https://yjs-diagram.synergy.codes/).
Nimbus Web. :star: Nimbus Web. :star:
* [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to * [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to
collaboratively organize radio broadcasts. :star: 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 * [Sana](https://sanalabs.com/) A learning platform with collaborative text
editing powered by Yjs. editing powered by Yjs.
* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted * [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted
collaborative notes app. 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 * [Alldone](https://alldone.app/) A next-gen project management and
collaboration platform. collaboration platform.
* [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate. * [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate.
@@ -91,6 +100,21 @@ Showcase](https://yjs-diagram.synergy.codes/).
* [AWS SageMaker](https://aws.amazon.com/sagemaker/) Tools for building Machine * [AWS SageMaker](https://aws.amazon.com/sagemaker/) Tools for building Machine
Learning Models Learning Models
* [linear](https://linear.app) Streamline issues, projects, and product roadmaps. * [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 ## Table of Contents
@@ -145,9 +169,10 @@ collaborative app.
<dt><a href="https://github.com/yjs/y-websocket">y-websocket</a></dt> <dt><a href="https://github.com/yjs/y-websocket">y-websocket</a></dt>
<dd> <dd>
A module that contains a simple websocket backend and a websocket client that 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 connects to that backend. <a href="https://github.com/yjs/y-redis/"><b>y-redis</b></a>,
leveldb database. <b>y-sweet</b> and <b>ypy-websocket</b> (see below) are <b>y-sweet</b>, <b>ypy-websocket</b> and <a href="https://tiptap.dev/docs/hocuspocus/introduction">
compatible to the y-wesocket protocol. <b>Hocuspocus</b></a> (see below) are alternative
backends to y-websocket.
</dd> </dd>
<dt><a href="https://github.com/yjs/y-webrtc">y-webrtc</a></dt> <dt><a href="https://github.com/yjs/y-webrtc">y-webrtc</a></dt>
<dd> <dd>
@@ -169,6 +194,10 @@ browser DevTools extension.
<dd> <dd>
A standalone yjs server with persistence to S3 or filesystem. They offer a A standalone yjs server with persistence to S3 or filesystem. They offer a
<a href="https://y-sweet.cloud">cloud service</a> as well. <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> </dd>
<dt><a href="https://docs.partykit.io/reference/y-partykit-api/">PartyKit</a></dt> <dt><a href="https://docs.partykit.io/reference/y-partykit-api/">PartyKit</a></dt>
<dd> <dd>
@@ -205,6 +234,15 @@ An ActionCable companion for Yjs clients. There is a fitting
<dd> <dd>
Websocket backend, written in Python. Websocket backend, written in Python.
</dd> </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> </dl>
#### Persistence Providers #### Persistence Providers
@@ -230,6 +268,17 @@ Like y-indexeddb, but with sub-documents support and fully TypeScript.
<dd> <dd>
A database and connection provider for Yjs based on Firestore. A database and connection provider for Yjs based on Firestore.
</dd> </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> </dl>
# Ports # Ports
@@ -259,7 +308,7 @@ npm i yjs y-websocket
Start the y-websocket server: Start the y-websocket server:
```sh ```sh
PORT=1234 node ./node_modules/y-websocket/bin/server.js PORT=1234 node ./node_modules/y-websocket/bin/server.cjs
``` ```
### Example: Observe types ### Example: Observe types

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.6.13", "version": "13.6.16",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "yjs", "name": "yjs",
"version": "13.6.13", "version": "13.6.16",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lib0": "^0.2.86" "lib0": "^0.2.86"

View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.6.13", "version": "13.6.16",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.cjs", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",

View File

@@ -100,6 +100,7 @@ export {
UpdateDecoderV1, UpdateDecoderV1,
UpdateDecoderV2, UpdateDecoderV2,
equalDeleteSets, equalDeleteSets,
mergeDeleteSets,
snapshotContainsUpdate snapshotContainsUpdate
} from './internals.js' } from './internals.js'

View File

@@ -316,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>} * @return {AbstractType<EventType>}
*/ */
clone () { clone () {
@@ -477,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 {AbstractType<any>} type
* @param {function(any,number,any):void} f A function to execute on every element of this YArray. * @param {function(any,number,any):void} f A function to execute on every element of this YArray.
@@ -569,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. * Operates on a snapshotted state of the document.
* *
* @param {AbstractType<any>} type * @param {AbstractType<any>} type

View File

@@ -25,16 +25,7 @@ import { typeListSlice } from './AbstractType.js'
* @template T * @template T
* @extends YEvent<YArray<T>> * @extends YEvent<YArray<T>>
*/ */
export class YArrayEvent extends YEvent { export class YArrayEvent extends YEvent {}
/**
* @param {YArray<T>} yarray The changed type
* @param {Transaction} transaction The transaction object
*/
constructor (yarray, transaction) {
super(yarray, transaction)
this._transaction = transaction
}
}
/** /**
* A shared Array implementation. * 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>} * @return {YArray<T>}
*/ */
clone () { 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) { unshift (content) {
this.insert(0, 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} [start]
* @param {number} [end] * @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. * @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
*/ */

View File

@@ -88,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>} * @return {YMap<MapType>}
*/ */
clone () { clone () {

View File

@@ -201,7 +201,7 @@ const minimizeAttributeChanges = (currPos, attributes) => {
while (true) { while (true) {
if (currPos.right === null) { if (currPos.right === null) {
break 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 { } else {
break break
@@ -227,7 +227,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
// insert format-start items // insert format-start items
for (const key in attributes) { for (const key in attributes) {
const val = attributes[key] const val = attributes[key]
const currentVal = currPos.currentAttributes.get(key) || null const currentVal = currPos.currentAttributes.get(key) ?? null
if (!equalAttrs(currentVal, val)) { if (!equalAttrs(currentVal, val)) {
// save negated attribute (set null if currentVal undefined) // save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal) negatedAttributes.set(key, currentVal)
@@ -389,12 +389,12 @@ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAtt
switch (content.constructor) { switch (content.constructor) {
case ContentFormat: { case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (content) 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) { if (endFormats.get(key) !== content || startAttrValue === value) {
// Either this format is overwritten or it is not necessary because the attribute already existed. // Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction) start.delete(transaction)
cleanups++ cleanups++
if (!reachedCurr && (currAttributes.get(key) || null) === value && startAttrValue !== value) { if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
if (startAttrValue === null) { if (startAttrValue === null) {
currAttributes.delete(key) currAttributes.delete(key)
} else { } else {
@@ -769,12 +769,12 @@ export class YTextEvent extends YEvent {
const { key, value } = /** @type {ContentFormat} */ (item.content) const { key, value } = /** @type {ContentFormat} */ (item.content)
if (this.adds(item)) { if (this.adds(item)) {
if (!this.deletes(item)) { if (!this.deletes(item)) {
const curVal = currentAttributes.get(key) || null const curVal = currentAttributes.get(key) ?? null
if (!equalAttrs(curVal, value)) { if (!equalAttrs(curVal, value)) {
if (action === 'retain') { if (action === 'retain') {
addOp() addOp()
} }
if (equalAttrs(value, (oldAttributes.get(key) || null))) { if (equalAttrs(value, (oldAttributes.get(key) ?? null))) {
delete attributes[key] delete attributes[key]
} else { } else {
attributes[key] = value attributes[key] = value
@@ -785,7 +785,7 @@ export class YTextEvent extends YEvent {
} }
} else if (this.deletes(item)) { } else if (this.deletes(item)) {
oldAttributes.set(key, value) oldAttributes.set(key, value)
const curVal = currentAttributes.get(key) || null const curVal = currentAttributes.get(key) ?? null
if (!equalAttrs(curVal, value)) { if (!equalAttrs(curVal, value)) {
if (action === 'retain') { if (action === 'retain') {
addOp() 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} * @return {YText}
*/ */
clone () { clone () {

View File

@@ -81,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>} * @return {YXmlElement<KV>}
*/ */
clone () { clone () {

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} * @return {YXmlFragment}
*/ */
clone () { 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) { unshift (content) {
this.insert(0, 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} [start]
* @param {number} [end] * @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. * @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray.
*/ */

View File

@@ -29,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} * @return {YXmlHook}
*/ */
clone () { clone () {

View File

@@ -30,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} * @return {YXmlText}
*/ */
clone () { clone () {

View File

@@ -187,22 +187,22 @@ export class Doc extends ObservableV2 {
/** /**
* Define a shared data type. * 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. * 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:* * *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)`, .. * Also use the typed methods `getText(name)`, `getArray(name)`, ..
* *
* @template {typeof AbstractType<any>} Type * @template {typeof AbstractType<any>} Type
* @example * @example
* const y = new Y(..) * const ydoc = new Y.Doc(..)
* const appState = { * const appState = {
* document: y.getText('document') * document: ydoc.getText('document')
* comments: y.getArray('comments') * comments: ydoc.getArray('comments')
* } * }
* *
* @param {string} name * @param {string} name

View File

@@ -8,6 +8,7 @@ import {
createID, createID,
ContentType, ContentType,
followRedone, followRedone,
getItem,
ID, Doc, AbstractType // eslint-disable-line ID, Doc, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@@ -256,13 +257,24 @@ export const readRelativePosition = decoder => {
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array)) 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 {RelativePosition} rpos
* @param {Doc} doc * @param {Doc} doc
* @param {boolean} followUndoneDeletions - whether to follow undone deletions - see https://github.com/yjs/yjs/issues/638
* @return {AbsolutePosition|null} * @return {AbsolutePosition|null}
* *
* @function * @function
*/ */
export const createAbsolutePositionFromRelativePosition = (rpos, doc) => { export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndoneDeletions = true) => {
const store = doc.store const store = doc.store
const rightID = rpos.item const rightID = rpos.item
const typeID = rpos.type const typeID = rpos.type
@@ -274,7 +286,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
if (getState(store, rightID.client) <= rightID.clock) { if (getState(store, rightID.client) <= rightID.clock) {
return null return null
} }
const res = followRedone(store, rightID) const res = followUndoneDeletions ? followRedone(store, rightID) : { item: getItem(store, rightID), diff: 0 }
const right = res.item const right = res.item
if (!(right instanceof Item)) { if (!(right instanceof Item)) {
return null return null
@@ -298,7 +310,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
// type does not exist yet // type does not exist yet
return null 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) { if (item instanceof Item && item.content instanceof ContentType) {
type = item.content.type type = item.content.type
} else { } else {

View File

@@ -28,7 +28,8 @@ import { callAll } from 'lib0/function'
* possible. Here is an example to illustrate the advantages of bundling: * possible. Here is an example to illustrate the advantages of bundling:
* *
* @example * @example
* const map = y.define('map', YMap) * const ydoc = new Y.Doc()
* const map = ydoc.getMap('map')
* // Log content when change is triggered * // Log content when change is triggered
* map.observe(() => { * map.observe(() => {
* console.log('change triggered') * console.log('change triggered')
@@ -37,7 +38,7 @@ import { callAll } from 'lib0/function'
* map.set('a', 0) // => "change triggered" * map.set('a', 0) // => "change triggered"
* map.set('b', 0) // => "change triggered" * map.set('b', 0) // => "change triggered"
* // When put in a transaction, it will trigger the log after the transaction: * // When put in a transaction, it will trigger the log after the transaction:
* y.transact(() => { * ydoc.transact(() => {
* map.set('a', 1) * map.set('a', 1)
* map.set('b', 1) * map.set('b', 1)
* }) // => "change triggered" * }) // => "change triggered"
@@ -224,7 +225,7 @@ const tryGcDeleteSet = (ds, store, gcFilter) => {
*/ */
const tryMergeDeleteSet = (ds, store) => { const tryMergeDeleteSet = (ds, store) => {
// try to merge deleted / gc'd items // 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) => { ds.clients.forEach((deleteItems, client) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client)) const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
for (let di = deleteItems.length - 1; di >= 0; di--) { for (let di = deleteItems.length - 1; di >= 0; di--) {

View File

@@ -264,8 +264,8 @@ const getPathTo = (parent, child) => {
let i = 0 let i = 0
let c = /** @type {AbstractType<any>} */ (child._item.parent)._start let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
while (c !== child._item && c !== null) { while (c !== child._item && c !== null) {
if (!c.deleted) { if (!c.deleted && c.countable) {
i++ i += c.length
} }
c = c.right c = c.right
} }

View File

@@ -154,7 +154,7 @@ export const readClientsStructRefs = (decoder, doc) => {
// @type {string|null} // @type {string|null}
const struct = new Item( const struct = new Item(
createID(client, clock), createID(client, clock),
null, // leftd null, // left
(info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
null, // right null, // right
(info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
@@ -178,7 +178,7 @@ export const readClientsStructRefs = (decoder, doc) => {
const struct = new Item( const struct = new Item(
createID(client, clock), createID(client, clock),
null, // leftd null, // left
origin, // origin origin, // origin
null, // right null, // right
rightOrigin, // right origin rightOrigin, // right origin
@@ -370,7 +370,7 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
/** /**
* Read and apply a document update. * 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 {decoding.Decoder} decoder
* @param {Doc} ydoc * @param {Doc} ydoc
@@ -451,7 +451,7 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n
/** /**
* Read and apply a document update. * 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 {decoding.Decoder} decoder
* @param {Doc} ydoc * @param {Doc} ydoc

View File

@@ -101,3 +101,25 @@ export const testRelativePositionAssociationDifference = tc => {
t.assert(posRight != null && posRight.index === 2) t.assert(posRight != null && posRight.index === 2)
t.assert(posLeft != null && posLeft.index === 1) 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

@@ -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 * @param {t.TestCase} tc
*/ */

View File

@@ -1746,6 +1746,27 @@ export const testBasicFormat = tc => {
compare(users) 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 * @param {t.TestCase} _tc
*/ */