
Offers a persistent storage solution using LMDB for efficient key-value storage with ACID transactions, type-safe serialization, and shared environment patterns for state and saga management.
LMDB-backed StateStorage, VersionedStateStorage, and SagaStorage for duks 0.4+, plus a typed key-value helper.
dependencies {
implementation("io.github.crowded-libs:duks-storage-lmdb:0.3.0")
}Requires duks 0.4.0+. Apply the kotlinx.serialization plugin if you use jsonSerializer().
Open one LMDB Env and pass it to every storage instance. Each distinct databaseName needs a slot in maxDatabases (set before open).
import duks.storage.lmdb.jsonSerializer
import duks.storage.lmdb.LmdbStateStorage
import duks.storage.lmdb.Utf8StringSerializer
import duks.storage.lmdb.LmdbKeyValueStorage
import lmdb.Env
import kotlinx.serialization.Serializable
@Serializable
data class AppState(val counter: Int = 0, val message: String = "")
val env = Env()
env.mapSize = 10UL * 1024UL * 1024UL
env.maxDatabases = 8u
env.open("/path/to/database")
val stateStorage = LmdbStateStorage(
env = env,
serializer = jsonSerializer<AppState>(),
// databaseName = "state", stateKey = "app_state"
)
val stringKv = LmdbKeyValueStorage(
env = env,
serializer = Utf8StringSerializer,
databaseName = "strings",
)Storage types do not close the environment. After duks KStore.close() (or other teardown), call env.close(). Do not open multiple write environments on the same path.
import duks.createStore
import duks.storage.PersistenceStrategy
import duks.storage.lmdb.LmdbStateStorage
import duks.storage.lmdb.jsonSerializer
val store = createStore(AppState()) {
middleware {
exceptionHandling()
persistence(
storage = LmdbStateStorage(env, jsonSerializer<AppState>()),
strategy = PersistenceStrategy.Debounced(500),
)
// domain middleware, sagas, async
}
reduceWith(appReducer)
}
// store.close() then env.close() when disposing the storeLmdbVersionedStateStorage implements duks VersionedStateStorage. On disk, each value is a 4-byte big-endian version followed by the state payload from your Serializer. Unversioned save() keeps the previous version tag, or 0 if the key is empty.
That framing is not compatible with LmdbStateStorage bytes for the same key. Use a new databaseName/stateKey when adopting versioning, or migrate data yourself.
import duks.storage.MigratingStateStorage
import duks.storage.lmdb.LmdbVersionedStateStorage
import duks.storage.lmdb.jsonSerializer
val backend = LmdbVersionedStateStorage(
env = env,
serializer = jsonSerializer<AppState>(),
databaseName = "state_v",
)
val storage = MigratingStateStorage(
storage = backend,
currentVersion = 2,
migrations = mapOf(
0 to { s -> /* migrate v0 -> v1 */ s },
1 to { s -> /* migrate v1 -> v2 */ s },
),
)
// persistence(storage = storage, strategy = ...)LmdbSagaStorage implements duks SagaStorage. It requires:
Serializer<PersistedSagaInstance> for the envelope (id, name, serialized state string, timestamps, metadata). duks does not ship a kotlinx serializer for this type.SagaStateSerializer to convert domain saga state to/from the string stored in that envelope.import duks.storage.PersistedSagaInstance
import duks.storage.SagaPersistenceStrategy
import duks.storage.SagaStateSerializer
import duks.storage.lmdb.LmdbSagaStorage
import duks.storage.lmdb.Serializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
private data class PersistedSagaDto(
val id: String,
val sagaName: String,
val state: String,
val startedAt: Long,
val lastUpdatedAt: Long,
val metadata: Map<String, String> = emptyMap(),
)
val persistedSagaSerializer = object : Serializer<PersistedSagaInstance> {
private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }
override fun serialize(value: PersistedSagaInstance): ByteArray =
json.encodeToString(
PersistedSagaDto.serializer(),
PersistedSagaDto(
value.id, value.sagaName, value.state,
value.startedAt, value.lastUpdatedAt, value.metadata,
),
).encodeToByteArray()
override fun deserialize(bytes: ByteArray): PersistedSagaInstance {
val dto = json.decodeFromString(PersistedSagaDto.serializer(), bytes.decodeToString())
return PersistedSagaInstance(
dto.id, dto.sagaName, dto.state,
dto.startedAt, dto.lastUpdatedAt, dto.metadata,
)
}
}
// Map sagaName (and/or runtime type) to the correct domain state codec.
val sagaStateSerializer = object : SagaStateSerializer {
override fun serialize(state: Any): String = /* encode domain state */ error("implement")
override fun deserialize(data: String, sagaName: String): Any = /* decode */ error("implement")
}
val sagaStorage = LmdbSagaStorage(
env = env,
persistedSagaSerializer = persistedSagaSerializer,
sagaStateSerializer = sagaStateSerializer,
// databaseName = "sagas"
// metadataFor = { instance -> mapOf("name" to instance.sagaName) }
)
// In createStore { middleware { ... } }:
// sagas(
// storage = sagaStorage,
// persistenceStrategy = SagaPersistenceStrategy.Debounced(250),
// ) { register(MySaga()) }LmdbSagaStorageWithStateSerializer is deprecated; use LmdbSagaStorage.
interface Serializer<T> {
fun serialize(value: T): ByteArray
fun deserialize(bytes: ByteArray): T
}| Helper | Use |
|---|---|
jsonSerializer<T>() / JsonSerializer
|
kotlinx.serialization JSON; default prettyPrint = false, ignoreUnknownKeys = true, encodeDefaults = true
|
Utf8StringSerializer |
Raw UTF-8 strings (no JSON) |
Pass a custom Json { ... } into jsonSerializer(json) or JsonSerializer(json, kSerializer) when needed.
LmdbKeyValueStorage suspend methods: put, get, delete, exists, getAllKeys.
| Type | duks role | Defaults |
|---|---|---|
LmdbStateStorage |
StateStorage |
DB "state", key "app_state"
|
LmdbVersionedStateStorage |
VersionedStateStorage (use with MigratingStateStorage) |
DB "state", key "app_state"; framed payload |
LmdbSagaStorage |
SagaStorage |
DB "sagas"; requires envelope + SagaStateSerializer
|
LmdbKeyValueStorage |
(none) | caller supplies databaseName
|
JsonSerializer / jsonSerializer
|
(none) | compact JSON defaults |
Utf8StringSerializer |
(none) | UTF-8 string bytes |
JVM, Android, iOS (arm64, simulatorArm64), WasmJS.
Apache License 2.0
LMDB-backed StateStorage, VersionedStateStorage, and SagaStorage for duks 0.4+, plus a typed key-value helper.
dependencies {
implementation("io.github.crowded-libs:duks-storage-lmdb:0.3.0")
}Requires duks 0.4.0+. Apply the kotlinx.serialization plugin if you use jsonSerializer().
Open one LMDB Env and pass it to every storage instance. Each distinct databaseName needs a slot in maxDatabases (set before open).
import duks.storage.lmdb.jsonSerializer
import duks.storage.lmdb.LmdbStateStorage
import duks.storage.lmdb.Utf8StringSerializer
import duks.storage.lmdb.LmdbKeyValueStorage
import lmdb.Env
import kotlinx.serialization.Serializable
@Serializable
data class AppState(val counter: Int = 0, val message: String = "")
val env = Env()
env.mapSize = 10UL * 1024UL * 1024UL
env.maxDatabases = 8u
env.open("/path/to/database")
val stateStorage = LmdbStateStorage(
env = env,
serializer = jsonSerializer<AppState>(),
// databaseName = "state", stateKey = "app_state"
)
val stringKv = LmdbKeyValueStorage(
env = env,
serializer = Utf8StringSerializer,
databaseName = "strings",
)Storage types do not close the environment. After duks KStore.close() (or other teardown), call env.close(). Do not open multiple write environments on the same path.
import duks.createStore
import duks.storage.PersistenceStrategy
import duks.storage.lmdb.LmdbStateStorage
import duks.storage.lmdb.jsonSerializer
val store = createStore(AppState()) {
middleware {
exceptionHandling()
persistence(
storage = LmdbStateStorage(env, jsonSerializer<AppState>()),
strategy = PersistenceStrategy.Debounced(500),
)
// domain middleware, sagas, async
}
reduceWith(appReducer)
}
// store.close() then env.close() when disposing the storeLmdbVersionedStateStorage implements duks VersionedStateStorage. On disk, each value is a 4-byte big-endian version followed by the state payload from your Serializer. Unversioned save() keeps the previous version tag, or 0 if the key is empty.
That framing is not compatible with LmdbStateStorage bytes for the same key. Use a new databaseName/stateKey when adopting versioning, or migrate data yourself.
import duks.storage.MigratingStateStorage
import duks.storage.lmdb.LmdbVersionedStateStorage
import duks.storage.lmdb.jsonSerializer
val backend = LmdbVersionedStateStorage(
env = env,
serializer = jsonSerializer<AppState>(),
databaseName = "state_v",
)
val storage = MigratingStateStorage(
storage = backend,
currentVersion = 2,
migrations = mapOf(
0 to { s -> /* migrate v0 -> v1 */ s },
1 to { s -> /* migrate v1 -> v2 */ s },
),
)
// persistence(storage = storage, strategy = ...)LmdbSagaStorage implements duks SagaStorage. It requires:
Serializer<PersistedSagaInstance> for the envelope (id, name, serialized state string, timestamps, metadata). duks does not ship a kotlinx serializer for this type.SagaStateSerializer to convert domain saga state to/from the string stored in that envelope.import duks.storage.PersistedSagaInstance
import duks.storage.SagaPersistenceStrategy
import duks.storage.SagaStateSerializer
import duks.storage.lmdb.LmdbSagaStorage
import duks.storage.lmdb.Serializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
private data class PersistedSagaDto(
val id: String,
val sagaName: String,
val state: String,
val startedAt: Long,
val lastUpdatedAt: Long,
val metadata: Map<String, String> = emptyMap(),
)
val persistedSagaSerializer = object : Serializer<PersistedSagaInstance> {
private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }
override fun serialize(value: PersistedSagaInstance): ByteArray =
json.encodeToString(
PersistedSagaDto.serializer(),
PersistedSagaDto(
value.id, value.sagaName, value.state,
value.startedAt, value.lastUpdatedAt, value.metadata,
),
).encodeToByteArray()
override fun deserialize(bytes: ByteArray): PersistedSagaInstance {
val dto = json.decodeFromString(PersistedSagaDto.serializer(), bytes.decodeToString())
return PersistedSagaInstance(
dto.id, dto.sagaName, dto.state,
dto.startedAt, dto.lastUpdatedAt, dto.metadata,
)
}
}
// Map sagaName (and/or runtime type) to the correct domain state codec.
val sagaStateSerializer = object : SagaStateSerializer {
override fun serialize(state: Any): String = /* encode domain state */ error("implement")
override fun deserialize(data: String, sagaName: String): Any = /* decode */ error("implement")
}
val sagaStorage = LmdbSagaStorage(
env = env,
persistedSagaSerializer = persistedSagaSerializer,
sagaStateSerializer = sagaStateSerializer,
// databaseName = "sagas"
// metadataFor = { instance -> mapOf("name" to instance.sagaName) }
)
// In createStore { middleware { ... } }:
// sagas(
// storage = sagaStorage,
// persistenceStrategy = SagaPersistenceStrategy.Debounced(250),
// ) { register(MySaga()) }LmdbSagaStorageWithStateSerializer is deprecated; use LmdbSagaStorage.
interface Serializer<T> {
fun serialize(value: T): ByteArray
fun deserialize(bytes: ByteArray): T
}| Helper | Use |
|---|---|
jsonSerializer<T>() / JsonSerializer
|
kotlinx.serialization JSON; default prettyPrint = false, ignoreUnknownKeys = true, encodeDefaults = true
|
Utf8StringSerializer |
Raw UTF-8 strings (no JSON) |
Pass a custom Json { ... } into jsonSerializer(json) or JsonSerializer(json, kSerializer) when needed.
LmdbKeyValueStorage suspend methods: put, get, delete, exists, getAllKeys.
| Type | duks role | Defaults |
|---|---|---|
LmdbStateStorage |
StateStorage |
DB "state", key "app_state"
|
LmdbVersionedStateStorage |
VersionedStateStorage (use with MigratingStateStorage) |
DB "state", key "app_state"; framed payload |
LmdbSagaStorage |
SagaStorage |
DB "sagas"; requires envelope + SagaStateSerializer
|
LmdbKeyValueStorage |
(none) | caller supplies databaseName
|
JsonSerializer / jsonSerializer
|
(none) | compact JSON defaults |
Utf8StringSerializer |
(none) | UTF-8 string bytes |
JVM, Android, iOS (arm64, simulatorArm64), WasmJS.
Apache License 2.0