import { writeTokenSyncRequest, writeAuthenticated, writePermissionDenied, AuthMessageType, WsReadyStates, ResetConnection, ConnectionTimeout, Unauthorized, Forbidden, awarenessStatesToArray } from '@hocuspocus/common';
import * as Y from 'yjs';
import { Doc, applyUpdate, encodeStateAsUpdate } from 'yjs';
import { Mutex } from 'async-mutex';
import crypto from 'node:crypto';
import { URLSearchParams } from 'node:url';
import { createServer } from 'node:http';
import kleur from 'kleur';
import { WebSocketServer } from 'ws';

/* eslint-env browser */

const BIT8 = 128;
const BITS7 = 127;

/**
 * Common Math expressions.
 *
 * @module math
 */

const floor = Math.floor;

/**
 * @function
 * @param {number} a
 * @param {number} b
 * @return {number} The smaller element of a and b
 */
const min = (a, b) => a < b ? a : b;

/**
 * @function
 * @param {number} a
 * @param {number} b
 * @return {number} The bigger element of a and b
 */
const max = (a, b) => a > b ? a : b;

/**
 * Utility helpers for working with numbers.
 *
 * @module number
 */


const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;

/**
 * Utility module to work with sets.
 *
 * @module set
 */

const create$2 = () => new Set();

/**
 * Utility module to work with Arrays.
 *
 * @module array
 */


/**
 * Transforms something array-like to an actual Array.
 *
 * @function
 * @template T
 * @param {ArrayLike<T>|Iterable<T>} arraylike
 * @return {T}
 */
const from = Array.from;

/**
 * @param {string} str
 * @return {Uint8Array}
 */
const _encodeUtf8Polyfill = str => {
  const encodedString = unescape(encodeURIComponent(str));
  const len = encodedString.length;
  const buf = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    buf[i] = /** @type {number} */ (encodedString.codePointAt(i));
  }
  return buf
};

/* c8 ignore next */
const utf8TextEncoder = /** @type {TextEncoder} */ (typeof TextEncoder !== 'undefined' ? new TextEncoder() : null);

/**
 * @param {string} str
 * @return {Uint8Array}
 */
const _encodeUtf8Native = str => utf8TextEncoder.encode(str);

/**
 * @param {string} str
 * @return {Uint8Array}
 */
/* c8 ignore next */
const encodeUtf8 = utf8TextEncoder ? _encodeUtf8Native : _encodeUtf8Polyfill;

/* c8 ignore next */
let utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });

/* c8 ignore start */
if (utf8TextDecoder && utf8TextDecoder.decode(new Uint8Array()).length === 1) {
  // Safari doesn't handle BOM correctly.
  // This fixes a bug in Safari 13.0.5 where it produces a BOM the first time it is called.
  // utf8TextDecoder.decode(new Uint8Array()).length === 1 on the first call and
  // utf8TextDecoder.decode(new Uint8Array()).length === 1 on the second call
  // Another issue is that from then on no BOM chars are recognized anymore
  /* c8 ignore next */
  utf8TextDecoder = null;
}

/**
 * Error helpers.
 *
 * @module error
 */

/**
 * @param {string} s
 * @return {Error}
 */
/* c8 ignore next */
const create$1 = s => new Error(s);

/**
 * Efficient schema-less binary encoding with support for variable length encoding.
 *
 * Use [lib0/encoding] with [lib0/decoding]. Every encoding function has a corresponding decoding function.
 *
 * Encodes numbers in little-endian order (least to most significant byte order)
 * and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/)
 * which is also used in Protocol Buffers.
 *
 * ```js
 * // encoding step
 * const encoder = encoding.createEncoder()
 * encoding.writeVarUint(encoder, 256)
 * encoding.writeVarString(encoder, 'Hello world!')
 * const buf = encoding.toUint8Array(encoder)
 * ```
 *
 * ```js
 * // decoding step
 * const decoder = decoding.createDecoder(buf)
 * decoding.readVarUint(decoder) // => 256
 * decoding.readVarString(decoder) // => 'Hello world!'
 * decoding.hasContent(decoder) // => false - all data is read
 * ```
 *
 * @module encoding
 */


/**
 * A BinaryEncoder handles the encoding to an Uint8Array.
 */
class Encoder {
  constructor () {
    this.cpos = 0;
    this.cbuf = new Uint8Array(100);
    /**
     * @type {Array<Uint8Array>}
     */
    this.bufs = [];
  }
}

/**
 * @function
 * @return {Encoder}
 */
const createEncoder = () => new Encoder();

/**
 * The current length of the encoded data.
 *
 * @function
 * @param {Encoder} encoder
 * @return {number}
 */
const length$1 = encoder => {
  let len = encoder.cpos;
  for (let i = 0; i < encoder.bufs.length; i++) {
    len += encoder.bufs[i].length;
  }
  return len
};

/**
 * Transform to Uint8Array.
 *
 * @function
 * @param {Encoder} encoder
 * @return {Uint8Array} The created ArrayBuffer.
 */
const toUint8Array = encoder => {
  const uint8arr = new Uint8Array(length$1(encoder));
  let curPos = 0;
  for (let i = 0; i < encoder.bufs.length; i++) {
    const d = encoder.bufs[i];
    uint8arr.set(d, curPos);
    curPos += d.length;
  }
  uint8arr.set(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos), curPos);
  return uint8arr
};

/**
 * Write one byte to the encoder.
 *
 * @function
 * @param {Encoder} encoder
 * @param {number} num The byte that is to be encoded.
 */
const write = (encoder, num) => {
  const bufferLen = encoder.cbuf.length;
  if (encoder.cpos === bufferLen) {
    encoder.bufs.push(encoder.cbuf);
    encoder.cbuf = new Uint8Array(bufferLen * 2);
    encoder.cpos = 0;
  }
  encoder.cbuf[encoder.cpos++] = num;
};

/**
 * Write a variable length unsigned integer. Max encodable integer is 2^53.
 *
 * @function
 * @param {Encoder} encoder
 * @param {number} num The number that is to be encoded.
 */
const writeVarUint = (encoder, num) => {
  while (num > BITS7) {
    write(encoder, BIT8 | (BITS7 & num));
    num = floor(num / 128); // shift >>> 7
  }
  write(encoder, BITS7 & num);
};

/**
 * A cache to store strings temporarily
 */
const _strBuffer = new Uint8Array(30000);
const _maxStrBSize = _strBuffer.length / 3;

/**
 * Write a variable length string.
 *
 * @function
 * @param {Encoder} encoder
 * @param {String} str The string that is to be encoded.
 */
const _writeVarStringNative = (encoder, str) => {
  if (str.length < _maxStrBSize) {
    // We can encode the string into the existing buffer
    /* c8 ignore next */
    const written = utf8TextEncoder.encodeInto(str, _strBuffer).written || 0;
    writeVarUint(encoder, written);
    for (let i = 0; i < written; i++) {
      write(encoder, _strBuffer[i]);
    }
  } else {
    writeVarUint8Array(encoder, encodeUtf8(str));
  }
};

/**
 * Write a variable length string.
 *
 * @function
 * @param {Encoder} encoder
 * @param {String} str The string that is to be encoded.
 */
const _writeVarStringPolyfill = (encoder, str) => {
  const encodedString = unescape(encodeURIComponent(str));
  const len = encodedString.length;
  writeVarUint(encoder, len);
  for (let i = 0; i < len; i++) {
    write(encoder, /** @type {number} */ (encodedString.codePointAt(i)));
  }
};

/**
 * Write a variable length string.
 *
 * @function
 * @param {Encoder} encoder
 * @param {String} str The string that is to be encoded.
 */
/* c8 ignore next */
const writeVarString = (utf8TextEncoder && /** @type {any} */ (utf8TextEncoder).encodeInto) ? _writeVarStringNative : _writeVarStringPolyfill;

/**
 * Append fixed-length Uint8Array to the encoder.
 *
 * @function
 * @param {Encoder} encoder
 * @param {Uint8Array} uint8Array
 */
const writeUint8Array = (encoder, uint8Array) => {
  const bufferLen = encoder.cbuf.length;
  const cpos = encoder.cpos;
  const leftCopyLen = min(bufferLen - cpos, uint8Array.length);
  const rightCopyLen = uint8Array.length - leftCopyLen;
  encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos);
  encoder.cpos += leftCopyLen;
  if (rightCopyLen > 0) {
    // Still something to write, write right half..
    // Append new buffer
    encoder.bufs.push(encoder.cbuf);
    // must have at least size of remaining buffer
    encoder.cbuf = new Uint8Array(max(bufferLen * 2, rightCopyLen));
    // copy array
    encoder.cbuf.set(uint8Array.subarray(leftCopyLen));
    encoder.cpos = rightCopyLen;
  }
};

/**
 * Append an Uint8Array to Encoder.
 *
 * @function
 * @param {Encoder} encoder
 * @param {Uint8Array} uint8Array
 */
const writeVarUint8Array = (encoder, uint8Array) => {
  writeVarUint(encoder, uint8Array.byteLength);
  writeUint8Array(encoder, uint8Array);
};

/**
 * Efficient schema-less binary decoding with support for variable length encoding.
 *
 * Use [lib0/decoding] with [lib0/encoding]. Every encoding function has a corresponding decoding function.
 *
 * Encodes numbers in little-endian order (least to most significant byte order)
 * and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/)
 * which is also used in Protocol Buffers.
 *
 * ```js
 * // encoding step
 * const encoder = encoding.createEncoder()
 * encoding.writeVarUint(encoder, 256)
 * encoding.writeVarString(encoder, 'Hello world!')
 * const buf = encoding.toUint8Array(encoder)
 * ```
 *
 * ```js
 * // decoding step
 * const decoder = decoding.createDecoder(buf)
 * decoding.readVarUint(decoder) // => 256
 * decoding.readVarString(decoder) // => 'Hello world!'
 * decoding.hasContent(decoder) // => false - all data is read
 * ```
 *
 * @module decoding
 */


const errorUnexpectedEndOfArray = create$1('Unexpected end of array');
const errorIntegerOutOfRange = create$1('Integer out of Range');

/**
 * A Decoder handles the decoding of an Uint8Array.
 */
