jsMain.com.ditchoom.mqtt3.persistence.IDBPersistence.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mqtt-4-models Show documentation
Show all versions of mqtt-4-models Show documentation
Defines the MQTT 3 and 4 control packets
package com.ditchoom.mqtt3.persistence
import com.ditchoom.mqtt.Persistence
import com.ditchoom.mqtt.connection.MqttBroker
import com.ditchoom.mqtt.connection.MqttConnectionOptions
import com.ditchoom.mqtt.controlpacket.ControlPacket
import com.ditchoom.mqtt.controlpacket.IConnectionRequest
import com.ditchoom.mqtt.controlpacket.IPublishAcknowledgment
import com.ditchoom.mqtt.controlpacket.IPublishComplete
import com.ditchoom.mqtt.controlpacket.IPublishMessage
import com.ditchoom.mqtt.controlpacket.IPublishReceived
import com.ditchoom.mqtt.controlpacket.IPublishRelease
import com.ditchoom.mqtt.controlpacket.ISubscribeAcknowledgement
import com.ditchoom.mqtt.controlpacket.ISubscribeRequest
import com.ditchoom.mqtt.controlpacket.ISubscription
import com.ditchoom.mqtt.controlpacket.IUnsubscribeAcknowledgment
import com.ditchoom.mqtt.controlpacket.IUnsubscribeRequest
import com.ditchoom.mqtt.controlpacket.QualityOfService
import com.ditchoom.mqtt.controlpacket.Topic
import com.ditchoom.mqtt3.controlpacket.ConnectionRequest
import com.ditchoom.mqtt3.controlpacket.PublishComplete
import com.ditchoom.mqtt3.controlpacket.PublishMessage
import com.ditchoom.mqtt3.controlpacket.PublishReceived
import com.ditchoom.mqtt3.controlpacket.PublishRelease
import com.ditchoom.mqtt3.controlpacket.SubscribeRequest
import com.ditchoom.mqtt3.controlpacket.UnsubscribeRequest
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import web.errors.DOMException
import web.idb.IDBDatabase
import web.idb.IDBFactory
import web.idb.IDBKeyRange
import web.idb.IDBRequest
import web.idb.IDBTransaction
import web.idb.IDBTransactionMode
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class IDBPersistence(private val db: IDBDatabase) : Persistence {
private val dispatcher = defaultDispatcher(0, "unused")
override suspend fun ackPub(broker: MqttBroker, packet: IPublishAcknowledgment) {
val tx = db.transaction(PubMsg, IDBTransactionMode.readwrite)
val queuedMsgStore = tx.objectStore(PubMsg)
request { queuedMsgStore.delete(arrayOf(broker.identifier, packet.packetIdentifier, 0)) }
commitTransaction(tx, "ackPub")
}
override suspend fun ackPubComplete(broker: MqttBroker, packet: IPublishComplete) {
val tx = db.transaction(QoS2Msg, IDBTransactionMode.readwrite)
val qos2MsgStore = tx.objectStore(QoS2Msg)
qos2MsgStore.delete(arrayOf(broker.identifier, packet.packetIdentifier, 1))
commitTransaction(tx, "ackPubComplete")
}
override suspend fun ackPubReceivedQueuePubRelease(
broker: MqttBroker,
incomingPubRecv: IPublishReceived,
pubRel: IPublishRelease
) {
val tx = db.transaction(arrayOf(PubMsg, QoS2Msg), IDBTransactionMode.readwrite)
val queuedMsgStore = tx.objectStore(PubMsg)
val qos2MsgStore = tx.objectStore(QoS2Msg)
queuedMsgStore.delete(arrayOf(broker.identifier, incomingPubRecv.packetIdentifier, 0))
qos2MsgStore.put(
PersistableQos2Message(
broker.identifier,
pubRel.packetIdentifier,
pubRel.controlPacketValue,
1
)
)
commitTransaction(tx, "ackPubReceivedQueuePubRelease")
}
override suspend fun ackPubRelease(
broker: MqttBroker,
incomingPubRel: IPublishRelease,
outPubComp: IPublishComplete
) {
val tx = db.transaction(QoS2Msg, IDBTransactionMode.readwrite)
val qos2MsgStore = tx.objectStore(QoS2Msg)
qos2MsgStore.put(
PersistableQos2Message(
broker.identifier,
outPubComp.packetIdentifier,
outPubComp.controlPacketValue,
0
)
)
commitTransaction(tx, "ackPubRelease")
}
override suspend fun ackSub(broker: MqttBroker, subAck: ISubscribeAcknowledgement) {
val tx = db.transaction(SubMsg, IDBTransactionMode.readwrite)
val subMsgStore = tx.objectStore(SubMsg)
subMsgStore.delete(arrayOf(broker.identifier, subAck.packetIdentifier))
commitTransaction(tx, "ackSub")
}
override suspend fun ackUnsub(broker: MqttBroker, unsubAck: IUnsubscribeAcknowledgment) {
val key = arrayOf(broker.identifier, unsubAck.packetIdentifier)
val tx = db.transaction(arrayOf(UnsubMsg, Subscription), IDBTransactionMode.readwrite)
val unsubMsgStore = tx.objectStore(UnsubMsg)
unsubMsgStore.delete(arrayOf(broker.identifier, unsubAck.packetIdentifier))
val subStore = tx.objectStore(Subscription)
val unsubIndex = subStore.index(UnsubIndex)
val unsubSubscriptions = request { unsubIndex.getAll(key) }
for (unsubscription in unsubSubscriptions) {
val d = unsubscription.asDynamic()
PersistableUnsubscribe(d.brokerId.unsafeCast(), unsubAck.packetIdentifier)
subStore.delete(arrayOf(broker.identifier, d.topicFilter))
}
commitTransaction(tx, "ackUnsub")
}
override suspend fun activeSubscriptions(
broker: MqttBroker,
includePendingUnsub: Boolean
): Map {
val tx = db.transaction(Subscription, IDBTransactionMode.readonly)
val subStore = tx.objectStore(Subscription)
val index = subStore.index(BrokerIndex)
val subscriptionsRaw = request { index.getAll(broker.identifier) }
commitTransaction(tx, "activeSubscriptions")
return subscriptionsRaw
.map {
val d = it.asDynamic()
PersistableSubscription(
d.brokerId as Int,
d.topicFilter as String,
d.subscribeId as Int,
d.unsubscribeId as Int,
d.qos as Byte
)
}
.filter {
if (includePendingUnsub) {
it.unsubscribeId > -1
} else {
true
}
}
.map { toSubscription(it) }
.associateBy { it.topicFilter }
}
override suspend fun addBroker(
connectionOps: Collection,
connectionRequest: IConnectionRequest
): MqttBroker {
val tx = db.transaction(Broker, IDBTransactionMode.readwrite)
val store = tx.objectStore(Broker)
val connections = PersistableSocketConnection.from(connectionOps)
val persistableRequest = PersistableConnectionRequest.from(connectionRequest as ConnectionRequest)
val countOp = request { store.count() }
val broker = PersistableBroker(countOp, connections, persistableRequest)
store.put(broker)
commitTransaction(tx, "addBroker")
return MqttBroker(countOp.unsafeCast(), connectionOps, connectionRequest)
}
private suspend fun request(block: () -> IDBRequest): T = withContext(dispatcher) {
suspendCoroutine { cont ->
val op = block()
op.onsuccess = {
cont.resume(op.result)
}
op.onerror = {
console.error("request error, cast throwable", it)
cont.resumeWithException(op.error as Throwable)
}
}
}
private suspend fun commitTransaction(tx: IDBTransaction, logName: String) = withContext(dispatcher) {
suspendCancellableCoroutine { cont ->
tx.oncomplete = {
cont.resume(Unit)
}
tx.onerror = {
console.error("error committing tx $logName", tx, it)
cont.resumeWithException(tx.error as Throwable)
}
tx.onabort = {
console.error("abort committing tx $logName", tx, it)
cont.resumeWithException(tx.error as Throwable)
}
cont.invokeOnCancellation {
if (!cont.isCompleted) {
tx.abort()
}
}
tx.commit()
}
}
override suspend fun brokerWithId(identifier: Int): MqttBroker? {
val tx = db.transaction(Broker, IDBTransactionMode.readonly)
try {
val store = tx.objectStore(Broker)
val result = request { store.get(arrayOf(identifier)) } ?: return null
val d = result.asDynamic()
return MqttBroker(
d.id as Int,
(d.connectionOptions as Array<*>).map { toSocketConnection(it) }.toSet(),
toConnectionRequest(d.connectionRequest)
)
} finally {
commitTransaction(tx, "broker v4 $identifier")
}
}
override suspend fun allBrokers(): Collection {
val tx = db.transaction(Broker, IDBTransactionMode.readonly)
try {
val store = tx.objectStore(Broker)
val results = request { store.getAll() }
if (results.isEmpty()) {
return emptyList()
}
return results.toList().map { persistableBroker ->
val d = persistableBroker.asDynamic()
MqttBroker(
d.id as Int,
(d.connectionOptions as Array<*>).map { toSocketConnection(it) }.toSet(),
toConnectionRequest(d.connectionRequest)
)
}
} finally {
try {
commitTransaction(tx, "allBrokers v4")
} catch (e: DOMException) {
// ignore. happens when it's an empty list
}
}
}
override suspend fun clearMessages(broker: MqttBroker) {
val tx = db.transaction(arrayOf(PubMsg, PacketId), IDBTransactionMode.readwrite)
val queued = tx.objectStore(PubMsg)
val packet = tx.objectStore(PacketId)
queued.delete(broker.identifier)
packet.delete(broker.identifier)
commitTransaction(tx, "clearMessages")
}
override suspend fun incomingPublish(broker: MqttBroker, packet: IPublishMessage, replyMessage: ControlPacket) {
if (packet.qualityOfService != QualityOfService.EXACTLY_ONCE) {
return
}
val tx = db.transaction(QoS2Msg, IDBTransactionMode.readwrite)
val qos2MsgStore = tx.objectStore(QoS2Msg)
qos2MsgStore.put(
PersistableQos2Message(
broker.identifier,
replyMessage.packetIdentifier,
replyMessage.controlPacketValue,
0
)
)
commitTransaction(tx, "incomingPublish")
}
override suspend fun messagesToSendOnReconnect(broker: MqttBroker): Collection {
val tx = db.transaction(arrayOf(PubMsg, QoS2Msg, SubMsg, UnsubMsg, Subscription), IDBTransactionMode.readonly)
val queuedMsgStore = tx.objectStore(PubMsg)
val qos2Store = tx.objectStore(QoS2Msg)
val subStore = tx.objectStore(SubMsg)
val subscriptionStore = tx.objectStore(Subscription)
val unsubStore = tx.objectStore(UnsubMsg)
val pub = request { queuedMsgStore.index(BrokerIncomingIndex).getAll(arrayOf(broker.identifier, 0)) }
val qos2Persistable = request { qos2Store.index(BrokerIndex).getAll(broker.identifier) }
val subscribeRequests = request { subStore.index(BrokerIndex).getAll(broker.identifier) }
val subIndex = subscriptionStore.index(AllSubIndex)
val retrievedSubscribeRequests = if (subscribeRequests.isNotEmpty()) {
subscribeRequests
.map {
PersistableSubscribe(it.asDynamic().brokerId as Int, it.asDynamic().packetId as Int)
}
.map { persistableSubscribe ->
val subscriptions =
request { subIndex.getAll(arrayOf(broker.identifier, persistableSubscribe.packetId)) }
.map { toSubscription(it.unsafeCast()) }
SubscribeRequest(persistableSubscribe.packetId, subscriptions.toSet())
}
} else {
emptyList()
}
val unsubscribeRequests = request { unsubStore.index(BrokerIndex).getAll(broker.identifier) }
val unsubIndex = subscriptionStore.index(UnsubIndex)
val unsubs = unsubscribeRequests
.map { unsubscribeRequestObject ->
val packetId = unsubscribeRequestObject.asDynamic().packetId.unsafeCast()
val topics =
request { unsubIndex.getAll(arrayOf(broker.identifier, packetId)) }
.map { it.asDynamic().topicFilter.toString() }
UnsubscribeRequest(packetId, topics)
}
commitTransaction(tx, "messagesToSendOnReconnect")
val pubs = pub.map { toPub(it.unsafeCast()).setDupFlagNewPubMessage() }
val qos2 = qos2Persistable.map {
val dynamicIt = it.asDynamic()
val msg = PersistableQos2Message(
dynamicIt.brokerId as Int,
dynamicIt.packetId as Int,
dynamicIt.type as Byte,
dynamicIt.incoming as Int
)
when (msg.type) {
IPublishReceived.controlPacketValue -> PublishReceived(msg.packetId)
IPublishRelease.controlPacketValue -> PublishRelease(msg.packetId)
IPublishComplete.controlPacketValue -> PublishComplete(msg.packetId)
else -> error("IDB Persistence failed to get a valid qos 2 type")
}
}
return (pubs + retrievedSubscribeRequests + unsubs + qos2).sortedBy { it.packetIdentifier }
}
override suspend fun onPubCompWritten(broker: MqttBroker, outPubComp: IPublishComplete) {
val tx = db.transaction(QoS2Msg, IDBTransactionMode.readwrite)
val queuedMsgStore = tx.objectStore(QoS2Msg)
queuedMsgStore.delete(arrayOf(broker.identifier, outPubComp.packetIdentifier, 0))
commitTransaction(tx, "onPubCompWritten")
}
override suspend fun removeBroker(identifier: Int) {
val tx = db.transaction(
arrayOf(Broker, PacketId, PubMsg, Subscription, QoS2Msg, SubMsg, UnsubMsg),
IDBTransactionMode.readwrite
)
val packetIdStore = tx.objectStore(PacketId)
val queuedMsgStore = tx.objectStore(PubMsg)
val subscriptionStore = tx.objectStore(Subscription)
val brokerStore = tx.objectStore(Broker)
val qos2Store = tx.objectStore(QoS2Msg)
val subStore = tx.objectStore(SubMsg)
val unsubStore = tx.objectStore(UnsubMsg)
val key = IDBKeyRange.only(arrayOf(identifier))
packetIdStore.delete(key)
queuedMsgStore.delete(key)
subscriptionStore.delete(key)
brokerStore.delete(key)
qos2Store.delete(key)
subStore.delete(key)
unsubStore.delete(key)
commitTransaction(tx, "removeBroker")
}
override suspend fun writePubGetPacketId(broker: MqttBroker, pub: IPublishMessage): Int {
val tx = db.transaction(arrayOf(PacketId, PubMsg), IDBTransactionMode.readwrite)
val newPacketId = getAndIncrementPacketId(tx, broker)
val queuedMsgStore = tx.objectStore(PubMsg)
val packetIdPub = pub.maybeCopyWithNewPacketIdentifier(newPacketId) as PublishMessage
val persistablePub = PersistablePublishMessage(broker.identifier, false, packetIdPub)
queuedMsgStore.put(persistablePub)
commitTransaction(tx, "writePubGetPacketId")
return newPacketId
}
override suspend fun getPubWithPacketId(broker: MqttBroker, packetId: Int): IPublishMessage? {
val tx = db.transaction(arrayOf(PubMsg), IDBTransactionMode.readonly)
try {
val queuedMsgStore = tx.objectStore(PubMsg)
if (request { queuedMsgStore.count(arrayOf(broker.identifier, packetId, 0)) } == 0) {
return null
}
val pub = request { queuedMsgStore.get(arrayOf(broker.identifier, packetId, 0)) }
return toPub(pub.unsafeCast())
} finally {
commitTransaction(tx, "getPubWithPacketId")
}
}
private suspend fun getAndIncrementPacketId(tx: IDBTransaction, broker: MqttBroker): Int {
val packetIdStore = tx.objectStore(PacketId)
val brokerIdKey = IDBKeyRange.only(broker.identifier)
val result = request { packetIdStore.get(brokerIdKey) }
val value = if (result == undefined) {
1
} else {
result.unsafeCast()
}
val next = value.toString().toInt() + 1
request { packetIdStore.put(next, broker.identifier) }
return value.toString().toInt()
}
override suspend fun writeSubUpdatePacketIdAndSimplifySubscriptions(
broker: MqttBroker,
sub: ISubscribeRequest
): ISubscribeRequest {
val tx = db.transaction(arrayOf(PacketId, SubMsg, Subscription), IDBTransactionMode.readwrite)
val newPacketId = getAndIncrementPacketId(tx, broker)
val subMsgStore = tx.objectStore(SubMsg)
val newSub = sub.copyWithNewPacketIdentifier(newPacketId)
val persistableSubscribe = PersistableSubscribe(broker.identifier, newSub.packetIdentifier)
subMsgStore.add(persistableSubscribe)
val subStore = tx.objectStore(Subscription)
for (subscription in sub.subscriptions) {
subStore.add(PersistableSubscription(broker.identifier, newPacketId, subscription))
}
commitTransaction(tx, "writeSubUpdatePacketIdAndSimplifySubscriptions")
return newSub
}
override suspend fun getSubWithPacketId(broker: MqttBroker, packetId: Int): ISubscribeRequest? {
val tx = db.transaction(arrayOf(SubMsg, Subscription), IDBTransactionMode.readonly)
try {
val subStore = tx.objectStore(SubMsg)
if (request { subStore.count(arrayOf(broker.identifier, packetId)) } == 0) {
return null
}
val subscriptionStore = tx.objectStore(Subscription)
val subIndex = subscriptionStore.index(AllSubIndex)
val obj = request { subStore.get(arrayOf(broker.identifier, packetId)) }
val persistableSubscribe =
PersistableSubscribe(obj.asDynamic().brokerId as Int, obj.asDynamic().packetId as Int)
val subscriptions =
request { subIndex.getAll(arrayOf(broker.identifier, persistableSubscribe.packetId)) }
.map { toSubscription(it.unsafeCast()) }
return SubscribeRequest(persistableSubscribe.packetId, subscriptions.toSet())
} finally {
commitTransaction(tx, "getSubWithPacketId")
}
}
override suspend fun writeUnsubGetPacketId(broker: MqttBroker, unsub: IUnsubscribeRequest): Int {
val tx = db.transaction(arrayOf(PacketId, UnsubMsg, Subscription), IDBTransactionMode.readwrite)
val newPacketId = getAndIncrementPacketId(tx, broker)
val newUnsub = unsub.copyWithNewPacketIdentifier(newPacketId)
val persistableUnsub = PersistableUnsubscribe(broker.identifier, newUnsub as UnsubscribeRequest)
val unsubMsgStore = tx.objectStore(UnsubMsg)
unsubMsgStore.put(persistableUnsub)
val subscriptions = tx.objectStore(Subscription)
val persistableSubscriptions = unsub.topics.map {
val persistableSubscription = request { subscriptions.get(arrayOf(broker.identifier, it.toString())) }
PersistableSubscription(
persistableSubscription.asDynamic().brokerId as Int,
persistableSubscription.asDynamic().topicFilter as String,
persistableSubscription.asDynamic().subscribeId as Int,
newPacketId,
persistableSubscription.asDynamic().qos as Byte
)
}
persistableSubscriptions.forEach {
subscriptions.put(it)
}
commitTransaction(tx, "writeUnsubGetPacketId")
return newPacketId
}
override suspend fun getUnsubWithPacketId(broker: MqttBroker, packetId: Int): IUnsubscribeRequest? {
val tx = db.transaction(arrayOf(UnsubMsg, Subscription), IDBTransactionMode.readonly)
try {
val unsubStore = tx.objectStore(UnsubMsg)
if (request { unsubStore.count(arrayOf(broker.identifier, packetId)) } == 0) {
return null
}
val subStore = tx.objectStore(Subscription)
val unsubIndex = subStore.index(UnsubIndex)
val topics =
request { unsubIndex.getAll(arrayOf(broker.identifier, packetId)) }
.map { it.asDynamic().topicFilter.toString() }
return UnsubscribeRequest(packetId, topics)
} finally {
commitTransaction(tx, "getUnsubWithPacketId")
}
}
override suspend fun isQueueClear(broker: MqttBroker, includeSubscriptions: Boolean): Boolean {
val packets = messagesToSendOnReconnect(broker)
if (packets.isNotEmpty()) {
println(packets.joinToString())
return false
}
return true
}
companion object {
private const val Broker = "Broker"
private const val BrokerIndex = "BrokerId"
private const val BrokerIncomingIndex = "brokerIncomingIndex"
private const val PacketId = "PacketId"
private const val PubMsg = "PubMsg"
private const val Subscription = "Subscription"
private const val QoS2Msg = "QoS2Msg"
private const val SubMsg = "SubMsg"
private const val SubIndex = "sub"
private const val AllSubIndex = "allSub"
private const val UnsubMsg = "UnsubMsg"
private const val UnsubIndex = "unsub"
private const val AllUnsubIndex = "allUnsub"
suspend fun idbPersistence(indexedDb: IDBFactory, name: String): IDBPersistence {
val database = suspendCoroutine { cont ->
val openRequest = indexedDb.open(name, 1)
openRequest.onsuccess = {
cont.resume(openRequest.result)
}
openRequest.onupgradeneeded = {
val db = openRequest.result
db.createObjectStore(Broker, js("{ keyPath: [\"id\"] }"))
db.createObjectStore(PacketId)
val pubStore =
db.createObjectStore(PubMsg, js("{ keyPath: [\"brokerId\", \"packetId\", \"incoming\"] }"))
val subscriptionStore =
db.createObjectStore(Subscription, js("{ keyPath: [\"brokerId\", \"topicFilter\"] }"))
val qos2Store =
db.createObjectStore(QoS2Msg, js("{ keyPath: [\"brokerId\", \"packetId\", \"incoming\"] }"))
val subStore = db.createObjectStore(SubMsg, js("{ keyPath: [\"brokerId\", \"packetId\"] }"))
val unsubStore = db.createObjectStore(UnsubMsg, js("{ keyPath: [\"brokerId\", \"packetId\"] }"))
pubStore.createIndex(BrokerIncomingIndex, arrayOf("brokerId", "incoming"))
qos2Store.createIndex(BrokerIndex, "brokerId")
subStore.createIndex(BrokerIndex, "brokerId")
unsubStore.createIndex(BrokerIndex, "brokerId")
subscriptionStore.createIndex(BrokerIndex, "brokerId")
subscriptionStore.createIndex(SubIndex, arrayOf("brokerId", "topicFilter", "subscribeId"))
subscriptionStore.createIndex(AllSubIndex, arrayOf("brokerId", "subscribeId"))
subscriptionStore.createIndex(UnsubIndex, arrayOf("brokerId", "unsubscribeId"))
}
openRequest.onerror = {
console.error("open request error, cast throwable", it)
cont.resumeWithException(openRequest.error as Throwable)
}
}
return IDBPersistence(database)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy