-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Wasm Support for kotlin-document-store #47
Open
ApoloApps
wants to merge
3
commits into
lamba92:master
Choose a base branch
from
ApoloApps:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
37 changes: 37 additions & 0 deletions
37
...wasmJsMain/kotlin/com/github/lamba92/kotlin/document/store/stores/browser/BrowserStore.kt
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,37 @@ | ||
package com.github.lamba92.kotlin.document.store.stores.browser | ||
|
||
import com.github.lamba92.kotlin.document.store.core.AbstractDataStore | ||
import com.github.lamba92.kotlin.document.store.core.DataStore | ||
import com.github.lamba92.kotlin.document.store.core.PersistentMap | ||
import keyval.delMany | ||
import keyval.keys | ||
import kotlinx.coroutines.await | ||
|
||
/** | ||
* Implementation of the [DataStore] for use in web browsers. | ||
* | ||
* `BrowserStore` uses `IndexedDB` as the underlying storage mechanism, providing | ||
* persistent key-value storage in the user's browser. It is designed for use in | ||
* web applications that require durable storage across browser sessions. | ||
* | ||
* This class supports the creation, retrieval, and deletion of named maps, where | ||
* each map is implemented as an [IndexedDBMap]. Concurrency and synchronization | ||
* are managed using locks to ensure thread safety during access to individual maps. | ||
* | ||
* This implementation extends [AbstractDataStore], inheriting utility methods for | ||
* managing locks and operations related to the data store. | ||
*/ | ||
public object BrowserStore : AbstractDataStore() { | ||
override suspend fun getMap(name: String): PersistentMap<String, String> = withStoreLock { IndexedDBMap(name, getMutex(name)) } | ||
|
||
override suspend fun deleteMap(name: String): Unit = | ||
withStoreLock { | ||
lockAndRemoveMutex(name) { | ||
keys() | ||
.await<JsArray<JsString>>() | ||
.toList() | ||
.filter { it.toString().startsWith(IndexedDBMap.buildPrefix(name)) } | ||
.let { delMany(it.toJsArray()).await() } | ||
} | ||
} | ||
} |
121 changes: 121 additions & 0 deletions
121
...wasmJsMain/kotlin/com/github/lamba92/kotlin/document/store/stores/browser/IndexedDBMap.kt
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,121 @@ | ||
package com.github.lamba92.kotlin.document.store.stores.browser | ||
|
||
import com.github.lamba92.kotlin.document.store.core.PersistentMap | ||
import com.github.lamba92.kotlin.document.store.core.SerializableEntry | ||
import com.github.lamba92.kotlin.document.store.core.UpdateResult | ||
import keyval.del | ||
import keyval.delMany | ||
import keyval.keys | ||
import keyval.set | ||
import kotlinx.coroutines.await | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.asFlow | ||
import kotlinx.coroutines.flow.filter | ||
import kotlinx.coroutines.flow.flow | ||
import kotlinx.coroutines.sync.Mutex | ||
import kotlinx.coroutines.sync.withLock | ||
|
||
/** | ||
* A browser-based implementation of the `DataStore` that uses `IndexedDB` for persistent storage. | ||
* | ||
* The `BrowserStore` enables web applications to store and manage named maps persistently | ||
* in a client-side database (IndexedDB). It supports creating, retrieving, and deleting | ||
* persistent maps, while ensuring thread safety through synchronization mechanisms. | ||
* | ||
* Each persistent map is backed by an `IndexedDBMap`, which provides efficient key-value | ||
* storage and ensures data durability across browser sessions. | ||
* | ||
* This implementation is ideal for client-side scenarios where durable, structured storage | ||
* is required in the browser environment. | ||
*/ | ||
public class IndexedDBMap( | ||
private val name: String, | ||
private val mutex: Mutex, | ||
) : PersistentMap<String, String> { | ||
public companion object { | ||
private const val SEPARATOR = "." | ||
|
||
internal fun buildPrefix(name: String) = "$name$SEPARATOR" | ||
} | ||
|
||
private val prefixed | ||
get() = buildPrefix(name) | ||
|
||
private fun String.prefixed() = "$prefixed$this" | ||
|
||
override suspend fun clear(): Unit = | ||
keys() | ||
.await<JsArray<JsString>>() | ||
.toList() | ||
.filter { it.toString().startsWith(prefixed) } | ||
.let { delMany(it.toJsArray()).await() } | ||
|
||
override suspend fun size(): Long = | ||
keys() | ||
.await<JsArray<JsString>>() | ||
.toList() | ||
.filter { it.toString().startsWith(prefixed) } | ||
.size | ||
.toLong() | ||
|
||
override suspend fun isEmpty(): Boolean = size() == 0L | ||
|
||
override suspend fun get(key: String): String? = keyval.get(key.prefixed()).await<JsString?>()?.toString() | ||
|
||
override suspend fun put( | ||
key: String, | ||
value: String, | ||
): String? = mutex.withLock { unsafePut(key, value) } | ||
|
||
private suspend fun IndexedDBMap.unsafePut( | ||
key: String, | ||
value: String, | ||
): String? { | ||
val previous = get(key) | ||
set(key.prefixed(), value.toJsString()).await<JsAny?>() | ||
return previous | ||
} | ||
|
||
override suspend fun remove(key: String): String? = | ||
mutex.withLock { | ||
val previous = get(key) | ||
del(key.prefixed()).await<JsAny?>() | ||
previous | ||
} | ||
|
||
override suspend fun containsKey(key: String): Boolean = get(key) != null | ||
|
||
override suspend fun update( | ||
key: String, | ||
value: String, | ||
updater: (String) -> String, | ||
): UpdateResult<String> = | ||
mutex.withLock { | ||
val oldValue = get(key) | ||
val newValue = oldValue?.let(updater) ?: value | ||
set(key.prefixed(), newValue.toJsString()).await<JsAny?>() | ||
UpdateResult(oldValue, newValue) | ||
} | ||
|
||
override suspend fun getOrPut( | ||
key: String, | ||
defaultValue: () -> String, | ||
): String = | ||
mutex.withLock { | ||
get(key) ?: defaultValue().also { unsafePut(key, it) } | ||
} | ||
|
||
override fun entries(): Flow<Map.Entry<String, String>> = | ||
flow { | ||
keys() | ||
.await<JsArray<JsString>>() | ||
.toList() | ||
.asFlow() | ||
.filter { it.toString().startsWith(prefixed) } | ||
.collect { key -> | ||
keyval.get(key.toString()).await<JsString?>()?.let { value -> | ||
emit(SerializableEntry(key.toString().removePrefix(prefixed), value.toString())) | ||
} | ||
} | ||
} | ||
} |
85 changes: 85 additions & 0 deletions
85
stores/browser/src/wasmJsMain/kotlin/externalTypes/ExternalTypes.kt
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,85 @@ | ||
package externalTypes | ||
|
||
public external interface IDBRequest { | ||
public var oncomplete: (() -> Unit)? | ||
public var onsuccess: (() -> Unit)? | ||
public var onabort: (() -> Unit)? | ||
public var onerror: (() -> Unit)? | ||
public val result: JsAny? | ||
public val error: DOMException? | ||
} | ||
|
||
public external interface IDBTransaction { | ||
public var oncomplete: (() -> Unit)? | ||
public var onsuccess: (() -> Unit)? | ||
public var onabort: (() -> Unit)? | ||
public var onerror: (() -> Unit)? | ||
public val result: JsAny? | ||
public val error: DOMException? | ||
} | ||
|
||
public external interface IDBObjectStore { | ||
public fun put( | ||
value: JsAny?, | ||
key: String, | ||
): IDBRequest | ||
|
||
public fun get(key: String): IDBRequest | ||
|
||
public fun delete(key: String): IDBRequest | ||
|
||
public fun clear(): IDBRequest | ||
|
||
public fun openCursor(): IDBRequest | ||
|
||
public fun getAll(): IDBRequest | ||
|
||
public fun getAllKeys(): IDBRequest | ||
|
||
public val transaction: IDBTransaction | ||
} | ||
|
||
public external interface IDBCursorWithValue { | ||
public val key: String | ||
public val value: String | ||
|
||
@JsName("continue") | ||
public fun next() | ||
} | ||
|
||
public external class DOMException( | ||
message: String = definedExternally, | ||
name: String = definedExternally, | ||
) { | ||
public val name: String | ||
public val message: String | ||
public val code: Short | ||
|
||
public companion object { | ||
public val INDEX_SIZE_ERR: Short | ||
public val DOMSTRING_SIZE_ERR: Short | ||
public val HIERARCHY_REQUEST_ERR: Short | ||
public val WRONG_DOCUMENT_ERR: Short | ||
public val INVALID_CHARACTER_ERR: Short | ||
public val NO_DATA_ALLOWED_ERR: Short | ||
public val NO_MODIFICATION_ALLOWED_ERR: Short | ||
public val NOT_FOUND_ERR: Short | ||
public val NOT_SUPPORTED_ERR: Short | ||
public val INUSE_ATTRIBUTE_ERR: Short | ||
public val INVALID_STATE_ERR: Short | ||
public val SYNTAX_ERR: Short | ||
public val INVALID_MODIFICATION_ERR: Short | ||
public val NAMESPACE_ERR: Short | ||
public val INVALID_ACCESS_ERR: Short | ||
public val VALIDATION_ERR: Short | ||
public val TYPE_MISMATCH_ERR: Short | ||
public val SECURITY_ERR: Short | ||
public val NETWORK_ERR: Short | ||
public val ABORT_ERR: Short | ||
public val URL_MISMATCH_ERR: Short | ||
public val QUOTA_EXCEEDED_ERR: Short | ||
public val TIMEOUT_ERR: Short | ||
public val INVALID_NODE_TYPE_ERR: Short | ||
public val DATA_CLONE_ERR: Short | ||
} | ||
} |
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,66 @@ | ||
@file:JsModule("idb-keyval") | ||
@file:Suppress("unused") | ||
|
||
package keyval | ||
|
||
import externalTypes.IDBObjectStore | ||
import kotlin.js.Promise | ||
|
||
public external interface UseStore { | ||
public operator fun invoke( | ||
txMode: String, | ||
callback: (store: IDBObjectStore) -> JsAny?, | ||
): Promise<JsAny?> | ||
} | ||
|
||
public external fun promisifyRequest(request: JsAny?): Promise<JsAny?> | ||
|
||
public external fun createStore( | ||
dbName: String, | ||
storeName: String, | ||
): UseStore | ||
|
||
public external fun get( | ||
key: String?, | ||
customStore: UseStore = definedExternally, | ||
): Promise<JsString?> | ||
|
||
public external fun set( | ||
key: String, | ||
value: JsAny?, | ||
customStore: UseStore = definedExternally, | ||
): Promise<JsAny?> | ||
|
||
public external fun setMany( | ||
entries: JsArray<JsArray<JsAny?>>, | ||
customStore: UseStore = definedExternally, | ||
): Promise<JsAny?> | ||
|
||
public external fun getMany( | ||
keys: JsArray<JsString>, | ||
customStore: UseStore = definedExternally, | ||
): Promise<JsArray<JsAny?>> | ||
|
||
public external fun update( | ||
key: String, | ||
updater: (oldValue: JsAny?) -> JsAny?, | ||
customStore: UseStore = definedExternally, | ||
): Promise<JsAny?> | ||
|
||
public external fun del( | ||
key: String, | ||
customStore: UseStore = definedExternally, | ||
): Promise<JsAny?> | ||
|
||
public external fun delMany( | ||
keys: JsArray<JsString>, | ||
customStore: UseStore = definedExternally, | ||
): Promise<JsAny?> | ||
|
||
public external fun clear(customStore: UseStore = definedExternally): Promise<JsAny?> | ||
|
||
public external fun keys(customStore: UseStore = definedExternally): Promise<JsArray<JsString>> | ||
|
||
public external fun values(customStore: UseStore = definedExternally): Promise<JsArray<JsString>> | ||
|
||
public external fun entries(customStore: UseStore = definedExternally): Promise<JsArray<JsArray<JsString>>> |
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 @@ | ||
import com.github.lamba92.kotlin.document.store.core.DataStore | ||
import com.github.lamba92.kotlin.document.store.stores.browser.BrowserStore | ||
import com.github.lamba92.kotlin.document.store.tests.AbstractDeleteTests | ||
import com.github.lamba92.kotlin.document.store.tests.AbstractDocumentDatabaseTests | ||
import com.github.lamba92.kotlin.document.store.tests.AbstractFindTests | ||
import com.github.lamba92.kotlin.document.store.tests.AbstractIndexTests | ||
import com.github.lamba92.kotlin.document.store.tests.AbstractInsertTests | ||
import com.github.lamba92.kotlin.document.store.tests.AbstractObjectCollectionTests | ||
import com.github.lamba92.kotlin.document.store.tests.AbstractUpdateTests | ||
import com.github.lamba92.kotlin.document.store.tests.DataStoreProvider | ||
import kotlinx.coroutines.await | ||
|
||
class BrowserDeleteTests : AbstractDeleteTests(BrowserStoreProvider) | ||
|
||
class BrowserDocumentDatabaseTests : AbstractDocumentDatabaseTests(BrowserStoreProvider) | ||
|
||
class BrowserIndexTests : AbstractIndexTests(BrowserStoreProvider) | ||
|
||
class BrowserInsertTests : AbstractInsertTests(BrowserStoreProvider) | ||
|
||
class BrowserUpdateTests : AbstractUpdateTests(BrowserStoreProvider) | ||
|
||
class BrowserFindTests : AbstractFindTests(BrowserStoreProvider) | ||
|
||
class BrowserObjectCollectionTests : AbstractObjectCollectionTests(BrowserStoreProvider) | ||
|
||
object BrowserStoreProvider : DataStoreProvider { | ||
override suspend fun deleteDatabase(testName: String) { | ||
keyval.clear().await<JsAny?>() | ||
} | ||
|
||
override fun provide(testName: String): DataStore = BrowserStore | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing! You can use NPM libraries in wasmJs as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes! it can be used but they need to be compatible with browser (which keyval is)