Merge pull request 'Fix build:ts: one text-only error factory in cultnet-ts (6 TS2345 in cultmesh-ts)' (#29) from libby/cultmesh-ts-error-message-sites into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled

This commit is contained in:
Libby 2026-11-07 16:27:31 +00:00
commit da515b0f5e
5 changed files with 19 additions and 28 deletions

View file

@ -20,6 +20,7 @@ import {
CultNetRudpSocketTransportConnection,
rudpClientBindHost,
CultNetSchemaCatalog,
cultNetTextError,
CultNetShardCatalog,
cultNetBuiltinSchemaRegistry,
decodeRudpPacket,
@ -5577,10 +5578,7 @@ export class CultMesh {
switch (message.schemaVersion) {
case "cultnet.snapshot_request.v0": {
if (!options.getCache) {
sendSchemaMessage(record, {
schemaVersion: "cultnet.error.v0",
error: "CultMesh RUDP document server has no cache for snapshot requests.",
});
sendSchemaMessage(record, cultNetTextError("CultMesh RUDP document server has no cache for snapshot requests."));
return;
}
let reply: CultNetMessage;
@ -5613,7 +5611,7 @@ export class CultMesh {
const unservable = unservablePut(message);
if (unservable) {
reportError(unservable);
sendSchemaMessage(record, { schemaVersion: "cultnet.error.v0", error: unservable.message });
sendSchemaMessage(record, cultNetTextError(unservable.message));
return;
}
const receipt = await options.onDocumentPutRaw(normalizeRudpDocumentPut(message, record.remote, options.documents));
@ -5623,29 +5621,20 @@ export class CultMesh {
} catch (error) {
// The put is stored: the peer must hear that its receipt is not coming.
reportError(error);
sendSchemaMessage(record, {
schemaVersion: "cultnet.error.v0",
error: `CultMesh RUDP document server stored a put but could not send its receipt.`,
});
sendSchemaMessage(record, cultNetTextError(`CultMesh RUDP document server stored a put but could not send its receipt.`));
}
}
}
return;
case "cultnet.operation_request.v0":
if (!options.onOperationRequest) {
sendSchemaMessage(record, {
schemaVersion: "cultnet.error.v0",
error: "CultMesh RUDP document server has no operation handler.",
});
sendSchemaMessage(record, cultNetTextError("CultMesh RUDP document server has no operation handler."));
return;
}
sendSchemaMessage(record, await options.onOperationRequest(message, publicSession(record)));
return;
default:
sendSchemaMessage(record, {
schemaVersion: "cultnet.error.v0",
error: `Unsupported CultMesh RUDP document request ${message.schemaVersion}.`,
});
sendSchemaMessage(record, cultNetTextError(`Unsupported CultMesh RUDP document request ${message.schemaVersion}.`));
}
}
@ -5741,7 +5730,7 @@ export class CultMesh {
*/
function refuse(record: SessionRecord, text: string): void {
if (record.closed) return;
const payload = encodeSchemaMessage({ schemaVersion: "cultnet.error.v0", error: text });
const payload = encodeSchemaMessage(cultNetTextError(text));
for (const packet of record.session.sendMany("schema", payload, {
reliable: false,
ordered: false,

View file

@ -12,6 +12,7 @@ import {
CultNetPeer,
CultNetRudpSession,
cultNetBuiltinSchemaRegistry,
cultNetTextError,
decodeRudpPacket,
defineCultNetDocumentBinding,
encodeRudpPacket,
@ -3708,7 +3709,7 @@ test("CultMesh TS RUDP document server answers a peer whose reliable queue is fu
while (!refusalIn(rig.inbox) && Date.now() - startedAt < 3_000) await delay(5);
const refusal = refusalIn(rig.inbox);
assert.ok(refusal, "the refusal reaches the peer");
assert.deepEqual(refusal.message, { schemaVersion: "cultnet.error.v0", error: "the snapshot response could not be queued" });
assert.deepEqual(refusal.message, cultNetTextError("the snapshot response could not be queued"));
for (const part of refusal.parts) {
assert.ok(refused.every((sequence) => !acknowledges(part, sequence)), `ack ${part.ack} mask ${part.ackMask}`);
}
@ -3725,7 +3726,7 @@ test("CultMesh TS RUDP document server refuses a snapshot with fixed text, never
while (!refusalIn(rig.inbox) && Date.now() - startedAt < 3_000) await delay(5);
const refusal = refusalIn(rig.inbox);
assert.ok(refusal, "the refusal reaches the peer");
assert.deepEqual(refusal.message, { schemaVersion: "cultnet.error.v0", error: "the snapshot source failed" });
assert.deepEqual(refusal.message, cultNetTextError("the snapshot source failed"));
assert.match(rig.errors[0]?.message ?? "", /CANARY-7f3a/);
});
});

View file

@ -7,6 +7,7 @@ All notable changes to `@gamecult/cultnet-ts` are recorded here, following
### Added
- `cultNetTextError(error)`: the text-only `cultnet.error.v0` refusal, with `routingHint`, `code` and `details` null. The legacy-union error decode uses it, so there is one place that spells the null members.
- `CultNetRudpSocketTransportServer`: one UDP socket serving many peers, each remote address and port with its own reliable session (`peerConnected`, `frame`, `peerDisconnected` events; `send(peer, ...)` answers on the peer a frame came from). A second client no longer replaces the first; Connects beyond `maxPeers` (default 256) are dropped, and `close()` resolves once every peer's goodbye has left the socket. The shape follows the Rust `CultNetRudpServerHub` and C# `CultNetRudpSocketTransportServer`, and the wire format is unchanged. The optional `peerTimeoutMs` (idle peers ended and told so; default off) exists only in TypeScript. `CultNetRudpSocketTransportConnection` in `server` mode still serves exactly one peer.
- The `cultnet.snapshot_request.v1`, `cultnet.database_subscribe.v1` and `cultnet.snapshot_response_raw.v1` carriers and the selection payload types (`CultNetSelection`, `CultNetRawDocumentHeader`, `CultNetEdge`). `parseCultNetMessage` accepts them (absent members arrive as absent keys; a page carries exactly one of `headers`/`documents`; an empty or blank `schemas`/`keys` entry throws `CultNetSelectionInvalidError` naming its field), and `encodeCultNetMessageForWire` writes the reference's nil-keyed, declaration-ordered map. Judged against bytes the C# reference wrote (`contracts/cultnet/interop/carrier-vectors.cs-written.json`) and the selection vectors C# and Rust wrote. The selection engine (evaluation, cursors, the declaration-dependent door checks) is not ported.

View file

@ -199,6 +199,11 @@ export const CULTNET_ERROR_CODES = [
export type CultNetErrorCode = (typeof CULTNET_ERROR_CODES)[number];
/** A refusal that carries text only: `routingHint`, `code` and `details` are written as null, as the reference writes an absent member. */
export function cultNetTextError(error: string): CultNetErrorMessage {
return { schemaVersion: "cultnet.error.v0", error, routingHint: null, code: null, details: null };
}
export interface CultNetErrorRoutingHint {
shard: Record<string, unknown> | null;
reason: string;
@ -958,14 +963,8 @@ function parseGameCultNetworkingMessage(input: unknown): CultNetMessage {
session: requireLegacyBytes(payload[1], "LoginSuccessMessage.Session"),
};
case 4:
return {
schemaVersion: "cultnet.error.v0",
error: requireLegacyString(payload[0], "ErrorMessage.Error"),
// The legacy union ErrorMessage carries only Error; the R-N fields exist on cultnet.schema.v0 alone.
routingHint: null,
code: null,
details: null,
};
// The legacy union ErrorMessage carries only Error; the R-N fields exist on cultnet.schema.v0 alone.
return cultNetTextError(requireLegacyString(payload[0], "ErrorMessage.Error"));
case 5:
return {
schemaVersion: "cultnet.sample.change_name.v0",

View file

@ -17,6 +17,7 @@ export {
type CultNetRawDocumentRecord,
type CultNetRawPayloadEncoding,
CULTNET_ERROR_CODES,
cultNetTextError,
type CultNetErrorCode,
type CultNetErrorDetails,
type CultNetErrorMessage,