@seaverse/data-service-sdk 0.10.1 → 0.10.2

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/README.md CHANGED
@@ -632,9 +632,31 @@ initializeWithToken(tokenResponse: FirestoreTokenResponse): Promise<{
632
632
  db: Firestore;
633
633
  userId: string;
634
634
  appId: string;
635
+ helper: FirestoreHelper;
635
636
  }>
636
637
  ```
637
638
 
639
+ **⚠️ IMPORTANT: Safe for Multiple Calls**
640
+
641
+ This function has been updated to safely handle multiple calls:
642
+ - ✅ **Reuses existing Firebase app** if already initialized
643
+ - ✅ **Re-authenticates with new token** for token refresh scenarios
644
+ - ✅ **Handles errors gracefully** with automatic cleanup
645
+ - ✅ **Validates app_id** before initialization
646
+
647
+ **Common Use Cases:**
648
+ ```typescript
649
+ // ✅ SAFE: First-time initialization
650
+ const { helper } = await initializeWithToken(tokenResponse);
651
+
652
+ // ✅ SAFE: Token refresh (reuses existing app)
653
+ const newTokenResponse = await client.generateFirestoreToken({ ... });
654
+ const { helper: newHelper } = await initializeWithToken(newTokenResponse);
655
+
656
+ // ✅ SAFE: User re-login (reuses existing app)
657
+ const { helper } = await initializeWithToken(newUserTokenResponse);
658
+ ```
659
+
638
660
  **Example:**
639
661
 
640
662
  ```typescript
@@ -643,7 +665,7 @@ import { initializeWithToken } from '@seaverse/data-service-sdk';
643
665
  const tokenResponse = await client.generateGuestFirestoreToken({ app_id: 'my-app' });
644
666
 
645
667
  // One line to get everything!
646
- const { db, appId, userId } = await initializeWithToken(tokenResponse);
668
+ const { db, appId, userId, helper } = await initializeWithToken(tokenResponse);
647
669
 
648
670
  // Ready to use Firestore
649
671
  await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... });
@@ -654,6 +676,22 @@ await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... })
654
676
  npm install firebase
655
677
  ```
656
678
 
679
+ **Error Handling:**
680
+
681
+ ```typescript
682
+ try {
683
+ const { helper } = await initializeWithToken(tokenResponse);
684
+ } catch (error) {
685
+ if (error.message.includes('app_id is required')) {
686
+ // Handle missing app_id in token response
687
+ } else if (error.message.includes('Firebase SDK not found')) {
688
+ // Handle missing Firebase installation
689
+ } else if (error.message.includes('Failed to initialize Firebase')) {
690
+ // Handle authentication or initialization errors
691
+ }
692
+ }
693
+ ```
694
+
657
695
  ## Common Use Cases
658
696
 
659
697
  ### Use Case 1: Public Forum with Comments
@@ -791,6 +829,264 @@ import type {
791
829
  } from '@seaverse/data-service-sdk';
792
830
  ```
793
831
 
