@hocuspocus/provider 2.2.3 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,8 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var Y = require('yjs');
6
5
  var common = require('@hocuspocus/common');
6
+ var Y = require('yjs');
7
7
  var attempt = require('@lifeomic/attempt');
8
8
 
9
9
  function _interopNamespace(e) {
@@ -74,6 +74,22 @@ const setIfUndefined = (map, key, createT) => {
74
74
 
75
75
  const create$1 = () => new Set();
76
76
 
77
+ /**
78
+ * Utility module to work with Arrays.
79
+ *
80
+ * @module array
81
+ */
82
+
83
+ /**
84
+ * Transforms something array-like to an actual Array.
85
+ *
86
+ * @function
87
+ * @template T
88
+ * @param {ArrayLike<T>|Iterable<T>} arraylike
89
+ * @return {T}
90
+ */
91
+ const from = Array.from;
92
+
77
93
  /**
78
94
  * Utility module to work with strings.
79
95
  *
@@ -236,22 +252,6 @@ const onChange = eventHandler => usePolyfill || addEventListener('storage', /**
236
252
  /* c8 ignore next */
237
253
  const offChange = eventHandler => usePolyfill || removeEventListener('storage', /** @type {any} */ (eventHandler));
238
254
 
239
- /**
240
- * Utility module to work with Arrays.
241
- *
242
- * @module array
243
- */
244
-
245
- /**
246
- * Transforms something array-like to an actual Array.
247
- *
248
- * @function
249
- * @template T
250
- * @param {ArrayLike<T>|Iterable<T>} arraylike
251
- * @return {T}
252
- */
253
- const from = Array.from;
254
-
255
255
  /**
256
256
  * Utility functions for working with EcmaScript objects.
257
257
  *
@@ -399,7 +399,6 @@ const equalityDeep = (a, b) => {
399
399
  */
400
400
  // @ts-ignore
401
401
  const isOneOf = (value, options) => options.includes(value);
402
- /* c8 ignore stop */
403
402
 
404
403
  /**
405
404
  * Isomorphic module to work access the environment (query params, env variables).
@@ -531,7 +530,9 @@ const min = (a, b) => a < b ? a : b;
531
530
  const max = (a, b) => a > b ? a : b;
532
531
 
533
532
  /* eslint-env browser */
533
+ const BIT7 = 64;
534
534
  const BIT8 = 128;
535
+ const BITS6 = 63;
535
536
  const BITS7 = 127;
536
537
 
537
538
  /**
@@ -889,6 +890,44 @@ const readVarUint = decoder => {
889
890
  throw errorUnexpectedEndOfArray
890
891
  };
891
892
 
893
+ /**
894
+ * Read signed integer (32bit) with variable length.
895
+ * 1/8th of the storage is used as encoding overhead.
896
+ * * numbers < 2^7 is stored in one bytlength
897
+ * * numbers < 2^14 is stored in two bylength
898
+ * @todo This should probably create the inverse ~num if number is negative - but this would be a breaking change.
899
+ *
900
+ * @function
901
+ * @param {Decoder} decoder
902
+ * @return {number} An unsigned integer.length
903
+ */
904
+ const readVarInt = decoder => {
905
+ let r = decoder.arr[decoder.pos++];
906
+ let num = r & BITS6;
907
+ let mult = 64;
908
+ const sign = (r & BIT7) > 0 ? -1 : 1;
909
+ if ((r & BIT8) === 0) {
910
+ // don't continue reading
911
+ return sign * num
912
+ }
913
+ const len = decoder.arr.length;
914
+ while (decoder.pos < len) {
915
+ r = decoder.arr[decoder.pos++];
916
+ // num = num | ((r & binary.BITS7) << len)
917
+ num = num + (r & BITS7) * mult;
918
+ mult *= 128;
919
+ if (r < BIT8) {
920
+ return sign * num
921
+ }
922
+ /* c8 ignore start */
923
+ if (num > MAX_SAFE_INTEGER) {
924
+ throw errorIntegerOutOfRange
925
+ }
926
+ /* c8 ignore stop */
927
+ }
928
+ throw errorUnexpectedEndOfArray
929
+ };
930
+
892
931
  /**
893
932
  * We don't test this function anymore as we use native decoding/encoding by default now.
894
933
  * Better not modify this anymore..
@@ -1136,6 +1175,50 @@ const publish = (room, data, origin = null) => {
1136
1175
  c.subs.forEach(sub => sub(data, origin));
1137
1176
  };
1138
1177
 
1178
+ /**
1179
+ * Mutual exclude for JavaScript.
1180
+ *
1181
+ * @module mutex
1182
+ */
1183
+
1184
+ /**
1185
+ * @callback mutex
1186
+ * @param {function():void} cb Only executed when this mutex is not in the current stack
1187
+ * @param {function():void} [elseCb] Executed when this mutex is in the current stack
1188
+ */
1189
+
1190
+ /**
1191
+ * Creates a mutual exclude function with the following property:
1192
+ *
1193
+ * ```js
1194
+ * const mutex = createMutex()
1195
+ * mutex(() => {
1196
+ * // This function is immediately executed
1197
+ * mutex(() => {
1198
+ * // This function is not executed, as the mutex is already active.
1199
+ * })
1200
+ * })
1201
+ * ```
1202
+ *
1203
+ * @return {mutex} A mutual exclude function
1204
+ * @public
1205
+ */
1206
+ const createMutex = () => {
1207
+ let token = true;
1208
+ return (f, g) => {
1209
+ if (token) {
1210
+ token = false;
1211
+ try {
1212
+ f();
1213
+ } finally {
1214
+ token = true;
1215
+ }
1216
+ } else if (g !== undefined) {
1217
+ g();
1218
+ }
1219
+ }
1220
+ };
1221
+
1139
1222
  /**
1140
1223
  * Utility module to work with time.
1141
1224
  *
@@ -1486,50 +1569,6 @@ const applyAwarenessUpdate = (awareness, update, origin) => {
1486
1569
  }
1487
1570
  };
1488
1571
 
1489
- /**
1490
- * Mutual exclude for JavaScript.
1491
- *
1492
- * @module mutex
1493
- */
1494
-
1495
- /**
1496
- * @callback mutex
1497
- * @param {function():void} cb Only executed when this mutex is not in the current stack
1498
- * @param {function():void} [elseCb] Executed when this mutex is in the current stack
1499
- */
1500
-
1501
- /**
1502
- * Creates a mutual exclude function with the following property:
1503
- *
1504
- * ```js
1505
- * const mutex = createMutex()
1506
- * mutex(() => {
1507
- * // This function is immediately executed
1508
- * mutex(() => {
1509
- * // This function is not executed, as the mutex is already active.
1510
- * })
1511
- * })
1512
- * ```
1513
- *
1514
- * @return {mutex} A mutual exclude function
1515
- * @public
1516
- */
1517
- const createMutex = () => {
1518
- let token = true;
1519
- return (f, g) => {
1520
- if (token) {
1521
- token = false;
1522
- try {
1523
- f();
1524
- } finally {
1525
- token = true;
1526
- }
1527
- } else if (g !== undefined) {
1528
- g();
1529
- }
1530
- }
1531
- };
1532
-
1533
1572
  class EventEmitter {
1534
1573
  constructor() {
1535
1574
  this.callbacks = {};
@@ -1565,717 +1604,711 @@ class EventEmitter {
1565
1604
  }
1566
1605
  }
1567
1606
 
1568
- class IncomingMessage {
1569
- constructor(data) {
1570
- this.data = data;
1571
- this.encoder = createEncoder();
1572
- this.decoder = createDecoder(new Uint8Array(this.data));
1573
- }
1574
- readVarUint() {
1575
- return readVarUint(this.decoder);
1576
- }
1577
- readVarString() {
1578
- return readVarString(this.decoder);
1579
- }
1580
- readVarUint8Array() {
1581
- return readVarUint8Array(this.decoder);
1582
- }
1583
- writeVarUint(type) {
1584
- return writeVarUint(this.encoder, type);
1585
- }
1586
- writeVarString(string) {
1587
- return writeVarString(this.encoder, string);
1588
- }
1589
- writeVarUint8Array(data) {
1590
- return writeVarUint8Array(this.encoder, data);
1591
- }
1592
- length() {
1593
- return length(this.encoder);
1594
- }
1595
- }
1596
-
1597
1607
  /**
1598
- * @module sync-protocol
1599
- */
1600
-
1601
- /**
1602
- * @typedef {Map<number, number>} StateMap
1603
- */
1604
-
1605
- /**
1606
- * Core Yjs defines two message types:
1607
- * • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
1608
- * • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
1609
- * received all information from the remote client.
1610
- *
1611
- * In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
1612
- * with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
1613
- * SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
1614
- *
1615
- * In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
1616
- * When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
1617
- * with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
1618
- * client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
1619
- * easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
1620
- * Therefore it is necesarry that the client initiates the sync.
1621
- *
1622
- * Construction of a message:
1623
- * [messageType : varUint, message definition..]
1624
- *
1625
- * Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
1608
+ * Utility module to work with urls.
1626
1609
  *
1627
- * stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
1610
+ * @module url
1628
1611
  */
1629
1612
 
1630
- const messageYjsSyncStep1 = 0;
1631
- const messageYjsSyncStep2 = 1;
1632
- const messageYjsUpdate = 2;
1633
-
1634
1613
  /**
1635
- * Create a sync step 1 message based on the state of the current shared document.
1636
- *
1637
- * @param {encoding.Encoder} encoder
1638
- * @param {Y.Doc} doc
1614
+ * @param {Object<string,string>} params
1615
+ * @return {string}
1639
1616
  */
1640
- const writeSyncStep1 = (encoder, doc) => {
1641
- writeVarUint(encoder, messageYjsSyncStep1);
1642
- const sv = Y__namespace.encodeStateVector(doc);
1643
- writeVarUint8Array(encoder, sv);
1644
- };
1617
+ const encodeQueryParams = params =>
1618
+ map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
1645
1619
 
1646
- /**
1647
- * @param {encoding.Encoder} encoder
1648
- * @param {Y.Doc} doc
1649
- * @param {Uint8Array} [encodedStateVector]
1650
- */
1651
- const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
1652
- writeVarUint(encoder, messageYjsSyncStep2);
1653
- writeVarUint8Array(encoder, Y__namespace.encodeStateAsUpdate(doc, encodedStateVector));
1654
- };
1620
+ exports.MessageType = void 0;
1621
+ (function (MessageType) {
1622
+ MessageType[MessageType["Sync"] = 0] = "Sync";
1623
+ MessageType[MessageType["Awareness"] = 1] = "Awareness";
1624
+ MessageType[MessageType["Auth"] = 2] = "Auth";
1625
+ MessageType[MessageType["QueryAwareness"] = 3] = "QueryAwareness";
1626
+ MessageType[MessageType["Stateless"] = 5] = "Stateless";
1627
+ MessageType[MessageType["CLOSE"] = 7] = "CLOSE";
1628
+ MessageType[MessageType["SyncStatus"] = 8] = "SyncStatus";
1629
+ })(exports.MessageType || (exports.MessageType = {}));
1630
+ exports.WebSocketStatus = void 0;
1631
+ (function (WebSocketStatus) {
1632
+ WebSocketStatus["Connecting"] = "connecting";
1633
+ WebSocketStatus["Connected"] = "connected";
1634
+ WebSocketStatus["Disconnected"] = "disconnected";
1635
+ })(exports.WebSocketStatus || (exports.WebSocketStatus = {}));
1655
1636
 
1656
- /**
1657
- * Read SyncStep1 message and reply with SyncStep2.
1658
- *
1659
- * @param {decoding.Decoder} decoder The reply to the received message
1660
- * @param {encoding.Encoder} encoder The received message
1661
- * @param {Y.Doc} doc
1662
- */
1663
- const readSyncStep1 = (decoder, encoder, doc) =>
1664
- writeSyncStep2(encoder, doc, readVarUint8Array(decoder));
1665
-
1666
- /**
1667
- * Read and apply Structs and then DeleteStore to a y instance.
1668
- *
1669
- * @param {decoding.Decoder} decoder
1670
- * @param {Y.Doc} doc
1671
- * @param {any} transactionOrigin
1672
- */
1673
- const readSyncStep2 = (decoder, doc, transactionOrigin) => {
1674
- try {
1675
- Y__namespace.applyUpdate(doc, readVarUint8Array(decoder), transactionOrigin);
1676
- } catch (error) {
1677
- // This catches errors that are thrown by event handlers
1678
- console.error('Caught error while handling a Yjs update', error);
1679
- }
1680
- };
1681
-
1682
- /**
1683
- * @param {encoding.Encoder} encoder
1684
- * @param {Uint8Array} update
1685
- */
1686
- const writeUpdate = (encoder, update) => {
1687
- writeVarUint(encoder, messageYjsUpdate);
1688
- writeVarUint8Array(encoder, update);
1689
- };
1690
-
1691
- /**
1692
- * Read and apply Structs and then DeleteStore to a y instance.
1693
- *
1694
- * @param {decoding.Decoder} decoder
1695
- * @param {Y.Doc} doc
1696
- * @param {any} transactionOrigin
1697
- */
1698
- const readUpdate = readSyncStep2;
1699
-
1700
- /**
1701
- * @param {decoding.Decoder} decoder A message received from another client
1702
- * @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
1703
- * @param {Y.Doc} doc
1704
- * @param {any} transactionOrigin
1705
- */
1706
- const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
1707
- const messageType = readVarUint(decoder);
1708
- switch (messageType) {
1709
- case messageYjsSyncStep1:
1710
- readSyncStep1(decoder, encoder, doc);
1711
- break
1712
- case messageYjsSyncStep2:
1713
- readSyncStep2(decoder, doc, transactionOrigin);
1714
- break
1715
- case messageYjsUpdate:
1716
- readUpdate(decoder, doc, transactionOrigin);
1717
- break
1718
- default:
1719
- throw new Error('Unknown message type')
1720
- }
1721
- return messageType
1722
- };
1723
-
1724
- exports.MessageType = void 0;
1725
- (function (MessageType) {
1726
- MessageType[MessageType["Sync"] = 0] = "Sync";
1727
- MessageType[MessageType["Awareness"] = 1] = "Awareness";
1728
- MessageType[MessageType["Auth"] = 2] = "Auth";
1729
- MessageType[MessageType["QueryAwareness"] = 3] = "QueryAwareness";
1730
- MessageType[MessageType["Stateless"] = 5] = "Stateless";
1731
- MessageType[MessageType["CLOSE"] = 7] = "CLOSE";
1732
- MessageType[MessageType["SyncStatus"] = 8] = "SyncStatus";
1733
- })(exports.MessageType || (exports.MessageType = {}));
1734
- exports.WebSocketStatus = void 0;
1735
- (function (WebSocketStatus) {
1736
- WebSocketStatus["Connecting"] = "connecting";
1737
- WebSocketStatus["Connected"] = "connected";
1738
- WebSocketStatus["Disconnected"] = "disconnected";
1739
- })(exports.WebSocketStatus || (exports.WebSocketStatus = {}));
1740
-
1741
- class OutgoingMessage {
1742
- constructor() {
1743
- this.encoder = createEncoder();
1637
+ class HocuspocusProviderWebsocket extends EventEmitter {
1638
+ constructor(configuration) {
1639
+ super();
1640
+ this.messageQueue = [];
1641
+ this.configuration = {
1642
+ url: '',
1643
+ // @ts-ignore
1644
+ document: undefined,
1645
+ // @ts-ignore
1646
+ awareness: undefined,
1647
+ WebSocketPolyfill: undefined,
1648
+ parameters: {},
1649
+ connect: true,
1650
+ broadcast: true,
1651
+ forceSyncInterval: false,
1652
+ // TODO: this should depend on awareness.outdatedTime
1653
+ messageReconnectTimeout: 30000,
1654
+ // 1 second
1655
+ delay: 1000,
1656
+ // instant
1657
+ initialDelay: 0,
1658
+ // double the delay each time
1659
+ factor: 2,
1660
+ // unlimited retries
1661
+ maxAttempts: 0,
1662
+ // wait at least 1 second
1663
+ minDelay: 1000,
1664
+ // at least every 30 seconds
1665
+ maxDelay: 30000,
1666
+ // randomize
1667
+ jitter: true,
1668
+ // retry forever
1669
+ timeout: 0,
1670
+ onOpen: () => null,
1671
+ onConnect: () => null,
1672
+ onMessage: () => null,
1673
+ onOutgoingMessage: () => null,
1674
+ onStatus: () => null,
1675
+ onDisconnect: () => null,
1676
+ onClose: () => null,
1677
+ onDestroy: () => null,
1678
+ onAwarenessUpdate: () => null,
1679
+ onAwarenessChange: () => null,
1680
+ quiet: false,
1681
+ };
1682
+ this.subscribedToBroadcastChannel = false;
1683
+ this.webSocket = null;
1684
+ this.shouldConnect = true;
1685
+ this.status = exports.WebSocketStatus.Disconnected;
1686
+ this.lastMessageReceived = 0;
1687
+ this.mux = createMutex();
1688
+ this.intervals = {
1689
+ forceSync: null,
1690
+ connectionChecker: null,
1691
+ };
1692
+ this.connectionAttempt = null;
1693
+ this.receivedOnOpenPayload = undefined;
1694
+ this.receivedOnStatusPayload = undefined;
1695
+ this.boundConnect = this.connect.bind(this);
1696
+ this.closeTries = 0;
1697
+ this.setConfiguration(configuration);
1698
+ this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
1699
+ this.on('open', this.configuration.onOpen);
1700
+ this.on('open', this.onOpen.bind(this));
1701
+ this.on('connect', this.configuration.onConnect);
1702
+ this.on('message', this.configuration.onMessage);
1703
+ this.on('outgoingMessage', this.configuration.onOutgoingMessage);
1704
+ this.on('status', this.configuration.onStatus);
1705
+ this.on('status', this.onStatus.bind(this));
1706
+ this.on('disconnect', this.configuration.onDisconnect);
1707
+ this.on('close', this.configuration.onClose);
1708
+ this.on('destroy', this.configuration.onDestroy);
1709
+ this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
1710
+ this.on('awarenessChange', this.configuration.onAwarenessChange);
1711
+ this.on('close', this.onClose.bind(this));
1712
+ this.on('message', this.onMessage.bind(this));
1713
+ this.registerEventListeners();
1714
+ this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
1715
+ if (typeof configuration.connect !== 'undefined') {
1716
+ this.shouldConnect = configuration.connect;
1717
+ }
1718
+ if (!this.shouldConnect) {
1719
+ return;
1720
+ }
1721
+ this.connect();
1744
1722
  }
1745
- get(args) {
1746
- return args.encoder;
1723
+ async onOpen(event) {
1724
+ this.receivedOnOpenPayload = event;
1747
1725
  }
1748
- toUint8Array() {
1749
- return toUint8Array(this.encoder);
1726
+ async onStatus(data) {
1727
+ this.receivedOnStatusPayload = data;
1750
1728
  }
1751
- }
1752
-
1753
- class MessageReceiver {
1754
- constructor(message) {
1755
- this.broadcasted = false;
1756
- this.message = message;
1729
+ attach(provider) {
1730
+ if (this.status === exports.WebSocketStatus.Disconnected && this.shouldConnect) {
1731
+ this.connect();
1732
+ }
1733
+ if (this.receivedOnOpenPayload) {
1734
+ provider.onOpen(this.receivedOnOpenPayload);
1735
+ }
1736
+ if (this.receivedOnStatusPayload) {
1737
+ provider.onStatus(this.receivedOnStatusPayload);
1738
+ }
1757
1739
  }
1758
- setBroadcasted(value) {
1759
- this.broadcasted = value;
1760
- return this;
1740
+ detach(provider) {
1741
+ // tell the server to remove the listener
1761
1742
  }
1762
- apply(provider, emitSynced = true) {
1763
- const { message } = this;
1764
- const type = message.readVarUint();
1765
- const emptyMessageLength = message.length();
1766
- switch (type) {
1767
- case exports.MessageType.Sync:
1768
- this.applySyncMessage(provider, emitSynced);
1769
- break;
1770
- case exports.MessageType.Awareness:
1771
- this.applyAwarenessMessage(provider);
1772
- break;
1773
- case exports.MessageType.Auth:
1774
- this.applyAuthMessage(provider);
1775
- break;
1776
- case exports.MessageType.QueryAwareness:
1777
- this.applyQueryAwarenessMessage(provider);
1778
- break;
1779
- case exports.MessageType.Stateless:
1780
- provider.receiveStateless(readVarString(message.decoder));
1781
- break;
1782
- case exports.MessageType.SyncStatus:
1783
- // nothing for now; forward-compatability
1784
- break;
1785
- default:
1786
- throw new Error(`Can’t apply message of unknown type: ${type}`);
1743
+ setConfiguration(configuration = {}) {
1744
+ this.configuration = { ...this.configuration, ...configuration };
1745
+ }
1746
+ async connect() {
1747
+ if (this.status === exports.WebSocketStatus.Connected) {
1748
+ return;
1787
1749
  }
1788
- // Reply
1789
- if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
1790
- if (this.broadcasted) {
1791
- // TODO: Some weird TypeScript error
1792
- // @ts-ignore
1793
- provider.broadcast(OutgoingMessage, { encoder: message.encoder });
1750
+ // Always cancel any previously initiated connection retryer instances
1751
+ if (this.cancelWebsocketRetry) {
1752
+ this.cancelWebsocketRetry();
1753
+ this.cancelWebsocketRetry = undefined;
1754
+ }
1755
+ this.receivedOnOpenPayload = undefined;
1756
+ this.receivedOnStatusPayload = undefined;
1757
+ this.shouldConnect = true;
1758
+ const abortableRetry = () => {
1759
+ let cancelAttempt = false;
1760
+ const retryPromise = attempt.retry(this.createWebSocketConnection.bind(this), {
1761
+ delay: this.configuration.delay,
1762
+ initialDelay: this.configuration.initialDelay,
1763
+ factor: this.configuration.factor,
1764
+ maxAttempts: this.configuration.maxAttempts,
1765
+ minDelay: this.configuration.minDelay,
1766
+ maxDelay: this.configuration.maxDelay,
1767
+ jitter: this.configuration.jitter,
1768
+ timeout: this.configuration.timeout,
1769
+ beforeAttempt: context => {
1770
+ if (!this.shouldConnect || cancelAttempt) {
1771
+ context.abort();
1772
+ }
1773
+ },
1774
+ }).catch((error) => {
1775
+ // If we aborted the connection attempt then don’t throw an error
1776
+ // ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
1777
+ if (error && error.code !== 'ATTEMPT_ABORTED') {
1778
+ throw error;
1779
+ }
1780
+ });
1781
+ return {
1782
+ retryPromise,
1783
+ cancelFunc: () => {
1784
+ cancelAttempt = true;
1785
+ },
1786
+ };
1787
+ };
1788
+ const { retryPromise, cancelFunc } = abortableRetry();
1789
+ this.cancelWebsocketRetry = cancelFunc;
1790
+ return retryPromise;
1791
+ }
1792
+ createWebSocketConnection() {
1793
+ return new Promise((resolve, reject) => {
1794
+ if (this.webSocket) {
1795
+ this.messageQueue = [];
1796
+ this.webSocket.close();
1797
+ this.webSocket = null;
1798
+ }
1799
+ // Init the WebSocket connection
1800
+ const ws = new this.configuration.WebSocketPolyfill(this.url);
1801
+ ws.binaryType = 'arraybuffer';
1802
+ ws.onmessage = (payload) => this.emit('message', payload);
1803
+ ws.onclose = (payload) => this.emit('close', { event: payload });
1804
+ ws.onopen = (payload) => this.emit('open', payload);
1805
+ ws.onerror = (err) => {
1806
+ reject(err);
1807
+ };
1808
+ this.webSocket = ws;
1809
+ // Reset the status
1810
+ this.status = exports.WebSocketStatus.Connecting;
1811
+ this.emit('status', { status: exports.WebSocketStatus.Connecting });
1812
+ // Store resolve/reject for later use
1813
+ this.connectionAttempt = {
1814
+ resolve,
1815
+ reject,
1816
+ };
1817
+ });
1818
+ }
1819
+ onMessage(event) {
1820
+ this.resolveConnectionAttempt();
1821
+ this.lastMessageReceived = getUnixTime();
1822
+ }
1823
+ resolveConnectionAttempt() {
1824
+ if (this.connectionAttempt) {
1825
+ this.connectionAttempt.resolve();
1826
+ this.connectionAttempt = null;
1827
+ this.status = exports.WebSocketStatus.Connected;
1828
+ this.emit('status', { status: exports.WebSocketStatus.Connected });
1829
+ this.emit('connect');
1830
+ this.messageQueue.forEach(message => this.send(message));
1831
+ this.messageQueue = [];
1832
+ }
1833
+ }
1834
+ stopConnectionAttempt() {
1835
+ this.connectionAttempt = null;
1836
+ }
1837
+ rejectConnectionAttempt() {
1838
+ var _a;
1839
+ (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
1840
+ this.connectionAttempt = null;
1841
+ }
1842
+ checkConnection() {
1843
+ var _a;
1844
+ // Don’t check the connection when it’s not even established
1845
+ if (this.status !== exports.WebSocketStatus.Connected) {
1846
+ return;
1847
+ }
1848
+ // Don’t close then connection while waiting for the first message
1849
+ if (!this.lastMessageReceived) {
1850
+ return;
1851
+ }
1852
+ // Don’t close the connection when a message was received recently
1853
+ if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
1854
+ return;
1855
+ }
1856
+ // No message received in a long time, not even your own
1857
+ // Awareness updates, which are updated every 15 seconds.
1858
+ this.closeTries += 1;
1859
+ // https://bugs.webkit.org/show_bug.cgi?id=247943
1860
+ if (this.closeTries > 2) {
1861
+ this.onClose({
1862
+ event: {
1863
+ code: 4408,
1864
+ reason: 'forced',
1865
+ },
1866
+ });
1867
+ this.closeTries = 0;
1868
+ }
1869
+ else {
1870
+ (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
1871
+ this.messageQueue = [];
1872
+ }
1873
+ }
1874
+ registerEventListeners() {
1875
+ if (typeof window === 'undefined') {
1876
+ return;
1877
+ }
1878
+ window.addEventListener('online', this.boundConnect);
1879
+ }
1880
+ // Ensure that the URL always ends with /
1881
+ get serverUrl() {
1882
+ while (this.configuration.url[this.configuration.url.length - 1] === '/') {
1883
+ return this.configuration.url.slice(0, this.configuration.url.length - 1);
1884
+ }
1885
+ return this.configuration.url;
1886
+ }
1887
+ get url() {
1888
+ const encodedParams = encodeQueryParams(this.configuration.parameters);
1889
+ return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
1890
+ }
1891
+ disconnect() {
1892
+ this.shouldConnect = false;
1893
+ if (this.webSocket === null) {
1894
+ return;
1895
+ }
1896
+ try {
1897
+ this.webSocket.close();
1898
+ this.messageQueue = [];
1899
+ }
1900
+ catch {
1901
+ //
1902
+ }
1903
+ }
1904
+ send(message) {
1905
+ var _a;
1906
+ if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === common.WsReadyStates.Open) {
1907
+ this.webSocket.send(message);
1908
+ }
1909
+ else {
1910
+ this.messageQueue.push(message);
1911
+ }
1912
+ }
1913
+ onClose({ event }) {
1914
+ this.closeTries = 0;
1915
+ this.webSocket = null;
1916
+ if (this.status === exports.WebSocketStatus.Connected) {
1917
+ this.status = exports.WebSocketStatus.Disconnected;
1918
+ this.emit('status', { status: exports.WebSocketStatus.Disconnected });
1919
+ this.emit('disconnect', { event });
1920
+ }
1921
+ if (event.code === common.Unauthorized.code) {
1922
+ if (event.reason === common.Unauthorized.reason) {
1923
+ console.warn('[HocuspocusProvider] An authentication token is required, but you didn’t send one. Try adding a `token` to your HocuspocusProvider configuration. Won’t try again.');
1794
1924
  }
1795
1925
  else {
1796
- // TODO: Some weird TypeScript error
1797
- // @ts-ignore
1798
- provider.send(OutgoingMessage, { encoder: message.encoder });
1926
+ console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
1799
1927
  }
1928
+ this.shouldConnect = false;
1800
1929
  }
1801
- }
1802
- applySyncMessage(provider, emitSynced) {
1803
- const { message } = this;
1804
- message.writeVarUint(exports.MessageType.Sync);
1805
- // Apply update
1806
- const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
1807
- // Synced once we receive Step2
1808
- if (emitSynced && syncMessageType === messageYjsSyncStep2) {
1809
- provider.synced = true;
1930
+ if (event.code === common.Forbidden.code) {
1931
+ if (!this.configuration.quiet) {
1932
+ console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
1933
+ return; // TODO REMOVE ME
1934
+ }
1935
+ }
1936
+ if (event.code === common.MessageTooBig.code) {
1937
+ console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
1938
+ this.shouldConnect = false;
1939
+ }
1940
+ if (this.connectionAttempt) {
1941
+ // That connection attempt failed.
1942
+ this.rejectConnectionAttempt();
1943
+ }
1944
+ else if (this.shouldConnect) {
1945
+ // The connection was closed by the server. Let’s just try again.
1946
+ this.connect();
1947
+ }
1948
+ // If we’ll reconnect, we’re done for now.
1949
+ if (this.shouldConnect) {
1950
+ return;
1810
1951
  }
1811
- if (syncMessageType === messageYjsUpdate || syncMessageType === messageYjsSyncStep2) {
1812
- if (provider.unsyncedChanges > 0) {
1813
- provider.updateUnsyncedChanges(-1);
1814
- }
1952
+ // The status is set correctly already.
1953
+ if (this.status === exports.WebSocketStatus.Disconnected) {
1954
+ return;
1815
1955
  }
1956
+ // Let’s update the connection status.
1957
+ this.status = exports.WebSocketStatus.Disconnected;
1958
+ this.emit('status', { status: exports.WebSocketStatus.Disconnected });
1959
+ this.emit('disconnect', { event });
1816
1960
  }
1817
- applyAwarenessMessage(provider) {
1818
- const { message } = this;
1819
- applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
1820
- }
1821
- applyAuthMessage(provider) {
1822
- const { message } = this;
1823
- common.readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
1824
- }
1825
- applyQueryAwarenessMessage(provider) {
1826
- const { message } = this;
1827
- message.writeVarUint(exports.MessageType.Awareness);
1828
- message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
1961
+ destroy() {
1962
+ this.emit('destroy');
1963
+ if (this.intervals.forceSync) {
1964
+ clearInterval(this.intervals.forceSync);
1965
+ }
1966
+ clearInterval(this.intervals.connectionChecker);
1967
+ // If there is still a connection attempt outstanding then we should stop
1968
+ // it before calling disconnect, otherwise it will be rejected in the onClose
1969
+ // handler and trigger a retry
1970
+ this.stopConnectionAttempt();
1971
+ this.disconnect();
1972
+ this.removeAllListeners();
1973
+ if (typeof window === 'undefined') {
1974
+ return;
1975
+ }
1976
+ window.removeEventListener('online', this.boundConnect);
1829
1977
  }
1830
1978
  }
1831
1979
 
1832
- class MessageSender {
1833
- constructor(Message, args = {}) {
1834
- this.message = new Message();
1835
- this.encoder = this.message.get(args);
1980
+ class IncomingMessage {
1981
+ constructor(data) {
1982
+ this.data = data;
1983
+ this.encoder = createEncoder();
1984
+ this.decoder = createDecoder(new Uint8Array(this.data));
1836
1985
  }
1837
- create() {
1838
- return toUint8Array(this.encoder);
1986
+ readVarUint() {
1987
+ return readVarUint(this.decoder);
1839
1988
  }
1840
- send(webSocket) {
1841
- webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
1989
+ readVarString() {
1990
+ return readVarString(this.decoder);
1842
1991
  }
1843
- broadcast(channel) {
1844
- publish(channel, this.create());
1992
+ readVarUint8Array() {
1993
+ return readVarUint8Array(this.decoder);
1845
1994
  }
1846
- }
1847
-
1848
- class SyncStepOneMessage extends OutgoingMessage {
1849
- constructor() {
1850
- super(...arguments);
1851
- this.type = exports.MessageType.Sync;
1852
- this.description = 'First sync step';
1995
+ writeVarUint(type) {
1996
+ return writeVarUint(this.encoder, type);
1853
1997
  }
1854
- get(args) {
1855
- if (typeof args.document === 'undefined') {
1856
- throw new Error('The sync step one message requires document as an argument');
1857
- }
1858
- writeVarString(this.encoder, args.documentName);
1859
- writeVarUint(this.encoder, this.type);
1860
- writeSyncStep1(this.encoder, args.document);
1861
- return this.encoder;
1998
+ writeVarString(string) {
1999
+ return writeVarString(this.encoder, string);
1862
2000
  }
1863
- }
1864
-
1865
- class SyncStepTwoMessage extends OutgoingMessage {
1866
- constructor() {
1867
- super(...arguments);
1868
- this.type = exports.MessageType.Sync;
1869
- this.description = 'Second sync step';
2001
+ writeVarUint8Array(data) {
2002
+ return writeVarUint8Array(this.encoder, data);
1870
2003
  }
1871
- get(args) {
1872
- if (typeof args.document === 'undefined') {
1873
- throw new Error('The sync step two message requires document as an argument');
1874
- }
1875
- writeVarString(this.encoder, args.documentName);
1876
- writeVarUint(this.encoder, this.type);
1877
- writeSyncStep2(this.encoder, args.document);
1878
- return this.encoder;
2004
+ length() {
2005
+ return length(this.encoder);
1879
2006
  }
1880
2007
  }
1881
2008
 
1882
- class QueryAwarenessMessage extends OutgoingMessage {
1883
- constructor() {
1884
- super(...arguments);
1885
- this.type = exports.MessageType.QueryAwareness;
1886
- this.description = 'Queries awareness states';
1887
- }
1888
- get(args) {
1889
- console.log('queryAwareness: writing string docName', args.documentName);
1890
- console.log(this.encoder.cpos);
1891
- writeVarString(this.encoder, args.documentName);
1892
- writeVarUint(this.encoder, this.type);
1893
- return this.encoder;
1894
- }
1895
- }
2009
+ /**
2010
+ * @module sync-protocol
2011
+ */
1896
2012
 
1897
- class AuthenticationMessage extends OutgoingMessage {
1898
- constructor() {
1899
- super(...arguments);
1900
- this.type = exports.MessageType.Auth;
1901
- this.description = 'Authentication';
1902
- }
1903
- get(args) {
1904
- if (typeof args.token === 'undefined') {
1905
- throw new Error('The authentication message requires `token` as an argument.');
1906
- }
1907
- writeVarString(this.encoder, args.documentName);
1908
- writeVarUint(this.encoder, this.type);
1909
- common.writeAuthentication(this.encoder, args.token);
1910
- return this.encoder;
1911
- }
1912
- }
2013
+ /**
2014
+ * @typedef {Map<number, number>} StateMap
2015
+ */
1913
2016
 
1914
- class AwarenessMessage extends OutgoingMessage {
1915
- constructor() {
1916
- super(...arguments);
1917
- this.type = exports.MessageType.Awareness;
1918
- this.description = 'Awareness states update';
1919
- }
1920
- get(args) {
1921
- if (typeof args.awareness === 'undefined') {
1922
- throw new Error('The awareness message requires awareness as an argument');
1923
- }
1924
- if (typeof args.clients === 'undefined') {
1925
- throw new Error('The awareness message requires clients as an argument');
1926
- }
1927
- writeVarString(this.encoder, args.documentName);
1928
- writeVarUint(this.encoder, this.type);
1929
- let awarenessUpdate;
1930
- if (args.states === undefined) {
1931
- awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
1932
- }
1933
- else {
1934
- awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
1935
- }
1936
- writeVarUint8Array(this.encoder, awarenessUpdate);
1937
- return this.encoder;
1938
- }
1939
- }
2017
+ /**
2018
+ * Core Yjs defines two message types:
2019
+ * • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
2020
+ * • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
2021
+ * received all information from the remote client.
2022
+ *
2023
+ * In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
2024
+ * with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
2025
+ * SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
2026
+ *
2027
+ * In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
2028
+ * When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
2029
+ * with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
2030
+ * client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
2031
+ * easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
2032
+ * Therefore it is necesarry that the client initiates the sync.
2033
+ *
2034
+ * Construction of a message:
2035
+ * [messageType : varUint, message definition..]
2036
+ *
2037
+ * Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
2038
+ *
2039
+ * stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
2040
+ */
1940
2041
 
1941
- class UpdateMessage extends OutgoingMessage {
1942
- constructor() {
1943
- super(...arguments);
1944
- this.type = exports.MessageType.Sync;
1945
- this.description = 'A document update';
1946
- }
1947
- get(args) {
1948
- writeVarString(this.encoder, args.documentName);
1949
- writeVarUint(this.encoder, this.type);
1950
- writeUpdate(this.encoder, args.update);
1951
- return this.encoder;
1952
- }
1953
- }
2042
+ const messageYjsSyncStep1 = 0;
2043
+ const messageYjsSyncStep2 = 1;
2044
+ const messageYjsUpdate = 2;
1954
2045
 
1955
2046
  /**
1956
- * Utility module to work with urls.
2047
+ * Create a sync step 1 message based on the state of the current shared document.
2048
+ *
2049
+ * @param {encoding.Encoder} encoder
2050
+ * @param {Y.Doc} doc
2051
+ */
2052
+ const writeSyncStep1 = (encoder, doc) => {
2053
+ writeVarUint(encoder, messageYjsSyncStep1);
2054
+ const sv = Y__namespace.encodeStateVector(doc);
2055
+ writeVarUint8Array(encoder, sv);
2056
+ };
2057
+
2058
+ /**
2059
+ * @param {encoding.Encoder} encoder
2060
+ * @param {Y.Doc} doc
2061
+ * @param {Uint8Array} [encodedStateVector]
2062
+ */
2063
+ const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
2064
+ writeVarUint(encoder, messageYjsSyncStep2);
2065
+ writeVarUint8Array(encoder, Y__namespace.encodeStateAsUpdate(doc, encodedStateVector));
2066
+ };
2067
+
2068
+ /**
2069
+ * Read SyncStep1 message and reply with SyncStep2.
2070
+ *
2071
+ * @param {decoding.Decoder} decoder The reply to the received message
2072
+ * @param {encoding.Encoder} encoder The received message
2073
+ * @param {Y.Doc} doc
2074
+ */
2075
+ const readSyncStep1 = (decoder, encoder, doc) =>
2076
+ writeSyncStep2(encoder, doc, readVarUint8Array(decoder));
2077
+
2078
+ /**
2079
+ * Read and apply Structs and then DeleteStore to a y instance.
2080
+ *
2081
+ * @param {decoding.Decoder} decoder
2082
+ * @param {Y.Doc} doc
2083
+ * @param {any} transactionOrigin
2084
+ */
2085
+ const readSyncStep2 = (decoder, doc, transactionOrigin) => {
2086
+ try {
2087
+ Y__namespace.applyUpdate(doc, readVarUint8Array(decoder), transactionOrigin);
2088
+ } catch (error) {
2089
+ // This catches errors that are thrown by event handlers
2090
+ console.error('Caught error while handling a Yjs update', error);
2091
+ }
2092
+ };
2093
+
2094
+ /**
2095
+ * @param {encoding.Encoder} encoder
2096
+ * @param {Uint8Array} update
2097
+ */
2098
+ const writeUpdate = (encoder, update) => {
2099
+ writeVarUint(encoder, messageYjsUpdate);
2100
+ writeVarUint8Array(encoder, update);
2101
+ };
2102
+
2103
+ /**
2104
+ * Read and apply Structs and then DeleteStore to a y instance.
1957
2105
  *
1958
- * @module url
2106
+ * @param {decoding.Decoder} decoder
2107
+ * @param {Y.Doc} doc
2108
+ * @param {any} transactionOrigin
1959
2109
  */
2110
+ const readUpdate = readSyncStep2;
1960
2111
 
1961
2112
  /**
1962
- * @param {Object<string,string>} params
1963
- * @return {string}
2113
+ * @param {decoding.Decoder} decoder A message received from another client
2114
+ * @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
2115
+ * @param {Y.Doc} doc
2116
+ * @param {any} transactionOrigin
1964
2117
  */
1965
- const encodeQueryParams = params =>
1966
- map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
2118
+ const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
2119
+ const messageType = readVarUint(decoder);
2120
+ switch (messageType) {
2121
+ case messageYjsSyncStep1:
2122
+ readSyncStep1(decoder, encoder, doc);
2123
+ break
2124
+ case messageYjsSyncStep2:
2125
+ readSyncStep2(decoder, doc, transactionOrigin);
2126
+ break
2127
+ case messageYjsUpdate:
2128
+ readUpdate(decoder, doc, transactionOrigin);
2129
+ break
2130
+ default:
2131
+ throw new Error('Unknown message type')
2132
+ }
2133
+ return messageType
2134
+ };
1967
2135
 
1968
- class HocuspocusProviderWebsocket extends EventEmitter {
1969
- constructor(configuration) {
1970
- super();
1971
- this.configuration = {
1972
- url: '',
1973
- // @ts-ignore
1974
- document: undefined,
1975
- // @ts-ignore
1976
- awareness: undefined,
1977
- WebSocketPolyfill: undefined,
1978
- parameters: {},
1979
- connect: true,
1980
- broadcast: true,
1981
- forceSyncInterval: false,
1982
- // TODO: this should depend on awareness.outdatedTime
1983
- messageReconnectTimeout: 30000,
1984
- // 1 second
1985
- delay: 1000,
1986
- // instant
1987
- initialDelay: 0,
1988
- // double the delay each time
1989
- factor: 2,
1990
- // unlimited retries
1991
- maxAttempts: 0,
1992
- // wait at least 1 second
1993
- minDelay: 1000,
1994
- // at least every 30 seconds
1995
- maxDelay: 30000,
1996
- // randomize
1997
- jitter: true,
1998
- // retry forever
1999
- timeout: 0,
2000
- onOpen: () => null,
2001
- onConnect: () => null,
2002
- onMessage: () => null,
2003
- onOutgoingMessage: () => null,
2004
- onStatus: () => null,
2005
- onDisconnect: () => null,
2006
- onClose: () => null,
2007
- onDestroy: () => null,
2008
- onAwarenessUpdate: () => null,
2009
- onAwarenessChange: () => null,
2010
- quiet: false,
2011
- };
2012
- this.subscribedToBroadcastChannel = false;
2013
- this.webSocket = null;
2014
- this.shouldConnect = true;
2015
- this.status = exports.WebSocketStatus.Disconnected;
2016
- this.lastMessageReceived = 0;
2017
- this.mux = createMutex();
2018
- this.intervals = {
2019
- forceSync: null,
2020
- connectionChecker: null,
2021
- };
2022
- this.connectionAttempt = null;
2023
- this.receivedOnOpenPayload = undefined;
2024
- this.receivedOnStatusPayload = undefined;
2025
- this.boundConnect = this.connect.bind(this);
2026
- this.setConfiguration(configuration);
2027
- this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
2028
- this.on('open', this.configuration.onOpen);
2029
- this.on('open', this.onOpen.bind(this));
2030
- this.on('connect', this.configuration.onConnect);
2031
- this.on('message', this.configuration.onMessage);
2032
- this.on('outgoingMessage', this.configuration.onOutgoingMessage);
2033
- this.on('status', this.configuration.onStatus);
2034
- this.on('status', this.onStatus.bind(this));
2035
- this.on('disconnect', this.configuration.onDisconnect);
2036
- this.on('close', this.configuration.onClose);
2037
- this.on('destroy', this.configuration.onDestroy);
2038
- this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
2039
- this.on('awarenessChange', this.configuration.onAwarenessChange);
2040
- this.on('close', this.onClose.bind(this));
2041
- this.on('message', this.onMessage.bind(this));
2042
- this.registerEventListeners();
2043
- this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
2044
- if (typeof configuration.connect !== 'undefined') {
2045
- this.shouldConnect = configuration.connect;
2046
- }
2047
- if (!this.shouldConnect) {
2048
- return;
2049
- }
2050
- this.connect();
2051
- }
2052
- async onOpen(event) {
2053
- this.receivedOnOpenPayload = event;
2136
+ class OutgoingMessage {
2137
+ constructor() {
2138
+ this.encoder = createEncoder();
2054
2139
  }
2055
- async onStatus(data) {
2056
- this.receivedOnStatusPayload = data;
2140
+ get(args) {
2141
+ return args.encoder;
2057
2142
  }
2058
- attach(provider) {
2059
- if (this.receivedOnOpenPayload) {
2060
- provider.onOpen(this.receivedOnOpenPayload);
2061
- }
2062
- if (this.receivedOnStatusPayload) {
2063
- provider.onStatus(this.receivedOnStatusPayload);
2064
- }
2143
+ toUint8Array() {
2144
+ return toUint8Array(this.encoder);
2065
2145
  }
2066
- detach(provider) {
2067
- // tell the server to remove the listener
2146
+ }
2147
+
2148
+ class MessageReceiver {
2149
+ constructor(message) {
2150
+ this.broadcasted = false;
2151
+ this.message = message;
2068
2152
  }
2069
- setConfiguration(configuration = {}) {
2070
- this.configuration = { ...this.configuration, ...configuration };
2153
+ setBroadcasted(value) {
2154
+ this.broadcasted = value;
2155
+ return this;
2071
2156
  }
2072
- async connect() {
2073
- if (this.status === exports.WebSocketStatus.Connected) {
2074
- return;
2075
- }
2076
- // Always cancel any previously initiated connection retryer instances
2077
- if (this.cancelWebsocketRetry) {
2078
- this.cancelWebsocketRetry();
2079
- this.cancelWebsocketRetry = undefined;
2157
+ apply(provider, emitSynced) {
2158
+ const { message } = this;
2159
+ const type = message.readVarUint();
2160
+ const emptyMessageLength = message.length();
2161
+ switch (type) {
2162
+ case exports.MessageType.Sync:
2163
+ this.applySyncMessage(provider, emitSynced);
2164
+ break;
2165
+ case exports.MessageType.Awareness:
2166
+ this.applyAwarenessMessage(provider);
2167
+ break;
2168
+ case exports.MessageType.Auth:
2169
+ this.applyAuthMessage(provider);
2170
+ break;
2171
+ case exports.MessageType.QueryAwareness:
2172
+ this.applyQueryAwarenessMessage(provider);
2173
+ break;
2174
+ case exports.MessageType.Stateless:
2175
+ provider.receiveStateless(readVarString(message.decoder));
2176
+ break;
2177
+ case exports.MessageType.SyncStatus:
2178
+ this.applySyncStatusMessage(provider, readVarInt(message.decoder) === 1);
2179
+ break;
2180
+ default:
2181
+ throw new Error(`Can’t apply message of unknown type: ${type}`);
2080
2182
  }
2081
- this.shouldConnect = true;
2082
- const abortableRetry = () => {
2083
- let cancelAttempt = false;
2084
- const retryPromise = attempt.retry(this.createWebSocketConnection.bind(this), {
2085
- delay: this.configuration.delay,
2086
- initialDelay: this.configuration.initialDelay,
2087
- factor: this.configuration.factor,
2088
- maxAttempts: this.configuration.maxAttempts,
2089
- minDelay: this.configuration.minDelay,
2090
- maxDelay: this.configuration.maxDelay,
2091
- jitter: this.configuration.jitter,
2092
- timeout: this.configuration.timeout,
2093
- beforeAttempt: context => {
2094
- if (!this.shouldConnect || cancelAttempt) {
2095
- context.abort();
2096
- }
2097
- },
2098
- }).catch((error) => {
2099
- // If we aborted the connection attempt then don’t throw an error
2100
- // ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
2101
- if (error && error.code !== 'ATTEMPT_ABORTED') {
2102
- throw error;
2103
- }
2104
- });
2105
- return {
2106
- retryPromise,
2107
- cancelFunc: () => {
2108
- cancelAttempt = true;
2109
- },
2110
- };
2111
- };
2112
- const { retryPromise, cancelFunc } = abortableRetry();
2113
- this.cancelWebsocketRetry = cancelFunc;
2114
- return retryPromise;
2115
- }
2116
- createWebSocketConnection() {
2117
- return new Promise((resolve, reject) => {
2118
- if (this.webSocket) {
2119
- this.webSocket.close();
2120
- this.webSocket = null;
2183
+ // Reply
2184
+ if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
2185
+ if (this.broadcasted) {
2186
+ // TODO: Some weird TypeScript error
2187
+ // @ts-ignore
2188
+ provider.broadcast(OutgoingMessage, { encoder: message.encoder });
2121
2189
  }
2122
- // Init the WebSocket connection
2123
- const ws = new this.configuration.WebSocketPolyfill(this.url);
2124
- ws.binaryType = 'arraybuffer';
2125
- ws.onmessage = (payload) => this.emit('message', payload);
2126
- ws.onclose = (payload) => this.emit('close', { event: payload });
2127
- ws.onopen = (payload) => this.emit('open', payload);
2128
- ws.onerror = (err) => {
2129
- reject(err);
2130
- };
2131
- this.webSocket = ws;
2132
- // Reset the status
2133
- this.status = exports.WebSocketStatus.Connecting;
2134
- this.emit('status', { status: exports.WebSocketStatus.Connecting });
2135
- // Store resolve/reject for later use
2136
- this.connectionAttempt = {
2137
- resolve,
2138
- reject,
2139
- };
2140
- });
2190
+ else {
2191
+ // TODO: Some weird TypeScript error
2192
+ // @ts-ignore
2193
+ provider.send(OutgoingMessage, { encoder: message.encoder });
2194
+ }
2195
+ }
2141
2196
  }
2142
- onMessage(event) {
2143
- this.resolveConnectionAttempt();
2144
- this.lastMessageReceived = getUnixTime();
2197
+ applySyncMessage(provider, emitSynced) {
2198
+ const { message } = this;
2199
+ message.writeVarUint(exports.MessageType.Sync);
2200
+ // Apply update
2201
+ const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
2202
+ // Synced once we receive Step2
2203
+ if (emitSynced && syncMessageType === messageYjsSyncStep2) {
2204
+ provider.synced = true;
2205
+ }
2145
2206
  }
2146
- resolveConnectionAttempt() {
2147
- if (this.connectionAttempt) {
2148
- this.connectionAttempt.resolve();
2149
- this.connectionAttempt = null;
2150
- this.status = exports.WebSocketStatus.Connected;
2151
- this.emit('status', { status: exports.WebSocketStatus.Connected });
2152
- this.emit('connect');
2207
+ applySyncStatusMessage(provider, applied) {
2208
+ if (applied) {
2209
+ provider.decrementUnsyncedChanges();
2153
2210
  }
2154
2211
  }
2155
- stopConnectionAttempt() {
2156
- this.connectionAttempt = null;
2212
+ applyAwarenessMessage(provider) {
2213
+ const { message } = this;
2214
+ applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
2157
2215
  }
2158
- rejectConnectionAttempt() {
2159
- var _a;
2160
- (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
2161
- this.connectionAttempt = null;
2216
+ applyAuthMessage(provider) {
2217
+ const { message } = this;
2218
+ common.readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
2162
2219
  }
2163
- checkConnection() {
2164
- var _a;
2165
- // Don’t check the connection when it’s not even established
2166
- if (this.status !== exports.WebSocketStatus.Connected) {
2167
- return;
2168
- }
2169
- // Don’t close then connection while waiting for the first message
2170
- if (!this.lastMessageReceived) {
2171
- return;
2172
- }
2173
- // Don’t close the connection when a message was received recently
2174
- if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
2175
- return;
2176
- }
2177
- // No message received in a long time, not even your own
2178
- // Awareness updates, which are updated every 15 seconds.
2179
- (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
2220
+ applyQueryAwarenessMessage(provider) {
2221
+ const { message } = this;
2222
+ message.writeVarUint(exports.MessageType.Awareness);
2223
+ message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
2180
2224
  }
2181
- registerEventListeners() {
2182
- if (typeof window === 'undefined') {
2183
- return;
2184
- }
2185
- window.addEventListener('online', this.boundConnect);
2225
+ }
2226
+
2227
+ class MessageSender {
2228
+ constructor(Message, args = {}) {
2229
+ this.message = new Message();
2230
+ this.encoder = this.message.get(args);
2186
2231
  }
2187
- // Ensure that the URL always ends with /
2188
- get serverUrl() {
2189
- while (this.configuration.url[this.configuration.url.length - 1] === '/') {
2190
- return this.configuration.url.slice(0, this.configuration.url.length - 1);
2191
- }
2192
- return this.configuration.url;
2232
+ create() {
2233
+ return toUint8Array(this.encoder);
2193
2234
  }
2194
- get url() {
2195
- const encodedParams = encodeQueryParams(this.configuration.parameters);
2196
- return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
2235
+ send(webSocket) {
2236
+ webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
2197
2237
  }
2198
- disconnect() {
2199
- this.shouldConnect = false;
2200
- if (this.webSocket === null) {
2201
- return;
2202
- }
2203
- try {
2204
- this.webSocket.close();
2205
- }
2206
- catch {
2207
- //
2208
- }
2238
+ broadcast(channel) {
2239
+ publish(channel, this.create());
2209
2240
  }
2210
- send(message) {
2211
- var _a;
2212
- if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === common.WsReadyStates.Open) {
2213
- this.webSocket.send(message);
2214
- }
2241
+ }
2242
+
2243
+ class AuthenticationMessage extends OutgoingMessage {
2244
+ constructor() {
2245
+ super(...arguments);
2246
+ this.type = exports.MessageType.Auth;
2247
+ this.description = 'Authentication';
2215
2248
  }
2216
- onClose({ event }) {
2217
- this.webSocket = null;
2218
- if (this.status === exports.WebSocketStatus.Connected) {
2219
- this.status = exports.WebSocketStatus.Disconnected;
2220
- this.emit('status', { status: exports.WebSocketStatus.Disconnected });
2221
- this.emit('disconnect', { event });
2222
- }
2223
- if (event.code === common.Unauthorized.code) {
2224
- if (event.reason === common.Unauthorized.reason) {
2225
- console.warn('[HocuspocusProvider] An authentication token is required, but you didn’t send one. Try adding a `token` to your HocuspocusProvider configuration. Won’t try again.');
2226
- }
2227
- else {
2228
- console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
2229
- }
2230
- this.shouldConnect = false;
2231
- }
2232
- if (event.code === common.Forbidden.code) {
2233
- if (!this.configuration.quiet) {
2234
- console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
2235
- return; // TODO REMOVE ME
2236
- }
2237
- }
2238
- if (event.code === common.MessageTooBig.code) {
2239
- console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
2240
- this.shouldConnect = false;
2249
+ get(args) {
2250
+ if (typeof args.token === 'undefined') {
2251
+ throw new Error('The authentication message requires `token` as an argument.');
2241
2252
  }
2242
- if (this.connectionAttempt) {
2243
- // That connection attempt failed.
2244
- this.rejectConnectionAttempt();
2253
+ writeVarString(this.encoder, args.documentName);
2254
+ writeVarUint(this.encoder, this.type);
2255
+ common.writeAuthentication(this.encoder, args.token);
2256
+ return this.encoder;
2257
+ }
2258
+ }
2259
+
2260
+ class AwarenessMessage extends OutgoingMessage {
2261
+ constructor() {
2262
+ super(...arguments);
2263
+ this.type = exports.MessageType.Awareness;
2264
+ this.description = 'Awareness states update';
2265
+ }
2266
+ get(args) {
2267
+ if (typeof args.awareness === 'undefined') {
2268
+ throw new Error('The awareness message requires awareness as an argument');
2245
2269
  }
2246
- else if (this.shouldConnect) {
2247
- // The connection was closed by the server. Let’s just try again.
2248
- this.connect();
2270
+ if (typeof args.clients === 'undefined') {
2271
+ throw new Error('The awareness message requires clients as an argument');
2249
2272
  }
2250
- // If we’ll reconnect, we’re done for now.
2251
- if (this.shouldConnect) {
2252
- return;
2273
+ writeVarString(this.encoder, args.documentName);
2274
+ writeVarUint(this.encoder, this.type);
2275
+ let awarenessUpdate;
2276
+ if (args.states === undefined) {
2277
+ awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
2253
2278
  }
2254
- // The status is set correctly already.
2255
- if (this.status === exports.WebSocketStatus.Disconnected) {
2256
- return;
2279
+ else {
2280
+ awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
2257
2281
  }
2258
- // Let’s update the connection status.
2259
- this.status = exports.WebSocketStatus.Disconnected;
2260
- this.emit('status', { status: exports.WebSocketStatus.Disconnected });
2261
- this.emit('disconnect', { event });
2282
+ writeVarUint8Array(this.encoder, awarenessUpdate);
2283
+ return this.encoder;
2262
2284
  }
2263
- destroy() {
2264
- this.emit('destroy');
2265
- if (this.intervals.forceSync) {
2266
- clearInterval(this.intervals.forceSync);
2267
- }
2268
- clearInterval(this.intervals.connectionChecker);
2269
- // If there is still a connection attempt outstanding then we should stop
2270
- // it before calling disconnect, otherwise it will be rejected in the onClose
2271
- // handler and trigger a retry
2272
- this.stopConnectionAttempt();
2273
- this.disconnect();
2274
- this.removeAllListeners();
2275
- if (typeof window === 'undefined') {
2276
- return;
2277
- }
2278
- window.removeEventListener('online', this.boundConnect);
2285
+ }
2286
+
2287
+ class CloseMessage extends OutgoingMessage {
2288
+ constructor() {
2289
+ super(...arguments);
2290
+ this.type = exports.MessageType.CLOSE;
2291
+ this.description = 'Ask the server to close the connection';
2292
+ }
2293
+ get(args) {
2294
+ writeVarString(this.encoder, args.documentName);
2295
+ writeVarUint(this.encoder, this.type);
2296
+ return this.encoder;
2297
+ }
2298
+ }
2299
+
2300
+ class QueryAwarenessMessage extends OutgoingMessage {
2301
+ constructor() {
2302
+ super(...arguments);
2303
+ this.type = exports.MessageType.QueryAwareness;
2304
+ this.description = 'Queries awareness states';
2305
+ }
2306
+ get(args) {
2307
+ console.log('queryAwareness: writing string docName', args.documentName);
2308
+ console.log(this.encoder.cpos);
2309
+ writeVarString(this.encoder, args.documentName);
2310
+ writeVarUint(this.encoder, this.type);
2311
+ return this.encoder;
2279
2312
  }
2280
2313
  }
2281
2314
 
@@ -2294,15 +2327,50 @@ class StatelessMessage extends OutgoingMessage {
2294
2327
  }
2295
2328
  }
2296
2329
 
2297
- class CloseMessage extends OutgoingMessage {
2330
+ class SyncStepOneMessage extends OutgoingMessage {
2298
2331
  constructor() {
2299
2332
  super(...arguments);
2300
- this.type = exports.MessageType.CLOSE;
2301
- this.description = 'Ask the server to close the connection';
2333
+ this.type = exports.MessageType.Sync;
2334
+ this.description = 'First sync step';
2335
+ }
2336
+ get(args) {
2337
+ if (typeof args.document === 'undefined') {
2338
+ throw new Error('The sync step one message requires document as an argument');
2339
+ }
2340
+ writeVarString(this.encoder, args.documentName);
2341
+ writeVarUint(this.encoder, this.type);
2342
+ writeSyncStep1(this.encoder, args.document);
2343
+ return this.encoder;
2344
+ }
2345
+ }
2346
+
2347
+ class SyncStepTwoMessage extends OutgoingMessage {
2348
+ constructor() {
2349
+ super(...arguments);
2350
+ this.type = exports.MessageType.Sync;
2351
+ this.description = 'Second sync step';
2352
+ }
2353
+ get(args) {
2354
+ if (typeof args.document === 'undefined') {
2355
+ throw new Error('The sync step two message requires document as an argument');
2356
+ }
2357
+ writeVarString(this.encoder, args.documentName);
2358
+ writeVarUint(this.encoder, this.type);
2359
+ writeSyncStep2(this.encoder, args.document);
2360
+ return this.encoder;
2361
+ }
2362
+ }
2363
+
2364
+ class UpdateMessage extends OutgoingMessage {
2365
+ constructor() {
2366
+ super(...arguments);
2367
+ this.type = exports.MessageType.Sync;
2368
+ this.description = 'A document update';
2302
2369
  }
2303
2370
  get(args) {
2304
2371
  writeVarString(this.encoder, args.documentName);
2305
2372
  writeVarUint(this.encoder, this.type);
2373
+ writeUpdate(this.encoder, args.update);
2306
2374
  return this.encoder;
2307
2375
  }
2308
2376
  }
@@ -2335,6 +2403,8 @@ class HocuspocusProvider extends EventEmitter {
2335
2403
  onAwarenessChange: () => null,
2336
2404
  onStateless: () => null,
2337
2405
  quiet: false,
2406
+ connect: true,
2407
+ preserveConnection: true,
2338
2408
  };
2339
2409
  this.subscribedToBroadcastChannel = false;
2340
2410
  this.isSynced = false;
@@ -2348,7 +2418,7 @@ class HocuspocusProvider extends EventEmitter {
2348
2418
  };
2349
2419
  this.isConnected = true;
2350
2420
  this.boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this);
2351
- this.boundBeforeUnload = this.beforeUnload.bind(this);
2421
+ this.boundPageUnload = this.pageUnload.bind(this);
2352
2422
  this.boundOnOpen = this.onOpen.bind(this);
2353
2423
  this.boundOnMessage = this.onMessage.bind(this);
2354
2424
  this.boundOnClose = this.onClose.bind(this);
@@ -2408,6 +2478,7 @@ class HocuspocusProvider extends EventEmitter {
2408
2478
  const websocketProviderConfig = configuration;
2409
2479
  this.configuration.websocketProvider = new HocuspocusProviderWebsocket({
2410
2480
  url: websocketProviderConfig.url,
2481
+ connect: websocketProviderConfig.connect,
2411
2482
  parameters: websocketProviderConfig.parameters,
2412
2483
  });
2413
2484
  }
@@ -2422,21 +2493,28 @@ class HocuspocusProvider extends EventEmitter {
2422
2493
  get hasUnsyncedChanges() {
2423
2494
  return this.unsyncedChanges > 0;
2424
2495
  }
2425
- updateUnsyncedChanges(unsyncedChanges = 0) {
2426
- this.unsyncedChanges += unsyncedChanges;
2496
+ incrementUnsyncedChanges() {
2497
+ this.unsyncedChanges += 1;
2498
+ this.emit('unsyncedChanges', this.unsyncedChanges);
2499
+ }
2500
+ decrementUnsyncedChanges() {
2501
+ this.unsyncedChanges -= 1;
2502
+ if (this.unsyncedChanges === 0) {
2503
+ this.synced = true;
2504
+ }
2427
2505
  this.emit('unsyncedChanges', this.unsyncedChanges);
2428
2506
  }
2429
2507
  forceSync() {
2430
2508
  this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
2431
2509
  }
2432
- beforeUnload() {
2510
+ pageUnload() {
2433
2511
  removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload');
2434
2512
  }
2435
2513
  registerEventListeners() {
2436
2514
  if (typeof window === 'undefined') {
2437
2515
  return;
2438
2516
  }
2439
- window.addEventListener('beforeunload', this.boundBeforeUnload);
2517
+ window.addEventListener('unload', this.boundPageUnload);
2440
2518
  }
2441
2519
  sendStateless(payload) {
2442
2520
  this.send(StatelessMessage, { documentName: this.configuration.name, payload });
@@ -2445,7 +2523,7 @@ class HocuspocusProvider extends EventEmitter {
2445
2523
  if (origin === this) {
2446
2524
  return;
2447
2525
  }
2448
- this.updateUnsyncedChanges(1);
2526
+ this.incrementUnsyncedChanges();
2449
2527
  this.send(UpdateMessage, { update, documentName: this.configuration.name }, true);
2450
2528
  }
2451
2529
  awarenessUpdateHandler({ added, updated, removed }, origin) {
@@ -2456,6 +2534,12 @@ class HocuspocusProvider extends EventEmitter {
2456
2534
  documentName: this.configuration.name,
2457
2535
  }, true);
2458
2536
  }
2537
+ /**
2538
+ * Indicates whether a first handshake with the server has been established
2539
+ *
2540
+ * Note: this does not mean all updates from the client have been persisted to the backend. For this,
2541
+ * use `hasUnsyncedChanges`.
2542
+ */
2459
2543
  get synced() {
2460
2544
  return this.isSynced;
2461
2545
  }
@@ -2463,9 +2547,6 @@ class HocuspocusProvider extends EventEmitter {
2463
2547
  if (this.isSynced === state) {
2464
2548
  return;
2465
2549
  }
2466
- if (state && this.unsyncedChanges > 0) {
2467
- this.updateUnsyncedChanges(-1 * this.unsyncedChanges);
2468
- }
2469
2550
  this.isSynced = state;
2470
2551
  this.emit('synced', { state });
2471
2552
  this.emit('sync', { state });
@@ -2483,6 +2564,9 @@ class HocuspocusProvider extends EventEmitter {
2483
2564
  disconnect() {
2484
2565
  this.disconnectBroadcastChannel();
2485
2566
  this.configuration.websocketProvider.detach(this);
2567
+ if (!this.configuration.preserveConnection) {
2568
+ this.configuration.websocketProvider.disconnect();
2569
+ }
2486
2570
  }
2487
2571
  async onOpen(event) {
2488
2572
  this.isAuthenticated = false;
@@ -2503,6 +2587,7 @@ class HocuspocusProvider extends EventEmitter {
2503
2587
  return this.configuration.token;
2504
2588
  }
2505
2589
  startSync() {
2590
+ this.incrementUnsyncedChanges();
2506
2591
  this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
2507
2592
  if (this.awareness.getLocalState() !== null) {
2508
2593
  this.send(AwarenessMessage, {
@@ -2513,8 +2598,9 @@ class HocuspocusProvider extends EventEmitter {
2513
2598
  }
2514
2599
  }
2515
2600
  send(message, args, broadcast = false) {
2516
- if (!this.isConnected)
2601
+ if (!this.isConnected) {
2517
2602
  return;
2603
+ }
2518
2604
  if (broadcast) {
2519
2605
  this.mux(() => { this.broadcast(message, args); });
2520
2606
  }
@@ -2530,7 +2616,7 @@ class HocuspocusProvider extends EventEmitter {
2530
2616
  }
2531
2617
  message.writeVarString(documentName);
2532
2618
  this.emit('message', { event, message: new IncomingMessage(event.data) });
2533
- new MessageReceiver(message).apply(this);
2619
+ new MessageReceiver(message).apply(this, true);
2534
2620
  }
2535
2621
  onClose(event) {
2536
2622
  this.isAuthenticated = false;
@@ -2566,7 +2652,7 @@ class HocuspocusProvider extends EventEmitter {
2566
2652
  if (typeof window === 'undefined') {
2567
2653
  return;
2568
2654
  }
2569
- window.removeEventListener('beforeunload', this.boundBeforeUnload);
2655
+ window.removeEventListener('unload', this.boundPageUnload);
2570
2656
  }
2571
2657
  permissionDeniedHandler(reason) {
2572
2658
  this.emit('authenticationFailed', { reason });