@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/client/operations.d.ts +1 -0
- package/dist/client/subscription-v2.d.ts +47 -0
- package/dist/client/subscription.d.ts +54 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +527 -131
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +524 -132
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
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;
|
|
3728
|
+
const expirationTime = payload.exp * 1000;
|
|
3691
3729
|
const currentTime = Date.now();
|
|
3692
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3790
|
+
console.error('[WS v2] Error refreshing token:', error);
|
|
3725
3791
|
}
|
|
3726
|
-
return currentToken;
|
|
3792
|
+
return currentToken;
|
|
3727
3793
|
}
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
3767
|
-
wsUrl.searchParams.append('_t', Date.now().toString());
|
|
3768
|
-
//
|
|
3769
|
-
const authToken = await getFreshAuthToken(
|
|
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
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
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
|
-
|
|
3788
|
-
|
|
3840
|
+
connection.isConnecting = false;
|
|
3841
|
+
connection.isConnected = true;
|
|
3842
|
+
// Schedule token refresh before expiry
|
|
3789
3843
|
(async () => {
|
|
3790
|
-
|
|
3791
|
-
|
|
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
|
-
|
|
3809
|
-
|
|
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
|
-
|
|
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(
|
|
3829
|
-
|
|
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
|
-
|
|
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
|
|
3880
|
+
return connection;
|
|
3835
3881
|
}
|
|
3836
|
-
function
|
|
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
|
-
(
|
|
3845
|
-
|
|
3846
|
-
|
|
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
|
|
3947
|
+
function notifyCallbacks(subscription, data) {
|
|
3850
3948
|
var _a;
|
|
3851
|
-
(
|
|
3852
|
-
|
|
3853
|
-
|
|
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
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
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
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
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
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
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
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
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
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
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;
|