@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.
- package/dist/client/operations.d.ts +87 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +248 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +247 -34
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/utils/server-session-manager.d.ts +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
70
|
-
|
|
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
|
|
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
|
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
|
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,
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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;
|