hyperstack-react 0.5.10 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6770,6 +6770,19 @@ const DEFAULT_CONFIG = {
6770
6770
  maxReconnectAttempts: 5,
6771
6771
  maxEntriesPerView: DEFAULT_MAX_ENTRIES_PER_VIEW,
6772
6772
  };
6773
+ /**
6774
+ * Determines if the error indicates the client should fetch a new token
6775
+ */
6776
+ function shouldRefreshToken(code) {
6777
+ return [
6778
+ 'TOKEN_EXPIRED',
6779
+ 'TOKEN_INVALID_SIGNATURE',
6780
+ 'TOKEN_INVALID_FORMAT',
6781
+ 'TOKEN_INVALID_ISSUER',
6782
+ 'TOKEN_INVALID_AUDIENCE',
6783
+ 'TOKEN_KEY_NOT_FOUND',
6784
+ ].includes(code);
6785
+ }
6773
6786
  class HyperStackError extends Error {
6774
6787
  constructor(message, code, details) {
6775
6788
  super(message);
@@ -6778,6 +6791,42 @@ class HyperStackError extends Error {
6778
6791
  this.name = 'HyperStackError';
6779
6792
  }
6780
6793
  }
6794
+ /**
6795
+ * Parse a kebab-case error code string (from X-Error-Code header) to AuthErrorCode
6796
+ */
6797
+ function parseErrorCode(errorCode) {
6798
+ const codeMap = {
6799
+ 'token-missing': 'TOKEN_MISSING',
6800
+ 'token-expired': 'TOKEN_EXPIRED',
6801
+ 'token-invalid-signature': 'TOKEN_INVALID_SIGNATURE',
6802
+ 'token-invalid-format': 'TOKEN_INVALID_FORMAT',
6803
+ 'token-invalid-issuer': 'TOKEN_INVALID_ISSUER',
6804
+ 'token-invalid-audience': 'TOKEN_INVALID_AUDIENCE',
6805
+ 'token-missing-claim': 'TOKEN_MISSING_CLAIM',
6806
+ 'token-key-not-found': 'TOKEN_KEY_NOT_FOUND',
6807
+ 'origin-mismatch': 'ORIGIN_MISMATCH',
6808
+ 'origin-required': 'ORIGIN_REQUIRED',
6809
+ 'origin-not-allowed': 'ORIGIN_NOT_ALLOWED',
6810
+ 'rate-limit-exceeded': 'RATE_LIMIT_EXCEEDED',
6811
+ 'websocket-session-rate-limit-exceeded': 'WEBSOCKET_SESSION_RATE_LIMIT_EXCEEDED',
6812
+ 'connection-limit-exceeded': 'CONNECTION_LIMIT_EXCEEDED',
6813
+ 'subscription-limit-exceeded': 'SUBSCRIPTION_LIMIT_EXCEEDED',
6814
+ 'snapshot-limit-exceeded': 'SNAPSHOT_LIMIT_EXCEEDED',
6815
+ 'egress-limit-exceeded': 'EGRESS_LIMIT_EXCEEDED',
6816
+ 'invalid-static-token': 'INVALID_STATIC_TOKEN',
6817
+ 'internal-error': 'INTERNAL_ERROR',
6818
+ 'auth-required': 'AUTH_REQUIRED',
6819
+ 'missing-authorization-header': 'MISSING_AUTHORIZATION_HEADER',
6820
+ 'invalid-authorization-format': 'INVALID_AUTHORIZATION_FORMAT',
6821
+ 'invalid-api-key': 'INVALID_API_KEY',
6822
+ 'expired-api-key': 'EXPIRED_API_KEY',
6823
+ 'user-not-found': 'USER_NOT_FOUND',
6824
+ 'secret-key-required': 'SECRET_KEY_REQUIRED',
6825
+ 'deployment-access-denied': 'DEPLOYMENT_ACCESS_DENIED',
6826
+ 'quota-exceeded': 'QUOTA_EXCEEDED',
6827
+ };
6828
+ return codeMap[errorCode.toLowerCase()] || 'INTERNAL_ERROR';
6829
+ }
6781
6830
 
6782
6831
  const GZIP_MAGIC_0 = 0x1f;
6783
6832
  const GZIP_MAGIC_1 = 0x8b;
@@ -6825,27 +6874,364 @@ function isValidFrame(frame) {
6825
6874
  ['create', 'upsert', 'patch', 'delete'].includes(f['op']));
6826
6875
  }
6827
6876
 
