prelim refactor commit

This commit is contained in:
Kevin Jahns
2019-03-26 01:14:15 +01:00
parent 293527e62b
commit d9ab593b07
44 changed files with 2263 additions and 1914 deletions

View File

@@ -1,67 +0,0 @@
/**
* Type that maps from Yjs type to Target type.
* Used to implement double bindings.
*
* @private
* @template Y
* @template T
*/
export class BindMapping {
/**
*/
constructor () {
/**
* @type Map<Y, T>
*/
this.yt = new Map()
/**
* @type Map<T, Y>
*/
this.ty = new Map()
}
/**
* Map y to t. Removes all existing bindings from y and t
* @param {Y} y
* @param {T} t
*/
bind (y, t) {
const existingT = this.yt.get(y)
if (existingT !== undefined) {
this.ty.delete(existingT)
}
const existingY = this.ty.get(t)
if (existingY !== undefined) {
this.yt.delete(existingY)
}
this.yt.set(y, t)
this.ty.set(t, y)
}
/**
* @param {Y} y
* @return {boolean}
*/
hasY (y) {
return this.yt.has(y)
}
/**
* @param {T} t
* @return {boolean}
*/
hasT (t) {
return this.ty.has(t)
}
/**
* @param {Y} y
* @return {T}
*/
getY (y) {
return this.yt.get(y)
}
/**
* @param {T} t
* @return {Y}
*/
getT (t) {
return this.ty.get(t)
}
}

142
src/utils/DeleteSet.js Normal file
View File

@@ -0,0 +1,142 @@
import * as map from 'lib0/map.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import { StructStore, getItemRange } from './StructStore.js' // eslint-disable-line
import { Transaction } from './Transaction.js' // eslint-disable-line
class DeleteItem {
/**
* @param {number} clock
* @param {number} len
*/
constructor (clock, len) {
/**
* @type {number}
*/
this.clock = clock
/**
* @type {number}
*/
this.len = len
}
}
/**
* We no longer maintain a DeleteStore. DeleteSet is a temporary object that is created when needed.
* - When created in a transaction, it must only be accessed after sorting, and merging
* - This DeleteSet is send to other clients
* - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore
* - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged.
*
*/
export class DeleteSet {
constructor () {
/**
* @type {Map<number,Array<DeleteItem>>}
*/
this.clients = new Map()
}
}
/**
* @param {DeleteSet} ds
*/
export const sortAndMergeDeleteSet = ds => {
ds.clients.forEach(dels => {
dels.sort((a, b) => a.clock - b.clock)
// i is the current pointer
// j refers to the current insert position for the pointed item
// try to merge dels[i] with dels[i-1]
let i, j
for (i = 1, j = 1; i < dels.length; i++) {
const left = dels[i - 1]
const right = dels[i]
if (left.clock + left.len === right.clock) {
left.len += right.len
} else {
if (j < i) {
dels[j] = right
}
j++
}
}
dels.length = j
})
}
/**
* @param {Transaction} transaction
*/
export const createDeleteSetFromTransaction = transaction => {
const ds = new DeleteSet()
transaction.deleted.forEach(item => {
map.setTfUndefined(ds.clients, item.id.client, () => []).push(new DeleteItem(item.id.clock, item.length))
})
sortAndMergeDeleteSet(ds)
return ds
}
/**
* @param {StructStore} ss
* @return {DeleteSet} Merged and sorted DeleteSet
*/
export const createDeleteSetFromStructStore = ss => {
const ds = new DeleteSet()
ss.clients.forEach((structs, client) => {
/**
* @type {Array<DeleteItem>}
*/
const dsitems = []
for (let i = 0; i < structs.length; i++) {
const struct = structs[i]
const clock = struct.id.clock
let len = struct.length
if (i + 1 < structs.length) {
for (let next = structs[i + 1]; i + 1 < structs.length && next.id.clock === clock + len; i++) {
len += next.length
}
}
dsitems.push(new DeleteItem(clock, len))
}
if (dsitems.length > 0) {
ds.clients.set(client, dsitems)
}
})
return ds
}
/**
* @param {encoding.Encoder} encoder
* @param {DeleteSet} ds
*/
export const writeDeleteSet = (encoder, ds) => {
encoding.writeVarUint(encoder, ds.clients.size)
ds.clients.forEach((dsitems, client) => {
encoding.writeVarUint(encoder, client)
const len = dsitems.length
encoding.writeVarUint(encoder, len)
for (let i = 0; i < len; i++) {
const item = dsitems[i]
encoding.writeVarUint(encoder, item.clock)
encoding.writeVarUint(encoder, item.len)
}
})
}
/**
* @param {decoding.Decoder} decoder
* @param {StructStore} ss
* @param {Transaction} transaction
*/
export const readDeleteSet = (decoder, ss, transaction) => {
const numClients = decoding.readVarUint(decoder)
for (let i = 0; i < numClients; i++) {
const client = decoding.readVarUint(decoder)
const len = decoding.readVarUint(decoder)
for (let i = 0; i < len; i++) {
const clock = decoding.readVarUint(decoder)
const len = decoding.readVarUint(decoder)
getItemRange(ss, transaction, client, clock, len).forEach(struct => struct.delete(transaction))
}
}
}

