@pooflabs/core 0.0.33 → 0.0.35
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 +102 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +102 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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 =
|
|
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,
|
|
@@ -4035,6 +4037,7 @@ const WS_CONFIG = {
|
|
|
4035
4037
|
const WS_V2_PATH = '/ws/v2';
|
|
4036
4038
|
let browserReconnectHooksAttached = false;
|
|
4037
4039
|
let lastBrowserTriggeredReconnectAt = 0;
|
|
4040
|
+
let reconnectInProgress = null;
|
|
4038
4041
|
// ============ Helper Functions ============
|
|
4039
4042
|
function generateSubscriptionId() {
|
|
4040
4043
|
return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
@@ -4074,29 +4077,54 @@ function getTokenExpirationTime(token) {
|
|
|
4074
4077
|
return null;
|
|
4075
4078
|
}
|
|
4076
4079
|
}
|
|
4077
|
-
function scheduleTokenRefresh(connection,
|
|
4080
|
+
function scheduleTokenRefresh(connection, isServer) {
|
|
4078
4081
|
// Clear any existing timer
|
|
4079
4082
|
if (connection.tokenRefreshTimer) {
|
|
4080
|
-
|
|
4083
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4081
4084
|
connection.tokenRefreshTimer = null;
|
|
4082
4085
|
}
|
|
4083
|
-
if
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4086
|
+
// Periodic check: every TOKEN_CHECK_INTERVAL, check if the token needs refreshing.
|
|
4087
|
+
// This replaces the old single setTimeout approach which was unreliable for long
|
|
4088
|
+
// delays (browsers throttle/suspend timers in background tabs).
|
|
4089
|
+
connection.tokenRefreshTimer = setInterval(async () => {
|
|
4090
|
+
try {
|
|
4091
|
+
const currentToken = await getIdToken(isServer);
|
|
4092
|
+
if (!currentToken)
|
|
4093
|
+
return; // Unauthenticated, nothing to refresh
|
|
4094
|
+
const expirationTime = getTokenExpirationTime(currentToken);
|
|
4095
|
+
if (!expirationTime)
|
|
4096
|
+
return;
|
|
4097
|
+
const timeUntilExpiry = expirationTime - Date.now();
|
|
4098
|
+
if (timeUntilExpiry <= TOKEN_REFRESH_BUFFER) {
|
|
4099
|
+
console.info('[WS v2] Token expiring soon, proactively refreshing');
|
|
4100
|
+
// Refresh the token and store it. Do NOT reconnect — the server only
|
|
4101
|
+
// verifies JWT at $connect time, so the existing connection continues
|
|
4102
|
+
// working. The fresh token is picked up by urlProvider the next time
|
|
4103
|
+
// a reconnect naturally happens (network drop, keepalive timeout, etc.).
|
|
4104
|
+
try {
|
|
4105
|
+
const currentRefreshToken = await getRefreshToken(isServer);
|
|
4106
|
+
if (!currentRefreshToken) {
|
|
4107
|
+
console.warn('[WS v2] No refresh token available for proactive refresh');
|
|
4108
|
+
return;
|
|
4109
|
+
}
|
|
4110
|
+
const refreshData = await refreshSession(currentRefreshToken);
|
|
4111
|
+
if (refreshData && refreshData.idToken && refreshData.accessToken) {
|
|
4112
|
+
await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer);
|
|
4113
|
+
console.info('[WS v2] Token refreshed successfully, stored for next connection');
|
|
4114
|
+
}
|
|
4115
|
+
else {
|
|
4116
|
+
console.warn('[WS v2] Proactive token refresh returned incomplete data');
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
catch (refreshError) {
|
|
4120
|
+
console.warn('[WS v2] Proactive token refresh failed, will retry next interval:', refreshError);
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
catch (error) {
|
|
4125
|
+
console.error('[WS v2] Error in periodic token check:', error);
|
|
4126
|
+
}
|
|
4127
|
+
}, TOKEN_CHECK_INTERVAL);
|
|
4100
4128
|
}
|
|
4101
4129
|
async function getFreshAuthToken(isServer) {
|
|
4102
4130
|
const currentToken = await getIdToken(isServer);
|
|
@@ -4106,10 +4134,12 @@ async function getFreshAuthToken(isServer) {
|
|
|
4106
4134
|
if (!isTokenExpired(currentToken)) {
|
|
4107
4135
|
return currentToken;
|
|
4108
4136
|
}
|
|
4137
|
+
// Token is expired — attempt refresh
|
|
4109
4138
|
try {
|
|
4110
4139
|
const refreshToken = await getRefreshToken(isServer);
|
|
4111
4140
|
if (!refreshToken) {
|
|
4112
|
-
|
|
4141
|
+
console.warn('[WS v2] Token expired but no refresh token available');
|
|
4142
|
+
return null;
|
|
4113
4143
|
}
|
|
4114
4144
|
const refreshData = await refreshSession(refreshToken);
|
|
4115
4145
|
if (refreshData && refreshData.idToken && refreshData.accessToken) {
|
|
@@ -4120,7 +4150,11 @@ async function getFreshAuthToken(isServer) {
|
|
|
4120
4150
|
catch (error) {
|
|
4121
4151
|
console.error('[WS v2] Error refreshing token:', error);
|
|
4122
4152
|
}
|
|
4123
|
-
|
|
4153
|
+
// Return null instead of the expired token to prevent infinite 401 reconnect storms.
|
|
4154
|
+
// The server accepts unauthenticated connections; auth-required subscriptions will
|
|
4155
|
+
// receive per-subscription errors via onError callbacks.
|
|
4156
|
+
console.warn('[WS v2] Token refresh failed, connecting without auth to prevent reconnect storm');
|
|
4157
|
+
return null;
|
|
4124
4158
|
}
|
|
4125
4159
|
function hasDisconnectedActiveConnections() {
|
|
4126
4160
|
for (const connection of connections.values()) {
|
|
@@ -4186,6 +4220,7 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4186
4220
|
tokenRefreshTimer: null,
|
|
4187
4221
|
lastMessageAt: Date.now(),
|
|
4188
4222
|
keepaliveTimer: null,
|
|
4223
|
+
consecutiveAuthFailures: 0,
|
|
4189
4224
|
};
|
|
4190
4225
|
connections.set(appId, connection);
|
|
4191
4226
|
// URL provider for reconnection with fresh tokens
|
|
@@ -4205,12 +4240,26 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4205
4240
|
else {
|
|
4206
4241
|
wsUrl.searchParams.append('appId', config.appId);
|
|
4207
4242
|
}
|
|
4208
|
-
// Add timestamp to prevent connection reuse issues
|
|
4209
|
-
wsUrl.searchParams.append('_t', Date.now().toString());
|
|
4210
4243
|
// Add auth token if available
|
|
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
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4568
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4522
4569
|
connection.tokenRefreshTimer = null;
|
|
4523
4570
|
}
|
|
4524
4571
|
connection.ws.close();
|
|
@@ -4532,17 +4579,23 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
|
|
|
4532
4579
|
async function closeAllSubscriptionsV2() {
|
|
4533
4580
|
const closePromises = [];
|
|
4534
4581
|
for (const [appId, connection] of connections) {
|
|
4535
|
-
// Clear token refresh timer
|
|
4536
4582
|
if (connection.tokenRefreshTimer) {
|
|
4537
|
-
|
|
4583
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4538
4584
|
connection.tokenRefreshTimer = null;
|
|
4539
4585
|
}
|
|
4586
|
+
if (connection.keepaliveTimer) {
|
|
4587
|
+
clearInterval(connection.keepaliveTimer);
|
|
4588
|
+
connection.keepaliveTimer = null;
|
|
4589
|
+
}
|
|
4540
4590
|
if (connection.ws) {
|
|
4591
|
+
const ws = connection.ws;
|
|
4592
|
+
connection.ws = null;
|
|
4593
|
+
connection.isConnected = false;
|
|
4541
4594
|
closePromises.push(new Promise((resolve) => {
|
|
4542
|
-
|
|
4595
|
+
ws.addEventListener('close', () => {
|
|
4543
4596
|
resolve();
|
|
4544
4597
|
});
|
|
4545
|
-
|
|
4598
|
+
ws.close();
|
|
4546
4599
|
}));
|
|
4547
4600
|
}
|
|
4548
4601
|
connections.delete(appId);
|
|
@@ -4589,7 +4642,18 @@ function getCachedDataV2(path, prompt) {
|
|
|
4589
4642
|
* Note: Existing subscriptions will receive new initial data after reconnection.
|
|
4590
4643
|
*/
|
|
4591
4644
|
async function reconnectWithNewAuthV2() {
|
|
4592
|
-
|
|
4645
|
+
if (reconnectInProgress) {
|
|
4646
|
+
return reconnectInProgress;
|
|
4647
|
+
}
|
|
4648
|
+
reconnectInProgress = doReconnectWithNewAuth();
|
|
4649
|
+
try {
|
|
4650
|
+
await reconnectInProgress;
|
|
4651
|
+
}
|
|
4652
|
+
finally {
|
|
4653
|
+
reconnectInProgress = null;
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
async function doReconnectWithNewAuth() {
|
|
4593
4657
|
for (const [appId, connection] of connections) {
|
|
4594
4658
|
if (!connection.ws) {
|
|
4595
4659
|
continue;
|
|
@@ -4598,16 +4662,15 @@ async function reconnectWithNewAuthV2() {
|
|
|
4598
4662
|
pending.reject(new Error('Connection reconnecting due to auth change'));
|
|
4599
4663
|
}
|
|
4600
4664
|
connection.pendingSubscriptions.clear();
|
|
4601
|
-
// Resolve any pending unsubscriptions - connection is closing anyway
|
|
4602
4665
|
for (const [, pending] of connection.pendingUnsubscriptions) {
|
|
4603
4666
|
pending.resolve();
|
|
4604
4667
|
}
|
|
4605
4668
|
connection.pendingUnsubscriptions.clear();
|
|
4669
|
+
// Reset auth failure counter — this is a proactive reconnect (login, token refresh)
|
|
4670
|
+
connection.consecutiveAuthFailures = 0;
|
|
4606
4671
|
// Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
|
|
4607
4672
|
// We use reconnect() which will close and re-open with fresh URL (including new token)
|
|
4608
4673
|
try {
|
|
4609
|
-
// ReconnectingWebSocket.reconnect() closes current connection and opens a new one
|
|
4610
|
-
// The urlProvider will be called again, getting a fresh auth token
|
|
4611
4674
|
connection.ws.reconnect();
|
|
4612
4675
|
}
|
|
4613
4676
|
catch (error) {
|