@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.
@@ -1,5 +1,5 @@
1
+ import { WsReadyStates, Unauthorized, Forbidden, MessageTooBig, readAuthMessage, writeAuthentication, awarenessStatesToArray } from '@hocuspocus/common';
1
2
  import * as Y from 'yjs';
2
- import { readAuthMessage, writeAuthentication, WsReadyStates, Unauthorized, Forbidden, MessageTooBig, awarenessStatesToArray } from '@hocuspocus/common';
3
3
  import { retry } from '@lifeomic/attempt';
4
4
 
5
5
  /**
@@ -50,6 +50,22 @@ const setIfUndefined = (map, key, createT) => {
50
50
 
51
51
  const create$1 = () => new Set();
52
52
 
53
+ /**
54
+ * Utility module to work with Arrays.
55
+ *
56
+ * @module array
57
+ */
58
+
59
+ /**
60
+ * Transforms something array-like to an actual Array.
61
+ *
62
+ * @function
63
+ * @template T
64
+ * @param {ArrayLike<T>|Iterable<T>} arraylike
65
+ * @return {T}
66
+ */
67
+ const from = Array.from;
68
+
53
69
  /**
54
70
  * Utility module to work with strings.
55
71
  *
@@ -212,22 +228,6 @@ const onChange = eventHandler => usePolyfill || addEventListener('storage', /**
212
228
  /* c8 ignore next */
213
229
  const offChange = eventHandler => usePolyfill || removeEventListener('storage', /** @type {any} */ (eventHandler));
214
230
 
215
- /**
216
- * Utility module to work with Arrays.
217
- *
218
- * @module array
219
- */
220
-
221
- /**
222
- * Transforms something array-like to an actual Array.
223
- *
224
- * @function
225
- * @template T
226
- * @param {ArrayLike<T>|Iterable<T>} arraylike
227
- * @return {T}
228
- */
229
- const from = Array.from;
230
-
231
231
  /**
232
232
  * Utility functions for working with EcmaScript objects.
233
233
  *
@@ -375,7 +375,6 @@ const equalityDeep = (a, b) => {
375
375
  */
376
376
  // @ts-ignore
377
377
  const isOneOf = (value, options) => options.includes(value);
378
- /* c8 ignore stop */
379
378
 
380
379
  /**
381
380
  * Isomorphic module to work access the environment (query params, env variables).
@@ -507,7 +506,9 @@ const min = (a, b) => a < b ? a : b;
507
506
  const max = (a, b) => a > b ? a : b;
508
507
 
509
508
  /* eslint-env browser */
509
+ const BIT7 = 64;
510
510
  const BIT8 = 128;
511
+ const BITS6 = 63;
511
512
  const BITS7 = 127;
512
513
 
513
514
  /**
@@ -865,6 +866,44 @@ const readVarUint = decoder => {
865
866
  throw errorUnexpectedEndOfArray
866
867
  };
867
868
 
869
+ /**
870
+ * Read signed integer (32bit) with variable length.
871
+ * 1/8th of the storage is used as encoding overhead.
872
+ * * numbers < 2^7 is stored in one bytlength
873
+ * * numbers < 2^14 is stored in two bylength
874
+ * @todo This should probably create the inverse ~num if number is negative - but this would be a breaking change.
875
+ *
876
+ * @function
877
+ * @param {Decoder} decoder
878
+ * @return {number} An unsigned integer.length
879
+ */
880
+ const readVarInt = decoder => {
881
+ let r = decoder.arr[decoder.pos++];
882
+ let num = r & BITS6;
883
+ let mult = 64;
884
+ const sign = (r & BIT7) > 0 ? -1 : 1;
885
+ if ((r & BIT8) === 0) {
886
+ // don't continue reading
887
+ return sign * num
888
+ }
889
+ const len = decoder.arr.length;
890
+ while (decoder.pos < len) {
891
+ r = decoder.arr[decoder.pos++];
892
+ // num = num | ((r & binary.BITS7) << len)
893
+ num = num + (r & BITS7) * mult;
894
+ mult *= 128;
895
+ if (r < BIT8) {
896
+ return sign * num
897
+ }
898
+ /* c8 ignore start */
899
+ if (num > MAX_SAFE_INTEGER) {
900
+ throw errorIntegerOutOfRange
901
+ }
902
+ /* c8 ignore stop */
903
+ }
904
+ throw errorUnexpectedEndOfArray
905
+ };
906
+
868
907
  /**
869
908
  * We don't test this function anymore as we use native decoding/encoding by default now.
870
909
  * Better not modify this anymore..
@@ -1112,6 +1151,50 @@ const publish = (room, data, origin = null) => {
1112
1151
  c.subs.forEach(sub => sub(data, origin));
1113
1152
  };
1114
1153
 
1154
+ /**
1155
+ * Mutual exclude for JavaScript.
1156
+ *
1157
+ * @module mutex
1158
+ */
1159
+
1160
+ /**
1161
+ * @callback mutex
1162
+ * @param {function():void} cb Only executed when this mutex is not in the current stack
1163
+ * @param {function():void} [elseCb] Executed when this mutex is in the current stack
1164
+ */
1165
+
1166
+ /**
1167
+ * Creates a mutual exclude function with the following property:
1168
+ *
1169
+ * ```js
1170
+ * const mutex = createMutex()
1171
+ * mutex(() => {
1172
+ * // This function is immediately executed
1173
+ * mutex(() => {
1174
+ * // This function is not executed, as the mutex is already active.
1175
+ * })
1176
+ * })
1177
+ * ```
1178
+ *
1179
+ * @return {mutex} A mutual exclude function
1180
+ * @public
1181
+ */
1182
+ const createMutex = () => {
1183
+ let token = true;
1184
+ return (f, g) => {
1185
+ if (token) {
1186
+ token = false;
1187
+ try {
1188
+ f();
1189
+ } finally {
1190
+ token = true;
1191
+ }
1192
+ } else if (g !== undefined) {
1193
+ g();
1194
+ }
1195
+ }
1196
+ };
1197
+
1115
1198
  /**
1116
1199
  * Utility module to work with time.
1117
1200
  *
@@ -1462,50 +1545,6 @@ const applyAwarenessUpdate = (awareness, update, origin) => {
1462
1545
  }
1463
1546
  };
1464
1547
 
1465
- /**
1466
- * Mutual exclude for JavaScript.
1467
- *
1468
- * @module mutex
1469
- */
1470
-
1471
- /**
1472
- * @callback mutex
1473
- * @param {function():void} cb Only executed when this mutex is not in the current stack
1474
- * @param {function():void} [elseCb] Executed when this mutex is in the current stack
1475
- */
1476
-
1477
- /**
1478
- * Creates a mutual exclude function with the following property:
1479
- *
1480
- * ```js
1481
- * const mutex = createMutex()
1482
- * mutex(() => {
1483
- * // This function is immediately executed
1484
- * mutex(() => {
1485
- * // This function is not executed, as the mutex is already active.
1486
- * })
1487
- * })
1488
- * ```
1489
- *
1490
- * @return {mutex} A mutual exclude function
1491
- * @public
1492
- */
1493
- const createMutex = () => {
1494
- let token = true;
1495
- return (f, g) => {
1496
- if (token) {
1497
- token = false;
1498
- try {
1499
- f();
1500
- } finally {
1501
- token = true;
1502
- }
1503
- } else if (g !== undefined) {
1504
- g();
1505
- }
1506
- }
1507
- };
1508
-
1509
1548
  class EventEmitter {
1510
1549
  constructor() {
1511
1550
  this.callbacks = {};
@@ -1541,717 +1580,711 @@ class EventEmitter {
1541
1580
  }
1542
1581
  }
1543
1582
 
1544
- class IncomingMessage {
1545
- constructor(data) {
1546
- this.data = data;
1547
- this.encoder = createEncoder();
1548
- this.decoder = createDecoder(new Uint8Array(this.data));
1549
- }
1550
- readVarUint() {
1551
- return readVarUint(this.decoder);
1552
- }
1553
- readVarString() {
1554
- return readVarString(this.decoder);
1555
- }
1556
- readVarUint8Array() {
1557
- return readVarUint8Array(this.decoder);
1558
- }
1559
- writeVarUint(type) {
1560
- return writeVarUint(this.encoder, type);
1561
- }
1562
- writeVarString(string) {
1563
- return writeVarString(this.encoder, string);
1564
- }
1565
- writeVarUint8Array(data) {
1566
- return writeVarUint8Array(this.encoder, data);
1567
- }
1568
- length() {
1569
- return length(this.encoder);
1570
- }
1571
- }
1572
-
1573
- /**
1574
- * @module sync-protocol
1575
- */
1576
-
1577
- /**
1578
- * @typedef {Map<number, number>} StateMap
1579
- */
1580
-
1581
1583
  /**
1582
- * Core Yjs defines two message types:
1583
- * • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
1584
- * • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
1585
- * received all information from the remote client.
1586
- *
1587
- * In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
1588
- * with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
1589
- * SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
1590
- *
1591
- * In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
1592
- * When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
1593
- * with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
1594
- * client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
1595
- * easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
1596
- * Therefore it is necesarry that the client initiates the sync.
1597
- *
1598
- * Construction of a message:
1599
- * [messageType : varUint, message definition..]
1600
- *
1601
- * Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
1584
+ * Utility module to work with urls.
1602
1585
  *
1603
- * stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
1586
+ * @module url
1604
1587
  */
1605
1588
 
1606
- const messageYjsSyncStep1 = 0;
1607
- const messageYjsSyncStep2 = 1;
1608
- const messageYjsUpdate = 2;
1609
-
1610
1589
  /**
1611
- * Create a sync step 1 message based on the state of the current shared document.
1612
- *
1613
- * @param {encoding.Encoder} encoder
1614
- * @param {Y.Doc} doc
1590
+ * @param {Object<string,string>} params
1591
+ * @return {string}
1615
1592
  */
1616
- const writeSyncStep1 = (encoder, doc) => {
1617
- writeVarUint(encoder, messageYjsSyncStep1);
1618
- const sv = Y.encodeStateVector(doc);
1619
- writeVarUint8Array(encoder, sv);
1620
- };
1593
+ const encodeQueryParams = params =>
1594
+ map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
1621
1595
 
1622
- /**
1623
- * @param {encoding.Encoder} encoder
1624
- * @param {Y.Doc} doc
1625
- * @param {Uint8Array} [encodedStateVector]
1626
- */
1627
- const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
1628
- writeVarUint(encoder, messageYjsSyncStep2);
1629
- writeVarUint8Array(encoder, Y.encodeStateAsUpdate(doc, encodedStateVector));
1630
- };
1596
+ var MessageType;
1597
+ (function (MessageType) {
1598
+ MessageType[MessageType["Sync"] = 0] = "Sync";
1599
+ MessageType[MessageType["Awareness"] = 1] = "Awareness";
1600
+ MessageType[MessageType["Auth"] = 2] = "Auth";
1601
+ MessageType[MessageType["QueryAwareness"] = 3] = "QueryAwareness";
1602
+ MessageType[MessageType["Stateless"] = 5] = "Stateless";
1603
+ MessageType[MessageType["CLOSE"] = 7] = "CLOSE";
1604
+ MessageType[MessageType["SyncStatus"] = 8] = "SyncStatus";
1605
+ })(MessageType || (MessageType = {}));
1606
+ var WebSocketStatus;
1607
+ (function (WebSocketStatus) {
1608
+ WebSocketStatus["Connecting"] = "connecting";
1609
+ WebSocketStatus["Connected"] = "connected";
1610
+ WebSocketStatus["Disconnected"] = "disconnected";
1611
+ })(WebSocketStatus || (WebSocketStatus = {}));
1631
1612
 
