-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
39 changed files
with
1,439 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package com.juul.indexeddb | ||
|
||
import com.juul.indexeddb.external.IDBCursor | ||
import com.juul.indexeddb.external.IDBCursorWithValue | ||
import kotlinx.coroutines.channels.SendChannel | ||
|
||
public open class Cursor internal constructor( | ||
internal open val cursor: IDBCursor, | ||
private val channel: SendChannel<*>, | ||
) { | ||
public val key: JsAny | ||
get() = cursor.key | ||
|
||
public val primaryKey: JsAny | ||
get() = cursor.primaryKey | ||
|
||
public fun close() { | ||
channel.close() | ||
} | ||
|
||
public fun `continue`() { | ||
cursor.`continue`() | ||
} | ||
|
||
public fun advance(count: Int) { | ||
cursor.advance(count) | ||
} | ||
|
||
public fun `continue`(key: Key) { | ||
cursor.`continue`(key.toJs()) | ||
} | ||
|
||
public fun continuePrimaryKey(key: Key, primaryKey: Key) { | ||
cursor.continuePrimaryKey(key.toJs(), primaryKey.toJs()) | ||
} | ||
|
||
public enum class Direction( | ||
internal val constant: String, | ||
) { | ||
Next("next"), | ||
NextUnique("nextunique"), | ||
Previous("prev"), | ||
PreviousUnique("prevunique"), | ||
} | ||
} | ||
|
||
public class CursorWithValue internal constructor( | ||
override val cursor: IDBCursorWithValue, | ||
channel: SendChannel<*>, | ||
) : Cursor(cursor, channel) { | ||
public val value: JsAny? | ||
get() = cursor.value | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package com.juul.indexeddb | ||
|
||
import com.juul.indexeddb.external.IDBCursor | ||
|
||
public sealed class CursorStart { | ||
|
||
internal abstract fun apply(cursor: IDBCursor) | ||
|
||
public data class Advance( | ||
val count: Int, | ||
) : CursorStart() { | ||
override fun apply(cursor: IDBCursor) { | ||
cursor.advance(count) | ||
} | ||
} | ||
|
||
public data class Continue( | ||
val key: Key, | ||
) : CursorStart() { | ||
override fun apply(cursor: IDBCursor) { | ||
cursor.`continue`(key.toJs()) | ||
} | ||
} | ||
|
||
public data class ContinuePrimaryKey( | ||
val key: Key, | ||
val primaryKey: Key, | ||
) : CursorStart() { | ||
override fun apply(cursor: IDBCursor) { | ||
cursor.continuePrimaryKey(key.toJs(), primaryKey.toJs()) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package com.juul.indexeddb | ||
|
||
import com.juul.indexeddb.external.IDBDatabase | ||
import com.juul.indexeddb.external.IDBFactory | ||
import com.juul.indexeddb.external.IDBTransactionDurability | ||
import com.juul.indexeddb.external.IDBTransactionOptions | ||
import com.juul.indexeddb.external.IDBVersionChangeEvent | ||
import com.juul.indexeddb.external.indexedDB | ||
import kotlinx.browser.window | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.withContext | ||
|
||
/** | ||
* Inside the [initialize] block, you must not call any `suspend` functions except for: | ||
* - those provided by this library and scoped on [Transaction] (and its subclasses) | ||
* - flow operations on the flows returns by [Transaction.openCursor] and [Transaction.openKeyCursor] | ||
* - `suspend` functions composed entirely of other legal functions | ||
*/ | ||
public suspend fun openDatabase( | ||
name: String, | ||
version: Int, | ||
initialize: suspend VersionChangeTransaction.( | ||
database: Database, | ||
oldVersion: Int, | ||
newVersion: Int, | ||
) -> Unit, | ||
): Database = withContext(Dispatchers.Unconfined) { | ||
val indexedDB: IDBFactory? = selfIndexedDB | ||
val factory = checkNotNull(indexedDB) { "Your browser doesn't support IndexedDB." } | ||
val request = factory.open(name, version) | ||
val versionChangeEvent = request.onNextEvent("success", "upgradeneeded", "error", "blocked") { event -> | ||
when (event.type) { | ||
"upgradeneeded" -> event as IDBVersionChangeEvent | ||
"error" -> throw ErrorEventException(event) | ||
"blocked" -> throw OpenBlockedException(name, event) | ||
else -> null | ||
} | ||
} | ||
Database(request.result).also { database -> | ||
if (versionChangeEvent != null) { | ||
val transaction = VersionChangeTransaction(checkNotNull(request.transaction)) | ||
transaction.initialize( | ||
database, | ||
versionChangeEvent.oldVersion, | ||
versionChangeEvent.newVersion, | ||
) | ||
transaction.awaitCompletion() | ||
} | ||
} | ||
} | ||
|
||
public suspend fun deleteDatabase(name: String) { | ||
val factory = checkNotNull(window.indexedDB) { "Your browser doesn't support IndexedDB." } | ||
val request = factory.deleteDatabase(name) | ||
request.onNextEvent("success", "error", "blocked") { event -> | ||
when (event.type) { | ||
"error", "blocked" -> throw ErrorEventException(event) | ||
else -> null | ||
} | ||
} | ||
} | ||
|
||
public class Database internal constructor( | ||
database: IDBDatabase, | ||
) { | ||
private var database: IDBDatabase? = database | ||
|
||
init { | ||
// listen for database structure changes (e.g., upgradeneeded while DB is open or deleteDatabase) | ||
database.addEventListener("versionchange") { close() } | ||
// listen for force close, e.g., browser profile on a USB drive that's ejected or db deleted through dev tools | ||
database.addEventListener("close") { close() } | ||
} | ||
|
||
internal fun ensureDatabase(): IDBDatabase = checkNotNull(database) { "database is closed" } | ||
|
||
/** | ||
* Inside the [action] block, you must not call any `suspend` functions except for: | ||
* - those provided by this library and scoped on [Transaction] (and its subclasses) | ||
* - flow operations on the flows returns by [Transaction.openCursor] and [Transaction.openKeyCursor] | ||
* - `suspend` functions composed entirely of other legal functions | ||
*/ | ||
public suspend fun <T> transaction( | ||
vararg store: String, | ||
durability: IDBTransactionDurability = IDBTransactionDurability.Default, | ||
action: suspend Transaction.() -> T, | ||
): T = withContext(Dispatchers.Unconfined) { | ||
check(store.isNotEmpty()) { | ||
"At least one store needs to be passed to transaction" | ||
} | ||
|
||
val transaction = Transaction( | ||
ensureDatabase().transaction( | ||
storeNames = ReadonlyArray( | ||
*store.map { it.toJsString() }.toTypedArray(), | ||
), | ||
mode = "readonly", | ||
options = IDBTransactionOptions(durability), | ||
), | ||
) | ||
val result = transaction.action() | ||
transaction.awaitCompletion() | ||
result | ||
} | ||
|
||
/** | ||
* Inside the [action] block, you must not call any `suspend` functions except for: | ||
* - those provided by this library and scoped on [Transaction] (and its subclasses) | ||
* - flow operations on the flows returns by [Transaction.openCursor] and [Transaction.openKeyCursor] | ||
* - `suspend` functions composed entirely of other legal functions | ||
*/ | ||
public suspend fun <T> writeTransaction( | ||
vararg store: String, | ||
durability: IDBTransactionDurability = IDBTransactionDurability.Default, | ||
action: suspend WriteTransaction.() -> T, | ||
): T = withContext(Dispatchers.Unconfined) { | ||
check(store.isNotEmpty()) { | ||
"At least one store needs to be passed to writeTransaction" | ||
} | ||
|
||
val transaction = WriteTransaction( | ||
ensureDatabase() | ||
.transaction( | ||
storeNames = ReadonlyArray( | ||
*store.map { it.toJsString() }.toTypedArray(), | ||
), | ||
mode = "readwrite", | ||
options = IDBTransactionOptions(durability), | ||
), | ||
) | ||
|
||
with(transaction) { | ||
// Force overlapping transactions to not call `action` until prior transactions complete. | ||
objectStore(store.first()) | ||
.openKeyCursor(autoContinue = false) | ||
.collect { it.close() } | ||
} | ||
val result = transaction.action() | ||
transaction.awaitCompletion() | ||
result | ||
} | ||
|
||
public fun close() { | ||
database?.close() | ||
database = null | ||
} | ||
} | ||
|
||
@Suppress("RedundantNullableReturnType") | ||
private val selfIndexedDB: IDBFactory? = js("self.indexedDB || self.webkitIndexedDB") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.juul.indexeddb | ||
|
||
import org.w3c.dom.events.Event | ||
|
||
public abstract class EventException( | ||
message: String?, | ||
cause: Throwable?, | ||
public val event: Event, | ||
) : Exception(message, cause) | ||
|
||
public class EventHandlerException( | ||
cause: Throwable?, | ||
event: Event, | ||
) : EventException("An inner exception was thrown: $cause", cause, event) | ||
|
||
public class ErrorEventException( | ||
event: Event, | ||
) : EventException("An error event was received.", cause = null, event) | ||
public class OpenBlockedException( | ||
public val name: String, | ||
event: Event, | ||
) : EventException("Resource in use: $name.", cause = null, event) | ||
public class AbortTransactionException( | ||
event: Event, | ||
) : EventException("Transaction aborted while waiting for completion.", cause = null, event) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.juul.indexeddb | ||
|
||
import com.juul.indexeddb.external.IDBCursor | ||
import com.juul.indexeddb.external.IDBCursorWithValue | ||
import com.juul.indexeddb.external.IDBIndex | ||
import com.juul.indexeddb.external.ReadonlyArray | ||
|
||
public class Index internal constructor( | ||
internal val index: IDBIndex, | ||
) : Queryable() { | ||
override fun requestGet(key: Key): Request<*> = | ||
Request(index.get(key.toJs())) | ||
|
||
override fun requestGetAll(query: Key?): Request<ReadonlyArray<*>> = | ||
Request(index.getAll(query?.toJs())) | ||
|
||
override fun requestOpenCursor(query: Key?, direction: Cursor.Direction): Request<IDBCursorWithValue?> = | ||
Request(index.openCursor(query?.toJs(), direction.constant)) | ||
|
||
override fun requestOpenKeyCursor(query: Key?, direction: Cursor.Direction): Request<IDBCursor?> = | ||
Request(index.openKeyCursor(query?.toJs(), direction.constant)) | ||
|
||
override fun requestCount(query: Key?): Request<JsNumber> = | ||
Request(index.count(query?.toJs())) | ||
} |
Oops, something went wrong.