class Decoder {
  /**
   * @param {Uint8Array} uint8Array Binary data to decode
   */
  constructor (uint8Array) {
    /**
     * Decoding target.
     *
     * @type {Uint8Array}
     */
    this.arr = uint8Array;
    /**
     * Current decoding position.
     *
     * @type {number}
     */
    this.pos = 0;
  }
}

/**
 * @function
 * @param {Uint8Array} uint8Array
 * @return {Decoder}
 */
const createDecoder = uint8Array => new Decoder(uint8Array);

/**
 * Create an Uint8Array view of the next `len` bytes and advance the position by `len`.
 *
 * Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks.
 *            Use `buffer.copyUint8Array` to copy the result into a new Uint8Array.
 *
 * @function
 * @param {Decoder} decoder The decoder instance
 * @param {number} len The length of bytes to read
 * @return {Uint8Array}
 */
const readUint8Array = (decoder, len) => {
  const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len);
  decoder.pos += len;
  return view
};

/**
 * Read variable length Uint8Array.
 *
 * Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks.
 *            Use `buffer.copyUint8Array` to copy the result into a new Uint8Array.
 *
 * @function
 * @param {Decoder} decoder
 * @return {Uint8Array}
 */
const readVarUint8Array = decoder => readUint8Array(decoder, readVarUint(decoder));

/**
 * Read one byte as unsigned integer.
 * @function
 * @param {Decoder} decoder The decoder instance
 * @return {number} Unsigned 8-bit integer
 */
const readUint8 = decoder => decoder.arr[decoder.pos++];

/**
 * Read unsigned integer (32bit) with variable length.
 * 1/8th of the storage is used as encoding overhead.
 *  * numbers < 2^7 is stored in one bytlength
 *  * numbers < 2^14 is stored in two bylength
 *
 * @function
 * @param {Decoder} decoder
 * @return {number} An unsigned integer.length
 */
const readVarUint = decoder => {
  let num = 0;
  let mult = 1;
  const len = decoder.arr.length;
  while (decoder.pos < len) {
    const r = decoder.arr[decoder.pos++];
    // num = num | ((r & binary.BITS7) << len)
    num = num + (r & BITS7) * mult; // shift $r << (7*#iterations) and add it to num
    mult *= 128; // next iteration, shift 7 "more" to the left
    if (r < BIT8) {
      return num
    }
    /* c8 ignore start */
    if (num > MAX_SAFE_INTEGER) {
      throw errorIntegerOutOfRange
    }
    /* c8 ignore stop */
  }
  throw errorUnexpectedEndOfArray
};

/**
 * We don't test this function anymore as we use native decoding/encoding by default now.
 * Better not modify this anymore..
 *
 * Transforming utf8 to a string is pretty expensive. The code performs 10x better
 * when String.fromCodePoint is fed with all characters as arguments.
 * But most environments have a maximum number of arguments per functions.
 * For effiency reasons we apply a maximum of 10000 characters at once.
 *
 * @function
 * @param {Decoder} decoder
 * @return {String} The read String.
 */
/* c8 ignore start */
const _readVarStringPolyfill = decoder => {
  let remainingLen = readVarUint(decoder);
  if (remainingLen === 0) {
    return ''
  } else {
    let encodedString = String.fromCodePoint(readUint8(decoder)); // remember to decrease remainingLen
    if (--remainingLen < 100) { // do not create a Uint8Array for small strings
      while (remainingLen--) {
        encodedString += String.fromCodePoint(readUint8(decoder));
      }
    } else {
      while (remainingLen > 0) {
        const nextLen = remainingLen < 10000 ? remainingLen : 10000;
        // this is dangerous, we create a fresh array view from the existing buffer
        const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen);
        decoder.pos += nextLen;
        // Starting with ES5.1 we can supply a generic array-like object as arguments
        encodedString += String.fromCodePoint.apply(null, /** @type {any} */ (bytes));
        remainingLen -= nextLen;
      }
    }
    return decodeURIComponent(escape(encodedString))
  }
};
/* c8 ignore stop */

/**
 * @function
 * @param {Decoder} decoder
 * @return {String} The read String
 */
const _readVarStringNative = decoder =>
  /** @type any */ (utf8TextDecoder).decode(readVarUint8Array(decoder));

/**
 * Read string of variable length
 * * varUint is used to store the length of the string
 *
 * @function
 * @param {Decoder} decoder
 * @return {String} The read String
 *
 */
/* c8 ignore next */
const readVarString = utf8TextDecoder ? _readVarStringNative : _readVarStringPolyfill;

class IncomingMessage {
    constructor(input) {
        if (!(input instanceof Uint8Array)) {
            input = new Uint8Array(input);
        }
        this.decoder = createDecoder(input);
    }
    get encoder() {
        if (!this.encoderInternal) {
            this.encoderInternal = createEncoder();
        }
        return this.encoderInternal;
    }
    readVarUint8Array() {
        return readVarUint8Array(this.decoder);
    }
    peekVarUint8Array() {
        const { pos } = this.decoder;
        const result = readVarUint8Array(this.decoder);
        this.decoder.pos = pos;
        return result;
    }
    readVarUint() {
        return readVarUint(this.decoder);
    }
    readVarString() {
        return readVarString(this.decoder);
    }
    toUint8Array() {
        return toUint8Array(this.encoder);
    }
    writeVarUint(type) {
        writeVarUint(this.encoder, type);
    }
    writeVarString(string) {
        writeVarString(this.encoder, string);
    }
    get length() {
        return length$1(this.encoder);
    }
}

/**
 * Utility module to work with time.
 *
 * @module time
 */


/**
 * Return current unix time.
 *
 * @return {number}
 */
const getUnixTime = Date.now;

/**
 * Utility module to work with key-value stores.
 *
 * @module map
 */

/**
 * Creates a new Map instance.
 *
 * @function
 * @return {Map<any, any>}
 *
 * @function
 */
const create = () => new Map();

/**
 * Get map property. Create T if property is undefined and set T on map.
 *
 * ```js
 * const listeners = map.setIfUndefined(events, 'eventName', set.create)
 * listeners.add(listener)
 * ```
 *
 * @function
 * @template {Map<any, any>} MAP
 * @template {MAP extends Map<any,infer V> ? function():V : unknown} CF
 * @param {MAP} map
 * @param {MAP extends Map<infer K,any> ? K : unknown} key
 * @param {CF} createT
 * @return {ReturnType<CF>}
 */
const setIfUndefined = (map, key, createT) => {
  let set = map.get(key);
  if (set === undefined) {
    map.set(key, set = createT());
  }
  return set
};

/**
 * Observable class prototype.
 *
 * @module observable
 */


/* c8 ignore start */
/**
 * Handles named events.
 *
 * @deprecated
 * @template N
 */
class Observable {
  constructor () {
    /**
     * Some desc.
     * @type {Map<N, any>}
     */
    this._observers = create();
  }

  /**
   * @param {N} name
   * @param {function} f
   */
  on (name, f) {
    setIfUndefined(this._observers, name, create$2).add(f);
  }

  /**
   * @param {N} name
   * @param {function} f
   */
  once (name, f) {
    /**
     * @param  {...any} args
     */
    const _f = (...args) => {
      this.off(name, _f);
      f(...args);
    };
    this.on(name, _f);
  }

  /**
   * @param {N} name
   * @param {function} f
   */
  off (name, f) {
    const observers = this._observers.get(name);
    if (observers !== undefined) {
      observers.delete(f);
      if (observers.size === 0) {
        this._observers.delete(name);
      }
    }
  }

  /**
   * Emit a named event. All registered event listeners that listen to the
   * specified name will receive the event.
   *
   * @todo This should catch exceptions
   *
   * @param {N} name The event name.
   * @param {Array<any>} args The arguments that are applied to the event listener.
   */
  emit (name, args) {
    // copy all listeners to an array first to make sure that no event is emitted to listeners that are subscribed while the event handler is called.
    return from((this._observers.get(name) || create()).values()).forEach(f => f(...args))
  }

  destroy () {
    this._observers = create();
  }
}
/* c8 ignore end */

/**
 * Utility functions for working with EcmaScript objects.
 *
 * @module object
 */


/**
 * @param {Object<string,any>} obj
 */
const keys = Object.keys;

/**
 * @deprecated use object.size instead
 * @param {Object<string,any>} obj
 * @return {number}
 */
const length = obj => keys(obj).length;

/**
 * Calls `Object.prototype.hasOwnProperty`.
 *
 * @param {any} obj
 * @param {string|symbol} key
 * @return {boolean}
 */
const hasProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);

/**
 * Common functions and function call helpers.
 *
 * @module function
 */


/**
 * @template T
 *
 * @param {T} a
 * @param {T} b
 * @return {boolean}
 */
const equalityStrict = (a, b) => a === b;

/* c8 ignore start */

/**
 * @param {any} a
 * @param {any} b
 * @return {boolean}
 */
const equalityDeep = (a, b) => {
  if (a == null || b == null) {
    return equalityStrict(a, b)
  }
  if (a.constructor !== b.constructor) {
    return false
  }
  if (a === b) {
    return true
  }
  switch (a.constructor) {
    case ArrayBuffer:
      a = new Uint8Array(a);
      b = new Uint8Array(b);
    // eslint-disable-next-line no-fallthrough
    case Uint8Array: {
      if (a.byteLength !== b.byteLength) {
        return false
      }
      for (let i = 0; i < a.length; i++) {
        if (a[i] !== b[i]) {
          return false
        }
      }
      break
    }
    case Set: {
      if (a.size !== b.size) {
        return false
      }
      for (const value of a) {
        if (!b.has(value)) {
          return false
        }
      }
      break
    }
    case Map: {
      if (a.size !== b.size) {
        return false
      }
      for (const key of a.keys()) {
        if (!b.has(key) || !equalityDeep(a.get(key), b.get(key))) {
          return false
        }
      }
      break
    }
    case Object:
      if (length(a) !== length(b)) {
        return false
      }
      for (const key in a) {
        if (!hasProperty(a, key) || !equalityDeep(a[key], b[key])) {
          return false
        }
      }
      break
    case Array:
      if (a.length !== b.length) {
        return false
      }
      for (let i = 0; i < a.length; i++) {
        if (!equalityDeep(a[i], b[i])) {
          return false
        }
      }
      break
    default:
      return false
  }
  return true
};

/**
 * @module awareness-protocol
 */


const outdatedTimeout = 30000;

