Compare commits

..

8 Commits

Author SHA1 Message Date
Kevin Jahns
658c520b93 13.5.47 2023-02-21 14:37:24 +01:00
Kevin Jahns
2576d4efca increasing sort of ds encoding 2023-02-21 14:35:28 +01:00
Kevin Jahns
58b754950e Merge pull request #439 from Synthesia-Technologies/feat/deterministic-update-encoding
Make encodeStateAsUpdate deterministic
2023-02-21 10:59:31 +01:00
Kevin Jahns
ea7ad07f34 13.5.46 2023-02-14 16:21:01 +01:00
Kevin Jahns
1c999b250e fix #474 - formatting bug 2023-02-14 16:19:22 +01:00
Kevin Jahns
e9189365ee add debugging case for #474 - unfininished 2023-02-13 14:27:57 +01:00
Adam Chelminski
6b7b3136e0 delete set encoding should be in descending order 2022-06-23 16:01:29 +02:00
Adam Chelminski
da052bdb0a Make encodeStateAsUpdate deterministic 2022-06-23 15:50:35 +02:00
8 changed files with 439 additions and 21 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "yjs",
"version": "13.5.45",
"version": "13.5.47",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "yjs",
"version": "13.5.45",
"version": "13.5.47",
"license": "MIT",
"dependencies": {
"lib0": "^0.2.49"

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.5.45",
"version": "13.5.47",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",

View File

@@ -382,12 +382,17 @@ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAtt
switch (content.constructor) {
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (content)
if ((endAttributes.get(key) || null) !== value || (startAttributes.get(key) || null) === value) {
const startAttrValue = startAttributes.get(key) || null
if ((endAttributes.get(key) || null) !== value || startAttrValue === value) {
// Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction)
cleanups++
if (!reachedEndOfCurr && (currAttributes.get(key) || null) === value && (startAttributes.get(key) || null) !== value) {
currAttributes.delete(key)
if (startAttrValue === null) {
currAttributes.delete(key)
} else {
currAttributes.set(key, startAttrValue)
}
}
}
break

View File

@@ -21,6 +21,7 @@ import {
} from '../internals.js'
import * as error from 'lib0/error'
import * as array from 'lib0/array'
/**
* Define the elements to which a set of CSS queries apply.
@@ -237,7 +238,7 @@ export class YXmlFragment extends AbstractType {
querySelectorAll (query) {
query = query.toUpperCase()
// @ts-ignore
return Array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
return array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
}
/**

View File

@@ -219,17 +219,21 @@ export const createDeleteSetFromStructStore = ss => {
*/
export const writeDeleteSet = (encoder, ds) => {
encoding.writeVarUint(encoder.restEncoder, ds.clients.size)
ds.clients.forEach((dsitems, client) => {
encoder.resetDsCurVal()
encoding.writeVarUint(encoder.restEncoder, client)
const len = dsitems.length
encoding.writeVarUint(encoder.restEncoder, len)
for (let i = 0; i < len; i++) {
const item = dsitems[i]
encoder.writeDsClock(item.clock)
encoder.writeDsLen(item.len)
}
})
// Ensure that the delete set is written in a deterministic order
array.from(ds.clients.entries())
.sort((a, b) => b[0] - a[0])
.forEach(([client, dsitems]) => {
encoder.resetDsCurVal()
encoding.writeVarUint(encoder.restEncoder, client)
const len = dsitems.length
encoding.writeVarUint(encoder.restEncoder, len)
for (let i = 0; i < len; i++) {
const item = dsitems[i]
encoder.writeDsClock(item.clock)
encoder.writeDsLen(item.len)
}
})
}
/**

View File

@@ -147,7 +147,7 @@ export class Doc extends Observable {
}
getSubdocGuids () {
return new Set(Array.from(this.subdocs).map(doc => doc.guid))
return new Set(array.from(this.subdocs).map(doc => doc.guid))
}
/**

View File

@@ -45,6 +45,7 @@ import * as decoding from 'lib0/decoding'
import * as binary from 'lib0/binary'
import * as map from 'lib0/map'
import * as math from 'lib0/math'
import * as array from 'lib0/array'
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
@@ -96,7 +97,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
encoding.writeVarUint(encoder.restEncoder, sm.size)
// Write items with higher client ids first
// This heavily improves the conflict algorithm.
Array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
// @ts-ignore
writeStructs(encoder, store.clients.get(client), client, clock)
})
@@ -231,7 +232,7 @@ const integrateStructs = (transaction, store, clientsStructRefs) => {
*/
const stack = []
// sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user.
let clientsStructRefsIds = Array.from(clientsStructRefs.keys()).sort((a, b) => a - b)
let clientsStructRefsIds = array.from(clientsStructRefs.keys()).sort((a, b) => a - b)
if (clientsStructRefsIds.length === 0) {
return null
}
@@ -601,7 +602,7 @@ export const decodeStateVector = decodedState => readStateVector(new DSDecoderV1
*/
export const writeStateVector = (encoder, sv) => {
encoding.writeVarUint(encoder.restEncoder, sv.size)
Array.from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
array.from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping
encoding.writeVarUint(encoder.restEncoder, clock)
})

View File

@@ -5,6 +5,413 @@ import * as math from 'lib0/math'
const { init, compare } = Y
/**
* https://github.com/yjs/yjs/issues/474
* @todo Remove debug: 127.0.0.1:8080/test.html?filter=\[88/
* @param {t.TestCase} _tc
*/
export const testDeltaBug = _tc => {
const initialDelta = [{
attributes: {
'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087'
},
insert: '\n'
},
{
attributes: {
'table-col': {
width: '150'
}
},
insert: '\n\n\n'
},
{
attributes: {
'block-id': 'block-9144be72-e528-4f91-b0b2-82d20408e9ea',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-6kv2ls',
cell: 'cell-apba4k'
},
row: 'row-6kv2ls',
cell: 'cell-apba4k',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-639adacb-1516-43ed-b272-937c55669a1c',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-6kv2ls',
cell: 'cell-a8qf0r'
},
row: 'row-6kv2ls',
cell: 'cell-a8qf0r',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-6302ca4a-73a3-4c25-8c1e-b542f048f1c6',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-6kv2ls',
cell: 'cell-oi9ikb'
},
row: 'row-6kv2ls',
cell: 'cell-oi9ikb',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-ceeddd05-330e-4f86-8017-4a3a060c4627',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-d1sv2g',
cell: 'cell-dt6ks2'
},
row: 'row-d1sv2g',
cell: 'cell-dt6ks2',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-37b19322-cb57-4e6f-8fad-0d1401cae53f',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-d1sv2g',
cell: 'cell-qah2ay'
},
row: 'row-d1sv2g',
cell: 'cell-qah2ay',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-468a69b5-9332-450b-9107-381d593de249',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-d1sv2g',
cell: 'cell-fpcz5a'
},
row: 'row-d1sv2g',
cell: 'cell-fpcz5a',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-26b1d252-9b2e-4808-9b29-04e76696aa3c',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-pflz90',
cell: 'cell-zrhylp'
},
row: 'row-pflz90',
cell: 'cell-zrhylp',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-6af97ba7-8cf9-497a-9365-7075b938837b',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-pflz90',
cell: 'cell-s1q9nt'
},
row: 'row-pflz90',
cell: 'cell-s1q9nt',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-107e273e-86bc-44fd-b0d7-41ab55aca484',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-pflz90',
cell: 'cell-20b0j9'
},
row: 'row-pflz90',
cell: 'cell-20b0j9',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-38161f9c-6f6d-44c5-b086-54cc6490f1e3'
},
insert: '\n'
},
{
insert: 'Content after table'
},
{
attributes: {
'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce'
},
insert: '\n'
}
]
const ydoc1 = new Y.Doc()
const ytext = ydoc1.getText()
ytext.applyDelta(initialDelta)
const addingDash = [
{
retain: 12
},
{
insert: '-'
}
]
ytext.applyDelta(addingDash)
const addingSpace = [
{
retain: 13
},
{
insert: ' '
}
]
ytext.applyDelta(addingSpace)
const addingList = [
{
retain: 12
},
{
delete: 2
},
{
retain: 1,
attributes: {
// Clear table line attribute
'table-cell-line': null,
// Add list attribute in place of table-cell-line
list: {
rowspan: '1',
colspan: '1',
row: 'row-pflz90',
cell: 'cell-20b0j9',
list: 'bullet'
}
}
}
]
ytext.applyDelta(addingList)
const result = ytext.toDelta()
const expectedResult = [
{
attributes: {
'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087'
},
insert: '\n'
},
{
attributes: {
'table-col': {
width: '150'
}
},
insert: '\n\n\n'
},
{
attributes: {
'block-id': 'block-9144be72-e528-4f91-b0b2-82d20408e9ea',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-6kv2ls',
cell: 'cell-apba4k'
},
row: 'row-6kv2ls',
cell: 'cell-apba4k',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-639adacb-1516-43ed-b272-937c55669a1c',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-6kv2ls',
cell: 'cell-a8qf0r'
},
row: 'row-6kv2ls',
cell: 'cell-a8qf0r',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-6302ca4a-73a3-4c25-8c1e-b542f048f1c6',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-6kv2ls',
cell: 'cell-oi9ikb'
},
row: 'row-6kv2ls',
cell: 'cell-oi9ikb',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-ceeddd05-330e-4f86-8017-4a3a060c4627',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-d1sv2g',
cell: 'cell-dt6ks2'
},
row: 'row-d1sv2g',
cell: 'cell-dt6ks2',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-37b19322-cb57-4e6f-8fad-0d1401cae53f',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-d1sv2g',
cell: 'cell-qah2ay'
},
row: 'row-d1sv2g',
cell: 'cell-qah2ay',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-468a69b5-9332-450b-9107-381d593de249',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-d1sv2g',
cell: 'cell-fpcz5a'
},
row: 'row-d1sv2g',
cell: 'cell-fpcz5a',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-26b1d252-9b2e-4808-9b29-04e76696aa3c',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-pflz90',
cell: 'cell-zrhylp'
},
row: 'row-pflz90',
cell: 'cell-zrhylp',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
'block-id': 'block-6af97ba7-8cf9-497a-9365-7075b938837b',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-pflz90',
cell: 'cell-s1q9nt'
},
row: 'row-pflz90',
cell: 'cell-s1q9nt',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
insert: '\n',
// This attibutes has only list and no table-cell-line
attributes: {
list: {
rowspan: '1',
colspan: '1',
row: 'row-pflz90',
cell: 'cell-20b0j9',
list: 'bullet'
},
'block-id': 'block-107e273e-86bc-44fd-b0d7-41ab55aca484',
row: 'row-pflz90',
cell: 'cell-20b0j9',
rowspan: '1',
colspan: '1'
}
},
// No table-cell-line below here
{
attributes: {
'block-id': 'block-38161f9c-6f6d-44c5-b086-54cc6490f1e3'
},
insert: '\n'
},
{
insert: 'Content after table'
},
{
attributes: {
'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce'
},
insert: '\n'
}
]
t.compare(result, expectedResult)
}
/**
* In this test we are mainly interested in the cleanup behavior and whether the resulting delta makes sense.
* It is fine if the resulting delta is not minimal. But applying the delta to a rich-text editor should result in a