# src/Y.js

    import DeleteStore from './Store/DeleteStore.js'
    import OperationStore from './Store/OperationStore.js'
    import StateStore from './Store/StateStore.js'
    import { generateRandomUint32 } from './Util/generateRandomUint32.js'
    import RootID from './Util/ID/RootID.js'
    import NamedEventHandler from './Util/NamedEventHandler.js'
    import Transaction from './Transaction.js'

    export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'

    /**
     * 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, opts, persistence) {
        super()
        /**
         * The room name that this Yjs instance connects to.
         * @type {String}
         */
        this.room = room
        if (opts != null) {
          opts.connector.room = room
        }
        this._contentReady = false
        this._opts = opts
        if (typeof opts.userID !== 'number') {
          this.userID = generateRandomUint32()
        } else {
          this.userID = opts.userID
        }
        // 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 (opts != null) {
            this.connector = new Y[opts.connector.name](this, opts.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!')
        }
      }
    }