@pooflabs/core 0.0.41 → 0.0.43

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/README.md CHANGED
@@ -39,6 +39,15 @@ export interface User {
39
39
  address: string;
40
40
  provider: AuthProvider;
41
41
  }
42
+
43
+ export interface GetManyResult {
44
+ path: string;
45
+ data: any | null;
46
+ error?: {
47
+ code: 'NOT_FOUND' | 'UNAUTHORIZED' | 'INVALID_PATH';
48
+ message: string;
49
+ };
50
+ }
42
51
  ```
43
52
 
44
53
  ### Core Operations
@@ -52,6 +61,7 @@ function getConfig(): Promise<ClientConfig>;
52
61
 
53
62
  // Data operations
54
63
  function get(path: string): Promise<any>;
64
+ function getMany(paths: string[], options?: { bypassCache?: boolean }): Promise<GetManyResult[]>;
55
65
  function set(path: string, data: any, options?: SetOptions): Promise<any>;
56
66
  function setMany(paths: { [key: string]: any }, options?: SetOptions): Promise<any>;
57
67
  function setFile(path: string, file: File, metadata?: any): Promise<any>;
@@ -7,6 +7,7 @@ export type RequestOverrides = {
7
7
  _getAuthHeaders?: () => Promise<Record<string, string>>;
8
8
  _clearAuth?: () => Promise<void>;
9
9
  _walletAddress?: string;
10
+ timeout?: number;
10
11
  };
11
12
  export type SetOptions = {
12
13
  shouldSubmitTx?: boolean;
@@ -113,6 +114,20 @@ export declare function count(path: string, opts?: CountOptions): Promise<Aggreg
113
114
  */
114
115
  export declare function aggregate(path: string, operation: AggregateOperation, opts?: AggregateOptions): Promise<AggregateResult>;
115
116
  export declare function get(path: string, opts?: GetOptions): Promise<any>;
117
+ export type GetManyResult = {
118
+ path: string;
119
+ data: any | null;
120
+ error?: {
121
+ code: 'NOT_FOUND' | 'UNAUTHORIZED' | 'INVALID_PATH';
122
+ message: string;
123
+ };
124
+ };
125
+ export declare function getMany(paths: string[], opts?: {
126
+ bypassCache?: boolean;
127
+ _overrides?: {
128
+ headers?: Record<string, string>;
129
+ };
130
+ }): Promise<GetManyResult[]>;
116
131
  export type RunExpressionOptions = {
117
132
  returnType?: 'Bool' | 'String' | 'Int' | 'UInt';
118
133
  _overrides?: RequestOverrides;
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, count, aggregate, RequestOverrides, SetOptions, GetOptions, RunQueryOptions, CountOptions, AggregateOptions, AggregateOperation, AggregateResult, RunExpressionOptions, RunExpressionResult, InsufficientBalanceError } from './client/operations';
3
+ export { get, getMany, set, setMany, setFile, getFiles, runQuery, runQueryMany, runExpression, runExpressionMany, signMessage, signTransaction, signAndSubmitTransaction, count, aggregate, RequestOverrides, SetOptions, GetOptions, RunQueryOptions, CountOptions, AggregateOptions, AggregateOperation, AggregateResult, RunExpressionOptions, RunExpressionResult, InsufficientBalanceError, GetManyResult } 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
@@ -8,24 +8,279 @@ var BN = require('bn.js');
8
8
  var ReconnectingWebSocket = require('reconnecting-websocket');
9
9
 
10
10
  function _interopNamespaceDefault(e) {
11
- var n = Object.create(null);
12
- if (e) {
13
- Object.keys(e).forEach(function (k) {
14
- if (k !== 'default') {
15
- var d = Object.getOwnPropertyDescriptor(e, k);
16
- Object.defineProperty(n, k, d.get ? d : {
17
- enumerable: true,
18
- get: function () { return e[k]; }
19
- });
20
- }
21
- });
22
- }
23
- n.default = e;
24
- return Object.freeze(n);
11
+ var n = Object.create(null);
12
+ if (e) {
13
+ Object.keys(e).forEach(function (k) {
14
+ if (k !== 'default') {
15
+ var d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: function () { return e[k]; }
19
+ });
20
+ }
21
+ });
22
+ }
23
+ n.default = e;
24
+ return Object.freeze(n);
25
25
  }
26
26
 
27
27
  var anchor__namespace = /*#__PURE__*/_interopNamespaceDefault(anchor);
28
28
 
29
+ function getDefaultExportFromCjs (x) {
30
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
31
+ }
32
+
33
+ var isRetryAllowed$1;
34
+ var hasRequiredIsRetryAllowed;
35
+
36
+ function requireIsRetryAllowed () {
37
+ if (hasRequiredIsRetryAllowed) return isRetryAllowed$1;
38
+ hasRequiredIsRetryAllowed = 1;
39
+
40
+ const denyList = new Set([
41
+ 'ENOTFOUND',
42
+ 'ENETUNREACH',
43
+
44
+ // SSL errors from https://github.com/nodejs/node/blob/fc8e3e2cdc521978351de257030db0076d79e0ab/src/crypto/crypto_common.cc#L301-L328
45
+ 'UNABLE_TO_GET_ISSUER_CERT',
46
+ 'UNABLE_TO_GET_CRL',
47
+ 'UNABLE_TO_DECRYPT_CERT_SIGNATURE',
48
+ 'UNABLE_TO_DECRYPT_CRL_SIGNATURE',
49
+ 'UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY',
50
+ 'CERT_SIGNATURE_FAILURE',
51
+ 'CRL_SIGNATURE_FAILURE',
52
+ 'CERT_NOT_YET_VALID',
53
+ 'CERT_HAS_EXPIRED',
54
+ 'CRL_NOT_YET_VALID',
55
+ 'CRL_HAS_EXPIRED',
56
+ 'ERROR_IN_CERT_NOT_BEFORE_FIELD',
57
+ 'ERROR_IN_CERT_NOT_AFTER_FIELD',
58
+ 'ERROR_IN_CRL_LAST_UPDATE_FIELD',
59
+ 'ERROR_IN_CRL_NEXT_UPDATE_FIELD',
60
+ 'OUT_OF_MEM',
61
+ 'DEPTH_ZERO_SELF_SIGNED_CERT',
62
+ 'SELF_SIGNED_CERT_IN_CHAIN',
63
+ 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
64
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
65
+ 'CERT_CHAIN_TOO_LONG',
66
+ 'CERT_REVOKED',
67
+ 'INVALID_CA',
68
+ 'PATH_LENGTH_EXCEEDED',
69
+ 'INVALID_PURPOSE',
70
+ 'CERT_UNTRUSTED',
71
+ 'CERT_REJECTED',
72
+ 'HOSTNAME_MISMATCH'
73
+ ]);
74
+
75
+ // TODO: Use `error?.code` when targeting Node.js 14
76
+ isRetryAllowed$1 = error => !denyList.has(error && error.code);
77
+ return isRetryAllowed$1;
78
+ }
79
+
80
+ var isRetryAllowedExports = requireIsRetryAllowed();
81
+ var isRetryAllowed = /*@__PURE__*/getDefaultExportFromCjs(isRetryAllowedExports);
82
+
83
+ const namespace = 'axios-retry';
84
+ function isNetworkError(error) {
85
+ const CODE_EXCLUDE_LIST = ['ERR_CANCELED', 'ECONNABORTED'];
86
+ if (error.response) {
87
+ return false;
88
+ }
89
+ if (!error.code) {
90
+ return false;
91
+ }
92
+ // Prevents retrying timed out & cancelled requests
93
+ if (CODE_EXCLUDE_LIST.includes(error.code)) {
94
+ return false;
95
+ }
96
+ // Prevents retrying unsafe errors
97
+ return isRetryAllowed(error);
98
+ }
99
+ const SAFE_HTTP_METHODS = ['get', 'head', 'options'];
100
+ const IDEMPOTENT_HTTP_METHODS = SAFE_HTTP_METHODS.concat(['put', 'delete']);
101
+ function isRetryableError(error) {
102
+ return (error.code !== 'ECONNABORTED' &&
103
+ (!error.response ||
104
+ error.response.status === 429 ||
105
+ (error.response.status >= 500 && error.response.status <= 599)));
106
+ }
107
+ function isSafeRequestError(error) {
108
+ if (!error.config?.method) {
109
+ // Cannot determine if the request can be retried
110
+ return false;
111
+ }
112
+ return isRetryableError(error) && SAFE_HTTP_METHODS.indexOf(error.config.method) !== -1;
113
+ }
114
+ function isIdempotentRequestError(error) {
115
+ if (!error.config?.method) {
116
+ // Cannot determine if the request can be retried
117
+ return false;
118
+ }
119
+ return isRetryableError(error) && IDEMPOTENT_HTTP_METHODS.indexOf(error.config.method) !== -1;
120
+ }
121
+ function isNetworkOrIdempotentRequestError(error) {
122
+ return isNetworkError(error) || isIdempotentRequestError(error);
123
+ }
124
+ function retryAfter(error = undefined) {
125
+ const retryAfterHeader = error?.response?.headers['retry-after'];
126
+ if (!retryAfterHeader) {
127
+ return 0;
128
+ }
129
+ // if the retry after header is a number, convert it to milliseconds
130
+ let retryAfterMs = (Number(retryAfterHeader) || 0) * 1000;
131
+ // If the retry after header is a date, get the number of milliseconds until that date
132
+ if (retryAfterMs === 0) {
133
+ retryAfterMs = (new Date(retryAfterHeader).valueOf() || 0) - Date.now();
134
+ }
135
+ return Math.max(0, retryAfterMs);
136
+ }
137
+ function noDelay(_retryNumber = 0, error = undefined) {
138
+ return Math.max(0, retryAfter(error));
139
+ }
140
+ function exponentialDelay(retryNumber = 0, error = undefined, delayFactor = 100) {
141
+ const calculatedDelay = 2 ** retryNumber * delayFactor;
142
+ const delay = Math.max(calculatedDelay, retryAfter(error));
143
+ const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay
144
+ return delay + randomSum;
145
+ }
146
+ /**
147
+ * Linear delay
148
+ * @param {number | undefined} delayFactor - delay factor in milliseconds (default: 100)
149
+ * @returns {function} (retryNumber: number, error: AxiosError | undefined) => number
150
+ */
151
+ function linearDelay(delayFactor = 100) {
152
+ return (retryNumber = 0, error = undefined) => {
153
+ const delay = retryNumber * delayFactor;
154
+ return Math.max(delay, retryAfter(error));
155
+ };
156
+ }
157
+ const DEFAULT_OPTIONS = {
158
+ retries: 3,
159
+ retryCondition: isNetworkOrIdempotentRequestError,
160
+ retryDelay: noDelay,
161
+ shouldResetTimeout: false,
162
+ onRetry: () => { },
163
+ onMaxRetryTimesExceeded: () => { },
164
+ validateResponse: null
165
+ };
166
+ function getRequestOptions(config, defaultOptions) {
167
+ return { ...DEFAULT_OPTIONS, ...defaultOptions, ...config[namespace] };
168
+ }
169
+ function setCurrentState(config, defaultOptions, resetLastRequestTime = false) {
170
+ const currentState = getRequestOptions(config, defaultOptions || {});
171
+ currentState.retryCount = currentState.retryCount || 0;
172
+ if (!currentState.lastRequestTime || resetLastRequestTime) {
173
+ currentState.lastRequestTime = Date.now();
174
+ }
175
+ config[namespace] = currentState;
176
+ return currentState;
177
+ }
178
+ function fixConfig(axiosInstance, config) {
179
+ // @ts-ignore
180
+ if (axiosInstance.defaults.agent === config.agent) {
181
+ // @ts-ignore
182
+ delete config.agent;
183
+ }
184
+ if (axiosInstance.defaults.httpAgent === config.httpAgent) {
185
+ delete config.httpAgent;
186
+ }
187
+ if (axiosInstance.defaults.httpsAgent === config.httpsAgent) {
188
+ delete config.httpsAgent;
189
+ }
190
+ }
191
+ async function shouldRetry(currentState, error) {
192
+ const { retries, retryCondition } = currentState;
193
+ const shouldRetryOrPromise = (currentState.retryCount || 0) < retries && retryCondition(error);
194
+ // This could be a promise
195
+ if (typeof shouldRetryOrPromise === 'object') {
196
+ try {
197
+ const shouldRetryPromiseResult = await shouldRetryOrPromise;
198
+ // keep return true unless shouldRetryPromiseResult return false for compatibility
199
+ return shouldRetryPromiseResult !== false;
200
+ }
201
+ catch (_err) {
202
+ return false;
203
+ }
204
+ }
205
+ return shouldRetryOrPromise;
206
+ }
207
+ async function handleRetry(axiosInstance, currentState, error, config) {
208
+ currentState.retryCount += 1;
209
+ const { retryDelay, shouldResetTimeout, onRetry } = currentState;
210
+ const delay = retryDelay(currentState.retryCount, error);
211
+ // Axios fails merging this configuration to the default configuration because it has an issue
212
+ // with circular structures: https://github.com/mzabriskie/axios/issues/370
213
+ fixConfig(axiosInstance, config);
214
+ if (!shouldResetTimeout && config.timeout && currentState.lastRequestTime) {
215
+ const lastRequestDuration = Date.now() - currentState.lastRequestTime;
216
+ const timeout = config.timeout - lastRequestDuration - delay;
217
+ if (timeout <= 0) {
218
+ return Promise.reject(error);
219
+ }
220
+ config.timeout = timeout;
221
+ }
222
+ config.transformRequest = [(data) => data];
223
+ await onRetry(currentState.retryCount, error, config);
224
+ if (config.signal?.aborted) {
225
+ return Promise.resolve(axiosInstance(config));
226
+ }
227
+ return new Promise((resolve) => {
228
+ const abortListener = () => {
229
+ clearTimeout(timeout);
230
+ resolve(axiosInstance(config));
231
+ };
232
+ const timeout = setTimeout(() => {
233
+ resolve(axiosInstance(config));
234
+ if (config.signal?.removeEventListener) {
235
+ config.signal.removeEventListener('abort', abortListener);
236
+ }
237
+ }, delay);
238
+ if (config.signal?.addEventListener) {
239
+ config.signal.addEventListener('abort', abortListener, { once: true });
240
+ }
241
+ });
242
+ }
243
+ async function handleMaxRetryTimesExceeded(currentState, error) {
244
+ if (currentState.retryCount >= currentState.retries)
245
+ await currentState.onMaxRetryTimesExceeded(error, currentState.retryCount);
246
+ }
247
+ const axiosRetry = (axiosInstance, defaultOptions) => {
248
+ const requestInterceptorId = axiosInstance.interceptors.request.use((config) => {
249
+ setCurrentState(config, defaultOptions, true);
250
+ if (config[namespace]?.validateResponse) {
251
+ // by setting this, all HTTP responses will be go through the error interceptor first
252
+ config.validateStatus = () => false;
253
+ }
254
+ return config;
255
+ });
256
+ const responseInterceptorId = axiosInstance.interceptors.response.use(null, async (error) => {
257
+ const { config } = error;
258
+ // If we have no information to retry the request
259
+ if (!config) {
260
+ return Promise.reject(error);
261
+ }
262
+ const currentState = setCurrentState(config, defaultOptions);
263
+ if (error.response && currentState.validateResponse?.(error.response)) {
264
+ // no issue with response
265
+ return error.response;
266
+ }
267
+ if (await shouldRetry(currentState, error)) {
268
+ return handleRetry(axiosInstance, currentState, error, config);
269
+ }
270
+ await handleMaxRetryTimesExceeded(currentState, error);
271
+ return Promise.reject(error);
272
+ });
273
+ return { requestInterceptorId, responseInterceptorId };
274
+ };
275
+ // Compatibility with CommonJS
276
+ axiosRetry.isNetworkError = isNetworkError;
277
+ axiosRetry.isSafeRequestError = isSafeRequestError;
278
+ axiosRetry.isIdempotentRequestError = isIdempotentRequestError;
279
+ axiosRetry.isNetworkOrIdempotentRequestError = isNetworkOrIdempotentRequestError;
280
+ axiosRetry.exponentialDelay = exponentialDelay;
281
+ axiosRetry.linearDelay = linearDelay;
282
+ axiosRetry.isRetryableError = isRetryableError;
283
+
29
284
  let axiosClient;
30
285
  async function getAxiosAuthClient() {
31
286
  if (!axiosClient) {
@@ -35,6 +290,7 @@ async function getAxiosAuthClient() {
35
290
  headers: {
36
291
  'Content-Type': 'application/json',
37
292
  },
293
+ timeout: 30000, // 30s timeout, matching makeApiRequest
38
294
  });
39
295
  }
40
296
  return axiosClient;
@@ -81,15 +337,41 @@ async function createSessionWithPrivy(authToken, address, privyIdToken) {
81
337
  });
82
338
  return response.data;
83
339
  }
340
+ // Deduplicate concurrent refreshSession calls to prevent multiple code paths
341
+ // (WebSocket reconnection, periodic refresh, API 401 retry) from all hitting
342
+ // /session/refresh simultaneously.
343
+ // Note: module-level state is shared across requests in SSR/Node — acceptable
344
+ // since there is only one active session per server instance (same pattern as
345
+ // refreshInFlight in api.ts).
346
+ let refreshInFlight$1 = null;
347
+ let refreshInFlightToken = null;
84
348
  async function refreshSession(refreshToken) {
85
- const client = await getAxiosAuthClient();
86
- const config = await getConfig();
87
- const appId = config.appId;
88
- const response = await client.post('/session/refresh', {
89
- refreshToken,
90
- appId
91
- });
92
- return response.data;
349
+ if (refreshInFlight$1 && refreshInFlightToken === refreshToken) {
350
+ return refreshInFlight$1;
351
+ }
352
+ refreshInFlightToken = refreshToken;
353
+ let currentFlight;
354
+ currentFlight = refreshInFlight$1 = (async () => {
355
+ try {
356
+ const client = await getAxiosAuthClient();
357
+ const config = await getConfig();
358
+ const appId = config.appId;
359
+ const response = await client.post('/session/refresh', {
360
+ refreshToken,
361
+ appId
362
+ });
363
+ return response.data;
364
+ }
365
+ finally {
366
+ // Only clear if we're still the active in-flight request.
367
+ // A second call with a different token may have replaced us.
368
+ if (refreshInFlight$1 === currentFlight) {
369
+ refreshInFlight$1 = null;
370
+ refreshInFlightToken = null;
371
+ }
372
+ }
373
+ })();
374
+ return refreshInFlight$1;
93
375
  }
94
376
  async function signSessionCreateMessage(_signMessageFunction) {
95
377
  }
@@ -227,14 +509,10 @@ class WebSessionManager {
227
509
  WebSessionManager.TAROBASE_SESSION_STORAGE_KEY = "tarobase_session_storage";
228
510
 
229
511
  var webSessionManager = /*#__PURE__*/Object.freeze({
230
- __proto__: null,
231
- WebSessionManager: WebSessionManager
512
+ __proto__: null,
513
+ WebSessionManager: WebSessionManager
232
514
  });
233
515
 
234
- function getDefaultExportFromCjs (x) {
235
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
236
- }
237
-
238
516
  var buffer = {};
239
517
 
240
518
  var base64Js = {};
@@ -3031,7 +3309,19 @@ async function buildSetDocumentsTransaction(connection, idl, anchorProvider, pay
3031
3309
  }
3032
3310
  }
3033
3311
  else if (lutKey != null) {
3034
- const { value: table } = await connection.getAddressLookupTable(new web3_js.PublicKey(lutKey));
3312
+ // The LUT may have just been created server-side and the client's RPC node
3313
+ // may not have indexed it yet (load-balancer split). Retry with exponential
3314
+ // back-off; first retry at 100 ms almost always resolves it.
3315
+ let table = null;
3316
+ for (let attempt = 0; attempt < 5; attempt++) {
3317
+ const { value } = await connection.getAddressLookupTable(new web3_js.PublicKey(lutKey));
3318
+ if (value) {
3319
+ table = value;
3320
+ break;
3321
+ }
3322
+ if (attempt < 4)
3323
+ await new Promise(r => setTimeout(r, 100 * Math.pow(2, attempt)));
3324
+ }
3035
3325
  if (!table)
3036
3326
  throw new Error('LUT not found after creation/extend');
3037
3327
  lookupTables.push(table);
@@ -3166,8 +3456,8 @@ class ServerSessionManager {
3166
3456
  ServerSessionManager.instance = new ServerSessionManager();
3167
3457
 
3168
3458
  var serverSessionManager = /*#__PURE__*/Object.freeze({
3169
- __proto__: null,
3170
- ServerSessionManager: ServerSessionManager
3459
+ __proto__: null,
3460
+ ServerSessionManager: ServerSessionManager
3171
3461
  });
3172
3462
 
3173
3463
  /**
@@ -3266,6 +3556,23 @@ async function updateIdTokenAndAccessToken(idToken, accessToken, isServer = fals
3266
3556
  await WebSessionManager.updateIdTokenAndAccessToken(idToken, accessToken);
3267
3557
  }
3268
3558
 
3559
+ const apiClient = axios.create();
3560
+ axiosRetry(apiClient, {
3561
+ retries: 2,
3562
+ retryCondition: (error) => {
3563
+ var _a, _b;
3564
+ // Only retry GET requests on network errors (ECONNRESET, ETIMEDOUT, etc.)
3565
+ // Writes (POST/PUT) are not retried — the server may have processed
3566
+ // the request before the connection was reset.
3567
+ return axiosRetry.isNetworkError(error) && ((_b = (_a = error.config) === null || _a === void 0 ? void 0 : _a.method) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === 'get';
3568
+ },
3569
+ retryDelay: axiosRetry.exponentialDelay,
3570
+ shouldResetTimeout: true,
3571
+ onRetry: (retryCount, error, requestConfig) => {
3572
+ var _a;
3573
+ console.warn(`[tarobase-sdk] retry ${retryCount} for ${(_a = requestConfig.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()} ${requestConfig.url} — ${error.code || error.message}`);
3574
+ },
3575
+ });
3269
3576
  const refreshInFlight = new Map();
3270
3577
  async function refreshAuthSessionOnce(appId, isServer) {
3271
3578
  const key = `${isServer ? "server" : "web"}:${appId}`;
@@ -3312,6 +3619,7 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
3312
3619
  ServerSessionManager.instance.clearSession();
3313
3620
  };
3314
3621
  async function executeRequest() {
3622
+ var _a;
3315
3623
  // When _getAuthHeaders is provided (wallet client), use it as the sole auth source.
3316
3624
  // Otherwise use the global createAuthHeader (default path).
3317
3625
  const authHeader = (_overrides === null || _overrides === void 0 ? void 0 : _overrides._getAuthHeaders)
@@ -3333,11 +3641,12 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
3333
3641
  method,
3334
3642
  url: `${config.apiUrl}${urlPath.startsWith("/") ? urlPath : `/${urlPath}`}`,
3335
3643
  headers,
3644
+ timeout: (_a = _overrides === null || _overrides === void 0 ? void 0 : _overrides.timeout) !== null && _a !== void 0 ? _a : 30000,
3336
3645
  };
3337
3646
  if (method !== "GET" && method !== "get") {
3338
3647
  requestConfig.data = data ? JSON.stringify(data) : {};
3339
3648
  }
3340
- const response = await axios(requestConfig);
3649
+ const response = await apiClient(requestConfig);
3341
3650
  return { data: response.data, status: response.status, headers: response.headers };
3342
3651
  }
3343
3652
  try {
@@ -3748,6 +4057,90 @@ function cleanupExpiredCache() {
3748
4057
  });
3749
4058
  lastCacheCleanup = now;
3750
4059
  }
4060
+ async function getMany(paths, opts = {}) {
4061
+ if (paths.length === 0) {
4062
+ return [];
4063
+ }
4064
+ if (paths.length > 30) {
4065
+ throw new Error('Cannot fetch more than 30 documents at once');
4066
+ }
4067
+ const normalizedPaths = [];
4068
+ for (const path of paths) {
4069
+ let normalizedPath = path.startsWith("/") ? path.slice(1) : path;
4070
+ if (normalizedPath.endsWith("*") && normalizedPath.length > 1) {
4071
+ normalizedPath = normalizedPath.slice(0, -1);
4072
+ }
4073
+ if (!normalizedPath || normalizedPath.length === 0) {
4074
+ throw new Error(`Invalid path provided: ${path}`);
4075
+ }
4076
+ const pathIsDocument = normalizedPath.split("/").length % 2 === 0;
4077
+ if (!pathIsDocument) {
4078
+ throw new Error(`Path must point to a document (even number of segments): ${path}`);
4079
+ }
4080
+ normalizedPaths.push(normalizedPath);
4081
+ }
4082
+ const now = Date.now();
4083
+ const results = new Array(paths.length);
4084
+ const uncachedIndices = [];
4085
+ const uncachedPaths = [];
4086
+ for (let i = 0; i < normalizedPaths.length; i++) {
4087
+ const normalizedPath = normalizedPaths[i];
4088
+ const cacheKey = `${normalizedPath}:`;
4089
+ if (!opts.bypassCache && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
4090
+ results[i] = { path: normalizedPath, data: getCache[cacheKey].data };
4091
+ }
4092
+ else {
4093
+ uncachedIndices.push(i);
4094
+ uncachedPaths.push(normalizedPath);
4095
+ }
4096
+ }
4097
+ if (uncachedPaths.length > 0) {
4098
+ try {
4099
+ const response = await makeApiRequest('POST', 'items/batch', { paths: uncachedPaths }, opts._overrides);
4100
+ const serverResults = response.data.results;
4101
+ const serverResultsMap = new Map();
4102
+ for (const result of serverResults) {
4103
+ serverResultsMap.set(result.path, result);
4104
+ }
4105
+ for (let i = 0; i < uncachedIndices.length; i++) {
4106
+ const originalIndex = uncachedIndices[i];
4107
+ const normalizedPath = uncachedPaths[i];
4108
+ const serverResult = serverResultsMap.get(normalizedPath);
4109
+ if (serverResult) {
4110
+ results[originalIndex] = serverResult;
4111
+ if (!serverResult.error && !opts.bypassCache) {
4112
+ const cacheKey = `${normalizedPath}:`;
4113
+ getCache[cacheKey] = {
4114
+ data: serverResult.data,
4115
+ expiresAt: now + GET_CACHE_TTL
4116
+ };
4117
+ }
4118
+ }
4119
+ else {
4120
+ results[originalIndex] = {
4121
+ path: normalizedPath,
4122
+ data: null,
4123
+ error: { code: 'NOT_FOUND', message: `No result returned for path ${normalizedPath}` }
4124
+ };
4125
+ }
4126
+ }
4127
+ if (now - lastCacheCleanup > 5000) {
4128
+ cleanupExpiredCache();
4129
+ lastCacheCleanup = now;
4130
+ }
4131
+ }
4132
+ catch (error) {
4133
+ for (const originalIndex of uncachedIndices) {
4134
+ results[originalIndex] = {
4135
+ path: normalizedPaths[originalIndex],
4136
+ data: null,
4137
+ error: { code: 'NOT_FOUND', message: error instanceof Error ? error.message : 'Unknown error' }
4138
+ };
4139
+ }
4140
+ }
4141
+ }
4142
+ return results;
4143
+ }
3751
4144
  async function runQuery(absolutePath, queryName, queryArgs, opts) {
3752
4145
  const result = await runQueryMany([{ absolutePath, queryName, queryArgs }], opts);
3753
4146
  return result[0];
@@ -4189,6 +4582,7 @@ function scheduleTokenRefresh(connection, isServer) {
4189
4582
  // This replaces the old single setTimeout approach which was unreliable for long
4190
4583
  // delays (browsers throttle/suspend timers in background tabs).
4191
4584
  connection.tokenRefreshTimer = setInterval(async () => {
4585
+ var _a, _b;
4192
4586
  try {
4193
4587
  const currentToken = await getIdToken(isServer);
4194
4588
  if (!currentToken)
@@ -4219,6 +4613,17 @@ function scheduleTokenRefresh(connection, isServer) {
4219
4613
  }
4220
4614
  }
4221
4615
  catch (refreshError) {
4616
+ // If the refresh token itself is invalid (401/403), stop retrying —
4617
+ // the token won't magically become valid next interval, and continuing
4618
+ // to hammer /session/refresh causes 429 rate-limit storms.
4619
+ if (((_a = refreshError === null || refreshError === void 0 ? void 0 : refreshError.response) === null || _a === void 0 ? void 0 : _a.status) === 401 || ((_b = refreshError === null || refreshError === void 0 ? void 0 : refreshError.response) === null || _b === void 0 ? void 0 : _b.status) === 403) {
4620
+ console.warn('[WS v2] Refresh token rejected (401/403), stopping periodic refresh');
4621
+ if (connection.tokenRefreshTimer) {
4622
+ clearInterval(connection.tokenRefreshTimer);
4623
+ connection.tokenRefreshTimer = null;
4624
+ }
4625
+ return;
4626
+ }
4222
4627
  console.warn('[WS v2] Proactive token refresh failed, will retry next interval:', refreshError);
4223
4628
  }
4224
4629
  }
@@ -4229,6 +4634,7 @@ function scheduleTokenRefresh(connection, isServer) {
4229
4634
  }, TOKEN_CHECK_INTERVAL);
4230
4635
  }
4231
4636
  async function getFreshAuthToken(isServer) {
4637
+ var _a, _b;
4232
4638
  const currentToken = await getIdToken(isServer);
4233
4639
  if (!currentToken) {
4234
4640
  return null;
@@ -4251,6 +4657,17 @@ async function getFreshAuthToken(isServer) {
4251
4657
  }
4252
4658
  catch (error) {
4253
4659
  console.error('[WS v2] Error refreshing token:', error);
4660
+ // If the refresh token is permanently invalid (401/403), clear the stale
4661
+ // session from storage so future attempts don't keep retrying with it.
4662
+ if (!isServer && (((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 || ((_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.status) === 403)) {
4663
+ try {
4664
+ const { WebSessionManager } = await Promise.resolve().then(function () { return webSessionManager; });
4665
+ WebSessionManager.clearSession();
4666
+ }
4667
+ catch (clearError) {
4668
+ console.warn('[WS v2] Failed to clear stale session:', clearError);
4669
+ }
4670
+ }
4254
4671
  }
4255
4672
  // Return null instead of the expired token to prevent infinite 401 reconnect storms.
4256
4673
  // The server accepts unauthenticated connections; auth-required subscriptions will
@@ -4371,7 +4788,16 @@ async function getOrCreateConnection(appId, isServer) {
4371
4788
  ws.addEventListener('open', () => {
4372
4789
  connection.isConnecting = false;
4373
4790
  connection.isConnected = true;
4374
- connection.consecutiveAuthFailures = 0;
4791
+ // NOTE: Do NOT reset consecutiveAuthFailures here. It is reset when a
4792
+ // fresh auth token is actually obtained (in urlProvider, line ~389) or on
4793
+ // an explicit auth change (reconnectWithNewAuthV2, line ~854). Resetting
4794
+ // on every 'open' event created an infinite loop: auth fails 5x → connect
4795
+ // without auth → open resets counter → disconnect → auth fails 5x again →
4796
+ // repeat forever, hammering /session/refresh and causing 429s.
4797
+ //
4798
+ // An elevated counter is safe for anonymous/guest sessions: when there's no
4799
+ // token at all (getIdToken returns null), the counter is never checked —
4800
+ // urlProvider skips straight to unauthenticated connection.
4375
4801
  // Schedule periodic token freshness checks
4376
4802
  scheduleTokenRefresh(connection, isServer);
4377
4803
  // Re-subscribe to all existing subscriptions after reconnect
@@ -5007,6 +5433,7 @@ exports.getCachedData = getCachedData;
5007
5433
  exports.getConfig = getConfig;
5008
5434
  exports.getFiles = getFiles;
5009
5435
  exports.getIdToken = getIdToken;
5436
+ exports.getMany = getMany;
5010
5437
  exports.init = init;
5011
5438
  exports.reconnectWithNewAuth = reconnectWithNewAuth;
5012
5439
  exports.refreshSession = refreshSession;