implement PermanentUserData storage prototype

This commit is contained in:
Kevin Jahns 2019-09-17 18:53:59 +02:00
parent 1d297601e8
commit d1f5ff0f59
16 changed files with 215 additions and 68 deletions

6
package-lock.json generated
View File

@ -2596,9 +2596,9 @@
} }
}, },
"lib0": { "lib0": {
"version": "0.1.0", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.1.0.tgz", "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.1.1.tgz",
"integrity": "sha512-pkpnv2IJEOb6iwpcJ6BVQu9GkZ9VINKeQ/0BcArHpozqaGQYWe+ychf2p9wHKToHUnivPoGZZ7rFqrxNXjqFBg==" "integrity": "sha512-ghjoI4xL/xzVR1fRLYEOnJjYMguoI2dnDUf5HYOpTfD6R5GPKLml6xNKl4ZfBVmczkIOQPNthhukp6nlgbmDLw=="
}, },
"linkify-it": { "linkify-it": {
"version": "2.2.0", "version": "2.2.0",

View File

@ -51,7 +51,7 @@
}, },
"homepage": "https://yjs.dev", "homepage": "https://yjs.dev",
"dependencies": { "dependencies": {
"lib0": "^0.1.0" "lib0": "^0.1.1"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^3.6.1", "concurrently": "^3.6.1",

View File

@ -53,5 +53,6 @@ export {
decodeSnapshot, decodeSnapshot,
encodeSnapshot, encodeSnapshot,
isDeleted, isDeleted,
equalSnapshots equalSnapshots,
PermanentUserData // @TODO experimental
} from './internals.js' } from './internals.js'

View File

@ -1,13 +1,16 @@
export * from './utils/DeleteSet.js' export * from './utils/DeleteSet.js'
export * from './utils/Doc.js'
export * from './utils/encoding.js'
export * from './utils/EventHandler.js' export * from './utils/EventHandler.js'
export * from './utils/ID.js' export * from './utils/ID.js'
export * from './utils/isParentOf.js' export * from './utils/isParentOf.js'
export * from './utils/PermanentUserData.js'
export * from './utils/RelativePosition.js' export * from './utils/RelativePosition.js'
export * from './utils/Snapshot.js' export * from './utils/Snapshot.js'
export * from './utils/StructStore.js' export * from './utils/StructStore.js'
export * from './utils/Transaction.js' export * from './utils/Transaction.js'
export * from './utils/UndoManager.js' export * from './utils/UndoManager.js'
export * from './utils/Doc.js'
export * from './utils/YEvent.js' export * from './utils/YEvent.js'
export * from './types/AbstractType.js' export * from './types/AbstractType.js'
@ -31,5 +34,3 @@ export * from './structs/ContentAny.js'
export * from './structs/ContentString.js' export * from './structs/ContentString.js'
export * from './structs/ContentType.js' export * from './structs/ContentType.js'
export * from './structs/Item.js' export * from './structs/Item.js'
export * from './utils/encoding.js'

View File

@ -135,7 +135,7 @@ export const splitItem = (transaction, leftItem, diff) => {
*/ */
export const redoItem = (transaction, item, redoitems) => { export const redoItem = (transaction, item, redoitems) => {
if (item.redone !== null) { if (item.redone !== null) {
return getItemCleanStart(transaction, transaction.doc.store, item.redone) return getItemCleanStart(transaction, item.redone)
} }
let parentItem = item.parent._item let parentItem = item.parent._item
/** /**
@ -175,7 +175,7 @@ export const redoItem = (transaction, item, redoitems) => {
} }
if (parentItem !== null && parentItem.redone !== null) { if (parentItem !== null && parentItem.redone !== null) {
while (parentItem.redone !== null) { while (parentItem.redone !== null) {
parentItem = getItemCleanStart(transaction, transaction.doc.store, parentItem.redone) parentItem = getItemCleanStart(transaction, parentItem.redone)
} }
// find next cloned_redo items // find next cloned_redo items
while (left !== null) { while (left !== null) {
@ -185,7 +185,7 @@ export const redoItem = (transaction, item, redoitems) => {
let leftTrace = left let leftTrace = left
// trace redone until parent matches // trace redone until parent matches
while (leftTrace !== null && leftTrace.parent._item !== parentItem) { while (leftTrace !== null && leftTrace.parent._item !== parentItem) {
leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, transaction.doc.store, leftTrace.redone) leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
} }
if (leftTrace !== null && leftTrace.parent._item === parentItem) { if (leftTrace !== null && leftTrace.parent._item === parentItem) {
left = leftTrace left = leftTrace
@ -200,7 +200,7 @@ export const redoItem = (transaction, item, redoitems) => {
let rightTrace = right let rightTrace = right
// trace redone until parent matches // trace redone until parent matches
while (rightTrace !== null && rightTrace.parent._item !== parentItem) { while (rightTrace !== null && rightTrace.parent._item !== parentItem) {
rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, transaction.doc.store, rightTrace.redone) rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
} }
if (rightTrace !== null && rightTrace.parent._item === parentItem) { if (rightTrace !== null && rightTrace.parent._item === parentItem) {
right = rightTrace right = rightTrace
@ -726,7 +726,7 @@ export class ItemRef extends AbstractStructRef {
} }
const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left) const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
const right = this.right === null ? null : getItemCleanStart(transaction, store, this.right) const right = this.right === null ? null : getItemCleanStart(transaction, this.right)
let parent = null let parent = null
let parentSub = this.parentSub let parentSub = this.parentSub
if (this.parent !== null) { if (this.parent !== null) {

View File

@ -428,7 +428,7 @@ export const typeListInsertGenerics = (transaction, parent, index, content) => {
if (index <= n.length) { if (index <= n.length) {
if (index < n.length) { if (index < n.length) {
// insert in-between // insert in-between
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + index)) getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
} }
break break
} }
@ -454,7 +454,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
for (; n !== null && index > 0; n = n.right) { for (; n !== null && index > 0; n = n.right) {
if (!n.deleted && n.countable) { if (!n.deleted && n.countable) {
if (index < n.length) { if (index < n.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + index)) getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
} }
index -= n.length index -= n.length
} }
@ -463,7 +463,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
while (length > 0 && n !== null) { while (length > 0 && n !== null) {
if (!n.deleted) { if (!n.deleted) {
if (length < n.length) { if (length < n.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + length)) getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length))
} }
n.delete(transaction) n.delete(transaction)
length -= n.length length -= n.length

View File

@ -17,7 +17,7 @@ import {
ContentFormat, ContentFormat,
ContentString, ContentString,
splitSnapshotAffectedStructs, splitSnapshotAffectedStructs,
Doc, Item, Snapshot, StructStore, Transaction // eslint-disable-line ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line import * as decoding from 'lib0/decoding.js' // eslint-disable-line
@ -68,7 +68,6 @@ export class ItemInsertionResult extends ItemListPosition {
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {Item|null} left * @param {Item|null} left
* @param {Item|null} right * @param {Item|null} right
@ -78,7 +77,7 @@ export class ItemInsertionResult extends ItemListPosition {
* @private * @private
* @function * @function
*/ */
const findNextPosition = (transaction, store, currentAttributes, left, right, count) => { const findNextPosition = (transaction, currentAttributes, left, right, count) => {
while (right !== null && count > 0) { while (right !== null && count > 0) {
switch (right.content.constructor) { switch (right.content.constructor) {
case ContentEmbed: case ContentEmbed:
@ -86,7 +85,7 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
if (!right.deleted) { if (!right.deleted) {
if (count < right.length) { if (count < right.length) {
// split right // split right
getItemCleanStart(transaction, store, createID(right.id.client, right.id.clock + count)) getItemCleanStart(transaction, createID(right.id.client, right.id.clock + count))
} }
count -= right.length count -= right.length
} }
@ -105,7 +104,6 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {number} index * @param {number} index
* @return {ItemTextListPosition} * @return {ItemTextListPosition}
@ -113,11 +111,11 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
* @private * @private
* @function * @function
*/ */
const findPosition = (transaction, store, parent, index) => { const findPosition = (transaction, parent, index) => {
let currentAttributes = new Map() let currentAttributes = new Map()
let left = null let left = null
let right = parent._start let right = parent._start
return findNextPosition(transaction, store, currentAttributes, left, right, index) return findNextPosition(transaction, currentAttributes, left, right, index)
} }
/** /**
@ -299,7 +297,7 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
case ContentEmbed: case ContentEmbed:
case ContentString: case ContentString:
if (length < right.length) { if (length < right.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length)) getItemCleanStart(transaction, createID(right.id.client, right.id.clock + length))
} }
length -= right.length length -= right.length
break break
@ -343,7 +341,7 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
case ContentEmbed: case ContentEmbed:
case ContentString: case ContentString:
if (length < right.length) { if (length < right.length) {
getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length)) getItemCleanStart(transaction, createID(right.id.client, right.id.clock + length))
} }
length -= right.length length -= right.length
right.delete(transaction) right.delete(transaction)
@ -714,11 +712,12 @@ export class YText extends AbstractType {
* *
* @param {Snapshot} [snapshot] * @param {Snapshot} [snapshot]
* @param {Snapshot} [prevSnapshot] * @param {Snapshot} [prevSnapshot]
* @param {function('removed' | 'added', ID):any} [computeYChange]
* @return {any} The Delta representation of this type. * @return {any} The Delta representation of this type.
* *
* @public * @public
*/ */
toDelta (snapshot, prevSnapshot) { toDelta (snapshot, prevSnapshot, computeYChange) {
/** /**
* @type{Array<any>} * @type{Array<any>}
*/ */
@ -767,12 +766,12 @@ export class YText extends AbstractType {
if (snapshot !== undefined && !isVisible(n, snapshot)) { if (snapshot !== undefined && !isVisible(n, snapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') { if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
packStr() packStr()
currentAttributes.set('ychange', { user: n.id.client, state: 'removed' }) currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
} }
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) { } else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') { if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') {
packStr() packStr()
currentAttributes.set('ychange', { user: n.id.client, state: 'added' }) currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
} }
} else if (cur !== undefined) { } else if (cur !== undefined) {
packStr() packStr()
@ -818,7 +817,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
if (!attributes) { if (!attributes) {
attributes = {} attributes = {}
currentAttributes.forEach((v, k) => { attributes[k] = v }) currentAttributes.forEach((v, k) => { attributes[k] = v })
@ -847,7 +846,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
insertText(transaction, this, left, right, currentAttributes, embed, attributes) insertText(transaction, this, left, right, currentAttributes, embed, attributes)
}) })
} else { } else {
@ -870,7 +869,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, this, index)
deleteText(transaction, left, right, currentAttributes, length) deleteText(transaction, left, right, currentAttributes, length)
}) })
} else { } else {
@ -892,7 +891,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
let { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) let { left, right, currentAttributes } = findPosition(transaction, this, index)
if (right === null) { if (right === null) {
return return
} }

View File

@ -8,6 +8,7 @@ import {
Item, GC, StructStore, Transaction, ID // eslint-disable-line Item, GC, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as array from 'lib0/array.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@ -52,14 +53,13 @@ export class DeleteSet {
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {DeleteSet} ds * @param {DeleteSet} ds
* @param {StructStore} store
* @param {function(GC|Item):void} f * @param {function(GC|Item):void} f
* *
* @function * @function
*/ */
export const iterateDeletedStructs = (transaction, ds, store, f) => export const iterateDeletedStructs = (transaction, ds, f) =>
ds.clients.forEach((deletes, clientid) => { ds.clients.forEach((deletes, clientid) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(clientid)) const structs = /** @type {Array<GC|Item>} */ (transaction.doc.store.clients.get(clientid))
for (let i = 0; i < deletes.length; i++) { for (let i = 0; i < deletes.length; i++) {
const del = deletes[i] const del = deletes[i]
iterateStructs(transaction, structs, del.clock, del.len, f) iterateStructs(transaction, structs, del.clock, del.len, f)
@ -137,22 +137,27 @@ export const sortAndMergeDeleteSet = ds => {
} }
/** /**
* @param {DeleteSet} ds1 * @param {Array<DeleteSet>} dss
* @param {DeleteSet} ds2
* @return {DeleteSet} A fresh DeleteSet * @return {DeleteSet} A fresh DeleteSet
*/ */
export const mergeDeleteSets = (ds1, ds2) => { export const mergeDeleteSets = dss => {
const merged = new DeleteSet() const merged = new DeleteSet()
// Write all keys from ds1 to merged. If ds2 has the same key, combine the sets. for (let dssI = 0; dssI < dss.length; dssI++) {
ds1.clients.forEach((dels1, client) => dss[dssI].clients.forEach((delsLeft, client) => {
merged.clients.set(client, dels1.concat(ds2.clients.get(client) || []))
)
// Write all missing keys from ds2 to merged.
ds2.clients.forEach((dels2, client) => {
if (!merged.clients.has(client)) { if (!merged.clients.has(client)) {
merged.clients.set(client, dels2) // Write all missing keys from current ds and all following.
// If merged already contains `client` current ds has already been added.
/**
* @type {Array<DeleteItem>}
*/
const dels = delsLeft.slice()
for (let i = dssI + 1; i < dss.length; i++) {
array.appendTo(dels, dss[i].clients.get(client) || [])
}
merged.clients.set(client, dels)
} }
}) })
}
sortAndMergeDeleteSet(merged) sortAndMergeDeleteSet(merged)
return merged return merged
} }

View File

@ -0,0 +1,134 @@
import {
YArray,
YMap,
readDeleteSet,
writeDeleteSet,
createDeleteSet,
ID, DeleteSet, YArrayEvent, Transaction, Doc // eslint-disable-line
} from '../internals.js'
import * as decoding from 'lib0/decoding.js'
import * as encoding from 'lib0/encoding.js'
import { mergeDeleteSets, isDeleted } from './DeleteSet.js'
export class PermanentUserData {
/**
* @param {Doc} doc
* @param {string} key
*/
constructor (doc, key = 'users') {
const users = doc.getMap(key)
/**
* @type {Map<string,DeleteSet>}
*/
const dss = new Map()
this.yusers = users
this.doc = doc
/**
* Maps from clientid to userDescription
*
* @type {Map<number,string>}
*/
this.clients = new Map()
this.dss = dss
/**
* @param {YMap<any>} user
* @param {string} userDescription
*/
const initUser = (user, userDescription) => {
/**
* @type {YArray<Uint8Array>}
*/
const ds = user.get('ds')
const ids = user.get('ids')
const addClientId = /** @param {number} clientid */ clientid => this.clients.set(clientid, userDescription)
ds.observe(/** @param {YArrayEvent<any>} event */ event => {
event.changes.added.forEach(item => {
item.content.getContent().forEach(encodedDs => {
if (encodedDs instanceof Uint8Array) {
this.dss.set(userDescription, mergeDeleteSets([this.dss.get(userDescription) || createDeleteSet(), readDeleteSet(decoding.createDecoder(encodedDs))]))
}
})
})
})
this.dss.set(userDescription, mergeDeleteSets(ds.map(encodedDs => readDeleteSet(decoding.createDecoder(encodedDs)))))
ids.observe(/** @param {YArrayEvent<any>} event */ event =>
event.changes.added.forEach(item => item.content.getContent().forEach(addClientId))
)
ids.forEach(addClientId)
}
// observe users
users.observe(event => {
event.keysChanged.forEach(userDescription =>
initUser(users.get(userDescription), userDescription)
)
})
// add intial data
users.forEach(initUser)
}
/**
* @param {Doc} doc
* @param {number} clientid
* @param {string} userDescription
*/
setUserMapping (doc, clientid, userDescription) {
const users = this.yusers
let user = users.get(userDescription)
if (!user) {
user = new YMap()
user.set('ids', new YArray())
user.set('ds', new YArray())
users.set(userDescription, user)
}
user.get('ids').push([clientid])
users.observe(event => {
const userOverwrite = users.get(userDescription)
if (userOverwrite !== user) {
// user was overwritten, port all data over to the next user object
// @todo Experiment with Y.Sets here
user = userOverwrite
// @todo iterate over old type
this.clients.forEach((_userDescription, clientid) => {
if (userDescription === _userDescription) {
user.get('ids').push([clientid])
}
})
const encoder = encoding.createEncoder()
const ds = this.dss.get(userDescription)
if (ds) {
writeDeleteSet(encoder, ds)
user.get('ds').push([encoding.toUint8Array(encoder)])
}
}
})
doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
const yds = user.get('ds')
const ds = transaction.deleteSet
if (transaction.local && ds.clients.size > 0) {
const encoder = encoding.createEncoder()
writeDeleteSet(encoder, ds)
yds.push([encoding.toUint8Array(encoder)])
}
})
}
/**
* @param {number} clientid
* @return {any}
*/
getUserByClientId (clientid) {
return this.clients.get(clientid) || null
}
/**
* @param {ID} id
* @return {string | null}
*/
getUserByDeletedId (id) {
for (const [userDescription, ds] of this.dss) {
if (isDeleted(ds, id)) {
return userDescription
}
}
return null
}
}

View File

@ -131,10 +131,10 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
if (!meta.has(snapshot)) { if (!meta.has(snapshot)) {
snapshot.sv.forEach((clock, client) => { snapshot.sv.forEach((clock, client) => {
if (clock < getState(store, client)) { if (clock < getState(store, client)) {
getItemCleanStart(transaction, store, createID(client, clock)) getItemCleanStart(transaction, createID(client, clock))
} }
}) })
iterateDeletedStructs(transaction, snapshot.ds, store, item => {}) iterateDeletedStructs(transaction, snapshot.ds, item => {})
meta.add(snapshot) meta.add(snapshot)
} }
} }

View File

@ -197,16 +197,15 @@ export const findIndexCleanStart = (transaction, structs, clock) => {
* Expects that id is actually in store. This function throws or is an infinite loop otherwise. * Expects that id is actually in store. This function throws or is an infinite loop otherwise.
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store
* @param {ID} id * @param {ID} id
* @return {Item} * @return {Item}
* *
* @private * @private
* @function * @function
*/ */
export const getItemCleanStart = (transaction, store, id) => { export const getItemCleanStart = (transaction, id) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(id.client)) const structs = /** @type {Array<Item>} */ (transaction.doc.store.clients.get(id.client))
return /** @type {Item} */ (structs[findIndexCleanStart(transaction, structs, id.clock)]) return structs[findIndexCleanStart(transaction, structs, id.clock)]
} }
/** /**

View File

@ -46,8 +46,9 @@ export class Transaction {
/** /**
* @param {Doc} doc * @param {Doc} doc
* @param {any} origin * @param {any} origin
* @param {boolean} local
*/ */
constructor (doc, origin) { constructor (doc, origin, local) {
/** /**
* The Yjs instance. * The Yjs instance.
* @type {Doc} * @type {Doc}
@ -95,6 +96,11 @@ export class Transaction {
* @type {Map<any,any>} * @type {Map<any,any>}
*/ */
this.meta = new Map() this.meta = new Map()
/**
* Whether this change originates from this doc.
* @type {boolean}
*/
this.local = local
} }
} }
@ -143,17 +149,17 @@ export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
* *
* @param {Doc} doc * @param {Doc} doc
* @param {function(Transaction):void} f * @param {function(Transaction):void} f
* @param {any} [origin] * @param {any} [origin=true]
* *
* @private * @private
* @function * @function
*/ */
export const transact = (doc, f, origin = null) => { export const transact = (doc, f, origin = null, local = true) => {
const transactionCleanups = doc._transactionCleanups const transactionCleanups = doc._transactionCleanups
let initialCall = false let initialCall = false
if (doc._transaction === null) { if (doc._transaction === null) {
initialCall = true initialCall = true
doc._transaction = new Transaction(doc, origin) doc._transaction = new Transaction(doc, origin, local)
transactionCleanups.push(doc._transaction) transactionCleanups.push(doc._transaction)
doc.emit('beforeTransaction', [doc._transaction, doc]) doc.emit('beforeTransaction', [doc._transaction, doc])
} }

View File

@ -52,7 +52,7 @@ const popStackItem = (undoManager, stack, eventType) => {
const stackItem = /** @type {StackItem} */ (stack.pop()) const stackItem = /** @type {StackItem} */ (stack.pop())
const itemsToRedo = new Set() const itemsToRedo = new Set()
let performedChange = false let performedChange = false
iterateDeletedStructs(transaction, stackItem.ds, store, struct => { iterateDeletedStructs(transaction, stackItem.ds, struct => {
if (struct instanceof Item && scope.some(type => isParentOf(type, struct))) { if (struct instanceof Item && scope.some(type => isParentOf(type, struct))) {
itemsToRedo.add(struct) itemsToRedo.add(struct)
} }
@ -70,10 +70,10 @@ const popStackItem = (undoManager, stack, eventType) => {
if (struct.redone !== null) { if (struct.redone !== null) {
let { item, diff } = followRedone(store, struct.id) let { item, diff } = followRedone(store, struct.id)
if (diff > 0) { if (diff > 0) {
item = getItemCleanStart(transaction, store, createID(item.id.client, item.id.clock + diff)) item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
} }
if (item.length > stackItem.len) { if (item.length > stackItem.len) {
getItemCleanStart(transaction, store, createID(item.id.client, item.id.clock + stackItem.len)) getItemCleanStart(transaction, createID(item.id.client, item.id.clock + stackItem.len))
} }
struct = item struct = item
} }
@ -168,7 +168,7 @@ export class UndoManager extends Observable {
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) { if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
// append change to last stack op // append change to last stack op
const lastOp = stack[stack.length - 1] const lastOp = stack[stack.length - 1]
lastOp.ds = mergeDeleteSets(lastOp.ds, transaction.deleteSet) lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet])
lastOp.len = afterState - lastOp.start lastOp.len = afterState - lastOp.start
} else { } else {
// create a new stack op // create a new stack op
@ -178,7 +178,7 @@ export class UndoManager extends Observable {
this.lastChange = now this.lastChange = now
} }
// make sure that deleted structs are not gc'd // make sure that deleted structs are not gc'd
iterateDeletedStructs(transaction, transaction.deleteSet, transaction.doc.store, /** @param {Item|GC} item */ item => { iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) { if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
keepItem(item) keepItem(item)
} }

View File

@ -26,6 +26,7 @@ import {
readAndApplyDeleteSet, readAndApplyDeleteSet,
writeDeleteSet, writeDeleteSet,
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
transact,
Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@ -299,10 +300,10 @@ export const readStructs = (decoder, transaction, store) => {
* @function * @function
*/ */
export const readUpdate = (decoder, ydoc, transactionOrigin) => export const readUpdate = (decoder, ydoc, transactionOrigin) =>
ydoc.transact(transaction => { transact(ydoc, transaction => {
readStructs(decoder, transaction, ydoc.store) readStructs(decoder, transaction, ydoc.store)
readAndApplyDeleteSet(decoder, transaction, ydoc.store) readAndApplyDeleteSet(decoder, transaction, ydoc.store)
}, transactionOrigin) }, transactionOrigin, false)
/** /**
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`. * Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.

View File

@ -3,6 +3,7 @@ import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing.js'
import * as prng from 'lib0/prng.js' import * as prng from 'lib0/prng.js'
import * as math from 'lib0/math.js'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
@ -362,12 +363,12 @@ const arrayTransactions = [
var length = yarray.length var length = yarray.length
if (length > 0) { if (length > 0) {
var somePos = prng.int31(gen, 0, length - 1) var somePos = prng.int31(gen, 0, length - 1)
var delLength = prng.int31(gen, 1, Math.min(2, length - somePos)) var delLength = prng.int31(gen, 1, math.min(2, length - somePos))
if (prng.bool(gen)) { if (prng.bool(gen)) {
var type = yarray.get(somePos) var type = yarray.get(somePos)
if (type.length > 0) { if (type.length > 0) {
somePos = prng.int31(gen, 0, type.length - 1) somePos = prng.int31(gen, 0, type.length - 1)
delLength = prng.int31(gen, 0, Math.min(2, type.length - somePos)) delLength = prng.int31(gen, 0, math.min(2, type.length - somePos))
type.delete(somePos, delLength) type.delete(somePos, delLength)
} }
} else { } else {

View File

@ -127,7 +127,7 @@ export const testSnapshot = tc => {
delete v.attributes.ychange.user delete v.attributes.ychange.user
} }
}) })
t.compare(state2Diff, [{insert: 'a'}, {insert: 'x', attributes: {ychange: { state: 'added' }}}, {insert: 'b', attributes: {ychange: { state: 'removed' }}}, { insert: 'cd' }]) t.compare(state2Diff, [{insert: 'a'}, {insert: 'x', attributes: {ychange: { type: 'added' }}}, {insert: 'b', attributes: {ychange: { type: 'removed' }}}, { insert: 'cd' }])
} }
/** /**