Add TypeScript TCP peer helper
This commit is contained in:
parent
49142a78e5
commit
805539384e
6 changed files with 105 additions and 17 deletions
|
|
@ -167,7 +167,10 @@ Current progress:
|
|||
over either the compatibility `Duplex` stream path or the transport
|
||||
connection path, while the TypeScript interop peer now accepts and dials TCP
|
||||
peers through `TcpFramedTransportConnection` so the parity harness exercises
|
||||
the shared port instead of raw socket framing.
|
||||
the shared port instead of raw socket framing. `createTcpFramedCultNetPeer`
|
||||
gives application callers the same transport-first path without hand-building
|
||||
the profile and adapter; raw `Duplex` peer construction is now explicitly the
|
||||
compatibility lane.
|
||||
- Python now has the same `tcp_framed` transport profile helper plus a
|
||||
synchronous `TcpFramedTransportConnection` with `send`, `receive`, `close`,
|
||||
and stats. `CultNetRawClient`, database subscriptions, and the local
|
||||
|
|
@ -315,11 +318,13 @@ Current progress:
|
|||
contact hints. The facade delegates to the existing Rust RUDP socket
|
||||
transport; discovery contact remains separate from trust, which is still
|
||||
gated by the authority lease catalog.
|
||||
- The remaining parity work is to move the remaining LiteNetLib/WebSocket
|
||||
service bodies behind their shared transport ports, then wire any remaining
|
||||
daemon/socket reconnect loops through the shared controller. The core
|
||||
schema-message RUDP matrix is live in the TypeScript interop gate; broader
|
||||
service adoption remains the unclaimed layer.
|
||||
- The remaining parity work is to finish production service-body adoption
|
||||
behind the shared transport ports, then wire any remaining daemon/socket
|
||||
reconnect loops through the shared controller. C# built-in LiteNetLib service
|
||||
wrappers now route through `CultNetServerPeer`, TypeScript exposes a
|
||||
transport-first TCP peer helper, and the core schema-message RUDP matrix is
|
||||
live in the TypeScript interop gate; broader service adoption remains the
|
||||
unclaimed layer.
|
||||
|
||||
## RUDP Packet Contract V0
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ CultMesh.
|
|||
| --- | --- | --- | --- |
|
||||
| C# | Reference implementation and production game/server substrate | CultCache typed documents and SoA, CultNet schema-v0, LiteNetLib production path with adapter profile, shared transport profiles, shared RUDP socket transport, RUDP reconnect policy/controller and reconnect-loop helper, LiteNetLib client reconnect-controller adoption, CultMesh node/session/catalog/stream/simulation surfaces | No need to abandon LiteNetLib before the CultNet adapter fully owns production service adoption |
|
||||
| Python | Package runtime, local daemon endpoint, raw interop peer, typed state participant | CultCache/CultNet/CultMesh package surface, schema catalogs, shard catalogs/logs, local TCP CultMesh server, session/prediction/simulation fact helpers, authority leases, streams, RUDP codec/session/socket transport, RUDP reconnect policy/controller and reconnect-loop helper, schema-v0 MessagePack over RUDP evidence, benchmark evidence against C# cache paths | Universal C# throughput, full C# daemon/server stack, and RUDP as the only public service pipe |
|
||||
| TypeScript | Node/browser-adjacent projection and interop runtime | CultCache parity tests, CultNet schema-v0 peer, transport profiles, TCP-framed transport port, shared RUDP socket transport, RUDP reconnect policy/controller, RUDP reconnect-loop helper, local CultMesh node/catalog/authority/stream ergonomics | Full C# game session/server stack |
|
||||
| TypeScript | Node/browser-adjacent projection and interop runtime | CultCache parity tests, CultNet schema-v0 peer, transport profiles, TCP-framed transport port and peer helper, shared RUDP socket transport, RUDP reconnect policy/controller, RUDP reconnect-loop helper, local CultMesh node/catalog/authority/stream ergonomics | Full C# game session/server stack |
|
||||
| Rust | Low-level cache/transport/runtime substrate | CultCache/CultNet interop lane, transport profiles, TCP-framed port, shared RUDP codec/session/socket transport, RUDP reconnect policy/controller and reconnect-loop helper, schema-v0 MessagePack over RUDP evidence, small CultMesh facade for peer cards, authority leases, RUDP endpoint parsing, and peer-card RUDP client construction | Full CultMesh game-session/server facade, stream catalog, or production game-session server |
|
||||
| Kotlin | JVM/Android client substrate | Typed MessagePack cache, schema/shard catalogs, authority leases, stream catalog, branded `CultMesh` facade, WebSocket transport profile/connection helper, RUDP client/server sugar, RUDP reconnect policy/controller and reconnect-loop helper, schema-v0 MessagePack over RUDP evidence | C# SoA store, full C# server daemon, or Unity hot-loop memory layout |
|
||||
|
||||
|
|
@ -57,9 +57,11 @@ CultMesh.
|
|||
|
||||
## Remaining Work Before A Full Completion Claim
|
||||
|
||||
- Broaden service adoption so remaining daemon and production paths use the
|
||||
shared transport port consistently instead of keeping LiteNetLib/WebSocket
|
||||
ownership in individual bodies.
|
||||
- Broaden production service adoption so remaining daemon paths use the shared
|
||||
transport port consistently. The C# built-in LiteNetLib service wrappers now
|
||||
route through transport-aware peer context, and TypeScript TCP peer creation
|
||||
has a transport-first helper; broader daemon adoption remains the unclaimed
|
||||
layer.
|
||||
- Extend RUDP adoption through daemon/socket reconnect loops. RUDP profiles now
|
||||
advertise the portable reconnect policy, runtime-local tests prove the shared
|
||||
controller across the targeted runtimes, and the C# LiteNetLib client plus
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ polymorphic attack surface.
|
|||
import { CultCache, defineDocumentType } from "cultcache-ts";
|
||||
import {
|
||||
CultNetDocumentRegistry,
|
||||
CultNetPeer,
|
||||
createTcpFramedCultNetPeer,
|
||||
defineCultNetDocumentBinding,
|
||||
ghostlightAgentStateGeneratedContract,
|
||||
} from "cultnet-ts";
|
||||
|
|
@ -198,8 +198,10 @@ const registry = new CultNetDocumentRegistry([
|
|||
}),
|
||||
]);
|
||||
|
||||
const peer = new CultNetPeer(stream, {
|
||||
const peer = createTcpFramedCultNetPeer(stream, {
|
||||
runtimeId: "node-worker",
|
||||
wireContract: "cultnet.schema.v0",
|
||||
transportId: "worker-tcp",
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -231,8 +233,10 @@ maps them back to the exact C# union payload shape at the wire boundary.
|
|||
- The Ghostlight agent-state mirror is an exact copy of Ghostlight's current
|
||||
canonical schema. If Ghostlight changes the contract, this mirror must be
|
||||
updated in lockstep instead of playing coy with partial validation.
|
||||
- The direct transport is any Node `Duplex`; named-pipe/socket server wrappers
|
||||
can be added without changing the message contract.
|
||||
- The preferred direct transport path is `CultNetTransportConnection`.
|
||||
`createTcpFramedCultNetPeer(...)` wraps a Node `Duplex` in the shared
|
||||
`tcp_framed` profile before schema messages reach `CultNetPeer`. Passing a
|
||||
raw `Duplex` to `CultNetPeer` remains as compatibility for older callers.
|
||||
|
||||
## Development
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export {
|
|||
type GhostlightWorldState,
|
||||
} from "./contracts";
|
||||
export { encodeFrame, LengthPrefixedMessageFramer } from "./framing";
|
||||
export { CultNetPeer, type CultNetPeerEvents } from "./peer";
|
||||
export { CultNetPeer, createTcpFramedCultNetPeer, type CultNetPeerEvents, type TcpFramedCultNetPeerOptions } from "./peer";
|
||||
export {
|
||||
CultNetReconnectController,
|
||||
TcpFramedTransportConnection,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import type { Duplex } from "node:stream";
|
|||
|
||||
import { decode, encode } from "@msgpack/msgpack";
|
||||
|
||||
import { encodeCultNetMessageForWire, parseCultNetMessage, type CultNetDocumentDeleteMessage, type CultNetDocumentPutMessage, type CultNetErrorMessage, type CultNetHelloMessage, type CultNetLoginMessage, type CultNetLoginSuccessMessage, type CultNetMessage, type CultNetRegisterMessage, type CultNetSampleChangeNameMessage, type CultNetSampleChatMessage, type CultNetSchemaCatalogRequestMessage, type CultNetSchemaCatalogResponseMessage, type CultNetSnapshotRequestMessage, type CultNetSnapshotResponseMessage, type CultNetVerifyMessage, type CultNetWireContract } from "./contracts";
|
||||
import { encodeCultNetMessageForWire, parseCultNetMessage, type CultNetDocumentDeleteMessage, type CultNetDocumentPutMessage, type CultNetErrorMessage, type CultNetHelloMessage, type CultNetLoginMessage, type CultNetLoginSuccessMessage, type CultNetMessage, type CultNetRegisterMessage, type CultNetSampleChangeNameMessage, type CultNetSampleChatMessage, type CultNetSchemaCatalogRequestMessage, type CultNetSchemaCatalogResponseMessage, type CultNetSnapshotRequestMessage, type CultNetSnapshotResponseMessage, type CultNetTransportProfile, type CultNetVerifyMessage, type CultNetWireContract } from "./contracts";
|
||||
import { encodeFrame, LengthPrefixedMessageFramer } from "./framing";
|
||||
import type { CultNetTransportConnection } from "./transport";
|
||||
import { createTcpFramedTransportProfile, TcpFramedTransportConnection, type CultNetTransportConnection, type TcpFramedTransportProfileOptions } from "./transport";
|
||||
|
||||
export interface CultNetPeerEvents {
|
||||
message: (message: CultNetMessage) => void;
|
||||
|
|
@ -18,6 +18,36 @@ export interface CultNetPeerOptions {
|
|||
wireContract: CultNetWireContract;
|
||||
}
|
||||
|
||||
export interface TcpFramedCultNetPeerOptions extends CultNetPeerOptions, TcpFramedTransportProfileOptions {
|
||||
runtimeId: string;
|
||||
}
|
||||
|
||||
export function createTcpFramedCultNetPeer(
|
||||
stream: Duplex,
|
||||
options: TcpFramedCultNetPeerOptions,
|
||||
): CultNetPeer {
|
||||
const {
|
||||
runtimeId,
|
||||
wireContract,
|
||||
transportId,
|
||||
host,
|
||||
port,
|
||||
maxPayloadBytes,
|
||||
maxFragmentBytes,
|
||||
} = options;
|
||||
const transport = new TcpFramedTransportConnection(
|
||||
stream,
|
||||
createTcpFramedTransportProfile(runtimeId, {
|
||||
transportId,
|
||||
host,
|
||||
port,
|
||||
maxPayloadBytes,
|
||||
maxFragmentBytes,
|
||||
}),
|
||||
);
|
||||
return new CultNetPeer(transport, { wireContract });
|
||||
}
|
||||
|
||||
export class CultNetPeer extends EventEmitter {
|
||||
readonly #stream?: Duplex;
|
||||
readonly #transport?: CultNetTransportConnection;
|
||||
|
|
@ -49,6 +79,10 @@ export class CultNetPeer extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
get transportProfile(): CultNetTransportProfile | undefined {
|
||||
return this.#transport?.profile;
|
||||
}
|
||||
|
||||
send(message: CultNetMessage): void {
|
||||
const wireValue = encodeCultNetMessageForWire(message, this.#wireContract);
|
||||
const payload = encode(wireValue);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
cultNetBuiltinSchemaRegistry,
|
||||
computeCultNetReconnectDelayMs,
|
||||
createCultNetReconnectPolicy,
|
||||
createTcpFramedCultNetPeer,
|
||||
createTcpFramedTransportProfile,
|
||||
createRudpTransportProfile,
|
||||
defineCultNetDocumentBinding,
|
||||
|
|
@ -734,6 +735,48 @@ test("CultNet peer can speak through a transport connection", async () => {
|
|||
receiver.close();
|
||||
});
|
||||
|
||||
test("CultNet TCP helper constructs a transport-backed peer with profile metadata", async () => {
|
||||
const { a, b } = createDuplexPair();
|
||||
const sender = createTcpFramedCultNetPeer(a, {
|
||||
runtimeId: "tcp-helper-left",
|
||||
wireContract: "cultnet.schema.v0",
|
||||
transportId: "helper-left",
|
||||
host: "127.0.0.1",
|
||||
port: 17777,
|
||||
maxPayloadBytes: 64 * 1024,
|
||||
});
|
||||
const receiver = createTcpFramedCultNetPeer(b, {
|
||||
runtimeId: "tcp-helper-right",
|
||||
wireContract: "cultnet.schema.v0",
|
||||
});
|
||||
|
||||
assert.equal(sender.transportProfile?.runtimeId, "tcp-helper-left");
|
||||
assert.equal(sender.transportProfile?.transports[0]?.transportId, "helper-left");
|
||||
assert.equal(sender.transportProfile?.transports[0]?.protocol, "tcp_framed");
|
||||
assert.equal(sender.transportProfile?.transports[0]?.host, "127.0.0.1");
|
||||
assert.equal(sender.transportProfile?.transports[0]?.port, 17777);
|
||||
assert.equal(sender.transportProfile?.transports[0]?.channels[0]?.channelId, "schema");
|
||||
assert.equal(sender.transportProfile?.transports[0]?.channels[0]?.maxPayloadBytes, 64 * 1024);
|
||||
|
||||
const message = await new Promise<ReturnType<typeof parseCultNetMessage>>((resolve, reject) => {
|
||||
receiver.once("message", resolve);
|
||||
receiver.once("invalidMessage", reject);
|
||||
sender.sendHello({
|
||||
schemaVersion: "cultnet.hello.v0",
|
||||
runtimeId: "tcp-helper-sender",
|
||||
runtimeKind: "node-worker",
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(message.schemaVersion, "cultnet.hello.v0");
|
||||
if (message.schemaVersion === "cultnet.hello.v0") {
|
||||
assert.equal(message.runtimeId, "tcp-helper-sender");
|
||||
}
|
||||
|
||||
sender.close();
|
||||
receiver.close();
|
||||
});
|
||||
|
||||
test("CultNet can round-trip gamecult.networking.v0 auth messages through the explicit legacy contract", () => {
|
||||
const message: CultNetLoginMessage = {
|
||||
schemaVersion: "cultnet.login.v0",
|
||||
|
|
|
|||
Loading…
Reference in a new issue