View File

@@ -1,89 +0,0 @@
/**
* @module utils
*/
import { Tree } from 'lib0/tree.js'
import * as ID from './ID.js'
export class DSNode {
constructor (id, len, gc) {
this._id = id
this.len = len
this.gc = gc
}
clone () {
return new DSNode(this._id, this.len, this.gc)
}
}
export class DeleteStore extends Tree {
logTable () {
const deletes = []
this.iterate(null, null, n => {
deletes.push({
user: n._id.user,
clock: n._id.clock,
len: n.len,
gc: n.gc
})
})
console.table(deletes)
}
isDeleted (id) {
var n = this.findWithUpperBound(id)
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
}
mark (id, length, gc) {
if (length === 0) return
// Step 1. Unmark range
const leftD = this.findWithUpperBound(ID.createID(id.user, id.clock - 1))
// Resize left DSNode if necessary
if (leftD !== null && leftD._id.user === id.user) {
if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) {
// node is overlapping. need to resize
if (id.clock + length < leftD._id.clock + leftD.len) {
// overlaps new mark range and some more
// create another DSNode to the right of new mark
this.put(new DSNode(ID.createID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc))
}
// resize left DSNode
leftD.len = id.clock - leftD._id.clock
} // Otherwise there is no overlapping
}
// Resize right DSNode if necessary
const upper = ID.createID(id.user, id.clock + length - 1)
const rightD = this.findWithUpperBound(upper)
if (rightD !== null && rightD._id.user === id.user) {
if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node
const d = id.clock + length - rightD._id.clock
rightD._id = ID.createID(rightD._id.user, rightD._id.clock + d)
rightD.len -= d
}
}
// Now we only have to delete all inner marks
const deleteNodeIds = []
this.iterate(id, upper, m => {
deleteNodeIds.push(m._id)
})
for (let i = deleteNodeIds.length - 1; i >= 0; i--) {
this.delete(deleteNodeIds[i])
}
let newMark = new DSNode(id, length, gc)
// Step 2. Check if we can extend left or right
if (leftD !== null && leftD._id.user === id.user && leftD._id.clock + leftD.len === id.clock && leftD.gc === gc) {
// We can extend left
leftD.len += length
newMark = leftD
}
const rightNext = this.find(ID.createID(id.user, id.clock + length))
if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) {
// We can merge newMark and rightNext
newMark.len += rightNext.len
this.delete(rightNext._id)
}
if (leftD !== newMark) {
// only put if we didn't extend left
this.put(newMark)
}
}
}

View File