6877
+ const TOKEN_REFRESH_BUFFER_SECONDS = 60;
6878
+ const MIN_REFRESH_DELAY_MS = 1000;
6879
+ const DEFAULT_QUERY_PARAMETER = 'hs_token';
6880
+ const DEFAULT_HOSTED_TOKEN_ENDPOINT = 'https://api.usehyperstack.com/ws/sessions';
6881
+ const HOSTED_WEBSOCKET_SUFFIX = '.stack.usehyperstack.com';
6882
+ function normalizeTokenResult(result) {
6883
+ if (typeof result === 'string') {
6884
+ return { token: result };
6885
+ }
6886
+ return result;
6887
+ }
6888
+ function decodeBase64Url(value) {
6889
+ const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
6890
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
6891
+ if (typeof atob === 'function') {
6892
+ return atob(padded);
6893
+ }
6894
+ const bufferCtor = globalThis.Buffer;
6895
+ if (bufferCtor) {
6896
+ return bufferCtor.from(padded, 'base64').toString('utf-8');
6897
+ }
6898
+ return undefined;
6899
+ }
6900
+ function parseJwtExpiry(token) {
6901
+ const parts = token.split('.');
6902
+ if (parts.length !== 3) {
6903
+ return undefined;
6904
+ }
6905
+ const payload = decodeBase64Url(parts[1] ?? '');
6906
+ if (!payload) {
6907
+ return undefined;
6908
+ }
6909
+ try {
6910
+ const decoded = JSON.parse(payload);
6911
+ return typeof decoded.exp === 'number' ? decoded.exp : undefined;
6912
+ }
6913
+ catch {
6914
+ return undefined;
6915
+ }
6916
+ }
6917
+ function normalizeExpiryTimestamp(expiresAt, expires_at) {
6918
+ return expiresAt ?? expires_at;
6919
+ }
6920
+ function isRefreshAuthResponseMessage(value) {
6921
+ if (typeof value !== 'object' || value === null) {
6922
+ return false;
6923
+ }
6924
+ const candidate = value;
6925
+ return typeof candidate['success'] === 'boolean'
6926
+ && !('op' in candidate)
6927
+ && !('entity' in candidate)
6928
+ && !('mode' in candidate);
6929
+ }
6930
+ function isSocketIssueMessage(value) {
6931
+ if (typeof value !== 'object' || value === null) {
6932
+ return false;
6933
+ }
6934
+ const candidate = value;
6935
+ return candidate['type'] === 'error'
6936
+ && typeof candidate['message'] === 'string'
6937
+ && typeof candidate['code'] === 'string'
6938
+ && typeof candidate['retryable'] === 'boolean'
6939
+ && typeof candidate['fatal'] === 'boolean';
6940
+ }
6941
+ function isHostedHyperstackWebsocketUrl(websocketUrl) {
6942
+ try {
6943
+ return new URL(websocketUrl).hostname.toLowerCase().endsWith(HOSTED_WEBSOCKET_SUFFIX);
6944
+ }
6945
+ catch {
6946
+ return false;
6947
+ }
6948
+ }
6828
6949
  class ConnectionManager {
6829
6950
  constructor(config) {
6830
6951
  this.ws = null;
6831
6952
  this.reconnectAttempts = 0;
6832
6953
  this.reconnectTimeout = null;
6833
6954
  this.pingInterval = null;
6955
+ this.tokenRefreshTimeout = null;
6956
+ this.tokenRefreshInFlight = null;
6834
6957
  this.currentState = 'disconnected';
6835
6958
  this.subscriptionQueue = [];
6836
6959
  this.activeSubscriptions = new Set();
6837
6960
  this.frameHandlers = new Set();
6838
6961
  this.stateHandlers = new Set();
6962
+ this.socketIssueHandlers = new Set();
6963
+ this.reconnectForTokenRefresh = false;
6839
6964
  if (!config.websocketUrl) {
6840
6965
  throw new HyperStackError('websocketUrl is required', 'INVALID_CONFIG');
6841
6966
  }
6842
6967
  this.websocketUrl = config.websocketUrl;
6968
+ this.hostedHyperstackUrl = isHostedHyperstackWebsocketUrl(config.websocketUrl);
6843
6969
  this.reconnectIntervals = config.reconnectIntervals ?? DEFAULT_CONFIG.reconnectIntervals;
6844
- this.maxReconnectAttempts = config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts;
6970
+ this.maxReconnectAttempts =
6971
+ config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts;
6972
+ this.authConfig = config.auth;
6845
6973
  if (config.initialSubscriptions) {
6846
6974
  this.subscriptionQueue.push(...config.initialSubscriptions);
6847
6975
  }
6848
6976
  }
6977
+ getTokenEndpoint() {
6978
+ if (this.authConfig?.tokenEndpoint) {
6979
+ return this.authConfig.tokenEndpoint;
6980
+ }
6981
+ if (this.hostedHyperstackUrl && this.authConfig?.publishableKey) {
6982
+ return DEFAULT_HOSTED_TOKEN_ENDPOINT;
6983
+ }
6984
+ return undefined;
6985
+ }
6986
+ getAuthStrategy() {
6987
+ if (this.authConfig?.token) {
6988
+ return { kind: 'static-token', token: this.authConfig.token };
6989
+ }
6990
+ if (this.authConfig?.getToken) {
6991
+ return { kind: 'token-provider', getToken: this.authConfig.getToken };
6992
+ }
6993
+ const tokenEndpoint = this.getTokenEndpoint();
6994
+ if (tokenEndpoint) {
6995
+ return { kind: 'token-endpoint', endpoint: tokenEndpoint };
6996
+ }
6997
+ return { kind: 'none' };
6998
+ }
6999
+ hasRefreshableAuth() {
7000
+ const strategy = this.getAuthStrategy();
7001
+ return strategy.kind === 'token-provider' || strategy.kind === 'token-endpoint';
7002
+ }
7003
+ updateTokenState(result) {
7004
+ const normalized = normalizeTokenResult(result);
7005
+ if (!normalized.token) {
7006
+ throw new HyperStackError('Authentication provider returned an empty token', 'TOKEN_INVALID');
7007
+ }
7008
+ this.currentToken = normalized.token;
7009
+ this.tokenExpiry = normalizeExpiryTimestamp(normalized.expiresAt, normalized.expires_at)
7010
+ ?? parseJwtExpiry(normalized.token);
7011
+ if (this.isTokenExpired()) {
7012
+ throw new HyperStackError('Authentication token is expired', 'TOKEN_EXPIRED');
7013
+ }
7014
+ return normalized.token;
7015
+ }
7016
+ clearTokenState() {
7017
+ this.currentToken = undefined;
7018
+ this.tokenExpiry = undefined;
7019
+ }
7020
+ async getOrRefreshToken(forceRefresh = false) {
7021
+ if (!forceRefresh && this.currentToken && !this.isTokenExpired()) {
7022
+ return this.currentToken;
7023
+ }
7024
+ const strategy = this.getAuthStrategy();
7025
+ if (strategy.kind === 'none' && this.hostedHyperstackUrl) {
7026
+ throw new HyperStackError('Hosted Hyperstack websocket connections require auth.publishableKey, auth.getToken, auth.tokenEndpoint, or auth.token', 'AUTH_REQUIRED');
7027
+ }
7028
+ switch (strategy.kind) {
7029
+ case 'static-token':
7030
+ return this.updateTokenState(strategy.token);
7031
+ case 'token-provider':
7032
+ try {
7033
+ return this.updateTokenState(await strategy.getToken());
7034
+ }
7035
+ catch (error) {
7036
+ if (error instanceof HyperStackError) {
7037
+ throw error;
7038
+ }
7039
+ throw new HyperStackError('Failed to get authentication token', 'AUTH_REQUIRED', error);
7040
+ }
7041
+ case 'token-endpoint':
7042
+ try {
7043
+ return this.updateTokenState(await this.fetchTokenFromEndpoint(strategy.endpoint));
7044
+ }
7045
+ catch (error) {
7046
+ if (error instanceof HyperStackError) {
7047
+ throw error;
7048
+ }
7049
+ throw new HyperStackError('Failed to fetch authentication token from endpoint', 'AUTH_REQUIRED', error);
7050
+ }
7051
+ case 'none':
7052
+ return undefined;
7053
+ }
7054
+ }
7055
+ createTokenEndpointRequestBody() {
7056
+ return {
7057
+ websocket_url: this.websocketUrl,
7058
+ };
7059
+ }
7060
+ async fetchTokenFromEndpoint(tokenEndpoint) {
7061
+ const response = await fetch(tokenEndpoint, {
7062
+ method: 'POST',
7063
+ headers: {
7064
+ ...(this.authConfig?.publishableKey
7065
+ ? { Authorization: `Bearer ${this.authConfig.publishableKey}` }
7066
+ : {}),
7067
+ ...(this.authConfig?.tokenEndpointHeaders ?? {}),
7068
+ 'Content-Type': 'application/json',
7069
+ },
7070
+ credentials: this.authConfig?.tokenEndpointCredentials,
7071
+ body: JSON.stringify(this.createTokenEndpointRequestBody()),
7072
+ });
7073
+ if (!response.ok) {
7074
+ const rawError = await response.text();
7075
+ let parsedError;
7076
+ if (rawError) {
7077
+ try {
7078
+ parsedError = JSON.parse(rawError);
7079
+ }
7080
+ catch {
7081
+ parsedError = undefined;
7082
+ }
7083
+ }
7084
+ const wireErrorCode = response.headers.get('X-Error-Code')
7085
+ ?? (typeof parsedError?.code === 'string' ? parsedError.code : null);
7086
+ const errorCode = wireErrorCode
7087
+ ? parseErrorCode(wireErrorCode)
7088
+ : response.status === 429
7089
+ ? 'QUOTA_EXCEEDED'
7090
+ : 'AUTH_REQUIRED';
7091
+ const errorMessage = typeof parsedError?.error === 'string' && parsedError.error.length > 0
7092
+ ? parsedError.error
7093
+ : rawError || response.statusText || 'Authentication request failed';
7094
+ throw new HyperStackError(`Token endpoint returned ${response.status}: ${errorMessage}`, errorCode, {
7095
+ status: response.status,
7096
+ wireErrorCode,
7097
+ responseBody: rawError || null,
7098
+ });
7099
+ }
7100
+ const data = (await response.json());
7101
+ if (!data.token) {
7102
+ throw new HyperStackError('Token endpoint did not return a token', 'TOKEN_INVALID');
7103
+ }
7104
+ return data;
7105
+ }
7106
+ isTokenExpired() {
7107
+ if (!this.tokenExpiry) {
7108
+ return false;
7109
+ }
7110
+ return Date.now() >= (this.tokenExpiry - TOKEN_REFRESH_BUFFER_SECONDS) * 1000;
7111
+ }
7112
+ scheduleTokenRefresh() {
7113
+ this.clearTokenRefreshTimeout();
7114
+ if (!this.hasRefreshableAuth() || !this.tokenExpiry) {
7115
+ return;
7116
+ }
7117
+ const refreshAtMs = Math.max(Date.now() + MIN_REFRESH_DELAY_MS, (this.tokenExpiry - TOKEN_REFRESH_BUFFER_SECONDS) * 1000);
7118
+ const delayMs = Math.max(MIN_REFRESH_DELAY_MS, refreshAtMs - Date.now());
7119
+ this.tokenRefreshTimeout = setTimeout(() => {
7120
+ void this.refreshTokenInBackground();
7121
+ }, delayMs);
7122
+ }
7123
+ clearTokenRefreshTimeout() {
7124
+ if (this.tokenRefreshTimeout) {
7125
+ clearTimeout(this.tokenRefreshTimeout);
7126
+ this.tokenRefreshTimeout = null;
7127
+ }
7128
+ }
7129
+ async refreshTokenInBackground() {
7130
+ if (!this.hasRefreshableAuth()) {
7131
+ return;
7132
+ }
7133
+ if (this.tokenRefreshInFlight) {
7134
+ return this.tokenRefreshInFlight;
7135
+ }
7136
+ this.tokenRefreshInFlight = (async () => {
7137
+ const previousToken = this.currentToken;
7138
+ try {
7139
+ await this.getOrRefreshToken(true);
7140
+ if (previousToken &&
7141
+ this.currentToken &&
7142
+ this.currentToken !== previousToken &&
7143
+ this.ws?.readyState === WebSocket.OPEN) {
7144
+ // Try in-band auth refresh first
7145
+ const refreshed = await this.sendInBandAuthRefresh(this.currentToken);
7146
+ if (!refreshed) {
7147
+ // Fall back to reconnecting if in-band refresh failed
7148
+ this.rotateConnectionForTokenRefresh();
7149
+ }
7150
+ }
7151
+ this.scheduleTokenRefresh();
7152
+ }
7153
+ catch {
7154
+ this.scheduleTokenRefresh();
7155
+ }
7156
+ finally {
7157
+ this.tokenRefreshInFlight = null;
7158
+ }
7159
+ })();
7160
+ return this.tokenRefreshInFlight;
7161
+ }
7162
+ async sendInBandAuthRefresh(token) {
7163
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
7164
+ return false;
7165
+ }
7166
+ try {
7167
+ const message = JSON.stringify({
7168
+ type: 'refresh_auth',
7169
+ token: token,
7170
+ });
7171
+ this.ws.send(message);
7172
+ return true;
7173
+ }
7174
+ catch (error) {
7175
+ console.warn('Failed to send in-band auth refresh:', error);
7176
+ return false;
7177
+ }
7178
+ }
7179
+ handleRefreshAuthResponse(message) {
7180
+ if (message.success) {
7181
+ const expiresAt = normalizeExpiryTimestamp(message.expiresAt, message.expires_at);
7182
+ if (typeof expiresAt === 'number') {
7183
+ this.tokenExpiry = expiresAt;
7184
+ }
7185
+ this.scheduleTokenRefresh();
7186
+ return true;
7187
+ }
7188
+ const errorCode = message.error ? parseErrorCode(message.error) : 'INTERNAL_ERROR';
7189
+ if (shouldRefreshToken(errorCode)) {
7190
+ this.clearTokenState();
7191
+ }
7192
+ this.rotateConnectionForTokenRefresh();
7193
+ return true;
7194
+ }
7195
+ handleSocketIssueMessage(message) {
7196
+ this.notifySocketIssue(message);
7197
+ if (message.fatal) {
7198
+ this.updateState('error', message.message);
7199
+ }
7200
+ return true;
7201
+ }
7202
+ rotateConnectionForTokenRefresh() {
7203
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || this.reconnectForTokenRefresh) {
7204
+ return;
7205
+ }
7206
+ this.reconnectForTokenRefresh = true;
7207
+ this.updateState('reconnecting');
7208
+ this.ws.close(1000, 'token refresh');
7209
+ }
7210
+ buildAuthUrl(token) {
7211
+ if (this.authConfig?.tokenTransport === 'bearer') {
7212
+ return this.websocketUrl;
7213
+ }
7214
+ if (!token) {
7215
+ return this.websocketUrl;
7216
+ }
7217
+ const separator = this.websocketUrl.includes('?') ? '&' : '?';
7218
+ return `${this.websocketUrl}${separator}${DEFAULT_QUERY_PARAMETER}=${encodeURIComponent(token)}`;
7219
+ }
7220
+ createWebSocket(url, token) {
7221
+ if (this.authConfig?.tokenTransport === 'bearer') {
7222
+ const init = token
7223
+ ? { headers: { Authorization: `Bearer ${token}` } }
7224
+ : undefined;
7225
+ if (this.authConfig.websocketFactory) {
7226
+ return this.authConfig.websocketFactory(url, init);
7227
+ }
7228
+ throw new HyperStackError('auth.tokenTransport="bearer" requires auth.websocketFactory in this environment', 'INVALID_CONFIG');
7229
+ }
7230
+ if (this.authConfig?.websocketFactory) {
7231
+ return this.authConfig.websocketFactory(url);
7232
+ }
7233
+ return new WebSocket(url);
7234
+ }
6849
7235
  getState() {
6850
7236
  return this.currentState;
6851
7237
  }
