cultmesh-ts: count and answer a frame that decodes but is no message the parser takes
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 6m9s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 1m17s

A map with an unknown schemaVersion was rethrown: unanswered, uncounted toward the
session cap. It is now answered message_invalid, reported, and counted like any other
unreadable frame, so the cap holds for it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Libby 2026-11-08 09:52:22 +00:00
commit 23c7ce3738
2 changed files with 33 additions and 2 deletions

View file

@ -5678,9 +5678,13 @@ export class CultMesh {
// A frame the server cannot read is judged as the write it looks like (a put when it does not even decode), so
// an ungranted peer hears the same refusal for it as for any other write, not a parse error.
const write = readableWrite(decoded);
if (decoded !== undefined && !write) throw error;
record.unreadableInARow += 1;
if (!await writeRefused(record, write?.operation ?? "put", write?.schemaId ?? "", write?.recordKey ?? "", null, null)) {
if (decoded !== undefined && !write) {
// It decodes but is no message the parser takes (an unknown schemaVersion, a non-map) and is no write the
// authority could judge: answered and counted like any other unreadable frame.
reportError(error);
sendSchemaMessage(record, cultNetMessageInvalidError("message", "CultMesh RUDP document server could not read the message."));
} else if (!await writeRefused(record, write?.operation ?? "put", write?.schemaId ?? "", write?.recordKey ?? "", null, null)) {
reportError(error);
sendSchemaMessage(record, cultNetMessageInvalidError("message", "CultMesh RUDP document server could not read the write."));
}

View file

@ -563,3 +563,30 @@ test("deletes the authority refuses are answered and the session kept until eigh
assert.equal(r.deletes.length, 1);
});
});
test("a frame that decodes but is no message the parser takes is answered, counted, and ended at eight in a row like any unreadable frame", async () => {
const r = await rawRig(new Recording(grant));
const unknown = () => encodeWireValue({ schemaVersion: "cultnet.not-a-message.v0", messageId: "x" });
try {
const batch = async (frames: number) => {
for (let i = 0; i < frames; i++) r.send(unknown());
await sleep(400);
};
await batch(CAP - 1);
assert.equal(r.replies.length, CAP - 1, "each is answered");
assert.equal(r.errors.length, CAP - 1, "and reported");
r.send(encodeWireValue(put(KEY_A)));
await sleep(250);
assert.deepEqual(r.puts.map((p) => p.recordKey), [KEY_A], "the session was kept through seven");
await batch(CAP - 1);
assert.equal(r.replies.length, 2 * (CAP - 1), "the decoded put reset the run");
r.send(unknown());
await sleep(300);
assert.equal(r.replies.length, 2 * (CAP - 1) + 1, "the eighth is answered too");
for (const reply of r.replies) {
assert.equal((reply as { code?: string }).code, "message_invalid");
assert.equal((reply as { details: { field: string } }).details.field, "message");
}
assert.throws(() => r.send(unknown()), /before the session is connected/, "then the session ended");
} finally { r.close(); }
});