@seaverse/data-service-sdk 0.10.1 → 0.11.0

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
@@ -16,6 +16,7 @@ SeaVerse Data Service SDK for accessing Firestore with secure token management a
16
16
  |---------------------|---------------|--------------|
17
17
  | **Write Public Data** | `helper.addToPublicData()` | `await helper.addToPublicData('posts', {title: 'Hello'})` |
18
18
  | **Read Public Data** | `helper.getPublicData()` | `const posts = await helper.getPublicData('posts')` |
19
+ | **Read with Pagination** | `helper.getPublicData()` | `const posts = await helper.getPublicData('posts', false, {limit: 10})` |
19
20
  | **Write Private Data** | `helper.addToUserData()` | `await helper.addToUserData('notes', {text: 'Secret'})` |
20
21
  | **Read Private Data** | `helper.getUserData()` | `const notes = await helper.getUserData('notes')` |
21
22
 
@@ -378,6 +379,16 @@ posts.forEach(doc => {
378
379
  console.log(doc.id, doc.data());
379
380
  });
380
381
 
382
+ // ✅ Pagination support: Get first 10 posts
383
+ const firstPage = await helper.getPublicData('posts', false, { limit: 10 });
384
+
385
+ // Get next 10 posts (start after last document)
386
+ const lastDoc = firstPage.docs[firstPage.docs.length - 1];
387
+ const nextPage = await helper.getPublicData('posts', false, {
388
+ limit: 10,
389
+ startAfter: lastDoc
390
+ });
391
+
381
392
  // ✅ LLM-FRIENDLY: Write to userData (private) - auto-isolated!
382
393
  await helper.addToUserData('notes', {
383
394
  title: 'Private Note',
@@ -632,9 +643,31 @@ initializeWithToken(tokenResponse: FirestoreTokenResponse): Promise<{
632
643
  db: Firestore;
633
644
  userId: string;
634
645
  appId: string;
646
+ helper: FirestoreHelper;
635
647
  }>
636
648
  ```
637
649
 
650
+ **⚠️ IMPORTANT: Safe for Multiple Calls**
651
+
652
+ This function has been updated to safely handle multiple calls:
653
+ - ✅ **Reuses existing Firebase app** if already initialized
654
+ - ✅ **Re-authenticates with new token** for token refresh scenarios
655
+ - ✅ **Handles errors gracefully** with automatic cleanup
656
+ - ✅ **Validates app_id** before initialization
657
+
658
+ **Common Use Cases:**
659
+ ```typescript
660
+ // ✅ SAFE: First-time initialization
661
+ const { helper } = await initializeWithToken(tokenResponse);
662
+
663
+ // ✅ SAFE: Token refresh (reuses existing app)
664
+ const newTokenResponse = await client.generateFirestoreToken({ ... });
665
+ const { helper: newHelper } = await initializeWithToken(newTokenResponse);
666
+
667
+ // ✅ SAFE: User re-login (reuses existing app)
668
+ const { helper } = await initializeWithToken(newUserTokenResponse);
669
+ ```
670
+
638
671
  **Example:**
639
672
 
640
673
  ```typescript
@@ -643,7 +676,7 @@ import { initializeWithToken } from '@seaverse/data-service-sdk';
643
676
  const tokenResponse = await client.generateGuestFirestoreToken({ app_id: 'my-app' });
644
677
 
645
678
  // One line to get everything!
646
- const { db, appId, userId } = await initializeWithToken(tokenResponse);
679
+ const { db, appId, userId, helper } = await initializeWithToken(tokenResponse);
647
680
 
648
681
  // Ready to use Firestore
649
682
  await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... });
@@ -654,6 +687,22 @@ await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... })
654
687
  npm install firebase