/**
 * @typedef {Object} MetaClientState
 * @property {number} MetaClientState.clock
 * @property {number} MetaClientState.lastUpdated unix timestamp
 */

/**
 * The Awareness class implements a simple shared state protocol that can be used for non-persistent data like awareness information
 * (cursor, username, status, ..). Each client can update its own local state and listen to state changes of
 * remote clients. Every client may set a state of a remote peer to `null` to mark the client as offline.
 *
 * Each client is identified by a unique client id (something we borrow from `doc.clientID`). A client can override
 * its own state by propagating a message with an increasing timestamp (`clock`). If such a message is received, it is
 * applied if the known state of that client is older than the new state (`clock < newClock`). If a client thinks that
 * a remote client is offline, it may propagate a message with
 * `{ clock: currentClientClock, state: null, client: remoteClient }`. If such a
 * message is received, and the known clock of that client equals the received clock, it will override the state with `null`.
 *
 * Before a client disconnects, it should propagate a `null` state with an updated clock.
 *
 * Awareness states must be updated every 30 seconds. Otherwise the Awareness instance will delete the client state.
 *
 * @extends {Observable<string>}
 */
class Awareness extends Observable {
  /**
   * @param {Y.Doc} doc
   */
  constructor (doc) {
    super();
    this.doc = doc;
    /**
     * @type {number}
     */
    this.clientID = doc.clientID;
    /**
     * Maps from client id to client state
     * @type {Map<number, Object<string, any>>}
     */
    this.states = new Map();
    /**
     * @type {Map<number, MetaClientState>}
     */
    this.meta = new Map();
    this._checkInterval = /** @type {any} */ (setInterval(() => {
      const now = getUnixTime();
      if (this.getLocalState() !== null && (outdatedTimeout / 2 <= now - /** @type {{lastUpdated:number}} */ (this.meta.get(this.clientID)).lastUpdated)) {
        // renew local clock
        this.setLocalState(this.getLocalState());
      }
      /**
       * @type {Array<number>}
       */
      const remove = [];
      this.meta.forEach((meta, clientid) => {
        if (clientid !== this.clientID && outdatedTimeout <= now - meta.lastUpdated && this.states.has(clientid)) {
          remove.push(clientid);
        }
      });
      if (remove.length > 0) {
        removeAwarenessStates(this, remove, 'timeout');
      }
    }, floor(outdatedTimeout / 10)));
    doc.on('destroy', () => {
      this.destroy();
    });
    this.setLocalState({});
  }

  destroy () {
    this.emit('destroy', [this]);
    this.setLocalState(null);
    super.destroy();
    clearInterval(this._checkInterval);
  }

  /**
   * @return {Object<string,any>|null}
   */
  getLocalState () {
    return this.states.get(this.clientID) || null
  }

  /**
   * @param {Object<string,any>|null} state
   */
  setLocalState (state) {
    const clientID = this.clientID;
    const currLocalMeta = this.meta.get(clientID);
    const clock = currLocalMeta === undefined ? 0 : currLocalMeta.clock + 1;
    const prevState = this.states.get(clientID);
    if (state === null) {
      this.states.delete(clientID);
    } else {
      this.states.set(clientID, state);
    }
    this.meta.set(clientID, {
      clock,
      lastUpdated: getUnixTime()
    });
    const added = [];
    const updated = [];
    const filteredUpdated = [];
    const removed = [];
    if (state === null) {
      removed.push(clientID);
    } else if (prevState == null) {
      if (state != null) {
        added.push(clientID);
      }
    } else {
      updated.push(clientID);
      if (!equalityDeep(prevState, state)) {
        filteredUpdated.push(clientID);
      }
    }
    if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) {
      this.emit('change', [{ added, updated: filteredUpdated, removed }, 'local']);
    }
    this.emit('update', [{ added, updated, removed }, 'local']);
  }

  /**
   * @param {string} field
   * @param {any} value
   */
  setLocalStateField (field, value) {
    const state = this.getLocalState();
    if (state !== null) {
      this.setLocalState({
        ...state,
        [field]: value
      });
    }
  }

  /**
   * @return {Map<number,Object<string,any>>}
   */
  getStates () {
    return this.states
  }
}

/**
 * Mark (remote) clients as inactive and remove them from the list of active peers.
 * This change will be propagated to remote clients.
 *
 * @param {Awareness} awareness
 * @param {Array<number>} clients
 * @param {any} origin
 */
const removeAwarenessStates = (awareness, clients, origin) => {
  const removed = [];
  for (let i = 0; i < clients.length; i++) {
    const clientID = clients[i];
    if (awareness.states.has(clientID)) {
      awareness.states.delete(clientID);
      if (clientID === awareness.clientID) {
        const curMeta = /** @type {MetaClientState} */ (awareness.meta.get(clientID));
        awareness.meta.set(clientID, {
          clock: curMeta.clock + 1,
          lastUpdated: getUnixTime()
        });
      }
      removed.push(clientID);
    }
  }
  if (removed.length > 0) {
    awareness.emit('change', [{ added: [], updated: [], removed }, origin]);
    awareness.emit('update', [{ added: [], updated: [], removed }, origin]);
  }
};

/**
 * @param {Awareness} awareness
 * @param {Array<number>} clients
 * @return {Uint8Array}
 */
const encodeAwarenessUpdate = (awareness, clients, states = awareness.states) => {
  const len = clients.length;
  const encoder = createEncoder();
  writeVarUint(encoder, len);
  for (let i = 0; i < len; i++) {
    const clientID = clients[i];
    const state = states.get(clientID) || null;
    const clock = /** @type {MetaClientState} */ (awareness.meta.get(clientID)).clock;
    writeVarUint(encoder, clientID);
    writeVarUint(encoder, clock);
    writeVarString(encoder, JSON.stringify(state));
  }
  return toUint8Array(encoder)
};

/**
 * @param {Awareness} awareness
 * @param {Uint8Array} update
 * @param {any} origin This will be added to the emitted change event
 */
const applyAwarenessUpdate = (awareness, update, origin) => {
  const decoder = createDecoder(update);
  const timestamp = getUnixTime();
  const added = [];
  const updated = [];
  const filteredUpdated = [];
  const removed = [];
  const len = readVarUint(decoder);
  for (let i = 0; i < len; i++) {
    const clientID = readVarUint(decoder);
    let clock = readVarUint(decoder);
    const state = JSON.parse(readVarString(decoder));
    const clientMeta = awareness.meta.get(clientID);
    const prevState = awareness.states.get(clientID);
    const currClock = clientMeta === undefined ? 0 : clientMeta.clock;
    if (currClock < clock || (currClock === clock && state === null && awareness.states.has(clientID))) {
      if (state === null) {
        // never let a remote client remove this local state
        if (clientID === awareness.clientID && awareness.getLocalState() != null) {
          // remote client removed the local state. Do not remote state. Broadcast a message indicating
          // that this client still exists by increasing the clock
          clock++;
        } else {
          awareness.states.delete(clientID);
        }
      } else {
        awareness.states.set(clientID, state);
      }
      awareness.meta.set(clientID, {
        clock,
        lastUpdated: timestamp
      });
      if (clientMeta === undefined && state !== null) {
        added.push(clientID);
      } else if (clientMeta !== undefined && state === null) {
        removed.push(clientID);
      } else if (state !== null) {
        if (!equalityDeep(state, prevState)) {
          filteredUpdated.push(clientID);
        }
        updated.push(clientID);
      }
    }
  }
  if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) {
    awareness.emit('change', [{
      added, updated: filteredUpdated, removed
    }, origin]);
  }
  if (added.length > 0 || updated.length > 0 || removed.length > 0) {
    awareness.emit('update', [{
      added, updated, removed
    }, origin]);
  }
};

/**
 * @module sync-protocol
 */


/**
 * @typedef {Map<number, number>} StateMap
 */

/**
 * Core Yjs defines two message types:
 * • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
 * • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
 *   received all information from the remote client.
 *
 * In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
 * with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
 * SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
 *
 * In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
 * When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
 * with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
 * client knows that the sync is finished.  There are two reasons for this more elaborated sync model: 1. This protocol can
 * easily be implemented on top of http and websockets. 2. The server should only reply to requests, and not initiate them.
 * Therefore it is necessary that the client initiates the sync.
 *
 * Construction of a message:
 * [messageType : varUint, message definition..]
 *
 * Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
 *
 * stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
 */

const messageYjsSyncStep1 = 0;
const messageYjsSyncStep2 = 1;
const messageYjsUpdate = 2;

/**
 * Create a sync step 1 message based on the state of the current shared document.
 *
 * @param {encoding.Encoder} encoder
 * @param {Y.Doc} doc
 */
const writeSyncStep1 = (encoder, doc) => {
  writeVarUint(encoder, messageYjsSyncStep1);
  const sv = Y.encodeStateVector(doc);
  writeVarUint8Array(encoder, sv);
};

/**
 * @param {encoding.Encoder} encoder
 * @param {Y.Doc} doc
 * @param {Uint8Array} [encodedStateVector]
 */
const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
  writeVarUint(encoder, messageYjsSyncStep2);
  writeVarUint8Array(encoder, Y.encodeStateAsUpdate(doc, encodedStateVector));
};

/**
 * Read SyncStep1 message and reply with SyncStep2.
 *
 * @param {decoding.Decoder} decoder The reply to the received message
 * @param {encoding.Encoder} encoder The received message
 * @param {Y.Doc} doc
 */
const readSyncStep1 = (decoder, encoder, doc) =>
  writeSyncStep2(encoder, doc, readVarUint8Array(decoder));

/**
 * Read and apply Structs and then DeleteStore to a y instance.
 *
 * @param {decoding.Decoder} decoder
 * @param {Y.Doc} doc
 * @param {any} transactionOrigin
 */
const readSyncStep2 = (decoder, doc, transactionOrigin) => {
  try {
    Y.applyUpdate(doc, readVarUint8Array(decoder), transactionOrigin);
  } catch (error) {
    // This catches errors that are thrown by event handlers
    console.error('Caught error while handling a Yjs update', error);
  }
};

/**
 * @param {encoding.Encoder} encoder
 * @param {Uint8Array} update
 */
const writeUpdate = (encoder, update) => {
  writeVarUint(encoder, messageYjsUpdate);
  writeVarUint8Array(encoder, update);
};

