Revert "fix: hold a payload whose parent has not arrived instead of losing the event"
This reverts commit d7aac49.
Reverting restores the defect it addressed: a payload referencing a row
the receiver does not have violates a foreign key, and SQLite aborts,
rolling back the whole inbound transaction -- the nostr event, the group
event, the submission and the transcript line, none of them retried.
That is what produced the observed `FOREIGN KEY constraint failed` on an
artifact whose dialect had not arrived.
Also drops the schema back to v5. Any device already migrated to v6 will
refuse to open its database, since the builder sets no destructive
fallback on downgrade; clear that app's data before installing a build
from this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -164,7 +164,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
|
||||
UnsignedNostrEvent::class,
|
||||
Zap::class
|
||||
],
|
||||
version = 6,
|
||||
version = 5,
|
||||
autoMigrations = [
|
||||
// v2 only adds the DkgSession/DkgParticipantMessage tables, so Room can
|
||||
// generate the migration itself — nothing existing changes shape.
|
||||
@@ -183,12 +183,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
|
||||
// nip30303 event a SubmissionEvent rumor carries. Rumors queued before
|
||||
// this come back null, which reads as "not a submission" -- correct,
|
||||
// since none of them were.
|
||||
AutoMigration(from = 4, to = 5),
|
||||
// v6 adds the nullable MarmotInnerEvent.awaitingEventId, holding a
|
||||
// submission that arrived before the event it references. Nothing
|
||||
// queued before this was ever held, so null is the right answer for
|
||||
// every existing row.
|
||||
AutoMigration(from = 5, to = 6)
|
||||
AutoMigration(from = 4, to = 5)
|
||||
]
|
||||
)
|
||||
@ColumnTypeConverters(MantraConverters::class)
|
||||
|
||||
@@ -15,9 +15,6 @@ interface MantraTranslationChunkDao {
|
||||
@Query("SELECT * FROM MantraTranslationChunk WHERE translationChapterId = :translationChapterId ORDER BY `index` ASC")
|
||||
suspend fun getTranslationChunksByTranslationChapterId(translationChapterId: String): List<MantraTranslationChunk>
|
||||
|
||||
@Query("SELECT * FROM MantraTranslationChunk WHERE id = :id")
|
||||
suspend fun getTranslationChunkById(id: String): MantraTranslationChunk?
|
||||
|
||||
@Query("DELETE FROM MantraTranslationChunk WHERE id = :id")
|
||||
suspend fun deleteById(id: String)
|
||||
}
|
||||
@@ -26,13 +26,4 @@ interface MarmotInnerEventDao {
|
||||
*/
|
||||
@Query("DELETE FROM MarmotInnerEvent WHERE payloadEventId = :payloadEventId")
|
||||
suspend fun deleteByPayloadEventId(payloadEventId: String)
|
||||
|
||||
/**
|
||||
* The submissions held back waiting for [eventId], oldest first.
|
||||
*
|
||||
* Oldest first because a backlog usually arrives in the order it was
|
||||
* written, so applying it that way unblocks the most in one pass.
|
||||
*/
|
||||
@Query("SELECT * FROM MarmotInnerEvent WHERE awaitingEventId = :eventId ORDER BY createdAt ASC")
|
||||
suspend fun getSubmissionsAwaiting(eventId: String): List<MarmotInnerEvent>
|
||||
}
|
||||
@@ -219,18 +219,19 @@ data class ChatMessage(
|
||||
}
|
||||
val payload = submission?.payload()
|
||||
|
||||
val innerEvent = MarmotInnerEvent(
|
||||
id = event.id,
|
||||
publicKey = event.pubKey,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
tags = event.tags,
|
||||
content = event.content,
|
||||
chatRoomId = groupEventResult.groupId,
|
||||
kind = event.kind,
|
||||
payloadEventId = payload?.id,
|
||||
createdAt = Instant.fromEpochSeconds(event.createdAt)
|
||||
database.marmotInnerEventDao().upsert(
|
||||
MarmotInnerEvent(
|
||||
id = event.id,
|
||||
publicKey = event.pubKey,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
tags = event.tags,
|
||||
content = event.content,
|
||||
chatRoomId = groupEventResult.groupId,
|
||||
kind = event.kind,
|
||||
payloadEventId = payload?.id,
|
||||
createdAt = Instant.fromEpochSeconds(event.createdAt)
|
||||
)
|
||||
)
|
||||
database.marmotInnerEventDao().upsert(innerEvent)
|
||||
|
||||
// A submission whose payload will not parse, or which carries
|
||||
// another submission, is kept but not applied: there is nothing
|
||||
@@ -248,13 +249,15 @@ data class ChatMessage(
|
||||
content = event.content,
|
||||
)
|
||||
} else {
|
||||
applyOrHold(
|
||||
applyInnerEvent(
|
||||
database = database,
|
||||
activeKeyPair = activeKeyPair,
|
||||
groupEvent = groupEvent,
|
||||
groupId = groupEventResult.groupId,
|
||||
innerEvent = innerEvent,
|
||||
event = payload ?: event,
|
||||
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
|
||||
marmotInnerEventId = event.id,
|
||||
senderPublicKey = event.pubKey,
|
||||
createdAt = Instant.fromEpochSeconds(event.createdAt),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -352,283 +355,14 @@ data class ChatMessage(
|
||||
* from [event], so the chat line says who added it and the row says who
|
||||
* wrote it.
|
||||
*/
|
||||
/**
|
||||
* Apply [event] now, or hold it until what it references turns up.
|
||||
*
|
||||
* Foreign keys mean a payload cannot become a row before its parent
|
||||
* does, and inserting one anyway does not fail politely: SQLite aborts
|
||||
* the statement, which rolls back the whole transaction the inbound
|
||||
* pipeline runs in -- losing the nostr event, the group event, this
|
||||
* submission and the transcript line with it, none of which is retried.
|
||||
*
|
||||
* So the parent is checked first. A payload that arrives early is kept
|
||||
* against the id it waits for and applied later, which is what makes
|
||||
* order stop mattering: an admin submitting a backlog can send it in
|
||||
* whatever order they hold it, and a member who joined last week can be
|
||||
* sent what the group was told last month.
|
||||
*
|
||||
* A held payload writes no chat line. Nobody said anything yet -- the
|
||||
* line appears when it is applied, in the transcript position its own
|
||||
* timestamp gives it.
|
||||
*/
|
||||
private suspend fun applyOrHold(
|
||||
database: MantraDatabase,
|
||||
activeKeyPair: KeyPair,
|
||||
groupId: String,
|
||||
innerEvent: MarmotInnerEvent,
|
||||
event: Event,
|
||||
isUserMessage: Boolean,
|
||||
): ChatMessage? {
|
||||
missingParentOf(database, event)?.let { missingParent ->
|
||||
database.marmotInnerEventDao().upsert(
|
||||
innerEvent.copy(awaitingEventId = missingParent)
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
val chatMessage = applyInnerEvent(
|
||||
database = database,
|
||||
groupId = groupId,
|
||||
event = event,
|
||||
marmotGroupEventId = innerEvent.marmotGroupEventId,
|
||||
marmotInnerEventId = innerEvent.id,
|
||||
senderPublicKey = innerEvent.publicKey,
|
||||
isUserMessage = isUserMessage,
|
||||
createdAt = innerEvent.createdAt,
|
||||
)
|
||||
|
||||
// This may be the parent something else was held for.
|
||||
releaseAwaiting(database, activeKeyPair, groupId, event.id)
|
||||
|
||||
return chatMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply whatever was waiting on [arrivedEventId], now that it is here.
|
||||
*
|
||||
* Releasing one can release another -- a version unblocks its chapters,
|
||||
* a chapter unblocks its chunks -- so this keeps going until nothing
|
||||
* more comes unstuck. A payload with a second parent still missing is
|
||||
* re-pointed at that one rather than applied, so it waits for the right
|
||||
* thing instead of being retried on every arrival.
|
||||
*/
|
||||
private suspend fun releaseAwaiting(
|
||||
database: MantraDatabase,
|
||||
activeKeyPair: KeyPair,
|
||||
groupId: String,
|
||||
arrivedEventId: HexKey,
|
||||
) {
|
||||
val arrived = ArrayDeque(listOf(arrivedEventId))
|
||||
|
||||
while (arrived.isNotEmpty()) {
|
||||
val parentId = arrived.removeFirst()
|
||||
|
||||
database.marmotInnerEventDao().getSubmissionsAwaiting(parentId).forEach { held ->
|
||||
// A submission's content is its payload verbatim; anything
|
||||
// else held is a bare nip30303 event, where the row is the
|
||||
// event and its own columns rebuild it. Reading both out of
|
||||
// the content would strand every bare one here forever.
|
||||
val payload = if (held.kind == SubmissionEvent.KIND) {
|
||||
Event.fromJsonOrNull(held.content)
|
||||
} else {
|
||||
Event(
|
||||
id = held.id,
|
||||
pubKey = held.publicKey,
|
||||
createdAt = held.createdAt.epochSeconds,
|
||||
kind = held.kind,
|
||||
tags = held.tags,
|
||||
content = held.content,
|
||||
sig = "",
|
||||
)
|
||||
} ?: return@forEach
|
||||
|
||||
missingParentOf(database, payload)?.let { stillMissing ->
|
||||
database.marmotInnerEventDao().upsert(
|
||||
held.copy(awaitingEventId = stillMissing)
|
||||
)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
database.marmotInnerEventDao().upsert(held.copy(awaitingEventId = null))
|
||||
|
||||
applyInnerEvent(
|
||||
database = database,
|
||||
groupId = groupId,
|
||||
event = payload,
|
||||
marmotGroupEventId = held.marmotGroupEventId,
|
||||
marmotInnerEventId = held.id,
|
||||
senderPublicKey = held.publicKey,
|
||||
// Held payloads are never ours: we hold the parents of
|
||||
// anything we wrote, having written those too.
|
||||
isUserMessage = false,
|
||||
createdAt = held.createdAt,
|
||||
)?.let { database.chatMessageDao().upsert(it) }
|
||||
|
||||
arrived.addLast(payload.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The event [event] references but which is not on file yet, or null
|
||||
* when everything it needs is already here.
|
||||
*/
|
||||
private suspend fun missingParentOf(
|
||||
database: MantraDatabase,
|
||||
event: Event,
|
||||
): HexKey? = parentRefsOf(event).firstOrNull { !it.exists(database) }?.id
|
||||
|
||||
/**
|
||||
* A row some event references, named by id and by the kind of event
|
||||
* that would have created it.
|
||||
*/
|
||||
internal data class ParentRef(
|
||||
val id: HexKey,
|
||||
val kind: Int,
|
||||
) {
|
||||
suspend fun exists(database: MantraDatabase): Boolean = when (kind) {
|
||||
DialectEvent.KIND ->
|
||||
database.mantraDialectDao().getDialectById(id) != null
|
||||
|
||||
ArtifactEvent.KIND ->
|
||||
database.mantraArtifactDao().getArtifactById(id) != null
|
||||
|
||||
ArtifactVersionEvent.KIND ->
|
||||
database.mantraArtifactVersionDao().getArtifactVersionById(id) != null
|
||||
|
||||
ChapterEvent.KIND ->
|
||||
database.mantraChapterDao().getChapterById(id) != null
|
||||
|
||||
ChunkEvent.KIND ->
|
||||
database.mantraChunkDao().getChunkById(id) != null
|
||||
|
||||
TranslationArtifactVersionEvent.KIND ->
|
||||
database.mantraTranslationArtifactVersionDao().getTranslationById(id) != null
|
||||
|
||||
TranslationChapterEvent.KIND ->
|
||||
database.mantraTranslationChapterDao().getTranslationChapterById(id) != null
|
||||
|
||||
TranslationChunkEvent.KIND ->
|
||||
database.mantraTranslationChunkDao().getTranslationChunkById(id) != null
|
||||
|
||||
// Not a parent anything waits on.
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every row [event] references and the database will insist exists.
|
||||
*
|
||||
* This is the foreign keys on the Mantra* entities, read off the event
|
||||
* instead of the schema. The two have to agree: a parent listed here
|
||||
* that the schema does not enforce just delays a payload for no reason,
|
||||
* and one the schema enforces but is missing here is a payload that
|
||||
* takes the whole inbound transaction down with it.
|
||||
*
|
||||
* The chat room is deliberately not among them. It is a foreign key,
|
||||
* but a payload for a room we are not in never reaches this far.
|
||||
*/
|
||||
internal fun parentRefsOf(event: Event): List<ParentRef> = when (event.kind) {
|
||||
ArtifactEvent.KIND ->
|
||||
event.asArtifactEvent().let {
|
||||
listOfNotNull(it.dialectId()?.let { id -> ParentRef(id, DialectEvent.KIND) })
|
||||
}
|
||||
|
||||
ArtifactVersionEvent.KIND ->
|
||||
event.asArtifactVersionEvent().let {
|
||||
listOfNotNull(it.artifactId()?.let { id -> ParentRef(id, ArtifactEvent.KIND) })
|
||||
}
|
||||
|
||||
ChapterEvent.KIND ->
|
||||
event.asChapterEvent().let {
|
||||
listOfNotNull(
|
||||
it.artifactVersionId()?.let { id -> ParentRef(id, ArtifactVersionEvent.KIND) }
|
||||
)
|
||||
}
|
||||
|
||||
ChunkEvent.KIND ->
|
||||
event.asChunkEvent().let {
|
||||
listOfNotNull(it.chapterId()?.let { id -> ParentRef(id, ChapterEvent.KIND) })
|
||||
}
|
||||
|
||||
TranslationArtifactVersionEvent.KIND ->
|
||||
event.asTranslationArtifactVersionEvent().let {
|
||||
listOfNotNull(
|
||||
it.artifactVersionId()?.let { id -> ParentRef(id, ArtifactVersionEvent.KIND) },
|
||||
it.dialectId()?.let { id -> ParentRef(id, DialectEvent.KIND) },
|
||||
)
|
||||
}
|
||||
|
||||
TranslationChapterEvent.KIND ->
|
||||
event.asTranslationChapterEvent().let {
|
||||
listOfNotNull(
|
||||
it.translationArtifactVersionId()?.let { id ->
|
||||
ParentRef(id, TranslationArtifactVersionEvent.KIND)
|
||||
},
|
||||
it.chapterId()?.let { id -> ParentRef(id, ChapterEvent.KIND) },
|
||||
)
|
||||
}
|
||||
|
||||
TranslationChunkEvent.KIND ->
|
||||
event.asTranslationChunkEvent().let {
|
||||
listOfNotNull(
|
||||
it.translationChapterId()?.let { id ->
|
||||
ParentRef(id, TranslationChapterEvent.KIND)
|
||||
},
|
||||
it.chunkId()?.let { id -> ParentRef(id, ChunkEvent.KIND) },
|
||||
)
|
||||
}
|
||||
|
||||
TranslationEvent.KIND ->
|
||||
event.asTranslationEvent().let {
|
||||
listOfNotNull(
|
||||
it.translationChunkId()?.let { id ->
|
||||
ParentRef(id, TranslationChunkEvent.KIND)
|
||||
},
|
||||
it.translationArtifactVersionId()?.let { id ->
|
||||
ParentRef(id, TranslationArtifactVersionEvent.KIND)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Kinds with no parent to wait for.
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
// A submission's payload arrives as a base Event; these read it back as
|
||||
// the kind it says it is, so its tag accessors can be used.
|
||||
private fun Event.asArtifactEvent() =
|
||||
ArtifactEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
private fun Event.asArtifactVersionEvent() =
|
||||
ArtifactVersionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
private fun Event.asChapterEvent() =
|
||||
ChapterEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
private fun Event.asChunkEvent() =
|
||||
ChunkEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
private fun Event.asTranslationArtifactVersionEvent() =
|
||||
TranslationArtifactVersionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
private fun Event.asTranslationChapterEvent() =
|
||||
TranslationChapterEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
private fun Event.asTranslationChunkEvent() =
|
||||
TranslationChunkEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
private fun Event.asTranslationEvent() =
|
||||
TranslationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
private suspend fun applyInnerEvent(
|
||||
database: MantraDatabase,
|
||||
activeKeyPair: KeyPair,
|
||||
groupEvent: GroupEvent,
|
||||
groupId: String,
|
||||
event: Event,
|
||||
marmotGroupEventId: HexKey?,
|
||||
marmotInnerEventId: HexKey,
|
||||
senderPublicKey: HexKey,
|
||||
isUserMessage: Boolean,
|
||||
createdAt: Instant,
|
||||
): ChatMessage? {
|
||||
return when (event.kind) {
|
||||
@@ -636,10 +370,10 @@ data class ChatMessage(
|
||||
ChatMessage(
|
||||
giftWrapPayloadId = null,
|
||||
messageType = "message",
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
marmotInnerEventId = marmotInnerEventId,
|
||||
senderPublicKey = senderPublicKey,
|
||||
isUserMessage = isUserMessage,
|
||||
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
|
||||
chatRoomId = groupId,
|
||||
createdAt = createdAt,
|
||||
content = event.content, // TODO: Figure out what to do here...
|
||||
@@ -659,17 +393,17 @@ data class ChatMessage(
|
||||
)?.let { mantraArtifact ->
|
||||
database.mantraArtifactDao().upsert(
|
||||
mantraArtifact.copy(
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
)
|
||||
)
|
||||
|
||||
ChatMessage(
|
||||
giftWrapPayloadId = null,
|
||||
messageType = "artifact",
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
marmotInnerEventId = marmotInnerEventId,
|
||||
senderPublicKey = senderPublicKey,
|
||||
isUserMessage = isUserMessage,
|
||||
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
|
||||
chatRoomId = groupId,
|
||||
createdAt = createdAt,
|
||||
content = "Added ${mantraArtifact.name} to artifacts"
|
||||
@@ -690,17 +424,17 @@ data class ChatMessage(
|
||||
)?.let { mantraArtifactVersion ->
|
||||
database.mantraArtifactVersionDao().upsert(
|
||||
mantraArtifactVersion.copy(
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
)
|
||||
)
|
||||
|
||||
ChatMessage(
|
||||
giftWrapPayloadId = null,
|
||||
messageType = "artifactVersion",
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
marmotInnerEventId = marmotInnerEventId,
|
||||
senderPublicKey = senderPublicKey,
|
||||
isUserMessage = isUserMessage,
|
||||
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
|
||||
chatRoomId = groupId,
|
||||
createdAt = createdAt,
|
||||
content = "Added ${mantraArtifactVersion.versionLabel} to artifact versions" // TODO: Use artifact name...
|
||||
@@ -721,17 +455,17 @@ data class ChatMessage(
|
||||
)?.let { mantraChapter ->
|
||||
database.mantraChapterDao().upsert(
|
||||
mantraChapter.copy(
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
)
|
||||
)
|
||||
|
||||
ChatMessage(
|
||||
giftWrapPayloadId = null,
|
||||
messageType = "chapter",
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
marmotInnerEventId = marmotInnerEventId,
|
||||
senderPublicKey = senderPublicKey,
|
||||
isUserMessage = isUserMessage,
|
||||
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
|
||||
chatRoomId = groupId,
|
||||
createdAt = createdAt,
|
||||
content = "Added ${mantraChapter.name} to chapters" // TODO: Use artifact name...
|
||||
@@ -753,7 +487,7 @@ data class ChatMessage(
|
||||
)?.let { mantraChunk ->
|
||||
database.mantraChunkDao().upsert(
|
||||
mantraChunk.copy(
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -775,7 +509,7 @@ data class ChatMessage(
|
||||
)?.let { mantraDialect ->
|
||||
database.mantraDialectDao().upsert(
|
||||
mantraDialect.copy(
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -783,10 +517,10 @@ data class ChatMessage(
|
||||
ChatMessage(
|
||||
giftWrapPayloadId = null,
|
||||
messageType = "dialect",
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
marmotInnerEventId = marmotInnerEventId,
|
||||
senderPublicKey = senderPublicKey,
|
||||
isUserMessage = isUserMessage,
|
||||
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
|
||||
chatRoomId = groupId,
|
||||
createdAt = createdAt,
|
||||
content = "Added ${mantraDialect.name} to dialects" // TODO: Use artifact name...
|
||||
@@ -807,17 +541,17 @@ data class ChatMessage(
|
||||
)?.let { mantraTranslationArtifactVersion ->
|
||||
database.mantraTranslationArtifactVersionDao().upsert(
|
||||
mantraTranslationArtifactVersion.copy(
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
)
|
||||
)
|
||||
|
||||
ChatMessage(
|
||||
giftWrapPayloadId = null,
|
||||
messageType = "translationArtifactVersion",
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
marmotInnerEventId = marmotInnerEventId,
|
||||
senderPublicKey = senderPublicKey,
|
||||
isUserMessage = isUserMessage,
|
||||
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
|
||||
chatRoomId = groupId,
|
||||
createdAt = createdAt,
|
||||
content = "Added ${mantraTranslationArtifactVersion.name} to translation artifact versions" // TODO: Use artifact name...
|
||||
@@ -842,7 +576,7 @@ data class ChatMessage(
|
||||
)?.let { mantraTranslationChapter ->
|
||||
database.mantraTranslationChapterDao().upsert(
|
||||
mantraTranslationChapter.copy(
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
)
|
||||
)
|
||||
// TODO: translation chapter might be too noisy for chat updates
|
||||
@@ -863,7 +597,7 @@ data class ChatMessage(
|
||||
)?.let { mantraTranslationChunk ->
|
||||
database.mantraTranslationChunkDao().upsert(
|
||||
mantraTranslationChunk.copy(
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -889,16 +623,16 @@ data class ChatMessage(
|
||||
)?.let { mantraTranslation ->
|
||||
database.mantraTranslationDao().upsert(
|
||||
mantraTranslation.copy(
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
)
|
||||
)
|
||||
ChatMessage(
|
||||
giftWrapPayloadId = null,
|
||||
messageType = "translation",
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
marmotInnerEventId = marmotInnerEventId,
|
||||
senderPublicKey = senderPublicKey,
|
||||
isUserMessage = isUserMessage,
|
||||
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
|
||||
chatRoomId = groupId,
|
||||
createdAt = createdAt,
|
||||
content = "Added ${mantraTranslation.text} to translations" // TODO: Reference original text
|
||||
@@ -909,10 +643,10 @@ data class ChatMessage(
|
||||
ChatMessage(
|
||||
giftWrapPayloadId = null,
|
||||
messageType = "unsupported",
|
||||
marmotGroupEventId = marmotGroupEventId,
|
||||
marmotGroupEventId = groupEvent.id,
|
||||
marmotInnerEventId = marmotInnerEventId,
|
||||
senderPublicKey = senderPublicKey,
|
||||
isUserMessage = isUserMessage,
|
||||
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
|
||||
chatRoomId = groupId,
|
||||
createdAt = createdAt,
|
||||
content = event.toJson(),
|
||||
|
||||
@@ -73,17 +73,6 @@ data class MarmotInnerEvent( // TODO: Rename this to GiftWrapPayload...
|
||||
*/
|
||||
val payloadEventId: HexKey? = null,
|
||||
|
||||
/**
|
||||
* The event this one references but which has not arrived yet, or null
|
||||
* when there is nothing left to wait for.
|
||||
*
|
||||
* Every nip30303 entity is a child of another and the database enforces
|
||||
* that, so a payload cannot become a row before its parent does. Rather
|
||||
* than drop one that arrives early, it is held here against the id it is
|
||||
* waiting on and applied when that turns up.
|
||||
*/
|
||||
val awaitingEventId: HexKey? = null,
|
||||
|
||||
/**
|
||||
* Associated MarmotGroupEvent
|
||||
*/
|
||||
@@ -138,7 +127,6 @@ data class MarmotInnerEvent( // TODO: Rename this to GiftWrapPayload...
|
||||
if (content != other.content) return false
|
||||
if (quotedEventId != other.quotedEventId) return false
|
||||
if (payloadEventId != other.payloadEventId) return false
|
||||
if (awaitingEventId != other.awaitingEventId) return false
|
||||
if (marmotGroupEventId != other.marmotGroupEventId) return false
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (updatedAt != other.updatedAt) return false
|
||||
@@ -159,7 +147,6 @@ data class MarmotInnerEvent( // TODO: Rename this to GiftWrapPayload...
|
||||
result = 31 * result + content.hashCode()
|
||||
result = 31 * result + (quotedEventId?.hashCode() ?: 0)
|
||||
result = 31 * result + (payloadEventId?.hashCode() ?: 0)
|
||||
result = 31 * result + (awaitingEventId?.hashCode() ?: 0)
|
||||
result = 31 * result + (marmotGroupEventId?.hashCode() ?: 0)
|
||||
result = 31 * result + createdAt.hashCode()
|
||||
result = 31 * result + updatedAt.hashCode()
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
package press.mantra.compose.database.model
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import press.mantra.compose.nostr.nip30303.ArtifactEvent
|
||||
import press.mantra.compose.nostr.nip30303.ArtifactVersionEvent
|
||||
import press.mantra.compose.nostr.nip30303.ChapterEvent
|
||||
import press.mantra.compose.nostr.nip30303.ChunkEvent
|
||||
import press.mantra.compose.nostr.nip30303.DialectEvent
|
||||
import press.mantra.compose.nostr.nip30303.TranslationArtifactVersionEvent
|
||||
import press.mantra.compose.nostr.nip30303.TranslationChapterEvent
|
||||
import press.mantra.compose.nostr.nip30303.TranslationChunkEvent
|
||||
import press.mantra.compose.nostr.nip30303.tags.ArtifactIdTag
|
||||
|
||||
/**
|
||||
* What the inbound side must wait for before it writes a row.
|
||||
*
|
||||
* These have to match the foreign keys on the Mantra* entities exactly. A
|
||||
* parent claimed here that the schema does not enforce holds a payload back
|
||||
* for nothing; one the schema enforces but that is missing here is an insert
|
||||
* that violates a constraint, and SQLite answers that by rolling back the
|
||||
* whole inbound transaction -- losing the group event, the submission and the
|
||||
* transcript line, none of which is ever retried. So the mapping is asserted
|
||||
* rather than trusted to stay in step.
|
||||
*/
|
||||
class ParentRefsTest {
|
||||
private val author = "a".repeat(64)
|
||||
private val dialect = "d".repeat(64)
|
||||
private val artifact = "1".repeat(64)
|
||||
private val version = "2".repeat(64)
|
||||
private val chapter = "3".repeat(64)
|
||||
private val chunk = "4".repeat(64)
|
||||
private val translationVersion = "5".repeat(64)
|
||||
private val translationChapter = "6".repeat(64)
|
||||
|
||||
private fun eventOf(template: EventTemplate<*>) = Event(
|
||||
id = "f".repeat(64),
|
||||
pubKey = author,
|
||||
createdAt = template.createdAt,
|
||||
kind = template.kind,
|
||||
tags = template.tags,
|
||||
content = template.content,
|
||||
sig = "",
|
||||
)
|
||||
|
||||
private fun refsOf(template: EventTemplate<*>) =
|
||||
ChatMessage.parentRefsOf(eventOf(template)).map { it.id to it.kind }
|
||||
|
||||
@Test
|
||||
fun `an artifact waits for its dialect`() {
|
||||
val refs = refsOf(
|
||||
ArtifactEvent.build(
|
||||
name = "In Detention",
|
||||
url = "example.com",
|
||||
visibility = "private",
|
||||
license = "cc",
|
||||
dialectId = dialect,
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(listOf(dialect to DialectEvent.KIND), refs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a version waits for its artifact`() {
|
||||
val refs = refsOf(
|
||||
ArtifactVersionEvent.build(content = "1.0") {
|
||||
addUnique(ArtifactIdTag.assemble(artifact))
|
||||
}
|
||||
)
|
||||
|
||||
assertEquals(listOf(artifact to ArtifactEvent.KIND), refs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a chapter waits for its version and a chunk for its chapter`() {
|
||||
val chapterRefs = refsOf(
|
||||
ChapterEvent.build(
|
||||
artifactVersionId = version,
|
||||
name = "Chapter 1",
|
||||
originalText = "text",
|
||||
index = 0,
|
||||
wordCount = 1,
|
||||
characterCount = 4,
|
||||
)
|
||||
)
|
||||
val chunkRefs = refsOf(
|
||||
ChunkEvent.build(
|
||||
chapterId = chapter,
|
||||
text = "text",
|
||||
index = 0,
|
||||
wordCount = 1,
|
||||
characterCount = 4,
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(listOf(version to ArtifactVersionEvent.KIND), chapterRefs)
|
||||
assertEquals(listOf(chapter to ChapterEvent.KIND), chunkRefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a translation waits for both of its parents`() {
|
||||
val refs = refsOf(
|
||||
TranslationArtifactVersionEvent.build(
|
||||
artifactVersionId = version,
|
||||
dialectId = dialect,
|
||||
name = "Sesotho",
|
||||
visibility = "private",
|
||||
license = "cc",
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
version to ArtifactVersionEvent.KIND,
|
||||
dialect to DialectEvent.KIND,
|
||||
),
|
||||
refs
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a translated chapter and chunk each wait for both of their parents`() {
|
||||
val chapterRefs = refsOf(
|
||||
TranslationChapterEvent.build(
|
||||
translationArtifactVersionId = translationVersion,
|
||||
chapterId = chapter,
|
||||
index = 0,
|
||||
)
|
||||
)
|
||||
val chunkRefs = refsOf(
|
||||
TranslationChunkEvent.build(
|
||||
translationChapterId = translationChapter,
|
||||
chunkId = chunk,
|
||||
index = 0,
|
||||
text = "translated",
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
translationVersion to TranslationArtifactVersionEvent.KIND,
|
||||
chapter to ChapterEvent.KIND,
|
||||
),
|
||||
chapterRefs
|
||||
)
|
||||
assertEquals(
|
||||
listOf(
|
||||
translationChapter to TranslationChapterEvent.KIND,
|
||||
chunk to ChunkEvent.KIND,
|
||||
),
|
||||
chunkRefs
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a dialect waits for nothing, so it can always start a group off`() {
|
||||
val refs = refsOf(
|
||||
DialectEvent.build(name = "Sesotho", country = "Lesotho", language = "st")
|
||||
)
|
||||
|
||||
assertTrue(refs.isEmpty())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user