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