/**
 * Read and apply Structs and then DeleteStore to a y instance.
 *
 * @param {decoding.Decoder} decoder
 * @param {Y.Doc} doc
 * @param {any} transactionOrigin
 */
const readUpdate = readSyncStep2;

var MessageType;
(function (MessageType) {
    MessageType[MessageType["Unknown"] = -1] = "Unknown";
    MessageType[MessageType["Sync"] = 0] = "Sync";
    MessageType[MessageType["Awareness"] = 1] = "Awareness";
    MessageType[MessageType["Auth"] = 2] = "Auth";
    MessageType[MessageType["QueryAwareness"] = 3] = "QueryAwareness";
    MessageType[MessageType["SyncReply"] = 4] = "SyncReply";
    MessageType[MessageType["Stateless"] = 5] = "Stateless";
    MessageType[MessageType["BroadcastStateless"] = 6] = "BroadcastStateless";
    MessageType[MessageType["CLOSE"] = 7] = "CLOSE";
    MessageType[MessageType["SyncStatus"] = 8] = "SyncStatus";
})(MessageType || (MessageType = {}));

class OutgoingMessage {
    constructor(documentName) {
        this.encoder = createEncoder();
        writeVarString(this.encoder, documentName);
    }
    createSyncMessage() {
        this.type = MessageType.Sync;
        writeVarUint(this.encoder, MessageType.Sync);
        return this;
    }
    createSyncReplyMessage() {
        this.type = MessageType.SyncReply;
        writeVarUint(this.encoder, MessageType.SyncReply);
        return this;
    }
    createAwarenessUpdateMessage(awareness, changedClients) {
        this.type = MessageType.Awareness;
        this.category = "Update";
        const message = encodeAwarenessUpdate(awareness, changedClients || Array.from(awareness.getStates().keys()));
        writeVarUint(this.encoder, MessageType.Awareness);
        writeVarUint8Array(this.encoder, message);
        return this;
    }
    writeQueryAwareness() {
        this.type = MessageType.QueryAwareness;
        this.category = "Update";
        writeVarUint(this.encoder, MessageType.QueryAwareness);
        return this;
    }
    writeTokenSyncRequest() {
        this.type = MessageType.Auth;
        this.category = "TokenSync";
        writeVarUint(this.encoder, MessageType.Auth);
        writeTokenSyncRequest(this.encoder);
        return this;
    }
    writeAuthenticated(readonly) {
        this.type = MessageType.Auth;
        this.category = "Authenticated";
        writeVarUint(this.encoder, MessageType.Auth);
        writeAuthenticated(this.encoder, readonly ? "readonly" : "read-write");
        return this;
    }
    writePermissionDenied(reason) {
        this.type = MessageType.Auth;
        this.category = "PermissionDenied";
        writeVarUint(this.encoder, MessageType.Auth);
        writePermissionDenied(this.encoder, reason);
        return this;
    }
    writeFirstSyncStepFor(document) {
        this.category = "SyncStep1";
        writeSyncStep1(this.encoder, document);
        return this;
    }
    writeUpdate(update) {
        this.category = "Update";
        writeUpdate(this.encoder, update);
        return this;
    }
    writeStateless(payload) {
        this.category = "Stateless";
        writeVarUint(this.encoder, MessageType.Stateless);
        writeVarString(this.encoder, payload);
        return this;
    }
    writeBroadcastStateless(payload) {
        this.category = "Stateless";
        writeVarUint(this.encoder, MessageType.BroadcastStateless);
        writeVarString(this.encoder, payload);
        return this;
    }
    // TODO: should this be write* or create* as method name?
    writeSyncStatus(updateSaved) {
        this.category = "SyncStatus";
        writeVarUint(this.encoder, MessageType.SyncStatus);
        writeVarUint(this.encoder, updateSaved ? 1 : 0);
        return this;
    }
    writeCloseMessage(reason) {
        this.type = MessageType.CLOSE;
        writeVarUint(this.encoder, MessageType.CLOSE);
        writeVarString(this.encoder, reason);
        return this;
    }
    toUint8Array() {
        return toUint8Array(this.encoder);
    }
}

class MessageReceiver {
    constructor(message, defaultTransactionOrigin) {
        this.message = message;
        this.defaultTransactionOrigin = defaultTransactionOrigin;
    }
    apply(document, connection, reply) {
        const { message } = this;
        const type = message.readVarUint();
        const emptyMessageLength = message.length;
        switch (type) {
            case MessageType.Sync:
            case MessageType.SyncReply: {
                message.writeVarUint(MessageType.Sync);
                this.readSyncMessage(message, document, connection, reply, type !== MessageType.SyncReply);
                if (message.length > emptyMessageLength + 1) {
                    if (reply) {
                        reply(message.toUint8Array());
                    }
                    else if (connection) {
                        // TODO: We should log this, shouldn’t we?
                        // this.logger.log({
                        //   direction: 'out',
                        //   type: MessageType.Awareness,
                        //   category: 'Update',
                        // })
                        connection.send(message.toUint8Array());
                    }
                }
                break;
            }
            case MessageType.Awareness: {
                applyAwarenessUpdate(document.awareness, message.readVarUint8Array(), connection === null || connection === void 0 ? void 0 : connection.webSocket);
                break;
            }
            case MessageType.QueryAwareness: {
                this.applyQueryAwarenessMessage(document, reply);
                break;
            }
            case MessageType.Stateless: {
                connection === null || connection === void 0 ? void 0 : connection.callbacks.statelessCallback({
                    connection,
                    documentName: document.name,
                    document,
                    payload: readVarString(message.decoder),
                });
                break;
            }
            case MessageType.BroadcastStateless: {
                const msg = message.readVarString();
                document.getConnections().forEach((connection) => {
                    connection.sendStateless(msg);
                });
                break;
            }
            case MessageType.CLOSE: {
                connection === null || connection === void 0 ? void 0 : connection.close({
                    code: 1000,
                    reason: "provider_initiated",
                });
                break;
            }
            case MessageType.Auth: {
                const authType = message.readVarUint();
                if (authType === AuthMessageType.Token) {
                    connection === null || connection === void 0 ? void 0 : connection.callbacks.onTokenSyncCallback({
                        token: message.readVarString(),
                    });
                    break;
                }
                console.error("Received an authentication message on a connection that is already fully authenticated. Probably your provider has been destroyed + recreated really fast.");
                break;
            }
            default:
                console.error(`Unable to handle message of type ${type}: no handler defined! Are your provider/server versions aligned?`);
            // Do nothing
        }
    }
    readSyncMessage(message, document, connection, reply, requestFirstSync = true) {
        const type = message.readVarUint();
        if (connection) {
            connection.callbacks.beforeSync(connection, {
                type,
                payload: message.peekVarUint8Array(),
            });
        }
        switch (type) {
            case messageYjsSyncStep1: {
                readSyncStep1(message.decoder, message.encoder, document);
                // When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1.
                if (reply && requestFirstSync) {
                    const syncMessage = new OutgoingMessage(document.name)
                        .createSyncReplyMessage()
                        .writeFirstSyncStepFor(document);
                    reply(syncMessage.toUint8Array());
                }
                else if (connection) {
                    const syncMessage = new OutgoingMessage(document.name)
                        .createSyncMessage()
                        .writeFirstSyncStepFor(document);
                    connection.send(syncMessage.toUint8Array());
                }
                break;
            }
            case messageYjsSyncStep2:
                if (connection === null || connection === void 0 ? void 0 : connection.readOnly) {
                    // We're in read-only mode, so we can't apply the update.
                    // Let's use snapshotContainsUpdate to see if the update actually contains changes.
                    // If not, we can still ack the update
                    const snapshot = Y.snapshot(document);
                    const update = readVarUint8Array(message.decoder);
                    if (Y.snapshotContainsUpdate(snapshot, update)) {
                        // no new changes in update
                        const ackMessage = new OutgoingMessage(document.name).writeSyncStatus(true);
                        connection.send(ackMessage.toUint8Array());
                    }
                    else {
                        // new changes in update that we can't apply, because readOnly
                        const ackMessage = new OutgoingMessage(document.name).writeSyncStatus(false);
                        connection.send(ackMessage.toUint8Array());
                    }
                    break;
                }
                readSyncStep2(message.decoder, document, connection !== null && connection !== void 0 ? connection : this.defaultTransactionOrigin);
                if (connection) {
                    connection.send(new OutgoingMessage(document.name)
                        .writeSyncStatus(true)
                        .toUint8Array());
                }
                break;
            case messageYjsUpdate:
                if (connection === null || connection === void 0 ? void 0 : connection.readOnly) {
                    connection.send(new OutgoingMessage(document.name)
                        .writeSyncStatus(false)
                        .toUint8Array());
                    break;
                }
                readUpdate(message.decoder, document, connection);
                if (connection) {
                    connection.send(new OutgoingMessage(document.name)
                        .writeSyncStatus(true)
                        .toUint8Array());
                }
                break;
            default:
                throw new Error(`Received a message with an unknown type: ${type}`);
        }
        return type;
    }
    applyQueryAwarenessMessage(document, reply) {
        const message = new OutgoingMessage(document.name).createAwarenessUpdateMessage(document.awareness);
        if (reply) {
            reply(message.toUint8Array());
        }
    }
}

