@usearete/sdk 0.22.4 → 0.23.1
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/dist/index.cjs +216 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +67 -3
- package/dist/index.esm.js +216 -57
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -10,6 +10,10 @@ const DEFAULT_CONFIG = {
|
|
|
10
10
|
maxReconnectAttempts: 5,
|
|
11
11
|
maxEntriesPerView: DEFAULT_MAX_ENTRIES_PER_VIEW,
|
|
12
12
|
};
|
|
13
|
+
/** True for the session refusals that no retry or reconnect can resolve. */
|
|
14
|
+
function isStackVersionRefusalCode(code) {
|
|
15
|
+
return code === 'STACK_VERSION_RETIRED' || code === 'STACK_VERSION_UNKNOWN';
|
|
16
|
+
}
|
|
13
17
|
const REPLAY_ERROR_CODES = {
|
|
14
18
|
'cursor-expired': true,
|
|
15
19
|
'cursor-epoch-changed': true,
|
|
@@ -48,41 +52,66 @@ class AreteError extends Error {
|
|
|
48
52
|
* collapsing them into a generic failure would destroy the only distinction.
|
|
49
53
|
*/
|
|
50
54
|
function parseErrorCode(errorCode) {
|
|
51
|
-
const codeMap = {
|
|
52
|
-
'token-missing': 'TOKEN_MISSING',
|
|
53
|
-
'token-expired': 'TOKEN_EXPIRED',
|
|
54
|
-
'token-invalid-signature': 'TOKEN_INVALID_SIGNATURE',
|
|
55
|
-
'token-invalid-format': 'TOKEN_INVALID_FORMAT',
|
|
56
|
-
'token-invalid-issuer': 'TOKEN_INVALID_ISSUER',
|
|
57
|
-
'token-invalid-audience': 'TOKEN_INVALID_AUDIENCE',
|
|
58
|
-
'token-missing-claim': 'TOKEN_MISSING_CLAIM',
|
|
59
|
-
'token-key-not-found': 'TOKEN_KEY_NOT_FOUND',
|
|
60
|
-
'origin-mismatch': 'ORIGIN_MISMATCH',
|
|
61
|
-
'origin-required': 'ORIGIN_REQUIRED',
|
|
62
|
-
'origin-not-allowed': 'ORIGIN_NOT_ALLOWED',
|
|
63
|
-
'rate-limit-exceeded': 'RATE_LIMIT_EXCEEDED',
|
|
64
|
-
'websocket-session-rate-limit-exceeded': 'WEBSOCKET_SESSION_RATE_LIMIT_EXCEEDED',
|
|
65
|
-
'connection-limit-exceeded': 'CONNECTION_LIMIT_EXCEEDED',
|
|
66
|
-
'subscription-limit-exceeded': 'SUBSCRIPTION_LIMIT_EXCEEDED',
|
|
67
|
-
'snapshot-limit-exceeded': 'SNAPSHOT_LIMIT_EXCEEDED',
|
|
68
|
-
'egress-limit-exceeded': 'EGRESS_LIMIT_EXCEEDED',
|
|
69
|
-
'invalid-static-token': 'INVALID_STATIC_TOKEN',
|
|
70
|
-
'internal-error': 'INTERNAL_ERROR',
|
|
71
|
-
'auth-required': 'AUTH_REQUIRED',
|
|
72
|
-
'missing-authorization-header': 'MISSING_AUTHORIZATION_HEADER',
|
|
73
|
-
'invalid-authorization-format': 'INVALID_AUTHORIZATION_FORMAT',
|
|
74
|
-
'invalid-api-key': 'INVALID_API_KEY',
|
|
75
|
-
'expired-api-key': 'EXPIRED_API_KEY',
|
|
76
|
-
'user-not-found': 'USER_NOT_FOUND',
|
|
77
|
-
'secret-key-required': 'SECRET_KEY_REQUIRED',
|
|
78
|
-
'deployment-access-denied': 'DEPLOYMENT_ACCESS_DENIED',
|
|
79
|
-
'quota-exceeded': 'QUOTA_EXCEEDED',
|
|
80
|
-
};
|
|
81
55
|
const normalized = errorCode.toLowerCase();
|
|
82
56
|
if (isReplayErrorCode(normalized))
|
|
83
57
|
return normalized;
|
|
84
|
-
return
|
|
58
|
+
return authErrorCodeFromWire(normalized) ?? 'INTERNAL_ERROR';
|
|
85
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a wire error code the way {@link parseErrorCode} does, except that a
|
|
62
|
+
* code this SDK does not know is returned as sent instead of being collapsed
|
|
63
|
+
* into `INTERNAL_ERROR`. A newer server's refusal then still reaches the
|
|
64
|
+
* consumer under its own name.
|
|
65
|
+
*/
|
|
66
|
+
/** Whether `code` is a wire error code this SDK knows. */
|
|
67
|
+
function isKnownWireErrorCode(code) {
|
|
68
|
+
const normalized = code.trim().toLowerCase();
|
|
69
|
+
return isReplayErrorCode(normalized) || authErrorCodeFromWire(normalized) !== undefined;
|
|
70
|
+
}
|
|
71
|
+
function parseWireErrorCode(errorCode) {
|
|
72
|
+
const trimmed = errorCode.trim();
|
|
73
|
+
const normalized = trimmed.toLowerCase();
|
|
74
|
+
if (isReplayErrorCode(normalized))
|
|
75
|
+
return normalized;
|
|
76
|
+
return authErrorCodeFromWire(normalized) ?? trimmed;
|
|
77
|
+
}
|
|
78
|
+
function authErrorCodeFromWire(normalized) {
|
|
79
|
+
return Object.prototype.hasOwnProperty.call(AUTH_ERROR_CODES_BY_WIRE, normalized)
|
|
80
|
+
? AUTH_ERROR_CODES_BY_WIRE[normalized]
|
|
81
|
+
: undefined;
|
|
82
|
+
}
|
|
83
|
+
const AUTH_ERROR_CODES_BY_WIRE = {
|
|
84
|
+
'token-missing': 'TOKEN_MISSING',
|
|
85
|
+
'token-expired': 'TOKEN_EXPIRED',
|
|
86
|
+
'token-invalid-signature': 'TOKEN_INVALID_SIGNATURE',
|
|
87
|
+
'token-invalid-format': 'TOKEN_INVALID_FORMAT',
|
|
88
|
+
'token-invalid-issuer': 'TOKEN_INVALID_ISSUER',
|
|
89
|
+
'token-invalid-audience': 'TOKEN_INVALID_AUDIENCE',
|
|
90
|
+
'token-missing-claim': 'TOKEN_MISSING_CLAIM',
|
|
91
|
+
'token-key-not-found': 'TOKEN_KEY_NOT_FOUND',
|
|
92
|
+
'origin-mismatch': 'ORIGIN_MISMATCH',
|
|
93
|
+
'origin-required': 'ORIGIN_REQUIRED',
|
|
94
|
+
'origin-not-allowed': 'ORIGIN_NOT_ALLOWED',
|
|
95
|
+
'rate-limit-exceeded': 'RATE_LIMIT_EXCEEDED',
|
|
96
|
+
'websocket-session-rate-limit-exceeded': 'WEBSOCKET_SESSION_RATE_LIMIT_EXCEEDED',
|
|
97
|
+
'connection-limit-exceeded': 'CONNECTION_LIMIT_EXCEEDED',
|
|
98
|
+
'subscription-limit-exceeded': 'SUBSCRIPTION_LIMIT_EXCEEDED',
|
|
99
|
+
'snapshot-limit-exceeded': 'SNAPSHOT_LIMIT_EXCEEDED',
|
|
100
|
+
'egress-limit-exceeded': 'EGRESS_LIMIT_EXCEEDED',
|
|
101
|
+
'invalid-static-token': 'INVALID_STATIC_TOKEN',
|
|
102
|
+
'internal-error': 'INTERNAL_ERROR',
|
|
103
|
+
'auth-required': 'AUTH_REQUIRED',
|
|
104
|
+
'missing-authorization-header': 'MISSING_AUTHORIZATION_HEADER',
|
|
105
|
+
'invalid-authorization-format': 'INVALID_AUTHORIZATION_FORMAT',
|
|
106
|
+
'invalid-api-key': 'INVALID_API_KEY',
|
|
107
|
+
'expired-api-key': 'EXPIRED_API_KEY',
|
|
108
|
+
'user-not-found': 'USER_NOT_FOUND',
|
|
109
|
+
'secret-key-required': 'SECRET_KEY_REQUIRED',
|
|
110
|
+
'deployment-access-denied': 'DEPLOYMENT_ACCESS_DENIED',
|
|
111
|
+
'quota-exceeded': 'QUOTA_EXCEEDED',
|
|
112
|
+
'stack-version-retired': 'STACK_VERSION_RETIRED',
|
|
113
|
+
'stack-version-unknown': 'STACK_VERSION_UNKNOWN',
|
|
114
|
+
};
|
|
86
115
|
|
|
87
116
|
const GZIP_MAGIC_0 = 0x1f;
|
|
88
117
|
const GZIP_MAGIC_1 = 0x8b;
|
|
@@ -625,6 +654,73 @@ function hostedWebsocketSuffixes() {
|
|
|
625
654
|
return [HOSTED_WEBSOCKET_SUFFIX, ...extraHostedSuffixes];
|
|
626
655
|
}
|
|
627
656
|
const MAX_HTTP_AUTH_TOKEN_STATES = 32;
|
|
657
|
+
function optionalString(value) {
|
|
658
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
659
|
+
}
|
|
660
|
+
/** The structured fields of a stack version refusal, dropping malformed ones. */
|
|
661
|
+
function parseStackVersionRefusal(body) {
|
|
662
|
+
const replacementBody = typeof body?.replacement === 'object' && body.replacement !== null
|
|
663
|
+
? body.replacement
|
|
664
|
+
: undefined;
|
|
665
|
+
const version = optionalString(replacementBody?.['version']);
|
|
666
|
+
const stackManifestHash = optionalString(replacementBody?.['stackManifestHash']);
|
|
667
|
+
const upgradeCommand = optionalString(body?.upgradeCommand);
|
|
668
|
+
const retiredAt = optionalString(body?.retiredAt);
|
|
669
|
+
return {
|
|
670
|
+
...(version !== undefined || stackManifestHash !== undefined
|
|
671
|
+
? {
|
|
672
|
+
replacement: {
|
|
673
|
+
...(version !== undefined ? { version } : {}),
|
|
674
|
+
...(stackManifestHash !== undefined ? { stackManifestHash } : {}),
|
|
675
|
+
},
|
|
676
|
+
}
|
|
677
|
+
: {}),
|
|
678
|
+
...(upgradeCommand !== undefined ? { upgradeCommand } : {}),
|
|
679
|
+
...(retiredAt !== undefined ? { retiredAt } : {}),
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
/** The server's message, followed by the replacement and how to install it. */
|
|
683
|
+
function describeStackVersionRefusal(message, refusal) {
|
|
684
|
+
const guidance = [];
|
|
685
|
+
const replacement = refusal.replacement?.version ?? refusal.replacement?.stackManifestHash;
|
|
686
|
+
if (replacement !== undefined)
|
|
687
|
+
guidance.push(`Replacement: ${replacement}.`);
|
|
688
|
+
if (refusal.upgradeCommand !== undefined)
|
|
689
|
+
guidance.push(`Upgrade with: ${refusal.upgradeCommand}`);
|
|
690
|
+
if (guidance.length === 0)
|
|
691
|
+
return message;
|
|
692
|
+
const trimmed = message.trimEnd();
|
|
693
|
+
return `${trimmed}${/[.!?]$/.test(trimmed) ? '' : '.'} ${guidance.join(' ')}`;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* The wire code a close reason carries: the `code` of `code: message`, or the
|
|
697
|
+
* whole reason when it is exactly a known wire code. A free-form reason
|
|
698
|
+
* carries none.
|
|
699
|
+
*/
|
|
700
|
+
function closeReasonWireCode(reason) {
|
|
701
|
+
if (!reason)
|
|
702
|
+
return undefined;
|
|
703
|
+
const prefixed = reason.match(/^([\w-]+):/)?.[1];
|
|
704
|
+
if (prefixed !== undefined)
|
|
705
|
+
return prefixed;
|
|
706
|
+
const bare = reason.trim();
|
|
707
|
+
return isKnownWireErrorCode(bare) ? bare : undefined;
|
|
708
|
+
}
|
|
709
|
+
/** A stack version refusal ends the connection: no retry or reconnect can fix it. */
|
|
710
|
+
function isTerminalRefusal(error) {
|
|
711
|
+
return error instanceof AreteError && isStackVersionRefusalCode(error.code);
|
|
712
|
+
}
|
|
713
|
+
function normalizeStackRelease(release) {
|
|
714
|
+
if (release === undefined)
|
|
715
|
+
return undefined;
|
|
716
|
+
if (typeof release.stackManifestHash !== 'string'
|
|
717
|
+
|| release.stackManifestHash.length === 0
|
|
718
|
+
|| typeof release.liveAlias !== 'string'
|
|
719
|
+
|| release.liveAlias.length === 0) {
|
|
720
|
+
throw new AreteError('Stack release requires a non-empty stackManifestHash and liveAlias', 'INVALID_CONFIG');
|
|
721
|
+
}
|
|
722
|
+
return { stackManifestHash: release.stackManifestHash, liveAlias: release.liveAlias };
|
|
723
|
+
}
|
|
628
724
|
function normalizeTokenResult(result) {
|
|
629
725
|
if (typeof result === 'string') {
|
|
630
726
|
return { token: result };
|
|
@@ -801,6 +897,7 @@ class ConnectionManager {
|
|
|
801
897
|
this.maxReconnectAttempts =
|
|
802
898
|
config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts;
|
|
803
899
|
this.authConfig = config.auth;
|
|
900
|
+
this.release = normalizeStackRelease(config.release);
|
|
804
901
|
this.authFetch = config.fetch ?? ((input, init) => {
|
|
805
902
|
if (typeof globalThis.fetch !== 'function') {
|
|
806
903
|
throw new AreteError('A fetch implementation is required for authentication token requests', 'INVALID_CONFIG');
|
|
@@ -903,7 +1000,15 @@ class ConnectionManager {
|
|
|
903
1000
|
}
|
|
904
1001
|
}
|
|
905
1002
|
async fetchAuthToken(scopes, isCurrent) {
|
|
906
|
-
const request = {
|
|
1003
|
+
const request = {
|
|
1004
|
+
scopes: normalizeScopes(scopes),
|
|
1005
|
+
...(this.release
|
|
1006
|
+
? {
|
|
1007
|
+
stackManifestHash: this.release.stackManifestHash,
|
|
1008
|
+
liveAlias: this.release.liveAlias,
|
|
1009
|
+
}
|
|
1010
|
+
: {}),
|
|
1011
|
+
};
|
|
907
1012
|
const result = await this.requestAuthToken(request, isCurrent);
|
|
908
1013
|
return result === undefined ? undefined : this.updateTokenState(result, request.scopes);
|
|
909
1014
|
}
|
|
@@ -1032,9 +1137,14 @@ class ConnectionManager {
|
|
|
1032
1137
|
? { ...target, programReleaseHash: request.programReleaseHash }
|
|
1033
1138
|
: target;
|
|
1034
1139
|
}
|
|
1140
|
+
// Session requests from a stack definition without a release stay
|
|
1141
|
+
// byte-for-byte what older clients send.
|
|
1035
1142
|
return {
|
|
1036
1143
|
websocket_url: this.websocketUrl ?? '',
|
|
1037
1144
|
scopes: request.scopes,
|
|
1145
|
+
...(request.stackManifestHash !== undefined && request.liveAlias !== undefined
|
|
1146
|
+
? { stackManifestHash: request.stackManifestHash, liveAlias: request.liveAlias }
|
|
1147
|
+
: {}),
|
|
1038
1148
|
};
|
|
1039
1149
|
}
|
|
1040
1150
|
async fetchTokenFromEndpoint(tokenEndpoint, request) {
|
|
@@ -1071,6 +1181,15 @@ class ConnectionManager {
|
|
|
1071
1181
|
const errorMessage = typeof parsedError?.error === 'string' && parsedError.error.length > 0
|
|
1072
1182
|
? parsedError.error
|
|
1073
1183
|
: rawError || response.statusText || 'Authentication request failed';
|
|
1184
|
+
if (isStackVersionRefusalCode(errorCode)) {
|
|
1185
|
+
const refusal = parseStackVersionRefusal(parsedError);
|
|
1186
|
+
throw new AreteError(describeStackVersionRefusal(`Token endpoint returned ${response.status}: ${errorMessage}`, refusal), errorCode, {
|
|
1187
|
+
status: response.status,
|
|
1188
|
+
wireErrorCode,
|
|
1189
|
+
responseBody: rawError || null,
|
|
1190
|
+
...refusal,
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1074
1193
|
throw new AreteError(`Token endpoint returned ${response.status}: ${errorMessage}`, errorCode, {
|
|
1075
1194
|
status: response.status,
|
|
1076
1195
|
wireErrorCode,
|
|
@@ -1136,8 +1255,10 @@ class ConnectionManager {
|
|
|
1136
1255
|
this.scheduleTokenRefresh();
|
|
1137
1256
|
}
|
|
1138
1257
|
}
|
|
1139
|
-
catch {
|
|
1140
|
-
|
|
1258
|
+
catch (error) {
|
|
1259
|
+
// A stack version refusal will not change on retry. Stop refreshing;
|
|
1260
|
+
// the next connect after this session ends reports it.
|
|
1261
|
+
if (this.tokenRefreshInFlight === refresh && !isTerminalRefusal(error)) {
|
|
1141
1262
|
this.scheduleTokenRefresh();
|
|
1142
1263
|
}
|
|
1143
1264
|
}
|
|
@@ -1330,11 +1451,11 @@ class ConnectionManager {
|
|
|
1330
1451
|
const issue = {
|
|
1331
1452
|
error: message.error ?? message.code,
|
|
1332
1453
|
message: message.message ?? message.error ?? message.code,
|
|
1333
|
-
code:
|
|
1454
|
+
code: parseWireErrorCode(message.code),
|
|
1334
1455
|
retryable: message.retryable ?? false,
|
|
1335
|
-
retryAfter: message.retry_after,
|
|
1336
|
-
suggestedAction: message.suggested_action,
|
|
1337
|
-
docsUrl: message.docs_url,
|
|
1456
|
+
retryAfter: message.retryAfter ?? message.retry_after,
|
|
1457
|
+
suggestedAction: message.suggestedAction ?? message.suggested_action,
|
|
1458
|
+
docsUrl: message.docsUrl ?? message.docs_url,
|
|
1338
1459
|
fatal: message.fatal,
|
|
1339
1460
|
subscriptionId: message.subscriptionId,
|
|
1340
1461
|
};
|
|
@@ -1370,7 +1491,7 @@ class ConnectionManager {
|
|
|
1370
1491
|
this.pendingConnect = null;
|
|
1371
1492
|
}
|
|
1372
1493
|
if (generation === this.socketGeneration) {
|
|
1373
|
-
this.updateState(recovering ? 'reconnecting' : 'error', error instanceof Error ? error.message : 'Failed to get token');
|
|
1494
|
+
this.updateState(recovering && !isTerminalRefusal(error) ? 'reconnecting' : 'error', error instanceof Error ? error.message : 'Failed to get token');
|
|
1374
1495
|
}
|
|
1375
1496
|
throw error;
|
|
1376
1497
|
}
|
|
@@ -1398,6 +1519,7 @@ class ConnectionManager {
|
|
|
1398
1519
|
if (!isCurrentSocket())
|
|
1399
1520
|
return;
|
|
1400
1521
|
this.reconnectAttempts = 0;
|
|
1522
|
+
this.lastDisconnectReason = undefined;
|
|
1401
1523
|
this.updateState('connected');
|
|
1402
1524
|
this.startPingInterval();
|
|
1403
1525
|
this.scheduleTokenRefresh();
|
|
@@ -1471,8 +1593,19 @@ class ConnectionManager {
|
|
|
1471
1593
|
? `${event.code}: ${event.reason}`
|
|
1472
1594
|
: `code ${event.code}`;
|
|
1473
1595
|
const errorMessage = `WebSocket closed before open (${detail})`;
|
|
1474
|
-
|
|
1475
|
-
|
|
1596
|
+
const reasonCode = closeReasonWireCode(event.reason);
|
|
1597
|
+
const wireErrorCode = reasonCode === undefined
|
|
1598
|
+
? undefined
|
|
1599
|
+
: parseWireErrorCode(reasonCode);
|
|
1600
|
+
// A stack version refusal is terminal even mid-reconnect: carry it
|
|
1601
|
+
// as the error code, so recovery stops instead of retrying.
|
|
1602
|
+
const refused = wireErrorCode !== undefined && isStackVersionRefusalCode(wireErrorCode);
|
|
1603
|
+
this.updateState(recovering && !refused ? 'reconnecting' : 'error', errorMessage);
|
|
1604
|
+
finish(() => reject(new AreteError(errorMessage, refused ? wireErrorCode : 'CONNECTION_ERROR', {
|
|
1605
|
+
closeCode: event.code,
|
|
1606
|
+
closeReason: event.reason || undefined,
|
|
1607
|
+
wireErrorCode,
|
|
1608
|
+
})));
|
|
1476
1609
|
return;
|
|
1477
1610
|
}
|
|
1478
1611
|
if (this.reconnectForTokenRefresh) {
|
|
@@ -1481,15 +1614,19 @@ class ConnectionManager {
|
|
|
1481
1614
|
this.updateState('error', 'WebSocket closed for token refresh and automatic reconnection is disabled');
|
|
1482
1615
|
return;
|
|
1483
1616
|
}
|
|
1484
|
-
void this.connect(true).catch(() => {
|
|
1485
|
-
this.
|
|
1617
|
+
void this.connect(true).catch((error) => {
|
|
1618
|
+
this.recoverFromFailedConnect(error);
|
|
1486
1619
|
});
|
|
1487
1620
|
return;
|
|
1488
1621
|
}
|
|
1489
1622
|
// Parse close reason for error codes (e.g., "token-expired: Token has expired")
|
|
1490
1623
|
const closeReason = event.reason || '';
|
|
1491
|
-
const
|
|
1492
|
-
const errorCode =
|
|
1624
|
+
const reasonCode = closeReasonWireCode(closeReason);
|
|
1625
|
+
const errorCode = reasonCode === undefined ? null : parseWireErrorCode(reasonCode);
|
|
1626
|
+
if (errorCode !== null && isStackVersionRefusalCode(errorCode)) {
|
|
1627
|
+
this.updateState('error', `WebSocket closed (${event.code}: ${closeReason})`);
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1493
1630
|
// Check for auth errors that require token refresh
|
|
1494
1631
|
if (event.code === 1008 || errorCode) {
|
|
1495
1632
|
const isAuthError = errorCode
|
|
@@ -1502,8 +1639,8 @@ class ConnectionManager {
|
|
|
1502
1639
|
return;
|
|
1503
1640
|
}
|
|
1504
1641
|
// Try to reconnect immediately with a fresh token
|
|
1505
|
-
void this.connect(true).catch(() => {
|
|
1506
|
-
this.
|
|
1642
|
+
void this.connect(true).catch((error) => {
|
|
1643
|
+
this.recoverFromFailedConnect(error);
|
|
1507
1644
|
});
|
|
1508
1645
|
return;
|
|
1509
1646
|
}
|
|
@@ -1518,13 +1655,13 @@ class ConnectionManager {
|
|
|
1518
1655
|
}
|
|
1519
1656
|
}
|
|
1520
1657
|
if (this.currentState !== 'disconnected') {
|
|
1658
|
+
const detail = event.reason
|
|
1659
|
+
? `${event.code}: ${event.reason}`
|
|
1660
|
+
: `code ${event.code}`;
|
|
1521
1661
|
if (this.autoReconnect) {
|
|
1522
|
-
this.handleReconnect();
|
|
1662
|
+
this.handleReconnect(`WebSocket closed (${detail})`);
|
|
1523
1663
|
}
|
|
1524
1664
|
else {
|
|
1525
|
-
const detail = event.reason
|
|
1526
|
-
? `${event.code}: ${event.reason}`
|
|
1527
|
-
: `code ${event.code}`;
|
|
1528
1665
|
this.updateState('error', `WebSocket closed (${detail}) and automatic reconnection is disabled`);
|
|
1529
1666
|
}
|
|
1530
1667
|
}
|
|
@@ -1543,6 +1680,7 @@ class ConnectionManager {
|
|
|
1543
1680
|
this.stopPingInterval();
|
|
1544
1681
|
this.clearTokenRefreshTimeout();
|
|
1545
1682
|
this.reconnectForTokenRefresh = false;
|
|
1683
|
+
this.lastDisconnectReason = undefined;
|
|
1546
1684
|
const pendingConnect = this.pendingConnect;
|
|
1547
1685
|
this.pendingConnect = null;
|
|
1548
1686
|
this.tokenRequestInFlight = null;
|
|
@@ -1672,29 +1810,47 @@ class ConnectionManager {
|
|
|
1672
1810
|
handler(frame);
|
|
1673
1811
|
}
|
|
1674
1812
|
}
|
|
1675
|
-
|
|
1813
|
+
/**
|
|
1814
|
+
* @param reason Why the connection was lost, when known. It is reported
|
|
1815
|
+
* with the `reconnecting` state and kept for the terminal error if the
|
|
1816
|
+
* attempts run out, so a server's close reason is never dropped.
|
|
1817
|
+
*/
|
|
1818
|
+
handleReconnect(reason) {
|
|
1819
|
+
if (reason !== undefined) {
|
|
1820
|
+
this.lastDisconnectReason = reason;
|
|
1821
|
+
}
|
|
1676
1822
|
if (!this.autoReconnect) {
|
|
1677
1823
|
this.updateState('error', 'Automatic reconnection is disabled');
|
|
1678
1824
|
return;
|
|
1679
1825
|
}
|
|
1680
1826
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1681
|
-
|
|
1827
|
+
const lastReason = this.lastDisconnectReason;
|
|
1828
|
+
this.updateState('error', `Max reconnection attempts (${this.reconnectAttempts}) reached`
|
|
1829
|
+
+ (lastReason ? `; last disconnect: ${lastReason}` : ''));
|
|
1682
1830
|
return;
|
|
1683
1831
|
}
|
|
1684
|
-
this.updateState('reconnecting');
|
|
1832
|
+
this.updateState('reconnecting', this.lastDisconnectReason);
|
|
1685
1833
|
const attemptIndex = Math.min(this.reconnectAttempts, this.reconnectIntervals.length - 1);
|
|
1686
1834
|
const delay = this.reconnectIntervals[attemptIndex] ?? 1000;
|
|
1687
1835
|
this.reconnectAttempts++;
|
|
1688
1836
|
this.reconnectTimeout = setTimeout(() => {
|
|
1689
|
-
this.connect(true).catch(() => {
|
|
1837
|
+
this.connect(true).catch((error) => {
|
|
1690
1838
|
// Once a socket exists, its close event owns the next retry. Token
|
|
1691
1839
|
// acquisition and socket construction can fail before that point.
|
|
1692
1840
|
if (this.ws === null && this.currentState !== 'disconnected') {
|
|
1693
|
-
this.
|
|
1841
|
+
this.recoverFromFailedConnect(error);
|
|
1694
1842
|
}
|
|
1695
1843
|
});
|
|
1696
1844
|
}, delay);
|
|
1697
1845
|
}
|
|
1846
|
+
/** Schedule the next attempt after a failed reconnect, unless nothing can succeed. */
|
|
1847
|
+
recoverFromFailedConnect(error) {
|
|
1848
|
+
if (isTerminalRefusal(error)) {
|
|
1849
|
+
// connect() already reported the refusal as the terminal error state.
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
this.handleReconnect(error instanceof Error ? error.message : undefined);
|
|
1853
|
+
}
|
|
1698
1854
|
clearReconnectTimeout() {
|
|
1699
1855
|
if (this.reconnectTimeout) {
|
|
1700
1856
|
clearTimeout(this.reconnectTimeout);
|
|
@@ -7032,6 +7188,9 @@ let Arete$1 = class Arete {
|
|
|
7032
7188
|
maxReconnectAttempts: options.maxReconnectAttempts,
|
|
7033
7189
|
auth: options.auth,
|
|
7034
7190
|
fetch: this.fetchImpl,
|
|
7191
|
+
// The release names the version served at the generated endpoint. A
|
|
7192
|
+
// different `url` points somewhere the generator knew nothing about.
|
|
7193
|
+
release: url !== null && url === this.stack.endpoints.ws ? this.stack.release : undefined,
|
|
7035
7194
|
});
|
|
7036
7195
|
this.processor.useCursorTracker(this.connection.cursors);
|
|
7037
7196
|
this.subscriptionRegistry = new SubscriptionRegistry(this.connection, this.queryStore);
|
|
@@ -8013,6 +8172,7 @@ exports.isErrorFrame = isErrorFrame;
|
|
|
8013
8172
|
exports.isHostedAreteEndpoint = isHostedAreteEndpoint;
|
|
8014
8173
|
exports.isReplayErrorCode = isReplayErrorCode;
|
|
8015
8174
|
exports.isSnapshotFrame = isSnapshotFrame;
|
|
8175
|
+
exports.isStackVersionRefusalCode = isStackVersionRefusalCode;
|
|
8016
8176
|
exports.isSubscribedFrame = isSubscribedFrame;
|
|
8017
8177
|
exports.isUnsubscribedFrame = isUnsubscribedFrame;
|
|
8018
8178
|
exports.isValidFrame = isValidFrame;
|