@hocuspocus/provider 2.0.0-alpha.1 → 2.0.0-beta.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
1
  import * as Y from 'yjs';
2
- import { readAuthMessage, writeAuthentication, awarenessStatesToArray, WsReadyStates, Unauthorized, Forbidden } from '@hocuspocus/common';
2
+ import { readAuthMessage, writeAuthentication, WsReadyStates, Unauthorized, Forbidden, awarenessStatesToArray } from '@hocuspocus/common';
3
3
  import { retry } from '@lifeomic/attempt';
4
4
 
5
5
  /**
@@ -1742,151 +1742,477 @@ class UpdateMessage extends OutgoingMessage {
1742
1742
  }
1743
1743
  }
1744
1744
 
1745
- class StatelessMessage extends OutgoingMessage {
1746
- constructor() {
1747
- super(...arguments);
1748
- this.type = MessageType.Stateless;
1749
- this.description = 'A stateless message';
1750
- }
1751
- get(args) {
1752
- var _a;
1753
- writeVarString(this.encoder, args.documentName);
1754
- writeVarUint(this.encoder, this.type);
1755
- writeVarString(this.encoder, (_a = args.payload) !== null && _a !== void 0 ? _a : '');
1756
- return this.encoder;
1757
- }
1758
- }
1745
+ /**
1746
+ * Utility module to work with urls.
1747
+ *
1748
+ * @module url
1749
+ */
1759
1750
 
1760
- class CloseMessage extends OutgoingMessage {
1761
- constructor() {
1762
- super(...arguments);
1763
- this.type = MessageType.CLOSE;
1764
- this.description = 'Ask the server to close the connection';
1765
- }
1766
- get(args) {
1767
- writeVarString(this.encoder, args.documentName);
1768
- writeVarUint(this.encoder, this.type);
1769
- return this.encoder;
1770
- }
1771
- }
1751
+ /**
1752
+ * @param {Object<string,string>} params
1753
+ * @return {string}
1754
+ */
1755
+ const encodeQueryParams = params =>
1756
+ map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
1772
1757
 
