@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/client/operations.d.ts +2 -1
- package/dist/client/subscription-v2.d.ts +47 -0
- package/dist/client/subscription.d.ts +54 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +522 -131
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +518 -132
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
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
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
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;
|
|
3695
|
+
const expirationTime = payload.exp * 1000;
|
|
3664
3696
|
const currentTime = Date.now();
|
|
3665
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3757
|
+
console.error('[WS v2] Error refreshing token:', error);
|
|
3698
3758
|
}
|
|
3699
|
-
return currentToken;
|
|
3759
|
+
return currentToken;
|
|
3700
3760
|
}
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
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
|
-
//
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
3740
|
-
wsUrl.searchParams.append('_t', Date.now().toString());
|
|
3741
|
-
//
|
|
3742
|
-
const authToken = await getFreshAuthToken(
|
|
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
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
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
|
-
|
|
3761
|
-
|
|
3807
|
+
connection.isConnecting = false;
|
|
3808
|
+
connection.isConnected = true;
|
|
3809
|
+
// Schedule token refresh before expiry
|
|
3762
3810
|
(async () => {
|
|
3763
|
-
|
|
3764
|
-
|
|
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
|
-
|
|
3782
|
-
|
|
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
|
-
|
|
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(
|
|
3802
|
-
|
|
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
|
-
|
|
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
|
|
3808
|
-
}
|
|
3809
|
-
function updateCache(path, data) {
|
|
3810
|
-
responseCache[path] = {
|
|
3811
|
-
data,
|
|
3812
|
-
timestamp: Date.now()
|
|
3813
|
-
};
|
|
3847
|
+
return connection;
|
|
3814
3848
|
}
|
|
3815
|
-
function
|
|
3849
|
+
function handleServerMessage(connection, message) {
|
|
3816
3850
|
var _a;
|
|
3817
|
-
(
|
|
3818
|
-
|
|
3819
|
-
|
|
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
|
|
3914
|
+
function notifyCallbacks(subscription, data) {
|
|
3823
3915
|
var _a;
|
|
3824
|
-
(
|
|
3825
|
-
|
|
3826
|
-
|
|
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
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
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
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
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
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
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
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
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
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
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
|