diff --git a/docs/cultnet-transport-parity.md b/docs/cultnet-transport-parity.md index 4fd323b8..3672d9c7 100644 --- a/docs/cultnet-transport-parity.md +++ b/docs/cultnet-transport-parity.md @@ -224,6 +224,11 @@ Current progress: media, sensor, tensor, and byte streams: stream declaration, clock metadata, preferred body transports, consumer negotiation, copy-budget classification, max in-flight frame pressure, and latest-frame cursors. +- Kotlin now has a branded `CultMesh` facade matching the C#/TypeScript/Python + entrypoint pattern for local nodes, Verse catalogs, peer catalogs, authority + lease catalogs, stream catalogs, and RUDP client/server construction. The + facade delegates to the same owners as the top-level helpers instead of + creating a second authority path. - The remaining parity work is to deepen Python's server-side use of its port and move each runtime's existing TCP/LiteNetLib/WebSocket bodies behind those ports, then broaden RUDP interop into reconnect policy and the wider diff --git a/packages/cultmesh-kotlin/README.md b/packages/cultmesh-kotlin/README.md index a8d14ca1..25db1a04 100644 --- a/packages/cultmesh-kotlin/README.md +++ b/packages/cultmesh-kotlin/README.md @@ -6,6 +6,16 @@ It provides typed MessagePack document codecs, a tiny WebSocket CultNet lane, a single-peer CultNet RUDP socket transport, stream catalog negotiation, an in-memory CultCache, and the first Eve dashboard/sensor document contracts. +The branded entrypoint mirrors C#/TypeScript/Python: + +```kotlin +val node = CultMesh.startNode() +val verses = CultMesh.createVerseCatalog() +val peers = CultMesh.createPeerCatalog() +val leases = CultMesh.createAuthorityLeaseCatalog() +val streams = CultMesh.createStreamCatalog() +``` + ## CultCache Happy Path Kotlin keeps the cache small but typed: document definitions wrap codecs, @@ -130,12 +140,12 @@ write client/server code without hand-binding sockets or repeating channel strings: ```kotlin -cultNetRudpServer( +CultMesh.createRudpServer( runtimeId = "kotlin-server", connectionId = 0x10203040, tuning = CultNetRudpSocketTuning(maxFragmentBytes = 1024), ).use { server -> - cultNetRudpClient( + CultMesh.createRudpClient( runtimeId = "kotlin-client", connectionId = 0x10203040, remoteHost = "127.0.0.1", @@ -225,7 +235,7 @@ val peer = CultMeshPeerCard( authorityLeaseId = "lease:kotlin-peer", ) -val leases = createAuthorityLeaseCatalog() +val leases = CultMesh.createAuthorityLeaseCatalog() leases.upsert( CultMeshAuthorityLease( leaseId = "lease:kotlin-peer", @@ -250,7 +260,7 @@ stream identity, clock metadata, body transport negotiation, and latest-frame cursors; it does not own the frame bytes themselves. ```kotlin -val streams = createStreamCatalog() +val streams = CultMesh.createStreamCatalog() streams.declare( CultMeshStreamDescriptor( streamId = "mimir:camera", diff --git a/packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt b/packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt index 7728c4a5..41fd61e0 100644 --- a/packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt +++ b/packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt @@ -268,6 +268,52 @@ class CultMeshNode( fun applyShardLogResponse(response: CultNetMessage): List = cache.applyShardLogResponse(response) } +object CultMesh { + fun createNode(cache: CultCache = CultCache()): CultMeshNode = CultMeshNode(cache) + + fun startNode(cache: CultCache = CultCache()): CultMeshNode = createNode(cache) + + fun createVerseCatalog(): CultMeshVerseCatalog = CultMeshVerseCatalog() + + fun createPeerCatalog(): CultMeshPeerCatalog = CultMeshPeerCatalog() + + fun createAuthorityLeaseCatalog(): CultMeshAuthorityLeaseCatalog = CultMeshAuthorityLeaseCatalog() + + fun createStreamCatalog(): CultMeshStreamCatalog = CultMeshStreamCatalog() + + fun createRudpServer( + runtimeId: String, + connectionId: Long, + bindHost: String = "127.0.0.1", + bindPort: Int = 0, + tuning: CultNetRudpSocketTuning = CultNetRudpSocketTuning(), + ): CultNetRudpSocketTransportConnection = cultNetRudpServer( + runtimeId = runtimeId, + connectionId = connectionId, + bindHost = bindHost, + bindPort = bindPort, + tuning = tuning, + ) + + fun createRudpClient( + runtimeId: String, + connectionId: Long, + remoteHost: String, + remotePort: Int, + bindHost: String = "127.0.0.1", + bindPort: Int = 0, + tuning: CultNetRudpSocketTuning = CultNetRudpSocketTuning(), + ): CultNetRudpSocketTransportConnection = cultNetRudpClient( + runtimeId = runtimeId, + connectionId = connectionId, + remoteHost = remoteHost, + remotePort = remotePort, + bindHost = bindHost, + bindPort = bindPort, + tuning = tuning, + ) +} + data class CultNetFrame(val opcode: Int, val payload: ByteArray) data class CultNetTransportStats( @@ -2236,6 +2282,7 @@ private fun nowMs(): Long = Instant.now().toEpochMilli() fun main(args: Array) { if (args.isEmpty()) { cultCacheErgonomicsCoverTypedDocumentsAndGlobals() + cultMeshFacadeRoutesErgonomicEntrypoints() cultCacheRawSnapshotsRoundTripThroughCultNetMessages() cultNetSchemaMessagesUseMessagePackMaps() cultNetSchemaCatalogsRoundTripDescriptors() @@ -2294,6 +2341,36 @@ private fun cultCacheErgonomicsCoverTypedDocumentsAndGlobals() { check(node.forget(notes, "node-note")) } +private fun cultMeshFacadeRoutesErgonomicEntrypoints() { + val notes = stringDocument("kotlin.facade_note", "kotlin.facade_note.v1") + val cache = CultCache() + cache.register(notes) + val node = CultMesh.startNode(cache) + node.remember(notes, "note:facade", "facade") + check(node.require(notes, "note:facade") == "facade") + + val verses = CultMesh.createVerseCatalog() + verses.upsert( + CultMeshVerseDescriptor( + verseId = "facade", + displayName = "Facade Verse", + authorityModel = "federated", + compatibility = CultMeshVerseCompatibility("cultmesh.v0", "rules"), + ), + ) + check(verses.get("facade")?.displayName == "Facade Verse") + + val peers = CultMesh.createPeerCatalog() + peers.upsert(CultMeshPeerCard("facade-peer", "facade", listOf("rudp://127.0.0.1:4100"), roles = listOf("schema"))) + check(peers.find("facade", "schema").single().peerId == "facade-peer") + + val leases = CultMesh.createAuthorityLeaseCatalog() + check(leases.leases.isEmpty()) + + val streams = CultMesh.createStreamCatalog() + check(streams.streams.isEmpty()) +} + private fun cultCacheRawSnapshotsRoundTripThroughCultNetMessages() { val notes = stringDocument("kotlin.note", "kotlin.note.v1") val settings = stringDocument("kotlin.settings", "kotlin.settings.v1", global = true) @@ -3036,12 +3113,12 @@ private fun rudpSessionFragmentsAndReassemblesReliableOrderedPayloads() { private fun rudpSocketTransportErgonomicFactoriesCarrySchemaFrames() { val connectionId = 0x10203042L - cultNetRudpServer( + CultMesh.createRudpServer( runtimeId = "kotlin-rudp-sugar-server", connectionId = connectionId, tuning = CultNetRudpSocketTuning(resendDelayMs = 25, maxFragmentBytes = 8, maxPendingReliablePackets = 16), ).use { server -> - cultNetRudpClient( + CultMesh.createRudpClient( runtimeId = "kotlin-rudp-sugar-client", connectionId = connectionId, remoteHost = "127.0.0.1",