@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 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 codeMap[normalized] || 'INTERNAL_ERROR';
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 = { scopes: normalizeScopes(scopes) };
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
- if (this.tokenRefreshInFlight === refresh) {
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: parseErrorCode(message.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
- this.updateState(recovering ? 'reconnecting' : 'error', errorMessage);
1475
- finish(() => reject(new AreteError(errorMessage, 'CONNECTION_ERROR')));
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.handleReconnect();
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 errorCodeMatch = closeReason.match(/^([\w-]+):/);
1492
- const errorCode = errorCodeMatch ? parseErrorCode(errorCodeMatch[1]) : null;
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.handleReconnect();
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
- handleReconnect() {
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
- this.updateState('error', `Max reconnection attempts (${this.reconnectAttempts}) reached`);
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.handleReconnect();
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;