Compare commits

...

15 Commits

Author SHA1 Message Date
Kevin Jahns
78e0527b46 13.6.24 2025-03-04 14:44:19 +01:00
Kevin Jahns
69d4a5c821 [UndoManager] support global undo 2025-03-04 14:42:19 +01:00
Kevin Jahns
cc9a857441 slightly optimize TreeWalker and integration process 2025-02-24 20:30:48 +01:00
Kevin Jahns
4b865764b8 Merge pull request #691 from reknih/add-typst
Add Typst to Yjs users in README
2025-01-17 12:00:53 +01:00
Martin Haug
40725e373b Add Typst to Yjs users in README 2025-01-17 11:58:36 +01:00
Kevin Jahns
c05b815b4c 13.6.23 2025-01-15 21:46:23 +01:00
Kevin Jahns
e53c44e3a6 expose getItemCleanStart/End 2025-01-15 21:44:18 +01:00
Kevin Jahns
1bec008862 13.6.22 2025-01-12 19:45:03 +01:00
Kevin Jahns
bb5410b6dd marginally better typings for applyDelta - #689 2025-01-12 19:41:19 +01:00
Kevin Jahns
2d2e662d4d Merge pull request #690 from yjs/revert-689-patch-1
Revert "fix(yText): applyDelta should support both Delta and Ops[]"
2025-01-12 19:33:12 +01:00
Kevin Jahns
80e83a84c6 Revert "fix(yText): applyDelta should support both Delta and Ops[]" 2025-01-12 19:32:51 +01:00
Kevin Jahns
3c9c0f17d1 Merge pull request #689 from ykou-clickup/patch-1
fix(yText): applyDelta should support both Delta and Ops[]
2025-01-12 19:26:30 +01:00
Yuxiang Kou
e67b1296a7 fix(yText): applyDelta should support both Delta and Ops[]
Fixed an issue that the yText.applyDelta() accepted only Ops[], but not Delta.
2025-01-09 14:58:47 -08:00
Kevin Jahns
1a0d4aa797 Merge pull request #685 from szepeviktor/typos
Fix typos
2025-01-07 12:36:44 +01:00
Viktor Szépe
f18eab2dfe Fix typos 2025-01-03 18:11:43 +00:00
23 changed files with 82 additions and 61 deletions

View File

@@ -60,7 +60,7 @@ characters have either been deleted or all characters are not deleted. The item
will be split if the run is interrupted for any reason (eg a character in the
middle of the run is deleted).
When an item is created, it stores a reference to the IDs of the preceeding and
When an item is created, it stores a reference to the IDs of the preceding and
succeeding item. These are stored in the item's `origin` and `originRight`
fields, respectively. These are used when peers concurrently insert at the same
location in a document. Though quite rare in practice, Yjs needs to make sure

View File

