Faster Event parsers

This commit is contained in:
Vitor Pamplona
2025-12-22 14:18:59 -05:00
parent 59413f3051
commit 10bba83ee3
14 changed files with 151 additions and 150 deletions
@@ -20,6 +20,8 @@
*/
package com.vitorpamplona.quartz.nip01Core.core
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
import kotlinx.serialization.json.Json
interface OptimizedSerializable
@@ -31,6 +33,10 @@ expect object OptimizedJsonMapper {
fun fromJsonToTagArray(json: String): Array<Array<String>>
fun fromJsonToEventTemplate(json: String): EventTemplate<Event>
fun fromJsonToRumor(json: String): Rumor
fun toJson(tags: Array<Array<String>>): String
inline fun <reified T : OptimizedSerializable> fromJsonTo(json: String): T
@@ -38,7 +38,7 @@ class EventTemplate<T : Event>(
fun toJson(): String = OptimizedJsonMapper.toJson(this)
companion object {
fun fromJson(json: String): EventTemplate<Event> = OptimizedJsonMapper.fromJsonTo<EventTemplate<Event>>(json)
fun fromJson(json: String): EventTemplate<Event> = OptimizedJsonMapper.fromJsonToEventTemplate(json)
}
}
@@ -34,6 +34,14 @@ class BunkerResponseEvent(
id: String,
result: String,
error: String? = null,
) = BunkerResponseEvent(id, Event.fromJson(result))
): BunkerResponseEvent {
val event = Event.fromJson(result)
if (event.sig.isEmpty()) {
throw IllegalStateException("Invalid event")
}
return BunkerResponseEvent(id, event)
}
}
}
@@ -20,6 +20,9 @@
*/
package com.vitorpamplona.quartz.nip01Core.core
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
actual object OptimizedJsonMapper {
actual fun fromJson(json: String): Event = TODO("Not yet implemented")
@@ -27,6 +30,10 @@ actual object OptimizedJsonMapper {
actual fun fromJsonToTagArray(json: String): Array<Array<String>> = TODO("Not yet implemented")
actual fun fromJsonToEventTemplate(json: String): EventTemplate<Event> = TODO("Not yet implemented")
actual fun fromJsonToRumor(json: String): Rumor = TODO("Not yet implemented")
actual fun toJson(tags: Array<Array<String>>): String = TODO("Not yet implemented")
actual inline fun <reified T : OptimizedSerializable> fromJsonTo(json: String): T = TODO("Not yet implemented")
@@ -21,6 +21,8 @@
package com.vitorpamplona.quartz.nip01Core.core
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
actual object OptimizedJsonMapper {
inline fun <T> runCatching(parsingAction: () -> T): T =
@@ -37,6 +39,10 @@ actual object OptimizedJsonMapper {
actual fun fromJsonToTagArray(json: String): Array<Array<String>> = runCatching { JacksonMapper.fromJsonToTagArray(json) }
actual fun fromJsonToRumor(json: String): Rumor = runCatching { JacksonMapper.fromJsonToRumor(json) }
actual fun fromJsonToEventTemplate(json: String): EventTemplate<Event> = runCatching { JacksonMapper.fromJsonToEventTemplate(json) }
actual fun toJson(tags: Array<Array<String>>): String = JacksonMapper.toJson(tags)
actual inline fun <reified T : OptimizedSerializable> fromJsonTo(json: String): T = runCatching { JacksonMapper.fromJsonTo<T>(json) }
@@ -21,13 +21,49 @@
package com.vitorpamplona.quartz.nip01Core.jackson
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.utils.EventFactory
class EventDeserializer : StdDeserializer<Event>(Event::class.java) {
val tagsDeserializer = TagArrayDeserializer()
val emptyArray = emptyArray<Array<String>>()
override fun deserialize(
jp: JsonParser,
p: JsonParser,
ctxt: DeserializationContext,
): Event = EventManualDeserializer.fromJson(jp.codec.readTree(jp))
): Event {
var id: HexKey = ""
var pubKey: HexKey = ""
var createdAt: Long = 0
var kind: Kind = 0
var tags: TagArray = emptyArray
var content: String = ""
var sig: HexKey = ""
while (p.nextToken() != JsonToken.END_OBJECT) {
val fieldName = p.currentName()
p.nextToken()
when (fieldName.hashCode()) {
3355 -> id = p.text.intern()
-977424830 -> pubKey = p.text.intern()
1369680106 -> createdAt = p.longValue
3292052 -> kind = p.intValue
3552281 -> tags = tagsDeserializer.deserialize(p, ctxt)
951530617 -> content = p.text
113873 -> sig = p.text
else -> p.skipChildren()
}
}
// NPE on purpose. If the object isn't fully filled, it should throw.
return EventFactory.create(id, pubKey, createdAt, kind, tags, content, sig)
}
}
@@ -1,40 +0,0 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.jackson
import com.fasterxml.jackson.databind.JsonNode
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.utils.EventFactory
class EventManualDeserializer {
companion object {
fun fromJson(jsonObject: JsonNode): Event =
EventFactory.create(
id = jsonObject.get("id").asText().intern(),
pubKey = jsonObject.get("pubkey").asText().intern(),
createdAt = jsonObject.get("created_at").asLong(),
kind = jsonObject.get("kind").asInt(),
tags = TagArrayManualDeserializer.fromJson(jsonObject.get("tags")),
content = jsonObject.get("content").asText(),
sig = jsonObject.get("sig").asText(),
)
}
}
@@ -22,11 +22,12 @@ package com.vitorpamplona.quartz.nip01Core.jackson
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable
@@ -58,6 +59,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.jackson.ResponseDeserializer
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorDeserializer
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorSerializer
import kotlinx.serialization.json.JsonNull.content
import java.io.InputStream
class JacksonMapper {
@@ -67,6 +69,8 @@ class JacksonMapper {
val mapper =
jacksonObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, false)
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, false)
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
.setDefaultPrettyPrinter(defaultPrettyPrinter)
.registerModule(
@@ -98,11 +102,21 @@ class JacksonMapper {
.addDeserializer(BunkerResponse::class.java, BunkerResponseDeserializer()),
)
fun fromJson(json: String): Event = mapper.readValue<Event>(json)
/**
* Shortcuts
*/
val eventTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef<Event>())
val tagArrayTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef<TagArray>())
val rumorTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef<Rumor>())
val eventTemplateTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRef<EventTemplate<Event>>())
fun fromJson(json: JsonNode): Event = EventManualDeserializer.fromJson(json)
fun fromJson(json: String): Event = mapper.readValue(json, eventTypeInstance)
fun fromJsonToTagArray(json: String): TagArray = mapper.readValue<TagArray>(json)
fun fromJsonToTagArray(json: String): TagArray = mapper.readValue(json, tagArrayTypeInstance)
fun fromJsonToRumor(json: String): Rumor = mapper.readValue(json, rumorTypeInstance)
fun fromJsonToEventTemplate(json: String): EventTemplate<Event> = mapper.readValue(json, eventTemplateTypeInstance)
inline fun <reified T : OptimizedSerializable> fromJsonTo(json: String): T = mapper.readValue<T>(json)
@@ -1,39 +0,0 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.jackson
import com.fasterxml.jackson.databind.JsonNode
import com.vitorpamplona.quartz.nip01Core.core.TagArray
class TagArrayManualDeserializer {
companion object {
fun fromJson(tagArray: JsonNode): TagArray =
tagArray.toTypedArray { tag ->
tag.toTypedArray { s ->
if (s.isNull) {
""
} else {
s.asText().intern()
}
}
}
}
}
@@ -25,9 +25,11 @@ import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip01Core.jackson.EventManualDeserializer
import com.vitorpamplona.quartz.nip01Core.jackson.EventDeserializer
class MessageDeserializer : StdDeserializer<Message>(Message::class.java) {
val eventDeserializer = EventDeserializer()
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
@@ -42,11 +44,10 @@ class MessageDeserializer : StdDeserializer<Message>(Message::class.java) {
EventMessage.LABEL -> {
val subId = jp.nextTextValue()
jp.nextToken()
val event: JsonNode = jp.codec.readTree(jp)
EventMessage(
subId = subId,
event = EventManualDeserializer.fromJson(event),
event = eventDeserializer.deserialize(jp, ctxt),
)
}
@@ -23,15 +23,16 @@ package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.node.ObjectNode
import com.vitorpamplona.quartz.nip01Core.jackson.EventManualDeserializer
import com.vitorpamplona.quartz.nip01Core.jackson.EventDeserializer
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.filters.ManualFilterDeserializer
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
class CommandDeserializer : StdDeserializer<Command>(Command::class.java) {
val eventDeserializer = EventDeserializer()
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
@@ -77,10 +78,9 @@ class CommandDeserializer : StdDeserializer<Command>(Command::class.java) {
EventCmd.LABEL -> {
jp.nextToken()
val event: JsonNode = jp.codec.readTree(jp)
EventCmd(
event = EventManualDeserializer.fromJson(event),
event = eventDeserializer.deserialize(jp, ctxt),
)
}
@@ -91,9 +91,8 @@ class CommandDeserializer : StdDeserializer<Command>(Command::class.java) {
AuthCmd.LABEL -> {
jp.nextToken()
val event: JsonNode = jp.codec.readTree(jp)
AuthCmd(
event = EventManualDeserializer.fromJson(event) as RelayAuthEvent,
event = eventDeserializer.deserialize(jp, ctxt) as RelayAuthEvent,
)
}
@@ -21,13 +21,38 @@
package com.vitorpamplona.quartz.nip01Core.signers
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.jackson.TagArrayDeserializer
class EventTemplateDeserializer : StdDeserializer<EventTemplate<Event>>(EventTemplate::class.java) {
val tagsDeserializer = TagArrayDeserializer()
val emptyArray = emptyArray<Array<String>>()
override fun deserialize(
jp: JsonParser,
p: JsonParser,
ctxt: DeserializationContext,
): EventTemplate<Event> = EventTemplateManualDeserializer.fromJson(jp.codec.readTree(jp))
): EventTemplate<Event> {
var createdAt = 0L
var kind = 0
var tags = emptyArray
var content = ""
while (p.nextToken() != JsonToken.END_OBJECT) {
val fieldName = p.currentName()
p.nextToken()
when (fieldName.hashCode()) {
1369680106 -> createdAt = p.longValue
3292052 -> kind = p.intValue
3552281 -> tags = tagsDeserializer.deserialize(p, ctxt)
951530617 -> content = p.text
else -> p.skipChildren()
}
}
return EventTemplate(createdAt, kind, tags, content)
}
}
@@ -1,37 +0,0 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.signers
import com.fasterxml.jackson.databind.JsonNode
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.jackson.TagArrayManualDeserializer
class EventTemplateManualDeserializer {
companion object {
fun fromJson(jsonObject: JsonNode): EventTemplate<Event> =
EventTemplate(
createdAt = jsonObject.get("created_at").asLong(),
kind = jsonObject.get("kind").asInt(),
tags = TagArrayManualDeserializer.fromJson(jsonObject.get("tags")),
content = jsonObject.get("content").asText(),
)
}
}
@@ -21,28 +21,43 @@
package com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip01Core.jackson.TagArrayManualDeserializer
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.jackson.TagArrayDeserializer
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
import com.vitorpamplona.quartz.utils.asIntOrNull
import com.vitorpamplona.quartz.utils.asLongOrNull
import com.vitorpamplona.quartz.utils.asTextOrNull
class RumorDeserializer : StdDeserializer<Rumor>(Rumor::class.java) {
val tagsDeserializer = TagArrayDeserializer()
override fun deserialize(
jp: JsonParser,
p: JsonParser,
ctxt: DeserializationContext,
): Rumor {
val jsonObject: JsonNode = jp.codec.readTree(jp)
return Rumor(
id = jsonObject.get("id")?.asTextOrNull()?.intern(),
pubKey = jsonObject.get("pubkey")?.asTextOrNull()?.intern(),
createdAt = jsonObject.get("created_at")?.asLongOrNull(),
kind = jsonObject.get("kind")?.asIntOrNull(),
tags = TagArrayManualDeserializer.fromJson(jsonObject.get("tags")),
content = jsonObject.get("content")?.asTextOrNull(),
)
var id: HexKey? = null
var pubKey: HexKey? = null
var createdAt: Long? = null
var kind: Int? = null
var tags: TagArray? = null
var content: String? = null
while (p.nextToken() != JsonToken.END_OBJECT) {
val fieldName = p.currentName()
p.nextToken()
when (fieldName.hashCode()) {
3355 -> id = p.text.intern()
-977424830 -> pubKey = p.text.intern()
1369680106 -> createdAt = p.longValue
3292052 -> kind = p.intValue
3552281 -> tags = tagsDeserializer.deserialize(p, ctxt)
951530617 -> content = p.text
else -> p.skipChildren()
}
}
return Rumor(id, pubKey, createdAt, kind, tags, content)
}
}