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