Kottage
Kotlin Multiplatform Key-Value Store Local Cache Storage for Single Source of Truth.
Features
- A Kotlin Multiplatform library
- Key-Value Store with no schemas, values are stored to SQLite
- Observing events of item updates as Flow
- Cache Expiration
- Cache Eviction Strategies:
- Expiration Time
- FIFO Strategy
- LRU Strategy
- KVS Cache mode
- Expired items are evicted automatically
- KVS Storage mode
- List structures for Paging are supported
- Support primitive values and
@Serializable classes
Requires
- New memory manager
enabled with Kotlin/Native platform.
- SQLite3 dynamic link library on runtime environment
Usage
Setup
Add Kottage as gradle dependency.
Kotlin Multiplatform:
build.gradle.kts
plugins {
kotlin("multiplatform")
}
kotlin {
sourceSets {
commonMain {
dependencies {
implementation("io.github.irgaly.kottage:kottage:1.11.0")
}
}
}
}
Android or JVM without Kotlin Multiplatform:
build.gradle.kts
plugins {
id("com.android.application")
kotlin("android")
}
dependencies {
implementation("io.github.irgaly.kottage:kottage:1.11.0")
}
Use Kottage
Use Kottage as KVS cache or KVS storage.
First, get a Kottage instance. Even though you can use Kottage instance as a singleton, multiple
Kottage instances creation is allowed. Kottage instances and methods are thread safe.
import io.github.irgaly.kottage.platform.contextOf
val databaseDirectory: String = ...
kottageEnvironment: KottageEnvironment = KottageEnvironment(
context = contextOf(context)
)
kottage: Kottage = Kottage(
name = ,
directoryPath = databaseDirectory,
environment = kottageEnvironment,
scope = scope,
json = Json.Default
)
Then, use it as KVS Cache.
Use it as KVS Storage with no expiration.
Property Delegation
KottageStorage provides property delegate.
val storage: KottageStorage = kottage.storage("app_configs")
val myConfig: String by storage.property { "default value" }
val myConfigNullable: String? by storage.nullableProperty()
myConfig.write("value")
val config: String = myConfig.read()
For example, this is strictly typed data access class:
class AppConfiguration(kottage: Kottage) {
private val storage: KottageStorage = kottage.storage("app_configs")
val myConfig: String by storage.property { "default value" }
val myConfigNullable: String? by storage.nullableProperty()
}
val configuration: AppConfiguration = AppConfiguration(kottage)
configuration.myConfig.write("value")
val config: String = configuration.myConfig.read()
List / Paging
Kottage has a List feature for make Paging UIs and for Single Source of Truth.
Serialization
Kottage can store and restore Serializable classes.
@Serializable
data class MyData( myValue: )
: MyData = MyData()
list: List<String> = listOf(, )
cache: KottageStorage = kottage.cache()
cache.put(, )
cache.put(, list)
storedData: MyData = cache.<MyData>()
storedList: List<String> = cache.<List<String>>()
Type mismatch error
Store and restore works correctly with same type. It throws ClassCastException if restore with wrong
types.
val cache: KottageStorage = kottage.cache("type_items")
cache.put("item1", 0)
cache.put("item2", "strings")
cache.get<String>("item1")
cache.get<Int>("item2")
Serializable types are stored as String. It throws SerializationException if restore with wrong
types.
@Serializable
data class Data(val data: Int)
( data2: )
cache: KottageStorage = kottage.cache()
cache.put(, Data())
cache.<String>()
cache.<Data2>()
Event Observing
Kottage supports observing events of item updates for implementing Single Source of Truth.
val cache: KottageStorage = kottage.cache("my_item_cache")
val now: Long = ...
launch {
cache.eventFlow(now).collect { event ->
val eventType: KottageEventType = event.eventType
updatedValue: String = cache.<String>(event.itemKey)
}
}
cache.put(, )
events: List<KottageEvent> = cache.getEvents(now)
updatedValue: String = cache.<String>(event.first().itemKey)
An eventFlow (KottageEventFlow) can automatically resume from previous emitted event.
For example, on Android platform, collect events while Lifecycle is at least STARTED.
val cache = kottage.cache("my_item_cache")
val now = ...
val eventFlow = cache.eventFlow(now)
...
override fun onCreate(...) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
eventFlow.collect { event ->
}
}
}
}
Encryption
User defined encryption are supported. Only KVS value part is encrypted, while other part (KVS
key, storage name...) remains plain data.
- Recommendation: Krypto is a cool library to use encryption
feature in Kotlin Multiplatform.
Supporting Data Types
- Primitives:
Double, Float, , , , ,
Multiplatform
Kottage is a Kotlin Multiplatform library. Please feel free to report a issue if it doesn't
work correctly on these platforms.
There is also Kottage for SwiftPM that is just for
experimental build.
Kotlin/JS (browser/nodejs) Support
Kottage supports Kottage/JS browser and nodejs. Kottage on browser uses IndexedDB as persistent
database instead of SQLite. Kottage on nodejs uses SQLite.
| Kotlin/JS type | Database |
|---|
| browser | IndexedDB |
| nodejs | SQLite |
Browsers will clear IndexedDB data when user's disk storage gets low disk space.
You can request browsers your IndexedDB's data not to be cleared by using StorageManager.persist()
.
See Web API documents for
more details.
Kotlin/JS browser Setup
Kotlin/JS's library contains both of implementations for browser and for nodejs, so additional
Webpack config is required for browser to use Kottage.
When you run jsBrowserRun (jsBrowserDevelopmentRun or jsBrowserProductionRun), some Webpack Errors
occurs:
Compiled with problems:X
WARNING in ../../node_modules/better-sqlite3/lib/database.js 50:10-81
Critical dependency: the request of a dependency is an expression
ERROR in ../../node_modules/better-sqlite3/lib/database.js 2:11-24
Module not found: Error: Can't resolve 'fs' in '(projectdir)/build/js/node_modules/better-sqlite3/lib'
ERROR in ../../node_modules/better-sqlite3/lib/database.js 3:13-28
Module not found: Error: Can't resolve 'path' in '(projectdir)/build/js/node_modules/better-sqlite3/lib'
BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.
If you want to include a polyfill, you need to:
- add a fallback 'resolve.fallback: { "path": require.resolve("path-browserify") }'
- install 'path-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
resolve.fallback: { "path": false }
...
To suppress this errors, add Webpack config file to project directory. This config will ignore
better-sqlite3 module that is not needed in browser application, and exclude packed files.
{youar application module path}/webpack.config.d/kottage.webpack.config.js:
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
os: false
}
config.externals = {
...config.externals,
"better-sqlite3": "better-sqlite3"
}
Sample application project is available in sample/js-browser.
Kotlin/JS nodejs Setup
Kottage uses better-sqlite3 on nodejs.
better-sqlite3 requires sqlite3 FFI file better_sqlite3.node on runtime environment.
If there is no FFI file, Could not locate the bindings file error occurred:
CoroutinesInternalError: Fatal exception in coroutines machinery for AwaitContinuation(DispatchedContinuation[NodeDispatcher@1, [object Object]]){Completed}@2. Please read KDoc to 'handleFatalException' method and report this incident to maintainers
at AwaitContinuation.DispatchedTask.handleFatalException_56zdfo_k$ ((projectdir)/DispatchedTask.kt:144:22)
at AwaitContinuation.DispatchedTask.run_mw4iiu_k$ ((projectdir)/DispatchedTask.kt:115:13)
at ScheduledMessageQueue.MessageQueue.process_mza50i_k$ ((projectdir)/JSDispatcher.kt:153:25)
at (projectdir)/JSDispatcher.kt:19:48
at processTicksAndRejections (node:internal/process/task_queues:77:11) {
cause: Error: Could not locate the bindings file. Tried:
→ (projectdir)/build/js/node_modules/better-sqlite3/build/better_sqlite3.node
→ (projectdir)/build/js/node_modules/better-sqlite3/build/Debug/better_sqlite3.node
→ (projectdir)/build/js/node_modules/better-sqlite3/build/Release/better_sqlite3.node
→ (projectdir)/build/js/node_modules/better-sqlite3/out/Debug/better_sqlite3.node
...
To prevent this error, you should build FFI file with a custom Gradle Task.
- Make sure your machine environment has
python3.
- Register
installBetterSqlite3 task. This task will
make {rootProject}/build/js/node_modules/better-sqlite3/build/Release/better_sqlite3.node.
{rootProject}/build.gradle.kts (sample build.gradle.kts is here)
{nodejs project}/build.gradle.kts
plugins {
kotlin("multiplatform")
}
kotlin {
js(IR) {
...
}
...
}
...
tasks.withType<NodeJsExec>().configureEach {
dependsOn(rootProject.tasks.named("installBetterSqlite3"))
}
...
- Then, execute
jsNodeRun (jsNodeDevelopmentRun or jsNodeProductionRun) task. There are no FFI
errors.
Kottage Internals
TBA: I'll write details of library here.
- Lazy item expiration.
- Automated clean up of expired item.
- Limit item counts.