@hocuspocus/provider 2.2.2 → 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,103 +1580,461 @@ 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
- /**
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!
1602
- *
1603
- * stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
1604
- */
1605
-
1606
- const messageYjsSyncStep1 = 0;
1607
- const messageYjsSyncStep2 = 1;
1608
- const messageYjsUpdate = 2;
1609
-
1610
1583
  /**
1611
- * Create a sync step 1 message based on the state of the current shared document.
1584
+ * Utility module to work with urls.
1612
1585
  *
1613
- * @param {encoding.Encoder} encoder
1614
- * @param {Y.Doc} doc
1586
+ * @module url
1615
1587
  */
1616
- const writeSyncStep1 = (encoder, doc) => {
1617
- writeVarUint(encoder, messageYjsSyncStep1);
1618
- const sv = Y.encodeStateVector(doc);
1619
- writeVarUint8Array(encoder, sv);
1620
- };
1621
1588
 
1622
1589
  /**
1623
- * @param {encoding.Encoder} encoder
1624
- * @param {Y.Doc} doc
1625
- * @param {Uint8Array} [encodedStateVector]
1590
+ * @param {Object<string,string>} params
1591
+ * @return {string}
1626
1592
  */
1627
- const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
1628
- writeVarUint(encoder, messageYjsSyncStep2);
1629
- writeVarUint8Array(encoder, Y.encodeStateAsUpdate(doc, encodedStateVector));
1630
- };
1593
+ const encodeQueryParams = params =>
1594
+ map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
1631
1595
 
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));
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 = {}));
1612
+
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();
1697
+ }
1698
+ async onOpen(event) {
1699
+ this.receivedOnOpenPayload = event;
1700
+ }
1701
+ async onStatus(data) {
1702
+ this.receivedOnStatusPayload = data;
1703
+ }
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
+ }
1714
+ }
1715
+ detach(provider) {
1716
+ // tell the server to remove the listener
1717
+ }
1718
+ setConfiguration(configuration = {}) {
1719
+ this.configuration = { ...this.configuration, ...configuration };
1720
+ }
1721
+ async connect() {
1722
+ if (this.status === WebSocketStatus.Connected) {
1723
+ return;
1724
+ }
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;
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 = [];
1807
+ }
1808
+ }
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);
1846
+ }
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.');
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;
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 });
1921
+ }
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);
1938
+ }
1939
+ }
1940
+
1941
+ class IncomingMessage {
1942
+ constructor(data) {
1943
+ this.data = data;
1944
+ this.encoder = createEncoder();
1945
+ this.decoder = createDecoder(new Uint8Array(this.data));
1946
+ }
1947
+ readVarUint() {
1948
+ return readVarUint(this.decoder);
1949
+ }
1950
+ readVarString() {
1951
+ return readVarString(this.decoder);
1952
+ }
1953
+ readVarUint8Array() {
1954
+ return readVarUint8Array(this.decoder);
1955
+ }
1956
+ writeVarUint(type) {
1957
+ return writeVarUint(this.encoder, type);
1958
+ }
1959
+ writeVarString(string) {
1960
+ return writeVarString(this.encoder, string);
1961
+ }
1962
+ writeVarUint8Array(data) {
1963
+ return writeVarUint8Array(this.encoder, data);
1964
+ }
1965
+ length() {
1966
+ return length(this.encoder);
1967
+ }
1968
+ }
1969
+
1970
+ /**
1971
+ * @module sync-protocol
1972
+ */
1973
+
1974
+ /**
1975
+ * @typedef {Map<number, number>} StateMap
1976
+ */
1977
+
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
+ */
2002
+
2003
+ const messageYjsSyncStep1 = 0;
2004
+ const messageYjsSyncStep2 = 1;
2005
+ const messageYjsUpdate = 2;
2006
+
2007
+ /**
2008
+ * Create a sync step 1 message based on the state of the current shared document.
2009
+ *
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));
1641
2038
 
1642
2039
  /**
1643
2040
  * Read and apply Structs and then DeleteStore to a y instance.
@@ -1695,559 +2092,184 @@ const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
1695
2092
  throw new Error('Unknown message type')
1696
2093
  }
1697
2094
  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 = {}));
1709
- var WebSocketStatus;
1710
- (function (WebSocketStatus) {
1711
- WebSocketStatus["Connecting"] = "connecting";
1712
- WebSocketStatus["Connected"] = "connected";
1713
- WebSocketStatus["Disconnected"] = "disconnected";
1714
- })(WebSocketStatus || (WebSocketStatus = {}));
1715
-
1716
- class OutgoingMessage {
1717
- constructor() {
1718
- this.encoder = createEncoder();
1719
- }
1720
- get(args) {
1721
- return args.encoder;
1722
- }
1723
- toUint8Array() {
1724
- return toUint8Array(this.encoder);
1725
- }
1726
- }
1727
-
1728
- class MessageReceiver {
1729
- constructor(message) {
1730
- this.broadcasted = false;
1731
- this.message = message;
1732
- }
1733
- setBroadcasted(value) {
1734
- this.broadcasted = value;
1735
- return this;
1736
- }
1737
- apply(provider, emitSynced = true) {
1738
- const { message } = this;
1739
- const type = message.readVarUint();
1740
- const emptyMessageLength = message.length();
1741
- switch (type) {
1742
- case MessageType.Sync:
1743
- this.applySyncMessage(provider, emitSynced);
1744
- break;
1745
- case MessageType.Awareness:
1746
- this.applyAwarenessMessage(provider);
1747
- break;
1748
- case MessageType.Auth:
1749
- this.applyAuthMessage(provider);
1750
- break;
1751
- case MessageType.QueryAwareness:
1752
- this.applyQueryAwarenessMessage(provider);
1753
- break;
1754
- case MessageType.Stateless:
1755
- provider.receiveStateless(readVarString(message.decoder));
1756
- break;
1757
- default:
1758
- throw new Error(`Can’t apply message of unknown type: ${type}`);
1759
- }
1760
- // Reply
1761
- if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
1762
- if (this.broadcasted) {
1763
- // TODO: Some weird TypeScript error
1764
- // @ts-ignore
1765
- provider.broadcast(OutgoingMessage, { encoder: message.encoder });
1766
- }
1767
- else {
1768
- // TODO: Some weird TypeScript error
1769
- // @ts-ignore
1770
- provider.send(OutgoingMessage, { encoder: message.encoder });
1771
- }
1772
- }
1773
- }
1774
- applySyncMessage(provider, emitSynced) {
1775
- const { message } = this;
1776
- message.writeVarUint(MessageType.Sync);
1777
- // Apply update
1778
- const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
1779
- // Synced once we receive Step2
1780
- if (emitSynced && syncMessageType === messageYjsSyncStep2) {
1781
- provider.synced = true;
1782
- }
1783
- if (syncMessageType === messageYjsUpdate || syncMessageType === messageYjsSyncStep2) {
1784
- if (provider.unsyncedChanges > 0) {
1785
- provider.updateUnsyncedChanges(-1);
1786
- }
1787
- }
1788
- }
1789
- applyAwarenessMessage(provider) {
1790
- const { message } = this;
1791
- applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
1792
- }
1793
- applyAuthMessage(provider) {
1794
- const { message } = this;
1795
- readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
1796
- }
1797
- applyQueryAwarenessMessage(provider) {
1798
- const { message } = this;
1799
- message.writeVarUint(MessageType.Awareness);
1800
- message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
1801
- }
1802
- }
1803
-
1804
- class MessageSender {
1805
- constructor(Message, args = {}) {
1806
- this.message = new Message();
1807
- this.encoder = this.message.get(args);
1808
- }
1809
- create() {
1810
- return toUint8Array(this.encoder);
1811
- }
1812
- send(webSocket) {
1813
- webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
1814
- }
1815
- broadcast(channel) {
1816
- publish(channel, this.create());
1817
- }
1818
- }
1819
-
1820
- class SyncStepOneMessage extends OutgoingMessage {
1821
- constructor() {
1822
- super(...arguments);
1823
- this.type = MessageType.Sync;
1824
- this.description = 'First sync step';
1825
- }
1826
- get(args) {
1827
- if (typeof args.document === 'undefined') {
1828
- throw new Error('The sync step one message requires document as an argument');
1829
- }
1830
- writeVarString(this.encoder, args.documentName);
1831
- writeVarUint(this.encoder, this.type);
1832
- writeSyncStep1(this.encoder, args.document);
1833
- return this.encoder;
1834
- }
1835
- }
1836
-
1837
- class SyncStepTwoMessage extends OutgoingMessage {
1838
- constructor() {
1839
- super(...arguments);
1840
- this.type = MessageType.Sync;
1841
- this.description = 'Second sync step';
1842
- }
1843
- get(args) {
1844
- if (typeof args.document === 'undefined') {
1845
- throw new Error('The sync step two message requires document as an argument');
1846
- }
1847
- writeVarString(this.encoder, args.documentName);
1848
- writeVarUint(this.encoder, this.type);
1849
- writeSyncStep2(this.encoder, args.document);
1850
- return this.encoder;
1851
- }
1852
- }
1853
-
1854
- class QueryAwarenessMessage extends OutgoingMessage {
1855
- constructor() {
1856
- super(...arguments);
1857
- this.type = MessageType.QueryAwareness;
1858
- this.description = 'Queries awareness states';
1859
- }
1860
- get(args) {
1861
- console.log('queryAwareness: writing string docName', args.documentName);
1862
- console.log(this.encoder.cpos);
1863
- writeVarString(this.encoder, args.documentName);
1864
- writeVarUint(this.encoder, this.type);
1865
- return this.encoder;
1866
- }
1867
- }
1868
-
1869
- class AuthenticationMessage extends OutgoingMessage {
1870
- constructor() {
1871
- super(...arguments);
1872
- this.type = MessageType.Auth;
1873
- this.description = 'Authentication';
1874
- }
1875
- get(args) {
1876
- if (typeof args.token === 'undefined') {
1877
- throw new Error('The authentication message requires `token` as an argument.');
1878
- }
1879
- writeVarString(this.encoder, args.documentName);
1880
- writeVarUint(this.encoder, this.type);
1881
- writeAuthentication(this.encoder, args.token);
1882
- return this.encoder;
1883
- }
1884
- }
1885
-
1886
- class AwarenessMessage extends OutgoingMessage {
1887
- constructor() {
1888
- super(...arguments);
1889
- this.type = MessageType.Awareness;
1890
- this.description = 'Awareness states update';
1891
- }
1892
- get(args) {
1893
- if (typeof args.awareness === 'undefined') {
1894
- throw new Error('The awareness message requires awareness as an argument');
1895
- }
1896
- if (typeof args.clients === 'undefined') {
1897
- throw new Error('The awareness message requires clients as an argument');
1898
- }
1899
- writeVarString(this.encoder, args.documentName);
1900
- writeVarUint(this.encoder, this.type);
1901
- let awarenessUpdate;
1902
- if (args.states === undefined) {
1903
- awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
1904
- }
1905
- else {
1906
- awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
1907
- }
1908
- writeVarUint8Array(this.encoder, awarenessUpdate);
1909
- return this.encoder;
1910
- }
1911
- }
2095
+ };
1912
2096
 
1913
- class UpdateMessage extends OutgoingMessage {
2097
+ class OutgoingMessage {
1914
2098
  constructor() {
1915
- super(...arguments);
1916
- this.type = MessageType.Sync;
1917
- this.description = 'A document update';
2099
+ this.encoder = createEncoder();
1918
2100
  }
1919
2101
  get(args) {
1920
- writeVarString(this.encoder, args.documentName);
1921
- writeVarUint(this.encoder, this.type);
1922
- writeUpdate(this.encoder, args.update);
1923
- return this.encoder;
2102
+ return args.encoder;
2103
+ }
2104
+ toUint8Array() {
2105
+ return toUint8Array(this.encoder);
1924
2106
  }
1925
2107
  }
1926
2108
 
1927
- /**
1928
- * Utility module to work with urls.
1929
- *
1930
- * @module url
1931
- */
1932
-
1933
- /**
1934
- * @param {Object<string,string>} params
1935
- * @return {string}
1936
- */
1937
- const encodeQueryParams = params =>
1938
- map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
1939
-
1940
- class HocuspocusProviderWebsocket extends EventEmitter {
1941
- constructor(configuration) {
1942
- super();
1943
- this.configuration = {
1944
- url: '',
1945
- // @ts-ignore
1946
- document: undefined,
1947
- // @ts-ignore
1948
- awareness: undefined,
1949
- WebSocketPolyfill: undefined,
1950
- parameters: {},
1951
- connect: true,
1952
- broadcast: true,
1953
- forceSyncInterval: false,
1954
- // TODO: this should depend on awareness.outdatedTime
1955
- messageReconnectTimeout: 30000,
1956
- // 1 second
1957
- delay: 1000,
1958
- // instant
1959
- initialDelay: 0,
1960
- // double the delay each time
1961
- factor: 2,
1962
- // unlimited retries
1963
- maxAttempts: 0,
1964
- // wait at least 1 second
1965
- minDelay: 1000,
1966
- // at least every 30 seconds
1967
- maxDelay: 30000,
1968
- // randomize
1969
- jitter: true,
1970
- // retry forever
1971
- timeout: 0,
1972
- onOpen: () => null,
1973
- onConnect: () => null,
1974
- onMessage: () => null,
1975
- onOutgoingMessage: () => null,
1976
- onStatus: () => null,
1977
- onDisconnect: () => null,
1978
- onClose: () => null,
1979
- onDestroy: () => null,
1980
- onAwarenessUpdate: () => null,
1981
- onAwarenessChange: () => null,
1982
- quiet: false,
1983
- };
1984
- this.subscribedToBroadcastChannel = false;
1985
- this.webSocket = null;
1986
- this.shouldConnect = true;
1987
- this.status = WebSocketStatus.Disconnected;
1988
- this.lastMessageReceived = 0;
1989
- this.mux = createMutex();
1990
- this.intervals = {
1991
- forceSync: null,
1992
- connectionChecker: null,
1993
- };
1994
- this.connectionAttempt = null;
1995
- this.receivedOnOpenPayload = undefined;
1996
- this.receivedOnStatusPayload = undefined;
1997
- this.boundConnect = this.connect.bind(this);
1998
- this.setConfiguration(configuration);
1999
- this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
2000
- this.on('open', this.configuration.onOpen);
2001
- this.on('open', this.onOpen.bind(this));
2002
- this.on('connect', this.configuration.onConnect);
2003
- this.on('message', this.configuration.onMessage);
2004
- this.on('outgoingMessage', this.configuration.onOutgoingMessage);
2005
- this.on('status', this.configuration.onStatus);
2006
- this.on('status', this.onStatus.bind(this));
2007
- this.on('disconnect', this.configuration.onDisconnect);
2008
- this.on('close', this.configuration.onClose);
2009
- this.on('destroy', this.configuration.onDestroy);
2010
- this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
2011
- this.on('awarenessChange', this.configuration.onAwarenessChange);
2012
- this.on('close', this.onClose.bind(this));
2013
- this.on('message', this.onMessage.bind(this));
2014
- this.registerEventListeners();
2015
- this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
2016
- if (typeof configuration.connect !== 'undefined') {
2017
- this.shouldConnect = configuration.connect;
2018
- }
2019
- if (!this.shouldConnect) {
2020
- return;
2021
- }
2022
- this.connect();
2023
- }
2024
- async onOpen(event) {
2025
- this.receivedOnOpenPayload = event;
2109
+ class MessageReceiver {
2110
+ constructor(message) {
2111
+ this.broadcasted = false;
2112
+ this.message = message;
2026
2113
  }
2027
- async onStatus(data) {
2028
- this.receivedOnStatusPayload = data;
2114
+ setBroadcasted(value) {
2115
+ this.broadcasted = value;
2116
+ return this;
2029
2117
  }
2030
- attach(provider) {
2031
- if (this.receivedOnOpenPayload) {
2032
- provider.onOpen(this.receivedOnOpenPayload);
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}`);
2033
2143
  }
2034
- if (this.receivedOnStatusPayload) {
2035
- provider.onStatus(this.receivedOnStatusPayload);
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 });
2150
+ }
2151
+ else {
2152
+ // TODO: Some weird TypeScript error
2153
+ // @ts-ignore
2154
+ provider.send(OutgoingMessage, { encoder: message.encoder });
2155
+ }
2036
2156
  }
2037
2157
  }
2038
- detach(provider) {
2039
- // tell the server to remove the listener
2040
- }
2041
- setConfiguration(configuration = {}) {
2042
- this.configuration = { ...this.configuration, ...configuration };
2043
- }
2044
- async connect() {
2045
- if (this.status === WebSocketStatus.Connected) {
2046
- return;
2047
- }
2048
- // Always cancel any previously initiated connection retryer instances
2049
- if (this.cancelWebsocketRetry) {
2050
- this.cancelWebsocketRetry();
2051
- this.cancelWebsocketRetry = undefined;
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;
2052
2166
  }
2053
- this.shouldConnect = true;
2054
- const abortableRetry = () => {
2055
- let cancelAttempt = false;
2056
- const retryPromise = retry(this.createWebSocketConnection.bind(this), {
2057
- delay: this.configuration.delay,
2058
- initialDelay: this.configuration.initialDelay,
2059
- factor: this.configuration.factor,
2060
- maxAttempts: this.configuration.maxAttempts,
2061
- minDelay: this.configuration.minDelay,
2062
- maxDelay: this.configuration.maxDelay,
2063
- jitter: this.configuration.jitter,
2064
- timeout: this.configuration.timeout,
2065
- beforeAttempt: context => {
2066
- if (!this.shouldConnect || cancelAttempt) {
2067
- context.abort();
2068
- }
2069
- },
2070
- }).catch((error) => {
2071
- // If we aborted the connection attempt then don’t throw an error
2072
- // ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
2073
- if (error && error.code !== 'ATTEMPT_ABORTED') {
2074
- throw error;
2075
- }
2076
- });
2077
- return {
2078
- retryPromise,
2079
- cancelFunc: () => {
2080
- cancelAttempt = true;
2081
- },
2082
- };
2083
- };
2084
- const { retryPromise, cancelFunc } = abortableRetry();
2085
- this.cancelWebsocketRetry = cancelFunc;
2086
- return retryPromise;
2087
- }
2088
- createWebSocketConnection() {
2089
- return new Promise((resolve, reject) => {
2090
- if (this.webSocket) {
2091
- this.webSocket.close();
2092
- this.webSocket = null;
2093
- }
2094
- // Init the WebSocket connection
2095
- const ws = new this.configuration.WebSocketPolyfill(this.url);
2096
- ws.binaryType = 'arraybuffer';
2097
- ws.onmessage = (payload) => this.emit('message', payload);
2098
- ws.onclose = (payload) => this.emit('close', { event: payload });
2099
- ws.onopen = (payload) => this.emit('open', payload);
2100
- ws.onerror = (err) => {
2101
- reject(err);
2102
- };
2103
- this.webSocket = ws;
2104
- // Reset the status
2105
- this.status = WebSocketStatus.Connecting;
2106
- this.emit('status', { status: WebSocketStatus.Connecting });
2107
- // Store resolve/reject for later use
2108
- this.connectionAttempt = {
2109
- resolve,
2110
- reject,
2111
- };
2112
- });
2113
- }
2114
- onMessage(event) {
2115
- this.resolveConnectionAttempt();
2116
- this.lastMessageReceived = getUnixTime();
2117
- }
2118
- resolveConnectionAttempt() {
2119
- if (this.connectionAttempt) {
2120
- this.connectionAttempt.resolve();
2121
- this.connectionAttempt = null;
2122
- this.status = WebSocketStatus.Connected;
2123
- this.emit('status', { status: WebSocketStatus.Connected });
2124
- this.emit('connect');
2167
+ }
2168
+ applySyncStatusMessage(provider, applied) {
2169
+ if (applied) {
2170
+ provider.decrementUnsyncedChanges();
2125
2171
  }
2126
2172
  }
2127
- stopConnectionAttempt() {
2128
- this.connectionAttempt = null;
2173
+ applyAwarenessMessage(provider) {
2174
+ const { message } = this;
2175
+ applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
2129
2176
  }
2130
- rejectConnectionAttempt() {
2131
- var _a;
2132
- (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
2133
- this.connectionAttempt = null;
2177
+ applyAuthMessage(provider) {
2178
+ const { message } = this;
2179
+ readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
2134
2180
  }
2135
- checkConnection() {
2136
- var _a;
2137
- // Don’t check the connection when it’s not even established
2138
- if (this.status !== WebSocketStatus.Connected) {
2139
- return;
2140
- }
2141
- // Don’t close then connection while waiting for the first message
2142
- if (!this.lastMessageReceived) {
2143
- return;
2144
- }
2145
- // Don’t close the connection when a message was received recently
2146
- if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
2147
- return;
2148
- }
2149
- // No message received in a long time, not even your own
2150
- // Awareness updates, which are updated every 15 seconds.
2151
- (_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())));
2152
2185
  }
2153
- registerEventListeners() {
2154
- if (typeof window === 'undefined') {
2155
- return;
2156
- }
2157
- 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);
2158
2192
  }
2159
- // Ensure that the URL always ends with /
2160
- get serverUrl() {
2161
- while (this.configuration.url[this.configuration.url.length - 1] === '/') {
2162
- return this.configuration.url.slice(0, this.configuration.url.length - 1);
2163
- }
2164
- return this.configuration.url;
2193
+ create() {
2194
+ return toUint8Array(this.encoder);
2165
2195
  }
2166
- get url() {
2167
- const encodedParams = encodeQueryParams(this.configuration.parameters);
2168
- return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
2196
+ send(webSocket) {
2197
+ webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
2169
2198
  }
2170
- disconnect() {
2171
- this.shouldConnect = false;
2172
- if (this.webSocket === null) {
2173
- return;
2174
- }
2175
- try {
2176
- this.webSocket.close();
2177
- }
2178
- catch {
2179
- //
2180
- }
2199
+ broadcast(channel) {
2200
+ publish(channel, this.create());
2181
2201
  }
2182
- send(message) {
2183
- var _a;
2184
- if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === WsReadyStates.Open) {
2185
- this.webSocket.send(message);
2186
- }
2202
+ }
2203
+
2204
+ class AuthenticationMessage extends OutgoingMessage {
2205
+ constructor() {
2206
+ super(...arguments);
2207
+ this.type = MessageType.Auth;
2208
+ this.description = 'Authentication';
2187
2209
  }
2188
- onClose({ event }) {
2189
- this.webSocket = null;
2190
- if (this.status === WebSocketStatus.Connected) {
2191
- this.status = WebSocketStatus.Disconnected;
2192
- this.emit('status', { status: WebSocketStatus.Disconnected });
2193
- this.emit('disconnect', { event });
2194
- }
2195
- if (event.code === Unauthorized.code) {
2196
- if (event.reason === Unauthorized.reason) {
2197
- 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.');
2198
- }
2199
- else {
2200
- console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
2201
- }
2202
- this.shouldConnect = false;
2203
- }
2204
- if (event.code === Forbidden.code) {
2205
- if (!this.configuration.quiet) {
2206
- console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
2207
- return; // TODO REMOVE ME
2208
- }
2209
- }
2210
- if (event.code === MessageTooBig.code) {
2211
- console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
2212
- this.shouldConnect = false;
2210
+ get(args) {
2211
+ if (typeof args.token === 'undefined') {
2212
+ throw new Error('The authentication message requires `token` as an argument.');
2213
2213
  }
2214
- if (this.connectionAttempt) {
2215
- // That connection attempt failed.
2216
- 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');
2217
2230
  }
2218
- else if (this.shouldConnect) {
2219
- // The connection was closed by the server. Let’s just try again.
2220
- this.connect();
2231
+ if (typeof args.clients === 'undefined') {
2232
+ throw new Error('The awareness message requires clients as an argument');
2221
2233
  }
2222
- // If we’ll reconnect, we’re done for now.
2223
- if (this.shouldConnect) {
2224
- 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);
2225
2239
  }
2226
- // The status is set correctly already.
2227
- if (this.status === WebSocketStatus.Disconnected) {
2228
- return;
2240
+ else {
2241
+ awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
2229
2242
  }
2230
- // Let’s update the connection status.
2231
- this.status = WebSocketStatus.Disconnected;
2232
- this.emit('status', { status: WebSocketStatus.Disconnected });
2233
- this.emit('disconnect', { event });
2243
+ writeVarUint8Array(this.encoder, awarenessUpdate);
2244
+ return this.encoder;
2234
2245
  }
2235
- destroy() {
2236
- this.emit('destroy');
2237
- if (this.intervals.forceSync) {
2238
- clearInterval(this.intervals.forceSync);
2239
- }
2240
- clearInterval(this.intervals.connectionChecker);
2241
- // If there is still a connection attempt outstanding then we should stop
2242
- // it before calling disconnect, otherwise it will be rejected in the onClose
2243
- // handler and trigger a retry
2244
- this.stopConnectionAttempt();
2245
- this.disconnect();
2246
- this.removeAllListeners();
2247
- if (typeof window === 'undefined') {
2248
- return;
2249
- }
2250
- 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;
2251
2273
  }
2252
2274
  }
2253
2275
 
@@ -2266,15 +2288,50 @@ class StatelessMessage extends OutgoingMessage {
2266
2288
  }
2267
2289
  }
2268
2290
 
2269
- class CloseMessage extends OutgoingMessage {
2291
+ class SyncStepOneMessage extends OutgoingMessage {
2270
2292
  constructor() {
2271
2293
  super(...arguments);
2272
- this.type = MessageType.CLOSE;
2273
- 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';
2274
2330
  }
2275
2331
  get(args) {
2276
2332
  writeVarString(this.encoder, args.documentName);
2277
2333
  writeVarUint(this.encoder, this.type);
2334
+ writeUpdate(this.encoder, args.update);
2278
2335
  return this.encoder;
2279
2336
  }
2280
2337
  }
@@ -2307,6 +2364,8 @@ class HocuspocusProvider extends EventEmitter {
2307
2364
  onAwarenessChange: () => null,
2308
2365
  onStateless: () => null,
2309
2366
  quiet: false,
2367
+ connect: true,
2368
+ preserveConnection: true,
2310
2369
  };
2311
2370
  this.subscribedToBroadcastChannel = false;
2312
2371
  this.isSynced = false;
@@ -2320,7 +2379,7 @@ class HocuspocusProvider extends EventEmitter {
2320
2379
  };
2321
2380
  this.isConnected = true;
2322
2381
  this.boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this);
2323
- this.boundBeforeUnload = this.beforeUnload.bind(this);
2382
+ this.boundPageUnload = this.pageUnload.bind(this);
2324
2383
  this.boundOnOpen = this.onOpen.bind(this);
2325
2384
  this.boundOnMessage = this.onMessage.bind(this);
2326
2385
  this.boundOnClose = this.onClose.bind(this);
@@ -2380,6 +2439,7 @@ class HocuspocusProvider extends EventEmitter {
2380
2439
  const websocketProviderConfig = configuration;
2381
2440
  this.configuration.websocketProvider = new HocuspocusProviderWebsocket({
2382
2441
  url: websocketProviderConfig.url,
2442
+ connect: websocketProviderConfig.connect,
2383
2443
  parameters: websocketProviderConfig.parameters,
2384
2444
  });
2385
2445
  }
@@ -2394,21 +2454,28 @@ class HocuspocusProvider extends EventEmitter {
2394
2454
  get hasUnsyncedChanges() {
2395
2455
  return this.unsyncedChanges > 0;
2396
2456
  }
2397
- updateUnsyncedChanges(unsyncedChanges = 0) {
2398
- 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
+ }
2399
2466
  this.emit('unsyncedChanges', this.unsyncedChanges);
2400
2467
  }
2401
2468
  forceSync() {
2402
2469
  this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
2403
2470
  }
2404
- beforeUnload() {
2471
+ pageUnload() {
2405
2472
  removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload');
2406
2473
  }
2407
2474
  registerEventListeners() {
2408
2475
  if (typeof window === 'undefined') {
2409
2476
  return;
2410
2477
  }
2411
- window.addEventListener('beforeunload', this.boundBeforeUnload);
2478
+ window.addEventListener('unload', this.boundPageUnload);
2412
2479
  }
2413
2480
  sendStateless(payload) {
2414
2481
  this.send(StatelessMessage, { documentName: this.configuration.name, payload });
@@ -2417,7 +2484,7 @@ class HocuspocusProvider extends EventEmitter {
2417
2484
  if (origin === this) {
2418
2485
  return;
2419
2486
  }
2420
- this.updateUnsyncedChanges(1);
2487
+ this.incrementUnsyncedChanges();
2421
2488
  this.send(UpdateMessage, { update, documentName: this.configuration.name }, true);
2422
2489
  }
2423
2490
  awarenessUpdateHandler({ added, updated, removed }, origin) {
@@ -2428,6 +2495,12 @@ class HocuspocusProvider extends EventEmitter {
2428
2495
  documentName: this.configuration.name,
2429
2496
  }, true);
2430
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
+ */
2431
2504
  get synced() {
2432
2505
  return this.isSynced;
2433
2506
  }
@@ -2435,9 +2508,6 @@ class HocuspocusProvider extends EventEmitter {
2435
2508
  if (this.isSynced === state) {
2436
2509
  return;
2437
2510
  }
2438
- if (state && this.unsyncedChanges > 0) {
2439
- this.updateUnsyncedChanges(-1 * this.unsyncedChanges);
2440
- }
2441
2511
  this.isSynced = state;
2442
2512
  this.emit('synced', { state });
2443
2513
  this.emit('sync', { state });
@@ -2455,6 +2525,9 @@ class HocuspocusProvider extends EventEmitter {
2455
2525
  disconnect() {
2456
2526
  this.disconnectBroadcastChannel();
2457
2527
  this.configuration.websocketProvider.detach(this);
2528
+ if (!this.configuration.preserveConnection) {
2529
+ this.configuration.websocketProvider.disconnect();
2530
+ }
2458
2531
  }
2459
2532
  async onOpen(event) {
2460
2533
  this.isAuthenticated = false;
@@ -2475,6 +2548,7 @@ class HocuspocusProvider extends EventEmitter {
2475
2548
  return this.configuration.token;
2476
2549
  }
2477
2550
  startSync() {
2551
+ this.incrementUnsyncedChanges();
2478
2552
  this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
2479
2553
  if (this.awareness.getLocalState() !== null) {
2480
2554
  this.send(AwarenessMessage, {
@@ -2485,8 +2559,9 @@ class HocuspocusProvider extends EventEmitter {
2485
2559
  }
2486
2560
  }
2487
2561
  send(message, args, broadcast = false) {
2488
- if (!this.isConnected)
2562
+ if (!this.isConnected) {
2489
2563
  return;
2564
+ }
2490
2565
  if (broadcast) {
2491
2566
  this.mux(() => { this.broadcast(message, args); });
2492
2567
  }
@@ -2502,7 +2577,7 @@ class HocuspocusProvider extends EventEmitter {
2502
2577
  }
2503
2578
  message.writeVarString(documentName);
2504
2579
  this.emit('message', { event, message: new IncomingMessage(event.data) });
2505
- new MessageReceiver(message).apply(this);
2580
+ new MessageReceiver(message).apply(this, true);
2506
2581
  }
2507
2582
  onClose(event) {
2508
2583
  this.isAuthenticated = false;
@@ -2538,7 +2613,7 @@ class HocuspocusProvider extends EventEmitter {
2538
2613
  if (typeof window === 'undefined') {
2539
2614
  return;
2540
2615
  }
2541
- window.removeEventListener('beforeunload', this.boundBeforeUnload);
2616
+ window.removeEventListener('unload', this.boundPageUnload);
2542
2617
  }
2543
2618
  permissionDeniedHandler(reason) {
2544
2619
  this.emit('authenticationFailed', { reason });
@@ -2619,6 +2694,39 @@ class TiptapCollabProvider extends HocuspocusProvider {
2619
2694
  configuration.token = 'notoken'; // need to send a token anyway (which will be ignored)
2620
2695
  }
2621
2696
  super(configuration);
2697
+ this.tiptapCollabConfigurationPrefix = '__tiptapcollab__';
2698
+ }
2699
+ createVersion(name) {
2700
+ console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
2701
+ return this.sendStateless(JSON.stringify({ action: 'version.create', name }));
2702
+ }
2703
+ revertToVersion(targetVersion) {
2704
+ console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
2705
+ return this.sendStateless(JSON.stringify({ action: 'version.revert', version: targetVersion }));
2706
+ }
2707
+ getVersions() {
2708
+ console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
2709
+ return this.configuration.document.getArray(`${this.tiptapCollabConfigurationPrefix}versions`).toArray();
2710
+ }
2711
+ watchVersions(callback) {
2712
+ console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
2713
+ return this.configuration.document.getArray('__tiptapcollab__versions').observe(callback);
2714
+ }
2715
+ unwatchVersions(callback) {
2716
+ console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
2717
+ return this.configuration.document.getArray('__tiptapcollab__versions').unobserve(callback);
2718
+ }
2719
+ isAutoVersioning() {
2720
+ console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
2721
+ return !!this.configuration.document.getMap(`${this.tiptapCollabConfigurationPrefix}config`).get('autoVersioning');
2722
+ }
2723
+ enableAutoVersioning() {
2724
+ console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
2725
+ return this.configuration.document.getMap(`${this.tiptapCollabConfigurationPrefix}config`).set('autoVersioning', 1);
2726
+ }
2727
+ disableAutoVersioning() {
2728
+ console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
2729
+ return this.configuration.document.getMap(`${this.tiptapCollabConfigurationPrefix}config`).set('autoVersioning', 0);
2622
2730
  }
2623
2731
  }
2624
2732