@pooflabs/core 0.0.27 → 0.0.29

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.
@@ -5,13 +5,24 @@ export type SetOptions = {
5
5
  headers?: Record<string, string>;
6
6
  };
7
7
  };
8
- export declare function get(path: string, opts?: {
8
+ /**
9
+ * Options for the get function.
10
+ */
11
+ export type GetOptions = {
12
+ /** Natural language prompt for AI-powered queries (collections only) */
9
13
  prompt?: string | undefined;
14
+ /** Bypass the local cache and fetch fresh data */
10
15
  bypassCache?: boolean;
16
+ /** Include documents from sub-paths (nested collections) */
17
+ includeSubPaths?: boolean;
18
+ /** Shape object for relationship resolution - specifies which related documents to include */
19
+ shape?: Record<string, any>;
20
+ /** Internal overrides for headers */
11
21
  _overrides?: {
12
22
  headers?: Record<string, string>;
13
23
  };
14
- }): Promise<any>;
24
+ };
25
+ export declare function get(path: string, opts?: GetOptions): Promise<any>;
15
26
  export type RunExpressionOptions = {
16
27
  returnType?: 'Bool' | 'String' | 'Int' | 'UInt';
17
28
  _overrides?: {
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, RunExpressionOptions, RunExpressionResult } from './client/operations';
3
+ export { get, set, setMany, setFile, getFiles, runQuery, runQueryMany, runExpression, runExpressionMany, signMessage, signTransaction, signAndSubmitTransaction, SetOptions, GetOptions, 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
@@ -204,6 +204,11 @@ class WebSessionManager {
204
204
  }
205
205
  WebSessionManager.TAROBASE_SESSION_STORAGE_KEY = "tarobase_session_storage";
206
206
 
207
+ var webSessionManager = /*#__PURE__*/Object.freeze({
208
+ __proto__: null,
209
+ WebSessionManager: WebSessionManager
210
+ });
211
+
207
212
  function getDefaultExportFromCjs (x) {
208
213
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
209
214
  }
@@ -3101,11 +3106,19 @@ class ServerSessionManager {
3101
3106
  /* The single, shared instance */
3102
3107
  ServerSessionManager.instance = new ServerSessionManager();
3103
3108
 
3109
+ var serverSessionManager = /*#__PURE__*/Object.freeze({
3110
+ __proto__: null,
3111
+ ServerSessionManager: ServerSessionManager
3112
+ });
3113
+
3104
3114
  async function createBearerToken(isServer) {
3105
3115
  if (isServer) {
3106
3116
  const sessionMgr = ServerSessionManager.instance;
3107
3117
  const session = await sessionMgr.getSession();
3108
- return `Bearer ${session === null || session === void 0 ? void 0 : session.idToken}`;
3118
+ if (!(session === null || session === void 0 ? void 0 : session.idToken)) {
3119
+ return null;
3120
+ }
3121
+ return `Bearer ${session.idToken}`;
3109
3122
  }
3110
3123
  const idToken = WebSessionManager.getIdToken();
3111
3124
  if (!idToken) {
@@ -3138,13 +3151,72 @@ async function getRefreshToken(isServer) {
3138
3151
  }
3139
3152
  return WebSessionManager.getRefreshToken();
3140
3153
  }
3141
- function updateIdTokenAndAccessToken(idToken, accessToken) {
3142
- WebSessionManager.updateIdTokenAndAccessToken(idToken, accessToken);
3154
+ async function updateIdTokenAndAccessToken(idToken, accessToken, isServer = false) {
3155
+ if (isServer) {
3156
+ const sessionMgr = ServerSessionManager.instance;
3157
+ // Ensure we have a session to update; if missing, let lazy creation happen on next request.
3158
+ let session = null;
3159
+ try {
3160
+ session = await sessionMgr.getSession();
3161
+ }
3162
+ catch (_a) {
3163
+ return;
3164
+ }
3165
+ if (!session) {
3166
+ return;
3167
+ }
3168
+ sessionMgr.setSession(Object.assign(Object.assign({}, session), { idToken,
3169
+ accessToken }));
3170
+ return;
3171
+ }
3172
+ await WebSessionManager.updateIdTokenAndAccessToken(idToken, accessToken);
3143
3173
  }
3144
3174
 
3175
+ const refreshInFlight = new Map();
3176
+ async function refreshAuthSessionOnce(appId, isServer) {
3177
+ const key = `${isServer ? "server" : "web"}:${appId}`;
3178
+ const existing = refreshInFlight.get(key);
3179
+ if (existing) {
3180
+ return existing;
3181
+ }
3182
+ const refreshPromise = (async () => {
3183
+ var _a, _b;
3184
+ try {
3185
+ const refreshToken = await getRefreshToken(isServer);
3186
+ if (!refreshToken) {
3187
+ return false;
3188
+ }
3189
+ const refreshData = await refreshSession(refreshToken);
3190
+ if (!(refreshData === null || refreshData === void 0 ? void 0 : refreshData.idToken) || !(refreshData === null || refreshData === void 0 ? void 0 : refreshData.accessToken)) {
3191
+ return false;
3192
+ }
3193
+ await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer);
3194
+ return true;
3195
+ }
3196
+ catch (error) {
3197
+ 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)) {
3198
+ const { WebSessionManager } = await Promise.resolve().then(function () { return webSessionManager; });
3199
+ WebSessionManager.clearSession();
3200
+ }
3201
+ return false;
3202
+ }
3203
+ finally {
3204
+ refreshInFlight.delete(key);
3205
+ }
3206
+ })();
3207
+ refreshInFlight.set(key, refreshPromise);
3208
+ return refreshPromise;
3209
+ }
3145
3210
  async function makeApiRequest(method, urlPath, data, _overrides) {
3146
3211
  var _a, _b, _c, _d, _e, _f;
3147
3212
  const config = await getConfig();
3213
+ let hasRetriedAfterServerSessionReset = false;
3214
+ const clearServerSession = async () => {
3215
+ if (!config.isServer)
3216
+ return;
3217
+ const { ServerSessionManager } = await Promise.resolve().then(function () { return serverSessionManager; });
3218
+ ServerSessionManager.instance.clearSession();
3219
+ };
3148
3220
  async function executeRequest() {
3149
3221
  const authHeader = await createAuthHeader(config.isServer);
3150
3222
  const headers = Object.assign({ "Content-Type": "application/json", "X-Public-App-Id": config.appId, "X-App-Id": config.appId }, authHeader);
@@ -3176,19 +3248,20 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
3176
3248
  catch (error) {
3177
3249
  if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 401) {
3178
3250
  try {
3179
- const refreshToken = await getRefreshToken(config.isServer);
3180
- if (!refreshToken) {
3181
- throw new Error("No refresh token found");
3182
- }
3183
- const refreshData = await refreshSession(refreshToken);
3184
- if (refreshData &&
3185
- refreshData.idToken &&
3186
- refreshData.accessToken) {
3187
- updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken);
3251
+ const refreshed = await refreshAuthSessionOnce(config.appId, config.isServer);
3252
+ if (!refreshed) {
3253
+ throw new Error("Unable to refresh auth session");
3188
3254
  }
3189
3255
  return await executeRequest();
3190
3256
  }
3191
- catch (refreshError) {
3257
+ catch (_refreshError) {
3258
+ // Server-side fallback: clear cached session and retry once to force
3259
+ // createSession() with a fresh Cognito session.
3260
+ if (config.isServer && !hasRetriedAfterServerSessionReset) {
3261
+ hasRetriedAfterServerSessionReset = true;
3262
+ await clearServerSession();
3263
+ return await executeRequest();
3264
+ }
3192
3265
  throw error;
3193
3266
  }
3194
3267
  }
@@ -3279,8 +3352,10 @@ async function get(path, opts = {}) {
3279
3352
  if (!normalizedPath || normalizedPath.length === 0) {
3280
3353
  return new Error("Invalid path provided.");
3281
3354
  }
3282
- // Create cache key combining path and prompt
3283
- const cacheKey = `${normalizedPath}:${opts.prompt || ''}`;
3355
+ // Create cache key combining path, prompt, includeSubPaths, and shape
3356
+ const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
3357
+ const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
3358
+ const cacheKey = `${normalizedPath}:${opts.prompt || ''}${includeSubPathsKey}:${shapeKey}`;
3284
3359
  const now = Date.now();
3285
3360
  // Check for valid cache entry if not bypassing cache
3286
3361
  if (!opts.bypassCache && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
@@ -3301,15 +3376,20 @@ async function get(path, opts = {}) {
3301
3376
  // Cache miss or bypass - proceed with API request
3302
3377
  const pathIsDocument = normalizedPath.split("/").length % 2 === 0;
3303
3378
  let response;
3379
+ // Build common query params
3380
+ const includeSubPathsParam = opts.includeSubPaths ? '&includeSubPaths=true' : '';
3381
+ const shapeParam = opts.shape ? `&shape=${encodeURIComponent(JSON.stringify(opts.shape))}` : '';
3304
3382
  if (pathIsDocument) {
3305
3383
  const itemId = encodeURIComponent(normalizedPath);
3306
- const apiPath = `items/${itemId}`;
3384
+ // For documents, query params go after the path
3385
+ const queryParams = [includeSubPathsParam, shapeParam].filter(p => p).join('');
3386
+ const apiPath = queryParams ? `items/${itemId}?${queryParams.substring(1)}` : `items/${itemId}`;
3307
3387
  response = await makeApiRequest('GET', apiPath, null, opts._overrides);
3308
3388
  }
3309
3389
  else {
3310
3390
  const path = encodeURIComponent(normalizedPath);
3311
3391
  const promptQueryParam = (opts === null || opts === void 0 ? void 0 : opts.prompt) ? `&prompt=${btoa(opts.prompt)}` : "";
3312
- const apiPath = `items?path=${path}${promptQueryParam}`;
3392
+ const apiPath = `items?path=${path}${promptQueryParam}${includeSubPathsParam}${shapeParam}`;
3313
3393
  response = await makeApiRequest('GET', apiPath, null, opts._overrides);
3314
3394
  }
3315
3395
  // Cache the response (unless bypassing cache)
@@ -3713,21 +3793,34 @@ const responseCache = new Map();
3713
3793
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
3714
3794
  const TOKEN_REFRESH_BUFFER = 5 * 60 * 1000; // Refresh token 5 minutes before expiry
3715
3795
  // ============ WebSocket Config ============
3796
+ const BASE_MIN_RECONNECT_DELAY_MS = 1000;
3797
+ const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
3798
+ const MAX_RECONNECT_DELAY_MS = 300000;
3799
+ const RECONNECT_DELAY_GROW_FACTOR = 1.8;
3800
+ const MIN_BROWSER_RECONNECT_INTERVAL_MS = 30000;
3716
3801
  const WS_CONFIG = {
3717
- maxRetries: 10,
3718
- minReconnectionDelay: 1000,
3719
- maxReconnectionDelay: 30000,
3720
- reconnectionDelayGrowFactor: 1.3,
3721
- connectionTimeout: 4000,
3802
+ // Keep retrying indefinitely so long outages recover without page refresh.
3803
+ maxRetries: Infinity,
3804
+ // Conservative starting delay with jitter spreads reconnects across clients.
3805
+ minReconnectionDelay: BASE_MIN_RECONNECT_DELAY_MS + Math.floor(Math.random() * MIN_RECONNECT_DELAY_JITTER_MS),
3806
+ // Stretch backoff to 5 minutes during prolonged outages to protect backend pools.
3807
+ maxReconnectionDelay: MAX_RECONNECT_DELAY_MS,
3808
+ // Moderate growth to avoid rapid retry bursts while still converging to max delay.
3809
+ reconnectionDelayGrowFactor: RECONNECT_DELAY_GROW_FACTOR,
3810
+ // Give handshakes more time before considering the attempt failed.
3811
+ connectionTimeout: 10000,
3722
3812
  };
3723
3813
  const WS_V2_PATH = '/ws/v2';
3814
+ let browserReconnectHooksAttached = false;
3815
+ let lastBrowserTriggeredReconnectAt = 0;
3724
3816
  // ============ Helper Functions ============
3725
3817
  function generateSubscriptionId() {
3726
3818
  return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
3727
3819
  }
3728
- function getCacheKey(path, prompt) {
3820
+ function getCacheKey(path, prompt, shape) {
3729
3821
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
3730
- return `${normalizedPath}:${prompt || 'default'}`;
3822
+ const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
3823
+ return `${normalizedPath}:${prompt || 'default'}:${shapeKey}`;
3731
3824
  }
3732
3825
  function isTokenExpired(token) {
3733
3826
  try {
@@ -3789,7 +3882,7 @@ async function getFreshAuthToken(isServer) {
3789
3882
  }
3790
3883
  const refreshData = await refreshSession(refreshToken);
3791
3884
  if (refreshData && refreshData.idToken && refreshData.accessToken) {
3792
- updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken);
3885
+ await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer);
3793
3886
  return refreshData.idToken;
3794
3887
  }
3795
3888
  }
@@ -3798,8 +3891,54 @@ async function getFreshAuthToken(isServer) {
3798
3891
  }
3799
3892
  return currentToken;
3800
3893
  }
3894
+ function hasDisconnectedActiveConnections() {
3895
+ for (const connection of connections.values()) {
3896
+ if (!connection.ws) {
3897
+ continue;
3898
+ }
3899
+ // Only nudge reconnection when socket is fully closed.
3900
+ // If ReconnectingWebSocket is already in CONNECTING/backoff state, don't interfere.
3901
+ if (connection.ws.readyState === WS_READY_STATE_CLOSED) {
3902
+ return true;
3903
+ }
3904
+ }
3905
+ return false;
3906
+ }
3907
+ function maybeReconnectFromBrowserSignal() {
3908
+ const now = Date.now();
3909
+ if (now - lastBrowserTriggeredReconnectAt < MIN_BROWSER_RECONNECT_INTERVAL_MS) {
3910
+ return;
3911
+ }
3912
+ if (!hasDisconnectedActiveConnections()) {
3913
+ return;
3914
+ }
3915
+ lastBrowserTriggeredReconnectAt = now;
3916
+ reconnectWithNewAuthV2().catch((error) => {
3917
+ console.error('[WS v2] Browser-triggered reconnect failed:', error);
3918
+ });
3919
+ }
3920
+ function attachBrowserReconnectHooksOnce() {
3921
+ if (browserReconnectHooksAttached) {
3922
+ return;
3923
+ }
3924
+ if (typeof window === 'undefined') {
3925
+ return;
3926
+ }
3927
+ browserReconnectHooksAttached = true;
3928
+ window.addEventListener('online', () => {
3929
+ maybeReconnectFromBrowserSignal();
3930
+ });
3931
+ if (typeof document !== 'undefined') {
3932
+ document.addEventListener('visibilitychange', () => {
3933
+ if (document.visibilityState === 'visible') {
3934
+ maybeReconnectFromBrowserSignal();
3935
+ }
3936
+ });
3937
+ }
3938
+ }
3801
3939
  // ============ Connection Management ============
3802
3940
  async function getOrCreateConnection(appId, isServer) {
3941
+ attachBrowserReconnectHooksOnce();
3803
3942
  let connection = connections.get(appId);
3804
3943
  if (connection && connection.ws) {
3805
3944
  return connection;
@@ -3892,13 +4031,15 @@ function handleServerMessage(connection, message) {
3892
4031
  case 'subscribed': {
3893
4032
  const subscription = connection.subscriptions.get(message.subscriptionId);
3894
4033
  if (subscription) {
3895
- // Update cache
3896
- const cacheKey = getCacheKey(subscription.path, subscription.prompt);
3897
- responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
3898
- // Store last data
3899
- subscription.lastData = message.data;
3900
- // Notify callbacks
3901
- notifyCallbacks(subscription, message.data);
4034
+ // A data update can race ahead of subscribed during reconnect.
4035
+ // If we already received data for this subscription, treat subscribed
4036
+ // as an ack only and avoid regressing to an older snapshot.
4037
+ if (subscription.lastData === undefined) {
4038
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape);
4039
+ responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
4040
+ subscription.lastData = message.data;
4041
+ notifyCallbacks(subscription, message.data);
4042
+ }
3902
4043
  }
3903
4044
  // Resolve pending subscription promise
3904
4045
  const pending = connection.pendingSubscriptions.get(message.subscriptionId);
@@ -3921,7 +4062,7 @@ function handleServerMessage(connection, message) {
3921
4062
  const subscription = connection.subscriptions.get(message.subscriptionId);
3922
4063
  if (subscription) {
3923
4064
  // Update cache
3924
- const cacheKey = getCacheKey(subscription.path, subscription.prompt);
4065
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape);
3925
4066
  responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
3926
4067
  // Store last data
3927
4068
  subscription.lastData = message.data;
@@ -3964,6 +4105,7 @@ function notifyCallbacks(subscription, data) {
3964
4105
  }
3965
4106
  // WebSocket readyState constants
3966
4107
  const WS_READY_STATE_OPEN = 1;
4108
+ const WS_READY_STATE_CLOSED = 3;
3967
4109
  function sendSubscribe(connection, subscription) {
3968
4110
  if (!connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN) {
3969
4111
  return;
@@ -3974,6 +4116,9 @@ function sendSubscribe(connection, subscription) {
3974
4116
  path: subscription.path,
3975
4117
  prompt: subscription.prompt ? btoa(subscription.prompt) : undefined,
3976
4118
  includeSubPaths: subscription.includeSubPaths,
4119
+ shape: subscription.shape && Object.keys(subscription.shape).length > 0
4120
+ ? subscription.shape
4121
+ : undefined,
3977
4122
  };
3978
4123
  try {
3979
4124
  connection.ws.send(JSON.stringify(message));
@@ -4005,7 +4150,7 @@ function sendUnsubscribe(connection, subscriptionId) {
4005
4150
  async function subscribeV2(path, subscriptionOptions) {
4006
4151
  const config = await getConfig();
4007
4152
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
4008
- const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt);
4153
+ const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape);
4009
4154
  // Deliver cached data immediately if available
4010
4155
  const cachedEntry = responseCache.get(cacheKey);
4011
4156
  if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
@@ -4016,10 +4161,12 @@ async function subscribeV2(path, subscriptionOptions) {
4016
4161
  }
4017
4162
  // Get or create connection for this appId
4018
4163
  const connection = await getOrCreateConnection(config.appId, config.isServer);
4019
- // Check if we already have a subscription for this path+prompt
4164
+ // Check if we already have a subscription for this path+prompt+shape
4165
+ const shapeKey = subscriptionOptions.shape ? JSON.stringify(subscriptionOptions.shape) : '';
4020
4166
  let existingSubscription;
4021
4167
  for (const sub of connection.subscriptions.values()) {
4022
- if (sub.path === normalizedPath && sub.prompt === subscriptionOptions.prompt) {
4168
+ const subShapeKey = sub.shape ? JSON.stringify(sub.shape) : '';
4169
+ if (sub.path === normalizedPath && sub.prompt === subscriptionOptions.prompt && subShapeKey === shapeKey) {
4023
4170
  existingSubscription = sub;
4024
4171
  break;
4025
4172
  }
@@ -4044,6 +4191,7 @@ async function subscribeV2(path, subscriptionOptions) {
4044
4191
  subscriptionId,
4045
4192
  path: normalizedPath,
4046
4193
  prompt: subscriptionOptions.prompt,
4194
+ shape: subscriptionOptions.shape,
4047
4195
  includeSubPaths: false,
4048
4196
  callbacks: [subscriptionOptions],
4049
4197
  lastData: undefined,