CultMesh Kotlin: a cache refuses a second type for one schema id, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea, I support retiring it." CultCache.register wrote its type and schema maps unconditionally, and put, get, getAll and delete register implicitly, so one Kotlin cache could hold two types for one schema and codecForSchema answered with the last. - register refuses a codec whose type already holds another schema id, or whose schema id another type holds, with the new CultSchemaConflictException (schema id, both document types, empty record key), the Kotlin counterpart of the other runtimes' typed conflict. The implicit registration in put, get, getAll and delete goes through the same check. Registering the same type for the same schema again stays allowed. - Self-test: other-type same-schema and same-type other-schema claimants, through register, put, get, getAll and delete, each in a fresh cache; the first codec still answers for its schema afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fc3f3ee5ff
commit
4002a996b8
1 changed files with 59 additions and 0 deletions
|
|
@ -55,6 +55,17 @@ data class CultCacheEnvelope(
|
|||
val payload: ByteArray,
|
||||
)
|
||||
|
||||
/**
|
||||
* A schema this cache cannot give to one more type: another type already holds the schema id, or the type already holds
|
||||
* another schema id. Nothing is registered. It names the refused codec's schema id and both document types.
|
||||
*/
|
||||
class CultSchemaConflictException(
|
||||
val schemaId: String,
|
||||
val schemaNames: List<String>,
|
||||
val recordKey: String,
|
||||
message: String,
|
||||
) : IOException(message)
|
||||
|
||||
class CultCache {
|
||||
companion object {
|
||||
const val GLOBAL_KEY = "__global__"
|
||||
|
|
@ -64,7 +75,19 @@ class CultCache {
|
|||
private val codecsBySchema = linkedMapOf<String, CultDocumentCodec<*>>()
|
||||
private val values = linkedMapOf<String, LinkedHashMap<String, ByteArray>>()
|
||||
|
||||
// One type owns a schema id. Every path that names a codec (register, put, get, getAll, delete) comes through here.
|
||||
fun <T : Any> register(codec: CultDocumentCodec<T>) {
|
||||
val holder = codecs[codec.documentType]?.takeIf { it.schemaVersion != codec.schemaVersion }
|
||||
?: codecsBySchema[codec.schemaVersion]?.takeIf { it.documentType != codec.documentType }
|
||||
if (holder != null) {
|
||||
throw CultSchemaConflictException(
|
||||
codec.schemaVersion,
|
||||
listOf(holder.documentType, codec.documentType),
|
||||
"",
|
||||
"Kotlin CultCache type ${holder.documentType} holds schema ${holder.schemaVersion}; " +
|
||||
"type ${codec.documentType} cannot also be registered for schema id ${codec.schemaVersion}",
|
||||
)
|
||||
}
|
||||
codecs[codec.documentType] = codec
|
||||
codecsBySchema[codec.schemaVersion] = codec
|
||||
}
|
||||
|
|
@ -3539,6 +3562,42 @@ private fun cultCacheRawSnapshotsRoundTripThroughCultNetMessages() {
|
|||
check(syncedAlias == KotlinUiNote("kotlin.alias_note.v1", "canonical-to-ui"))
|
||||
check(target.require(uiNote, "note:alias") == syncedAlias)
|
||||
|
||||
// One type per schema in one cache: a second type for a held schema id, or a held type for another schema id, is
|
||||
// refused typed, by register and by the implicit registration in put, get, getAll and delete alike.
|
||||
fun refusal(action: () -> Unit): CultSchemaConflictException? =
|
||||
try { action(); null } catch (conflict: CultSchemaConflictException) { conflict }
|
||||
val canonicalOtherSchema = cultDocument(KotlinAliasNoteCodec(
|
||||
documentType = "kotlin.alias_note",
|
||||
schemaVersion = "kotlin.alias_note.v2",
|
||||
create = ::KotlinCanonicalNote,
|
||||
readSchemaVersion = KotlinCanonicalNote::schemaVersion,
|
||||
readBody = KotlinCanonicalNote::body,
|
||||
))
|
||||
for ((first, second) in listOf(canonicalNote to uiNote, uiNote to canonicalNote, canonicalNote to canonicalOtherSchema)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val claimant = second as CultDocumentDefinition<Any>
|
||||
val claimantValue = claimant.codec.decode(canonicalNote.codec.encode(KotlinCanonicalNote("kotlin.alias_note.v1", "x")))
|
||||
val attempts: List<(CultCache) -> Unit> = listOf(
|
||||
{ cache -> cache.register(claimant) },
|
||||
{ cache -> cache.put(claimant, "note:second", claimantValue) },
|
||||
{ cache -> cache.get(claimant, "note:second") },
|
||||
{ cache -> cache.getAll(claimant) },
|
||||
{ cache -> cache.delete(claimant.codec, "note:second") },
|
||||
)
|
||||
for (attempt in attempts) {
|
||||
val cache = CultCache()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
cache.register(first as CultDocumentDefinition<Any>)
|
||||
val conflict = refusal { attempt(cache) }
|
||||
check(conflict != null) { "${second.documentType}/${second.schemaVersion} beside ${first.documentType}/${first.schemaVersion} was not refused" }
|
||||
check(conflict.schemaId == second.schemaVersion && conflict.recordKey == "")
|
||||
check(conflict.schemaNames == listOf(first.documentType, second.documentType))
|
||||
check(conflict.message!!.contains(second.schemaVersion))
|
||||
check(cache.codecForSchema(first.schemaVersion).documentType == first.documentType)
|
||||
}
|
||||
}
|
||||
CultCache().apply { register(canonicalNote); register(canonicalNote); put(canonicalNote, "note:again", KotlinCanonicalNote("kotlin.alias_note.v1", "again")) }
|
||||
|
||||
val reactiveUiNote = cultDocument(KotlinReactiveNoteCodec(
|
||||
documentType = "kotlin.reactive_note.ui",
|
||||
schemaVersion = "kotlin.reactive_note.v1",
|
||||
|
|
|
|||
Loading…
Reference in a new issue