package com.sptmobile.link import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json /** * One spool-drain wire row (`rust/link-proto` `SpoolEntry`, verbatim): an * inbound `` the host queued for this device. `kind` is the event * type attr (`msg`, `user-msg`, …); `from` is the sending endpoint, which * names the notification channel. */ @Serializable data class SpoolEntry( val msg_id: String? = null, val ts_ms: Long, val from: String, val kind: String, val body: String, val json: String? = null, ) // [impl->REQ-INBOUND-NOTIFS] /** * Pure half of the inbound-notification path: parse a drain reply, drop * re-deliveries. The host commits spool removal only AFTER its response * frame is on the wire (late-never-lost, ruling 7), so a link drop mid-drain * re-delivers the same batch — the phone dedups by msg-id, exact only * (the REQ-HAZARD-DUP-ROWS discipline: no fuzzy matching; rows without a * msg-id are never guessed at and always pass). Free of Android/JNI so the * dedup semantics are JVM-unit-testable. */ class SpoolInbox(capacity: Int = 512) { /** Insertion-ordered so overflow evicts the oldest ids first. */ private val seen = object : LinkedHashMap() { override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = size > capacity } /** Entries not yet notified about, in drain (FIFO) order. */ fun accept(entries: List): List = entries.filter { e -> val id = e.msg_id ?: return@filter true seen.put(id, Unit) == null } companion object { private val json = Json { ignoreUnknownKeys = true } fun parse(reply: String): List = json.decodeFromString(reply) } }