@pooflabs/core 0.0.21 → 0.0.23

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/index.js CHANGED
@@ -3468,6 +3468,10 @@ async function setMany(many, options) {
3468
3468
  lastTxSignature = transactionResult.transactionSignature;
3469
3469
  signedTransaction = transactionResult.signedTransaction;
3470
3470
  }
3471
+ // Sync items after all transactions are confirmed
3472
+ // Wait for 1.5 seconds to ensure all transactions are confirmed
3473
+ await new Promise(resolve => setTimeout(resolve, 1500));
3474
+ await syncItems(many.map(m => m.path));
3471
3475
  // TODO: Should we wait here or do the optimistic subscription updates like below?
3472
3476
  return Object.assign(Object.assign({}, documents.map(d => d.document)), { transactionId: lastTxSignature, signedTransaction: signedTransaction });
3473
3477
  }
@@ -3679,40 +3683,102 @@ async function signAndSubmitTransaction(transaction, feePayer) {
3679
3683
  }
3680
3684
  return config.authProvider.signAndSubmitTransaction(transaction, feePayer);
3681
3685
  }
3686
+ async function syncItems(paths) {
3687
+ try {
3688
+ const response = await makeApiRequest('PUT', 'items/sync', { paths }, undefined);
3689
+ return response.data;
3690
+ }
3691
+ catch (error) {
3692
+ throw error;
3693
+ }
3694
+ }
3682
3695
 
3683
- const activeConnections = {};
3684
- const responseCache = {};
3685
- const CACHE_TTL = 5 * 60 * 1000; // 5 minutes cache TTL
3686
- // Check if a JWT token is expired (with 60 second buffer)
3696
+ /**
3697
+ * WebSocket v2 Subscription Manager
3698
+ *
3699
+ * This module implements multiplexed subscriptions over a single WebSocket connection.
3700
+ * It maintains the same external API as v1 but uses the new v2 protocol internally.
3701
+ */
3702
+ // ============ Global State ============
3703
+ // One connection per appId
3704
+ const connections = new Map();
3705
+ const responseCache = new Map();
3706
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
3707
+ const TOKEN_REFRESH_BUFFER = 5 * 60 * 1000; // Refresh token 5 minutes before expiry
3708
+ // ============ WebSocket Config ============
3709
+ const WS_CONFIG = {
3710
+ maxRetries: 10,
3711
+ minReconnectionDelay: 1000,
3712
+ maxReconnectionDelay: 30000,
3713
+ reconnectionDelayGrowFactor: 1.3,
3714
+ connectionTimeout: 4000,
3715
+ };
3716
+ const WS_V2_PATH = '/ws/v2';
3717
+ // ============ Helper Functions ============
3718
+ function generateSubscriptionId() {
3719
+ return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
3720
+ }
3721
+ function getCacheKey(path, prompt) {
3722
+ const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
3723
+ return `${normalizedPath}:${prompt || 'default'}`;
3724
+ }
3687
3725
  function isTokenExpired(token) {
3688
3726
  try {
3689
3727
  const payload = JSON.parse(atob(token.split('.')[1]));
3690
- const expirationTime = payload.exp * 1000; // Convert to milliseconds
3728
+ const expirationTime = payload.exp * 1000;
3691
3729
  const currentTime = Date.now();
3692
- // Add 60 second buffer to refresh before actual expiration
3693
- return currentTime > (expirationTime - 60000);
3730
+ return currentTime > (expirationTime - 60000); // 60 second buffer
3694
3731
  }
3695
3732
  catch (error) {
3696
- console.error('Error checking token expiration:', error);
3697
- return true; // Assume expired if we can't parse
3733
+ console.error('[WS v2] Error checking token expiration:', error);
3734
+ return true;
3735
+ }
3736
+ }
3737
+ function getTokenExpirationTime(token) {
3738
+ try {
3739
+ const payload = JSON.parse(atob(token.split('.')[1]));
3740
+ return payload.exp ? payload.exp * 1000 : null;
3741
+ }
3742
+ catch (_a) {
3743
+ return null;
3698
3744
  }
3699
3745
  }
