All Downloads are FREE. Search and download functionalities are using the official Maven repository.

appleMain.kotbase.Database.apple.kt Maven / Gradle / Ivy

There is a newer version: 3.1.3-1.1.0
Show newest version
/*
 * Copyright 2022-2023 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.CBLDatabase
import cocoapods.CouchbaseLite.isClosed
import kotbase.internal.DelegatedClass
import kotbase.ext.asDispatchQueue
import kotbase.ext.wrapCBLError
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.datetime.Instant
import kotlinx.datetime.toKotlinInstant
import kotlinx.datetime.toNSDate
import platform.objc.objc_sync_enter
import platform.objc.objc_sync_exit
import kotlin.coroutines.CoroutineContext

public actual class Database
internal constructor(actual: CBLDatabase) : DelegatedClass(actual) {

    @Throws(CouchbaseLiteException::class)
    public actual constructor(name: String) : this(
        wrapCBLError { error ->
            require(name.isNotEmpty()) { "db name must not be empty" }
            CBLDatabase(name, error)
        }
    )

    @Throws(CouchbaseLiteException::class)
    public actual constructor(name: String, config: DatabaseConfiguration) : this(
        wrapCBLError { error ->
            CBLDatabase(name, config.actual, error)
        }
    )

    public actual companion object {

        public actual val log: Log by lazy {
            Log(CBLDatabase.log())
        }

        @Throws(CouchbaseLiteException::class)
        public actual fun delete(name: String, directory: String?) {
            // Java SDK throws not found error
            if (!exists(name, directory ?: DatabaseConfiguration(null).directory)) {
                throw CouchbaseLiteException(
                    "Database not found for delete",
                    CBLError.Domain.CBLITE,
                    CBLError.Code.NOT_FOUND
                )
            }
            wrapCBLError { error ->
                CBLDatabase.deleteDatabase(name, directory, error)
            }
        }

        public actual fun exists(name: String, directory: String?): Boolean =
            CBLDatabase.databaseExists(name, directory)

        @Throws(CouchbaseLiteException::class)
        public actual fun copy(path: String, name: String, config: DatabaseConfiguration?) {
            wrapCBLError { error ->
                CBLDatabase.copyFromPath(path, name, config?.actual, error)
            }
        }
    }

    public actual val name: String
        get() = actual.name

    public actual val path: String?
        get() = actual.path

    public actual val count: Long
        get() = actual.count.toLong()

    public actual val config: DatabaseConfiguration
        get() = DatabaseConfiguration(actual.config)

    public actual fun getDocument(id: String): Document? {
        return mustBeOpen {
            actual.documentWithID(id)?.asDocument()
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun save(document: MutableDocument) {
        mustBeOpen {
            wrapCBLError { error ->
                actual.saveDocument(document.actual, error)
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun save(
        document: MutableDocument,
        concurrencyControl: ConcurrencyControl
    ): Boolean {
        return try {
            mustBeOpen {
                wrapCBLError { error ->
                    actual.saveDocument(document.actual, concurrencyControl.actual, error)
                }
            }
        } catch (e: CouchbaseLiteException) {
            if (e.code == CBLError.Code.CONFLICT && e.domain == CBLError.Domain.CBLITE) {
                // Java SDK doesn't throw exception on conflict, only returns false
                false
            } else {
                throw e
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun save(document: MutableDocument, conflictHandler: ConflictHandler): Boolean {
        return mustBeOpen {
            wrapCBLError { error ->
                try {
                    actual.saveDocument(document.actual, conflictHandler.convert(), error)
                } catch (e: Exception) {
                    throw CouchbaseLiteException(
                        "Conflict handler threw an exception",
                        e,
                        CBLError.Domain.CBLITE,
                        CBLError.Code.CONFLICT
                    )
                }
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun delete(document: Document) {
        mustBeOpen {
            wrapCBLError { error ->
                actual.deleteDocument(document.actual, error)
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun delete(document: Document, concurrencyControl: ConcurrencyControl): Boolean {
        return try {
            mustBeOpen {
                wrapCBLError { error ->
                    actual.deleteDocument(document.actual, concurrencyControl.actual, error)
                }
            }
        } catch (e: CouchbaseLiteException) {
            if (e.code == CBLError.Code.CONFLICT && e.domain == CBLError.Domain.CBLITE) {
                // Java SDK doesn't throw exception on conflict, only returns false
                false
            } else {
                throw e
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun purge(document: Document) {
        try {
            mustBeOpen {
                wrapCBLError { error ->
                    actual.purgeDocument(document.actual, error)
                }
            }
        } catch (e: CouchbaseLiteException) {
            // Java SDK ignores not found error, except for new document
            val isNew = document.revisionID == null
            if (isNew || e.code != CBLError.Code.NOT_FOUND || e.domain != CBLError.Domain.CBLITE) {
                throw e
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun purge(id: String) {
        mustBeOpen {
            wrapCBLError { error ->
                actual.purgeDocumentWithID(id, error)
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun setDocumentExpiration(id: String, expiration: Instant?) {
        mustBeOpen {
            wrapCBLError { error ->
                actual.setDocumentExpirationWithID(id, expiration?.toNSDate(), error)
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun getDocumentExpiration(id: String): Instant? {
        return mustBeOpen {
            actual.getDocumentExpirationWithID(id)?.toKotlinInstant()
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun  inBatch(work: Database.() -> R): R {
        return mustBeOpen {
            @Suppress("UNCHECKED_CAST")
            wrapCBLError { error ->
                var result: R? = null
                actual.inBatch(error) {
                    result = [email protected]()
                }
                result
            } as R
        }
    }

    public actual fun addChangeListener(listener: DatabaseChangeListener): ListenerToken {
        return DelegatedListenerToken(
            mustBeOpen {
                actual.addChangeListener(listener.convert())
            }
        )
    }

    @OptIn(ExperimentalStdlibApi::class)
    public actual fun addChangeListener(context: CoroutineContext, listener: DatabaseChangeSuspendListener): ListenerToken {
        return mustBeOpen {
            val scope = CoroutineScope(SupervisorJob() + context)
            val token = actual.addChangeListenerWithQueue(
                context[CoroutineDispatcher]?.asDispatchQueue(),
                listener.convert(scope)
            )
            SuspendListenerToken(scope, DelegatedListenerToken(token))
        }
    }

    @OptIn(ExperimentalStdlibApi::class)
    public actual fun addChangeListener(scope: CoroutineScope, listener: DatabaseChangeSuspendListener) {
        mustBeOpen {
            val token = actual.addChangeListenerWithQueue(
                scope.coroutineContext[CoroutineDispatcher]?.asDispatchQueue(),
                listener.convert(scope)
            )
            scope.coroutineContext[Job]?.invokeOnCompletion {
                actual.removeChangeListenerWithToken(token)
            }
        }
    }

    public actual fun removeChangeListener(token: ListenerToken) {
        if (token is SuspendListenerToken) {
            actual.removeChangeListenerWithToken(token.token.actual)
            token.scope.cancel()
        } else {
            token as DelegatedListenerToken
            actual.removeChangeListenerWithToken(token.actual)
        }
    }

    public actual fun addDocumentChangeListener(id: String, listener: DocumentChangeListener): ListenerToken {
        return DelegatedListenerToken(
            mustBeOpen {
                actual.addDocumentChangeListenerWithID(id, listener.convert())
            }
        )
    }

    @OptIn(ExperimentalStdlibApi::class)
    public actual fun addDocumentChangeListener(
        id: String,
        context: CoroutineContext,
        listener: DocumentChangeSuspendListener
    ): ListenerToken {
        return mustBeOpen {
            val scope = CoroutineScope(SupervisorJob() + context)
            val token = actual.addDocumentChangeListenerWithID(
                id,
                context[CoroutineDispatcher]?.asDispatchQueue(),
                listener.convert(scope)
            )
            SuspendListenerToken(scope, DelegatedListenerToken(token))
        }
    }

    @OptIn(ExperimentalStdlibApi::class)
    public actual fun addDocumentChangeListener(
        id: String,
        scope: CoroutineScope,
        listener: DocumentChangeSuspendListener
    ) {
        mustBeOpen {
            val token = actual.addDocumentChangeListenerWithID(
                id,
                scope.coroutineContext[CoroutineDispatcher]?.asDispatchQueue(),
                listener.convert(scope)
            )
            scope.coroutineContext[Job]?.invokeOnCompletion {
                actual.removeChangeListenerWithToken(token)
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun close() {
        wrapCBLError { error ->
            actual.close(error)
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun delete() {
        mustBeOpen {
            wrapCBLError { error ->
                actual.delete(error)
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun createQuery(query: String): Query {
        val actualQuery = mustBeOpen {
            wrapCBLError { error ->
                actual.createQuery(query, error)
            }
        }
        return DelegatedQuery(actualQuery!!)
    }

    @Suppress("UNCHECKED_CAST")
    @Throws(CouchbaseLiteException::class)
    public actual fun getIndexes(): List {
        return mustBeOpen {
            actual.indexes as List
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun createIndex(name: String, index: Index) {
        mustBeOpen {
            wrapCBLError { error ->
                actual.createIndex(index.actual, name, error)
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun createIndex(name: String, config: IndexConfiguration) {
        mustBeOpen {
            wrapCBLError { error ->
                actual.createIndexWithConfig(config.actual, name, error)
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun deleteIndex(name: String) {
        mustBeOpen {
            wrapCBLError { error ->
                actual.deleteIndexForName(name, error)
            }
        }
    }

    @Throws(CouchbaseLiteException::class)
    public actual fun performMaintenance(type: MaintenanceType): Boolean {
        return mustBeOpen {
            wrapCBLError { error ->
                actual.performMaintenance(type.actual, error)
            }
        }
    }

    internal fun mustBeOpen() {
        mustBeOpen { }
    }

    internal inline fun  withLock(action: () -> R): R {
        // TODO: uses _mutex instead of self as lock in 3.1, use - (id) mutex
        objc_sync_enter(actual)
        return try {
            action()
        } finally {
            objc_sync_exit(actual)
        }
    }

    private fun  mustBeOpen(action: () -> R): R {
        return withLock {
            if (actual.isClosed()) {
                throw IllegalStateException("Attempt to perform an operation on a closed database.")
            }
            action()
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy