diff --git a/android/src/main/java/com/cblreactnative/CblReactnativeModule.kt b/android/src/main/java/com/cblreactnative/CblReactnativeModule.kt index 60b108a..6a44f3f 100644 --- a/android/src/main/java/com/cblreactnative/CblReactnativeModule.kt +++ b/android/src/main/java/com/cblreactnative/CblReactnativeModule.kt @@ -1490,7 +1490,7 @@ fun replicator_RemoveChangeListener( if (!DataValidation.validateReplicatorId(replicatorId, promise)){ return@launch } - ReplicatorManager.start(replicatorId) + ReplicatorManager.start(replicatorId, false) context.runOnUiQueueThread { promise.resolve(null) } diff --git a/android/src/main/java/com/cblreactnative/cbl-js-kotlin b/android/src/main/java/com/cblreactnative/cbl-js-kotlin index 66c5f24..6266c71 160000 --- a/android/src/main/java/com/cblreactnative/cbl-js-kotlin +++ b/android/src/main/java/com/cblreactnative/cbl-js-kotlin @@ -1 +1 @@ -Subproject commit 66c5f24a72f7db2d0cb016ff9cf45c62f39493f2 +Subproject commit 6266c718cef8b3e1465e5eafb8fd29266f609073 diff --git a/expo-example/.env.example b/expo-example/.env.example new file mode 100644 index 0000000..c1d55ed --- /dev/null +++ b/expo-example/.env.example @@ -0,0 +1,11 @@ +# Couchbase Lite React Native - Test Environment Configuration +# Copy this file to .env.local and fill in your actual values +# Note: .env.local is git-ignored for security + +# Sync Gateway Configuration +EXPO_PUBLIC_SYNC_GATEWAY_URL=wss://your-gateway-url:4984/your-endpoint +EXPO_PUBLIC_SYNC_USERNAME=your_username +EXPO_PUBLIC_SYNC_PASSWORD=your_password + +# Note: These credentials are used for testing purposes only +# Never commit actual credentials to version control diff --git a/expo-example/.gitignore b/expo-example/.gitignore index cca551d..2e662a6 100644 --- a/expo-example/.gitignore +++ b/expo-example/.gitignore @@ -8,6 +8,8 @@ node_modules/ dist/ web-build/ +build-log.txt + # Native artifacts & keys *.orig.* *.jks diff --git a/expo-example/android/app/proguard-rules.pro b/expo-example/android/app/proguard-rules.pro index 551eb41..561eda6 100644 --- a/expo-example/android/app/proguard-rules.pro +++ b/expo-example/android/app/proguard-rules.pro @@ -11,4 +11,43 @@ -keep class com.swmansion.reanimated.** { *; } -keep class com.facebook.react.turbomodule.** { *; } +# Couchbase Lite - Keep all classes and methods +-keep class com.couchbase.lite.** { *; } +-keep class com.couchbase.litecore.** { *; } +-keepattributes Signature +-keepattributes *Annotation* + +# Keep native methods +-keepclasseswithmembernames class * { + native ; +} + +# Keep Kotlin metadata for Couchbase Lite Kotlin extensions +-keep class kotlin.Metadata { *; } +-keep class kotlin.reflect.** { *; } + +# Keep our React Native module +-keep class com.cblreactnative.** { *; } +-keep class cbl.js.kotiln.** { *; } + +# Keep J2V8 (JavaScript engine used for filters) +-keep class com.eclipsesource.v8.** { *; } +-keepclassmembers class com.eclipsesource.v8.** { *; } + +# Keep enums (used for ReplicatorType, ActivityLevel, etc.) +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +# Keep serialization classes +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + # Add any project specific keep options here: diff --git a/expo-example/android/app/src/main/AndroidManifest.xml b/expo-example/android/app/src/main/AndroidManifest.xml index f4ade3f..dfaf44b 100644 --- a/expo-example/android/app/src/main/AndroidManifest.xml +++ b/expo-example/android/app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ - + diff --git a/expo-example/android/app/src/main/res/xml/network_security_config.xml b/expo-example/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..3609b91 --- /dev/null +++ b/expo-example/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,12 @@ + + + + + + 10.0.2.2 + + localhost + 127.0.0.1 + + + diff --git a/expo-example/android/settings.gradle b/expo-example/android/settings.gradle index 8b2bb13..477a094 100644 --- a/expo-example/android/settings.gradle +++ b/expo-example/android/settings.gradle @@ -1,5 +1,5 @@ pluginManagement { - includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile().toString()) + includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().toString()) } plugins { id("com.facebook.react.settings") } @@ -31,7 +31,7 @@ dependencyResolutionManagement { } } -apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); + apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); useExpoModules() include ':app' diff --git a/expo-example/app/tests/custom-bug-fix.tsx b/expo-example/app/tests/custom-bug-fix.tsx index 2e3a412..2786f59 100644 --- a/expo-example/app/tests/custom-bug-fix.tsx +++ b/expo-example/app/tests/custom-bug-fix.tsx @@ -14,8 +14,10 @@ import { Collection, MutableDocument, Document, - ConcurrencyControl - } from 'cbl-reactnative';import getFileDefaultPath from '@/service/file/getFileDefaultPath'; + ConcurrencyControl, + CollectionConfiguration + } from 'cbl-reactnative'; + import getFileDefaultPath from '@/service/file/getFileDefaultPath'; export default function CustomBugFixScreen() { function reset() {} @@ -63,22 +65,26 @@ export default function CustomBugFixScreen() { const connectToSyncGateway = async () => { setListOfLogs(prev => [...prev, 'Connecting to Sync Gateway']); // ✅ Use prev - const defaultCollection = await database?.defaultCollection(); + if(database === null || database === undefined) { + throw Error("Database is undefined") + } + const defaultCollection = await database.defaultCollection(); - const syncGatewayUrl = "wss://nasm0fvdr-jnehnb.apps.cloud.couchbase.com:4984/testendpoint" + const syncGatewayUrl = process.env.EXPO_PUBLIC_SYNC_GATEWAY_URL || "wss://localhost:4984/testendpoint" const endpoint = new URLEndpoint(syncGatewayUrl); - const username = "jayantdhingra" - const password = "f9yu5QT4B5jpZep@" + const username = process.env.EXPO_PUBLIC_SYNC_USERNAME || "test_user" + const password = process.env.EXPO_PUBLIC_SYNC_PASSWORD || "test_pass" - const replicatorConfig = new ReplicatorConfiguration(endpoint) + + const collectionConfig = new CollectionConfiguration(defaultCollection) + const listOfCollectionConfig = [collectionConfig] + + const replicatorConfig = new ReplicatorConfiguration(listOfCollectionConfig, endpoint) replicatorConfig.setAuthenticator(new BasicAuthenticator(username, password)) // replicatorConfig.setContinuous(true) replicatorConfig.setAcceptOnlySelfSignedCerts(false); - if (defaultCollection) { - replicatorConfig.addCollection(defaultCollection) - } const replicator = await Replicator.create(replicatorConfig) diff --git a/expo-example/app/tests/replication-new.tsx b/expo-example/app/tests/replication-new.tsx new file mode 100644 index 0000000..9dc12cb --- /dev/null +++ b/expo-example/app/tests/replication-new.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import TestRunnerContainer from '@/components/TestRunnerContainer/TestRunnerContainer'; + +import { ReplicatorNewApiTests } from '../../cblite-js-tests/cblite-tests/e2e/replicator-new-api-test'; + +export default function TestsReplicatorScreen() { + function reset() {} + + async function update(): Promise { + try { + return ['']; + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + return [errorMessage]; + } + } + + return ( + + ); +} diff --git a/expo-example/app/tests/replicator-listeners.tsx b/expo-example/app/tests/replicator-listeners-new.tsx similarity index 96% rename from expo-example/app/tests/replicator-listeners.tsx rename to expo-example/app/tests/replicator-listeners-new.tsx index 9a6c799..a1cc7f0 100644 --- a/expo-example/app/tests/replicator-listeners.tsx +++ b/expo-example/app/tests/replicator-listeners-new.tsx @@ -9,6 +9,7 @@ import { URLEndpoint, BasicAuthenticator, MutableDocument, + CollectionConfiguration, ListenerToken } from 'cbl-reactnative'; import getFileDefaultPath from '@/service/file/getFileDefaultPath'; @@ -24,10 +25,10 @@ export default function ReplicatorListenersScreen() { const [documentToken, setDocumentToken] = useState(null); const [listOfDocuments, setListOfDocuments] = useState([]); - // Configuration for Sync Gateway - const SYNC_GATEWAY_URL = "wss://nasm0fvdr-jnehnb.apps.cloud.couchbase.com:4984/testendpoint"; - const USERNAME = "jayantdhingra"; - const PASSWORD = "f9yu5QT4B5jpZep@"; + // Configuration for Sync Gateway (from environment variables) + const SYNC_GATEWAY_URL = process.env.EXPO_PUBLIC_SYNC_GATEWAY_URL || "wss://localhost:4984/testendpoint"; + const USERNAME = process.env.EXPO_PUBLIC_SYNC_USERNAME || "test_user"; + const PASSWORD = process.env.EXPO_PUBLIC_SYNC_PASSWORD || "test_pass"; const openDatabase = async () => { try { @@ -62,12 +63,15 @@ export default function ReplicatorListenersScreen() { } const endpoint = new URLEndpoint(SYNC_GATEWAY_URL); - const replicatorConfig = new ReplicatorConfiguration(endpoint); + + const collectionConfig = new CollectionConfiguration(defaultCollection); + const listOfCollectionConfig = [collectionConfig] + // Pass array of configs and endpoint to constructor + const replicatorConfig = new ReplicatorConfiguration(listOfCollectionConfig , endpoint); replicatorConfig.setAuthenticator(new BasicAuthenticator(USERNAME, PASSWORD)); replicatorConfig.setContinuous(true); replicatorConfig.setAcceptOnlySelfSignedCerts(false); - replicatorConfig.addCollection(defaultCollection); const replicator = await Replicator.create(replicatorConfig); setReplicator(replicator); diff --git a/expo-example/app/tests/replicator-listeners-old.tsx b/expo-example/app/tests/replicator-listeners-old.tsx new file mode 100644 index 0000000..59a2d81 --- /dev/null +++ b/expo-example/app/tests/replicator-listeners-old.tsx @@ -0,0 +1,389 @@ +import React, { useState } from 'react'; +import { SafeAreaView, Text, Button, ScrollView } from 'react-native'; +import { View } from '@/components/Themed/Themed'; +import { + Database, + DatabaseConfiguration, + Replicator, + ReplicatorConfiguration, + URLEndpoint, + BasicAuthenticator, + MutableDocument, + CollectionConfig +} from 'cbl-reactnative'; +import getFileDefaultPath from '@/service/file/getFileDefaultPath'; + +export default function ReplicatorListenersOldScreen() { + const [listOfLogs, setListOfLogs] = useState([]); + const [errorLogs, setErrorLogs] = useState([]); + + const [database, setDatabase] = useState(null); + const [replicator, setReplicator] = useState(null); + + const [statusToken, setStatusToken] = useState(''); + const [documentToken, setDocumentToken] = useState(''); + const [listOfDocuments, setListOfDocuments] = useState([]); + + // Configuration for Sync Gateway (from environment variables) + const SYNC_GATEWAY_URL = process.env.EXPO_PUBLIC_SYNC_GATEWAY_URL || "wss://localhost:4984/testendpoint"; + const USERNAME = process.env.EXPO_PUBLIC_SYNC_USERNAME || "test_user"; + const PASSWORD = process.env.EXPO_PUBLIC_SYNC_PASSWORD || "test_pass"; + + const openDatabase = async () => { + try { + setListOfLogs(prev => [...prev, 'Opening Database']); + const databaseName = 'replicator_test_db_old'; + const directory = await getFileDefaultPath(); + const dbConfig = new DatabaseConfiguration(); + const database = new Database(databaseName, dbConfig); + await database.open(); + setListOfLogs(prev => [...prev, `Database opened with name: ${database.getName()}`]); + setDatabase(database); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error opening database: ${error.message}`]); + } + } + + const createReplicator = async () => { + try { + setListOfLogs(prev => [...prev, 'Creating Replicator with OLD API (Default Collection)']); + + if (!database) { + setErrorLogs(prev => [...prev, 'Database not opened yet']); + return; + } + + const defaultCollection = await database.defaultCollection(); + + if (!defaultCollection) { + setErrorLogs(prev => [...prev, 'Could not get default collection']); + return; + } + + const endpoint = new URLEndpoint(SYNC_GATEWAY_URL); + + // OLD API: Create config with endpoint only + const replicatorConfig = new ReplicatorConfiguration(endpoint); + + // OLD API: Create CollectionConfig and add collections + const collectionConfig = new CollectionConfig(); + // No channels set - will replicate all + + // OLD API: Add collection with config + replicatorConfig.addCollection(defaultCollection, collectionConfig); + + replicatorConfig.setAuthenticator(new BasicAuthenticator(USERNAME, PASSWORD)); + replicatorConfig.setContinuous(true); + replicatorConfig.setAcceptOnlySelfSignedCerts(false); + + const replicator = await Replicator.create(replicatorConfig); + setReplicator(replicator); + setListOfLogs(prev => [...prev, `Replicator created with OLD API, ID: ${replicator.getId()}`]); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error creating replicator: ${error.message}`]); + } + } + + const startStatusChangeListener = async () => { + try { + if (!replicator) { + setErrorLogs(prev => [...prev, 'Replicator not created yet']); + return; + } + + setListOfLogs(prev => [...prev, 'Starting status change listener']); + + const token = await replicator.addChangeListener((change) => { + const date = new Date().toISOString(); + const status = change.status; + + // Status object has methods (it's a ReplicatorStatus instance in the listener) + const activityLevel = status.getActivityLevel(); + const progress = status.getProgress(); + const error = status.getError(); + + let logMessage = `${date} Status: ${activityLevel}`; + + if (progress) { + logMessage += ` | Progress: ${progress.getCompleted()}/${progress.getTotal()}`; + } + + if (error) { + setErrorLogs(prev => [...prev, `${date} Replicator Error: ${error}`]); + } + + setListOfLogs(prev => [...prev, logMessage]); + }); + + setStatusToken(token); + setListOfLogs(prev => [...prev, `Status change listener started with token: ${token}`]); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error starting status listener: ${error.message}`]); + } + } + + const stopStatusChangeListener = async () => { + try { + if (replicator && statusToken) { + await replicator.removeChangeListener(statusToken); + setStatusToken(''); + setListOfLogs(prev => [...prev, 'Status change listener stopped']); + } else { + setErrorLogs(prev => [...prev, 'No active status listener to stop']); + } + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error stopping status listener: ${error.message}`]); + } + } + + const startDocumentChangeListener = async () => { + try { + if (!replicator) { + setErrorLogs(prev => [...prev, 'Replicator not created yet']); + return; + } + + setListOfLogs(prev => [...prev, 'Starting document change listener']); + + const token = await replicator.addDocumentChangeListener((documentReplication) => { + const date = new Date().toISOString(); + const docs = documentReplication.documents; + const direction = documentReplication.isPush ? 'PUSH' : 'PULL'; + + docs.forEach(doc => { + const flags = doc.flags ? doc.flags.join(', ') : 'none'; + const error = doc.error ? ` | Error: ${doc.error}` : ''; + const logMessage = `${date} ${direction} - Scope: ${doc.scopeName}, Collection: ${doc.collectionName}, ID: ${doc.id}, Flags: [${flags}]${error}`; + setListOfLogs(prev => [...prev, logMessage]); + }); + }); + + setDocumentToken(token); + setListOfLogs(prev => [...prev, `Document change listener started with token: ${token}`]); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error starting document listener: ${error.message}`]); + } + } + + const stopDocumentChangeListener = async () => { + try { + if (replicator && documentToken) { + await replicator.removeChangeListener(documentToken); + setDocumentToken(''); + setListOfLogs(prev => [...prev, 'Document change listener stopped']); + } else { + setErrorLogs(prev => [...prev, 'No active document listener to stop']); + } + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error stopping document listener: ${error.message}`]); + } + } + + const startReplicator = async () => { + try { + if (replicator) { + setListOfLogs(prev => [...prev, 'Starting replicator']); + await replicator.start(false); + setListOfLogs(prev => [...prev, 'Replicator started']); + } else { + setErrorLogs(prev => [...prev, 'Replicator not created yet']); + } + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error starting replicator: ${error.message}`]); + } + } + + const stopReplicator = async () => { + try { + if (replicator) { + setListOfLogs(prev => [...prev, 'Stopping replicator']); + await replicator.stop(); + setListOfLogs(prev => [...prev, 'Replicator stopped']); + } else { + setErrorLogs(prev => [...prev, 'Replicator not created yet']); + } + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error stopping replicator: ${error.message}`]); + } + } + + const createDocument = async () => { + setListOfLogs(prev => [...prev, 'Creating Document']); + try { + const defaultCollection = await database?.defaultCollection(); + + if (!defaultCollection) { + setErrorLogs(prev => [...prev, 'Could not get default collection']); + return; + } + + const doc = new MutableDocument(); + doc.setString('type', 'test-replication-old-api'); + doc.setString('name', `Test Doc ${Date.now()}`); + doc.setString('description', 'This document should replicate to server (OLD API)'); + doc.setNumber('value', Math.floor(Math.random() * 1000)); + doc.setDate('createdAt', new Date()); + + await defaultCollection.save(doc); + setListOfLogs(prev => [...prev, `Document created with ID: ${doc.getId()}`]); + setListOfDocuments(prev => [...prev, doc.getId()]); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error creating document: ${error.message}`]); + } + } + + const getReplicatorStatus = async () => { + try { + if (replicator) { + const status: any = await replicator.getStatus(); + + // Status is returned as a plain object from native, not a ReplicatorStatus instance + const activityLevel = status.activityLevel; + const progress = status.progress; + const error = status.error; + + let statusMessage = `Current Status: ${activityLevel}`; + if (progress) { + statusMessage += ` | Progress: ${progress.completed}/${progress.total}`; + } + if (error) { + statusMessage += ` | Error: ${error}`; + } + + setListOfLogs(prev => [...prev, statusMessage]); + } else { + setErrorLogs(prev => [...prev, 'Replicator not created yet']); + } + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error getting status: ${error.message}`]); + } + } + + return ( + + + + + + Replicator Listeners Test - OLD API + + + Using deprecated CollectionConfig and ReplicatorConfiguration(endpoint) constructor + + + Setup Steps: +