class Connection {
    /**
     * Constructor.
     */
    constructor(connection, request, document, socketId, context, readOnly = false) {
        this.callbacks = {
            onClose: [(document, event) => { }],
            beforeHandleMessage: (connection, update) => Promise.resolve(),
            beforeSync: (connection, payload) => Promise.resolve(),
            statelessCallback: (payload) => Promise.resolve(),
            onTokenSyncCallback: (payload) => Promise.resolve(),
        };
        this.webSocket = connection;
        this.context = context;
        this.document = document;
        this.request = request;
        this.socketId = socketId;
        this.readOnly = readOnly;
        this.webSocket.binaryType = "nodebuffer";
        this.document.addConnection(this);
        this.sendCurrentAwareness();
    }
    /**
     * Set a callback that will be triggered when the connection is closed
     */
    onClose(callback) {
        this.callbacks.onClose.push(callback);
        return this;
    }
    /**
     * Set a callback that will be triggered when an stateless message is received
     */
    onStatelessCallback(callback) {
        this.callbacks.statelessCallback = callback;
        return this;
    }
    /**
     * Set a callback that will be triggered before an message is handled
     */
    beforeHandleMessage(callback) {
        this.callbacks.beforeHandleMessage = callback;
        return this;
    }
    /**
     * Set a callback that will be triggered before a sync message is handled
     */
    beforeSync(callback) {
        this.callbacks.beforeSync = callback;
        return this;
    }
    /**
     * Set a callback that will be triggered when on token sync message is received
     */
    onTokenSyncCallback(callback) {
        this.callbacks.onTokenSyncCallback = callback;
        return this;
    }
    /**
     * Send the given message
     */
    send(message) {
        if (this.webSocket.readyState === WsReadyStates.Closing ||
            this.webSocket.readyState === WsReadyStates.Closed) {
            this.close();
            return;
        }
        try {
            this.webSocket.send(message, (error) => {
                if (error != null)
                    this.close();
            });
        }
        catch (exception) {
            this.close();
        }
    }
    /**
     * Send a stateless message with payload
     */
    sendStateless(payload) {
        const message = new OutgoingMessage(this.document.name).writeStateless(payload);
        this.send(message.toUint8Array());
    }
    /**
     * Request current token from the client
     */
    requestToken() {
        const message = new OutgoingMessage(this.document.name).writeTokenSyncRequest();
        this.send(message.toUint8Array());
    }
    /**
     * Graceful wrapper around the WebSocket close method.
     */
    close(event) {
        var _a;
        if (this.document.hasConnection(this)) {
            this.document.removeConnection(this);
            this.callbacks.onClose.forEach((callback) => callback(this.document, event));
            const closeMessage = new OutgoingMessage(this.document.name);
            closeMessage.writeCloseMessage((_a = event === null || event === void 0 ? void 0 : event.reason) !== null && _a !== void 0 ? _a : "Server closed the connection");
            this.send(closeMessage.toUint8Array());
        }
    }
    /**
     * Send the current document awareness to the client, if any
     * @private
     */
    sendCurrentAwareness() {
        if (!this.document.hasAwarenessStates()) {
            return;
        }
        const awarenessMessage = new OutgoingMessage(this.document.name).createAwarenessUpdateMessage(this.document.awareness);
        this.send(awarenessMessage.toUint8Array());
    }
    /**
     * Handle an incoming message
     * @public
     */
    handleMessage(data) {
        const message = new IncomingMessage(data);
        const documentName = message.readVarString();
        if (documentName !== this.document.name)
            return;
        message.writeVarString(documentName);
        this.callbacks
            .beforeHandleMessage(this, data)
            .then(() => {
            try {
                new MessageReceiver(message).apply(this.document, this);
            }
            catch (e) {
                console.error(`closing connection ${this.socketId} (while handling ${documentName}) because of exception`, e);
                this.close({
                    code: "code" in e ? e.code : ResetConnection.code,
                    reason: "reason" in e ? e.reason : ResetConnection.reason,
                });
            }
        })
            .catch((e) => {
            console.error(`closing connection ${this.socketId} (while handling ${documentName}) because of exception`, e);
            this.close({
                code: "code" in e ? e.code : ResetConnection.code,
                reason: "reason" in e ? e.reason : ResetConnection.reason,
            });
        });
    }
}

class Document extends Doc {
    /**
     * Constructor.
     */
    constructor(name, yDocOptions) {
        super(yDocOptions);
        this.callbacks = {
            // eslint-disable-next-line @typescript-eslint/no-empty-function
            onUpdate: (document, connection, update) => { },
            beforeBroadcastStateless: (document, stateless) => { },
        };
        this.connections = new Map();
        // The number of direct (non-websocket) connections to this document
        this.directConnectionsCount = 0;
        this.isDestroyed = false;
        this.saveMutex = new Mutex();
        this.lastChangeTime = 0;
        this.name = name;
        this.awareness = new Awareness(this);
        this.awareness.setLocalState(null);
        this.awareness.on("update", this.handleAwarenessUpdate.bind(this));
        this.on("update", this.handleUpdate.bind(this));
        this.isLoading = true;
    }
    /**
     * Check if the Document (XMLFragment or Map) is empty
     */
    isEmpty(fieldName) {
        // eslint-disable-next-line no-underscore-dangle
        return !this.get(fieldName)._start && !this.get(fieldName)._map.size;
    }
    /**
     * Merge the given document(s) into this one
     */
    merge(documents) {
        (Array.isArray(documents) ? documents : [documents]).forEach((document) => {
            applyUpdate(this, encodeStateAsUpdate(document));
        });
        return this;
    }
    /**
     * Set a callback that will be triggered when the document is updated
     */
    onUpdate(callback) {
        this.callbacks.onUpdate = callback;
        return this;
    }
    /**
     * Set a callback that will be triggered before a stateless message is broadcasted
     */
    beforeBroadcastStateless(callback) {
        this.callbacks.beforeBroadcastStateless = callback;
        return this;
    }
    /**
     * Register a connection and a set of clients on this document keyed by the
     * underlying websocket connection
     */
    addConnection(connection) {
        this.connections.set(connection.webSocket, {
            clients: new Set(),
            connection,
        });
        return this;
    }
    /**
     * Is the given connection registered on this document
     */
    hasConnection(connection) {
        return this.connections.has(connection.webSocket);
    }
    /**
     * Remove the given connection from this document
     */
    removeConnection(connection) {
        removeAwarenessStates(this.awareness, Array.from(this.getClients(connection.webSocket)), null);
        this.connections.delete(connection.webSocket);
        return this;
    }
    addDirectConnection() {
        this.directConnectionsCount += 1;
        return this;
    }
    removeDirectConnection() {
        if (this.directConnectionsCount > 0) {
            this.directConnectionsCount -= 1;
        }
        return this;
    }
    /**
     * Get the number of active connections for this document
     */
    getConnectionsCount() {
        return this.connections.size + this.directConnectionsCount;
    }
    /**
     * Get an array of registered connections
     */
    getConnections() {
        return Array.from(this.connections.values()).map((data) => data.connection);
    }
    /**
     * Get the client ids for the given connection instance
     */
    getClients(connectionInstance) {
        const connection = this.connections.get(connectionInstance);
        return (connection === null || connection === void 0 ? void 0 : connection.clients) === undefined ? new Set() : connection.clients;
    }
    /**
     * Has the document awareness states
     */
    hasAwarenessStates() {
        return this.awareness.getStates().size > 0;
    }
    /**
     * Apply the given awareness update
     */
    applyAwarenessUpdate(connection, update) {
        applyAwarenessUpdate(this.awareness, update, connection.webSocket);
        return this;
    }
    /**
     * Handle an awareness update and sync changes to clients
     * @private
     */
    handleAwarenessUpdate({ added, updated, removed }, connectionInstance) {
        const changedClients = added.concat(updated, removed);
        if (connectionInstance !== null) {
            const connection = this.connections.get(connectionInstance);
            if (connection) {
                added.forEach((clientId) => connection.clients.add(clientId));
                removed.forEach((clientId) => connection.clients.delete(clientId));
            }
        }
        this.getConnections().forEach((connection) => {
            const awarenessMessage = new OutgoingMessage(this.name).createAwarenessUpdateMessage(this.awareness, changedClients);
            connection.send(awarenessMessage.toUint8Array());
        });
        return this;
    }
    /**
     * Handle an updated document and sync changes to clients
     */
    handleUpdate(update, connection) {
        this.callbacks.onUpdate(this, connection, update);
        const message = new OutgoingMessage(this.name)
            .createSyncMessage()
            .writeUpdate(update);
        this.getConnections().forEach((connection) => {
            connection.send(message.toUint8Array());
        });
        return this;
    }
    /**
     * Broadcast stateless message to all connections
     */
    broadcastStateless(payload, filter) {
        this.callbacks.beforeBroadcastStateless(this, payload);
        const connections = filter
            ? this.getConnections().filter(filter)
            : this.getConnections();
        connections.forEach((connection) => {
            connection.sendStateless(payload);
        });
    }
    destroy() {
        super.destroy();
        this.isDestroyed = true;
    }
}

var version = "3.4.4";
var meta = {
	version: version};

/**
 * Get parameters by the given request
 */
function getParameters(request) {
    var _a;
    const query = ((_a = request === null || request === void 0 ? void 0 : request.url) === null || _a === void 0 ? void 0 : _a.split("?")) || [];
    return new URLSearchParams(query[1] ? query[1] : "");
}

/**
 * The `ClientConnection` class is responsible for handling an incoming WebSocket
 *
 * TODO-refactor:
 * - use event handlers instead of calling hooks directly, hooks should probably be called from Hocuspocus.ts
 */
