implement snapshot & event.changes

This commit is contained in:
Kevin Jahns 2019-09-03 16:33:29 +02:00
parent 8bcff6138c
commit bb1c0b809f
9 changed files with 319 additions and 29 deletions

View File

@ -38,7 +38,8 @@ export {
getState,
Snapshot,
createSnapshot,
createSnapshotFromDoc,
snapshot,
emptySnapshot,
findRootTypeKey,
typeListToArraySnapshot,
typeMapGetSnapshot,
@ -46,5 +47,9 @@ export {
applyUpdate,
encodeStateAsUpdate,
encodeStateVector,
UndoManager
UndoManager,
decodeSnapshot,
encodeSnapshot,
isDeleted,
equalSnapshots
} from './internals.js'

View File

@ -584,7 +584,7 @@ export const typeMapHas = (parent, key) => {
*/
export const typeMapGetSnapshot = (parent, key, snapshot) => {
let v = parent._map.get(key) || null
while (v !== null && (!snapshot.sm.has(v.id.client) || v.id.clock >= (snapshot.sm.get(v.id.client) || 0))) {
while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) {
v = v.left
}
return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined

View File

@ -169,6 +169,8 @@ export const addToDeleteSet = (ds, id, length) => {
map.setIfUndefined(ds.clients, id.client, () => []).push(new DeleteItem(id.clock, length))
}
export const createDeleteSet = () => new DeleteSet()
/**
* @param {StructStore} ss
* @return {DeleteSet} Merged and sorted DeleteSet
@ -177,7 +179,7 @@ export const addToDeleteSet = (ds, id, length) => {
* @function
*/
export const createDeleteSetFromStructStore = ss => {
const ds = new DeleteSet()
const ds = createDeleteSet()
ss.clients.forEach((structs, client) => {
/**
* @type {Array<DeleteItem>}
@ -224,6 +226,26 @@ export const writeDeleteSet = (encoder, ds) => {
})
}
/**
* @param {decoding.Decoder} decoder
* @return {DeleteSet}
*
* @private
* @function
*/
export const readDeleteSet = decoder => {
const ds = new DeleteSet()
const numClients = decoding.readVarUint(decoder)
for (let i = 0; i < numClients; i++) {
const client = decoding.readVarUint(decoder)
const numberOfDeletes = decoding.readVarUint(decoder)
for (let i = 0; i < numberOfDeletes; i++) {
addToDeleteSet(ds, createID(client, decoding.readVarUint(decoder)), decoding.readVarUint(decoder))
}
}
return ds
}
/**
* @param {decoding.Decoder} decoder
* @param {Transaction} transaction
@ -232,7 +254,7 @@ export const writeDeleteSet = (encoder, ds) => {
* @private
* @function
*/
export const readDeleteSet = (decoder, transaction, store) => {
export const readAndApplyDeleteSet = (decoder, transaction, store) => {
const unappliedDS = new DeleteSet()
const numClients = decoding.readVarUint(decoder)
for (let i = 0; i < numClients; i++) {
@ -279,6 +301,7 @@ export const readDeleteSet = (decoder, transaction, store) => {
}
}
if (unappliedDS.clients.size > 0) {
// TODO: no need for encoding+decoding ds anymore
const unappliedDSEncoder = encoding.createEncoder()
writeDeleteSet(unappliedDSEncoder, unappliedDS)
store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toUint8Array(unappliedDSEncoder)))

View File

@ -6,18 +6,26 @@ import {
getItemCleanStart,
createID,
iterateDeletedStructs,
writeDeleteSet,
writeStateVector,
readDeleteSet,
readStateVector,
createDeleteSet,
getState,
Transaction, Doc, DeleteSet, Item // eslint-disable-line
} from '../internals.js'
import * as map from 'lib0/map.js'
import * as set from 'lib0/set.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
export class Snapshot {
/**
* @param {DeleteSet} ds
* @param {Map<number,number>} sm state map
* @param {Map<number,number>} sv state map
*/
constructor (ds, sm) {
constructor (ds, sv) {
/**
* @type {DeleteSet}
* @private
@ -28,10 +36,64 @@ export class Snapshot {
* @type {Map<number,number>}
* @private
*/
this.sm = sm
this.sv = sv
}
}
/**
* @param {Snapshot} snap1
* @param {Snapshot} snap2
* @return {boolean}
*/
export const equalSnapshots = (snap1, snap2) => {
const ds1 = snap1.ds.clients
const ds2 = snap2.ds.clients
const sv1 = snap1.sv
const sv2 = snap2.sv
if (sv1.size !== sv2.size || ds1.size !== ds2.size) {
return false
}
for (const [key, value] of sv1) {
if (sv2.get(key) !== value) {
return false
}
}
for (const [client, dsitems1] of ds1) {
const dsitems2 = ds2.get(client) || []
if (dsitems1.length !== dsitems2.length) {
return false
}
for (let i = 0; i < dsitems1.length; i++) {
const dsitem1 = dsitems1[i]
const dsitem2 = dsitems2[i]
if (dsitem1.clock !== dsitem2.clock || dsitem1.len !== dsitem2.len) {
return false
}
}
}
return true
}
/**
* @param {Snapshot} snapshot
* @return {Uint8Array}
*/
export const encodeSnapshot = snapshot => {
const encoder = encoding.createEncoder()
writeDeleteSet(encoder, snapshot.ds)
writeStateVector(encoder, snapshot.sv)
return encoding.toUint8Array(encoder)
}
/**
* @param {Uint8Array} buf
* @return {Snapshot}
*/
export const decodeSnapshot = buf => {
const decoder = decoding.createDecoder(buf)
return new Snapshot(readDeleteSet(decoder), readStateVector(decoder))
}
/**
* @param {DeleteSet} ds
* @param {Map<number,number>} sm
@ -39,11 +101,13 @@ export class Snapshot {
*/
export const createSnapshot = (ds, sm) => new Snapshot(ds, sm)
export const emptySnapshot = createSnapshot(createDeleteSet(), new Map())
/**
* @param {Doc} doc
* @return {Snapshot}
*/
export const createSnapshotFromDoc = doc => createSnapshot(createDeleteSetFromStructStore(doc.store), getStateVector(doc.store))
export const snapshot = doc => createSnapshot(createDeleteSetFromStructStore(doc.store), getStateVector(doc.store))
/**
* @param {Item} item
@ -53,7 +117,7 @@ export const createSnapshotFromDoc = doc => createSnapshot(createDeleteSetFromSt
* @function
*/
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)
snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
)
/**
@ -65,8 +129,10 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
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))
snapshot.sv.forEach((clock, client) => {
if (clock < getState(store, client)) {
getItemCleanStart(transaction, store, createID(client, clock))
}
})
iterateDeletedStructs(transaction, snapshot.ds, store, item => {})
meta.add(snapshot)

View File

@ -1,9 +1,12 @@
import {
isDeleted,
AbstractType, Transaction, AbstractStruct // eslint-disable-line
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
} from '../internals.js'
import * as set from 'lib0/set.js'
import * as array from 'lib0/array.js'
/**
* YEvent describes the changes on a YType.
*/
@ -28,6 +31,10 @@ export class YEvent {
* @type {Transaction}
*/
this.transaction = transaction
/**
* @type {Object|null}
*/
this._changes = null
}
/**
@ -65,6 +72,113 @@ export class YEvent {
adds (struct) {
return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0)
}
/**
* @return {{added:Set<Item>,deleted:Set<Item>,delta:Array<{insert:Array<any>}|{delete:number}|{retain:number}>}}
*/
get changes () {
let changes = this._changes
if (changes === null) {
const target = this.target
const added = set.create()
const deleted = set.create()
/**
* @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>}
*/
const delta = []
/**
* @type {Map<string,{ action: 'add' | 'update' | 'delete', oldValue: any}>}
*/
const keys = new Map()
changes = {
added, deleted, delta, keys
}
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
if (changed.has(null)) {
/**
* @type {any}
*/
let lastOp = null
const packOp = () => {
if (lastOp) {
delta.push(lastOp)
}
}
for (let item = target._start; item !== null; item = item.right) {
if (item.deleted) {
if (this.deletes(item)) {
if (lastOp === null || lastOp.delete === undefined) {
packOp()
lastOp = { delete: 0 }
}
lastOp.delete += item.length
deleted.add(item)
} // else nop
} else {
if (this.adds(item)) {
if (lastOp === null || lastOp.insert === undefined) {
packOp()
lastOp = { insert: [] }
}
lastOp.insert = lastOp.insert.concat(item.content.getContent())
added.add(item)
} else {
if (lastOp === null || lastOp.retain === undefined) {
packOp()
lastOp = { retain: 0 }
}
lastOp.retain += item.length
}
}
}
if (lastOp !== null && lastOp.retain === undefined) {
packOp()
}
}
changed.forEach(key => {
if (key !== null) {
const item = /** @type {Item} */ (target._map.get(key))
/**
* @type {'delete' | 'add' | 'update'}
*/
let action
let oldValue
if (this.adds(item)) {
let prev = item.left
while (prev !== null && this.adds(prev)) {
prev = prev.left
}
if (this.deletes(item)) {
if (prev !== null && this.deletes(prev)) {
action = 'delete'
oldValue = array.last(prev.content.getContent())
} else {
return
}
} else {
if (prev !== null && this.deletes(prev)) {
action = 'update'
oldValue = array.last(prev.content.getContent())
} else {
action = 'add'
oldValue = undefined
}
}
} else {
if (this.deletes(item)) {
action = 'delete'
oldValue = array.last(/** @type {Item} */ item.content.getContent())
} else {
return // nop
}
}
keys.set(key, { action, oldValue })
}
})
this._changes = changes
}
return changes
}
}
/**

View File

@ -23,7 +23,7 @@ import {
readID,
getState,
getStateVector,
readDeleteSet,
readAndApplyDeleteSet,
writeDeleteSet,
createDeleteSetFromStructStore,
Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line
@ -230,7 +230,7 @@ export const tryResumePendingDeleteReaders = (transaction, store) => {
const pendingReaders = store.pendingDeleteReaders
store.pendingDeleteReaders = []
for (let i = 0; i < pendingReaders.length; i++) {
readDeleteSet(pendingReaders[i], transaction, store)
readAndApplyDeleteSet(pendingReaders[i], transaction, store)
}
}
@ -301,7 +301,7 @@ export const readStructs = (decoder, transaction, store) => {
export const readUpdate = (decoder, ydoc, transactionOrigin) =>
ydoc.transact(transaction => {
readStructs(decoder, transaction, ydoc.store)
readDeleteSet(decoder, transaction, ydoc.store)
readAndApplyDeleteSet(decoder, transaction, ydoc.store)
}, transactionOrigin)
/**
@ -381,6 +381,22 @@ export const readStateVector = decoder => {
*/
export const decodeStateVector = decodedState => readStateVector(decoding.createDecoder(decodedState))
/**
* Write State Vector to `lib0/encoding.js#Encoder`.
*
* @param {encoding.Encoder} encoder
* @param {Map<number,number>} sv
* @function
*/
export const writeStateVector = (encoder, sv) => {
encoding.writeVarUint(encoder, sv.size)
sv.forEach((clock, client) => {
encoding.writeVarUint(encoder, client)
encoding.writeVarUint(encoder, clock)
})
return encoder
}
/**
* Write State Vector to `lib0/encoding.js#Encoder`.
*
@ -389,16 +405,7 @@ export const decodeStateVector = decodedState => readStateVector(decoding.create
*
* @function
*/
export const writeDocumentStateVector = (encoder, doc) => {
encoding.writeVarUint(encoder, doc.store.clients.size)
doc.store.clients.forEach((structs, client) => {
const struct = structs[structs.length - 1]
const id = struct.id
encoding.writeVarUint(encoder, id.client)
encoding.writeVarUint(encoder, id.clock + struct.length)
})
return encoder
}
export const writeDocumentStateVector = (encoder, doc) => writeStateVector(encoder, getStateVector(doc.store))
/**
* Encode State as Uint8Array.

View File

@ -191,6 +191,33 @@ export const testInsertAndDeleteEventsForTypes = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testChangeEvent = tc => {
const { array0, users } = init(tc, { users: 2 })
/**
* @type {any}
*/
let changes = null
array0.observe(e => {
changes = e.changes
})
const newArr = new Y.Array()
array0.insert(0, [newArr, 4, 'dtrn'])
t.assert(changes !== null && changes.added.size === 2 && changes.deleted.size === 0)
t.compare(changes.delta, [{insert: [newArr, 4, 'dtrn']}])
changes = null
array0.delete(0, 2)
t.assert(changes !== null && changes.added.size === 0 && changes.deleted.size === 2)
t.compare(changes.delta, [{ delete: 2 }])
changes = null
array0.insert(1, [0.1])
t.assert(changes !== null && changes.added.size === 1 && changes.deleted.size === 0)
t.compare(changes.delta, [{ retain: 1 }, { insert: [0.1] }])
compare(users)
}
/**
* @param {t.TestCase} tc
*/

View File

@ -292,6 +292,54 @@ export const testThrowsAddAndUpdateAndDeleteEvents = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testChangeEvent = tc => {
const { map0, users } = init(tc, { users: 2 })
/**
* @type {any}
*/
let changes = null
/**
* @type {any}
*/
let keyChange = null
map0.observe(e => {
changes = e.changes
})
map0.set('a', 1)
keyChange = changes.keys.get('a')
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined)
map0.set('a', 2)
keyChange = changes.keys.get('a')
t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 1)
users[0].transact(() => {
map0.set('a', 3)
map0.set('a', 4)
})
keyChange = changes.keys.get('a')
t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 2)
users[0].transact(() => {
map0.set('b', 1)
map0.set('b', 2)
})
keyChange = changes.keys.get('b')
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined)
users[0].transact(() => {
map0.set('c', 1)
map0.delete('c')
})
t.assert(changes !== null && changes.keys.size === 0)
users[0].transact(() => {
map0.set('d', 1)
map0.set('d', 2)
})
keyChange = changes.keys.get('d')
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined)
compare(users)
}
/**
* @param {t.TestCase} tc
*/

View File

@ -98,7 +98,7 @@ export const testSnapshot = tc => {
text0.applyDelta([{
insert: 'abcd'
}])
const snapshot1 = Y.createSnapshotFromDoc(doc0)
const snapshot1 = Y.snapshot(doc0)
text0.applyDelta([{
retain: 1
}, {
@ -106,7 +106,7 @@ export const testSnapshot = tc => {
}, {
delete: 1
}])
const snapshot2 = Y.createSnapshotFromDoc(doc0)
const snapshot2 = Y.snapshot(doc0)
text0.applyDelta([{
retain: 2
}, {
@ -140,7 +140,7 @@ export const testSnapshotDeleteAfter = tc => {
text0.applyDelta([{
insert: 'abcd'
}])
const snapshot1 = Y.createSnapshotFromDoc(doc0)
const snapshot1 = Y.snapshot(doc0)
text0.applyDelta([{
retain: 4
}, {