@pooflabs/core 0.0.31 → 0.0.33

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.
@@ -17,6 +17,10 @@ export type GetOptions = {
17
17
  includeSubPaths?: boolean;
18
18
  /** Shape object for relationship resolution - specifies which related documents to include */
19
19
  shape?: Record<string, any>;
20
+ /** Maximum number of items to return (opt-in pagination) */
21
+ limit?: number;
22
+ /** Opaque cursor for cursor-based pagination (used with limit) */
23
+ cursor?: string;
20
24
  /** Internal overrides for headers */
21
25
  _overrides?: {
22
26
  headers?: Record<string, string>;
@@ -27,6 +31,79 @@ export type RunQueryOptions = {
27
31
  headers?: Record<string, string>;
28
32
  };
29
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>;
30
107
  export declare function get(path: string, opts?: GetOptions): Promise<any>;
31
108
  export type RunExpressionOptions = {
32
109
  returnType?: 'Bool' | 'String' | 'Int' | 'UInt';
@@ -66,8 +143,16 @@ export declare function setMany(many: {
66
143
  export declare function clearCache(path?: string, opts?: {
67
144
  prompt?: string;
68
145
  }): void;
69
- export declare function getFiles(path: string): Promise<any>;
70
- export declare function setFile(path: string, file: File | null): Promise<boolean>;
146
+ export declare function getFiles(path: string, options?: {
147
+ _overrides?: {
148
+ headers?: Record<string, string>;
149
+ };
150
+ }): Promise<any>;
151
+ export declare function setFile(path: string, file: File | null, options?: {
152
+ _overrides?: {
153
+ headers?: Record<string, string>;
154
+ };
155
+ }): Promise<boolean>;
71
156
  export declare function signMessage(message: string): Promise<string>;
72
157
  export declare function signTransaction(transaction: Transaction | VersionedTransaction): Promise<Transaction | VersionedTransaction>;
73
158
  export declare function signAndSubmitTransaction(transaction: Transaction | VersionedTransaction, feePayer?: PublicKey): Promise<string>;
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
@@ -3240,7 +3260,7 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
3240
3260
  requestConfig.data = data ? JSON.stringify(data) : {};
3241
3261
  }
3242
3262
  const response = await axios(requestConfig);
3243
- return { data: response.data, status: response.status };
3263
+ return { data: response.data, status: response.status, headers: response.headers };
3244
3264
  }
3245
3265
  try {
3246
3266
  return await executeRequest();
@@ -3382,6 +3402,157 @@ const pendingRequests = {};
3382
3402
  const GET_CACHE_TTL = 500; // Adjust this value as needed (in milliseconds)
3383
3403
  // Last time we cleaned up the cache
3384
3404
  let lastCacheCleanup = Date.now();
3405
+ function hashForKey$1(value) {
3406
+ let h = 5381;
3407
+ for (let i = 0; i < value.length; i++) {
3408
+ h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
3409
+ }
3410
+ return h.toString(36);
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
+ }
3385
3556
  async function get(path, opts = {}) {
3386
3557
  try {
3387
3558
  let normalizedPath = path.startsWith("/") ? path.slice(1) : path;
@@ -3392,10 +3563,12 @@ async function get(path, opts = {}) {
3392
3563
  if (!normalizedPath || normalizedPath.length === 0) {
3393
3564
  return new Error("Invalid path provided.");
3394
3565
  }
3395
- // Create cache key combining path, prompt, includeSubPaths, and shape
3566
+ // Create cache key combining path, prompt, includeSubPaths, shape, limit, and cursor
3396
3567
  const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
3397
3568
  const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
3398
- const cacheKey = `${normalizedPath}:${opts.prompt || ''}${includeSubPathsKey}:${shapeKey}`;
3569
+ const limitKey = opts.limit !== undefined ? `:l${opts.limit}` : '';
3570
+ const cursorKey = opts.cursor ? `:c${hashForKey$1(opts.cursor)}` : '';
3571
+ const cacheKey = `${normalizedPath}:${opts.prompt || ''}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
3399
3572
  const now = Date.now();
3400
3573
  // Check for valid cache entry if not bypassing cache
3401
3574
  if (!opts.bypassCache && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
@@ -3419,6 +3592,8 @@ async function get(path, opts = {}) {
3419
3592
  // Build common query params
3420
3593
  const includeSubPathsParam = opts.includeSubPaths ? '&includeSubPaths=true' : '';
3421
3594
  const shapeParam = opts.shape ? `&shape=${encodeURIComponent(JSON.stringify(opts.shape))}` : '';
3595
+ const limitParam = opts.limit !== undefined ? `&limit=${opts.limit}` : '';
3596
+ const cursorParam = opts.cursor ? `&cursor=${encodeURIComponent(opts.cursor)}` : '';
3422
3597
  if (pathIsDocument) {
3423
3598
  const itemId = encodeURIComponent(normalizedPath);
3424
3599
  // For documents, query params go after the path
@@ -3429,13 +3604,14 @@ async function get(path, opts = {}) {
3429
3604
  else {
3430
3605
  const path = encodeURIComponent(normalizedPath);
3431
3606
  const promptQueryParam = (opts === null || opts === void 0 ? void 0 : opts.prompt) ? `&prompt=${btoa(opts.prompt)}` : "";
3432
- const apiPath = `items?path=${path}${promptQueryParam}${includeSubPathsParam}${shapeParam}`;
3607
+ const apiPath = `items?path=${path}${promptQueryParam}${includeSubPathsParam}${shapeParam}${limitParam}${cursorParam}`;
3433
3608
  response = await makeApiRequest('GET', apiPath, null, opts._overrides);
3434
3609
  }
3610
+ const responseData = response.data;
3435
3611
  // Cache the response (unless bypassing cache)
3436
3612
  if (!opts.bypassCache) {
3437
3613
  getCache[cacheKey] = {
3438
- data: response.data,
3614
+ data: responseData,
3439
3615
  expiresAt: now + GET_CACHE_TTL
3440
3616
  };
3441
3617
  // Periodically clean up expired cache entries (every 5 seconds)
@@ -3445,7 +3621,7 @@ async function get(path, opts = {}) {
3445
3621
  }
3446
3622
  }
3447
3623
  // Return the data from the response
3448
- return response.data;
3624
+ return responseData;
3449
3625
  }
3450
3626
  finally {
3451
3627
  // Remove this request from pendingRequests regardless of success/failure
@@ -3742,21 +3918,21 @@ function clearCacheByPrefix(prefix) {
3742
3918
  }
3743
3919
  });
3744
3920
  }
3745
- async function getFiles(path) {
3921
+ async function getFiles(path, options) {
3746
3922
  try {
3747
3923
  const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
3748
3924
  if (!normalizedPath || normalizedPath.length === 0) {
3749
3925
  return new Error("Invalid path provided.");
3750
3926
  }
3751
3927
  const apiPath = `storage?path=${normalizedPath}`;
3752
- const response = await makeApiRequest('GET', apiPath, null, undefined);
3928
+ const response = await makeApiRequest('GET', apiPath, null, options === null || options === void 0 ? void 0 : options._overrides);
3753
3929
  return response.data;
3754
3930
  }
3755
3931
  catch (error) {
3756
3932
  throw error;
3757
3933
  }
3758
3934
  }
3759
- async function setFile(path, file) {
3935
+ async function setFile(path, file, options) {
3760
3936
  var _a;
3761
3937
  // 1) Get the presigned URL from your backend
3762
3938
  const requestBody = {
@@ -3767,7 +3943,7 @@ async function setFile(path, file) {
3767
3943
  if (file) {
3768
3944
  requestBody.contentLength = file.size;
3769
3945
  }
3770
- const response = await makeApiRequest('POST', 'storage/url', requestBody, undefined);
3946
+ const response = await makeApiRequest('POST', 'storage/url', requestBody, options === null || options === void 0 ? void 0 : options._overrides);
3771
3947
  if (file == null) {
3772
3948
  return true;
3773
3949
  }
@@ -3843,7 +4019,7 @@ const BASE_MIN_RECONNECT_DELAY_MS = 1000;
3843
4019
  const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
3844
4020
  const MAX_RECONNECT_DELAY_MS = 300000;
3845
4021
  const RECONNECT_DELAY_GROW_FACTOR = 1.8;
3846
- const MIN_BROWSER_RECONNECT_INTERVAL_MS = 30000;
4022
+ const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
3847
4023
  const WS_CONFIG = {
3848
4024
  // Keep retrying indefinitely so long outages recover without page refresh.
3849
4025
  maxRetries: Infinity,
@@ -3863,10 +4039,19 @@ let lastBrowserTriggeredReconnectAt = 0;
3863
4039
  function generateSubscriptionId() {
3864
4040
  return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
3865
4041
  }
3866
- function getCacheKey(path, prompt, shape) {
4042
+ function hashForKey(value) {
4043
+ let h = 5381;
4044
+ for (let i = 0; i < value.length; i++) {
4045
+ h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
4046
+ }
4047
+ return h.toString(36);
4048
+ }
4049
+ function getCacheKey(path, prompt, shape, limit, cursor) {
3867
4050
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
3868
4051
  const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
3869
- return `${normalizedPath}:${prompt || 'default'}:${shapeKey}`;
4052
+ const limitKey = limit !== undefined ? `:l${limit}` : '';
4053
+ const cursorKey = cursor ? `:c${hashForKey(cursor)}` : '';
4054
+ return `${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}`;
3870
4055
  }
3871
4056
  function isTokenExpired(token) {
3872
4057
  try {
@@ -3999,6 +4184,8 @@ async function getOrCreateConnection(appId, isServer) {
3999
4184
  isConnected: false,
4000
4185
  appId,
4001
4186
  tokenRefreshTimer: null,
4187
+ lastMessageAt: Date.now(),
4188
+ keepaliveTimer: null,
4002
4189
  };
4003
4190
  connections.set(appId, connection);
4004
4191
  // URL provider for reconnection with fresh tokens
@@ -4007,8 +4194,12 @@ async function getOrCreateConnection(appId, isServer) {
4007
4194
  const wsUrl = new URL(config.wsApiUrl);
4008
4195
  // Always use v2 path
4009
4196
  wsUrl.pathname = WS_V2_PATH;
4010
- // Set appId
4011
- if (typeof window !== 'undefined' && window.CUSTOM_TAROBASE_APP_ID_HEADER) {
4197
+ // Set appId — prefer the explicit appId passed to getOrCreateConnection,
4198
+ // fall back to window global for legacy callers, then config default
4199
+ if (appId && appId !== config.appId) {
4200
+ wsUrl.searchParams.append('appId', appId);
4201
+ }
4202
+ else if (typeof window !== 'undefined' && window.CUSTOM_TAROBASE_APP_ID_HEADER) {
4012
4203
  wsUrl.searchParams.append('appId', window.CUSTOM_TAROBASE_APP_ID_HEADER);
4013
4204
  }
4014
4205
  else {
@@ -4031,6 +4222,7 @@ async function getOrCreateConnection(appId, isServer) {
4031
4222
  ws.addEventListener('open', () => {
4032
4223
  connection.isConnecting = false;
4033
4224
  connection.isConnected = true;
4225
+ connection.lastMessageAt = Date.now();
4034
4226
  // Schedule token refresh before expiry
4035
4227
  (async () => {
4036
4228
  const token = await getIdToken(isServer);
@@ -4038,11 +4230,28 @@ async function getOrCreateConnection(appId, isServer) {
4038
4230
  })();
4039
4231
  // Re-subscribe to all existing subscriptions after reconnect
4040
4232
  for (const sub of connection.subscriptions.values()) {
4233
+ sub.lastData = undefined;
4041
4234
  sendSubscribe(connection, sub);
4042
4235
  }
4236
+ // Start keepalive detection — if no messages for 90s, force reconnect
4237
+ if (connection.keepaliveTimer) {
4238
+ clearInterval(connection.keepaliveTimer);
4239
+ }
4240
+ connection.keepaliveTimer = setInterval(() => {
4241
+ var _a;
4242
+ if (Date.now() - connection.lastMessageAt > 90000) {
4243
+ console.warn('[WS v2] No messages received for 90s, forcing reconnect');
4244
+ if (connection.keepaliveTimer) {
4245
+ clearInterval(connection.keepaliveTimer);
4246
+ connection.keepaliveTimer = null;
4247
+ }
4248
+ (_a = connection.ws) === null || _a === void 0 ? void 0 : _a.reconnect();
4249
+ }
4250
+ }, 30000);
4043
4251
  });
4044
4252
  // Handle incoming messages
4045
4253
  ws.addEventListener('message', (event) => {
4254
+ connection.lastMessageAt = Date.now();
4046
4255
  try {
4047
4256
  const message = JSON.parse(event.data);
4048
4257
  handleServerMessage(connection, message);
@@ -4054,20 +4263,22 @@ async function getOrCreateConnection(appId, isServer) {
4054
4263
  // Handle errors
4055
4264
  ws.addEventListener('error', (event) => {
4056
4265
  console.error('[WS v2] WebSocket error:', event);
4057
- // Reject all pending subscriptions
4058
- for (const [id, pending] of connection.pendingSubscriptions) {
4266
+ for (const [, pending] of connection.pendingSubscriptions) {
4059
4267
  pending.reject(new Error('WebSocket error'));
4060
- connection.pendingSubscriptions.delete(id);
4061
4268
  }
4269
+ connection.pendingSubscriptions.clear();
4062
4270
  });
4063
4271
  // Handle close
4064
4272
  ws.addEventListener('close', () => {
4065
4273
  connection.isConnected = false;
4066
- // Clear token refresh timer
4067
4274
  if (connection.tokenRefreshTimer) {
4068
4275
  clearTimeout(connection.tokenRefreshTimer);
4069
4276
  connection.tokenRefreshTimer = null;
4070
4277
  }
4278
+ if (connection.keepaliveTimer) {
4279
+ clearInterval(connection.keepaliveTimer);
4280
+ connection.keepaliveTimer = null;
4281
+ }
4071
4282
  });
4072
4283
  return connection;
4073
4284
  }
@@ -4081,7 +4292,7 @@ function handleServerMessage(connection, message) {
4081
4292
  // If we already received data for this subscription, treat subscribed
4082
4293
  // as an ack only and avoid regressing to an older snapshot.
4083
4294
  if (subscription.lastData === undefined) {
4084
- const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape);
4295
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor);
4085
4296
  responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
4086
4297
  subscription.lastData = message.data;
4087
4298
  notifyCallbacks(subscription, message.data);
@@ -4108,7 +4319,7 @@ function handleServerMessage(connection, message) {
4108
4319
  const subscription = connection.subscriptions.get(message.subscriptionId);
4109
4320
  if (subscription) {
4110
4321
  // Update cache
4111
- const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape);
4322
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor);
4112
4323
  responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
4113
4324
  // Store last data
4114
4325
  subscription.lastData = message.data;
@@ -4165,6 +4376,8 @@ function sendSubscribe(connection, subscription) {
4165
4376
  shape: subscription.shape && Object.keys(subscription.shape).length > 0
4166
4377
  ? subscription.shape
4167
4378
  : undefined,
4379
+ limit: subscription.limit,
4380
+ cursor: subscription.cursor,
4168
4381
  };
4169
4382
  try {
4170
4383
  connection.ws.send(JSON.stringify(message));
@@ -4196,7 +4409,7 @@ function sendUnsubscribe(connection, subscriptionId) {
4196
4409
  async function subscribeV2(path, subscriptionOptions) {
4197
4410
  const config = await getConfig();
4198
4411
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
4199
- const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape);
4412
+ const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor);
4200
4413
  // Deliver cached data immediately if available
4201
4414
  const cachedEntry = responseCache.get(cacheKey);
4202
4415
  if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
@@ -4205,14 +4418,16 @@ async function subscribeV2(path, subscriptionOptions) {
4205
4418
  (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, cachedEntry.data);
4206
4419
  }, 0);
4207
4420
  }
4421
+ // Use explicit appId override if provided, otherwise fall back to config
4422
+ const effectiveAppId = subscriptionOptions.appId || config.appId;
4208
4423
  // Get or create connection for this appId
4209
- const connection = await getOrCreateConnection(config.appId, config.isServer);
4210
- // Check if we already have a subscription for this path+prompt+shape
4424
+ const connection = await getOrCreateConnection(effectiveAppId, config.isServer);
4425
+ // Check if we already have a subscription for this path+prompt+shape+limit+cursor
4211
4426
  const shapeKey = subscriptionOptions.shape ? JSON.stringify(subscriptionOptions.shape) : '';
4212
4427
  let existingSubscription;
4213
4428
  for (const sub of connection.subscriptions.values()) {
4214
4429
  const subShapeKey = sub.shape ? JSON.stringify(sub.shape) : '';
4215
- if (sub.path === normalizedPath && sub.prompt === subscriptionOptions.prompt && subShapeKey === shapeKey) {
4430
+ if (sub.path === normalizedPath && sub.prompt === subscriptionOptions.prompt && subShapeKey === shapeKey && sub.limit === subscriptionOptions.limit && sub.cursor === subscriptionOptions.cursor) {
4216
4431
  existingSubscription = sub;
4217
4432
  break;
4218
4433
  }
@@ -4238,6 +4453,8 @@ async function subscribeV2(path, subscriptionOptions) {
4238
4453
  path: normalizedPath,
4239
4454
  prompt: subscriptionOptions.prompt,
4240
4455
  shape: subscriptionOptions.shape,
4456
+ limit: subscriptionOptions.limit,
4457
+ cursor: subscriptionOptions.cursor,
4241
4458
  includeSubPaths: false,
4242
4459
  callbacks: [subscriptionOptions],
4243
4460
  lastData: undefined,
@@ -4261,9 +4478,7 @@ async function subscribeV2(path, subscriptionOptions) {
4261
4478
  await subscriptionPromise;
4262
4479
  }
4263
4480
  catch (error) {
4264
- // Remove subscription on error
4265
- connection.subscriptions.delete(subscriptionId);
4266
- throw error;
4481
+ console.warn('[WS v2] Subscription confirmation failed, keeping for reconnect recovery:', error);
4267
4482
  }
4268
4483
  }
4269
4484
  // Return unsubscribe function
@@ -4379,8 +4594,6 @@ async function reconnectWithNewAuthV2() {
4379
4594
  if (!connection.ws) {
4380
4595
  continue;
4381
4596
  }
4382
- // Reject any pending subscriptions - they'll need to be retried after reconnect
4383
- // This prevents hanging promises during auth transitions
4384
4597
  for (const [, pending] of connection.pendingSubscriptions) {
4385
4598
  pending.reject(new Error('Connection reconnecting due to auth change'));
4386
4599
  }
@@ -4474,10 +4687,12 @@ async function reconnectWithNewAuth() {
4474
4687
 
4475
4688
  exports.ServerSessionManager = ServerSessionManager;
4476
4689
  exports.WebSessionManager = WebSessionManager;
4690
+ exports.aggregate = aggregate;
4477
4691
  exports.buildSetDocumentsTransaction = buildSetDocumentsTransaction;
4478
4692
  exports.clearCache = clearCache;
4479
4693
  exports.closeAllSubscriptions = closeAllSubscriptions;
4480
4694
  exports.convertRemainingAccounts = convertRemainingAccounts;
4695
+ exports.count = count;
4481
4696
  exports.createSessionWithPrivy = createSessionWithPrivy;
4482
4697
  exports.createSessionWithSignature = createSessionWithSignature;
4483
4698
  exports.genAuthNonce = genAuthNonce;