class ClientConnection {
    /**
     * The `ClientConnection` class receives incoming WebSocket connections,
     * runs all hooks:
     *
     *  - onConnect for all connections
     *  - onAuthenticate only if required
     *
     * … and if nothings fails it’ll fully establish the connection and
     * load the Document then.
     */
    constructor(websocket, request, documentProvider, 
    // TODO: change to events
    hooks, opts, defaultContext = {}) {
        this.websocket = websocket;
        this.request = request;
        this.documentProvider = documentProvider;
        this.hooks = hooks;
        this.opts = opts;
        this.defaultContext = defaultContext;
        // this map indicates whether a `Connection` instance has already taken over for incoming message for the key (i.e. documentName)
        this.documentConnections = {};
        // While the connection will be establishing messages will
        // be queued and handled later.
        this.incomingMessageQueue = {};
        // While the connection is establishing, kee
        this.documentConnectionsEstablished = new Set();
        // hooks payload by Document
        this.hookPayloads = {};
        this.callbacks = {
            onClose: [(document, payload) => { }],
        };
        // Every new connection gets a unique identifier.
        this.socketId = crypto.randomUUID();
        this.pongReceived = true;
        this.handleWebsocketClose = (code, reason) => {
            this.close({ code, reason: reason.toString() });
            this.websocket.removeListener("message", this.messageHandler);
            this.websocket.removeListener("pong", this.handlePong);
            clearInterval(this.pingInterval);
        };
        this.handlePong = () => {
            this.pongReceived = true;
        };
        /**
         * Check if pong was received and close the connection otherwise
         * @private
         */
        this.check = () => {
            if (!this.pongReceived) {
                return this.close(ConnectionTimeout);
            }
            this.pongReceived = false;
            try {
                this.websocket.ping();
            }
            catch (error) {
                this.close(ConnectionTimeout);
            }
        };
        // Once all hooks are run, we’ll fully establish the connection:
        this.setUpNewConnection = async (documentName) => {
            const hookPayload = this.hookPayloads[documentName];
            // If no hook interrupts, create a document and connection
            const document = await this.documentProvider.createDocument(documentName, hookPayload.request, hookPayload.socketId, hookPayload.connectionConfig, hookPayload.context);
            const connection = this.createConnection(this.websocket, document);
            connection.onClose((document, event) => {
                delete this.hookPayloads[documentName];
                delete this.documentConnections[documentName];
                delete this.incomingMessageQueue[documentName];
                this.documentConnectionsEstablished.delete(documentName);
            });
            connection.onTokenSyncCallback(async (payload) => {
                try {
                    return await this.hooks("onTokenSync", {
                        ...hookPayload,
                        ...payload,
                        connection,
                        document,
                        documentName,
                    }, (contextAdditions) => {
                        hookPayload.context = {
                            ...hookPayload.context,
                            ...contextAdditions,
                        };
                    });
                }
                catch (err) {
                    console.error(err);
                    const error = { ...Unauthorized, ...err };
                    connection.close({ code: error.code, reason: error.reason });
                }
            });
            this.documentConnections[documentName] = connection;
            // If the WebSocket has already disconnected (wow, that was fast) – then
            // immediately call close to cleanup the connection and document in memory.
            if (this.websocket.readyState === WsReadyStates.Closing ||
                this.websocket.readyState === WsReadyStates.Closed) {
                this.close();
                return;
            }
            // There’s no need to queue messages anymore.
            // Let’s work through queued messages.
            this.incomingMessageQueue[documentName].forEach((input) => {
                this.websocket.emit("message", input);
            });
            await this.hooks("connected", {
                ...hookPayload,
                documentName,
                context: hookPayload.context,
                connection,
            });
        };
        // This listener handles authentication messages and queues everything else.
        this.handleQueueingMessage = async (data) => {
            var _a;
            try {
                const tmpMsg = new IncomingMessage(data);
                const documentName = readVarString(tmpMsg.decoder);
                const type = readVarUint(tmpMsg.decoder);
                if (!(type === MessageType.Auth &&
                    !this.documentConnectionsEstablished.has(documentName))) {
                    this.incomingMessageQueue[documentName].push(data);
                    return;
                }
                // Okay, we’ve got the authentication message we’re waiting for:
                this.documentConnectionsEstablished.add(documentName);
                // The 2nd integer contains the submessage type
                // which will always be authentication when sent from client -> server
                readVarUint(tmpMsg.decoder);
                const token = readVarString(tmpMsg.decoder);
                try {
                    const hookPayload = this.hookPayloads[documentName];
                    await this.hooks("onConnect", { ...hookPayload, documentName }, (contextAdditions) => {
                        // merge context from all hooks
                        hookPayload.context = {
                            ...hookPayload.context,
                            ...contextAdditions,
                        };
                    });
                    await this.hooks("onAuthenticate", {
                        token,
                        ...hookPayload,
                        documentName,
                    }, (contextAdditions) => {
                        // Hooks are allowed to give us even more context and we’ll merge everything together.
                        // We’ll pass the context to other hooks then.
                        hookPayload.context = {
                            ...hookPayload.context,
                            ...contextAdditions,
                        };
                    });
                    // All `onAuthenticate` hooks passed.
                    hookPayload.connectionConfig.isAuthenticated = true;
                    // Let the client know that authentication was successful.
                    const message = new OutgoingMessage(documentName).writeAuthenticated(hookPayload.connectionConfig.readOnly);
                    this.websocket.send(message.toUint8Array());
                    // Time to actually establish the connection.
                    await this.setUpNewConnection(documentName);
                }
                catch (err) {
                    const error = err || Forbidden;
                    const message = new OutgoingMessage(documentName).writePermissionDenied((_a = error.reason) !== null && _a !== void 0 ? _a : "permission-denied");
                    this.websocket.send(message.toUint8Array());
                }
                // Catch errors due to failed decoding of data
            }
            catch (error) {
                console.error(error);
                this.websocket.close(ResetConnection.code, ResetConnection.reason);
            }
        };
        this.messageHandler = (data) => {
            try {
                const tmpMsg = new IncomingMessage(data);
                const documentName = readVarString(tmpMsg.decoder);
                const connection = this.documentConnections[documentName];
                if (connection) {
                    // forward the message to the connection
                    connection.handleMessage(data);
                    // we already have a `Connection` set up for this document
                    return;
                }
                const isFirst = this.incomingMessageQueue[documentName] === undefined;
                if (isFirst) {
                    this.incomingMessageQueue[documentName] = [];
                    if (this.hookPayloads[documentName]) {
                        throw new Error("first message, but hookPayloads exists");
                    }
                    const hookPayload = {
                        instance: this.documentProvider,
                        request: this.request,
                        connectionConfig: {
                            readOnly: false,
                            isAuthenticated: false,
                        },
                        requestHeaders: this.request.headers,
                        requestParameters: getParameters(this.request),
                        socketId: this.socketId,
                        context: {
                            ...this.defaultContext,
                        },
                    };
                    this.hookPayloads[documentName] = hookPayload;
                }
                this.handleQueueingMessage(data);
            }
            catch (closeError) {
                // catch is needed in case an invalid payload crashes the parsing of the Uint8Array
                console.error(closeError);
                this.websocket.close(Unauthorized.code, Unauthorized.reason);
            }
        };
        this.timeout = opts.timeout;
        this.pingInterval = setInterval(this.check, this.timeout);
        websocket.on("pong", this.handlePong);
        websocket.on("message", this.messageHandler);
        websocket.once("close", this.handleWebsocketClose);
    }
    close(event) {
        Object.values(this.documentConnections).forEach((connection) => connection.close(event));
    }
    /**
     * Set a callback that will be triggered when the connection is closed
     */
    onClose(callback) {
        this.callbacks.onClose.push(callback);
        return this;
    }
    /**
     * Create a new connection by the given request and document
     */
    createConnection(connection, document) {
        const hookPayload = this.hookPayloads[document.name];
        const instance = new Connection(connection, hookPayload.request, document, hookPayload.socketId, hookPayload.context, hookPayload.connectionConfig.readOnly);
        instance.onClose(async (document, event) => {
            const disconnectHookPayload = {
                instance: this.documentProvider, // TODO, this will be removed when we use events instead of hooks for this class
                clientsCount: document.getConnectionsCount(),
                context: hookPayload.context,
                document,
                socketId: hookPayload.socketId,
                documentName: document.name,
                requestHeaders: hookPayload.request.headers,
                requestParameters: getParameters(hookPayload.request),
            };
            await this.hooks("onDisconnect", disconnectHookPayload);
            this.callbacks.onClose.forEach((callback) => callback(document, disconnectHookPayload));
        });
        instance.onStatelessCallback(async (payload) => {
            try {
                return await this.hooks("onStateless", payload);
            }
            catch (error) {
                if (error === null || error === void 0 ? void 0 : error.message) {
                    // if a hook rejects and the error is empty, do nothing
                    // this is only meant to prevent later hooks and the
                    // default handler to do something. if an error is present
                    // just rethrow it
                    throw error;
                }
            }
        });
        instance.beforeHandleMessage((connection, update) => {
            const beforeHandleMessagePayload = {
                instance: this.documentProvider, // TODO, this will be removed when we use events instead of hooks for this class
                clientsCount: document.getConnectionsCount(),
                context: hookPayload.context,
                document,
                socketId: hookPayload.socketId,
                connection,
                documentName: document.name,
                requestHeaders: hookPayload.request.headers,
                requestParameters: getParameters(hookPayload.request),
                update,
            };
            return this.hooks("beforeHandleMessage", beforeHandleMessagePayload);
        });
        instance.beforeSync((connection, payload) => {
            const beforeSyncPayload = {
                clientsCount: document.getConnectionsCount(),
                context: hookPayload.context,
                document,
                documentName: document.name,
                connection,
                type: payload.type,
                payload: payload.payload,
            };
            return this.hooks("beforeSync", beforeSyncPayload);
        });
        return instance;
    }
}

class DirectConnection {
    /**
     * Constructor.
     */
    constructor(document, instance, context) {
        this.document = null;
        this.document = document;
        this.instance = instance;
        this.context = context;
        this.document.addDirectConnection();
    }
    async transact(transaction) {
        if (!this.document) {
            throw new Error("direct connection closed");
        }
        transaction(this.document);
        await this.instance.storeDocumentHooks(this.document, {
            clientsCount: this.document.getConnectionsCount(),
            context: this.context,
            document: this.document,
            documentName: this.document.name,
            instance: this.instance,
            requestHeaders: {},
            requestParameters: new URLSearchParams(),
            socketId: "server",
        }, true);
    }
    async disconnect() {
        var _a;
        if (this.document) {
            (_a = this.document) === null || _a === void 0 ? void 0 : _a.removeDirectConnection();
            await this.instance.storeDocumentHooks(this.document, {
                clientsCount: this.document.getConnectionsCount(),
                context: this.context,
                document: this.document,
                documentName: this.document.name,
                instance: this.instance,
                requestHeaders: {},
                requestParameters: new URLSearchParams(),
                socketId: "server",
            }, true);
            // If the direct connection was the only connection to the document
            // then we should trigger the onDisconnect hook for
            // this doc and unload the document
            if (this.document.getConnectionsCount() === 0 &&
                !this.document.saveMutex.isLocked()) {
                await this.instance.hooks("onDisconnect", {
                    instance: this.instance,
                    clientsCount: this.document.getConnectionsCount(),
                    context: this.context,
                    document: this.document,
                    socketId: "server",
                    documentName: this.document.name,
                    requestHeaders: {},
                    requestParameters: new URLSearchParams(),
                });
                await this.instance.unloadDocument(this.document);
            }
            this.document = null;
        }
    }
}

