@atlaskit/collab-provider 36.0.4 → 36.0.6

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @atlaskit/collab-provider
2
2
 
3
+ ## 36.0.6
4
+
5
+ ### Patch Changes
6
+
7
+ - [`283c2b7c4aed3`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/283c2b7c4aed3) -
8
+ Presence connects with WebSocket first and falls back to long polling only when WebSocket fails,
9
+ behind `platform_editor_presence_ws_first_with_fallback`
10
+ - Updated dependencies
11
+
12
+ ## 36.0.5
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies
17
+
3
18
  ## 36.0.4
4
19
 
5
20
  ### Patch Changes
@@ -4,12 +4,14 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
+ exports.WEBSOCKET_CONNECT_TIMEOUT_MS = void 0;
7
8
  exports.createSocketIOCollabProvider = createSocketIOCollabProvider;
8
9
  exports.createSocketIOSocket = createSocketIOSocket;
9
10
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
10
11
  var _socket = require("socket.io-client");
11
12
  var _fg = require("@atlaskit/platform-feature-flags/fg");
12
13
  var _config = require("./config");
14
+ var _const = require("./helpers/const");
13
15
  var _utils = require("./helpers/utils");
14
16
  var _provider = require("./provider");
15
17
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
@@ -21,6 +23,7 @@ function createSocketIOSocket(url, auth, productInfo, isPresenceOnly, analyticsH
21
23
  var socketIOOptions = _config.SOCKET_IO_OPTIONS;
22
24
  // Default: polling first, with WebSocket upgrade fallback
23
25
  var transports = ['polling', 'websocket'];
26
+ var presenceWebSocketFirst = false;
24
27
 
25
28
  // Determine transport strategy based on connection type and tenant.
26
29
  // https://socket.io/docs/v4/client-options/#transports
@@ -39,10 +42,11 @@ function createSocketIOSocket(url, auth, productInfo, isPresenceOnly, analyticsH
39
42
  };
40
43
  switch (getConnectionCase()) {
41
44
  case 'presence':
42
- // Presence for all tenants (commercial, GCP, IC, …): WebSocket only when flag is enabled
45
+ // Presence for all tenants (commercial, GCP, IC, …): WebSocket first, polling only when WebSocket fails
43
46
  socketIOOptions = _config.SOCKET_IO_OPTIONS_WITH_HIGH_JITTER;
44
- if ((0, _fg.fg)('platform-editor-presence-websocket-only')) {
45
- transports = ['websocket'];
47
+ if ((0, _fg.fg)('platform_editor_presence_ws_first_with_fallback')) {
48
+ transports = ['websocket', 'polling'];
49
+ presenceWebSocketFirst = true;
46
50
  }
47
51
  break;
48
52
  case 'gcp-collab':
@@ -67,15 +71,74 @@ function createSocketIOSocket(url, auth, productInfo, isPresenceOnly, analyticsH
67
71
  closeOnBeforeunload: false,
68
72
  withCredentials: true,
69
73
  transports: transports,
74
+ // When WebSocket is rejected while opening, try polling in the same attempt
75
+ tryAllTransports: presenceWebSocketFirst,
70
76
  path: path ? "".concat(path, "/socket.io") : "/".concat(pathname.split('/')[1], "/socket.io"),
71
77
  auth: auth,
72
78
  extraHeaders: extraHeaders,
73
- query: {
79
+ query: _objectSpread({
74
80
  sourceId: documentAri === null || documentAri === void 0 ? void 0 : documentAri.split('/')[1]
75
- }
81
+ }, presenceWebSocketFirst ? extraHeaders : {})
76
82
  });
83
+ if (presenceWebSocketFirst) {
84
+ addPollingFallback(client, url, analyticsHelper);
85
+ }
77
86
  return client;
78
87
  }
88
+
89
+ // Long enough for the permission token fetch on a slow network. A false trigger only restores polling-first.
90
+ var WEBSOCKET_CONNECT_TIMEOUT_MS = exports.WEBSOCKET_CONNECT_TIMEOUT_MS = 8000;
91
+
92
+ /**
93
+ * `tryAllTransports` only covers a WebSocket that fails fast. Some proxies hang the upgrade, or accept it and
94
+ * drop every frame, and then no error fires. Switch to polling-first when WebSocket does not connect in time,
95
+ * or fails at the transport level. A server rejection (error with `data`) proves WebSocket works, so it never
96
+ * triggers the fallback. Once switched, all reconnects of this socket use polling-first.
97
+ */
98
+ function addPollingFallback(client, url, analyticsHelper) {
99
+ var timer;
100
+ var webSocketWorks = false;
101
+ var isWebSocketFirst = function isWebSocketFirst() {
102
+ var _client$io$opts$trans;
103
+ return !webSocketWorks && ((_client$io$opts$trans = client.io.opts.transports) === null || _client$io$opts$trans === void 0 ? void 0 : _client$io$opts$trans[0]) === 'websocket';
104
+ };
105
+ var fallbackToPolling = function fallbackToPolling(reason) {
106
+ clearTimeout(timer);
107
+ if (!isWebSocketFirst() || typeof navigator !== 'undefined' && navigator.onLine === false) {
108
+ return;
109
+ }
110
+ client.io.opts.transports = ['polling', 'websocket'];
111
+ client.io.opts.tryAllTransports = false;
112
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(_const.EVENT_ACTION.POLLING_FALLBACK, _const.EVENT_STATUS.INFO, {
113
+ url: url,
114
+ reason: reason
115
+ });
116
+ client.disconnect().connect();
117
+ };
118
+ var startTimer = function startTimer() {
119
+ clearTimeout(timer);
120
+ if (isWebSocketFirst()) {
121
+ timer = setTimeout(function () {
122
+ return !client.connected && fallbackToPolling('timeout');
123
+ }, WEBSOCKET_CONNECT_TIMEOUT_MS);
124
+ }
125
+ };
126
+ startTimer();
127
+ client.io.on('reconnect_attempt', startTimer);
128
+ client.on('connect', function () {
129
+ clearTimeout(timer);
130
+ webSocketWorks || (webSocketWorks = client.io.engine.transport.name === 'websocket');
131
+ });
132
+ client.on('connect_error', function (error) {
133
+ if (error.data) {
134
+ var _client$io$opts$trans2;
135
+ clearTimeout(timer);
136
+ webSocketWorks || (webSocketWorks = ((_client$io$opts$trans2 = client.io.opts.transports) === null || _client$io$opts$trans2 === void 0 ? void 0 : _client$io$opts$trans2[0]) === 'websocket');
137
+ } else if (error.type === 'TransportError' || error.message === 'timeout') {
138
+ fallbackToPolling(error.message);
139
+ }
140
+ });
141
+ }
79
142
  function createSocketIOCollabProvider(config) {
80
143
  return new _provider.Provider(_objectSpread(_objectSpread({}, config), {}, {
81
144
  createSocket: createSocketIOSocket
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.version = exports.nextMajorVersion = exports.name = void 0;
7
7
  var name = exports.name = "@atlaskit/collab-provider";
8
- var version = exports.version = "36.0.3";
8
+ var version = exports.version = "36.0.5";
9
9
  var nextMajorVersion = exports.nextMajorVersion = function nextMajorVersion() {
10
10
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
11
11
  };
@@ -1,6 +1,7 @@
1
1
  import { io } from 'socket.io-client';
2
2
  import { fg } from '@atlaskit/platform-feature-flags/fg';
3
3
  import { SOCKET_IO_OPTIONS, SOCKET_IO_OPTIONS_WITH_HIGH_JITTER } from './config';
4
+ import { EVENT_ACTION, EVENT_STATUS } from './helpers/const';
4
5
  import { getProduct, getSubProduct, isGCPtenant } from './helpers/utils';
5
6
  import { Provider } from './provider';
6
7
  export function createSocketIOSocket(url, auth, productInfo, isPresenceOnly, analyticsHelper, path, documentAri) {
@@ -11,6 +12,7 @@ export function createSocketIOSocket(url, auth, productInfo, isPresenceOnly, ana
11
12
  let socketIOOptions = SOCKET_IO_OPTIONS;
12
13
  // Default: polling first, with WebSocket upgrade fallback
13
14
  let transports = ['polling', 'websocket'];
15
+ let presenceWebSocketFirst = false;
14
16
 
15
17
  // Determine transport strategy based on connection type and tenant.
16
18
  // https://socket.io/docs/v4/client-options/#transports
@@ -29,10 +31,11 @@ export function createSocketIOSocket(url, auth, productInfo, isPresenceOnly, ana
29
31
  };
30
32
  switch (getConnectionCase()) {
31
33
  case 'presence':
32
- // Presence for all tenants (commercial, GCP, IC, …): WebSocket only when flag is enabled
34
+ // Presence for all tenants (commercial, GCP, IC, …): WebSocket first, polling only when WebSocket fails
33
35
  socketIOOptions = SOCKET_IO_OPTIONS_WITH_HIGH_JITTER;
34
- if (fg('platform-editor-presence-websocket-only')) {
35
- transports = ['websocket'];
36
+ if (fg('platform_editor_presence_ws_first_with_fallback')) {
37
+ transports = ['websocket', 'polling'];
38
+ presenceWebSocketFirst = true;
36
39
  }
37
40
  break;
38
41
  case 'gcp-collab':
@@ -57,15 +60,74 @@ export function createSocketIOSocket(url, auth, productInfo, isPresenceOnly, ana
57
60
  closeOnBeforeunload: false,
58
61
  withCredentials: true,
59
62
  transports,
63
+ // When WebSocket is rejected while opening, try polling in the same attempt
64
+ tryAllTransports: presenceWebSocketFirst,
60
65
  path: path ? `${path}/socket.io` : `/${pathname.split('/')[1]}/socket.io`,
61
66
  auth,
62
67
  extraHeaders,
63
68
  query: {
64
- sourceId: documentAri === null || documentAri === void 0 ? void 0 : documentAri.split('/')[1]
69
+ sourceId: documentAri === null || documentAri === void 0 ? void 0 : documentAri.split('/')[1],
70
+ // Browsers cannot send extraHeaders on a WebSocket handshake, so the BE reads these from the query too
71
+ ...(presenceWebSocketFirst ? extraHeaders : {})
65
72
  }
66
73
  });
74
+ if (presenceWebSocketFirst) {
75
+ addPollingFallback(client, url, analyticsHelper);
76
+ }
67
77
  return client;
68
78
  }
79
+
80
+ // Long enough for the permission token fetch on a slow network. A false trigger only restores polling-first.
81
+ export const WEBSOCKET_CONNECT_TIMEOUT_MS = 8000;
82
+
83
+ /**
84
+ * `tryAllTransports` only covers a WebSocket that fails fast. Some proxies hang the upgrade, or accept it and
85
+ * drop every frame, and then no error fires. Switch to polling-first when WebSocket does not connect in time,
86
+ * or fails at the transport level. A server rejection (error with `data`) proves WebSocket works, so it never
87
+ * triggers the fallback. Once switched, all reconnects of this socket use polling-first.
88
+ */
89
+ function addPollingFallback(client, url, analyticsHelper) {
90
+ let timer;
91
+ let webSocketWorks = false;
92
+ const isWebSocketFirst = () => {
93
+ var _client$io$opts$trans;
94
+ return !webSocketWorks && ((_client$io$opts$trans = client.io.opts.transports) === null || _client$io$opts$trans === void 0 ? void 0 : _client$io$opts$trans[0]) === 'websocket';
95
+ };
96
+ const fallbackToPolling = reason => {
97
+ clearTimeout(timer);
98
+ if (!isWebSocketFirst() || typeof navigator !== 'undefined' && navigator.onLine === false) {
99
+ return;
100
+ }
101
+ client.io.opts.transports = ['polling', 'websocket'];
102
+ client.io.opts.tryAllTransports = false;
103
+ analyticsHelper === null || analyticsHelper === void 0 ? void 0 : analyticsHelper.sendActionEvent(EVENT_ACTION.POLLING_FALLBACK, EVENT_STATUS.INFO, {
104
+ url,
105
+ reason
106
+ });
107
+ client.disconnect().connect();
108
+ };
109
+ const startTimer = () => {
110
+ clearTimeout(timer);
111
+ if (isWebSocketFirst()) {
112
+ timer = setTimeout(() => !client.connected && fallbackToPolling('timeout'), WEBSOCKET_CONNECT_TIMEOUT_MS);
113
+ }
114
+ };
115
+ startTimer();
116
+ client.io.on('reconnect_attempt', startTimer);
117
+ client.on('connect', () => {
118
+ clearTimeout(timer);
119
+ webSocketWorks ||= client.io.engine.transport.name === 'websocket';
120
+ });
121
+ client.on('connect_error', error => {
122
+ if (error.data) {
123
+ var _client$io$opts$trans2;
124
+ clearTimeout(timer);
125
+ webSocketWorks ||= ((_client$io$opts$trans2 = client.io.opts.transports) === null || _client$io$opts$trans2 === void 0 ? void 0 : _client$io$opts$trans2[0]) === 'websocket';
126
+ } else if (error.type === 'TransportError' || error.message === 'timeout') {
127
+ fallbackToPolling(error.message);
128
+ }
129
+ });
130
+ }
69
131
  export function createSocketIOCollabProvider(config) {
70
132
  return new Provider({
71
133
  ...config,
@@ -1,5 +1,5 @@
1
1
  export const name = "@atlaskit/collab-provider";
2
- export const version = "36.0.3";
2
+ export const version = "36.0.5";
3
3
  export const nextMajorVersion = () => {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -4,6 +4,7 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
4
4
  import { io } from 'socket.io-client';
5
5
  import { fg } from '@atlaskit/platform-feature-flags/fg';
6
6
  import { SOCKET_IO_OPTIONS, SOCKET_IO_OPTIONS_WITH_HIGH_JITTER } from './config';
7
+ import { EVENT_ACTION, EVENT_STATUS } from './helpers/const';
7
8
  import { getProduct, getSubProduct, isGCPtenant } from './helpers/utils';
8
9
  import { Provider } from './provider';
9
10
  export function createSocketIOSocket(url, auth, productInfo, isPresenceOnly, analyticsHelper, path, documentAri) {
@@ -13,6 +14,7 @@ export function createSocketIOSocket(url, auth, productInfo, isPresenceOnly, ana
13
14
  var socketIOOptions = SOCKET_IO_OPTIONS;
14
15
  // Default: polling first, with WebSocket upgrade fallback
15
16
  var transports = ['polling', 'websocket'];
17
+ var presenceWebSocketFirst = false;
16
18
 
17
19
  // Determine transport strategy based on connection type and tenant.
18
20
  // https://socket.io/docs/v4/client-options/#transports
@@ -31,10 +33,11 @@ export function createSocketIOSocket(url, auth, productInfo, isPresenceOnly, ana
31
33
  };
32
34
  switch (getConnectionCase()) {
33
35
  case 'presence':
34
- // Presence for all tenants (commercial, GCP, IC, …): WebSocket only when flag is enabled
36
+ // Presence for all tenants (commercial, GCP, IC, …): WebSocket first, polling only when WebSocket fails
35
37
  socketIOOptions = SOCKET_IO_OPTIONS_WITH_HIGH_JITTER;
36
- if (fg('platform-editor-presence-websocket-only')) {
37
- transports = ['websocket'];
38
+ if (fg('platform_editor_presence_ws_first_with_fallback')) {
39
+ transports = ['websocket', 'polling'];
40
+ presenceWebSocketFirst = true;
38
41
  }
39
42
  break;
40
43
  case 'gcp-collab':
@@ -59,15 +62,74 @@ export function createSocketIOSocket(url, auth, productInfo, isPresenceOnly, ana
59
62
  closeOnBeforeunload: false,
60
63
  withCredentials: true,
61
64
  transports: transports,
65
+ // When WebSocket is rejected while opening, try polling in the same attempt
66
+ tryAllTransports: presenceWebSocketFirst,
62
67
  path: path ? "".concat(path, "/socket.io") : "/".concat(pathname.split('/')[1], "/socket.io"),
63
68
  auth: auth,
64
69
  extraHeaders: extraHeaders,
65
- query: {
70
+ query: _objectSpread({
66
71
  sourceId: documentAri === null || documentAri === void 0 ? void 0 : documentAri.split('/')[1]
67
- }
72
+ }, presenceWebSocketFirst ? extraHeaders : {})
68
73
  });
74
+ if (presenceWebSocketFirst) {
75
+ addPollingFallback(client, url, analyticsHelper);
76
+ }
69
77
  return client;
70
78
  }
79
+
80
+ // Long enough for the permission token fetch on a slow network. A false trigger only restores polling-first.
81
+ export var WEBSOCKET_CONNECT_TIMEOUT_MS = 8000;
82
+
83
+ /**
84
+ * `tryAllTransports` only covers a WebSocket that fails fast. Some proxies hang the upgrade, or accept it and
85
+ * drop every frame, and then no error fires. Switch to polling-first when WebSocket does not connect in time,
86
+ * or fails at the transport level. A server rejection (error with `data`) proves WebSocket works, so it never
87
+ * triggers the fallback. Once switched, all reconnects of this socket use polling-first.
88
+ */
89
+ function addPollingFallback(client, url, analyticsHelper) {
90
+ var timer;
91
+ var webSocketWorks = false;
92
+ var isWebSocketFirst = function isWebSocketFirst() {
93
+ var _client$io$opts$trans;
94
+ return !webSocketWorks && ((_client$io$opts$trans = client.io.opts.transports) === null || _client$io$opts$trans === void 0 ? void 0 : _client$io$opts$trans[0]) === 'websocket';
95
+ };
96
+ var fallbackToPolling = function fallbackToPolling(reason) {
97
+ clearTimeout(timer);
98
+ if (!isWebSocketFirst() || typeof navigator !== 'undefined' && navigator.onLine === false) {
99
+ return;
100
+ }
101
+ client.io.opts.transports = ['polling', 'websocket'];
102
+ client.io.opts.tryAllTransports = false;
103
+ analyticsHelper === null || analyticsHelper === void 0 || analyticsHelper.sendActionEvent(EVENT_ACTION.POLLING_FALLBACK, EVENT_STATUS.INFO, {
104
+ url: url,
105
+ reason: reason
106
+ });
107
+ client.disconnect().connect();
108
+ };
109
+ var startTimer = function startTimer() {
110
+ clearTimeout(timer);
111
+ if (isWebSocketFirst()) {
112
+ timer = setTimeout(function () {
113
+ return !client.connected && fallbackToPolling('timeout');
114
+ }, WEBSOCKET_CONNECT_TIMEOUT_MS);
115
+ }
116
+ };
117
+ startTimer();
118
+ client.io.on('reconnect_attempt', startTimer);
119
+ client.on('connect', function () {
120
+ clearTimeout(timer);
121
+ webSocketWorks || (webSocketWorks = client.io.engine.transport.name === 'websocket');
122
+ });
123
+ client.on('connect_error', function (error) {
124
+ if (error.data) {
125
+ var _client$io$opts$trans2;
126
+ clearTimeout(timer);
127
+ webSocketWorks || (webSocketWorks = ((_client$io$opts$trans2 = client.io.opts.transports) === null || _client$io$opts$trans2 === void 0 ? void 0 : _client$io$opts$trans2[0]) === 'websocket');
128
+ } else if (error.type === 'TransportError' || error.message === 'timeout') {
129
+ fallbackToPolling(error.message);
130
+ }
131
+ });
132
+ }
71
133
  export function createSocketIOCollabProvider(config) {
72
134
  return new Provider(_objectSpread(_objectSpread({}, config), {}, {
73
135
  createSocket: createSocketIOSocket
@@ -1,5 +1,5 @@
1
1
  export var name = "@atlaskit/collab-provider";
2
- export var version = "36.0.3";
2
+ export var version = "36.0.5";
3
3
  export var nextMajorVersion = function nextMajorVersion() {
4
4
  return [Number(version.split('.')[0]) + 1, 0, 0].join('.');
5
5
  };
@@ -302,7 +302,8 @@ type StepsRebasedAnalyticsEvent = {
302
302
  type PollingFallbackAnalyticsEvent = {
303
303
  attributes: {
304
304
  eventStatus: EVENT_STATUS.INFO;
305
- url: 'string';
305
+ reason: string;
306
+ url: string;
306
307
  } & BaseActionAnalyticsEventAttributes;
307
308
  eventAction: EVENT_ACTION.POLLING_FALLBACK;
308
309
  };
@@ -3,4 +3,5 @@ import type AnalyticsHelper from './analytics/analytics-helper';
3
3
  import { Provider } from './provider';
4
4
  import type { Config, ProductInformation, InitAndAuthData, AuthCallback } from './types';
5
5
  export declare function createSocketIOSocket(url: string, auth?: AuthCallback | InitAndAuthData, productInfo?: ProductInformation, isPresenceOnly?: boolean, analyticsHelper?: AnalyticsHelper, path?: string, documentAri?: string): Socket;
6
+ export declare const WEBSOCKET_CONNECT_TIMEOUT_MS = 8000;
6
7
  export declare function createSocketIOCollabProvider(config: Omit<Config, 'createSocket'>): Provider;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/collab-provider",
3
- "version": "36.0.4",
3
+ "version": "36.0.6",
4
4
  "description": "A provider for collaborative editing.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -30,7 +30,7 @@
30
30
  "@atlaskit/platform-feature-flags": "^2.2.0",
31
31
  "@atlaskit/prosemirror-collab": "^1.1.0",
32
32
  "@atlaskit/react-ufo": "^8.1.0",
33
- "@atlaskit/tmp-editor-statsig": "^239.0.0",
33
+ "@atlaskit/tmp-editor-statsig": "^240.0.0",
34
34
  "@atlaskit/ufo": "^2.0.0",
35
35
  "@atlaskit/util-service-support": "^7.4.0",
36
36
  "@babel/runtime": "^7.0.0",
@@ -38,7 +38,7 @@
38
38
  "eventemitter2": "^4.1.0",
39
39
  "lodash": "^4.17.21",
40
40
  "prosemirror-changeset": "^2.3.1",
41
- "socket.io-client": "^4.7.5",
41
+ "socket.io-client": "^4.8.4",
42
42
  "uuid": "^11.1.1"
43
43
  },
44
44
  "devDependencies": {
@@ -64,7 +64,7 @@
64
64
  "type": "boolean",
65
65
  "referenceOnly": true
66
66
  },
67
- "platform-editor-presence-websocket-only": {
67
+ "platform_editor_presence_ws_first_with_fallback": {
68
68
  "type": "boolean"
69
69
  },
70
70
  "collab_edit_via_websocket_only_for_gcp": {