diff --git a/buildSrc/src/main/kotlin/multiplatform-convention.gradle.kts b/buildSrc/src/main/kotlin/multiplatform-convention.gradle.kts index c849a93a4..cb175f604 100644 --- a/buildSrc/src/main/kotlin/multiplatform-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/multiplatform-convention.gradle.kts @@ -8,22 +8,22 @@ plugins { kotlin { /* * Source set hierarchy: - * ┌────────┐ - * │ common │ - * └───┬────┘ - * ┌──────────┴───────────┐ - * │ ┌───┴────┐ - * │ │ native │ - * │ └───┬────┘ - * │ ┌──────────┴──────────┐ - * ┌─────┴─────┐ ┌───┴───┐ ┌─────┴──────┐ - * │ jvmCommon │ │ apple │ │ linuxMingw │ - * └─────┬─────┘ └───┬───┘ └─────┬──────┘ - * ┌────┴────┐ │ ┌─────────────┼─────────────┐ - * ╔════╧════╗ ╔══╧══╗ │ ╔════╧═════╗ ╔═════╧══════╗ ╔════╧═════╗ - * ║ android ║ ║ jvm ║ │ ║ linuxX64 ║ ║ linuxArm64 ║ ║ mingwX64 ║ - * ╚═════════╝ ╚═════╝ │ ╚══════════╝ ╚════════════╝ ╚══════════╝ - * ┌─────────────────┬──┴────────────┬───────────┬─────────────┐ + * ┌────────┐ + * │ common │ + * └───┬────┘ + * ┌───────────┬─┴─────────────┐ + * │ ┌──────┴───────┐ ┌───┴────┐ + * │ │ androidApple │ │ native │ + * │ └──────┬───────┘ └───┬────┘ + * │ ┌───┴───┐ ┌─────────┴──────────┐ + * ┌─────┴─────┐ │ ┌──┴─┴──┐ ┌─────┴──────┐ + * │ jvmCommon │ │ │ apple │ │ linuxMingw │ + * └─────┬─────┘ │ └───┬───┘ └─────┬──────┘ + * ┌───┴───┐ │ │ ┌─────────────┼─────────────┐ + * ╔══╧══╗ ╔══╧═══╧══╗ │ ╔════╧═════╗ ╔═════╧══════╗ ╔════╧═════╗ + * ║ jvm ║ ║ android ║ │ ║ linuxX64 ║ ║ linuxArm64 ║ ║ mingwX64 ║ + * ╚═════╝ ╚═════════╝ │ ╚══════════╝ ╚════════════╝ ╚══════════╝ + * ┌─────────────────┬───┴───────────┬───────────┬─────────────┐ * ╔════╧═════╗ ╔═════════╧═════════╗ ╔═══╧════╗ ╔════╧═════╗ ╔═════╧══════╗ * ║ iosArm64 ║ ║ iosSimulatorArm64 ║ ║ iosX64 ║ ║ macosX64 ║ ║ macosArm64 ║ * ╚══════════╝ ╚═══════════════════╝ ╚════════╝ ╚══════════╝ ╚════════════╝ @@ -36,6 +36,10 @@ kotlin { withCompilations { it is KotlinMultiplatformAndroidCompilation } withJvm() } + group("androidApple") { + withCompilations { it is KotlinMultiplatformAndroidCompilation } + group("apple") + } group("native") { group("linuxMingw") { group("linux") diff --git a/couchbase-lite-ee/api/couchbase-lite-ee.klib.api b/couchbase-lite-ee/api/couchbase-lite-ee.klib.api index fc77f78b4..1fcdf4fc6 100644 --- a/couchbase-lite-ee/api/couchbase-lite-ee.klib.api +++ b/couchbase-lite-ee/api/couchbase-lite-ee.klib.api @@ -1235,6 +1235,19 @@ final fun (kotbase/Replicator).kotbase/replicatorChangesFlow(kotlin.coroutines/C final fun (kotbase/ReplicatorConfiguration).kotbase/setAcceptOnlySelfSignedServerCertificate(kotlin/Boolean): kotbase/ReplicatorConfiguration // kotbase/setAcceptOnlySelfSignedServerCertificate|setAcceptOnlySelfSignedServerCertificate@kotbase.ReplicatorConfiguration(kotlin.Boolean){}[0] final fun (kotbase/ReplicatorConfiguration).kotbase/setAllowReplicatingInBackground(kotlin/Boolean): kotbase/ReplicatorConfiguration // kotbase/setAllowReplicatingInBackground|setAllowReplicatingInBackground@kotbase.ReplicatorConfiguration(kotlin.Boolean){}[0] +// Targets: [apple] +final enum class kotbase/MultipeerTransport : kotlin/Enum { // kotbase/MultipeerTransport|null[0] + enum entry BLUETOOTH // kotbase/MultipeerTransport.BLUETOOTH|null[0] + enum entry WIFI // kotbase/MultipeerTransport.WIFI|null[0] + + final val entries // kotbase/MultipeerTransport.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // kotbase/MultipeerTransport.entries.|#static(){}[0] + + final fun toString(): kotlin/String // kotbase/MultipeerTransport.toString|toString(){}[0] + final fun valueOf(kotlin/String): kotbase/MultipeerTransport // kotbase/MultipeerTransport.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // kotbase/MultipeerTransport.values|values#static(){}[0] +} + // Targets: [apple] abstract class <#A: platform.darwin/NSObject> kotbase.internal/AbstractDelegatedClass { // kotbase.internal/AbstractDelegatedClass|null[0] constructor () // kotbase.internal/AbstractDelegatedClass.|(){}[0] @@ -1606,6 +1619,110 @@ final class kotbase/MessagingError : kotbase.internal/DelegatedClass(): kotlin/Boolean // kotbase/MessagingError.isRecoverable.|(){}[0] } +// Targets: [apple] +final class kotbase/MultipeerCertificateAuthenticator : kotbase/MultipeerAuthenticator { // kotbase/MultipeerCertificateAuthenticator|null[0] + constructor (kotlin.collections/List) // kotbase/MultipeerCertificateAuthenticator.|(kotlin.collections.List){}[0] + constructor (kotlin/Function2, kotlin/Boolean>) // kotbase/MultipeerCertificateAuthenticator.|(kotlin.Function2,kotlin.Boolean>){}[0] +} + +// Targets: [apple] +final class kotbase/MultipeerCollectionConfiguration { // kotbase/MultipeerCollectionConfiguration|null[0] + constructor (kotbase/Collection) // kotbase/MultipeerCollectionConfiguration.|(kotbase.Collection){}[0] + + final val collection // kotbase/MultipeerCollectionConfiguration.collection|{}collection[0] + final fun (): kotbase/Collection // kotbase/MultipeerCollectionConfiguration.collection.|(){}[0] + + final var conflictResolver // kotbase/MultipeerCollectionConfiguration.conflictResolver|{}conflictResolver[0] + final fun (): kotlin/Function2? // kotbase/MultipeerCollectionConfiguration.conflictResolver.|(){}[0] + final fun (kotlin/Function2?) // kotbase/MultipeerCollectionConfiguration.conflictResolver.|(kotlin.Function2?){}[0] + final var documentIDs // kotbase/MultipeerCollectionConfiguration.documentIDs|{}documentIDs[0] + final fun (): kotlin.collections/List? // kotbase/MultipeerCollectionConfiguration.documentIDs.|(){}[0] + final fun (kotlin.collections/List?) // kotbase/MultipeerCollectionConfiguration.documentIDs.|(kotlin.collections.List?){}[0] + final var pullFilter // kotbase/MultipeerCollectionConfiguration.pullFilter|{}pullFilter[0] + final fun (): kotlin/Function3, kotlin/Boolean>? // kotbase/MultipeerCollectionConfiguration.pullFilter.|(){}[0] + final fun (kotlin/Function3, kotlin/Boolean>?) // kotbase/MultipeerCollectionConfiguration.pullFilter.|(kotlin.Function3,kotlin.Boolean>?){}[0] + final var pushFilter // kotbase/MultipeerCollectionConfiguration.pushFilter|{}pushFilter[0] + final fun (): kotlin/Function3, kotlin/Boolean>? // kotbase/MultipeerCollectionConfiguration.pushFilter.|(){}[0] + final fun (kotlin/Function3, kotlin/Boolean>?) // kotbase/MultipeerCollectionConfiguration.pushFilter.|(kotlin.Function3,kotlin.Boolean>?){}[0] + + final fun setConflictResolver(kotlin/Function2?): kotbase/MultipeerCollectionConfiguration // kotbase/MultipeerCollectionConfiguration.setConflictResolver|setConflictResolver(kotlin.Function2?){}[0] + final fun setDocumentIDs(kotlin.collections/List?): kotbase/MultipeerCollectionConfiguration // kotbase/MultipeerCollectionConfiguration.setDocumentIDs|setDocumentIDs(kotlin.collections.List?){}[0] + final fun setPullFilter(kotlin/Function3, kotlin/Boolean>?): kotbase/MultipeerCollectionConfiguration // kotbase/MultipeerCollectionConfiguration.setPullFilter|setPullFilter(kotlin.Function3,kotlin.Boolean>?){}[0] + final fun setPushFilter(kotlin/Function3, kotlin/Boolean>?): kotbase/MultipeerCollectionConfiguration // kotbase/MultipeerCollectionConfiguration.setPushFilter|setPushFilter(kotlin.Function3,kotlin.Boolean>?){}[0] + + final object Companion { // kotbase/MultipeerCollectionConfiguration.Companion|null[0] + final fun fromCollections(kotlin.collections/Collection): kotlin.collections/Set // kotbase/MultipeerCollectionConfiguration.Companion.fromCollections|fromCollections(kotlin.collections.Collection){}[0] + } +} + +// Targets: [apple] +final class kotbase/MultipeerReplicator : kotbase.internal/DelegatedClass, kotlin/AutoCloseable { // kotbase/MultipeerReplicator|null[0] + constructor (kotbase/MultipeerReplicatorConfiguration) // kotbase/MultipeerReplicator.|(kotbase.MultipeerReplicatorConfiguration){}[0] + + final val config // kotbase/MultipeerReplicator.config|{}config[0] + final fun (): kotbase/MultipeerReplicatorConfiguration // kotbase/MultipeerReplicator.config.|(){}[0] + final val neighborPeers // kotbase/MultipeerReplicator.neighborPeers|{}neighborPeers[0] + final fun (): kotlin.collections/Set // kotbase/MultipeerReplicator.neighborPeers.|(){}[0] + final val peerId // kotbase/MultipeerReplicator.peerId|{}peerId[0] + final fun (): kotbase/PeerInfo.PeerId // kotbase/MultipeerReplicator.peerId.|(){}[0] + + final fun addPeerDiscoveryStatusListener(kotlin.coroutines/CoroutineContext, kotlin.coroutines/SuspendFunction1): kotbase/ListenerToken // kotbase/MultipeerReplicator.addPeerDiscoveryStatusListener|addPeerDiscoveryStatusListener(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1){}[0] + final fun addPeerDiscoveryStatusListener(kotlin/Function1): kotbase/ListenerToken // kotbase/MultipeerReplicator.addPeerDiscoveryStatusListener|addPeerDiscoveryStatusListener(kotlin.Function1){}[0] + final fun addPeerDiscoveryStatusListener(kotlinx.coroutines/CoroutineScope, kotlin.coroutines/SuspendFunction1) // kotbase/MultipeerReplicator.addPeerDiscoveryStatusListener|addPeerDiscoveryStatusListener(kotlinx.coroutines.CoroutineScope;kotlin.coroutines.SuspendFunction1){}[0] + final fun addPeerDocumentReplicationListener(kotlin.coroutines/CoroutineContext, kotlin.coroutines/SuspendFunction1): kotbase/ListenerToken // kotbase/MultipeerReplicator.addPeerDocumentReplicationListener|addPeerDocumentReplicationListener(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1){}[0] + final fun addPeerDocumentReplicationListener(kotlin/Function1): kotbase/ListenerToken // kotbase/MultipeerReplicator.addPeerDocumentReplicationListener|addPeerDocumentReplicationListener(kotlin.Function1){}[0] + final fun addPeerDocumentReplicationListener(kotlinx.coroutines/CoroutineScope, kotlin.coroutines/SuspendFunction1) // kotbase/MultipeerReplicator.addPeerDocumentReplicationListener|addPeerDocumentReplicationListener(kotlinx.coroutines.CoroutineScope;kotlin.coroutines.SuspendFunction1){}[0] + final fun addPeerReplicatorStatusListener(kotlin.coroutines/CoroutineContext, kotlin.coroutines/SuspendFunction1): kotbase/ListenerToken // kotbase/MultipeerReplicator.addPeerReplicatorStatusListener|addPeerReplicatorStatusListener(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1){}[0] + final fun addPeerReplicatorStatusListener(kotlin/Function1): kotbase/ListenerToken // kotbase/MultipeerReplicator.addPeerReplicatorStatusListener|addPeerReplicatorStatusListener(kotlin.Function1){}[0] + final fun addPeerReplicatorStatusListener(kotlinx.coroutines/CoroutineScope, kotlin.coroutines/SuspendFunction1) // kotbase/MultipeerReplicator.addPeerReplicatorStatusListener|addPeerReplicatorStatusListener(kotlinx.coroutines.CoroutineScope;kotlin.coroutines.SuspendFunction1){}[0] + final fun addStatusListener(kotlin.coroutines/CoroutineContext, kotlin.coroutines/SuspendFunction1): kotbase/ListenerToken // kotbase/MultipeerReplicator.addStatusListener|addStatusListener(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1){}[0] + final fun addStatusListener(kotlin/Function1): kotbase/ListenerToken // kotbase/MultipeerReplicator.addStatusListener|addStatusListener(kotlin.Function1){}[0] + final fun addStatusListener(kotlinx.coroutines/CoroutineScope, kotlin.coroutines/SuspendFunction1) // kotbase/MultipeerReplicator.addStatusListener|addStatusListener(kotlinx.coroutines.CoroutineScope;kotlin.coroutines.SuspendFunction1){}[0] + final fun close() // kotbase/MultipeerReplicator.close|close(){}[0] + final fun getPeerInfo(kotbase/PeerInfo.PeerId): kotbase/PeerInfo? // kotbase/MultipeerReplicator.getPeerInfo|getPeerInfo(kotbase.PeerInfo.PeerId){}[0] + final fun getStatus(kotbase/MultipeerTransport): kotbase/MultipeerReplicatorStatus? // kotbase/MultipeerReplicator.getStatus|getStatus(kotbase.MultipeerTransport){}[0] + final fun start() // kotbase/MultipeerReplicator.start|start(){}[0] + final fun stop() // kotbase/MultipeerReplicator.stop|stop(){}[0] +} + +// Targets: [apple] +final class kotbase/MultipeerReplicatorConfiguration { // kotbase/MultipeerReplicatorConfiguration|null[0] + constructor (kotbase/MultipeerReplicatorConfiguration) // kotbase/MultipeerReplicatorConfiguration.|(kotbase.MultipeerReplicatorConfiguration){}[0] + constructor (kotlin/String, kotbase/TLSIdentity, kotbase/MultipeerCertificateAuthenticator, kotlin.collections/Collection, kotlin.collections/Set = ...) // kotbase/MultipeerReplicatorConfiguration.|(kotlin.String;kotbase.TLSIdentity;kotbase.MultipeerCertificateAuthenticator;kotlin.collections.Collection;kotlin.collections.Set){}[0] + + final var authenticator // kotbase/MultipeerReplicatorConfiguration.authenticator|{}authenticator[0] + final fun (): kotbase/MultipeerCertificateAuthenticator // kotbase/MultipeerReplicatorConfiguration.authenticator.|(){}[0] + final fun (kotbase/MultipeerCertificateAuthenticator) // kotbase/MultipeerReplicatorConfiguration.authenticator.|(kotbase.MultipeerCertificateAuthenticator){}[0] + final var collections // kotbase/MultipeerReplicatorConfiguration.collections|{}collections[0] + final fun (): kotlin.collections/Set // kotbase/MultipeerReplicatorConfiguration.collections.|(){}[0] + final fun (kotlin.collections/Set) // kotbase/MultipeerReplicatorConfiguration.collections.|(kotlin.collections.Set){}[0] + final var identity // kotbase/MultipeerReplicatorConfiguration.identity|{}identity[0] + final fun (): kotbase/TLSIdentity // kotbase/MultipeerReplicatorConfiguration.identity.|(){}[0] + final fun (kotbase/TLSIdentity) // kotbase/MultipeerReplicatorConfiguration.identity.|(kotbase.TLSIdentity){}[0] + final var peerGroupID // kotbase/MultipeerReplicatorConfiguration.peerGroupID|{}peerGroupID[0] + final fun (): kotlin/String // kotbase/MultipeerReplicatorConfiguration.peerGroupID.|(){}[0] + final fun (kotlin/String) // kotbase/MultipeerReplicatorConfiguration.peerGroupID.|(kotlin.String){}[0] + final var transports // kotbase/MultipeerReplicatorConfiguration.transports|{}transports[0] + final fun (): kotlin.collections/Set // kotbase/MultipeerReplicatorConfiguration.transports.|(){}[0] + final fun (kotlin.collections/Set) // kotbase/MultipeerReplicatorConfiguration.transports.|(kotlin.collections.Set){}[0] + + final fun setAuthenticator(kotbase/MultipeerCertificateAuthenticator): kotbase/MultipeerReplicatorConfiguration // kotbase/MultipeerReplicatorConfiguration.setAuthenticator|setAuthenticator(kotbase.MultipeerCertificateAuthenticator){}[0] + final fun setCollections(kotlin.collections/Collection): kotbase/MultipeerReplicatorConfiguration // kotbase/MultipeerReplicatorConfiguration.setCollections|setCollections(kotlin.collections.Collection){}[0] + final fun setIdentity(kotbase/TLSIdentity): kotbase/MultipeerReplicatorConfiguration // kotbase/MultipeerReplicatorConfiguration.setIdentity|setIdentity(kotbase.TLSIdentity){}[0] + final fun setPeerGroupID(kotlin/String): kotbase/MultipeerReplicatorConfiguration // kotbase/MultipeerReplicatorConfiguration.setPeerGroupID|setPeerGroupID(kotlin.String){}[0] + final fun setTransports(kotlin.collections/Set): kotbase/MultipeerReplicatorConfiguration // kotbase/MultipeerReplicatorConfiguration.setTransports|setTransports(kotlin.collections.Set){}[0] +} + +// Targets: [apple] +final class kotbase/MultipeerReplicatorStatus : kotbase.internal/DelegatedClass { // kotbase/MultipeerReplicatorStatus|null[0] + final val error // kotbase/MultipeerReplicatorStatus.error|{}error[0] + final fun (): kotbase/CouchbaseLiteException? // kotbase/MultipeerReplicatorStatus.error.|(){}[0] + final val isActive // kotbase/MultipeerReplicatorStatus.isActive|{}isActive[0] + final fun (): kotlin/Boolean // kotbase/MultipeerReplicatorStatus.isActive.|(){}[0] + final val transport // kotbase/MultipeerReplicatorStatus.transport|{}transport[0] + final fun (): kotbase/MultipeerTransport? // kotbase/MultipeerReplicatorStatus.transport.|(){}[0] +} + // Targets: [apple] final class kotbase/OrderBy : kotbase.internal/AbstractDelegatedClass, kotbase/LimitRouter, kotbase/Query { // kotbase/OrderBy|null[0] final var parameters // kotbase/OrderBy.parameters|{}parameters[0] @@ -1641,6 +1758,68 @@ final class kotbase/Parameters : kotbase.internal/DelegatedClass { // kotbase/PeerDiscoveryStatus|null[0] + final val isOnline // kotbase/PeerDiscoveryStatus.isOnline|{}isOnline[0] + final fun (): kotlin/Boolean // kotbase/PeerDiscoveryStatus.isOnline.|(){}[0] + final val peer // kotbase/PeerDiscoveryStatus.peer|{}peer[0] + final fun (): kotbase/PeerInfo.PeerId // kotbase/PeerDiscoveryStatus.peer.|(){}[0] + final val transport // kotbase/PeerDiscoveryStatus.transport|{}transport[0] + final fun (): kotbase/MultipeerTransport // kotbase/PeerDiscoveryStatus.transport.|(){}[0] +} + +// Targets: [apple] +final class kotbase/PeerDocumentReplication : kotbase.internal/DelegatedClass { // kotbase/PeerDocumentReplication|null[0] + final val documents // kotbase/PeerDocumentReplication.documents|{}documents[0] + final fun (): kotlin.collections/List // kotbase/PeerDocumentReplication.documents.|(){}[0] + final val isPush // kotbase/PeerDocumentReplication.isPush|{}isPush[0] + final fun (): kotlin/Boolean // kotbase/PeerDocumentReplication.isPush.|(){}[0] + final val peer // kotbase/PeerDocumentReplication.peer|{}peer[0] + final fun (): kotbase/PeerInfo.PeerId // kotbase/PeerDocumentReplication.peer.|(){}[0] + final val transport // kotbase/PeerDocumentReplication.transport|{}transport[0] + final fun (): kotbase/MultipeerTransport // kotbase/PeerDocumentReplication.transport.|(){}[0] +} + +// Targets: [apple] +final class kotbase/PeerInfo : kotbase.internal/DelegatedClass { // kotbase/PeerInfo|null[0] + final val certificate // kotbase/PeerInfo.certificate|{}certificate[0] + final fun (): kotlin/ByteArray? // kotbase/PeerInfo.certificate.|(){}[0] + final val isOnline // kotbase/PeerInfo.isOnline|{}isOnline[0] + final fun (): kotlin/Boolean // kotbase/PeerInfo.isOnline.|(){}[0] + final val neighbors // kotbase/PeerInfo.neighbors|{}neighbors[0] + final fun (): kotlin.collections/Set // kotbase/PeerInfo.neighbors.|(){}[0] + final val peerId // kotbase/PeerInfo.peerId|{}peerId[0] + final fun (): kotbase/PeerInfo.PeerId // kotbase/PeerInfo.peerId.|(){}[0] + final val replicatorStatus // kotbase/PeerInfo.replicatorStatus|{}replicatorStatus[0] + final fun (): kotbase/ReplicatorStatus // kotbase/PeerInfo.replicatorStatus.|(){}[0] + final val replicatorTransport // kotbase/PeerInfo.replicatorTransport|{}replicatorTransport[0] + final fun (): kotbase/MultipeerTransport? // kotbase/PeerInfo.replicatorTransport.|(){}[0] + final val transports // kotbase/PeerInfo.transports|{}transports[0] + final fun (): kotlin.collections/Set // kotbase/PeerInfo.transports.|(){}[0] + + final class PeerId : kotbase.internal/DelegatedClass { // kotbase/PeerInfo.PeerId|null[0] + final val bytes // kotbase/PeerInfo.PeerId.bytes|{}bytes[0] + final fun (): kotlin/ByteArray // kotbase/PeerInfo.PeerId.bytes.|(){}[0] + } + + final object Companion { // kotbase/PeerInfo.Companion|null[0] + final val PEER_ID_LENGTH // kotbase/PeerInfo.Companion.PEER_ID_LENGTH|{}PEER_ID_LENGTH[0] + final fun (): kotlin/Int // kotbase/PeerInfo.Companion.PEER_ID_LENGTH.|(){}[0] + } +} + +// Targets: [apple] +final class kotbase/PeerReplicatorStatus : kotbase.internal/DelegatedClass { // kotbase/PeerReplicatorStatus|null[0] + final val isOutgoing // kotbase/PeerReplicatorStatus.isOutgoing|{}isOutgoing[0] + final fun (): kotlin/Boolean // kotbase/PeerReplicatorStatus.isOutgoing.|(){}[0] + final val peerId // kotbase/PeerReplicatorStatus.peerId|{}peerId[0] + final fun (): kotbase/PeerInfo.PeerId // kotbase/PeerReplicatorStatus.peerId.|(){}[0] + final val status // kotbase/PeerReplicatorStatus.status|{}status[0] + final fun (): kotbase/ReplicatorStatus // kotbase/PeerReplicatorStatus.status.|(){}[0] + final val transport // kotbase/PeerReplicatorStatus.transport|{}transport[0] + final fun (): kotbase/MultipeerTransport // kotbase/PeerReplicatorStatus.transport.|(){}[0] +} + // Targets: [apple] final class kotbase/QueryChange : kotbase.internal/DelegatedClass { // kotbase/QueryChange|null[0] final val error // kotbase/QueryChange.error|{}error[0] @@ -2193,6 +2372,9 @@ sealed class kotbase/ListenerToken : kotbase.internal/DelegatedProtocol // kotbase/MultipeerAuthenticator|null[0] + // Targets: [apple] sealed class kotbase/Ordering : kotbase.internal/DelegatedClass { // kotbase/Ordering|null[0] final class SortOrder : kotbase/Ordering { // kotbase/Ordering.SortOrder|null[0] @@ -2214,6 +2396,9 @@ final var kotbase/pinnedServerSecCertificate // kotbase/pinnedServerSecCertifica // Targets: [apple] final fun (kotbase/ReplicatorConfiguration).kotbase/setPinnedServerSecCertificate(kotlinx.cinterop/CPointer?): kotbase/ReplicatorConfiguration // kotbase/setPinnedServerSecCertificate|setPinnedServerSecCertificate@kotbase.ReplicatorConfiguration(kotlinx.cinterop.CPointer?){}[0] +// Targets: [apple] +final fun (kotbase/TLSIdentity.Companion).kotbase/createdSignedIdentityInsecure(kotlin.collections/Set, kotlin.collections/Map, kotlin/ByteArray, kotlin/ByteArray, kotlin.time/Instant?, kotlin/String): kotbase/TLSIdentity // kotbase/createdSignedIdentityInsecure|createdSignedIdentityInsecure@kotbase.TLSIdentity.Companion(kotlin.collections.Set;kotlin.collections.Map;kotlin.ByteArray;kotlin.ByteArray;kotlin.time.Instant?;kotlin.String){}[0] + // Targets: [apple] final fun (kotlin/ByteArray).kotbase.ext/toCFData(): kotlinx.cinterop/CPointer // kotbase.ext/toCFData|toCFData@kotlin.ByteArray(){}[0] diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerAuthenticator.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerAuthenticator.kt new file mode 100644 index 000000000..d6ec1e581 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerAuthenticator.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * **ENTERPRISE EDITION API** + * + * Opaque interface for the authenticator used by a [MultipeerReplicator] + * to authenticate peers. + */ +public expect sealed class MultipeerAuthenticator diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerCertificateAuthenticator.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerCertificateAuthenticator.kt new file mode 100644 index 000000000..b19797b71 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerCertificateAuthenticator.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * **ENTERPRISE EDITION API** + * + * A certificate authenticator used by the [MultipeerReplicator] to authenticate + * a peer by verifying the peer's certificate. + */ +public expect class MultipeerCertificateAuthenticator : MultipeerAuthenticator { + + /** + * Initialize with the root certificates to trust. + * + * The peer's certificate will be accepted only if it can be verified + * using (only) one of the certificates in the passed list. + * + * @param rootCerts root certificates used to verify peer certificate chains. + */ + public constructor(rootCerts: List) + + /** + * Initialize with a delegate implementing custom validation logic for verifying + * the received certificate chain. See [MultipeerCertificateAuthenticatorDelegate]. + * + * @param delegate The delegate to use for authentication. + */ + public constructor(delegate: MultipeerCertificateAuthenticatorDelegate) +} diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerCertificateAuthenticatorDelegate.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerCertificateAuthenticatorDelegate.kt new file mode 100644 index 000000000..8a9aff9bc --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerCertificateAuthenticatorDelegate.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * **ENTERPRISE EDITION API** + * + * Functional interface for custom authentication logic, allowing clients to + * implement their own authentication strategy. Pass implementations of this + * interface to the [MultipeerCertificateAuthenticator] to realize specific + * authentication strategies. + * + * Authenticate a peer based on the passed certificates. + * Note that the passed certificates have not been validated. All validation and + * authorization are the responsibility of the implementation. + * + * @param peerId the ID of the peer being authenticated. + * @param certs the certificate chain received from the peer. + * @return true to accept the peer, false to reject. + */ +public typealias MultipeerCertificateAuthenticatorDelegate = (peerId: PeerInfo.PeerId, certs: List) -> Boolean diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerCollectionConfiguration.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerCollectionConfiguration.kt new file mode 100644 index 000000000..456534dfc --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerCollectionConfiguration.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * **ENTERPRISE EDITION API** + * + * A configuration for a collection in a multipeer environment. + * + * This class allows you to configure document IDs to filter, push and pull filters, + * and a custom conflict resolver for an associated collection. + * + * @constructor Creates a new configuration instance for the given collection. + * + * @param collection The [Collection] instance to replicate. + */ +public class MultipeerCollectionConfiguration( + + /** + * The collection this configuration replicates. + */ + public val collection: Collection +) { + + internal constructor(config: MultipeerCollectionConfiguration) : this(config.collection) { + documentIDs = config.documentIDs + pushFilter = config.pushFilter + pullFilter = config.pullFilter + conflictResolver = config.conflictResolver + } + + /** + * Sets a collection of document IDs to filter by: if given, only documents + * with these IDs will be pushed and/or pulled. + * + * @param documentIDs The document IDs. + * @return this. + */ + public fun setDocumentIDs(documentIDs: List?): MultipeerCollectionConfiguration { + this.documentIDs = documentIDs + return this + } + + /** + * Sets a filter object for validating whether the documents can be pushed + * to the remote peer. Only documents for which the function returns true + * are replicated. + * + * @param pushFilter The filter to filter the document to be pushed. + * @return this. + */ + public fun setPushFilter(pushFilter: MultipeerReplicationFilter?): MultipeerCollectionConfiguration { + this.pushFilter = pushFilter + return this + } + + /** + * Sets a filter object for validating whether the documents can be pulled from the + * remote peer. Only documents for which the function returns true are replicated. + * + * @param pullFilter The filter to filter the document to be pulled. + * @return this. + */ + public fun setPullFilter(pullFilter: MultipeerReplicationFilter?): MultipeerCollectionConfiguration { + this.pullFilter = pullFilter + return this + } + + /** + * Sets the custom conflict resolver. If not specified, the default conflict + * resolver will be used. + * + * @param conflictResolver A conflict resolver. + * @return this. + */ + public fun setConflictResolver(conflictResolver: MultipeerConflictResolver?): MultipeerCollectionConfiguration { + this.conflictResolver = conflictResolver + return this + } + + /** + * A collection of document IDs to filter (possibly null): if not null, only + * documents with these IDs will be pushed and/or pulled. + */ + public var documentIDs: List? = null + set(value) { + if (value.isNullOrEmpty()) { + field = null + return + } + value.forEach { + require(it.isNotEmpty()) { "Document Id cannot be null or empty" } + } + field = value.toList() + } + + /** + * The filter used to determine whether a document will be pushed + * to the remote peer (possibly null). + */ + public var pushFilter: MultipeerReplicationFilter? = null + + /** + * The filter used to determine whether a document will be pulled + * from the remote peer (possibly null). + */ + public var pullFilter: MultipeerReplicationFilter? = null + + /** + * The custom conflict resolver (possibly null). + */ + public var conflictResolver: MultipeerConflictResolver? = null + + public companion object { + + /** + * Creates a set of [MultipeerCollectionConfiguration] instances from the given collections. + * + * Each specified collection will be wrapped in a [MultipeerCollectionConfiguration] + * using default settings (no filters and no custom conflict resolvers). + * + * This is a convenience method for configuring multiple collections with + * default replication settings. If custom configurations are needed, + * construct [MultipeerCollectionConfiguration] instances directly instead. + * + * @param collections A collection of [Collection] instances to replicate. + * @return A set of [MultipeerCollectionConfiguration] instances for the provided collections. + */ + public fun fromCollections( + collections: kotlin.collections.Collection + ): Set { + require(collections.isNotEmpty()) { "collections must not be empty" } + return collections.map { MultipeerCollectionConfiguration(it) }.toSet() + } + } +} diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerConflictResolver.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerConflictResolver.kt new file mode 100644 index 000000000..59c09fdd9 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerConflictResolver.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * The multipeer conflict resolver. + * + * Callback: called to resolve conflicts that arise during the replication process. + * + * @param peerId the ID of the peer that the conflicting document came from. + * @param conflict description of the conflicting documents. + * @return the resolved document, or null to delete the document. + */ +public typealias MultipeerConflictResolver = (peerId: PeerInfo.PeerId, conflict: Conflict) -> Document? diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicationFilter.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicationFilter.kt new file mode 100644 index 000000000..c13247af3 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicationFilter.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * The multipeer replication filter. + * + * Filter used to determine whether a document should be replicated or not. + * Document push and pull will be allowed if the predicate returns true. + * The document will not be replicated, push or pull, otherwise. + * + * @param peerId the ID of the peer the document is replicated to or from. + * @param document the document to be replicated. + * @param flags the document flags. + * @return true to allow the document to be replicated. + */ +public typealias MultipeerReplicationFilter = (peerId: PeerInfo.PeerId, document: Document, flags: Set) -> Boolean diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicator.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicator.kt new file mode 100644 index 000000000..f8c56fa67 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicator.kt @@ -0,0 +1,248 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotlinx.coroutines.CoroutineScope +import kotlin.coroutines.CoroutineContext + +/** + * **ENTERPRISE EDITION API** + * + * The MultipeerReplicator replicates database collections between devices in a + * multipeer mesh network. It provides peer discovery, connection management, + * and data replication. + * + * @constructor Initializes the MultipeerReplicator with the given configuration. + * + * @param config The configuration for the replicator. + * @throws CouchbaseLiteException on error creating the replicator. + */ +public expect class MultipeerReplicator +@Throws(CouchbaseLiteException::class) +public constructor(config: MultipeerReplicatorConfiguration) : AutoCloseable { + + /** + * The configuration. + */ + public val config: MultipeerReplicatorConfiguration + + /** + * The peer ID of this replicator. + */ + public val peerId: PeerInfo.PeerId + + /** + * The set of peer IDs currently online; empty if none. + * + * Warning: This property can block for several seconds in some cases. It is recommended + * to not access it on the UI thread. This is a known issue we plan to address in a + * future release. + */ + public val neighborPeers: Set + + /** + * Returns the current status of this replicator for the given transport, or null + * if the transport is not configured. + * + * Warning: This method can block for several seconds in some cases. It is recommended to not + * call this on the UI thread. This is a known issue we plan to address in a future release. + * + * @param transport The transport whose status is being queried. + * @return The status for the given transport, or null if the transport + * is not configured for this replicator. + */ + public fun getStatus(transport: MultipeerTransport): MultipeerReplicatorStatus? + + /** + * Starts peer discovery and initiates replication with connected peers. + */ + public fun start() + + /** + * Stops peer discovery and all replicators. + */ + public fun stop() + + /** + * Returns information about the peer with the given ID, or null if no such peer + * is known. + * + * Warning: This method can block for several seconds in some cases. It is recommended to not + * call this on the UI thread. This is a known issue we plan to address in a future release. + * + * @param peerId The ID of the peer. + * @return The peer's info, or null if no peer with that ID is known. + */ + public fun getPeerInfo(peerId: PeerInfo.PeerId): PeerInfo? + + /** + * Adds a listener to receive updates on the multipeer replicator status. + * + * The updates will be delivered on the main thread. + * + * @param listener The listener to be added. + * @return An opaque listener token object for removing the listener. + * + * @see ListenerToken.remove + */ + public fun addStatusListener(listener: MultipeerReplicatorStatusListener): ListenerToken + + /** + * Adds a listener to receive updates on the multipeer replicator status with a [CoroutineContext] + * that will be used to launch coroutines the listener will be called on. Coroutines will be launched in + * a [CoroutineScope] that is canceled when the listener is removed. + * + * @param context coroutine context in which the listener will run + * @param listener The listener to be added. + * @return An opaque listener token object for removing the listener. + * + * @see ListenerToken.remove + */ + public fun addStatusListener( + context: CoroutineContext, + listener: MultipeerReplicatorStatusSuspendListener + ): ListenerToken + + /** + * Adds a listener to receive updates on the multipeer replicator status with a [CoroutineScope] + * that will be used to launch coroutines the listener will be called on. The listener is removed when + * the scope is canceled. + * + * @param scope coroutine scope in which the listener will run + * @param listener The listener to be added. + */ + public fun addStatusListener(scope: CoroutineScope, listener: MultipeerReplicatorStatusSuspendListener) + + /** + * Adds a listener to receive updates on the peer discovery status. + * + * The updates will be delivered on the main thread. + * + * @param listener The listener to be added. + * @return An opaque listener token object for removing the listener. + * + * @see ListenerToken.remove + */ + public fun addPeerDiscoveryStatusListener(listener: PeerDiscoveryStatusListener): ListenerToken + + /** + * Adds a listener to receive updates on the peer discovery status with a [CoroutineContext] + * that will be used to launch coroutines the listener will be called on. Coroutines will be launched in + * a [CoroutineScope] that is canceled when the listener is removed. + * + * @param context coroutine context in which the listener will run + * @param listener The listener to be added. + * @return An opaque listener token object for removing the listener. + * + * @see ListenerToken.remove + */ + public fun addPeerDiscoveryStatusListener( + context: CoroutineContext, + listener: PeerDiscoveryStatusSuspendListener + ): ListenerToken + + /** + * Adds a listener to receive updates on the peer discovery status with a [CoroutineScope] + * that will be used to launch coroutines the listener will be called on. The listener is removed when + * the scope is canceled. + * + * @param scope coroutine scope in which the listener will run + * @param listener The listener to be added. + */ + public fun addPeerDiscoveryStatusListener(scope: CoroutineScope, listener: PeerDiscoveryStatusSuspendListener) + + /** + * Adds a listener to receive updates on the replicator status for each connected peer. + * + * The updates will be delivered on the main thread. + * + * @param listener The listener to be added. + * @return An opaque listener token object for removing the listener. + * + * @see ListenerToken.remove + */ + public fun addPeerReplicatorStatusListener(listener: PeerReplicatorStatusListener): ListenerToken + + /** + * Adds a listener to receive updates on the replicator status for each connected peer with a + * [CoroutineContext] that will be used to launch coroutines the listener will be called on. + * Coroutines will be launched in a [CoroutineScope] that is canceled when the listener is removed. + * + * @param context coroutine context in which the listener will run + * @param listener The listener to be added. + * @return An opaque listener token object for removing the listener. + * + * @see ListenerToken.remove + */ + public fun addPeerReplicatorStatusListener( + context: CoroutineContext, + listener: PeerReplicatorStatusSuspendListener + ): ListenerToken + + /** + * Adds a listener to receive updates on the replicator status for each connected peer with a + * [CoroutineScope] that will be used to launch coroutines the listener will be called on. + * The listener is removed when the scope is canceled. + * + * @param scope coroutine scope in which the listener will run + * @param listener The listener to be added. + */ + public fun addPeerReplicatorStatusListener(scope: CoroutineScope, listener: PeerReplicatorStatusSuspendListener) + + /** + * Adds a listener to receive updates on the document replication status for each connected peer. + * + * The updates will be delivered on the main thread. + * + * @param listener The listener to be added. + * @return An opaque listener token object for removing the listener. + * + * @see ListenerToken.remove + */ + public fun addPeerDocumentReplicationListener(listener: PeerDocumentReplicationListener): ListenerToken + + /** + * Adds a listener to receive updates on the document replication status for each connected peer with a + * [CoroutineContext] that will be used to launch coroutines the listener will be called on. + * Coroutines will be launched in a [CoroutineScope] that is canceled when the listener is removed. + * + * @param context coroutine context in which the listener will run + * @param listener The listener to be added. + * @return An opaque listener token object for removing the listener. + * + * @see ListenerToken.remove + */ + public fun addPeerDocumentReplicationListener( + context: CoroutineContext, + listener: PeerDocumentReplicationSuspendListener + ): ListenerToken + + /** + * Adds a listener to receive updates on the document replication status for each connected peer with a + * [CoroutineScope] that will be used to launch coroutines the listener will be called on. + * The listener is removed when the scope is canceled. + * + * @param scope coroutine scope in which the listener will run + * @param listener The listener to be added. + */ + public fun addPeerDocumentReplicationListener(scope: CoroutineScope, listener: PeerDocumentReplicationSuspendListener) + + /** + * Immediately release the native replicator and all its resources. If the replicator is + * still running, this method makes a best-effort attempt to stop it. + */ + override fun close() +} diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicatorConfiguration.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicatorConfiguration.kt new file mode 100644 index 000000000..8b305fcc9 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicatorConfiguration.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * **ENTERPRISE EDITION API** + * + * Configuration for a MultipeerReplicator. + * + * This configuration is used to set up an immutable [MultipeerReplicator] + * with necessary parameters. + * + * @constructor Initializes the configuration with a peer group identifier, + * identity, authenticator and collections. + * + * @param peerGroupID The group identifier for discovering and connecting peers. + * @param identity The peer's TLS identity. The certificate must be valid for both client and server authentication. + * @param authenticator Peer authenticator for verifying the connecting peer's certificate. + * @param collections A collection of [MultipeerCollectionConfiguration] instances + * representing the collections to replicate and their configurations. + * @param transports Transport options for peer discovery and replication. The default is Wi-Fi only. + */ +public class MultipeerReplicatorConfiguration( + peerGroupID: String, + + /** + * Peer identity. + * + * Note: The identity's certificate must be both server and client certificate. + */ + public var identity: TLSIdentity, + + /** + * Peer authenticator for verifying the connecting peer's certificate. + */ + public var authenticator: MultipeerCertificateAuthenticator, + + collections: kotlin.collections.Collection, + transports: Set = setOf(MultipeerTransport.WIFI) +) { + + /** + * Clone an existing configuration. + * + * @param config A multipeer replicator configuration to copy. + */ + public constructor(config: MultipeerReplicatorConfiguration) : this( + config.peerGroupID, + config.identity, + config.authenticator, + config.collections.map { MultipeerCollectionConfiguration(it) }, + config.transports + ) + + /** + * Identifier for discovering and connecting peers. + */ + public var peerGroupID: String = checkPeerGroupID(peerGroupID) + set(value) { + field = checkPeerGroupID(value) + } + + /** + * A set of collections with their configurations, to be replicated. + */ + public var collections: Set = checkCollections(collections) + set(value) { + field = checkCollections(value) + } + + /** + * Transport options for peer discovery and replication. The default is Wi-Fi only. + * When multiple transports are specified, the replicator automatically selects and + * switches between available transports, preferring Wi-Fi over Bluetooth. + * + * Setting an empty set will reset the transports to the default, Wi-Fi only. + */ + public var transports: Set = defaultTransports(transports) + set(value) { + field = defaultTransports(value) + } + + /** + * Sets the peer group ID. + * + * @param peerGroupID The peer group ID. + * @return this. + */ + public fun setPeerGroupID(peerGroupID: String): MultipeerReplicatorConfiguration { + this.peerGroupID = peerGroupID + return this + } + + /** + * Set the peer identity. + * + * Note: The identity's certificate must be both server and client certificate. + * + * @param identity The peer identity. + * @return this. + */ + public fun setIdentity(identity: TLSIdentity): MultipeerReplicatorConfiguration { + this.identity = identity + return this + } + + /** + * Set the peer authenticator. + * + * @param authenticator The peer authenticator. + * @return this. + */ + public fun setAuthenticator(authenticator: MultipeerCertificateAuthenticator): MultipeerReplicatorConfiguration { + this.authenticator = authenticator + return this + } + + /** + * Sets the collections to replicate, each with its own configuration, such as filters + * and a custom conflict resolver. + * + * If custom configuration is not needed, use [MultipeerCollectionConfiguration.fromCollections] + * to create configurations directly from a collection of [Collection] instances. + * + * @param collections A collection of [MultipeerCollectionConfiguration] + * instances representing the collections and their configurations. + * @return this. + */ + public fun setCollections( + collections: kotlin.collections.Collection + ): MultipeerReplicatorConfiguration { + this.collections = collections.toSet() + return this + } + + /** + * Set the transport options for peer discovery and replication. + * + * @param transports The transport options. An empty set will reset + * the transports to the default, Wi-Fi only. + * @return this. + */ + public fun setTransports(transports: Set): MultipeerReplicatorConfiguration { + this.transports = transports + return this + } + + private companion object { + + private fun checkPeerGroupID(peerGroupID: String): String { + require(peerGroupID.isNotEmpty()) { "peerGroupID must not be empty" } + return peerGroupID + } + + private fun checkCollections( + collections: kotlin.collections.Collection + ): Set { + require(collections.isNotEmpty()) { "collections must not be empty" } + return collections.toSet() + } + + private fun defaultTransports(transports: Set): Set = + transports.ifEmpty { setOf(MultipeerTransport.WIFI) } + } +} diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicatorStatus.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicatorStatus.kt new file mode 100644 index 000000000..fe2f2823c --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicatorStatus.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * **ENTERPRISE EDITION API** + * + * The multipeer replicator's status. + */ +public expect class MultipeerReplicatorStatus { + + /** + * The transport the status is reported for, or null if the status is an aggregate + * across all transports. + */ + public val transport: MultipeerTransport? + + /** + * Whether the multipeer replicator is active or not. + * This means the replicator is discovering peers or performing replication. + * + * Note that this value is approximate. + */ + public val isActive: Boolean + + /** + * The error encountered, if any. + */ + public val error: CouchbaseLiteException? +} diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicatorStatusListener.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicatorStatusListener.kt new file mode 100644 index 000000000..ea157c3ec --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerReplicatorStatusListener.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * The listener interface for receiving multipeer replicator status events. + * + * The callback function from MultipeerReplicator + * + * @param status the multipeer replicator status + */ +public typealias MultipeerReplicatorStatusListener = (status: MultipeerReplicatorStatus) -> Unit + +/** + * The listener interface for receiving multipeer replicator status events, called within a coroutine. + * + * The callback function from MultipeerReplicator + * + * @param status the multipeer replicator status + */ +public typealias MultipeerReplicatorStatusSuspendListener = suspend (status: MultipeerReplicatorStatus) -> Unit diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerTransport.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerTransport.kt new file mode 100644 index 000000000..75dff3cb5 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/MultipeerTransport.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * **ENTERPRISE EDITION API** + * + * Transport types for multipeer communication. + */ +public expect enum class MultipeerTransport { + + /** + * Wi-Fi using DNS-SD for peer discovery. + */ + WIFI, + + /** + * Bluetooth Low Energy (BLE). + */ + BLUETOOTH +} diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDiscoveryStatus.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDiscoveryStatus.kt new file mode 100644 index 000000000..f8d387d2d --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDiscoveryStatus.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * **ENTERPRISE EDITION API** + * + * Discovery status of a specific peer. + */ +public expect class PeerDiscoveryStatus { + + /** + * The associated peer. + */ + public val peer: PeerInfo.PeerId + + /** + * Whether the peer is discovered (online) or no longer visible (offline): best effort. + */ + public val isOnline: Boolean + + /** + * The transport on which the peer was discovered. + */ + public val transport: MultipeerTransport +} diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDiscoveryStatusListener.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDiscoveryStatusListener.kt new file mode 100644 index 000000000..6eaee7e82 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDiscoveryStatusListener.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * The listener interface for receiving peer discovery status events. + * + * The callback function from MultipeerReplicator + * + * @param status the peer discovery status + */ +public typealias PeerDiscoveryStatusListener = (status: PeerDiscoveryStatus) -> Unit + +/** + * The listener interface for receiving peer discovery status events, called within a coroutine. + * + * The callback function from MultipeerReplicator + * + * @param status the peer discovery status + */ +public typealias PeerDiscoveryStatusSuspendListener = suspend (status: PeerDiscoveryStatus) -> Unit diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDocumentReplication.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDocumentReplication.kt new file mode 100644 index 000000000..534705b33 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDocumentReplication.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * **ENTERPRISE EDITION API** + * + * Document replication status for a peer. + */ +public expect class PeerDocumentReplication { + + /** + * The associated peer. + */ + public val peer: PeerInfo.PeerId + + /** + * The transport currently used for replication. + */ + public val transport: MultipeerTransport + + /** + * A flag indicating the direction of the replication: + * true if the replication is push, false if it is pull. + */ + public val isPush: Boolean + + /** + * The replicated documents. + */ + public val documents: List +} diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDocumentReplicationListener.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDocumentReplicationListener.kt new file mode 100644 index 000000000..eb3d2f08b --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerDocumentReplicationListener.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * The listener interface for receiving per-peer document replication events. + * + * The callback function from MultipeerReplicator + * + * @param replication the peer document replication information + */ +public typealias PeerDocumentReplicationListener = (replication: PeerDocumentReplication) -> Unit + +/** + * The listener interface for receiving per-peer document replication events, called within a coroutine. + * + * The callback function from MultipeerReplicator + * + * @param replication the peer document replication information + */ +public typealias PeerDocumentReplicationSuspendListener = suspend (replication: PeerDocumentReplication) -> Unit diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerInfo.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerInfo.kt new file mode 100644 index 000000000..6471c59b4 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerInfo.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * **ENTERPRISE EDITION API** + * + * Information about a peer during multipeer replication. + */ +public expect class PeerInfo { + + /** + * A peer identifier based on 32-byte SHA-256 binary data. + */ + public class PeerId { + + /** + * The peer's unique ID: the raw 32-byte SHA-256 data. + */ + public val bytes: ByteArray + } + + /** + * The peer ID. + */ + public val peerId: PeerId + + /** + * The peer's TLS certificate. This will only be available after the peer has + * been authenticated during the connection process established for replication. + */ + public val certificate: ByteArray? + + /** + * Whether the peer is online. + */ + public val isOnline: Boolean + + /** + * The transports on which the peer was discovered. + */ + public val transports: Set + + /** + * The transport currently used for replication, if any. + */ + public val replicatorTransport: MultipeerTransport? + + /** + * The current status of the replicator. + */ + public val replicatorStatus: ReplicatorStatus + + /** + * The set of visible neighbors. + */ + public val neighbors: Set + + public companion object { + + /** + * The length, in bytes, of a peer ID. + */ + public val PEER_ID_LENGTH: Int + } +} diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerReplicatorStatus.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerReplicatorStatus.kt new file mode 100644 index 000000000..2134fc148 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerReplicatorStatus.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * **ENTERPRISE EDITION API** + * + * The replicator's status for a peer. + */ +public expect class PeerReplicatorStatus { + + /** + * The associated peer. + */ + public val peerId: PeerInfo.PeerId + + /** + * The transport currently used for replication. + */ + public val transport: MultipeerTransport + + /** + * A flag indicating the direction of the replication: + * true if the replicator is outgoing (i.e. initiated the outgoing connection), + * false otherwise (i.e. accepted the incoming connection). + */ + public val isOutgoing: Boolean + + /** + * Replicator status and progress. + */ + public val status: ReplicatorStatus +} diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerReplicatorStatusListener.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerReplicatorStatusListener.kt new file mode 100644 index 000000000..a8771b9c7 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/PeerReplicatorStatusListener.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +/** + * The listener interface for receiving peer replicator status events. + * + * The callback function from MultipeerReplicator + * + * @param status the peer replicator status + */ +public typealias PeerReplicatorStatusListener = (status: PeerReplicatorStatus) -> Unit + +/** + * The listener interface for receiving peer replicator status events, called within a coroutine. + * + * The callback function from MultipeerReplicator + * + * @param status the peer replicator status + */ +public typealias PeerReplicatorStatusSuspendListener = suspend (status: PeerReplicatorStatus) -> Unit diff --git a/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/TLSIdentityExt.kt b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/TLSIdentityExt.kt new file mode 100644 index 000000000..7acdb651d --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleMain/ee/kotbase/TLSIdentityExt.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotlin.time.Instant + +/** + * **ENTERPRISE EDITION API** + * + * Create a certificate signed by the provided CA. This is inherently insecure and should + * only be used in environments that are meant for testing, or environments that are + * otherwise secured against intruders by other mechanisms. + * + * The created identity key will be stored in the secure storage using the specified alias, + * while neither the CA certificate nor the CA signing key will be stored anywhere. + * The identity can be recovered using the provided alias, after this method returns. + * + * @param usage certificate usage: server or client + * @param attributes certificate attributes + * @param caKey The PEM or DER data of the CA signing key + * @param caCertificate The PEM or DER data of the CA certificate + * @param expiration expiration date + * @param alias alias used to identify the key/certificate entry, in the keystore + * @return the new identity + * @throws CouchbaseLiteException on failure to create + */ +@Throws(CouchbaseLiteException::class) +public expect fun TLSIdentity.Companion.createdSignedIdentityInsecure( + usage: Set, + attributes: Map, + caKey: ByteArray, + caCertificate: ByteArray, + expiration: Instant?, + alias: String +): TLSIdentity diff --git a/couchbase-lite-ee/src/androidAppleTest/ee/kotbase/MultipeerReplicatorTest.kt b/couchbase-lite-ee/src/androidAppleTest/ee/kotbase/MultipeerReplicatorTest.kt new file mode 100644 index 000000000..b36a918a4 --- /dev/null +++ b/couchbase-lite-ee/src/androidAppleTest/ee/kotbase/MultipeerReplicatorTest.kt @@ -0,0 +1,1144 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotlinx.atomicfu.locks.reentrantLock +import kotlinx.atomicfu.locks.withLock +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.CountDownLatch +import kotlin.test.* +import kotlin.time.Clock +import kotlin.time.Duration.Companion.seconds + +// MultipeerReplicatorTest.m +// +// Test Spec: https://github.com/couchbaselabs/couchbase-lite-api/blob/master/spec/tests/T0012-MultipeerReplicator.md +// Version: 1.1.0 +// +// Tests that perform actual peer-to-peer networking are @Ignored: +// they require local network access (macOS and iOS prompt for local-network +// permission, and they cannot run on CI VMs or the iOS simulator) and are +// flaky. The upstream iOS test suite disables them as well +// (kDisableMultipeerTests in MultipeerReplicatorTest.m). +class MultipeerReplicatorTest : BaseDbTest() { + + private var identityCount = 0 + + private lateinit var otherDatabase: Database + + private val lock = reentrantLock() + private val replicatorStatuses = + mutableMapOf>() + private val listenerTokens = mutableMapOf>() + + @BeforeTest + fun setUpMultipeerReplicatorTest() { + if (!keyStoreAccessAllowed) return + initPlatformKeyStore() + cleanupIdentities() + otherDatabase = createDb("other_db") + } + + @AfterTest + fun tearDownMultipeerReplicatorTest() { + if (::otherDatabase.isInitialized) { + eraseDb(otherDatabase) + } + if (keyStoreAccessAllowed) { + cleanupIdentities() + } + } + + private fun identityName(num: Int): String = + "$TEST_IDENTITY_COMMON_NAME-$num" + + private fun cleanupIdentities() { + for (i in 1..TEST_MAX_IDENTITY) { + try { + TLSIdentity.deleteIdentity(identityName(i)) + } catch (_: Exception) { + // ignore + } + } + } + + /** + * Creates a self-signed identity valid for both client and server authentication. + * + * Adaptation from the iOS test suite: the iOS tests create identities signed by a + * CA issuer from a test asset, using an ObjC-only API. Kotbase uses self-signed + * identities, which the multipeer replicator accepts equally. + */ + private fun createIdentity(): TLSIdentity { + assertTrue(identityCount++ <= TEST_MAX_IDENTITY) + val alias = identityName(identityCount) + val cn = "$alias-${Clock.System.now().toEpochMilliseconds()}" + return TLSIdentity.createIdentity( + TEST_KEY_USAGES, + mapOf(TLSIdentity.CERT_ATTRIBUTE_COMMON_NAME to cn), + null, + alias + ) + } + + private fun multipeerReplicator( + database: Database, + authenticator: MultipeerCertificateAuthenticator? = null, + identity: TLSIdentity? = null, + configure: (MultipeerCollectionConfiguration.() -> Unit)? = null + ): MultipeerReplicator { + val collection = database.defaultCollection + + val collConfig = MultipeerCollectionConfiguration(collection) + configure?.invoke(collConfig) + + val auth = authenticator ?: MultipeerCertificateAuthenticator { peerId, certs -> + assertNotNull(peerId) + assertTrue(certs.isNotEmpty()) + true + } + + val config = MultipeerReplicatorConfiguration( + TEST_PEER_GROUP_ID, + identity ?: createIdentity(), + auth, + listOf(collConfig) + ) + + return MultipeerReplicator(config) + } + + private fun addListenerToken(repl: MultipeerReplicator, token: ListenerToken) { + lock.withLock { + listenerTokens.getOrPut(repl) { mutableListOf() }.add(token) + } + } + + private fun resetListenerTokens(repl: MultipeerReplicator) { + lock.withLock { + listenerTokens.remove(repl) + }?.forEach { it.remove() } + } + + private fun startMultipeerReplicator(repl: MultipeerReplicator) { + val active = CountDownLatch(1) + val token = repl.addStatusListener { status -> + if (status.transport != null) return@addStatusListener // Ignore per transport status + assertNull(status.error) + if (status.isActive) { + active.countDown() + } + } + + addListenerToken( + repl, + repl.addPeerReplicatorStatusListener { status -> + lock.withLock { + replicatorStatuses.getOrPut(repl) { mutableMapOf() }[status.peerId] = status + } + } + ) + + repl.start() + runBlocking { assertTrue(active.stdWait()) } + token.remove() + } + + private fun stopMultipeerReplicator(repl: MultipeerReplicator) { + val inactive = CountDownLatch(1) + val token = repl.addStatusListener { status -> + if (status.transport != null) return@addStatusListener // Ignore per transport status + assertNull(status.error) + if (!status.isActive) { + inactive.countDown() + } + } + repl.stop() + runBlocking { assertTrue(inactive.stdWait()) } + token.remove() + + resetListenerTokens(repl) + } + + private fun replicatorStatus(repl: MultipeerReplicator, peerId: PeerInfo.PeerId): PeerReplicatorStatus? = + lock.withLock { + replicatorStatuses[repl]?.get(peerId) + } + + private fun waitForReplicatorStatus( + repl: MultipeerReplicator, + peerId: PeerInfo.PeerId, + activityLevel: ReplicatorActivityLevel, + errorCode: Int = 0 + ) { + var idleCount = 0 + val deadline = Clock.System.now() + STD_TIMEOUT_SEC.seconds + while (Clock.System.now() < deadline) { + val status = replicatorStatus(repl, peerId) + if (status != null && status.status.activityLevel == activityLevel) { + when (activityLevel) { + ReplicatorActivityLevel.IDLE -> { + if (++idleCount >= 3) return // IDLE confirmed + runBlocking { delay(0.5.seconds) } + continue + } + ReplicatorActivityLevel.STOPPED -> { + if (errorCode != 0) { + assertEquals(errorCode, status.status.error?.code) + } else { + assertNull(status.status.error) + } + return + } + else -> return + } + } + idleCount = 0 // Reset if not idle + runBlocking { delay(0.5.seconds) } + } + fail("Timeout waiting for activity level: $activityLevel") + } + + // Configuration + + /** + * ### 1. TestConfiguration + * + * Test that the configuration can be created successfully + * and that each property returns the correct value. + */ + @Test + fun testConfiguration() { + if (!keyStoreAccessAllowed) return + + // Create the configuration: + val identity = createIdentity() + + val auth = MultipeerCertificateAuthenticator { _, _ -> true } + + val collConfig = MultipeerCollectionConfiguration(testDatabase.defaultCollection) + + collConfig.documentIDs = listOf("doc1") + + collConfig.pushFilter = { _, doc, _ -> doc.id == "doc1" } + collConfig.pullFilter = { _, doc, _ -> doc.id == "doc4" } + + val resolver: MultipeerConflictResolver = { _, conflict -> conflict.remoteDocument } + collConfig.conflictResolver = resolver + + val config = MultipeerReplicatorConfiguration( + TEST_PEER_GROUP_ID, + identity, + auth, + listOf(collConfig) + ) + + // Check that each property returns the correct value: + assertEquals(TEST_PEER_GROUP_ID, config.peerGroupID) + assertEquals(identity, config.identity) + assertEquals(auth, config.authenticator) + assertEquals(setOf(collConfig), config.collections) + assertEquals(setOf(MultipeerTransport.WIFI), config.transports) + + val configuredCollection = config.collections.first() + assertEquals(testDatabase.defaultCollection, configuredCollection.collection) + assertEquals(listOf("doc1"), configuredCollection.documentIDs) + assertNotNull(configuredCollection.pushFilter) + assertNotNull(configuredCollection.pullFilter) + assertEquals(resolver, configuredCollection.conflictResolver) + } + + /** + * ### 2. TestConfigurationValidation + * + * Validate that a configuration cannot be created if the peerGroupID + * or collections are invalid. + */ + @Test + fun testConfigurationValidation() { + if (!keyStoreAccessAllowed) return + + val identity = createIdentity() + + val auth = MultipeerCertificateAuthenticator { _, _ -> true } + + val collConfig = MultipeerCollectionConfiguration(testDatabase.defaultCollection) + + // Create the configuration with an empty peerGroupID: + assertFailsWith { + MultipeerReplicatorConfiguration("", identity, auth, listOf(collConfig)) + } + + // Create the configuration with an empty collections: + assertFailsWith { + MultipeerReplicatorConfiguration(TEST_PEER_GROUP_ID, identity, auth, emptyList()) + } + } + + // Stop and Restart + + /** + * ### 3. TestStartStop + * + * Test that a MultipeerReplicator can be started and stopped. + */ + @Test + @Ignore + fun testStartStop() { + if (!keyStoreAccessAllowed) return + + val repl = multipeerReplicator(testDatabase) + + val active = CountDownLatch(1) + val inactive = CountDownLatch(1) + + val token = repl.addStatusListener { status -> + if (status.transport != null) return@addStatusListener // Ignore per transport status + assertNull(status.error) + if (status.isActive) { + active.countDown() + } else { + inactive.countDown() + } + } + + repl.start() + runBlocking { assertTrue(active.stdWait()) } + + repl.stop() + runBlocking { assertTrue(inactive.stdWait()) } + + token.remove() + } + + /** + * ### 4. TestImmediateStartStop + * + * Test that a MultipeerReplicator can be started then stopped immediately. + */ + @Test + @Ignore + fun testImmediateStartStop() { + if (!keyStoreAccessAllowed) return + + val repl = multipeerReplicator(testDatabase) + + val inactive = CountDownLatch(1) + val token = repl.addStatusListener { status -> + if (status.transport != null) return@addStatusListener // Ignore per transport status + if (!status.isActive) { + inactive.countDown() + } + } + + repl.start() + repl.stop() + + runBlocking { assertTrue(inactive.stdWait()) } + + token.remove() + } + + // Database Close + + /** + * ### 5. TestStopOnCloseDatabase + * + * Test that a MultipeerReplicator is stopped when the database is closed. + */ + @Test + @Ignore + fun testStopOnCloseDatabase() { + if (!keyStoreAccessAllowed) return + + val repl = multipeerReplicator(testDatabase) + + val active = CountDownLatch(1) + val inactive = CountDownLatch(1) + val token = repl.addStatusListener { status -> + if (status.transport != null) return@addStatusListener // Ignore per transport status + assertNull(status.error) + if (status.isActive) { + active.countDown() + } else { + inactive.countDown() + } + } + + repl.start() + runBlocking { assertTrue(active.stdWait()) } + + testDatabase.close() + + runBlocking { assertTrue(inactive.stdWait()) } + + token.remove() + } + + // Replication + + /** + * ### 6. TestBasicReplication + * + * Test basic replication between two multipeer replicators. + */ + @Test + @Ignore + fun testBasicReplication() { + if (!keyStoreAccessAllowed) return + + val doc1 = MutableDocument("doc1") + doc1.setString("greeting", "hello") + testDatabase.defaultCollection.save(doc1) + + val doc2 = MutableDocument("doc2") + doc2.setString("greeting", "hi") + otherDatabase.defaultCollection.save(doc2) + + val repl1 = multipeerReplicator(testDatabase) + startMultipeerReplicator(repl1) + + val repl2 = multipeerReplicator(otherDatabase) + startMultipeerReplicator(repl2) + + waitForReplicatorStatus(repl1, repl2.peerId, ReplicatorActivityLevel.IDLE) + waitForReplicatorStatus(repl2, repl1.peerId, ReplicatorActivityLevel.IDLE) + + assertNotNull(testDatabase.defaultCollection.getDocument("doc1")) + assertNotNull(testDatabase.defaultCollection.getDocument("doc2")) + assertNotNull(otherDatabase.defaultCollection.getDocument("doc1")) + assertNotNull(otherDatabase.defaultCollection.getDocument("doc2")) + + stopMultipeerReplicator(repl1) + stopMultipeerReplicator(repl2) + } + + // Authenticator + + /** + * ### 7. TestAuthenticationWithRootCerts + * + * Test that the authenticator can authenticate a peer's certificate that is + * signed by the given root certificates. + * + * Adaptation from the iOS test suite: with self-signed identities, the peers' + * own certificates serve as the trusted root certificates. + */ + @Test + @Ignore + fun testAuthenticateWithRootCerts() { + if (!keyStoreAccessAllowed) return + + val identity1 = createIdentity() + val identity2 = createIdentity() + + val auth = MultipeerCertificateAuthenticator( + listOf(identity1.certs[0], identity2.certs[0]) + ) + + // Peer#1 + val repl1 = multipeerReplicator(testDatabase, authenticator = auth, identity = identity1) + startMultipeerReplicator(repl1) + + // Peer#2 + val repl2 = multipeerReplicator(otherDatabase, authenticator = auth, identity = identity2) + startMultipeerReplicator(repl2) + + // Wait until the replicator is IDLE + waitForReplicatorStatus(repl1, repl2.peerId, ReplicatorActivityLevel.IDLE) + waitForReplicatorStatus(repl2, repl1.peerId, ReplicatorActivityLevel.IDLE) + + stopMultipeerReplicator(repl1) + stopMultipeerReplicator(repl2) + } + + /** + * ### 8. TestAuthenticationWithRootCertsFailed + * + * Test that the authenticator can reject a peer's certificate that is not + * signed by the given root certificates. + * + * Adaptation from the iOS test suite: an unrelated self-signed certificate is + * used as the trusted root certificate, instead of a CA cert from a test asset. + */ + @Test + @Ignore + fun testAuthenticateWithRootCertsFailed() { + if (!keyStoreAccessAllowed) return + + // A root certificate unrelated to either peer's identity: + val unrelatedCert = createIdentity().certs[0] + val auth = MultipeerCertificateAuthenticator(listOf(unrelatedCert)) + + // Note: Only the active replicator will have the replicator status error. The passive + // replicator doesn't get started due to the authentication error. As we don't know in + // advance which peer will have an active replicator, set up the listener for both. + val stopped = CountDownLatch(1) + + val listener: PeerReplicatorStatusListener = { status -> + if (status.status.activityLevel == ReplicatorActivityLevel.STOPPED) { + val error = status.status.error + assertNotNull(error) + assertEquals(CBLError.Code.TLS_CLIENT_CERT_REJECTED, error.code) + assertEquals(CBLError.Domain.CBLITE, error.domain) + stopped.countDown() + } + } + + // Peer#1 + val repl1 = multipeerReplicator(testDatabase, authenticator = auth) + val token1 = repl1.addPeerReplicatorStatusListener(listener) + startMultipeerReplicator(repl1) + + // Peer#2 + val repl2 = multipeerReplicator(otherDatabase, authenticator = auth) + val token2 = repl2.addPeerReplicatorStatusListener(listener) + startMultipeerReplicator(repl2) + + runBlocking { assertTrue(stopped.stdWait()) } + + stopMultipeerReplicator(repl1) + stopMultipeerReplicator(repl2) + + token1.remove() + token2.remove() + } + + /** + * ### 9. TestAuthenticationWithCallbackFailed + * + * Test that the authenticator can reject via the authentication callback. + */ + @Test + @Ignore + fun testAuthenticateWithCallbackFailed() { + if (!keyStoreAccessAllowed) return + + val auth = MultipeerCertificateAuthenticator { peerId, certs -> + assertNotNull(peerId) + assertTrue(certs.isNotEmpty()) + false + } + + // Note: Only the active replicator will have the replicator status error. The passive + // replicator doesn't get started due to the authentication error. As we don't know in + // advance which peer will have an active replicator, set up the listener for both. + val stopped = CountDownLatch(1) + + val listener: PeerReplicatorStatusListener = { status -> + if (status.status.activityLevel == ReplicatorActivityLevel.STOPPED) { + val error = status.status.error + assertNotNull(error) + assertEquals(CBLError.Code.TLS_CLIENT_CERT_REJECTED, error.code) + assertEquals(CBLError.Domain.CBLITE, error.domain) + stopped.countDown() + } + } + + // Peer#1 + val repl1 = multipeerReplicator(testDatabase, authenticator = auth) + val token1 = repl1.addPeerReplicatorStatusListener(listener) + startMultipeerReplicator(repl1) + + // Peer#2 + val repl2 = multipeerReplicator(otherDatabase, authenticator = auth) + val token2 = repl2.addPeerReplicatorStatusListener(listener) + startMultipeerReplicator(repl2) + + runBlocking { assertTrue(stopped.stdWait()) } + + stopMultipeerReplicator(repl1) + stopMultipeerReplicator(repl2) + + token1.remove() + token2.remove() + } + + // Listener + + /** + * ### 10. TestPeerDiscoveryListener + * + * Test that the peer discovery listener can receive the peer discovery events. + */ + @Test + @Ignore + fun testPeerDiscoveryListener() { + if (!keyStoreAccessAllowed) return + + val repl1 = multipeerReplicator(testDatabase) + val repl2 = multipeerReplicator(otherDatabase) + + val peer2Online = CountDownLatch(1) + val token1 = repl1.addPeerDiscoveryStatusListener { status -> + assertEquals(repl2.peerId, status.peer) + if (status.isOnline) { + peer2Online.countDown() + } + } + startMultipeerReplicator(repl1) + + val peer1Online = CountDownLatch(1) + val peer1Offline = CountDownLatch(1) + val token2 = repl2.addPeerDiscoveryStatusListener { status -> + assertEquals(repl1.peerId, status.peer) + if (status.isOnline) { + peer1Online.countDown() + } else { + peer1Offline.countDown() + } + } + startMultipeerReplicator(repl2) + + runBlocking { + assertTrue(peer2Online.stdWait()) + assertTrue(peer1Online.stdWait()) + } + + runBlocking { delay(1.seconds) } + + stopMultipeerReplicator(repl1) + + runBlocking { assertTrue(peer1Offline.stdWait()) } + + stopMultipeerReplicator(repl2) + + token1.remove() + token2.remove() + } + + /** + * ### 11. TestDocumentReplicationListener + * + * Test that the document replication listener can receive the document + * replication events. + */ + @Test + @Ignore + fun testDocumentReplicationListener() { + if (!keyStoreAccessAllowed) return + + val repl1 = multipeerReplicator(testDatabase) + val docRepl1 = mutableListOf() + val token1 = repl1.addPeerDocumentReplicationListener { docRepl -> + lock.withLock { docRepl1.add(docRepl) } + } + + val repl2 = multipeerReplicator(otherDatabase) + val docRepl2 = mutableListOf() + val token2 = repl2.addPeerDocumentReplicationListener { docRepl -> + lock.withLock { docRepl2.add(docRepl) } + } + + startMultipeerReplicator(repl1) + startMultipeerReplicator(repl2) + + // Save a doc in one peer: + val doc1 = MutableDocument("doc1") + doc1.setString("greeting", "hello") + testDatabase.defaultCollection.save(doc1) + + waitForReplicatorStatus(repl1, repl2.peerId, ReplicatorActivityLevel.IDLE) + waitForReplicatorStatus(repl2, repl1.peerId, ReplicatorActivityLevel.IDLE) + + lock.withLock { + assertEquals(1, docRepl1.size) + assertTrue(docRepl1[0].isPush) + assertEquals(1, docRepl1[0].documents.size) + with(docRepl1[0].documents[0]) { + assertEquals("doc1", id) + assertTrue(flags.isEmpty()) + assertNull(error) + assertEquals("_default", scope) + assertEquals("_default", collection) + } + + assertEquals(1, docRepl2.size) + assertFalse(docRepl2[0].isPush) + assertEquals(1, docRepl2[0].documents.size) + with(docRepl2[0].documents[0]) { + assertEquals("doc1", id) + assertTrue(flags.isEmpty()) + assertNull(error) + assertEquals("_default", scope) + assertEquals("_default", collection) + } + } + + stopMultipeerReplicator(repl1) + stopMultipeerReplicator(repl2) + + token1.remove() + token2.remove() + } + + // Conflict Resolver + + private fun runConflictResolverTest( + resolver: MultipeerConflictResolver?, + verify: (Document?) -> Boolean + ) { + // Peer#1 + var repl1 = multipeerReplicator(testDatabase) { + conflictResolver = resolver + } + startMultipeerReplicator(repl1) + + // Peer#2 + var repl2 = multipeerReplicator(otherDatabase) { + conflictResolver = resolver + } + startMultipeerReplicator(repl2) + + // Save a doc in peer #1: + val doc = MutableDocument("doc1") + doc.setString("greeting", "hello") + testDatabase.defaultCollection.save(doc) + + // Wait until the replicator is IDLE + waitForReplicatorStatus(repl1, repl2.peerId, ReplicatorActivityLevel.IDLE) + waitForReplicatorStatus(repl2, repl1.peerId, ReplicatorActivityLevel.IDLE) + + // Stops: + stopMultipeerReplicator(repl1) + stopMultipeerReplicator(repl2) + + // Update the doc in both peers, saving on the otherDB twice: + val doc1 = testDatabase.defaultCollection.getDocument("doc1")!!.toMutable() + doc1.setString("greeting", "hi") + testDatabase.defaultCollection.save(doc1) + + val doc2 = otherDatabase.defaultCollection.getDocument("doc1")!!.toMutable() + doc2.setString("greeting", "hey") + otherDatabase.defaultCollection.save(doc2) + + doc2.setString("greeting", "howdy") + otherDatabase.defaultCollection.save(doc2) + + // Restart both peers: + repl1 = multipeerReplicator(testDatabase) { + conflictResolver = resolver + } + startMultipeerReplicator(repl1) + + repl2 = multipeerReplicator(otherDatabase) { + conflictResolver = resolver + } + startMultipeerReplicator(repl2) + + // Wait until the replicator is IDLE + waitForReplicatorStatus(repl1, repl2.peerId, ReplicatorActivityLevel.IDLE) + waitForReplicatorStatus(repl2, repl1.peerId, ReplicatorActivityLevel.IDLE) + + // Check the doc on both peers: + assertTrue(verify(testDatabase.defaultCollection.getDocument("doc1"))) + assertTrue(verify(otherDatabase.defaultCollection.getDocument("doc1"))) + + stopMultipeerReplicator(repl1) + stopMultipeerReplicator(repl2) + } + + /** + * ### 12. TestDefaultConflictResolver + * + * Test that the default conflict resolver is used when no custom conflict + * resolver is specified. + */ + @Test + @Ignore + fun testDefaultConflictResolver() { + if (!keyStoreAccessAllowed) return + + runConflictResolverTest(null) { resolvedDoc -> + resolvedDoc?.getString("greeting") == "howdy" + } + } + + /** + * ### 13. TestCustomConflictResolver + * + * Test that the custom conflict resolver is used when it is specified. + */ + @Test + @Ignore + fun testCustomConflictResolver() { + if (!keyStoreAccessAllowed) return + + val resolver: MultipeerConflictResolver = { _, conflict -> + val greeting = conflict.localDocument?.getString("greeting") + if (greeting == "hi") conflict.localDocument else conflict.remoteDocument + } + + runConflictResolverTest(resolver) { resolvedDoc -> + resolvedDoc?.getString("greeting") == "hi" + } + } + + // Replication Filter + + /** + * ### 14. TestDocumentIDsFilter + * + * Test that the documentIDs filter is used when it's specified. + */ + @Test + @Ignore + fun testDocumentIDsFilter() { + if (!keyStoreAccessAllowed) return + + // Peer#1 + val repl1 = multipeerReplicator(testDatabase) { + documentIDs = listOf("doc1", "doc3") + } + startMultipeerReplicator(repl1) + + // Peer#2 + val repl2 = multipeerReplicator(otherDatabase) + startMultipeerReplicator(repl2) + + // Save 3 docs in peer #1: + val doc1 = MutableDocument("doc1") + doc1.setString("greeting", "hello") + testDatabase.defaultCollection.save(doc1) + + val doc2 = MutableDocument("doc2") + doc2.setString("greeting", "hi") + testDatabase.defaultCollection.save(doc2) + + val doc3 = MutableDocument("doc3") + doc3.setString("greeting", "howdy") + testDatabase.defaultCollection.save(doc3) + + // Wait until the replicator is IDLE + waitForReplicatorStatus(repl1, repl2.peerId, ReplicatorActivityLevel.IDLE) + waitForReplicatorStatus(repl2, repl1.peerId, ReplicatorActivityLevel.IDLE) + + // Check that only doc1 and doc3 are pushed to Peer #2 + val checkDoc1 = otherDatabase.defaultCollection.getDocument("doc1") + assertNotNull(checkDoc1) + assertEquals("hello", checkDoc1.getString("greeting")) + + assertNull(otherDatabase.defaultCollection.getDocument("doc2")) + + val checkDoc3 = otherDatabase.defaultCollection.getDocument("doc3") + assertNotNull(checkDoc3) + assertEquals("howdy", checkDoc3.getString("greeting")) + + stopMultipeerReplicator(repl1) + stopMultipeerReplicator(repl2) + } + + /** + * ### 15. TestPushPullFilter + * + * Test that the push and pull filters are used when they are specified. + */ + @Test + @Ignore + fun testPushPullFilter() { + if (!keyStoreAccessAllowed) return + + // Peer#1 + val repl1 = multipeerReplicator(testDatabase) { + pushFilter = { _, doc, _ -> doc.id == "doc1" } + pullFilter = { _, doc, _ -> doc.id == "doc4" } + } + startMultipeerReplicator(repl1) + + // Peer#2 + val repl2 = multipeerReplicator(otherDatabase) + startMultipeerReplicator(repl2) + + // Save doc1 and doc2 in peer #1: + val doc1 = MutableDocument("doc1") + doc1.setString("greeting", "hello") + testDatabase.defaultCollection.save(doc1) + + val doc2 = MutableDocument("doc2") + doc2.setString("greeting", "hi") + testDatabase.defaultCollection.save(doc2) + + // Save doc3 and doc4 in peer #2: + val doc3 = MutableDocument("doc3") + doc3.setString("greeting", "howdy") + otherDatabase.defaultCollection.save(doc3) + + val doc4 = MutableDocument("doc4") + doc4.setString("greeting", "hey") + otherDatabase.defaultCollection.save(doc4) + + // Wait until the replicator is IDLE: + waitForReplicatorStatus(repl1, repl2.peerId, ReplicatorActivityLevel.IDLE) + waitForReplicatorStatus(repl2, repl1.peerId, ReplicatorActivityLevel.IDLE) + + // Check that only doc4 but not doc3 are pulled to peer #1: + val checkDoc4 = testDatabase.defaultCollection.getDocument("doc4") + assertNotNull(checkDoc4) + assertEquals("hey", checkDoc4.getString("greeting")) + + assertNull(testDatabase.defaultCollection.getDocument("doc3")) + + // Check that only doc1 but not doc2 are pushed to peer #2: + val checkDoc1 = otherDatabase.defaultCollection.getDocument("doc1") + assertNotNull(checkDoc1) + assertEquals("hello", checkDoc1.getString("greeting")) + + assertNull(otherDatabase.defaultCollection.getDocument("doc2")) + + stopMultipeerReplicator(repl1) + stopMultipeerReplicator(repl2) + } + + // Peer Info + + /** + * ### 16. TestPeerID + * + * Test that getting the MultipeerReplicator's peerID returns a value as expected. + */ + @Test + @Ignore + fun testPeerID() { + if (!keyStoreAccessAllowed) return + + val repl = multipeerReplicator(testDatabase) + val peerId = repl.peerId + assertEquals(PeerInfo.PEER_ID_LENGTH, peerId.bytes.size) + + startMultipeerReplicator(repl) + assertContentEquals(peerId.bytes, repl.peerId.bytes) + + stopMultipeerReplicator(repl) + assertContentEquals(peerId.bytes, repl.peerId.bytes) + } + + /** + * ### 17. TestNeighborPeers + * + * Test that getting the MultipeerReplicator's neighborPeers returns the + * online peers as expected. + */ + @Test + @Ignore + fun testNeighborPeers() { + if (!keyStoreAccessAllowed) return + + val repl1 = multipeerReplicator(testDatabase) + assertTrue(repl1.neighborPeers.isEmpty()) + + startMultipeerReplicator(repl1) + assertTrue(repl1.neighborPeers.isEmpty()) + + val repl2 = multipeerReplicator(otherDatabase) + assertTrue(repl2.neighborPeers.isEmpty()) + + startMultipeerReplicator(repl2) + + waitForReplicatorStatus(repl2, repl1.peerId, ReplicatorActivityLevel.IDLE) + + assertEquals(setOf(repl2.peerId), repl1.neighborPeers) + assertEquals(setOf(repl1.peerId), repl2.neighborPeers) + + stopMultipeerReplicator(repl1) + assertTrue(repl1.neighborPeers.isEmpty()) + + // Wait to ensure the neighborPeers are updated + runBlocking { delay(5.seconds) } + assertTrue(repl2.neighborPeers.isEmpty()) + + stopMultipeerReplicator(repl2) + assertTrue(repl2.neighborPeers.isEmpty()) + } + + /** + * ### 18. TestPeerInfo + * + * Verify that retrieving peer info returns the expected PeerInfo object for + * known peers. If the peer is unknown, ensure that null is returned as expected. + */ + @Test + @Ignore + fun testPeerInfo() { + if (!keyStoreAccessAllowed) return + + val repl1 = multipeerReplicator(testDatabase) + val repl2 = multipeerReplicator(otherDatabase) + + var repl1Peer1 = repl1.getPeerInfo(repl1.peerId) + assertNotNull(repl1Peer1) + assertEquals(repl1.peerId, repl1Peer1.peerId) + assertNotNull(repl1Peer1.certificate) + assertTrue(repl1Peer1.neighbors.isEmpty()) + assertNull(repl1Peer1.replicatorTransport) + assertTrue(repl1Peer1.transports.isEmpty()) + assertEquals(ReplicatorActivityLevel.STOPPED, repl1Peer1.replicatorStatus.activityLevel) + + assertNull(repl1.getPeerInfo(repl2.peerId)) + + startMultipeerReplicator(repl1) + startMultipeerReplicator(repl2) + + waitForReplicatorStatus(repl1, repl2.peerId, ReplicatorActivityLevel.IDLE) + waitForReplicatorStatus(repl2, repl1.peerId, ReplicatorActivityLevel.IDLE) + + repl1Peer1 = repl1.getPeerInfo(repl1.peerId) + assertNotNull(repl1Peer1) + assertEquals(setOf(repl2.peerId), repl1Peer1.neighbors) + assertNull(repl1Peer1.replicatorTransport) // No replication to itself + assertTrue(repl1Peer1.transports.isEmpty()) // No replication to itself + assertEquals(ReplicatorActivityLevel.STOPPED, repl1Peer1.replicatorStatus.activityLevel) // No replication to itself + + val repl1Peer2 = repl1.getPeerInfo(repl2.peerId) + assertNotNull(repl1Peer2) + assertEquals(setOf(repl1.peerId), repl1Peer2.neighbors) + assertEquals(MultipeerTransport.WIFI, repl1Peer2.replicatorTransport) + assertEquals(setOf(MultipeerTransport.WIFI), repl1Peer2.transports) + assertEquals(ReplicatorActivityLevel.IDLE, repl1Peer2.replicatorStatus.activityLevel) + + stopMultipeerReplicator(repl1) + stopMultipeerReplicator(repl2) + } + + // Transport Status + + /** + * ### 19. TestGetTransportStatus + * + * Test that getting status returns the correct status for the transport. + */ + @Test + @Ignore + fun testGetTransportStatus() { + if (!keyStoreAccessAllowed) return + + val repl = multipeerReplicator(testDatabase) + + var status = repl.getStatus(MultipeerTransport.WIFI) + assertNotNull(status) + assertFalse(status.isActive) + assertNull(status.error) + + startMultipeerReplicator(repl) + + status = repl.getStatus(MultipeerTransport.WIFI) + assertNotNull(status) + assertTrue(status.isActive) + assertNull(status.error) + assertEquals(MultipeerTransport.WIFI, status.transport) + + stopMultipeerReplicator(repl) + + status = repl.getStatus(MultipeerTransport.WIFI) + assertNotNull(status) + assertFalse(status.isActive) + assertNull(status.error) + } + + /** + * ### 20. TestGetTransportStatusNotConfigured + * + * Test that getting status returns null for a transport that is not configured. + */ + @Test + @Ignore + fun testGetTransportStatusNotConfigured() { + if (!keyStoreAccessAllowed) return + + val repl = multipeerReplicator(testDatabase) + + startMultipeerReplicator(repl) + + assertNull(repl.getStatus(MultipeerTransport.BLUETOOTH)) + + stopMultipeerReplicator(repl) + } + + /** + * ### 21. TestGetTransportStatusBeforeStart + * + * Test that getting status returns inactive status when the replicator + * has not been started. + */ + @Test + fun testGetTransportStatusBeforeStart() { + if (!keyStoreAccessAllowed) return + + val repl = multipeerReplicator(testDatabase) + + val status = repl.getStatus(MultipeerTransport.WIFI) + assertNotNull(status) + assertFalse(status.isActive) + assertNull(status.error) + } + + /** + * ### 22. TestTransportSpecificStatusListener + * + * Test that the status listener receives both transport-specific and aggregate + * status events when the replicator starts and stops. + */ + @Test + @Ignore + fun testTransportSpecificStatusListener() { + if (!keyStoreAccessAllowed) return + + val repl = multipeerReplicator(testDatabase) + + val wifiActive = CountDownLatch(1) + val aggregateActive = CountDownLatch(1) + val wifiInactive = CountDownLatch(1) + val aggregateInactive = CountDownLatch(1) + + val token = repl.addStatusListener { status -> + when (status.transport) { + null -> { + assertNull(status.error) + if (status.isActive) aggregateActive.countDown() + else aggregateInactive.countDown() + } + MultipeerTransport.WIFI -> { + if (status.isActive) wifiActive.countDown() + else wifiInactive.countDown() + } + else -> {} + } + } + + repl.start() + runBlocking { + assertTrue(wifiActive.stdWait()) + assertTrue(aggregateActive.stdWait()) + } + + repl.stop() + runBlocking { + assertTrue(wifiInactive.stdWait()) + assertTrue(aggregateInactive.stdWait()) + } + + token.remove() + } + + companion object { + private const val TEST_PEER_GROUP_ID = "CBLMultipeerReplTestGroup" + + private const val TEST_MAX_IDENTITY = 10 + private const val TEST_IDENTITY_COMMON_NAME = "cbl-multipeer-repl-test" + + private val TEST_KEY_USAGES = setOf(KeyUsage.CLIENT_AUTH, KeyUsage.SERVER_AUTH) + } +} diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerAuthenticator.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerAuthenticator.android.kt new file mode 100644 index 000000000..a9aaeffd1 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerAuthenticator.android.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotbase.internal.DelegatedClass +import com.couchbase.lite.MultipeerAuthenticator as CBLMultipeerAuthenticator + +public actual sealed class MultipeerAuthenticator(actual: CBLMultipeerAuthenticator) : + DelegatedClass(actual) diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerCertificateAuthenticator.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerCertificateAuthenticator.android.kt new file mode 100644 index 000000000..081f8f9d7 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerCertificateAuthenticator.android.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotbase.ext.toCertificates +import java.security.cert.X509Certificate +import com.couchbase.lite.MultipeerCertificateAuthenticator as CBLMultipeerCertificateAuthenticator + +public actual class MultipeerCertificateAuthenticator +internal constructor(override val actual: CBLMultipeerCertificateAuthenticator) : MultipeerAuthenticator(actual) { + + public actual constructor(rootCerts: List) : this( + CBLMultipeerCertificateAuthenticator(rootCerts.toCertificates().map { it as X509Certificate }) + ) + + public actual constructor(delegate: MultipeerCertificateAuthenticatorDelegate) : this( + CBLMultipeerCertificateAuthenticator(delegate.convert()) + ) +} diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerCertificateAuthenticatorDelegate.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerCertificateAuthenticatorDelegate.android.kt new file mode 100644 index 000000000..a6bc006f4 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerCertificateAuthenticatorDelegate.android.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import com.couchbase.lite.MultipeerCertificateAuthenticator as CBLMultipeerCertificateAuthenticator + +internal fun MultipeerCertificateAuthenticatorDelegate.convert(): CBLMultipeerCertificateAuthenticator.AuthenticationDelegate = + CBLMultipeerCertificateAuthenticator.AuthenticationDelegate { peer, certs -> + invoke(PeerInfo.PeerId(peer), certs.map { it.encoded }) + } diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerCollectionConfiguration.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerCollectionConfiguration.android.kt new file mode 100644 index 000000000..53859baf9 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerCollectionConfiguration.android.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import com.couchbase.lite.MultipeerCollectionConfiguration as CBLMultipeerCollectionConfiguration + +internal val MultipeerCollectionConfiguration.actual: CBLMultipeerCollectionConfiguration + get() = CBLMultipeerCollectionConfiguration.Builder(collection.actual) + .setDocumentIDs(documentIDs) + .setPushFilter(pushFilter?.convert()) + .setPullFilter(pullFilter?.convert()) + .setConflictResolver(conflictResolver?.convert()) + .build() diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerConflictResolver.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerConflictResolver.android.kt new file mode 100644 index 000000000..1626e516c --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerConflictResolver.android.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import com.couchbase.lite.MultipeerCollectionConfiguration as CBLMultipeerCollectionConfiguration + +internal fun MultipeerConflictResolver.convert(): CBLMultipeerCollectionConfiguration.ConflictResolver = + CBLMultipeerCollectionConfiguration.ConflictResolver { peerId, conflict -> + invoke(PeerInfo.PeerId(peerId), Conflict(conflict))?.actual + } diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicationFilter.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicationFilter.android.kt new file mode 100644 index 000000000..01d80fbe5 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicationFilter.android.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import com.couchbase.lite.MultipeerCollectionConfiguration as CBLMultipeerCollectionConfiguration + +internal fun MultipeerReplicationFilter.convert(): CBLMultipeerCollectionConfiguration.ReplicationFilter = + CBLMultipeerCollectionConfiguration.ReplicationFilter { peerId, document, flags -> + invoke(PeerInfo.PeerId(peerId), Document(document, null), flags) + } diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicator.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicator.android.kt new file mode 100644 index 000000000..5c5e24a3c --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicator.android.kt @@ -0,0 +1,191 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import android.content.Context +import kotbase.ext.dispatcher +import kotbase.internal.DelegatedClass +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asExecutor +import kotlin.coroutines.CoroutineContext +import com.couchbase.lite.MultipeerReplicator as CBLMultipeerReplicator + +public actual class MultipeerReplicator +internal constructor( + actual: CBLMultipeerReplicator, + private val _config: MultipeerReplicatorConfiguration +) : DelegatedClass(actual), AutoCloseable { + + @Throws(CouchbaseLiteException::class) + public actual constructor(config: MultipeerReplicatorConfiguration) : this( + CBLMultipeerReplicator(config.actual), + config + ) + + public actual val config: MultipeerReplicatorConfiguration + get() = MultipeerReplicatorConfiguration(_config) + + public actual val peerId: PeerInfo.PeerId + get() = PeerInfo.PeerId(actual.peerId) + + public actual val neighborPeers: Set + get() = actual.neighborPeers.map { PeerInfo.PeerId(it) }.toSet() + + public actual fun getStatus(transport: MultipeerTransport): MultipeerReplicatorStatus? = + actual.getStatus(transport)?.let { MultipeerReplicatorStatus(it) } + + public actual fun start() { + actual.start() + } + + public actual fun stop() { + actual.stop() + } + + public actual fun getPeerInfo(peerId: PeerInfo.PeerId): PeerInfo? = + actual.getPeerInfo(peerId.actual)?.let { PeerInfo(it) } + + /** + * Returns the set of necessary permissions required by the application. + * + * @return A set of necessary permissions (Strings). + */ + public fun getNecessaryPermissions(): Set = + actual.necessaryPermissions + + /** + * Return the set of missing (not granted) permissions. + * + * @param context Application context + */ + public fun getMissingPermissions(context: Context): Set = + actual.getMissingPermissions(context) + + public actual fun addStatusListener(listener: MultipeerReplicatorStatusListener): ListenerToken = + DelegatedListenerToken(actual.addStatusListener(listener.convert())) + + public actual fun addStatusListener( + context: CoroutineContext, + listener: MultipeerReplicatorStatusSuspendListener + ): ListenerToken { + val scope = CoroutineScope(SupervisorJob() + context) + val token = actual.addStatusListener( + context.dispatcher?.asExecutor(), + listener.convert(scope) + ) + return SuspendListenerToken(scope, token) + } + + public actual fun addStatusListener(scope: CoroutineScope, listener: MultipeerReplicatorStatusSuspendListener) { + val token = actual.addStatusListener( + scope.coroutineContext.dispatcher?.asExecutor(), + listener.convert(scope) + ) + scope.coroutineContext[Job]?.invokeOnCompletion { + token.remove() + } + } + + public actual fun addPeerDiscoveryStatusListener(listener: PeerDiscoveryStatusListener): ListenerToken = + DelegatedListenerToken(actual.addPeerDiscoveryStatusListener(listener.convert())) + + public actual fun addPeerDiscoveryStatusListener( + context: CoroutineContext, + listener: PeerDiscoveryStatusSuspendListener + ): ListenerToken { + val scope = CoroutineScope(SupervisorJob() + context) + val token = actual.addPeerDiscoveryStatusListener( + context.dispatcher?.asExecutor(), + listener.convert(scope) + ) + return SuspendListenerToken(scope, token) + } + + public actual fun addPeerDiscoveryStatusListener( + scope: CoroutineScope, + listener: PeerDiscoveryStatusSuspendListener + ) { + val token = actual.addPeerDiscoveryStatusListener( + scope.coroutineContext.dispatcher?.asExecutor(), + listener.convert(scope) + ) + scope.coroutineContext[Job]?.invokeOnCompletion { + token.remove() + } + } + + public actual fun addPeerReplicatorStatusListener(listener: PeerReplicatorStatusListener): ListenerToken = + DelegatedListenerToken(actual.addPeerReplicatorStatusListener(listener.convert())) + + public actual fun addPeerReplicatorStatusListener( + context: CoroutineContext, + listener: PeerReplicatorStatusSuspendListener + ): ListenerToken { + val scope = CoroutineScope(SupervisorJob() + context) + val token = actual.addPeerReplicatorStatusListener( + context.dispatcher?.asExecutor(), + listener.convert(scope) + ) + return SuspendListenerToken(scope, token) + } + + public actual fun addPeerReplicatorStatusListener( + scope: CoroutineScope, + listener: PeerReplicatorStatusSuspendListener + ) { + val token = actual.addPeerReplicatorStatusListener( + scope.coroutineContext.dispatcher?.asExecutor(), + listener.convert(scope) + ) + scope.coroutineContext[Job]?.invokeOnCompletion { + token.remove() + } + } + + public actual fun addPeerDocumentReplicationListener(listener: PeerDocumentReplicationListener): ListenerToken = + DelegatedListenerToken(actual.addPeerDocumentReplicationListener(listener.convert())) + + public actual fun addPeerDocumentReplicationListener( + context: CoroutineContext, + listener: PeerDocumentReplicationSuspendListener + ): ListenerToken { + val scope = CoroutineScope(SupervisorJob() + context) + val token = actual.addPeerDocumentReplicationListener( + context.dispatcher?.asExecutor(), + listener.convert(scope) + ) + return SuspendListenerToken(scope, token) + } + + public actual fun addPeerDocumentReplicationListener( + scope: CoroutineScope, + listener: PeerDocumentReplicationSuspendListener + ) { + val token = actual.addPeerDocumentReplicationListener( + scope.coroutineContext.dispatcher?.asExecutor(), + listener.convert(scope) + ) + scope.coroutineContext[Job]?.invokeOnCompletion { + token.remove() + } + } + + actual override fun close() { + actual.close() + } +} diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicatorConfiguration.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicatorConfiguration.android.kt new file mode 100644 index 000000000..5d848caa8 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicatorConfiguration.android.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import com.couchbase.lite.MultipeerReplicatorConfiguration as CBLMultipeerReplicatorConfiguration + +internal val MultipeerReplicatorConfiguration.actual: CBLMultipeerReplicatorConfiguration + get() = CBLMultipeerReplicatorConfiguration.Builder() + .setPeerGroupID(peerGroupID) + .setIdentity(identity.actual) + .setAuthenticator(authenticator.actual) + .setCollections(collections.map { it.actual }) + .setTransports(transports) + .build() diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicatorStatus.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicatorStatus.android.kt new file mode 100644 index 000000000..a85d2a726 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicatorStatus.android.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotbase.internal.DelegatedClass +import com.couchbase.lite.MultipeerReplicatorStatus as CBLMultipeerReplicatorStatus + +public actual class MultipeerReplicatorStatus +internal constructor(actual: CBLMultipeerReplicatorStatus) : DelegatedClass(actual) { + + public actual val transport: MultipeerTransport? + get() = actual.transport + + public actual val isActive: Boolean + get() = actual.isActive + + public actual val error: CouchbaseLiteException? + get() = actual.error +} diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicatorStatusListener.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicatorStatusListener.android.kt new file mode 100644 index 000000000..87327ce10 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerReplicatorStatusListener.android.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import com.couchbase.lite.MultipeerReplicatorStatus as CBLMultipeerReplicatorStatus + +internal fun MultipeerReplicatorStatusListener.convert(): CBLMultipeerReplicatorStatus.Listener = + CBLMultipeerReplicatorStatus.Listener { status -> + invoke(MultipeerReplicatorStatus(status)) + } + +internal fun MultipeerReplicatorStatusSuspendListener.convert( + scope: CoroutineScope +): CBLMultipeerReplicatorStatus.Listener = + CBLMultipeerReplicatorStatus.Listener { status -> + scope.launch { + invoke(MultipeerReplicatorStatus(status)) + } + } diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerTransport.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerTransport.android.kt new file mode 100644 index 000000000..dbe5c5138 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/MultipeerTransport.android.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +public actual typealias MultipeerTransport = com.couchbase.lite.MultipeerTransport diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDiscoveryStatus.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDiscoveryStatus.android.kt new file mode 100644 index 000000000..2dd2e9280 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDiscoveryStatus.android.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotbase.internal.DelegatedClass +import com.couchbase.lite.PeerDiscoveryStatus as CBLPeerDiscoveryStatus + +public actual class PeerDiscoveryStatus +internal constructor(actual: CBLPeerDiscoveryStatus) : DelegatedClass(actual) { + + public actual val peer: PeerInfo.PeerId + get() = PeerInfo.PeerId(actual.peer) + + public actual val isOnline: Boolean + get() = actual.isOnline + + public actual val transport: MultipeerTransport + get() = actual.transport +} diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDiscoveryStatusListener.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDiscoveryStatusListener.android.kt new file mode 100644 index 000000000..c2f195d5d --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDiscoveryStatusListener.android.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import com.couchbase.lite.PeerDiscoveryStatus as CBLPeerDiscoveryStatus + +internal fun PeerDiscoveryStatusListener.convert(): CBLPeerDiscoveryStatus.Listener = + CBLPeerDiscoveryStatus.Listener { status -> + invoke(PeerDiscoveryStatus(status)) + } + +internal fun PeerDiscoveryStatusSuspendListener.convert( + scope: CoroutineScope +): CBLPeerDiscoveryStatus.Listener = + CBLPeerDiscoveryStatus.Listener { status -> + scope.launch { + invoke(PeerDiscoveryStatus(status)) + } + } diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDocumentReplication.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDocumentReplication.android.kt new file mode 100644 index 000000000..2f4b8a4a3 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDocumentReplication.android.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotbase.internal.DelegatedClass +import com.couchbase.lite.PeerDocumentReplication as CBLPeerDocumentReplication + +public actual class PeerDocumentReplication +internal constructor(actual: CBLPeerDocumentReplication) : DelegatedClass(actual) { + + public actual val peer: PeerInfo.PeerId + get() = PeerInfo.PeerId(actual.peer) + + public actual val transport: MultipeerTransport + get() = actual.transport + + public actual val isPush: Boolean + get() = actual.isPush + + public actual val documents: List + get() = actual.documents.map { ReplicatedDocument(it) } +} diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDocumentReplicationListener.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDocumentReplicationListener.android.kt new file mode 100644 index 000000000..d66414eba --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerDocumentReplicationListener.android.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import com.couchbase.lite.PeerDocumentReplication as CBLPeerDocumentReplication + +internal fun PeerDocumentReplicationListener.convert(): CBLPeerDocumentReplication.Listener = + CBLPeerDocumentReplication.Listener { replication -> + invoke(PeerDocumentReplication(replication)) + } + +internal fun PeerDocumentReplicationSuspendListener.convert( + scope: CoroutineScope +): CBLPeerDocumentReplication.Listener = + CBLPeerDocumentReplication.Listener { replication -> + scope.launch { + invoke(PeerDocumentReplication(replication)) + } + } diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerInfo.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerInfo.android.kt new file mode 100644 index 000000000..a88a7d2db --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerInfo.android.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotbase.internal.DelegatedClass +import com.couchbase.lite.PeerInfo as CBLPeerInfo + +public actual class PeerInfo +internal constructor(actual: CBLPeerInfo) : DelegatedClass(actual) { + + public actual class PeerId + internal constructor(actual: CBLPeerInfo.PeerId) : DelegatedClass(actual) { + + public actual val bytes: ByteArray + get() = actual.bytes + } + + public actual val peerId: PeerId + get() = PeerId(actual.peerId) + + public actual val certificate: ByteArray? + get() = actual.certificate?.encoded + + public actual val isOnline: Boolean + get() = actual.isOnline + + public actual val transports: Set + get() = actual.transports + + public actual val replicatorTransport: MultipeerTransport? + get() = actual.replicatorTransport + + public actual val replicatorStatus: ReplicatorStatus + get() = ReplicatorStatus(actual.replicatorStatus) + + public actual val neighbors: Set + get() = actual.neighbors.map { PeerId(it) }.toSet() + + public actual companion object { + + public actual val PEER_ID_LENGTH: Int = CBLPeerInfo.PEER_ID_LENGTH + } +} diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerReplicatorStatus.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerReplicatorStatus.android.kt new file mode 100644 index 000000000..2d4a01dc6 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerReplicatorStatus.android.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotbase.internal.DelegatedClass +import com.couchbase.lite.PeerReplicatorStatus as CBLPeerReplicatorStatus + +public actual class PeerReplicatorStatus +internal constructor(actual: CBLPeerReplicatorStatus) : DelegatedClass(actual) { + + public actual val peerId: PeerInfo.PeerId + get() = PeerInfo.PeerId(actual.peerId) + + public actual val transport: MultipeerTransport + get() = actual.transport + + public actual val isOutgoing: Boolean + get() = actual.isOutgoing + + public actual val status: ReplicatorStatus + get() = ReplicatorStatus(actual.status) +} diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerReplicatorStatusListener.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerReplicatorStatusListener.android.kt new file mode 100644 index 000000000..4ad6ee35e --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/PeerReplicatorStatusListener.android.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import com.couchbase.lite.PeerReplicatorStatus as CBLPeerReplicatorStatus + +internal fun PeerReplicatorStatusListener.convert(): CBLPeerReplicatorStatus.Listener = + CBLPeerReplicatorStatus.Listener { status -> + invoke(PeerReplicatorStatus(status)) + } + +internal fun PeerReplicatorStatusSuspendListener.convert( + scope: CoroutineScope +): CBLPeerReplicatorStatus.Listener = + CBLPeerReplicatorStatus.Listener { status -> + scope.launch { + invoke(PeerReplicatorStatus(status)) + } + } diff --git a/couchbase-lite-ee/src/androidMain/ee/kotbase/TLSIdentityExt.android.kt b/couchbase-lite-ee/src/androidMain/ee/kotbase/TLSIdentityExt.android.kt new file mode 100644 index 000000000..22dc21768 --- /dev/null +++ b/couchbase-lite-ee/src/androidMain/ee/kotbase/TLSIdentityExt.android.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import kotbase.ext.toDate +import kotlin.time.Instant +import com.couchbase.lite.TLSIdentity as CBLTLSIdentity + +@Throws(CouchbaseLiteException::class) +public actual fun TLSIdentity.Companion.createdSignedIdentityInsecure( + usage: Set, + attributes: Map, + caKey: ByteArray, + caCertificate: ByteArray, + expiration: Instant?, + alias: String +): TLSIdentity = CBLTLSIdentity.createdSignedIdentityInsecure( + usage, + attributes, + caKey, + caCertificate, + expiration?.toDate(), + alias +).asTLSIdentity() diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerAuthenticator.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerAuthenticator.apple.kt new file mode 100644 index 000000000..224923e80 --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerAuthenticator.apple.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLMultipeerAuthenticatorProtocol +import kotbase.internal.DelegatedProtocol + +public actual sealed class MultipeerAuthenticator(actual: CBLMultipeerAuthenticatorProtocol) : + DelegatedProtocol(actual) diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerCertificateAuthenticator.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerCertificateAuthenticator.apple.kt new file mode 100644 index 000000000..19387baae --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerCertificateAuthenticator.apple.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLMultipeerCertificateAuthenticator +import kotbase.ext.toSecCertificate + +public actual class MultipeerCertificateAuthenticator +internal constructor(override val actual: CBLMultipeerCertificateAuthenticator) : MultipeerAuthenticator(actual) { + + public actual constructor(rootCerts: List) : this( + CBLMultipeerCertificateAuthenticator( + rootCerts.map { it.toSecCertificate() } + ) + ) + + public actual constructor(delegate: MultipeerCertificateAuthenticatorDelegate) : this( + CBLMultipeerCertificateAuthenticator(delegate.convert()) + ) +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerCertificateAuthenticatorDelegate.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerCertificateAuthenticatorDelegate.apple.kt new file mode 100644 index 000000000..96f7d4d71 --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerCertificateAuthenticatorDelegate.apple.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLMultipeerCertificateAuthBlock +import kotbase.ext.toByteArray +import platform.Security.SecCertificateRef + +internal fun MultipeerCertificateAuthenticatorDelegate.convert(): CBLMultipeerCertificateAuthBlock { + return { peerId, certs -> + invoke( + PeerInfo.PeerId(peerId!!), + certs!!.map { + @Suppress("UNCHECKED_CAST") + (it as SecCertificateRef).toByteArray() + } + ) + } +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerCollectionConfiguration.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerCollectionConfiguration.apple.kt new file mode 100644 index 000000000..065286eef --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerCollectionConfiguration.apple.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLMultipeerCollectionConfiguration + +internal val MultipeerCollectionConfiguration.actual: CBLMultipeerCollectionConfiguration + get() = CBLMultipeerCollectionConfiguration(collection.actual).also { config -> + config.documentIDs = documentIDs + config.pushFilter = pushFilter?.convert() + config.pullFilter = pullFilter?.convert() + config.conflictResolver = conflictResolver?.convert() + } diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerConflictResolver.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerConflictResolver.apple.kt new file mode 100644 index 000000000..d5c328a43 --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerConflictResolver.apple.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLConflict +import cocoapods.CouchbaseLite.CBLDocument +import cocoapods.CouchbaseLite.CBLMultipeerConflictResolverProtocol +import cocoapods.CouchbaseLite.CBLPeerID +import platform.darwin.NSObject + +internal fun MultipeerConflictResolver.convert(): CBLMultipeerConflictResolverProtocol { + return object : NSObject(), CBLMultipeerConflictResolverProtocol { + + override fun resolveConflict(conflict: CBLConflict, forPeer: CBLPeerID): CBLDocument? = + invoke(PeerInfo.PeerId(forPeer), Conflict(conflict))?.actual + } +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicationFilter.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicationFilter.apple.kt new file mode 100644 index 000000000..407220cbc --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicationFilter.apple.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLMultipeerReplicationFilter + +internal fun MultipeerReplicationFilter.convert(): CBLMultipeerReplicationFilter { + return { peerId, document, flags -> + invoke(PeerInfo.PeerId(peerId!!), Document(document!!, null), flags.toDocumentFlags()) + } +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicator.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicator.apple.kt new file mode 100644 index 000000000..df6f95cd8 --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicator.apple.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLMultipeerReplicator +import cocoapods.CouchbaseLite.CBLPeerID +import kotbase.ext.asDispatchQueue +import kotbase.ext.dispatcher +import kotbase.ext.wrapCBLError +import kotbase.internal.DelegatedClass +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlin.coroutines.CoroutineContext + +public actual class MultipeerReplicator +internal constructor( + actual: CBLMultipeerReplicator, + private val _config: MultipeerReplicatorConfiguration +) : DelegatedClass(actual), AutoCloseable { + + @Throws(CouchbaseLiteException::class) + public actual constructor(config: MultipeerReplicatorConfiguration) : this( + wrapCBLError { error -> + CBLMultipeerReplicator(config.actual, error) + }, + config + ) + + public actual val config: MultipeerReplicatorConfiguration + get() = MultipeerReplicatorConfiguration(_config) + + public actual val peerId: PeerInfo.PeerId + get() = PeerInfo.PeerId(actual.peerID) + + public actual val neighborPeers: Set + get() = actual.neighborPeers.map { + @Suppress("UNCHECKED_CAST") + PeerInfo.PeerId(it as CBLPeerID) + }.toSet() + + public actual fun getStatus(transport: MultipeerTransport): MultipeerReplicatorStatus? = + actual.statusForTransport(transport.actual)?.let { MultipeerReplicatorStatus(it) } + + public actual fun start() { + actual.start() + } + + public actual fun stop() { + actual.stop() + } + + public actual fun getPeerInfo(peerId: PeerInfo.PeerId): PeerInfo? = + actual.peerInfoForPeerID(peerId.actual)?.let { PeerInfo(it) } + + public actual fun addStatusListener(listener: MultipeerReplicatorStatusListener): ListenerToken = + DelegatedListenerToken( + actual.addStatusListenerWithQueue(null, listener.convert()) + ) + + public actual fun addStatusListener( + context: CoroutineContext, + listener: MultipeerReplicatorStatusSuspendListener + ): ListenerToken { + val scope = CoroutineScope(SupervisorJob() + context) + val token = actual.addStatusListenerWithQueue( + context.dispatcher?.asDispatchQueue(), + listener.convert(scope) + ) + return SuspendListenerToken(scope, token) + } + + public actual fun addStatusListener(scope: CoroutineScope, listener: MultipeerReplicatorStatusSuspendListener) { + val token = actual.addStatusListenerWithQueue( + scope.coroutineContext.dispatcher?.asDispatchQueue(), + listener.convert(scope) + ) + scope.coroutineContext[Job]?.invokeOnCompletion { + token.remove() + } + } + + public actual fun addPeerDiscoveryStatusListener(listener: PeerDiscoveryStatusListener): ListenerToken = + DelegatedListenerToken( + actual.addPeerDiscoveryStatusListenerWithQueue(null, listener.convert()) + ) + + public actual fun addPeerDiscoveryStatusListener( + context: CoroutineContext, + listener: PeerDiscoveryStatusSuspendListener + ): ListenerToken { + val scope = CoroutineScope(SupervisorJob() + context) + val token = actual.addPeerDiscoveryStatusListenerWithQueue( + context.dispatcher?.asDispatchQueue(), + listener.convert(scope) + ) + return SuspendListenerToken(scope, token) + } + + public actual fun addPeerDiscoveryStatusListener( + scope: CoroutineScope, + listener: PeerDiscoveryStatusSuspendListener + ) { + val token = actual.addPeerDiscoveryStatusListenerWithQueue( + scope.coroutineContext.dispatcher?.asDispatchQueue(), + listener.convert(scope) + ) + scope.coroutineContext[Job]?.invokeOnCompletion { + token.remove() + } + } + + public actual fun addPeerReplicatorStatusListener(listener: PeerReplicatorStatusListener): ListenerToken = + DelegatedListenerToken( + actual.addPeerReplicatorStatusListenerWithQueue(null, listener.convert()) + ) + + public actual fun addPeerReplicatorStatusListener( + context: CoroutineContext, + listener: PeerReplicatorStatusSuspendListener + ): ListenerToken { + val scope = CoroutineScope(SupervisorJob() + context) + val token = actual.addPeerReplicatorStatusListenerWithQueue( + context.dispatcher?.asDispatchQueue(), + listener.convert(scope) + ) + return SuspendListenerToken(scope, token) + } + + public actual fun addPeerReplicatorStatusListener( + scope: CoroutineScope, + listener: PeerReplicatorStatusSuspendListener + ) { + val token = actual.addPeerReplicatorStatusListenerWithQueue( + scope.coroutineContext.dispatcher?.asDispatchQueue(), + listener.convert(scope) + ) + scope.coroutineContext[Job]?.invokeOnCompletion { + token.remove() + } + } + + public actual fun addPeerDocumentReplicationListener(listener: PeerDocumentReplicationListener): ListenerToken = + DelegatedListenerToken( + actual.addPeerDocumentReplicationListenerWithQueue(null, listener.convert()) + ) + + public actual fun addPeerDocumentReplicationListener( + context: CoroutineContext, + listener: PeerDocumentReplicationSuspendListener + ): ListenerToken { + val scope = CoroutineScope(SupervisorJob() + context) + val token = actual.addPeerDocumentReplicationListenerWithQueue( + context.dispatcher?.asDispatchQueue(), + listener.convert(scope) + ) + return SuspendListenerToken(scope, token) + } + + public actual fun addPeerDocumentReplicationListener( + scope: CoroutineScope, + listener: PeerDocumentReplicationSuspendListener + ) { + val token = actual.addPeerDocumentReplicationListenerWithQueue( + scope.coroutineContext.dispatcher?.asDispatchQueue(), + listener.convert(scope) + ) + scope.coroutineContext[Job]?.invokeOnCompletion { + token.remove() + } + } + + actual override fun close() { + // no close() in Objective-C SDK: make a best-effort attempt to stop + stop() + } +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicatorConfiguration.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicatorConfiguration.apple.kt new file mode 100644 index 000000000..64b2a3f87 --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicatorConfiguration.apple.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLMultipeerReplicatorConfiguration + +internal val MultipeerReplicatorConfiguration.actual: CBLMultipeerReplicatorConfiguration + get() = CBLMultipeerReplicatorConfiguration( + peerGroupID, + identity.actual, + authenticator.actual, + collections.map { it.actual } + ).also { config -> + config.transports = transports.actual + } diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicatorStatus.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicatorStatus.apple.kt new file mode 100644 index 000000000..4dd0112fd --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicatorStatus.apple.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLMultipeerReplicatorStatus +import kotbase.ext.toCouchbaseLiteException +import kotbase.internal.DelegatedClass + +public actual class MultipeerReplicatorStatus +internal constructor(actual: CBLMultipeerReplicatorStatus) : DelegatedClass(actual) { + + public actual val transport: MultipeerTransport? + get() = actual.transport.toMultipeerTransportOrNull() + + public actual val isActive: Boolean + get() = actual.active + + public actual val error: CouchbaseLiteException? + get() = actual.error?.toCouchbaseLiteException() +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicatorStatusListener.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicatorStatusListener.apple.kt new file mode 100644 index 000000000..db8a2e066 --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerReplicatorStatusListener.apple.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLMultipeerReplicatorStatus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +internal fun MultipeerReplicatorStatusListener.convert(): (CBLMultipeerReplicatorStatus?) -> Unit { + return { status -> + invoke(MultipeerReplicatorStatus(status!!)) + } +} + +internal fun MultipeerReplicatorStatusSuspendListener.convert( + scope: CoroutineScope +): (CBLMultipeerReplicatorStatus?) -> Unit { + return { status -> + scope.launch { + invoke(MultipeerReplicatorStatus(status!!)) + } + } +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerTransport.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerTransport.apple.kt new file mode 100644 index 000000000..257d163ad --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/MultipeerTransport.apple.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLMultipeerTransport +import cocoapods.CouchbaseLite.CBLMultipeerTransportSet +import cocoapods.CouchbaseLite.kCBLMultipeerTransportBluetooth +import cocoapods.CouchbaseLite.kCBLMultipeerTransportNone +import cocoapods.CouchbaseLite.kCBLMultipeerTransportWifi + +public actual enum class MultipeerTransport { + WIFI, + BLUETOOTH; + + internal val actual: CBLMultipeerTransport + get() = when (this) { + WIFI -> kCBLMultipeerTransportWifi + BLUETOOTH -> kCBLMultipeerTransportBluetooth + } + + override fun toString(): String = when (this) { + WIFI -> "wifi" + BLUETOOTH -> "bluetooth" + } +} + +internal fun CBLMultipeerTransport.toMultipeerTransport(): MultipeerTransport = when (this) { + kCBLMultipeerTransportWifi -> MultipeerTransport.WIFI + kCBLMultipeerTransportBluetooth -> MultipeerTransport.BLUETOOTH + else -> error("Unknown transport: $this") +} + +internal fun CBLMultipeerTransport.toMultipeerTransportOrNull(): MultipeerTransport? = + if (this == kCBLMultipeerTransportNone) null else toMultipeerTransport() + +internal val Set.actual: CBLMultipeerTransportSet + get() = fold(kCBLMultipeerTransportNone) { acc, transport -> acc or transport.actual } + +internal fun CBLMultipeerTransportSet.toMultipeerTransportSet(): Set = buildSet { + if (this@toMultipeerTransportSet and kCBLMultipeerTransportWifi != kCBLMultipeerTransportNone) { + add(MultipeerTransport.WIFI) + } + if (this@toMultipeerTransportSet and kCBLMultipeerTransportBluetooth != kCBLMultipeerTransportNone) { + add(MultipeerTransport.BLUETOOTH) + } +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDiscoveryStatus.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDiscoveryStatus.apple.kt new file mode 100644 index 000000000..2b6d4c412 --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDiscoveryStatus.apple.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLPeerDiscoveryStatus +import kotbase.internal.DelegatedClass + +public actual class PeerDiscoveryStatus +internal constructor(actual: CBLPeerDiscoveryStatus) : DelegatedClass(actual) { + + public actual val peer: PeerInfo.PeerId + get() = PeerInfo.PeerId(actual.peerID) + + public actual val isOnline: Boolean + get() = actual.online + + public actual val transport: MultipeerTransport + get() = actual.transport.toMultipeerTransport() +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDiscoveryStatusListener.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDiscoveryStatusListener.apple.kt new file mode 100644 index 000000000..4b94da23a --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDiscoveryStatusListener.apple.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLPeerDiscoveryStatus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +internal fun PeerDiscoveryStatusListener.convert(): (CBLPeerDiscoveryStatus?) -> Unit { + return { status -> + invoke(PeerDiscoveryStatus(status!!)) + } +} + +internal fun PeerDiscoveryStatusSuspendListener.convert( + scope: CoroutineScope +): (CBLPeerDiscoveryStatus?) -> Unit { + return { status -> + scope.launch { + invoke(PeerDiscoveryStatus(status!!)) + } + } +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDocumentReplication.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDocumentReplication.apple.kt new file mode 100644 index 000000000..52145cc5d --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDocumentReplication.apple.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLPeerDocumentReplication +import cocoapods.CouchbaseLite.CBLReplicatedDocument +import kotbase.internal.DelegatedClass + +public actual class PeerDocumentReplication +internal constructor(actual: CBLPeerDocumentReplication) : DelegatedClass(actual) { + + public actual val peer: PeerInfo.PeerId + get() = PeerInfo.PeerId(actual.peerID) + + public actual val transport: MultipeerTransport + get() = actual.transport.toMultipeerTransport() + + public actual val isPush: Boolean + get() = actual.isPush() + + public actual val documents: List + get() = actual.documents.map { + @Suppress("UNCHECKED_CAST") + ReplicatedDocument(it as CBLReplicatedDocument) + } +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDocumentReplicationListener.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDocumentReplicationListener.apple.kt new file mode 100644 index 000000000..d624677ca --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerDocumentReplicationListener.apple.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLPeerDocumentReplication +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +internal fun PeerDocumentReplicationListener.convert(): (CBLPeerDocumentReplication?) -> Unit { + return { replication -> + invoke(PeerDocumentReplication(replication!!)) + } +} + +internal fun PeerDocumentReplicationSuspendListener.convert( + scope: CoroutineScope +): (CBLPeerDocumentReplication?) -> Unit { + return { replication -> + scope.launch { + invoke(PeerDocumentReplication(replication!!)) + } + } +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerInfo.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerInfo.apple.kt new file mode 100644 index 000000000..0cfe13f72 --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerInfo.apple.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLPeerID +import cocoapods.CouchbaseLite.CBLPeerInfo +import kotbase.ext.toByteArray +import kotbase.internal.DelegatedClass + +public actual class PeerInfo +internal constructor(actual: CBLPeerInfo) : DelegatedClass(actual) { + + public actual class PeerId + internal constructor(actual: CBLPeerID) : DelegatedClass(actual) { + + public actual val bytes: ByteArray + get() = actual.bytes.toByteArray() + } + + public actual val peerId: PeerId + get() = PeerId(actual.peerID) + + public actual val certificate: ByteArray? + get() = actual.certificate?.toByteArray() + + public actual val isOnline: Boolean + get() = actual.online + + public actual val transports: Set + get() = actual.transports.toMultipeerTransportSet() + + public actual val replicatorTransport: MultipeerTransport? + get() = actual.replicatorTransport.toMultipeerTransportOrNull() + + public actual val replicatorStatus: ReplicatorStatus + get() = ReplicatorStatus(actual.replicatorStatus) + + public actual val neighbors: Set + get() = actual.neighborPeers.map { + @Suppress("UNCHECKED_CAST") + PeerId(it as CBLPeerID) + }.toSet() + + public actual companion object { + + public actual val PEER_ID_LENGTH: Int = 32 + } +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerReplicatorStatus.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerReplicatorStatus.apple.kt new file mode 100644 index 000000000..44a69564b --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerReplicatorStatus.apple.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLPeerReplicatorStatus +import kotbase.internal.DelegatedClass + +public actual class PeerReplicatorStatus +internal constructor(actual: CBLPeerReplicatorStatus) : DelegatedClass(actual) { + + public actual val peerId: PeerInfo.PeerId + get() = PeerInfo.PeerId(actual.peerID) + + public actual val transport: MultipeerTransport + get() = actual.transport.toMultipeerTransport() + + public actual val isOutgoing: Boolean + get() = actual.outgoing + + public actual val status: ReplicatorStatus + get() = ReplicatorStatus(actual.status) +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerReplicatorStatusListener.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerReplicatorStatusListener.apple.kt new file mode 100644 index 000000000..137dc8341 --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/PeerReplicatorStatusListener.apple.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLPeerReplicatorStatus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +internal fun PeerReplicatorStatusListener.convert(): (CBLPeerReplicatorStatus?) -> Unit { + return { status -> + invoke(PeerReplicatorStatus(status!!)) + } +} + +internal fun PeerReplicatorStatusSuspendListener.convert( + scope: CoroutineScope +): (CBLPeerReplicatorStatus?) -> Unit { + return { status -> + scope.launch { + invoke(PeerReplicatorStatus(status!!)) + } + } +} diff --git a/couchbase-lite-ee/src/appleMain/ee/kotbase/TLSIdentityExt.apple.kt b/couchbase-lite-ee/src/appleMain/ee/kotbase/TLSIdentityExt.apple.kt new file mode 100644 index 000000000..7fd725746 --- /dev/null +++ b/couchbase-lite-ee/src/appleMain/ee/kotbase/TLSIdentityExt.apple.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Jeff Lockhart + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package kotbase + +import cocoapods.CouchbaseLite.CBLTLSIdentity +import kotbase.ext.toNSData +import kotbase.ext.wrapCBLError +import kotlin.time.Instant +import kotlinx.datetime.toNSDate + +@Throws(CouchbaseLiteException::class) +public actual fun TLSIdentity.Companion.createdSignedIdentityInsecure( + usage: Set, + attributes: Map, + caKey: ByteArray, + caCertificate: ByteArray, + expiration: Instant?, + alias: String +): TLSIdentity { + return wrapCBLError { error -> + @Suppress("UNCHECKED_CAST") + CBLTLSIdentity.createSignedIdentityInsecureForKeyUsages( + usage.actual, + attributes as Map, + expiration?.toNSDate(), + caKey.toNSData(), + caCertificate.toNSData(), + alias, + error + ) + }!!.asTLSIdentity() +} diff --git a/couchbase-lite/src/appleMain/kotlin/kotbase/Expression.apple.kt b/couchbase-lite/src/appleMain/kotlin/kotbase/Expression.apple.kt index af963a6c5..676c9665f 100644 --- a/couchbase-lite/src/appleMain/kotlin/kotbase/Expression.apple.kt +++ b/couchbase-lite/src/appleMain/kotlin/kotbase/Expression.apple.kt @@ -37,6 +37,9 @@ internal constructor(actual: CBLQueryExpression) : DelegatedClass dispatch_get_main_queue() diff --git a/couchbase-lite/src/commonTest/kotlin/kotbase/DocumentTest.kt b/couchbase-lite/src/commonTest/kotlin/kotbase/DocumentTest.kt index 4128648f1..ca3a80dd1 100755 --- a/couchbase-lite/src/commonTest/kotlin/kotbase/DocumentTest.kt +++ b/couchbase-lite/src/commonTest/kotlin/kotbase/DocumentTest.kt @@ -2701,7 +2701,7 @@ class DocumentTest : BaseDbTest() { fun testDocToJSON() { val mDoc = makeDocument() saveDocInCollection(mDoc) - verifyDocument(JsonObject(testCollection.getDocument(mDoc.id)!!.toJSON()!!)) + verifyDocument(JsonObject(testCollection.getDocument(mDoc.id)!!.toJSON())) } // JSON 3.5.? @@ -2719,7 +2719,7 @@ class DocumentTest : BaseDbTest() { val dbDoc = saveDocInCollection(MutableDocument("fromJSON", readJSONResource("document.json"))) testDatabase.saveBlob(makeBlob()) // be sure the blob is there... verifyDocument(dbDoc.content, true) - verifyDocument(JsonObject(dbDoc.toJSON()!!)) + verifyDocument(JsonObject(dbDoc.toJSON())) } // JSON 3.5.d.1 diff --git a/couchbase-lite/src/jvmCommonMain/kotlin/kotbase/Document.jvmCommon.kt b/couchbase-lite/src/jvmCommonMain/kotlin/kotbase/Document.jvmCommon.kt index 0206e5928..e674be0d4 100644 --- a/couchbase-lite/src/jvmCommonMain/kotlin/kotbase/Document.jvmCommon.kt +++ b/couchbase-lite/src/jvmCommonMain/kotlin/kotbase/Document.jvmCommon.kt @@ -108,7 +108,7 @@ internal constructor( actual.toMap().delegateIfNecessary() actual override fun toJSON(): String = - actual.toJSON() ?: throw CouchbaseLiteException("Failed encoding document as JSON") + actual.toJSON() actual override operator fun contains(key: String): Boolean = actual.contains(key) diff --git a/couchbase-lite/src/jvmCommonMain/kotlin/kotbase/QueryBuilder.jvmCommon.kt b/couchbase-lite/src/jvmCommonMain/kotlin/kotbase/QueryBuilder.jvmCommon.kt index aeb292435..c02c7aced 100644 --- a/couchbase-lite/src/jvmCommonMain/kotlin/kotbase/QueryBuilder.jvmCommon.kt +++ b/couchbase-lite/src/jvmCommonMain/kotlin/kotbase/QueryBuilder.jvmCommon.kt @@ -26,6 +26,7 @@ public actual object QueryBuilder { public actual fun selectDistinct(vararg results: SelectResult): Select = Select(CBLQueryBuilder.selectDistinct(*results.actuals())) + @Throws(CouchbaseLiteException::class) public actual fun createQuery(query: String, database: Database): Query = database.createQuery(query) } diff --git a/couchbase-lite/src/linuxMingwMain/kotlin/kotbase/QueryBuilder.linuxMingw.kt b/couchbase-lite/src/linuxMingwMain/kotlin/kotbase/QueryBuilder.linuxMingw.kt index b12cb0d55..04b9f0466 100644 --- a/couchbase-lite/src/linuxMingwMain/kotlin/kotbase/QueryBuilder.linuxMingw.kt +++ b/couchbase-lite/src/linuxMingwMain/kotlin/kotbase/QueryBuilder.linuxMingw.kt @@ -23,6 +23,7 @@ public actual object QueryBuilder { public actual fun selectDistinct(vararg results: SelectResult): Select = Select(QueryState(results.asList(), true)) + @Throws(CouchbaseLiteException::class) public actual fun createQuery(query: String, database: Database): Query = database.createQuery(query) }