@@ -6861,21 +7247,52 @@ class ConnectionManager {
6861
7247
  this.stateHandlers.delete(handler);
6862
7248
  };
6863
7249
  }
6864
- connect() {
7250
+ onSocketIssue(handler) {
7251
+ this.socketIssueHandlers.add(handler);
7252
+ return () => {
7253
+ this.socketIssueHandlers.delete(handler);
7254
+ };
7255
+ }
7256
+ notifySocketIssue(message) {
7257
+ const issue = {
7258
+ error: message.error,
7259
+ message: message.message,
7260
+ code: parseErrorCode(message.code),
7261
+ retryable: message.retryable,
7262
+ retryAfter: message.retry_after,
7263
+ suggestedAction: message.suggested_action,
7264
+ docsUrl: message.docs_url,
7265
+ fatal: message.fatal,
7266
+ };
7267
+ for (const handler of this.socketIssueHandlers) {
7268
+ handler(issue);
7269
+ }
7270
+ return issue;
7271
+ }
7272
+ async connect() {
7273
+ if (this.ws?.readyState === WebSocket.OPEN ||
7274
+ this.ws?.readyState === WebSocket.CONNECTING ||
7275
+ this.currentState === 'connecting') {
7276
+ return;
7277
+ }
7278
+ this.updateState('connecting');
7279
+ let token;
7280
+ try {
7281
+ token = await this.getOrRefreshToken();
7282
+ }
7283
+ catch (error) {
7284
+ this.updateState('error', error instanceof Error ? error.message : 'Failed to get token');
7285
+ throw error;
7286
+ }
7287
+ const wsUrl = this.buildAuthUrl(token);
6865
7288
  return new Promise((resolve, reject) => {
6866
- if (this.ws?.readyState === WebSocket.OPEN ||
6867
- this.ws?.readyState === WebSocket.CONNECTING ||
6868
- this.currentState === 'connecting') {
6869
- resolve();
6870
- return;
6871
- }
6872
- this.updateState('connecting');
6873
7289
  try {
6874
- this.ws = new WebSocket(this.websocketUrl);
7290
+ this.ws = this.createWebSocket(wsUrl, token);
6875
7291
  this.ws.onopen = () => {
6876
7292
  this.reconnectAttempts = 0;
6877
7293
  this.updateState('connected');
6878
7294
  this.startPingInterval();
7295
+ this.scheduleTokenRefresh();
6879
7296
  this.resubscribeActive();
6880
7297
  this.flushSubscriptionQueue();
6881
7298
  resolve();
@@ -6890,14 +7307,23 @@ class ConnectionManager {
6890
7307
  frame = await parseFrameFromBlob(event.data);
6891
7308
  }
6892
7309
  else if (typeof event.data === 'string') {
6893
- frame = parseFrame(event.data);
7310
+ const parsed = JSON.parse(event.data);
7311
+ if (isRefreshAuthResponseMessage(parsed)) {
7312
+ this.handleRefreshAuthResponse(parsed);
7313
+ return;
7314
+ }
7315
+ if (isSocketIssueMessage(parsed)) {
7316
+ this.handleSocketIssueMessage(parsed);
7317
+ return;
7318
+ }
7319
+ frame = parseFrame(JSON.stringify(parsed));
6894
7320
  }
6895
7321
  else {
6896
7322
  throw new HyperStackError(`Unsupported message type: ${typeof event.data}`, 'PARSE_ERROR');
6897
7323
  }
6898
7324
  this.notifyFrameHandlers(frame);
6899
7325
  }
6900
- catch (error) {
7326
+ catch {
6901
7327
  this.updateState('error', 'Failed to parse frame from server');
6902
7328
  }
6903
7329
  };
@@ -6908,9 +7334,44 @@ class ConnectionManager {
6908
7334
  reject(error);
6909
7335
  }
6910
7336
  };
6911
- this.ws.onclose = () => {
7337
+ this.ws.onclose = (event) => {
6912
7338
  this.stopPingInterval();
7339
+ this.clearTokenRefreshTimeout();
6913
7340
  this.ws = null;
7341
+ if (this.reconnectForTokenRefresh) {
7342
+ this.reconnectForTokenRefresh = false;
7343
+ void this.connect().catch(() => {
7344
+ this.handleReconnect();
7345
+ });
7346
+ return;
7347
+ }
7348
+ // Parse close reason for error codes (e.g., "token-expired: Token has expired")
7349
+ const closeReason = event.reason || '';
7350
+ const errorCodeMatch = closeReason.match(/^([\w-]+):/);
7351
+ const errorCode = errorCodeMatch ? parseErrorCode(errorCodeMatch[1]) : null;
7352
+ // Check for auth errors that require token refresh
7353
+ if (event.code === 1008 || errorCode) {
7354
+ const isAuthError = errorCode
7355
+ ? shouldRefreshToken(errorCode)
7356
+ : /expired|invalid|token/i.test(closeReason);
7357
+ if (isAuthError) {
7358
+ this.clearTokenState();
7359
+ // Try to reconnect immediately with a fresh token
7360
+ void this.connect().catch(() => {
7361
+ this.handleReconnect();
7362
+ });
7363
+ return;
7364
+ }
7365
+ // Check for rate limit errors
7366
+ const isRateLimit = errorCode === 'RATE_LIMIT_EXCEEDED' ||
7367
+ errorCode === 'CONNECTION_LIMIT_EXCEEDED' ||
7368
+ /rate.?limit|quota|limit.?exceeded/i.test(closeReason);
7369
+ if (isRateLimit) {
7370
+ this.updateState('error', `Rate limit exceeded: ${closeReason}`);
7371
+ // Don't auto-reconnect on rate limits, let user handle it
7372
+ return;
7373
+ }
7374
+ }
6914
7375
  if (this.currentState !== 'disconnected') {
6915
7376
  this.handleReconnect();
6916
7377
  }
@@ -6926,6 +7387,8 @@ class ConnectionManager {
6926
7387
  disconnect() {
6927
7388
  this.clearReconnectTimeout();
6928
7389
  this.stopPingInterval();
7390
+ this.clearTokenRefreshTimeout();
7391
+ this.reconnectForTokenRefresh = false;
6929
7392
  this.updateState('disconnected');
6930
7393
  if (this.ws) {
6931
7394
  this.ws.close();
@@ -6943,7 +7406,7 @@ class ConnectionManager {
6943
7406
  this.activeSubscriptions.add(subKey);
6944
7407
  }
6945
7408
  else {
6946
- const alreadyQueued = this.subscriptionQueue.some((s) => this.makeSubKey(s) === subKey);
7409
+ const alreadyQueued = this.subscriptionQueue.some((queuedSubscription) => this.makeSubKey(queuedSubscription) === subKey);
6947
7410
  if (!alreadyQueued) {
6948
7411
  this.subscriptionQueue.push(subscription);
6949
7412
  }
@@ -6968,9 +7431,9 @@ class ConnectionManager {
6968
7431
  }
6969
7432
  flushSubscriptionQueue() {
6970
7433
  while (this.subscriptionQueue.length > 0) {
6971
- const sub = this.subscriptionQueue.shift();
6972
- if (sub) {
6973
- this.subscribe(sub);
7434
+ const subscription = this.subscriptionQueue.shift();
7435
+ if (subscription) {
7436
+ this.subscribe(subscription);
6974
7437
  }
6975
7438
  }
6976
7439
  }
@@ -8690,6 +9153,7 @@ class HyperStack {
8690
9153
  websocketUrl: url,
8691
9154
  reconnectIntervals: options.reconnectIntervals,
8692
9155
  maxReconnectAttempts: options.maxReconnectAttempts,
9156
+ auth: options.auth,
8693
9157
  });
8694
9158
  this.subscriptionRegistry = new SubscriptionRegistry(this.connection);
8695
9159
  this.connection.onFrame((frame) => {
@@ -8723,6 +9187,7 @@ class HyperStack {
8723
9187
  reconnectIntervals: options?.reconnectIntervals,
8724
9188
  maxReconnectAttempts: options?.maxReconnectAttempts,
8725
9189
  validateFrames: options?.validateFrames,
9190
+ auth: options?.auth,
8726
9191
  };
8727
9192
  const client = new HyperStack(url, internalOptions);
8728
9193
  if (options?.autoReconnect !== false) {
@@ -8751,6 +9216,9 @@ class HyperStack {
8751
9216
  onFrame(callback) {
8752
9217
  return this.connection.onFrame(callback);
8753
9218
  }
9219
+ onSocketIssue(callback) {
9220
+ return this.connection.onSocketIssue(callback);
9221
+ }
8754
9222
  async connect() {
8755
9223
  await this.connection.connect();
8756
9224
  }
@@ -8976,6 +9444,7 @@ function HyperstackProvider({ children, fallback = null, ...config }) {
8976
9444
  maxReconnectAttempts: config.maxReconnectAttempts,
8977
9445
  maxEntriesPerView: config.maxEntriesPerView,
8978
9446
  flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
9447
+ auth: config.auth,
8979
9448
  }).then((client) => {
8980
9449
  client.onConnectionStateChange((state, error) => {
8981
9450
  adapter.setConnectionState(state, error);
@@ -8991,7 +9460,7 @@ function HyperstackProvider({ children, fallback = null, ...config }) {
8991
9460
  });
8992
9461
  connectingRef.current.set(cacheKey, connectionPromise);
8993
9462
  return connectionPromise;
8994
- }, [config.autoConnect, config.reconnectIntervals, config.maxReconnectAttempts, config.maxEntriesPerView, notifyClientChange]);
9463
+ }, [config.autoConnect, config.reconnectIntervals, config.maxReconnectAttempts, config.maxEntriesPerView, config.flushIntervalMs, config.auth, notifyClientChange]);
8995
9464
  const getClient = React.useCallback((stack) => {
8996
9465
  if (!stack) {
8997
9466
  if (clientsRef.current.size === 1) {
@@ -9104,7 +9573,7 @@ function shallowArrayEqual(a, b) {
9104
9573
  return true;
9105
9574
  }
9106
9575
  function useStateView(viewDef, client, key, options) {
9107
- const [isLoading, setIsLoading] = React.useState(!options?.initialData);
9576
+ const [isLoading, setIsLoading] = React.useState(!options?.initialData && options?.withSnapshot !== false);
9108
9577
  const [error, setError] = React.useState();
9109
9578
  const clientRef = React.useRef(client);
9110
9579
  clientRef.current = client;
@@ -9112,13 +9581,24 @@ function useStateView(viewDef, client, key, options) {
9112
9581
  const keyString = key ? Object.values(key)[0] : undefined;
9113
9582
  const enabled = options?.enabled !== false;
9114
9583
  const schema = options?.schema;
9584
+ const withSnapshot = options?.withSnapshot;
9585
+ const after = options?.after;
9586
+ const snapshotLimit = options?.snapshotLimit;
9115
9587
  React.useEffect(() => {
9116
9588
  if (!enabled || !clientRef.current)
9117
9589
  return undefined;
9118
9590
  try {
9119
9591
  const registry = clientRef.current.getSubscriptionRegistry();
9120
- const unsubscribe = registry.subscribe({ view: viewDef.view, key: keyString });
9121
- setIsLoading(true);
9592
+ const unsubscribe = registry.subscribe({
9593
+ view: viewDef.view,
9594
+ key: keyString,
9595
+ withSnapshot,
9596
+ after,
9597
+ snapshotLimit
9598
+ });
9599
+ if (withSnapshot !== false) {
9600
+ setIsLoading(true);
9601
+ }
9122
9602
  return () => {
9123
9603
  try {
9124
9604
  unsubscribe();
@@ -9133,14 +9613,23 @@ function useStateView(viewDef, client, key, options) {
9133
9613
  setIsLoading(false);
9134
9614
  return undefined;
9135
9615
  }
9136
- }, [viewDef.view, keyString, enabled, client]);
9616
+ }, [viewDef.view, keyString, enabled, withSnapshot, after, snapshotLimit, client]);
9137
9617
  const refresh = React.useCallback(() => {
9138
9618
  if (!enabled || !clientRef.current)
9139
9619
  return;
9140
9620
  try {
9141
9621
  const registry = clientRef.current.getSubscriptionRegistry();
9142
- const unsubscribe = registry.subscribe({ view: viewDef.view, key: keyString });
9143
- setIsLoading(true);
9622
+ const unsubscribe = registry.subscribe({
9623
+ view: viewDef.view,
9624
+ key: keyString,
9625
+ withSnapshot,
9626
+ after,
9627
+ snapshotLimit
9628
+ });
9629
+ const shouldLoad = withSnapshot ?? true;
9630
+ if (shouldLoad) {
9631
+ setIsLoading(true);
9632
+ }
9144
9633
  setTimeout(() => {
9145
9634
  try {
9146
9635
  unsubscribe();
@@ -9154,7 +9643,7 @@ function useStateView(viewDef, client, key, options) {
9154
9643
  setError(err instanceof Error ? err : new Error('Refresh failed'));
9155
9644
  setIsLoading(false);
9156
9645
  }
9157
- }, [viewDef.view, keyString, enabled]);
9646
+ }, [viewDef.view, keyString, enabled, withSnapshot, after, snapshotLimit]);
9158
9647
  const subscribe = React.useCallback((callback) => {
9159
9648
  if (!clientRef.current)
9160
9649
  return () => { };
@@ -9188,7 +9677,7 @@ function useStateView(viewDef, client, key, options) {
9188
9677
  };
9189
9678
  }
9190
9679
  function useListView(viewDef, client, params, options) {
9191
- const [isLoading, setIsLoading] = React.useState(!options?.initialData);
9680
+ const [isLoading, setIsLoading] = React.useState(!options?.initialData && params?.withSnapshot !== false);
9192
9681
  const [error, setError] = React.useState();
9193
9682
  const clientRef = React.useRef(client);
9194
9683
  clientRef.current = client;
@@ -9201,6 +9690,9 @@ function useListView(viewDef, client, params, options) {
9201
9690
  const filtersJson = params?.filters ? JSON.stringify(params.filters) : undefined;
9202
9691
  const limit = params?.limit;
9203
9692
  const schema = params?.schema;
9693
+ const withSnapshot = params?.withSnapshot;
9694
+ const after = params?.after;
9695
+ const snapshotLimit = params?.snapshotLimit;
9204
9696
  React.useEffect(() => {
9205
9697
  if (!enabled || !clientRef.current)
9206
9698
  return undefined;
@@ -9211,9 +9703,14 @@ function useListView(viewDef, client, params, options) {
9211
9703
  key,
9212
9704
  filters: params?.filters,
9213
9705
  take,
9214
- skip
9706
+ skip,
9707
+ withSnapshot,
9708
+ after,
9709
+ snapshotLimit
9215
9710
  });
9216
- setIsLoading(true);
9711
+ if (withSnapshot !== false) {
9712
+ setIsLoading(true);
9713
+ }
9217
9714
  return () => {
9218
9715
  try {
9219
9716
  unsubscribe();
@@ -9228,7 +9725,7 @@ function useListView(viewDef, client, params, options) {
9228
9725
  setIsLoading(false);
9229
9726
  return undefined;
9230
9727
  }
9231
- }, [viewDef.view, enabled, key, filtersJson, take, skip, client]);
9728
+ }, [viewDef.view, enabled, key, filtersJson, take, skip, withSnapshot, after, snapshotLimit, client]);
9232
9729
  const refresh = React.useCallback(() => {
9233
9730
  if (!enabled || !clientRef.current)
9234
9731
  return;
@@ -9239,9 +9736,15 @@ function useListView(viewDef, client, params, options) {
9239
9736
  key,
9240
9737
  filters: params?.filters,
9241
9738
  take,
9242
- skip
9739
+ skip,
9740
+ withSnapshot,
9741
+ after,
9742
+ snapshotLimit
9243
9743
  });
9244
- setIsLoading(true);
9744
+ const shouldLoad = withSnapshot ?? true;
9745
+ if (shouldLoad) {
9746
+ setIsLoading(true);
9747
+ }
9245
9748
  setTimeout(() => {
9246
9749
  try {
9247
9750
  unsubscribe();
@@ -9255,7 +9758,7 @@ function useListView(viewDef, client, params, options) {
9255
9758
  setError(err instanceof Error ? err : new Error('Refresh failed'));
9256
9759
  setIsLoading(false);
9257
9760
  }
9258
- }, [viewDef.view, enabled, key, filtersJson, take, skip]);
9761
+ }, [viewDef.view, enabled, key, filtersJson, take, skip, withSnapshot, after, snapshotLimit]);
9259
9762
  const subscribe = React.useCallback((callback) => {
9260
9763
  if (!clientRef.current)
9261
9764
  return () => { };