Compare commits

...

4 Commits

Author SHA1 Message Date
Kevin Jahns
5ee6992d1f 13.6.7 2023-07-17 14:43:07 +02:00
Kevin Jahns
dd31040656 Merge pull request #553 from yjs/simpler-bulk-changes-fix-542
bulk-merging structs - replaces #542, fixes #541
2023-07-17 14:35:18 +02:00
Kevin Jahns
c77dedb68d bulk-merging structs - replaces #542, fixes #541 2023-07-17 14:29:54 +02:00
Kevin Jahns
90f2a06b5e throw error when event changes are computed after a transaction 2023-06-27 13:20:53 +02:00
6 changed files with 65 additions and 20 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "yjs",
"version": "13.6.6",
"version": "13.6.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "yjs",
"version": "13.6.6",
"version": "13.6.7",
"license": "MIT",
"dependencies": {
"lib0": "^0.2.74"

View File

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

View File

@@ -108,7 +108,7 @@ export class ContentType {
while (item !== null) {
if (!item.deleted) {
item.delete(transaction)
} else {
} else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) {
// This will be gc'd later and we want to merge it if possible
// We try to merge all deleted items after each transaction,
// but we have no knowledge about that this needs to be merged
@@ -120,7 +120,7 @@ export class ContentType {
this.type._map.forEach(item => {
if (!item.deleted) {
item.delete(transaction)
} else {
} else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) {
// same as above
transaction._mergeStructs.push(item)
}

View File

@@ -166,18 +166,29 @@ export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
* @return {number} # of merged structs
*/
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1]
const right = structs[pos]
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1)
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
/** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
const tryToMergeWithLefts = (structs, pos) => {
let right = structs[pos]
let left = structs[pos - 1]
let i = pos
for (; i > 0; right = left, left = structs[--i - 1]) {
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
/** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left))
}
continue
}
}
break
}
const merged = pos - i
if (merged) {
// remove all merged structs from the array
structs.splice(pos + 1 - merged, merged)
}
return merged
}
/**
@@ -224,9 +235,9 @@ const tryMergeDeleteSet = (ds, store) => {
for (
let si = mostRightIndexToCheck, struct = structs[si];
si > 0 && struct.id.clock >= deleteItem.clock;
struct = structs[--si]
struct = structs[si]
) {
tryToMergeWithLeft(structs, si)
si -= 1 + tryToMergeWithLefts(structs, si)
}
}
})
@@ -318,23 +329,25 @@ const cleanupTransactions = (transactionCleanups, i) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
// we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i)
for (let i = structs.length - 1; i >= firstChangePos;) {
i -= 1 + tryToMergeWithLefts(structs, i)
}
}
})
// try to merge mergeStructs
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
// but at the moment DS does not handle duplicates
for (let i = 0; i < mergeStructs.length; i++) {
for (let i = mergeStructs.length - 1; i >= 0; i--) {
const { client, clock } = mergeStructs[i].id
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
if (tryToMergeWithLefts(structs, replacedStructPos + 1) > 1) {
continue // no need to perform next check, both are already merged
}
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
tryToMergeWithLefts(structs, replacedStructPos)
}
}
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {

View File

@@ -6,6 +6,9 @@ import {
import * as set from 'lib0/set'
import * as array from 'lib0/array'
import * as error from 'lib0/error'
const errorComputeChanges = 'You must not compute changes after the event-handler fired.'
/**
* @template {AbstractType<any>} T
@@ -84,6 +87,9 @@ export class YEvent {
*/
get keys () {
if (this._keys === null) {
if (this.transaction.doc._transactionCleanups.length === 0) {
throw error.create(errorComputeChanges)
}
const keys = new Map()
const target = this.target
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
@@ -167,6 +173,9 @@ export class YEvent {
get changes () {
let changes = this._changes
if (changes === null) {
if (this.transaction.doc._transactionCleanups.length === 0) {
throw error.create(errorComputeChanges)
}
const target = this.target
const added = set.create()
const deleted = set.create()

View File

@@ -8,6 +8,29 @@ import * as Y from '../src/index.js'
import * as t from 'lib0/testing'
import * as prng from 'lib0/prng'
/**
* Computing event changes after transaction should result in an error. See yjs#539
*
* @param {t.TestCase} _tc
*/
export const testMapEventError = _tc => {
const doc = new Y.Doc()
const ymap = doc.getMap()
/**
* @type {any}
*/
let event = null
ymap.observe((e) => {
event = e
})
t.fails(() => {
t.info(event.keys)
})
t.fails(() => {
t.info(event.keys)
})
}
/**
* @param {t.TestCase} tc
*/