Compare commits

..

14 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
Kevin Jahns
8586806932 13.6.6 2023-06-25 19:10:34 +02:00
Kevin Jahns
981340139f skip iterating when there are no formatting items - replaces #547 2023-06-25 12:46:02 +02:00
Kevin Jahns
b792902f17 13.6.5 2023-06-22 17:55:45 +02:00
Kevin Jahns
83b7c6839e Merge pull request #548 from YousefED/fix/equalDeleteSets
fix equalDeleteSets
2023-06-22 17:46:34 +02:00
Kevin Jahns
65c4d40a87 Merge branch 'NilSet-path-cache-invalidation' 2023-06-22 17:48:19 +02:00
Kevin Jahns
942c8a267b remove duplicate Transaction.callAll logic 2023-06-22 17:46:49 +02:00
yousefed
eda085936a keep original imports 2023-06-21 18:29:40 +02:00
yousefed
12be6c006a fix equalDeleteSets 2023-06-21 18:28:53 +02:00
Noel Levy
5d862477cd invalidate cached path when changing currentTarget of event
fixes #544
2023-06-19 11:31:45 -07:00
Kevin Jahns
c398448152 add blocksuite editor by affine 2023-06-16 16:04:30 +02:00
10 changed files with 137 additions and 56 deletions

View File

@@ -91,6 +91,7 @@ are implemented in separate modules.
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
| [BlockSuite](https://github.com/toeverything/blocksuite) | ✔ | (native) | [demo](https://blocksuite-toeverything.vercel.app/?init) |
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
| [immer](https://github.com/immerjs/immer) | | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) |
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |

4
package-lock.json generated
View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import {
AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
YText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error'
@@ -47,28 +47,30 @@ export class ContentFormat {
}
/**
* @param {number} offset
* @param {number} _offset
* @return {ContentFormat}
*/
splice (offset) {
splice (_offset) {
throw error.methodUnimplemented()
}
/**
* @param {ContentFormat} right
* @param {ContentFormat} _right
* @return {boolean}
*/
mergeWith (right) {
mergeWith (_right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Transaction} _transaction
* @param {Item} item
*/
integrate (transaction, item) {
integrate (_transaction, item) {
// @todo searchmarker are currently unsupported for rich text documents
/** @type {AbstractType<any>} */ (item.parent)._searchMarker = null
const p = /** @type {YText} */ (item.parent)
p._searchMarker = null
p._hasFormatting = true
}
/**

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

@@ -505,7 +505,7 @@ export const cleanupYTextAfterTransaction = transaction => {
// cleanup in a new transaction
transact(doc, (t) => {
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
if (item instanceof GC || needFullCleanup.has(/** @type {YText} */ (item.parent))) {
if (item instanceof GC || !(/** @type {YText} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YText} */ (item.parent))) {
return
}
const parent = /** @type {YText} */ (item.parent)
@@ -859,9 +859,14 @@ export class YText extends AbstractType {
*/
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
/**
* @type {Array<ArraySearchMarker>}
* @type {Array<ArraySearchMarker>|null}
*/
this._searchMarker = []
/**
* Whether this YText contains formatting attributes.
* This flag is updated when a formatting item is integrated (see ContentFormat.integrate)
*/
this._hasFormatting = false
}
/**
@@ -911,7 +916,7 @@ export class YText extends AbstractType {
const event = new YTextEvent(this, transaction, parentSubs)
callTypeObservers(this, transaction, event)
// If a remote change happened, we try to cleanup potential formatting duplicates.
if (!transaction.local) {
if (!transaction.local && this._hasFormatting) {
transaction._needFormattingCleanup = true
}
}

View File

@@ -335,7 +335,7 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
*/
export const equalDeleteSets = (ds1, ds2) => {
if (ds1.clients.size !== ds2.clients.size) return false
ds1.clients.forEach((deleteItems1, client) => {
for (const [client, deleteItems1] of ds1.clients.entries()) {
const deleteItems2 = /** @type {Array<import('../internals.js').DeleteItem>} */ (ds2.clients.get(client))
if (deleteItems2 === undefined || deleteItems1.length !== deleteItems2.length) return false
for (let i = 0; i < deleteItems1.length; i++) {
@@ -345,6 +345,6 @@ export const equalDeleteSets = (ds1, ds2) => {
return false
}
}
})
}
return true
}

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)
}
}
})
@@ -275,30 +286,30 @@ const cleanupTransactions = (transactionCleanups, i) => {
)
fs.push(() => {
// deep observe events
transaction.changedParentTypes.forEach((events, type) =>
fs.push(() => {
// We need to think about the possibility that the user transforms the
// Y.Doc in the event.
if (type._item === null || !type._item.deleted) {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.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)
}
})
)
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
transaction.changedParentTypes.forEach((events, type) => {
// We need to think about the possibility that the user transforms the
// Y.Doc in the event.
if (type._dEH.l.length > 0 && (type._item === null || !type._item.deleted)) {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
)
events
.forEach(event => {
event.currentTarget = type
// path is relative to the current target
event._path = null
})
// 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)
}
})
})
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
callAll(fs, [])
if (transaction._needFormattingCleanup) {
cleanupYTextAfterTransaction(transaction)
@@ -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
*/
@@ -337,6 +360,34 @@ export const testObserversUsingObservedeep = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testPathsOfSiblingEvents = tc => {
const { users, map0 } = init(tc, { users: 2 })
/**
* @type {Array<Array<string|number>>}
*/
const pathes = []
let calls = 0
const doc = users[0]
map0.set('map', new Y.Map())
map0.get('map').set('text1', new Y.Text('initial'))
map0.observeDeep(events => {
events.forEach(event => {
pathes.push(event.path)
})
calls++
})
doc.transact(() => {
map0.get('map').get('text1').insert(0, 'post-')
map0.get('map').set('text2', new Y.Text('new'))
})
t.assert(calls === 1)
t.compare(pathes, [['map'], ['map', 'text1']])
compare(users)
}
// TODO: Test events in Y.Map
/**
* @param {Object<string,any>} is