@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.mjs
CHANGED
|
@@ -3993,13 +3993,15 @@ async function syncItems(paths, options) {
|
|
|
3993
3993
|
const connections = new Map();
|
|
3994
3994
|
const responseCache = new Map();
|
|
3995
3995
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
3996
|
-
const TOKEN_REFRESH_BUFFER =
|
|
3996
|
+
const TOKEN_REFRESH_BUFFER = 10 * 60 * 1000; // Refresh token 10 minutes before expiry
|
|
3997
|
+
const TOKEN_CHECK_INTERVAL = 5 * 60 * 1000; // Check token freshness every 5 minutes
|
|
3997
3998
|
// ============ WebSocket Config ============
|
|
3998
3999
|
const BASE_MIN_RECONNECT_DELAY_MS = 1000;
|
|
3999
4000
|
const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
|
|
4000
4001
|
const MAX_RECONNECT_DELAY_MS = 300000;
|
|
4001
4002
|
const RECONNECT_DELAY_GROW_FACTOR = 1.8;
|
|
4002
4003
|
const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
|
|
4004
|
+
const MAX_AUTH_REFRESH_RETRIES = 5;
|
|
4003
4005
|
const WS_CONFIG = {
|
|
4004
4006
|
// Keep retrying indefinitely so long outages recover without page refresh.
|
|
4005
4007
|
maxRetries: Infinity,
|
|
@@ -4015,6 +4017,7 @@ const WS_CONFIG = {
|
|
|
4015
4017
|
const WS_V2_PATH = '/ws/v2';
|
|
4016
4018
|
let browserReconnectHooksAttached = false;
|
|
4017
4019
|
let lastBrowserTriggeredReconnectAt = 0;
|
|
4020
|
+
let reconnectInProgress = null;
|
|
4018
4021
|
// ============ Helper Functions ============
|
|
4019
4022
|
function generateSubscriptionId() {
|
|
4020
4023
|
return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
@@ -4054,29 +4057,54 @@ function getTokenExpirationTime(token) {
|
|
|
4054
4057
|
return null;
|
|
4055
4058
|
}
|
|
4056
4059
|
}
|
|
4057
|
-
function scheduleTokenRefresh(connection,
|
|
4060
|
+
function scheduleTokenRefresh(connection, isServer) {
|
|
4058
4061
|
// Clear any existing timer
|
|
4059
4062
|
if (connection.tokenRefreshTimer) {
|
|
4060
|
-
|
|
4063
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4061
4064
|
connection.tokenRefreshTimer = null;
|
|
4062
4065
|
}
|
|
4063
|
-
if
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4066
|
+
// Periodic check: every TOKEN_CHECK_INTERVAL, check if the token needs refreshing.
|
|
4067
|
+
// This replaces the old single setTimeout approach which was unreliable for long
|
|
4068
|
+
// delays (browsers throttle/suspend timers in background tabs).
|
|
4069
|
+
connection.tokenRefreshTimer = setInterval(async () => {
|
|
4070
|
+
try {
|
|
4071
|
+
const currentToken = await getIdToken(isServer);
|
|
4072
|
+
if (!currentToken)
|
|
4073
|
+
return; // Unauthenticated, nothing to refresh
|
|
4074
|
+
const expirationTime = getTokenExpirationTime(currentToken);
|
|
4075
|
+
if (!expirationTime)
|
|
4076
|
+
return;
|
|
4077
|
+
const timeUntilExpiry = expirationTime - Date.now();
|
|
4078
|
+
if (timeUntilExpiry <= TOKEN_REFRESH_BUFFER) {
|
|
4079
|
+
console.info('[WS v2] Token expiring soon, proactively refreshing');
|
|
4080
|
+
// Refresh the token and store it. Do NOT reconnect — the server only
|
|
4081
|
+
// verifies JWT at $connect time, so the existing connection continues
|
|
4082
|
+
// working. The fresh token is picked up by urlProvider the next time
|
|
4083
|
+
// a reconnect naturally happens (network drop, keepalive timeout, etc.).
|
|
4084
|
+
try {
|
|
4085
|
+
const currentRefreshToken = await getRefreshToken(isServer);
|
|
4086
|
+
if (!currentRefreshToken) {
|
|
4087
|
+
console.warn('[WS v2] No refresh token available for proactive refresh');
|
|
4088
|
+
return;
|
|
4089
|
+
}
|
|
4090
|
+
const refreshData = await refreshSession(currentRefreshToken);
|
|
4091
|
+
if (refreshData && refreshData.idToken && refreshData.accessToken) {
|
|
4092
|
+
await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer);
|
|
4093
|
+
console.info('[WS v2] Token refreshed successfully, stored for next connection');
|
|
4094
|
+
}
|
|
4095
|
+
else {
|
|
4096
|
+
console.warn('[WS v2] Proactive token refresh returned incomplete data');
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
catch (refreshError) {
|
|
4100
|
+
console.warn('[WS v2] Proactive token refresh failed, will retry next interval:', refreshError);
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
catch (error) {
|
|
4105
|
+
console.error('[WS v2] Error in periodic token check:', error);
|
|
4106
|
+
}
|
|
4107
|
+
}, TOKEN_CHECK_INTERVAL);
|
|
4080
4108
|
}
|
|
4081
4109
|
async function getFreshAuthToken(isServer) {
|
|
4082
4110
|
const currentToken = await getIdToken(isServer);
|
|
@@ -4086,10 +4114,12 @@ async function getFreshAuthToken(isServer) {
|
|
|
4086
4114
|
if (!isTokenExpired(currentToken)) {
|
|
4087
4115
|
return currentToken;
|
|
4088
4116
|
}
|
|
4117
|
+
// Token is expired — attempt refresh
|
|
4089
4118
|
try {
|
|
4090
4119
|
const refreshToken = await getRefreshToken(isServer);
|
|
4091
4120
|
if (!refreshToken) {
|
|
4092
|
-
|
|
4121
|
+
console.warn('[WS v2] Token expired but no refresh token available');
|
|
4122
|
+
return null;
|
|
4093
4123
|
}
|
|
4094
4124
|
const refreshData = await refreshSession(refreshToken);
|
|
4095
4125
|
if (refreshData && refreshData.idToken && refreshData.accessToken) {
|
|
@@ -4100,7 +4130,11 @@ async function getFreshAuthToken(isServer) {
|
|
|
4100
4130
|
catch (error) {
|
|
4101
4131
|
console.error('[WS v2] Error refreshing token:', error);
|
|
4102
4132
|
}
|
|
4103
|
-
|
|
4133
|
+
// Return null instead of the expired token to prevent infinite 401 reconnect storms.
|
|
4134
|
+
// The server accepts unauthenticated connections; auth-required subscriptions will
|
|
4135
|
+
// receive per-subscription errors via onError callbacks.
|
|
4136
|
+
console.warn('[WS v2] Token refresh failed, connecting without auth to prevent reconnect storm');
|
|
4137
|
+
return null;
|
|
4104
4138
|
}
|
|
4105
4139
|
function hasDisconnectedActiveConnections() {
|
|
4106
4140
|
for (const connection of connections.values()) {
|
|
@@ -4166,6 +4200,7 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4166
4200
|
tokenRefreshTimer: null,
|
|
4167
4201
|
lastMessageAt: Date.now(),
|
|
4168
4202
|
keepaliveTimer: null,
|
|
4203
|
+
consecutiveAuthFailures: 0,
|
|
4169
4204
|
};
|
|
4170
4205
|
connections.set(appId, connection);
|
|
4171
4206
|
// URL provider for reconnection with fresh tokens
|
|
@@ -4185,12 +4220,26 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4185
4220
|
else {
|
|
4186
4221
|
wsUrl.searchParams.append('appId', config.appId);
|
|
4187
4222
|
}
|
|
4188
|
-
// Add timestamp to prevent connection reuse issues
|
|
4189
|
-
wsUrl.searchParams.append('_t', Date.now().toString());
|
|
4190
4223
|
// Add auth token if available
|
|
4191
4224
|
const authToken = await getFreshAuthToken(isServer);
|
|
4192
4225
|
if (authToken) {
|
|
4193
4226
|
wsUrl.searchParams.append('authorization', authToken);
|
|
4227
|
+
// Successful token acquisition — reset failure counter
|
|
4228
|
+
connection.consecutiveAuthFailures = 0;
|
|
4229
|
+
}
|
|
4230
|
+
else {
|
|
4231
|
+
// Check if user WAS authenticated (had a token that expired).
|
|
4232
|
+
// If so, retry with exponential backoff before falling back to unauthenticated.
|
|
4233
|
+
const expiredToken = await getIdToken(isServer);
|
|
4234
|
+
if (expiredToken && isTokenExpired(expiredToken)) {
|
|
4235
|
+
connection.consecutiveAuthFailures++;
|
|
4236
|
+
if (connection.consecutiveAuthFailures <= MAX_AUTH_REFRESH_RETRIES) {
|
|
4237
|
+
console.warn(`[WS v2] Auth refresh failed (attempt ${connection.consecutiveAuthFailures}/${MAX_AUTH_REFRESH_RETRIES}), retrying with backoff`);
|
|
4238
|
+
throw new Error('Auth token refresh failed, retrying with backoff');
|
|
4239
|
+
}
|
|
4240
|
+
console.warn('[WS v2] Auth refresh retries exhausted, falling back to unauthenticated connection');
|
|
4241
|
+
}
|
|
4242
|
+
// No token at all (never authenticated) or retries exhausted — connect without auth
|
|
4194
4243
|
}
|
|
4195
4244
|
return wsUrl.toString();
|
|
4196
4245
|
};
|
|
@@ -4203,11 +4252,9 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4203
4252
|
connection.isConnecting = false;
|
|
4204
4253
|
connection.isConnected = true;
|
|
4205
4254
|
connection.lastMessageAt = Date.now();
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
scheduleTokenRefresh(connection, token);
|
|
4210
|
-
})();
|
|
4255
|
+
connection.consecutiveAuthFailures = 0;
|
|
4256
|
+
// Schedule periodic token freshness checks
|
|
4257
|
+
scheduleTokenRefresh(connection, isServer);
|
|
4211
4258
|
// Re-subscribe to all existing subscriptions after reconnect
|
|
4212
4259
|
for (const sub of connection.subscriptions.values()) {
|
|
4213
4260
|
sub.lastData = undefined;
|
|
@@ -4252,7 +4299,7 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4252
4299
|
ws.addEventListener('close', () => {
|
|
4253
4300
|
connection.isConnected = false;
|
|
4254
4301
|
if (connection.tokenRefreshTimer) {
|
|
4255
|
-
|
|
4302
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4256
4303
|
connection.tokenRefreshTimer = null;
|
|
4257
4304
|
}
|
|
4258
4305
|
if (connection.keepaliveTimer) {
|
|
@@ -4498,7 +4545,7 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
|
|
|
4498
4545
|
if (connection.subscriptions.size === 0 && connection.ws) {
|
|
4499
4546
|
// Clear token refresh timer
|
|
4500
4547
|
if (connection.tokenRefreshTimer) {
|
|
4501
|
-
|
|
4548
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4502
4549
|
connection.tokenRefreshTimer = null;
|
|
4503
4550
|
}
|
|
4504
4551
|
connection.ws.close();
|
|
@@ -4512,17 +4559,23 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
|
|
|
4512
4559
|
async function closeAllSubscriptionsV2() {
|
|
4513
4560
|
const closePromises = [];
|
|
4514
4561
|
for (const [appId, connection] of connections) {
|
|
4515
|
-
// Clear token refresh timer
|
|
4516
4562
|
if (connection.tokenRefreshTimer) {
|
|
4517
|
-
|
|
4563
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4518
4564
|
connection.tokenRefreshTimer = null;
|
|
4519
4565
|
}
|
|
4566
|
+
if (connection.keepaliveTimer) {
|
|
4567
|
+
clearInterval(connection.keepaliveTimer);
|
|
4568
|
+
connection.keepaliveTimer = null;
|
|
4569
|
+
}
|
|
4520
4570
|
if (connection.ws) {
|
|
4571
|
+
const ws = connection.ws;
|
|
4572
|
+
connection.ws = null;
|
|
4573
|
+
connection.isConnected = false;
|
|
4521
4574
|
closePromises.push(new Promise((resolve) => {
|
|
4522
|
-
|
|
4575
|
+
ws.addEventListener('close', () => {
|
|
4523
4576
|
resolve();
|
|
4524
4577
|
});
|
|
4525
|
-
|
|
4578
|
+
ws.close();
|
|
4526
4579
|
}));
|
|
4527
4580
|
}
|
|
4528
4581
|
connections.delete(appId);
|
|
@@ -4569,7 +4622,18 @@ function getCachedDataV2(path, prompt) {
|
|
|
4569
4622
|
* Note: Existing subscriptions will receive new initial data after reconnection.
|
|
4570
4623
|
*/
|
|
4571
4624
|
async function reconnectWithNewAuthV2() {
|
|
4572
|
-
|
|
4625
|
+
if (reconnectInProgress) {
|
|
4626
|
+
return reconnectInProgress;
|
|
4627
|
+
}
|
|
4628
|
+
reconnectInProgress = doReconnectWithNewAuth();
|
|
4629
|
+
try {
|
|
4630
|
+
await reconnectInProgress;
|
|
4631
|
+
}
|
|
4632
|
+
finally {
|
|
4633
|
+
reconnectInProgress = null;
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
async function doReconnectWithNewAuth() {
|
|
4573
4637
|
for (const [appId, connection] of connections) {
|
|
4574
4638
|
if (!connection.ws) {
|
|
4575
4639
|
continue;
|
|
@@ -4578,16 +4642,15 @@ async function reconnectWithNewAuthV2() {
|
|
|
4578
4642
|
pending.reject(new Error('Connection reconnecting due to auth change'));
|
|
4579
4643
|
}
|
|
4580
4644
|
connection.pendingSubscriptions.clear();
|
|
4581
|
-
// Resolve any pending unsubscriptions - connection is closing anyway
|
|
4582
4645
|
for (const [, pending] of connection.pendingUnsubscriptions) {
|
|
4583
4646
|
pending.resolve();
|
|
4584
4647
|
}
|
|
4585
4648
|
connection.pendingUnsubscriptions.clear();
|
|
4649
|
+
// Reset auth failure counter — this is a proactive reconnect (login, token refresh)
|
|
4650
|
+
connection.consecutiveAuthFailures = 0;
|
|
4586
4651
|
// Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
|
|
4587
4652
|
// We use reconnect() which will close and re-open with fresh URL (including new token)
|
|
4588
4653
|
try {
|
|
4589
|
-
// ReconnectingWebSocket.reconnect() closes current connection and opens a new one
|
|
4590
|
-
// The urlProvider will be called again, getting a fresh auth token
|
|
4591
4654
|
connection.ws.reconnect();
|
|
4592
4655
|
}
|
|
4593
4656
|
catch (error) {
|