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