@hocuspocus/provider 1.0.0-alpha.14 → 1.0.0-alpha.18

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,4 +1,5 @@
1
1
  import * as Y from 'yjs';
2
+ import { retry } from '@lifeomic/attempt';
2
3
 
3
4
  /**
4
5
  * Utility module to work with key-value stores.
@@ -258,8 +259,6 @@ hasConf('production');
258
259
  */
259
260
 
260
261
  const floor = Math.floor;
261
- const round = Math.round;
262
- const log10 = Math.log10;
263
262
 
264
263
  /**
265
264
  * @function
@@ -1575,7 +1574,8 @@ class MessageReceiver {
1575
1574
  return this;
1576
1575
  }
1577
1576
  apply(provider, emitSynced = true) {
1578
- const type = this.message.readVarUint();
1577
+ const { message } = this;
1578
+ const type = message.readVarUint();
1579
1579
  switch (type) {
1580
1580
  case MessageType.Sync:
1581
1581
  this.applySyncMessage(provider, emitSynced);
@@ -1592,16 +1592,6 @@ class MessageReceiver {
1592
1592
  default:
1593
1593
  throw new Error(`Can’t apply message of unknown type: ${type}`);
1594
1594
  }
1595
- }
1596
- applySyncMessage(provider, emitSynced) {
1597
- const { message } = this;
1598
- message.writeVarUint(MessageType.Sync);
1599
- // Apply update
1600
- const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
1601
- // Synced
1602
- if (emitSynced && syncMessageType === messageYjsSyncStep2) {
1603
- provider.synced = true;
1604
- }
1605
1595
  // Reply
1606
1596
  if (message.length() > 1) {
1607
1597
  if (this.broadcasted) {
@@ -1616,6 +1606,16 @@ class MessageReceiver {
1616
1606
  }
1617
1607
  }
1618
1608
  }
1609
+ applySyncMessage(provider, emitSynced) {
1610
+ const { message } = this;
1611
+ message.writeVarUint(MessageType.Sync);
1612
+ // Apply update
1613
+ const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
1614
+ // Synced once we receive Step2
1615
+ if (emitSynced && syncMessageType === messageYjsSyncStep2) {
1616
+ provider.synced = true;
1617
+ }
1618
+ }
1619
1619
  applyAwarenessMessage(provider) {
1620
1620
  const { message } = this;
1621
1621
  applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
@@ -1755,7 +1755,6 @@ var awarenessStatesToArray = (states) => {
1755
1755
  });
1756
1756
  };
1757
1757
 
1758
- // @ts-nocheck
1759
1758
  var WebSocketStatus;
1760
1759
  (function (WebSocketStatus) {
1761
1760
  WebSocketStatus["Connecting"] = "connecting";
@@ -1766,18 +1765,36 @@ class HocuspocusProvider extends EventEmitter {
1766
1765
  constructor(options = {}) {
1767
1766
  super();
1768
1767
  this.options = {
1768
+ // @ts-ignore
1769
+ document: undefined,
1770
+ // @ts-ignore
1771
+ awareness: undefined,
1772
+ WebSocketPolyfill: undefined,
1769
1773
  url: '',
1770
1774
  name: '',
1771
1775
  token: null,
1772
1776
  parameters: {},
1773
- debug: false,
1774
1777
  connect: true,
1775
1778
  broadcast: true,
1776
1779
  forceSyncInterval: false,
1777
- reconnectTimeoutBase: 1200,
1778
- maxReconnectTimeout: 2500,
1779
1780
  // TODO: this should depend on awareness.outdatedTime
1780
1781
  messageReconnectTimeout: 30000,
1782
+ // 1 second
1783
+ delay: 1000,
1784
+ // instant
1785
+ initialDelay: 0,
1786
+ // double the delay each time
1787
+ factor: 2,
1788
+ // unlimited retries
1789
+ maxAttempts: 0,
1790
+ // wait at least 1 second
1791
+ minDelay: 1000,
1792
+ // at least every 30 seconds
1793
+ maxDelay: 30000,
1794
+ // randomize
1795
+ jitter: true,
1796
+ // retry forever
1797
+ timeout: 0,
1781
1798
  onAuthenticated: () => null,
1782
1799
  onAuthenticationFailed: () => null,
1783
1800
  onOpen: () => null,
@@ -1796,7 +1813,6 @@ class HocuspocusProvider extends EventEmitter {
1796
1813
  this.webSocket = null;
1797
1814
  this.shouldConnect = true;
1798
1815
  this.status = WebSocketStatus.Disconnected;
1799
- this.failedConnectionAttempts = 0;
1800
1816
  this.isSynced = false;
1801
1817
  this.isAuthenticated = false;
1802
1818
  this.lastMessageReceived = 0;
@@ -1805,11 +1821,10 @@ class HocuspocusProvider extends EventEmitter {
1805
1821
  forceSync: null,
1806
1822
  connectionChecker: null,
1807
1823
  };
1824
+ this.connectionAttempt = null;
1808
1825
  this.setOptions(options);
1809
- this.options.document = options.document ? options.document : new Y.Doc();
1810
1826
  this.options.awareness = options.awareness ? options.awareness : new Awareness(this.document);
1811
1827
  this.options.WebSocketPolyfill = options.WebSocketPolyfill ? options.WebSocketPolyfill : WebSocket;
1812
- this.shouldConnect = options.connect !== undefined ? options.connect : this.shouldConnect;
1813
1828
  this.on('open', this.options.onOpen);
1814
1829
  this.on('authenticated', this.options.onAuthenticated);
1815
1830
  this.on('authenticationFailed', this.options.onAuthenticationFailed);
@@ -1824,29 +1839,92 @@ class HocuspocusProvider extends EventEmitter {
1824
1839
  this.on('awarenessUpdate', this.options.onAwarenessUpdate);
1825
1840
  this.on('awarenessChange', this.options.onAwarenessChange);
1826
1841
  this.awareness.on('update', () => {
1827
- this.emit('awarenessUpdate', {
1828
- states: awarenessStatesToArray(this.awareness.getStates()),
1829
- });
1842
+ this.emit('awarenessUpdate', { states: awarenessStatesToArray(this.awareness.getStates()) });
1830
1843
  });
1831
1844
  this.awareness.on('change', () => {
1832
- this.emit('awarenessChange', {
1833
- states: awarenessStatesToArray(this.awareness.getStates()),
1834
- });
1845
+ this.emit('awarenessChange', { states: awarenessStatesToArray(this.awareness.getStates()) });
1835
1846
  });
1836
- this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.options.messageReconnectTimeout / 10);
1837
1847
  this.document.on('update', this.documentUpdateHandler.bind(this));
1838
1848
  this.awareness.on('update', this.awarenessUpdateHandler.bind(this));
1839
1849
  this.registerBeforeUnloadEventListener();
1850
+ this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.options.messageReconnectTimeout / 10);
1840
1851
  if (this.options.forceSyncInterval) {
1841
1852
  this.intervals.forceSync = setInterval(this.forceSync.bind(this), this.options.forceSyncInterval);
1842
1853
  }
1843
- if (this.options.connect) {
1844
- this.connect();
1854
+ if (typeof options.connect !== 'undefined') {
1855
+ this.shouldConnect = options.connect;
1856
+ }
1857
+ if (!this.shouldConnect) {
1858
+ return;
1845
1859
  }
1860
+ this.connect();
1846
1861
  }
1847
1862
  setOptions(options = {}) {
1848
1863
  this.options = { ...this.options, ...options };
1849
1864
  }
1865
+ async connect() {
1866
+ if (this.status === WebSocketStatus.Connected) {
1867
+ return;
1868
+ }
1869
+ this.shouldConnect = true;
1870
+ this.subscribeToBroadcastChannel();
1871
+ try {
1872
+ await retry(this.createWebSocketConnection.bind(this), {
1873
+ delay: this.options.delay,
1874
+ initialDelay: this.options.initialDelay,
1875
+ factor: this.options.factor,
1876
+ maxAttempts: this.options.maxAttempts,
1877
+ minDelay: this.options.minDelay,
1878
+ maxDelay: this.options.maxDelay,
1879
+ jitter: this.options.jitter,
1880
+ timeout: this.options.timeout,
1881
+ beforeAttempt: context => {
1882
+ if (!this.shouldConnect) {
1883
+ context.abort();
1884
+ }
1885
+ },
1886
+ });
1887
+ }
1888
+ catch (err) {
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 (err.code !== 'ATTEMPT_ABORTED') {
1892
+ throw err;
1893
+ }
1894
+ }
1895
+ }
1896
+ createWebSocketConnection() {
1897
+ return new Promise((resolve, reject) => {
1898
+ // Init the WebSocket connection
1899
+ this.webSocket = new this.options.WebSocketPolyfill(this.url);
1900
+ this.webSocket.binaryType = 'arraybuffer';
1901
+ this.webSocket.onmessage = this.onMessage.bind(this);
1902
+ this.webSocket.onclose = this.onClose.bind(this);
1903
+ this.webSocket.onopen = this.onOpen.bind(this);
1904
+ this.webSocket.onerror = () => {
1905
+ reject();
1906
+ };
1907
+ // Reset the status
1908
+ this.synced = false;
1909
+ this.status = WebSocketStatus.Connecting;
1910
+ this.emit('status', { status: 'connecting' });
1911
+ // Store resolve/reject for later use
1912
+ this.connectionAttempt = {
1913
+ resolve,
1914
+ reject,
1915
+ };
1916
+ });
1917
+ }
1918
+ resolveConnectionAttempt() {
1919
+ var _a;
1920
+ (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.resolve();
1921
+ this.connectionAttempt = null;
1922
+ }
1923
+ rejectConnectionAttempt() {
1924
+ var _a;
1925
+ (_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
1926
+ this.connectionAttempt = null;
1927
+ }
1850
1928
  get document() {
1851
1929
  return this.options.document;
1852
1930
  }
@@ -1854,11 +1932,11 @@ class HocuspocusProvider extends EventEmitter {
1854
1932
  return this.options.awareness;
1855
1933
  }
1856
1934
  checkConnection() {
1857
- // Don’t close the connection when it’s not established anyway
1935
+ // Don’t check the connection when it’s not even established
1858
1936
  if (this.status !== WebSocketStatus.Connected) {
1859
1937
  return;
1860
1938
  }
1861
- // Don’t just close then connection while waiting for the first message
1939
+ // Don’t close then connection while waiting for the first message
1862
1940
  if (!this.lastMessageReceived) {
1863
1941
  return;
1864
1942
  }
@@ -1899,7 +1977,6 @@ class HocuspocusProvider extends EventEmitter {
1899
1977
  }
1900
1978
  permissionDeniedHandler(reason) {
1901
1979
  this.emit('authenticationFailed', { reason });
1902
- this.log('Permission denied', reason);
1903
1980
  this.isAuthenticated = false;
1904
1981
  this.shouldConnect = false;
1905
1982
  }
@@ -1933,13 +2010,6 @@ class HocuspocusProvider extends EventEmitter {
1933
2010
  get isAuthenticationRequired() {
1934
2011
  return !!this.options.token && !this.isAuthenticated;
1935
2012
  }
1936
- connect() {
1937
- this.shouldConnect = true;
1938
- if (this.status !== WebSocketStatus.Connected) {
1939
- this.createWebSocketConnection();
1940
- this.subscribeToBroadcastChannel();
1941
- }
1942
- }
1943
2013
  disconnect() {
1944
2014
  this.shouldConnect = false;
1945
2015
  this.disconnectBroadcastChannel();
@@ -1953,19 +2023,6 @@ class HocuspocusProvider extends EventEmitter {
1953
2023
  //
1954
2024
  }
1955
2025
  }
1956
- createWebSocketConnection() {
1957
- if (this.webSocket !== null) {
1958
- return;
1959
- }
1960
- this.webSocket = new this.options.WebSocketPolyfill(this.url);
1961
- this.webSocket.binaryType = 'arraybuffer';
1962
- this.status = WebSocketStatus.Connecting;
1963
- this.synced = false;
1964
- this.webSocket.onmessage = this.onMessage.bind(this);
1965
- this.webSocket.onclose = this.onClose.bind(this);
1966
- this.webSocket.onopen = this.onOpen.bind(this);
1967
- this.emit('status', { status: 'connecting' });
1968
- }
1969
2026
  onOpen(event) {
1970
2027
  this.emit('open', { event });
1971
2028
  if (this.status !== WebSocketStatus.Connected) {
@@ -1980,16 +2037,17 @@ class HocuspocusProvider extends EventEmitter {
1980
2037
  return this.options.token;
1981
2038
  }
1982
2039
  async webSocketConnectionEstablished() {
1983
- this.failedConnectionAttempts = 0;
1984
2040
  this.status = WebSocketStatus.Connected;
1985
2041
  this.emit('status', { status: 'connected' });
1986
2042
  this.emit('connect');
1987
2043
  if (this.isAuthenticationRequired) {
1988
- const token = await this.getToken();
1989
- this.send(AuthenticationMessage, { token });
2044
+ this.send(AuthenticationMessage, {
2045
+ token: await this.getToken(),
2046
+ });
1990
2047
  return;
1991
2048
  }
1992
2049
  this.startSync();
2050
+ this.resolveConnectionAttempt();
1993
2051
  }
1994
2052
  startSync() {
1995
2053
  this.send(SyncStepOneMessage, { document: this.document });
@@ -2002,9 +2060,7 @@ class HocuspocusProvider extends EventEmitter {
2002
2060
  }
2003
2061
  send(Message, args, broadcast = false) {
2004
2062
  if (broadcast) {
2005
- this.mux(() => {
2006
- this.broadcast(Message, args);
2007
- });
2063
+ this.mux(() => { this.broadcast(Message, args); });
2008
2064
  }
2009
2065
  if (this.status === WebSocketStatus.Connected) {
2010
2066
  const messageSender = new MessageSender(Message, args);
@@ -2020,30 +2076,36 @@ class HocuspocusProvider extends EventEmitter {
2020
2076
  }
2021
2077
  onClose(event) {
2022
2078
  this.emit('close', { event });
2023
- this.isAuthenticated = false;
2024
2079
  this.webSocket = null;
2080
+ this.isAuthenticated = false;
2081
+ this.synced = false;
2025
2082
  if (this.status === WebSocketStatus.Connected) {
2026
- this.synced = false;
2027
2083
  // update awareness (all users except local left)
2028
2084
  removeAwarenessStates(this.awareness, Array.from(this.awareness.getStates().keys()).filter(client => client !== this.document.clientID), this);
2029
2085
  this.status = WebSocketStatus.Disconnected;
2030
2086
  this.emit('status', { status: 'disconnected' });
2031
2087
  this.emit('disconnect', { event });
2032
2088
  }
2033
- else {
2034
- this.failedConnectionAttempts += 1;
2089
+ if (this.connectionAttempt) {
2090
+ // Okay, that connection attempt failed …
2091
+ this.rejectConnectionAttempt();
2092
+ }
2093
+ else if (this.shouldConnect) {
2094
+ // The connection was closed by the server, so let’s just try again.
2095
+ this.connect();
2035
2096
  }
2097
+ // If we’ll reconnect anyway, we’re done for now.
2036
2098
  if (this.shouldConnect) {
2037
- const wait = round(min(log10(this.failedConnectionAttempts + 1) * this.options.reconnectTimeoutBase, this.options.maxReconnectTimeout));
2038
- this.log(`[close] Reconnecting in ${wait}ms …`);
2039
- setTimeout(this.createWebSocketConnection.bind(this), wait);
2040
2099
  return;
2041
2100
  }
2042
- if (this.status !== WebSocketStatus.Disconnected) {
2043
- this.status = WebSocketStatus.Disconnected;
2044
- this.emit('status', { status: 'disconnected' });
2045
- this.emit('disconnect', { event });
2101
+ // The status is set correctly already.
2102
+ if (this.status === WebSocketStatus.Disconnected) {
2103
+ return;
2046
2104
  }
2105
+ // Let’s update the connection status.
2106
+ this.status = WebSocketStatus.Disconnected;
2107
+ this.emit('status', { status: 'disconnected' });
2108
+ this.emit('disconnect', { event });
2047
2109
  }
2048
2110
  destroy() {
2049
2111
  this.emit('destroy');
@@ -2051,6 +2113,11 @@ class HocuspocusProvider extends EventEmitter {
2051
2113
  clearInterval(this.intervals.forceSync);
2052
2114
  }
2053
2115
  clearInterval(this.intervals.connectionChecker);
2116
+ removeAwarenessStates(this.awareness, [this.document.clientID], 'provider destroy');
2117
+ // If there is still a connection attempt outstanding then we should resolve
2118
+ // it before calling disconnect, otherwise it will be rejected in the onClose
2119
+ // handler and trigger a retry
2120
+ this.resolveConnectionAttempt();
2054
2121
  this.disconnect();
2055
2122
  this.awareness.off('update', this.awarenessUpdateHandler);
2056
2123
  this.document.off('update', this.documentUpdateHandler);
@@ -2062,7 +2129,7 @@ class HocuspocusProvider extends EventEmitter {
2062
2129
  broadcastChannelSubscriber(data) {
2063
2130
  this.mux(() => {
2064
2131
  const message = new IncomingMessage(data);
2065
- new MessageReceiver(message, this)
2132
+ new MessageReceiver(message)
2066
2133
  .setBroadcasted(true)
2067
2134
  .apply(this, false);
2068
2135
  });
@@ -2100,12 +2167,6 @@ class HocuspocusProvider extends EventEmitter {
2100
2167
  }
2101
2168
  new MessageSender(Message, args).broadcast(this.broadcastChannel);
2102
2169
  }
2103
- log(message) {
2104
- if (!this.options.debug) {
2105
- return;
2106
- }
2107
- console.log(message);
2108
- }
2109
2170
  setAwarenessField(key, value) {
2110
2171
  this.awareness.setLocalStateField(key, value);
2111
2172
  }