@hocuspocus/provider 2.2.3 → 2.3.0

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,696 @@ 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.setConfiguration(configuration);
1697
+ this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
1698
+ this.on('open', this.configuration.onOpen);
1699
+ this.on('open', this.onOpen.bind(this));
1700
+ this.on('connect', this.configuration.onConnect);
1701
+ this.on('message', this.configuration.onMessage);
1702
+ this.on('outgoingMessage', this.configuration.onOutgoingMessage);
1703
+ this.on('status', this.configuration.onStatus);
1704
+ this.on('status', this.onStatus.bind(this));
1705
+ this.on('disconnect', this.configuration.onDisconnect);
1706
+ this.on('close', this.configuration.onClose);
1707
+ this.on('destroy', this.configuration.onDestroy);
1708
+ this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
1709
+ this.on('awarenessChange', this.configuration.onAwarenessChange);
1710
+ this.on('close', this.onClose.bind(this));
1711
+ this.on('message', this.onMessage.bind(this));
1712
+ this.registerEventListeners();
1713
+ this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
1714
+ if (typeof configuration.connect !== 'undefined') {
1715
+ this.shouldConnect = configuration.connect;
1716
+ }
1717
+ if (!this.shouldConnect) {
1718
+ return;
1719
+ }
1720
+ this.connect();
1744
1721
  }
