@pooflabs/core 0.0.33 → 0.0.34

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
@@ -4013,13 +4013,15 @@ async function syncItems(paths, options) {
4013
4013
  const connections = new Map();
4014
4014
  const responseCache = new Map();
4015
4015
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
4016
- const TOKEN_REFRESH_BUFFER = 5 * 60 * 1000; // Refresh token 5 minutes before expiry
4016
+ const TOKEN_REFRESH_BUFFER = 10 * 60 * 1000; // Refresh token 10 minutes before expiry
4017
+ const TOKEN_CHECK_INTERVAL = 5 * 60 * 1000; // Check token freshness every 5 minutes
4017
4018
  // ============ WebSocket Config ============
4018
4019
  const BASE_MIN_RECONNECT_DELAY_MS = 1000;
4019
4020
  const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
4020
4021
  const MAX_RECONNECT_DELAY_MS = 300000;
4021
4022
  const RECONNECT_DELAY_GROW_FACTOR = 1.8;
4022
4023
  const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
4024
+ const MAX_AUTH_REFRESH_RETRIES = 5;
4023
4025
  const WS_CONFIG = {
4024
4026
  // Keep retrying indefinitely so long outages recover without page refresh.
4025
4027
  maxRetries: Infinity,
@@ -4074,29 +4076,53 @@ function getTokenExpirationTime(token) {
4074
4076
  return null;
4075
4077
  }
4076
4078
  }
4077
- function scheduleTokenRefresh(connection, token) {
4079
+ function scheduleTokenRefresh(connection, isServer) {
4078
4080
  // Clear any existing timer
4079
4081
  if (connection.tokenRefreshTimer) {
4080
- clearTimeout(connection.tokenRefreshTimer);
4082
+ clearInterval(connection.tokenRefreshTimer);
4081
4083
  connection.tokenRefreshTimer = null;
4082
4084
  }
4083
- if (!token) {
4084
- return; // No token = unauthenticated, no refresh needed
4085
- }
4086
- const expirationTime = getTokenExpirationTime(token);
4087
- if (!expirationTime) {
4088
- return; // Can't parse expiration
4089
- }
4090
- const refreshTime = expirationTime - TOKEN_REFRESH_BUFFER;
4091
- const delay = refreshTime - Date.now();
4092
- if (delay <= 0) {
4093
- // Token already expired or about to expire, reconnect immediately
4094
- reconnectWithNewAuthV2();
4095
- return;
4096
- }
4097
- connection.tokenRefreshTimer = setTimeout(() => {
4098
- reconnectWithNewAuthV2();
4099
- }, delay);
4085
+ // Periodic check: every TOKEN_CHECK_INTERVAL, check if the token needs refreshing.
4086
+ // This replaces the old single setTimeout approach which was unreliable for long
4087
+ // delays (browsers throttle/suspend timers in background tabs).
4088
+ connection.tokenRefreshTimer = setInterval(async () => {
4089
+ try {
4090
+ const currentToken = await getIdToken(isServer);
4091
+ if (!currentToken)
4092
+ return; // Unauthenticated, nothing to refresh
4093
+ const expirationTime = getTokenExpirationTime(currentToken);
4094
+ if (!expirationTime)
4095
+ return;
4096
+ const timeUntilExpiry = expirationTime - Date.now();
4097
+ if (timeUntilExpiry <= TOKEN_REFRESH_BUFFER) {
4098
+ console.info('[WS v2] Token expiring soon, proactively refreshing and reconnecting');
4099
+ // Refresh the token directly rather than going through getFreshAuthToken(),
4100
+ // which only refreshes when the token is within 60s of expiry. We want to
4101
+ // refresh as soon as we enter the buffer window to avoid unnecessary reconnects.
4102
+ try {
4103
+ const currentRefreshToken = await getRefreshToken(isServer);
4104
+ if (!currentRefreshToken) {
4105
+ console.warn('[WS v2] No refresh token available for proactive refresh');
4106
+ return;
4107
+ }
4108
+ const refreshData = await refreshSession(currentRefreshToken);
4109
+ if (refreshData && refreshData.idToken && refreshData.accessToken) {
4110
+ await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer);
4111
+ reconnectWithNewAuthV2();
4112
+ }
4113
+ else {
4114
+ console.warn('[WS v2] Proactive token refresh returned incomplete data');
4115
+ }
4116
+ }
4117
+ catch (refreshError) {
4118
+ console.warn('[WS v2] Proactive token refresh failed, will retry next interval:', refreshError);
4119
+ }
4120
+ }
4121
+ }
4122
+ catch (error) {
4123
+ console.error('[WS v2] Error in periodic token check:', error);
4124
+ }
4125
+ }, TOKEN_CHECK_INTERVAL);
4100
4126
  }
4101
4127
  async function getFreshAuthToken(isServer) {
4102
4128
  const currentToken = await getIdToken(isServer);
@@ -4106,10 +4132,12 @@ async function getFreshAuthToken(isServer) {
4106
4132
  if (!isTokenExpired(currentToken)) {
4107
4133
  return currentToken;
4108
4134
  }
4135
+ // Token is expired — attempt refresh
4109
4136
  try {
4110
4137
  const refreshToken = await getRefreshToken(isServer);
4111
4138
  if (!refreshToken) {
4112
- return currentToken;
4139
+ console.warn('[WS v2] Token expired but no refresh token available');
4140
+ return null;
4113
4141
  }
4114
4142
  const refreshData = await refreshSession(refreshToken);
4115
4143
  if (refreshData && refreshData.idToken && refreshData.accessToken) {
@@ -4120,7 +4148,11 @@ async function getFreshAuthToken(isServer) {
4120
4148
  catch (error) {
4121
4149
  console.error('[WS v2] Error refreshing token:', error);
4122
4150
  }
4123
- return currentToken;
4151
+ // Return null instead of the expired token to prevent infinite 401 reconnect storms.
4152
+ // The server accepts unauthenticated connections; auth-required subscriptions will
4153
+ // receive per-subscription errors via onError callbacks.
4154
+ console.warn('[WS v2] Token refresh failed, connecting without auth to prevent reconnect storm');
4155
+ return null;
4124
4156
  }
4125
4157
  function hasDisconnectedActiveConnections() {
4126
4158
  for (const connection of connections.values()) {
@@ -4186,6 +4218,7 @@ async function getOrCreateConnection(appId, isServer) {
4186
4218
  tokenRefreshTimer: null,
4187
4219
  lastMessageAt: Date.now(),
4188
4220
  keepaliveTimer: null,
4221
+ consecutiveAuthFailures: 0,
4189
4222
  };
4190
4223
  connections.set(appId, connection);
4191
4224
  // URL provider for reconnection with fresh tokens
@@ -4211,6 +4244,22 @@ async function getOrCreateConnection(appId, isServer) {
4211
4244
  const authToken = await getFreshAuthToken(isServer);
4212
4245
  if (authToken) {
4213
4246
  wsUrl.searchParams.append('authorization', authToken);
4247
+ // Successful token acquisition — reset failure counter
4248
+ connection.consecutiveAuthFailures = 0;
4249
+ }
4250
+ else {
4251
+ // Check if user WAS authenticated (had a token that expired).
4252
+ // If so, retry with exponential backoff before falling back to unauthenticated.
4253
+ const expiredToken = await getIdToken(isServer);
4254
+ if (expiredToken && isTokenExpired(expiredToken)) {
4255
+ connection.consecutiveAuthFailures++;
4256
+ if (connection.consecutiveAuthFailures <= MAX_AUTH_REFRESH_RETRIES) {
4257
+ console.warn(`[WS v2] Auth refresh failed (attempt ${connection.consecutiveAuthFailures}/${MAX_AUTH_REFRESH_RETRIES}), retrying with backoff`);
4258
+ throw new Error('Auth token refresh failed, retrying with backoff');
4259
+ }
4260
+ console.warn('[WS v2] Auth refresh retries exhausted, falling back to unauthenticated connection');
4261
+ }
4262
+ // No token at all (never authenticated) or retries exhausted — connect without auth
4214
4263
  }
4215
4264
  return wsUrl.toString();
4216
4265
  };
@@ -4223,11 +4272,9 @@ async function getOrCreateConnection(appId, isServer) {
4223
4272
  connection.isConnecting = false;
4224
4273
  connection.isConnected = true;
4225
4274
  connection.lastMessageAt = Date.now();
4226
- // Schedule token refresh before expiry
4227
- (async () => {
4228
- const token = await getIdToken(isServer);
4229
- scheduleTokenRefresh(connection, token);
4230
- })();
4275
+ connection.consecutiveAuthFailures = 0;
4276
+ // Schedule periodic token freshness checks
4277
+ scheduleTokenRefresh(connection, isServer);
4231
4278
  // Re-subscribe to all existing subscriptions after reconnect
4232
4279
  for (const sub of connection.subscriptions.values()) {
4233
4280
  sub.lastData = undefined;
@@ -4272,7 +4319,7 @@ async function getOrCreateConnection(appId, isServer) {
4272
4319
  ws.addEventListener('close', () => {
4273
4320
  connection.isConnected = false;
4274
4321
  if (connection.tokenRefreshTimer) {
4275
- clearTimeout(connection.tokenRefreshTimer);
4322
+ clearInterval(connection.tokenRefreshTimer);
4276
4323
  connection.tokenRefreshTimer = null;
4277
4324
  }
4278
4325
  if (connection.keepaliveTimer) {
@@ -4518,7 +4565,7 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
4518
4565
  if (connection.subscriptions.size === 0 && connection.ws) {
4519
4566
  // Clear token refresh timer
4520
4567
  if (connection.tokenRefreshTimer) {
4521
- clearTimeout(connection.tokenRefreshTimer);
4568
+ clearInterval(connection.tokenRefreshTimer);
4522
4569
  connection.tokenRefreshTimer = null;
4523
4570
  }
4524
4571
  connection.ws.close();
@@ -4534,7 +4581,7 @@ async function closeAllSubscriptionsV2() {
4534
4581
  for (const [appId, connection] of connections) {
4535
4582
  // Clear token refresh timer
4536
4583
  if (connection.tokenRefreshTimer) {
4537
- clearTimeout(connection.tokenRefreshTimer);
4584
+ clearInterval(connection.tokenRefreshTimer);
4538
4585
  connection.tokenRefreshTimer = null;
4539
4586
  }
4540
4587
  if (connection.ws) {
@@ -4603,6 +4650,8 @@ async function reconnectWithNewAuthV2() {
4603
4650
  pending.resolve();
4604
4651
  }
4605
4652
  connection.pendingUnsubscriptions.clear();
4653
+ // Reset auth failure counter — this is a proactive reconnect (login, token refresh)
4654
+ connection.consecutiveAuthFailures = 0;
4606
4655
  // Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
4607
4656
  // We use reconnect() which will close and re-open with fresh URL (including new token)
4608
4657
  try {