const useDebounce = () => {
    const timers = new Map();
    const runningExecutions = new Map();
    const debounce = async (id, 
    // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
    func, debounce, maxDebounce) => {
        const old = timers.get(id);
        const start = (old === null || old === void 0 ? void 0 : old.start) || Date.now();
        const run = async () => {
            if (runningExecutions.has(id)) {
                // wait for previous execution to finish
                await runningExecutions.get(id);
            }
            timers.delete(id);
            const execution = func();
            runningExecutions.set(id, execution);
            const executionResult = await execution;
            runningExecutions.delete(id);
            return executionResult;
        };
        if (old === null || old === void 0 ? void 0 : old.timeout) {
            clearTimeout(old.timeout);
        }
        if (debounce === 0) {
            return run();
        }
        if (Date.now() - start >= maxDebounce) {
            return run();
        }
        timers.set(id, {
            start,
            timeout: setTimeout(run, debounce),
            func: run,
        });
    };
    const executeNow = (id) => {
        const old = timers.get(id);
        if (old) {
            clearTimeout(old.timeout);
            return old.func();
        }
    };
    const isDebounced = (id) => {
        return timers.has(id);
    };
    const isCurrentlyExecuting = (id) => {
        return runningExecutions.has(id);
    };
    return { debounce, isDebounced, isCurrentlyExecuting, executeNow };
};

const defaultConfiguration = {
    name: null,
    timeout: 30000,
    debounce: 2000,
    maxDebounce: 10000,
    quiet: false,
    yDocOptions: {
        gc: true,
        gcFilter: () => true,
    },
    unloadImmediately: true,
};
class Hocuspocus {
    constructor(configuration) {
        this.configuration = {
            ...defaultConfiguration,
            extensions: [],
            onConfigure: () => new Promise((r) => r(null)),
            onListen: () => new Promise((r) => r(null)),
            onUpgrade: () => new Promise((r) => r(null)),
            onConnect: () => new Promise((r) => r(null)),
            connected: () => new Promise((r) => r(null)),
            beforeHandleMessage: () => new Promise((r) => r(null)),
            beforeSync: () => new Promise((r) => r(null)),
            beforeBroadcastStateless: () => new Promise((r) => r(null)),
            onStateless: () => new Promise((r) => r(null)),
            onChange: () => new Promise((r) => r(null)),
            onCreateDocument: () => new Promise((r) => r(null)),
            onLoadDocument: () => new Promise((r) => r(null)),
            onStoreDocument: () => new Promise((r) => r(null)),
            afterStoreDocument: () => new Promise((r) => r(null)),
            onAwarenessUpdate: () => new Promise((r) => r(null)),
            onRequest: () => new Promise((r) => r(null)),
            onDisconnect: () => new Promise((r) => r(null)),
            onDestroy: () => new Promise((r) => r(null)),
        };
        this.loadingDocuments = new Map();
        this.unloadingDocuments = new Map();
        this.documents = new Map();
        this.debouncer = useDebounce();
        if (configuration) {
            this.configure(configuration);
        }
    }
    /**
     * Configure Hocuspocus
     */
    configure(configuration) {
        this.configuration = {
            ...this.configuration,
            ...configuration,
        };
        this.configuration.extensions.sort((a, b) => {
            const one = typeof a.priority === "undefined" ? 100 : a.priority;
            const two = typeof b.priority === "undefined" ? 100 : b.priority;
            if (one > two) {
                return -1;
            }
            if (one < two) {
                return 1;
            }
            return 0;
        });
        this.configuration.extensions.push({
            onConfigure: this.configuration.onConfigure,
            onListen: this.configuration.onListen,
            onUpgrade: this.configuration.onUpgrade,
            onConnect: this.configuration.onConnect,
            connected: this.configuration.connected,
            onAuthenticate: this.configuration.onAuthenticate,
            onTokenSync: this.configuration.onTokenSync,
            onLoadDocument: this.configuration.onLoadDocument,
            afterLoadDocument: this.configuration.afterLoadDocument,
            beforeHandleMessage: this.configuration.beforeHandleMessage,
            beforeBroadcastStateless: this.configuration.beforeBroadcastStateless,
            beforeSync: this.configuration.beforeSync,
            onStateless: this.configuration.onStateless,
            onChange: this.configuration.onChange,
            onStoreDocument: this.configuration.onStoreDocument,
            afterStoreDocument: this.configuration.afterStoreDocument,
            onAwarenessUpdate: this.configuration.onAwarenessUpdate,
            onRequest: this.configuration.onRequest,
            beforeUnloadDocument: this.configuration.beforeUnloadDocument,
            afterUnloadDocument: this.configuration.afterUnloadDocument,
            onDisconnect: this.configuration.onDisconnect,
            onDestroy: this.configuration.onDestroy,
        });
        this.hooks("onConfigure", {
            configuration: this.configuration,
            version: meta.version,
            instance: this,
        });
        return this;
    }
    /**
     * Get the total number of active documents
     */
    getDocumentsCount() {
        return this.documents.size;
    }
    /**
     * Get the total number of active connections
     */
    getConnectionsCount() {
        const uniqueSocketIds = new Set();
        const totalDirectConnections = Array.from(this.documents.values()).reduce((acc, document) => {
            // Accumulate unique socket IDs
            document.getConnections().forEach(({ socketId }) => {
                uniqueSocketIds.add(socketId);
            });
            // Accumulate direct connections
            return acc + document.directConnectionsCount;
        }, 0);
        // Return the sum of unique socket IDs and direct connections
        return uniqueSocketIds.size + totalDirectConnections;
    }
    /**
     * Force close one or more connections
     */
    closeConnections(documentName) {
        // Iterate through all connections for all documents
        // and invoke their close method, which is a graceful
        // disconnect wrapper around the underlying websocket.close
        this.documents.forEach((document) => {
            // If a documentName was specified, bail if it doesn't match
            if (documentName && document.name !== documentName) {
                return;
            }
            document.connections.forEach(({ connection }) => {
                connection.close(ResetConnection);
            });
        });
    }
    /**
     * The `handleConnection` method receives incoming WebSocket connections,
     * runs all hooks:
     *
     *  - onConnect for all connections
     *  - onAuthenticate only if required
     *
     * … and if nothing fails it’ll fully establish the connection and
     * load the Document then.
     */
    handleConnection(incoming, request, defaultContext = {}) {
        const clientConnection = new ClientConnection(incoming, request, this, this.hooks.bind(this), {
            timeout: this.configuration.timeout,
        }, defaultContext);
        clientConnection.onClose((document, hookPayload) => {
            // Check if there are still no connections to the document, as these hooks
            // may take some time to resolve (e.g. database queries). If a
            // new connection were to come in during that time it would rely on the
            // document in the map that we remove now.
            if (document.getConnectionsCount() > 0) {
                return;
            }
            // If it’s the last connection, we need to make sure to store the
            // document. Use the debouncer executeNow helper, to run scheduled
            // onStoreDocument immediately and clear running timers.
            // If there is no scheduled run for this document there is no point in
            // triggering onStoreDocument hook, as everything seems to be stored already.
            // Only run this if the document has finished loading earlier (i.e. not to persist the empty
            // ydoc if the onLoadDocument hook returned an error)
            if (!document.isLoading &&
                this.debouncer.isDebounced(`onStoreDocument-${document.name}`)) {
                if (this.configuration.unloadImmediately) {
                    this.debouncer.executeNow(`onStoreDocument-${document.name}`);
                }
            }
            else {
                // Remove document from memory immediately
                this.unloadDocument(document);
            }
        });
    }
    /**
     * Handle update of the given document
     *
     * "connection" is not necessarily type "Connection", it's the Yjs "origin" (which is "Connection" if
     * the update is incoming from the provider, but can be anything if the updates is originated from an extension.
     */
    handleDocumentUpdate(document, connection, update, request) {
        var _a, _b;
        const hookPayload = {
            instance: this,
            clientsCount: document.getConnectionsCount(),
            context: (connection === null || connection === void 0 ? void 0 : connection.context) || {},
            document,
            documentName: document.name,
            requestHeaders: (_a = request === null || request === void 0 ? void 0 : request.headers) !== null && _a !== void 0 ? _a : {},
            requestParameters: getParameters(request),
            socketId: (_b = connection === null || connection === void 0 ? void 0 : connection.socketId) !== null && _b !== void 0 ? _b : "",
            update,
            transactionOrigin: connection,
        };
        this.hooks("onChange", hookPayload);
        // If the update was received through other ways than the
        // WebSocket connection, we don’t need to feel responsible for
        // storing the content.
        // also ignore changes incoming through redis connection, as this would be a breaking change (#730, #696, #606)
        if (!connection ||
            connection === "__hocuspocus__redis__origin__") {
            return;
        }
        this.storeDocumentHooks(document, hookPayload);
    }
    /**
     * Create a new document by the given request
     */
    async createDocument(documentName, request, socketId, connection, context) {
        const existingLoadingDoc = this.loadingDocuments.get(documentName);
        if (existingLoadingDoc) {
            return existingLoadingDoc;
        }
        const existingDoc = this.documents.get(documentName);
        if (existingDoc) {
            return Promise.resolve(existingDoc);
        }
        const loadDocPromise = this.loadDocument(documentName, request, socketId, connection, context);
        this.loadingDocuments.set(documentName, loadDocPromise);
        try {
            const doc = await loadDocPromise;
            this.documents.set(documentName, doc);
            this.loadingDocuments.delete(documentName);
            return doc;
        }
        catch (e) {
            this.loadingDocuments.delete(documentName);
            throw e;
        }
    }
    async loadDocument(documentName, request, socketId, connectionConfig, context) {
        var _a;
        const requestHeaders = (_a = request.headers) !== null && _a !== void 0 ? _a : {};
        const requestParameters = getParameters(request);
        const yDocOptions = await this.hooks("onCreateDocument", {
            documentName,
            requestHeaders,
            requestParameters,
            connectionConfig,
            context,
            socketId,
            instance: this,
        });
        const document = new Document(documentName, {
            ...this.configuration.yDocOptions,
            ...yDocOptions,
        });
        const hookPayload = {
            instance: this,
            context,
            connectionConfig,
            document,
            documentName,
            socketId,
            requestHeaders,
            requestParameters,
        };
        try {
            await this.hooks("onLoadDocument", hookPayload, (loadedDocument) => {
                // if a hook returns a Y-Doc, encode the document state as update
                // and apply it to the newly created document
                // Note: instanceof doesn't work, because Doc !== Doc for some reason I don't understand
                if ((loadedDocument === null || loadedDocument === void 0 ? void 0 : loadedDocument.constructor.name) === "Document" ||
                    (loadedDocument === null || loadedDocument === void 0 ? void 0 : loadedDocument.constructor.name) === "Doc") {
                    applyUpdate(document, encodeStateAsUpdate(loadedDocument));
                }
            });
        }
        catch (e) {
            this.closeConnections(documentName);
            this.unloadDocument(document);
            throw e;
        }
        document.isLoading = false;
        document.onUpdate((document, connection, update) => {
            document.lastChangeTime = Date.now();
            this.handleDocumentUpdate(document, connection, update, connection === null || connection === void 0 ? void 0 : connection.request);
        });
        await this.hooks("afterLoadDocument", hookPayload);
        document.beforeBroadcastStateless((document, stateless) => {
            const hookPayload = {
                document,
                documentName: document.name,
                payload: stateless,
            };
            this.hooks("beforeBroadcastStateless", hookPayload);
        });
        document.awareness.on("update", (update) => {
            this.hooks("onAwarenessUpdate", {
                ...hookPayload,
                ...update,
                awareness: document.awareness,
                states: awarenessStatesToArray(document.awareness.getStates()),
            });
        });
        return document;
    }
    storeDocumentHooks(document, hookPayload, immediately) {
        const debounceId = `onStoreDocument-${document.name}`;
        return this.debouncer.debounce(debounceId, async () => {
            try {
                await document.saveMutex.runExclusive(async () => {
                    await this.hooks("onStoreDocument", hookPayload);
                    await this.hooks("afterStoreDocument", hookPayload);
                });
            }
            catch (error) {
                console.error("Caught error during storeDocumentHooks", error);
                if (error === null || error === void 0 ? void 0 : error.message) {
                    throw error;
                }
            }
            finally {
                setTimeout(() => {
                    if (this.shouldUnloadDocument(document)) {
                        this.unloadDocument(document);
                    }
                }, 0);
            }
        }, immediately ? 0 : this.configuration.debounce, this.configuration.maxDebounce);
    }
    /**
     * Run the given hook on all configured extensions.
     * Runs the given callback after each hook.
     */
    // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
    hooks(name, payload, callback = null) {
        const { extensions } = this.configuration;
        // create a new `thenable` chain
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
        let chain = Promise.resolve();
        extensions
            // get me all extensions which have the given hook
            .filter((extension) => typeof extension[name] === "function")
            // run through all the configured hooks
            .forEach((extension) => {
            chain = chain
                .then(() => { var _a; return (_a = extension[name]) === null || _a === void 0 ? void 0 : _a.call(extension, payload); })
                .catch((error) => {
                // make sure to log error messages
                if (error === null || error === void 0 ? void 0 : error.message) {
                    console.error(`[${name}]`, error.message);
                }
                throw error;
            });
            if (callback) {
                chain = chain.then((...args) => callback(...args));
            }
        });
        return chain;
    }
    shouldUnloadDocument(document) {
        const hasPendingWork = this.debouncer.isDebounced(`onStoreDocument-${document.name}`) ||
            this.debouncer.isCurrentlyExecuting(`onStoreDocument-${document.name}`) ||
            document.saveMutex.isLocked();
        return hasPendingWork === false && document.getConnectionsCount() === 0;
    }
    async unloadDocument(document) {
        const documentName = document.name;
        if (!this.shouldUnloadDocument(document))
            return;
        if (!this.documents.has(documentName))
            return;
        if (this.unloadingDocuments.has(documentName))
            return this.unloadingDocuments.get(documentName);
        // we need to make sure that the logic runs just once, even if multiple clients disconnect together
        const actualUnloadingLogic = async () => {
            try {
                await this.hooks("beforeUnloadDocument", {
                    instance: this,
                    documentName,
                    document,
                });
            }
            catch (e) {
                return;
            }
            // need sync check here as well, to avoid timing problems
            if (!this.shouldUnloadDocument(document))
                return;
            this.documents.delete(documentName);
            document.destroy();
            await this.hooks("afterUnloadDocument", { instance: this, documentName });
        };
        const unloading = actualUnloadingLogic();
        this.unloadingDocuments.set(documentName, Promise.resolve(unloading));
        await unloading;
        this.unloadingDocuments.delete(documentName);
    }
    async openDirectConnection(documentName, context) {
        const connectionConfig = {
            isAuthenticated: true,
            readOnly: false,
        };
        const document = await this.createDocument(documentName, {}, // direct connection has no request params
        crypto.randomUUID(), connectionConfig, context);
        return new DirectConnection(document, this, context);
    }
}