1745
- get(args) {
1746
- return args.encoder;
1722
+ async onOpen(event) {
1723
+ this.receivedOnOpenPayload = event;
1747
1724
  }
1748
- toUint8Array() {
1749
- return toUint8Array(this.encoder);
1725
+ async onStatus(data) {
1726
+ this.receivedOnStatusPayload = data;
1750
1727
  }
1751
- }
1752
-
1753
- class MessageReceiver {
1754
- constructor(message) {
1755
- this.broadcasted = false;
1756
- this.message = message;
1728
+ attach(provider) {
1729
+ if (this.status === exports.WebSocketStatus.Disconnected && this.shouldConnect) {
1730
+ this.connect();
1731
+ }
1732
+ if (this.receivedOnOpenPayload) {
1733
+ provider.onOpen(this.receivedOnOpenPayload);
1734
+ }
1735
+ if (this.receivedOnStatusPayload) {
1736
+ provider.onStatus(this.receivedOnStatusPayload);
1737
+ }
1757
1738
  }
1758
- setBroadcasted(value) {
1759
- this.broadcasted = value;
1760
- return this;
1739
+ detach(provider) {
1740
+ // tell the server to remove the listener
1761
1741
  }
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}`);
1742
+ setConfiguration(configuration = {}) {
1743
+ this.configuration = { ...this.configuration, ...configuration };
1744
+ }
1745
+ async connect() {
1746
+ if (this.status === exports.WebSocketStatus.Connected) {
1747
+ return;
1787
1748
  }
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 });
1794
- }
1795
- else {
1796
- // TODO: Some weird TypeScript error
1797
- // @ts-ignore
1798
- provider.send(OutgoingMessage, { encoder: message.encoder });
1749
+ // Always cancel any previously initiated connection retryer instances
1750
+ if (this.cancelWebsocketRetry) {
1751
+ this.cancelWebsocketRetry();
1752
+ this.cancelWebsocketRetry = undefined;
1753
+ }
1754
+ this.receivedOnOpenPayload = undefined;
1755
+ this.receivedOnStatusPayload = undefined;
1756
+ this.shouldConnect = true;
1757
+ const abortableRetry = () => {
1758
+ let cancelAttempt = false;
1759
+ const retryPromise = attempt.retry(this.createWebSocketConnection.bind(this), {
1760
+ delay: this.configuration.delay,
1761
+ initialDelay: this.configuration.initialDelay,
1762
+ factor: this.configuration.factor,
1763
+ maxAttempts: this.configuration.maxAttempts,
1764
+ minDelay: this.configuration.minDelay,
1765
+ maxDelay: this.configuration.maxDelay,
1766
+ jitter: this.configuration.jitter,
1767
+ timeout: this.configuration.timeout,
1768
+ beforeAttempt: context => {
1769
+ if (!this.shouldConnect || cancelAttempt) {
1770
+ context.abort();
1771
+ }
1772
+ },
1773
+ }).catch((error) => {
1774
+ // If we aborted the connection attempt then don’t throw an error
1775
+ // ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
1776
+ if (error && error.code !== 'ATTEMPT_ABORTED') {
1777
+ throw error;
1778
+ }
1779
+ });
1780
+ return {
1781
+ retryPromise,
1782
+ cancelFunc: () => {
1783
+ cancelAttempt = true;
1784
+ },
1785
+ };
1786
+ };
1787
+ const { retryPromise, cancelFunc } = abortableRetry();
1788
+ this.cancelWebsocketRetry = cancelFunc;
1789
+ return retryPromise;
1790
+ }
1791
+ createWebSocketConnection() {
1792
+ return new Promise((resolve, reject) => {
1793
+ if (this.webSocket) {
1794
+ this.messageQueue = [];
1795
+ this.webSocket.close();
1796
+ this.webSocket = null;
1799
1797
  }
1798
+ // Init the WebSocket connection
1799
+ const ws = new this.configuration.WebSocketPolyfill(this.url);
1800
+ ws.binaryType = 'arraybuffer';
1801
+ ws.onmessage = (payload) => this.emit('message', payload);
1802
+ ws.onclose = (payload) => this.emit('close', { event: payload });
1803
+ ws.onopen = (payload) => this.emit('open', payload);
1804
+ ws.onerror = (err) => {
1805
+ reject(err);
1806
+ };
1807
+ this.webSocket = ws;
1808
+ // Reset the status
1809
+ this.status = exports.WebSocketStatus.Connecting;
1810
+ this.emit('status', { status: exports.WebSocketStatus.Connecting });
1811
+ // Store resolve/reject for later use
1812
+ this.connectionAttempt = {
1813
+ resolve,
1814
+ reject,
1815
+ };
1816
+ });
1817
+ }
1818
+ onMessage(event) {
1819
+ this.resolveConnectionAttempt();
1820
+ this.lastMessageReceived = getUnixTime();
1821
+ }
1822
+ resolveConnectionAttempt() {
1823
+ if (this.connectionAttempt) {
1824
+ this.connectionAttempt.resolve();
1825
+ this.connectionAttempt = null;
1826
+ this.status = exports.WebSocketStatus.Connected;
1827
+ this.emit('status', { status: exports.WebSocketStatus.Connected });
1828
+ this.emit('connect');
1829
+ this.messageQueue.forEach(message => this.send(message));
1830
+ this.messageQueue = [];
1800
1831
  }
1801
1832
  }
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;
1833
+ stopConnectionAttempt() {
1834
+ this.connectionAttempt = null;
1835
+ }
1836
+ rejectConnectionAttempt() {
1837
+ var _a;
1838
+ (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
1839
+ this.connectionAttempt = null;
1840
+ }
1841
+ checkConnection() {
1842
+ var _a;
1843
+ // Don’t check the connection when it’s not even established
1844
+ if (this.status !== exports.WebSocketStatus.Connected) {
1845
+ return;
1846
+ }
1847
+ // Don’t close then connection while waiting for the first message
1848
+ if (!this.lastMessageReceived) {
1849
+ return;
1850
+ }
1851
+ // Don’t close the connection when a message was received recently
1852
+ if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
1853
+ return;
1854
+ }
1855
+ // No message received in a long time, not even your own
1856
+ // Awareness updates, which are updated every 15 seconds.
1857
+ (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
1858
+ this.messageQueue = [];
1859
+ }
1860
+ registerEventListeners() {
1861
+ if (typeof window === 'undefined') {
1862
+ return;
1863
+ }
1864
+ window.addEventListener('online', this.boundConnect);
1865
+ }
1866
+ // Ensure that the URL always ends with /
1867
+ get serverUrl() {
1868
+ while (this.configuration.url[this.configuration.url.length - 1] === '/') {
1869
+ return this.configuration.url.slice(0, this.configuration.url.length - 1);
1870
+ }
1871
+ return this.configuration.url;
1872
+ }
1873
+ get url() {
1874
+ const encodedParams = encodeQueryParams(this.configuration.parameters);
1875
+ return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
1876
+ }
1877
+ disconnect() {
1878
+ this.shouldConnect = false;
1879
+ if (this.webSocket === null) {
1880
+ return;
1810
1881
  }
1811
- if (syncMessageType === messageYjsUpdate || syncMessageType === messageYjsSyncStep2) {
1812
- if (provider.unsyncedChanges > 0) {
1813
- provider.updateUnsyncedChanges(-1);
1882
+ try {
1883
+ this.webSocket.close();
1884
+ this.messageQueue = [];
1885
+ }
1886
+ catch {
1887
+ //
1888
+ }
1889
+ }
1890
+ send(message) {
1891
+ var _a;
1892
+ if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === common.WsReadyStates.Open) {
1893
+ this.webSocket.send(message);
1894
+ }
1895
+ else {
1896
+ this.messageQueue.push(message);
1897
+ }
1898
+ }
1899
+ onClose({ event }) {
1900
+ this.webSocket = null;
1901
+ if (this.status === exports.WebSocketStatus.Connected) {
1902
+ this.status = exports.WebSocketStatus.Disconnected;
1903
+ this.emit('status', { status: exports.WebSocketStatus.Disconnected });
1904
+ this.emit('disconnect', { event });
1905
+ }
1906
+ if (event.code === common.Unauthorized.code) {
1907
+ if (event.reason === common.Unauthorized.reason) {
1908
+ 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.');
1909
+ }
1910
+ else {
1911
+ console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
1814
1912
  }
1913
+ this.shouldConnect = false;
1914
+ }
1915
+ if (event.code === common.Forbidden.code) {
1916
+ if (!this.configuration.quiet) {
1917
+ console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
1918
+ return; // TODO REMOVE ME
1919
+ }
1920
+ }
1921
+ if (event.code === common.MessageTooBig.code) {
1922
+ console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
1923
+ this.shouldConnect = false;
1924
+ }
1925
+ if (this.connectionAttempt) {
1926
+ // That connection attempt failed.
1927
+ this.rejectConnectionAttempt();
1928
+ }
1929
+ else if (this.shouldConnect) {
1930
+ // The connection was closed by the server. Let’s just try again.
1931
+ this.connect();
1932
+ }
1933
+ // If we’ll reconnect, we’re done for now.
1934
+ if (this.shouldConnect) {
1935
+ return;
1936
+ }
1937
+ // The status is set correctly already.
1938
+ if (this.status === exports.WebSocketStatus.Disconnected) {
1939
+ return;
1815
1940
  }
1941
+ // Let’s update the connection status.
1942
+ this.status = exports.WebSocketStatus.Disconnected;
1943
+ this.emit('status', { status: exports.WebSocketStatus.Disconnected });
1944
+ this.emit('disconnect', { event });
1816
1945
  }
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())));
1946
+ destroy() {
1947
+ this.emit('destroy');
1948
+ if (this.intervals.forceSync) {
1949
+ clearInterval(this.intervals.forceSync);
1950
+ }
1951
+ clearInterval(this.intervals.connectionChecker);
1952
+ // If there is still a connection attempt outstanding then we should stop
1953
+ // it before calling disconnect, otherwise it will be rejected in the onClose
1954
+ // handler and trigger a retry
1955
+ this.stopConnectionAttempt();
1956
+ this.disconnect();
1957
+ this.removeAllListeners();
1958
+ if (typeof window === 'undefined') {
1959
+ return;
1960
+ }
1961
+ window.removeEventListener('online', this.boundConnect);
1829
1962
  }
1830
1963
  }
1831
1964
 
1832
- class MessageSender {
1833
- constructor(Message, args = {}) {
1834
- this.message = new Message();
1835
- this.encoder = this.message.get(args);
1965
+ class IncomingMessage {
1966
+ constructor(data) {
1967
+ this.data = data;
1968
+ this.encoder = createEncoder();
1969
+ this.decoder = createDecoder(new Uint8Array(this.data));
1836
1970
  }
1837
- create() {
1838
- return toUint8Array(this.encoder);
1971
+ readVarUint() {
1972
+ return readVarUint(this.decoder);
1839
1973
  }
1840
- send(webSocket) {
1841
- webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
1974
+ readVarString() {
1975
+ return readVarString(this.decoder);
1842
1976
  }
1843
- broadcast(channel) {
1844
- publish(channel, this.create());
1977
+ readVarUint8Array() {
1978
+ return readVarUint8Array(this.decoder);
1845
1979
  }
1846
- }
1847
-
1848
- class SyncStepOneMessage extends OutgoingMessage {
1849
- constructor() {
1850
- super(...arguments);
1851
- this.type = exports.MessageType.Sync;
1852
- this.description = 'First sync step';
1980
+ writeVarUint(type) {
1981
+ return writeVarUint(this.encoder, type);
1853
1982
  }
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;
1983
+ writeVarString(string) {
1984
+ return writeVarString(this.encoder, string);
1862
1985
  }
1863
- }
1864
-
1865
- class SyncStepTwoMessage extends OutgoingMessage {
1866
- constructor() {
1867
- super(...arguments);
1868
- this.type = exports.MessageType.Sync;
1869
- this.description = 'Second sync step';
1986
+ writeVarUint8Array(data) {
1987
+ return writeVarUint8Array(this.encoder, data);
1870
1988
  }
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;
1989
+ length() {
1990
+ return length(this.encoder);
1879
1991
  }
1880
1992
  }
1881
1993
 
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
- }
1994
+ /**
1995
+ * @module sync-protocol
1996
+ */
1896
1997
 
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
- }
1998
+ /**
1999
+ * @typedef {Map<number, number>} StateMap
2000
+ */
1913
2001
 
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
- }
2002
+ /**
2003
+ * Core Yjs defines two message types:
2004
+ * • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
2005
+ * • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
2006
+ * received all information from the remote client.
2007
+ *
2008
+ * In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
2009
+ * with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
2010
+ * SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
2011
+ *
2012
+ * In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
2013
+ * When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
2014
+ * with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
2015
+ * client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
2016
+ * easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
2017
+ * Therefore it is necesarry that the client initiates the sync.
2018
+ *
2019
+ * Construction of a message:
2020
+ * [messageType : varUint, message definition..]
2021
+ *
2022
+ * Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
2023
+ *
2024
+ * stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
2025
+ */
1940
2026
 
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
- }
2027
+ const messageYjsSyncStep1 = 0;
2028
+ const messageYjsSyncStep2 = 1;
2029
+ const messageYjsUpdate = 2;
1954
2030
 
1955
2031
  /**
1956
- * Utility module to work with urls.
2032
+ * Create a sync step 1 message based on the state of the current shared document.
1957
2033
  *
1958
- * @module url
2034
+ * @param {encoding.Encoder} encoder
2035
+ * @param {Y.Doc} doc
2036
+ */
2037
+ const writeSyncStep1 = (encoder, doc) => {
2038
+ writeVarUint(encoder, messageYjsSyncStep1);
2039
+ const sv = Y__namespace.encodeStateVector(doc);
2040
+ writeVarUint8Array(encoder, sv);
2041
+ };
2042
+
2043
+ /**
2044
+ * @param {encoding.Encoder} encoder
2045
+ * @param {Y.Doc} doc
2046
+ * @param {Uint8Array} [encodedStateVector]
2047
+ */
2048
+ const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
2049
+ writeVarUint(encoder, messageYjsSyncStep2);
2050
+ writeVarUint8Array(encoder, Y__namespace.encodeStateAsUpdate(doc, encodedStateVector));
2051
+ };
2052
+
2053
+ /**
2054
+ * Read SyncStep1 message and reply with SyncStep2.
2055
+ *
2056
+ * @param {decoding.Decoder} decoder The reply to the received message
2057
+ * @param {encoding.Encoder} encoder The received message
2058
+ * @param {Y.Doc} doc
2059
+ */
2060
+ const readSyncStep1 = (decoder, encoder, doc) =>
2061
+ writeSyncStep2(encoder, doc, readVarUint8Array(decoder));
2062
+
2063
+ /**
2064
+ * Read and apply Structs and then DeleteStore to a y instance.
2065
+ *
2066
+ * @param {decoding.Decoder} decoder
2067
+ * @param {Y.Doc} doc
2068
+ * @param {any} transactionOrigin
2069
+ */
2070
+ const readSyncStep2 = (decoder, doc, transactionOrigin) => {
2071
+ try {
2072
+ Y__namespace.applyUpdate(doc, readVarUint8Array(decoder), transactionOrigin);
2073
+ } catch (error) {
2074
+ // This catches errors that are thrown by event handlers
2075
+ console.error('Caught error while handling a Yjs update', error);
2076
+ }
2077
+ };
2078
+
2079
+ /**
2080
+ * @param {encoding.Encoder} encoder
2081
+ * @param {Uint8Array} update
2082
+ */
2083
+ const writeUpdate = (encoder, update) => {
2084
+ writeVarUint(encoder, messageYjsUpdate);
2085
+ writeVarUint8Array(encoder, update);
2086
+ };
2087
+
2088
+ /**
2089
+ * Read and apply Structs and then DeleteStore to a y instance.
2090
+ *
2091
+ * @param {decoding.Decoder} decoder
2092
+ * @param {Y.Doc} doc
2093
+ * @param {any} transactionOrigin
1959
2094
  */
2095
+ const readUpdate = readSyncStep2;
1960
2096
 
1961
2097
  /**
1962
- * @param {Object<string,string>} params
1963
- * @return {string}
2098
+ * @param {decoding.Decoder} decoder A message received from another client
2099
+ * @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
2100
+ * @param {Y.Doc} doc
2101
+ * @param {any} transactionOrigin
1964
2102
  */
1965
- const encodeQueryParams = params =>
1966
- map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
2103
+ const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
2104
+ const messageType = readVarUint(decoder);
2105
+ switch (messageType) {
2106
+ case messageYjsSyncStep1:
2107
+ readSyncStep1(decoder, encoder, doc);
2108
+ break
2109
+ case messageYjsSyncStep2:
2110
+ readSyncStep2(decoder, doc, transactionOrigin);
2111
+ break
2112
+ case messageYjsUpdate:
2113
+ readUpdate(decoder, doc, transactionOrigin);
2114
+ break
2115
+ default:
2116
+ throw new Error('Unknown message type')
2117
+ }
2118
+ return messageType
2119
+ };
1967
2120
 
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;
2121
+ class OutgoingMessage {
2122
+ constructor() {
2123
+ this.encoder = createEncoder();
2054
2124
  }
2055
- async onStatus(data) {
2056
- this.receivedOnStatusPayload = data;
2125
+ get(args) {
2126
+ return args.encoder;
2057
2127
  }
2058
- attach(provider) {
2059
- if (this.receivedOnOpenPayload) {
2060
- provider.onOpen(this.receivedOnOpenPayload);
2061
- }
2062
- if (this.receivedOnStatusPayload) {
2063
- provider.onStatus(this.receivedOnStatusPayload);
2064
- }
2128
+ toUint8Array() {
2129
+ return toUint8Array(this.encoder);
2065
2130
  }
2066
- detach(provider) {
2067
- // tell the server to remove the listener
2131
+ }
2132
+
2133
+ class MessageReceiver {
2134
+ constructor(message) {
2135
+ this.broadcasted = false;
2136
+ this.message = message;
2068
2137
  }
2069
- setConfiguration(configuration = {}) {
2070
- this.configuration = { ...this.configuration, ...configuration };
2138
+ setBroadcasted(value) {
2139
+ this.broadcasted = value;
2140
+ return this;
2071
2141
  }
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;
2142
+ apply(provider, emitSynced) {
2143
+ const { message } = this;
2144
+ const type = message.readVarUint();
2145
+ const emptyMessageLength = message.length();
2146
+ switch (type) {
2147
+ case exports.MessageType.Sync:
2148
+ this.applySyncMessage(provider, emitSynced);
2149
+ break;
2150
+ case exports.MessageType.Awareness:
2151
+ this.applyAwarenessMessage(provider);
2152
+ break;
2153
+ case exports.MessageType.Auth:
2154
+ this.applyAuthMessage(provider);
2155
+ break;
2156
+ case exports.MessageType.QueryAwareness:
2157
+ this.applyQueryAwarenessMessage(provider);
2158
+ break;
2159
+ case exports.MessageType.Stateless:
2160
+ provider.receiveStateless(readVarString(message.decoder));
2161
+ break;
2162
+ case exports.MessageType.SyncStatus:
2163
+ this.applySyncStatusMessage(provider, readVarInt(message.decoder) === 1);
2164
+ break;
2165
+ default:
2166
+ throw new Error(`Can’t apply message of unknown type: ${type}`);
2080
2167
  }
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;
2168
+ // Reply
2169
+ if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
2170
+ if (this.broadcasted) {
2171
+ // TODO: Some weird TypeScript error
2172
+ // @ts-ignore
2173
+ provider.broadcast(OutgoingMessage, { encoder: message.encoder });
2121
2174
  }
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
- });
2175
+ else {
2176
+ // TODO: Some weird TypeScript error
2177
+ // @ts-ignore
2178
+ provider.send(OutgoingMessage, { encoder: message.encoder });
2179
+ }
2180
+ }
2141
2181
  }
2142
- onMessage(event) {
2143
- this.resolveConnectionAttempt();
2144
- this.lastMessageReceived = getUnixTime();
2182
+ applySyncMessage(provider, emitSynced) {
2183
+ const { message } = this;
2184
+ message.writeVarUint(exports.MessageType.Sync);
2185
+ // Apply update
2186
+ const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
2187
+ // Synced once we receive Step2
2188
+ if (emitSynced && syncMessageType === messageYjsSyncStep2) {
2189
+ provider.synced = true;
2190
+ }
2145
2191
  }
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');
2192
+ applySyncStatusMessage(provider, applied) {
2193
+ if (applied) {
2194
+ provider.decrementUnsyncedChanges();
2153
2195
  }
2154
2196
  }
2155
- stopConnectionAttempt() {
2156
- this.connectionAttempt = null;
2197
+ applyAwarenessMessage(provider) {
2198
+ const { message } = this;
2199
+ applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
2157
2200
  }
2158
- rejectConnectionAttempt() {
2159
- var _a;
2160
- (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
2161
- this.connectionAttempt = null;
2201
+ applyAuthMessage(provider) {
2202
+ const { message } = this;
2203
+ common.readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
2162
2204
  }
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();
2205
+ applyQueryAwarenessMessage(provider) {
2206
+ const { message } = this;
2207
+ message.writeVarUint(exports.MessageType.Awareness);
2208
+ message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
2180
2209
  }
2181
- registerEventListeners() {
2182
- if (typeof window === 'undefined') {
2183
- return;
2184
- }
2185
- window.addEventListener('online', this.boundConnect);
2210
+ }
2211
+
2212
+ class MessageSender {
2213
+ constructor(Message, args = {}) {
2214
+ this.message = new Message();
2215
+ this.encoder = this.message.get(args);
2186
2216
  }
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;
2217
+ create() {
2218
+ return toUint8Array(this.encoder);
2193
2219
  }
2194
- get url() {
2195
- const encodedParams = encodeQueryParams(this.configuration.parameters);
2196
- return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
2220
+ send(webSocket) {
2221
+ webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
2197
2222
  }
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
- }
2223
+ broadcast(channel) {
2224
+ publish(channel, this.create());
2209
2225
  }
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
- }
2226
+ }
2227
+
2228
+ class AuthenticationMessage extends OutgoingMessage {
2229
+ constructor() {
2230
+ super(...arguments);
2231
+ this.type = exports.MessageType.Auth;
2232
+ this.description = 'Authentication';
2215
2233
  }
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;
2234
+ get(args) {
2235
+ if (typeof args.token === 'undefined') {
2236
+ throw new Error('The authentication message requires `token` as an argument.');
2241
2237
  }
2242
- if (this.connectionAttempt) {
2243
- // That connection attempt failed.
2244
- this.rejectConnectionAttempt();
2238
+ writeVarString(this.encoder, args.documentName);
2239
+ writeVarUint(this.encoder, this.type);
2240
+ common.writeAuthentication(this.encoder, args.token);
2241
+ return this.encoder;
2242
+ }
2243
+ }
2244
+
2245
+ class AwarenessMessage extends OutgoingMessage {
2246
+ constructor() {
2247
+ super(...arguments);
2248
+ this.type = exports.MessageType.Awareness;
2249
+ this.description = 'Awareness states update';
2250
+ }
2251
+ get(args) {
2252
+ if (typeof args.awareness === 'undefined') {
2253
+ throw new Error('The awareness message requires awareness as an argument');
2245
2254
  }
2246
- else if (this.shouldConnect) {
2247
- // The connection was closed by the server. Let’s just try again.
2248
- this.connect();
2255
+ if (typeof args.clients === 'undefined') {
2256
+ throw new Error('The awareness message requires clients as an argument');
2249
2257
  }
2250
- // If we’ll reconnect, we’re done for now.
2251
- if (this.shouldConnect) {
2252
- return;
2258
+ writeVarString(this.encoder, args.documentName);
2259
+ writeVarUint(this.encoder, this.type);
2260
+ let awarenessUpdate;
2261
+ if (args.states === undefined) {
2262
+ awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
2253
2263
  }
2254
- // The status is set correctly already.
2255
- if (this.status === exports.WebSocketStatus.Disconnected) {
2256
- return;
2264
+ else {
2265
+ awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
2257
2266
  }
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 });
2267
+ writeVarUint8Array(this.encoder, awarenessUpdate);
2268
+ return this.encoder;
2262
2269
  }
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);
2270
+ }
2271
+
2272
+ class CloseMessage extends OutgoingMessage {
2273
+ constructor() {
2274
+ super(...arguments);
2275
+ this.type = exports.MessageType.CLOSE;
2276
+ this.description = 'Ask the server to close the connection';
2277
+ }
2278
+ get(args) {
2279
+ writeVarString(this.encoder, args.documentName);
2280
+ writeVarUint(this.encoder, this.type);
2281
+ return this.encoder;
2282
+ }
2283
+ }
2284
+
2285
+ class QueryAwarenessMessage extends OutgoingMessage {
2286
+ constructor() {
2287
+ super(...arguments);
2288
+ this.type = exports.MessageType.QueryAwareness;
2289
+ this.description = 'Queries awareness states';
2290
+ }
2291
+ get(args) {
2292
+ console.log('queryAwareness: writing string docName', args.documentName);
2293
+ console.log(this.encoder.cpos);
2294
+ writeVarString(this.encoder, args.documentName);
2295
+ writeVarUint(this.encoder, this.type);
2296
+ return this.encoder;
2279
2297
  }
2280
2298
  }
2281
2299
 
@@ -2294,15 +2312,50 @@ class StatelessMessage extends OutgoingMessage {
2294
2312
  }
2295
2313
  }
2296
2314
 
2297
- class CloseMessage extends OutgoingMessage {
2315
+ class SyncStepOneMessage extends OutgoingMessage {
2298
2316
  constructor() {
2299
2317
  super(...arguments);
2300
- this.type = exports.MessageType.CLOSE;
2301
- this.description = 'Ask the server to close the connection';
2318
+ this.type = exports.MessageType.Sync;
2319
+ this.description = 'First sync step';
2320
+ }
2321
+ get(args) {
2322
+ if (typeof args.document === 'undefined') {
2323
+ throw new Error('The sync step one message requires document as an argument');
2324
+ }
2325
+ writeVarString(this.encoder, args.documentName);
2326
+ writeVarUint(this.encoder, this.type);
2327
+ writeSyncStep1(this.encoder, args.document);
2328
+ return this.encoder;
2329
+ }
2330
+ }
2331
+
2332
+ class SyncStepTwoMessage extends OutgoingMessage {
2333
+ constructor() {
2334
+ super(...arguments);
2335
+ this.type = exports.MessageType.Sync;
2336
+ this.description = 'Second sync step';
2337
+ }
2338
+ get(args) {
2339
+ if (typeof args.document === 'undefined') {
2340
+ throw new Error('The sync step two message requires document as an argument');
2341
+ }
2342
+ writeVarString(this.encoder, args.documentName);
2343
+ writeVarUint(this.encoder, this.type);
2344
+ writeSyncStep2(this.encoder, args.document);
2345
+ return this.encoder;
2346
+ }
2347
+ }
2348
+
2349
+ class UpdateMessage extends OutgoingMessage {
2350
+ constructor() {
2351
+ super(...arguments);
2352
+ this.type = exports.MessageType.Sync;
2353
+ this.description = 'A document update';
2302
2354
  }
2303
2355
  get(args) {
2304
2356
  writeVarString(this.encoder, args.documentName);
2305
2357
  writeVarUint(this.encoder, this.type);
2358
+ writeUpdate(this.encoder, args.update);
2306
2359
  return this.encoder;
2307
2360
  }
2308
2361
  }
@@ -2335,6 +2388,8 @@ class HocuspocusProvider extends EventEmitter {
2335
2388
  onAwarenessChange: () => null,
2336
2389
  onStateless: () => null,
2337
2390
  quiet: false,
2391
+ connect: true,
2392
+ preserveConnection: true,
2338
2393
  };
2339
2394
  this.subscribedToBroadcastChannel = false;
2340
2395
  this.isSynced = false;
@@ -2348,7 +2403,7 @@ class HocuspocusProvider extends EventEmitter {
2348
2403
  };
2349
2404
  this.isConnected = true;
2350
2405
  this.boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this);
2351
- this.boundBeforeUnload = this.beforeUnload.bind(this);
2406
+ this.boundPageUnload = this.pageUnload.bind(this);
2352
2407
  this.boundOnOpen = this.onOpen.bind(this);
2353
2408
  this.boundOnMessage = this.onMessage.bind(this);
2354
2409
  this.boundOnClose = this.onClose.bind(this);
@@ -2408,6 +2463,7 @@ class HocuspocusProvider extends EventEmitter {
2408
2463
  const websocketProviderConfig = configuration;
2409
2464
  this.configuration.websocketProvider = new HocuspocusProviderWebsocket({
2410
2465
  url: websocketProviderConfig.url,
2466
+ connect: websocketProviderConfig.connect,
2411
2467
  parameters: websocketProviderConfig.parameters,
2412
2468
  });
2413
2469
  }
@@ -2422,21 +2478,28 @@ class HocuspocusProvider extends EventEmitter {
2422
2478
  get hasUnsyncedChanges() {
2423
2479
  return this.unsyncedChanges > 0;
2424
2480
  }
2425
- updateUnsyncedChanges(unsyncedChanges = 0) {
2426
- this.unsyncedChanges += unsyncedChanges;
2481
+ incrementUnsyncedChanges() {
2482
+ this.unsyncedChanges += 1;
2483
+ this.emit('unsyncedChanges', this.unsyncedChanges);
2484
+ }
2485
+ decrementUnsyncedChanges() {
2486
+ this.unsyncedChanges -= 1;
2487
+ if (this.unsyncedChanges === 0) {
2488
+ this.synced = true;
2489
+ }
2427
2490
  this.emit('unsyncedChanges', this.unsyncedChanges);
2428
2491
  }
2429
2492
  forceSync() {
2430
2493
  this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
2431
2494
  }
2432
- beforeUnload() {
2495
+ pageUnload() {
2433
2496
  removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload');
2434
2497
  }
2435
2498
  registerEventListeners() {
2436
2499
  if (typeof window === 'undefined') {
2437
2500
  return;
2438
2501
  }
2439
- window.addEventListener('beforeunload', this.boundBeforeUnload);
2502
+ window.addEventListener('unload', this.boundPageUnload);
2440
2503
  }
2441
2504
  sendStateless(payload) {
2442
2505
  this.send(StatelessMessage, { documentName: this.configuration.name, payload });
@@ -2445,7 +2508,7 @@ class HocuspocusProvider extends EventEmitter {
2445
2508
  if (origin === this) {
2446
2509
  return;
2447
2510
  }
2448
- this.updateUnsyncedChanges(1);
2511
+ this.incrementUnsyncedChanges();
2449
2512
  this.send(UpdateMessage, { update, documentName: this.configuration.name }, true);
2450
2513
  }
2451
2514
  awarenessUpdateHandler({ added, updated, removed }, origin) {
@@ -2456,6 +2519,12 @@ class HocuspocusProvider extends EventEmitter {
2456
2519
  documentName: this.configuration.name,
2457
2520
  }, true);
2458
2521
  }
2522
+ /**
2523
+ * Indicates whether a first handshake with the server has been established
2524
+ *
2525
+ * Note: this does not mean all updates from the client have been persisted to the backend. For this,
2526
+ * use `hasUnsyncedChanges`.
2527
+ */
2459
2528
  get synced() {
2460
2529
  return this.isSynced;
2461
2530
  }
@@ -2463,9 +2532,6 @@ class HocuspocusProvider extends EventEmitter {
2463
2532
  if (this.isSynced === state) {
2464
2533
  return;
2465
2534
  }
2466
- if (state && this.unsyncedChanges > 0) {
2467
- this.updateUnsyncedChanges(-1 * this.unsyncedChanges);
2468
- }
2469
2535
  this.isSynced = state;
2470
2536
  this.emit('synced', { state });
2471
2537
  this.emit('sync', { state });
@@ -2483,6 +2549,9 @@ class HocuspocusProvider extends EventEmitter {
2483
2549
  disconnect() {
2484
2550
  this.disconnectBroadcastChannel();
2485
2551
  this.configuration.websocketProvider.detach(this);
2552
+ if (!this.configuration.preserveConnection) {
2553
+ this.configuration.websocketProvider.disconnect();
2554
+ }
2486
2555
  }
2487
2556
  async onOpen(event) {
2488
2557
  this.isAuthenticated = false;
@@ -2503,6 +2572,7 @@ class HocuspocusProvider extends EventEmitter {
2503
2572
  return this.configuration.token;
2504
2573
  }
2505
2574
  startSync() {
2575
+ this.incrementUnsyncedChanges();
2506
2576
  this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
2507
2577
  if (this.awareness.getLocalState() !== null) {
2508
2578
  this.send(AwarenessMessage, {
@@ -2513,8 +2583,9 @@ class HocuspocusProvider extends EventEmitter {
2513
2583
  }
2514
2584
  }
2515
2585
  send(message, args, broadcast = false) {
2516
- if (!this.isConnected)
2586
+ if (!this.isConnected) {
2517
2587
  return;
2588
+ }
2518
2589
  if (broadcast) {
2519
2590
  this.mux(() => { this.broadcast(message, args); });
2520
2591
  }
@@ -2530,7 +2601,7 @@ class HocuspocusProvider extends EventEmitter {
2530
2601
  }
2531
2602
  message.writeVarString(documentName);
2532
2603
  this.emit('message', { event, message: new IncomingMessage(event.data) });
2533
- new MessageReceiver(message).apply(this);
2604
+ new MessageReceiver(message).apply(this, true);
2534
2605
  }
2535
2606
  onClose(event) {
2536
2607
  this.isAuthenticated = false;
@@ -2566,7 +2637,7 @@ class HocuspocusProvider extends EventEmitter {
2566
2637
  if (typeof window === 'undefined') {
2567
2638
  return;
2568
2639
  }
2569
- window.removeEventListener('beforeunload', this.boundBeforeUnload);
2640
+ window.removeEventListener('unload', this.boundPageUnload);
2570
2641
  }
2571
2642
  permissionDeniedHandler(reason) {
2572
2643
  this.emit('authenticationFailed', { reason });