1632
- /**
1633
- * Read SyncStep1 message and reply with SyncStep2.
1634
- *
1635
- * @param {decoding.Decoder} decoder The reply to the received message
1636
- * @param {encoding.Encoder} encoder The received message
1637
- * @param {Y.Doc} doc
1638
- */
1639
- const readSyncStep1 = (decoder, encoder, doc) =>
1640
- writeSyncStep2(encoder, doc, readVarUint8Array(decoder));
1641
-
1642
- /**
1643
- * Read and apply Structs and then DeleteStore to a y instance.
1644
- *
1645
- * @param {decoding.Decoder} decoder
1646
- * @param {Y.Doc} doc
1647
- * @param {any} transactionOrigin
1648
- */
1649
- const readSyncStep2 = (decoder, doc, transactionOrigin) => {
1650
- try {
1651
- Y.applyUpdate(doc, readVarUint8Array(decoder), transactionOrigin);
1652
- } catch (error) {
1653
- // This catches errors that are thrown by event handlers
1654
- console.error('Caught error while handling a Yjs update', error);
1655
- }
1656
- };
1657
-
1658
- /**
1659
- * @param {encoding.Encoder} encoder
1660
- * @param {Uint8Array} update
1661
- */
1662
- const writeUpdate = (encoder, update) => {
1663
- writeVarUint(encoder, messageYjsUpdate);
1664
- writeVarUint8Array(encoder, update);
1665
- };
1666
-
1667
- /**
1668
- * Read and apply Structs and then DeleteStore to a y instance.
1669
- *
1670
- * @param {decoding.Decoder} decoder
1671
- * @param {Y.Doc} doc
1672
- * @param {any} transactionOrigin
1673
- */
1674
- const readUpdate = readSyncStep2;
1675
-
1676
- /**
1677
- * @param {decoding.Decoder} decoder A message received from another client
1678
- * @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
1679
- * @param {Y.Doc} doc
1680
- * @param {any} transactionOrigin
1681
- */
1682
- const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
1683
- const messageType = readVarUint(decoder);
1684
- switch (messageType) {
1685
- case messageYjsSyncStep1:
1686
- readSyncStep1(decoder, encoder, doc);
1687
- break
1688
- case messageYjsSyncStep2:
1689
- readSyncStep2(decoder, doc, transactionOrigin);
1690
- break
1691
- case messageYjsUpdate:
1692
- readUpdate(decoder, doc, transactionOrigin);
1693
- break
1694
- default:
1695
- throw new Error('Unknown message type')
1696
- }
1697
- return messageType
1698
- };
1699
-
1700
- var MessageType;
1701
- (function (MessageType) {
1702
- MessageType[MessageType["Sync"] = 0] = "Sync";
1703
- MessageType[MessageType["Awareness"] = 1] = "Awareness";
1704
- MessageType[MessageType["Auth"] = 2] = "Auth";
1705
- MessageType[MessageType["QueryAwareness"] = 3] = "QueryAwareness";
1706
- MessageType[MessageType["Stateless"] = 5] = "Stateless";
1707
- MessageType[MessageType["CLOSE"] = 7] = "CLOSE";
1708
- MessageType[MessageType["SyncStatus"] = 8] = "SyncStatus";
1709
- })(MessageType || (MessageType = {}));
1710
- var WebSocketStatus;
1711
- (function (WebSocketStatus) {
1712
- WebSocketStatus["Connecting"] = "connecting";
1713
- WebSocketStatus["Connected"] = "connected";
1714
- WebSocketStatus["Disconnected"] = "disconnected";
1715
- })(WebSocketStatus || (WebSocketStatus = {}));
1716
-
1717
- class OutgoingMessage {
1718
- constructor() {
1719
- this.encoder = createEncoder();
1613
+ class HocuspocusProviderWebsocket extends EventEmitter {
1614
+ constructor(configuration) {
1615
+ super();
1616
+ this.messageQueue = [];
1617
+ this.configuration = {
1618
+ url: '',
1619
+ // @ts-ignore
1620
+ document: undefined,
1621
+ // @ts-ignore
1622
+ awareness: undefined,
1623
+ WebSocketPolyfill: undefined,
1624
+ parameters: {},
1625
+ connect: true,
1626
+ broadcast: true,
1627
+ forceSyncInterval: false,
1628
+ // TODO: this should depend on awareness.outdatedTime
1629
+ messageReconnectTimeout: 30000,
1630
+ // 1 second
1631
+ delay: 1000,
1632
+ // instant
1633
+ initialDelay: 0,
1634
+ // double the delay each time
1635
+ factor: 2,
1636
+ // unlimited retries
1637
+ maxAttempts: 0,
1638
+ // wait at least 1 second
1639
+ minDelay: 1000,
1640
+ // at least every 30 seconds
1641
+ maxDelay: 30000,
1642
+ // randomize
1643
+ jitter: true,
1644
+ // retry forever
1645
+ timeout: 0,
1646
+ onOpen: () => null,
1647
+ onConnect: () => null,
1648
+ onMessage: () => null,
1649
+ onOutgoingMessage: () => null,
1650
+ onStatus: () => null,
1651
+ onDisconnect: () => null,
1652
+ onClose: () => null,
1653
+ onDestroy: () => null,
1654
+ onAwarenessUpdate: () => null,
1655
+ onAwarenessChange: () => null,
1656
+ quiet: false,
1657
+ };
1658
+ this.subscribedToBroadcastChannel = false;
1659
+ this.webSocket = null;
1660
+ this.shouldConnect = true;
1661
+ this.status = WebSocketStatus.Disconnected;
1662
+ this.lastMessageReceived = 0;
1663
+ this.mux = createMutex();
1664
+ this.intervals = {
1665
+ forceSync: null,
1666
+ connectionChecker: null,
1667
+ };
1668
+ this.connectionAttempt = null;
1669
+ this.receivedOnOpenPayload = undefined;
1670
+ this.receivedOnStatusPayload = undefined;
1671
+ this.boundConnect = this.connect.bind(this);
1672
+ this.closeTries = 0;
1673
+ this.setConfiguration(configuration);
1674
+ this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
1675
+ this.on('open', this.configuration.onOpen);
1676
+ this.on('open', this.onOpen.bind(this));
1677
+ this.on('connect', this.configuration.onConnect);
1678
+ this.on('message', this.configuration.onMessage);
1679
+ this.on('outgoingMessage', this.configuration.onOutgoingMessage);
1680
+ this.on('status', this.configuration.onStatus);
1681
+ this.on('status', this.onStatus.bind(this));
1682
+ this.on('disconnect', this.configuration.onDisconnect);
1683
+ this.on('close', this.configuration.onClose);
1684
+ this.on('destroy', this.configuration.onDestroy);
1685
+ this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
1686
+ this.on('awarenessChange', this.configuration.onAwarenessChange);
1687
+ this.on('close', this.onClose.bind(this));
1688
+ this.on('message', this.onMessage.bind(this));
1689
+ this.registerEventListeners();
1690
+ this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
1691
+ if (typeof configuration.connect !== 'undefined') {
1692
+ this.shouldConnect = configuration.connect;
1693
+ }
1694
+ if (!this.shouldConnect) {
1695
+ return;
1696
+ }
1697
+ this.connect();
1720
1698
  }
1721
- get(args) {
1722
- return args.encoder;
1699
+ async onOpen(event) {
1700
+ this.receivedOnOpenPayload = event;
1723
1701
  }
1724
- toUint8Array() {
1725
- return toUint8Array(this.encoder);
1702
+ async onStatus(data) {
1703
+ this.receivedOnStatusPayload = data;
1726
1704
  }
1727
- }
1728
-
1729
- class MessageReceiver {
1730
- constructor(message) {
1731
- this.broadcasted = false;
1732
- this.message = message;
1705
+ attach(provider) {
1706
+ if (this.status === WebSocketStatus.Disconnected && this.shouldConnect) {
1707
+ this.connect();
1708
+ }
1709
+ if (this.receivedOnOpenPayload) {
1710
+ provider.onOpen(this.receivedOnOpenPayload);
1711
+ }
1712
+ if (this.receivedOnStatusPayload) {
1713
+ provider.onStatus(this.receivedOnStatusPayload);
1714
+ }
1733
1715
  }
1734
- setBroadcasted(value) {
1735
- this.broadcasted = value;
1736
- return this;
1716
+ detach(provider) {
1717
+ // tell the server to remove the listener
1737
1718
  }
1738
- apply(provider, emitSynced = true) {
1739
- const { message } = this;
1740
- const type = message.readVarUint();
1741
- const emptyMessageLength = message.length();
1742
- switch (type) {
1743
- case MessageType.Sync:
1744
- this.applySyncMessage(provider, emitSynced);
1745
- break;
1746
- case MessageType.Awareness:
1747
- this.applyAwarenessMessage(provider);
1748
- break;
1749
- case MessageType.Auth:
1750
- this.applyAuthMessage(provider);
1751
- break;
1752
- case MessageType.QueryAwareness:
1753
- this.applyQueryAwarenessMessage(provider);
1754
- break;
1755
- case MessageType.Stateless:
1756
- provider.receiveStateless(readVarString(message.decoder));
1757
- break;
1758
- case MessageType.SyncStatus:
1759
- // nothing for now; forward-compatability
1760
- break;
1761
- default:
1762
- throw new Error(`Can’t apply message of unknown type: ${type}`);
1719
+ setConfiguration(configuration = {}) {
1720
+ this.configuration = { ...this.configuration, ...configuration };
1721
+ }
1722
+ async connect() {
1723
+ if (this.status === WebSocketStatus.Connected) {
1724
+ return;
1763
1725
  }
1764
- // Reply
1765
- if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
1766
- if (this.broadcasted) {
1767
- // TODO: Some weird TypeScript error
1768
- // @ts-ignore
1769
- provider.broadcast(OutgoingMessage, { encoder: message.encoder });
1726
+ // Always cancel any previously initiated connection retryer instances
1727
+ if (this.cancelWebsocketRetry) {
1728
+ this.cancelWebsocketRetry();
1729
+ this.cancelWebsocketRetry = undefined;
1730
+ }
1731
+ this.receivedOnOpenPayload = undefined;
1732
+ this.receivedOnStatusPayload = undefined;
1733
+ this.shouldConnect = true;
1734
+ const abortableRetry = () => {
1735
+ let cancelAttempt = false;
1736
+ const retryPromise = retry(this.createWebSocketConnection.bind(this), {
1737
+ delay: this.configuration.delay,
1738
+ initialDelay: this.configuration.initialDelay,
1739
+ factor: this.configuration.factor,
1740
+ maxAttempts: this.configuration.maxAttempts,
1741
+ minDelay: this.configuration.minDelay,
1742
+ maxDelay: this.configuration.maxDelay,
1743
+ jitter: this.configuration.jitter,
1744
+ timeout: this.configuration.timeout,
1745
+ beforeAttempt: context => {
1746
+ if (!this.shouldConnect || cancelAttempt) {
1747
+ context.abort();
1748
+ }
1749
+ },
1750
+ }).catch((error) => {
1751
+ // If we aborted the connection attempt then don’t throw an error
1752
+ // ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
1753
+ if (error && error.code !== 'ATTEMPT_ABORTED') {
1754
+ throw error;
1755
+ }
1756
+ });
1757
+ return {
1758
+ retryPromise,
1759
+ cancelFunc: () => {
1760
+ cancelAttempt = true;
1761
+ },
1762
+ };
1763
+ };
1764
+ const { retryPromise, cancelFunc } = abortableRetry();
1765
+ this.cancelWebsocketRetry = cancelFunc;
1766
+ return retryPromise;
1767
+ }
1768
+ createWebSocketConnection() {
1769
+ return new Promise((resolve, reject) => {
1770
+ if (this.webSocket) {
1771
+ this.messageQueue = [];
1772
+ this.webSocket.close();
1773
+ this.webSocket = null;
1774
+ }
1775
+ // Init the WebSocket connection
1776
+ const ws = new this.configuration.WebSocketPolyfill(this.url);
1777
+ ws.binaryType = 'arraybuffer';
1778
+ ws.onmessage = (payload) => this.emit('message', payload);
1779
+ ws.onclose = (payload) => this.emit('close', { event: payload });
1780
+ ws.onopen = (payload) => this.emit('open', payload);
1781
+ ws.onerror = (err) => {
1782
+ reject(err);
1783
+ };
1784
+ this.webSocket = ws;
1785
+ // Reset the status
1786
+ this.status = WebSocketStatus.Connecting;
1787
+ this.emit('status', { status: WebSocketStatus.Connecting });
1788
+ // Store resolve/reject for later use
1789
+ this.connectionAttempt = {
1790
+ resolve,
1791
+ reject,
1792
+ };
1793
+ });
1794
+ }
1795
+ onMessage(event) {
1796
+ this.resolveConnectionAttempt();
1797
+ this.lastMessageReceived = getUnixTime();
1798
+ }
1799
+ resolveConnectionAttempt() {
1800
+ if (this.connectionAttempt) {
1801
+ this.connectionAttempt.resolve();
1802
+ this.connectionAttempt = null;
1803
+ this.status = WebSocketStatus.Connected;
1804
+ this.emit('status', { status: WebSocketStatus.Connected });
1805
+ this.emit('connect');
1806
+ this.messageQueue.forEach(message => this.send(message));
1807
+ this.messageQueue = [];
1808
+ }
1809
+ }
1810
+ stopConnectionAttempt() {
1811
+ this.connectionAttempt = null;
1812
+ }
1813
+ rejectConnectionAttempt() {
1814
+ var _a;
1815
+ (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
1816
+ this.connectionAttempt = null;
1817
+ }
1818
+ checkConnection() {
1819
+ var _a;
1820
+ // Don’t check the connection when it’s not even established
1821
+ if (this.status !== WebSocketStatus.Connected) {
1822
+ return;
1823
+ }
1824
+ // Don’t close then connection while waiting for the first message
1825
+ if (!this.lastMessageReceived) {
1826
+ return;
1827
+ }
1828
+ // Don’t close the connection when a message was received recently
1829
+ if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
1830
+ return;
1831
+ }
1832
+ // No message received in a long time, not even your own
1833
+ // Awareness updates, which are updated every 15 seconds.
1834
+ this.closeTries += 1;
1835
+ // https://bugs.webkit.org/show_bug.cgi?id=247943
1836
+ if (this.closeTries > 2) {
1837
+ this.onClose({
1838
+ event: {
1839
+ code: 4408,
1840
+ reason: 'forced',
1841
+ },
1842
+ });
1843
+ this.closeTries = 0;
1844
+ }
1845
+ else {
1846
+ (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
1847
+ this.messageQueue = [];
1848
+ }
1849
+ }
1850
+ registerEventListeners() {
1851
+ if (typeof window === 'undefined') {
1852
+ return;
1853
+ }
1854
+ window.addEventListener('online', this.boundConnect);
1855
+ }
1856
+ // Ensure that the URL always ends with /
1857
+ get serverUrl() {
1858
+ while (this.configuration.url[this.configuration.url.length - 1] === '/') {
1859
+ return this.configuration.url.slice(0, this.configuration.url.length - 1);
1860
+ }
1861
+ return this.configuration.url;
1862
+ }
1863
+ get url() {
1864
+ const encodedParams = encodeQueryParams(this.configuration.parameters);
1865
+ return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
1866
+ }
1867
+ disconnect() {
1868
+ this.shouldConnect = false;
1869
+ if (this.webSocket === null) {
1870
+ return;
1871
+ }
1872
+ try {
1873
+ this.webSocket.close();
1874
+ this.messageQueue = [];
1875
+ }
1876
+ catch {
1877
+ //
1878
+ }
1879
+ }
1880
+ send(message) {
1881
+ var _a;
1882
+ if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === WsReadyStates.Open) {
1883
+ this.webSocket.send(message);
1884
+ }
1885
+ else {
1886
+ this.messageQueue.push(message);
1887
+ }
1888
+ }
1889
+ onClose({ event }) {
1890
+ this.closeTries = 0;
1891
+ this.webSocket = null;
1892
+ if (this.status === WebSocketStatus.Connected) {
1893
+ this.status = WebSocketStatus.Disconnected;
1894
+ this.emit('status', { status: WebSocketStatus.Disconnected });
1895
+ this.emit('disconnect', { event });
1896
+ }
1897
+ if (event.code === Unauthorized.code) {
1898
+ if (event.reason === Unauthorized.reason) {
1899
+ 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.');
1770
1900
  }
1771
1901
  else {
1772
- // TODO: Some weird TypeScript error
1773
- // @ts-ignore
1774
- provider.send(OutgoingMessage, { encoder: message.encoder });
1902
+ console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
1775
1903
  }
1904
+ this.shouldConnect = false;
1776
1905
  }
1777
- }
1778
- applySyncMessage(provider, emitSynced) {
1779
- const { message } = this;
1780
- message.writeVarUint(MessageType.Sync);
1781
- // Apply update
1782
- const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
1783
- // Synced once we receive Step2
1784
- if (emitSynced && syncMessageType === messageYjsSyncStep2) {
1785
- provider.synced = true;
1906
+ if (event.code === Forbidden.code) {
1907
+ if (!this.configuration.quiet) {
1908
+ console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
1909
+ return; // TODO REMOVE ME
1910
+ }
1911
+ }
1912
+ if (event.code === MessageTooBig.code) {
1913
+ console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
1914
+ this.shouldConnect = false;
1915
+ }
1916
+ if (this.connectionAttempt) {
1917
+ // That connection attempt failed.
1918
+ this.rejectConnectionAttempt();
1919
+ }
1920
+ else if (this.shouldConnect) {
1921
+ // The connection was closed by the server. Let’s just try again.
1922
+ this.connect();
1923
+ }
1924
+ // If we’ll reconnect, we’re done for now.
1925
+ if (this.shouldConnect) {
1926
+ return;
1786
1927
  }
1787
- if (syncMessageType === messageYjsUpdate || syncMessageType === messageYjsSyncStep2) {
1788
- if (provider.unsyncedChanges > 0) {
1789
- provider.updateUnsyncedChanges(-1);
1790
- }
1928
+ // The status is set correctly already.
1929
+ if (this.status === WebSocketStatus.Disconnected) {
1930
+ return;
1791
1931
  }
1932
+ // Let’s update the connection status.
1933
+ this.status = WebSocketStatus.Disconnected;
1934
+ this.emit('status', { status: WebSocketStatus.Disconnected });
1935
+ this.emit('disconnect', { event });
1792
1936
  }
1793
- applyAwarenessMessage(provider) {
1794
- const { message } = this;
1795
- applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
1796
- }
1797
- applyAuthMessage(provider) {
1798
- const { message } = this;
1799
- readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
1800
- }
1801
- applyQueryAwarenessMessage(provider) {
1802
- const { message } = this;
1803
- message.writeVarUint(MessageType.Awareness);
1804
- message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
1937
+ destroy() {
1938
+ this.emit('destroy');
1939
+ if (this.intervals.forceSync) {
1940
+ clearInterval(this.intervals.forceSync);
1941
+ }
1942
+ clearInterval(this.intervals.connectionChecker);
1943
+ // If there is still a connection attempt outstanding then we should stop
1944
+ // it before calling disconnect, otherwise it will be rejected in the onClose
1945
+ // handler and trigger a retry
1946
+ this.stopConnectionAttempt();
1947
+ this.disconnect();
1948
+ this.removeAllListeners();
1949
+ if (typeof window === 'undefined') {
1950
+ return;
1951
+ }
1952
+ window.removeEventListener('online', this.boundConnect);
1805
1953
  }
1806
1954
  }
1807
1955
 
1808
- class MessageSender {
1809
- constructor(Message, args = {}) {
1810
- this.message = new Message();
1811
- this.encoder = this.message.get(args);
1956
+ class IncomingMessage {
1957
+ constructor(data) {
1958
+ this.data = data;
1959
+ this.encoder = createEncoder();
1960
+ this.decoder = createDecoder(new Uint8Array(this.data));
1812
1961
  }
1813
- create() {
1814
- return toUint8Array(this.encoder);
1962
+ readVarUint() {
1963
+ return readVarUint(this.decoder);
1815
1964
  }
1816
- send(webSocket) {
1817
- webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
1965
+ readVarString() {
1966
+ return readVarString(this.decoder);
1818
1967
  }
1819
- broadcast(channel) {
1820
- publish(channel, this.create());
1968
+ readVarUint8Array() {
1969
+ return readVarUint8Array(this.decoder);
1821
1970
  }
1822
- }
1823
-
1824
- class SyncStepOneMessage extends OutgoingMessage {
1825
- constructor() {
1826
- super(...arguments);
1827
- this.type = MessageType.Sync;
1828
- this.description = 'First sync step';
1971
+ writeVarUint(type) {
1972
+ return writeVarUint(this.encoder, type);
1829
1973
  }
1830
- get(args) {
1831
- if (typeof args.document === 'undefined') {
1832
- throw new Error('The sync step one message requires document as an argument');
1833
- }
1834
- writeVarString(this.encoder, args.documentName);
1835
- writeVarUint(this.encoder, this.type);
1836
- writeSyncStep1(this.encoder, args.document);
1837
- return this.encoder;
1974
+ writeVarString(string) {
1975
+ return writeVarString(this.encoder, string);
1838
1976
  }
1839
- }
1840
-
1841
- class SyncStepTwoMessage extends OutgoingMessage {
1842
- constructor() {
1843
- super(...arguments);
1844
- this.type = MessageType.Sync;
1845
- this.description = 'Second sync step';
1977
+ writeVarUint8Array(data) {
1978
+ return writeVarUint8Array(this.encoder, data);
1846
1979
  }
1847
- get(args) {
1848
- if (typeof args.document === 'undefined') {
1849
- throw new Error('The sync step two message requires document as an argument');
1850
- }
1851
- writeVarString(this.encoder, args.documentName);
1852
- writeVarUint(this.encoder, this.type);
1853
- writeSyncStep2(this.encoder, args.document);
1854
- return this.encoder;
1980
+ length() {
1981
+ return length(this.encoder);
1855
1982
  }
1856
1983
  }
1857
1984
 
1858
- class QueryAwarenessMessage extends OutgoingMessage {
1859
- constructor() {
1860
- super(...arguments);
1861
- this.type = MessageType.QueryAwareness;
1862
- this.description = 'Queries awareness states';
1863
- }
1864
- get(args) {
1865
- console.log('queryAwareness: writing string docName', args.documentName);
1866
- console.log(this.encoder.cpos);
1867
- writeVarString(this.encoder, args.documentName);
1868
- writeVarUint(this.encoder, this.type);
1869
- return this.encoder;
1870
- }
1871
- }
1985
+ /**
1986
+ * @module sync-protocol
1987
+ */
1872
1988
 
1873
- class AuthenticationMessage extends OutgoingMessage {
1874
- constructor() {
1875
- super(...arguments);
1876
- this.type = MessageType.Auth;
1877
- this.description = 'Authentication';
1878
- }
1879
- get(args) {
1880
- if (typeof args.token === 'undefined') {
1881
- throw new Error('The authentication message requires `token` as an argument.');
1882
- }
1883
- writeVarString(this.encoder, args.documentName);
1884
- writeVarUint(this.encoder, this.type);
1885
- writeAuthentication(this.encoder, args.token);
1886
- return this.encoder;
1887
- }
1888
- }
1989
+ /**
1990
+ * @typedef {Map<number, number>} StateMap
1991
+ */
1889
1992
 
1890
- class AwarenessMessage extends OutgoingMessage {
1891
- constructor() {
1892
- super(...arguments);
1893
- this.type = MessageType.Awareness;
1894
- this.description = 'Awareness states update';
1895
- }
1896
- get(args) {
1897
- if (typeof args.awareness === 'undefined') {
1898
- throw new Error('The awareness message requires awareness as an argument');
1899
- }
1900
- if (typeof args.clients === 'undefined') {
1901
- throw new Error('The awareness message requires clients as an argument');
1902
- }
1903
- writeVarString(this.encoder, args.documentName);
1904
- writeVarUint(this.encoder, this.type);
1905
- let awarenessUpdate;
1906
- if (args.states === undefined) {
1907
- awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
1908
- }
1909
- else {
1910
- awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
1911
- }
1912
- writeVarUint8Array(this.encoder, awarenessUpdate);
1913
- return this.encoder;
1914
- }
1915
- }
1993
+ /**
1994
+ * Core Yjs defines two message types:
1995
+ * • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
1996
+ * • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
1997
+ * received all information from the remote client.
1998
+ *
1999
+ * In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
2000
+ * with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
2001
+ * SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
2002
+ *
2003
+ * In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
2004
+ * When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
2005
+ * with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
2006
+ * client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
2007
+ * easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
2008
+ * Therefore it is necesarry that the client initiates the sync.
2009
+ *
2010
+ * Construction of a message:
2011
+ * [messageType : varUint, message definition..]
2012
+ *
2013
+ * Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
2014
+ *
2015
+ * stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
2016
+ */
1916
2017
 
1917
- class UpdateMessage extends OutgoingMessage {
1918
- constructor() {
1919
- super(...arguments);
1920
- this.type = MessageType.Sync;
1921
- this.description = 'A document update';
1922
- }
1923
- get(args) {
1924
- writeVarString(this.encoder, args.documentName);
1925
- writeVarUint(this.encoder, this.type);
1926
- writeUpdate(this.encoder, args.update);
1927
- return this.encoder;
1928
- }
1929
- }
2018
+ const messageYjsSyncStep1 = 0;
2019
+ const messageYjsSyncStep2 = 1;
2020
+ const messageYjsUpdate = 2;
1930
2021
 
1931
2022
  /**
1932
- * Utility module to work with urls.
2023
+ * Create a sync step 1 message based on the state of the current shared document.
2024
+ *
2025
+ * @param {encoding.Encoder} encoder
2026
+ * @param {Y.Doc} doc
2027
+ */
2028
+ const writeSyncStep1 = (encoder, doc) => {
2029
+ writeVarUint(encoder, messageYjsSyncStep1);
2030
+ const sv = Y.encodeStateVector(doc);
2031
+ writeVarUint8Array(encoder, sv);
2032
+ };
2033
+
2034
+ /**
2035
+ * @param {encoding.Encoder} encoder
2036
+ * @param {Y.Doc} doc
2037
+ * @param {Uint8Array} [encodedStateVector]
2038
+ */
2039
+ const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
2040
+ writeVarUint(encoder, messageYjsSyncStep2);
2041
+ writeVarUint8Array(encoder, Y.encodeStateAsUpdate(doc, encodedStateVector));
2042
+ };
2043
+
2044
+ /**
2045
+ * Read SyncStep1 message and reply with SyncStep2.
2046
+ *
2047
+ * @param {decoding.Decoder} decoder The reply to the received message
2048
+ * @param {encoding.Encoder} encoder The received message
2049
+ * @param {Y.Doc} doc
2050
+ */
2051
+ const readSyncStep1 = (decoder, encoder, doc) =>
2052
+ writeSyncStep2(encoder, doc, readVarUint8Array(decoder));
2053
+
2054
+ /**
2055
+ * Read and apply Structs and then DeleteStore to a y instance.
2056
+ *
2057
+ * @param {decoding.Decoder} decoder
2058
+ * @param {Y.Doc} doc
2059
+ * @param {any} transactionOrigin
2060
+ */
2061
+ const readSyncStep2 = (decoder, doc, transactionOrigin) => {
2062
+ try {
2063
+ Y.applyUpdate(doc, readVarUint8Array(decoder), transactionOrigin);
2064
+ } catch (error) {
2065
+ // This catches errors that are thrown by event handlers
2066
+ console.error('Caught error while handling a Yjs update', error);
2067
+ }
2068
+ };
2069
+
2070
+ /**
2071
+ * @param {encoding.Encoder} encoder
2072
+ * @param {Uint8Array} update
2073
+ */
2074
+ const writeUpdate = (encoder, update) => {
2075
+ writeVarUint(encoder, messageYjsUpdate);
2076
+ writeVarUint8Array(encoder, update);
2077
+ };
2078
+
2079
+ /**
2080
+ * Read and apply Structs and then DeleteStore to a y instance.
1933
2081
  *
1934
- * @module url
2082
+ * @param {decoding.Decoder} decoder
2083
+ * @param {Y.Doc} doc
2084
+ * @param {any} transactionOrigin
1935
2085
  */
2086
+ const readUpdate = readSyncStep2;
1936
2087
 
1937
2088
  /**
1938
- * @param {Object<string,string>} params
1939
- * @return {string}
2089
+ * @param {decoding.Decoder} decoder A message received from another client
2090
+ * @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
2091
+ * @param {Y.Doc} doc
2092
+ * @param {any} transactionOrigin
1940
2093
  */
1941
- const encodeQueryParams = params =>
1942
- map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
2094
+ const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
2095
+ const messageType = readVarUint(decoder);
2096
+ switch (messageType) {
2097
+ case messageYjsSyncStep1:
2098
+ readSyncStep1(decoder, encoder, doc);
2099
+ break
2100
+ case messageYjsSyncStep2:
2101
+ readSyncStep2(decoder, doc, transactionOrigin);
2102
+ break
2103
+ case messageYjsUpdate:
2104
+ readUpdate(decoder, doc, transactionOrigin);
2105
+ break
2106
+ default:
2107
+ throw new Error('Unknown message type')
2108
+ }
2109
+ return messageType
2110
+ };
1943
2111
 
1944
- class HocuspocusProviderWebsocket extends EventEmitter {
1945
- constructor(configuration) {
1946
- super();
1947
- this.configuration = {
1948
- url: '',
1949
- // @ts-ignore
1950
- document: undefined,
1951
- // @ts-ignore
1952
- awareness: undefined,
1953
- WebSocketPolyfill: undefined,
1954
- parameters: {},
1955
- connect: true,
1956
- broadcast: true,
1957
- forceSyncInterval: false,
1958
- // TODO: this should depend on awareness.outdatedTime
1959
- messageReconnectTimeout: 30000,
1960
- // 1 second
1961
- delay: 1000,
1962
- // instant
1963
- initialDelay: 0,
1964
- // double the delay each time
1965
- factor: 2,
1966
- // unlimited retries
1967
- maxAttempts: 0,
1968
- // wait at least 1 second
1969
- minDelay: 1000,
1970
- // at least every 30 seconds
1971
- maxDelay: 30000,
1972
- // randomize
1973
- jitter: true,
1974
- // retry forever
1975
- timeout: 0,
1976
- onOpen: () => null,
1977
- onConnect: () => null,
1978
- onMessage: () => null,
1979
- onOutgoingMessage: () => null,
1980
- onStatus: () => null,
1981
- onDisconnect: () => null,
1982
- onClose: () => null,
1983
- onDestroy: () => null,
1984
- onAwarenessUpdate: () => null,
1985
- onAwarenessChange: () => null,
1986
- quiet: false,
1987
- };
1988
- this.subscribedToBroadcastChannel = false;
1989
- this.webSocket = null;
1990
- this.shouldConnect = true;
1991
- this.status = WebSocketStatus.Disconnected;
1992
- this.lastMessageReceived = 0;
1993
- this.mux = createMutex();
1994
- this.intervals = {
1995
- forceSync: null,
1996
- connectionChecker: null,
1997
- };
1998
- this.connectionAttempt = null;
1999
- this.receivedOnOpenPayload = undefined;
2000
- this.receivedOnStatusPayload = undefined;
2001
- this.boundConnect = this.connect.bind(this);
2002
- this.setConfiguration(configuration);
2003
- this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
2004
- this.on('open', this.configuration.onOpen);
2005
- this.on('open', this.onOpen.bind(this));
2006
- this.on('connect', this.configuration.onConnect);
2007
- this.on('message', this.configuration.onMessage);
2008
- this.on('outgoingMessage', this.configuration.onOutgoingMessage);
2009
- this.on('status', this.configuration.onStatus);
2010
- this.on('status', this.onStatus.bind(this));
2011
- this.on('disconnect', this.configuration.onDisconnect);
2012
- this.on('close', this.configuration.onClose);
2013
- this.on('destroy', this.configuration.onDestroy);
2014
- this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
2015
- this.on('awarenessChange', this.configuration.onAwarenessChange);
2016
- this.on('close', this.onClose.bind(this));
2017
- this.on('message', this.onMessage.bind(this));
2018
- this.registerEventListeners();
2019
- this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
2020
- if (typeof configuration.connect !== 'undefined') {
2021
- this.shouldConnect = configuration.connect;
2022
- }
2023
- if (!this.shouldConnect) {
2024
- return;
2025
- }
2026
- this.connect();
2027
- }
2028
- async onOpen(event) {
2029
- this.receivedOnOpenPayload = event;
2112
+ class OutgoingMessage {
2113
+ constructor() {
2114
+ this.encoder = createEncoder();
2030
2115
  }
2031
- async onStatus(data) {
2032
- this.receivedOnStatusPayload = data;
2116
+ get(args) {
2117
+ return args.encoder;
2033
2118
  }
2034
- attach(provider) {
2035
- if (this.receivedOnOpenPayload) {
2036
- provider.onOpen(this.receivedOnOpenPayload);
2037
- }
2038
- if (this.receivedOnStatusPayload) {
2039
- provider.onStatus(this.receivedOnStatusPayload);
2040
- }
2119
+ toUint8Array() {
2120
+ return toUint8Array(this.encoder);
2041
2121
  }
2042
- detach(provider) {
2043
- // tell the server to remove the listener
2122
+ }
2123
+
2124
+ class MessageReceiver {
2125
+ constructor(message) {
2126
+ this.broadcasted = false;
2127
+ this.message = message;
2044
2128
  }
2045
- setConfiguration(configuration = {}) {
2046
- this.configuration = { ...this.configuration, ...configuration };
2129
+ setBroadcasted(value) {
2130
+ this.broadcasted = value;
2131
+ return this;
2047
2132
  }
2048
- async connect() {
2049
- if (this.status === WebSocketStatus.Connected) {
2050
- return;
2051
- }
2052
- // Always cancel any previously initiated connection retryer instances
2053
- if (this.cancelWebsocketRetry) {
2054
- this.cancelWebsocketRetry();
2055
- this.cancelWebsocketRetry = undefined;
2133
+ apply(provider, emitSynced) {
2134
+ const { message } = this;
2135
+ const type = message.readVarUint();
2136
+ const emptyMessageLength = message.length();
2137
+ switch (type) {
2138
+ case MessageType.Sync:
2139
+ this.applySyncMessage(provider, emitSynced);
2140
+ break;
2141
+ case MessageType.Awareness:
2142
+ this.applyAwarenessMessage(provider);
2143
+ break;
2144
+ case MessageType.Auth:
2145
+ this.applyAuthMessage(provider);
2146
+ break;
2147
+ case MessageType.QueryAwareness:
2148
+ this.applyQueryAwarenessMessage(provider);
2149
+ break;
2150
+ case MessageType.Stateless:
2151
+ provider.receiveStateless(readVarString(message.decoder));
2152
+ break;
2153
+ case MessageType.SyncStatus:
2154
+ this.applySyncStatusMessage(provider, readVarInt(message.decoder) === 1);
2155
+ break;
2156
+ default:
2157
+ throw new Error(`Can’t apply message of unknown type: ${type}`);
2056
2158
  }
2057
- this.shouldConnect = true;
2058
- const abortableRetry = () => {
2059
- let cancelAttempt = false;
2060
- const retryPromise = retry(this.createWebSocketConnection.bind(this), {
2061
- delay: this.configuration.delay,
2062
- initialDelay: this.configuration.initialDelay,
2063
- factor: this.configuration.factor,
2064
- maxAttempts: this.configuration.maxAttempts,
2065
- minDelay: this.configuration.minDelay,
2066
- maxDelay: this.configuration.maxDelay,
2067
- jitter: this.configuration.jitter,
2068
- timeout: this.configuration.timeout,
2069
- beforeAttempt: context => {
2070
- if (!this.shouldConnect || cancelAttempt) {
2071
- context.abort();
2072
- }
2073
- },
2074
- }).catch((error) => {
2075
- // If we aborted the connection attempt then don’t throw an error
2076
- // ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
2077
- if (error && error.code !== 'ATTEMPT_ABORTED') {
2078
- throw error;
2079
- }
2080
- });
2081
- return {
2082
- retryPromise,
2083
- cancelFunc: () => {
2084
- cancelAttempt = true;
2085
- },
2086
- };
2087
- };
2088
- const { retryPromise, cancelFunc } = abortableRetry();
2089
- this.cancelWebsocketRetry = cancelFunc;
2090
- return retryPromise;
2091
- }
2092
- createWebSocketConnection() {
2093
- return new Promise((resolve, reject) => {
2094
- if (this.webSocket) {
2095
- this.webSocket.close();
2096
- this.webSocket = null;
2159
+ // Reply
2160
+ if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
2161
+ if (this.broadcasted) {
2162
+ // TODO: Some weird TypeScript error
2163
+ // @ts-ignore
2164
+ provider.broadcast(OutgoingMessage, { encoder: message.encoder });
2097
2165
  }
2098
- // Init the WebSocket connection
2099
- const ws = new this.configuration.WebSocketPolyfill(this.url);
2100
- ws.binaryType = 'arraybuffer';
2101
- ws.onmessage = (payload) => this.emit('message', payload);
2102
- ws.onclose = (payload) => this.emit('close', { event: payload });
2103
- ws.onopen = (payload) => this.emit('open', payload);
2104
- ws.onerror = (err) => {
2105
- reject(err);
2106
- };
2107
- this.webSocket = ws;
2108
- // Reset the status
2109
- this.status = WebSocketStatus.Connecting;
2110
- this.emit('status', { status: WebSocketStatus.Connecting });
2111
- // Store resolve/reject for later use
2112
- this.connectionAttempt = {
2113
- resolve,
2114
- reject,
2115
- };
2116
- });
2166
+ else {
2167
+ // TODO: Some weird TypeScript error
2168
+ // @ts-ignore
2169
+ provider.send(OutgoingMessage, { encoder: message.encoder });
2170
+ }
2171
+ }
2117
2172
  }
2118
- onMessage(event) {
2119
- this.resolveConnectionAttempt();
2120
- this.lastMessageReceived = getUnixTime();
2173
+ applySyncMessage(provider, emitSynced) {
2174
+ const { message } = this;
2175
+ message.writeVarUint(MessageType.Sync);
2176
+ // Apply update
2177
+ const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
2178
+ // Synced once we receive Step2
2179
+ if (emitSynced && syncMessageType === messageYjsSyncStep2) {
2180
+ provider.synced = true;
2181
+ }
2121
2182
  }
2122
- resolveConnectionAttempt() {
2123
- if (this.connectionAttempt) {
2124
- this.connectionAttempt.resolve();
2125
- this.connectionAttempt = null;
2126
- this.status = WebSocketStatus.Connected;
2127
- this.emit('status', { status: WebSocketStatus.Connected });
2128
- this.emit('connect');
2183
+ applySyncStatusMessage(provider, applied) {
2184
+ if (applied) {
2185
+ provider.decrementUnsyncedChanges();
2129
2186
  }
2130
2187
  }
2131
- stopConnectionAttempt() {
2132
- this.connectionAttempt = null;
2188
+ applyAwarenessMessage(provider) {
2189
+ const { message } = this;
2190
+ applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
2133
2191
  }
2134
- rejectConnectionAttempt() {
2135
- var _a;
2136
- (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
2137
- this.connectionAttempt = null;
2192
+ applyAuthMessage(provider) {
2193
+ const { message } = this;
2194
+ readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
2138
2195
  }
2139
- checkConnection() {
2140
- var _a;
2141
- // Don’t check the connection when it’s not even established
2142
- if (this.status !== WebSocketStatus.Connected) {
2143
- return;
2144
- }
2145
- // Don’t close then connection while waiting for the first message
2146
- if (!this.lastMessageReceived) {
2147
- return;
2148
- }
2149
- // Don’t close the connection when a message was received recently
2150
- if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
2151
- return;
2152
- }
2153
- // No message received in a long time, not even your own
2154
- // Awareness updates, which are updated every 15 seconds.
2155
- (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
2196
+ applyQueryAwarenessMessage(provider) {
2197
+ const { message } = this;
2198
+ message.writeVarUint(MessageType.Awareness);
2199
+ message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
2156
2200
  }
2157
- registerEventListeners() {
2158
- if (typeof window === 'undefined') {
2159
- return;
2160
- }
2161
- window.addEventListener('online', this.boundConnect);
2201
+ }
2202
+
2203
+ class MessageSender {
2204
+ constructor(Message, args = {}) {
2205
+ this.message = new Message();
2206
+ this.encoder = this.message.get(args);
2162
2207
  }
2163
- // Ensure that the URL always ends with /
2164
- get serverUrl() {
2165
- while (this.configuration.url[this.configuration.url.length - 1] === '/') {
2166
- return this.configuration.url.slice(0, this.configuration.url.length - 1);
2167
- }
2168
- return this.configuration.url;
2208
+ create() {
2209
+ return toUint8Array(this.encoder);
2169
2210
  }
2170
- get url() {
2171
- const encodedParams = encodeQueryParams(this.configuration.parameters);
2172
- return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
2211
+ send(webSocket) {
2212
+ webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
2173
2213
  }
2174
- disconnect() {
2175
- this.shouldConnect = false;
2176
- if (this.webSocket === null) {
2177
- return;
2178
- }
2179
- try {
2180
- this.webSocket.close();
2181
- }
2182
- catch {
2183
- //
2184
- }
2214
+ broadcast(channel) {
2215
+ publish(channel, this.create());
2185
2216
  }
2186
- send(message) {
2187
- var _a;
2188
- if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === WsReadyStates.Open) {
2189
- this.webSocket.send(message);
2190
- }
2217
+ }
2218
+
2219
+ class AuthenticationMessage extends OutgoingMessage {
2220
+ constructor() {
2221
+ super(...arguments);
2222
+ this.type = MessageType.Auth;
2223
+ this.description = 'Authentication';
2191
2224
  }
2192
- onClose({ event }) {
2193
- this.webSocket = null;
2194
- if (this.status === WebSocketStatus.Connected) {
2195
- this.status = WebSocketStatus.Disconnected;
2196
- this.emit('status', { status: WebSocketStatus.Disconnected });
2197
- this.emit('disconnect', { event });
2198
- }
2199
- if (event.code === Unauthorized.code) {
2200
- if (event.reason === Unauthorized.reason) {
2201
- 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.');
2202
- }
2203
- else {
2204
- console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
2205
- }
2206
- this.shouldConnect = false;
2207
- }
2208
- if (event.code === Forbidden.code) {
2209
- if (!this.configuration.quiet) {
2210
- console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
2211
- return; // TODO REMOVE ME
2212
- }
2213
- }
2214
- if (event.code === MessageTooBig.code) {
2215
- console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
2216
- this.shouldConnect = false;
2225
+ get(args) {
2226
+ if (typeof args.token === 'undefined') {
2227
+ throw new Error('The authentication message requires `token` as an argument.');
2217
2228
  }
2218
- if (this.connectionAttempt) {
2219
- // That connection attempt failed.
2220
- this.rejectConnectionAttempt();
2229
+ writeVarString(this.encoder, args.documentName);
2230
+ writeVarUint(this.encoder, this.type);
2231
+ writeAuthentication(this.encoder, args.token);
2232
+ return this.encoder;
2233
+ }
2234
+ }
2235
+
2236
+ class AwarenessMessage extends OutgoingMessage {
2237
+ constructor() {
2238
+ super(...arguments);
2239
+ this.type = MessageType.Awareness;
2240
+ this.description = 'Awareness states update';
2241
+ }
2242
+ get(args) {
2243
+ if (typeof args.awareness === 'undefined') {
2244
+ throw new Error('The awareness message requires awareness as an argument');
2221
2245
  }
2222
- else if (this.shouldConnect) {
2223
- // The connection was closed by the server. Let’s just try again.
2224
- this.connect();
2246
+ if (typeof args.clients === 'undefined') {
2247
+ throw new Error('The awareness message requires clients as an argument');
2225
2248
  }
2226
- // If we’ll reconnect, we’re done for now.
2227
- if (this.shouldConnect) {
2228
- return;
2249
+ writeVarString(this.encoder, args.documentName);
2250
+ writeVarUint(this.encoder, this.type);
2251
+ let awarenessUpdate;
2252
+ if (args.states === undefined) {
2253
+ awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
2229
2254
  }
2230
- // The status is set correctly already.
2231
- if (this.status === WebSocketStatus.Disconnected) {
2232
- return;
2255
+ else {
2256
+ awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
2233
2257
  }
2234
- // Let’s update the connection status.
2235
- this.status = WebSocketStatus.Disconnected;
2236
- this.emit('status', { status: WebSocketStatus.Disconnected });
2237
- this.emit('disconnect', { event });
2258
+ writeVarUint8Array(this.encoder, awarenessUpdate);
2259
+ return this.encoder;
2238
2260
  }
2239
- destroy() {
2240
- this.emit('destroy');
2241
- if (this.intervals.forceSync) {
2242
- clearInterval(this.intervals.forceSync);
2243
- }
2244
- clearInterval(this.intervals.connectionChecker);
2245
- // If there is still a connection attempt outstanding then we should stop
2246
- // it before calling disconnect, otherwise it will be rejected in the onClose
2247
- // handler and trigger a retry
2248
- this.stopConnectionAttempt();
2249
- this.disconnect();
2250
- this.removeAllListeners();
2251
- if (typeof window === 'undefined') {
2252
- return;
2253
- }
2254
- window.removeEventListener('online', this.boundConnect);
2261
+ }
2262
+
2263
+ class CloseMessage extends OutgoingMessage {
2264
+ constructor() {
2265
+ super(...arguments);
2266
+ this.type = MessageType.CLOSE;
2267
+ this.description = 'Ask the server to close the connection';
2268
+ }
2269
+ get(args) {
2270
+ writeVarString(this.encoder, args.documentName);
2271
+ writeVarUint(this.encoder, this.type);
2272
+ return this.encoder;
2273
+ }
2274
+ }
2275
+
2276
+ class QueryAwarenessMessage extends OutgoingMessage {
2277
+ constructor() {
2278
+ super(...arguments);
2279
+ this.type = MessageType.QueryAwareness;
2280
+ this.description = 'Queries awareness states';
2281
+ }
2282
+ get(args) {
2283
+ console.log('queryAwareness: writing string docName', args.documentName);
2284
+ console.log(this.encoder.cpos);
2285
+ writeVarString(this.encoder, args.documentName);
2286
+ writeVarUint(this.encoder, this.type);
2287
+ return this.encoder;
2255
2288
  }
2256
2289
  }
2257
2290
 
@@ -2270,15 +2303,50 @@ class StatelessMessage extends OutgoingMessage {
2270
2303
  }
2271
2304
  }
2272
2305
 
2273
- class CloseMessage extends OutgoingMessage {
2306
+ class SyncStepOneMessage extends OutgoingMessage {
2274
2307
  constructor() {
2275
2308
  super(...arguments);
2276
- this.type = MessageType.CLOSE;
2277
- this.description = 'Ask the server to close the connection';
2309
+ this.type = MessageType.Sync;
2310
+ this.description = 'First sync step';
2311
+ }
2312
+ get(args) {
2313
+ if (typeof args.document === 'undefined') {
2314
+ throw new Error('The sync step one message requires document as an argument');
2315
+ }
2316
+ writeVarString(this.encoder, args.documentName);
2317
+ writeVarUint(this.encoder, this.type);
2318
+ writeSyncStep1(this.encoder, args.document);
2319
+ return this.encoder;
2320
+ }
2321
+ }
2322
+
2323
+ class SyncStepTwoMessage extends OutgoingMessage {
2324
+ constructor() {
2325
+ super(...arguments);
2326
+ this.type = MessageType.Sync;
2327
+ this.description = 'Second sync step';
2328
+ }
2329
+ get(args) {
2330
+ if (typeof args.document === 'undefined') {
2331
+ throw new Error('The sync step two message requires document as an argument');
2332
+ }
2333
+ writeVarString(this.encoder, args.documentName);
2334
+ writeVarUint(this.encoder, this.type);
2335
+ writeSyncStep2(this.encoder, args.document);
2336
+ return this.encoder;
2337
+ }
2338
+ }
2339
+
2340
+ class UpdateMessage extends OutgoingMessage {
2341
+ constructor() {
2342
+ super(...arguments);
2343
+ this.type = MessageType.Sync;
2344
+ this.description = 'A document update';
2278
2345
  }
2279
2346
  get(args) {
2280
2347
  writeVarString(this.encoder, args.documentName);
2281
2348
  writeVarUint(this.encoder, this.type);
2349
+ writeUpdate(this.encoder, args.update);
2282
2350
  return this.encoder;
2283
2351
  }
2284
2352
  }
@@ -2311,6 +2379,8 @@ class HocuspocusProvider extends EventEmitter {
2311
2379
  onAwarenessChange: () => null,
2312
2380
  onStateless: () => null,
2313
2381
  quiet: false,
2382
+ connect: true,
2383
+ preserveConnection: true,
2314
2384
  };
2315
2385
  this.subscribedToBroadcastChannel = false;
2316
2386
  this.isSynced = false;
@@ -2324,7 +2394,7 @@ class HocuspocusProvider extends EventEmitter {
2324
2394
  };
2325
2395
  this.isConnected = true;
2326
2396
  this.boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this);
2327
- this.boundBeforeUnload = this.beforeUnload.bind(this);
2397
+ this.boundPageUnload = this.pageUnload.bind(this);
2328
2398
  this.boundOnOpen = this.onOpen.bind(this);
2329
2399
  this.boundOnMessage = this.onMessage.bind(this);
2330
2400
  this.boundOnClose = this.onClose.bind(this);
@@ -2384,6 +2454,7 @@ class HocuspocusProvider extends EventEmitter {
2384
2454
  const websocketProviderConfig = configuration;
2385
2455
  this.configuration.websocketProvider = new HocuspocusProviderWebsocket({
2386
2456
  url: websocketProviderConfig.url,
2457
+ connect: websocketProviderConfig.connect,
2387
2458
  parameters: websocketProviderConfig.parameters,
2388
2459
  });
2389
2460
  }
@@ -2398,21 +2469,28 @@ class HocuspocusProvider extends EventEmitter {
2398
2469
  get hasUnsyncedChanges() {
2399
2470
  return this.unsyncedChanges > 0;
2400
2471
  }
2401
- updateUnsyncedChanges(unsyncedChanges = 0) {
2402
- this.unsyncedChanges += unsyncedChanges;
2472
+ incrementUnsyncedChanges() {
2473
+ this.unsyncedChanges += 1;
2474
+ this.emit('unsyncedChanges', this.unsyncedChanges);
2475
+ }
2476
+ decrementUnsyncedChanges() {
2477
+ this.unsyncedChanges -= 1;
2478
+ if (this.unsyncedChanges === 0) {
2479
+ this.synced = true;
2480
+ }
2403
2481
  this.emit('unsyncedChanges', this.unsyncedChanges);
2404
2482
  }
2405
2483
  forceSync() {
2406
2484
  this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
2407
2485
  }
2408
- beforeUnload() {
2486
+ pageUnload() {
2409
2487
  removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload');
2410
2488
  }
2411
2489
  registerEventListeners() {
2412
2490
  if (typeof window === 'undefined') {
2413
2491
  return;
2414
2492
  }
2415
- window.addEventListener('beforeunload', this.boundBeforeUnload);
2493
+ window.addEventListener('unload', this.boundPageUnload);
2416
2494
  }
2417
2495
  sendStateless(payload) {
2418
2496
  this.send(StatelessMessage, { documentName: this.configuration.name, payload });
@@ -2421,7 +2499,7 @@ class HocuspocusProvider extends EventEmitter {
2421
2499
  if (origin === this) {
2422
2500
  return;
2423
2501
  }
2424
- this.updateUnsyncedChanges(1);
2502
+ this.incrementUnsyncedChanges();
2425
2503
  this.send(UpdateMessage, { update, documentName: this.configuration.name }, true);
2426
2504
  }
2427
2505
  awarenessUpdateHandler({ added, updated, removed }, origin) {
@@ -2432,6 +2510,12 @@ class HocuspocusProvider extends EventEmitter {
2432
2510
  documentName: this.configuration.name,
2433
2511
  }, true);
2434
2512
  }
2513
+ /**
2514
+ * Indicates whether a first handshake with the server has been established
2515
+ *
2516
+ * Note: this does not mean all updates from the client have been persisted to the backend. For this,
2517
+ * use `hasUnsyncedChanges`.
2518
+ */
2435
2519
  get synced() {
2436
2520
  return this.isSynced;
2437
2521
  }
@@ -2439,9 +2523,6 @@ class HocuspocusProvider extends EventEmitter {
2439
2523
  if (this.isSynced === state) {
2440
2524
  return;
2441
2525
  }
2442
- if (state && this.unsyncedChanges > 0) {
2443
- this.updateUnsyncedChanges(-1 * this.unsyncedChanges);
2444
- }
2445
2526
  this.isSynced = state;
2446
2527
  this.emit('synced', { state });
2447
2528
  this.emit('sync', { state });
@@ -2459,6 +2540,9 @@ class HocuspocusProvider extends EventEmitter {
2459
2540
  disconnect() {
2460
2541
  this.disconnectBroadcastChannel();
2461
2542
  this.configuration.websocketProvider.detach(this);
2543
+ if (!this.configuration.preserveConnection) {
2544
+ this.configuration.websocketProvider.disconnect();
2545
+ }
2462
2546
  }
2463
2547
  async onOpen(event) {
2464
2548
  this.isAuthenticated = false;
@@ -2479,6 +2563,7 @@ class HocuspocusProvider extends EventEmitter {
2479
2563
  return this.configuration.token;
2480
2564
  }
2481
2565
  startSync() {
2566
+ this.incrementUnsyncedChanges();
2482
2567
  this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
2483
2568
  if (this.awareness.getLocalState() !== null) {
2484
2569
  this.send(AwarenessMessage, {
@@ -2489,8 +2574,9 @@ class HocuspocusProvider extends EventEmitter {
2489
2574
  }
2490
2575
  }
2491
2576
  send(message, args, broadcast = false) {
2492
- if (!this.isConnected)
2577
+ if (!this.isConnected) {
2493
2578
  return;
2579
+ }
2494
2580
  if (broadcast) {
2495
2581
  this.mux(() => { this.broadcast(message, args); });
2496
2582
  }
@@ -2506,7 +2592,7 @@ class HocuspocusProvider extends EventEmitter {
2506
2592
  }
2507
2593
  message.writeVarString(documentName);
2508
2594
  this.emit('message', { event, message: new IncomingMessage(event.data) });
2509
- new MessageReceiver(message).apply(this);
2595
+ new MessageReceiver(message).apply(this, true);
2510
2596
  }
2511
2597
  onClose(event) {
2512
2598
  this.isAuthenticated = false;
@@ -2542,7 +2628,7 @@ class HocuspocusProvider extends EventEmitter {
2542
2628
  if (typeof window === 'undefined') {
2543
2629
  return;
2544
2630
  }
2545
- window.removeEventListener('beforeunload', this.boundBeforeUnload);
2631
+ window.removeEventListener('unload', this.boundPageUnload);
2546
2632
  }
2547
2633
  permissionDeniedHandler(reason) {
2548
2634
  this.emit('authenticationFailed', { reason });