2017-10-14 23:03:24 +02:00
|
|
|
import DeleteStore from './Store/DeleteStore.js'
|
|
|
|
|
import OperationStore from './Store/OperationStore.js'
|
|
|
|
|
import StateStore from './Store/StateStore.js'
|
|
|
|
|
import { generateUserID } from './Util/generateUserID.js'
|
2018-03-06 05:22:18 +01:00
|
|
|
import RootID from './Util/ID/RootID.js'
|
2017-10-16 04:53:12 +02:00
|
|
|
import NamedEventHandler from './Util/NamedEventHandler.js'
|
2017-10-22 19:12:50 +02:00
|
|
|
import Transaction from './Transaction.js'
|
2017-10-19 17:36:28 +02:00
|
|
|
|
2018-03-23 01:55:47 +01:00
|
|
|
export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
|
|
|
|
|
|
2018-03-06 03:17:36 +01:00
|
|
|
/**
|
|
|
|
|
* A positive natural number including zero: 0, 1, 2, ..
|
|
|
|
|
*
|
|
|
|
|
* @typedef {number} NaturalNumber
|
|
|
|
|
*/
|
2018-01-29 16:39:09 -08:00
|
|
|
|
2018-03-05 03:03:40 +01:00
|
|
|
/**
|
|
|
|
|
* 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)} encodable
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A Yjs instance handles the state of shared data.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} room Users in the same room share the same content
|
|
|
|
|
* @param {Object} opts Connector definition
|
|
|
|
|
* @param {AbstractPersistence} persistence Persistence adapter instance
|
|
|
|
|
*/
|
2017-10-16 04:53:12 +02:00
|
|
|
export default class Y extends NamedEventHandler {
|
2018-01-10 00:11:25 +01:00
|
|
|
constructor (room, opts, persistence) {
|
2017-10-16 04:53:12 +02:00
|
|
|
super()
|
2018-03-05 03:03:40 +01:00
|
|
|
/**
|
|
|
|
|
* The room name that this Yjs instance connects to.
|
|
|
|
|
* @type {String}
|
|
|
|
|
*/
|
2018-01-10 00:11:25 +01:00
|
|
|
this.room = room
|
|
|
|
|
if (opts != null) {
|
|
|
|
|
opts.connector.room = room
|
|
|
|
|
}
|
2018-01-08 17:30:30 +01:00
|
|
|
this._contentReady = false
|
2017-10-16 04:53:12 +02:00
|
|
|
this._opts = opts
|
2018-03-23 01:55:47 +01:00
|
|
|
if (typeof opts.userID !== 'number') {
|
|
|
|
|
this.userID = generateUserID()
|
|
|
|
|
} else {
|
|
|
|
|
this.userID = opts.userID
|
|
|
|
|
}
|
2018-03-05 03:03:40 +01:00
|
|
|
// TODO: This should be a Map so we can use encodables as keys
|
2017-11-07 18:31:04 -08:00
|
|
|
this.share = {}
|
2017-10-11 03:41:54 +02:00
|
|
|
this.ds = new DeleteStore(this)
|
|
|
|
|
this.os = new OperationStore(this)
|
|
|
|
|
this.ss = new StateStore(this)
|
|
|
|
|
this._missingStructs = new Map()
|
2017-10-16 04:53:12 +02:00
|
|
|
this._readyToIntegrate = []
|
2017-10-22 19:12:50 +02:00
|
|
|
this._transaction = null
|
2018-03-05 03:03:40 +01:00
|
|
|
/**
|
|
|
|
|
* The {@link AbstractConnector}.that is used by this Yjs instance.
|
|
|
|
|
* @type {AbstractConnector}
|
|
|
|
|
*/
|
2017-12-24 03:18:00 +01:00
|
|
|
this.connector = null
|
|
|
|
|
this.connected = false
|
|
|
|
|
let initConnection = () => {
|
2018-01-10 00:11:25 +01:00
|
|
|
if (opts != null) {
|
|
|
|
|
this.connector = new Y[opts.connector.name](this, opts.connector)
|
|
|
|
|
this.connected = true
|
|
|
|
|
this.emit('connectorReady')
|
|
|
|
|
}
|
2017-12-24 03:18:00 +01:00
|
|
|
}
|
2018-03-05 03:03:40 +01:00
|
|
|
/**
|
|
|
|
|
* The {@link AbstractPersistence} that is used by this Yjs instance.
|
|
|
|
|
* @type {AbstractPersistence}
|
|
|
|
|
*/
|
|
|
|
|
this.persistence = null
|
2018-01-08 17:30:30 +01:00
|
|
|
if (persistence != null) {
|
2017-12-24 03:18:00 +01:00
|
|
|
this.persistence = persistence
|
|
|
|
|
persistence._init(this).then(initConnection)
|
|
|
|
|
} else {
|
|
|
|
|
initConnection()
|
|
|
|
|
}
|
2018-03-23 01:55:47 +01:00
|
|
|
// for compatibility with isParentOf
|
|
|
|
|
this._parent = null
|
2017-10-19 17:36:28 +02:00
|
|
|
}
|
2018-01-08 17:30:30 +01:00
|
|
|
_setContentReady () {
|
|
|
|
|
if (!this._contentReady) {
|
|
|
|
|
this._contentReady = true
|
|
|
|
|
this.emit('content')
|
2018-01-08 02:28:46 +01:00
|
|
|
}
|
|
|
|
|
}
|
2018-01-18 18:44:20 +01:00
|
|
|
whenContentReady () {
|
|
|
|
|
if (this._contentReady) {
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
} else {
|
|
|
|
|
return new Promise(resolve => {
|
|
|
|
|
this.once('content', resolve)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-10-19 17:36:28 +02:00
|
|
|
_beforeChange () {}
|
2018-03-05 03:03:40 +01:00
|
|
|
/**
|
|
|
|
|
* 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
|
2018-03-06 03:17:36 +01:00
|
|
|
* @param {?Boolean} remote Optional. Whether this transaction is initiated by
|
|
|
|
|
* a remote peer. This should not be set manually!
|
|
|
|
|
* Defaults to false.
|
2018-03-05 03:03:40 +01:00
|
|
|
*/
|
2017-10-22 19:12:50 +02:00
|
|
|
transact (f, remote = false) {
|
|
|
|
|
let initialCall = this._transaction === null
|
|
|
|
|
if (initialCall) {
|
|
|
|
|
this._transaction = new Transaction(this)
|
2017-11-12 13:34:23 -08:00
|
|
|
this.emit('beforeTransaction', this, this._transaction, remote)
|
2017-10-22 19:12:50 +02:00
|
|
|
}
|
2017-10-19 17:36:28 +02:00
|
|
|
try {
|
2017-10-27 22:28:32 +02:00
|
|
|
f(this)
|
2017-10-19 17:36:28 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e)
|
|
|
|
|
}
|
2017-10-22 19:12:50 +02:00
|
|
|
if (initialCall) {
|
2017-11-26 14:39:47 -08:00
|
|
|
this.emit('beforeObserverCalls', this, this._transaction, remote)
|
2017-11-12 13:34:23 -08:00
|
|
|
const transaction = this._transaction
|
|
|
|
|
this._transaction = null
|
2017-10-22 19:12:50 +02:00
|
|
|
// emit change events on changed types
|
2017-11-12 13:34:23 -08:00
|
|
|
transaction.changedTypes.forEach(function (subs, type) {
|
2017-11-07 22:44:43 -08:00
|
|
|
if (!type._deleted) {
|
2017-11-12 13:34:23 -08:00
|
|
|
type._callObserver(transaction, subs, remote)
|
2017-11-07 22:44:43 -08:00
|
|
|
}
|
|
|
|
|
})
|
2017-11-12 13:34:23 -08:00
|
|
|
transaction.changedParentTypes.forEach(function (events, type) {
|
2017-11-07 22:44:43 -08:00
|
|
|
if (!type._deleted) {
|
2017-11-08 17:31:12 -08:00
|
|
|
events = events
|
|
|
|
|
.filter(event =>
|
|
|
|
|
!event.target._deleted
|
|
|
|
|
)
|
|
|
|
|
events
|
|
|
|
|
.forEach(event => {
|
|
|
|
|
event.currentTarget = type
|
|
|
|
|
})
|
2017-11-07 22:44:43 -08:00
|
|
|
// we don't have to check for events.length
|
|
|
|
|
// because there is no way events is empty..
|
2017-11-12 13:34:23 -08:00
|
|
|
type._deepEventHandler.callEventListeners(transaction, events)
|
2017-11-07 22:44:43 -08:00
|
|
|
}
|
2017-10-22 19:12:50 +02:00
|
|
|
})
|
|
|
|
|
// when all changes & events are processed, emit afterTransaction event
|
2017-11-12 13:34:23 -08:00
|
|
|
this.emit('afterTransaction', this, transaction, remote)
|
2017-10-19 17:36:28 +02:00
|
|
|
}
|
2017-10-16 04:53:12 +02:00
|
|
|
}
|
2018-03-05 03:03:40 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @private
|
|
|
|
|
* Fake _start for root properties (y.set('name', type))
|
|
|
|
|
*/
|
2017-10-16 04:53:12 +02:00
|
|
|
get _start () {
|
|
|
|
|
return null
|
|
|
|
|
}
|
2018-03-05 03:03:40 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @private
|
|
|
|
|
* Fake _start for root properties (y.set('name', type))
|
|
|
|
|
*/
|
2017-10-16 04:53:12 +02:00
|
|
|
set _start (start) {
|
|
|
|
|
return null
|
2017-10-11 03:41:54 +02:00
|
|
|
}
|
2018-03-05 03:03:40 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.share[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.share[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 {YType Constructor} TypeConstructor The constructor of the type definition
|
|
|
|
|
* @returns {YType} The created type
|
|
|
|
|
*/
|
2017-11-07 18:31:04 -08:00
|
|
|
define (name, TypeConstructor) {
|
2017-10-11 03:41:54 +02:00
|
|
|
let id = new RootID(name, TypeConstructor)
|
|
|
|
|
let type = this.os.get(id)
|
2017-11-07 18:31:04 -08:00
|
|
|
if (this.share[name] === undefined) {
|
|
|
|
|
this.share[name] = type
|
2017-11-26 14:39:47 -08:00
|
|
|
} else if (this.share[name] !== type) {
|
|
|
|
|
throw new Error('Type is already defined with a different constructor')
|
2017-10-11 03:41:54 +02:00
|
|
|
}
|
|
|
|
|
return type
|
|
|
|
|
}
|
2018-03-05 03:03:40 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
2017-11-07 18:31:04 -08:00
|
|
|
get (name) {
|
|
|
|
|
return this.share[name]
|
|
|
|
|
}
|
2018-03-05 03:03:40 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Disconnect this Yjs Instance from the network. The connector will
|
|
|
|
|
* unsubscribe from the room and document updates are not shared anymore.
|
|
|
|
|
*/
|
2017-10-11 03:41:54 +02:00
|
|
|
disconnect () {
|
|
|
|
|
if (this.connected) {
|
|
|
|
|
this.connected = false
|
|
|
|
|
return this.connector.disconnect()
|
|
|
|
|
} else {
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-03-05 03:03:40 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* If disconnected, tell the connector to reconnect to the room.
|
|
|
|
|
*/
|
2017-10-11 03:41:54 +02:00
|
|
|
reconnect () {
|
|
|
|
|
if (!this.connected) {
|
|
|
|
|
this.connected = true
|
|
|
|
|
return this.connector.reconnect()
|
|
|
|
|
} else {
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-03-05 03:03:40 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Disconnect from the room, and destroy all traces of this Yjs instance.
|
|
|
|
|
* Persisted data will remain until removed by the persistence adapter.
|
|
|
|
|
*/
|
2017-10-11 03:41:54 +02:00
|
|
|
destroy () {
|
2017-12-24 03:18:00 +01:00
|
|
|
super.destroy()
|
2017-10-11 03:41:54 +02:00
|
|
|
this.share = null
|
2018-01-10 00:11:25 +01:00
|
|
|
if (this.connector != null) {
|
|
|
|
|
if (this.connector.destroy != null) {
|
|
|
|
|
this.connector.destroy()
|
|
|
|
|
} else {
|
|
|
|
|
this.connector.disconnect()
|
|
|
|
|
}
|
2017-10-11 03:41:54 +02:00
|
|
|
}
|
2017-12-24 03:18:00 +01:00
|
|
|
if (this.persistence !== null) {
|
|
|
|
|
this.persistence.deinit(this)
|
|
|
|
|
this.persistence = null
|
|
|
|
|
}
|
2017-10-11 03:41:54 +02:00
|
|
|
this.os = null
|
|
|
|
|
this.ds = null
|
|
|
|
|
this.ss = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Y.extend = function extendYjs () {
|
|
|
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
|
|
|
var f = arguments[i]
|
|
|
|
|
if (typeof f === 'function') {
|
|
|
|
|
f(Y)
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('Expected a function!')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|