@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.
@@ -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 === null) {
3075
- this.session = await createSession();
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
- return this.session;
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 = 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
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 = 30000;
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, token) {
4079
+ function scheduleTokenRefresh(connection, isServer) {
3914
4080
  // Clear any existing timer
3915
4081
  if (connection.tokenRefreshTimer) {
3916
- clearTimeout(connection.tokenRefreshTimer);
4082
+ clearInterval(connection.tokenRefreshTimer);
3917
4083
  connection.tokenRefreshTimer = null;
3918
4084
  }
3919
- if (!token) {
3920
- return; // No token = unauthenticated, no refresh needed
3921
- }
3922
- const expirationTime = getTokenExpirationTime(token);
3923
- if (!expirationTime) {
3924
- return; // Can't parse expiration
3925
- }
3926
- const refreshTime = expirationTime - TOKEN_REFRESH_BUFFER;
3927
- const delay = refreshTime - Date.now();
3928
- if (delay <= 0) {
3929
- // Token already expired or about to expire, reconnect immediately
3930
- reconnectWithNewAuthV2();
3931
- return;
3932
- }
3933
- connection.tokenRefreshTimer = setTimeout(() => {
3934
- reconnectWithNewAuthV2();
3935
- }, 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);
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
- return currentToken;
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
- 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;
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
- // Schedule token refresh before expiry
4060
- (async () => {
4061
- const token = await getIdToken(isServer);
4062
- scheduleTokenRefresh(connection, token);
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
- // Reject all pending subscriptions
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
- clearTimeout(connection.tokenRefreshTimer);
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
- // Remove subscription on error
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
- clearTimeout(connection.tokenRefreshTimer);
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
- clearTimeout(connection.tokenRefreshTimer);
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;