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