const defaultServerConfiguration = {
    port: 80,
    address: "0.0.0.0",
    stopOnSignals: true,
};
class Server {
    constructor(configuration, websocketOptions = {}) {
        this.configuration = {
            ...defaultConfiguration,
            ...defaultServerConfiguration,
            extensions: [],
        };
        this.setupWebsocketConnection = () => {
            this.webSocketServer.on("connection", async (incoming, request) => {
                incoming.setMaxListeners(Number.POSITIVE_INFINITY);
                incoming.on("error", (error) => {
                    /**
                     * Handle a ws instance error, which is required to prevent
                     * the server from crashing when one happens
                     * See https://github.com/websockets/ws/issues/1777#issuecomment-660803472
                     * @private
                     */
                    console.error("Error emitted from webSocket instance:");
                    console.error(error);
                });
                this.hocuspocus.handleConnection(incoming, request);
            });
        };
        this.setupHttpUpgrade = () => {
            this.httpServer.on("upgrade", async (request, socket, head) => {
                try {
                    await this.hocuspocus.hooks("onUpgrade", {
                        request,
                        socket,
                        head,
                        instance: this.hocuspocus,
                    });
                    // let the default websocket server handle the connection if
                    // prior hooks don't interfere
                    this.webSocketServer.handleUpgrade(request, socket, head, (ws) => {
                        this.webSocketServer.emit("connection", ws, request);
                    });
                }
                catch (error) {
                    // if a hook rejects and the error is empty, do nothing
                    // this is only meant to prevent later hooks and the
                    // default handler to do something. if a error is present
                    // just rethrow it
                    if (error) {
                        throw error;
                    }
                }
            });
        };
        this.requestHandler = async (request, response) => {
            try {
                await this.hocuspocus.hooks("onRequest", {
                    request,
                    response,
                    instance: this.hocuspocus,
                });
                // default response if all prior hooks don't interfere
                response.writeHead(200, { "Content-Type": "text/plain" });
                response.end("Welcome to Hocuspocus!");
            }
            catch (error) {
                // if a hook rejects and the error is empty, do nothing
                // this is only meant to prevent later hooks and the
                // default handler to do something. if a error is present
                // just rethrow it
                if (error) {
                    throw error;
                }
            }
        };
        if (configuration) {
            this.configuration = {
                ...this.configuration,
                ...configuration,
            };
        }
        this.hocuspocus = new Hocuspocus(this.configuration);
        this.hocuspocus.server = this;
        this.httpServer = createServer(this.requestHandler);
        this.webSocketServer = new WebSocketServer({
            noServer: true,
            ...websocketOptions,
        });
        this.setupWebsocketConnection();
        this.setupHttpUpgrade();
    }
    async listen(port, callback = null) {
        if (port) {
            this.configuration.port = port;
        }
        if (typeof callback === "function") {
            this.hocuspocus.configuration.extensions.push({
                onListen: callback,
            });
        }
        if (this.configuration.stopOnSignals) {
            const signalHandler = async () => {
                await this.destroy();
                process.exit(0);
            };
            process.on("SIGINT", signalHandler);
            process.on("SIGQUIT", signalHandler);
            process.on("SIGTERM", signalHandler);
        }
        // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
        return new Promise((resolve, reject) => {
            this.httpServer.listen({
                port: this.configuration.port,
                address: this.configuration.address,
            }, async () => {
                if (!this.configuration.quiet &&
                    String(process.env.NODE_ENV) !== "testing") {
                    this.showStartScreen();
                }
                const onListenPayload = {
                    instance: this.hocuspocus,
                    configuration: this.configuration,
                    port: this.address.port,
                };
                try {
                    await this.hocuspocus.hooks("onListen", onListenPayload);
                    resolve(this.hocuspocus);
                }
                catch (e) {
                    reject(e);
                }
            });
        });
    }
    get address() {
        return (this.httpServer.address() || {
            port: this.configuration.port,
            address: this.configuration.address,
            family: "IPv4",
        });
    }
    async destroy() {
        await new Promise(async (resolve) => {
            this.httpServer.close();
            try {
                this.configuration.extensions.push({
                    async afterUnloadDocument({ instance }) {
                        if (instance.getDocumentsCount() === 0)
                            resolve("");
                    },
                });
                this.webSocketServer.close();
                if (this.hocuspocus.getDocumentsCount() === 0)
                    resolve("");
                this.hocuspocus.closeConnections();
            }
            catch (error) {
                console.error(error);
            }
        });
        await this.hocuspocus.hooks("onDestroy", { instance: this.hocuspocus });
    }
    get URL() {
        return `${this.configuration.address}:${this.address.port}`;
    }
    get webSocketURL() {
        return `ws://${this.URL}`;
    }
    get httpURL() {
        return `http://${this.URL}`;
    }
    showStartScreen() {
        var _a;
        const name = this.configuration.name ? ` (${this.configuration.name})` : "";
        console.log();
        console.log(`  ${kleur.cyan(`Hocuspocus v${meta.version}${name}`)}${kleur.green(" running at:")}`);
        console.log();
        console.log(`  > HTTP: ${kleur.cyan(`${this.httpURL}`)}`);
        console.log(`  > WebSocket: ${this.webSocketURL}`);
        const extensions = (_a = this.configuration) === null || _a === void 0 ? void 0 : _a.extensions.map((extension) => {
            var _a, _b;
            return (_a = extension.extensionName) !== null && _a !== void 0 ? _a : (_b = extension.constructor) === null || _b === void 0 ? void 0 : _b.name;
        }).filter((name) => name).filter((name) => name !== "Object");
        if (!extensions.length) {
            return;
        }
        console.log();
        console.log("  Extensions:");
        extensions.forEach((name) => {
            console.log(`  - ${name}`);
        });
        console.log();
        console.log(`  ${kleur.green("Ready.")}`);
        console.log();
    }
}

export { Connection, Document, Hocuspocus, IncomingMessage, MessageReceiver, MessageType, OutgoingMessage, Server, defaultConfiguration, defaultServerConfiguration, useDebounce };
//# sourceMappingURL=hocuspocus-server.esm.js.map