3700
- // Get a fresh auth token, refreshing if necessary
3746
+ function scheduleTokenRefresh(connection, token) {
3747
+ // Clear any existing timer
3748
+ if (connection.tokenRefreshTimer) {
3749
+ clearTimeout(connection.tokenRefreshTimer);
3750
+ connection.tokenRefreshTimer = null;
3751
+ }
3752
+ if (!token) {
3753
+ return; // No token = unauthenticated, no refresh needed
3754
+ }
3755
+ const expirationTime = getTokenExpirationTime(token);
3756
+ if (!expirationTime) {
3757
+ return; // Can't parse expiration
3758
+ }
3759
+ const refreshTime = expirationTime - TOKEN_REFRESH_BUFFER;
3760
+ const delay = refreshTime - Date.now();
3761
+ if (delay <= 0) {
3762
+ // Token already expired or about to expire, reconnect immediately
3763
+ reconnectWithNewAuthV2();
3764
+ return;
3765
+ }
3766
+ connection.tokenRefreshTimer = setTimeout(() => {
3767
+ reconnectWithNewAuthV2();
3768
+ }, delay);
3769
+ }
3701
3770
  async function getFreshAuthToken(isServer) {
3702
3771
  const currentToken = await getIdToken(isServer);
3703
3772
  if (!currentToken) {
3704
3773
  return null;
3705
3774
  }
3706
- // If token is not expired, return it
3707
3775
  if (!isTokenExpired(currentToken)) {
3708
3776
  return currentToken;
3709
3777
  }
3710
- // Token is expired, try to refresh
3711
3778
  try {
3712
3779
  const refreshToken = await getRefreshToken(isServer);
3713
3780
  if (!refreshToken) {
3714
- console.warn('No refresh token available for WebSocket reconnection');
3715
- return currentToken; // Return expired token, backend will reject
3781
+ return currentToken;
3716
3782
  }
3717
3783
  const refreshData = await refreshSession(refreshToken);
3718
3784
  if (refreshData && refreshData.idToken && refreshData.accessToken) {
@@ -3721,177 +3787,507 @@ async function getFreshAuthToken(isServer) {
3721
3787
  }
3722
3788
  }
3723
3789
  catch (error) {
3724
- console.error('Error refreshing token for WebSocket:', error);
3790
+ console.error('[WS v2] Error refreshing token:', error);
3725
3791
  }
3726
- return currentToken; // Return current token even if refresh failed
3792
+ return currentToken;
3727
3793
  }
3728
- const WS_CONFIG = {
3729
- maxRetries: 10,
3730
- minReconnectionDelay: 1000,
3731
- maxReconnectionDelay: 30000,
3732
- reconnectionDelayGrowFactor: 1.3,
3733
- };
3734
- async function subscribe(path, subscriptionOptions) {
3735
- var _a;
3736
- const config = await getConfig();
3737
- const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
3738
- // Create unique connection key that includes prompt to support multiple subscriptions with different prompts
3739
- const connectionKey = `${normalizedPath}:${subscriptionOptions.prompt || 'default'}`;
3740
- // Deliver cached data immediately if available and not expired
3741
- const cachedEntry = responseCache[connectionKey];
3742
- if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
3743
- setTimeout(() => {
3744
- var _a;
3745
- (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, cachedEntry.data);
3746
- }, 0);
3747
- }
3748
- // If there's already an active connection with the same prompt
3749
- if (activeConnections[connectionKey]) {
3750
- activeConnections[connectionKey].subscriptions.push(subscriptionOptions);
3751
- return async () => await removeSubscription(connectionKey, subscriptionOptions);
3794
+ // ============ Connection Management ============
3795
+ async function getOrCreateConnection(appId, isServer) {
3796
+ let connection = connections.get(appId);
3797
+ if (connection && connection.ws) {
3798
+ return connection;
3752
3799
  }
3753
- // URL provider function that generates fresh URLs with refreshed tokens on each reconnect
3800
+ // Create new connection
3801
+ connection = {
3802
+ ws: null,
3803
+ subscriptions: new Map(),
3804
+ pendingSubscriptions: new Map(),
3805
+ pendingUnsubscriptions: new Map(),
3806
+ isConnecting: false,
3807
+ isConnected: false,
3808
+ appId,
3809
+ tokenRefreshTimer: null,
3810
+ };
3811
+ connections.set(appId, connection);
3812
+ // URL provider for reconnection with fresh tokens
3754
3813
  const urlProvider = async () => {
3814
+ const config = await getConfig();
3755
3815
  const wsUrl = new URL(config.wsApiUrl);
3756
- // Logic for console - preserve existing custom app ID logic
3816
+ // Always use v2 path
3817
+ wsUrl.pathname = WS_V2_PATH;
3818
+ // Set appId
3757
3819
  if (typeof window !== 'undefined' && window.CUSTOM_TAROBASE_APP_ID_HEADER) {
3758
- const customAppId = window.CUSTOM_TAROBASE_APP_ID_HEADER;
3759
- if (customAppId) {
3760
- wsUrl.searchParams.append('appId', customAppId);
3761
- }
3820
+ wsUrl.searchParams.append('appId', window.CUSTOM_TAROBASE_APP_ID_HEADER);
3762
3821
  }
3763
3822
  else {
3764
3823
  wsUrl.searchParams.append('appId', config.appId);
3765
3824
  }
3766
- wsUrl.searchParams.append('path', normalizedPath);
3767
- wsUrl.searchParams.append('_t', Date.now().toString()); // Add timestamp to prevent connection reuse issues
3768
- // Get fresh auth token (will refresh if expired)
3769
- const authToken = await getFreshAuthToken(config.isServer);
3825
+ // Add timestamp to prevent connection reuse issues
3826
+ wsUrl.searchParams.append('_t', Date.now().toString());
3827
+ // Add auth token if available
3828
+ const authToken = await getFreshAuthToken(isServer);
3770
3829
  if (authToken) {
3771
3830
  wsUrl.searchParams.append('authorization', authToken);
3772
3831
  }
3773
- if (subscriptionOptions.prompt) {
3774
- wsUrl.searchParams.append('prompt', btoa(subscriptionOptions.prompt));
3775
- }
3776
3832
  return wsUrl.toString();
3777
3833
  };
3778
- // Create a new WebSocket connection with urlProvider for token refresh on reconnect
3779
- const ws = new ReconnectingWebSocket(urlProvider, [], Object.assign(Object.assign({}, WS_CONFIG), { connectionTimeout: 4000, debug: typeof process !== 'undefined' && ((_a = process.env) === null || _a === void 0 ? void 0 : _a.NODE_ENV) === 'development' }));
3780
- // Create connection object with connecting flag
3781
- activeConnections[connectionKey] = {
3782
- ws,
3783
- subscriptions: [subscriptionOptions],
3784
- isConnecting: true,
3785
- };
3834
+ // Create WebSocket connection
3835
+ connection.isConnecting = true;
3836
+ const ws = new ReconnectingWebSocket(urlProvider, [], Object.assign(Object.assign({}, WS_CONFIG), { debug: false }));
3837
+ connection.ws = ws;
3838
+ // Handle connection open
3786
3839
  ws.addEventListener('open', () => {
3787
- activeConnections[connectionKey].isConnecting = false;
3788
- // Fetch initial data if needed (currently commented out)
3840
+ connection.isConnecting = false;
3841
+ connection.isConnected = true;
3842
+ // Schedule token refresh before expiry
3789
3843
  (async () => {
3790
- try {
3791
- // For ID Paths, we should fetch the initial data from the api.
3792
- // This helps us determine if it's null or just loading.
3793
- if (normalizedPath.split('/').length % 2 === 0) {
3794
- const initialData = await get(path, { prompt: subscriptionOptions.prompt });
3795
- if (initialData) {
3796
- updateCache(connectionKey, initialData);
3797
- notifySubscribers(connectionKey, initialData);
3798
- }
3799
- }
3800
- }
3801
- catch (error) {
3802
- notifyError(normalizedPath, error);
3803
- }
3844
+ const token = await getIdToken(isServer);
3845
+ scheduleTokenRefresh(connection, token);
3804
3846
  })();
3847
+ // Re-subscribe to all existing subscriptions after reconnect
3848
+ for (const sub of connection.subscriptions.values()) {
3849
+ sendSubscribe(connection, sub);
3850
+ }
3805
3851
  });
3852
+ // Handle incoming messages
3806
3853
  ws.addEventListener('message', (event) => {
3807
3854
  try {
3808
- let data = JSON.parse(event.data);
3809
- let value = null;
3810
- if (data && data.length && data.length > 0 && data[0] && data[0].value) {
3811
- value = data[0].value;
3812
- }
3813
- else if (data && data.length) {
3814
- // If we had a length but didn't have a proper value or element in there, then this is an empty message, and we should just send null.
3815
- value = null;
3816
- }
3817
- else {
3818
- value = data.value;
3819
- }
3820
- updateCache(connectionKey, value);
3821
- notifySubscribers(connectionKey, value);
3855
+ const message = JSON.parse(event.data);
3856
+ handleServerMessage(connection, message);
3822
3857
  }
3823
3858
  catch (error) {
3824
- notifyError(connectionKey, error);
3859
+ console.error('[WS v2] Error parsing message:', error);
3825
3860
  }
3826
3861
  });
3862
+ // Handle errors
3827
3863
  ws.addEventListener('error', (event) => {
3828
- console.error(`WebSocket error for path: ${connectionKey}`, event);
3829
- notifyError(connectionKey, event);
3864
+ console.error('[WS v2] WebSocket error:', event);
3865
+ // Reject all pending subscriptions
3866
+ for (const [id, pending] of connection.pendingSubscriptions) {
3867
+ pending.reject(new Error('WebSocket error'));
3868
+ connection.pendingSubscriptions.delete(id);
3869
+ }
3830
3870
  });
3871
+ // Handle close
3831
3872
  ws.addEventListener('close', () => {
3832
- // Connection will be recreated on next subscription if needed
3873
+ connection.isConnected = false;
3874
+ // Clear token refresh timer
3875
+ if (connection.tokenRefreshTimer) {
3876
+ clearTimeout(connection.tokenRefreshTimer);
3877
+ connection.tokenRefreshTimer = null;
3878
+ }
3833
3879
  });
3834
- return async () => await removeSubscription(connectionKey, subscriptionOptions);
3880
+ return connection;
3835
3881
  }
3836
- function updateCache(path, data) {
3837
- responseCache[path] = {
3838
- data,
3839
- timestamp: Date.now()
3840
- };
3841
- }
3842
- function notifySubscribers(connectionKey, data) {
3882
+ function handleServerMessage(connection, message) {
3843
3883
  var _a;
3844
- (_a = activeConnections[connectionKey]) === null || _a === void 0 ? void 0 : _a.subscriptions.forEach(sub => {
3845
- var _a;
3846
- (_a = sub.onData) === null || _a === void 0 ? void 0 : _a.call(sub, data);
3847
- });
3884
+ switch (message.type) {
3885
+ case 'subscribed': {
3886
+ const subscription = connection.subscriptions.get(message.subscriptionId);
3887
+ if (subscription) {
3888
+ // Update cache
3889
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt);
3890
+ responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
3891
+ // Store last data
3892
+ subscription.lastData = message.data;
3893
+ // Notify callbacks
3894
+ notifyCallbacks(subscription, message.data);
3895
+ }
3896
+ // Resolve pending subscription promise
3897
+ const pending = connection.pendingSubscriptions.get(message.subscriptionId);
3898
+ if (pending) {
3899
+ pending.resolve();
3900
+ connection.pendingSubscriptions.delete(message.subscriptionId);
3901
+ }
3902
+ break;
3903
+ }
3904
+ case 'unsubscribed': {
3905
+ // Resolve pending unsubscription promise
3906
+ const pending = connection.pendingUnsubscriptions.get(message.subscriptionId);
3907
+ if (pending) {
3908
+ pending.resolve();
3909
+ connection.pendingUnsubscriptions.delete(message.subscriptionId);
3910
+ }
3911
+ break;
3912
+ }
3913
+ case 'data': {
3914
+ const subscription = connection.subscriptions.get(message.subscriptionId);
3915
+ if (subscription) {
3916
+ // Update cache
3917
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt);
3918
+ responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
3919
+ // Store last data
3920
+ subscription.lastData = message.data;
3921
+ // Notify callbacks
3922
+ notifyCallbacks(subscription, message.data);
3923
+ }
3924
+ break;
3925
+ }
3926
+ case 'error': {
3927
+ console.error('[WS v2] Server error:', message.code, message.message);
3928
+ if (message.subscriptionId) {
3929
+ // Reject pending subscription if this is a subscription error
3930
+ const pending = connection.pendingSubscriptions.get(message.subscriptionId);
3931
+ if (pending) {
3932
+ pending.reject(new Error(`${message.code}: ${message.message}`));
3933
+ connection.pendingSubscriptions.delete(message.subscriptionId);
3934
+ }
3935
+ // Notify error callbacks for this subscription
3936
+ const subscription = connection.subscriptions.get(message.subscriptionId);
3937
+ if (subscription) {
3938
+ for (const callback of subscription.callbacks) {
3939
+ (_a = callback.onError) === null || _a === void 0 ? void 0 : _a.call(callback, new Error(`${message.code}: ${message.message}`));
3940
+ }
3941
+ }
3942
+ }
3943
+ break;
3944
+ }
3945
+ }
3848
3946
  }
3849
- function notifyError(connectionKey, error) {
3947
+ function notifyCallbacks(subscription, data) {
3850
3948
  var _a;
3851
- (_a = activeConnections[connectionKey]) === null || _a === void 0 ? void 0 : _a.subscriptions.forEach(sub => {
3852
- var _a;
3853
- (_a = sub.onError) === null || _a === void 0 ? void 0 : _a.call(sub, error);
3854
- });
3949
+ for (const callback of subscription.callbacks) {
3950
+ try {
3951
+ (_a = callback.onData) === null || _a === void 0 ? void 0 : _a.call(callback, data);
3952
+ }
3953
+ catch (error) {
3954
+ console.error('[WS v2] Error in subscription callback:', error);
3955
+ }
3956
+ }
3855
3957
  }
3856
- async function removeSubscription(connectionKey, subscription) {
3857
- const connection = activeConnections[connectionKey];
3858
- if (!connection)
3958
+ // WebSocket readyState constants
3959
+ const WS_READY_STATE_OPEN = 1;
3960
+ function sendSubscribe(connection, subscription) {
3961
+ if (!connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN) {
3859
3962
  return;
3860
- // Remove the specific subscription
3861
- connection.subscriptions = connection.subscriptions.filter(sub => sub !== subscription);
3862
- // If there are still subscriptions, resolve immediately
3863
- if (connection.subscriptions.length > 0) {
3963
+ }
3964
+ const message = {
3965
+ type: 'subscribe',
3966
+ subscriptionId: subscription.subscriptionId,
3967
+ path: subscription.path,
3968
+ prompt: subscription.prompt ? btoa(subscription.prompt) : undefined,
3969
+ includeSubPaths: subscription.includeSubPaths,
3970
+ };
3971
+ try {
3972
+ connection.ws.send(JSON.stringify(message));
3973
+ }
3974
+ catch (error) {
3975
+ console.error('[WS v2] Error sending subscribe message:', error);
3976
+ }
3977
+ }
3978
+ function sendUnsubscribe(connection, subscriptionId) {
3979
+ if (!connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN) {
3864
3980
  return;
3865
3981
  }
3866
- // No subscriptions left, close the WebSocket and wait for closure
3867
- const ws = connection.ws;
3868
- await new Promise((resolve) => {
3869
- ws.addEventListener('close', () => {
3870
- resolve();
3982
+ const message = {
3983
+ type: 'unsubscribe',
3984
+ subscriptionId,
3985
+ };
3986
+ try {
3987
+ connection.ws.send(JSON.stringify(message));
3988
+ }
3989
+ catch (error) {
3990
+ console.error('[WS v2] Error sending unsubscribe message:', error);
3991
+ }
3992
+ }
3993
+ // ============ Public API ============
3994
+ /**
3995
+ * Subscribe to data at a path using WebSocket v2.
3996
+ * Returns an unsubscribe function.
3997
+ */
3998
+ async function subscribeV2(path, subscriptionOptions) {
3999
+ const config = await getConfig();
4000
+ const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
4001
+ const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt);
4002
+ // Deliver cached data immediately if available
4003
+ const cachedEntry = responseCache.get(cacheKey);
4004
+ if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
4005
+ setTimeout(() => {
4006
+ var _a;
4007
+ (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, cachedEntry.data);
4008
+ }, 0);
4009
+ }
4010
+ // Get or create connection for this appId
4011
+ const connection = await getOrCreateConnection(config.appId, config.isServer);
4012
+ // Check if we already have a subscription for this path+prompt
4013
+ let existingSubscription;
4014
+ for (const sub of connection.subscriptions.values()) {
4015
+ if (sub.path === normalizedPath && sub.prompt === subscriptionOptions.prompt) {
4016
+ existingSubscription = sub;
4017
+ break;
4018
+ }
4019
+ }
4020
+ if (existingSubscription) {
4021
+ // Add callback to existing subscription
4022
+ existingSubscription.callbacks.push(subscriptionOptions);
4023
+ // Deliver last known data immediately
4024
+ if (existingSubscription.lastData !== undefined && subscriptionOptions.onData) {
4025
+ setTimeout(() => {
4026
+ var _a;
4027
+ (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, existingSubscription.lastData);
4028
+ }, 0);
4029
+ }
4030
+ return async () => {
4031
+ await removeCallbackFromSubscription(connection, existingSubscription.subscriptionId, subscriptionOptions);
4032
+ };
4033
+ }
4034
+ // Create new subscription
4035
+ const subscriptionId = generateSubscriptionId();
4036
+ const subscription = {
4037
+ subscriptionId,
4038
+ path: normalizedPath,
4039
+ prompt: subscriptionOptions.prompt,
4040
+ includeSubPaths: false,
4041
+ callbacks: [subscriptionOptions],
4042
+ lastData: undefined,
4043
+ };
4044
+ connection.subscriptions.set(subscriptionId, subscription);
4045
+ // Send subscribe message if connected
4046
+ if (connection.isConnected) {
4047
+ // Create a promise to wait for subscription confirmation
4048
+ const subscriptionPromise = new Promise((resolve, reject) => {
4049
+ connection.pendingSubscriptions.set(subscriptionId, { resolve, reject });
4050
+ // Timeout after 10 seconds
4051
+ setTimeout(() => {
4052
+ if (connection.pendingSubscriptions.has(subscriptionId)) {
4053
+ connection.pendingSubscriptions.delete(subscriptionId);
4054
+ reject(new Error('Subscription timeout'));
4055
+ }
4056
+ }, 10000);
3871
4057
  });
3872
- ws.addEventListener('error', (err) => {
3873
- console.error(`WebSocket closure error for connection: ${connectionKey}`, err);
3874
- resolve(); // Resolve even on error to avoid hanging
4058
+ sendSubscribe(connection, subscription);
4059
+ try {
4060
+ await subscriptionPromise;
4061
+ }
4062
+ catch (error) {
4063
+ // Remove subscription on error
4064
+ connection.subscriptions.delete(subscriptionId);
4065
+ throw error;
4066
+ }
4067
+ }
4068
+ // Return unsubscribe function
4069
+ return async () => {
4070
+ await removeCallbackFromSubscription(connection, subscriptionId, subscriptionOptions);
4071
+ };
4072
+ }
4073
+ async function removeCallbackFromSubscription(connection, subscriptionId, callback) {
4074
+ const subscription = connection.subscriptions.get(subscriptionId);
4075
+ if (!subscription) {
4076
+ return;
4077
+ }
4078
+ // Remove this callback
4079
+ subscription.callbacks = subscription.callbacks.filter(cb => cb !== callback);
4080
+ // If there are still callbacks, don't unsubscribe
4081
+ if (subscription.callbacks.length > 0) {
4082
+ return;
4083
+ }
4084
+ // No more callbacks, unsubscribe from server
4085
+ connection.subscriptions.delete(subscriptionId);
4086
+ if (connection.isConnected) {
4087
+ // Create a promise to wait for unsubscription confirmation
4088
+ const unsubscribePromise = new Promise((resolve, reject) => {
4089
+ connection.pendingUnsubscriptions.set(subscriptionId, { resolve, reject });
4090
+ // Timeout after 5 seconds
4091
+ setTimeout(() => {
4092
+ if (connection.pendingUnsubscriptions.has(subscriptionId)) {
4093
+ connection.pendingUnsubscriptions.delete(subscriptionId);
4094
+ resolve(); // Don't throw on unsubscribe timeout
4095
+ }
4096
+ }, 5000);
3875
4097
  });
3876
- ws.close();
3877
- });
3878
- // Cleanup after closure is confirmed
3879
- delete activeConnections[connectionKey];
4098
+ sendUnsubscribe(connection, subscriptionId);
4099
+ await unsubscribePromise;
4100
+ }
4101
+ // If no more subscriptions, close connection
4102
+ if (connection.subscriptions.size === 0 && connection.ws) {
4103
+ // Clear token refresh timer
4104
+ if (connection.tokenRefreshTimer) {
4105
+ clearTimeout(connection.tokenRefreshTimer);
4106
+ connection.tokenRefreshTimer = null;
4107
+ }
4108
+ connection.ws.close();
4109
+ connection.ws = null;
4110
+ connections.delete(connection.appId);
4111
+ }
4112
+ }
4113
+ /**
4114
+ * Close all v2 subscriptions.
4115
+ */
4116
+ async function closeAllSubscriptionsV2() {
4117
+ const closePromises = [];
4118
+ for (const [appId, connection] of connections) {
4119
+ // Clear token refresh timer
4120
+ if (connection.tokenRefreshTimer) {
4121
+ clearTimeout(connection.tokenRefreshTimer);
4122
+ connection.tokenRefreshTimer = null;
4123
+ }
4124
+ if (connection.ws) {
4125
+ closePromises.push(new Promise((resolve) => {
4126
+ connection.ws.addEventListener('close', () => {
4127
+ resolve();
4128
+ });
4129
+ connection.ws.close();
4130
+ }));
4131
+ }
4132
+ connections.delete(appId);
4133
+ }
4134
+ await Promise.all(closePromises);
4135
+ }
4136
+ /**
4137
+ * Clear the v2 cache.
4138
+ */
4139
+ function clearCacheV2(path) {
4140
+ if (path) {
4141
+ const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
4142
+ // Clear all cache entries that start with this path
4143
+ for (const key of responseCache.keys()) {
4144
+ if (key.startsWith(normalizedPath)) {
4145
+ responseCache.delete(key);
4146
+ }
4147
+ }
4148
+ }
4149
+ else {
4150
+ responseCache.clear();
4151
+ }
4152
+ }
4153
+ /**
4154
+ * Get cached data for a path (v2).
4155
+ */
4156
+ function getCachedDataV2(path, prompt) {
4157
+ const cacheKey = getCacheKey(path, prompt);
4158
+ const cachedEntry = responseCache.get(cacheKey);
4159
+ if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL) {
4160
+ return cachedEntry.data;
4161
+ }
4162
+ return null;
4163
+ }
4164
+ /**
4165
+ * Reconnect all v2 WebSocket connections with fresh authentication.
4166
+ * Call this when the user's auth state changes (login/logout).
4167
+ *
4168
+ * This will:
4169
+ * 1. Close existing connections
4170
+ * 2. Re-establish with new auth token
4171
+ * 3. Re-subscribe to all active paths
4172
+ *
4173
+ * Note: Existing subscriptions will receive new initial data after reconnection.
4174
+ */
4175
+ async function reconnectWithNewAuthV2() {
4176
+ // For each active connection
4177
+ for (const [appId, connection] of connections) {
4178
+ if (!connection.ws) {
4179
+ continue;
4180
+ }
4181
+ // Reject any pending subscriptions - they'll need to be retried after reconnect
4182
+ // This prevents hanging promises during auth transitions
4183
+ for (const [, pending] of connection.pendingSubscriptions) {
4184
+ pending.reject(new Error('Connection reconnecting due to auth change'));
4185
+ }
4186
+ connection.pendingSubscriptions.clear();
4187
+ // Resolve any pending unsubscriptions - connection is closing anyway
4188
+ for (const [, pending] of connection.pendingUnsubscriptions) {
4189
+ pending.resolve();
4190
+ }
4191
+ connection.pendingUnsubscriptions.clear();
4192
+ // Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
4193
+ // We use reconnect() which will close and re-open with fresh URL (including new token)
4194
+ try {
4195
+ // ReconnectingWebSocket.reconnect() closes current connection and opens a new one
4196
+ // The urlProvider will be called again, getting a fresh auth token
4197
+ connection.ws.reconnect();
4198
+ }
4199
+ catch (error) {
4200
+ console.error('[WS v2] Error reconnecting:', error);
4201
+ }
4202
+ }
4203
+ }
4204
+
4205
+ /**
4206
+ * WebSocket Subscription Module
4207
+ *
4208
+ * This module provides real-time data subscriptions using WebSocket v2 protocol
4209
+ * with multiplexed connections (one connection per client, multiple subscriptions).
4210
+ *
4211
+ * The external API remains unchanged - just use subscribe(path, options) as before.
4212
+ */
4213
+ /**
4214
+ * Subscribe to real-time updates for a path.
4215
+ *
4216
+ * @param path - The path to subscribe to (e.g., 'users/123' or 'posts')
4217
+ * @param subscriptionOptions - Options including onData and onError callbacks
4218
+ * @returns A function to unsubscribe
4219
+ *
4220
+ * @example
4221
+ * ```typescript
4222
+ * const unsubscribe = await subscribe('users/123', {
4223
+ * onData: (data) => console.log('User updated:', data),
4224
+ * onError: (error) => console.error('Subscription error:', error),
4225
+ * });
4226
+ *
4227
+ * // Later, to unsubscribe:
4228
+ * await unsubscribe();
4229
+ * ```
4230
+ */
4231
+ async function subscribe(path, subscriptionOptions) {
4232
+ return subscribeV2(path, subscriptionOptions);
4233
+ }
4234
+ /**
4235
+ * Close all active subscriptions.
4236
+ * Call this when cleaning up (e.g., on logout or component unmount).
4237
+ */
4238
+ async function closeAllSubscriptions() {
4239
+ return closeAllSubscriptionsV2();
4240
+ }
4241
+ /**
4242
+ * Clear the subscription cache.
4243
+ *
4244
+ * @param path - Optional path to clear. If not provided, clears all cached data.
4245
+ */
4246
+ function clearCache(path) {
4247
+ clearCacheV2(path);
4248
+ }
4249
+ /**
4250
+ * Get cached data for a path without making a network request.
4251
+ *
4252
+ * @param path - The path to get cached data for
4253
+ * @param prompt - Optional prompt that was used for the subscription
4254
+ * @returns The cached data, or null if not cached or expired
4255
+ */
4256
+ function getCachedData(path, prompt) {
4257
+ return getCachedDataV2(path, prompt);
4258
+ }
4259
+ /**
4260
+ * Reconnect all WebSocket connections with fresh authentication.
4261
+ * Call this when the user's auth state changes (login/logout).
4262
+ *
4263
+ * This will:
4264
+ * 1. Close existing connections
4265
+ * 2. Re-establish with new auth token
4266
+ * 3. Re-subscribe to all active paths
4267
+ *
4268
+ * Existing subscriptions will receive new initial data after reconnection.
4269
+ */
4270
+ async function reconnectWithNewAuth() {
4271
+ return reconnectWithNewAuthV2();
3880
4272
  }
3881
4273
 
3882
4274
  exports.ServerSessionManager = ServerSessionManager;
3883
4275
  exports.WebSessionManager = WebSessionManager;
3884
4276
  exports.buildSetDocumentsTransaction = buildSetDocumentsTransaction;
4277
+ exports.clearCache = clearCache;
4278
+ exports.closeAllSubscriptions = closeAllSubscriptions;
3885
4279
  exports.convertRemainingAccounts = convertRemainingAccounts;
3886
4280
  exports.createSessionWithPrivy = createSessionWithPrivy;
3887
4281
  exports.createSessionWithSignature = createSessionWithSignature;
3888
4282
  exports.genAuthNonce = genAuthNonce;
3889
4283
  exports.genSolanaMessage = genSolanaMessage;
3890
4284
  exports.get = get;
4285
+ exports.getCachedData = getCachedData;
3891
4286
  exports.getConfig = getConfig;
3892
4287
  exports.getFiles = getFiles;
3893
4288
  exports.getIdToken = getIdToken;
3894
4289
  exports.init = init;
4290
+ exports.reconnectWithNewAuth = reconnectWithNewAuth;
3895
4291
  exports.refreshSession = refreshSession;
3896
4292
  exports.runExpression = runExpression;
3897
4293
  exports.runExpressionMany = runExpressionMany;