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
|
||||
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:
|
||||
|
||||
- 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",
|
||||
"version": "13.4.1",
|
||||
"version": "13.4.3",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.4.1",
|
||||
"version": "13.4.3",
|
||||
"description": "Shared Editing Library",
|
||||
"main": "./dist/yjs.cjs",
|
||||
"module": "./dist/yjs.mjs",
|
||||
|
||||
@@ -51,6 +51,17 @@ export class ContentString {
|
||||
splice (offset) {
|
||||
const right = new ContentString(this.str.slice(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
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@ import {
|
||||
} from '../internals.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'
|
||||
|
||||
/**
|
||||
@@ -594,7 +592,7 @@ export class Item extends AbstractStruct {
|
||||
}
|
||||
this.markDeleted()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,12 @@ export const addEventHandlerListener = (eventHandler, f) =>
|
||||
* @function
|
||||
*/
|
||||
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 => {
|
||||
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
|
||||
// because we know it has at least one element
|
||||
callEventHandlerListeners(type._dEH, events, transaction)
|
||||
|
||||
@@ -204,6 +204,34 @@ export const testInsertAndDeleteEventsForTypes = tc => {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -249,6 +249,8 @@ export const testAppendChars = tc => {
|
||||
t.assert(text0.length === N)
|
||||
}
|
||||
|
||||
const largeDocumentSize = 100000
|
||||
|
||||
const id = Y.createID(0, 0)
|
||||
const c = new Y.ContentString('a')
|
||||
|
||||
@@ -256,7 +258,7 @@ const c = new Y.ContentString('a')
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testBestCase = tc => {
|
||||
const N = 2000000
|
||||
const N = largeDocumentSize
|
||||
const items = new Array(N)
|
||||
t.measureTime('time to create two million items in the best case', () => {
|
||||
const parent = /** @type {any} */ ({})
|
||||
@@ -293,7 +295,7 @@ const tryGc = () => {
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testLargeFragmentedDocument = tc => {
|
||||
const itemsToInsert = 1000000
|
||||
const itemsToInsert = largeDocumentSize
|
||||
let update = /** @type {any} */ (null)
|
||||
;(() => {
|
||||
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
|
||||
|
||||
let charCounter = 0
|
||||
|
||||
Reference in New Issue
Block a user