@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
|
@@ -31,6 +31,79 @@ export type RunQueryOptions = {
|
|
|
31
31
|
headers?: Record<string, string>;
|
|
32
32
|
};
|
|
33
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Supported aggregate operations for count/aggregate queries.
|
|
36
|
+
*/
|
|
37
|
+
export type AggregateOperation = 'count' | 'uniqueCount' | 'sum' | 'avg' | 'min' | 'max';
|
|
38
|
+
/**
|
|
39
|
+
* Result of a count or aggregate query — always a single numeric value.
|
|
40
|
+
*/
|
|
41
|
+
export type AggregateResult = {
|
|
42
|
+
value: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Options for the count function.
|
|
46
|
+
*/
|
|
47
|
+
export type CountOptions = {
|
|
48
|
+
/** Natural language filter prompt (e.g., "posts created in the last 7 days") */
|
|
49
|
+
prompt?: string;
|
|
50
|
+
/** Internal overrides for headers */
|
|
51
|
+
_overrides?: {
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Options for the aggregate function.
|
|
57
|
+
*/
|
|
58
|
+
export type AggregateOptions = {
|
|
59
|
+
/** Natural language filter prompt */
|
|
60
|
+
prompt?: string;
|
|
61
|
+
/** Field name to aggregate on (required for sum, avg, min, max) */
|
|
62
|
+
field?: string;
|
|
63
|
+
/** Internal overrides for headers */
|
|
64
|
+
_overrides?: {
|
|
65
|
+
headers?: Record<string, string>;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Count items in a collection path. Returns a numeric result.
|
|
70
|
+
*
|
|
71
|
+
* This uses the AI query engine with a count-specific prompt prefix,
|
|
72
|
+
* so TaroBase will generate a $count aggregation pipeline and return
|
|
73
|
+
* just the count rather than full documents.
|
|
74
|
+
*
|
|
75
|
+
* IMPORTANT: This only works for collections where the read policy is "true".
|
|
76
|
+
* If the read policy requires per-document checks, the server will return
|
|
77
|
+
* an error because aggregate counts cannot be performed without pulling all
|
|
78
|
+
* documents for access control evaluation.
|
|
79
|
+
*
|
|
80
|
+
* @param path - Collection path (e.g., "posts", "users/abc/comments")
|
|
81
|
+
* @param opts - Optional filter prompt and overrides
|
|
82
|
+
* @returns AggregateResult with the count value
|
|
83
|
+
*/
|
|
84
|
+
export declare function count(path: string, opts?: CountOptions): Promise<AggregateResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Run an aggregate operation on a collection path. Returns a numeric result.
|
|
87
|
+
*
|
|
88
|
+
* Supported operations:
|
|
89
|
+
* - count: Total number of documents
|
|
90
|
+
* - uniqueCount: Number of distinct values for a field
|
|
91
|
+
* - sum: Sum of a numeric field
|
|
92
|
+
* - avg: Average of a numeric field
|
|
93
|
+
* - min: Minimum value of a numeric field
|
|
94
|
+
* - max: Maximum value of a numeric field
|
|
95
|
+
*
|
|
96
|
+
* IMPORTANT: This only works for collections where the read policy is "true".
|
|
97
|
+
* If the read policy requires per-document checks, the server will return
|
|
98
|
+
* an error because aggregate operations cannot be performed without pulling
|
|
99
|
+
* all documents for access control evaluation.
|
|
100
|
+
*
|
|
101
|
+
* @param path - Collection path (e.g., "posts", "users/abc/comments")
|
|
102
|
+
* @param operation - The aggregate operation to perform
|
|
103
|
+
* @param opts - Options including optional filter prompt and field name
|
|
104
|
+
* @returns AggregateResult with the computed numeric value
|
|
105
|
+
*/
|
|
106
|
+
export declare function aggregate(path: string, operation: AggregateOperation, opts?: AggregateOptions): Promise<AggregateResult>;
|
|
34
107
|
export declare function get(path: string, opts?: GetOptions): Promise<any>;
|
|
35
108
|
export type RunExpressionOptions = {
|
|
36
109
|
returnType?: 'Bool' | 'String' | 'Int' | 'UInt';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { init } from './client/config';
|
|
2
2
|
export { getConfig, ClientConfig } from './client/config';
|
|
3
|
-
export { get, set, setMany, setFile, getFiles, runQuery, runQueryMany, runExpression, runExpressionMany, signMessage, signTransaction, signAndSubmitTransaction, SetOptions, GetOptions, RunExpressionOptions, RunExpressionResult } from './client/operations';
|
|
3
|
+
export { get, set, setMany, setFile, getFiles, runQuery, runQueryMany, runExpression, runExpressionMany, signMessage, signTransaction, signAndSubmitTransaction, count, aggregate, SetOptions, GetOptions, CountOptions, AggregateOptions, AggregateOperation, AggregateResult, RunExpressionOptions, RunExpressionResult } from './client/operations';
|
|
4
4
|
export { subscribe, closeAllSubscriptions, clearCache, getCachedData, reconnectWithNewAuth } from './client/subscription';
|
|
5
5
|
export * from './types';
|
|
6
6
|
export { getIdToken } from './utils/utils';
|
package/dist/index.js
CHANGED
|
@@ -139,10 +139,14 @@ class WebSessionManager {
|
|
|
139
139
|
const newObj = JSON.parse(newSession);
|
|
140
140
|
return { address: newObj.address, session: newObj };
|
|
141
141
|
}
|
|
142
|
+
// Refresh failed — clear stale session to prevent retry loops
|
|
143
|
+
this.clearSession();
|
|
142
144
|
return null;
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
catch (err) {
|
|
148
|
+
// Token decode or refresh failed — clear stale session to prevent retry loops
|
|
149
|
+
this.clearSession();
|
|
146
150
|
return null;
|
|
147
151
|
}
|
|
148
152
|
return { address: sessionObj.address, session: sessionObj };
|
|
@@ -3066,15 +3070,30 @@ class ServerSessionManager {
|
|
|
3066
3070
|
constructor() {
|
|
3067
3071
|
/* Private cache (lives for the life of the process) */
|
|
3068
3072
|
this.session = null;
|
|
3073
|
+
/* Coalesce concurrent getSession() calls into a single in-flight request */
|
|
3074
|
+
this.pendingSession = null;
|
|
3069
3075
|
}
|
|
3070
3076
|
/* ---------------------------------------------- *
|
|
3071
3077
|
* GET (lazy-fetch)
|
|
3072
3078
|
* ---------------------------------------------- */
|
|
3073
3079
|
async getSession() {
|
|
3074
|
-
if (this.session
|
|
3075
|
-
this.session
|
|
3080
|
+
if (this.session !== null) {
|
|
3081
|
+
return this.session;
|
|
3082
|
+
}
|
|
3083
|
+
// If a session creation is already in-flight, reuse that promise
|
|
3084
|
+
// instead of firing a second concurrent request.
|
|
3085
|
+
if (this.pendingSession !== null) {
|
|
3086
|
+
return this.pendingSession;
|
|
3076
3087
|
}
|
|
3077
|
-
|
|
3088
|
+
this.pendingSession = createSession()
|
|
3089
|
+
.then((session) => {
|
|
3090
|
+
this.session = session;
|
|
3091
|
+
return session;
|
|
3092
|
+
})
|
|
3093
|
+
.finally(() => {
|
|
3094
|
+
this.pendingSession = null;
|
|
3095
|
+
});
|
|
3096
|
+
return this.pendingSession;
|
|
3078
3097
|
}
|
|
3079
3098
|
/* ---------------------------------------------- *
|
|
3080
3099
|
* STORE (overwrites the cached value)
|
|
@@ -3087,6 +3106,7 @@ class ServerSessionManager {
|
|
|
3087
3106
|
* ---------------------------------------------- */
|
|
3088
3107
|
clearSession() {
|
|
3089
3108
|
this.session = null;
|
|
3109
|
+
this.pendingSession = null;
|
|
3090
3110
|
}
|
|
3091
3111
|
/* ---------------------------------------------- *
|
|
3092
3112
|
* QUICK helpers
|
|
@@ -3389,6 +3409,150 @@ function hashForKey$1(value) {
|
|
|
3389
3409
|
}
|
|
3390
3410
|
return h.toString(36);
|
|
3391
3411
|
}
|
|
3412
|
+
/**
|
|
3413
|
+
* Validates that a field name is a safe identifier (alphanumeric, underscores, dots for nested paths).
|
|
3414
|
+
* Prevents prompt injection via crafted field names.
|
|
3415
|
+
*/
|
|
3416
|
+
function validateFieldName(field) {
|
|
3417
|
+
if (!/^[a-zA-Z0-9_.]+$/.test(field)) {
|
|
3418
|
+
throw new Error(`Invalid field name "${field}". Field names must only contain letters, numbers, underscores, and dots.`);
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Parses a raw aggregation result (e.g. [{ count: 42 }] or [{ _id: null, total: 100 }])
|
|
3423
|
+
* into a single numeric value.
|
|
3424
|
+
*/
|
|
3425
|
+
function parseAggregateValue(result) {
|
|
3426
|
+
if (typeof result === 'number')
|
|
3427
|
+
return result;
|
|
3428
|
+
if (Array.isArray(result)) {
|
|
3429
|
+
if (result.length === 0)
|
|
3430
|
+
return 0;
|
|
3431
|
+
// Multiple elements — not a collapsed aggregate result
|
|
3432
|
+
if (result.length > 1) {
|
|
3433
|
+
throw new Error(`Unexpected aggregate result: got array with ${result.length} elements. The AI may have returned full documents instead of an aggregation.`);
|
|
3434
|
+
}
|
|
3435
|
+
const first = result[0];
|
|
3436
|
+
if (typeof first === 'number')
|
|
3437
|
+
return first;
|
|
3438
|
+
if (first && typeof first === 'object') {
|
|
3439
|
+
// $count stage returns { count: N }
|
|
3440
|
+
if (typeof first.count === 'number')
|
|
3441
|
+
return first.count;
|
|
3442
|
+
// $group stage returns { _id: null, result: N } — expect _id + one numeric field
|
|
3443
|
+
const numericEntries = Object.entries(first).filter(([key, val]) => key !== '_id' && typeof val === 'number');
|
|
3444
|
+
if (numericEntries.length === 1)
|
|
3445
|
+
return numericEntries[0][1];
|
|
3446
|
+
}
|
|
3447
|
+
// Avoid leaking document contents into error messages
|
|
3448
|
+
const shape = first && typeof first === 'object' ? `{${Object.keys(first).join(', ')}}` : String(first);
|
|
3449
|
+
throw new Error(`Unexpected aggregate result shape: ${shape}. Expected {count: N} or {_id: null, <field>: N}.`);
|
|
3450
|
+
}
|
|
3451
|
+
if (result && typeof result === 'object' && typeof result.count === 'number') {
|
|
3452
|
+
return result.count;
|
|
3453
|
+
}
|
|
3454
|
+
throw new Error(`Unexpected aggregate result type: ${typeof result}. Expected a number, array, or object with a count field.`);
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Count items in a collection path. Returns a numeric result.
|
|
3458
|
+
*
|
|
3459
|
+
* This uses the AI query engine with a count-specific prompt prefix,
|
|
3460
|
+
* so TaroBase will generate a $count aggregation pipeline and return
|
|
3461
|
+
* just the count rather than full documents.
|
|
3462
|
+
*
|
|
3463
|
+
* IMPORTANT: This only works for collections where the read policy is "true".
|
|
3464
|
+
* If the read policy requires per-document checks, the server will return
|
|
3465
|
+
* an error because aggregate counts cannot be performed without pulling all
|
|
3466
|
+
* documents for access control evaluation.
|
|
3467
|
+
*
|
|
3468
|
+
* @param path - Collection path (e.g., "posts", "users/abc/comments")
|
|
3469
|
+
* @param opts - Optional filter prompt and overrides
|
|
3470
|
+
* @returns AggregateResult with the count value
|
|
3471
|
+
*/
|
|
3472
|
+
async function count(path, opts = {}) {
|
|
3473
|
+
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.';
|
|
3474
|
+
const fullPrompt = opts.prompt
|
|
3475
|
+
? `${prefix} Filter: ${opts.prompt}`
|
|
3476
|
+
: prefix;
|
|
3477
|
+
const result = await get(path, { prompt: fullPrompt, bypassCache: true, _overrides: opts._overrides });
|
|
3478
|
+
return { value: parseAggregateValue(result) };
|
|
3479
|
+
}
|
|
3480
|
+
/**
|
|
3481
|
+
* Run an aggregate operation on a collection path. Returns a numeric result.
|
|
3482
|
+
*
|
|
3483
|
+
* Supported operations:
|
|
3484
|
+
* - count: Total number of documents
|
|
3485
|
+
* - uniqueCount: Number of distinct values for a field
|
|
3486
|
+
* - sum: Sum of a numeric field
|
|
3487
|
+
* - avg: Average of a numeric field
|
|
3488
|
+
* - min: Minimum value of a numeric field
|
|
3489
|
+
* - max: Maximum value of a numeric field
|
|
3490
|
+
*
|
|
3491
|
+
* IMPORTANT: This only works for collections where the read policy is "true".
|
|
3492
|
+
* If the read policy requires per-document checks, the server will return
|
|
3493
|
+
* an error because aggregate operations cannot be performed without pulling
|
|
3494
|
+
* all documents for access control evaluation.
|
|
3495
|
+
*
|
|
3496
|
+
* @param path - Collection path (e.g., "posts", "users/abc/comments")
|
|
3497
|
+
* @param operation - The aggregate operation to perform
|
|
3498
|
+
* @param opts - Options including optional filter prompt and field name
|
|
3499
|
+
* @returns AggregateResult with the computed numeric value
|
|
3500
|
+
*/
|
|
3501
|
+
async function aggregate(path, operation, opts = {}) {
|
|
3502
|
+
let prefix;
|
|
3503
|
+
switch (operation) {
|
|
3504
|
+
case 'count':
|
|
3505
|
+
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.';
|
|
3506
|
+
break;
|
|
3507
|
+
case 'uniqueCount':
|
|
3508
|
+
if (!opts.field)
|
|
3509
|
+
throw new Error('aggregate "uniqueCount" requires a field option');
|
|
3510
|
+
validateFieldName(opts.field);
|
|
3511
|
+
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.`;
|
|
3512
|
+
break;
|
|
3513
|
+
case 'sum':
|
|
3514
|
+
if (!opts.field)
|
|
3515
|
+
throw new Error('aggregate "sum" requires a field option');
|
|
3516
|
+
validateFieldName(opts.field);
|
|
3517
|
+
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.`;
|
|
3518
|
+
break;
|
|
3519
|
+
case 'avg':
|
|
3520
|
+
if (!opts.field)
|
|
3521
|
+
throw new Error('aggregate "avg" requires a field option');
|
|
3522
|
+
validateFieldName(opts.field);
|
|
3523
|
+
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.`;
|
|
3524
|
+
break;
|
|
3525
|
+
case 'min':
|
|
3526
|
+
if (!opts.field)
|
|
3527
|
+
throw new Error('aggregate "min" requires a field option');
|
|
3528
|
+
validateFieldName(opts.field);
|
|
3529
|
+
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.`;
|
|
3530
|
+
break;
|
|
3531
|
+
case 'max':
|
|
3532
|
+
if (!opts.field)
|
|
3533
|
+
throw new Error('aggregate "max" requires a field option');
|
|
3534
|
+
validateFieldName(opts.field);
|
|
3535
|
+
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.`;
|
|
3536
|
+
break;
|
|
3537
|
+
default:
|
|
3538
|
+
throw new Error(`Unsupported aggregate operation: ${operation}`);
|
|
3539
|
+
}
|
|
3540
|
+
const fullPrompt = opts.prompt
|
|
3541
|
+
? `${prefix} Filter: ${opts.prompt}`
|
|
3542
|
+
: prefix;
|
|
3543
|
+
const result = await get(path, { prompt: fullPrompt, bypassCache: true, _overrides: opts._overrides });
|
|
3544
|
+
// For uniqueCount, the AI may return $group results without the final $count
|
|
3545
|
+
// stage, producing [{_id: "val1"}, {_id: "val2"}, ...]. Verify elements look
|
|
3546
|
+
// like $group output (only _id key) before using array length as the count.
|
|
3547
|
+
if (operation === 'uniqueCount' && Array.isArray(result) && result.length > 1) {
|
|
3548
|
+
const looksLikeGroupOutput = result.every((el) => el && typeof el === 'object' && Object.keys(el).length === 1 && '_id' in el);
|
|
3549
|
+
if (looksLikeGroupOutput) {
|
|
3550
|
+
return { value: result.length };
|
|
3551
|
+
}
|
|
3552
|
+
throw new Error(`Unexpected uniqueCount result: got ${result.length} elements that don't match $group output shape.`);
|
|
3553
|
+
}
|
|
3554
|
+
return { value: parseAggregateValue(result) };
|
|
3555
|
+
}
|
|
3392
3556
|
async function get(path, opts = {}) {
|
|
3393
3557
|
try {
|
|
3394
3558
|
let normalizedPath = path.startsWith("/") ? path.slice(1) : path;
|
|
@@ -3849,13 +4013,15 @@ async function syncItems(paths, options) {
|
|
|
3849
4013
|
const connections = new Map();
|
|
3850
4014
|
const responseCache = new Map();
|
|
3851
4015
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
3852
|
-
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
|
|
3853
4018
|
// ============ WebSocket Config ============
|
|
3854
4019
|
const BASE_MIN_RECONNECT_DELAY_MS = 1000;
|
|
3855
4020
|
const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
|
|
3856
4021
|
const MAX_RECONNECT_DELAY_MS = 300000;
|
|
3857
4022
|
const RECONNECT_DELAY_GROW_FACTOR = 1.8;
|
|
3858
|
-
const MIN_BROWSER_RECONNECT_INTERVAL_MS =
|
|
4023
|
+
const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
|
|
4024
|
+
const MAX_AUTH_REFRESH_RETRIES = 5;
|
|
3859
4025
|
const WS_CONFIG = {
|
|
3860
4026
|
// Keep retrying indefinitely so long outages recover without page refresh.
|
|
3861
4027
|
maxRetries: Infinity,
|
|
@@ -3910,29 +4076,53 @@ function getTokenExpirationTime(token) {
|
|
|
3910
4076
|
return null;
|
|
3911
4077
|
}
|
|
3912
4078
|
}
|
|
3913
|
-
function scheduleTokenRefresh(connection,
|
|
4079
|
+
function scheduleTokenRefresh(connection, isServer) {
|
|
3914
4080
|
// Clear any existing timer
|
|
3915
4081
|
if (connection.tokenRefreshTimer) {
|
|
3916
|
-
|
|
4082
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
3917
4083
|
connection.tokenRefreshTimer = null;
|
|
3918
4084
|
}
|
|
3919
|
-
if
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
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);
|
|
3936
4126
|
}
|
|
3937
4127
|
async function getFreshAuthToken(isServer) {
|
|
3938
4128
|
const currentToken = await getIdToken(isServer);
|
|
@@ -3942,10 +4132,12 @@ async function getFreshAuthToken(isServer) {
|
|
|
3942
4132
|
if (!isTokenExpired(currentToken)) {
|
|
3943
4133
|
return currentToken;
|
|
3944
4134
|
}
|
|
4135
|
+
// Token is expired — attempt refresh
|
|
3945
4136
|
try {
|
|
3946
4137
|
const refreshToken = await getRefreshToken(isServer);
|
|
3947
4138
|
if (!refreshToken) {
|
|
3948
|
-
|
|
4139
|
+
console.warn('[WS v2] Token expired but no refresh token available');
|
|
4140
|
+
return null;
|
|
3949
4141
|
}
|
|
3950
4142
|
const refreshData = await refreshSession(refreshToken);
|
|
3951
4143
|
if (refreshData && refreshData.idToken && refreshData.accessToken) {
|
|
@@ -3956,7 +4148,11 @@ async function getFreshAuthToken(isServer) {
|
|
|
3956
4148
|
catch (error) {
|
|
3957
4149
|
console.error('[WS v2] Error refreshing token:', error);
|
|
3958
4150
|
}
|
|
3959
|
-
|
|
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;
|
|
3960
4156
|
}
|
|
3961
4157
|
function hasDisconnectedActiveConnections() {
|
|
3962
4158
|
for (const connection of connections.values()) {
|
|
@@ -4020,6 +4216,9 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4020
4216
|
isConnected: false,
|
|
4021
4217
|
appId,
|
|
4022
4218
|
tokenRefreshTimer: null,
|
|
4219
|
+
lastMessageAt: Date.now(),
|
|
4220
|
+
keepaliveTimer: null,
|
|
4221
|
+
consecutiveAuthFailures: 0,
|
|
4023
4222
|
};
|
|
4024
4223
|
connections.set(appId, connection);
|
|
4025
4224
|
// URL provider for reconnection with fresh tokens
|
|
@@ -4045,6 +4244,22 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4045
4244
|
const authToken = await getFreshAuthToken(isServer);
|
|
4046
4245
|
if (authToken) {
|
|
4047
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
|
|
4048
4263
|
}
|
|
4049
4264
|
return wsUrl.toString();
|
|
4050
4265
|
};
|
|
@@ -4056,18 +4271,34 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4056
4271
|
ws.addEventListener('open', () => {
|
|
4057
4272
|
connection.isConnecting = false;
|
|
4058
4273
|
connection.isConnected = true;
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
})();
|
|
4274
|
+
connection.lastMessageAt = Date.now();
|
|
4275
|
+
connection.consecutiveAuthFailures = 0;
|
|
4276
|
+
// Schedule periodic token freshness checks
|
|
4277
|
+
scheduleTokenRefresh(connection, isServer);
|
|
4064
4278
|
// Re-subscribe to all existing subscriptions after reconnect
|
|
4065
4279
|
for (const sub of connection.subscriptions.values()) {
|
|
4280
|
+
sub.lastData = undefined;
|
|
4066
4281
|
sendSubscribe(connection, sub);
|
|
4067
4282
|
}
|
|
4283
|
+
// Start keepalive detection — if no messages for 90s, force reconnect
|
|
4284
|
+
if (connection.keepaliveTimer) {
|
|
4285
|
+
clearInterval(connection.keepaliveTimer);
|
|
4286
|
+
}
|
|
4287
|
+
connection.keepaliveTimer = setInterval(() => {
|
|
4288
|
+
var _a;
|
|
4289
|
+
if (Date.now() - connection.lastMessageAt > 90000) {
|
|
4290
|
+
console.warn('[WS v2] No messages received for 90s, forcing reconnect');
|
|
4291
|
+
if (connection.keepaliveTimer) {
|
|
4292
|
+
clearInterval(connection.keepaliveTimer);
|
|
4293
|
+
connection.keepaliveTimer = null;
|
|
4294
|
+
}
|
|
4295
|
+
(_a = connection.ws) === null || _a === void 0 ? void 0 : _a.reconnect();
|
|
4296
|
+
}
|
|
4297
|
+
}, 30000);
|
|
4068
4298
|
});
|
|
4069
4299
|
// Handle incoming messages
|
|
4070
4300
|
ws.addEventListener('message', (event) => {
|
|
4301
|
+
connection.lastMessageAt = Date.now();
|
|
4071
4302
|
try {
|
|
4072
4303
|
const message = JSON.parse(event.data);
|
|
4073
4304
|
handleServerMessage(connection, message);
|
|
@@ -4079,20 +4310,22 @@ async function getOrCreateConnection(appId, isServer) {
|
|
|
4079
4310
|
// Handle errors
|
|
4080
4311
|
ws.addEventListener('error', (event) => {
|
|
4081
4312
|
console.error('[WS v2] WebSocket error:', event);
|
|
4082
|
-
|
|
4083
|
-
for (const [id, pending] of connection.pendingSubscriptions) {
|
|
4313
|
+
for (const [, pending] of connection.pendingSubscriptions) {
|
|
4084
4314
|
pending.reject(new Error('WebSocket error'));
|
|
4085
|
-
connection.pendingSubscriptions.delete(id);
|
|
4086
4315
|
}
|
|
4316
|
+
connection.pendingSubscriptions.clear();
|
|
4087
4317
|
});
|
|
4088
4318
|
// Handle close
|
|
4089
4319
|
ws.addEventListener('close', () => {
|
|
4090
4320
|
connection.isConnected = false;
|
|
4091
|
-
// Clear token refresh timer
|
|
4092
4321
|
if (connection.tokenRefreshTimer) {
|
|
4093
|
-
|
|
4322
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4094
4323
|
connection.tokenRefreshTimer = null;
|
|
4095
4324
|
}
|
|
4325
|
+
if (connection.keepaliveTimer) {
|
|
4326
|
+
clearInterval(connection.keepaliveTimer);
|
|
4327
|
+
connection.keepaliveTimer = null;
|
|
4328
|
+
}
|
|
4096
4329
|
});
|
|
4097
4330
|
return connection;
|
|
4098
4331
|
}
|
|
@@ -4292,9 +4525,7 @@ async function subscribeV2(path, subscriptionOptions) {
|
|
|
4292
4525
|
await subscriptionPromise;
|
|
4293
4526
|
}
|
|
4294
4527
|
catch (error) {
|
|
4295
|
-
|
|
4296
|
-
connection.subscriptions.delete(subscriptionId);
|
|
4297
|
-
throw error;
|
|
4528
|
+
console.warn('[WS v2] Subscription confirmation failed, keeping for reconnect recovery:', error);
|
|
4298
4529
|
}
|
|
4299
4530
|
}
|
|
4300
4531
|
// Return unsubscribe function
|
|
@@ -4334,7 +4565,7 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
|
|
|
4334
4565
|
if (connection.subscriptions.size === 0 && connection.ws) {
|
|
4335
4566
|
// Clear token refresh timer
|
|
4336
4567
|
if (connection.tokenRefreshTimer) {
|
|
4337
|
-
|
|
4568
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4338
4569
|
connection.tokenRefreshTimer = null;
|
|
4339
4570
|
}
|
|
4340
4571
|
connection.ws.close();
|
|
@@ -4350,7 +4581,7 @@ async function closeAllSubscriptionsV2() {
|
|
|
4350
4581
|
for (const [appId, connection] of connections) {
|
|
4351
4582
|
// Clear token refresh timer
|
|
4352
4583
|
if (connection.tokenRefreshTimer) {
|
|
4353
|
-
|
|
4584
|
+
clearInterval(connection.tokenRefreshTimer);
|
|
4354
4585
|
connection.tokenRefreshTimer = null;
|
|
4355
4586
|
}
|
|
4356
4587
|
if (connection.ws) {
|
|
@@ -4410,8 +4641,6 @@ async function reconnectWithNewAuthV2() {
|
|
|
4410
4641
|
if (!connection.ws) {
|
|
4411
4642
|
continue;
|
|
4412
4643
|
}
|
|
4413
|
-
// Reject any pending subscriptions - they'll need to be retried after reconnect
|
|
4414
|
-
// This prevents hanging promises during auth transitions
|
|
4415
4644
|
for (const [, pending] of connection.pendingSubscriptions) {
|
|
4416
4645
|
pending.reject(new Error('Connection reconnecting due to auth change'));
|
|
4417
4646
|
}
|
|
@@ -4421,6 +4650,8 @@ async function reconnectWithNewAuthV2() {
|
|
|
4421
4650
|
pending.resolve();
|
|
4422
4651
|
}
|
|
4423
4652
|
connection.pendingUnsubscriptions.clear();
|
|
4653
|
+
// Reset auth failure counter — this is a proactive reconnect (login, token refresh)
|
|
4654
|
+
connection.consecutiveAuthFailures = 0;
|
|
4424
4655
|
// Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
|
|
4425
4656
|
// We use reconnect() which will close and re-open with fresh URL (including new token)
|
|
4426
4657
|
try {
|
|
@@ -4505,10 +4736,12 @@ async function reconnectWithNewAuth() {
|
|
|
4505
4736
|
|
|
4506
4737
|
exports.ServerSessionManager = ServerSessionManager;
|
|
4507
4738
|
exports.WebSessionManager = WebSessionManager;
|
|
4739
|
+
exports.aggregate = aggregate;
|
|
4508
4740
|
exports.buildSetDocumentsTransaction = buildSetDocumentsTransaction;
|
|
4509
4741
|
exports.clearCache = clearCache;
|
|
4510
4742
|
exports.closeAllSubscriptions = closeAllSubscriptions;
|
|
4511
4743
|
exports.convertRemainingAccounts = convertRemainingAccounts;
|
|
4744
|
+
exports.count = count;
|
|
4512
4745
|
exports.createSessionWithPrivy = createSessionWithPrivy;
|
|
4513
4746
|
exports.createSessionWithSignature = createSessionWithSignature;
|
|
4514
4747
|
exports.genAuthNonce = genAuthNonce;
|