Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91b718cde0 | ||
|
|
d56221b66a | ||
|
|
ce43124ad0 | ||
|
|
0af69cf6d6 | ||
|
|
927c2369aa | ||
|
|
8270373c9f | ||
|
|
5e712e39b1 | ||
|
|
4ffd23fd0b | ||
|
|
05d974cee1 | ||
|
|
f1532771b7 | ||
|
|
aee9e14d09 | ||
|
|
f5aa852054 | ||
|
|
b990ad9f86 | ||
|
|
43e17802a6 | ||
|
|
01c3668a0b | ||
|
|
52b906898f | ||
|
|
d119459fad | ||
|
|
d730abe594 | ||
|
|
ca24f1ee76 | ||
|
|
dc45a8d3cf | ||
|
|
2062f52a90 | ||
|
|
6e674ff5f7 | ||
|
|
2fba694cd4 | ||
|
|
b235c57d76 | ||
|
|
6beab79eb4 |
@@ -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
|
||||
|
||||
42
README.md
42
README.md
@@ -49,7 +49,9 @@ 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:
|
||||
@@ -64,11 +66,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 +97,18 @@ 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
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -145,9 +163,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 +188,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 +228,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
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.6.14",
|
||||
"version": "13.6.15",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "yjs",
|
||||
"version": "13.6.14",
|
||||
"version": "13.6.15",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.86"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.6.14",
|
||||
"version": "13.6.15",
|
||||
"description": "Shared Editing Library",
|
||||
"main": "./dist/yjs.cjs",
|
||||
"module": "./dist/yjs.mjs",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,6 +95,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 () {
|
||||
@@ -244,7 +248,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 () {
|
||||
|
||||
@@ -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 () {
|
||||
@@ -406,7 +410,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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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