Compare commits

..

17 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
Kevin Jahns
e0a2f11db3 13.5.45 2023-01-31 12:57:56 +01:00
Kevin Jahns
7445a9ce5f add whenSynced and isSynced property with refined logic 2023-01-31 12:56:07 +01:00
Kevin Jahns
7f6c12a541 bump typescript and fix type issues 2023-01-31 12:16:03 +01:00
Kevin Jahns
370d0c138d Merge pull request #496 from neftaly/array.from
Add test for Y.Array.from
2023-01-25 13:12:52 +01:00
Neftaly Hernandez
d29de75f85 Add test for Y.Array.from 2023-01-23 06:41:57 +00:00
Kevin Jahns
f215866429 remove poxi on request.. 2023-01-19 15:24:53 +01:00
Kevin Jahns
093b41ccc4 Merge pull request #495 from laem/patch-1
New user
2023-01-19 15:20:41 +01:00
Mael
ab60cd1ff8 New user 2023-01-19 15:06:20 +01:00
Kevin Jahns
1130abe05b add POXi as a user 2023-01-18 12:20:52 +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
17 changed files with 1508 additions and 636 deletions

View File

@@ -34,11 +34,11 @@ on Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%2
## Who is using Yjs
* [Sana](https://sanalabs.com/) A learning platform with collaborative text
editing powered by Yjs.
* [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source
knowledge base. 🏅
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2:
* [Sana](https://sanalabs.com/) A learning platform with collaborative text
editing powered by Yjs.
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
community. :star:
* [Room.sh](https://room.sh/) A meeting application with integrated
@@ -58,6 +58,8 @@ on Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%2
* [Skiff](https://skiff.org/) Private, decentralized workspace.
* [Hyperquery](https://hyperquery.ai/) A collaborative data workspace for
sharing analyses, documentation, spreadsheets, and dashboards.
* [Nosgestesclimat](https://nosgestesclimat.fr/groupe) The french carbon
footprint calculator has a group P2P mode based on yjs
## Table of Contents

1476
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.5.44",
"version": "13.5.47",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",
@@ -81,13 +81,13 @@
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.2.1",
"concurrently": "^3.6.1",
"typescript": "^4.9.5",
"http-server": "^0.12.3",
"jsdoc": "^3.6.7",
"markdownlint-cli": "^0.23.2",
"rollup": "^2.60.0",
"standard": "^16.0.4",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^4.4.4",
"y-protocols": "^1.0.5"
}
}

View File

@@ -324,9 +324,9 @@ export class AbstractType {
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder
*/
_write (encoder) { }
_write (_encoder) { }
/**
* The first non-deleted item
@@ -344,9 +344,9 @@ export class AbstractType {
* Must be implemented by each type.
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
* @param {Set<null|string>} _parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
_callObserver (transaction, _parentSubs) {
if (!transaction.local && this._searchMarker) {
this._searchMarker.length = 0
}

View File

@@ -58,11 +58,14 @@ export class YArray extends AbstractType {
/**
* Construct a new YArray containing the specified items.
* @template T
* @template {Object<string,any>|Array<any>|number|null|string|Uint8Array} T
* @param {Array<T>} items
* @return {YArray<T>}
*/
static from (items) {
/**
* @type {YArray<T>}
*/
const a = new YArray()
a.push(items)
return a
@@ -84,6 +87,9 @@ export class YArray extends AbstractType {
this._prelimContent = null
}
/**
* @return {YArray<T>}
*/
_copy () {
return new YArray()
}
@@ -92,9 +98,12 @@ export class YArray extends AbstractType {
* @return {YArray<T>}
*/
clone () {
/**
* @type {YArray<T>}
*/
const arr = new YArray()
arr.insert(0, this.toArray().map(el =>
el instanceof AbstractType ? el.clone() : el
el instanceof AbstractType ? /** @type {typeof el} */ (el.clone()) : el
))
return arr
}
@@ -133,7 +142,7 @@ export class YArray extends AbstractType {
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, content)
typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content))
})
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
@@ -150,7 +159,7 @@ export class YArray extends AbstractType {
push (content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListPushGenerics(transaction, this, content)
typeListPushGenerics(transaction, this, /** @type {any} */ (content))
})
} else {
/** @type {Array<any>} */ (this._prelimContent).push(...content)
@@ -259,9 +268,9 @@ export class YArray extends AbstractType {
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
*
* @private
* @function
*/
export const readYArray = decoder => new YArray()
export const readYArray = _decoder => new YArray()

View File

@@ -81,6 +81,9 @@ export class YMap extends AbstractType {
this._prelimContent = null
}
/**
* @return {YMap<MapType>}
*/
_copy () {
return new YMap()
}
@@ -89,9 +92,12 @@ export class YMap extends AbstractType {
* @return {YMap<MapType>}
*/
clone () {
/**
* @type {YMap<MapType>}
*/
const map = new YMap()
this.forEach((value, key) => {
map.set(key, value instanceof AbstractType ? value.clone() : value)
map.set(key, value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value)
})
return map
}
@@ -207,7 +213,7 @@ export class YMap extends AbstractType {
set (key, value) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapSet(transaction, this, key, value)
typeMapSet(transaction, this, key, /** @type {any} */ (value))
})
} else {
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
@@ -241,7 +247,7 @@ export class YMap extends AbstractType {
clear () {
if (this.doc !== null) {
transact(this.doc, transaction => {
this.forEach(function (value, key, map) {
this.forEach(function (_value, key, map) {
typeMapDelete(transaction, map, key)
})
})
@@ -259,9 +265,9 @@ export class YMap extends AbstractType {
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
*
* @private
* @function
*/
export const readYMap = decoder => new YMap()
export const readYMap = _decoder => new YMap()

View File

@@ -251,7 +251,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
* @function
**/
const insertText = (transaction, parent, currPos, text, attributes) => {
currPos.currentAttributes.forEach((val, key) => {
currPos.currentAttributes.forEach((_val, key) => {
if (attributes[key] === undefined) {
attributes[key] = null
}
@@ -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
@@ -927,7 +932,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 {object} [opts]
* @param {object} opts
* @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true.
*
*
@@ -1229,12 +1234,11 @@ export class YText extends AbstractType {
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @param {Snapshot} [snapshot]
* @return {Object<string, any>} A JSON Object that describes the attributes.
*
* @public
*/
getAttributes (snapshot) {
getAttributes () {
return typeMapGetAll(this)
}
@@ -1247,10 +1251,10 @@ export class YText extends AbstractType {
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {YText}
*
* @private
* @function
*/
export const readYText = decoder => new YText()
export const readYText = _decoder => new YText()

View File

@@ -9,7 +9,7 @@ import {
typeMapGetAll,
typeListForEach,
YXmlElementRefID,
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Snapshot, Doc, Item // eslint-disable-line
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
} from '../internals.js'
/**

View File

@@ -17,10 +17,11 @@ import {
transact,
typeListGet,
typeListSlice,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook // eslint-disable-line
} 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))
}
/**
@@ -407,7 +408,7 @@ export class YXmlFragment extends AbstractType {
/**
* Executes a provided function on once on overy child element.
*
* @param {function(YXmlElement|YXmlText,number, typeof this):void} f A function to execute on every element of this YArray.
* @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
typeListForEach(this, f)
@@ -427,10 +428,10 @@ export class YXmlFragment extends AbstractType {
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {YXmlFragment}
*
* @private
* @function
*/
export const readYXmlFragment = decoder => new YXmlFragment()
export const readYXmlFragment = _decoder => new YXmlFragment()

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

@@ -38,7 +38,7 @@ export const generateNewClientId = random.uint32
*/
export class Doc extends Observable {
/**
* @param {DocOpts} [opts] configuration
* @param {DocOpts} opts configuration
*/
constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) {
super()
@@ -72,13 +72,57 @@ export class Doc extends Observable {
this.shouldLoad = shouldLoad
this.autoLoad = autoLoad
this.meta = meta
/**
* This is set to true when the persistence provider loaded the document from the database or when the `sync` event fires.
* Note that not all providers implement this feature. Provider authors are encouraged to fire the `load` event when the doc content is loaded from the database.
*
* @type {boolean}
*/
this.isLoaded = false
/**
* This is set to true when the connection provider has successfully synced with a backend.
* Note that when using peer-to-peer providers this event may not provide very useful.
* Also note that not all providers implement this feature. Provider authors are encouraged to fire
* the `sync` event when the doc has been synced (with `true` as a parameter) or if connection is
* lost (with false as a parameter).
*/
this.isSynced = false
/**
* Promise that resolves once the document has been loaded from a presistence provider.
*/
this.whenLoaded = promise.create(resolve => {
this.on('load', () => {
this.isLoaded = true
resolve(this)
})
})
const provideSyncedPromise = () => promise.create(resolve => {
/**
* @param {boolean} isSynced
*/
const eventHandler = (isSynced) => {
if (isSynced === undefined || isSynced === true) {
this.off('sync', eventHandler)
resolve()
}
}
this.on('sync', eventHandler)
})
this.on('sync', isSynced => {
if (isSynced === false && this.isSynced) {
this.whenSynced = provideSyncedPromise()
}
this.isSynced = isSynced === undefined || isSynced === true
if (!this.isLoaded) {
this.emit('load', [])
}
})
/**
* Promise that resolves once the document has been synced with a backend.
* This promise is recreated when the connection is lost.
* Note the documentation about the `isSynced` property.
*/
this.whenSynced = provideSyncedPromise()
}
/**
@@ -103,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

@@ -71,7 +71,7 @@ export class PermanentUserData {
* @param {Doc} doc
* @param {number} clientid
* @param {string} userDescription
* @param {Object} [conf]
* @param {Object} conf
* @param {function(Transaction, DeleteSet):boolean} [conf.filter]
*/
setUserMapping (doc, clientid, userDescription, { filter = () => true } = {}) {
@@ -84,7 +84,7 @@ export class PermanentUserData {
users.set(userDescription, user)
}
user.get('ids').push([clientid])
users.observe(event => {
users.observe(_event => {
setTimeout(() => {
const userOverwrite = users.get(userDescription)
if (userOverwrite !== user) {

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

@@ -30,9 +30,9 @@ export const testOriginInTransaction = _tc => {
/**
* Client id should be changed when an instance receives updates from another client using the same client id.
*
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testClientIdDuplicateChange = tc => {
export const testClientIdDuplicateChange = _tc => {
const doc1 = new Y.Doc()
doc1.clientID = 0
const doc2 = new Y.Doc()
@@ -44,9 +44,9 @@ export const testClientIdDuplicateChange = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testGetTypeEmptyId = tc => {
export const testGetTypeEmptyId = _tc => {
const doc1 = new Y.Doc()
doc1.getText('').insert(0, 'h')
doc1.getText().insert(1, 'i')
@@ -57,9 +57,9 @@ export const testGetTypeEmptyId = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testToJSON = tc => {
export const testToJSON = _tc => {
const doc = new Y.Doc()
t.compare(doc.toJSON(), {}, 'doc.toJSON yields empty object')
@@ -84,9 +84,9 @@ export const testToJSON = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testSubdoc = tc => {
export const testSubdoc = _tc => {
const doc = new Y.Doc()
doc.load() // doesn't do anything
{
@@ -151,9 +151,9 @@ export const testSubdoc = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testSubdocLoadEdgeCases = tc => {
export const testSubdocLoadEdgeCases = _tc => {
const ydoc = new Y.Doc()
const yarray = ydoc.getArray()
const subdoc1 = new Y.Doc()
@@ -198,9 +198,9 @@ export const testSubdocLoadEdgeCases = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testSubdocLoadEdgeCasesAutoload = tc => {
export const testSubdocLoadEdgeCasesAutoload = _tc => {
const ydoc = new Y.Doc()
const yarray = ydoc.getArray()
const subdoc1 = new Y.Doc({ autoLoad: true })
@@ -240,9 +240,9 @@ export const testSubdocLoadEdgeCasesAutoload = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testSubdocsUndo = tc => {
export const testSubdocsUndo = _tc => {
const ydoc = new Y.Doc()
const elems = ydoc.getXmlFragment()
const undoManager = new Y.UndoManager(elems)
@@ -255,9 +255,9 @@ export const testSubdocsUndo = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testLoadDocs = async tc => {
export const testLoadDocsEvent = async _tc => {
const ydoc = new Y.Doc()
t.assert(ydoc.isLoaded === false)
let loadedEvent = false
@@ -269,3 +269,44 @@ export const testLoadDocs = async tc => {
t.assert(loadedEvent)
t.assert(ydoc.isLoaded)
}
/**
* @param {t.TestCase} _tc
*/
export const testSyncDocsEvent = async _tc => {
const ydoc = new Y.Doc()
t.assert(ydoc.isLoaded === false)
t.assert(ydoc.isSynced === false)
let loadedEvent = false
ydoc.once('load', () => {
loadedEvent = true
})
let syncedEvent = false
ydoc.once('sync', /** @param {any} isSynced */ (isSynced) => {
syncedEvent = true
t.assert(isSynced)
})
ydoc.emit('sync', [true, ydoc])
await ydoc.whenLoaded
const oldWhenSynced = ydoc.whenSynced
await ydoc.whenSynced
t.assert(loadedEvent)
t.assert(syncedEvent)
t.assert(ydoc.isLoaded)
t.assert(ydoc.isSynced)
let loadedEvent2 = false
ydoc.on('load', () => {
loadedEvent2 = true
})
let syncedEvent2 = false
ydoc.on('sync', (isSynced) => {
syncedEvent2 = true
t.assert(isSynced === false)
})
ydoc.emit('sync', [false, ydoc])
t.assert(!loadedEvent2)
t.assert(syncedEvent2)
t.assert(ydoc.isLoaded)
t.assert(!ydoc.isSynced)
t.assert(ydoc.whenSynced !== oldWhenSynced)
}

View File

@@ -32,6 +32,17 @@ export const testSlice = tc => {
t.compareArrays(arr.slice(0, 2), [0, 1])
}
/**
* @param {t.TestCase} tc
*/
export const testArrayFrom = tc => {
const doc1 = new Y.Doc()
const db1 = doc1.getMap('root')
const nestedArray1 = Y.Array.from([0, 1, 2])
db1.set('array', nestedArray1)
t.compare(nestedArray1.toArray(), [0, 1, 2])
}
/**
* Debugging yjs#297 - a critical bug connected to the search-marker approach
*

View File

@@ -455,9 +455,9 @@ export const testChangeEvent = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testYmapEventExceptionsShouldCompleteTransaction = tc => {
export const testYmapEventExceptionsShouldCompleteTransaction = _tc => {
const doc = new Y.Doc()
const map = doc.getMap('map')

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