yjs/utils/Y.js
2018-12-03 17:09:00 +01:00

214 lines
6.4 KiB
JavaScript

import { DeleteStore } from './DeleteStore.js'
import { OperationStore } from './OperationStore.js'
import { StateStore } from './StateStore.js'
import { generateRandomUint32 } from './generateRandomUint32.js'
import { createRootID } from './ID.js'
import { NamedEventHandler } from '../lib/NamedEventHandler.js'
import { Transaction } from './Transaction.js'
import * as encoding from '../lib/encoding.js'
import * as message from '../protocols/syncProtocol.js'
import { integrateRemoteStructs } from './integrateRemoteStructs.js'
import { Type } from '../structs/Type.js' // eslint-disable-line
import { Decoder } from '../lib/decoding.js' // eslint-disable-line
/**
* Anything that can be encoded with `JSON.stringify` and can be decoded with
* `JSON.parse`.
*
* The following property should hold:
* `JSON.parse(JSON.stringify(key))===key`
*
* At the moment the only safe values are number and string.
*
* @typedef {(number|string|Object)} encodable
*/
/**
* A Yjs instance handles the state of shared data.
*/
export class Y extends NamedEventHandler {
/**
* @param {Object} [conf] configuration
*/
constructor (conf = {}) {
super()
this.gcEnabled = conf.gc || false
this._contentReady = false
this.userID = generateRandomUint32()
// 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._missingStructs = new Map()
this._readyToIntegrate = []
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
}
/**
* Read the Decoder and fill the Yjs instance with data in the decoder.
*
* @param {Decoder} decoder The BinaryDecoder to read from.
*/
importModel (decoder) {
this.transact(() => {
integrateRemoteStructs(decoder, this)
message.readDeleteSet(decoder, this)
})
}
/**
* Encode the Yjs model to ArrayBuffer
*
* @return {ArrayBuffer} The Yjs model as ArrayBuffer
*/
exportModel () {
const encoder = encoding.createEncoder()
message.writeStructs(encoder, this, new Map())
message.writeDeleteSet(encoder, this)
return encoding.toBuffer(encoder)
}
_beforeChange () {}
_callObserver (transaction, subs, remote) {}
/**
* Changes that happen inside of a transaction are bundled. This means that
* the observer fires _after_ the transaction is finished and that all changes
* that happened inside of the transaction are sent as one message to the
* other peers.
*
* @param {Function} f The function that should be executed as a transaction
* @param {?Boolean} remote Optional. Whether this transaction is initiated by
* a remote peer. This should not be set manually!
* Defaults to false.
*/
transact (f, remote = false) {
let initialCall = this._transaction === null
if (initialCall) {
this._transaction = new Transaction(this)
this.emit('beforeTransaction', this, this._transaction, remote)
}
try {
f(this)
} catch (e) {
console.error(e)
}
if (initialCall) {
this.emit('beforeObserverCalls', this, this._transaction, remote)
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.changedParentTypes.forEach((events, type) => {
if (!type._deleted) {
events = events
.filter(event =>
!event.target._deleted
)
events
.forEach(event => {
event.currentTarget = type
})
// we don't have to check for events.length
// because there is no way events is empty..
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
* and do not overwrite each other. I.e.
* `y.define(name, type) === y.define(name, type)`
*
* After this method is called, the type is also available on `y._map.get(name)`.
*
* *Best Practices:*
* Either define all types right after the Yjs instance is created or always
* use `y.define(..)` when accessing a type.
*
* @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(..)
*
* // Option2
* const y = new Y(..)
* // .. when accessing the type use `y.define(..)`
* y.define('myArray', YArray).insert(..)
* y.define('myMap', YMap).set(..)
*
* @param {String} name
* @param {Function} TypeConstructor The constructor of the type definition
* @returns {Type} 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')
}
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
*/
get (name) {
return this._map.get(name)
}
/**
* Disconnect from the room, and destroy all traces of this Yjs instance.
*/
destroy () {
this.emit('destroyed', true)
super.destroy()
this._map = null
this.os = null
this.ds = null
this.ss = null
}
}