832
+ ## Production Usage Recommendations
833
+
834
+ ### Token Expiration Management
835
+
836
+ Firestore tokens expire after 1 hour (3600 seconds). For production applications, you should implement token refresh logic:
837
+
838
+ ```typescript
839
+ import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';
840
+
841
+ class FirestoreManager {
842
+ private tokenExpiryTime: number | null = null;
843
+ private dataClient = new DataServiceClient();
844
+ private userToken: string | null = null;
845
+ private appId: string;
846
+
847
+ constructor(appId: string) {
848
+ this.appId = appId;
849
+ }
850
+
851
+ async initialize(userToken: string) {
852
+ this.userToken = userToken;
853
+
854
+ // Generate Firestore token
855
+ const tokenResponse = await this.dataClient.generateFirestoreToken({
856
+ token: userToken,
857
+ app_id: this.appId
858
+ });
859
+
860
+ // Initialize Firebase (safe to call multiple times)
861
+ const result = await initializeWithToken(tokenResponse);
862
+
863
+ // Track token expiry (expires_in is in seconds)
864
+ this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);
865
+
866
+ return result;
867
+ }
868
+
869
+ isTokenExpiringSoon(): boolean {
870
+ if (!this.tokenExpiryTime) return true;
871
+
872
+ // Check if token expires within 5 minutes
873
+ const fiveMinutes = 5 * 60 * 1000;
874
+ return Date.now() + fiveMinutes >= this.tokenExpiryTime;
875
+ }
876
+
877
+ async refreshTokenIfNeeded() {
878
+ if (this.isTokenExpiringSoon() && this.userToken) {
879
+ console.log('Token expiring soon, refreshing...');
880
+ return await this.initialize(this.userToken);
881
+ }
882
+ }
883
+ }
884
+
885
+ // Usage
886
+ const manager = new FirestoreManager('my-app-123');
887
+
888
+ // Initialize
889
+ const { helper } = await manager.initialize(userJwtToken);
890
+
891
+ // Before making Firestore operations, check token
892
+ await manager.refreshTokenIfNeeded();
893
+ await helper.addToPublicData('posts', { title: 'My Post' });
894
+ ```
895
+
896
+ ### Advanced: Singleton Pattern with React
897
+
898
+ For React applications, you can create a singleton manager with hooks:
899
+
900
+ ```typescript
901
+ // firestore-manager.ts
902
+ class FirestoreManager {
903
+ private static instance: FirestoreManager | null = null;
904
+ private helper: FirestoreHelper | null = null;
905
+ private db: Firestore | null = null;
906
+ private appId: string | null = null;
907
+ private userId: string | null = null;
908
+ private tokenExpiryTime: number | null = null;
909
+
910
+ static getInstance(): FirestoreManager {
911
+ if (!FirestoreManager.instance) {
912
+ FirestoreManager.instance = new FirestoreManager();
913
+ }
914
+ return FirestoreManager.instance;
915
+ }
916
+
917
+ async initialize(tokenResponse: FirestoreTokenResponse) {
918
+ // Always call initializeWithToken - it's safe for multiple calls
919
+ const result = await initializeWithToken(tokenResponse);
920
+
921
+ this.db = result.db;
922
+ this.appId = result.appId;
923
+ this.userId = result.userId;
924
+ this.helper = result.helper;
925
+ this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);
926
+
927
+ return result;
928
+ }
929
+
930
+ getHelper(): FirestoreHelper | null {
931
+ return this.helper;
932
+ }
933
+
934
+ isTokenExpiringSoon(): boolean {
935
+ if (!this.tokenExpiryTime) return true;
936
+ const fiveMinutes = 5 * 60 * 1000;
937
+ return Date.now() + fiveMinutes >= this.tokenExpiryTime;
938
+ }
939
+
940
+ reset() {
941
+ this.db = null;
942
+ this.appId = null;
943
+ this.userId = null;
944
+ this.helper = null;
945
+ this.tokenExpiryTime = null;
946
+ }
947
+ }
948
+
949
+ export default FirestoreManager;
950
+
951
+ // useFirestore.ts (React Hook)
952
+ import { useState, useEffect } from 'react';
953
+ import { DataServiceClient } from '@seaverse/data-service-sdk';
954
+ import FirestoreManager from './firestore-manager';
955
+
956
+ export function useFirestore(userToken: string, appId: string) {
957
+ const [helper, setHelper] = useState<FirestoreHelper | null>(null);
958
+ const [loading, setLoading] = useState(true);
959
+ const [error, setError] = useState<Error | null>(null);
960
+
961
+ useEffect(() => {
962
+ let mounted = true;
963
+
964
+ async function init() {
965
+ try {
966
+ const dataClient = new DataServiceClient();
967
+ const tokenResponse = await dataClient.generateFirestoreToken({
968
+ token: userToken,
969
+ app_id: appId
970
+ });
971
+
972
+ const manager = FirestoreManager.getInstance();
973
+ const { helper } = await manager.initialize(tokenResponse);
974
+
975
+ if (mounted) {
976
+ setHelper(helper);
977
+ setLoading(false);
978
+ }
979
+ } catch (err) {
980
+ if (mounted) {
981
+ setError(err as Error);
982
+ setLoading(false);
983
+ }
984
+ }
985
+ }
986
+
987
+ init();
988
+
989
+ return () => {
990
+ mounted = false;
991
+ };
992
+ }, [userToken, appId]);
993
+
994
+ return { helper, loading, error };
995
+ }
996
+
997
+ // Usage in component
998
+ function MyComponent() {
999
+ const { helper, loading, error } = useFirestore(userToken, 'my-app-123');
1000
+
1001
+ if (loading) return <div>Loading...</div>;
1002
+ if (error) return <div>Error: {error.message}</div>;
1003
+
1004
+ const handleAddPost = async () => {
1005
+ await helper.addToPublicData('posts', {
1006
+ title: 'My Post',
1007
+ content: 'Hello World'
1008
+ });
1009
+ };
1010
+
1011
+ return <button onClick={handleAddPost}>Add Post</button>;
1012
+ }
1013
+ ```
1014
+
1015
+ ### Background Token Refresh
1016
+
1017
+ For long-running applications, set up automatic background token refresh:
1018
+
1019
+ ```typescript
1020
+ class FirestoreManager {
1021
+ private refreshTimer: NodeJS.Timeout | null = null;
1022
+
1023
+ async initialize(userToken: string, appId: string) {
1024
+ const dataClient = new DataServiceClient();
1025
+ const tokenResponse = await dataClient.generateFirestoreToken({
1026
+ token: userToken,
1027
+ app_id: appId
1028
+ });
1029
+
1030
+ const result = await initializeWithToken(tokenResponse);
1031
+
1032
+ // Schedule token refresh 5 minutes before expiry
1033
+ const refreshIn = (tokenResponse.expires_in - 5 * 60) * 1000;
1034
+ this.scheduleRefresh(userToken, appId, refreshIn);
1035
+
1036
+ return result;
1037
+ }
1038
+
1039
+ private scheduleRefresh(userToken: string, appId: string, delay: number) {
1040
+ if (this.refreshTimer) {
1041
+ clearTimeout(this.refreshTimer);
1042
+ }
1043
+
1044
+ this.refreshTimer = setTimeout(async () => {
1045
+ console.log('Auto-refreshing Firestore token...');
1046
+ try {
1047
+ await this.initialize(userToken, appId);
1048
+ } catch (error) {
1049
+ console.error('Failed to refresh token:', error);
1050
+ }
1051
+ }, delay);
1052
+ }
1053
+
1054
+ cleanup() {
1055
+ if (this.refreshTimer) {
1056
+ clearTimeout(this.refreshTimer);
1057
+ this.refreshTimer = null;
1058
+ }
1059
+ }
1060
+ }
1061
+ ```
1062
+
1063
+ ### Error Recovery
1064
+
1065
+ Implement proper error recovery for network failures:
1066
+
1067
+ ```typescript
1068
+ async function initializeWithRetry(
1069
+ tokenResponse: FirestoreTokenResponse,
1070
+ maxRetries = 3
1071
+ ) {
1072
+ for (let i = 0; i < maxRetries; i++) {
1073
+ try {
1074
+ return await initializeWithToken(tokenResponse);
1075
+ } catch (error) {
1076
+ console.error(`Initialization attempt ${i + 1} failed:`, error);
1077
+
1078
+ if (i === maxRetries - 1) {
1079
+ throw error; // Last attempt failed
1080
+ }
1081
+
1082
+ // Wait before retry (exponential backoff)
1083
+ const delay = Math.pow(2, i) * 1000;
1084
+ await new Promise(resolve => setTimeout(resolve, delay));
1085
+ }
1086
+ }
1087
+ }
1088
+ ```
1089
+
794
1090
  ## Best Practices for LLM