655
688
  ```
656
689
 
690
+ **Error Handling:**
691
+
692
+ ```typescript
693
+ try {
694
+ const { helper } = await initializeWithToken(tokenResponse);
695
+ } catch (error) {
696
+ if (error.message.includes('app_id is required')) {
697
+ // Handle missing app_id in token response
698
+ } else if (error.message.includes('Firebase SDK not found')) {
699
+ // Handle missing Firebase installation
700
+ } else if (error.message.includes('Failed to initialize Firebase')) {
701
+ // Handle authentication or initialization errors
702
+ }
703
+ }
704
+ ```
705
+
657
706
  ## Common Use Cases
658
707
 
659
708
  ### Use Case 1: Public Forum with Comments
@@ -724,6 +773,42 @@ const userPosts = query(
724
773
  const snapshot = await getDocs(userPosts);
725
774
  ```
726
775
 
776
+ ### Use Case 5: Pagination (New!)
777
+
778
+ ```typescript
779
+ // ✅ Simple pagination with FirestoreHelper
780
+
781
+ // Get first page (10 posts)
782
+ const firstPage = await helper.getPublicData('posts', false, { limit: 10 });
783
+
784
+ console.log(`Fetched ${firstPage.docs.length} posts`);
785
+ firstPage.forEach(doc => {
786
+ console.log('Post:', doc.id, doc.data());
787
+ });
788
+
789
+ // Check if there are more pages
790
+ if (firstPage.docs.length === 10) {
791
+ // Get next page (start after last document)
792
+ const lastDoc = firstPage.docs[firstPage.docs.length - 1];
793
+ const nextPage = await helper.getPublicData('posts', false, {
794
+ limit: 10,
795
+ startAfter: lastDoc
796
+ });
797
+
798
+ console.log(`Next page: ${nextPage.docs.length} posts`);
799
+ }
800
+
801
+ // Also works with getUserData and getPublicRead
802
+ const userNotes = await helper.getUserData('notes', false, { limit: 20 });
803
+ const configs = await helper.getPublicRead('config', { limit: 5 });
804
+ ```
805
+
806
+ **Pagination Best Practices:**
807
+ - Use a consistent `limit` value across pages for predictable UX
808
+ - Store the last document from the current page to fetch the next page
809
+ - Check if `docs.length` equals your limit to determine if there might be more pages
810
+ - For infinite scroll, keep appending results; for traditional pagination, replace the view
811
+
727
812
  ## Permission Examples
728
813
 
729
814
  ### What Users CAN Do
@@ -791,6 +876,264 @@ import type {
791
876
  } from '@seaverse/data-service-sdk';
