Y.Text snapshot support (toDelta)

This commit is contained in:
Kevin Jahns 2019-08-31 22:42:18 +02:00
parent e78d84ee59
commit 8bcff6138c
5 changed files with 153 additions and 38 deletions

View File

@ -37,6 +37,8 @@ export {
compareIDs,
getState,
Snapshot,
createSnapshot,
createSnapshotFromDoc,
findRootTypeKey,
typeListToArraySnapshot,
typeMapGetSnapshot,

View File

@ -16,6 +16,7 @@ import {
ContentEmbed,
ContentFormat,
ContentString,
splitSnapshotAffectedStructs,
Doc, Item, Snapshot, StructStore, Transaction // eslint-disable-line
} from '../internals.js'
@ -723,6 +724,7 @@ export class YText extends AbstractType {
*/
const ops = []
const currentAttributes = new Map()
const doc = /** @type {Doc} */ (this.doc)
let str = ''
let n = this._start
function packStr () {
@ -748,42 +750,54 @@ export class YText extends AbstractType {
str = ''
}
}
while (n !== null) {
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
switch (n.content.constructor) {
case ContentString:
const cur = currentAttributes.get('ychange')
if (snapshot !== undefined && !isVisible(n, snapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
packStr()
currentAttributes.set('ychange', { user: n.id.client, state: 'removed' })
}
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') {
packStr()
currentAttributes.set('ychange', { user: n.id.client, state: 'added' })
}
} else if (cur !== undefined) {
packStr()
currentAttributes.delete('ychange')
}
str += /** @type {ContentString} */ (n.content).str
break
case ContentEmbed:
packStr()
ops.push({
insert: /** @type {ContentEmbed} */ (n.content).embed
})
break
case ContentFormat:
packStr()
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content))
break
}
// snapshots are merged again after the transaction, so we need to keep the
// transalive until we are done
transact(doc, transaction => {
if (snapshot) {
splitSnapshotAffectedStructs(transaction, snapshot)
}
n = n.right
}
packStr()
if (prevSnapshot) {
splitSnapshotAffectedStructs(transaction, prevSnapshot)
}
while (n !== null) {
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
switch (n.content.constructor) {
case ContentString:
const cur = currentAttributes.get('ychange')
if (snapshot !== undefined && !isVisible(n, snapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
packStr()
currentAttributes.set('ychange', { user: n.id.client, state: 'removed' })
}
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') {
packStr()
currentAttributes.set('ychange', { user: n.id.client, state: 'added' })
}
} else if (cur !== undefined) {
packStr()
currentAttributes.delete('ychange')
}
str += /** @type {ContentString} */ (n.content).str
break
case ContentEmbed:
packStr()
ops.push({
insert: /** @type {ContentEmbed} */ (n.content).embed
})
break
case ContentFormat:
if (isVisible(n, snapshot)) {
packStr()
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content))
}
break
}
}
n = n.right
}
packStr()
}, splitSnapshotAffectedStructs)
return ops
}

View File

@ -1,9 +1,17 @@
import {
isDeleted,
DeleteSet, Item // eslint-disable-line
createDeleteSetFromStructStore,
getStateVector,
getItemCleanStart,
createID,
iterateDeletedStructs,
Transaction, Doc, DeleteSet, Item // eslint-disable-line
} from '../internals.js'
import * as map from 'lib0/map.js'
import * as set from 'lib0/set.js'
export class Snapshot {
/**
* @param {DeleteSet} ds
@ -27,9 +35,16 @@ export class Snapshot {
/**
* @param {DeleteSet} ds
* @param {Map<number,number>} sm
* @return {Snapshot}
*/
export const createSnapshot = (ds, sm) => new Snapshot(ds, sm)
/**
* @param {Doc} doc
* @return {Snapshot}
*/
export const createSnapshotFromDoc = doc => createSnapshot(createDeleteSetFromStructStore(doc.store), getStateVector(doc.store))
/**
* @param {Item} item
* @param {Snapshot|undefined} snapshot
@ -40,3 +55,20 @@ export const createSnapshot = (ds, sm) => new Snapshot(ds, sm)
export const isVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : (
snapshot.sm.has(item.id.client) && (snapshot.sm.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
)
/**
* @param {Transaction} transaction
* @param {Snapshot} snapshot
*/
export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
const meta = map.setIfUndefined(transaction.meta, splitSnapshotAffectedStructs, set.create)
const store = transaction.doc.store
// check if we already split for this snapshot
if (!meta.has(snapshot)) {
snapshot.sm.forEach((clock, client) => {
getItemCleanStart(transaction, store, createID(client, clock))
})
iterateDeletedStructs(transaction, snapshot.ds, store, item => {})
meta.add(snapshot)
}
}

View File

@ -90,6 +90,11 @@ export class Transaction {
* @type {any}
*/
this.origin = origin
/**
* Stores meta information on the transaction
* @type {Map<any,any>}
*/
this.meta = new Map()
}
}

View File

@ -1,6 +1,6 @@
import { init, compare } from './testHelper.js'
import * as Y from './testHelper.js'
import * as t from 'lib0/testing.js'
const { init, compare } = Y
/**
* @param {t.TestCase} tc
@ -87,3 +87,65 @@ export const testGetDeltaWithEmbeds = tc => {
insert: {linebreak: 's'}
}])
}
/**
* @param {t.TestCase} tc
*/
export const testSnapshot = tc => {
const { text0 } = init(tc, { users: 1 })
const doc0 = /** @type {Y.Doc} */ (text0.doc)
doc0.gc = false
text0.applyDelta([{
insert: 'abcd'
}])
const snapshot1 = Y.createSnapshotFromDoc(doc0)
text0.applyDelta([{
retain: 1
}, {
insert: 'x'
}, {
delete: 1
}])
const snapshot2 = Y.createSnapshotFromDoc(doc0)
text0.applyDelta([{
retain: 2
}, {
delete: 3
}, {
insert: 'x'
}, {
delete: 1
}])
const state1 = text0.toDelta(snapshot1)
t.compare(state1, [{ insert: 'abcd' }])
const state2 = text0.toDelta(snapshot2)
t.compare(state2, [{ insert: 'axcd' }])
const state2Diff = text0.toDelta(snapshot2, snapshot1)
// @ts-ignore Remove userid info
state2Diff.forEach(v => {
if (v.attributes && v.attributes.ychange) {
delete v.attributes.ychange.user
}
})
t.compare(state2Diff, [{insert: 'a'}, {insert: 'x', attributes: {ychange: { state: 'added' }}}, {insert: 'b', attributes: {ychange: { state: 'removed' }}}, { insert: 'cd' }])
}
/**
* @param {t.TestCase} tc
*/
export const testSnapshotDeleteAfter = tc => {
const { text0 } = init(tc, { users: 1 })
const doc0 = /** @type {Y.Doc} */ (text0.doc)
doc0.gc = false
text0.applyDelta([{
insert: 'abcd'
}])
const snapshot1 = Y.createSnapshotFromDoc(doc0)
text0.applyDelta([{
retain: 4
}, {
insert: 'e'
}])
const state1 = text0.toDelta(snapshot1)
t.compare(state1, [{ insert: 'abcd' }])
}