working on snapshotting and version history

This commit is contained in:
Kevin Jahns
2019-01-09 23:54:36 +01:00
parent ec58a99748
commit 77e479c03b
17 changed files with 648 additions and 378 deletions

View File

@@ -5,6 +5,10 @@
import { Tree } from '../lib/Tree.js'
import * as ID from './ID.js'
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
import { deleteItemRange } from '../utils/structManipulation.js'
class DSNode {
constructor (id, len, gc) {
this._id = id
@@ -86,8 +90,168 @@ export class DeleteStore extends Tree {
this.put(newMark)
}
}
// TODO: exchange markDeleted for mark()
markDeleted (id, length) {
this.mark(id, length, false)
}
/**
* Stringifies a message-encoded Delete Set.
*
* @param {decoding.Decoder} decoder
* @return {string}
*/
export const stringifyDeleteStore = (decoder) => {
let str = ''
const dsLength = decoding.readUint32(decoder)
for (let i = 0; i < dsLength; i++) {
str += ' -' + decoding.readVarUint(decoder) + ':\n' // decodes user
const dvLength = decoding.readUint32(decoder)
for (let j = 0; j < dvLength; j++) {
str += `clock: ${decoding.readVarUint(decoder)}, length: ${decoding.readVarUint(decoder)}, gc: ${decoding.readUint8(decoder) === 1}\n`
}
}
return str
}
/**
* Write the DeleteSet of a shared document to an Encoder.
*
* @param {encoding.Encoder} encoder
* @param {DeleteStore} ds
*/
export const writeDeleteStore = (encoder, ds) => {
let currentUser = null
let currentLength
let lastLenPos
let numberOfUsers = 0
const laterDSLenPus = encoding.length(encoder)
encoding.writeUint32(encoder, 0)
ds.iterate(null, null, n => {
const user = n._id.user
const clock = n._id.clock
const len = n.len
const gc = n.gc
if (currentUser !== user) {
numberOfUsers++
// a new user was found
if (currentUser !== null) { // happens on first iteration
encoding.setUint32(encoder, lastLenPos, currentLength)
}
currentUser = user
encoding.writeVarUint(encoder, user)
// pseudo-fill pos
lastLenPos = encoding.length(encoder)
encoding.writeUint32(encoder, 0)
currentLength = 0
}
encoding.writeVarUint(encoder, clock)
encoding.writeVarUint(encoder, len)
encoding.writeUint8(encoder, gc ? 1 : 0)
currentLength++
})
if (currentUser !== null) { // happens on first iteration
encoding.setUint32(encoder, lastLenPos, currentLength)
}
encoding.setUint32(encoder, laterDSLenPus, numberOfUsers)
}
/**
* Read delete store from Decoder and create a fresh DeleteStore
*
* @param {decoding.Decoder} decoder
* @return {DeleteStore}
*/
export const readFreshDeleteStore = decoder => {
const ds = new DeleteStore()
const dsLength = decoding.readUint32(decoder)
for (let i = 0; i < dsLength; i++) {
const user = decoding.readVarUint(decoder)
const dvLength = decoding.readUint32(decoder)
for (let j = 0; j < dvLength; j++) {
const from = decoding.readVarUint(decoder)
const len = decoding.readVarUint(decoder)
const gc = decoding.readUint8(decoder)
ds.put(new DSNode(ID.createID(user, from), len, gc))
}
}
return ds
}
/**
* Read delete set from Decoder and apply it to a shared document.
*
* @param {decoding.Decoder} decoder
* @param {Y} y
*/
export const readDeleteStore = (decoder, y) => {
const dsLength = decoding.readUint32(decoder)
for (let i = 0; i < dsLength; i++) {
const user = decoding.readVarUint(decoder)
const dv = []
const dvLength = decoding.readUint32(decoder)
for (let j = 0; j < dvLength; j++) {
const from = decoding.readVarUint(decoder)
const len = decoding.readVarUint(decoder)
const gc = decoding.readUint8(decoder) === 1
dv.push({from, len, gc})
}
if (dvLength > 0) {
const deletions = []
let pos = 0
let d = dv[pos]
y.ds.iterate(ID.createID(user, 0), ID.createID(user, Number.MAX_VALUE), n => {
// cases:
// 1. d deletes something to the right of n
// => go to next n (break)
// 2. d deletes something to the left of n
// => create deletions
// => reset d accordingly
// *)=> if d doesn't delete anything anymore, go to next d (continue)
// 3. not 2) and d deletes something that also n deletes
// => reset d so that it doesn't contain n's deletion
// *)=> if d does not delete anything anymore, go to next d (continue)
while (d != null) {
var diff = 0 // describe the diff of length in 1) and 2)
if (n._id.clock + n.len <= d.from) {
// 1)
break
} else if (d.from < n._id.clock) {
// 2)
// delete maximum the len of d
// else delete as much as possible
diff = Math.min(n._id.clock - d.from, d.len)
// deleteItemRange(y, user, d.from, diff, true)
deletions.push([user, d.from, diff])
} else {
// 3)
diff = n._id.clock + n.len - d.from // never null (see 1)
if (d.gc && !n.gc) {
// d marks as gc'd but n does not
// then delete either way
// deleteItemRange(y, user, d.from, Math.min(diff, d.len), true)
deletions.push([user, d.from, Math.min(diff, d.len)])
}
}
if (d.len <= diff) {
// d doesn't delete anything anymore
d = dv[++pos]
} else {
d.from = d.from + diff // reset pos
d.len = d.len - diff // reset length
}
}
})
// TODO: It would be more performant to apply the deletes in the above loop
// Adapt the Tree implementation to support delete while iterating
for (let i = deletions.length - 1; i >= 0; i--) {
const del = deletions[i]
deleteItemRange(y, del[0], del[1], del[2], true)
}
// for the rest.. just apply it
for (; pos < dv.length; pos++) {
d = dv[pos]
deleteItemRange(y, user, d.from, d.len, true)
// deletions.push([user, d.from, d.len, d.gc)
}
}
}
}

View File

@@ -4,12 +4,65 @@
import * as ID from '../utils/ID.js'
import * as encoding from '../lib/encoding.js'
import * as decoding from '../lib/decoding.js'
const writeStateStore = (encoder, ss) => {
}
/**
* @typedef {Map<number, number>} StateSet
* @typedef {Map<number, number>} StateMap
*/
/**
* @private
* Read StateMap from Decoder and return as Map
*
* @param {decoding.Decoder} decoder
* @return {StateMap}
*/
export const readStateMap = decoder => {
const ss = new Map()
const ssLength = decoding.readUint32(decoder)
for (let i = 0; i < ssLength; i++) {
const user = decoding.readVarUint(decoder)
const clock = decoding.readVarUint(decoder)
ss.set(user, clock)
}
return ss
}
/**
* Write StateMap to Encoder
*
* @param {encoding.Encoder} encoder
* @param {StateMap} state
*/
export const writeStateMap = (encoder, state) => {
// write as fixed-size number to stay consistent with the other encode functions.
// => anytime we write the number of objects that follow, encode as fixed-size number.
encoding.writeUint32(encoder, state.size)
state.forEach((clock, user) => {
encoding.writeVarUint(encoder, user)
encoding.writeVarUint(encoder, clock)
})
}
/**
* Read a StateMap from Decoder and return it as string.
*
* @param {decoding.Decoder} decoder
* @return {string}
*/
export const stringifyStateMap = decoder => {
let s = 'State Set: '
readStateMap(decoder).forEach((clock, user) => {
s += `(${user}: ${clock}), `
})
return s
}
/**
*/
export class StateStore {
constructor (y) {

View File

@@ -1,4 +1,4 @@
import { DeleteStore } from './DeleteStore.js'
import { DeleteStore, readDeleteStore, writeDeleteStore } from './DeleteStore.js'
import { OperationStore } from './OperationStore.js'
import { StateStore } from './StateStore.js'
import { generateRandomUint32 } from './generateRandomUint32.js'
@@ -59,7 +59,7 @@ export class Y extends NamedEventHandler {
importModel (decoder) {
this.transact(() => {
integrateRemoteStructs(decoder, this)
message.readDeleteSet(decoder, this)
readDeleteStore(decoder, this)
})
}
@@ -71,7 +71,7 @@ export class Y extends NamedEventHandler {
exportModel () {
const encoder = encoding.createEncoder()
message.writeStructs(encoder, this, new Map())
message.writeDeleteSet(encoder, this)
writeDeleteStore(encoder, this.ds)
return encoding.toBuffer(encoder)
}
_beforeChange () {}
@@ -174,7 +174,7 @@ export class Y extends NamedEventHandler {
*
* @param {String} name
* @param {Function} TypeConstructor The constructor of the type definition
* @returns {Type} The created type. Constructed with TypeConstructor
* @returns {any} The created type. Constructed with TypeConstructor
*/
define (name, TypeConstructor) {
let id = createRootID(name, TypeConstructor)
@@ -194,6 +194,7 @@ export class Y extends NamedEventHandler {
* This returns the same value as `y.share[name]`
*
* @param {String} name The typename
* @return {any}
*/
get (name) {
return this._map.get(name)

8
utils/snapshot.js Normal file
View File

@@ -0,0 +1,8 @@
/**
*
* @param {Item} item
* @param {import("../protocols/history").HistorySnapshot} [snapshot]
*/
export const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : (snapshot.sm.has(item._id.user) && snapshot.sm.get(item._id.user) > item._id.clock && !snapshot.ds.isDeleted(item._id))