1773
- class HocuspocusProvider extends EventEmitter {
1758
+ class HocuspocusProviderWebsocket extends EventEmitter {
1774
1759
  constructor(configuration) {
1775
1760
  super();
1776
1761
  this.configuration = {
1777
- name: '',
1762
+ url: '',
1778
1763
  // @ts-ignore
1779
1764
  document: undefined,
1780
1765
  // @ts-ignore
1781
1766
  awareness: undefined,
1782
- token: null,
1767
+ WebSocketPolyfill: undefined,
1783
1768
  parameters: {},
1769
+ connect: true,
1784
1770
  broadcast: true,
1785
1771
  forceSyncInterval: false,
1786
- onAuthenticated: () => null,
1787
- onAuthenticationFailed: () => null,
1772
+ // TODO: this should depend on awareness.outdatedTime
1773
+ messageReconnectTimeout: 30000,
1774
+ // 1 second
1775
+ delay: 1000,
1776
+ // instant
1777
+ initialDelay: 0,
1778
+ // double the delay each time
1779
+ factor: 2,
1780
+ // unlimited retries
1781
+ maxAttempts: 0,
1782
+ // wait at least 1 second
1783
+ minDelay: 1000,
1784
+ // at least every 30 seconds
1785
+ maxDelay: 30000,
1786
+ // randomize
1787
+ jitter: true,
1788
+ // retry forever
1789
+ timeout: 0,
1788
1790
  onOpen: () => null,
1789
1791
  onConnect: () => null,
1790
1792
  onMessage: () => null,
1791
1793
  onOutgoingMessage: () => null,
1792
1794
  onStatus: () => null,
1793
- onSynced: () => null,
1794
1795
  onDisconnect: () => null,
1795
1796
  onClose: () => null,
1796
1797
  onDestroy: () => null,
1797
1798
  onAwarenessUpdate: () => null,
1798
1799
  onAwarenessChange: () => null,
1799
- onStateless: () => null,
1800
1800
  quiet: false,
1801
1801
  };
1802
1802
  this.subscribedToBroadcastChannel = false;
1803
- this.isSynced = false;
1804
- this.unsyncedChanges = 0;
1803
+ this.webSocket = null;
1804
+ this.shouldConnect = true;
1805
1805
  this.status = WebSocketStatus.Disconnected;
1806
- this.isAuthenticated = false;
1806
+ this.lastMessageReceived = 0;
1807
1807
  this.mux = createMutex();
1808
1808
  this.intervals = {
1809
1809
  forceSync: null,
1810
+ connectionChecker: null,
1810
1811
  };
1811
- this.boundBeforeUnload = this.beforeUnload.bind(this);
1812
- this.boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this);
1812
+ this.connectionAttempt = null;
1813
+ this.receivedOnOpenPayload = undefined;
1814
+ this.receivedOnStatusPayload = undefined;
1815
+ this.boundConnect = this.connect.bind(this);
1813
1816
  this.setConfiguration(configuration);
1814
- this.configuration.document = configuration.document ? configuration.document : new Y.Doc();
1815
- this.configuration.awareness = configuration.awareness ? configuration.awareness : new Awareness(this.document);
1817
+ this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
1816
1818
  this.on('open', this.configuration.onOpen);
1819
+ this.on('open', this.onOpen.bind(this));
1820
+ this.on('connect', this.configuration.onConnect);
1817
1821
  this.on('message', this.configuration.onMessage);
1818
1822
  this.on('outgoingMessage', this.configuration.onOutgoingMessage);
1819
- this.on('synced', this.configuration.onSynced);
1823
+ this.on('status', this.configuration.onStatus);
1824
+ this.on('status', this.onStatus.bind(this));
1825
+ this.on('disconnect', this.configuration.onDisconnect);
1826
+ this.on('close', this.configuration.onClose);
1820
1827
  this.on('destroy', this.configuration.onDestroy);
1821
1828
  this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
1822
1829
  this.on('awarenessChange', this.configuration.onAwarenessChange);
1823
- this.on('stateless', this.configuration.onStateless);
1824
- this.on('authenticated', this.configuration.onAuthenticated);
1825
- this.on('authenticationFailed', this.configuration.onAuthenticationFailed);
1826
- this.configuration.websocketProvider.on('connect', this.configuration.onConnect);
1827
- this.configuration.websocketProvider.on('connect', (e) => this.emit('connect', e));
1828
- this.configuration.websocketProvider.on('open', this.onOpen.bind(this));
1829
- this.configuration.websocketProvider.on('open', (e) => this.emit('open', e));
1830
- this.configuration.websocketProvider.on('message', this.onMessage.bind(this));
1831
- this.configuration.websocketProvider.on('close', this.onClose.bind(this));
1832
- this.configuration.websocketProvider.on('close', this.configuration.onClose);
1833
- this.configuration.websocketProvider.on('close', (e) => this.emit('close', e));
1834
- this.configuration.websocketProvider.on('status', this.onStatus.bind(this));
1835
- this.configuration.websocketProvider.on('disconnect', this.configuration.onDisconnect);
1836
- this.configuration.websocketProvider.on('disconnect', (e) => this.emit('disconnect', e));
1837
- this.configuration.websocketProvider.on('destroy', this.configuration.onDestroy);
1838
- this.configuration.websocketProvider.on('destroy', (e) => this.emit('destroy', e));
1839
- this.awareness.on('update', () => {
1840
- this.emit('awarenessUpdate', { states: awarenessStatesToArray(this.awareness.getStates()) });
1841
- });
1842
- this.awareness.on('change', () => {
1843
- this.emit('awarenessChange', { states: awarenessStatesToArray(this.awareness.getStates()) });
1844
- });
1845
- this.document.on('update', this.documentUpdateHandler.bind(this));
1846
- this.awareness.on('update', this.awarenessUpdateHandler.bind(this));
1830
+ this.on('close', this.onClose.bind(this));
1831
+ this.on('message', this.onMessage.bind(this));
1847
1832
  this.registerEventListeners();
1848
- if (this.configuration.forceSyncInterval) {
1849
- this.intervals.forceSync = setInterval(this.forceSync.bind(this), this.configuration.forceSyncInterval);
1833
+ this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
1834
+ if (typeof configuration.connect !== 'undefined') {
1835
+ this.shouldConnect = configuration.connect;
1850
1836
  }
1851
- this.configuration.websocketProvider.attach(this);
1837
+ if (!this.shouldConnect) {
1838
+ return;
1839
+ }
1840
+ this.connect();
1852
1841
  }
1853
- onStatus({ status }) {
1854
- this.status = status;
1855
- this.configuration.onStatus({ status });
1856
- this.emit('status', { status });
1842
+ async onOpen(event) {
1843
+ this.receivedOnOpenPayload = event;
1844
+ }
1845
+ async onStatus(data) {
1846
+ this.receivedOnStatusPayload = data;
1847
+ }
1848
+ attach(provider) {
1849
+ if (this.receivedOnOpenPayload) {
1850
+ provider.onOpen(this.receivedOnOpenPayload);
1851
+ }
1852
+ if (this.receivedOnStatusPayload) {
1853
+ provider.onStatus(this.receivedOnStatusPayload);
1854
+ }
1855
+ }
1856
+ detach(provider) {
1857
+ // tell the server to remove the listener
1857
1858
  }
1858
1859
  setConfiguration(configuration = {}) {
1859
1860
  this.configuration = { ...this.configuration, ...configuration };
1860
1861
  }
1861
- get document() {
1862
- return this.configuration.document;
1862
+ async connect() {
1863
+ if (this.status === WebSocketStatus.Connected) {
1864
+ return;
1865
+ }
1866
+ // Always cancel any previously initiated connection retryer instances
1867
+ if (this.cancelWebsocketRetry) {
1868
+ this.cancelWebsocketRetry();
1869
+ this.cancelWebsocketRetry = undefined;
1870
+ }
1871
+ this.shouldConnect = true;
1872
+ const abortableRetry = () => {
1873
+ let cancelAttempt = false;
1874
+ const retryPromise = retry(this.createWebSocketConnection.bind(this), {
1875
+ delay: this.configuration.delay,
1876
+ initialDelay: this.configuration.initialDelay,
1877
+ factor: this.configuration.factor,
1878
+ maxAttempts: this.configuration.maxAttempts,
1879
+ minDelay: this.configuration.minDelay,
1880
+ maxDelay: this.configuration.maxDelay,
1881
+ jitter: this.configuration.jitter,
1882
+ timeout: this.configuration.timeout,
1883
+ beforeAttempt: context => {
1884
+ if (!this.shouldConnect || cancelAttempt) {
1885
+ context.abort();
1886
+ }
1887
+ },
1888
+ }).catch((error) => {
1889
+ // If we aborted the connection attempt then don’t throw an error
1890
+ // ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
1891
+ if (error && error.code !== 'ATTEMPT_ABORTED') {
1892
+ throw error;
1893
+ }
1894
+ });
1895
+ return {
1896
+ retryPromise,
1897
+ cancelFunc: () => {
1898
+ cancelAttempt = true;
1899
+ },
1900
+ };
1901
+ };
1902
+ const { retryPromise, cancelFunc } = abortableRetry();
1903
+ this.cancelWebsocketRetry = cancelFunc;
1904
+ return retryPromise;
1863
1905
  }
1864
- get awareness() {
1865
- return this.configuration.awareness;
1906
+ createWebSocketConnection() {
1907
+ return new Promise((resolve, reject) => {
1908
+ if (this.webSocket) {
1909
+ this.webSocket.close();
1910
+ this.webSocket = null;
1911
+ }
1912
+ // Init the WebSocket connection
1913
+ const ws = new this.configuration.WebSocketPolyfill(this.url);
1914
+ ws.binaryType = 'arraybuffer';
1915
+ ws.onmessage = (payload) => this.emit('message', payload);
1916
+ ws.onclose = (payload) => this.emit('close', { event: payload });
1917
+ ws.onopen = (payload) => this.emit('open', payload);
1918
+ ws.onerror = (err) => {
1919
+ reject(err);
1920
+ };
1921
+ this.webSocket = ws;
1922
+ // Reset the status
1923
+ this.status = WebSocketStatus.Connecting;
1924
+ this.emit('status', { status: WebSocketStatus.Connecting });
1925
+ // Store resolve/reject for later use
1926
+ this.connectionAttempt = {
1927
+ resolve,
1928
+ reject,
1929
+ };
1930
+ });
1866
1931
  }
1867
- get hasUnsyncedChanges() {
1868
- return this.unsyncedChanges > 0;
1932
+ onMessage(event) {
1933
+ this.resolveConnectionAttempt();
1869
1934
  }
1870
- forceSync() {
1871
- this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
1935
+ resolveConnectionAttempt() {
1936
+ if (this.connectionAttempt) {
1937
+ this.connectionAttempt.resolve();
1938
+ this.connectionAttempt = null;
1939
+ this.status = WebSocketStatus.Connected;
1940
+ this.emit('status', { status: WebSocketStatus.Connected });
1941
+ this.emit('connect');
1942
+ }
1872
1943
  }
1873
- beforeUnload() {
1874
- removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload');
1944
+ stopConnectionAttempt() {
1945
+ this.connectionAttempt = null;
1875
1946
  }
1876
- registerEventListeners() {
1877
- if (typeof window === 'undefined') {
1947
+ rejectConnectionAttempt() {
1948
+ var _a;
1949
+ (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
1950
+ this.connectionAttempt = null;
1951
+ }
1952
+ checkConnection() {
1953
+ var _a;
1954
+ // Don’t check the connection when it’s not even established
1955
+ if (this.status !== WebSocketStatus.Connected) {
1878
1956
  return;
1879
1957
  }
1880
- window.addEventListener('beforeunload', this.boundBeforeUnload);
1881
- }
1882
- sendStateless(payload) {
1883
- this.send(StatelessMessage, { documentName: this.configuration.name, payload });
1884
- }
1885
- documentUpdateHandler(update, origin) {
1886
- if (origin === this) {
1958
+ // Don’t close then connection while waiting for the first message
1959
+ if (!this.lastMessageReceived) {
1887
1960
  return;
1888
1961
  }
1889
- this.unsyncedChanges += 1;
1962
+ // Don’t close the connection when a message was received recently
1963
+ if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
1964
+ return;
1965
+ }
1966
+ // No message received in a long time, not even your own
1967
+ // Awareness updates, which are updated every 15 seconds.
1968
+ (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
1969
+ }
1970
+ registerEventListeners() {
1971
+ if (typeof window === 'undefined') {
1972
+ return;
1973
+ }
1974
+ window.addEventListener('online', this.boundConnect);
1975
+ }
1976
+ // Ensure that the URL always ends with /
1977
+ get serverUrl() {
1978
+ while (this.configuration.url[this.configuration.url.length - 1] === '/') {
1979
+ return this.configuration.url.slice(0, this.configuration.url.length - 1);
1980
+ }
1981
+ return this.configuration.url;
1982
+ }
1983
+ get url() {
1984
+ const encodedParams = encodeQueryParams(this.configuration.parameters);
1985
+ return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
1986
+ }
1987
+ disconnect() {
1988
+ this.shouldConnect = false;
1989
+ if (this.webSocket === null) {
1990
+ return;
1991
+ }
1992
+ try {
1993
+ this.webSocket.close();
1994
+ }
1995
+ catch {
1996
+ //
1997
+ }
1998
+ }
1999
+ send(message) {
2000
+ var _a;
2001
+ if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === WsReadyStates.Open) {
2002
+ this.webSocket.send(message);
2003
+ }
2004
+ }
2005
+ onClose({ event }) {
2006
+ this.webSocket = null;
2007
+ if (this.status === WebSocketStatus.Connected) {
2008
+ this.status = WebSocketStatus.Disconnected;
2009
+ this.emit('status', { status: WebSocketStatus.Disconnected });
2010
+ this.emit('disconnect', { event });
2011
+ }
2012
+ if (event.code === Unauthorized.code) {
2013
+ if (event.reason === Unauthorized.reason) {
2014
+ 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.');
2015
+ }
2016
+ else {
2017
+ console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
2018
+ }
2019
+ this.shouldConnect = false;
2020
+ }
2021
+ if (event.code === Forbidden.code) {
2022
+ if (!this.configuration.quiet) {
2023
+ console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
2024
+ return; // TODO REMOVE ME
2025
+ }
2026
+ }
2027
+ if (this.connectionAttempt) {
2028
+ // That connection attempt failed.
2029
+ this.rejectConnectionAttempt();
2030
+ }
2031
+ else if (this.shouldConnect) {
2032
+ // The connection was closed by the server. Let’s just try again.
2033
+ this.connect();
2034
+ }
2035
+ // If we’ll reconnect, we’re done for now.
2036
+ if (this.shouldConnect) {
2037
+ return;
2038
+ }
2039
+ // The status is set correctly already.
2040
+ if (this.status === WebSocketStatus.Disconnected) {
2041
+ return;
2042
+ }
2043
+ // Let’s update the connection status.
2044
+ this.status = WebSocketStatus.Disconnected;
2045
+ this.emit('status', { status: WebSocketStatus.Disconnected });
2046
+ this.emit('disconnect', { event });
2047
+ }
2048
+ destroy() {
2049
+ this.emit('destroy');
2050
+ if (this.intervals.forceSync) {
2051
+ clearInterval(this.intervals.forceSync);
2052
+ }
2053
+ clearInterval(this.intervals.connectionChecker);
2054
+ // If there is still a connection attempt outstanding then we should stop
2055
+ // it before calling disconnect, otherwise it will be rejected in the onClose
2056
+ // handler and trigger a retry
2057
+ this.stopConnectionAttempt();
2058
+ this.disconnect();
2059
+ this.removeAllListeners();
2060
+ if (typeof window === 'undefined') {
2061
+ return;
2062
+ }
2063
+ window.removeEventListener('online', this.boundConnect);
2064
+ }
2065
+ }
2066
+
2067
+ class StatelessMessage extends OutgoingMessage {
2068
+ constructor() {
2069
+ super(...arguments);
2070
+ this.type = MessageType.Stateless;
2071
+ this.description = 'A stateless message';
2072
+ }
2073
+ get(args) {
2074
+ var _a;
2075
+ writeVarString(this.encoder, args.documentName);
2076
+ writeVarUint(this.encoder, this.type);
2077
+ writeVarString(this.encoder, (_a = args.payload) !== null && _a !== void 0 ? _a : '');
2078
+ return this.encoder;
2079
+ }
2080
+ }
2081
+
2082
+ class CloseMessage extends OutgoingMessage {
2083
+ constructor() {
2084
+ super(...arguments);
2085
+ this.type = MessageType.CLOSE;
2086
+ this.description = 'Ask the server to close the connection';
2087
+ }
2088
+ get(args) {
2089
+ writeVarString(this.encoder, args.documentName);
2090
+ writeVarUint(this.encoder, this.type);
2091
+ return this.encoder;
2092
+ }
2093
+ }
2094
+
2095
+ class HocuspocusProvider extends EventEmitter {
2096
+ constructor(configuration) {
2097
+ super();
2098
+ this.configuration = {
2099
+ name: '',
2100
+ // @ts-ignore
2101
+ document: undefined,
2102
+ // @ts-ignore
2103
+ awareness: undefined,
2104
+ token: null,
2105
+ parameters: {},
2106
+ broadcast: true,
2107
+ forceSyncInterval: false,
2108
+ onAuthenticated: () => null,
2109
+ onAuthenticationFailed: () => null,
2110
+ onOpen: () => null,
2111
+ onConnect: () => null,
2112
+ onMessage: () => null,
2113
+ onOutgoingMessage: () => null,
2114
+ onStatus: () => null,
2115
+ onSynced: () => null,
2116
+ onDisconnect: () => null,
2117
+ onClose: () => null,
2118
+ onDestroy: () => null,
2119
+ onAwarenessUpdate: () => null,
2120
+ onAwarenessChange: () => null,
2121
+ onStateless: () => null,
2122
+ quiet: false,
2123
+ };
2124
+ this.subscribedToBroadcastChannel = false;
2125
+ this.isSynced = false;
2126
+ this.unsyncedChanges = 0;
2127
+ this.status = WebSocketStatus.Disconnected;
2128
+ this.isAuthenticated = false;
2129
+ this.mux = createMutex();
2130
+ this.intervals = {
2131
+ forceSync: null,
2132
+ };
2133
+ this.isConnected = true;
2134
+ this.boundBeforeUnload = this.beforeUnload.bind(this);
2135
+ this.boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this);
2136
+ this.setConfiguration(configuration);
2137
+ this.configuration.document = configuration.document ? configuration.document : new Y.Doc();
2138
+ this.configuration.awareness = configuration.awareness ? configuration.awareness : new Awareness(this.document);
2139
+ this.on('open', this.configuration.onOpen);
2140
+ this.on('message', this.configuration.onMessage);
2141
+ this.on('outgoingMessage', this.configuration.onOutgoingMessage);
2142
+ this.on('synced', this.configuration.onSynced);
2143
+ this.on('destroy', this.configuration.onDestroy);
2144
+ this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
2145
+ this.on('awarenessChange', this.configuration.onAwarenessChange);
2146
+ this.on('stateless', this.configuration.onStateless);
2147
+ this.on('authenticated', this.configuration.onAuthenticated);
2148
+ this.on('authenticationFailed', this.configuration.onAuthenticationFailed);
2149
+ this.configuration.websocketProvider.on('connect', this.configuration.onConnect);
2150
+ this.configuration.websocketProvider.on('connect', (e) => this.emit('connect', e));
2151
+ this.configuration.websocketProvider.on('open', this.onOpen.bind(this));
2152
+ this.configuration.websocketProvider.on('open', (e) => this.emit('open', e));
2153
+ this.configuration.websocketProvider.on('message', this.onMessage.bind(this));
2154
+ this.configuration.websocketProvider.on('close', this.onClose.bind(this));
2155
+ this.configuration.websocketProvider.on('close', this.configuration.onClose);
2156
+ this.configuration.websocketProvider.on('close', (e) => this.emit('close', e));
2157
+ this.configuration.websocketProvider.on('status', this.onStatus.bind(this));
2158
+ this.configuration.websocketProvider.on('disconnect', this.configuration.onDisconnect);
2159
+ this.configuration.websocketProvider.on('disconnect', (e) => this.emit('disconnect', e));
2160
+ this.configuration.websocketProvider.on('destroy', this.configuration.onDestroy);
2161
+ this.configuration.websocketProvider.on('destroy', (e) => this.emit('destroy', e));
2162
+ this.awareness.on('update', () => {
2163
+ this.emit('awarenessUpdate', { states: awarenessStatesToArray(this.awareness.getStates()) });
2164
+ });
2165
+ this.awareness.on('change', () => {
2166
+ this.emit('awarenessChange', { states: awarenessStatesToArray(this.awareness.getStates()) });
2167
+ });
2168
+ this.document.on('update', this.documentUpdateHandler.bind(this));
2169
+ this.awareness.on('update', this.awarenessUpdateHandler.bind(this));
2170
+ this.registerEventListeners();
2171
+ if (this.configuration.forceSyncInterval) {
2172
+ this.intervals.forceSync = setInterval(this.forceSync.bind(this), this.configuration.forceSyncInterval);
2173
+ }
2174
+ this.configuration.websocketProvider.attach(this);
2175
+ }
2176
+ onStatus({ status }) {
2177
+ this.status = status;
2178
+ this.configuration.onStatus({ status });
2179
+ this.emit('status', { status });
2180
+ }
2181
+ setConfiguration(configuration = {}) {
2182
+ if (!configuration.websocketProvider && configuration.url) {
2183
+ this.configuration.websocketProvider = new HocuspocusProviderWebsocket({ url: configuration.url });
2184
+ }
2185
+ this.configuration = { ...this.configuration, ...configuration };
2186
+ }
2187
+ get document() {
2188
+ return this.configuration.document;
2189
+ }
2190
+ get awareness() {
2191
+ return this.configuration.awareness;
2192
+ }
2193
+ get hasUnsyncedChanges() {
2194
+ return this.unsyncedChanges > 0;
2195
+ }
2196
+ forceSync() {
2197
+ this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
2198
+ }
2199
+ beforeUnload() {
2200
+ removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload');
2201
+ }
2202
+ registerEventListeners() {
2203
+ if (typeof window === 'undefined') {
2204
+ return;
2205
+ }
2206
+ window.addEventListener('beforeunload', this.boundBeforeUnload);
2207
+ }
2208
+ sendStateless(payload) {
2209
+ this.send(StatelessMessage, { documentName: this.configuration.name, payload });
2210
+ }
2211
+ documentUpdateHandler(update, origin) {
2212
+ if (origin === this) {
2213
+ return;
2214
+ }
2215
+ this.unsyncedChanges += 1;
1890
2216
  this.send(UpdateMessage, { update, documentName: this.configuration.name }, true);
1891
2217
  }
1892
2218
  awarenessUpdateHandler({ added, updated, removed }, origin) {
@@ -1950,6 +2276,8 @@ class HocuspocusProvider extends EventEmitter {
1950
2276
  }
1951
2277
  }
1952
2278
  send(message, args, broadcast = false) {
2279
+ if (!this.isConnected)
2280
+ return;
1953
2281
  if (broadcast) {
1954
2282
  this.mux(() => { this.broadcast(message, args); });
1955
2283
  }
@@ -1984,6 +2312,7 @@ class HocuspocusProvider extends EventEmitter {
1984
2312
  this.document.off('update', this.documentUpdateHandler);
1985
2313
  this.removeAllListeners();
1986
2314
  this.send(CloseMessage, { documentName: this.configuration.name });
2315
+ this.isConnected = false;
1987
2316
  if (typeof window === 'undefined') {
1988
2317
  return;
1989
2318
  }
@@ -2052,325 +2381,6 @@ class HocuspocusProvider extends EventEmitter {
2052
2381
  }
2053
2382
  }
2054
2383
 
2055
- /**
2056
- * Utility module to work with urls.
2057
- *
2058
- * @module url
2059
- */
2060
-
2061
- /**
2062
- * @param {Object<string,string>} params
2063
- * @return {string}
2064
- */
2065
- const encodeQueryParams = params =>
2066
- map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
2067
-
2068
- class HocuspocusProviderWebsocket extends EventEmitter {
2069
- constructor(configuration) {
2070
- super();
2071
- this.configuration = {
2072
- url: '',
2073
- // @ts-ignore
2074
- document: undefined,
2075
- // @ts-ignore
2076
- awareness: undefined,
2077
- WebSocketPolyfill: undefined,
2078
- parameters: {},
2079
- connect: true,
2080
- broadcast: true,
2081
- forceSyncInterval: false,
2082
- // TODO: this should depend on awareness.outdatedTime
2083
- messageReconnectTimeout: 30000,
2084
- // 1 second
2085
- delay: 1000,
2086
- // instant
2087
- initialDelay: 0,
2088
- // double the delay each time
2089
- factor: 2,
2090
- // unlimited retries
2091
- maxAttempts: 0,
2092
- // wait at least 1 second
2093
- minDelay: 1000,
2094
- // at least every 30 seconds
2095
- maxDelay: 30000,
2096
- // randomize
2097
- jitter: true,
2098
- // retry forever
2099
- timeout: 0,
2100
- onOpen: () => null,
2101
- onConnect: () => null,
2102
- onMessage: () => null,
2103
- onOutgoingMessage: () => null,
2104
- onStatus: () => null,
2105
- onDisconnect: () => null,
2106
- onClose: () => null,
2107
- onDestroy: () => null,
2108
- onAwarenessUpdate: () => null,
2109
- onAwarenessChange: () => null,
2110
- quiet: false,
2111
- };
2112
- this.subscribedToBroadcastChannel = false;
2113
- this.webSocket = null;
2114
- this.shouldConnect = true;
2115
- this.status = WebSocketStatus.Disconnected;
2116
- this.lastMessageReceived = 0;
2117
- this.mux = createMutex();
2118
- this.intervals = {
2119
- forceSync: null,
2120
- connectionChecker: null,
2121
- };
2122
- this.connectionAttempt = null;
2123
- this.receivedOnOpenPayload = undefined;
2124
- this.receivedOnStatusPayload = undefined;
2125
- this.boundConnect = this.connect.bind(this);
2126
- this.setConfiguration(configuration);
2127
- this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
2128
- this.on('open', this.configuration.onOpen);
2129
- this.on('open', this.onOpen.bind(this));
2130
- this.on('connect', this.configuration.onConnect);
2131
- this.on('message', this.configuration.onMessage);
2132
- this.on('outgoingMessage', this.configuration.onOutgoingMessage);
2133
- this.on('status', this.configuration.onStatus);
2134
- this.on('status', this.onStatus.bind(this));
2135
- this.on('disconnect', this.configuration.onDisconnect);
2136
- this.on('close', this.configuration.onClose);
2137
- this.on('destroy', this.configuration.onDestroy);
2138
- this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
2139
- this.on('awarenessChange', this.configuration.onAwarenessChange);
2140
- this.on('close', this.onClose.bind(this));
2141
- this.on('message', this.onMessage.bind(this));
2142
- this.registerEventListeners();
2143
- this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
2144
- if (typeof configuration.connect !== 'undefined') {
2145
- this.shouldConnect = configuration.connect;
2146
- }
2147
- if (!this.shouldConnect) {
2148
- return;
2149
- }
2150
- this.connect();
2151
- }
2152
- async onOpen(event) {
2153
- this.receivedOnOpenPayload = event;
2154
- }
2155
- async onStatus(data) {
2156
- this.receivedOnStatusPayload = data;
2157
- }
2158
- attach(provider) {
2159
- if (this.receivedOnOpenPayload) {
2160
- provider.onOpen(this.receivedOnOpenPayload);
2161
- }
2162
- if (this.receivedOnStatusPayload) {
2163
- provider.onStatus(this.receivedOnStatusPayload);
2164
- }
2165
- }
2166
- detach(provider) {
2167
- // tell the server to remove the listener
2168
- }
2169
- setConfiguration(configuration = {}) {
2170
- this.configuration = { ...this.configuration, ...configuration };
2171
- }
2172
- async connect() {
2173
- if (this.status === WebSocketStatus.Connected) {
2174
- return;
2175
- }
2176
- // Always cancel any previously initiated connection retryer instances
2177
- if (this.cancelWebsocketRetry) {
2178
- this.cancelWebsocketRetry();
2179
- this.cancelWebsocketRetry = undefined;
2180
- }
2181
- this.shouldConnect = true;
2182
- const abortableRetry = () => {
2183
- let cancelAttempt = false;
2184
- const retryPromise = retry(this.createWebSocketConnection.bind(this), {
2185
- delay: this.configuration.delay,
2186
- initialDelay: this.configuration.initialDelay,
2187
- factor: this.configuration.factor,
2188
- maxAttempts: this.configuration.maxAttempts,
2189
- minDelay: this.configuration.minDelay,
2190
- maxDelay: this.configuration.maxDelay,
2191
- jitter: this.configuration.jitter,
2192
- timeout: this.configuration.timeout,
2193
- beforeAttempt: context => {
2194
- if (!this.shouldConnect || cancelAttempt) {
2195
- context.abort();
2196
- }
2197
- },
2198
- }).catch((error) => {
2199
- // If we aborted the connection attempt then don’t throw an error
2200
- // ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
2201
- if (error && error.code !== 'ATTEMPT_ABORTED') {
2202
- throw error;
2203
- }
2204
- });
2205
- return {
2206
- retryPromise,
2207
- cancelFunc: () => {
2208
- cancelAttempt = true;
2209
- },
2210
- };
2211
- };
2212
- const { retryPromise, cancelFunc } = abortableRetry();
2213
- this.cancelWebsocketRetry = cancelFunc;
2214
- return retryPromise;
2215
- }
2216
- createWebSocketConnection() {
2217
- return new Promise((resolve, reject) => {
2218
- if (this.webSocket) {
2219
- this.webSocket.close();
2220
- this.webSocket = null;
2221
- }
2222
- // Init the WebSocket connection
2223
- const ws = new this.configuration.WebSocketPolyfill(this.url);
2224
- ws.binaryType = 'arraybuffer';
2225
- ws.onmessage = (payload) => this.emit('message', payload);
2226
- ws.onclose = (payload) => this.emit('close', { event: payload });
2227
- ws.onopen = (payload) => this.emit('open', payload);
2228
- ws.onerror = (err) => {
2229
- reject(err);
2230
- };
2231
- this.webSocket = ws;
2232
- // Reset the status
2233
- this.status = WebSocketStatus.Connecting;
2234
- this.emit('status', { status: WebSocketStatus.Connecting });
2235
- // Store resolve/reject for later use
2236
- this.connectionAttempt = {
2237
- resolve,
2238
- reject,
2239
- };
2240
- });
2241
- }
2242
- onMessage(event) {
2243
- this.resolveConnectionAttempt();
2244
- }
2245
- resolveConnectionAttempt() {
2246
- if (this.connectionAttempt) {
2247
- this.connectionAttempt.resolve();
2248
- this.connectionAttempt = null;
2249
- this.status = WebSocketStatus.Connected;
2250
- this.emit('status', { status: WebSocketStatus.Connected });
2251
- this.emit('connect');
2252
- }
2253
- }
2254
- stopConnectionAttempt() {
2255
- this.connectionAttempt = null;
2256
- }
2257
- rejectConnectionAttempt() {
2258
- var _a;
2259
- (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
2260
- this.connectionAttempt = null;
2261
- }
2262
- checkConnection() {
2263
- var _a;
2264
- // Don’t check the connection when it’s not even established
2265
- if (this.status !== WebSocketStatus.Connected) {
2266
- return;
2267
- }
2268
- // Don’t close then connection while waiting for the first message
2269
- if (!this.lastMessageReceived) {
2270
- return;
2271
- }
2272
- // Don’t close the connection when a message was received recently
2273
- if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
2274
- return;
2275
- }
2276
- // No message received in a long time, not even your own
2277
- // Awareness updates, which are updated every 15 seconds.
2278
- (_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
2279
- }
2280
- registerEventListeners() {
2281
- if (typeof window === 'undefined') {
2282
- return;
2283
- }
2284
- window.addEventListener('online', this.boundConnect);
2285
- }
2286
- // Ensure that the URL always ends with /
2287
- get serverUrl() {
2288
- while (this.configuration.url[this.configuration.url.length - 1] === '/') {
2289
- return this.configuration.url.slice(0, this.configuration.url.length - 1);
2290
- }
2291
- return this.configuration.url;
2292
- }
2293
- get url() {
2294
- const encodedParams = encodeQueryParams(this.configuration.parameters);
2295
- return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
2296
- }
2297
- disconnect() {
2298
- this.shouldConnect = false;
2299
- if (this.webSocket === null) {
2300
- return;
2301
- }
2302
- try {
2303
- this.webSocket.close();
2304
- }
2305
- catch {
2306
- //
2307
- }
2308
- }
2309
- send(message) {
2310
- var _a;
2311
- if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === WsReadyStates.Open) {
2312
- this.webSocket.send(message);
2313
- }
2314
- }
2315
- onClose({ event }) {
2316
- this.webSocket = null;
2317
- if (this.status === WebSocketStatus.Connected) {
2318
- this.status = WebSocketStatus.Disconnected;
2319
- this.emit('status', { status: WebSocketStatus.Disconnected });
2320
- this.emit('disconnect', { event });
2321
- }
2322
- if (event.code === Unauthorized.code) {
2323
- if (!this.configuration.quiet) {
2324
- 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.');
2325
- }
2326
- this.shouldConnect = false;
2327
- }
2328
- if (event.code === Forbidden.code) {
2329
- if (!this.configuration.quiet) {
2330
- console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
2331
- return; // TODO REMOVE ME
2332
- }
2333
- }
2334
- if (this.connectionAttempt) {
2335
- // That connection attempt failed.
2336
- this.rejectConnectionAttempt();
2337
- }
2338
- else if (this.shouldConnect) {
2339
- // The connection was closed by the server. Let’s just try again.
2340
- this.connect();
2341
- }
2342
- // If we’ll reconnect, we’re done for now.
2343
- if (this.shouldConnect) {
2344
- return;
2345
- }
2346
- // The status is set correctly already.
2347
- if (this.status === WebSocketStatus.Disconnected) {
2348
- return;
2349
- }
2350
- // Let’s update the connection status.
2351
- this.status = WebSocketStatus.Disconnected;
2352
- this.emit('status', { status: WebSocketStatus.Disconnected });
2353
- this.emit('disconnect', { event });
2354
- }
2355
- destroy() {
2356
- this.emit('destroy');
2357
- if (this.intervals.forceSync) {
2358
- clearInterval(this.intervals.forceSync);
2359
- }
2360
- clearInterval(this.intervals.connectionChecker);
2361
- // If there is still a connection attempt outstanding then we should stop
2362
- // it before calling disconnect, otherwise it will be rejected in the onClose
2363
- // handler and trigger a retry
2364
- this.stopConnectionAttempt();
2365
- this.disconnect();
2366
- this.removeAllListeners();
2367
- if (typeof window === 'undefined') {
2368
- return;
2369
- }
2370
- window.removeEventListener('online', this.boundConnect);
2371
- }
2372
- }
2373
-
2374
2384
  class HocuspocusCloudProvider extends HocuspocusProvider {
2375
2385
  constructor(configuration) {
2376
2386
  if (!configuration.url) {