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