Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34b06b6cf9 | ||
|
|
d4dac558c0 | ||
|
|
a47a48b891 | ||
|
|
2e79d0369e | ||
|
|
88506f6d78 | ||
|
|
fbd088ee78 | ||
|
|
0678ed1eb5 | ||
|
|
0973e0acd4 | ||
|
|
6932696795 | ||
|
|
a4303f914d | ||
|
|
03593aeeb1 | ||
|
|
d67a951104 | ||
|
|
72205a688f | ||
|
|
edad668dbd | ||
|
|
c264b1c291 | ||
|
|
cdd8e4f5fc | ||
|
|
06e71f651d | ||
|
|
54594a2d75 | ||
|
|
13772bf891 | ||
|
|
0896ed42b2 | ||
|
|
656b7e7f6a | ||
|
|
0511b66346 | ||
|
|
91b718cde0 | ||
|
|
d56221b66a | ||
|
|
ce43124ad0 | ||
|
|
0af69cf6d6 | ||
|
|
3df335cb4c | ||
|
|
387be70ae9 | ||
|
|
927c2369aa | ||
|
|
8270373c9f | ||
|
|
43815d8292 | ||
|
|
f0dc53f53f | ||
|
|
25ae9f3236 | ||
|
|
5e712e39b1 | ||
|
|
4ffd23fd0b | ||
|
|
05d974cee1 | ||
|
|
f1532771b7 | ||
|
|
aee9e14d09 | ||
|
|
f5aa852054 | ||
|
|
b990ad9f86 | ||
|
|
43e17802a6 | ||
|
|
01c3668a0b | ||
|
|
52b906898f | ||
|
|
d119459fad | ||
|
|
d730abe594 | ||
|
|
ca24f1ee76 | ||
|
|
dc45a8d3cf | ||
|
|
2062f52a90 | ||
|
|
6e674ff5f7 | ||
|
|
2fba694cd4 | ||
|
|
b235c57d76 | ||
|
|
6beab79eb4 | ||
|
|
1e69d650b8 | ||
|
|
133cfc9cdc | ||
|
|
83db6c814c | ||
|
|
cdbb55818d | ||
|
|
221cb81dbf |
@@ -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
|
||||
|
||||
60
README.md
60
README.md
@@ -49,12 +49,16 @@ Showcase](https://yjs-diagram.synergy.codes/).
|
||||
## Who is using Yjs
|
||||
|
||||
* [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:
|
||||
* [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:
|
||||
@@ -64,11 +68,15 @@ Showcase](https://yjs-diagram.synergy.codes/).
|
||||
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.
|
||||
@@ -91,6 +99,21 @@ Showcase](https://yjs-diagram.synergy.codes/).
|
||||
* [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
|
||||
|
||||
@@ -145,9 +168,10 @@ collaborative app.
|
||||
<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. The backend can be extended to persist updates in a
|
||||
leveldb database. <b>y-sweet</b> and <b>ypy-websocket</b> (see below) are
|
||||
compatible to the y-wesocket protocol.
|
||||
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>
|
||||
@@ -169,6 +193,10 @@ browser DevTools extension.
|
||||
<dd>
|
||||
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>
|
||||
@@ -205,6 +233,15 @@ An ActionCable companion for Yjs clients. There is a fitting
|
||||
<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
|
||||
@@ -230,6 +267,17 @@ Like y-indexeddb, but with sub-documents support and fully TypeScript.
|
||||
<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
|
||||
@@ -259,7 +307,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
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.6.13",
|
||||
"version": "13.6.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "yjs",
|
||||
"version": "13.6.13",
|
||||
"version": "13.6.17",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.86"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.6.13",
|
||||
"version": "13.6.17",
|
||||
"description": "Shared Editing Library",
|
||||
"main": "./dist/yjs.cjs",
|
||||
"module": "./dist/yjs.mjs",
|
||||
|
||||
@@ -100,6 +100,7 @@ export {
|
||||
UpdateDecoderV1,
|
||||
UpdateDecoderV2,
|
||||
equalDeleteSets,
|
||||
mergeDeleteSets,
|
||||
snapshotContainsUpdate
|
||||
} from './internals.js'
|
||||
|
||||
|
||||
@@ -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>}
|
||||
*/
|
||||
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 {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.
|
||||
*
|
||||
* @param {AbstractType<any>} type
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>}
|
||||
*/
|
||||
clone () {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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>}
|
||||
*/
|
||||
clone () {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
clone () {
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
clone () {
|
||||
|
||||
@@ -187,22 +187,22 @@ export class Doc extends ObservableV2 {
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createID,
|
||||
ContentType,
|
||||
followRedone,
|
||||
getItem,
|
||||
ID, Doc, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
@@ -101,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 {
|
||||
/**
|
||||
@@ -256,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
|
||||
@@ -274,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
|
||||
@@ -298,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 {
|
||||
|
||||
@@ -28,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')
|
||||
@@ -37,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"
|
||||
@@ -224,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--) {
|
||||
|
||||
@@ -264,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
|
||||
}
|
||||
|
||||
@@ -154,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
|
||||
@@ -178,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
|
||||
@@ -370,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
|
||||
@@ -451,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
|
||||
|
||||
@@ -101,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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user