@@ -110,7 +110,7 @@ Showcase](https://yjs-diagram.synergy.codes/).
* [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
application
* [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.
@@ -123,6 +123,7 @@ Showcase](https://yjs-diagram.synergy.codes/).
* [ScienHub](https://scienhub.com) - Collaborative LaTeX editor in the browser.
* [Open Collaboration Tools](https://www.open-collab.tools/) - Collaborative
editing for your IDE or custom editor
* [Typst](https://typst.app/) - Compose, edit, and automate technical documents
## Table of Contents
@@ -189,7 +190,7 @@ backends to y-websocket.
<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
signaling data over signaling servers. Publically available signaling servers
signaling data over signaling servers. Publicly available signaling servers
are available. Communication over the signaling servers can be encrypted by
providing a shared secret, keeping the connection information and the shared
document private.
@@ -1097,7 +1098,7 @@ encoding format for document updates. If you prefer JSON encoding, you can
simply JSON.stringify / JSON.parse the relative position instead.
</dd>
<b><code>Y.decodeRelativePosition(Uint8Array):RelativePosition</code></b>
<dd>Decode a binary-encoded relative position to a RelativePositon object.</dd>
<dd>Decode a binary-encoded relative position to a RelativePosition object.</dd>
</dl>
### Y.UndoManager
@@ -1277,11 +1278,11 @@ More information about the specific implementation is available in
CRDTs that are suitable for shared text editing suffer from the fact that they
only grow in size. There are CRDTs that do not grow in size, but they do not
have the characteristics that are benificial for shared text editing (like
have the characteristics that are beneficial for shared text editing (like
intention preservation). Yjs implements many improvements to the original
algorithm that diminish the trade-off that the document only grows in size. We
can't garbage collect deleted structs (tombstones) while ensuring a unique
order of the structs. But we can 1. merge preceeding structs into a single
order of the structs. But we can 1. merge preceding structs into a single
struct to reduce the amount of meta information, 2. we can delete content from
the struct if it is deleted, and 3. we can garbage collect tombstones if we
don't care about the order of the structs anymore (e.g. if the parent was

12
package-lock.json generated
View File

@@ -1,15 +1,15 @@
{
"name": "yjs",
"version": "13.6.21",
"version": "13.6.24",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "yjs",
"version": "13.6.21",
"version": "13.6.24",
"license": "MIT",
"dependencies": {
"lib0": "^0.2.98"
"lib0": "^0.2.99"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.1",
@@ -2785,9 +2785,9 @@
}
},
"node_modules/lib0": {
"version": "0.2.98",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz",
"integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==",
"version": "0.2.99",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.99.tgz",
"integrity": "sha512-vwztYuUf1uf/1zQxfzRfO5yzfNKhTtgOByCruuiQQxWQXnPb8Itaube5ylofcV0oM0aKal9Mv+S1s1Ky0UYP1w==",
"dependencies": {
"isomorphic.js": "^0.2.4"
},

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.6.21",
"version": "13.6.24",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",
@@ -76,7 +76,7 @@
},
"homepage": "https://docs.yjs.dev",
"dependencies": {
"lib0": "^0.2.98"
"lib0": "^0.2.99"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.1",

View File

@@ -50,6 +50,8 @@ export {
findRootTypeKey,
findIndexSS,
getItem,
getItemCleanStart,
getItemCleanEnd,
typeListToArraySnapshot,
typeMapGetSnapshot,
typeMapGetAllSnapshot,

View File

@@ -26,7 +26,7 @@ export class AbstractStruct {
* This method is already assuming that `this.id.clock + this.length === this.id.clock`.
* Also this method does *not* remove right from StructStore!
* @param {AbstractStruct} right
* @return {boolean} wether this merged with right
* @return {boolean} whether this merged with right
*/
mergeWith (right) {
return false

View File

@@ -393,8 +393,7 @@ export class Item extends AbstractStruct {
if (this.left && this.left.constructor === Item) {
this.parent = this.left.parent
this.parentSub = this.left.parentSub
}
if (this.right && this.right.constructor === Item) {
} else if (this.right && this.right.constructor === Item) {
this.parent = this.right.parent
this.parentSub = this.right.parentSub
}

View File

@@ -155,11 +155,11 @@ export const findMarker = (yarray, index) => {
// }
// }
// if (marker) {
// if (window.lengthes == null) {
// window.lengthes = []
// window.getLengthes = () => window.lengthes.sort((a, b) => a - b)
// if (window.lengths == null) {
// window.lengths = []
// window.getLengths = () => window.lengths.sort((a, b) => a - b)
// }
// window.lengthes.push(marker.index - pindex)
// window.lengths.push(marker.index - pindex)
// console.log('distance', marker.index - pindex, 'len', p && p.parent.length)
// }
if (marker !== null && math.abs(marker.index - pindex) < /** @type {YText|YArray<any>} */ (p.parent).length / maxSearchMarker) {
@@ -751,7 +751,7 @@ export const typeListInsertGenerics = (transaction, parent, index, content) => {
/**
* Pushing content is special as we generally want to push after the last item. So we don't have to update
* the serach marker.
* the search marker.
*
* @param {Transaction} transaction
* @param {AbstractType<any>} parent

View File

@@ -478,7 +478,7 @@ export const cleanupYTextFormatting = type => {
}
/**
* This will be called by the transction once the event handlers are called to potentially cleanup
* This will be called by the transaction once the event handlers are called to potentially cleanup
* formatting attributes.
*
* @param {Transaction} transaction
@@ -568,7 +568,7 @@ const deleteText = (transaction, currPos, length) => {
/**
* The Quill Delta format represents changes on a text document with
* formatting information. For mor information visit {@link https://quilljs.com/docs/delta/|Quill Delta}
* formatting information. For more information visit {@link https://quilljs.com/docs/delta/|Quill Delta}
*
* @example
* {
@@ -961,7 +961,7 @@ export class YText extends AbstractType {
/**
* Apply a {@link Delta} on this shared YText type.
*
* @param {any} delta The changes to apply on this element.
* @param {Array<any>} delta The changes to apply on this element.
* @param {object} opts
* @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true.
*

View File

@@ -12,7 +12,7 @@ export class YXmlEvent extends YEvent {
* @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created.
* @param {Set<string|null>} subs The set of changed attributes. `null` is included if the
* child list changed.
* @param {Transaction} transaction The transaction instance with wich the
* @param {Transaction} transaction The transaction instance with which the
* change was created.
*/
constructor (target, subs, transaction) {

View File

@@ -96,8 +96,12 @@ export class YXmlTreeWalker {
} else {
// walk right or up in the tree
while (n !== null) {
if (n.right !== null) {
n = n.right
/**
* @type {Item | null}
*/
const nxt = n.next
if (nxt !== null) {
n = nxt
break
} else if (n.parent === this._root) {
n = null

View File

@@ -106,7 +106,7 @@ export class Doc extends ObservableV2 {
this.isSynced = false
this.isDestroyed = false
/**
* Promise that resolves once the document has been loaded from a presistence provider.
* Promise that resolves once the document has been loaded from a persistence provider.
*/
this.whenLoaded = promise.create(resolve => {
this.on('load', () => {

View File

@@ -62,7 +62,7 @@ export class PermanentUserData {
initUser(storeType.get(userDescription), userDescription)
)
})
// add intial data
// add initial data
storeType.forEach(initUser)
}

View File

@@ -66,7 +66,7 @@ export class RelativePosition {
* after the meant position.
* I.e. position 1 in 'ab' is associated to character 'b'.
*
* If assoc < 0, then the relative position is associated to the caharacter
* If assoc < 0, then the relative position is associated to the character
* before the meant position.
*
* @type {number}

View File

@@ -66,13 +66,13 @@ export const getState = (store, client) => {
* @private
* @function
*/
export const integretyCheck = store => {
export const integrityCheck = store => {
store.clients.forEach(structs => {
for (let i = 1; i < structs.length; i++) {
const l = structs[i - 1]
const r = structs[i]
if (l.id.clock + l.length !== r.id.clock) {
throw new Error('StructStore failed integrety check')
throw new Error('StructStore failed integrity check')
}
}
})

View File

@@ -39,7 +39,7 @@ export class StackItem {
*/
const clearUndoManagerStackItem = (tr, um, stackItem) => {
iterateDeletedStructs(tr, stackItem.deletions, item => {
if (item instanceof Item && um.scope.some(type => isParentOf(type, item))) {
if (item instanceof Item && um.scope.some(type => type === tr.doc || isParentOf(/** @type {AbstractType<any>} */ (type), item))) {
keepItem(item, false)
}
})
@@ -81,7 +81,7 @@ const popStackItem = (undoManager, stack, eventType) => {
}
struct = item
}
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
if (!struct.deleted && scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct)
}
}
@@ -89,7 +89,7 @@ const popStackItem = (undoManager, stack, eventType) => {
iterateDeletedStructs(transaction, stackItem.deletions, struct => {
if (
struct instanceof Item &&
scope.some(type => isParentOf(type, struct)) &&
scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), struct)) &&
// Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
!isDeleted(stackItem.insertions, struct.id)
) {
@@ -159,7 +159,7 @@ const popStackItem = (undoManager, stack, eventType) => {
*/
export class UndoManager extends ObservableV2 {
/**
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
* @param {Doc|AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
* @param {UndoManagerOptions} options
*/
constructor (typeScope, {
@@ -168,11 +168,11 @@ export class UndoManager extends ObservableV2 {
deleteFilter = () => true,
trackedOrigins = new Set([null]),
ignoreRemoteMapChanges = false,
doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope.doc)
doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope instanceof Doc ? typeScope : typeScope.doc)
} = {}) {
super()
/**
* @type {Array<AbstractType<any>>}
* @type {Array<AbstractType<any> | Doc>}
*/
this.scope = []
this.doc = doc
@@ -212,7 +212,7 @@ export class UndoManager extends ObservableV2 {
// Only track certain transactions
if (
!this.captureTransaction(transaction) ||
!this.scope.some(type => transaction.changedParentTypes.has(type)) ||
!this.scope.some(type => transaction.changedParentTypes.has(/** @type {AbstractType<any>} */ (type)) || type === this.doc) ||
(!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))
) {
return
@@ -251,7 +251,7 @@ export class UndoManager extends ObservableV2 {
}
// make sure that deleted structs are not gc'd
iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
if (item instanceof Item && this.scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), item))) {
keepItem(item, true)
}
})
@@ -272,13 +272,15 @@ export class UndoManager extends ObservableV2 {
}
/**
* @param {Array<AbstractType<any>> | AbstractType<any>} ytypes
* @param {Array<AbstractType<any> | Doc> | AbstractType<any> | Doc} ytypes
*/
addToScope (ytypes) {
const tmpSet = new Set(this.scope)
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
if (!tmpSet.has(ytype)) {
tmpSet.add(ytype)
if (ytype instanceof AbstractType ? ytype.doc !== this.doc : ytype !== 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

@@ -167,7 +167,7 @@ export class UpdateEncoderV2 extends DSEncoderV2 {
*/
this.keyMap = new Map()
/**
* Refers to the next uniqe key-identifier to me used.
* Refers to the next unique key-identifier to me used.
* See writeKey method for more information.
*
* @type {number}

View File

@@ -211,7 +211,7 @@ export const readClientsStructRefs = (decoder, doc) => {
* then we start emptying the stack.
*
* It is not possible to have circles: i.e. struct1 (from client1) depends on struct2 (from client2)
* depends on struct3 (from client1). Therefore the max stack size is eqaul to `structReaders.length`.
* depends on struct3 (from client1). Therefore the max stack size is equal to `structReaders.length`.
*
* This method is implemented in a way so that we can resume computation if this update
* causally depends on another update.
@@ -279,14 +279,14 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
const addStackToRestSS = () => {
for (const item of stack) {
const client = item.id.client
const unapplicableItems = clientsStructRefs.get(client)
if (unapplicableItems) {
const inapplicableItems = clientsStructRefs.get(client)
if (inapplicableItems) {
// decrement because we weren't able to apply previous operation
unapplicableItems.i--
restStructs.clients.set(client, unapplicableItems.refs.slice(unapplicableItems.i))
inapplicableItems.i--
restStructs.clients.set(client, inapplicableItems.refs.slice(inapplicableItems.i))
clientsStructRefs.delete(client)
unapplicableItems.i = 0
unapplicableItems.refs = []
inapplicableItems.i = 0
inapplicableItems.refs = []
} else {
// item was the last item on clientsStructRefs and the field was already cleared. Add item to restStructs and continue
restStructs.clients.set(client, [item])

View File

@@ -1,5 +1,5 @@
/**
* Testing if encoding/decoding compatibility and integration compatiblity is given.
* Testing if encoding/decoding compatibility and integration compatibility is given.
* We expect that the document always looks the same, even if we upgrade the integration algorithm, or add additional encoding approaches.
*
* The v1 documents were generated with Yjs v13.2.0 based on the randomisized tests.

View File

@@ -58,7 +58,7 @@ export const testEmptyRestoreSnapshot = _tc => {
t.compare(docRestored.getArray().toArray(), [])
t.compare(doc.getArray().toArray(), ['world'])
// now this snapshot reflects the latest state. It shoult still work.
// now this snapshot reflects the latest state. It should still work.
const snap2 = Y.snapshot(doc)
const docRestored2 = Y.createDocFromSnapshot(doc, snap2)
t.compare(docRestored2.getArray().toArray(), ['world'])

View File

@@ -116,6 +116,19 @@ export const testEmptyTypeScope = _tc => {
t.assert(yarray.length === 0)
}
/**
* Test case to fix #241
* @param {t.TestCase} _tc
*/
export const testGlobalScope = _tc => {
const ydoc = new Y.Doc()
const um = new Y.UndoManager(ydoc)
const yarray = ydoc.getArray()
yarray.insert(0, [1])
um.undo()
t.assert(yarray.length === 0)
}
/**
* Test case to fix #241
* @param {t.TestCase} _tc

View File

@@ -369,11 +369,11 @@ export const testObserversUsingObservedeep = tc => {
/**
* @type {Array<Array<string|number>>}
*/
const pathes = []
const paths = []
let calls = 0
map0.observeDeep(events => {
events.forEach(event => {
pathes.push(event.path)
paths.push(event.path)
})
calls++
})
@@ -381,7 +381,7 @@ export const testObserversUsingObservedeep = tc => {
map0.get('map').set('array', new Y.Array())
map0.get('map').get('array').insert(0, ['content'])
t.assert(calls === 3)
t.compare(pathes, [[], ['map'], ['map', 'array']])
t.compare(paths, [[], ['map'], ['map', 'array']])
compare(users)
}
@@ -393,14 +393,14 @@ export const testPathsOfSiblingEvents = tc => {
/**
* @type {Array<Array<string|number>>}
*/
const pathes = []
const paths = []
let calls = 0
const doc = users[0]
map0.set('map', new Y.Map())
map0.get('map').set('text1', new Y.Text('initial'))
map0.observeDeep(events => {
events.forEach(event => {
pathes.push(event.path)
paths.push(event.path)
})
calls++
})
@@ -409,7 +409,7 @@ export const testPathsOfSiblingEvents = tc => {
map0.get('map').set('text2', new Y.Text('new'))
})
t.assert(calls === 1)
t.compare(pathes, [['map'], ['map', 'text1']])
t.compare(paths, [['map'], ['map', 'text1']])
compare(users)
}

View File

@@ -376,7 +376,7 @@ export const testDeltaBug = _tc => {
},
{
insert: '\n',
// This attibutes has only list and no table-cell-line
// This attributes has only list and no table-cell-line
attributes: {
list: {
rowspan: '1',