795
1091
 
796
1092
  When using this SDK with LLM-generated code:
package/dist/browser.js CHANGED
@@ -5149,7 +5149,7 @@ function getFirebaseConfig(tokenResponse) {
5149
5149
  *
5150
5150
  * This is a convenience function that automatically:
5151
5151
  * 1. Creates Firebase config from token response
5152
- * 2. Initializes Firebase app
5152
+ * 2. Initializes Firebase app (or reuses existing one)
5153
5153
  * 3. Signs in with the custom token
5154
5154
  * 4. Creates FirestoreHelper for LLM-friendly operations
5155
5155
  * 5. Returns authenticated Firebase instances
@@ -5157,11 +5157,20 @@ function getFirebaseConfig(tokenResponse) {
5157
5157
  * IMPORTANT: This function requires Firebase SDK to be installed separately:
5158
5158
  * npm install firebase
5159
5159
  *
5160
+ * ⚠️ IMPORTANT: This function can be called multiple times safely. It will:
5161
+ * - Reuse existing Firebase app if already initialized
5162
+ * - Re-authenticate with new token if needed
5163
+ * - Handle token refresh scenarios
5164
+ *
5160
5165
  * 🎯 LLM RECOMMENDED: Use the `helper` object for simplified operations
5161
5166
  *
5162
5167
  * @param tokenResponse - The Firestore token response from SDK
5163
5168
  * @returns Object containing initialized Firebase instances and LLM-friendly helper
5164
5169
  *
5170
+ * @throws {Error} If app_id is missing from token response
5171
+ * @throws {Error} If Firebase SDK is not installed
5172
+ * @throws {Error} If authentication fails
5173
+ *
5165
5174
  * @example
5166
5175
  * ```typescript
5167
5176
  * import { initializeWithToken } from '@seaverse/data-service-sdk';
@@ -5180,17 +5189,25 @@ function getFirebaseConfig(tokenResponse) {
5180
5189
  * ```
5181
5190
  */
5182
5191
  async function initializeWithToken(tokenResponse) {
5192
+ // ✅ FIX #3: Validate appId FIRST before any initialization
5193
+ const appId = tokenResponse.app_id;
5194
+ if (!appId) {
5195
+ throw new Error('app_id is required in token response. ' +
5196
+ 'Make sure your token response includes a valid app_id field.');
5197
+ }
5183
5198
  // Check if Firebase SDK is available
5184
5199
  let initializeApp;
5185
5200
  let getAuth;
5186
5201
  let signInWithCustomToken;
5187
5202
  let getFirestore;
5203
+ let getApps;
5188
5204
  try {
5189
5205
  // Try to import Firebase modules
5190
5206
  const firebaseApp = await import('firebase/app');
5191
5207
  const firebaseAuth = await import('firebase/auth');
5192
5208
  const firebaseFirestore = await import('firebase/firestore');
5193
5209
  initializeApp = firebaseApp.initializeApp;
5210
+ getApps = firebaseApp.getApps;
5194
5211
  getAuth = firebaseAuth.getAuth;
5195
5212
  signInWithCustomToken = firebaseAuth.signInWithCustomToken;
5196
5213
  getFirestore = firebaseFirestore.getFirestore;
@@ -5199,17 +5216,57 @@ async function initializeWithToken(tokenResponse) {
5199
5216
  throw new Error('Firebase SDK not found. Please install it: npm install firebase\n' +
5200
5217
  'Or import manually and use getFirebaseConfig() helper instead.');
5201
5218
  }
5202
- // Initialize Firebase
5203
- const config = getFirebaseConfig(tokenResponse);
5204
- const app = initializeApp(config);
5205
- // Sign in with custom token
5206
- const auth = getAuth(app);
5207
- await signInWithCustomToken(auth, tokenResponse.custom_token);
5208
- // Get Firestore instance with correct database ID
5209
- // IMPORTANT: Must specify database_id, not just use default!
5210
- const db = getFirestore(app, tokenResponse.database_id);
5219
+ // FIX #1: Check if Firebase app already exists
5220
+ const existingApps = getApps();
5221
+ const existingApp = existingApps.find((app) => app.name === '[DEFAULT]');
5222
+ let app;
5223
+ let auth;
5224
+ let db;
5225
+ if (existingApp) {
5226
+ // ✅ FIX #2: Reuse existing app instead of creating new one
5227
+ console.log('[SeaVerse SDK] Reusing existing Firebase app');
5228
+ app = existingApp;
5229
+ auth = getAuth(app);
5230
+ db = getFirestore(app, tokenResponse.database_id);
5231
+ // ✅ FIX #4: Re-authenticate with new token (for token refresh scenarios)
5232
+ try {
5233
+ await signInWithCustomToken(auth, tokenResponse.custom_token);
5234
+ console.log('[SeaVerse SDK] Re-authenticated with new token');
5235
+ }
5236
+ catch (error) {
5237
+ // If already signed in with same token, ignore error
5238
+ // Only throw if it's a real authentication failure
5239
+ if (error?.code !== 'auth/invalid-credential' && error?.code !== 'auth/network-request-failed') {
5240
+ console.warn('[SeaVerse SDK] Re-authentication warning:', error?.message || error);
5241
+ }
5242
+ }
5243
+ }
5244
+ else {
5245
+ // ✅ FIX #4: First-time initialization with proper error handling
5246
+ console.log('[SeaVerse SDK] Initializing new Firebase app');
5247
+ try {
5248
+ const config = getFirebaseConfig(tokenResponse);
5249
+ app = initializeApp(config);
5250
+ auth = getAuth(app);
5251
+ await signInWithCustomToken(auth, tokenResponse.custom_token);
5252
+ // IMPORTANT: Must specify database_id, not just use default!
5253
+ db = getFirestore(app, tokenResponse.database_id);
5254
+ }
5255
+ catch (error) {
5256
+ // ✅ FIX #4: Cleanup on failure to prevent half-initialized state
5257
+ if (app) {
5258
+ try {
5259
+ await app.delete();
5260
+ console.log('[SeaVerse SDK] Cleaned up failed Firebase app initialization');
5261
+ }
5262
+ catch (cleanupError) {
5263
+ console.warn('[SeaVerse SDK] Failed to cleanup app:', cleanupError);
5264
+ }
5265
+ }
5266
+ throw new Error(`Failed to initialize Firebase: ${error instanceof Error ? error.message : String(error)}`);
5267
+ }
5268
+ }
5211
5269
  const userId = tokenResponse.user_id;
5212
- const appId = tokenResponse.app_id || '';
5213
5270
  // Create LLM-friendly helper
5214
5271
  const helper = new FirestoreHelper(db, appId, userId);
5215
5272
  return {