From e39bba8ac5d2459f721e22a703de7fe80951ac29 Mon Sep 17 00:00:00 2001 From: jayant dhingra Date: Tue, 14 Oct 2025 22:11:02 +0530 Subject: [PATCH 01/20] chore: update submodule branches to main and upgrade CouchbaseLite-Swift-Enterprise to version 3.3.0 --- .gitmodules | 7 +++++++ android/build.gradle | 2 +- cbl-reactnative.podspec | 2 +- expo-example/cblite-js-tests | 2 +- expo-example/ios/Podfile.lock | 10 +++++----- expo-example/package-lock.json | 2 +- ios/cbl-js-swift | 2 +- src/cblite-js | 2 +- 8 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.gitmodules b/.gitmodules index beb5f31..e8e471c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,19 @@ [submodule "src/cblite-js"] path = src/cblite-js url = git@github.com:Couchbase-Ecosystem/cblite-js.git + branch = main + [submodule "ios/cbl-js-swift"] path = ios/cbl-js-swift url = git@github.com:Couchbase-Ecosystem/cbl-js-swift.git + branch = main + [submodule "android/src/main/java/com/cblreactnative/cbl-js-kotlin"] path = android/src/main/java/com/cblreactnative/cbl-js-kotlin url = git@github.com:Couchbase-Ecosystem/cbl-js-kotlin.git + branch = main + [submodule "expo-example/cblite-js-tests"] path = expo-example/cblite-js-tests url = git@github.com:Couchbase-Ecosystem/cblite-js-tests.git + branch = main \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 6e42986..1bece57 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -107,7 +107,7 @@ dependencies { implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.json:json:20240303" - implementation "com.couchbase.lite:couchbase-lite-android-ee-ktx:3.2.1" + implementation "com.couchbase.lite:couchbase-lite-android-ee-ktx:3.3.0" implementation 'com.eclipsesource.j2v8:j2v8:6.2.1@aar' } diff --git a/cbl-reactnative.podspec b/cbl-reactnative.podspec index 739acf4..53ab0bb 100644 --- a/cbl-reactnative.podspec +++ b/cbl-reactnative.podspec @@ -15,7 +15,7 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/Couchbase-Ecosystem/cbl-reactnative", :tag => "#{s.version}" } s.swift_version = '5.5' - s.dependency 'CouchbaseLite-Swift-Enterprise', '3.2.1' + s.dependency 'CouchbaseLite-Swift-Enterprise', '3.3.0' s.source_files = "ios/**/*.{h,m,mm,swift}" # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. diff --git a/expo-example/cblite-js-tests b/expo-example/cblite-js-tests index add3796..254d427 160000 --- a/expo-example/cblite-js-tests +++ b/expo-example/cblite-js-tests @@ -1 +1 @@ -Subproject commit add3796ec019e40f0b9d106302d192553fc849d6 +Subproject commit 254d427003637e7663c45ef345856b5c360660a9 diff --git a/expo-example/ios/Podfile.lock b/expo-example/ios/Podfile.lock index 28665e7..29b2f7c 100644 --- a/expo-example/ios/Podfile.lock +++ b/expo-example/ios/Podfile.lock @@ -1,7 +1,7 @@ PODS: - boost (1.84.0) - - cbl-reactnative (0.6.1): - - CouchbaseLite-Swift-Enterprise (= 3.2.1) + - cbl-reactnative (0.6.3): + - CouchbaseLite-Swift-Enterprise (= 3.3.0) - DoubleConversion - glog - hermes-engine @@ -22,7 +22,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - CouchbaseLite-Swift-Enterprise (3.2.1) + - CouchbaseLite-Swift-Enterprise (3.3.0) - DoubleConversion (1.1.6) - EXConstants (17.0.3): - ExpoModulesCore @@ -2185,8 +2185,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - cbl-reactnative: 9eee8c5a18460cfb06d338ea1f6415145d47063e - CouchbaseLite-Swift-Enterprise: 6a1eddeed0b450d00d2336bcf60c9a71e228f0e4 + cbl-reactnative: 26a0bae558743232f712dd55db1c2fdd348cd488 + CouchbaseLite-Swift-Enterprise: 6ad2f9e69180d8093753aa213cdcf90502989a0f DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 EXConstants: 277129d9a42ba2cf1fad375e7eaa9939005c60be EXJSONUtils: 01fc7492b66c234e395dcffdd5f53439c5c29c93 diff --git a/expo-example/package-lock.json b/expo-example/package-lock.json index e6e9327..ae0ad16 100644 --- a/expo-example/package-lock.json +++ b/expo-example/package-lock.json @@ -50,7 +50,7 @@ } }, "..": { - "version": "0.6.1", + "version": "0.6.3", "license": "Apache-2.0", "workspaces": [ "example" diff --git a/ios/cbl-js-swift b/ios/cbl-js-swift index d2040e6..05ef196 160000 --- a/ios/cbl-js-swift +++ b/ios/cbl-js-swift @@ -1 +1 @@ -Subproject commit d2040e6a39a9bf40f085a3c790e08eedf7820a41 +Subproject commit 05ef196d158bdcd49c51df6e59d44be9b4332b44 diff --git a/src/cblite-js b/src/cblite-js index 063f796..7f1a722 160000 --- a/src/cblite-js +++ b/src/cblite-js @@ -1 +1 @@ -Subproject commit 063f796bb3a984df820065e4b63f7ef9ad9a5bf4 +Subproject commit 7f1a7225f4a67dafa877692faacae50395a49a4f From d9f00a202009be5e939241848690e69a184f8c88 Mon Sep 17 00:00:00 2001 From: jayant dhingra Date: Tue, 14 Oct 2025 22:32:57 +0530 Subject: [PATCH 02/20] feat: implement placeholder methods for URLEndpointListener in CblReactNativeEngine --- src/CblReactNativeEngine.tsx | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/CblReactNativeEngine.tsx b/src/CblReactNativeEngine.tsx index bcf5865..2f2ec8d 100644 --- a/src/CblReactNativeEngine.tsx +++ b/src/CblReactNativeEngine.tsx @@ -47,6 +47,10 @@ import { ScopeArgs, ScopesResult, DocumentGetBlobContentArgs, + URLEndpointListenerCreateArgs, + URLEndpointListenerArgs, + URLEndpointListenerTLSIdentityArgs, + URLEndpointListenerStatus, } from './cblite-js/cblite/core-types'; import { EngineLocator } from './cblite-js/cblite/src/engine-locator'; @@ -1475,6 +1479,41 @@ export class CblReactNativeEngine implements ICoreEngine { }); } + URLEndpointListener_createListener( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + args: URLEndpointListenerCreateArgs + ): Promise<{ listenerId: string }> { + return Promise.reject(new Error('URLEndpointListener not implemented yet')); + } + + URLEndpointListener_startListener( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + args: URLEndpointListenerArgs + ): Promise { + return Promise.reject(new Error('URLEndpointListener not implemented yet')); + } + + URLEndpointListener_stopListener( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + args: URLEndpointListenerArgs + ): Promise { + return Promise.reject(new Error('URLEndpointListener not implemented yet')); + } + + URLEndpointListener_getStatus( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + args: URLEndpointListenerArgs + ): Promise { + return Promise.reject(new Error('URLEndpointListener not implemented yet')); + } + + URLEndpointListener_deleteIdentity( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + args: URLEndpointListenerTLSIdentityArgs + ): Promise { + return Promise.reject(new Error('URLEndpointListener not implemented yet')); + } + getUUID(): string { return uuid.v4().toString(); } From 24581efbea9993aba52a0ba93494bc1b2e07fab2 Mon Sep 17 00:00:00 2001 From: jayant dhingra Date: Tue, 21 Oct 2025 12:11:30 +0530 Subject: [PATCH 03/20] feat: add collection change event listener functionality - Add collection change listener tests - Add document change listener tests - Add custom bug fix tests - Update CblReactnativeModule.kt for Android - Update CblReactNativeEngine.tsx - Update iOS Swift bridge code - Update test navigation sections - Update cblite-js tests --- .../cblreactnative/CblReactnativeModule.kt | 21 +- couchbase-lite-ios | 1 + .../app/tests/collection-change-listener.tsx | 244 ++++++++++++++++++ expo-example/app/tests/custom-bug-fix.tsx | 201 +++++++++++++++ .../app/tests/document-change-listener.tsx | 211 +++++++++++++++ expo-example/cblite-js-tests | 2 +- .../hooks/useTestsNavigationSections.tsx | 15 ++ expo-example/ios/Podfile.lock | 4 +- ios/cbl-js-swift | 2 +- src/CblReactNativeEngine.tsx | 13 +- src/cblite-js | 2 +- 11 files changed, 703 insertions(+), 13 deletions(-) create mode 160000 couchbase-lite-ios create mode 100644 expo-example/app/tests/collection-change-listener.tsx create mode 100644 expo-example/app/tests/custom-bug-fix.tsx create mode 100644 expo-example/app/tests/document-change-listener.tsx diff --git a/android/src/main/java/com/cblreactnative/CblReactnativeModule.kt b/android/src/main/java/com/cblreactnative/CblReactnativeModule.kt index 24aac31..ba37e63 100644 --- a/android/src/main/java/com/cblreactnative/CblReactnativeModule.kt +++ b/android/src/main/java/com/cblreactnative/CblReactnativeModule.kt @@ -33,6 +33,7 @@ class CblReactnativeModule(reactContext: ReactApplicationContext) : private val replicatorChangeListeners: MutableMap = mutableMapOf() private val replicatorDocumentListeners: MutableMap = mutableMapOf() private val collectionChangeListeners: MutableMap = mutableMapOf() + private val collectionDocumentChangeListeners: MutableMap = mutableMapOf() init { CouchbaseLite.init(context, true) @@ -644,6 +645,7 @@ fun collection_RemoveChangeListener( ) { GlobalScope.launch(Dispatchers.IO) { try { + // Check for collection change listeners if (collectionChangeListeners.containsKey(changeListenerToken)) { val listener = collectionChangeListeners[changeListenerToken] listener?.remove() @@ -651,10 +653,23 @@ fun collection_RemoveChangeListener( context.runOnUiQueueThread { promise.resolve(null) } - } else { + return@launch + } + + // Check for document change listeners + if (collectionDocumentChangeListeners.containsKey(changeListenerToken)) { + val listener = collectionDocumentChangeListeners[changeListenerToken] + listener?.remove() + collectionDocumentChangeListeners.remove(changeListenerToken) context.runOnUiQueueThread { - promise.reject("COLLECTION_ERROR", "No such listener found with token $changeListenerToken") + promise.resolve(null) } + return@launch + } + + // No listener found + context.runOnUiQueueThread { + promise.reject("COLLECTION_ERROR", "No such listener found with token $changeListenerToken") } } catch (e: Throwable) { context.runOnUiQueueThread { @@ -700,7 +715,7 @@ fun collection_AddDocumentChangeListener( } } - collectionChangeListeners[changeListenerToken] = listener + collectionDocumentChangeListeners[changeListenerToken] = listener context.runOnUiQueueThread { promise.resolve(null) } diff --git a/couchbase-lite-ios b/couchbase-lite-ios new file mode 160000 index 0000000..b9c588d --- /dev/null +++ b/couchbase-lite-ios @@ -0,0 +1 @@ +Subproject commit b9c588d5abb46e614ee9e4564ac34790705b0f61 diff --git a/expo-example/app/tests/collection-change-listener.tsx b/expo-example/app/tests/collection-change-listener.tsx new file mode 100644 index 0000000..ed38c4f --- /dev/null +++ b/expo-example/app/tests/collection-change-listener.tsx @@ -0,0 +1,244 @@ +import React, { useState } from 'react'; +import TestRunnerContainer from '@/components/TestRunnerContainer/TestRunnerContainer'; +import { SafeAreaView, Text, Button, ScrollView} from 'react-native'; +import { View } from '@/components/Themed/Themed'; +import { + Database, + DatabaseConfiguration, + Replicator, + ReplicatorConfiguration, + URLEndpoint, + BasicAuthenticator, + SessionAuthenticator, + ReplicatorType, + Collection, + MutableDocument, + Document, + ConcurrencyControl + } from 'cbl-reactnative';import getFileDefaultPath from '@/service/file/getFileDefaultPath'; + +export default function CollectionChangeListenerScreen() { + + // open a database + // create a collection + // add a collecion change listener + + + + const [listOfLogs, setListOfLogs] = useState([]); + const [errorLogs, setErrorLogs] = useState([]); + + const [database, setDatabase] = useState(null); + const [collection, setCollection] = useState(null); + const [replicator, setReplicator] = useState(null); + + const [listOfDocuments, setListOfDocuments] = useState([]); + + const [token, setToken] = useState(''); + + + + const openDatabase = async () => { + try { + setListOfLogs(prev => [...prev, 'Opening Database']); // ✅ Use prev + const databaseName = 'databse_name_random'; + 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()} at default directory`]); // ✅ Use prev + setDatabase(database); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error opening database: ${error.message}`]); // ✅ Use prev + } + } + + const createCollection = async () => { + try { + setListOfLogs(prev => [...prev, 'Creating Collection']); // ✅ Use prev + const collection = await database?.createCollection('test_collection'); + if (collection) { + setCollection(collection); + setListOfLogs(prev => [...prev, `Collection created`]); // ✅ Use prev + } + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error creating collection: ${error.message}`]); + } + } + + const startCollectionChangeListener = async () => { + try{ + if (collection) { + setListOfLogs(prev => [...prev, `Starting collection change listener`]); + const token = await collection.addChangeListener((change) => { + setListOfLogs(prev => [...prev, `Collection changed:`, JSON.stringify(change)]); + }); + setToken(token); + + } + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error starting collection change listener: ${error.message}`]); + } + } + + const stopCollectionChangeListener = async () => { + try{ + if (collection) { + if (token) { + await collection.removeChangeListener(token); + setToken(''); + setListOfLogs(prev => [...prev, `Collection change listener stopped`]); + } else { + setErrorLogs(prev => [...prev, `No token found to stop collection change listener`]); + } + } + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error stopping collection change listener: ${error.message}`]); + } + } + + // const connectToSyncGateway = async () => { + // setListOfLogs(prev => [...prev, 'Connecting to Sync Gateway']); // ✅ Use prev + // const defaultCollection = await database?.defaultCollection(); + + // const syncGatewayUrl = "wss://nasm0fvdr-jnehnb.apps.cloud.couchbase.com:4984/testendpoint" + // const endpoint = new URLEndpoint(syncGatewayUrl); + // const username = "jayantdhingra" + // const password = "f9yu5QT4B5jpZep@" + + // const replicatorConfig = new ReplicatorConfiguration(endpoint) + // replicatorConfig.setAuthenticator(new BasicAuthenticator(username, password)) + // // replicatorConfig.setContinuous(true) + // replicatorConfig.setAcceptOnlySelfSignedCerts(false); + + + // if (defaultCollection) { + // replicatorConfig.addCollection(defaultCollection) + // } + + // const replicator = await Replicator.create(replicatorConfig) + + // replicator.addChangeListener((change) => { + // const status = change.status; + + // setListOfLogs(prev => [...prev, `Replicator changed:, ${status}`]); + + // if (status.getError()) { + // setErrorLogs(prev => [...prev, `Replication error: ${status.getError()}`]); + // } + + // }) + + // await replicator.start(false) + // setReplicator(replicator); + + // setListOfLogs(prev => [...prev, `Replicator created`]); // ✅ Use prev + // } + + + const createDocument = async () => { + setListOfLogs(prev => [...prev, 'Creating Document']); // ✅ Use prev + try { + const doc = new MutableDocument() + // const defaultCollection = await database?.defaultCollection(); + + + doc.setString('name', 'Jayanthealthy') + doc.setString('email', 'john@example.com') + doc.setString('phone', '1234567890') + doc.setString('address', '123 Main St, Anytown, USA') + doc.setString('city', 'Anytown') + doc.setString('state', 'CA') + doc.setString('zip', '12345') + doc.setNumber('age', 30) + doc.setBoolean('isActive', true) + doc.setDate('createdAt', new Date()) + doc.setArray('tags', ['tag1', 'tag2', 'tag3']) + doc.setDictionary('address', {street: '123 Main St', city: 'Anytown', state: 'CA', zip: '12345'}) + + + await collection?.save(doc) + setListOfLogs(prev => [...prev, `Document created`]); // ✅ Use prev + setListOfDocuments(prev => [...prev, doc.getId()]); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error creating document: ${error.message}`]); // ✅ Use prev + } + + } + + + // const deleteDocument = async (docId: string) => { + // setListOfLogs(prev => [...prev, 'Deleting Document']); // ✅ Use prev + // try { + // const defaultCollection = await database?.defaultCollection(); + // const doc = await defaultCollection?.document(docId) + // if(doc){ + // await defaultCollection?.deleteDocument(doc) + // setListOfDocuments(prev => prev.filter(id => id !== docId)); + // setListOfLogs(prev => [...prev, `Document deleted`]); // ✅ Use prev + // } else { + // setErrorLogs(prev => [...prev, `Document not found`]); // ✅ Use prev + // } + + // } catch (error) { + // // @ts-ignore + // setErrorLogs(prev => [...prev, `Error deleting document: ${error.message}`]); // ✅ Use prev + // } + // } + + + + return ( + + + + + ))} + + )} + + + + + + ); +} + diff --git a/expo-example/app/logsinks/custom.tsx b/expo-example/app/logsinks/custom.tsx new file mode 100644 index 0000000..ccd3638 --- /dev/null +++ b/expo-example/app/logsinks/custom.tsx @@ -0,0 +1,143 @@ +import React, { useState, useEffect } from 'react'; +import { SafeAreaView, View, ScrollView } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { useStyleScheme, Text } from '@/components/Themed/Themed'; +import ResultListView from '@/components/ResultsListView/ResultsListView'; +import useNavigationBarTitleResetOption from '@/hooks/useNavigationBarTitleResetOption'; +import { useLogDomainAsValues } from '@/hooks/useLogDomain'; +import HeaderRunActionView from '@/components/HeaderRunActionView/HeaderRunActionView'; +import SelectKeyValue from '@/components/SelectKeyValue/SelectKeyValue'; +import { useLogLevelAsValues } from '@/hooks/useLogLevel'; +import { Divider, Button, ButtonText } from '@gluestack-ui/themed'; +import setCustomSink from '@/service/logsinks/setCustomSink'; +import disableCustomSink from '@/service/logsinks/disableCustomSink'; + +export default function LogSinksCustomScreen() { + const [selectedLogLevel, setSelectedLogLevel] = useState(''); + const [selectedDomains, setSelectedDomains] = useState([]); + const [resultMessage, setResultsMessage] = useState([]); + const [isListening, setIsListening] = useState(false); + const [logMessages, setLogMessages] = useState([]); + const navigation = useNavigation(); + const styles = useStyleScheme(); + useNavigationBarTitleResetOption('LogSinks Custom', navigation, reset); + + const logDomains = useLogDomainAsValues(); + const logLevels = useLogLevelAsValues(); + + function reset() { + setSelectedLogLevel(''); + setSelectedDomains([]); + setResultsMessage([]); + setLogMessages([]); + setIsListening(false); + } + + const enable = async () => { + if (selectedLogLevel === '') { + setResultsMessage((prev) => [...prev, '❌ Please select a log level']); + return; + } + + // Callback that receives logs from native + const callback = (level: number, domain: string, message: string) => { + const timestamp = new Date().toLocaleTimeString(); + const logEntry = `[${timestamp}] [${domain}] ${message}`; + setLogMessages((prev) => [...prev, logEntry]); + }; + + const result = await setCustomSink(selectedLogLevel, selectedDomains, callback); + setResultsMessage((prev) => [...prev, result]); + setIsListening(true); + }; + + const disable = async () => { + const result = await disableCustomSink(); + setResultsMessage((prev) => [...prev, result]); + setIsListening(false); + setLogMessages([]); + }; + + const clearLogs = () => { + setLogMessages([]); + }; + + return ( + + + + + + + { + if (value && !selectedDomains.includes(value)) { + setSelectedDomains([...selectedDomains, value]); + } + }} + placeholder="Add Domain" + items={logDomains} + /> + {selectedDomains.length > 0 && ( + + {selectedDomains.map((domain, index) => ( + + ))} + + )} + + + + + + + + + + {isListening && ( + + + ✅ Listening for logs... ({logMessages.length} received) + + + )} + + + + Status Messages: + + + + {logMessages.length > 0 && ( + + Received Logs: + + + )} + + + ); +} + diff --git a/expo-example/app/logsinks/file.tsx b/expo-example/app/logsinks/file.tsx new file mode 100644 index 0000000..f577a54 --- /dev/null +++ b/expo-example/app/logsinks/file.tsx @@ -0,0 +1,217 @@ +import React, { useState } from 'react'; +import { + TextInput, + useColorScheme, + View, + ScrollView, + StyleSheet, + SafeAreaView, +} from 'react-native'; +import { Switch, Divider, Button, ButtonText } from '@gluestack-ui/themed'; +import { useNavigation } from '@react-navigation/native'; +import { + useStyleScheme, + useThemeColor, + Text, +} from '@/components/Themed/Themed'; +import ResultListView from '@/components/ResultsListView/ResultsListView'; +import useNavigationBarTitleResetOption from '@/hooks/useNavigationBarTitleResetOption'; +import SelectKeyValue from '@/components/SelectKeyValue/SelectKeyValue'; +import { useLogLevelAsValues } from '@/hooks/useLogLevel'; +import { usePlaceholderTextColor } from '@/hooks/usePlaceholderTextColor'; +import getFileDefaultPath from '@/service/file/getFileDefaultPath'; +import HeaderToolbarView from '@/components/HeaderToolbarView/HeaderToolbarView'; +import setFileSink from '@/service/logsinks/setFileSink'; +import disableFileSink from '@/service/logsinks/disableFileSink'; + +export default function LogSinksFileScreen() { + const [selectedLogLevel, setSelectedLogLevel] = useState(''); + const [directory, setDirectory] = useState(''); + const [maxRotateCount, setMaxRotateCount] = useState('5'); + const [maxSize, setMaxSize] = useState('10485760'); + const [usePlainText, setUsePlainText] = useState(false); + const [resultMessage, setResultsMessage] = useState([]); + + const navigation = useNavigation(); + useNavigationBarTitleResetOption('LogSinks File', navigation, reset); + + const scheme = useColorScheme(); + const styles = useStyleScheme(); + const textColor = useThemeColor({ light: 'black', dark: 'white' }, 'text'); + const placeholderTextColor = usePlaceholderTextColor(scheme); + const logLevels = useLogLevelAsValues(); + + function reset() { + setDirectory(''); + setMaxRotateCount('5'); + setMaxSize('10485760'); + setUsePlainText(false); + setSelectedLogLevel(''); + setResultsMessage([]); + } + + const enable = async () => { + const numericRegex = /^[0-9]*$/; + let maxRotateCountInt = 5; + let maxSizeInt = 10485760; + + if (numericRegex.test(maxRotateCount) && numericRegex.test(maxSize)) { + maxRotateCountInt = parseInt(maxRotateCount, 10); + maxSizeInt = parseInt(maxSize, 10); + } else { + setResultsMessage((prev) => [ + ...prev, + '❌ Max Rotate Count and Max Size must be numeric values', + ]); + return; + } + if (selectedLogLevel === '') { + setResultsMessage((prev) => [...prev, '❌ Please select a log level']); + return; + } + if (directory === '') { + setResultsMessage((prev) => [...prev, '❌ Please enter a directory path']); + return; + } + + const result = await setFileSink( + selectedLogLevel, + directory, + maxRotateCountInt, + maxSizeInt, + usePlainText + ); + setResultsMessage((prev) => [...prev, result]); + }; + + const disable = async () => { + const result = await disableFileSink(); + setResultsMessage((prev) => [...prev, result]); + }; + + const handleLocationPress = async () => { + try { + const defaultPath = await getFileDefaultPath(); + setDirectory(defaultPath); + } catch (error: any) { + setResultsMessage((prev) => [...prev, `❌ ${error.message}`]); + } + }; + + const icons = [ + { + iconName: 'folder-open', + onPress: handleLocationPress, + }, + { + iconName: 'play', + onPress: enable, + }, + ]; + + return ( + + + + + setDirectory(newText)} + value={directory} + multiline={true} + /> + + + + + + + Max Rotate Count + + + + + Max Size (in bytes) + + + + + + Use Plain Text + + + + + + + + + + + ); +} + +const fileStyles = StyleSheet.create({ + divider: { + marginTop: 12, + marginBottom: 12, + }, + logDirectory: { + height: undefined, + minHeight: 80, + }, + maxSizeInput: { + height: undefined, + minHeight: 30, + }, + switch: { + paddingRight: 16, + }, +}); + diff --git a/expo-example/app/tests/console-logging.tsx b/expo-example/app/tests/console-logging.tsx new file mode 100644 index 0000000..e7206ae --- /dev/null +++ b/expo-example/app/tests/console-logging.tsx @@ -0,0 +1,461 @@ +import React, { useState } from 'react'; +import { SafeAreaView, Text, Button, ScrollView, StyleSheet } from 'react-native'; +import { View } from '@/components/Themed/Themed'; +import { + Database, + DatabaseConfiguration, + Collection, + MutableDocument, + LogSinks, + LogLevel, + LogDomain +} from 'cbl-reactnative'; +import getFileDefaultPath from '@/service/file/getFileDefaultPath'; + +export default function ConsoleLoggingTestScreen() { + const [listOfLogs, setListOfLogs] = useState([]); + const [errorLogs, setErrorLogs] = useState([]); + + const [database, setDatabase] = useState(null); + const [collection, setCollection] = useState(null); + const [listOfDocuments, setListOfDocuments] = useState([]); + const [currentLogLevel, setCurrentLogLevel] = useState('NONE'); + const [currentDomains, setCurrentDomains] = useState('NONE'); + + // Console Logging Setup Functions + const enableConsoleLogging = async (level: LogLevel, domains: LogDomain[]) => { + try { + await LogSinks.setConsole({ + level: level, + domains: domains + }); + + const levelName = LogLevel[level]; + const domainNames = domains.map(d => d.toString()).join(', '); + + setCurrentLogLevel(levelName); + setCurrentDomains(domainNames); + setListOfLogs(prev => [...prev, `✅ Console logging enabled: Level=${levelName}, Domains=[${domainNames}]`]); + setListOfLogs(prev => [...prev, `⚠️ Check your terminal/console for Couchbase Lite logs!`]); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error enabling console logging: ${error.message}`]); + } + } + + const disableConsoleLogging = async () => { + try { + await LogSinks.setConsole(null); + setCurrentLogLevel('NONE'); + setCurrentDomains('NONE'); + setListOfLogs(prev => [...prev, `🚫 Console logging disabled`]); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error disabling console logging: ${error.message}`]); + } + } + + // Database Operations to Trigger Logs + const openDatabase = async () => { + try { + setListOfLogs(prev => [...prev, '📂 Opening Database (should trigger DATABASE logs)...']); + const databaseName = 'console_logging_test_db'; + const directory = await getFileDefaultPath(); + const dbConfig = new DatabaseConfiguration(); + const database = new Database(databaseName, dbConfig); + await database.open(); + setListOfLogs(prev => [...prev, `✅ Database opened: ${database.getName()}`]); + setDatabase(database); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error opening database: ${error.message}`]); + } + } + + const closeDatabase = async () => { + try { + if (!database) { + setErrorLogs(prev => [...prev, 'No database to close']); + return; + } + setListOfLogs(prev => [...prev, '🔒 Closing Database (should trigger DATABASE logs)...']); + await database.close(); + setListOfLogs(prev => [...prev, `✅ Database closed`]); + setDatabase(null); + setCollection(null); + setListOfDocuments([]); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error closing database: ${error.message}`]); + } + } + + const createCollection = async () => { + try { + if (!database) { + setErrorLogs(prev => [...prev, 'Database not opened']); + return; + } + setListOfLogs(prev => [...prev, '📁 Creating Collection (should trigger DATABASE logs)...']); + const collection = await database.createCollection('console_test_collection'); + if (collection) { + setCollection(collection); + setListOfLogs(prev => [...prev, `✅ Collection created`]); + } + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error creating collection: ${error.message}`]); + } + } + + const createDocument = async () => { + try { + if (!collection) { + setErrorLogs(prev => [...prev, 'Collection not created']); + return; + } + setListOfLogs(prev => [...prev, '📄 Creating Document (should trigger DATABASE logs)...']); + + const doc = new MutableDocument(); + doc.setString('type', 'test'); + doc.setString('name', 'Console Test Document'); + doc.setNumber('value', Math.floor(Math.random() * 100)); + doc.setDate('createdAt', new Date()); + + await collection.save(doc); + setListOfLogs(prev => [...prev, `✅ Document created: ${doc.getId()}`]); + setListOfDocuments(prev => [...prev, doc.getId()]); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error creating document: ${error.message}`]); + } + } + + const updateDocument = async () => { + try { + if (!collection || listOfDocuments.length === 0) { + setErrorLogs(prev => [...prev, 'No documents to update']); + return; + } + + const docId = listOfDocuments[listOfDocuments.length - 1]; + setListOfLogs(prev => [...prev, `✏️ Updating Document (should trigger DATABASE logs)...`]); + + const doc = await collection.document(docId); + if (doc) { + const mutableDoc = new MutableDocument(doc.getId(), doc.toDictionary()); + mutableDoc.setNumber('value', Math.floor(Math.random() * 100)); + mutableDoc.setDate('updatedAt', new Date()); + + await collection.save(mutableDoc); + setListOfLogs(prev => [...prev, `✅ Document updated: ${docId}`]); + } + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error updating document: ${error.message}`]); + } + } + + const deleteDocument = async () => { + try { + if (!collection || listOfDocuments.length === 0) { + setErrorLogs(prev => [...prev, 'No documents to delete']); + return; + } + + const docId = listOfDocuments[listOfDocuments.length - 1]; + setListOfLogs(prev => [...prev, `🗑️ Deleting Document (should trigger DATABASE logs)...`]); + + const doc = await collection.document(docId); + if (doc) { + await collection.deleteDocument(doc); + setListOfDocuments(prev => prev.filter(id => id !== docId)); + setListOfLogs(prev => [...prev, `✅ Document deleted: ${docId}`]); + } + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error deleting document: ${error.message}`]); + } + } + + const runQuery = async () => { + try { + if (!database) { + setErrorLogs(prev => [...prev, 'Database not opened']); + return; + } + setListOfLogs(prev => [...prev, '🔍 Running Query (should trigger QUERY logs)...']); + + const queryString = `SELECT * FROM _default.console_test_collection WHERE type = 'test'`; + const query = database.createQuery(queryString); + const results = await query.execute(); + + setListOfLogs(prev => [...prev, `✅ Query executed: Found ${results.length} document(s)`]); + } catch (error) { + // @ts-ignore + setErrorLogs(prev => [...prev, `Error running query: ${error.message}`]); + } + } + + return ( + + + + Console Logging Test + + + + ⚠️ Check your terminal/console for Couchbase Lite logs! + + + Current Level: {currentLogLevel} | Domains: {currentDomains} + + + + 1. Configure Console Logging + + + Log Levels: + + + + + + + + + Enable with Specific Domains: + + + + + + + + + + + + + + 2. Trigger Log Events + + + Database Operations: + + + + + + + + + + Document & Query Operations: + + + + + + + + + + + Activity Logs + + + {listOfLogs.length === 0 ? ( + No activity yet. Enable custom logging and trigger operations above. + ) : ( + listOfLogs.map((log, index) => ( + + {log} + + )) + )} + + + {capturedLogs.length > 0 && ( + <> + + 🎯 Captured Logs ({capturedLogs.length}) + + + {capturedLogs.map((log, index) => ( + + {log} + + ))} + + + )} + + {errorLogs.length > 0 && ( + <> + Errors + + {errorLogs.map((log, index) => ( + + {log} + + ))} + + + )} + + + + ); +} + +const localStyles = StyleSheet.create({ + container: { + flex: 1, + }, + scrollView: { + flex: 1, + }, + content: { + padding: 10, + }, + title: { + fontSize: 24, + fontWeight: 'bold', + marginBottom: 15, + textAlign: 'center', + }, + sectionTitle: { + fontSize: 16, + fontWeight: 'bold', + marginTop: 15, + marginBottom: 10, + }, + warningBox: { + backgroundColor: '#e8f5e9', + padding: 15, + borderRadius: 5, + marginBottom: 15, + borderWidth: 1, + borderColor: '#4caf50', + }, + warningText: { + fontSize: 16, + fontWeight: 'bold', + color: '#2e7d32', + marginBottom: 5, + }, + statusText: { + fontSize: 11, + color: '#2e7d32', + fontStyle: 'italic', + marginBottom: 3, + }, + listeningText: { + fontSize: 13, + color: '#2e7d32', + fontWeight: 'bold', + marginTop: 5, + }, + buttonGroup: { + marginBottom: 15, + }, + groupLabel: { + fontSize: 14, + fontWeight: '600', + marginBottom: 8, + color: '#666', + }, + buttonRow: { + flexDirection: 'row', + marginBottom: 5, + }, + buttonSpacer: { + width: 10, + }, + separator: { + height: 20, + }, + logContainer: { + padding: 10, + backgroundColor: '#f5f5f5', + borderRadius: 5, + minHeight: 100, + marginBottom: 15, + }, + logText: { + fontSize: 12, + marginBottom: 5, + lineHeight: 18, + }, + emptyLog: { + fontSize: 12, + fontStyle: 'italic', + color: '#999', + }, + capturedTitle: { + fontSize: 16, + fontWeight: 'bold', + marginTop: 10, + marginBottom: 10, + color: '#4caf50', + }, + capturedContainer: { + padding: 10, + backgroundColor: '#e8f5e9', + borderRadius: 5, + marginBottom: 15, + maxHeight: 300, + }, + capturedText: { + fontSize: 11, + marginBottom: 3, + color: '#2e7d32', + }, + errorTitle: { + fontSize: 16, + fontWeight: 'bold', + marginTop: 10, + marginBottom: 10, + color: 'red', + }, + errorContainer: { + padding: 10, + backgroundColor: '#ffe6e6', + borderRadius: 5, + marginBottom: 15, + }, + errorText: { + color: 'red', + fontSize: 12, + marginBottom: 5, + }, +}); \ No newline at end of file diff --git a/expo-example/app/tests/file-logging.tsx b/expo-example/app/tests/file-logging.tsx new file mode 100644 index 0000000..501abc9 --- /dev/null +++ b/expo-example/app/tests/file-logging.tsx @@ -0,0 +1,399 @@ +import React, { useState } from 'react'; +import { SafeAreaView, ScrollView, StyleSheet } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { useStyleScheme, Text } from '@/components/Themed/Themed'; +import { View } from 'react-native'; +import { Button, ButtonText, Divider } from '@gluestack-ui/themed'; +import { + Database, + DatabaseConfiguration, + Collection, + MutableDocument, + LogSinks, + LogLevel, + LogDomain +} from 'cbl-reactnative'; +import getFileDefaultPath from '@/service/file/getFileDefaultPath'; +import useNavigationBarTitleResetOption from '@/hooks/useNavigationBarTitleResetOption'; +import ResultListView from '@/components/ResultsListView/ResultsListView'; + +export default function FileLoggingTestScreen() { + const [listOfLogs, setListOfLogs] = useState([]); + const [errorLogs, setErrorLogs] = useState([]); + const [database, setDatabase] = useState(null); + const [collection, setCollection] = useState(null); + const [logDirectory, setLogDirectory] = useState(''); + const [currentLogLevel, setCurrentLogLevel] = useState('NONE'); + + const navigation = useNavigation(); + const styles = useStyleScheme(); + useNavigationBarTitleResetOption('File Logging Test', navigation, reset); + + function reset() { + setListOfLogs([]); + setErrorLogs([]); + setDatabase(null); + setCollection(null); + setLogDirectory(''); + setCurrentLogLevel('NONE'); + } + + // File Logging Setup Functions + const enableFileLogging = async (level: LogLevel, usePlaintext: boolean = false) => { + try { + const defaultPath = await getFileDefaultPath(); + const logsDir = `${defaultPath}/test_logs`; + setLogDirectory(logsDir); + + await LogSinks.setFile({ + level: level, + directory: logsDir, + usePlaintext: usePlaintext, + maxFileSize: 1024 * 1024, // 1 MB + maxKeptFiles: 3, + }); + + const levelName = LogLevel[level]; + const format = usePlaintext ? 'PLAINTEXT' : 'BINARY'; + + setCurrentLogLevel(levelName); + setListOfLogs(prev => [...prev, `✅ File logging enabled: Level=${levelName}, Format=${format}`]); + setListOfLogs(prev => [...prev, `📁 Log directory: ${logsDir}`]); + setListOfLogs(prev => [...prev, `⚠️ Check file system for log files!`]); + } catch (error: any) { + setErrorLogs(prev => [...prev, `Error enabling file logging: ${error.message}`]); + } + }; + + const disableFileLogging = async () => { + try { + await LogSinks.setFile(null); + setCurrentLogLevel('NONE'); + setListOfLogs(prev => [...prev, `🚫 File logging disabled`]); + } catch (error: any) { + setErrorLogs(prev => [...prev, `Error disabling file logging: ${error.message}`]); + } + }; + + // Database Operations to Trigger Logs + const openDatabase = async () => { + try { + setListOfLogs(prev => [...prev, '📂 Opening Database (should write DATABASE logs to file)...']); + const databaseName = 'file_logging_test_db'; + const dbConfig = new DatabaseConfiguration(); + const database = new Database(databaseName, dbConfig); + await database.open(); + setListOfLogs(prev => [...prev, `✅ Database opened: ${database.getName()}`]); + setDatabase(database); + } catch (error: any) { + setErrorLogs(prev => [...prev, `Error opening database: ${error.message}`]); + } + }; + + const closeDatabase = async () => { + try { + if (!database) { + setErrorLogs(prev => [...prev, 'No database to close']); + return; + } + setListOfLogs(prev => [...prev, '🔒 Closing Database (should write logs to file)...']); + await database.close(); + setListOfLogs(prev => [...prev, `✅ Database closed`]); + setDatabase(null); + setCollection(null); + } catch (error: any) { + setErrorLogs(prev => [...prev, `Error closing database: ${error.message}`]); + } + }; + + const createCollection = async () => { + try { + if (!database) { + setErrorLogs(prev => [...prev, 'Database not opened']); + return; + } + setListOfLogs(prev => [...prev, '📁 Creating Collection (should write logs to file)...']); + const collection = await database.createCollection('file_test_collection'); + if (collection) { + setCollection(collection); + setListOfLogs(prev => [...prev, `✅ Collection created`]); + } + } catch (error: any) { + setErrorLogs(prev => [...prev, `Error creating collection: ${error.message}`]); + } + }; + + const createDocuments = async () => { + try { + if (!collection) { + setErrorLogs(prev => [...prev, 'Collection not created']); + return; + } + setListOfLogs(prev => [...prev, '📄 Creating 5 Documents (should write logs to file)...']); + + for (let i = 0; i < 5; i++) { + const doc = new MutableDocument(); + doc.setString('type', 'test'); + doc.setString('name', `File Test Document ${i + 1}`); + doc.setNumber('value', Math.floor(Math.random() * 100)); + doc.setDate('createdAt', new Date()); + await collection.save(doc); + } + + setListOfLogs(prev => [...prev, `✅ Created 5 documents`]); + } catch (error: any) { + setErrorLogs(prev => [...prev, `Error creating documents: ${error.message}`]); + } + }; + + const runQuery = async () => { + try { + if (!database) { + setErrorLogs(prev => [...prev, 'Database not opened']); + return; + } + setListOfLogs(prev => [...prev, '🔍 Running Query (should write QUERY logs to file)...']); + + const queryString = `SELECT * FROM _default.file_test_collection WHERE type = 'test'`; + const query = database.createQuery(queryString); + const results = await query.execute(); + + setListOfLogs(prev => [...prev, `✅ Query executed: Found ${results.length} document(s)`]); + } catch (error: any) { + setErrorLogs(prev => [...prev, `Error running query: ${error.message}`]); + } + }; + + return ( + + + + File Logging Test + + + + ⚠️ Check file system for log files! + + + Current Level: {currentLogLevel} | Directory: {logDirectory || 'Not set'} + + + + 1. Configure File Logging + + + Log Levels (Binary Format): + + + + + + + + + + Plaintext Format: + + + + + + + + 2. Trigger Log Events + + + Database Operations: + + + + + + + + + + Document & Query Operations: + + + + + + + + + + + Activity Logs + + + {listOfLogs.length === 0 ? ( + No activity yet. Start by enabling file logging above. + ) : ( + listOfLogs.map((log, index) => ( + + {log} + + )) + )} + + + {errorLogs.length > 0 && ( + <> + Errors + + {errorLogs.map((log, index) => ( + + {log} + + ))} + + + )} + + + + ); +} + +const localStyles = StyleSheet.create({ + container: { + flex: 1, + }, + scrollView: { + flex: 1, + }, + content: { + padding: 10, + }, + title: { + fontSize: 24, + fontWeight: 'bold', + marginBottom: 15, + textAlign: 'center', + }, + sectionTitle: { + fontSize: 16, + fontWeight: 'bold', + marginTop: 15, + marginBottom: 10, + }, + warningBox: { + backgroundColor: '#fff3cd', + padding: 15, + borderRadius: 5, + marginBottom: 15, + borderWidth: 1, + borderColor: '#ffc107', + }, + warningText: { + fontSize: 16, + fontWeight: 'bold', + color: '#856404', + marginBottom: 5, + }, + statusText: { + fontSize: 11, + color: '#856404', + fontStyle: 'italic', + }, + buttonGroup: { + marginBottom: 15, + }, + groupLabel: { + fontSize: 14, + fontWeight: '600', + marginBottom: 8, + color: '#666', + }, + buttonRow: { + flexDirection: 'row', + marginBottom: 5, + }, + buttonSpacer: { + width: 10, + }, + separator: { + height: 20, + }, + logContainer: { + padding: 10, + backgroundColor: '#f5f5f5', + borderRadius: 5, + minHeight: 100, + marginBottom: 15, + }, + logText: { + fontSize: 12, + marginBottom: 5, + lineHeight: 18, + }, + emptyLog: { + fontSize: 12, + fontStyle: 'italic', + color: '#999', + }, + errorTitle: { + fontSize: 16, + fontWeight: 'bold', + marginTop: 10, + marginBottom: 10, + color: 'red', + }, + errorContainer: { + padding: 10, + backgroundColor: '#ffe6e6', + borderRadius: 5, + marginBottom: 15, + }, + errorText: { + color: 'red', + fontSize: 12, + marginBottom: 5, + }, +}); \ No newline at end of file diff --git a/expo-example/app/tests/logsinks.tsx b/expo-example/app/tests/logsinks.tsx new file mode 100644 index 0000000..d8d6879 --- /dev/null +++ b/expo-example/app/tests/logsinks.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import TestRunnerContainer from '@/components/TestRunnerContainer/TestRunnerContainer'; + +import { LogSinksTests } from '../../cblite-js-tests/cblite-tests/e2e/logsinks-test'; + +export default function TestsLogSinksScreen() { + function reset() {} + + async function update(): Promise { + try { + return ['']; + } catch (e) { + // @ts-ignore + return [error.message]; + } + } + + return ( + + ); +} + diff --git a/expo-example/cblite-js-tests b/expo-example/cblite-js-tests index 5b5ff2f..12a7e6b 160000 --- a/expo-example/cblite-js-tests +++ b/expo-example/cblite-js-tests @@ -1 +1 @@ -Subproject commit 5b5ff2f5dd1df1db53b6ae86dc0a3ead05b48814 +Subproject commit 12a7e6ba89ca604f6d2cbe24282f952f9ca808ff diff --git a/expo-example/hooks/useTestsNavigationSections.tsx b/expo-example/hooks/useTestsNavigationSections.tsx index bcb839d..571517d 100644 --- a/expo-example/hooks/useTestsNavigationSections.tsx +++ b/expo-example/hooks/useTestsNavigationSections.tsx @@ -76,9 +76,24 @@ export function useTestsNavigationSections() { }, { id: 15, + title: 'LogSinks API', + path: '/tests/logsinks', + }, + { + id: 16, title: 'Log Sink Console Logs', path: '/tests/console-logging', }, + { + id: 17, + title: 'File Logging Test', + path: '/tests/file-logging', + }, + { + id: 18, + title: 'Custom Logging Test', + path: '/tests/custom-logging', + }, ], }, ]; diff --git a/ios/CblReactnative.mm b/ios/CblReactnative.mm index 8eb92d4..fa7a8b8 100644 --- a/ios/CblReactnative.mm +++ b/ios/CblReactnative.mm @@ -245,21 +245,21 @@ @interface RCT_EXTERN_MODULE(CblReactnative, RCTEventEmitter) // MARK: - LogSinks Functions RCT_EXTERN_METHOD(logsinks_SetConsole: - (NSNumber *)level - withDomains:(NSArray *)domains + (nonnull NSNumber *)level + withDomains:(nonnull NSArray *)domains withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(logsinks_SetFile: - (NSNumber *)level - withConfig:(NSDictionary *)config + (nonnull NSNumber *)level + withConfig:(nonnull NSDictionary *)config withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(logsinks_SetCustom: - (NSNumber *)level - withDomains:(NSArray *)domains - withToken:(NSString *)token + (nonnull NSNumber *)level + withDomains:(nonnull NSArray *)domains + withToken:(nonnull NSString *)token withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) diff --git a/ios/CblReactnative.swift b/ios/CblReactnative.swift index a9c22a2..a8f925b 100644 --- a/ios/CblReactnative.swift +++ b/ios/CblReactnative.swift @@ -32,6 +32,17 @@ class CblReactnative: RCTEventEmitter { override init() { super.init() } + + // Track whether JavaScript is listening for events + // Note: These may not be called reliably by React Native/Expo + override func startObserving() { + hasListeners = true + } + + override func stopObserving() { + hasListeners = false + } + // MARK: - Setup Notifications // Required override to specify supported events @@ -1701,16 +1712,19 @@ func replicator_AddDocumentChangeListener( @objc(logsinks_SetConsole:withDomains:withResolver:withRejecter:) func logsinks_SetConsole( - level: NSNumber?, - domains: [String]?, + level: Any?, + domains: Any?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ){ backgroundQueue.async { do{ - // convert NSNumber? to Int? - let intLevel = level?.intValue - try LogSinksManager.shared.setConsoleSink(level: intLevel, domains: domains) + // convert Any? to Int? - treat -1 as nil (disable signal) + let levelNumber = (level as? NSNumber)?.intValue + let intLevel = (levelNumber == -1) ? nil : levelNumber + // convert Any? to [String]? + let domainsArray = domains as? [String] + try LogSinksManager.shared.setConsoleSink(level: intLevel, domains: domainsArray) resolve(nil) } catch let error as NSError { @@ -1724,16 +1738,17 @@ func replicator_AddDocumentChangeListener( @objc(logsinks_SetFile:withConfig:withResolver:withRejecter:) func logsinks_SetFile( - level: NSNumber?, - config: NSDictionary?, + level: Any?, + config: Any?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ){ backgroundQueue.async { do{ - // Convert NSNumber? to Int? - let intLevel = level?.intValue - // Convert NSDictionary? to [String: Any]? + // Convert Any? to Int? - treat -1 as nil (disable signal) + let levelNumber = (level as? NSNumber)?.intValue + let intLevel = (levelNumber == -1) ? nil : levelNumber + // Convert Any? to [String: Any]? let configDict = config as? [String: Any] try LogSinksManager.shared.setFileSink( @@ -1755,36 +1770,44 @@ func replicator_AddDocumentChangeListener( @objc(logsinks_SetCustom:withDomains:withToken:withResolver:withRejecter:) func logsinks_SetCustom( - level: NSNumber?, - domains: [String]?, - token: String?, + level: Any?, + domains: Any?, + token: Any?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { backgroundQueue.async { do { - // Convert NSNumber? to Int? - let intLevel = level?.intValue + // Convert Any? to Int? - treat -1 as nil (disable signal) + let levelNumber = (level as? NSNumber)?.intValue + let intLevel = (levelNumber == -1) ? nil : levelNumber + // Convert Any? to [String]? + let domainsArray = domains as? [String] + // Convert Any? to String? + let tokenString = token as? String // Create callback closure only if we have level and token (means enable, not disable) let callback: ((LogLevel, LogDomain, String) -> Void)? = - (intLevel != nil && token != nil) ? { [weak self] logLevel, logDomain, message in + (intLevel != nil && tokenString != nil && !tokenString!.isEmpty) ? { [weak self] logLevel, logDomain, message in guard let self = self else { return } + // Convert Swift types back to JavaScript-friendly types let eventData: [String: Any] = [ - "token": token!, + "token": tokenString!, "level": logLevel.rawValue, "domain": self.logDomainToString(logDomain), "message": message ] // Send event to JavaScript + // Note: React Native may show warnings about no listeners if events arrive before + // JS listener is fully initialized, but these warnings are harmless self.sendEvent(withName: self.kCustomLogMessage, body: eventData) } : nil try LogSinksManager.shared.setCustomSink( level: intLevel, - domains: domains, + domains: domainsArray, callback: callback ) resolve(nil) diff --git a/ios/cbl-js-swift b/ios/cbl-js-swift index 41809fa..d594cb3 160000 --- a/ios/cbl-js-swift +++ b/ios/cbl-js-swift @@ -1 +1 @@ -Subproject commit 41809fa7d343a112d688e62e79a5e9894e6f8491 +Subproject commit d594cb30e525b28e17841cee1b6cecf8309ea925 diff --git a/src/CblReactNativeEngine.tsx b/src/CblReactNativeEngine.tsx index 0cd382d..c68f084 100644 --- a/src/CblReactNativeEngine.tsx +++ b/src/CblReactNativeEngine.tsx @@ -129,11 +129,11 @@ export class CblReactNativeEngine implements ICoreEngine { if (customEventEmitter) { this.debugLog('Using provided custom event emitter'); this._eventEmitter = customEventEmitter; - return; + } else { + this._eventEmitter = new NativeEventEmitter(this.CblReactNative); } - this._eventEmitter = new NativeEventEmitter(this.CblReactNative); - + // Always add the customLogMessage listener regardless of emitter source this._eventEmitter.addListener( 'customLogMessage', (data: { diff --git a/src/cblite-js b/src/cblite-js index 4bbfb29..3b4f21b 160000 --- a/src/cblite-js +++ b/src/cblite-js @@ -1 +1 @@ -Subproject commit 4bbfb292bf7d43a220c2321e2485e59b5e7ffc53 +Subproject commit 3b4f21ba844d5d74123dd6aa395db26d85e606ca