@pooflabs/core 0.0.32 → 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/client/operations.d.ts +73 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +276 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +275 -44
- package/dist/index.mjs.map +1 -1
- package/dist/utils/server-session-manager.d.ts +1 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -119,10 +119,14 @@ class WebSessionManager {
|
|
|
119
119
|
const newObj = JSON.parse(newSession);
|
|
120
120
|
return { address: newObj.address, session: newObj };
|
|
121
121
|
}
|
|
122
|
+
// Refresh failed — clear stale session to prevent retry loops
|
|
123
|
+
this.clearSession();
|
|
122
124
|
return null;
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
catch (err) {
|
|
128
|
+
// Token decode or refresh failed — clear stale session to prevent retry loops
|
|
129
|
+
this.clearSession();
|
|
126
130
|
return null;
|
|
127
131
|
}
|
|
128
132
|
return { address: sessionObj.address, session: sessionObj };
|
|
@@ -3046,15 +3050,30 @@ class ServerSessionManager {
|
|
|
3046
3050
|
constructor() {
|
|
3047
3051
|
/* Private cache (lives for the life of the process) */
|
|
3048
3052
|
this.session = null;
|
|
3053
|
+
/* Coalesce concurrent getSession() calls into a single in-flight request */
|
|
3054
|
+
this.pendingSession = null;
|
|
3049
3055
|
}
|
|
3050
3056
|
/* ---------------------------------------------- *
|
|
3051
3057
|
* GET (lazy-fetch)
|
|
3052
3058
|
* ---------------------------------------------- */
|
|
3053
3059
|
async getSession() {
|
|
3054
|
-
if (this.session
|
|
3055
|
-
this.session
|
|
3060
|
+
if (this.session !== null) {
|
|
3061
|
+
return this.session;
|
|
3062
|
+
}
|
|
3063
|
+
// If a session creation is already in-flight, reuse that promise
|
|
3064
|
+
// instead of firing a second concurrent request.
|
|
3065
|
+
if (this.pendingSession !== null) {
|
|
3066
|
+
return this.pendingSession;
|
|
3056
3067
|
}
|
|
3057
|
-
|
|
3068
|
+
this.pendingSession = createSession()
|
|
3069
|
+
.then((session) => {
|
|
3070
|
+
this.session = session;
|
|
3071
|
+
return session;
|
|
3072
|
+
})
|
|
3073
|
+
.finally(() => {
|
|
3074
|
+
this.pendingSession = null;
|
|
3075
|
+
});
|
|
3076
|
+
return this.pendingSession;
|
|
3058
3077
|
}
|
|
3059
3078
|
/* ---------------------------------------------- *
|
|
3060
3079
|
* STORE (overwrites the cached value)
|
|
@@ -3067,6 +3086,7 @@ class ServerSessionManager {
|
|
|
3067
3086
|
* ---------------------------------------------- */
|
|
3068
3087
|
clearSession() {
|
|
3069
3088
|
this.session = null;
|
|
3089
|
+
this.pendingSession = null;
|
|
3070
3090
|
}
|
|
3071
3091
|
/* ---------------------------------------------- *
|
|
3072
3092
|
* QUICK helpers
|
|
@@ -3369,6 +3389,150 @@ function hashForKey$1(value) {
|
|
|
3369
3389
|
}
|
|
3370
3390
|
return h.toString(36);
|
|
3371
3391
|
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Validates that a field name is a safe identifier (alphanumeric, underscores, dots for nested paths).
|
|
3394
|
+
* Prevents prompt injection via crafted field names.
|
|
3395
|
+
*/
|
|
3396
|
+
function validateFieldName(field) {
|
|
3397
|
+
if (!/^[a-zA-Z0-9_.]+$/.test(field)) {
|
|
3398
|
+
throw new Error(`Invalid field name "${field}". Field names must only contain letters, numbers, underscores, and dots.`);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* Parses a raw aggregation result (e.g. [{ count: 42 }] or [{ _id: null, total: 100 }])
|
|
3403
|
+
* into a single numeric value.
|
|
3404
|
+
*/
|
|
3405
|
+
function parseAggregateValue(result) {
|
|
3406
|
+
if (typeof result === 'number')
|
|
3407
|
+
return result;
|
|
3408
|
+
if (Array.isArray(result)) {
|
|
3409
|
+
if (result.length === 0)
|
|
3410
|
+
return 0;
|
|
3411
|
+
// Multiple elements — not a collapsed aggregate result
|
|
3412
|
+
if (result.length > 1) {
|
|
3413
|
+
throw new Error(`Unexpected aggregate result: got array with ${result.length} elements. The AI may have returned full documents instead of an aggregation.`);
|
|
3414
|
+
}
|
|
3415
|
+
const first = result[0];
|
|
3416
|
+
if (typeof first === 'number')
|
|
3417
|
+
return first;
|
|
3418
|
+
if (first && typeof first === 'object') {
|
|
3419
|
+
// $count stage returns { count: N }
|
|
3420
|
+
if (typeof first.count === 'number')
|
|
3421
|
+
return first.count;
|
|
3422
|
+
// $group stage returns { _id: null, result: N } — expect _id + one numeric field
|
|
3423
|
+
const numericEntries = Object.entries(first).filter(([key, val]) => key !== '_id' && typeof val === 'number');
|
|
3424
|
+
if (numericEntries.length === 1)
|
|
3425
|
+
return numericEntries[0][1];
|
|
3426
|
+
}
|
|
3427
|
+
// Avoid leaking document contents into error messages
|
|
3428
|
+
const shape = first && typeof first === 'object' ? `{${Object.keys(first).join(', ')}}` : String(first);
|
|
3429
|
+
throw new Error(`Unexpected aggregate result shape: ${shape}. Expected {count: N} or {_id: null, <field>: N}.`);
|
|
3430
|
+
}
|
|
3431
|
+
if (result && typeof result === 'object' && typeof result.count === 'number') {
|
|
3432
|
+
return result.count;
|
|
3433
|
+
}
|
|
3434
|
+
throw new Error(`Unexpected aggregate result type: ${typeof result}. Expected a number, array, or object with a count field.`);
|
|
3435
|
+
}
|
|
3436
|
+
/**
|
|
3437
|
+
* Count items in a collection path. Returns a numeric result.
|
|
3438
|
+
*
|
|
3439
|
+
* This uses the AI query engine with a count-specific prompt prefix,
|
|
3440
|
+
* so TaroBase will generate a $count aggregation pipeline and return
|
|
3441
|
+
* just the count rather than full documents.
|
|
3442
|
+
*
|
|
3443
|
+
* IMPORTANT: This only works for collections where the read policy is "true".
|
|
3444
|
+
* If the read policy requires per-document checks, the server will return
|
|
3445
|
+
* an error because aggregate counts cannot be performed without pulling all
|
|
3446
|
+
* documents for access control evaluation.
|
|
3447
|
+
*
|
|
3448
|
+
* @param path - Collection path (e.g., "posts", "users/abc/comments")
|
|
3449
|
+
* @param opts - Optional filter prompt and overrides
|
|
3450
|
+
* @returns AggregateResult with the count value
|
|
3451
|
+
*/
|
|
3452
|
+
async function count(path, opts = {}) {
|
|
3453
|
+
const prefix = 'Return ONLY the total count of matching documents. Use the $count stage to produce a field named "count". Do NOT return the documents themselves.';
|
|
3454
|
+
const fullPrompt = opts.prompt
|
|
3455
|
+
? `${prefix} Filter: ${opts.prompt}`
|
|
3456
|
+
: prefix;
|
|
3457
|
+
const result = await get(path, { prompt: fullPrompt, bypassCache: true, _overrides: opts._overrides });
|
|
3458
|
+
return { value: parseAggregateValue(result) };
|
|
3459
|
+
}
|
|
3460
|
+
/**
|
|
3461
|
+
* Run an aggregate operation on a collection path. Returns a numeric result.
|
|
3462
|
+
*
|
|
3463
|
+
* Supported operations:
|
|
3464
|
+
* - count: Total number of documents
|
|
3465
|
+
* - uniqueCount: Number of distinct values for a field
|
|
3466
|
+
* - sum: Sum of a numeric field
|
|
3467
|
+
* - avg: Average of a numeric field
|
|
3468
|
+
* - min: Minimum value of a numeric field
|
|
3469
|
+
* - max: Maximum value of a numeric field
|
|
3470
|
+
*
|
|
3471
|
+
* IMPORTANT: This only works for collections where the read policy is "true".
|
|
3472
|
+
* If the read policy requires per-document checks, the server will return
|
|
3473
|
+
* an error because aggregate operations cannot be performed without pulling
|
|
3474
|
+
* all documents for access control evaluation.
|
|
3475
|
+
*
|
|
3476
|
+
* @param path - Collection path (e.g., "posts", "users/abc/comments")
|
|
3477
|
+
* @param operation - The aggregate operation to perform
|
|
3478
|
+
* @param opts - Options including optional filter prompt and field name
|
|
3479
|
+
* @returns AggregateResult with the computed numeric value
|
|
3480
|
+
*/
|
|
3481
|
+
async function aggregate(path, operation, opts = {}) {
|
|
3482
|
+
let prefix;
|
|
3483
|
+
switch (operation) {
|
|
3484
|
+
case 'count':
|
|
3485
|
+
prefix = 'Return ONLY the total count of matching documents. Use the $count stage to produce a field named "count". Do NOT return the documents themselves.';
|
|
3486
|
+
break;
|
|
3487
|
+
case 'uniqueCount':
|
|
3488
|
+
if (!opts.field)
|
|
3489
|
+
throw new Error('aggregate "uniqueCount" requires a field option');
|
|
3490
|
+
validateFieldName(opts.field);
|
|
3491
|
+
prefix = `Return ONLY the count of unique/distinct values of the "${opts.field}" field. Use $group with _id set to "$${opts.field}" then $count to produce a field named "count". Do NOT return the documents themselves.`;
|
|
3492
|
+
break;
|
|
3493
|
+
case 'sum':
|
|
3494
|
+
if (!opts.field)
|
|
3495
|
+
throw new Error('aggregate "sum" requires a field option');
|
|
3496
|
+
validateFieldName(opts.field);
|
|
3497
|
+
prefix = `Return ONLY the sum of the "${opts.field}" field across all matching documents. Use $group with _id: null and result: { $sum: "$${opts.field}" }. Do NOT return the documents themselves.`;
|
|
3498
|
+
break;
|
|
3499
|
+
case 'avg':
|
|
3500
|
+
if (!opts.field)
|
|
3501
|
+
throw new Error('aggregate "avg" requires a field option');
|
|
3502
|
+
validateFieldName(opts.field);
|
|
3503
|
+
prefix = `Return ONLY the average of the "${opts.field}" field across all matching documents. Use $group with _id: null and result: { $avg: "$${opts.field}" }. Do NOT return the documents themselves.`;
|
|
3504
|
+
break;
|
|
3505
|
+
case 'min':
|
|
3506
|
+
if (!opts.field)
|
|
3507
|
+
throw new Error('aggregate "min" requires a field option');
|
|
3508
|
+
validateFieldName(opts.field);
|
|
3509
|
+
prefix = `Return ONLY the minimum value of the "${opts.field}" field across all matching documents. Use $group with _id: null and result: { $min: "$${opts.field}" }. Do NOT return the documents themselves.`;
|
|
3510
|
+
break;
|
|
3511
|
+
case 'max':
|
|
3512
|
+
if (!opts.field)
|
|
3513
|
+
throw new Error('aggregate "max" requires a field option');
|
|
3514
|
+
validateFieldName(opts.field);
|
|
3515
|
+
prefix = `Return ONLY the maximum value of the "${opts.field}" field across all matching documents. Use $group with _id: null and result: { $max: "$${opts.field}" }. Do NOT return the documents themselves.`;
|
|
3516
|
+
break;
|
|
3517
|
+
default:
|
|
3518
|
+
throw new Error(`Unsupported aggregate operation: ${operation}`);
|
|
3519
|
+
}
|
|
3520
|
+
const fullPrompt = opts.prompt
|
|
3521
|
+
? `${prefix} Filter: ${opts.prompt}`
|
|
3522
|
+
: prefix;
|
|
3523
|
+
const result = await get(path, { prompt: fullPrompt, bypassCache: true, _overrides: opts._overrides });
|
|
3524
|
+
// For uniqueCount, the AI may return $group results without the final $count
|
|
3525
|
+
// stage, producing [{_id: "val1"}, {_id: "val2"}, ...]. Verify elements look
|
|
3526
|
+
// like $group output (only _id key) before using array length as the count.
|
|
3527
|
+
if (operation === 'uniqueCount' && Array.isArray(result) && result.length > 1) {
|
|
3528
|
+
const looksLikeGroupOutput = result.every((el) => el && typeof el === 'object' && Object.keys(el).length === 1 && '_id' in el);
|
|
3529
|
+
if (looksLikeGroupOutput) {
|
|
3530
|
+
return { value: result.length };
|
|
3531
|
+
}
|
|
3532
|
+
throw new Error(`Unexpected uniqueCount result: got ${result.length} elements that don't match $group output shape.`);
|
|
3533
|
+
}
|
|
3534
|
+
return { value: parseAggregateValue(result) };
|
|
3535
|
+
}
|
|
3372
3536
|
async function get(path, opts = {}) {
|
|
3373
3537
|
try {
|
|
3374
3538
|
let normalizedPath = path.startsWith("/") ? path.slice(1) : path;
|
|
@@ -3829,13 +3993,15 @@ async function syncItems(paths, options) {
|
|
|
3829
3993
|
const connections = new Map();
|
|
3830
3994
|
const responseCache = new Map();
|
|
3831
3995
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
3832
|
-
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
|
|
3833
3998
|
// ============ WebSocket Config ============
|
|
3834
3999
|
const BASE_MIN_RECONNECT_DELAY_MS = 1000;
|
|
3835
4000
|
const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
|
|
3836
4001
|
const MAX_RECONNECT_DELAY_MS = 300000;
|
|
3837
4002
|
const RECONNECT_DELAY_GROW_FACTOR = 1.8;
|
|
3838
|
-
const MIN_BROWSER_RECONNECT_INTERVAL_MS =
|
|
4003
|
+
const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
|
|
4004
|
+
const MAX_AUTH_REFRESH_RETRIES = 5;
|
|
3839
4005
|
const WS_CONFIG = {
|
|
3840
4006
|
// Keep retrying indefinitely so long outages recover without page refresh.
|
|
3841
4007
|
maxRetries: Infinity,
|
|
@@ -3890,29 +4056,53 @@ function getTokenExpirationTime(token) {
|
|
|
3890
4056
|
return null;
|
|
3891
4057
|
}
|
|
3892
4058
|
}
|
|
3893
|
-
function scheduleTokenRefresh(connection,
|
|
4059
|
+
function scheduleTokenRefresh(connection, isServer) {
|
|
3894
4060
|
// Clear any existing timer
|
|
3895
4061
|
if (connection.tokenRefreshTimer) {
|
|
3896
|
-
|
|
4062
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
3897
4063
|
connection.tokenRefreshTimer = null;
|
|
3898
4064
|
}
|
|
3899
|
-
if
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
4065
|
+
// Periodic check: every TOKEN_CHECK_INTERVAL, check if the token needs refreshing.
|
|
4066
|
+
// This replaces the old single setTimeout approach which was unreliable for long
|
|
4067
|
+
// delays (browsers throttle/suspend timers in background tabs).
|
|
4068
|
+
connection.tokenRefreshTimer = setInterval(async () => {
|
|
4069
|
+
try {
|
|
4070
|
+
const currentToken = await getIdToken(isServer);
|
|
4071
|
+
if (!currentToken)
|
|
4072
|
+
return; // Unauthenticated, nothing to refresh
|
|
4073
|
+
const expirationTime = getTokenExpirationTime(currentToken);
|
|
4074
|
+
if (!expirationTime)
|
|
4075
|
+
return;
|
|
4076
|
+
const timeUntilExpiry = expirationTime - Date.now();
|
|
4077
|
+
if (timeUntilExpiry <= TOKEN_REFRESH_BUFFER) {
|
|
4078
|
+
console.info('[WS v2] Token expiring soon, proactively refreshing and reconnecting');
|
|
4079
|
+
// Refresh the token directly rather than going through getFreshAuthToken(),
|
|
4080
|
+
// which only refreshes when the token is within 60s of expiry. We want to
|
|
4081
|
+
// refresh as soon as we enter the buffer window to avoid unnecessary reconnects.
|
|
4082
|
+
try {
|
|
4083
|
+
const currentRefreshToken = await getRefreshToken(isServer);
|
|
4084
|
+
if (!currentRefreshToken) {
|
|
4085
|
+
console.warn('[WS v2] No refresh token available for proactive refresh');
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
const refreshData = await refreshSession(currentRefreshToken);
|
|
4089
|
+
if (refreshData && refreshData.idToken && refreshData.accessToken) {
|
|
4090
|
+
await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer);
|
|
4091
|
+
reconnectWithNewAuthV2();
|
|
4092
|
+
}
|
|
4093
|
+
else {
|
|
4094
|
+
console.warn('[WS v2] Proactive token refresh returned incomplete data');
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
catch (refreshError) {
|
|
4098
|
+
console.warn('[WS v2] Proactive token refresh failed, will retry next interval:', refreshError);
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
catch (error) {
|
|
4103
|
+
console.error('[WS v2] Error in periodic token check:', error);
|
|
4104
|
+
}
|
|
4105
|
+
}, TOKEN_CHECK_INTERVAL);
|
|
3916
4106
|
}
|
|
3917
4107
|
async function getFreshAuthToken(isServer) {
|
|
3918
4108
|
const currentToken = await getIdToken(isServer);
|
|
@@ -3922,10 +4112,12 @@ async function getFreshAuthToken(isServer) {
|
|
|
3922
4112
|
if (!isTokenExpired(currentToken)) {
|
|
3923
4113
|
return currentToken;
|
|
3924
4114
|
}
|
|
4115
|
+
// Token is expired — attempt refresh
|
|
3925
4116
|
try {
|
|
3926
4117
|
const refreshToken = await getRefreshToken(isServer);
|
|
3927
4118
|
if (!refreshToken) {
|
|
3928
|
-
|
|
4119
|
+
console.warn('[WS v2] Token expired but no refresh token available');
|
|
4120
|
+
return null;
|
|
3929
4121
|
}
|
|
3930
4122
|
const refreshData = await refreshSession(refreshToken);
|
|
3931
4123
|
if (refreshData && refreshData.idToken && refreshData.accessToken) {
|
|
@@ -3936,7 +4128,11 @@ async function getFreshAuthToken(isServer) {
|
|
|
3936
4128
|
catch (error) {
|
|
3937
4129
|
console.error('[WS v2] Error refreshing token:', error);
|
|
3938
4130
|
}
|
|
3939
|
-
|
|
4131
|
+
// Return null instead of the expired token to prevent infinite 401 reconnect storms.
|
|
4132
|
+
// The server accepts unauthenticated connections; auth-required subscriptions will
|
|
4133
|
+
// receive per-subscription errors via onError callbacks.
|
|
4134
|
+
console.warn('[WS v2] Token refresh failed, connecting without auth to prevent reconnect storm');
|
|
4135
|
+
return null;
|
|
3940
4136
|
}
|
|
3941
4137
|
function hasDisconnectedActiveConnections() {
|
|
3942
4138
|
for (const connection of connections.values()) {
|
|
@@ -4000,6 +4196,9 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4000
4196
|
isConnected: false,
|
|
4001
4197
|
appId,
|
|
4002
4198
|
tokenRefreshTimer: null,
|
|
4199
|
+
lastMessageAt: Date.now(),
|
|
4200
|
+
keepaliveTimer: null,
|
|
4201
|
+
consecutiveAuthFailures: 0,
|
|
4003
4202
|
};
|
|
4004
4203
|
connections.set(appId, connection);
|
|
4005
4204
|
// URL provider for reconnection with fresh tokens
|
|
@@ -4025,6 +4224,22 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4025
4224
|
const authToken = await getFreshAuthToken(isServer);
|
|
4026
4225
|
if (authToken) {
|
|
4027
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
|
|
4028
4243
|
}
|
|
4029
4244
|
return wsUrl.toString();
|
|
4030
4245
|
};
|
|
@@ -4036,18 +4251,34 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4036
4251
|
ws.addEventListener('open', () => {
|
|
4037
4252
|
connection.isConnecting = false;
|
|
4038
4253
|
connection.isConnected = true;
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
})();
|
|
4254
|
+
connection.lastMessageAt = Date.now();
|
|
4255
|
+
connection.consecutiveAuthFailures = 0;
|
|
4256
|
+
// Schedule periodic token freshness checks
|
|
4257
|
+
scheduleTokenRefresh(connection, isServer);
|
|
4044
4258
|
// Re-subscribe to all existing subscriptions after reconnect
|
|
4045
4259
|
for (const sub of connection.subscriptions.values()) {
|
|
4260
|
+
sub.lastData = undefined;
|
|
4046
4261
|
sendSubscribe(connection, sub);
|
|
4047
4262
|
}
|
|
4263
|
+
// Start keepalive detection — if no messages for 90s, force reconnect
|
|
4264
|
+
if (connection.keepaliveTimer) {
|
|
4265
|
+
clearInterval(connection.keepaliveTimer);
|
|
4266
|
+
}
|
|
4267
|
+
connection.keepaliveTimer = setInterval(() => {
|
|
4268
|
+
var _a;
|
|
4269
|
+
if (Date.now() - connection.lastMessageAt > 90000) {
|
|
4270
|
+
console.warn('[WS v2] No messages received for 90s, forcing reconnect');
|
|
4271
|
+
if (connection.keepaliveTimer) {
|
|
4272
|
+
clearInterval(connection.keepaliveTimer);
|
|
4273
|
+
connection.keepaliveTimer = null;
|
|
4274
|
+
}
|
|
4275
|
+
(_a = connection.ws) === null || _a === void 0 ? void 0 : _a.reconnect();
|
|
4276
|
+
}
|
|
4277
|
+
}, 30000);
|
|
4048
4278
|
});
|
|
4049
4279
|
// Handle incoming messages
|
|
4050
4280
|
ws.addEventListener('message', (event) => {
|
|
4281
|
+
connection.lastMessageAt = Date.now();
|
|
4051
4282
|
try {
|
|
4052
4283
|
const message = JSON.parse(event.data);
|
|
4053
4284
|
handleServerMessage(connection, message);
|
|
@@ -4059,20 +4290,22 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4059
4290
|
// Handle errors
|
|
4060
4291
|
ws.addEventListener('error', (event) => {
|
|
4061
4292
|
console.error('[WS v2] WebSocket error:', event);
|
|
4062
|
-
|
|
4063
|
-
for (const [id, pending] of connection.pendingSubscriptions) {
|
|
4293
|
+
for (const [, pending] of connection.pendingSubscriptions) {
|
|
4064
4294
|
pending.reject(new Error('WebSocket error'));
|
|
4065
|
-
connection.pendingSubscriptions.delete(id);
|
|
4066
4295
|
}
|
|
4296
|
+
connection.pendingSubscriptions.clear();
|
|
4067
4297
|
});
|
|
4068
4298
|
// Handle close
|
|
4069
4299
|
ws.addEventListener('close', () => {
|
|
4070
4300
|
connection.isConnected = false;
|
|
4071
|
-
// Clear token refresh timer
|
|
4072
4301
|
if (connection.tokenRefreshTimer) {
|
|
4073
|
-
|
|
4302
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4074
4303
|
connection.tokenRefreshTimer = null;
|
|
4075
4304
|
}
|
|
4305
|
+
if (connection.keepaliveTimer) {
|
|
4306
|
+
clearInterval(connection.keepaliveTimer);
|
|
4307
|
+
connection.keepaliveTimer = null;
|
|
4308
|
+
}
|
|
4076
4309
|
});
|
|
4077
4310
|
return connection;
|
|
4078
4311
|
}
|
|
@@ -4272,9 +4505,7 @@ async function subscribeV2(path, subscriptionOptions) {
|
|
|
4272
4505
|
await subscriptionPromise;
|
|
4273
4506
|
}
|
|
4274
4507
|
catch (error) {
|
|
4275
|
-
|
|
4276
|
-
connection.subscriptions.delete(subscriptionId);
|
|
4277
|
-
throw error;
|
|
4508
|
+
console.warn('[WS v2] Subscription confirmation failed, keeping for reconnect recovery:', error);
|
|
4278
4509
|
}
|
|
4279
4510
|
}
|
|
4280
4511
|
// Return unsubscribe function
|
|
@@ -4314,7 +4545,7 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
|
|
|
4314
4545
|
if (connection.subscriptions.size === 0 && connection.ws) {
|
|
4315
4546
|
// Clear token refresh timer
|
|
4316
4547
|
if (connection.tokenRefreshTimer) {
|
|
4317
|
-
|
|
4548
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4318
4549
|
connection.tokenRefreshTimer = null;
|
|
4319
4550
|
}
|
|
4320
4551
|
connection.ws.close();
|
|
@@ -4330,7 +4561,7 @@ async function closeAllSubscriptionsV2() {
|
|
|
4330
4561
|
for (const [appId, connection] of connections) {
|
|
4331
4562
|
// Clear token refresh timer
|
|
4332
4563
|
if (connection.tokenRefreshTimer) {
|
|
4333
|
-
|
|
4564
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4334
4565
|
connection.tokenRefreshTimer = null;
|
|
4335
4566
|
}
|
|
4336
4567
|
if (connection.ws) {
|
|
@@ -4390,8 +4621,6 @@ async function reconnectWithNewAuthV2() {
|
|
|
4390
4621
|
if (!connection.ws) {
|
|
4391
4622
|
continue;
|
|
4392
4623
|
}
|
|
4393
|
-
// Reject any pending subscriptions - they'll need to be retried after reconnect
|
|
4394
|
-
// This prevents hanging promises during auth transitions
|
|
4395
4624
|
for (const [, pending] of connection.pendingSubscriptions) {
|
|
4396
4625
|
pending.reject(new Error('Connection reconnecting due to auth change'));
|
|
4397
4626
|
}
|
|
@@ -4401,6 +4630,8 @@ async function reconnectWithNewAuthV2() {
|
|
|
4401
4630
|
pending.resolve();
|
|
4402
4631
|
}
|
|
4403
4632
|
connection.pendingUnsubscriptions.clear();
|
|
4633
|
+
// Reset auth failure counter — this is a proactive reconnect (login, token refresh)
|
|
4634
|
+
connection.consecutiveAuthFailures = 0;
|
|
4404
4635
|
// Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
|
|
4405
4636
|
// We use reconnect() which will close and re-open with fresh URL (including new token)
|
|
4406
4637
|
try {
|
|
@@ -4483,5 +4714,5 @@ async function reconnectWithNewAuth() {
|
|
|
4483
4714
|
return reconnectWithNewAuthV2();
|
|
4484
4715
|
}
|
|
4485
4716
|
|
|
4486
|
-
export { ServerSessionManager, WebSessionManager, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, createSessionWithPrivy, createSessionWithSignature, genAuthNonce, genSolanaMessage, get, getCachedData, getConfig, getFiles, getIdToken, init, reconnectWithNewAuth, refreshSession, runExpression, runExpressionMany, runQuery, runQueryMany, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe };
|
|
4717
|
+
export { ServerSessionManager, WebSessionManager, aggregate, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithPrivy, createSessionWithSignature, genAuthNonce, genSolanaMessage, get, getCachedData, getConfig, getFiles, getIdToken, init, reconnectWithNewAuth, refreshSession, runExpression, runExpressionMany, runQuery, runQueryMany, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe };
|
|
4487
4718
|
//# sourceMappingURL=index.mjs.map
|