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