implement PermanentUserData storage prototype
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Item, GC, StructStore, Transaction, ID // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as array from 'lib0/array.js'
|
||||
import * as math from 'lib0/math.js'
|
||||
import * as map from 'lib0/map.js'
|
||||
import * as encoding from 'lib0/encoding.js'
|
||||
@@ -52,14 +53,13 @@ export class DeleteSet {
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {DeleteSet} ds
|
||||
* @param {StructStore} store
|
||||
* @param {function(GC|Item):void} f
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const iterateDeletedStructs = (transaction, ds, store, f) =>
|
||||
export const iterateDeletedStructs = (transaction, ds, f) =>
|
||||
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++) {
|
||||
const del = deletes[i]
|
||||
iterateStructs(transaction, structs, del.clock, del.len, f)
|
||||
@@ -137,22 +137,27 @@ export const sortAndMergeDeleteSet = ds => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DeleteSet} ds1
|
||||
* @param {DeleteSet} ds2
|
||||
* @param {Array<DeleteSet>} dss
|
||||
* @return {DeleteSet} A fresh DeleteSet
|
||||
*/
|
||||
export const mergeDeleteSets = (ds1, ds2) => {
|
||||
export const mergeDeleteSets = dss => {
|
||||
const merged = new DeleteSet()
|
||||
// Write all keys from ds1 to merged. If ds2 has the same key, combine the sets.
|
||||
ds1.clients.forEach((dels1, 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)) {
|
||||
merged.clients.set(client, dels2)
|
||||
}
|
||||
})
|
||||
for (let dssI = 0; dssI < dss.length; dssI++) {
|
||||
dss[dssI].clients.forEach((delsLeft, client) => {
|
||||
if (!merged.clients.has(client)) {
|
||||
// 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)
|
||||
return merged
|
||||
}
|
||||
|
||||
134
src/utils/PermanentUserData.js
Normal file
134
src/utils/PermanentUserData.js
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -131,10 +131,10 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
|
||||
if (!meta.has(snapshot)) {
|
||||
snapshot.sv.forEach((clock, 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {StructStore} store
|
||||
* @param {ID} id
|
||||
* @return {Item}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const getItemCleanStart = (transaction, store, id) => {
|
||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(id.client))
|
||||
return /** @type {Item} */ (structs[findIndexCleanStart(transaction, structs, id.clock)])
|
||||
export const getItemCleanStart = (transaction, id) => {
|
||||
const structs = /** @type {Array<Item>} */ (transaction.doc.store.clients.get(id.client))
|
||||
return structs[findIndexCleanStart(transaction, structs, id.clock)]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,8 +46,9 @@ export class Transaction {
|
||||
/**
|
||||
* @param {Doc} doc
|
||||
* @param {any} origin
|
||||
* @param {boolean} local
|
||||
*/
|
||||
constructor (doc, origin) {
|
||||
constructor (doc, origin, local) {
|
||||
/**
|
||||
* The Yjs instance.
|
||||
* @type {Doc}
|
||||
@@ -95,6 +96,11 @@ export class Transaction {
|
||||
* @type {Map<any,any>}
|
||||
*/
|
||||
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 {function(Transaction):void} f
|
||||
* @param {any} [origin]
|
||||
* @param {any} [origin=true]
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const transact = (doc, f, origin = null) => {
|
||||
export const transact = (doc, f, origin = null, local = true) => {
|
||||
const transactionCleanups = doc._transactionCleanups
|
||||
let initialCall = false
|
||||
if (doc._transaction === null) {
|
||||
initialCall = true
|
||||
doc._transaction = new Transaction(doc, origin)
|
||||
doc._transaction = new Transaction(doc, origin, local)
|
||||
transactionCleanups.push(doc._transaction)
|
||||
doc.emit('beforeTransaction', [doc._transaction, doc])
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
const stackItem = /** @type {StackItem} */ (stack.pop())
|
||||
const itemsToRedo = new Set()
|
||||
let performedChange = false
|
||||
iterateDeletedStructs(transaction, stackItem.ds, store, struct => {
|
||||
iterateDeletedStructs(transaction, stackItem.ds, struct => {
|
||||
if (struct instanceof Item && scope.some(type => isParentOf(type, struct))) {
|
||||
itemsToRedo.add(struct)
|
||||
}
|
||||
@@ -70,10 +70,10 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
if (struct.redone !== null) {
|
||||
let { item, diff } = followRedone(store, struct.id)
|
||||
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) {
|
||||
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
|
||||
}
|
||||
@@ -168,7 +168,7 @@ export class UndoManager extends Observable {
|
||||
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
||||
// append change to last stack op
|
||||
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
|
||||
} else {
|
||||
// create a new stack op
|
||||
@@ -178,7 +178,7 @@ export class UndoManager extends Observable {
|
||||
this.lastChange = now
|
||||
}
|
||||
// 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))) {
|
||||
keepItem(item)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
readAndApplyDeleteSet,
|
||||
writeDeleteSet,
|
||||
createDeleteSetFromStructStore,
|
||||
transact,
|
||||
Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
@@ -299,10 +300,10 @@ export const readStructs = (decoder, transaction, store) => {
|
||||
* @function
|
||||
*/
|
||||
export const readUpdate = (decoder, ydoc, transactionOrigin) =>
|
||||
ydoc.transact(transaction => {
|
||||
transact(ydoc, transaction => {
|
||||
readStructs(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()`.
|
||||
|
||||
Reference in New Issue
Block a user