Publish browser subscription wire contracts
This commit is contained in:
parent
54e4eacd13
commit
3fe2e9be1b
14 changed files with 462 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -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 } }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<string, unknown>).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<string, unknown> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ export {
|
|||
type CultNetDocumentPutMessage,
|
||||
type CultNetDocumentPutRawMessage,
|
||||
type CultNetDocumentRecord,
|
||||
type CultNetDatabaseSubscribeMessage,
|
||||
type CultNetDatabaseUnsubscribeMessage,
|
||||
type CultNetDatabaseChangeRawMessage,
|
||||
type CultNetRawDocumentRecord,
|
||||
type CultNetRawPayloadEncoding,
|
||||
type CultNetErrorMessage,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
156
src/GameCult.Mesh/docs/browser-verse-transport.md
Normal file
156
src/GameCult.Mesh/docs/browser-verse-transport.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -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 } }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
Loading…
Reference in a new issue