@pooflabs/core 0.0.20 → 0.0.22

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