792
877
  ```
793
878
 
879
+ ## Production Usage Recommendations
880
+
881
+ ### Token Expiration Management
882
+
883
+ Firestore tokens expire after 1 hour (3600 seconds). For production applications, you should implement token refresh logic:
884
+
885
+ ```typescript
886
+ import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';
887
+
888
+ class FirestoreManager {
889
+ private tokenExpiryTime: number | null = null;
890
+ private dataClient = new DataServiceClient();
891
+ private userToken: string | null = null;
892
+ private appId: string;
893
+
894
+ constructor(appId: string) {
895
+ this.appId = appId;
896
+ }
897
+
898
+ async initialize(userToken: string) {
899
+ this.userToken = userToken;
900
+
901
+ // Generate Firestore token
902
+ const tokenResponse = await this.dataClient.generateFirestoreToken({
903
+ token: userToken,
904
+ app_id: this.appId
905
+ });
906
+
907
+ // Initialize Firebase (safe to call multiple times)
908
+ const result = await initializeWithToken(tokenResponse);
909
+
910
+ // Track token expiry (expires_in is in seconds)
911
+ this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);
912
+
913
+ return result;
914
+ }
915
+
916
+ isTokenExpiringSoon(): boolean {
917
+ if (!this.tokenExpiryTime) return true;
918
+
919
+ // Check if token expires within 5 minutes
920
+ const fiveMinutes = 5 * 60 * 1000;
921
+ return Date.now() + fiveMinutes >= this.tokenExpiryTime;
922
+ }
923
+
924
+ async refreshTokenIfNeeded() {
925
+ if (this.isTokenExpiringSoon() && this.userToken) {
926
+ console.log('Token expiring soon, refreshing...');
927
+ return await this.initialize(this.userToken);
928
+ }
929
+ }
930
+ }
931
+
932
+ // Usage
933
+ const manager = new FirestoreManager('my-app-123');
934
+
935
+ // Initialize
936
+ const { helper } = await manager.initialize(userJwtToken);
937
+
938
+ // Before making Firestore operations, check token
939
+ await manager.refreshTokenIfNeeded();
940
+ await helper.addToPublicData('posts', { title: 'My Post' });
941
+ ```
942
+
943
+ ### Advanced: Singleton Pattern with React
944
+
945
+ For React applications, you can create a singleton manager with hooks:
946
+
947
+ ```typescript
948
+ // firestore-manager.ts
949
+ class FirestoreManager {
950
+ private static instance: FirestoreManager | null = null;
951
+ private helper: FirestoreHelper | null = null;
952
+ private db: Firestore | null = null;
953
+ private appId: string | null = null;
954
+ private userId: string | null = null;
955
+ private tokenExpiryTime: number | null = null;
956
+
957
+ static getInstance(): FirestoreManager {
958
+ if (!FirestoreManager.instance) {
959
+ FirestoreManager.instance = new FirestoreManager();
960
+ }
961
+ return FirestoreManager.instance;
962
+ }
963
+
964
+ async initialize(tokenResponse: FirestoreTokenResponse) {
965
+ // Always call initializeWithToken - it's safe for multiple calls
966
+ const result = await initializeWithToken(tokenResponse);
967
+
968
+ this.db = result.db;
969
+ this.appId = result.appId;
970
+ this.userId = result.userId;
971
+ this.helper = result.helper;
972
+ this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);
973
+
974
+ return result;
975
+ }
976
+
977
+ getHelper(): FirestoreHelper | null {
978
+ return this.helper;
979
+ }
980
+
981
+ isTokenExpiringSoon(): boolean {
982
+ if (!this.tokenExpiryTime) return true;
983
+ const fiveMinutes = 5 * 60 * 1000;
984
+ return Date.now() + fiveMinutes >= this.tokenExpiryTime;
985
+ }
986
+
987
+ reset() {
988
+ this.db = null;
989
+ this.appId = null;
990
+ this.userId = null;
991
+ this.helper = null;
992
+ this.tokenExpiryTime = null;
993
+ }
994
+ }
995
+
996
+ export default FirestoreManager;
997
+
998
+ // useFirestore.ts (React Hook)
999
+ import { useState, useEffect } from 'react';
1000
+ import { DataServiceClient } from '@seaverse/data-service-sdk';
1001
+ import FirestoreManager from './firestore-manager';
1002
+
1003
+ export function useFirestore(userToken: string, appId: string) {
1004
+ const [helper, setHelper] = useState<FirestoreHelper | null>(null);
1005
+ const [loading, setLoading] = useState(true);
1006
+ const [error, setError] = useState<Error | null>(null);
1007
+
1008
+ useEffect(() => {
1009
+ let mounted = true;
1010
+
1011
+ async function init() {
1012
+ try {
1013
+ const dataClient = new DataServiceClient();
1014
+ const tokenResponse = await dataClient.generateFirestoreToken({
1015
+ token: userToken,
1016
+ app_id: appId
1017
+ });
1018
+
1019
+ const manager = FirestoreManager.getInstance();
1020
+ const { helper } = await manager.initialize(tokenResponse);
1021
+
1022
+ if (mounted) {
1023
+ setHelper(helper);
1024
+ setLoading(false);
1025
+ }
1026
+ } catch (err) {
1027
+ if (mounted) {
1028
+ setError(err as Error);
1029
+ setLoading(false);
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ init();
1035
+
1036
+ return () => {
1037
+ mounted = false;
1038
+ };
1039
+ }, [userToken, appId]);
1040
+
1041
+ return { helper, loading, error };
1042
+ }
1043
+
1044
+ // Usage in component
1045
+ function MyComponent() {
1046
+ const { helper, loading, error } = useFirestore(userToken, 'my-app-123');
1047
+
1048
+ if (loading) return <div>Loading...</div>;
1049
+ if (error) return <div>Error: {error.message}</div>;
1050
+
1051
+ const handleAddPost = async () => {
1052
+ await helper.addToPublicData('posts', {
1053
+ title: 'My Post',
1054
+ content: 'Hello World'
1055
+ });
1056
+ };
1057
+
1058
+ return <button onClick={handleAddPost}>Add Post</button>;
1059
+ }
1060
+ ```
1061
+
1062
+ ### Background Token Refresh
1063
+
1064
+ For long-running applications, set up automatic background token refresh:
1065
+
1066
+ ```typescript
1067
+ class FirestoreManager {
1068
+ private refreshTimer: NodeJS.Timeout | null = null;
1069
+
1070
+ async initialize(userToken: string, appId: string) {
1071
+ const dataClient = new DataServiceClient();
1072
+ const tokenResponse = await dataClient.generateFirestoreToken({
1073
+ token: userToken,
1074
+ app_id: appId
1075
+ });
1076
+
1077
+ const result = await initializeWithToken(tokenResponse);
1078
+
1079
+ // Schedule token refresh 5 minutes before expiry
1080
+ const refreshIn = (tokenResponse.expires_in - 5 * 60) * 1000;
1081
+ this.scheduleRefresh(userToken, appId, refreshIn);
1082
+
1083
+ return result;
1084
+ }
1085
+
1086
+ private scheduleRefresh(userToken: string, appId: string, delay: number) {
1087
+ if (this.refreshTimer) {
1088
+ clearTimeout(this.refreshTimer);
1089
+ }
1090
+
1091
+ this.refreshTimer = setTimeout(async () => {
1092
+ console.log('Auto-refreshing Firestore token...');
1093
+ try {
1094
+ await this.initialize(userToken, appId);
1095
+ } catch (error) {
1096
+ console.error('Failed to refresh token:', error);
1097
+ }
1098
+ }, delay);
1099
+ }
1100
+
1101
+ cleanup() {
1102
+ if (this.refreshTimer) {
1103
+ clearTimeout(this.refreshTimer);
1104
+ this.refreshTimer = null;
1105
+ }
1106
+ }
1107
+ }
1108
+ ```
1109
+
1110
+ ### Error Recovery
1111
+
1112
+ Implement proper error recovery for network failures:
1113
+
1114
+ ```typescript
1115
+ async function initializeWithRetry(
1116
+ tokenResponse: FirestoreTokenResponse,
1117
+ maxRetries = 3
1118
+ ) {
1119
+ for (let i = 0; i < maxRetries; i++) {
1120
+ try {
1121
+ return await initializeWithToken(tokenResponse);
1122
+ } catch (error) {
1123
+ console.error(`Initialization attempt ${i + 1} failed:`, error);
1124
+
1125
+ if (i === maxRetries - 1) {
1126
+ throw error; // Last attempt failed
1127
+ }
1128
+
1129
+ // Wait before retry (exponential backoff)
1130
+ const delay = Math.pow(2, i) * 1000;
1131
+ await new Promise(resolve => setTimeout(resolve, delay));
1132
+ }
1133
+ }
1134
+ }
1135
+ ```
1136
+
794
1137
  ## Best Practices for LLM
795
1138
 
796
1139
  When using this SDK with LLM-generated code:
@@ -1020,4 +1363,4 @@ Here's a complete example for using the SDK directly in the browser without any
1020
1363
  ## Related SDKs
1021
1364
 
1022
1365
  - [@seaverse/auth-sdk](../auth-sdk) - User authentication and account management
1023
- - [@seaverse/payment-sdk](../payment-sdk) - Payment processing integration
1366
+ - [@seaverse/assets-sdk](../assets-sdk) - Assets and payment processing integration
package/dist/browser.js CHANGED
@@ -4782,6 +4782,7 @@ class FirestoreHelper {
4782
4782
  *
4783
4783
  * @param collectionName - Collection name
4784
4784
  * @param includeDeleted - Include soft-deleted documents (default: false)
4785
+ * @param options - Pagination options (limit and startAfter)
4785
4786
  * @returns QuerySnapshot with documents
4786
4787
  *
4787
4788
  * @example
@@ -4794,11 +4795,21 @@ class FirestoreHelper {
4794
4795
  *
4795
4796
  * // Include deleted posts (admin use case)
4796
4797
  * const allPosts = await helper.getPublicData('posts', true);
4798
+ *
4799
+ * // Pagination: Get first 10 posts
4800
+ * const firstPage = await helper.getPublicData('posts', false, { limit: 10 });
4801
+ *
4802
+ * // Get next 10 posts (start after last document)
4803
+ * const lastDoc = firstPage.docs[firstPage.docs.length - 1];
4804
+ * const nextPage = await helper.getPublicData('posts', false, {
4805
+ * limit: 10,
4806
+ * startAfter: lastDoc
4807
+ * });
4797
4808
  * ```
4798
4809
  */
4799
- async getPublicData(collectionName, includeDeleted = false) {
4810
+ async getPublicData(collectionName, includeDeleted = false, options) {
4800
4811
  const path = getPublicDataPath(this.appId, collectionName);
4801
- return this.getDocs(path, includeDeleted);
4812
+ return this.getDocs(path, includeDeleted, options);
4802
4813
  }
4803
4814
  /**
4804
4815
  * Get all documents from userData collection (user's private data)
@@ -4807,6 +4818,7 @@ class FirestoreHelper {
4807
4818
  *
4808
4819
  * @param collectionName - Collection name
4809
4820
  * @param includeDeleted - Include soft-deleted documents (default: false)
4821
+ * @param options - Pagination options (limit and startAfter)
4810
4822
  * @returns QuerySnapshot with documents
4811
4823
  *
4812
4824
  * @example
@@ -4815,26 +4827,45 @@ class FirestoreHelper {
4815
4827
  * snapshot.forEach(doc => {
4816
4828
  * console.log('My note:', doc.data());
4817
4829
  * });
4830
+ *
4831
+ * // Pagination: Get first 20 notes
4832
+ * const firstPage = await helper.getUserData('notes', false, { limit: 20 });
4833
+ *
4834
+ * // Get next page
4835
+ * const lastDoc = firstPage.docs[firstPage.docs.length - 1];
4836
+ * const nextPage = await helper.getUserData('notes', false, {
4837
+ * limit: 20,
4838
+ * startAfter: lastDoc
4839
+ * });
4818
4840
  * ```
4819
4841
  */
4820
- async getUserData(collectionName, includeDeleted = false) {
4842
+ async getUserData(collectionName, includeDeleted = false, options) {
4821
4843
  const path = getUserDataPath(this.appId, this.userId, collectionName);
4822
- return this.getDocs(path, includeDeleted);
4844
+ return this.getDocs(path, includeDeleted, options);
4823
4845
  }
4824
4846
  /**
4825
4847
  * Get all documents from publicRead collection (read-only for users)
4826
4848
  *
4827
4849
  * @param collectionName - Collection name
4850
+ * @param options - Pagination options (limit and startAfter)
4828
4851
  * @returns QuerySnapshot with documents
4829
4852
  *
4830
4853
  * @example
4831
4854
  * ```typescript
4832
4855
  * const configs = await helper.getPublicRead('config');
4856
+ *
4857
+ * // Pagination
4858
+ * const firstPage = await helper.getPublicRead('config', { limit: 10 });
4859
+ * const lastDoc = firstPage.docs[firstPage.docs.length - 1];
4860
+ * const nextPage = await helper.getPublicRead('config', {
4861
+ * limit: 10,
4862
+ * startAfter: lastDoc
4863
+ * });
4833
4864
  * ```
4834
4865
  */
4835
- async getPublicRead(collectionName) {
4866
+ async getPublicRead(collectionName, options) {
4836
4867
  const path = getPublicReadPath(this.appId, collectionName);
4837
- return this.getDocs(path);
4868
+ return this.getDocs(path, false, options);
4838
4869
  }
4839
4870
  /**
4840
4871
  * Get collection reference for publicData
@@ -5003,22 +5034,36 @@ class FirestoreHelper {
5003
5034
  }
5004
5035
  /**
5005
5036
  * Internal: Get all documents from collection
5006
- * Optionally filter out soft-deleted documents
5037
+ * Optionally filter out soft-deleted documents and support pagination
5007
5038
  */
5008
- async getDocs(collectionPath, includeDeleted = false) {
5009
- const { getDocs, collection, query, where } = await this.loadFirestore();
5039
+ async getDocs(collectionPath, includeDeleted = false, options) {
5040
+ const { getDocs, collection, query, where, limit, startAfter } = await this.loadFirestore();
5010
5041
  const colRef = collection(this.db, collectionPath);
5011
- if (includeDeleted) {
5012
- // Return all documents (including soft-deleted)
5013
- return getDocs(colRef);
5014
- }
5015
- else {
5042
+ // Build query constraints
5043
+ const constraints = [];
5044
+ // Add soft-delete filter if needed
5045
+ if (!includeDeleted) {
5016
5046
  // Filter out soft-deleted documents
5017
5047
  // Use '!=' to include documents without _deleted field (new documents)
5018
5048
  // This will return documents where _deleted is missing, null, undefined, or false
5019
- const q = query(colRef, where('_deleted', '!=', true));
5049
+ constraints.push(where('_deleted', '!=', true));
5050
+ }
5051
+ // Add pagination constraints if provided
5052
+ if (options?.limit) {
5053
+ constraints.push(limit(options.limit));
5054
+ }
5055
+ if (options?.startAfter) {
5056
+ constraints.push(startAfter(options.startAfter));
5057
+ }
5058
+ // Execute query
5059
+ if (constraints.length > 0) {
5060
+ const q = query(colRef, ...constraints);
5020
5061
  return getDocs(q);
5021
5062
  }
5063
+ else {
5064
+ // No constraints, return all documents
5065
+ return getDocs(colRef);
5066
+ }
5022
5067
  }
5023
5068
  /**
5024
5069
  * Internal: Get collection reference
@@ -5047,7 +5092,8 @@ class FirestoreHelper {
5047
5092
  query: firestore.query,
5048
5093
  where: firestore.where,
5049
5094
  orderBy: firestore.orderBy,
5050
- limit: firestore.limit
5095
+ limit: firestore.limit,
5096
+ startAfter: firestore.startAfter
5051
5097
  };
5052
5098
  }
5053
5099
  }
@@ -5149,7 +5195,7 @@ function getFirebaseConfig(tokenResponse) {
5149
5195
  *
5150
5196
  * This is a convenience function that automatically:
5151
5197
  * 1. Creates Firebase config from token response
5152
- * 2. Initializes Firebase app
5198
+ * 2. Initializes Firebase app (or reuses existing one)
5153
5199
  * 3. Signs in with the custom token
5154
5200
  * 4. Creates FirestoreHelper for LLM-friendly operations
5155
5201
  * 5. Returns authenticated Firebase instances
@@ -5157,11 +5203,20 @@ function getFirebaseConfig(tokenResponse) {
5157
5203
  * IMPORTANT: This function requires Firebase SDK to be installed separately:
5158
5204
  * npm install firebase
5159
5205
  *
5206
+ * ⚠️ IMPORTANT: This function can be called multiple times safely. It will:
5207
+ * - Reuse existing Firebase app if already initialized
5208
+ * - Re-authenticate with new token if needed
5209
+ * - Handle token refresh scenarios
5210
+ *
5160
5211
  * 🎯 LLM RECOMMENDED: Use the `helper` object for simplified operations
5161
5212
  *
5162
5213
  * @param tokenResponse - The Firestore token response from SDK
5163
5214
  * @returns Object containing initialized Firebase instances and LLM-friendly helper
5164
5215
  *
5216
+ * @throws {Error} If app_id is missing from token response
5217
+ * @throws {Error} If Firebase SDK is not installed
5218
+ * @throws {Error} If authentication fails
5219
+ *
5165
5220
  * @example
5166
5221
  * ```typescript
5167
5222
  * import { initializeWithToken } from '@seaverse/data-service-sdk';
@@ -5180,17 +5235,25 @@ function getFirebaseConfig(tokenResponse) {
5180
5235
  * ```
5181
5236
  */
5182
5237
  async function initializeWithToken(tokenResponse) {
5238
+ // ✅ FIX #3: Validate appId FIRST before any initialization
5239
+ const appId = tokenResponse.app_id;
5240
+ if (!appId) {
5241
+ throw new Error('app_id is required in token response. ' +
5242
+ 'Make sure your token response includes a valid app_id field.');
5243
+ }
5183
5244
  // Check if Firebase SDK is available
5184
5245
  let initializeApp;
5185
5246
  let getAuth;
5186
5247
  let signInWithCustomToken;
5187
5248
  let getFirestore;
5249
+ let getApps;
5188
5250
  try {
5189
5251
  // Try to import Firebase modules
5190
5252
  const firebaseApp = await import('firebase/app');
5191
5253
  const firebaseAuth = await import('firebase/auth');
5192
5254
  const firebaseFirestore = await import('firebase/firestore');
5193
5255
  initializeApp = firebaseApp.initializeApp;
5256
+ getApps = firebaseApp.getApps;
5194
5257
  getAuth = firebaseAuth.getAuth;
5195
5258
  signInWithCustomToken = firebaseAuth.signInWithCustomToken;
5196
5259
  getFirestore = firebaseFirestore.getFirestore;
@@ -5199,17 +5262,57 @@ async function initializeWithToken(tokenResponse) {
5199
5262
  throw new Error('Firebase SDK not found. Please install it: npm install firebase\n' +
5200
5263
  'Or import manually and use getFirebaseConfig() helper instead.');
5201
5264
  }
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);
5265
+ // FIX #1: Check if Firebase app already exists
5266
+ const existingApps = getApps();
5267
+ const existingApp = existingApps.find((app) => app.name === '[DEFAULT]');
5268
+ let app;
5269
+ let auth;
5270
+ let db;
5271
+ if (existingApp) {
5272
+ // ✅ FIX #2: Reuse existing app instead of creating new one
5273
+ console.log('[SeaVerse SDK] Reusing existing Firebase app');
5274
+ app = existingApp;
5275
+ auth = getAuth(app);
5276
+ db = getFirestore(app, tokenResponse.database_id);
5277
+ // ✅ FIX #4: Re-authenticate with new token (for token refresh scenarios)
5278
+ try {
5279
+ await signInWithCustomToken(auth, tokenResponse.custom_token);
5280
+ console.log('[SeaVerse SDK] Re-authenticated with new token');
5281
+ }
5282
+ catch (error) {
5283
+ // If already signed in with same token, ignore error
5284
+ // Only throw if it's a real authentication failure
5285
+ if (error?.code !== 'auth/invalid-credential' && error?.code !== 'auth/network-request-failed') {
5286
+ console.warn('[SeaVerse SDK] Re-authentication warning:', error?.message || error);
5287
+ }
5288
+ }
5289
+ }
5290
+ else {
5291
+ // ✅ FIX #4: First-time initialization with proper error handling
5292
+ console.log('[SeaVerse SDK] Initializing new Firebase app');
5293
+ try {
5294
+ const config = getFirebaseConfig(tokenResponse);
5295
+ app = initializeApp(config);
5296
+ auth = getAuth(app);
5297
+ await signInWithCustomToken(auth, tokenResponse.custom_token);
5298
+ // IMPORTANT: Must specify database_id, not just use default!
5299
+ db = getFirestore(app, tokenResponse.database_id);
5300
+ }
5301
+ catch (error) {
5302
+ // ✅ FIX #4: Cleanup on failure to prevent half-initialized state
5303
+ if (app) {
5304
+ try {
5305
+ await app.delete();
5306
+ console.log('[SeaVerse SDK] Cleaned up failed Firebase app initialization');
5307
+ }
5308
+ catch (cleanupError) {
5309
+ console.warn('[SeaVerse SDK] Failed to cleanup app:', cleanupError);
5310
+ }
5311
+ }
5312
+ throw new Error(`Failed to initialize Firebase: ${error instanceof Error ? error.message : String(error)}`);
5313
+ }
5314
+ }
5211
5315
  const userId = tokenResponse.user_id;
5212
- const appId = tokenResponse.app_id || '';
5213
5316
  // Create LLM-friendly helper
5214
5317
  const helper = new FirestoreHelper(db, appId, userId);
5215
5318
  return {