278 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			278 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import DeleteStore from './Store/DeleteStore.mjs'
 | |
| import OperationStore from './Store/OperationStore.mjs'
 | |
| import StateStore from './Store/StateStore.mjs'
 | |
| import { generateRandomUint32 } from './Util/generateRandomUint32.mjs'
 | |
| import RootID from './Util/ID/RootID.mjs'
 | |
| import NamedEventHandler from './Util/NamedEventHandler.mjs'
 | |
| import Transaction from './Transaction.mjs'
 | |
| 
 | |
| export { default as DomBinding } from './Bindings/DomBinding/DomBinding.mjs'
 | |
| 
 | |
| /**
 | |
|  * 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
 | |
|  */
 | |
| export default class Y extends NamedEventHandler {
 | |
|   constructor (room, connector, persistence, conf = {}) {
 | |
|     super()
 | |
|     this.gcEnabled = conf.gc || false
 | |
|     /**
 | |
|      * The room name that this Yjs instance connects to.
 | |
|      * @type {String}
 | |
|      */
 | |
|     this.room = room
 | |
|     this._contentReady = false
 | |
|     this.userID = generateRandomUint32()
 | |
|     // TODO: This should be a Map so we can use encodables as keys
 | |
|     this.share = {}
 | |
|     this.ds = new DeleteStore(this)
 | |
|     this.os = new OperationStore(this)
 | |
|     this.ss = new StateStore(this)
 | |
|     this._missingStructs = new Map()
 | |
|     this._readyToIntegrate = []
 | |
|     this._transaction = null
 | |
|     /**
 | |
|      * The {@link AbstractConnector}.that is used by this Yjs instance.
 | |
|      * @type {AbstractConnector}
 | |
|      */
 | |
|     this.connector = null
 | |
|     this.connected = false
 | |
|     let initConnection = () => {
 | |
|       if (connector != null) {
 | |
|         if (connector.constructor === Object) {
 | |
|           connector.connector.room = room
 | |
|           this.connector = new Y[connector.connector.name](this, connector.connector)
 | |
|           this.connected = true
 | |
|           this.emit('connectorReady')
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     /**
 | |
|      * The {@link AbstractPersistence} that is used by this Yjs instance.
 | |
|      * @type {AbstractPersistence}
 | |
|      */
 | |
|     this.persistence = null
 | |
|     if (persistence != null) {
 | |
|       this.persistence = persistence
 | |
|       persistence._init(this).then(initConnection)
 | |
|     } else {
 | |
|       initConnection()
 | |
|     }
 | |
|     // for compatibility with isParentOf
 | |
|     this._parent = null
 | |
|     this._hasUndoManager = false
 | |
|   }
 | |
|   _setContentReady () {
 | |
|     if (!this._contentReady) {
 | |
|       this._contentReady = true
 | |
|       this.emit('content')
 | |
|     }
 | |
|   }
 | |
|   whenContentReady () {
 | |
|     if (this._contentReady) {
 | |
|       return Promise.resolve()
 | |
|     } else {
 | |
|       return new Promise(resolve => {
 | |
|         this.once('content', resolve)
 | |
|       })
 | |
|     }
 | |
|   }
 | |
|   _beforeChange () {}
 | |
|   /**
 | |
|    * 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(function (subs, type) {
 | |
|         if (!type._deleted) {
 | |
|           type._callObserver(transaction, subs, remote)
 | |
|         }
 | |
|       })
 | |
|       transaction.changedParentTypes.forEach(function (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)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @private
 | |
|    * Fake _start for root properties (y.set('name', type))
 | |
|    */
 | |
|   get _start () {
 | |
|     return null
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @private
 | |
|    * Fake _start for root properties (y.set('name', type))
 | |
|    */
 | |
|   set _start (start) {
 | |
|     return null
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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
 | |
|    */
 | |
|   define (name, TypeConstructor) {
 | |
|     let id = new RootID(name, TypeConstructor)
 | |
|     let type = this.os.get(id)
 | |
|     if (this.share[name] === undefined) {
 | |
|       this.share[name] = type
 | |
|     } else if (this.share[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.share[name]
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Disconnect this Yjs Instance from the network. The connector will
 | |
|    * unsubscribe from the room and document updates are not shared anymore.
 | |
|    */
 | |
|   disconnect () {
 | |
|     if (this.connected) {
 | |
|       this.connected = false
 | |
|       return this.connector.disconnect()
 | |
|     } else {
 | |
|       return Promise.resolve()
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * If disconnected, tell the connector to reconnect to the room.
 | |
|    */
 | |
|   reconnect () {
 | |
|     if (!this.connected) {
 | |
|       this.connected = true
 | |
|       return this.connector.reconnect()
 | |
|     } else {
 | |
|       return Promise.resolve()
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Disconnect from the room, and destroy all traces of this Yjs instance.
 | |
|    * Persisted data will remain until removed by the persistence adapter.
 | |
|    */
 | |
|   destroy () {
 | |
|     super.destroy()
 | |
|     this.share = null
 | |
|     if (this.connector != null) {
 | |
|       if (this.connector.destroy != null) {
 | |
|         this.connector.destroy()
 | |
|       } else {
 | |
|         this.connector.disconnect()
 | |
|       }
 | |
|     }
 | |
|     if (this.persistence !== null) {
 | |
|       this.persistence.deinit(this)
 | |
|       this.persistence = null
 | |
|     }
 | |
|     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!')
 | |
|     }
 | |
|   }
 | |
| }
 |