Merge pull request #243 from yjs/create-doc-from-snapshot-3

Create doc from snapshot 3
This commit is contained in:
Kevin Jahns 2020-09-28 18:34:01 +02:00 committed by GitHub
commit 81f16ff0b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 228 additions and 3 deletions

View File

@ -47,6 +47,7 @@ export {
findRootTypeKey,
typeListToArraySnapshot,
typeMapGetSnapshot,
createDocFromSnapshot,
iterateDeletedStructs,
applyUpdate,
applyUpdateV2,

View File

@ -12,13 +12,17 @@ import {
createDeleteSet,
createID,
getState,
AbstractDSDecoder, AbstractDSEncoder, DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line
findIndexSS,
UpdateEncoderV2,
DefaultDSEncoder,
applyUpdateV2,
AbstractDSDecoder, AbstractDSEncoder, DSEncoderV2, DSDecoderV1, DSDecoderV2, 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 decoding from 'lib0/decoding.js'
import { DefaultDSEncoder } from './encoding.js'
import * as encoding from 'lib0/encoding.js'
export class Snapshot {
/**
@ -148,3 +152,51 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
meta.add(snapshot)
}
}
/**
* @param {Doc} originDoc
* @param {Snapshot} snapshot
* @param {Doc} [newDoc] Optionally, you may define the Yjs document that receives the data from originDoc
* @return {Doc}
*/
export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) => {
if (originDoc.gc) {
// we should not try to restore a GC-ed document, because some of the restored items might have their content deleted
throw new Error('originDoc must not be garbage collected')
}
const { sv, ds } = snapshot
const encoder = new UpdateEncoderV2()
originDoc.transact(transaction => {
let size = 0
sv.forEach(clock => {
if (clock > 0) {
size++
}
})
encoding.writeVarUint(encoder.restEncoder, size)
// splitting the structs before writing them to the encoder
for (const [client, clock] of sv) {
if (clock === 0) {
continue
}
if (clock < getState(originDoc.store, client)) {
getItemCleanStart(transaction, createID(client, clock))
}
const structs = originDoc.store.clients.get(client) || []
const lastStructIndex = findIndexSS(structs, clock - 1)
// write # encoded structs
encoding.writeVarUint(encoder.restEncoder, lastStructIndex + 1)
encoder.writeClient(client)
// first clock written is 0
encoding.writeVarUint(encoder.restEncoder, 0)
for (let i = 0; i <= lastStructIndex; i++) {
structs[i].write(encoder, 0)
}
}
writeDeleteSet(encoder, ds)
})
applyUpdateV2(newDoc, encoder.toUint8Array(), 'snapshot')
return newDoc
}

View File

@ -7,6 +7,7 @@ import * as encoding from './encoding.tests.js'
import * as undoredo from './undo-redo.tests.js'
import * as compatibility from './compatibility.tests.js'
import * as doc from './doc.tests.js'
import * as snapshot from './snapshot.tests.js'
import { runTests } from 'lib0/testing.js'
import { isBrowser, isNode } from 'lib0/environment.js'
@ -16,7 +17,7 @@ if (isBrowser) {
log.createVConsole(document.body)
}
runTests({
doc, map, array, text, xml, encoding, undoredo, compatibility
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot
}).then(success => {
/* istanbul ignore next */
if (isNode) {

171
tests/snapshot.tests.js Normal file
View File

@ -0,0 +1,171 @@
import { createDocFromSnapshot, Doc, snapshot, YMap } from '../src/internals'
import * as t from 'lib0/testing.js'
import { init } from './testHelper'
/**
* @param {t.TestCase} tc
*/
export const testBasicRestoreSnapshot = tc => {
const doc = new Doc({ gc: false })
doc.getArray('array').insert(0, ['hello'])
const snap = snapshot(doc)
doc.getArray('array').insert(1, ['world'])
const docRestored = createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['hello'])
t.compare(doc.getArray('array').toArray(), ['hello', 'world'])
}
/**
* @param {t.TestCase} tc
*/
export const testEmptyRestoreSnapshot = tc => {
const doc = new Doc({ gc: false })
const snap = snapshot(doc)
snap.sv.set(9999, 0)
doc.getArray().insert(0, ['world'])
const docRestored = createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray().toArray(), [])
t.compare(doc.getArray().toArray(), ['world'])
// now this snapshot reflects the latest state. It shoult still work.
const snap2 = snapshot(doc)
const docRestored2 = createDocFromSnapshot(doc, snap2)
t.compare(docRestored2.getArray().toArray(), ['world'])
}
/**
* @param {t.TestCase} tc
*/
export const testRestoreSnapshotWithSubType = tc => {
const doc = new Doc({ gc: false })
doc.getArray('array').insert(0, [new YMap()])
const subMap = doc.getArray('array').get(0)
subMap.set('key1', 'value1')
const snap = snapshot(doc)
subMap.set('key2', 'value2')
const docRestored = createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toJSON(), [{
key1: 'value1'
}])
t.compare(doc.getArray('array').toJSON(), [{
key1: 'value1',
key2: 'value2'
}])
}
/**
* @param {t.TestCase} tc
*/
export const testRestoreDeletedItem1 = tc => {
const doc = new Doc({ gc: false })
doc.getArray('array').insert(0, ['item1', 'item2'])
const snap = snapshot(doc)
doc.getArray('array').delete(0)
const docRestored = createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item1', 'item2'])
t.compare(doc.getArray('array').toArray(), ['item2'])
}
/**
* @param {t.TestCase} tc
*/
export const testRestoreLeftItem = tc => {
const doc = new Doc({ gc: false })
doc.getArray('array').insert(0, ['item1'])
doc.getMap('map').set('test', 1)
doc.getArray('array').insert(0, ['item0'])
const snap = snapshot(doc)
doc.getArray('array').delete(1)
const docRestored = createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item0', 'item1'])
t.compare(doc.getArray('array').toArray(), ['item0'])
}
/**
* @param {t.TestCase} tc
*/
export const testDeletedItemsBase = tc => {
const doc = new Doc({ gc: false })
doc.getArray('array').insert(0, ['item1'])
doc.getArray('array').delete(0)
const snap = snapshot(doc)
doc.getArray('array').insert(0, ['item0'])
const docRestored = createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), [])
t.compare(doc.getArray('array').toArray(), ['item0'])
}
/**
* @param {t.TestCase} tc
*/
export const testDeletedItems2 = tc => {
const doc = new Doc({ gc: false })
doc.getArray('array').insert(0, ['item1', 'item2', 'item3'])
doc.getArray('array').delete(1)
const snap = snapshot(doc)
doc.getArray('array').insert(0, ['item0'])
const docRestored = createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item1', 'item3'])
t.compare(doc.getArray('array').toArray(), ['item0', 'item1', 'item3'])
}
/**
* @param {t.TestCase} tc
*/
export const testDependentChanges = tc => {
const { array0, array1, testConnector } = init(tc, { users: 2 })
if (!array0.doc) {
throw new Error('no document 0')
}
if (!array1.doc) {
throw new Error('no document 1')
}
/**
* @type Doc
*/
const doc0 = array0.doc
/**
* @type Doc
*/
const doc1 = array1.doc
doc0.gc = false
doc1.gc = false
array0.insert(0, ['user1item1'])
testConnector.syncAll()
array1.insert(1, ['user2item1'])
testConnector.syncAll()
const snap = snapshot(array0.doc)
array0.insert(2, ['user1item2'])
testConnector.syncAll()
array1.insert(3, ['user2item2'])
testConnector.syncAll()
const docRestored0 = createDocFromSnapshot(array0.doc, snap)
t.compare(docRestored0.getArray('array').toArray(), ['user1item1', 'user2item1'])
const docRestored1 = createDocFromSnapshot(array1.doc, snap)
t.compare(docRestored1.getArray('array').toArray(), ['user1item1', 'user2item1'])
}