diff --git a/Jenkinsfile b/Jenkinsfile index 542f8bcf4..7140207bd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,25 +9,25 @@ pipeline { stage("Validate C#") { agent { label 's61113u16 (litecore)' } steps { - sh 'jenkins/dotnet_build.sh 4.0.0 2.0.0' + sh 'jenkins/dotnet_build.sh 4.1.0 2.0.0' } } stage("Validate C") { agent { label 's61113u16 (litecore)' } steps { - sh 'jenkins/c_build.sh 4.0.0' + sh 'jenkins/c_build.sh 4.1.0' } } stage("Validate iOS") { agent { label 'mobile-builder-ios-pull-request' } steps { - sh 'jenkins/ios.sh 4.0.0 2.0.0' + sh 'jenkins/ios.sh 4.1.0 2.0.0' } } stage("Validate Android") { agent { label 'cbl-android' } steps { - sh 'jenkins/android_build.sh 4.0.0 2.0.0' + sh 'jenkins/android_build.sh 4.1.0 2.0.0' } } } diff --git a/jenkins/android_build.sh b/jenkins/android_build.sh index 0b1e1a13a..fea9fbf2b 100755 --- a/jenkins/android_build.sh +++ b/jenkins/android_build.sh @@ -70,8 +70,35 @@ fi CBL_URL="http://proget.build.couchbase.com:8080/api/get_version?product=couchbase-lite-android&version=${CBL_VERSION}" VS_URL="http://proget.build.couchbase.com:8080/api/get_version?product=couchbase-lite-android-vector-search&version=${VS_VERSION}" -CBL_BUILD=$(curl -s $CBL_URL | $JQ -r '.BuildNumber') -VS_BUILD=$(curl -s $VS_URL | $JQ -r '.BuildNumber') +CBL_RESPONSE=$(curl -s $CBL_URL) +CBL_IS_RELEASE=$(echo -n "$CBL_RESPONSE" | $JQ -r '.IsRelease') +CBL_BUILD=$(echo -n "$CBL_RESPONSE" | $JQ -r '.BuildNumber') + +if [ "${CBL_BUILD}" == "" ] || [ "${CBL_BUILD}" == "null" ]; then + echo "No latest successful build found for CBL v${CBL_VERSION}" + exit 3 +fi + +VS_RESPONSE=$(curl -s $VS_URL) +VS_IS_RELEASE=$(echo -n "$VS_RESPONSE" | $JQ -r '.IsRelease') +VS_BUILD=$(echo -n "$VS_RESPONSE" | $JQ -r '.BuildNumber') + +if [ "${VS_BUILD}" == "" ] || [ "${VS_BUILD}" == "null" ]; then + echo "No latest successful build found for VS v${VS_VERSION}" + exit 3 +fi + +if [ "${CBL_IS_RELEASE}" == "true" ]; then + CBL_VERSION_ARG="${CBL_VERSION}" +else + CBL_VERSION_ARG="${CBL_VERSION}-${CBL_BUILD}" +fi + +if [ "${VS_IS_RELEASE}" == "true" ]; then + VS_VERSION_ARG="${VS_VERSION}" +else + VS_VERSION_ARG="${VS_VERSION}-${VS_BUILD}" +fi pushd $ANDROID_DIR/examples/ -./gradlew assembleDebug -PcblVersion=$CBL_VERSION-$CBL_BUILD -PextVersion=$VS_VERSION-$VS_BUILD \ No newline at end of file +./gradlew assembleDebug -PcblVersion=${CBL_VERSION_ARG} -PextVersion=${VS_VERSION_ARG} \ No newline at end of file diff --git a/jenkins/c_build.sh b/jenkins/c_build.sh index 2ae6755a5..5adecd105 100755 --- a/jenkins/c_build.sh +++ b/jenkins/c_build.sh @@ -2,23 +2,41 @@ if [[ "$#" -ne 1 ]]; then echo "This script requires a version argument" + exit 1 fi -THIS_DIR=$( dirname -- $(realpath "$0"); ) -version="$1" +THIS_DIR=$( dirname -- $(realpath "$0") ) +CBL_VERSION="$1" pushd "$THIS_DIR/../modules/c/examples/code_snippets" -build_num=$(curl -s http://dbapi.build.couchbase.com:8000/v1/products/couchbase-lite-c/releases/$version/versions/$version/builds?filter=last_complete | jq .build_num | bc) +RESPONSE=$(curl -s "http://proget.build.couchbase.com:8080/api/get_version?product=couchbase-lite-c&version=${CBL_VERSION}") +CBL_IS_RELEASE=$(echo -n "$RESPONSE" | jq .IsRelease) +CBL_BUILD_NO=$(echo -n "$RESPONSE" | jq .BuildNumber) + +if [ "${CBL_BUILD_NO}" == "" ] || [ "${CBL_BUILD_NO}" == "null" ]; then + echo "No latest successful build found for CBL v${CBL_VERSION}" + exit 3 +fi + +if [ "${CBL_IS_RELEASE}" == "true" ]; then + PACKAGE_NAME="couchbase-lite-c-enterprise-${CBL_VERSION}-linux-x86_64.tar.gz" + DOWNLOAD_URL="https://latestbuilds.service.couchbase.com/builds/releases/mobile/couchbase-lite-c/${CBL_VERSION}/${PACKAGE_NAME}" +else + PACKAGE_NAME="couchbase-lite-c-enterprise-${CBL_VERSION}-${CBL_BUILD_NO}-linux-x86_64.tar.gz" + DOWNLOAD_URL="https://latestbuilds.service.couchbase.com/builds/latestbuilds/couchbase-lite-c/${CBL_VERSION}/${CBL_BUILD_NO}/${PACKAGE_NAME}" +fi + +rm -rf downloaded mkdir -p downloaded pushd downloaded DOWNLOAD_DIR=$(pwd) -wget http://latestbuilds.service.couchbase.com/builds/latestbuilds/couchbase-lite-c/$version/$build_num/couchbase-lite-c-enterprise-$version-$build_num-linux-x86_64.tar.gz -tar xf couchbase-lite-c-enterprise-$version-$build_num-linux-x86_64.tar.gz -rm couchbase-lite-c-enterprise-$version-$build_num-linux-x86_64.tar.gz +wget "${DOWNLOAD_URL}" +tar xf "${PACKAGE_NAME}" +rm "${PACKAGE_NAME}" popd mkdir -p build pushd build -cmake -DCMAKE_PREFIX_PATH="$DOWNLOAD_DIR/libcblite-$version" .. +cmake -DCMAKE_PREFIX_PATH="$DOWNLOAD_DIR/libcblite-${CBL_VERSION}" .. make -j12 diff --git a/jenkins/ios.sh b/jenkins/ios.sh index 0552ad90f..a49ad3848 100755 --- a/jenkins/ios.sh +++ b/jenkins/ios.sh @@ -3,20 +3,19 @@ CBL_VERSION=$1 VS_VERSION=$2 -dir=$( dirname -- $(realpath "$0"); ) - -# Get latest good CBL iOS EE build for given version -CBL_URL="http://proget.build.couchbase.com:8080/api/open_latestbuilds?product=couchbase-lite-ios&version=${cbl_version}" +if [ -z "${CBL_VERSION}" ] || [ -z "${VS_VERSION}" ]; then + echo "Usage: $0 " + exit 1 +fi -# Grab the redirect url -CBL_SOURCE_URL=$(curl -s -L -o /dev/null -w '%{url_effective}' "${CBL_URL}") +dir=$( dirname -- $(realpath "$0") ) CBL="http://proget.build.couchbase.com:8080/api/get_version?product=couchbase-lite-ios&version=${CBL_VERSION}" RESPONSE=$(curl -s $CBL) -CBL_IS_RELEASE=$(echo -n $RESPONSE | jq .IsRelease) -CBL_BUILD_NO=$(echo -n $RESPONSE | jq .BuildNumber) +CBL_IS_RELEASE=$(echo -n "$RESPONSE" | jq .IsRelease) +CBL_BUILD_NO=$(echo -n "$RESPONSE" | jq .BuildNumber) -if [ "${CBL_BUILD_NO}" == "" ] +if [ "${CBL_BUILD_NO}" == "" ] || [ "${CBL_BUILD_NO}" == "null" ] then echo "No latest successful build found for CBL v${CBL_VERSION}" exit 3 @@ -24,51 +23,54 @@ fi VS="http://proget.build.couchbase.com:8080/api/get_version?product=couchbase-lite-ios-vector-search&version=${VS_VERSION}&ee=true" RESPONSE=$(curl -s $VS) -VS_BUILD_NO=$(echo -n $RESPONSE | jq .BuildNumber) +VS_IS_RELEASE=$(echo -n "$RESPONSE" | jq .IsRelease) +VS_BUILD_NO=$(echo -n "$RESPONSE" | jq .BuildNumber) -if [ "${VS_BUILD_NO}" == "" ] +if [ "${VS_BUILD_NO}" == "" ] || [ "${VS_BUILD_NO}" == "null" ] then echo "No latest successful build found for VS v${VS_VERSION}" exit 3 fi +# Download VS extension once (platform-independent) +if [ "${VS_IS_RELEASE}" == "true" ]; then + VS_PACKAGE_NAME="couchbase-lite-vector-search-${VS_VERSION}-apple.zip" + VS_URL="https://latestbuilds.service.couchbase.com/builds/releases/mobile/couchbase-lite-vector-search/${VS_VERSION}/${VS_PACKAGE_NAME}" +else + VS_PACKAGE_NAME="couchbase-lite-vector-search-${VS_VERSION}-${VS_BUILD_NO}-apple.zip" + VS_URL="https://latestbuilds.service.couchbase.com/builds/latestbuilds/couchbase-lite-vector-search/${VS_VERSION}/${VS_BUILD_NO}/${VS_PACKAGE_NAME}" +fi + +VS_DOWNLOAD_DIR="${dir}/../vs_downloaded" +rm -rf "${VS_DOWNLOAD_DIR}" +mkdir -p "${VS_DOWNLOAD_DIR}" +wget -P "${VS_DOWNLOAD_DIR}" "${VS_URL}" + for PLATFORM in "objc" "swift" do echo $PLATFORM pushd "${dir}/../modules/${PLATFORM}/examples" # In case script fails mid-way, cleanup on start - if [ -d "downloaded" ]; then - rm -rf "downloaded" - fi + rm -rf "downloaded" mkdir -p downloaded pushd downloaded - # Get CBL - CBL_SOURCE_URL=$(curl -s -L -o /dev/null -w '%{url_effective}' "http://proget.build.couchbase.com:8080/api/open_latestbuilds?product=couchbase-lite-ios&version=${CBL_VERSION}") - if [ $CBL_IS_RELEASE == true ]; then + # Get CBL + if [ "${CBL_IS_RELEASE}" == "true" ]; then CBL_PACKAGE_NAME="couchbase-lite-${PLATFORM}_xc_enterprise_${CBL_VERSION}.zip" + CBL_URL="https://latestbuilds.service.couchbase.com/builds/releases/mobile/couchbase-lite-ios/${CBL_VERSION}/${CBL_PACKAGE_NAME}" else CBL_PACKAGE_NAME="couchbase-lite-${PLATFORM}_xc_enterprise_${CBL_VERSION}-${CBL_BUILD_NO}.zip" + CBL_URL="https://latestbuilds.service.couchbase.com/builds/latestbuilds/couchbase-lite-ios/${CBL_VERSION}/${CBL_BUILD_NO}/${CBL_PACKAGE_NAME}" fi - wget "$CBL_SOURCE_URL$CBL_PACKAGE_NAME" - unzip -o $CBL_PACKAGE_NAME -d "../Frameworks/" + wget "${CBL_URL}" + unzip -o "${CBL_PACKAGE_NAME}" -d "../Frameworks/" # Get VS extension - VS_SOURCE_URL=$(curl -s -L -o /dev/null -w '%{url_effective}' "http://proget.build.couchbase.com:8080/api/open_latestbuilds?product=couchbase-lite-ios-vector-search&version=${VS_VERSION}") - echo $VS_SOURCE_URL - VS_PACKAGE_NAME="couchbase-lite-vector-search-${VS_VERSION}-${VS_BUILD_NO}-apple.zip" - wget "$VS_SOURCE_URL$VS_PACKAGE_NAME" - unzip -o $VS_PACKAGE_NAME -d "../Frameworks/" - - # Check if download was successful - if [ $? -eq 0 ]; then - echo "Package downloaded successfully." - else - echo "Failed to download the package." - fi + unzip -o "${VS_DOWNLOAD_DIR}/${VS_PACKAGE_NAME}" -d "../Frameworks/" popd @@ -79,3 +81,5 @@ do popd done + +rm -rf "${VS_DOWNLOAD_DIR}" diff --git a/modules/android/examples/build.gradle b/modules/android/examples/build.gradle index 6ca80790f..3d0ec6451 100644 --- a/modules/android/examples/build.gradle +++ b/modules/android/examples/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20" - classpath 'com.android.tools.build:gradle:8.0.2' + classpath 'com.android.tools.build:gradle:8.13.2' } } diff --git a/modules/android/examples/gradle/wrapper/gradle-wrapper.properties b/modules/android/examples/gradle/wrapper/gradle-wrapper.properties index a59520664..2733ed5dc 100644 --- a/modules/android/examples/gradle/wrapper/gradle-wrapper.properties +++ b/modules/android/examples/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/modules/android/examples/java_snippets/app/build.gradle b/modules/android/examples/java_snippets/app/build.gradle index d0c4c7a8d..8eb5f0c0b 100644 --- a/modules/android/examples/java_snippets/app/build.gradle +++ b/modules/android/examples/java_snippets/app/build.gradle @@ -33,10 +33,6 @@ android { targetCompatibility JavaVersion.VERSION_11 } - lintOptions { - disable 'UseSparseArrays' - abortOnError false - } sourceSets { main { @@ -46,6 +42,10 @@ android { ] } } + lint { + abortOnError false + disable 'UseSparseArrays' + } } repositories { diff --git a/modules/android/examples/kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/MultipeerExamples.kt b/modules/android/examples/kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/MultipeerExamples.kt index 9dc02dcfd..ac080ed73 100644 --- a/modules/android/examples/kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/MultipeerExamples.kt +++ b/modules/android/examples/kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/MultipeerExamples.kt @@ -210,6 +210,7 @@ class MultipeerExamples { return config } + fun createMultipeerReplicator() : MultipeerReplicator { val config = createConfig() diff --git a/modules/android/pages/p2psync-multipeer.adoc b/modules/android/pages/p2psync-multipeer.adoc index 4785f2f93..0342dd3d0 100644 --- a/modules/android/pages/p2psync-multipeer.adoc +++ b/modules/android/pages/p2psync-multipeer.adoc @@ -1,6 +1,6 @@ = Multipeer P2P Replicator ifdef::show_edition[:page-edition: Enterprise Edition] -:description: The Multipeer Replicator enables lightweight, self-organizing mesh networks for apps running on the same local Wi-Fi. +:description: The Multipeer Replicator enables lightweight, self-organizing mesh networks over Wi-Fi and Bluetooth Low Energy. :source-language: Kotlin @@ -9,14 +9,12 @@ ifdef::show_edition[:page-edition: Enterprise Edition] This approach requires minimal setup and automates peer discovery and connectivity management, making it simpler than xref:p2psync-websocket.adoc[active-passive P2P configurations]. - - [#introduction] == Introduction Couchbase Lite's Peer-to-Peer synchronization solution offers secure storage and bidirectional data synchronization between mobile and IoT devices without needing a centralized cloud-based control point. -For small mesh topologies, Multipeer Replicator offers `autodiscovery` for Wi-Fi-based networks and secure communication via TLS and certificate-based authentication. +For small mesh topologies, Multipeer Replicator offers autodiscovery over Wi-Fi and Bluetooth Low Energy, with secure communication via TLS and certificate-based authentication. The dynamic mesh topology gives optimal peer connectivity and the lightweight and low-maintenance configuration requires less management and less code than using active-passive peer-to-peer sync. @@ -24,7 +22,8 @@ The dynamic mesh topology gives optimal peer connectivity and the lightweight an To maintain optimal connectivity, efficient data transport, and balanced workloads, the Multipeer Replicator forms a dynamic mesh network among peers in the same group. -The mesh network provides resilience through multiple communication pathways - if one connection fails, data can flow through alternative routes. +The mesh network provides resilience through multiple communication pathways. +If one connection fails, data can flow through alternative routes. It avoids redundant direct connections, evenly distributes connections across peers, and optimizes communication paths through intelligent routing. @@ -32,7 +31,8 @@ The mesh network continuously adapts as peers join or leave, automatically heali This self-organizing approach ensures reliable data synchronization even in challenging network conditions, where individual peer connections may be intermittent or unreliable. -Multipeer Replicator supports Wi-Fi (IP-based transport) as of CBL 3.3. +NOTE: The Multipeer Replicator supports cross-platform replication between Android and iOS peers running Couchbase Lite 4.1 or higher. +An Android powered device and an iOS device using the same group ID, identity certificates from the same CA, and matching transport configuration can discover and replicate with each other over both Wi-Fi and Bluetooth. // Mesh Diagram // [graphviz] @@ -66,19 +66,93 @@ Multipeer Replicator supports Wi-Fi (IP-based transport) as of CBL 3.3. == Prerequisites +The Multipeer Replicator supports two transports for peer discovery and replication: Wi-Fi and Bluetooth Low Energy (BLE). Wi-Fi is enabled by default. +Bluetooth is optional and you enable it through the replicator configuration. +See <>. -=== Transport and Peer Discovery Protocol +=== Transport Support +.Transport support +[cols="1,1,2,1,2"] +|=== +|Transport |Available from |Discovery |Minimum Android API |Notes -Multipeer Replicator supports Wi-Fi transport, using `DNS-SD` (also known as Bonjour) for peer discovery. -Peers connect to the same Wi-Fi network to discover and establish connections with one another. +|Wi-Fi +|CBL 3.3 +|DNS-SD (Bonjour) +|API 24 +|Peers must connect to the same Wi-Fi network. +|Bluetooth Low Energy +|CBL 4.1 +|BLE advertising and scanning +|API 29 +|Requires additional manifest permissions and runtime permission requests. See <>. +|=== === Supported Platforms +For Wi-Fi transport, CBL supports Android API 24 and higher. + +For Bluetooth transport, CBL supports Android API 29 and higher. +On API 29 and 30, Bluetooth uses the legacy `BLUETOOTH`, `BLUETOOTH_ADMIN`, and `ACCESS_FINE_LOCATION` permissions. +On API 31 and higher, Bluetooth uses the runtime permissions `BLUETOOTH_SCAN`, `BLUETOOTH_ADVERTISE`, and `BLUETOOTH_CONNECT`. + +See xref:kotlin:supported-os.adoc[Supported Platforms] for the full platform support matrix. + +[#platform-configuration] +=== Platform Configuration + +This section applies only if your application enables Bluetooth transport. +Applications that use Wi-Fi only do not require these permissions. + +==== Manifest Permissions + +Declare the required Bluetooth permissions in your `AndroidManifest.xml`. The permissions differ between Android 11 (API 30) and lower, and Android 12 (API 31) and higher. + +[source,xml] +---- + + + + + + + + + + + + + +---- -For Android, we recommend using a recent release, preferably supporting minimum API level 24, or more recent but earlier versions should work with the multipeer sync feature. -See xref:kotlin:supported-os.adoc[Supported Platforms] for more details. +==== Runtime Permission Request + +On Android 12 (API 31) and higher, your application must request `BLUETOOTH_SCAN`, `BLUETOOTH_ADVERTISE`, and `BLUETOOTH_CONNECT` at runtime before starting the Multipeer Replicator. On Android 11 (API 30) and lower, your application must request `ACCESS_FINE_LOCATION` at runtime. + +[CAUTION] +-- +On Android 13 (API 33) and higher, the runtime permission prompt is labelled *Nearby devices*, not Bluetooth. +You must still explicitly request `BLUETOOTH_SCAN`, `BLUETOOTH_ADVERTISE`, and `BLUETOOTH_CONNECT` at runtime on these devices. +Declaring permissions in the manifest alone is not sufficient: Android 13+ devices will not show a prompt and BLE sync will not work. +-- + +.Requesting Bluetooth permissions at runtime +[source,kotlin] +---- +include::example$kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/MultipeerExamples.kt[tags="multipeer-bluetooth-permissions-request", indent=0] +---- == Configuration @@ -167,6 +241,33 @@ include::example$kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/ include::example$kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/MultipeerExamples.kt[tags="multipeer-authenticator-rootcerts", indent=0] ---- +[#transports] +=== Transports + +The `transports` property on `MultipeerReplicatorConfiguration` controls which transports the replicator uses for peer discovery and replication. The default is Wi-Fi only. + +To enable Bluetooth Low Energy alongside Wi-Fi, add `MultipeerTransport.BLUETOOTH` to the transports set. + +.Default (Wi-Fi only) +[source,kotlin] +---- +include::example$kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/MultipeerExamples.kt[tags="multipeer-config-transports-default", indent=0] +---- + +.Wi-Fi and Bluetooth +[source,kotlin] +---- +include::example$kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/MultipeerExamples.kt[tags="multipeer-config-transports-both", indent=0] +---- + +.Bluetooth only +[source,kotlin] +---- +include::example$kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/MultipeerExamples.kt[tags="multipeer-config-transports-bluetooth-only", indent=0] +---- + +When you enable both transports, the replicator automatically selects the best available transport for each peer and switches between them as reachability changes. See <>. + === Create MultipeerReplicatorConfiguration @@ -182,6 +283,25 @@ include::example$kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/ TIP: Performance may vary in mesh networks depending on your specific environment and number of peers. We recommend running tests with your network configuration to assess any effects on packet loss or latency. +[#automatic-transport-switching] +== Automatic Transport Switching + +When `MultipeerReplicator` is configured with both Wi-Fi and Bluetooth transports, it automatically selects the best available transport for each peer and switches transports as reachability changes. + +=== Transport Preference + +The replicator prefers Wi-Fi over Bluetooth when both transports can reach a peer. Bluetooth acts as a fallback when Wi-Fi cannot reach a peer. + +=== Fallback to Bluetooth + +For an individual peer, `MultipeerReplicator` falls back to Bluetooth when the peer is no longer reachable over Wi-Fi. This can occur if the peer disables Wi-Fi, becomes unreachable on the local network, or if replication over Wi-Fi fails because of a network-related error. + +In cases of connection or replication failure over Wi-Fi, `MultipeerReplicator` performs a small number of retries before falling back to Bluetooth. + +=== Return to Wi-Fi + +If a peer becomes reachable over Wi-Fi while replication is active over Bluetooth, `MultipeerReplicator` establishes a Wi-Fi connection in parallel with the existing Bluetooth connection. The Bluetooth connection remains active until the Wi-Fi connection is fully established and replication has resumed over Wi-Fi. This prevents any interruption in synchronization during the transition. + == Life Cycle @@ -240,6 +360,9 @@ You should make sure that the application has the necessary permissions to run i === Events In general, the connection should just work, and most of these optional listen events give status you may only want to use during development and testing. + +Status events include a `transport` property that identifies which transport the event applies to. `MultipeerReplicatorStatus` events are delivered per enabled transport and also as an aggregated status (where `transport` is `null`) representing the overall replicator state. + Event types include the following: @@ -276,17 +399,12 @@ include::example$kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/ ==== Peer's Document Replication - - .Peer's Document Replication Listener [source,kotlin] ---- include::example$kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/MultipeerExamples.kt[tags="multipeer-document-replication-listener", indent=0] ---- - - - == Peer Info @@ -319,14 +437,45 @@ include::example$kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/ === Peer Info +The `PeerInfo` object provides information about a peer, including its identifier, certificate, online status, replicator status, neighbor peers, the transports on which the peer was discovered, and the transport currently used for replication. -Getting peer info +.Getting peer info [source,kotlin] ---- include::example$kotlin_snippets/app/src/main/kotlin/com/couchbase/codesnippets/MultipeerExamples.kt[tags="multipeer-peer-info", indent=0] ---- +[#lbl-testing] +== Testing + +[#testing-device-requirements] +=== Device Requirements + +The Multipeer Replicator requires physical devices for end-to-end testing. +Android emulators and iOS simulators do not support Bluetooth Low Energy. +Test on real devices connected to the same Wi-Fi network for Wi-Fi transport, or in close physical proximity for Bluetooth. + +[#testing-isolating-ble] +=== Isolating the Bluetooth Path + +To verify that BLE transport is working independently of Wi-Fi: + +. Enable airplane mode on both test devices to disable Wi-Fi and cellular. +. Re-enable Bluetooth on both devices (airplane mode allows this on both Android and iOS). +. Start the Multipeer Replicator on both devices. +. Confirm replication completes using the BLE transport only. + +This approach removes any ambiguity about which transport is carrying the data. + +[#testing-android-doze] +=== Android Doze Mode + +Android Doze mode can silently suspend background network activity, including Multipeer Replication, when a device is idle. +During testing, keep the screen on or disable battery optimization for your test application to prevent Doze from interfering with replication. +In production, see <> for configuration guidance. + + == Logging @@ -604,14 +753,14 @@ You can find https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-kotl // /// The `attributes` must include a common name (CN); otherwise an error // /// will be thrown. // /// -// /// If no the expiration date is specified, the default validity of one year -// /// will be applied. -// /// // /// The optional issuer identity may be specified to sign the certificate. // /// The issuer identity can be created using // /// `createIdentity(privateKeyData:certificateData:)`. If no issuer is specified, // /// the certificate will be self-signed. // /// +// /// If no the expiration date is specified, the default validity of one year +// /// will be applied. +// /// // /// If a `label` is provided, the identity will be stored in the // /// platform’s secure key storage under that label. // public static func createIdentity(keyUsages: KeyUsages, @@ -652,6 +801,3 @@ You can find https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-kotl // *URL* - - - diff --git a/modules/android/pages/p2psync-websocket.adoc b/modules/android/pages/p2psync-websocket.adoc index 5d72796db..827824a53 100644 --- a/modules/android/pages/p2psync-websocket.adoc +++ b/modules/android/pages/p2psync-websocket.adoc @@ -25,7 +25,8 @@ xref:p2psync-multipeer.adoc[Multipeer P2P Replicator] ==== Multipeer P2P Replicator is available for Android, offering: -* Auto-discovery over local Wi-Fi (via `DNS-SD`) +* Auto-discovery over Wi-Fi and Bluetooth Low Energy +* Automatic transport switching between Wi-Fi and Bluetooth when both are enabled * Lightweight and low-maintenance configuration * Dynamic mesh topology for optimal peer connectivity * Secure communication via TLS and certificate-based authentication @@ -76,7 +77,7 @@ Therefore, to use Peer-to-Peer synchronization in your application, you must con . Point the replicator at the Listener . Initialize the replicator + . Replicator and Listener engage in the configured security protocol exchanges to confirm connection -. If connection is confirmed then replication will commence, synchronizing the two data stores. +. If connection is confirmed then replication commences, synchronizing the two data stores. ==== @@ -101,8 +102,8 @@ Having a Listener on a database still allows you to open replications to the oth For example, a Listener can actively begin replicating to other Listeners while listening for connections. These replications can be for the same or a different database. -The Listener will automatically select a port to use or a user-specified port. -It will also listen on all available networks, unless you specify a specific network. +The Listener automatically selects a port to use or a user-specified port. +It also listens on all available networks, unless you specify a specific network. [#security] === Security @@ -126,17 +127,19 @@ For testing and development purposes, support is provided for the client (active [#error-handling] === Error Handling -When a Listener is stopped, then all connected replicators are notified by a WebSocket error. Your application should distinguish between transient and permanent connectivity errors. +When a Listener is stopped, then all connected replicators are notified by a WebSocket error. +Your application should distinguish between transient and permanent connectivity errors. [#passive-peers] ==== Passive peers -A Passive Peer losing connectivity with an Active Peer will clean up any associated endpoint connections to that Peer. The Active Peer may attempt to reconnect to the Passive Peer. +A Passive Peer losing connectivity with an Active Peer cleans up any associated endpoint connections to that Peer. +The Active Peer may attempt to reconnect to the Passive Peer. [#active-peers] ==== Active peers -An Active Peer permanently losing connectivity with a Passive Peer will cease replicating. +An Active Peer permanently losing connectivity with a Passive Peer ceases replicating. -An Active Peer temporarily losing connectivity with a passive Peer will use exponential backoff functionality to attempt reconnection. +An Active Peer temporarily losing connectivity with a passive Peer uses exponential backoff feature to attempt reconnection. [#delta-sync] === Delta Sync @@ -162,7 +165,7 @@ You can configure a Peer-to-Peer synchronization with just a short amount of cod [#ex-simple-listener] ==== -This simple listener configuration will give you a listener ready to participate in an encrypted synchronization with a replicator providing a valid user name and password. +This simple listener configuration gives you a listener ready to participate in an encrypted synchronization with a replicator providing a valid user name and password. [tabs] ===== @@ -205,7 +208,7 @@ include::android:example$codesnippet_collection.java[tags="listener-simple", ind [#ex-simple-replicator] ==== -This simple replicator configuration will give you an encrypted, bi-directional Peer-to-Peer synchronization with automatic conflict resolution. +This simple replicator configuration gives you an encrypted, bi-directional Peer-to-Peer synchronization with automatic conflict resolution. [tabs] ===== @@ -270,9 +273,9 @@ Use this to create a configuration object you can then use to initialize the lis Port:: + -- -This is the port that the listener will listen to. +This is the port that the listener listens to. -If the port is null or zero, the listener will auto-assign an available port to listen on. +If the port is null or zero, the listener auto-assigns an available port to listen on. Default value is null or zero depending on platform. When the listener is not started, the port is null (or zero if the platform requires). @@ -283,7 +286,7 @@ Network Interface:: -- Use this to select a specific Network Interface to use, in the form of the IP Address or network interface name. -If the network interface is specified, only that interface wil be used. +If the network interface is specified, only that interface will be used. If the network interface is not specified, all available network interfaces will be used. @@ -306,7 +309,7 @@ https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-android}{empty}/c Active peers will use the `ws://` URL scheme used to connect to the listener. * If `disableTLS` is false or not specified -- TLS communication is enabled. + -Active peers will use the `wss://` URL scheme to connect to the listener. +Active peers uses the `wss://` URL scheme to connect to the listener. // end::config-disable-tls[] API Reference: https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-android}{empty}/couchbase-lite-android/com/couchbase/lite/URLEndpointListenerConfiguration.html#setDisableTls-boolean-[setDisableTLS] @@ -320,7 +323,7 @@ Use https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-android}{empt https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-android}{empty}/couchbase-lite-android/com/couchbase/lite/URLEndpointListenerConfiguration.html#setTlsIdentity-com.couchbase.lite.TLSIdentity-[setTlsIdentity] method to configure the TLS Identity used in TLS communication. If `TLSIdentity` is not set, then the listener uses an auto-generated anonymous self-signed identity (unless `disableTLS = true`). -Whilst the client cannot use this to authenticate the server, it will use it to encrypt communication, giving a more secure option than non-TLS communication. +Whilst the client cannot use this to authenticate the server, it uses it to encrypt communication, giving a more secure option than non-TLS communication. The auto-generated anonymous self-signed identity is saved in secure storage for future use to obviate the need to re-generate it. @@ -426,7 +429,7 @@ Please check this document for more info: https://source.android.com/security/ke -- -MacOS / iOS:: +MacOS / iOS:: + -- .Secure storage details @@ -586,6 +589,4 @@ xref:tutorials:cbl-p2p-sync-websockets:swift/cbl-p2p-sync-websockets.adoc[Gettin ++++ -++++ - - +++++ \ No newline at end of file diff --git a/modules/android/pages/supported-os.adoc b/modules/android/pages/supported-os.adoc index 38b4808e3..535d52f8e 100644 --- a/modules/android/pages/supported-os.adoc +++ b/modules/android/pages/supported-os.adoc @@ -1,4 +1,3 @@ - = Supported Operating System Versions :page-aliases: product/java-android-supported-os.adoc :page-role: -toc @@ -55,3 +54,22 @@ Couchbase does not test against, nor guarantee support for, uncertified Android |=== +[#feature-requirements] +== Feature Requirements + +Some features require a greater minimum API level to function than the general CBL requirement.. + +.Multipeer Replicator API level requirements +[%autowidth.stretch] +|=== +|Feature |Minimum Android API |Notes + +|Multipeer Replicator: Wi-Fi transport +|API 24 +|Peers must connect to the same Wi-Fi network. + +|Multipeer Replicator: Bluetooth Low Energy transport +|API 29 +|Requires additional manifest permissions and runtime permission requests. +See xref:android:p2psync-multipeer.adoc#bluetooth-platform-configuration[Bluetooth Platform Configuration]. +|=== \ No newline at end of file diff --git a/modules/c/examples/code_snippets/main.cpp b/modules/c/examples/code_snippets/main.cpp index cb34670c8..c5da8611a 100644 --- a/modules/c/examples/code_snippets/main.cpp +++ b/modules/c/examples/code_snippets/main.cpp @@ -1688,10 +1688,6 @@ static void start_replication() { } static void console_log_sink() { - // tag::console-logging[] - CBLLog_SetConsoleLevel(kCBLLogVerbose); - // end::console-logging[] - // tag::new-console-logging[] CBLConsoleLogSink logSink {}; logSink.level = kCBLLogVerbose; @@ -1701,23 +1697,6 @@ static void console_log_sink() { } static void file_log_sink() { - // tag::file-logging[] - // NOTE: No error handling, for brevity (see getting started) - - // NOTE: You will need to use a platform appropriate method for finding - // a temporary directory - - CBLLogFileConfiguration config {}; // Don't bother zeroing, since we set all properties - config.level = kCBLLogInfo; - config.directory = FLSTR("/tmp/logs");; - config.maxRotateCount = 12; - config.maxSize = 1048576; - config.usePlaintext = false; - - CBLError err{}; - CBLLog_SetFileConfig(config, &err); - // end::file-logging[] - // tag::new-file-logging[] CBLFileLogSink logSink {}; logSink.level = kCBLLogVerbose; @@ -1742,10 +1721,6 @@ static void custom_log_sink_callback(CBLLogDomain domain, CBLLogLevel level, FLS // end::new-custom-logging[] static void enable_custom_log_sink() { - // tag::set-custom-logging[] - CBLLog_SetCallback(custom_log_callback); - // end::set-custom-logging[] - // tag::set-new-custom-logging[] CBLCustomLogSink logSink {}; logSink.level = kCBLLogVerbose; diff --git a/modules/csharp/pages/supported-os.adoc b/modules/csharp/pages/supported-os.adoc index cd1e2c62c..62108e09d 100644 --- a/modules/csharp/pages/supported-os.adoc +++ b/modules/csharp/pages/supported-os.adoc @@ -1,4 +1,3 @@ - = Supported Operating System Versions :page-aliases: product/csharp-supported-os.adoc :page-role: -toc @@ -32,7 +31,7 @@ Plan to migrate your apps to use an appropriate alternative version. .Newer .NET Runtime Versions -- The *Minimum Runtime Version* column specifies the minimum required version for each .NET runtime. -Later versions, including .NET 10 and higher, also work because .NET provides backward compatibility. +Later versions, including .NET 11 and higher, also work because .NET provides backward compatibility. Couchbase Lite is built to be compatible with newer .NET runtime versions as they're released. If you encounter any issues with a newer .NET version, submit a support ticket. -- @@ -54,19 +53,19 @@ a| Windows 10 + (any Microsoft supported) | .NET Mac Catalyst -| 9.0 +| 10.0 a| MacOS 13 |WinUI -|9.0 +|10.0 |10.0.19041.0 |.NET iOS -|9.0 +|10.0 |15 |.NET Android -|9.0 +|10.0 |API 24 |=== @@ -88,6 +87,4 @@ The following run-times are compatible but are not QE tested, and so are not off | 8.0 |n/a -|=== - - +|=== \ No newline at end of file diff --git a/modules/objc/examples/code_snippets/MultipeerReplicator.m b/modules/objc/examples/code_snippets/MultipeerReplicator.m index 9a61856c3..e71b3ddbf 100644 --- a/modules/objc/examples/code_snippets/MultipeerReplicator.m +++ b/modules/objc/examples/code_snippets/MultipeerReplicator.m @@ -185,71 +185,106 @@ - (CBLMultipeerReplicatorConfiguration *)createConfig { return config; } -- (CBLMultipeerReplicator *)createMultipeerReplicator { - CBLMultipeerReplicatorConfiguration *config = [self createConfig]; - NSError *error = nil; - // tag::multipeer-replicator - CBLMultipeerReplicator *replicator = [[CBLMultipeerReplicator alloc] initWithConfig:config error:&error]; - // end::multipeer-replicator - return replicator; +- (CBLMultipeerReplicatorConfiguration *)createConfigTransportsDefault { + CBLTLSIdentity *identity = [self createCASignedIdentity]; + id authenticator = [self authenticatorWithRootCerts]; + NSArray *collections = [self collectionConfig]; + + // tag::multipeer-config-transports-default[] + // Wi-Fi is the default transport. No additional configuration is required. + CBLMultipeerReplicatorConfiguration *config = + [[CBLMultipeerReplicatorConfiguration alloc] initWithPeerGroupID:@"com.myapp" + identity:identity + authenticator:authenticator + collections:collections]; + // config.transports defaults to kCBLMultipeerTransportWifi + // end::multipeer-config-transports-default[] + return config; } -- (void)startReplicator { - CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; - // tag::multipeer-replicator-start - [replicator start]; - // end::multipeer-replicator-start +- (CBLMultipeerReplicatorConfiguration *)createConfigTransportsBoth { + CBLTLSIdentity *identity = [self createCASignedIdentity]; + id authenticator = [self authenticatorWithRootCerts]; + NSArray *collections = [self collectionConfig]; + + // tag::multipeer-config-transports-both[] + CBLMultipeerReplicatorConfiguration *config = + [[CBLMultipeerReplicatorConfiguration alloc] initWithPeerGroupID:@"com.myapp" + identity:identity + authenticator:authenticator + collections:collections]; + config.transports = kCBLMultipeerTransportWifi | kCBLMultipeerTransportBluetooth; + // end::multipeer-config-transports-both[] + return config; } -- (void)stopReplicator { - CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; - // tag::multipeer-replicator-stop - [replicator stop]; - // end::multipeer-replicator-stop +- (CBLMultipeerReplicatorConfiguration *)createConfigTransportsBluetoothOnly { + CBLTLSIdentity *identity = [self createCASignedIdentity]; + id authenticator = [self authenticatorWithRootCerts]; + NSArray *collections = [self collectionConfig]; + + // tag::multipeer-config-transports-bluetooth-only[] + CBLMultipeerReplicatorConfiguration *config = + [[CBLMultipeerReplicatorConfiguration alloc] initWithPeerGroupID:@"com.myapp" + identity:identity + authenticator:authenticator + collections:collections]; + config.transports = kCBLMultipeerTransportBluetooth; + // end::multipeer-config-transports-bluetooth-only[] + return config; } - (void)statusListener { CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; - // tag::multipeer-status-listener + // tag::multipeer-status-listener[] [replicator addStatusListenerWithQueue:nil listener:^(CBLMultipeerReplicatorStatus *status) { + // transport is nil for the aggregated overall status; + // non-nil for a per-transport status update. + NSString *transport = status.transport == nil ? @"all" + : (status.transport.unsignedIntegerValue == kCBLMultipeerTransportWifi ? @"wifi" : @"bluetooth"); NSString *state = status.active ? @"active" : @"inactive"; NSString *err = status.error ? status.error.localizedDescription : @"none"; - NSLog(@"Multipeer Replicator Status: %@, Error: %@", state, err); + NSLog(@"Multipeer Replicator [%@]: %@, Error: %@", transport, state, err); }]; - // end::multipeer-status-listener + // end::multipeer-status-listener[] } - (void)peerDiscoveryListener { CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; - // tag::multipeer-peer-discovery-listener + // tag::multipeer-peer-discovery-listener[] [replicator addPeerDiscoveryStatusListenerWithQueue:nil listener:^(CBLPeerDiscoveryStatus *status) { NSString *online = status.online ? @"online" : @"offline"; - NSLog(@"Peer Discovery Status - Peer ID: %@, Status: %@", status.peerID, online); + NSString *transport = (status.transport == kCBLMultipeerTransportWifi) ? @"wifi" : @"bluetooth"; + NSLog(@"Peer Discovery Status - Peer ID: %@, Transport: %@, Status: %@", + status.peerID, transport, online); }]; - // end::multipeer-peer-discovery-listener + // end::multipeer-peer-discovery-listener[] } - (void)peerReplicatorStatus { CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; - // tag::multipeer-replicator-status-listener + // tag::multipeer-replicator-status-listener[] NSArray *activities = @[ @"stopped", @"offline", @"connecting", @"idle", @"busy" ]; [replicator addPeerReplicatorStatusListenerWithQueue:nil listener:^(CBLPeerReplicatorStatus *replStatus) { NSString *direction = replStatus.outgoing ? @"outgoing" : @"incoming"; NSString *activity = activities[replStatus.status.activity]; + NSString *transport = (replStatus.transport == kCBLMultipeerTransportWifi) ? @"wifi" : @"bluetooth"; NSString *error = replStatus.status.error ? replStatus.status.error.localizedDescription : @"none"; NSLog(@"Peer Replicator Status - " - "Peer ID: %@, Direction: %@, Activity: %@, Error: %@", - replStatus.peerID, direction, activity, error); + "Peer ID: %@, Transport: %@, Direction: %@, Activity: %@, Error: %@", + replStatus.peerID, transport, direction, activity, error); }]; - // end::multipeer-replicator-status-listener + // end::multipeer-replicator-status-listener[] } - (void)peerDocumentReplication { CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; - // tag::multipeer-document-replication-listener + // tag::multipeer-document-replication-listener[] [replicator addPeerDocumentReplicationListenerWithQueue:nil listener:^(CBLPeerDocumentReplication *docRepl) { NSString *direction = docRepl.isPush ? @"Push" : @"Pull"; - NSLog(@"Peer Document Replication - Peer ID: %@, Direction: %@", docRepl.peerID, direction); + NSString *transport = (docRepl.transport == kCBLMultipeerTransportWifi) ? @"wifi" : @"bluetooth"; + NSLog(@"Peer Document Replication - Peer ID: %@, Transport: %@, Direction: %@", + docRepl.peerID, transport, direction); for (CBLReplicatedDocument *doc in docRepl.documents) { NSString *error = doc.error ? doc.error.localizedDescription : @"none"; NSString *collection = [NSString stringWithFormat:@"%@.%@", doc.scope, doc.collection]; @@ -257,35 +292,36 @@ - (void)peerDocumentReplication { collection, doc.id, (unsigned long)doc.flags, error); } }]; - // end::multipeer-document-replication-listener -} - -- (void)peerID { - CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; - // tag::multipeer-peer-id - CBLPeerID *peerID = replicator.peerID; - NSLog(@"Peer ID: %@", peerID); - // end::multipeer-peer-id -} - -- (void)neighborPeers { - CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; - // tag::multipeer-neighbor-peers - NSLog(@"Neighbor Peers:"); - for (CBLPeerID *peerID in replicator.neighborPeers) { - NSLog(@" %@", peerID); - } - // end::multipeer-neighbor-peers + // end::multipeer-document-replication-listener[] } - (void)peerInfo { CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; - // tag::multipeer-peer-info + // tag::multipeer-peer-info[] NSArray *activities = @[ @"stopped", @"offline", @"connecting", @"idle", @"busy" ]; void (^printPeerInfo)(CBLPeerInfo *) = ^(CBLPeerInfo *info) { NSLog(@"Peer ID: %@", info.peerID); NSLog(@" Status: %@", info.online ? @"online" : @"offline"); + + // transports: the set of transports on which this peer was discovered. + NSMutableArray *transportNames = [NSMutableArray array]; + if ((info.transports & kCBLMultipeerTransportWifi) != 0) { + [transportNames addObject:@"wifi"]; + } + + if ((info.transports & kCBLMultipeerTransportBluetooth) != 0) { + [transportNames addObject:@"bluetooth"]; + } + + // replicatorTransport: the transport currently used for replication. + // The value is kCBLMultipeerTransportWifi or kCBLMultipeerTransportBluetooth, + // or 0 if replication is not active. + NSString *replicatorTransport = (info.replicatorTransport == kCBLMultipeerTransportWifi) ? @"wifi" + : (info.replicatorTransport == kCBLMultipeerTransportBluetooth) ? @"bluetooth" + : @"none"; + NSLog(@" Replicating on: %@", replicatorTransport); + NSLog(@" Neighbor Peers:"); for (CBLPeerID *peerID in info.neighborPeers) { NSLog(@" %@", peerID); @@ -303,7 +339,49 @@ - (void)peerInfo { printPeerInfo(peerInfo); } } - // end::multipeer-peer-info + // end::multipeer-peer-info[] +} + + +- (CBLMultipeerReplicator *)createMultipeerReplicator { + CBLMultipeerReplicatorConfiguration *config = [self createConfig]; + NSError *error = nil; + // tag::multipeer-replicator + CBLMultipeerReplicator *replicator = [[CBLMultipeerReplicator alloc] initWithConfig:config error:&error]; + // end::multipeer-replicator + return replicator; +} + +- (void)startReplicator { + CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; + // tag::multipeer-replicator-start + [replicator start]; + // end::multipeer-replicator-start +} + +- (void)stopReplicator { + CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; + // tag::multipeer-replicator-stop + [replicator stop]; + // end::multipeer-replicator-stop +} + +- (void)peerID { + CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; + // tag::multipeer-peer-id + CBLPeerID *peerID = replicator.peerID; + NSLog(@"Peer ID: %@", peerID); + // end::multipeer-peer-id +} + +- (void)neighborPeers { + CBLMultipeerReplicator *replicator = [self createMultipeerReplicator]; + // tag::multipeer-neighbor-peers + NSLog(@"Neighbor Peers:"); + for (CBLPeerID *peerID in replicator.neighborPeers) { + NSLog(@" %@", peerID); + } + // end::multipeer-neighbor-peers } @end diff --git a/modules/objc/examples/code_snippets/SampleCodeTest.m b/modules/objc/examples/code_snippets/SampleCodeTest.m index 6b43de239..25a39f29f 100644 --- a/modules/objc/examples/code_snippets/SampleCodeTest.m +++ b/modules/objc/examples/code_snippets/SampleCodeTest.m @@ -683,11 +683,12 @@ - (void) dontTestCollectionOperatorIn { [CBLQueryExpression property:@"last"], [CBLQueryExpression property:@"username"]]; - [CBLQueryBuilder select:@[[CBLQuerySelectResult all]] + CBLQuery *query = [CBLQueryBuilder select:@[[CBLQuerySelectResult all]] from:[CBLQueryDataSource collection:self.collection] where:[[CBLQueryExpression string:@"Armani"] in:values]]; - NSLog(@"%@", query); // end::query-collection-operator-in[] + + NSLog(@"%@", query); } - (void) dontTestLikeOperator { diff --git a/modules/objc/nav-objc.adoc b/modules/objc/nav-objc.adoc index a755ed40e..384397e5a 100644 --- a/modules/objc/nav-objc.adoc +++ b/modules/objc/nav-objc.adoc @@ -38,7 +38,8 @@ include::partial$_set_page_context_for_objc.adoc[] * xref:objc:landing-replications.adoc[Data Sync] ** xref:objc:dbreplica.adoc[Intra-device Sync] ** xref:objc:replication.adoc[Remote Sync Gateway] - ** xref:objc:p2psync-websocket.adoc[Peer-to-Peer] + ** xref:objc:p2psync-multipeer.adoc[Multipeer Peer-to-Peer Replicator] + ** xref:objc:p2psync-websocket.adoc[Active-Passive Peer-to-Peer] *** xref:objc:p2psync-websocket-using-passive.adoc[Passive Peer] *** xref:objc:p2psync-websocket-using-active.adoc[Active Peer] *** xref:objc:p2psync-custom.adoc[Integrate Custom Listener] @@ -60,4 +61,4 @@ include::partial$_set_page_context_for_objc.adoc[] ** xref:objc:compatibility.adoc[Compatibility] ** xref:objc:supported-os.adoc[Supported Platforms] - // * xref:objc:refer-glossary.adoc[Glossary] + // * xref:objc:refer-glossary.adoc[Glossary] \ No newline at end of file diff --git a/modules/objc/pages/p2psync-multipeer.adoc b/modules/objc/pages/p2psync-multipeer.adoc new file mode 100644 index 000000000..f021dcf57 --- /dev/null +++ b/modules/objc/pages/p2psync-multipeer.adoc @@ -0,0 +1,393 @@ += Multipeer P2P Replicator +ifdef::show_edition[:page-edition: Enterprise Edition] +:description: The Multipeer Replicator enables lightweight, self-organizing mesh networks over Wi-Fi and Bluetooth Low Energy. +:source-language: objc + + +[abstract] +{description} +This approach requires minimal setup and automates peer discovery and connectivity management, making it simpler than xref:p2psync-websocket.adoc[active-passive P2P configurations]. + + +[#introduction] +== Introduction + +Couchbase Lite's Peer-to-Peer synchronization solution offers secure storage and bidirectional data synchronization between mobile and IoT devices without needing a centralized cloud-based control point. + +For small mesh topologies, Multipeer Replicator offers autodiscovery over Wi-Fi and Bluetooth Low Energy, with secure communication via TLS and certificate-based authentication. +The dynamic mesh topology gives optimal peer connectivity and the lightweight and low-maintenance configuration requires less management and less code than using active-passive peer-to-peer sync. + + +== Overview + +To maintain optimal connectivity, efficient data transport, and balanced workloads, the Multipeer Replicator forms a dynamic mesh network among peers in the same group. +The mesh network provides resilience through multiple communication pathways. +If one connection fails, data can flow through alternative routes. +It avoids redundant direct connections, evenly distributes connections across peers, and optimizes communication paths through intelligent routing. + +The mesh network continuously adapts as peers join or leave, automatically healing itself by establishing new connections and rerouting data flow to maintain network integrity. + +This self-organizing approach ensures reliable data synchronization even in challenging network conditions, where individual peer connections may be intermittent or unreliable. + + +== Prerequisites + +The Multipeer Replicator supports two transports for peer discovery and replication: Wi-Fi and Bluetooth Low Energy (BLE). +Wi-Fi is enabled by default. +The enabled transports are configured through the replicator configuration. +See <>. + +=== Transport Support + +.Transport support +[cols="1,1,2,2"] +|=== +|Transport |Available from |Discovery |Notes + +|Wi-Fi +|CBL 3.3 +|DNS-SD (Bonjour) +|Peers must connect to the same Wi-Fi network. Requires `NSBonjourServices` and `NSLocalNetworkUsageDescription` in `Info.plist`. See <>. + +|Bluetooth Low Energy +|CBL 4.1 +|BLE advertising and scanning +|Requires `NSBluetoothAlwaysUsageDescription` in `Info.plist`. See <>. +|=== + +NOTE: When you configure both transports, the Multipeer Replicator automatically selects the best available transport for each peer and switches between them as reachability changes, preferring Wi-Fi over Bluetooth. +See <>. + +=== Supported Platforms + +.iOS version requirements +[cols="2,1"] +|=== +|Feature |Minimum iOS + +|Couchbase Lite general support +|iOS 10 + +|Multipeer Replicator: Wi-Fi transport +|iOS 10 + +|Multipeer Replicator: Bluetooth transport +|iOS 15 +|=== + +See <> for the required `Info.plist` keys. + +See xref:objc:supported-os.adoc[Supported Platforms] for the full platform support matrix. + +[#platform-configuration] +=== Bluetooth Platform Configuration + +This section applies only if your application enables Bluetooth transport. +Applications that use Wi-Fi only do not require these keys. + +iOS applications must declare the required keys in `Info.plist` for each transport they enable. You can also configure these settings through Xcode's Info configuration UI. + +==== Wi-Fi Transport + +To use Wi-Fi transport, declare the Bonjour service type and a local network usage description. + +.NSBonjourServices +[source,xml] +---- +NSBonjourServices + + _couchbaseP2P._tcp + +---- + +.NSLocalNetworkUsageDescription +[source,xml] +---- +NSLocalNetworkUsageDescription +Used for discovering and connecting to peers for peer-to-peer sync. +---- + +==== Bluetooth Transport + +To use Bluetooth transport, declare a Bluetooth usage description. + +.NSBluetoothAlwaysUsageDescription +[source,xml] +---- +NSBluetoothAlwaysUsageDescription +Used for discovering and connecting to peers for peer-to-peer sync. +---- + + +== Configuration + +=== Collection Configurations + +You can specify one or more collections available for replication when creating a `CBLMultipeerReplicatorConfiguration`. +For each collection, you create `CBLMultipeerCollectionConfiguration` with the collection object and optionally configure a custom conflict resolver or any replication filters you want to use for the collection. + + +.Specify collections without any configurations +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-collection-simple", indent=0] +---- + +.Specify collections with some configuration +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-collection-config", indent=0] +---- + +=== Peer Identity + +Each peer in the Multipeer replication is uniquely identified and authenticated by using a peer's certificate. + +Multipeer Replicator uses TLS communication by default and requires a `CBLTLSIdentity` object for specifying the identity. + +You can use either a self-signed certificate for the identity or have an authority or issuer sign the identity's certificate. +The choice depends on your specific security requirements and deployment environment. + +As each peer could be either a client or a server to the other peer in the Multipeer replication environment, you must create the identity's certificate with the extension key usages for both client and server authentication to allow either direction to authenticate the certificate. + +==== CA-Signed Identity + +When using a certificate authority (CA) signed identity, the issuer's certificate authenticates the connecting peer. + +.Get and Create an identity signed by an issuer +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-tlsidentity", indent=0] +---- + +==== Self-Signed Identity + +For environments where certificate authority management is not feasible, you can implement peer identity using self-signed certificates. +This approach is commonly used in closed network environments where devices need to authenticate with each other without external certificate authorities. + +.Creating a self-signed identity for peer authentication +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-selfsigned-tlsidentity", indent=0] +---- + +When using self-signed certificates, implement your own certificate validation logic in the authenticator callback to make sure only trusted peers can join your mesh network. + +=== Peer Authenticator + +`CBLMultipeerReplicator` only supports certificate based authentication. +You can specify the authenticator in two ways: + +* certificate authentication callback +* root certificates. + +When specifying the certificate authentication callback, the callback receives the remote peer's identity certificate. + +When specifying the root certificates, the Multipeer replicator automatically authenticates the remote peer's identity certificate by verifying whether one of the specified root certificates signed the certificate. + + +.Authenticator with authentication callback +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-authenticator-callback", indent=0] +---- + +.Authenticator with root certificates +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-authenticator-rootcerts", indent=0] +---- + +[#transports] +=== Transports + +The `transports` property on `CBLMultipeerReplicatorConfiguration` controls which transports the replicator uses for peer discovery and replication. + +The property type is `CBLMultipeerTransportSet`, an `NS_OPTIONS` bitmask. To enable Bluetooth Low Energy alongside Wi-Fi, combine the transport options with the bitwise OR operator. + +==== Wi-Fi Only + +Wi-Fi is the default transport. Existing applications continue to operate on Wi-Fi only after upgrading to CBL 4.1 with no code changes required. + +.Default (Wi-Fi only) +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-config-transports-default", indent=0] +---- + +==== Bluetooth Only + +To use Bluetooth as the sole transport, set `transports` to `kCBLMultipeerTransportBluetooth`. Peers discover each other using BLE advertising and scanning rather than DNS-SD. + +.Bluetooth only +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-config-transports-bluetooth-only", indent=0] +---- + +==== Wi-Fi and Bluetooth with Automatic Switching + +To enable both transports, set `transports` to `kCBLMultipeerTransportWifi | kCBLMultipeerTransportBluetooth`. The replicator prefers Wi-Fi and falls back to Bluetooth automatically when Wi-Fi is unavailable. See <>. + +.Wi-Fi and Bluetooth +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-config-transports-both", indent=0] +---- + +=== Create MultipeerReplicatorConfiguration + +The `CBLMultipeerReplicatorConfiguration` is created with a `peerGroupID` that identifies the peer-to-peer network used by the app, collection configurations, peer identity, and authenticator. + + +.Creating MultipeerReplicatorConfiguration +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-config", indent=0] +---- + +TIP: Performance may vary in mesh networks depending on your specific environment and number of peers. +We recommend running tests with your network configuration to assess any effects on packet loss or latency. + +[#automatic-transport-switching] +== Automatic Transport Switching + +When `CBLMultipeerReplicator` is configured with both Wi-Fi and Bluetooth transports, it automatically selects the best available transport for each peer and switches transports as reachability changes. + +=== Transport Preference + +The replicator prefers Wi-Fi over Bluetooth when both transports can reach a peer. +Bluetooth acts as a fallback when Wi-Fi cannot reach a peer. + +=== Fallback to Bluetooth + +For an individual peer, `CBLMultipeerReplicator` falls back to Bluetooth when the peer is no longer reachable over Wi-Fi. +This can occur if the peer disables Wi-Fi, becomes unreachable on the local network, or if replication over Wi-Fi fails because of a network-related error. + +In cases of connection or replication failure over Wi-Fi, `CBLMultipeerReplicator` performs a small number of retries before falling back to Bluetooth. + +=== Return to Wi-Fi + +If a peer becomes reachable over Wi-Fi while replication is active over Bluetooth, `CBLMultipeerReplicator` establishes a Wi-Fi connection in parallel with the existing Bluetooth connection. +The Bluetooth connection remains active until the Wi-Fi connection is fully established and replication has resumed over Wi-Fi. +This prevents any interruption in synchronization during the transition. + + +== Life Cycle + +=== Create MultipeerReplicator with Configuration + +.Creating MultipeerReplicator +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-replicator", indent=0] +---- + +=== Start + +.Starting MultipeerReplicator +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-replicator-start", indent=0] +---- + +=== Stop + +.Stopping MultipeerReplicator +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-replicator-stop", indent=0] +---- + +=== Events + +In general, the connection should just work, and most of these optional listen events give status you may only want to use during development and testing. + +Status events include a `transport` property that identifies which transport the event applies to. `CBLMultipeerReplicatorStatus` events are delivered per enabled transport and also as an aggregated status (where `transport` is `nil`) representing the overall replicator state. + +Event types include the following: + +==== Multipeer Replicator Status + +.Multipeer Replicator Status Listener +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-status-listener", indent=0] +---- + +==== Peer Discovery Status + +.Peer Discovery Status Listener +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-peer-discovery-listener", indent=0] +---- + +==== Peer's Replicator Status + +.Peer's Replicator Status Listener +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-replicator-status-listener", indent=0] +---- + +==== Peer's Document Replication + +.Peer's Document Replication Listener +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-document-replication-listener", indent=0] +---- + + +== Peer Info + +=== Peer Identifier + +A unique `peerID`, which is a digest of the peer's identity certificate, identifies each peer. +You can get your `peerID` from the `peerID` property of the `CBLMultipeerReplicator`. + + +.Getting peer ID +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-peer-id", indent=0] +---- + +=== Neighbor Peers + +You can get a list of current online peers' identifiers from the `CBLMultipeerReplicator` using the `neighborPeers` property. + +.Getting neighbor peers +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-neighbor-peers", indent=0] +---- + +=== Peer Info + +The `CBLPeerInfo` object provides information about a peer, including its identifier, certificate, online status, replicator status, neighbor peers, the transports on which the peer was discovered, and the transport currently used for replication. + +.Getting peer info +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-peer-info", indent=0] +---- + + +== Logging + +`CBLLogDomain` sets up the logging of: + +. Peer discovery log messages +. Multipeer replication and mesh network management log messages + +[source,objc] +---- +include::objc:example$code_snippets/MultipeerReplicator.m[tags="multipeer-logdomain", indent=0] +---- + + +== API Reference + +You can find https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-ios}{empty}/couchbase-lite-objc[Objective-C API References] here. \ No newline at end of file diff --git a/modules/objc/pages/p2psync-websocket-using-active.adoc b/modules/objc/pages/p2psync-websocket-using-active.adoc index e9ffb0485..d2b9d2546 100644 --- a/modules/objc/pages/p2psync-websocket-using-active.adoc +++ b/modules/objc/pages/p2psync-websocket-using-active.adoc @@ -426,10 +426,10 @@ Use this to monitor changes and to inform on sync progress; this is an optional You can add and a replicator change listener at any point; it will report changes from the point it is registered. .Best Practice -TIP: Don't forget to save the token so you can remove the listener later +TIP: Do not forget to save the token so you can remove the listener later Use the https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-ios}{empty}/couchbase-lite-objc/Classes/CBLReplicator.html[Replicator] class to add a change listener as a callback to the Replicator (https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-ios}{empty}/couchbase-lite-objc/Classes/CBLReplicator.html#/c:objc(cs)CBLReplicator(im)addChangeListener:[addChangeListener(_:)]) -- see: <>. -You will then be asynchronously notified of state changes. +You are then asynchronously notified of state changes. You can remove a change listener with https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-ios}{empty}/couchbase-lite-objc/Classes/CBLReplicator.html#/c:objc(cs)CBLReplicator(im)removeChangeListenerWithToken[removeChangeListenerWithToken(CBLListenerToken:)]. @@ -440,7 +440,7 @@ You can remove a change listener with https://docs.couchbase.com/mobile/{major}. You can use the https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-ios}{empty}/couchbase-lite-objc/Classes/CBLReplicatorStatus.html[CBLReplicatorStatus] class to check the replicator status. -That is, whether it is actively transferring data or if it has stopped -- see: <>. +That's, whether it's actively transferring data or if it has stopped -- see: <>. The returned _ReplicationStatus_ structure comprises: diff --git a/modules/objc/pages/p2psync-websocket.adoc b/modules/objc/pages/p2psync-websocket.adoc index c9cdc01c2..42452f477 100644 --- a/modules/objc/pages/p2psync-websocket.adoc +++ b/modules/objc/pages/p2psync-websocket.adoc @@ -1,4 +1,3 @@ - = Data Sync Peer-to-Peer :page-aliases: learn/objc-p2psync-websocket.adoc ifdef::show_edition[:page-edition: Enterprise Edition] @@ -20,6 +19,21 @@ Related Content -- https://docs.couchbase.com/mobile/{major}.{minor}.{maintenanc -- +[TIP] +.Multipeer P2P Replicator +==== +xref:p2psync-multipeer.adoc[Multipeer P2P Replicator] is available for Objective-C, offering: + +* Auto-discovery over Wi-Fi and, from CBL 4.1, Bluetooth Low Energy +* Automatic transport switching between Wi-Fi and Bluetooth when both are enabled +* Lightweight and low-maintenance configuration +* Dynamic mesh topology for optimal peer connectivity +* Secure communication via TLS and certificate-based authentication + +For many use cases, this will be quicker to develop for than Active-Passive peer-to-peer sync. +==== + + [#introduction] == Introduction // tag::introduction-full[] @@ -369,7 +383,7 @@ Please check this document for more info: https://source.android.com/security/ke -- -MacOS / iOS:: +MacOS / iOS:: + -- .Secure storage details @@ -524,11 +538,7 @@ https://blog.couchbase.com/[Blog] | https://docs.couchbase.com/tutorials/[Tutorials] . -xref:tutorials:cbl-p2p-sync-websockets:swift/cbl-p2p-sync-websockets.adoc[Getting Started with Peer-to-Peer Synchronization] - ++++ -++++ - - +++++ \ No newline at end of file diff --git a/modules/swift/examples/code_snippets/MultipeerReplicator.swift b/modules/swift/examples/code_snippets/MultipeerReplicator.swift index 651ba8752..a7ce26fe4 100644 --- a/modules/swift/examples/code_snippets/MultipeerReplicator.swift +++ b/modules/swift/examples/code_snippets/MultipeerReplicator.swift @@ -68,7 +68,7 @@ class MultipeerReplicatorSnippets { // Define certificate attributes and expiration date. let attrs: [String: String] = [certAttrCommonName: "MyApp"] let expiration = Calendar.current.date(byAdding: .year, value: 2, to: Date())! - + // Create and store a new self-signed identity in the keychain with a persistent label. identity = try TLSIdentity.createIdentity( for: [.clientAuth, .serverAuth], @@ -79,7 +79,7 @@ class MultipeerReplicatorSnippets { // end::multipeer-selfsigned-tlsidentity[] return identity! } - + func createCASignedIdentity() throws -> TLSIdentity { // tag::multipeer-tlsidentity[] let persistentLabel = "com.myapp.identity" @@ -102,7 +102,7 @@ class MultipeerReplicatorSnippets { // Get issuer's private key and certificate data (DER format) for signing the identity's certificate. let caKey = try getIssuerPrivateKeyData() let caCert = try getIssuerCertificateData() - + // Create and store a new identity signed with the issuer in the keychain with a persistent label. identity = try TLSIdentity.createSignedIdentityInsecure( for: [.clientAuth, .serverAuth], @@ -158,52 +158,81 @@ class MultipeerReplicatorSnippets { return config } - func createMultipeerReplicator() throws -> MultipeerReplicator { - let config = try createConfig() - // tag::multipeer-replicator[] - let replicator = try MultipeerReplicator(config: config) - // end::multipeer-replicator[] - return replicator + func createConfigTransportsDefault() throws -> MultipeerReplicatorConfiguration { + let identity = try createCASignedIdentity() + let authenticator = try authenticatorWithRootCerts() + let collections = try collectionConfig() + + // tag::multipeer-config-transports-default[] + // Wi-Fi is the default transport. No additional configuration is required. + let config = MultipeerReplicatorConfiguration( + peerGroupID: "com.myapp", + identity: identity, + authenticator: authenticator, + collections: collections) + // config.transports defaults to [.wifi] + // end::multipeer-config-transports-default[] + return config } - func startReplicator() throws { - let replicator = try createMultipeerReplicator() - // tag::multipeer-replicator-start[] - replicator.start() - // end::multipeer-replicator-start[] + func createConfigTransportsBoth() throws -> MultipeerReplicatorConfiguration { + let identity = try createCASignedIdentity() + let authenticator = try authenticatorWithRootCerts() + let collections = try collectionConfig() + + // tag::multipeer-config-transports-both[] + var config = MultipeerReplicatorConfiguration( + peerGroupID: "com.myapp", + identity: identity, + authenticator: authenticator, + collections: collections) + config.transports = [.wifi, .bluetooth] + // end::multipeer-config-transports-both[] + return config } - func stopReplicator() throws { - let replicator = try createMultipeerReplicator() - // tag::multipeer-replicator-stop[] - replicator.stop() - // end::multipeer-replicator-stop[] + func createConfigTransportsBluetoothOnly() throws -> MultipeerReplicatorConfiguration { + let identity = try createCASignedIdentity() + let authenticator = try authenticatorWithRootCerts() + let collections = try collectionConfig() + + // tag::multipeer-config-transports-bluetooth-only[] + var config = MultipeerReplicatorConfiguration( + peerGroupID: "com.myapp", + identity: identity, + authenticator: authenticator, + collections: collections) + config.transports = [.bluetooth] + // end::multipeer-config-transports-bluetooth-only[] + return config } func statusListener() throws { let replicator = try createMultipeerReplicator() // tag::multipeer-status-listener[] let token = replicator.addStatusListener { status in + // transport is nil for the aggregated overall status; + // non-nil for a per-transport status update. + let transport = status.transport.map { $0 == .wifi ? "wifi" : "bluetooth" } ?? "all" let state = status.active ? "active" : "inactive" let error = status.error?.localizedDescription ?? "none" - print("Multipeer Replicator: \(state), Error: \(error)") + print("Multipeer Replicator [\(transport)]: \(state), Error: \(error)") } // end::multipeer-status-listener[] - - // to hide the warning print(token) } + func peerDiscoveryListener() throws { let replicator = try createMultipeerReplicator() // tag::multipeer-peer-discovery-listener[] let token = replicator.addPeerDiscoveryStatusListener { status in let online = status.online ? "online" : "offline" - print("Peer Discovery Status - Peer ID: \(status.peerID), Status: \(online)") + let transport = status.transport == .wifi ? "wifi" : "bluetooth" + print("Peer Discovery Status - Peer ID: \(status.peerID), " + + "Transport: \(transport), Status: \(online)") } // end::multipeer-peer-discovery-listener[] - - // to hide the warning print(token) } @@ -214,15 +243,15 @@ class MultipeerReplicatorSnippets { let token = replicator.addPeerReplicatorStatusListener { replStatus in let direction = replStatus.outgoing ? "outgoing" : "incoming" let activity = activities[Int(replStatus.status.activity.rawValue)] + let transport = replStatus.transport == .wifi ? "wifi" : "bluetooth" let error = replStatus.status.error?.localizedDescription ?? "none" print("Peer Replicator Status - Peer ID: \(replStatus.peerID), " + + "Transport: \(transport), " + "Direction: \(direction), " + "Activity: \(activity), " + "Error: \(error)") } // end::multipeer-replicator-status-listener[] - - // to hide the warning print(token) } @@ -231,7 +260,9 @@ class MultipeerReplicatorSnippets { // tag::multipeer-document-replication-listener[] let token = replicator.addPeerDocumentReplicationListener { docRepl in let direction = docRepl.isPush ? "Push" : "Pull" - print("Peer Document Replication - Peer ID: \(docRepl.peerID), Direction: \(direction)") + let transport = docRepl.transport == .wifi ? "wifi" : "bluetooth" + print("Peer Document Replication - Peer ID: \(docRepl.peerID), " + + "Transport: \(transport), Direction: \(direction)") docRepl.documents.forEach { doc in let error = doc.error?.localizedDescription ?? "none" let collection = "\(doc.scope).\(doc.collection)" @@ -242,29 +273,9 @@ class MultipeerReplicatorSnippets { } } // end::multipeer-document-replication-listener[] - - // to hide the warning print(token) } - func peerID() throws { - let replicator = try createMultipeerReplicator() - // tag::multipeer-peer-id[] - let peerID = replicator.peerID - print("Peer ID: \(peerID)") - // end::multipeer-peer-id[] - } - - func neighborPeers() throws { - let replicator = try createMultipeerReplicator() - // tag::multipeer-neighbor-peers[] - print("Neighbor Peers:") - replicator.neighborPeers.forEach { peerID in - print(" \(peerID)") - } - // end::multipeer-neighbor-peers[] - } - func peerInfo() throws { let replicator = try createMultipeerReplicator() // tag::multipeer-peer-info[] @@ -273,6 +284,16 @@ class MultipeerReplicatorSnippets { let printPeerInfo: (PeerInfo) -> Void = { info in print("Peer ID: \(info.peerID)") print(" Status: \(info.online ? "online" : "offline")") + + // transports: the set of transports on which this peer was discovered. + let transports = info.transports.map { $0 == .wifi ? "wifi" : "bluetooth" }.joined(separator: ", ") + print(" Discovered on: \(transports)") + + // replicatorTransport: the transport currently used for replication, + // or nil if replication is not active. + let replicatorTransport = info.replicatorTransport.map { $0 == .wifi ? "wifi" : "bluetooth" } ?? "none" + print(" Replicating on: \(replicatorTransport)") + print(" Neighbor Peers:") info.neighborPeers.forEach { peerID in print(" \(peerID)") @@ -289,9 +310,51 @@ class MultipeerReplicatorSnippets { printPeerInfo(peerInfo) } } + + } // end::multipeer-peer-info[] + + func createMultipeerReplicator() throws -> MultipeerReplicator { + let config = try createConfig() + // tag::multipeer-replicator[] + let replicator = try MultipeerReplicator(config: config) + // end::multipeer-replicator[] + return replicator } + func startReplicator() throws { + let replicator = try createMultipeerReplicator() + // tag::multipeer-replicator-start[] + replicator.start() + // end::multipeer-replicator-start[] + } + + func stopReplicator() throws { + let replicator = try createMultipeerReplicator() + // tag::multipeer-replicator-stop[] + replicator.stop() + // end::multipeer-replicator-stop[] + } + + func peerID() throws { + let replicator = try createMultipeerReplicator() + // tag::multipeer-peer-id[] + let peerID = replicator.peerID + print("Peer ID: \(peerID)") + // end::multipeer-peer-id[] + } + + func neighborPeers() throws { + let replicator = try createMultipeerReplicator() + // tag::multipeer-neighbor-peers[] + print("Neighbor Peers:") + replicator.neighborPeers.forEach { peerID in + print(" \(peerID)") + } + // end::multipeer-neighbor-peers[] + } + + func logging() throws { // tag::multipeer-logdomain[] // Enable verbose console logging for multipeer replicator-related domains only. diff --git a/modules/swift/pages/p2psync-multipeer.adoc b/modules/swift/pages/p2psync-multipeer.adoc index 0014ae233..069656ecc 100644 --- a/modules/swift/pages/p2psync-multipeer.adoc +++ b/modules/swift/pages/p2psync-multipeer.adoc @@ -1,6 +1,6 @@ = Multipeer P2P Replicator ifdef::show_edition[:page-edition: Enterprise Edition] -:description: The Multipeer Replicator enables lightweight, self-organizing mesh networks for apps running on the same local Wi-Fi. +:description: The Multipeer Replicator enables lightweight, self-organizing mesh networks over Wi-Fi and Bluetooth Low Energy. :source-language: swift @@ -15,7 +15,7 @@ This approach requires minimal setup and automates peer discovery and connectivi // tag::introduction[] Couchbase Lite's Peer-to-Peer synchronization solution offers secure storage and bidirectional data synchronization between mobile and IoT devices without needing a centralized cloud-based control point. -For small mesh topologies, Multipeer Replicator offers `autodiscovery` for Wi-Fi-based networks and secure communication via TLS and certificate-based authentication. +For small mesh topologies, Multipeer Replicator offers autodiscovery over Wi-Fi and Bluetooth Low Energy, with secure communication via TLS and certificate-based authentication. The dynamic mesh topology gives optimal peer connectivity and the lightweight and low-maintenance configuration requires less management and less code than using active-passive peer-to-peer sync. // end::introduction[] @@ -25,15 +25,14 @@ The dynamic mesh topology gives optimal peer connectivity and the lightweight an == Overview To maintain optimal connectivity, efficient data transport, and balanced workloads, the Multipeer Replicator forms a dynamic mesh network among peers in the same group. -The mesh network provides resilience through multiple communication pathways - if one connection fails, data can flow through alternative routes. +The mesh network provides resilience through multiple communication pathways. +If one connection fails, data can flow through alternative routes. It avoids redundant direct connections, evenly distributes connections across peers, and optimizes communication paths through intelligent routing. The mesh network continuously adapts as peers join or leave, automatically healing itself by establishing new connections and rerouting data flow to maintain network integrity. This self-organizing approach ensures reliable data synchronization even in challenging network conditions, where individual peer connections may be intermittent or unreliable. -Multipeer Replicator supports Wi-Fi (IP-based transport) as of CBL 3.3. - // Mesh Diagram @@ -69,19 +68,66 @@ Multipeer Replicator supports Wi-Fi (IP-based transport) as of CBL 3.3. == Prerequisites -=== Transport and Peer Discovery Protocol +The Multipeer Replicator supports two transports for peer discovery and replication: Wi-Fi and Bluetooth Low Energy (BLE). +Wi-Fi is enabled by default. +The enabled transports are configured through the replicator configuration. +See <>. + +=== Transport Support + +.Transport support +[cols="1,1,2,2"] +|=== +|Transport |Available from |Discovery |Notes + +|Wi-Fi +|CBL 3.3 +|DNS-SD (Bonjour) +|Peers must connect to the same Wi-Fi network. Requires `NSBonjourServices` and `NSLocalNetworkUsageDescription` in `Info.plist`. See <>. + +|Bluetooth Low Energy +|CBL 4.1 +|BLE advertising and scanning +|Requires `NSBluetoothAlwaysUsageDescription` in `Info.plist`. See <>. +|=== + +NOTE: When you configure both transports, the Multipeer Replicator automatically selects the best available transport for each peer and switches between them as reachability changes, preferring Wi-Fi over Bluetooth. +See <>. + +=== Supported Platforms + +.iOS version requirements +[cols="2,1"] +|=== +|Feature |Minimum iOS + +|Couchbase Lite general support +|iOS 10 + +|Multipeer Replicator: Wi-Fi transport +|iOS 10 + +|Multipeer Replicator: Bluetooth transport +|iOS 15 +|=== + +See <> for the required `Info.plist` keys. + +See xref:swift:supported-os.adoc[Supported Platforms] for the full platform support matrix. -Multipeer Replicator supports Wi-Fi transport, using `DNS-SD` (also known as *Bonjour*) for peer discovery. -You must connect Peers to the same Wi-Fi network to discover and establish connections with one another. +[#platform-configuration] +=== Bluetooth Platform Configuration -=== Configuration Requirements +This section applies only if your application enables Bluetooth transport. +Applications that use Wi-Fi only do not require these keys. -To use `DNS-SD` for peer discovery, iOS apps must declare the *Bonjour* service type and request local network access permissions. -Add the following keys to your app's `Info.plist`: +iOS applications must declare the required keys in `Info.plist` for each transport they enable. You can also configure these settings through Xcode's Info configuration UI. -==== NSBonjourServices -Declare the service type used by MultipeerReplicator: +==== Wi-Fi Transport +To use Wi-Fi transport, declare the Bonjour service type and a local network usage description. + +.NSBonjourServices [source,xml] ---- NSBonjourServices @@ -90,21 +136,23 @@ Declare the service type used by MultipeerReplicator: ---- -==== NSLocalNetworkUsageDescription -Add a usage description for local network access: - +.NSLocalNetworkUsageDescription [source,xml] ---- NSLocalNetworkUsageDescription Used for discovering and connecting to peers for peer-to-peer sync. ---- -TIP: You can also configure these settings through Xcode's Info configuration UI under "*Bonjour Services*" and "*Privacy – Local Network Usage Description*." +==== Bluetooth Transport +To use Bluetooth transport, declare a Bluetooth usage description. -=== Supported Platforms - -We recommend using a recent release of iOS, see xref:swift:supported-os.adoc[Supported Platforms] for details. +.NSBluetoothAlwaysUsageDescription +[source,xml] +---- +NSBluetoothAlwaysUsageDescription +Used for discovering and connecting to peers for peer-to-peer sync. +---- == Configuration @@ -163,7 +211,7 @@ When using self-signed certificates, implement your own certificate validation l === Peer Authenticator -MultpeerReplicator only supports certificate based authentication. +`MultipeerReplicator` only supports certificate based authentication. You can specify the authenticator in two ways: * certificate authentication callback @@ -186,6 +234,41 @@ include::swift:example$code_snippets/MultipeerReplicator.swift[tags="multipeer-a include::swift:example$code_snippets/MultipeerReplicator.swift[tags="multipeer-authenticator-rootcerts", indent=0] ---- +[#transports] +=== Transports + +The `transports` property on `MultipeerReplicatorConfiguration` controls which transports the replicator uses for peer discovery and replication. + +==== Wi-Fi Only + +Wi-Fi is the default transport. Existing applications continue to operate on Wi-Fi only after upgrading to CBL 4.1 with no code changes required. + +.Default (Wi-Fi only) +[source,swift] +---- +include::swift:example$code_snippets/MultipeerReplicator.swift[tags="multipeer-config-transports-default", indent=0] +---- + +==== Bluetooth Only + +To use Bluetooth as the sole transport, set `transports` to `[.bluetooth]`. Peers discover each other using BLE advertising and scanning rather than DNS-SD. + +.Bluetooth only +[source,swift] +---- +include::swift:example$code_snippets/MultipeerReplicator.swift[tags="multipeer-config-transports-bluetooth-only", indent=0] +---- + +==== Wi-Fi and Bluetooth with Automatic Switching + +To enable both transports, set `transports` to include both `.wifi` and `.bluetooth`. The replicator prefers Wi-Fi and falls back to Bluetooth automatically when Wi-Fi is unavailable. See <>. + +.Wi-Fi and Bluetooth +[source,swift] +---- +include::swift:example$code_snippets/MultipeerReplicator.swift[tags="multipeer-config-transports-both", indent=0] +---- + === Create MultipeerReplicatorConfiguration The `MultipeerReplicatorConfiguration` can be created with a `peerGroupID` which is an identity identifies the Peer-to-peer network used by the app, collection configurations, peer identity, and authenticator. @@ -200,6 +283,29 @@ include::swift:example$code_snippets/MultipeerReplicator.swift[tags="multipeer-c TIP: Performance may vary in mesh networks depending on your specific environment and number of peers. We recommend running tests with your network configuration to assess any effects on packet loss or latency. +[#automatic-transport-switching] +== Automatic Transport Switching + +When `MultipeerReplicator` is configured with both Wi-Fi and Bluetooth transports, it automatically selects the best available transport for each peer and switches transports as reachability changes. + +=== Transport Preference + +The replicator prefers Wi-Fi over Bluetooth when both transports can reach a peer. +Bluetooth acts as a fallback when Wi-Fi cannot reach a peer. + +=== Fallback to Bluetooth + +For an individual peer, `MultipeerReplicator` falls back to Bluetooth when the peer is no longer reachable over Wi-Fi. +This can occur if the peer disables Wi-Fi, becomes unreachable on the local network, or if replication over Wi-Fi fails because of a network-related error. + +In cases of connection or replication failure over Wi-Fi, `MultipeerReplicator` performs a small number of retries before falling back to Bluetooth. + +=== Return to Wi-Fi + +If a peer becomes reachable over Wi-Fi while replication is active over Bluetooth, `MultipeerReplicator` establishes a Wi-Fi connection in parallel with the existing Bluetooth connection. +The Bluetooth connection remains active until the Wi-Fi connection is fully established and replication has resumed over Wi-Fi. +This prevents any interruption in synchronization during the transition. + == Life Cycle @@ -230,6 +336,9 @@ include::swift:example$code_snippets/MultipeerReplicator.swift[tags="multipeer-r === Events In general, the connection should just work, and most of these optional listen events give status you may only want to use during development and testing. + +Status events include a `transport` property that identifies which transport the event applies to. `MultipeerReplicatorStatus` events are delivered per enabled transport and also as an aggregated status (where `transport` is `nil`) representing the overall replicator state. + Event types include the following: ==== Multipeer Replicator Status @@ -291,6 +400,8 @@ include::swift:example$code_snippets/MultipeerReplicator.swift[tags="multipeer-n === Peer Info +The `PeerInfo` object provides information about a peer, including its identifier, certificate, online status, replicator status, neighbor peers, the transports on which the peer was discovered, and the transport currently used for replication. + .Getting peer info [source,swift] ---- @@ -313,4 +424,4 @@ include::swift:example$code_snippets/MultipeerReplicator.swift[tags="multipeer-l == API Reference -You can find https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-swift}{empty}/couchbase-lite-swift[Swift API References] here. +You can find https://docs.couchbase.com/mobile/{major}.{minor}.{maintenance-swift}{empty}/couchbase-lite-swift[Swift API References] here. \ No newline at end of file diff --git a/modules/swift/pages/p2psync-websocket.adoc b/modules/swift/pages/p2psync-websocket.adoc index 79a1d779c..dc30802a2 100644 --- a/modules/swift/pages/p2psync-websocket.adoc +++ b/modules/swift/pages/p2psync-websocket.adoc @@ -20,12 +20,13 @@ Related Content -- https://docs.couchbase.com/mobile/{major}.{minor}.{maintenanc ==== xref:p2psync-multipeer.adoc[Multipeer P2P Replicator] is available for Swift, offering: -* Auto-discovery over local Wi-Fi (via `DNS-SD`) +* Auto-discovery over Wi-Fi and Bluetooth Low Energy +* Automatic transport switching between Wi-Fi and Bluetooth when both are enabled * Lightweight and low-maintenance configuration * Dynamic mesh topology for optimal peer connectivity * Secure communication via TLS and certificate-based authentication -For many use cases, this will be quicker to develop for than Active-Passive peer-to-peer sync. +For many use cases, this is quicker to develop for than Active-Passive peer-to-peer sync. ==== @@ -72,7 +73,7 @@ Therefore, to use Peer-to-Peer synchronization in your application, you must con . Point the replicator at the Listener . Initialize the replicator + . Replicator and Listener engage in the configured security protocol exchanges to confirm connection -. If connection is confirmed then replication will commence, synchronizing the two data stores. +. If connection is confirmed then replication commences, synchronizing the two data stores. ==== @@ -96,8 +97,8 @@ Having a Listener on a database still allows you to open replications to the oth For example, a Listener can actively begin replicating to other Listeners while listening for connections. These replications can be for the same or a different database. -The Listener will automatically select a port to use or a user-specified port. -It will also listen on all available networks, unless you specify a specific network. +The Listener automatically selects a port to use or a user-specified port. +It also listens on all available networks, unless you specify a specific network. [#security] === Security @@ -121,17 +122,19 @@ For testing and development purposes, support is provided for the client (active [#error-handling] === Error Handling -When a Listener is stopped, then all connected replicators are notified by a WebSocket error. Your application should distinguish between transient and permanent connectivity errors. +When a Listener is stopped, then all connected replicators are notified by a WebSocket error. +Your application should distinguish between transient and permanent connectivity errors. [#passive-peers] ==== Passive peers -A Passive Peer losing connectivity with an Active Peer will clean up any associated endpoint connections to that Peer. The Active Peer may attempt to reconnect to the Passive Peer. +A Passive Peer losing connectivity with an Active Peer cleans up any associated endpoint connections to that Peer. +The Active Peer may attempt to reconnect to the Passive Peer. [#active-peers] ==== Active peers -An Active Peer permanently losing connectivity with a Passive Peer will cease replicating. +An Active Peer permanently losing connectivity with a Passive Peer ceases replicating. -An Active Peer temporarily losing connectivity with a passive Peer will use exponential backoff functionality to attempt reconnection. +An Active Peer temporarily losing connectivity with a passive Peer uses exponential backoff feature to attempt reconnection. [#delta-sync] === Delta Sync @@ -384,7 +387,7 @@ Please check this document for more info: https://source.android.com/security/ke -- -MacOS / iOS:: +MacOS / iOS:: + -- .Secure storage details @@ -538,12 +541,8 @@ https://forums.couchbase.com/c/mobile/14[Mobile Forum] | https://blog.couchbase.com/[Blog] | https://docs.couchbase.com/tutorials/[Tutorials] -. -xref:tutorials:cbl-p2p-sync-websockets:swift/cbl-p2p-sync-websockets.adoc[Getting Started with Peer-to-Peer Synchronization] ++++ -++++ - - +++++ \ No newline at end of file