@@ -2,81 +2,72 @@
* @module utils
*/
import { getStructReference } from '../utils/structReferences.js'
import * as decoding from 'lib0/decoding.js'
import * as encoding from 'lib0/encoding.js'
export class ID {
constructor (user, clock) {
this.user = user // TODO: rename to client
/**
* @param {number} client client id
* @param {number} clock unique per client id, continuous number
*/
constructor (client, clock) {
/**
* @type {number} Client id
*/
this.client = client
/**
* @type {number} unique per client id, continuous number
*/
this.clock = clock
}
/**
* @return {ID}
*/
clone () {
return new ID(this.user, this.clock)
return new ID(this.client, this.clock)
}
/**
* @param {ID} id
* @return {boolean}
*/
equals (id) {
return id !== null && id.user === this.user && id.clock === this.clock
return id !== null && id.client === this.client && id.clock === this.clock
}
/**
* @param {ID} id
* @return {boolean}
*/
lessThan (id) {
if (id.constructor === ID) {
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
return this.client < id.client || (this.client === id.client && this.clock < id.clock)
} else {
return false
}
}
/**
* @param {encoding.Encoder} encoder
*/
encode (encoder) {
encoding.writeVarUint(encoder, this.user)
encoding.writeVarUint(encoder, this.clock)
}
}
export const createID = (user, clock) => new ID(user, clock)
export const RootFakeUserID = 0xFFFFFF
export class RootID {
/**
* @param {string} name
* @param {Function?} typeConstructor
*/
constructor (name, typeConstructor) {
this.user = RootFakeUserID
this.name = name
this.type = typeConstructor === null ? null : getStructReference(typeConstructor)
}
equals (id) {
return id !== null && id.user === this.user && id.name === this.name && id.type === this.type
}
lessThan (id) {
if (id.constructor === RootID) {
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
} else {
return true
}
}
/**
* @param {encoding.Encoder} encoder
*/
encode (encoder) {
encoding.writeVarUint(encoder, this.user)
encoding.writeVarString(encoder, this.name)
encoding.writeVarUint(encoder, this.type)
}
}
/**
* Create a new root id.
*
* @example
* y.define('name', Y.Array) // name, and typeConstructor
*
* @param {string} name
* @param {Function?} typeConstructor must be defined in structReferences
* @param {number} client
* @param {number} clock
*/
export const createRootID = (name, typeConstructor) => new RootID(name, typeConstructor)
export const createID = (client, clock) => new ID(client, clock)
const isNullID = 0xFFFFFF
/**
* @param {encoding.Encoder} encoder
* @param {ID} id
*/
export const writeID = (encoder, id) => {
encoding.writeVarUint(encoder, id.client)
encoding.writeVarUint(encoder, id.clock)
}
/**
* @param {encoding.Encoder} encoder
*/
export const writeNullID = (encoder) =>
encoding.writeVarUint(encoder, isNullID)
/**
* Read ID.
@@ -84,15 +75,9 @@ export const createRootID = (name, typeConstructor) => new RootID(name, typeCons
* * Otherwise an ID is returned
*
* @param {decoding.Decoder} decoder
* @return {ID|RootID}
* @return {ID | null}
*/
export const decode = decoder => {
const user = decoding.readVarUint(decoder)
if (user === RootFakeUserID) {
// read property name and type id
const rid = createRootID(decoding.readVarString(decoder), null)
rid.type = decoding.readVarUint(decoder)
return rid
}
return createID(user, decoding.readVarUint(decoder))
export const readID = decoder => {
const client = decoding.readVarUint(decoder)
return client === isNullID ? null : createID(client, decoding.readVarUint(decoder))
}

View File

@@ -1,98 +0,0 @@
/**
* @module utils
*/
import { Tree } from 'lib0/tree.js'
import * as ID from '../utils/ID.js'
import { getStruct } from './structReferences.js'
import { GC } from '../structs/GC.js'
import * as Item from '../structs/Item.js'
export class OperationStore extends Tree {
constructor (y) {
super()
this.y = y
}
logTable () {
const items = []
this.iterate(null, null, item => {
if (item.constructor === GC) {
items.push({
id: Item.stringifyItemID(item),
content: item._length,
deleted: 'GC'
})
} else {
items.push({
id: Item.stringifyItemID(item),
origin: item._origin === null ? '()' : Item.stringifyID(item._origin._lastId),
left: item._left === null ? '()' : Item.stringifyID(item._left._lastId),
right: Item.stringifyItemID(item._right),
right_origin: Item.stringifyItemID(item._right_origin),
parent: Item.stringifyItemID(item._parent),
parentSub: item._parentSub,
deleted: item._deleted,
content: JSON.stringify(item._content)
})
}
})
console.table(items)
}
get (id) {
let struct = this.find(id)
if (struct === null && id instanceof ID.RootID) {
const Constr = getStruct(id.type)
const y = this.y
struct = new Constr()
struct._id = id
struct._parent = y
y.transact(() => {
struct._integrate(y)
})
this.put(struct)
}
return struct
}
// Use getItem for structs with _length > 1
getItem (id) {
var item = this.findWithUpperBound(id)
if (item === null) {
return null
}
const itemID = item._id
if (id.user === itemID.user && id.clock < itemID.clock + item._length) {
return item
} else {
return null
}
}
// Return an insertion such that id is the first element of content
// This function manipulates an item, if necessary
getItemCleanStart (id) {
var ins = this.getItem(id)
if (ins === null || ins._length === 1) {
return ins
}
const insID = ins._id
if (insID.clock === id.clock) {
return ins
} else {
return ins._splitAt(this.y, id.clock - insID.clock)
}
}
// Return an insertion such that id is the last element of content
// This function manipulates an operation, if necessary
getItemCleanEnd (id) {
var ins = this.getItem(id)
if (ins === null || ins._length === 1) {
return ins
}
const insID = ins._id
if (insID.clock + ins._length - 1 === id.clock) {
return ins
} else {
ins._splitAt(this.y, id.clock - insID.clock + 1)
return ins
}
}
}

View File

@@ -1,53 +0,0 @@
/**
* @module utils
*/
import * as ID from '../utils/ID.js'
/**
*/
export class StateStore {
constructor (y) {
this.y = y
this.state = new Map()
}
logTable () {
const entries = []
for (let [user, state] of this.state) {
entries.push({
user, state
})
}
console.table(entries)
}
getNextID (len) {
const user = this.y.userID
const state = this.getState(user)
this.setState(user, state + len)
return ID.createID(user, state)
}
updateRemoteState (struct) {
let user = struct._id.user
let userState = this.state.get(user)
while (struct !== null && struct._id.clock === userState) {
userState += struct._length
struct = this.y.os.get(ID.createID(user, userState))
}
this.state.set(user, userState)
}
getState (user) {
let state = this.state.get(user)
if (state == null) {
return 0
}
return state
}
setState (user, state) {
// TODO: modify missingi structs here
const beforeState = this.y._transaction.beforeState
if (!beforeState.has(user)) {
beforeState.set(user, this.getState(user))
}
this.state.set(user, state)
}
}

223
src/utils/StructStore.js Normal file
View File

@@ -0,0 +1,223 @@
import { AbstractStruct } from '../structs/AbstractStruct.js' // eslint-disable-line
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
import { ID } from './ID.js' // eslint-disable-line
import { Transaction } from './Transaction.js' // eslint-disable-line
import * as map from 'lib0/map.js'
import * as math from 'lib0/math.js'
export class StructStore {
constructor () {
/**
* @type {Map<number,Array<AbstractStruct>>}
*/
this.clients = new Map()
}
}
/**
* Return the states as an array of {client,clock} pairs.
* Note that clock refers to the next expected clock id.
*
* @param {StructStore} store
* @return {Array<{client:number,clock:number}>}
*/
export const getStates = store =>
map.map(store.clients, (structs, client) => {
const struct = structs[structs.length - 1]
return {
client,
clock: struct.id.clock + struct.length
}
})
/**
* @param {StructStore} store
*/
export const integretyCheck = store => {
store.clients.forEach(structs => {
for (let i = 1; i < structs.length; i++) {
const l = structs[i - 1]
const r = structs[i]
if (l.id.clock + l.length !== r.id.clock) {
throw new Error('StructStore failed integrety check')
}
}
})
}
/**
* @param {StructStore} store
* @param {AbstractStruct} struct
*/
export const addStruct = (store, struct) => {
map.setTfUndefined(store.clients, struct.id.client, () => []).push(struct)
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
* @param {Array<AbstractStruct>} structs // ordered structs without holes
* @param {number} clock
* @return {number}
* @private
*/
export const findIndex = (structs, clock) => {
let left = 0
let right = structs.length
while (left <= right) {
const midindex = math.floor((left + right) / 2)
const mid = structs[midindex]
const midclock = mid.id.clock
if (midclock <= clock) {
if (clock < midclock + mid.length) {
return midindex
}
left = midindex
} else {
right = midindex
}
}
throw new Error('ID does not exist')
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {StructStore} store
* @param {ID} id
* @return {AbstractStruct}
* @private
*/
const find = (store, id) => {
/**
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(id.client)
return structs[findIndex(structs, id.clock)]
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {StructStore} store
* @param {ID} id
* @return {ItemType}
*/
// @ts-ignore
export const getItemType = (store, id) => find(store, id)
/**
* @param {Transaction} transaction
* @param {AbstractItem} struct
* @param {number} diff
*/
const splitStruct = (transaction, struct, diff) => {
const right = struct.splitAt(diff)
if (transaction.added.has(struct)) {
transaction.added.add(right)
} else if (transaction.deleted.has(struct)) {
transaction.deleted.add(right)
}
return right
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
* @param {StructStore} store
* @param {Transaction} transaction
* @param {ID} id
* @return {AbstractItem}
*
* @private
*/
export const getItemCleanStart = (store, transaction, id) => {
/**
* @type {Array<AbstractItem>}
*/
// @ts-ignore
const structs = store.clients.get(id.client)
const index = findIndex(structs, id.clock)
/**
* @type {any}
*/
let struct = structs[index]
if (struct.id.clock < id.clock) {
struct = splitStruct(transaction, struct, id.clock - struct.id.clock)
structs.splice(index, 0, struct)
}
return struct
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
* @param {StructStore} store
* @param {Transaction} transaction
* @param {ID} id
* @return {AbstractItem}
*
* @private
*/
export const getItemCleanEnd = (store, transaction, id) => {
/**
* @type {Array<AbstractItem>}
*/
// @ts-ignore
const structs = store.clients.get(id.client)
const index = findIndex(structs, id.clock)
const struct = structs[index]
if (id.clock !== struct.id.clock + struct.length - 1) {
structs.splice(index, 0, splitStruct(transaction, struct, id.clock - struct.id.clock + 1))
}
return struct
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
* @param {StructStore} store
* @param {Transaction} transaction
* @param {number} client
* @param {number} clock
* @param {number} len
* @return {Array<AbstractItem>}
*
* @private
*/
export const getItemRange = (store, transaction, client, clock, len) => {
/**
* @type {Array<AbstractItem>}
*/
// @ts-ignore
const structs = store.clients.get(client)
let index = findIndex(structs, clock)
let struct = structs[index]
let range = []
if (struct.id.clock < clock) {
struct = splitStruct(transaction, struct, clock - struct.id.clock)
structs.splice(index, 0, struct)
}
while (struct.id.clock + struct.length <= clock + len) {
range.push(struct)
struct = structs[++index]
}
if (clock < struct.id.clock + struct.length) {
structs.splice(index, 0, splitStruct(transaction, struct, clock + len - struct.id.clock))
range.push(struct)
}
return range
}
/**
* Replace `item` with `newitem` in store
* @param {StructStore} store
* @param {AbstractStruct} struct
* @param {AbstractStruct} newStruct
*/
export const replace = (store, struct, newStruct) => {
/**
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(struct.id.client)
structs[findIndex(structs, struct.id)] = newStruct
}

View File

@@ -3,6 +3,13 @@
*/
import * as encoding from 'lib0/encoding.js'
import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
import { Y } from './Y.js' // eslint-disable-line
import { YEvent } from './YEvent.js' // eslint-disable-line
import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
import { getState } from './StructStore.js'
import { createID } from './ID.js' // eslint-disable-line
/**
* A transaction is created for every change on the Yjs model. It is possible
@@ -28,42 +35,52 @@ import * as encoding from 'lib0/encoding.js'
*
*/
export class Transaction {
/**
* @param {Y} y
*/
constructor (y) {
/**
* @type {Y} The Yjs instance.
*/
this.y = y
/**
* All new types that are added during a transaction.
* @type {Set<Item>}
* All new items that are added during a transaction.
* @type {Set<AbstractItem>}
*/
this.newTypes = new Set()
this.added = new Set()
/**
* All types that were directly modified (property added or child
* inserted/deleted). New types are not included in this Set.
* Maps from type to parentSubs (`item._parentSub = null` for YArray)
* @type {Map<Type|Y,String>}
* Set of all deleted items
* @type {Set<AbstractItem>}
*/
this.changedTypes = new Map()
// TODO: rename deletedTypes
/**
* Set of all deleted Types and Structs.
* @type {Set<Item>}
*/
this.deletedStructs = new Set()
this.deleted = new Set()
/**
* Saves the old state set of the Yjs instance. If a state was modified,
* the original value is saved here.
* @type {Map<Number,Number>}
*/
this.beforeState = new Map()
/**
* All types that were directly modified (property added or child
* inserted/deleted). New types are not included in this Set.
* Maps from type to parentSubs (`item._parentSub = null` for YArray)
* @type {Map<ItemType,Set<String|null>>}
*/
this.changed = new Map()
/**
* Stores the events for the types that observe also child elements.
* It is mainly used by `observeDeep`.
* @type {Map<Type,Array<YEvent>>}
* @type {Map<ItemType,Array<YEvent>>}
*/
this.changedParentTypes = new Map()
this.encodedStructsLen = 0
this.encodedStructs = encoding.createEncoder()
}
}
/**
* @param {Transaction} transaction
*/
export const nextID = transaction => {
const y = transaction.y
return createID(y.clientID, getState(y.store, y.clientID))
}

View File

@@ -126,7 +126,7 @@ export class UndoManager {
this._redoing === false &&
this._lastTransactionWasUndo === false &&
lastUndoOp !== null &&
(options.captureTimeout < 0 || reverseOperation.created - lastUndoOp.created <= options.captureTimeout)
((options.captureTimeout < 0) || (reverseOperation.created.getTime() - lastUndoOp.created.getTime()) <= options.captureTimeout)
) {
lastUndoOp.created = reverseOperation.created
if (reverseOperation.toState !== null) {

View File

@@ -1,10 +1,14 @@
import { DeleteStore } from './DeleteStore.js'
import { DeleteStore } from './DeleteSet.js/index.js' // TODO: remove
import { OperationStore } from './OperationStore.js'
import { StateStore } from './StateStore.js'
import { StructStore } from './StructStore.js'
import * as random from 'lib0/random.js'
import { createRootID } from './ID.js'
import * as map from 'lib0/map.js'
import { Observable } from 'lib0/observable.js'
import { Transaction } from './Transaction.js'
import { AbstractStruct, AbstractRef } from '../structs/AbstractStruct.js' // eslint-disable-line
import { AbstractType } from '../types/AbstractType.js'
import { YArray } from '../types/YArray.js'
/**
* Anything that can be encoded with `JSON.stringify` and can be decoded with
@@ -28,25 +32,33 @@ export class Y extends Observable {
constructor (conf = {}) {
super()
this.gcEnabled = conf.gc || false
this._contentReady = false
this.userID = random.uint32()
// TODO: This should be a Map so we can use encodables as keys
this._map = new Map()
this.ds = new DeleteStore()
this.os = new OperationStore(this)
this.ss = new StateStore(this)
this.clientID = random.uint32()
this.share = new Map()
this.store = new StructStore()
/**
* @type {Map<number, Map<number, AbstractRef>>}
*/
this._missingStructs = new Map()
/**
* @type {Array<AbstractStruct>}
*/
this._readyToIntegrate = []
/**
* @type {Transaction | null}
*/
this._transaction = null
this.connected = false
// for compatibility with isParentOf
this._parent = null
this._hasUndoManager = false
this._deleted = false // for compatiblity of having this as a parent for types
this._id = null
}
_beforeChange () {}
_callObserver (transaction, subs, remote) {}
/**
* @type {Transaction}
*/
get transaction () {
const t = this._transaction
if (t === null) {
throw new Error('All changes must happen inside a transaction')
}
return t
}
/**
* Changes that happen inside of a transaction are bundled. This means that
* the observer fires _after_ the transaction is finished and that all changes
@@ -59,8 +71,9 @@ export class Y extends Observable {
* Defaults to false.
*/
transact (f, remote = false) {
let initialCall = this._transaction === null
if (initialCall) {
let initialCall = false
if (this._transaction === null) {
initialCall = true
this._transaction = new Transaction(this)
this.emit('beforeTransaction', [this, this._transaction, remote])
}
@@ -74,9 +87,9 @@ export class Y extends Observable {
const transaction = this._transaction
this._transaction = null
// emit change events on changed types
transaction.changedTypes.forEach((subs, type) => {
if (!type._deleted) {
type._callObserver(transaction, subs, remote)
transaction.changed.forEach((subs, itemtype) => {
if (!itemtype._deleted) {
itemtype.type._callObserver(transaction, subs, remote)
}
})
transaction.changedParentTypes.forEach((events, type) => {
@@ -91,86 +104,57 @@ export class Y extends Observable {
})
// we don't have to check for events.length
// because there is no way events is empty..
type._deepEventHandler.callEventListeners(transaction, events)
type.type._deepEventHandler.callEventListeners(transaction, events)
}
})
// when all changes & events are processed, emit afterTransaction event
this.emit('afterTransaction', [this, transaction, remote])
}
}
/**
* Fake _start for root properties (y.set('name', type))
*
* @private
*/
get _start () {
return null
}
/**
* Fake _start for root properties (y.set('name', type))
*
* @private
*/
set _start (start) {}
/**
* Define a shared data type.
*
* Multiple calls of `y.define(name, TypeConstructor)` yield the same result
* Multiple calls of `y.get(name, TypeConstructor)` yield the same result
* and do not overwrite each other. I.e.
* `y.define(name, type) === y.define(name, type)`
* `y.define(name, Y.Array) === y.define(name, Y.Array)`
*
* After this method is called, the type is also available on `y._map.get(name)`.
* After this method is called, the type is also available on `y.share.get(name)`.
*
* *Best Practices:*
* Either define all types right after the Yjs instance is created or always
* use `y.define(..)` when accessing a type.
* Define all types right after the Yjs instance is created and store them in a separate object.
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
*
* @example
* // Option 1
* const y = new Y(..)
* y.define('myArray', YArray)
* y.define('myMap', YMap)
* // .. when accessing the type use y._map.get(name)
* y.share.myArray.insert(..)
* y.share.myMap.set(..)
* const appState = {
* document: y.getText('document')
* comments: y.getArray('comments')
* }
*
* // Option2
* const y = new Y(..)
* // .. when accessing the type use `y.define(..)`
* y.define('myArray', YArray).insert(..)
* y.define('myMap', YMap).set(..)
* @TODO: implement getText, getArray, ..
*
* @param {String} name
* @param {string} name
* @param {Function} TypeConstructor The constructor of the type definition
* @returns {any} The created type. Constructed with TypeConstructor
* @return {AbstractType} The created type. Constructed with TypeConstructor
*/
define (name, TypeConstructor) {
let id = createRootID(name, TypeConstructor)
let type = this.os.get(id)
if (this._map.get(name) === undefined) {
this._map.set(name, type)
} else if (this._map.get(name) !== type) {
throw new Error('Type is already defined with a different constructor')
get (name, TypeConstructor = AbstractType) {
// @ts-ignore
const type = map.setTfUndefined(this.share, name, () => new TypeConstructor())
const Constr = type.constructor
if (Constr !== TypeConstructor) {
if (Constr === AbstractType) {
const t = new Constr()
t._map = type._map
t._start = type._start
t._length = type._length
this.share.set(name, t)
return t
} else {
throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
}
}
return type
}
/**
* Get a defined type. The type must be defined locally. First define the
* type with {@link define}.
*
* This returns the same value as `y.share[name]`
*
* @param {String} name The typename
* @return {any}
*/
get (name) {
return this._map.get(name)
}
/**
* Disconnect from the room, and destroy all traces of this Yjs instance.
*/

View File

@@ -1,3 +1,6 @@
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
import { AbstractType } from '../types/AbstractType.js' // eslint-disable-line
/**
* @module utils
*/
@@ -7,17 +10,17 @@
*/
export class YEvent {
/**
* @param {Type} target The changed type.
* @param {AbstractType} target The changed type.
*/
constructor (target) {
/**
* The type on which this event was created on.
* @type {Type}
* @type {AbstractType}
*/
this.target = target
/**
* The current target on which the observe callback is called.
* @type {Type}
* @type {AbstractType}
*/
this.currentTarget = target
}
@@ -34,6 +37,48 @@ export class YEvent {
* type === event.target // => true
*/
get path () {
return this.currentTarget.getPathTo(this.target)
// @ts-ignore _item is defined because target is integrated
return getPathTo(this.currentTarget, this.target._item)
}
}
/**
* Compute the path from this type to the specified target.
*
* @example
* // `child` should be accessible via `type.get(path[0]).get(path[1])..`
* const path = type.getPathTo(child)
* // assuming `type instanceof YArray`
* console.log(path) // might look like => [2, 'key1']
* child === type.get(path[0]).get(path[1])
*
* @param {AbstractType} parent
* @param {AbstractItem} child target
* @return {Array<string|number>} Path to the target
*/
const getPathTo = (parent, child) => {
const path = []
while (true) {
const cparent = child.parent
if (child.parentSub !== null) {
// parent is map-ish
path.unshift(child.parentSub)
} else {
// parent is array-ish
let i = 0
let c = cparent._start
while (c !== child && c !== null) {
if (!c.deleted) {
i++
}
c = c.right
}
path.unshift(i)
}
if (parent === cparent) {
return path
}
// @ts-ignore parent._item cannot be null, because it is integrated
child = parent._item
}
}

View File

@@ -1,61 +0,0 @@
/**
* @module utils
*/
import * as ID from '../utils/ID.js'
import { ItemJSON } from '../structs/ItemJSON.js'
import { ItemString } from '../structs/ItemString.js'
/**
* Try to merge all items in os with their successors.
*
* Some transformations (like delete) fragment items.
* Item(c: 'ab') + Delete(1,1) + Delete(0, 1) -> Item(c: 'a',deleted);Item(c: 'b',deleted)
*
* This functions merges the fragmented nodes together:
* Item(c: 'a',deleted);Item(c: 'b',deleted) -> Item(c: 'ab', deleted)
*
* TODO: The Tree implementation does not support deletions in-spot.
* This is why all deletions must be performed after the traversal.
*
*/
export const defragmentItemContent = y => {
const os = y.os
if (os.length < 2) {
return
}
let deletes = []
let node = os.findSmallestNode()
let next = node.next()
while (next !== null) {
let a = node.val
let b = next.val
if (
(a instanceof ItemJSON || a instanceof ItemString) &&
a.constructor === b.constructor &&
a._deleted === b._deleted &&
a._right === b &&
(ID.createID(a._id.user, a._id.clock + a._length)).equals(b._id)
) {
a._right = b._right
if (a instanceof ItemJSON) {
a._content = a._content.concat(b._content)
} else if (a instanceof ItemString) {
a._content += b._content
}
// delete b later
deletes.push(b._id)
// do not iterate node!
// !(node = next)
} else {
// not able to merge node, get next node
node = next
}
// update next
next = next.next()
}
for (let i = deletes.length - 1; i >= 0; i--) {
os.delete(deletes[i])
}
}

View File

@@ -4,9 +4,9 @@
import { getStruct } from '../utils/structReferences.js'
import * as decoding from 'lib0/decoding.js'
import { GC } from '../structs/GC.js'
import { GC } from '../structs/GC.js/index.js.js'
import { Y } from '../utils/Y.js' // eslint-disable-line
import { Item } from '../structs/Item.js' // eslint-disable-line
import { Item } from '../structs/AbstractItem.js/index.js' // eslint-disable-line
class MissingEntry {
constructor (decoder, missing, struct) {

View File

@@ -3,7 +3,7 @@
*/
import { Y } from '../utils/Y.js' // eslint-disable-line
import { Type } from '../structs/Type.js' // eslint-disable-line
import { Type } from '../types/AbstractType.js/index.js.js.js.js' // eslint-disable-line
/**
* Check if `parent` is a parent of `child`.

View File

@@ -3,7 +3,7 @@
*/
import * as ID from './ID.js'
import { GC } from '../structs/GC.js'
import { GC } from '../structs/GC.js/index.js.js'
// TODO: Implement function to describe ranges

View File

@@ -1,7 +1,22 @@
import { DeleteStore } from './DeleteSet'
export class HistorySnapshot {
/**
* @param {DeleteStore} ds delete store
* @param {Map<number,number>} sm state map
* @param {Map<number,string>} userMap
*/
constructor (ds, sm, userMap) {
this.ds = new DeleteStore()
this.sm = sm
this.userMap = userMap
}
}
/**
*
* @param {Item} item
* @param {import("../protocols/history").HistorySnapshot} [snapshot]
* @param {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))
export const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : (
snapshot.sm.has(item._id.user) && (snapshot.sm.get(item._id.user) || 0) > item._id.clock && !snapshot.ds.isDeleted(item._id)
)

View File

@@ -1,5 +1,30 @@
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import { getStructReference } from './structReferences.js'
import { ID, createID, writeID, writeNullID } from './ID.js'
import * as binary from 'lib0/binary.js'
export const writeStructToTransaction = (transaction, struct) => {
transaction.encodedStructsLen++
struct._toBinary(transaction.encodedStructs)
}
const structRefs = [
ItemBinaryRef
]
/**
* Read the next Item in a Decoder and fill this Item with the read data.
*
* This is called when data is received from a remote peer.
*
* @param {Y} y The Yjs instance that this Item belongs to.
* @param {decoding.Decoder} decoder The decoder object to read data from.
* @return {AbstractRef}
*
* @private
*/
export const read = decoder => {
const info = decoding.readUint8(decoder)
return new structRefs[binary.BITS5 & info](decoder, info)
}

View File

@@ -1,35 +0,0 @@
import * as ID from '../utils/ID.js'
/**
* @private
* Delete all items in an ID-range.
* Does not create delete operations!
* TODO: implement getItemCleanStartNode for better performance (only one lookup).
*/
export const deleteItemRange = (y, user, clock, range, gcChildren) => {
let item = y.os.getItemCleanStart(ID.createID(user, clock))
if (item !== null) {
if (!item._deleted) {
item._splitAt(y, range)
item._delete(y, false, true)
}
let itemLen = item._length
range -= itemLen
clock += itemLen
if (range > 0) {
let node = y.os.findNode(ID.createID(user, clock))
while (node !== null && node.val !== null && range > 0 && node.val._id.equals(ID.createID(user, clock))) {
const nodeVal = node.val
if (!nodeVal._deleted) {
nodeVal._splitAt(y, range)
nodeVal._delete(y, false, gcChildren)
}
const nodeLen = nodeVal._length
range -= nodeLen
clock += nodeLen
node = node.next()
}
}
}
}

View File

@@ -1,31 +0,0 @@
/**
* @module utils
*/
const structs = new Map()
const references = new Map()
/**
* Register a new Yjs types. The same type must be defined with the same
* reference on all clients!
*
* @param {Number} reference
* @param {Function} structConstructor
*
* @public
*/
export const registerStruct = (reference, structConstructor) => {
structs.set(reference, structConstructor)
references.set(structConstructor, reference)
}
/**
* @private
*/
export const getStruct = structs.get.bind(structs) // reference => structs.get(reference)
/**
* @private
*/
export const getStructReference = references.get.bind(references) // typeConstructor => references.get(typeConstructor)