diff --git a/README.md b/README.md index 013954e5..aee7644d 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ start with the maintained [`CultMesh And Eve Getting Started`](src/GameCult.Mesh/docs/getting-started/README.md) series. Its first executable package-consumer checkpoint is [`samples/eve-two-runtime`](samples/eve-two-runtime/README.md). +The missing real-browser/network boundary is mapped explicitly in +[`browser-verse-transport.md`](src/GameCult.Mesh/docs/browser-verse-transport.md). If you want only the smallest durable "open node -> put typed doc -> get typed doc" path, use diff --git a/packages/cultnet-ts/contracts/cultnet.database-change-raw.schema.json b/packages/cultnet-ts/contracts/cultnet.database-change-raw.schema.json new file mode 100644 index 00000000..9bedb72a --- /dev/null +++ b/packages/cultnet-ts/contracts/cultnet.database-change-raw.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.database-change-raw.schema.json", + "title": "CultNet Raw Database Change Message", + "type": "object", + "required": ["schemaVersion", "messageId", "subscriptionId", "changeKind"], + "additionalProperties": false, + "properties": { + "schemaVersion": { "const": "cultnet.database_change_raw.v0" }, + "messageId": { "type": "string", "minLength": 1 }, + "subscriptionId": { "type": "string", "minLength": 1 }, + "changeKind": { "enum": ["added", "updated", "removed"] }, + "document": { "$ref": "./cultnet.raw-document-record.schema.json" }, + "recordKey": { "type": "string", "minLength": 1 }, + "schemaId": { "type": "string", "minLength": 1 } + }, + "allOf": [ + { + "if": { "properties": { "changeKind": { "const": "removed" } } }, + "then": { "required": ["recordKey", "schemaId"] }, + "else": { "required": ["document"] } + } + ] +} + diff --git a/packages/cultnet-ts/contracts/cultnet.database-subscribe.schema.json b/packages/cultnet-ts/contracts/cultnet.database-subscribe.schema.json new file mode 100644 index 00000000..d4b65fcc --- /dev/null +++ b/packages/cultnet-ts/contracts/cultnet.database-subscribe.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.database-subscribe.schema.json", + "title": "CultNet Database Subscribe Message", + "type": "object", + "required": ["schemaVersion", "messageId", "subscriptionId", "includeSnapshot"], + "additionalProperties": false, + "properties": { + "schemaVersion": { "const": "cultnet.database_subscribe.v0" }, + "messageId": { "type": "string", "minLength": 1 }, + "subscriptionId": { "type": "string", "minLength": 1 }, + "schemaIds": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "recordKeys": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "includeSnapshot": { "type": "boolean" }, + "consumerRuntimeId": { "type": "string", "minLength": 1 }, + "bodyIds": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "supportedBodyTransports": { "type": "array", "items": { "type": "string", "minLength": 1 } } + } +} + diff --git a/packages/cultnet-ts/contracts/cultnet.database-unsubscribe.schema.json b/packages/cultnet-ts/contracts/cultnet.database-unsubscribe.schema.json new file mode 100644 index 00000000..04be3799 --- /dev/null +++ b/packages/cultnet-ts/contracts/cultnet.database-unsubscribe.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.database-unsubscribe.schema.json", + "title": "CultNet Database Unsubscribe Message", + "type": "object", + "required": ["schemaVersion", "messageId", "subscriptionId"], + "additionalProperties": false, + "properties": { + "schemaVersion": { "const": "cultnet.database_unsubscribe.v0" }, + "messageId": { "type": "string", "minLength": 1 }, + "subscriptionId": { "type": "string", "minLength": 1 } + } +} + diff --git a/packages/cultnet-ts/src/contracts.ts b/packages/cultnet-ts/src/contracts.ts index 8795b06a..56ece383 100644 --- a/packages/cultnet-ts/src/contracts.ts +++ b/packages/cultnet-ts/src/contracts.ts @@ -18,6 +18,9 @@ import transportProfileSchema from "../contracts/cultnet.transport-profile.schem import snapshotRequestSchema from "../contracts/cultnet.snapshot-request.schema.json"; import snapshotResponseSchema from "../contracts/cultnet.snapshot-response.schema.json"; import snapshotResponseRawSchema from "../contracts/cultnet.snapshot-response-raw.schema.json"; +import databaseSubscribeSchema from "../contracts/cultnet.database-subscribe.schema.json"; +import databaseUnsubscribeSchema from "../contracts/cultnet.database-unsubscribe.schema.json"; +import databaseChangeRawSchema from "../contracts/cultnet.database-change-raw.schema.json"; import schemaDescriptorSchema from "../contracts/cultnet.schema-descriptor.schema.json"; import schemaCatalogRequestSchema from "../contracts/cultnet.schema-catalog-request.schema.json"; import schemaCatalogResponseSchema from "../contracts/cultnet.schema-catalog-response.schema.json"; @@ -44,6 +47,9 @@ export type CultNetSchemaVersion = | "cultnet.snapshot_request.v0" | "cultnet.snapshot_response.v0" | "cultnet.snapshot_response_raw.v0" + | "cultnet.database_subscribe.v0" + | "cultnet.database_unsubscribe.v0" + | "cultnet.database_change_raw.v0" | "cultnet.schema_catalog_request.v0" | "cultnet.schema_catalog_response.v0" | "cultnet.shard_catalog_request.v0" @@ -217,6 +223,34 @@ export interface CultNetSnapshotResponseRawMessage { documents: CultNetRawDocumentRecord[]; } +export interface CultNetDatabaseSubscribeMessage { + schemaVersion: "cultnet.database_subscribe.v0"; + messageId: string; + subscriptionId: string; + schemaIds?: string[]; + recordKeys?: string[]; + includeSnapshot: boolean; + consumerRuntimeId?: string; + bodyIds?: string[]; + supportedBodyTransports?: string[]; +} + +export interface CultNetDatabaseUnsubscribeMessage { + schemaVersion: "cultnet.database_unsubscribe.v0"; + messageId: string; + subscriptionId: string; +} + +export interface CultNetDatabaseChangeRawMessage { + schemaVersion: "cultnet.database_change_raw.v0"; + messageId: string; + subscriptionId: string; + changeKind: "added" | "updated" | "removed"; + document?: CultNetRawDocumentRecord; + recordKey?: string; + schemaId?: string; +} + export interface CultNetSchemaDescriptor { schemaId: string; kind: CultNetSchemaKind; @@ -535,6 +569,9 @@ export type CultNetMessage = | CultNetSnapshotRequestMessage | CultNetSnapshotResponseMessage | CultNetSnapshotResponseRawMessage + | CultNetDatabaseSubscribeMessage + | CultNetDatabaseUnsubscribeMessage + | CultNetDatabaseChangeRawMessage | CultNetSchemaCatalogRequestMessage | CultNetSchemaCatalogResponseMessage | CultNetShardCatalogRequestMessage @@ -562,6 +599,9 @@ const CULTNET_MESSAGE_SCHEMAS = [ snapshotRequestSchema, snapshotResponseSchema, snapshotResponseRawSchema, + databaseSubscribeSchema, + databaseUnsubscribeSchema, + databaseChangeRawSchema, schemaCatalogRequestSchema, schemaCatalogResponseSchema, shardCatalogRequestSchema, @@ -620,6 +660,11 @@ export function parseCultNetMessage(input: unknown, wireContract: CultNetWireCon return input as CultNetSnapshotResponseRawMessage; } + if (schemaVersion === "cultnet.database_change_raw.v0") { + validateDatabaseChangeRawMessage(input); + return input as CultNetDatabaseChangeRawMessage; + } + if (!validator(input)) { const keys = Object.keys(input as Record).join(","); throw new Error(`${renderValidationErrors(schemaVersion, validator)}; keys=${keys}`); @@ -951,9 +996,39 @@ function validateSnapshotResponseRawMessage( }); } +function validateDatabaseChangeRawMessage( + input: unknown, +): asserts input is CultNetDatabaseChangeRawMessage { + const candidate = validateBinaryMessageRoot(input, "cultnet.database_change_raw.v0"); + requireNonEmptyString(candidate.subscriptionId, "/subscriptionId"); + if (candidate.changeKind !== "added" && + candidate.changeKind !== "updated" && + candidate.changeKind !== "removed") { + throw new Error( + "Validation failed for cultnet.database_change_raw.v0: /changeKind: must be added, updated, or removed", + ); + } + + if (candidate.changeKind === "removed") { + requireNonEmptyString(candidate.recordKey, "/recordKey"); + requireNonEmptyString(candidate.schemaId, "/schemaId"); + return; + } + + if (!("document" in candidate)) { + throw new Error( + "Validation failed for cultnet.database_change_raw.v0: /document: is required for added or updated changes", + ); + } + validateRawDocumentRecord(candidate.document, "/document"); +} + function validateBinaryMessageRoot( input: unknown, - schemaVersion: "cultnet.document_put_raw.v0" | "cultnet.snapshot_response_raw.v0", + schemaVersion: + | "cultnet.document_put_raw.v0" + | "cultnet.snapshot_response_raw.v0" + | "cultnet.database_change_raw.v0", ): Record { if (!input || typeof input !== "object") { throw new Error(`Validation failed for ${schemaVersion}: /: must be an object`); @@ -1043,6 +1118,15 @@ function normalizeCultNetOptionalNulls( acceptLegacyPropertyName(candidate, "ShardEpoch", "shardEpoch"); stripNullProperties(candidate, ["schemaIds", "recordKeys", "shardId", "shardEpoch"]); return; + case "cultnet.database_subscribe.v0": + stripNullProperties(candidate, [ + "schemaIds", + "recordKeys", + "consumerRuntimeId", + "bodyIds", + "supportedBodyTransports", + ]); + return; case "cultnet.schema_catalog_request.v0": stripNullProperties(candidate, ["schemaIds", "kinds"]); return; @@ -1092,6 +1176,9 @@ export const cultNetSchemas = { snapshotRequestSchema, snapshotResponseSchema, snapshotResponseRawSchema, + databaseSubscribeSchema, + databaseUnsubscribeSchema, + databaseChangeRawSchema, schemaDescriptorSchema, schemaCatalogRequestSchema, schemaCatalogResponseSchema, diff --git a/packages/cultnet-ts/src/index.ts b/packages/cultnet-ts/src/index.ts index cd602952..64d9c2fd 100644 --- a/packages/cultnet-ts/src/index.ts +++ b/packages/cultnet-ts/src/index.ts @@ -8,6 +8,9 @@ export { type CultNetDocumentPutMessage, type CultNetDocumentPutRawMessage, type CultNetDocumentRecord, + type CultNetDatabaseSubscribeMessage, + type CultNetDatabaseUnsubscribeMessage, + type CultNetDatabaseChangeRawMessage, type CultNetRawDocumentRecord, type CultNetRawPayloadEncoding, type CultNetErrorMessage, diff --git a/packages/cultnet-ts/test/cultnet.test.ts b/packages/cultnet-ts/test/cultnet.test.ts index 46e7cb6f..1a2ba805 100644 --- a/packages/cultnet-ts/test/cultnet.test.ts +++ b/packages/cultnet-ts/test/cultnet.test.ts @@ -1261,6 +1261,64 @@ test("operation envelopes preserve typed service routing and payload correlation assert.equal(response.messageId, request.messageId); }); +test("database subscription contracts match the C# live document lane", () => { + const subscribe = parseCultNetMessage({ + schemaVersion: "cultnet.database_subscribe.v0", + messageId: "subscribe-counter", + subscriptionId: "counter:browser-1", + schemaIds: ["sample.counter_state.v1"], + recordKeys: ["counter:main"], + includeSnapshot: true, + consumerRuntimeId: "browser-1", + }); + assert.equal(subscribe.schemaVersion, "cultnet.database_subscribe.v0"); + assert.equal(subscribe.subscriptionId, "counter:browser-1"); + + const added = parseCultNetMessage({ + schemaVersion: "cultnet.database_change_raw.v0", + messageId: "change-counter-1", + subscriptionId: "counter:browser-1", + changeKind: "added", + document: { + schemaId: "sample.counter_state.v1", + recordKey: "counter:main", + storedAt: "2026-08-17T00:00:00Z", + payloadEncoding: "messagepack", + payload: new Uint8Array([0x81, 0xa5, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x01]), + }, + }); + assert.equal(added.schemaVersion, "cultnet.database_change_raw.v0"); + assert.equal(added.document?.recordKey, "counter:main"); + + const removed = parseCultNetMessage({ + schemaVersion: "cultnet.database_change_raw.v0", + messageId: "change-counter-2", + subscriptionId: "counter:browser-1", + changeKind: "removed", + recordKey: "counter:main", + schemaId: "sample.counter_state.v1", + }); + assert.equal(removed.schemaVersion, "cultnet.database_change_raw.v0"); + assert.equal(removed.recordKey, "counter:main"); + + const unsubscribe = parseCultNetMessage({ + schemaVersion: "cultnet.database_unsubscribe.v0", + messageId: "unsubscribe-counter", + subscriptionId: "counter:browser-1", + }); + assert.equal(unsubscribe.schemaVersion, "cultnet.database_unsubscribe.v0"); + + assert.throws( + () => parseCultNetMessage({ + schemaVersion: "cultnet.database_change_raw.v0", + messageId: "invalid-change", + subscriptionId: "counter:browser-1", + changeKind: "updated", + }), + /document: is required/, + ); +}); + test("RUDP operation service correlates typed payload envelopes", async () => { const server = await startCultNetOperationServer({ runtimeId: "sai-sidecar", diff --git a/src/GameCult.Mesh/docs/browser-verse-transport.md b/src/GameCult.Mesh/docs/browser-verse-transport.md new file mode 100644 index 00000000..c8586021 --- /dev/null +++ b/src/GameCult.Mesh/docs/browser-verse-transport.md @@ -0,0 +1,156 @@ +# Browser Verse Transport Boundary + +Date: 2026-08-17 + +## Status + +This is the implementation map for the missing real-browser checkpoint. The +current `samples/eve-two-runtime` proof uses jsdom and one local TypeScript +CultMesh node. It is a clean package-consumer and contract-validation proof, +not browser networking. + +The current substrate is incomplete: + +- `cultmesh-ts` is a Node package and imports `node:dgram` at its root; +- `cultnet-ts` mixes browser-safe schema contracts with Node transport, + filesystem, crypto, stream, and UDP modules; +- C# and TypeScript now share `cultnet.database_subscribe.v0`, + `cultnet.database_unsubscribe.v0`, and `cultnet.database_change_raw.v0`; + the browser-safe transport package that consumes them does not exist yet; +- Kotlin has a channel-aware WebSocket transport adapter; C# and TypeScript do + not expose the equivalent browser/server pair; +- Hermodr can lower Odin state into a browser, but it is an infrastructure + runtime, not the missing clean-consumer game API. + +## Authority Map + +Owner: + +- CultNet owns binary WebSocket framing and schema-v0 message validation. +- CultMesh owns stable Verse/provider identity, document leases, + subscriptions, route replacement, and command/receipt correlation. +- Odin owns discovery of the current `wss://` candidate for a stable Verse and + provider identity. +- The provider daemon owns application state, operations, receipts, and the Eve + surface. +- Eve owns surface and command contracts; the browser lowerer owns DOM only. + +Inputs: + +- an Odin-discovered WebSocket transport candidate; +- stable Verse and provider identities; +- authenticated session evidence; +- requested schema ids and record keys; +- typed operation invocations and idempotency keys; +- canonical CultNet schema-v0 MessagePack messages. + +Outputs: + +- leased typed document snapshots and live changes; +- typed operation receipts; +- explicit connection/session diagnostics; +- deterministic unsubscribe and disposal; +- route replacement without changing provider or document identity. + +Derived state: + +- browser caches, DOM trees, loading/error presentation, and optimistic visual + affordances are projections only; +- a socket URL is a disposable discovery candidate, never provider identity; +- reconnect status is observable session state, not application truth. + +Forbidden writers: + +- browser lowerers cannot mutate provider documents directly; +- Hermodr, service workers, renderer caches, and app-specific HTTP endpoints + cannot become alternate gameplay authorities; +- no JSON polling API may mirror the database as a second state system; +- `node:dgram`, Node streams, filesystem stores, and Node crypto cannot enter a + browser bundle; +- a late socket, stale subscription generation, or reconnect replay cannot + overwrite a newer session; +- application code cannot own reconnect sleeps, endpoint ranking, or + resubscription loops. + +Shared paths: + +- initial load, live update, reconnect, tab background/resume, command retry, + and provider route rotation all use one CultMesh document-lease/session path; +- browser, Unity, headless C#, and future native clients consume the same + provider-owned documents and receipts; +- every command crosses the same typed operation boundary regardless of which + Eve lowerer emitted it. + +Deletion line: + +- stop calling jsdom a browser runtime; +- split browser-safe CultNet contracts from the Node transport package; +- add the missing subscription messages to the shared generated contract + catalog instead of retyping them in a browser adapter; +- do not retain a sample-only WebSocket protocol after the canonical adapter + exists. + +## Required Runtime Shape + +The browser-facing package must be importable by a standards-mode browser or a +normal bundler without Node polyfills. It exposes a narrow API shaped like: + +```ts +const mesh = await CultMeshBrowser.connect({ + rendezvous: odin, + verseId: "sample.counter", + providerId: "sample.counter-provider", +}); + +using surface = await mesh.leaseDocument(eveSurface, "eve:surface:counter"); +surface.watch(render); + +const receipt = await mesh.invoke(incrementCounter, {}, { + idempotencyKey: crypto.randomUUID(), +}); +``` + +The names are illustrative. The ownership is not: application code supplies +identity and intent; CultMesh owns route/session continuity; the provider owns +the result. + +## Wire Contract + +WebSocket carries binary MessagePack CultNet schema-v0 messages. The first +implementation must support: + +- `cultnet.hello.v0`; +- authentication/session verification; +- `cultnet.snapshot_request.v0` and + `cultnet.snapshot_response_raw.v0`; +- `cultnet.database_subscribe.v0`, + `cultnet.database_unsubscribe.v0`, and + `cultnet.database_change_raw.v0`; +- `cultnet.operation_request.v0` and + `cultnet.operation_response.v0`; +- `cultnet.error.v0`. + +The WebSocket frame is a transport body, not a second message schema. Payload +size, queue depth, subscription count, and command rate are bounded. Schema, +identity, authority, and idempotency validation happen before provider code. + +## Acceptance Gate + +From an empty temporary consumer using packed/released artifacts only: + +1. start a provider daemon with one durable counter document and one validated + Eve surface; +2. advertise it by stable identity through a local Odin fixture or real Odin; +3. connect one real Chromium page and one C# headless client; +4. lease the same surface and counter document in both runtimes; +5. click the browser button and require one provider-authored canonical receipt; +6. observe the same state version and receipt id in both clients; +7. disconnect or rotate the physical WebSocket endpoint, then require both + leases to resume without application reconnect code; +8. restart the provider and prove state plus receipt persistence; +9. reject malformed schemas, direct document writes, duplicate command + effects, stale subscription generations, and unsupported authority; +10. inspect the browser bundle and fail if it contains Node builtin imports. + +The gate runs on Windows and Linux. A screenshot is useful presentation +evidence, but the receipt/state chronology is the authority proof. diff --git a/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md b/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md index d28ef621..d16f21c1 100644 --- a/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md +++ b/src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md @@ -65,5 +65,7 @@ This checkpoint proves clean package consumption, canonical Eve contract validation, and two independent lowering/observation consumers inside Node. It does **not** claim a real browser process, an Odin-discovered network hop, or cross-language command execution. Those are the next public Verse conformance -gates; do not cite this sample as evidence that they exist. +gates; do not cite this sample as evidence that they exist. Their owner map and +executable acceptance contract are recorded in +[`browser-verse-transport.md`](../browser-verse-transport.md). diff --git a/src/GameCult.Networking/Contracts/cultnet.database-change-raw.schema.json b/src/GameCult.Networking/Contracts/cultnet.database-change-raw.schema.json new file mode 100644 index 00000000..9bedb72a --- /dev/null +++ b/src/GameCult.Networking/Contracts/cultnet.database-change-raw.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.database-change-raw.schema.json", + "title": "CultNet Raw Database Change Message", + "type": "object", + "required": ["schemaVersion", "messageId", "subscriptionId", "changeKind"], + "additionalProperties": false, + "properties": { + "schemaVersion": { "const": "cultnet.database_change_raw.v0" }, + "messageId": { "type": "string", "minLength": 1 }, + "subscriptionId": { "type": "string", "minLength": 1 }, + "changeKind": { "enum": ["added", "updated", "removed"] }, + "document": { "$ref": "./cultnet.raw-document-record.schema.json" }, + "recordKey": { "type": "string", "minLength": 1 }, + "schemaId": { "type": "string", "minLength": 1 } + }, + "allOf": [ + { + "if": { "properties": { "changeKind": { "const": "removed" } } }, + "then": { "required": ["recordKey", "schemaId"] }, + "else": { "required": ["document"] } + } + ] +} + diff --git a/src/GameCult.Networking/Contracts/cultnet.database-subscribe.schema.json b/src/GameCult.Networking/Contracts/cultnet.database-subscribe.schema.json new file mode 100644 index 00000000..d4b65fcc --- /dev/null +++ b/src/GameCult.Networking/Contracts/cultnet.database-subscribe.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.database-subscribe.schema.json", + "title": "CultNet Database Subscribe Message", + "type": "object", + "required": ["schemaVersion", "messageId", "subscriptionId", "includeSnapshot"], + "additionalProperties": false, + "properties": { + "schemaVersion": { "const": "cultnet.database_subscribe.v0" }, + "messageId": { "type": "string", "minLength": 1 }, + "subscriptionId": { "type": "string", "minLength": 1 }, + "schemaIds": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "recordKeys": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "includeSnapshot": { "type": "boolean" }, + "consumerRuntimeId": { "type": "string", "minLength": 1 }, + "bodyIds": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "supportedBodyTransports": { "type": "array", "items": { "type": "string", "minLength": 1 } } + } +} + diff --git a/src/GameCult.Networking/Contracts/cultnet.database-unsubscribe.schema.json b/src/GameCult.Networking/Contracts/cultnet.database-unsubscribe.schema.json new file mode 100644 index 00000000..04be3799 --- /dev/null +++ b/src/GameCult.Networking/Contracts/cultnet.database-unsubscribe.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.database-unsubscribe.schema.json", + "title": "CultNet Database Unsubscribe Message", + "type": "object", + "required": ["schemaVersion", "messageId", "subscriptionId"], + "additionalProperties": false, + "properties": { + "schemaVersion": { "const": "cultnet.database_unsubscribe.v0" }, + "messageId": { "type": "string", "minLength": 1 }, + "subscriptionId": { "type": "string", "minLength": 1 } + } +} + diff --git a/src/GameCult.Networking/CultNetSchemaRegistry.cs b/src/GameCult.Networking/CultNetSchemaRegistry.cs index 037c0d84..4fc68a13 100644 --- a/src/GameCult.Networking/CultNetSchemaRegistry.cs +++ b/src/GameCult.Networking/CultNetSchemaRegistry.cs @@ -370,6 +370,27 @@ namespace GameCult.Networking WireContracts = new[] { CultNetWireContracts.SchemaV0 } }, new SchemaResourceSpec + { + FileName = "cultnet.database-subscribe.schema.json", + Kind = "wire_message", + SchemaVersion = CultNetSchemaVersions.DatabaseSubscribe, + WireContracts = new[] { CultNetWireContracts.SchemaV0 } + }, + new SchemaResourceSpec + { + FileName = "cultnet.database-unsubscribe.schema.json", + Kind = "wire_message", + SchemaVersion = CultNetSchemaVersions.DatabaseUnsubscribe, + WireContracts = new[] { CultNetWireContracts.SchemaV0 } + }, + new SchemaResourceSpec + { + FileName = "cultnet.database-change-raw.schema.json", + Kind = "wire_message", + SchemaVersion = CultNetSchemaVersions.DatabaseChangeRaw, + WireContracts = new[] { CultNetWireContracts.SchemaV0 } + }, + new SchemaResourceSpec { FileName = "cultnet.schema-descriptor.schema.json", Kind = "shared_contract", diff --git a/tests/GameCult.Networking.Tests/NetworkingTests.cs b/tests/GameCult.Networking.Tests/NetworkingTests.cs index 9b1af662..44437cc3 100644 --- a/tests/GameCult.Networking.Tests/NetworkingTests.cs +++ b/tests/GameCult.Networking.Tests/NetworkingTests.cs @@ -5282,6 +5282,12 @@ namespace GameCult.Networking.Tests schema => schema.SchemaVersion == CultNetSchemaVersions.DocumentPutRaw); var rawSnapshot = Array.Find(response.Schemas, schema => schema.SchemaVersion == CultNetSchemaVersions.SnapshotResponseRaw); + var subscribe = Array.Find(response.Schemas, + schema => schema.SchemaVersion == CultNetSchemaVersions.DatabaseSubscribe); + var unsubscribe = Array.Find(response.Schemas, + schema => schema.SchemaVersion == CultNetSchemaVersions.DatabaseUnsubscribe); + var change = Array.Find(response.Schemas, + schema => schema.SchemaVersion == CultNetSchemaVersions.DatabaseChangeRaw); var ghostlight = Array.Find(response.Schemas, schema => schema.SchemaVersion == "ghostlight.agent_state.v0"); var transportProfile = Array.Find(response.Schemas, @@ -5295,6 +5301,13 @@ namespace GameCult.Networking.Tests Assert.That(rawSnapshot, Is.Not.Null); Assert.That(rawSnapshot!.SchemaJson, Does.Contain("cultnet.snapshot_response_raw.v0")); + Assert.That(subscribe, Is.Not.Null); + Assert.That(subscribe!.SchemaJson, Does.Contain("cultnet.database_subscribe.v0")); + Assert.That(unsubscribe, Is.Not.Null); + Assert.That(unsubscribe!.SchemaJson, Does.Contain("cultnet.database_unsubscribe.v0")); + Assert.That(change, Is.Not.Null); + Assert.That(change!.SchemaJson, Does.Contain("cultnet.database_change_raw.v0")); + Assert.That(ghostlight, Is.Not.Null); Assert.That(ghostlight!.DocumentType, Is.EqualTo("ghostlight.agent-state")); Assert.That(ghostlight.Kind, Is.EqualTo("document_payload"));