@atlaskit/editor-synced-block-provider 4.3.4 → 4.3.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 +14 -0
- package/dist/cjs/clients/block-service/blockSubscription.js +106 -5
- package/dist/cjs/index.js +13 -0
- package/dist/es2019/clients/block-service/blockSubscription.js +102 -3
- package/dist/es2019/index.js +1 -0
- package/dist/esm/clients/block-service/blockSubscription.js +104 -4
- package/dist/esm/index.js +1 -0
- package/dist/types/clients/block-service/blockSubscription.d.ts +12 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types-ts4.5/clients/block-service/blockSubscription.d.ts +12 -0
- package/dist/types-ts4.5/index.d.ts +1 -0
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @atlaskit/editor-synced-block-provider
|
|
2
2
|
|
|
3
|
+
## 4.3.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`540d72eba7200`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/540d72eba7200) -
|
|
8
|
+
Editor-5503: Add connection diagnostics to GraphQL WebSocket errors for improved debugging of
|
|
9
|
+
opaque browser error events
|
|
10
|
+
|
|
11
|
+
## 4.3.5
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies
|
|
16
|
+
|
|
3
17
|
## 4.3.4
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
@@ -1,14 +1,60 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
3
4
|
Object.defineProperty(exports, "__esModule", {
|
|
4
5
|
value: true
|
|
5
6
|
});
|
|
6
|
-
exports.subscribeToBlockUpdates = void 0;
|
|
7
|
+
exports.subscribeToBlockUpdates = exports.getConnectionDiagnosticsSummary = exports.extractGraphQLWSErrorMessage = void 0;
|
|
8
|
+
var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
|
|
7
9
|
var _graphqlWs = require("graphql-ws");
|
|
8
10
|
var _coreUtils = require("@atlaskit/editor-common/core-utils");
|
|
11
|
+
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
|
|
9
12
|
var _utils = require("../../utils/utils");
|
|
10
13
|
var GRAPHQL_WS_ENDPOINT = '/gateway/api/graphql/subscriptions';
|
|
11
14
|
var blockServiceClient = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Tracks the last known WebSocket connection context for diagnostics.
|
|
18
|
+
* Since browser WebSocket errors are intentionally opaque ({isTrusted: true}),
|
|
19
|
+
* we capture lifecycle events to provide meaningful context when errors occur.
|
|
20
|
+
*/
|
|
21
|
+
var connectionDiagnostics = {
|
|
22
|
+
/** Whether the last connection attempt was a retry */
|
|
23
|
+
wasRetry: false,
|
|
24
|
+
/** Timestamp of the last successful connection */
|
|
25
|
+
lastConnectedAt: 0,
|
|
26
|
+
/** The close code from the most recent WebSocket CloseEvent, see (https://websocket.org/reference/close-codes/)*/
|
|
27
|
+
lastCloseCode: 0,
|
|
28
|
+
/** The close reason from the most recent WebSocket CloseEvent */
|
|
29
|
+
lastCloseReason: '',
|
|
30
|
+
/** Whether the last close was clean */
|
|
31
|
+
lastCloseWasClean: true,
|
|
32
|
+
/** Current connection state */
|
|
33
|
+
state: 'idle',
|
|
34
|
+
/** Number of consecutive connection failures */
|
|
35
|
+
consecutiveFailures: 0
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns a diagnostic summary string from the last known connection state.
|
|
40
|
+
* This provides context that the opaque WebSocket error events cannot.
|
|
41
|
+
*/
|
|
42
|
+
var getConnectionDiagnosticsSummary = exports.getConnectionDiagnosticsSummary = function getConnectionDiagnosticsSummary() {
|
|
43
|
+
var parts = [];
|
|
44
|
+
if (connectionDiagnostics.lastCloseCode !== 0) {
|
|
45
|
+
parts.push("lastCloseCode=".concat(connectionDiagnostics.lastCloseCode));
|
|
46
|
+
parts.push("lastCloseReason=\"".concat(connectionDiagnostics.lastCloseReason || 'none', "\""));
|
|
47
|
+
parts.push("wasClean=".concat(connectionDiagnostics.lastCloseWasClean));
|
|
48
|
+
}
|
|
49
|
+
parts.push("state=".concat(connectionDiagnostics.state));
|
|
50
|
+
parts.push("wasRetry=".concat(connectionDiagnostics.wasRetry));
|
|
51
|
+
parts.push("consecutiveFailures=".concat(connectionDiagnostics.consecutiveFailures));
|
|
52
|
+
if (connectionDiagnostics.lastConnectedAt > 0) {
|
|
53
|
+
var elapsed = Date.now() - connectionDiagnostics.lastConnectedAt;
|
|
54
|
+
parts.push("timeSinceLastConnection=".concat(elapsed, "ms"));
|
|
55
|
+
}
|
|
56
|
+
return parts.join(', ');
|
|
57
|
+
};
|
|
12
58
|
var getBlockServiceClient = function getBlockServiceClient() {
|
|
13
59
|
// Don't create client during SSR
|
|
14
60
|
if ((0, _coreUtils.isSSR)()) {
|
|
@@ -20,12 +66,63 @@ var getBlockServiceClient = function getBlockServiceClient() {
|
|
|
20
66
|
blockServiceClient = (0, _graphqlWs.createClient)({
|
|
21
67
|
url: wsUrl,
|
|
22
68
|
lazy: true,
|
|
23
|
-
retryAttempts: 3
|
|
69
|
+
retryAttempts: 3,
|
|
70
|
+
on: (0, _platformFeatureFlags.fg)('platform_synced_block_add_info_web_socket_error') ? {
|
|
71
|
+
connecting: function connecting(isRetry) {
|
|
72
|
+
connectionDiagnostics.wasRetry = isRetry;
|
|
73
|
+
connectionDiagnostics.state = 'connecting';
|
|
74
|
+
},
|
|
75
|
+
connected: function connected(_socket, _payload, wasRetry) {
|
|
76
|
+
connectionDiagnostics.state = 'connected';
|
|
77
|
+
connectionDiagnostics.lastConnectedAt = Date.now();
|
|
78
|
+
connectionDiagnostics.wasRetry = wasRetry;
|
|
79
|
+
connectionDiagnostics.consecutiveFailures = 0;
|
|
80
|
+
},
|
|
81
|
+
closed: function closed(event) {
|
|
82
|
+
var _closeEvent$code, _closeEvent$reason, _closeEvent$wasClean;
|
|
83
|
+
connectionDiagnostics.state = 'closed';
|
|
84
|
+
var closeEvent = event;
|
|
85
|
+
connectionDiagnostics.lastCloseCode = (_closeEvent$code = closeEvent.code) !== null && _closeEvent$code !== void 0 ? _closeEvent$code : 0;
|
|
86
|
+
connectionDiagnostics.lastCloseReason = (_closeEvent$reason = closeEvent.reason) !== null && _closeEvent$reason !== void 0 ? _closeEvent$reason : '';
|
|
87
|
+
connectionDiagnostics.lastCloseWasClean = (_closeEvent$wasClean = closeEvent.wasClean) !== null && _closeEvent$wasClean !== void 0 ? _closeEvent$wasClean : false;
|
|
88
|
+
},
|
|
89
|
+
error: function error() {
|
|
90
|
+
connectionDiagnostics.state = 'error';
|
|
91
|
+
connectionDiagnostics.consecutiveFailures += 1;
|
|
92
|
+
}
|
|
93
|
+
} : undefined
|
|
24
94
|
});
|
|
25
95
|
}
|
|
26
96
|
return blockServiceClient;
|
|
27
97
|
};
|
|
28
98
|
var SUBSCRIPTION_QUERY = "\nsubscription EDITOR_SYNCED_BLOCK_ON_BLOCK_UPDATED($resourceId: ID!) {\n\tblockService_onBlockUpdated(resourceId: $resourceId) {\n\t\tblockAri\n\t\tblockInstanceId\n\t\tcontent\n\t\tcontentUpdatedAt\n\t\tcreatedAt\n\t\tcreatedBy\n\t\tdeletionReason\n\t\tproduct\n\t\tsourceAri\n\t\tstatus\n\t}\n}\n";
|
|
99
|
+
/**
|
|
100
|
+
* Extracts a meaningful error message from the error: GraphQL WebSocket Error: {"isTrusted":true}
|
|
101
|
+
*
|
|
102
|
+
* @param error - The error passed to the sink's error callback
|
|
103
|
+
* @returns A descriptive error message string
|
|
104
|
+
*/
|
|
105
|
+
var extractGraphQLWSErrorMessage = exports.extractGraphQLWSErrorMessage = function extractGraphQLWSErrorMessage(error) {
|
|
106
|
+
var diagnostics = getConnectionDiagnosticsSummary();
|
|
107
|
+
|
|
108
|
+
// Raw Event from WebSocket.onerror — browsers don't expose error details
|
|
109
|
+
// for security reasons, so {isTrusted: true} is all we get.
|
|
110
|
+
if ((0, _typeof2.default)(error) === 'object' && error !== null && 'isTrusted' in error) {
|
|
111
|
+
return "GraphQL WebSocket connection error (browser restricted error details). Diagnostics: ".concat(diagnostics);
|
|
112
|
+
}
|
|
113
|
+
if (error instanceof Error) {
|
|
114
|
+
return error.message;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fallback: try to stringify whatever we got
|
|
118
|
+
try {
|
|
119
|
+
var serialized = JSON.stringify(error);
|
|
120
|
+
return "GraphQL subscription error: ".concat(serialized);
|
|
121
|
+
} catch (_unused) {
|
|
122
|
+
return 'GraphQL subscription error: unknown error';
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
29
126
|
/**
|
|
30
127
|
* Extracts the resourceId from a block ARI.
|
|
31
128
|
* Block ARI format: ari:cloud:blocks:<cloudId>:synced-block/<resourceId>
|
|
@@ -65,7 +162,7 @@ var parseSubscriptionPayload = function parseSubscriptionPayload(payload) {
|
|
|
65
162
|
sourceAri: payload.sourceAri,
|
|
66
163
|
status: payload.status
|
|
67
164
|
};
|
|
68
|
-
} catch (
|
|
165
|
+
} catch (_unused2) {
|
|
69
166
|
return null;
|
|
70
167
|
}
|
|
71
168
|
};
|
|
@@ -111,8 +208,12 @@ var subscribeToBlockUpdates = exports.subscribeToBlockUpdates = function subscri
|
|
|
111
208
|
};
|
|
112
209
|
return error;
|
|
113
210
|
}(function (error) {
|
|
114
|
-
|
|
115
|
-
|
|
211
|
+
if ((0, _platformFeatureFlags.fg)('platform_synced_block_add_info_web_socket_error')) {
|
|
212
|
+
onError === null || onError === void 0 || onError(new Error(extractGraphQLWSErrorMessage(error)));
|
|
213
|
+
} else {
|
|
214
|
+
var errorMessage = error instanceof Error ? error.message : 'GraphQL subscription error';
|
|
215
|
+
onError === null || onError === void 0 || onError(new Error(errorMessage));
|
|
216
|
+
}
|
|
116
217
|
}),
|
|
117
218
|
complete: function complete() {
|
|
118
219
|
// Subscription completed
|
package/dist/cjs/index.js
CHANGED
|
@@ -87,6 +87,12 @@ Object.defineProperty(exports, "createSyncBlockNode", {
|
|
|
87
87
|
return _utils.createSyncBlockNode;
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
|
+
Object.defineProperty(exports, "extractGraphQLWSErrorMessage", {
|
|
91
|
+
enumerable: true,
|
|
92
|
+
get: function get() {
|
|
93
|
+
return _blockSubscription.extractGraphQLWSErrorMessage;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
90
96
|
Object.defineProperty(exports, "extractResourceIdFromBlockAri", {
|
|
91
97
|
enumerable: true,
|
|
92
98
|
get: function get() {
|
|
@@ -135,6 +141,12 @@ Object.defineProperty(exports, "getConfluencePageAri", {
|
|
|
135
141
|
return _ari2.getConfluencePageAri;
|
|
136
142
|
}
|
|
137
143
|
});
|
|
144
|
+
Object.defineProperty(exports, "getConnectionDiagnosticsSummary", {
|
|
145
|
+
enumerable: true,
|
|
146
|
+
get: function get() {
|
|
147
|
+
return _blockSubscription.getConnectionDiagnosticsSummary;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
138
150
|
Object.defineProperty(exports, "getContentIdAndProductFromResourceId", {
|
|
139
151
|
enumerable: true,
|
|
140
152
|
get: function get() {
|
|
@@ -262,6 +274,7 @@ var _useFetchSyncBlockTitle = require("./hooks/useFetchSyncBlockTitle");
|
|
|
262
274
|
var _useHandleContentChanges = require("./hooks/useHandleContentChanges");
|
|
263
275
|
var _ari = require("./clients/block-service/ari");
|
|
264
276
|
var _blockService = require("./clients/block-service/blockService");
|
|
277
|
+
var _blockSubscription = require("./clients/block-service/blockSubscription");
|
|
265
278
|
var _ari2 = require("./clients/confluence/ari");
|
|
266
279
|
var _fetchMediaToken = require("./clients/confluence/fetchMediaToken");
|
|
267
280
|
var _ari3 = require("./clients/jira/ari");
|
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
import { createClient } from 'graphql-ws';
|
|
2
2
|
import { isSSR } from '@atlaskit/editor-common/core-utils';
|
|
3
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
3
4
|
import { convertContentUpdatedAt } from '../../utils/utils';
|
|
4
5
|
const GRAPHQL_WS_ENDPOINT = '/gateway/api/graphql/subscriptions';
|
|
5
6
|
let blockServiceClient = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tracks the last known WebSocket connection context for diagnostics.
|
|
10
|
+
* Since browser WebSocket errors are intentionally opaque ({isTrusted: true}),
|
|
11
|
+
* we capture lifecycle events to provide meaningful context when errors occur.
|
|
12
|
+
*/
|
|
13
|
+
const connectionDiagnostics = {
|
|
14
|
+
/** Whether the last connection attempt was a retry */
|
|
15
|
+
wasRetry: false,
|
|
16
|
+
/** Timestamp of the last successful connection */
|
|
17
|
+
lastConnectedAt: 0,
|
|
18
|
+
/** The close code from the most recent WebSocket CloseEvent, see (https://websocket.org/reference/close-codes/)*/
|
|
19
|
+
lastCloseCode: 0,
|
|
20
|
+
/** The close reason from the most recent WebSocket CloseEvent */
|
|
21
|
+
lastCloseReason: '',
|
|
22
|
+
/** Whether the last close was clean */
|
|
23
|
+
lastCloseWasClean: true,
|
|
24
|
+
/** Current connection state */
|
|
25
|
+
state: 'idle',
|
|
26
|
+
/** Number of consecutive connection failures */
|
|
27
|
+
consecutiveFailures: 0
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns a diagnostic summary string from the last known connection state.
|
|
32
|
+
* This provides context that the opaque WebSocket error events cannot.
|
|
33
|
+
*/
|
|
34
|
+
export const getConnectionDiagnosticsSummary = () => {
|
|
35
|
+
const parts = [];
|
|
36
|
+
if (connectionDiagnostics.lastCloseCode !== 0) {
|
|
37
|
+
parts.push(`lastCloseCode=${connectionDiagnostics.lastCloseCode}`);
|
|
38
|
+
parts.push(`lastCloseReason="${connectionDiagnostics.lastCloseReason || 'none'}"`);
|
|
39
|
+
parts.push(`wasClean=${connectionDiagnostics.lastCloseWasClean}`);
|
|
40
|
+
}
|
|
41
|
+
parts.push(`state=${connectionDiagnostics.state}`);
|
|
42
|
+
parts.push(`wasRetry=${connectionDiagnostics.wasRetry}`);
|
|
43
|
+
parts.push(`consecutiveFailures=${connectionDiagnostics.consecutiveFailures}`);
|
|
44
|
+
if (connectionDiagnostics.lastConnectedAt > 0) {
|
|
45
|
+
const elapsed = Date.now() - connectionDiagnostics.lastConnectedAt;
|
|
46
|
+
parts.push(`timeSinceLastConnection=${elapsed}ms`);
|
|
47
|
+
}
|
|
48
|
+
return parts.join(', ');
|
|
49
|
+
};
|
|
6
50
|
const getBlockServiceClient = () => {
|
|
7
51
|
// Don't create client during SSR
|
|
8
52
|
if (isSSR()) {
|
|
@@ -14,7 +58,31 @@ const getBlockServiceClient = () => {
|
|
|
14
58
|
blockServiceClient = createClient({
|
|
15
59
|
url: wsUrl,
|
|
16
60
|
lazy: true,
|
|
17
|
-
retryAttempts: 3
|
|
61
|
+
retryAttempts: 3,
|
|
62
|
+
on: fg('platform_synced_block_add_info_web_socket_error') ? {
|
|
63
|
+
connecting: isRetry => {
|
|
64
|
+
connectionDiagnostics.wasRetry = isRetry;
|
|
65
|
+
connectionDiagnostics.state = 'connecting';
|
|
66
|
+
},
|
|
67
|
+
connected: (_socket, _payload, wasRetry) => {
|
|
68
|
+
connectionDiagnostics.state = 'connected';
|
|
69
|
+
connectionDiagnostics.lastConnectedAt = Date.now();
|
|
70
|
+
connectionDiagnostics.wasRetry = wasRetry;
|
|
71
|
+
connectionDiagnostics.consecutiveFailures = 0;
|
|
72
|
+
},
|
|
73
|
+
closed: event => {
|
|
74
|
+
var _closeEvent$code, _closeEvent$reason, _closeEvent$wasClean;
|
|
75
|
+
connectionDiagnostics.state = 'closed';
|
|
76
|
+
const closeEvent = event;
|
|
77
|
+
connectionDiagnostics.lastCloseCode = (_closeEvent$code = closeEvent.code) !== null && _closeEvent$code !== void 0 ? _closeEvent$code : 0;
|
|
78
|
+
connectionDiagnostics.lastCloseReason = (_closeEvent$reason = closeEvent.reason) !== null && _closeEvent$reason !== void 0 ? _closeEvent$reason : '';
|
|
79
|
+
connectionDiagnostics.lastCloseWasClean = (_closeEvent$wasClean = closeEvent.wasClean) !== null && _closeEvent$wasClean !== void 0 ? _closeEvent$wasClean : false;
|
|
80
|
+
},
|
|
81
|
+
error: () => {
|
|
82
|
+
connectionDiagnostics.state = 'error';
|
|
83
|
+
connectionDiagnostics.consecutiveFailures += 1;
|
|
84
|
+
}
|
|
85
|
+
} : undefined
|
|
18
86
|
});
|
|
19
87
|
}
|
|
20
88
|
return blockServiceClient;
|
|
@@ -35,6 +103,33 @@ subscription EDITOR_SYNCED_BLOCK_ON_BLOCK_UPDATED($resourceId: ID!) {
|
|
|
35
103
|
}
|
|
36
104
|
}
|
|
37
105
|
`;
|
|
106
|
+
/**
|
|
107
|
+
* Extracts a meaningful error message from the error: GraphQL WebSocket Error: {"isTrusted":true}
|
|
108
|
+
*
|
|
109
|
+
* @param error - The error passed to the sink's error callback
|
|
110
|
+
* @returns A descriptive error message string
|
|
111
|
+
*/
|
|
112
|
+
export const extractGraphQLWSErrorMessage = error => {
|
|
113
|
+
const diagnostics = getConnectionDiagnosticsSummary();
|
|
114
|
+
|
|
115
|
+
// Raw Event from WebSocket.onerror — browsers don't expose error details
|
|
116
|
+
// for security reasons, so {isTrusted: true} is all we get.
|
|
117
|
+
if (typeof error === 'object' && error !== null && 'isTrusted' in error) {
|
|
118
|
+
return `GraphQL WebSocket connection error (browser restricted error details). Diagnostics: ${diagnostics}`;
|
|
119
|
+
}
|
|
120
|
+
if (error instanceof Error) {
|
|
121
|
+
return error.message;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Fallback: try to stringify whatever we got
|
|
125
|
+
try {
|
|
126
|
+
const serialized = JSON.stringify(error);
|
|
127
|
+
return `GraphQL subscription error: ${serialized}`;
|
|
128
|
+
} catch {
|
|
129
|
+
return 'GraphQL subscription error: unknown error';
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
38
133
|
/**
|
|
39
134
|
* Extracts the resourceId from a block ARI.
|
|
40
135
|
* Block ARI format: ari:cloud:blocks:<cloudId>:synced-block/<resourceId>
|
|
@@ -112,8 +207,12 @@ export const subscribeToBlockUpdates = (blockAri, onData, onError) => {
|
|
|
112
207
|
}
|
|
113
208
|
},
|
|
114
209
|
error: error => {
|
|
115
|
-
|
|
116
|
-
|
|
210
|
+
if (fg('platform_synced_block_add_info_web_socket_error')) {
|
|
211
|
+
onError === null || onError === void 0 ? void 0 : onError(new Error(extractGraphQLWSErrorMessage(error)));
|
|
212
|
+
} else {
|
|
213
|
+
const errorMessage = error instanceof Error ? error.message : 'GraphQL subscription error';
|
|
214
|
+
onError === null || onError === void 0 ? void 0 : onError(new Error(errorMessage));
|
|
215
|
+
}
|
|
117
216
|
},
|
|
118
217
|
complete: () => {
|
|
119
218
|
// Subscription completed
|
package/dist/es2019/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export { useHandleContentChanges } from './hooks/useHandleContentChanges';
|
|
|
11
11
|
// clients
|
|
12
12
|
export { generateBlockAri, generateBlockAriFromReference, getLocalIdFromBlockResourceId } from './clients/block-service/ari';
|
|
13
13
|
export { BlockError, updateSyncedBlocks } from './clients/block-service/blockService';
|
|
14
|
+
export { extractGraphQLWSErrorMessage, getConnectionDiagnosticsSummary } from './clients/block-service/blockSubscription';
|
|
14
15
|
export { getConfluencePageAri, getPageIdAndTypeFromConfluencePageAri } from './clients/confluence/ari';
|
|
15
16
|
export { fetchMediaToken } from './clients/confluence/fetchMediaToken';
|
|
16
17
|
export { getJiraWorkItemAri, getJiraWorkItemIdFromAri } from './clients/jira/ari';
|
|
@@ -1,8 +1,53 @@
|
|
|
1
|
+
import _typeof from "@babel/runtime/helpers/typeof";
|
|
1
2
|
import { createClient } from 'graphql-ws';
|
|
2
3
|
import { isSSR } from '@atlaskit/editor-common/core-utils';
|
|
4
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
3
5
|
import { convertContentUpdatedAt } from '../../utils/utils';
|
|
4
6
|
var GRAPHQL_WS_ENDPOINT = '/gateway/api/graphql/subscriptions';
|
|
5
7
|
var blockServiceClient = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tracks the last known WebSocket connection context for diagnostics.
|
|
11
|
+
* Since browser WebSocket errors are intentionally opaque ({isTrusted: true}),
|
|
12
|
+
* we capture lifecycle events to provide meaningful context when errors occur.
|
|
13
|
+
*/
|
|
14
|
+
var connectionDiagnostics = {
|
|
15
|
+
/** Whether the last connection attempt was a retry */
|
|
16
|
+
wasRetry: false,
|
|
17
|
+
/** Timestamp of the last successful connection */
|
|
18
|
+
lastConnectedAt: 0,
|
|
19
|
+
/** The close code from the most recent WebSocket CloseEvent, see (https://websocket.org/reference/close-codes/)*/
|
|
20
|
+
lastCloseCode: 0,
|
|
21
|
+
/** The close reason from the most recent WebSocket CloseEvent */
|
|
22
|
+
lastCloseReason: '',
|
|
23
|
+
/** Whether the last close was clean */
|
|
24
|
+
lastCloseWasClean: true,
|
|
25
|
+
/** Current connection state */
|
|
26
|
+
state: 'idle',
|
|
27
|
+
/** Number of consecutive connection failures */
|
|
28
|
+
consecutiveFailures: 0
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns a diagnostic summary string from the last known connection state.
|
|
33
|
+
* This provides context that the opaque WebSocket error events cannot.
|
|
34
|
+
*/
|
|
35
|
+
export var getConnectionDiagnosticsSummary = function getConnectionDiagnosticsSummary() {
|
|
36
|
+
var parts = [];
|
|
37
|
+
if (connectionDiagnostics.lastCloseCode !== 0) {
|
|
38
|
+
parts.push("lastCloseCode=".concat(connectionDiagnostics.lastCloseCode));
|
|
39
|
+
parts.push("lastCloseReason=\"".concat(connectionDiagnostics.lastCloseReason || 'none', "\""));
|
|
40
|
+
parts.push("wasClean=".concat(connectionDiagnostics.lastCloseWasClean));
|
|
41
|
+
}
|
|
42
|
+
parts.push("state=".concat(connectionDiagnostics.state));
|
|
43
|
+
parts.push("wasRetry=".concat(connectionDiagnostics.wasRetry));
|
|
44
|
+
parts.push("consecutiveFailures=".concat(connectionDiagnostics.consecutiveFailures));
|
|
45
|
+
if (connectionDiagnostics.lastConnectedAt > 0) {
|
|
46
|
+
var elapsed = Date.now() - connectionDiagnostics.lastConnectedAt;
|
|
47
|
+
parts.push("timeSinceLastConnection=".concat(elapsed, "ms"));
|
|
48
|
+
}
|
|
49
|
+
return parts.join(', ');
|
|
50
|
+
};
|
|
6
51
|
var getBlockServiceClient = function getBlockServiceClient() {
|
|
7
52
|
// Don't create client during SSR
|
|
8
53
|
if (isSSR()) {
|
|
@@ -14,12 +59,63 @@ var getBlockServiceClient = function getBlockServiceClient() {
|
|
|
14
59
|
blockServiceClient = createClient({
|
|
15
60
|
url: wsUrl,
|
|
16
61
|
lazy: true,
|
|
17
|
-
retryAttempts: 3
|
|
62
|
+
retryAttempts: 3,
|
|
63
|
+
on: fg('platform_synced_block_add_info_web_socket_error') ? {
|
|
64
|
+
connecting: function connecting(isRetry) {
|
|
65
|
+
connectionDiagnostics.wasRetry = isRetry;
|
|
66
|
+
connectionDiagnostics.state = 'connecting';
|
|
67
|
+
},
|
|
68
|
+
connected: function connected(_socket, _payload, wasRetry) {
|
|
69
|
+
connectionDiagnostics.state = 'connected';
|
|
70
|
+
connectionDiagnostics.lastConnectedAt = Date.now();
|
|
71
|
+
connectionDiagnostics.wasRetry = wasRetry;
|
|
72
|
+
connectionDiagnostics.consecutiveFailures = 0;
|
|
73
|
+
},
|
|
74
|
+
closed: function closed(event) {
|
|
75
|
+
var _closeEvent$code, _closeEvent$reason, _closeEvent$wasClean;
|
|
76
|
+
connectionDiagnostics.state = 'closed';
|
|
77
|
+
var closeEvent = event;
|
|
78
|
+
connectionDiagnostics.lastCloseCode = (_closeEvent$code = closeEvent.code) !== null && _closeEvent$code !== void 0 ? _closeEvent$code : 0;
|
|
79
|
+
connectionDiagnostics.lastCloseReason = (_closeEvent$reason = closeEvent.reason) !== null && _closeEvent$reason !== void 0 ? _closeEvent$reason : '';
|
|
80
|
+
connectionDiagnostics.lastCloseWasClean = (_closeEvent$wasClean = closeEvent.wasClean) !== null && _closeEvent$wasClean !== void 0 ? _closeEvent$wasClean : false;
|
|
81
|
+
},
|
|
82
|
+
error: function error() {
|
|
83
|
+
connectionDiagnostics.state = 'error';
|
|
84
|
+
connectionDiagnostics.consecutiveFailures += 1;
|
|
85
|
+
}
|
|
86
|
+
} : undefined
|
|
18
87
|
});
|
|
19
88
|
}
|
|
20
89
|
return blockServiceClient;
|
|
21
90
|
};
|
|
22
91
|
var SUBSCRIPTION_QUERY = "\nsubscription EDITOR_SYNCED_BLOCK_ON_BLOCK_UPDATED($resourceId: ID!) {\n\tblockService_onBlockUpdated(resourceId: $resourceId) {\n\t\tblockAri\n\t\tblockInstanceId\n\t\tcontent\n\t\tcontentUpdatedAt\n\t\tcreatedAt\n\t\tcreatedBy\n\t\tdeletionReason\n\t\tproduct\n\t\tsourceAri\n\t\tstatus\n\t}\n}\n";
|
|
92
|
+
/**
|
|
93
|
+
* Extracts a meaningful error message from the error: GraphQL WebSocket Error: {"isTrusted":true}
|
|
94
|
+
*
|
|
95
|
+
* @param error - The error passed to the sink's error callback
|
|
96
|
+
* @returns A descriptive error message string
|
|
97
|
+
*/
|
|
98
|
+
export var extractGraphQLWSErrorMessage = function extractGraphQLWSErrorMessage(error) {
|
|
99
|
+
var diagnostics = getConnectionDiagnosticsSummary();
|
|
100
|
+
|
|
101
|
+
// Raw Event from WebSocket.onerror — browsers don't expose error details
|
|
102
|
+
// for security reasons, so {isTrusted: true} is all we get.
|
|
103
|
+
if (_typeof(error) === 'object' && error !== null && 'isTrusted' in error) {
|
|
104
|
+
return "GraphQL WebSocket connection error (browser restricted error details). Diagnostics: ".concat(diagnostics);
|
|
105
|
+
}
|
|
106
|
+
if (error instanceof Error) {
|
|
107
|
+
return error.message;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fallback: try to stringify whatever we got
|
|
111
|
+
try {
|
|
112
|
+
var serialized = JSON.stringify(error);
|
|
113
|
+
return "GraphQL subscription error: ".concat(serialized);
|
|
114
|
+
} catch (_unused) {
|
|
115
|
+
return 'GraphQL subscription error: unknown error';
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
23
119
|
/**
|
|
24
120
|
* Extracts the resourceId from a block ARI.
|
|
25
121
|
* Block ARI format: ari:cloud:blocks:<cloudId>:synced-block/<resourceId>
|
|
@@ -59,7 +155,7 @@ var parseSubscriptionPayload = function parseSubscriptionPayload(payload) {
|
|
|
59
155
|
sourceAri: payload.sourceAri,
|
|
60
156
|
status: payload.status
|
|
61
157
|
};
|
|
62
|
-
} catch (
|
|
158
|
+
} catch (_unused2) {
|
|
63
159
|
return null;
|
|
64
160
|
}
|
|
65
161
|
};
|
|
@@ -105,8 +201,12 @@ export var subscribeToBlockUpdates = function subscribeToBlockUpdates(blockAri,
|
|
|
105
201
|
};
|
|
106
202
|
return error;
|
|
107
203
|
}(function (error) {
|
|
108
|
-
|
|
109
|
-
|
|
204
|
+
if (fg('platform_synced_block_add_info_web_socket_error')) {
|
|
205
|
+
onError === null || onError === void 0 || onError(new Error(extractGraphQLWSErrorMessage(error)));
|
|
206
|
+
} else {
|
|
207
|
+
var errorMessage = error instanceof Error ? error.message : 'GraphQL subscription error';
|
|
208
|
+
onError === null || onError === void 0 || onError(new Error(errorMessage));
|
|
209
|
+
}
|
|
110
210
|
}),
|
|
111
211
|
complete: function complete() {
|
|
112
212
|
// Subscription completed
|
package/dist/esm/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export { useHandleContentChanges } from './hooks/useHandleContentChanges';
|
|
|
11
11
|
// clients
|
|
12
12
|
export { generateBlockAri, generateBlockAriFromReference, getLocalIdFromBlockResourceId } from './clients/block-service/ari';
|
|
13
13
|
export { BlockError, updateSyncedBlocks } from './clients/block-service/blockService';
|
|
14
|
+
export { extractGraphQLWSErrorMessage, getConnectionDiagnosticsSummary } from './clients/block-service/blockSubscription';
|
|
14
15
|
export { getConfluencePageAri, getPageIdAndTypeFromConfluencePageAri } from './clients/confluence/ari';
|
|
15
16
|
export { fetchMediaToken } from './clients/confluence/fetchMediaToken';
|
|
16
17
|
export { getJiraWorkItemAri, getJiraWorkItemIdFromAri } from './clients/jira/ari';
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ADFEntity } from '@atlaskit/adf-utils/types';
|
|
2
2
|
import type { SyncBlockProduct } from '../../common/types';
|
|
3
|
+
/**
|
|
4
|
+
* Returns a diagnostic summary string from the last known connection state.
|
|
5
|
+
* This provides context that the opaque WebSocket error events cannot.
|
|
6
|
+
*/
|
|
7
|
+
export declare const getConnectionDiagnosticsSummary: () => string;
|
|
3
8
|
export type BlockSubscriptionPayload = {
|
|
4
9
|
blockAri: string;
|
|
5
10
|
blockInstanceId: string;
|
|
@@ -27,6 +32,13 @@ export type ParsedBlockSubscriptionData = {
|
|
|
27
32
|
type SubscriptionCallback = (data: ParsedBlockSubscriptionData) => void;
|
|
28
33
|
type ErrorCallback = (error: Error) => void;
|
|
29
34
|
type Unsubscribe = () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Extracts a meaningful error message from the error: GraphQL WebSocket Error: {"isTrusted":true}
|
|
37
|
+
*
|
|
38
|
+
* @param error - The error passed to the sink's error callback
|
|
39
|
+
* @returns A descriptive error message string
|
|
40
|
+
*/
|
|
41
|
+
export declare const extractGraphQLWSErrorMessage: (error: unknown) => string;
|
|
30
42
|
/**
|
|
31
43
|
* Creates a GraphQL subscription to block updates using the shared graphql-ws client.
|
|
32
44
|
*
|
package/dist/types/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { useHandleContentChanges } from './hooks/useHandleContentChanges';
|
|
|
7
7
|
export { generateBlockAri, generateBlockAriFromReference, getLocalIdFromBlockResourceId, } from './clients/block-service/ari';
|
|
8
8
|
export type { BlockContentResponse, BatchRetrieveSyncedBlocksResponse, ErrorResponse, } from './clients/block-service/blockService';
|
|
9
9
|
export { BlockError, updateSyncedBlocks } from './clients/block-service/blockService';
|
|
10
|
+
export { extractGraphQLWSErrorMessage, getConnectionDiagnosticsSummary, } from './clients/block-service/blockSubscription';
|
|
10
11
|
export { getConfluencePageAri, getPageIdAndTypeFromConfluencePageAri, } from './clients/confluence/ari';
|
|
11
12
|
export { fetchMediaToken, type TokenData, type ConfigData, } from './clients/confluence/fetchMediaToken';
|
|
12
13
|
export { getJiraWorkItemAri, getJiraWorkItemIdFromAri } from './clients/jira/ari';
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ADFEntity } from '@atlaskit/adf-utils/types';
|
|
2
2
|
import type { SyncBlockProduct } from '../../common/types';
|
|
3
|
+
/**
|
|
4
|
+
* Returns a diagnostic summary string from the last known connection state.
|
|
5
|
+
* This provides context that the opaque WebSocket error events cannot.
|
|
6
|
+
*/
|
|
7
|
+
export declare const getConnectionDiagnosticsSummary: () => string;
|
|
3
8
|
export type BlockSubscriptionPayload = {
|
|
4
9
|
blockAri: string;
|
|
5
10
|
blockInstanceId: string;
|
|
@@ -27,6 +32,13 @@ export type ParsedBlockSubscriptionData = {
|
|
|
27
32
|
type SubscriptionCallback = (data: ParsedBlockSubscriptionData) => void;
|
|
28
33
|
type ErrorCallback = (error: Error) => void;
|
|
29
34
|
type Unsubscribe = () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Extracts a meaningful error message from the error: GraphQL WebSocket Error: {"isTrusted":true}
|
|
37
|
+
*
|
|
38
|
+
* @param error - The error passed to the sink's error callback
|
|
39
|
+
* @returns A descriptive error message string
|
|
40
|
+
*/
|
|
41
|
+
export declare const extractGraphQLWSErrorMessage: (error: unknown) => string;
|
|
30
42
|
/**
|
|
31
43
|
* Creates a GraphQL subscription to block updates using the shared graphql-ws client.
|
|
32
44
|
*
|
|
@@ -7,6 +7,7 @@ export { useHandleContentChanges } from './hooks/useHandleContentChanges';
|
|
|
7
7
|
export { generateBlockAri, generateBlockAriFromReference, getLocalIdFromBlockResourceId, } from './clients/block-service/ari';
|
|
8
8
|
export type { BlockContentResponse, BatchRetrieveSyncedBlocksResponse, ErrorResponse, } from './clients/block-service/blockService';
|
|
9
9
|
export { BlockError, updateSyncedBlocks } from './clients/block-service/blockService';
|
|
10
|
+
export { extractGraphQLWSErrorMessage, getConnectionDiagnosticsSummary, } from './clients/block-service/blockSubscription';
|
|
10
11
|
export { getConfluencePageAri, getPageIdAndTypeFromConfluencePageAri, } from './clients/confluence/ari';
|
|
11
12
|
export { fetchMediaToken, type TokenData, type ConfigData, } from './clients/confluence/fetchMediaToken';
|
|
12
13
|
export { getJiraWorkItemAri, getJiraWorkItemIdFromAri } from './clients/jira/ari';
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@atlaskit/editor-prosemirror": "^7.3.0",
|
|
30
30
|
"@atlaskit/node-data-provider": "^9.0.0",
|
|
31
31
|
"@atlaskit/platform-feature-flags": "^1.1.0",
|
|
32
|
-
"@atlaskit/tmp-editor-statsig": "^
|
|
32
|
+
"@atlaskit/tmp-editor-statsig": "^55.0.0",
|
|
33
33
|
"@babel/runtime": "^7.0.0",
|
|
34
34
|
"@compiled/react": "^0.20.0",
|
|
35
35
|
"graphql-ws": "^5.14.2",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"name": "@atlaskit/editor-synced-block-provider",
|
|
84
|
-
"version": "4.3.
|
|
84
|
+
"version": "4.3.6",
|
|
85
85
|
"description": "Synced Block Provider for @atlaskit/editor-plugin-synced-block",
|
|
86
86
|
"author": "Atlassian Pty Ltd",
|
|
87
87
|
"license": "Apache-2.0",
|
|
@@ -97,6 +97,9 @@
|
|
|
97
97
|
},
|
|
98
98
|
"platform_synced_block_patch_8": {
|
|
99
99
|
"type": "boolean"
|
|
100
|
+
},
|
|
101
|
+
"platform_synced_block_add_info_web_socket_error": {
|
|
102
|
+
"type": "boolean"
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
}
|