Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86f7631d1e | ||
|
|
3bb107504f | ||
|
|
39803c1d11 | ||
|
|
46fae57036 | ||
|
|
e9cb07da55 | ||
|
|
114f28f48e | ||
|
|
a1da486c8a | ||
|
|
4fb9cc2a30 | ||
|
|
72f3ce75b2 |
@@ -12,7 +12,7 @@ which aren't described in the paper. The most notable is that items have an
|
|||||||
`originRight` as well as an `origin` property, which improves performance when
|
`originRight` as well as an `origin` property, which improves performance when
|
||||||
many concurrent inserts happen after the same character.
|
many concurrent inserts happen after the same character.
|
||||||
|
|
||||||
At it heart, Yjs is a list CRDT. Everything is squeezed into a list in order to
|
At its heart, Yjs is a list CRDT. Everything is squeezed into a list in order to
|
||||||
reuse the CRDT resolution algorithm:
|
reuse the CRDT resolution algorithm:
|
||||||
|
|
||||||
- Arrays are easy - they're lists of arbitrary items.
|
- Arrays are easy - they're lists of arbitrary items.
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.4.1",
|
"version": "13.4.3",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.4.1",
|
"version": "13.4.3",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
|
|||||||
@@ -51,6 +51,17 @@ export class ContentString {
|
|||||||
splice (offset) {
|
splice (offset) {
|
||||||
const right = new ContentString(this.str.slice(offset))
|
const right = new ContentString(this.str.slice(offset))
|
||||||
this.str = this.str.slice(0, offset)
|
this.str = this.str.slice(0, offset)
|
||||||
|
|
||||||
|
// Prevent encoding invalid documents because of splitting of surrogate pairs: https://github.com/yjs/yjs/issues/248
|
||||||
|
const firstCharCode = this.str.charCodeAt(offset - 1)
|
||||||
|
if (firstCharCode >= 0xD800 && firstCharCode <= 0xDBFF) {
|
||||||
|
// Last character of the left split is the start of a surrogate utf16/ucs2 pair.
|
||||||
|
// We don't support splitting of surrogate pairs because this may lead to invalid documents.
|
||||||
|
// Replace the invalid character with a unicode replacement character (<28> / U+FFFD)
|
||||||
|
this.str = this.str.slice(0, offset - 1) + '<27>'
|
||||||
|
// replace right as well
|
||||||
|
right.str = '<27>' + right.str.slice(1)
|
||||||
|
}
|
||||||
return right
|
return right
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ import {
|
|||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error.js'
|
import * as error from 'lib0/error.js'
|
||||||
import * as maplib from 'lib0/map.js'
|
|
||||||
import * as set from 'lib0/set.js'
|
|
||||||
import * as binary from 'lib0/binary.js'
|
import * as binary from 'lib0/binary.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -594,7 +592,7 @@ export class Item extends AbstractStruct {
|
|||||||
}
|
}
|
||||||
this.markDeleted()
|
this.markDeleted()
|
||||||
addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length)
|
addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length)
|
||||||
maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub)
|
addChangedTypeToTransaction(transaction, parent, this.parentSub)
|
||||||
this.content.delete(transaction)
|
this.content.delete(transaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,12 @@ export const addEventHandlerListener = (eventHandler, f) =>
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const removeEventHandlerListener = (eventHandler, f) => {
|
export const removeEventHandlerListener = (eventHandler, f) => {
|
||||||
eventHandler.l = eventHandler.l.filter(g => f !== g)
|
const l = eventHandler.l
|
||||||
|
const len = l.length
|
||||||
|
eventHandler.l = l.filter(g => f !== g)
|
||||||
|
if (len === eventHandler.l.length) {
|
||||||
|
console.error('[yjs] Tried to remove event handler that doesn\'t exist.')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -284,6 +284,9 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
.forEach(event => {
|
.forEach(event => {
|
||||||
event.currentTarget = type
|
event.currentTarget = type
|
||||||
})
|
})
|
||||||
|
// sort events by path length so that top-level events are fired first.
|
||||||
|
events
|
||||||
|
.sort((event1, event2) => event1.path.length - event2.path.length)
|
||||||
// We don't need to check for events.length
|
// We don't need to check for events.length
|
||||||
// because we know it has at least one element
|
// because we know it has at least one element
|
||||||
callEventHandlerListeners(type._dEH, events, transaction)
|
callEventHandlerListeners(type._dEH, events, transaction)
|
||||||
|
|||||||
@@ -204,6 +204,34 @@ export const testInsertAndDeleteEventsForTypes = tc => {
|
|||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This issue has been reported in https://discuss.yjs.dev/t/order-in-which-events-yielded-by-observedeep-should-be-applied/261/2
|
||||||
|
*
|
||||||
|
* Deep observers generate multiple events. When an array added at item at, say, position 0,
|
||||||
|
* and item 1 changed then the array-add event should fire first so that the change event
|
||||||
|
* path is correct. A array binding might lead to an inconsistent state otherwise.
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testObserveDeepEventOrder = tc => {
|
||||||
|
const { array0, users } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {Array<any>}
|
||||||
|
*/
|
||||||
|
let events = []
|
||||||
|
array0.observeDeep(e => {
|
||||||
|
events = e
|
||||||
|
})
|
||||||
|
array0.insert(0, [new Y.Map()])
|
||||||
|
users[0].transact(() => {
|
||||||
|
array0.get(0).set('a', 'a')
|
||||||
|
array0.insert(0, [0])
|
||||||
|
})
|
||||||
|
for (let i = 1; i < events.length; i++) {
|
||||||
|
t.assert(events[i - 1].path.length <= events[i].path.length, 'path size increases, fire top-level events first')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -249,6 +249,8 @@ export const testAppendChars = tc => {
|
|||||||
t.assert(text0.length === N)
|
t.assert(text0.length === N)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const largeDocumentSize = 100000
|
||||||
|
|
||||||
const id = Y.createID(0, 0)
|
const id = Y.createID(0, 0)
|
||||||
const c = new Y.ContentString('a')
|
const c = new Y.ContentString('a')
|
||||||
|
|
||||||
@@ -256,7 +258,7 @@ const c = new Y.ContentString('a')
|
|||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testBestCase = tc => {
|
export const testBestCase = tc => {
|
||||||
const N = 2000000
|
const N = largeDocumentSize
|
||||||
const items = new Array(N)
|
const items = new Array(N)
|
||||||
t.measureTime('time to create two million items in the best case', () => {
|
t.measureTime('time to create two million items in the best case', () => {
|
||||||
const parent = /** @type {any} */ ({})
|
const parent = /** @type {any} */ ({})
|
||||||
@@ -293,7 +295,7 @@ const tryGc = () => {
|
|||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testLargeFragmentedDocument = tc => {
|
export const testLargeFragmentedDocument = tc => {
|
||||||
const itemsToInsert = 1000000
|
const itemsToInsert = largeDocumentSize
|
||||||
let update = /** @type {any} */ (null)
|
let update = /** @type {any} */ (null)
|
||||||
;(() => {
|
;(() => {
|
||||||
const doc1 = new Y.Doc()
|
const doc1 = new Y.Doc()
|
||||||
@@ -321,6 +323,40 @@ export const testLargeFragmentedDocument = tc => {
|
|||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splitting surrogates can lead to invalid encoded documents.
|
||||||
|
*
|
||||||
|
* https://github.com/yjs/yjs/issues/248
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testSplitSurrogateCharacter = tc => {
|
||||||
|
{
|
||||||
|
const { users, text0 } = init(tc, { users: 2 })
|
||||||
|
users[1].disconnect() // disconnecting forces the user to encode the split surrogate
|
||||||
|
text0.insert(0, '👾') // insert surrogate character
|
||||||
|
// split surrogate, which should not lead to an encoding error
|
||||||
|
text0.insert(1, 'hi!')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const { users, text0 } = init(tc, { users: 2 })
|
||||||
|
users[1].disconnect() // disconnecting forces the user to encode the split surrogate
|
||||||
|
text0.insert(0, '👾👾') // insert surrogate character
|
||||||
|
// partially delete surrogate
|
||||||
|
text0.delete(1, 2)
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const { users, text0 } = init(tc, { users: 2 })
|
||||||
|
users[1].disconnect() // disconnecting forces the user to encode the split surrogate
|
||||||
|
text0.insert(0, '👾👾') // insert surrogate character
|
||||||
|
// formatting will also split surrogates
|
||||||
|
text0.format(1, 2, { bold: true })
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RANDOM TESTS
|
// RANDOM TESTS
|
||||||
|
|
||||||
let charCounter = 0
|
let charCounter = 0
|
||||||
|
|||||||
Reference in New Issue
Block a user