From 6e000a71a9bd319559cb4bb6d621d78c2e2aafdf Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 27 Jun 2026 14:35:08 +0100 Subject: [PATCH] Mirror CultMesh primitives in TypeScript --- packages/cultmesh-ts/README.md | 148 + packages/cultmesh-ts/src/index.ts | 2694 +++++++++++++++++ packages/cultmesh-ts/test/cultmesh.test.ts | 1127 +++++++ .../docs/cross-runtime-primitives-roadmap.md | 298 ++ 4 files changed, 4267 insertions(+) create mode 100644 src/GameCult.Mesh/docs/cross-runtime-primitives-roadmap.md diff --git a/packages/cultmesh-ts/README.md b/packages/cultmesh-ts/README.md index e0481ed7..3772ef92 100644 --- a/packages/cultmesh-ts/README.md +++ b/packages/cultmesh-ts/README.md @@ -11,6 +11,154 @@ frame body path. The TypeScript projection keeps the same local catalog ergonomics as the other runtimes: sorted views, direct lookup, and unsubscribe-able watches. +## Shared Primitives + +CultMesh primitives are shared cross-runtime developer affordances, not just +DTOs. They bundle the typed surface, metadata, routing hints, diagnostics, and +fast-path hooks that let application code feel like it is talking to native +state while CultMesh chooses the right local, shared-memory, IPC, network, or +WASM route. Runtime code can ask for a whole XY rect, then adapt it to the +current wire schema at the edge: + +```ts +import { CultMesh } from "cultmesh-ts"; + +const viewport = CultMesh.rectFromBounds(-500, -300, 500, 300); +const request = CultMesh.viewportRequest(viewport, [3, 8]); +``` + +The request currently projects to the compact `{ minX, minY, maxX, maxY }` +shape used by Aetheria viewport queries, while the authored code keeps the +designer-facing concept as a `CultMeshRect`. + +Typed operations follow the same rule: client code holds a method-shaped handle +and a runtime context, while CultMesh hides the route and authority metadata +from call sites that do not need to inspect it. + +```ts +const move = CultMesh.operation("game.pilot.move.v1", async (request, context) => { + return submitToBestRoute(request, context); +}); + +await move.invoke({ direction: CultMesh.vec2(1, 0) }, CultMesh.operationContext("rts-client", { + routeHint: CultMesh.routeHint("network", "starfire Verse"), +})); +``` + +Typed query surfaces can be read once or watched through the same handle. A +projection recipe keeps its source metadata, route preference, and reactive +watcher when it becomes a query surface: + +```ts +const objects = CultMesh.projectionRecipe( + "aetheria.zone.objects.visible", + [CultMesh.projectionSource("daemon:aetheria.frame.latest.v1")], + projectObjects, + { + routeHint: CultMesh.routeHint("shared-memory", "co-located frame slab"), + watchProjection: watchObjects, + }, +).asQuerySurface(); + +const stop = objects.watch( + { viewport: CultMesh.viewportRequest(CultMesh.rectFromBounds(-10, -10, 10, 10)) }, + "browser-starfire", + (next) => render(next), +); +``` + +State pointers are the same kind of managed surface for UI and tools. They can +advertise source documents, inherit a Verse route when bound, and resolve +through that Verse without caller-side context plumbing: + +```ts +const framePointer = CultMesh.statePointer( + "daemon:aetheria.frame.latest.v1", + async (context) => readFrameViaBestRoute(context), + undefined, + { + sources: [CultMesh.projectionSource("daemon:aetheria.frame.latest.v1")], + routeHint: CultMesh.routeHint("shared-memory", "co-located daemon frame"), + }, +); + +const frame = await CultMesh + .bindStatePointer(verse.withRoute("ipc", "Bifrost tool bridge"), framePointer) + .resolve(); + +const stateBinding = CultMesh.stateBinding("value", framePointer); +const stateBindingRecord = CultMesh.stateBindingRecord(stateBinding); +const restoredStateBinding = CultMesh.stateBindingFromRecord(stateBindingRecord); +``` + +Eve/CultUI lowerers that still receive authored state refs should use a named +state-ref resolver instead of local string utilities. Resolvers compose, carry +source and route metadata, and can be described by tools: + +```ts +const daemonRefs = CultMesh.stateRefResolver( + "aetheria.daemon.refs", + (stateRef, context) => resolveDaemonStateRef(stateRef, context), + { + sources: [CultMesh.projectionSource("daemon:aetheria.frame.latest.v1")], + routeHint: CultMesh.routeHint("shared-memory", "co-located daemon state"), + }, +); + +const catalogRefs = CultMesh.stateRefResolver( + "aetheria.item_stats.refs", + (stateRef) => resolveItemStatRef(stateRef), +); + +const resolver = daemonRefs.or(catalogRefs); +const value = resolver.resolve("aetheria:daemon:item-stat:laser.output"); +const diagnostic = CultMesh.describeStateRefResolver(resolver); +``` + +Operation surfaces should preserve typed CultMesh identity all the way through +UI/runtime boundaries. Use invocation records and payload records at persistence +edges instead of local route parsers or string-map conventions: + +```ts +const binding = CultMesh.operationBinding("gamecult.aetheria.pilot.set_target.v1", { + schemaId: "gamecult.aetheria.pilot.set_target.request.v1", + routeHint: CultMesh.routeHint("network", "co-op Verse"), +}); +const bindingRecord = CultMesh.operationBindingRecord(binding); +const restoredBinding = CultMesh.operationBindingFromRecord(bindingRecord); + +const invocation = CultMesh.operationInvocation(binding, { + idempotencyKey: "target:starfire:42", +}); +const invocationRecord = CultMesh.operationInvocationRecord(invocation); +const payloadRecord = CultMesh.operationPayload({ targetEntityKey: "bandit-7" }).toRecord(); +const restored = CultMesh.operationInvocationFromRecord(invocationRecord); +``` + +Route fields use the same shared primitive: + +```ts +const routeRecord = CultMesh.routeRecord(CultMesh.routeHint("shared-memory", "co-located slab")); +const route = CultMesh.routeFromRecord(routeRecord); +``` + +When a client needs one coherent live view composed from several query surfaces, +use a live feed instead of putting a refresh loop in the UI: + +```ts +const viewportFeed = CultMesh.liveFeed( + "aetheria.rts.viewport.feed", + snapshotViewport, + { + sources: objects.sources, + routeHint: objects.routeHint, + watchFeed: CultMesh.pollingQueryWatcher(snapshotViewport, { intervalMs: 50 }), + }, +); + +const stopFeed = viewportFeed.watch(request, "browser-starfire", render); +``` + ```ts import { CultMesh } from "cultmesh-ts"; diff --git a/packages/cultmesh-ts/src/index.ts b/packages/cultmesh-ts/src/index.ts index 53225705..b87167d8 100644 --- a/packages/cultmesh-ts/src/index.ts +++ b/packages/cultmesh-ts/src/index.ts @@ -29,6 +29,2056 @@ import { type CultNetWireContract, } from "cultnet-ts"; +export interface CultMeshVec2 { + readonly x: number; + readonly y: number; +} + +export interface CultMeshRect { + readonly min: CultMeshVec2; + readonly max: CultMeshVec2; +} + +export interface CultMeshViewportRequest { + readonly minX: number; + readonly minY: number; + readonly maxX: number; + readonly maxY: number; + readonly controlledEntityIndices?: readonly number[]; +} + +export type CultMeshLocalityKind = + | "automatic" + | "in-process" + | "shared-memory" + | "ipc" + | "network" + | "wasm"; + +export interface CultMeshRouteHint { + readonly kind: CultMeshLocalityKind; + readonly description?: string; +} + +export interface CultMeshRouteRecord { + readonly kind: string; + readonly description: string; +} + +export interface CultMeshAuthorityClaim { + readonly role: string; + readonly shardId?: string; + readonly leaseId?: string; +} + +export interface CultMeshVerseContext { + readonly verseId: string; + readonly runtimeId: string; + readonly routeHint: CultMeshRouteHint; + readonly claims: readonly CultMeshAuthorityClaim[]; +} + +export interface CultMeshOperationContext { + readonly runtimeId: string; + readonly claims: readonly CultMeshAuthorityClaim[]; + readonly routeHint: CultMeshRouteHint; + readonly idempotencyKey?: string; +} + +export interface CultMeshOperationReceipt { + readonly operationId: string; + readonly accepted: boolean; + readonly route: CultMeshRouteHint; + readonly diagnostic?: string; +} + +export interface CultMeshQueryContext { + readonly runtimeId: string; + readonly routeHint: CultMeshRouteHint; +} + +export type CultMeshUnsubscribe = () => void; + +export type CultMeshStatePointerResolver = ( + context: CultMeshQueryContext, +) => Promise; + +export type CultMeshStatePointerWatcher = ( + context: CultMeshQueryContext, + callback: (value: T) => void, +) => CultMeshUnsubscribe; + +export type CultMeshMutableStatePointerReplacer = ( + context: CultMeshQueryContext, + value: T, +) => Promise; + +export type CultMeshQueryWatcher = ( + parameters: TParameters, + context: CultMeshQueryContext, + callback: (value: TResult) => void, +) => CultMeshUnsubscribe; + +export type CultMeshLiveFeedWatcher = ( + parameters: TParameters, + context: CultMeshQueryContext, + callback: (value: TResult) => void, +) => CultMeshUnsubscribe; + +export interface CultMeshPollingWatchOptions { + readonly intervalMs?: number; + readonly emitInitial?: boolean; + readonly equals?: (left: TResult, right: TResult) => boolean; +} + +export interface CultMeshNativeSliceColumn { + readonly name: string; + readonly valueType: string; + readonly elementSizeBytes: number; +} + +export interface CultMeshNativeSliceViewDescriptor { + readonly viewId: string; + readonly schemaId: string; + readonly rowCount: number; + readonly columns: readonly CultMeshNativeSliceColumn[]; + readonly route: CultMeshRouteHint; + readonly nativeHandle?: string; +} + +export interface CultMeshProjectionSource { + readonly sourceId: string; + readonly schemaId?: string; + readonly description?: string; +} + +export interface CultMeshStateBindingDescriptor { + readonly targetProp: string; + readonly pointerId: string; + readonly sourceId?: string; + readonly schemaId?: string; + readonly routeHint: CultMeshRouteHint; +} + +export interface CultMeshStateBindingRecord { + readonly targetProp: string; + readonly pointerId: string; + readonly sourceId: string; + readonly schemaId: string; + readonly routeKind: string; + readonly routeDescription: string; +} + +export interface CultMeshOperationBindingDescriptor { + readonly operationId: string; + readonly label: string; + readonly schemaId: string; + readonly routeHint: CultMeshRouteHint; +} + +export interface CultMeshOperationBindingRecord { + readonly operationId: string; + readonly label: string; + readonly schemaId: string; + readonly routeKind: string; + readonly routeDescription: string; +} + +export interface CultMeshOperationInvocationDescriptor { + readonly operationId: string; + readonly schemaId: string; + readonly routeHint: CultMeshRouteHint; + readonly idempotencyKey?: string; +} + +export interface CultMeshOperationInvocationRecord { + readonly operationId: string; + readonly schemaId: string; + readonly routeKind: string; + readonly routeDescription: string; + readonly idempotencyKey: string; +} + +export interface CultMeshOperationPayload { + readonly fields: Readonly>; + getString(key: string, defaultValue?: string): string; + getInt(key: string, defaultValue?: number): number; + getDouble(key: string, defaultValue?: number): number; + getBoolean(key: string, defaultValue?: boolean): boolean; + with(key: string, value: string | number | boolean): CultMeshOperationPayload; + toRecord(): Readonly>; +} + +export interface CultMeshQuerySurfaceDiagnostic { + readonly queryId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; +} + +export interface CultMeshOperationHandleDiagnostic { + readonly operationId: string; +} + +export interface CultMeshStatePointerDiagnostic { + readonly pointerId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; +} + +export interface CultMeshStateRefResolverDiagnostic { + readonly resolverId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; +} + +export interface CultMeshProjectionRecipeDiagnostic { + readonly projectionId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; +} + +export interface CultMeshLiveFeedDiagnostic { + readonly feedId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; +} + +export type CultMeshSurfaceKind = + | "query" + | "projection-recipe" + | "live-feed" + | "operation" + | "state-pointer" + | "native-slice-view"; + +export interface CultMeshSurfaceDiagnostic { + readonly kind: CultMeshSurfaceKind; + readonly surfaceId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; +} + +export interface CultMeshSurfaceCatalogDiagnostic { + readonly catalogId: string; + readonly surfaces: readonly CultMeshSurfaceDiagnostic[]; +} + +export interface CultMeshSurfaceCatalogIndexDiagnostic { + readonly catalogId: string; + readonly queries: readonly CultMeshSurfaceDiagnostic[]; + readonly projectionRecipes: readonly CultMeshSurfaceDiagnostic[]; + readonly liveFeeds: readonly CultMeshSurfaceDiagnostic[]; + readonly operations: readonly CultMeshSurfaceDiagnostic[]; + readonly statePointers: readonly CultMeshSurfaceDiagnostic[]; + readonly nativeSliceViews: readonly CultMeshSurfaceDiagnostic[]; +} + +export interface CultMeshNativeSliceViewDiagnostic { + readonly viewId: string; + readonly schemaId: string; + readonly rowCount: number; + readonly columns: readonly CultMeshNativeSliceColumn[]; + readonly route: CultMeshRouteHint; + readonly nativeHandle?: string; + readonly denseRowStrideBytes: number; +} + +export class CultMeshOperationContextBuilder { + readonly #runtimeId: string; + #claims: CultMeshAuthorityClaim[] = []; + #routeHint: CultMeshRouteHint = cultMeshRouteHint(); + #idempotencyKey: string | undefined; + + public constructor(runtimeId: string) { + requireNonEmpty(runtimeId, "runtimeId"); + this.#runtimeId = runtimeId; + } + + public claim( + role: string, + options: { shardId?: string; leaseId?: string } = {}, + ): this { + this.#claims.push(cultMeshAuthorityClaim(role, options)); + return this; + } + + public claims(claims: readonly CultMeshAuthorityClaim[]): this { + this.#claims.push(...claims); + return this; + } + + public route(kind: CultMeshLocalityKind, description?: string): this { + this.#routeHint = cultMeshRouteHint(kind, description); + return this; + } + + public idempotency(key: string): this { + requireNonEmpty(key, "idempotencyKey"); + this.#idempotencyKey = key; + return this; + } + + public build(): CultMeshOperationContext { + return cultMeshOperationContext(this.#runtimeId, { + claims: this.#claims, + routeHint: this.#routeHint, + idempotencyKey: this.#idempotencyKey, + }); + } +} + +export class CultMeshQueryContextBuilder { + readonly #runtimeId: string; + #routeHint: CultMeshRouteHint = cultMeshRouteHint(); + + public constructor(runtimeId: string) { + requireNonEmpty(runtimeId, "runtimeId"); + this.#runtimeId = runtimeId; + } + + public route(kind: CultMeshLocalityKind, description?: string): this { + this.#routeHint = cultMeshRouteHint(kind, description); + return this; + } + + public build(): CultMeshQueryContext { + return cultMeshQueryContext(this.#runtimeId, { + routeHint: this.#routeHint, + }); + } +} + +export class CultMeshVerse { + public constructor(public readonly context: CultMeshVerseContext) { + requireNonEmpty(context.verseId, "verseId"); + requireNonEmpty(context.runtimeId, "runtimeId"); + } + + public get verseId(): string { + return this.context.verseId; + } + + public get runtimeId(): string { + return this.context.runtimeId; + } + + public use(schemaFactory: (context: CultMeshVerseContext) => TSchema): TSchema { + return schemaFactory(this.context); + } + + public withRoute(kind: CultMeshLocalityKind, description?: string): CultMeshVerse { + return new CultMeshVerse({ + ...this.context, + routeHint: cultMeshRouteHint(kind, description), + }); + } + + public withClaim(role: string, options: { shardId?: string; leaseId?: string } = {}): CultMeshVerse { + return new CultMeshVerse({ + ...this.context, + claims: [...this.context.claims, cultMeshAuthorityClaim(role, options)], + }); + } + + public operationContext(options: { idempotencyKey?: string } = {}): CultMeshOperationContext { + return cultMeshOperationContextFromVerse(this.context, options); + } + + public queryContext(): CultMeshQueryContext { + return cultMeshQueryContextFromVerse(this.context); + } + + public bindOperation( + operation: CultMeshOperationHandle, + ): CultMeshBoundOperationHandle { + return cultMeshBindOperation(this.context, operation); + } + + public bindQuery( + query: CultMeshQuerySurface, + ): CultMeshBoundQuerySurface { + return cultMeshBindQuery(this.context, query); + } + + public bindLiveFeed( + feed: CultMeshLiveFeed, + ): CultMeshBoundLiveFeed { + return cultMeshBindLiveFeed(this.context, feed); + } + + public bindStatePointer( + pointer: CultMeshStatePointer, + ): CultMeshBoundStatePointer { + return cultMeshBindStatePointer(this.context, pointer); + } + + public bindMutableStatePointer( + pointer: CultMeshMutableStatePointer, + ): CultMeshBoundMutableStatePointer { + return cultMeshBindMutableStatePointer(this.context, pointer); + } +} + +export class CultMeshOperationHandle { + public constructor( + public readonly operationId: string, + private readonly invokeOperation: ( + request: TRequest, + context: CultMeshOperationContext, + ) => Promise, + ) { + requireNonEmpty(operationId, "operationId"); + } + + public invoke( + request: TRequest, + context: CultMeshOperationContext | string, + ): Promise { + return this.invokeOperation( + request, + typeof context === "string" ? cultMeshOperationContext(context) : context, + ); + } + + public bind( + verse: CultMeshVerseContext | CultMeshVerse, + ): CultMeshBoundOperationHandle { + return cultMeshBindOperation(verse, this); + } +} + +export class CultMeshBoundOperationHandle { + public constructor( + public readonly verse: CultMeshVerseContext, + public readonly operation: CultMeshOperationHandle, + ) {} + + public get operationId(): string { + return this.operation.operationId; + } + + public invoke( + request: TRequest, + options: { idempotencyKey?: string } = {}, + ): Promise { + return this.operation.invoke( + request, + cultMeshOperationContextFromVerse(this.verse, options), + ); + } +} + +export class CultMeshQuerySurface { + public readonly sources: readonly CultMeshProjectionSource[]; + public readonly routeHint: CultMeshRouteHint; + + public constructor( + public readonly queryId: string, + private readonly executeQuery: ( + parameters: TParameters, + context: CultMeshQueryContext, + ) => Promise, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + watchQuery?: CultMeshQueryWatcher; + } = {}, + ) { + requireNonEmpty(queryId, "queryId"); + this.sources = [...(options.sources ?? [])]; + this.routeHint = options.routeHint ?? cultMeshRouteHint(); + this.watchQuery = options.watchQuery; + } + + private readonly watchQuery: CultMeshQueryWatcher | undefined; + + public execute( + parameters: TParameters, + context: CultMeshQueryContext | string, + ): Promise { + return this.executeQuery( + parameters, + typeof context === "string" ? cultMeshQueryContext(context) : context, + ); + } + + public query(parameters: TParameters, context: CultMeshQueryContext | string): Promise { + return this.execute(parameters, context); + } + + public watch( + parameters: TParameters, + context: CultMeshQueryContext | string, + callback: (value: TResult) => void, + ): CultMeshUnsubscribe { + if (!this.watchQuery) { + throw new Error(`Query surface '${this.queryId}' does not support watches.`); + } + + return this.watchQuery( + parameters, + typeof context === "string" ? cultMeshQueryContext(context) : context, + callback, + ); + } + + public bind( + verse: CultMeshVerseContext | CultMeshVerse, + ): CultMeshBoundQuerySurface { + return cultMeshBindQuery(verse, this); + } +} + +export class CultMeshBoundQuerySurface { + public constructor( + public readonly verse: CultMeshVerseContext, + public readonly query: CultMeshQuerySurface, + ) {} + + public get queryId(): string { + return this.query.queryId; + } + + public get sources(): readonly CultMeshProjectionSource[] { + return this.query.sources; + } + + public get routeHint(): CultMeshRouteHint { + return this.query.routeHint; + } + + public execute(parameters: TParameters): Promise { + return this.query.execute(parameters, cultMeshQueryContextFromVerse(this.verse)); + } + + public queryOnce(parameters: TParameters): Promise { + return this.execute(parameters); + } + + public watch( + parameters: TParameters, + callback: (value: TResult) => void, + ): CultMeshUnsubscribe { + return this.query.watch(parameters, cultMeshQueryContextFromVerse(this.verse), callback); + } +} + +export class CultMeshLiveFeed { + public readonly sources: readonly CultMeshProjectionSource[]; + public readonly routeHint: CultMeshRouteHint; + + public constructor( + public readonly feedId: string, + private readonly snapshotFeed: ( + parameters: TParameters, + context: CultMeshQueryContext, + ) => Promise, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + watchFeed?: CultMeshLiveFeedWatcher; + } = {}, + ) { + requireNonEmpty(feedId, "feedId"); + this.sources = [...(options.sources ?? [])]; + this.routeHint = options.routeHint ?? cultMeshRouteHint(); + this.watchFeed = options.watchFeed; + } + + private readonly watchFeed: CultMeshLiveFeedWatcher | undefined; + + public snapshot( + parameters: TParameters, + context: CultMeshQueryContext | string, + ): Promise { + return this.snapshotFeed( + parameters, + this.resolveContext(typeof context === "string" ? cultMeshQueryContext(context) : context), + ); + } + + public watch( + parameters: TParameters, + context: CultMeshQueryContext | string, + callback: (value: TResult) => void, + ): CultMeshUnsubscribe { + if (!this.watchFeed) { + throw new Error(`Live feed '${this.feedId}' does not support watches.`); + } + + return this.watchFeed( + parameters, + this.resolveContext(typeof context === "string" ? cultMeshQueryContext(context) : context), + callback, + ); + } + + public bind( + verse: CultMeshVerseContext | CultMeshVerse, + ): CultMeshBoundLiveFeed { + return cultMeshBindLiveFeed(verse, this); + } + + private resolveContext(context: CultMeshQueryContext): CultMeshQueryContext { + if (context.routeHint.kind !== "automatic" || this.routeHint.kind === "automatic") { + return context; + } + + return cultMeshQueryContext(context.runtimeId, { + routeHint: this.routeHint, + }); + } +} + +export class CultMeshBoundLiveFeed { + public constructor( + public readonly verse: CultMeshVerseContext, + public readonly feed: CultMeshLiveFeed, + ) {} + + public get feedId(): string { + return this.feed.feedId; + } + + public get sources(): readonly CultMeshProjectionSource[] { + return this.feed.sources; + } + + public get routeHint(): CultMeshRouteHint { + return this.feed.routeHint; + } + + public snapshot(parameters: TParameters): Promise { + return this.feed.snapshot(parameters, cultMeshQueryContextFromVerse(this.verse)); + } + + public watch( + parameters: TParameters, + callback: (value: TResult) => void, + ): CultMeshUnsubscribe { + return this.feed.watch(parameters, cultMeshQueryContextFromVerse(this.verse), callback); + } +} + +export class CultMeshStatePointer { + public readonly sources: readonly CultMeshProjectionSource[]; + public readonly routeHint: CultMeshRouteHint; + + public constructor( + public readonly pointerId: string, + private readonly resolvePointer: (() => Promise) | CultMeshStatePointerResolver, + private readonly watchPointer?: + | ((callback: (value: T) => void) => CultMeshUnsubscribe) + | CultMeshStatePointerWatcher, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + } = {}, + ) { + requireNonEmpty(pointerId, "pointerId"); + this.sources = [...(options.sources ?? [])]; + this.routeHint = options.routeHint ?? cultMeshRouteHint(); + } + + public resolve(context?: CultMeshQueryContext | string): Promise { + return (this.resolvePointer as CultMeshStatePointerResolver)( + this.resolveContext( + typeof context === "string" ? cultMeshQueryContext(context) : context, + ), + ); + } + + public watch(callback: (value: T) => void): CultMeshUnsubscribe; + public watch(context: CultMeshQueryContext | string, callback: (value: T) => void): CultMeshUnsubscribe; + public watch( + contextOrCallback: CultMeshQueryContext | string | ((value: T) => void), + maybeCallback?: (value: T) => void, + ): CultMeshUnsubscribe { + if (!this.watchPointer) { + throw new Error(`State pointer '${this.pointerId}' does not support watches.`); + } + + const callback = + typeof contextOrCallback === "function" ? contextOrCallback : maybeCallback; + if (!callback) { + throw new Error(`State pointer '${this.pointerId}' requires a watch callback.`); + } + + if (this.watchPointer.length <= 1) { + return (this.watchPointer as (callback: (value: T) => void) => CultMeshUnsubscribe)(callback); + } + + const context = + typeof contextOrCallback === "function" + ? undefined + : typeof contextOrCallback === "string" + ? cultMeshQueryContext(contextOrCallback) + : contextOrCallback; + return (this.watchPointer as CultMeshStatePointerWatcher)( + this.resolveContext(context), + callback, + ); + } + + public bind( + verse: CultMeshVerseContext | CultMeshVerse, + ): CultMeshBoundStatePointer { + return cultMeshBindStatePointer(verse, this); + } + + private resolveContext(context?: CultMeshQueryContext): CultMeshQueryContext { + const resolved = context ?? cultMeshQueryContext("local"); + if (resolved.routeHint.kind !== "automatic" || this.routeHint.kind === "automatic") { + return resolved; + } + + return cultMeshQueryContext(resolved.runtimeId, { + routeHint: this.routeHint, + }); + } +} + +export class CultMeshBoundStatePointer { + public constructor( + public readonly verse: CultMeshVerseContext, + public readonly pointer: CultMeshStatePointer, + ) {} + + public get pointerId(): string { + return this.pointer.pointerId; + } + + public get sources(): readonly CultMeshProjectionSource[] { + return this.pointer.sources; + } + + public get routeHint(): CultMeshRouteHint { + return this.pointer.routeHint; + } + + public resolve(): Promise { + return this.pointer.resolve(cultMeshQueryContextFromVerse(this.verse)); + } + + public watch(callback: (value: T) => void): CultMeshUnsubscribe { + return this.pointer.watch(cultMeshQueryContextFromVerse(this.verse), callback); + } +} + +export class CultMeshMutableStatePointer { + public readonly sources: readonly CultMeshProjectionSource[]; + public readonly routeHint: CultMeshRouteHint; + + public constructor( + public readonly pointerId: string, + private readonly resolvePointer: (() => Promise) | CultMeshStatePointerResolver, + private readonly watchPointer: + | ((callback: (value: T) => void) => CultMeshUnsubscribe) + | CultMeshStatePointerWatcher, + private readonly replacePointer: + | ((value: T) => Promise) + | CultMeshMutableStatePointerReplacer, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + } = {}, + ) { + requireNonEmpty(pointerId, "pointerId"); + this.sources = [...(options.sources ?? [])]; + this.routeHint = options.routeHint ?? cultMeshRouteHint(); + } + + public resolve(context?: CultMeshQueryContext | string): Promise { + return (this.resolvePointer as CultMeshStatePointerResolver)( + this.resolveContext( + typeof context === "string" ? cultMeshQueryContext(context) : context, + ), + ); + } + + public read(context?: CultMeshQueryContext | string): Promise { + return this.resolve(context); + } + + public replace(value: T): Promise; + public replace(context: CultMeshQueryContext | string, value: T): Promise; + public replace( + contextOrValue: CultMeshQueryContext | string | T, + maybeValue?: T, + ): Promise { + if (this.replacePointer.length <= 1) { + return (this.replacePointer as (value: T) => Promise)( + contextOrValue as T, + ); + } + + const context = + typeof contextOrValue === "string" + ? cultMeshQueryContext(contextOrValue) + : isCultMeshQueryContext(contextOrValue) + ? contextOrValue + : undefined; + const value = context ? maybeValue : (contextOrValue as T); + if (value === undefined) { + throw new Error(`Mutable state pointer '${this.pointerId}' requires a replacement value.`); + } + + return (this.replacePointer as CultMeshMutableStatePointerReplacer)( + this.resolveContext(context), + value, + ); + } + + public watch(callback: (value: T) => void): CultMeshUnsubscribe; + public watch(context: CultMeshQueryContext | string, callback: (value: T) => void): CultMeshUnsubscribe; + public watch( + contextOrCallback: CultMeshQueryContext | string | ((value: T) => void), + maybeCallback?: (value: T) => void, + ): CultMeshUnsubscribe { + const callback = + typeof contextOrCallback === "function" ? contextOrCallback : maybeCallback; + if (!callback) { + throw new Error(`Mutable state pointer '${this.pointerId}' requires a watch callback.`); + } + + if (this.watchPointer.length <= 1) { + return (this.watchPointer as (callback: (value: T) => void) => CultMeshUnsubscribe)(callback); + } + + const context = + typeof contextOrCallback === "function" + ? undefined + : typeof contextOrCallback === "string" + ? cultMeshQueryContext(contextOrCallback) + : contextOrCallback; + return (this.watchPointer as CultMeshStatePointerWatcher)( + this.resolveContext(context), + callback, + ); + } + + public asStatePointer(): CultMeshStatePointer { + return cultMeshStatePointer( + this.pointerId, + (context) => this.resolve(context), + (context, callback) => this.watch(context, callback), + { + routeHint: this.routeHint, + sources: this.sources, + }, + ); + } + + public bind( + verse: CultMeshVerseContext | CultMeshVerse, + ): CultMeshBoundMutableStatePointer { + return cultMeshBindMutableStatePointer(verse, this); + } + + private resolveContext(context?: CultMeshQueryContext): CultMeshQueryContext { + const resolved = context ?? cultMeshQueryContext("local"); + if (resolved.routeHint.kind !== "automatic" || this.routeHint.kind === "automatic") { + return resolved; + } + + return cultMeshQueryContext(resolved.runtimeId, { + routeHint: this.routeHint, + }); + } +} + +export class CultMeshBoundMutableStatePointer { + public constructor( + public readonly verse: CultMeshVerseContext, + public readonly pointer: CultMeshMutableStatePointer, + ) {} + + public get pointerId(): string { + return this.pointer.pointerId; + } + + public get sources(): readonly CultMeshProjectionSource[] { + return this.pointer.sources; + } + + public get routeHint(): CultMeshRouteHint { + return this.pointer.routeHint; + } + + public resolve(): Promise { + return this.pointer.resolve(cultMeshQueryContextFromVerse(this.verse)); + } + + public read(): Promise { + return this.resolve(); + } + + public replace(value: T): Promise { + return this.pointer.replace(cultMeshQueryContextFromVerse(this.verse), value); + } + + public watch(callback: (value: T) => void): CultMeshUnsubscribe { + return this.pointer.watch(cultMeshQueryContextFromVerse(this.verse), callback); + } + + public asStatePointer(): CultMeshBoundStatePointer { + return cultMeshBindStatePointer(this.verse, this.pointer.asStatePointer()); + } +} + +export class CultMeshProjectionRecipe { + public readonly sources: readonly CultMeshProjectionSource[]; + public readonly routeHint: CultMeshRouteHint; + + public constructor( + public readonly projectionId: string, + sources: readonly CultMeshProjectionSource[], + private readonly projectProjection: ( + parameters: TParameters, + context: CultMeshQueryContext, + ) => Promise, + options: { + routeHint?: CultMeshRouteHint; + watchProjection?: ( + parameters: TParameters, + context: CultMeshQueryContext, + callback: (value: TResult) => void, + ) => CultMeshUnsubscribe; + } = {}, + ) { + requireNonEmpty(projectionId, "projectionId"); + this.sources = [...sources]; + this.routeHint = options.routeHint ?? cultMeshRouteHint(); + this.watchProjection = options.watchProjection; + } + + private readonly watchProjection: + | (( + parameters: TParameters, + context: CultMeshQueryContext, + callback: (value: TResult) => void, + ) => CultMeshUnsubscribe) + | undefined; + + public project( + parameters: TParameters, + context: CultMeshQueryContext | string, + ): Promise { + return this.projectProjection( + parameters, + this.resolveContext( + typeof context === "string" ? cultMeshQueryContext(context) : context, + ), + ); + } + + public watch( + parameters: TParameters, + context: CultMeshQueryContext | string, + callback: (value: TResult) => void, + ): CultMeshUnsubscribe { + if (!this.watchProjection) { + throw new Error(`Projection recipe '${this.projectionId}' does not support watches.`); + } + + return this.watchProjection( + parameters, + this.resolveContext( + typeof context === "string" ? cultMeshQueryContext(context) : context, + ), + callback, + ); + } + + public asQuerySurface(): CultMeshQuerySurface { + return cultMeshQuery(this.projectionId, (parameters, context) => + this.project(parameters, context), + { + sources: this.sources, + routeHint: this.routeHint, + watchQuery: this.watchProjection + ? (parameters, context, callback) => this.watch(parameters, context, callback) + : undefined, + }, + ); + } + + private resolveContext(context: CultMeshQueryContext): CultMeshQueryContext { + if (context.routeHint.kind !== "automatic" || this.routeHint.kind === "automatic") { + return context; + } + + return cultMeshQueryContext(context.runtimeId, { + routeHint: this.routeHint, + }); + } +} + +export function cultMeshOperationContextFor(runtimeId: string): CultMeshOperationContextBuilder { + return new CultMeshOperationContextBuilder(runtimeId); +} + +export function cultMeshQueryContextFor(runtimeId: string): CultMeshQueryContextBuilder { + return new CultMeshQueryContextBuilder(runtimeId); +} + +export function cultMeshVec2(x: number, y: number): CultMeshVec2 { + return { x, y }; +} + +export function cultMeshRect(min: CultMeshVec2, max: CultMeshVec2): CultMeshRect { + return { + min: cultMeshVec2(Math.min(min.x, max.x), Math.min(min.y, max.y)), + max: cultMeshVec2(Math.max(min.x, max.x), Math.max(min.y, max.y)), + }; +} + +export function cultMeshRectFromBounds( + minX: number, + minY: number, + maxX: number, + maxY: number, +): CultMeshRect { + return cultMeshRect(cultMeshVec2(minX, minY), cultMeshVec2(maxX, maxY)); +} + +export function cultMeshViewportRequest( + viewport: CultMeshRect, + controlledEntityIndices?: readonly number[], +): CultMeshViewportRequest { + return { + minX: viewport.min.x, + minY: viewport.min.y, + maxX: viewport.max.x, + maxY: viewport.max.y, + controlledEntityIndices, + }; +} + +export function cultMeshRouteHint( + kind: CultMeshLocalityKind = "automatic", + description?: string, +): CultMeshRouteHint { + return description ? { kind, description } : { kind }; +} + +export function cultMeshRouteRecord( + routeHint?: CultMeshRouteHint, +): CultMeshRouteRecord; +export function cultMeshRouteRecord( + kind?: string, + description?: string, +): CultMeshRouteRecord; +export function cultMeshRouteRecord( + routeOrKind?: CultMeshRouteHint | string, + description = "", +): CultMeshRouteRecord { + if (typeof routeOrKind === "string" || routeOrKind === undefined) { + return { + kind: routeOrKind ?? "", + description, + }; + } + + return { + kind: routeOrKind.kind, + description: routeOrKind.description ?? "", + }; +} + +export function cultMeshRouteFromRecord( + record: Partial, + fallback: CultMeshRouteHint = cultMeshRouteHint(), +): CultMeshRouteHint { + return cultMeshRouteHint( + parseCultMeshLocalityKind(record.kind, fallback.kind), + nonBlankOr(record.description, fallback.description), + ); +} + +export function cultMeshAuthorityClaim( + role: string, + options: { shardId?: string; leaseId?: string } = {}, +): CultMeshAuthorityClaim { + requireNonEmpty(role, "role"); + return { + role, + shardId: options.shardId, + leaseId: options.leaseId, + }; +} + +export function cultMeshVerseContext( + verseId: string, + runtimeId: string, + options: { + routeHint?: CultMeshRouteHint; + claims?: readonly CultMeshAuthorityClaim[]; + } = {}, +): CultMeshVerseContext { + requireNonEmpty(verseId, "verseId"); + requireNonEmpty(runtimeId, "runtimeId"); + return { + verseId, + runtimeId, + routeHint: options.routeHint ?? cultMeshRouteHint(), + claims: options.claims ? [...options.claims] : [], + }; +} + +export function cultMeshVerse( + verseId: string, + runtimeId: string, + options: { + routeHint?: CultMeshRouteHint; + claims?: readonly CultMeshAuthorityClaim[]; + } = {}, +): CultMeshVerse { + return new CultMeshVerse(cultMeshVerseContext(verseId, runtimeId, options)); +} + +export function cultMeshOperationContextFromVerse( + context: CultMeshVerseContext, + options: { idempotencyKey?: string } = {}, +): CultMeshOperationContext { + return cultMeshOperationContext(context.runtimeId, { + claims: context.claims, + routeHint: context.routeHint, + idempotencyKey: options.idempotencyKey, + }); +} + +export function cultMeshQueryContextFromVerse( + context: CultMeshVerseContext, +): CultMeshQueryContext { + return cultMeshQueryContext(context.runtimeId, { + routeHint: context.routeHint, + }); +} + +export function cultMeshBindOperation( + verse: CultMeshVerseContext | CultMeshVerse, + operation: CultMeshOperationHandle, +): CultMeshBoundOperationHandle { + return new CultMeshBoundOperationHandle(resolveCultMeshVerseContext(verse), operation); +} + +export function cultMeshBindQuery( + verse: CultMeshVerseContext | CultMeshVerse, + query: CultMeshQuerySurface, +): CultMeshBoundQuerySurface { + return new CultMeshBoundQuerySurface(resolveCultMeshVerseContext(verse), query); +} + +export function cultMeshBindLiveFeed( + verse: CultMeshVerseContext | CultMeshVerse, + feed: CultMeshLiveFeed, +): CultMeshBoundLiveFeed { + return new CultMeshBoundLiveFeed(resolveCultMeshVerseContext(verse), feed); +} + +export function cultMeshBindStatePointer( + verse: CultMeshVerseContext | CultMeshVerse, + pointer: CultMeshStatePointer, +): CultMeshBoundStatePointer { + return new CultMeshBoundStatePointer(resolveCultMeshVerseContext(verse), pointer); +} + +export function cultMeshBindMutableStatePointer( + verse: CultMeshVerseContext | CultMeshVerse, + pointer: CultMeshMutableStatePointer, +): CultMeshBoundMutableStatePointer { + return new CultMeshBoundMutableStatePointer(resolveCultMeshVerseContext(verse), pointer); +} + +function resolveCultMeshVerseContext(verse: CultMeshVerseContext | CultMeshVerse): CultMeshVerseContext { + return verse instanceof CultMeshVerse ? verse.context : verse; +} + +function isCultMeshQueryContext(value: unknown): value is CultMeshQueryContext { + if (typeof value !== "object" || value === null) { + return false; + } + + const maybeContext = value as Partial; + return typeof maybeContext.runtimeId === "string" && + typeof maybeContext.routeHint === "object" && + maybeContext.routeHint !== null; +} + +export function cultMeshOperationContext( + runtimeId: string, + options: { + claims?: readonly CultMeshAuthorityClaim[]; + routeHint?: CultMeshRouteHint; + idempotencyKey?: string; + } = {}, +): CultMeshOperationContext { + requireNonEmpty(runtimeId, "runtimeId"); + return { + runtimeId, + claims: options.claims ? [...options.claims] : [], + routeHint: options.routeHint ?? cultMeshRouteHint(), + idempotencyKey: options.idempotencyKey, + }; +} + +export function cultMeshQueryContext( + runtimeId: string, + options: { routeHint?: CultMeshRouteHint } = {}, +): CultMeshQueryContext { + requireNonEmpty(runtimeId, "runtimeId"); + return { + runtimeId, + routeHint: options.routeHint ?? cultMeshRouteHint(), + }; +} + +export function cultMeshOperationReceipt( + operationId: string, + accepted: boolean, + options: { route?: CultMeshRouteHint; diagnostic?: string } = {}, +): CultMeshOperationReceipt { + requireNonEmpty(operationId, "operationId"); + return { + operationId, + accepted, + route: options.route ?? cultMeshRouteHint(), + diagnostic: options.diagnostic, + }; +} + +export function cultMeshOperation( + operationId: string, + invokeOperation: ( + request: TRequest, + context: CultMeshOperationContext, + ) => Promise, +): CultMeshOperationHandle { + return new CultMeshOperationHandle(operationId, invokeOperation); +} + +export function cultMeshQuery( + queryId: string, + executeQuery: ( + parameters: TParameters, + context: CultMeshQueryContext, + ) => Promise, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + watchQuery?: CultMeshQueryWatcher; + } = {}, +): CultMeshQuerySurface { + return new CultMeshQuerySurface(queryId, executeQuery, options); +} + +export function cultMeshDescribeQuerySurface( + query: { + readonly queryId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + }, +): CultMeshQuerySurfaceDiagnostic { + return { + queryId: query.queryId, + routeHint: query.routeHint, + sources: [...query.sources], + }; +} + +export function cultMeshDescribeOperationHandle( + operation: { + readonly operationId: string; + }, +): CultMeshOperationHandleDiagnostic { + return { + operationId: operation.operationId, + }; +} + +export function cultMeshDescribeStatePointer( + pointer: { + readonly pointerId: string; + readonly routeHint?: CultMeshRouteHint; + readonly sources?: readonly CultMeshProjectionSource[]; + }, +): CultMeshStatePointerDiagnostic { + return { + pointerId: pointer.pointerId, + routeHint: pointer.routeHint ?? cultMeshRouteHint(), + sources: [...(pointer.sources ?? [])], + }; +} + +export function cultMeshLiveFeed( + feedId: string, + snapshotFeed: ( + parameters: TParameters, + context: CultMeshQueryContext, + ) => Promise, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + watchFeed?: CultMeshLiveFeedWatcher; + } = {}, +): CultMeshLiveFeed { + return new CultMeshLiveFeed(feedId, snapshotFeed, options); +} + +export function cultMeshDescribeLiveFeed( + feed: { + readonly feedId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + }, +): CultMeshLiveFeedDiagnostic { + return { + feedId: feed.feedId, + routeHint: feed.routeHint, + sources: [...feed.sources], + }; +} + +export function cultMeshPollingQueryWatcher( + executeQuery: ( + parameters: TParameters, + context: CultMeshQueryContext, + ) => Promise, + options: CultMeshPollingWatchOptions = {}, +): CultMeshQueryWatcher { + const intervalMs = Math.max(1, options.intervalMs ?? 50); + const emitInitial = options.emitInitial ?? true; + return (parameters, context, callback) => { + let disposed = false; + let running = false; + let last: TResult | undefined; + let hasLast = false; + let timer: ReturnType | undefined; + + const poll = async () => { + if (disposed || running) { + return; + } + + running = true; + try { + const next = await executeQuery(parameters, context); + const changed = !hasLast || !(options.equals?.(last as TResult, next) ?? Object.is(last, next)); + const shouldEmit = hasLast ? changed : emitInitial; + last = next; + hasLast = true; + if (shouldEmit) { + callback(next); + } + } finally { + running = false; + } + }; + + void poll(); + timer = setInterval(() => { + void poll(); + }, intervalMs); + + return () => { + disposed = true; + if (timer) { + clearInterval(timer); + } + }; + }; +} + +export function cultMeshStatePointer( + pointerId: string, + resolvePointer: (() => Promise) | CultMeshStatePointerResolver, + watchPointer?: + | ((callback: (value: T) => void) => CultMeshUnsubscribe) + | CultMeshStatePointerWatcher, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + } = {}, +): CultMeshStatePointer { + return new CultMeshStatePointer(pointerId, resolvePointer, watchPointer, options); +} + +export function cultMeshMutableStatePointer( + pointerId: string, + resolvePointer: (() => Promise) | CultMeshStatePointerResolver, + watchPointer: + | ((callback: (value: T) => void) => CultMeshUnsubscribe) + | CultMeshStatePointerWatcher, + replacePointer: + | ((value: T) => Promise) + | CultMeshMutableStatePointerReplacer, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + } = {}, +): CultMeshMutableStatePointer { + return new CultMeshMutableStatePointer( + pointerId, + resolvePointer, + watchPointer, + replacePointer, + options, + ); +} + +export function cultMeshProjectionSource( + sourceId: string, + options: { schemaId?: string; description?: string } = {}, +): CultMeshProjectionSource { + requireNonEmpty(sourceId, "sourceId"); + return { + sourceId, + schemaId: options.schemaId, + description: options.description, + }; +} + +export class CultMeshStateRefResolver { + public readonly sources: readonly CultMeshProjectionSource[]; + public readonly routeHint: CultMeshRouteHint; + + public constructor( + public readonly resolverId: string, + private readonly resolveRef: (stateRef: string, context: CultMeshQueryContext) => string | undefined, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + } = {}, + ) { + requireNonEmpty(resolverId, "resolverId"); + this.sources = [...(options.sources ?? [])]; + this.routeHint = options.routeHint ?? cultMeshRouteHint(); + } + + public resolve(stateRef: string, context: CultMeshQueryContext | string = "local"): string { + if (!stateRef.trim()) { + return ""; + } + return ( + this.resolveRef( + stateRef, + this.resolveContext(typeof context === "string" ? cultMeshQueryContext(context) : context), + ) ?? "" + ); + } + + public tryResolve( + stateRef: string, + context: CultMeshQueryContext | string = "local", + ): { resolved: boolean; value: string } { + const value = this.resolve(stateRef, context); + return { resolved: value.length > 0, value }; + } + + public or(fallback: CultMeshStateRefResolver): CultMeshStateRefResolver { + return new CultMeshStateRefResolver( + `${this.resolverId}|${fallback.resolverId}`, + (stateRef, context) => { + const value = this.resolve(stateRef, context); + return value.length > 0 ? value : fallback.resolve(stateRef, context); + }, + { + sources: [...this.sources, ...fallback.sources], + routeHint: this.routeHint.kind === "automatic" ? fallback.routeHint : this.routeHint, + }, + ); + } + + public asFunction(): (stateRef: string) => string { + return (stateRef) => this.resolve(stateRef); + } + + private resolveContext(context: CultMeshQueryContext): CultMeshQueryContext { + if (context.routeHint.kind !== "automatic" || this.routeHint.kind === "automatic") { + return context; + } + return cultMeshQueryContext(context.runtimeId, { routeHint: this.routeHint }); + } +} + +export function cultMeshStateRefResolver( + resolverId: string, + resolveRef: + | ((stateRef: string) => string | undefined) + | ((stateRef: string, context: CultMeshQueryContext) => string | undefined), + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + } = {}, +): CultMeshStateRefResolver { + return new CultMeshStateRefResolver( + resolverId, + (stateRef, context) => + resolveRef.length <= 1 + ? (resolveRef as (stateRef: string) => string | undefined)(stateRef) + : (resolveRef as (stateRef: string, context: CultMeshQueryContext) => string | undefined)( + stateRef, + context, + ), + options, + ); +} + +export function cultMeshDescribeStateRefResolver( + resolver: CultMeshStateRefResolver, +): CultMeshStateRefResolverDiagnostic { + return { + resolverId: resolver.resolverId, + routeHint: resolver.routeHint, + sources: [...resolver.sources], + }; +} + +export function cultMeshStateBinding( + targetProp: string, + pointer: + | { + readonly pointerId: string; + readonly routeHint?: CultMeshRouteHint; + readonly sources?: readonly CultMeshProjectionSource[]; + } + | string, + options: { + sourceId?: string; + schemaId?: string; + routeHint?: CultMeshRouteHint; + } = {}, +): CultMeshStateBindingDescriptor { + requireNonEmpty(targetProp, "targetProp"); + if (typeof pointer === "string") { + requireNonEmpty(pointer, "pointerId"); + return { + targetProp, + pointerId: pointer, + sourceId: options.sourceId, + schemaId: options.schemaId, + routeHint: options.routeHint ?? cultMeshRouteHint(), + }; + } + + requireNonEmpty(pointer.pointerId, "pointerId"); + const source = pointer.sources?.[0]; + return { + targetProp, + pointerId: pointer.pointerId, + sourceId: options.sourceId ?? source?.sourceId, + schemaId: options.schemaId ?? source?.schemaId, + routeHint: options.routeHint ?? pointer.routeHint ?? cultMeshRouteHint(), + }; +} + +export function cultMeshStateBindingRecord( + binding?: Partial, +): CultMeshStateBindingRecord; +export function cultMeshStateBindingRecord( + targetProp?: string, + pointerId?: string, + sourceId?: string, + schemaId?: string, + routeKind?: string, + routeDescription?: string, +): CultMeshStateBindingRecord; +export function cultMeshStateBindingRecord( + bindingOrTargetProp?: Partial | string, + pointerId?: string, + sourceId?: string, + schemaId?: string, + routeKind?: string, + routeDescription?: string, +): CultMeshStateBindingRecord { + if (typeof bindingOrTargetProp === "object" || bindingOrTargetProp === undefined) { + const binding = bindingOrTargetProp; + const route = cultMeshRouteRecord(binding?.routeHint); + return { + targetProp: binding?.targetProp?.trim() || "value", + pointerId: binding?.pointerId ?? "", + sourceId: binding?.sourceId ?? "", + schemaId: binding?.schemaId ?? "", + routeKind: route.kind, + routeDescription: route.description, + }; + } + + return { + targetProp: bindingOrTargetProp?.trim() || "value", + pointerId: pointerId ?? "", + sourceId: sourceId ?? "", + schemaId: schemaId ?? "", + routeKind: routeKind ?? "", + routeDescription: routeDescription ?? "", + }; +} + +export function cultMeshStateBindingFromRecord( + record: Partial, + options: { + fallbackRouteHint?: CultMeshRouteHint; + fallbackTargetProp?: string; + } = {}, +): CultMeshStateBindingDescriptor { + const targetProp = record.targetProp?.trim() || options.fallbackTargetProp?.trim() || "value"; + return { + targetProp, + pointerId: record.pointerId?.trim() || `${targetProp}.unknown`, + sourceId: record.sourceId ?? "", + schemaId: record.schemaId ?? "", + routeHint: cultMeshRouteFromRecord( + { + kind: record.routeKind, + description: record.routeDescription, + }, + options.fallbackRouteHint, + ), + }; +} + +export function cultMeshOperationBinding( + operation: + | { + readonly operationId: string; + } + | string, + options: { + label?: string; + schemaId?: string; + routeHint?: CultMeshRouteHint; + } = {}, +): CultMeshOperationBindingDescriptor { + const operationId = typeof operation === "string" ? operation : operation.operationId; + requireNonEmpty(operationId, "operationId"); + return { + operationId, + label: options.label ?? "", + schemaId: options.schemaId ?? "", + routeHint: options.routeHint ?? cultMeshRouteHint(), + }; +} + +export function cultMeshOperationBindingRecord( + binding?: Partial, +): CultMeshOperationBindingRecord; +export function cultMeshOperationBindingRecord( + operationId?: string, + label?: string, + schemaId?: string, + routeKind?: string, + routeDescription?: string, +): CultMeshOperationBindingRecord; +export function cultMeshOperationBindingRecord( + bindingOrOperationId?: Partial | string, + label?: string, + schemaId?: string, + routeKind?: string, + routeDescription?: string, +): CultMeshOperationBindingRecord { + if (typeof bindingOrOperationId === "object" || bindingOrOperationId === undefined) { + const binding = bindingOrOperationId; + const route = cultMeshRouteRecord(binding?.routeHint); + return { + operationId: binding?.operationId ?? "", + label: binding?.label ?? "", + schemaId: binding?.schemaId ?? "", + routeKind: route.kind, + routeDescription: route.description, + }; + } + + return { + operationId: bindingOrOperationId ?? "", + label: label ?? "", + schemaId: schemaId ?? "", + routeKind: routeKind ?? "", + routeDescription: routeDescription ?? "", + }; +} + +export function cultMeshOperationBindingFromRecord( + record: Partial, + options: { + fallbackRouteHint?: CultMeshRouteHint; + fallbackOperationId?: string; + } = {}, +): CultMeshOperationBindingDescriptor { + return { + operationId: record.operationId?.trim() || options.fallbackOperationId || "", + label: record.label ?? "", + schemaId: record.schemaId ?? "", + routeHint: cultMeshRouteFromRecord( + { + kind: record.routeKind, + description: record.routeDescription, + }, + options.fallbackRouteHint, + ), + }; +} + +export function cultMeshOperationInvocation( + operation: + | { + readonly operationId: string; + readonly schemaId?: string; + readonly routeHint?: CultMeshRouteHint; + } + | string, + options: { + schemaId?: string; + routeHint?: CultMeshRouteHint; + idempotencyKey?: string; + } = {}, +): CultMeshOperationInvocationDescriptor { + const operationId = typeof operation === "string" ? operation : operation.operationId; + requireNonEmpty(operationId, "operationId"); + return { + operationId, + schemaId: options.schemaId ?? (typeof operation === "string" ? "" : operation.schemaId) ?? "", + routeHint: + options.routeHint ?? + (typeof operation === "string" ? undefined : operation.routeHint) ?? + cultMeshRouteHint(), + idempotencyKey: options.idempotencyKey, + }; +} + +export function cultMeshOperationInvocationRecord( + invocation?: CultMeshOperationInvocationDescriptor, + options: { + fallbackOperationId?: string; + fallbackSchemaId?: string; + fallbackRouteHint?: CultMeshRouteHint; + fallbackIdempotencyKey?: string; + } = {}, +): CultMeshOperationInvocationRecord { + const route = cultMeshRouteRecord(invocation?.routeHint ?? options.fallbackRouteHint); + return { + operationId: nonBlankOr(invocation?.operationId, options.fallbackOperationId), + schemaId: nonBlankOr(invocation?.schemaId, options.fallbackSchemaId), + routeKind: route.kind, + routeDescription: route.description, + idempotencyKey: nonBlankOr(invocation?.idempotencyKey, options.fallbackIdempotencyKey), + }; +} + +export function cultMeshOperationInvocationFromRecord( + record: Partial, + options: { + fallbackOperationId?: string; + fallbackSchemaId?: string; + fallbackRouteHint?: CultMeshRouteHint; + fallbackIdempotencyKey?: string; + } = {}, +): CultMeshOperationInvocationDescriptor { + const operationId = nonBlankOr(record.operationId, options.fallbackOperationId); + requireNonEmpty(operationId, "operationId"); + const fallbackRoute = options.fallbackRouteHint ?? cultMeshRouteHint(); + const route = cultMeshRouteFromRecord( + { + kind: record.routeKind, + description: record.routeDescription, + }, + fallbackRoute, + ); + return { + operationId, + schemaId: nonBlankOr(record.schemaId, options.fallbackSchemaId), + routeHint: route, + idempotencyKey: nonBlankOr(record.idempotencyKey, options.fallbackIdempotencyKey) || undefined, + }; +} + +export function cultMeshOperationPayload( + fields: Readonly> = {}, +): CultMeshOperationPayload { + const normalized: Record = {}; + for (const [key, value] of Object.entries(fields)) { + if (!key.trim() || value === undefined) { + continue; + } + normalized[key] = String(value); + } + + return { + fields: Object.freeze({ ...normalized }), + getString(key, defaultValue = "") { + return normalized[key] ?? defaultValue; + }, + getInt(key, defaultValue = 0) { + const raw = normalized[key]; + if (raw === undefined || raw.trim() === "") { + return defaultValue; + } + const parsed = Number.parseInt(raw, 10); + return Number.isNaN(parsed) ? defaultValue : parsed; + }, + getDouble(key, defaultValue = 0) { + const raw = normalized[key]; + if (raw === undefined || raw.trim() === "") { + return defaultValue; + } + const parsed = Number.parseFloat(raw); + return Number.isNaN(parsed) ? defaultValue : parsed; + }, + getBoolean(key, defaultValue = false) { + const raw = normalized[key]?.toLowerCase(); + if (raw === "true" || raw === "1" || raw === "yes" || raw === "on") { + return true; + } + if (raw === "false" || raw === "0" || raw === "no" || raw === "off") { + return false; + } + return defaultValue; + }, + with(key, value) { + requireNonEmpty(key, "key"); + return cultMeshOperationPayload({ + ...normalized, + [key]: value, + }); + }, + toRecord() { + return Object.freeze({ ...normalized }); + }, + }; +} + +export function cultMeshProjectionRecipe( + projectionId: string, + sources: readonly CultMeshProjectionSource[], + projectProjection: ( + parameters: TParameters, + context: CultMeshQueryContext, + ) => Promise, + options: { + routeHint?: CultMeshRouteHint; + watchProjection?: ( + parameters: TParameters, + context: CultMeshQueryContext, + callback: (value: TResult) => void, + ) => CultMeshUnsubscribe; + } = {}, +): CultMeshProjectionRecipe { + return new CultMeshProjectionRecipe( + projectionId, + sources, + projectProjection, + options, + ); +} + +export function cultMeshDescribeProjectionRecipe( + recipe: { + readonly projectionId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + }, +): CultMeshProjectionRecipeDiagnostic { + return { + projectionId: recipe.projectionId, + routeHint: recipe.routeHint, + sources: [...recipe.sources], + }; +} + +export function cultMeshDescribeSurface( + surface: + | { + readonly queryId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + } + | { + readonly operationId: string; + } + | { + readonly projectionId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + } + | { + readonly feedId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + } + | { + readonly pointerId: string; + readonly routeHint?: CultMeshRouteHint; + readonly sources?: readonly CultMeshProjectionSource[]; + } + | CultMeshNativeSliceViewDescriptor, +): CultMeshSurfaceDiagnostic { + if ("queryId" in surface) { + return cultMeshSurfaceDiagnostic("query", surface.queryId, { + routeHint: surface.routeHint, + sources: surface.sources, + }); + } + + if ("operationId" in surface) { + return cultMeshSurfaceDiagnostic("operation", surface.operationId); + } + + if ("pointerId" in surface) { + return cultMeshSurfaceDiagnostic("state-pointer", surface.pointerId, { + routeHint: surface.routeHint, + sources: surface.sources, + }); + } + + if ("viewId" in surface) { + return cultMeshSurfaceDiagnostic("native-slice-view", surface.viewId, { + routeHint: surface.route, + }); + } + + if ("projectionId" in surface) { + return cultMeshSurfaceDiagnostic("projection-recipe", surface.projectionId, { + routeHint: surface.routeHint, + sources: surface.sources, + }); + } + + return cultMeshSurfaceDiagnostic("live-feed", surface.feedId, { + routeHint: surface.routeHint, + sources: surface.sources, + }); +} + +export function cultMeshSurfaceDiagnostic( + kind: CultMeshSurfaceKind, + surfaceId: string, + options: { + routeHint?: CultMeshRouteHint; + sources?: readonly CultMeshProjectionSource[]; + } = {}, +): CultMeshSurfaceDiagnostic { + requireNonEmpty(surfaceId, "surfaceId"); + return { + kind, + surfaceId, + routeHint: options.routeHint ?? cultMeshRouteHint(), + sources: [...(options.sources ?? [])], + }; +} + +export function cultMeshDescribeSurfaceCatalog( + catalogId: string, + surfaces: readonly CultMeshSurfaceDiagnostic[], +): CultMeshSurfaceCatalogDiagnostic { + requireNonEmpty(catalogId, "catalogId"); + return { + catalogId, + surfaces: surfaces.map(surface => cultMeshSurfaceDiagnostic(surface.kind, surface.surfaceId, { + routeHint: surface.routeHint, + sources: surface.sources, + })), + }; +} + +export function cultMeshFindSurface( + catalog: CultMeshSurfaceCatalogDiagnostic, + surfaceId: string, +): CultMeshSurfaceDiagnostic | undefined { + requireNonEmpty(surfaceId, "surfaceId"); + return catalog.surfaces.find(surface => surface.surfaceId === surfaceId); +} + +export function cultMeshSurfacesByKind( + catalog: CultMeshSurfaceCatalogDiagnostic, + kind: CultMeshSurfaceKind, +): CultMeshSurfaceDiagnostic[] { + return catalog.surfaces + .filter(surface => surface.kind === kind) + .map(surface => cultMeshSurfaceDiagnostic(surface.kind, surface.surfaceId, { + routeHint: surface.routeHint, + sources: surface.sources, + })); +} + +export function cultMeshSurfaceCatalogIndex( + catalog: CultMeshSurfaceCatalogDiagnostic, +): CultMeshSurfaceCatalogIndexDiagnostic { + return { + catalogId: catalog.catalogId, + queries: cultMeshSurfacesByKind(catalog, "query"), + projectionRecipes: cultMeshSurfacesByKind(catalog, "projection-recipe"), + liveFeeds: cultMeshSurfacesByKind(catalog, "live-feed"), + operations: cultMeshSurfacesByKind(catalog, "operation"), + statePointers: cultMeshSurfacesByKind(catalog, "state-pointer"), + nativeSliceViews: cultMeshSurfacesByKind(catalog, "native-slice-view"), + }; +} + +export function cultMeshNativeSliceColumn( + name: string, + valueType: string, + elementSizeBytes: number, +): CultMeshNativeSliceColumn { + requireNonEmpty(name, "name"); + requireNonEmpty(valueType, "valueType"); + if (!Number.isInteger(elementSizeBytes) || elementSizeBytes <= 0) { + throw new Error("elementSizeBytes must be a positive integer."); + } + + return { name, valueType, elementSizeBytes }; +} + +export function cultMeshNativeSliceView( + viewId: string, + schemaId: string, + rowCount: number, + columns: readonly CultMeshNativeSliceColumn[], + options: { route?: CultMeshRouteHint; nativeHandle?: string } = {}, +): CultMeshNativeSliceViewDescriptor { + requireNonEmpty(viewId, "viewId"); + requireNonEmpty(schemaId, "schemaId"); + if (!Number.isInteger(rowCount) || rowCount < 0) { + throw new Error("rowCount must be a non-negative integer."); + } + + return { + viewId, + schemaId, + rowCount, + columns: [...columns], + route: options.route ?? cultMeshRouteHint(), + nativeHandle: options.nativeHandle, + }; +} + +export function cultMeshDenseRowStrideBytes( + view: CultMeshNativeSliceViewDescriptor, +): number { + return view.columns.reduce((sum, column) => sum + column.elementSizeBytes, 0); +} + +export function cultMeshFindNativeSliceColumn( + view: CultMeshNativeSliceViewDescriptor, + name: string, +): CultMeshNativeSliceColumn | undefined { + requireNonEmpty(name, "name"); + return view.columns.find((column) => column.name === name); +} + +export function cultMeshDescribeNativeSliceView( + view: CultMeshNativeSliceViewDescriptor, +): CultMeshNativeSliceViewDiagnostic { + return { + viewId: view.viewId, + schemaId: view.schemaId, + rowCount: view.rowCount, + columns: [...view.columns], + route: view.route, + nativeHandle: view.nativeHandle, + denseRowStrideBytes: cultMeshDenseRowStrideBytes(view), + }; +} + export interface CultMeshNodeOptions { documents?: Iterable; bindings?: Iterable; @@ -544,6 +2594,622 @@ export class CultMeshStreamCatalog { } export class CultMesh { + public static vec2(x: number, y: number): CultMeshVec2 { + return cultMeshVec2(x, y); + } + + public static rect(min: CultMeshVec2, max: CultMeshVec2): CultMeshRect { + return cultMeshRect(min, max); + } + + public static rectFromBounds( + minX: number, + minY: number, + maxX: number, + maxY: number, + ): CultMeshRect { + return cultMeshRectFromBounds(minX, minY, maxX, maxY); + } + + public static viewportRequest( + viewport: CultMeshRect, + controlledEntityIndices?: readonly number[], + ): CultMeshViewportRequest { + return cultMeshViewportRequest(viewport, controlledEntityIndices); + } + + public static routeHint( + kind: CultMeshLocalityKind = "automatic", + description?: string, + ): CultMeshRouteHint { + return cultMeshRouteHint(kind, description); + } + + public static routeRecord(routeHint?: CultMeshRouteHint): CultMeshRouteRecord; + public static routeRecord(kind?: string, description?: string): CultMeshRouteRecord; + public static routeRecord( + routeOrKind?: CultMeshRouteHint | string, + description = "", + ): CultMeshRouteRecord { + return typeof routeOrKind === "string" || routeOrKind === undefined + ? cultMeshRouteRecord(routeOrKind, description) + : cultMeshRouteRecord(routeOrKind); + } + + public static routeFromRecord( + record: Partial, + fallback: CultMeshRouteHint = cultMeshRouteHint(), + ): CultMeshRouteHint { + return cultMeshRouteFromRecord(record, fallback); + } + + public static authorityClaim( + role: string, + options: { shardId?: string; leaseId?: string } = {}, + ): CultMeshAuthorityClaim { + return cultMeshAuthorityClaim(role, options); + } + + public static verseContext( + verseId: string, + runtimeId: string, + options: { + routeHint?: CultMeshRouteHint; + claims?: readonly CultMeshAuthorityClaim[]; + } = {}, + ): CultMeshVerseContext { + return cultMeshVerseContext(verseId, runtimeId, options); + } + + public static verse( + verseId: string, + runtimeId: string, + options: { + routeHint?: CultMeshRouteHint; + claims?: readonly CultMeshAuthorityClaim[]; + } = {}, + ): CultMeshVerse { + return cultMeshVerse(verseId, runtimeId, options); + } + + public static async connectVerse( + verseId: string, + runtimeId: string, + options: { + routeHint?: CultMeshRouteHint; + claims?: readonly CultMeshAuthorityClaim[]; + } = {}, + ): Promise { + return cultMeshVerse(verseId, runtimeId, options); + } + + public static operationContextFromVerse( + context: CultMeshVerseContext, + options: { idempotencyKey?: string } = {}, + ): CultMeshOperationContext { + return cultMeshOperationContextFromVerse(context, options); + } + + public static queryContextFromVerse( + context: CultMeshVerseContext, + ): CultMeshQueryContext { + return cultMeshQueryContextFromVerse(context); + } + + public static bindOperation( + verse: CultMeshVerseContext | CultMeshVerse, + operation: CultMeshOperationHandle, + ): CultMeshBoundOperationHandle { + return cultMeshBindOperation(verse, operation); + } + + public static bindQuery( + verse: CultMeshVerseContext | CultMeshVerse, + query: CultMeshQuerySurface, + ): CultMeshBoundQuerySurface { + return cultMeshBindQuery(verse, query); + } + + public static bindLiveFeed( + verse: CultMeshVerseContext | CultMeshVerse, + feed: CultMeshLiveFeed, + ): CultMeshBoundLiveFeed { + return cultMeshBindLiveFeed(verse, feed); + } + + public static operationContext( + runtimeId: string, + options: { + claims?: readonly CultMeshAuthorityClaim[]; + routeHint?: CultMeshRouteHint; + idempotencyKey?: string; + } = {}, + ): CultMeshOperationContext { + return cultMeshOperationContext(runtimeId, options); + } + + public static operationContextFor(runtimeId: string): CultMeshOperationContextBuilder { + return cultMeshOperationContextFor(runtimeId); + } + + public static queryContext( + runtimeId: string, + options: { routeHint?: CultMeshRouteHint } = {}, + ): CultMeshQueryContext { + return cultMeshQueryContext(runtimeId, options); + } + + public static queryContextFor(runtimeId: string): CultMeshQueryContextBuilder { + return cultMeshQueryContextFor(runtimeId); + } + + public static operationReceipt( + operationId: string, + accepted: boolean, + options: { route?: CultMeshRouteHint; diagnostic?: string } = {}, + ): CultMeshOperationReceipt { + return cultMeshOperationReceipt(operationId, accepted, options); + } + + public static operation( + operationId: string, + invokeOperation: ( + request: TRequest, + context: CultMeshOperationContext, + ) => Promise, + ): CultMeshOperationHandle { + return cultMeshOperation(operationId, invokeOperation); + } + + public static query( + queryId: string, + executeQuery: ( + parameters: TParameters, + context: CultMeshQueryContext, + ) => Promise, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + watchQuery?: CultMeshQueryWatcher; + } = {}, + ): CultMeshQuerySurface { + return cultMeshQuery(queryId, executeQuery, options); + } + + public static describeQuerySurface( + query: { + readonly queryId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + }, + ): CultMeshQuerySurfaceDiagnostic { + return cultMeshDescribeQuerySurface(query); + } + + public static describeOperationHandle( + operation: { + readonly operationId: string; + }, + ): CultMeshOperationHandleDiagnostic { + return cultMeshDescribeOperationHandle(operation); + } + + public static describeStatePointer( + pointer: { + readonly pointerId: string; + readonly routeHint?: CultMeshRouteHint; + readonly sources?: readonly CultMeshProjectionSource[]; + }, + ): CultMeshStatePointerDiagnostic { + return cultMeshDescribeStatePointer(pointer); + } + + public static liveFeed( + feedId: string, + snapshotFeed: ( + parameters: TParameters, + context: CultMeshQueryContext, + ) => Promise, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + watchFeed?: CultMeshLiveFeedWatcher; + } = {}, + ): CultMeshLiveFeed { + return cultMeshLiveFeed(feedId, snapshotFeed, options); + } + + public static describeLiveFeed( + feed: { + readonly feedId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + }, + ): CultMeshLiveFeedDiagnostic { + return cultMeshDescribeLiveFeed(feed); + } + + public static pollingQueryWatcher( + executeQuery: ( + parameters: TParameters, + context: CultMeshQueryContext, + ) => Promise, + options: CultMeshPollingWatchOptions = {}, + ): CultMeshQueryWatcher { + return cultMeshPollingQueryWatcher(executeQuery, options); + } + + public static statePointer( + pointerId: string, + resolvePointer: (() => Promise) | CultMeshStatePointerResolver, + watchPointer?: + | ((callback: (value: T) => void) => CultMeshUnsubscribe) + | CultMeshStatePointerWatcher, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + } = {}, + ): CultMeshStatePointer { + return cultMeshStatePointer(pointerId, resolvePointer, watchPointer, options); + } + + public static mutableStatePointer( + pointerId: string, + resolvePointer: (() => Promise) | CultMeshStatePointerResolver, + watchPointer: + | ((callback: (value: T) => void) => CultMeshUnsubscribe) + | CultMeshStatePointerWatcher, + replacePointer: + | ((value: T) => Promise) + | CultMeshMutableStatePointerReplacer, + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + } = {}, + ): CultMeshMutableStatePointer { + return cultMeshMutableStatePointer( + pointerId, + resolvePointer, + watchPointer, + replacePointer, + options, + ); + } + + public static bindStatePointer( + verse: CultMeshVerseContext | CultMeshVerse, + pointer: CultMeshStatePointer, + ): CultMeshBoundStatePointer { + return cultMeshBindStatePointer(verse, pointer); + } + + public static bindMutableStatePointer( + verse: CultMeshVerseContext | CultMeshVerse, + pointer: CultMeshMutableStatePointer, + ): CultMeshBoundMutableStatePointer { + return cultMeshBindMutableStatePointer(verse, pointer); + } + + public static projectionSource( + sourceId: string, + options: { schemaId?: string; description?: string } = {}, + ): CultMeshProjectionSource { + return cultMeshProjectionSource(sourceId, options); + } + + public static stateRefResolver( + resolverId: string, + resolveRef: + | ((stateRef: string) => string | undefined) + | ((stateRef: string, context: CultMeshQueryContext) => string | undefined), + options: { + sources?: readonly CultMeshProjectionSource[]; + routeHint?: CultMeshRouteHint; + } = {}, + ): CultMeshStateRefResolver { + return cultMeshStateRefResolver(resolverId, resolveRef, options); + } + + public static describeStateRefResolver( + resolver: CultMeshStateRefResolver, + ): CultMeshStateRefResolverDiagnostic { + return cultMeshDescribeStateRefResolver(resolver); + } + + public static stateBinding( + targetProp: string, + pointer: + | { + readonly pointerId: string; + readonly routeHint?: CultMeshRouteHint; + readonly sources?: readonly CultMeshProjectionSource[]; + } + | string, + options: { + sourceId?: string; + schemaId?: string; + routeHint?: CultMeshRouteHint; + } = {}, + ): CultMeshStateBindingDescriptor { + return cultMeshStateBinding(targetProp, pointer, options); + } + + public static stateBindingRecord( + binding?: Partial, + ): CultMeshStateBindingRecord; + public static stateBindingRecord( + targetProp?: string, + pointerId?: string, + sourceId?: string, + schemaId?: string, + routeKind?: string, + routeDescription?: string, + ): CultMeshStateBindingRecord; + public static stateBindingRecord( + bindingOrTargetProp?: Partial | string, + pointerId?: string, + sourceId?: string, + schemaId?: string, + routeKind?: string, + routeDescription?: string, + ): CultMeshStateBindingRecord { + return typeof bindingOrTargetProp === "object" || bindingOrTargetProp === undefined + ? cultMeshStateBindingRecord(bindingOrTargetProp) + : cultMeshStateBindingRecord( + bindingOrTargetProp, + pointerId, + sourceId, + schemaId, + routeKind, + routeDescription, + ); + } + + public static stateBindingFromRecord( + record: Partial, + options: { + fallbackRouteHint?: CultMeshRouteHint; + fallbackTargetProp?: string; + } = {}, + ): CultMeshStateBindingDescriptor { + return cultMeshStateBindingFromRecord(record, options); + } + + public static operationBinding( + operation: + | { + readonly operationId: string; + } + | string, + options: { + label?: string; + schemaId?: string; + routeHint?: CultMeshRouteHint; + } = {}, + ): CultMeshOperationBindingDescriptor { + return cultMeshOperationBinding(operation, options); + } + + public static operationBindingRecord( + binding?: Partial, + ): CultMeshOperationBindingRecord; + public static operationBindingRecord( + operationId?: string, + label?: string, + schemaId?: string, + routeKind?: string, + routeDescription?: string, + ): CultMeshOperationBindingRecord; + public static operationBindingRecord( + bindingOrOperationId?: Partial | string, + label?: string, + schemaId?: string, + routeKind?: string, + routeDescription?: string, + ): CultMeshOperationBindingRecord { + return typeof bindingOrOperationId === "object" || bindingOrOperationId === undefined + ? cultMeshOperationBindingRecord(bindingOrOperationId) + : cultMeshOperationBindingRecord( + bindingOrOperationId, + label, + schemaId, + routeKind, + routeDescription, + ); + } + + public static operationBindingFromRecord( + record: Partial, + options: { + fallbackRouteHint?: CultMeshRouteHint; + fallbackOperationId?: string; + } = {}, + ): CultMeshOperationBindingDescriptor { + return cultMeshOperationBindingFromRecord(record, options); + } + + public static operationInvocation( + operation: + | { + readonly operationId: string; + readonly schemaId?: string; + readonly routeHint?: CultMeshRouteHint; + } + | string, + options: { + schemaId?: string; + routeHint?: CultMeshRouteHint; + idempotencyKey?: string; + } = {}, + ): CultMeshOperationInvocationDescriptor { + return cultMeshOperationInvocation(operation, options); + } + + public static operationInvocationRecord( + invocation?: CultMeshOperationInvocationDescriptor, + options: { + fallbackOperationId?: string; + fallbackSchemaId?: string; + fallbackRouteHint?: CultMeshRouteHint; + fallbackIdempotencyKey?: string; + } = {}, + ): CultMeshOperationInvocationRecord { + return cultMeshOperationInvocationRecord(invocation, options); + } + + public static operationInvocationFromRecord( + record: Partial, + options: { + fallbackOperationId?: string; + fallbackSchemaId?: string; + fallbackRouteHint?: CultMeshRouteHint; + fallbackIdempotencyKey?: string; + } = {}, + ): CultMeshOperationInvocationDescriptor { + return cultMeshOperationInvocationFromRecord(record, options); + } + + public static operationPayload( + fields: Readonly> = {}, + ): CultMeshOperationPayload { + return cultMeshOperationPayload(fields); + } + + public static projectionRecipe( + projectionId: string, + sources: readonly CultMeshProjectionSource[], + projectProjection: ( + parameters: TParameters, + context: CultMeshQueryContext, + ) => Promise, + options: { + routeHint?: CultMeshRouteHint; + watchProjection?: ( + parameters: TParameters, + context: CultMeshQueryContext, + callback: (value: TResult) => void, + ) => CultMeshUnsubscribe; + } = {}, + ): CultMeshProjectionRecipe { + return cultMeshProjectionRecipe( + projectionId, + sources, + projectProjection, + options, + ); + } + + public static describeProjectionRecipe( + recipe: { + readonly projectionId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + }, + ): CultMeshProjectionRecipeDiagnostic { + return cultMeshDescribeProjectionRecipe(recipe); + } + + public static describeSurface( + surface: + | { + readonly queryId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + } + | { + readonly operationId: string; + } + | { + readonly projectionId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + } + | { + readonly feedId: string; + readonly routeHint: CultMeshRouteHint; + readonly sources: readonly CultMeshProjectionSource[]; + } + | { + readonly pointerId: string; + } + | CultMeshNativeSliceViewDescriptor, + ): CultMeshSurfaceDiagnostic { + return cultMeshDescribeSurface(surface); + } + + public static surfaceDiagnostic( + kind: CultMeshSurfaceKind, + surfaceId: string, + options: { + routeHint?: CultMeshRouteHint; + sources?: readonly CultMeshProjectionSource[]; + } = {}, + ): CultMeshSurfaceDiagnostic { + return cultMeshSurfaceDiagnostic(kind, surfaceId, options); + } + + public static describeSurfaceCatalog( + catalogId: string, + surfaces: readonly CultMeshSurfaceDiagnostic[], + ): CultMeshSurfaceCatalogDiagnostic { + return cultMeshDescribeSurfaceCatalog(catalogId, surfaces); + } + + public static findSurface( + catalog: CultMeshSurfaceCatalogDiagnostic, + surfaceId: string, + ): CultMeshSurfaceDiagnostic | undefined { + return cultMeshFindSurface(catalog, surfaceId); + } + + public static surfacesByKind( + catalog: CultMeshSurfaceCatalogDiagnostic, + kind: CultMeshSurfaceKind, + ): CultMeshSurfaceDiagnostic[] { + return cultMeshSurfacesByKind(catalog, kind); + } + + public static surfaceCatalogIndex( + catalog: CultMeshSurfaceCatalogDiagnostic, + ): CultMeshSurfaceCatalogIndexDiagnostic { + return cultMeshSurfaceCatalogIndex(catalog); + } + + public static nativeSliceColumn( + name: string, + valueType: string, + elementSizeBytes: number, + ): CultMeshNativeSliceColumn { + return cultMeshNativeSliceColumn(name, valueType, elementSizeBytes); + } + + public static nativeSliceView( + viewId: string, + schemaId: string, + rowCount: number, + columns: readonly CultMeshNativeSliceColumn[], + options: { route?: CultMeshRouteHint; nativeHandle?: string } = {}, + ): CultMeshNativeSliceViewDescriptor { + return cultMeshNativeSliceView(viewId, schemaId, rowCount, columns, options); + } + + public static denseRowStrideBytes(view: CultMeshNativeSliceViewDescriptor): number { + return cultMeshDenseRowStrideBytes(view); + } + + public static findNativeSliceColumn( + view: CultMeshNativeSliceViewDescriptor, + name: string, + ): CultMeshNativeSliceColumn | undefined { + return cultMeshFindNativeSliceColumn(view, name); + } + + public static describeNativeSliceView( + view: CultMeshNativeSliceViewDescriptor, + ): CultMeshNativeSliceViewDiagnostic { + return cultMeshDescribeNativeSliceView(view); + } + public static async createNode( cachePath: string, options: CultMeshNodeOptions = {}, @@ -1107,3 +3773,31 @@ function copyBudgetFor( return "opaque-runtime"; } } + +function nonBlankOr(value?: string, fallback = ""): string { + return value && value.trim() ? value : fallback; +} + +function parseCultMeshLocalityKind( + value: string | undefined, + fallback: CultMeshLocalityKind, +): CultMeshLocalityKind { + switch ((value ?? "").trim().toLowerCase()) { + case "automatic": + return "automatic"; + case "inprocess": + case "in-process": + return "in-process"; + case "sharedmemory": + case "shared-memory": + return "shared-memory"; + case "ipc": + return "ipc"; + case "network": + return "network"; + case "wasm": + return "wasm"; + default: + return fallback; + } +} diff --git a/packages/cultmesh-ts/test/cultmesh.test.ts b/packages/cultmesh-ts/test/cultmesh.test.ts index c14d3a47..1fd8ffa3 100644 --- a/packages/cultmesh-ts/test/cultmesh.test.ts +++ b/packages/cultmesh-ts/test/cultmesh.test.ts @@ -145,6 +145,1129 @@ test("CultMesh TS local Verse catalog exposes sorted views and watch ergonomics" ); }); +test("CultMesh TS exposes shared geometry query primitives", () => { + const viewport = CultMesh.rect(CultMesh.vec2(10, -5), CultMesh.vec2(-2, 7)); + assert.deepEqual(viewport, { + min: { x: -2, y: -5 }, + max: { x: 10, y: 7 }, + }); + + assert.deepEqual(CultMesh.viewportRequest(viewport, [3, 1]), { + minX: -2, + minY: -5, + maxX: 10, + maxY: 7, + controlledEntityIndices: [3, 1], + }); + + assert.deepEqual(CultMesh.rectFromBounds(4, 3, 2, 1), { + min: { x: 2, y: 1 }, + max: { x: 4, y: 3 }, + }); +}); + +test("CultMesh TS exposes typed operation primitives", async () => { + const operation = CultMesh.operation< + { actor: string; direction: { x: number; y: number } }, + { receipt: ReturnType; actor: string } + >("game.test.move", async (request, context) => { + assert.equal(context.runtimeId, "rts-client"); + assert.deepEqual(context.claims, [ + { role: "simulation-authority", shardId: "zone:0", leaseId: "lease:zone:0" }, + ]); + assert.deepEqual(context.routeHint, { + kind: "network", + description: "remote Verse node", + }); + assert.equal(context.idempotencyKey, "move-1"); + return { + receipt: CultMesh.operationReceipt("game.test.move", true, { + route: context.routeHint, + }), + actor: request.actor, + }; + }); + + const response = await operation.invoke( + { actor: "pawn:1", direction: CultMesh.vec2(1, 0) }, + CultMesh.operationContext("rts-client", { + claims: [ + CultMesh.authorityClaim("simulation-authority", { + shardId: "zone:0", + leaseId: "lease:zone:0", + }), + ], + routeHint: CultMesh.routeHint("network", "remote Verse node"), + idempotencyKey: "move-1", + }), + ); + + assert.equal(operation.operationId, "game.test.move"); + assert.equal(response.actor, "pawn:1"); + assert.deepEqual(CultMesh.describeOperationHandle(operation), { + operationId: "game.test.move", + }); + assert.deepEqual(response.receipt, { + operationId: "game.test.move", + accepted: true, + route: { + kind: "network", + description: "remote Verse node", + }, + diagnostic: undefined, + }); +}); + +test("CultMesh TS flattens and parses cross-runtime route records", () => { + const record = CultMesh.routeRecord( + CultMesh.routeHint("shared-memory", "co-located slab"), + ); + const restored = CultMesh.routeFromRecord(record); + const fromCsharp = CultMesh.routeFromRecord( + { + kind: "InProcess", + description: "", + }, + CultMesh.routeHint("network", "fallback route"), + ); + const fallback = CultMesh.routeFromRecord( + { + kind: "not-real", + description: "", + }, + CultMesh.routeHint("ipc", "fallback route"), + ); + + assert.deepEqual(record, { + kind: "shared-memory", + description: "co-located slab", + }); + assert.deepEqual(restored, { + kind: "shared-memory", + description: "co-located slab", + }); + assert.deepEqual(fromCsharp, { + kind: "in-process", + description: "fallback route", + }); + assert.deepEqual(fallback, { + kind: "ipc", + description: "fallback route", + }); +}); + +test("CultMesh TS exposes typed query primitives", async () => { + const watchedCounts: number[] = []; + const visibleObjects = CultMesh.query< + { viewport: ReturnType }, + { queryId: string; count: number } + >("game.test.visible_objects", async (parameters, context) => { + assert.equal(context.runtimeId, "browser-client"); + assert.deepEqual(context.routeHint, { + kind: "shared-memory", + description: "co-located daemon slab", + }); + assert.equal(parameters.viewport.minX, -10); + return { + queryId: "game.test.visible_objects", + count: 2, + }; + }, { + watchQuery: (parameters, context, callback) => { + assert.equal(context.runtimeId, "browser-client"); + assert.equal(parameters.viewport.maxX, 10); + callback({ queryId: "game.test.visible_objects", count: 3 }); + return () => watchedCounts.push(-1); + }, + }); + + const result = await visibleObjects.execute( + { viewport: CultMesh.viewportRequest(CultMesh.rectFromBounds(-10, -5, 10, 5)) }, + CultMesh.queryContext("browser-client", { + routeHint: CultMesh.routeHint("shared-memory", "co-located daemon slab"), + }), + ); + + assert.deepEqual(result, { + queryId: "game.test.visible_objects", + count: 2, + }); + + const diagnostic = CultMesh.describeQuerySurface(visibleObjects); + assert.equal(diagnostic.queryId, "game.test.visible_objects"); + assert.equal(diagnostic.routeHint.kind, "automatic"); + assert.deepEqual(diagnostic.sources, []); + assert.notEqual(diagnostic.sources, visibleObjects.sources); + + const unsubscribe = visibleObjects.watch( + { viewport: CultMesh.viewportRequest(CultMesh.rectFromBounds(-10, -5, 10, 5)) }, + "browser-client", + (value) => watchedCounts.push(value.count), + ); + unsubscribe(); + + assert.deepEqual(watchedCounts, [3, -1]); +}); + +test("CultMesh TS polling watcher turns snapshots into a query watch", async () => { + let value = 0; + const observed: number[] = []; + const query = CultMesh.query( + "game.test.counter", + async () => value, + { + watchQuery: CultMesh.pollingQueryWatcher(async () => value, { + intervalMs: 5, + equals: (left, right) => left === right, + }), + }, + ); + + const unsubscribe = query.watch(undefined, "browser-client", next => observed.push(next)); + await delay(15); + value = 1; + await delay(20); + value = 1; + await delay(12); + value = 2; + await delay(20); + unsubscribe(); + value = 3; + await delay(12); + + assert.deepEqual(observed, [0, 1, 2]); +}); + +test("CultMesh TS polling watcher can suppress the initial baseline", async () => { + let value = "baseline"; + const observed: string[] = []; + const watch = CultMesh.pollingQueryWatcher(async () => value, { + intervalMs: 5, + emitInitial: false, + }); + + const unsubscribe = watch(undefined, CultMesh.queryContext("browser-client"), next => observed.push(next)); + await delay(15); + value = "changed"; + await delay(15); + unsubscribe(); + + assert.deepEqual(observed, ["changed"]); +}); + +test("CultMesh TS exposes live feed primitives for reactive client views", async () => { + let frameId = 10; + const observed: number[] = []; + const feed = CultMesh.liveFeed< + { viewport: ReturnType }, + { frameId: number; route: string } + >( + "aetheria.rts.viewport.feed", + async (_parameters, context) => ({ + frameId, + route: context.routeHint.kind, + }), + { + sources: [ + CultMesh.projectionSource("daemon:aetheria.frame.latest.v1"), + CultMesh.projectionSource("daemon:aetheria.health.latest.v1"), + ], + routeHint: CultMesh.routeHint("shared-memory", "co-located RTS cache"), + watchFeed: CultMesh.pollingQueryWatcher(async (_parameters, context) => ({ + frameId, + route: context.routeHint.kind, + }), { + intervalMs: 5, + equals: (left, right) => left.frameId === right.frameId && left.route === right.route, + }), + }, + ); + + assert.equal(feed.feedId, "aetheria.rts.viewport.feed"); + assert.equal(feed.routeHint.kind, "shared-memory"); + assert.deepEqual(feed.sources.map(source => source.sourceId), [ + "daemon:aetheria.frame.latest.v1", + "daemon:aetheria.health.latest.v1", + ]); + + const diagnostic = CultMesh.describeLiveFeed(feed); + assert.equal(diagnostic.feedId, "aetheria.rts.viewport.feed"); + assert.equal(diagnostic.routeHint.description, "co-located RTS cache"); + assert.deepEqual(diagnostic.sources.map(source => source.sourceId), [ + "daemon:aetheria.frame.latest.v1", + "daemon:aetheria.health.latest.v1", + ]); + assert.notEqual(diagnostic.sources, feed.sources); + + const snapshot = await feed.snapshot( + { viewport: CultMesh.viewportRequest(CultMesh.rectFromBounds(-1, -1, 1, 1)) }, + "browser-client", + ); + + assert.deepEqual(snapshot, { + frameId: 10, + route: "shared-memory", + }); + + const unsubscribe = feed.watch( + { viewport: CultMesh.viewportRequest(CultMesh.rectFromBounds(-1, -1, 1, 1)) }, + "browser-client", + value => observed.push(value.frameId), + ); + await delay(15); + frameId = 11; + await delay(15); + unsubscribe(); + + assert.deepEqual(observed, [10, 11]); +}); + +test("CultMesh TS exposes projection recipes as shared query affordances", async () => { + const watchedRoutes: string[] = []; + const recipe = CultMesh.projectionRecipe< + { viewport: ReturnType }, + { projectionId: string; sourceCount: number; route: string } + >( + "aetheria.zone.objects.visible", + [ + CultMesh.projectionSource("daemon:aetheria.frame.latest.v1", { + schemaId: "gamecult.aetheria.daemon_frame.v1", + description: "latest daemon frame", + }), + CultMesh.projectionSource("daemon:aetheria.authority.policy.v1", { + schemaId: "gamecult.aetheria.authority_policy.v1", + }), + ], + async (_parameters, context) => ({ + projectionId: "aetheria.zone.objects.visible", + sourceCount: 2, + route: context.routeHint.kind, + }), + { + routeHint: CultMesh.routeHint("shared-memory", "co-located frame slab"), + watchProjection: (_parameters, context, callback) => { + callback({ + projectionId: "aetheria.zone.objects.visible", + sourceCount: 2, + route: context.routeHint.kind, + }); + return () => watchedRoutes.push("unsubscribed"); + }, + }, + ); + + assert.equal(recipe.projectionId, "aetheria.zone.objects.visible"); + assert.equal(recipe.routeHint.kind, "shared-memory"); + assert.deepEqual(recipe.sources.map((source) => source.sourceId), [ + "daemon:aetheria.frame.latest.v1", + "daemon:aetheria.authority.policy.v1", + ]); + + const recipeDiagnostic = CultMesh.describeProjectionRecipe(recipe); + assert.equal(recipeDiagnostic.projectionId, "aetheria.zone.objects.visible"); + assert.equal(recipeDiagnostic.routeHint.description, "co-located frame slab"); + assert.deepEqual(recipeDiagnostic.sources.map((source) => source.schemaId), [ + "gamecult.aetheria.daemon_frame.v1", + "gamecult.aetheria.authority_policy.v1", + ]); + assert.notEqual(recipeDiagnostic.sources, recipe.sources); + + const projected = await recipe.project( + { viewport: CultMesh.viewportRequest(CultMesh.rectFromBounds(-10, -5, 10, 5)) }, + CultMesh.queryContextFor("browser-starfire") + .route("wasm", "browser-local projection") + .build(), + ); + + assert.deepEqual(projected, { + projectionId: "aetheria.zone.objects.visible", + sourceCount: 2, + route: "wasm", + }); + + const query = recipe.asQuerySurface(); + assert.equal(query.routeHint.kind, "shared-memory"); + assert.deepEqual(query.sources.map((source) => source.schemaId), [ + "gamecult.aetheria.daemon_frame.v1", + "gamecult.aetheria.authority_policy.v1", + ]); + + const queried = await query.execute( + { viewport: CultMesh.viewportRequest(CultMesh.rectFromBounds(-1, -1, 1, 1)) }, + "unity-raven", + ); + + assert.equal(query.queryId, recipe.projectionId); + assert.equal(queried.route, "shared-memory"); + + const queryDiagnostic = CultMesh.describeQuerySurface(query); + assert.equal(queryDiagnostic.queryId, "aetheria.zone.objects.visible"); + assert.equal(queryDiagnostic.routeHint.kind, "shared-memory"); + assert.deepEqual(queryDiagnostic.sources.map((source) => source.sourceId), [ + "daemon:aetheria.frame.latest.v1", + "daemon:aetheria.authority.policy.v1", + ]); + assert.notEqual(queryDiagnostic.sources, query.sources); + + const unsubscribe = query.watch( + { viewport: CultMesh.viewportRequest(CultMesh.rectFromBounds(-1, -1, 1, 1)) }, + "unity-raven", + (value) => watchedRoutes.push(value.route), + ); + unsubscribe(); + + assert.deepEqual(watchedRoutes, ["shared-memory", "unsubscribed"]); +}); + +test("CultMesh TS describes typed surface catalogs for tools and generated bindings", () => { + const source = CultMesh.projectionSource("daemon:aetheria.frame.latest.v1", { + schemaId: "gamecult.aetheria.daemon_frame.v1", + }); + const routeHint = CultMesh.routeHint("shared-memory", "co-located frame slab"); + const query = CultMesh.query( + "aetheria.zone.objects.visible", + async () => "objects", + { + sources: [source], + routeHint, + }, + ); + const feed = CultMesh.liveFeed( + "aetheria.rts.viewport.feed", + async () => "frame", + { + sources: [source], + routeHint, + }, + ); + const operation = CultMesh.operation( + "aetheria.pilot.set_move_vector", + async () => "accepted", + ); + const pointer = CultMesh.statePointer( + "aetheria.selection.current", + async () => "entity:ship:1", + undefined, + { + sources: [source], + routeHint, + }, + ); + const nativeView = CultMesh.nativeSliceView( + "aetheria.zone.render", + "gamecult.aetheria.render_body.v1", + 128, + [CultMesh.nativeSliceColumn("position", "CultVec2", 8)], + { route: routeHint }, + ); + + const querySurface = CultMesh.describeSurface(query); + const feedSurface = CultMesh.describeSurface(feed); + const operationSurface = CultMesh.describeSurface(operation); + const pointerSurface = CultMesh.describeSurface(pointer); + const nativeViewSurface = CultMesh.describeSurface(nativeView); + const catalog = CultMesh.describeSurfaceCatalog("gamecult.aetheria.rts.surfaces.v1", [ + querySurface, + feedSurface, + operationSurface, + pointerSurface, + nativeViewSurface, + ]); + + assert.equal(querySurface.kind, "query"); + assert.equal(feedSurface.kind, "live-feed"); + assert.equal(pointerSurface.kind, "state-pointer"); + assert.equal(pointerSurface.routeHint.kind, "shared-memory"); + assert.deepEqual(pointerSurface.sources.map(next => next.schemaId), [ + "gamecult.aetheria.daemon_frame.v1", + ]); + assert.equal(nativeViewSurface.kind, "native-slice-view"); + assert.equal(catalog.catalogId, "gamecult.aetheria.rts.surfaces.v1"); + assert.deepEqual(catalog.surfaces.map(surface => surface.surfaceId), [ + "aetheria.zone.objects.visible", + "aetheria.rts.viewport.feed", + "aetheria.pilot.set_move_vector", + "aetheria.selection.current", + "aetheria.zone.render", + ]); + assert.equal(catalog.surfaces[0].routeHint.kind, "shared-memory"); + assert.deepEqual(catalog.surfaces[0].sources.map(next => next.schemaId), [ + "gamecult.aetheria.daemon_frame.v1", + ]); + assert.notEqual(catalog.surfaces[0], querySurface); + assert.notEqual(catalog.surfaces[0].sources, querySurface.sources); + assert.equal(catalog.surfaces[4].routeHint.kind, "shared-memory"); + + assert.equal( + CultMesh.findSurface(catalog, "aetheria.selection.current")?.kind, + "state-pointer", + ); + assert.equal(CultMesh.findSurface(catalog, "missing"), undefined); + + const operations = CultMesh.surfacesByKind(catalog, "operation"); + assert.deepEqual(operations.map(surface => surface.surfaceId), [ + "aetheria.pilot.set_move_vector", + ]); + assert.notEqual(operations, catalog.surfaces); + assert.notEqual(operations[0], operationSurface); + + const index = CultMesh.surfaceCatalogIndex(catalog); + assert.equal(index.catalogId, "gamecult.aetheria.rts.surfaces.v1"); + assert.deepEqual(index.queries.map(surface => surface.surfaceId), [ + "aetheria.zone.objects.visible", + ]); + assert.deepEqual(index.liveFeeds.map(surface => surface.surfaceId), [ + "aetheria.rts.viewport.feed", + ]); + assert.deepEqual(index.operations.map(surface => surface.surfaceId), [ + "aetheria.pilot.set_move_vector", + ]); + assert.deepEqual(index.statePointers.map(surface => surface.surfaceId), [ + "aetheria.selection.current", + ]); + assert.deepEqual(index.nativeSliceViews.map(surface => surface.surfaceId), [ + "aetheria.zone.render", + ]); + assert.deepEqual(index.projectionRecipes, []); + assert.notEqual(index.operations, operations); + assert.notEqual(index.operations[0], operations[0]); +}); + +test("CultMesh TS exposes fluent context builders for typed handles", async () => { + const operationContext = CultMesh.operationContextFor("unity-client") + .claim("pilot-authority", { shardId: "zone:raven" }) + .route("shared-memory", "co-located Verse") + .idempotency("move:raven:1") + .build(); + + assert.deepEqual(operationContext, { + runtimeId: "unity-client", + claims: [{ role: "pilot-authority", shardId: "zone:raven", leaseId: undefined }], + routeHint: { kind: "shared-memory", description: "co-located Verse" }, + idempotencyKey: "move:raven:1", + }); + + const queryContext = CultMesh.queryContextFor("browser-client") + .route("wasm", "browser-local projection") + .build(); + + assert.deepEqual(queryContext, { + runtimeId: "browser-client", + routeHint: { kind: "wasm", description: "browser-local projection" }, + }); +}); + +test("CultMesh TS Verse context lets generated domain sugar use shared typed contexts", async () => { + const verse = await CultMesh.connectVerse("starbridge", "browser-starfire", { + routeHint: CultMesh.routeHint("network", "remote Verse peer"), + claims: [ + CultMesh.authorityClaim("commander-control", { + shardId: "zone:frontier", + leaseId: "lease:starfire", + }), + ], + }); + const queryVerse = verse.withRoute("shared-memory", "local projection slab"); + const commandVerse = verse.withRoute("network", "remote command route"); + + const aetheria = queryVerse.use((queryContext) => { + const moveOperation = commandVerse.bindOperation(CultMesh.operation< + { entityId: number; direction: { x: number; y: number } }, + ReturnType + >("aetheria.entity.pilot.move", async (request, operationContext) => { + assert.equal(operationContext.runtimeId, "browser-starfire"); + assert.equal(operationContext.claims[0]?.role, "commander-control"); + assert.equal(operationContext.idempotencyKey, "move:starfire:1"); + return CultMesh.operationReceipt("aetheria.entity.pilot.move", request.entityId === 7, { + route: operationContext.routeHint, + }); + })); + const objectsVisible = CultMesh.bindQuery(queryContext, CultMesh.query< + { viewport: ReturnType }, + { runtimeId: string; zoneId: string; route: string; minX: number } + >("aetheria.zone.objects.visible", async (parameters, context) => ({ + runtimeId: context.runtimeId, + zoneId: "zone:frontier", + route: context.routeHint.kind, + minX: parameters.viewport.minX, + }))); + + return { + entity: (entityId: number) => ({ + pilot: { + move: (direction: { x: number; y: number }, idempotencyKey: string) => + moveOperation.invoke({ entityId, direction }, { idempotencyKey }), + }, + }), + zone: (zoneId: string) => ({ + objects: { + visibleWithin: async (viewport: ReturnType) => ({ + ...await objectsVisible.execute({ viewport }), + zoneId, + }), + }, + }), + }; + }); + + const receipt = await aetheria + .entity(7) + .pilot + .move(CultMesh.vec2(1, 0), "move:starfire:1"); + const viewport = await aetheria + .zone("zone:frontier") + .objects + .visibleWithin(CultMesh.viewportRequest(CultMesh.rectFromBounds(-16, -8, 16, 8))); + + assert.equal(verse.verseId, "starbridge"); + assert.equal(verse.runtimeId, "browser-starfire"); + assert.deepEqual(verse.operationContext({ idempotencyKey: "move:starfire:2" }), { + runtimeId: "browser-starfire", + claims: [{ role: "commander-control", shardId: "zone:frontier", leaseId: "lease:starfire" }], + routeHint: { kind: "network", description: "remote Verse peer" }, + idempotencyKey: "move:starfire:2", + }); + assert.deepEqual(queryVerse.queryContext(), { + runtimeId: "browser-starfire", + routeHint: { kind: "shared-memory", description: "local projection slab" }, + }); + assert.equal(commandVerse.bindOperation(CultMesh.operation("noop", async (_request, context) => + CultMesh.operationReceipt("noop", true, { route: context.routeHint }))).operationId, "noop"); + assert.deepEqual(receipt, { + operationId: "aetheria.entity.pilot.move", + accepted: true, + route: { + kind: "network", + description: "remote command route", + }, + diagnostic: undefined, + }); + assert.deepEqual(viewport, { + runtimeId: "browser-starfire", + zoneId: "zone:frontier", + route: "shared-memory", + minX: -16, + }); +}); + +test("CultMesh TS exposes typed state pointers for UI and tools", async () => { + const updates: string[] = []; + let value = "initial"; + const pointer = CultMesh.statePointer( + "aetheria.selection.current", + async () => value, + (callback: (value: string) => void) => { + callback(value); + return () => updates.push("unsubscribed"); + }, + { + sources: [ + CultMesh.projectionSource("daemon:aetheria.selection.current.v1", { + schemaId: "gamecult.aetheria.selection.v1", + }), + ], + routeHint: CultMesh.routeHint("shared-memory", "co-located selection cache"), + }, + ); + + assert.equal(pointer.pointerId, "aetheria.selection.current"); + assert.equal(await pointer.resolve(), "initial"); + assert.deepEqual(CultMesh.describeStatePointer(pointer), { + pointerId: "aetheria.selection.current", + routeHint: { + kind: "shared-memory", + description: "co-located selection cache", + }, + sources: [ + { + sourceId: "daemon:aetheria.selection.current.v1", + schemaId: "gamecult.aetheria.selection.v1", + description: undefined, + }, + ], + }); + + value = "selected:ship:1"; + const unsubscribe = pointer.watch((next) => updates.push(next)); + unsubscribe(); + + assert.deepEqual(updates, ["selected:ship:1", "unsubscribed"]); + + const contextualUpdates: string[] = []; + const contextualPointer = CultMesh.statePointer( + "aetheria.daemon.frame.latest", + async (context) => `${context.runtimeId}:${context.routeHint.kind}`, + (context, callback) => { + callback(`${context.runtimeId}:frame:12:${context.routeHint.kind}`); + return () => contextualUpdates.push("contextual-unsubscribed"); + }, + { + sources: [ + CultMesh.projectionSource("daemon:aetheria.frame.latest.v1", { + schemaId: "gamecult.aetheria.daemon_frame.v1", + }), + ], + routeHint: CultMesh.routeHint("shared-memory", "co-located daemon frame"), + }, + ); + const verse = CultMesh.verse("aetheria.local", "bifrost-tool") + .withRoute("ipc", "tool bridge"); + const bound = CultMesh.bindStatePointer(verse, contextualPointer); + + assert.equal(await bound.resolve(), "bifrost-tool:ipc"); + const contextualUnsubscribe = bound.watch((next) => contextualUpdates.push(next)); + contextualUnsubscribe(); + + assert.equal(bound.pointerId, "aetheria.daemon.frame.latest"); + assert.deepEqual(bound.sources.map(source => source.schemaId), [ + "gamecult.aetheria.daemon_frame.v1", + ]); + assert.deepEqual(contextualUpdates, [ + "bifrost-tool:frame:12:ipc", + "contextual-unsubscribed", + ]); +}); + +test("CultMesh TS mutable state pointers read watch and replace through a Verse", async () => { + let stored = "frame:0"; + let watcher: ((value: string) => void) | undefined; + const contexts: string[] = []; + const pointer = CultMesh.mutableStatePointer( + "aetheria.daemon.frame.latest", + async (context) => { + contexts.push(`read:${context.runtimeId}:${context.routeHint.kind}`); + return stored; + }, + (context, callback) => { + contexts.push(`watch:${context.runtimeId}:${context.routeHint.kind}`); + watcher = (value) => callback(`${context.runtimeId}:${value}:${context.routeHint.kind}`); + return () => { + watcher = undefined; + }; + }, + async (context, value) => { + contexts.push(`replace:${context.runtimeId}:${context.routeHint.kind}`); + stored = value; + watcher?.(value); + }, + { + routeHint: CultMesh.routeHint("shared-memory", "co-located daemon frame"), + sources: [ + CultMesh.projectionSource("daemon:aetheria.frame.latest.v1", { + schemaId: "gamecult.aetheria.daemon_frame.v1", + }), + ], + }, + ); + + const verse = CultMesh.verse("aetheria.local", "unity-raven", { + routeHint: CultMesh.routeHint("ipc", "tool bridge"), + }); + const bound = verse.bindMutableStatePointer(pointer); + + let observed = ""; + const unsubscribe = bound.watch((value) => { + observed = value; + }); + assert.equal(await bound.read(), "frame:0"); + await bound.replace("frame:1"); + unsubscribe(); + + assert.equal(stored, "frame:1"); + assert.equal(observed, "unity-raven:frame:1:ipc"); + assert.deepEqual(contexts, [ + "watch:unity-raven:ipc", + "read:unity-raven:ipc", + "replace:unity-raven:ipc", + ]); + assert.deepEqual(CultMesh.describeStatePointer(pointer), { + pointerId: "aetheria.daemon.frame.latest", + routeHint: { + kind: "shared-memory", + description: "co-located daemon frame", + }, + sources: [ + { + sourceId: "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1", + description: undefined, + }, + ], + }); + assert.deepEqual(CultMesh.stateBinding("frame", pointer), { + targetProp: "frame", + pointerId: "aetheria.daemon.frame.latest", + sourceId: "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1", + routeHint: { + kind: "shared-memory", + description: "co-located daemon frame", + }, + }); +}); + +test("CultMesh TS describes UI state bindings from typed state pointers", async () => { + const pointer = CultMesh.statePointer( + "aetheria.selection.current", + async () => "entity:station:0", + undefined, + { + sources: [ + CultMesh.projectionSource("daemon:aetheria.selection.current.v1", { + schemaId: "gamecult.aetheria.selection.v1", + }), + ], + routeHint: CultMesh.routeHint("shared-memory", "co-located selection cache"), + }, + ); + + const binding = CultMesh.stateBinding("value", pointer); + const explicitBinding = CultMesh.stateBinding( + "label", + "aetheria.selection.label", + { + sourceId: "daemon:aetheria.selection.label.v1", + schemaId: "gamecult.aetheria.selection_label.v1", + routeHint: CultMesh.routeHint("ipc", "tool bridge"), + }, + ); + + assert.deepEqual(binding, { + targetProp: "value", + pointerId: "aetheria.selection.current", + sourceId: "daemon:aetheria.selection.current.v1", + schemaId: "gamecult.aetheria.selection.v1", + routeHint: { + kind: "shared-memory", + description: "co-located selection cache", + }, + }); + assert.deepEqual(explicitBinding, { + targetProp: "label", + pointerId: "aetheria.selection.label", + sourceId: "daemon:aetheria.selection.label.v1", + schemaId: "gamecult.aetheria.selection_label.v1", + routeHint: { + kind: "ipc", + description: "tool bridge", + }, + }); +}); + +test("CultMesh TS flattens and rehydrates UI state binding records", () => { + const binding = CultMesh.stateBinding( + "status", + "aetheria.current.status", + { + sourceId: "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1", + routeHint: CultMesh.routeHint("shared-memory", "co-located frame slab"), + }, + ); + + assert.deepEqual(CultMesh.stateBindingRecord(binding), { + targetProp: "status", + pointerId: "aetheria.current.status", + sourceId: "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1", + routeKind: "shared-memory", + routeDescription: "co-located frame slab", + }); + + assert.deepEqual(CultMesh.stateBindingFromRecord({ + targetProp: "value", + pointerId: "aetheria.selection.current", + sourceId: "daemon:aetheria.selection.current.v1", + schemaId: "gamecult.aetheria.selection.v1", + routeKind: "InProcess", + routeDescription: "embedded tool host", + }), { + targetProp: "value", + pointerId: "aetheria.selection.current", + sourceId: "daemon:aetheria.selection.current.v1", + schemaId: "gamecult.aetheria.selection.v1", + routeHint: { + kind: "in-process", + description: "embedded tool host", + }, + }); +}); + +test("CultMesh TS describes UI command bindings from typed operations", async () => { + const operation = CultMesh.operation< + { actor: string }, + ReturnType + >("aetheria.entity.pilot.move", async (_request, context) => + CultMesh.operationReceipt("aetheria.entity.pilot.move", true, { + route: context.routeHint, + })); + + const binding = CultMesh.operationBinding(operation, { + label: "Move", + schemaId: "gamecult.aetheria.pilot_move.v1", + routeHint: CultMesh.routeHint("network", "remote Verse peer"), + }); + const explicitBinding = CultMesh.operationBinding("aetheria.surface.refresh", { + label: "Refresh", + }); + + assert.deepEqual(binding, { + operationId: "aetheria.entity.pilot.move", + label: "Move", + schemaId: "gamecult.aetheria.pilot_move.v1", + routeHint: { + kind: "network", + description: "remote Verse peer", + }, + }); + assert.deepEqual(explicitBinding, { + operationId: "aetheria.surface.refresh", + label: "Refresh", + schemaId: "", + routeHint: { + kind: "automatic", + }, + }); +}); + +test("CultMesh TS flattens and rehydrates UI operation binding records", () => { + const binding = CultMesh.operationBinding("aetheria.entity.pilot.move", { + label: "Move", + schemaId: "gamecult.aetheria.pilot_move.v1", + routeHint: CultMesh.routeHint("network", "remote Verse peer"), + }); + + assert.deepEqual(CultMesh.operationBindingRecord(binding), { + operationId: "aetheria.entity.pilot.move", + label: "Move", + schemaId: "gamecult.aetheria.pilot_move.v1", + routeKind: "network", + routeDescription: "remote Verse peer", + }); + + assert.deepEqual(CultMesh.operationBindingFromRecord({ + operationId: "aetheria.surface.refresh", + label: "Refresh", + schemaId: "gamecult.aetheria.refresh.v1", + routeKind: "SharedMemory", + routeDescription: "co-located command boundary", + }), { + operationId: "aetheria.surface.refresh", + label: "Refresh", + schemaId: "gamecult.aetheria.refresh.v1", + routeHint: { + kind: "shared-memory", + description: "co-located command boundary", + }, + }); +}); + +test("CultMesh TS carries UI command invocations as typed operation descriptors", () => { + const binding = CultMesh.operationBinding("aetheria.entity.pilot.move", { + label: "Move", + schemaId: "gamecult.aetheria.pilot_move.v1", + routeHint: CultMesh.routeHint("ipc", "local Verse node"), + }); + + const invocation = CultMesh.operationInvocation(binding, { + idempotencyKey: "move:42", + }); + const explicitInvocation = CultMesh.operationInvocation( + "aetheria.surface.refresh", + { + routeHint: CultMesh.routeHint("network", "remote Verse peer"), + }, + ); + + assert.deepEqual(invocation, { + operationId: "aetheria.entity.pilot.move", + schemaId: "gamecult.aetheria.pilot_move.v1", + routeHint: { + kind: "ipc", + description: "local Verse node", + }, + idempotencyKey: "move:42", + }); + assert.deepEqual(explicitInvocation, { + operationId: "aetheria.surface.refresh", + schemaId: "", + routeHint: { + kind: "network", + description: "remote Verse peer", + }, + idempotencyKey: undefined, + }); +}); + +test("CultMesh TS reads common operation payload scalar fields", () => { + const payload = CultMesh.operationPayload({ + value: "42.5", + tierIndex: 3, + enabled: "on", + name: "Starfire", + }); + const updated = payload.with("enabled", false); + + assert.equal(payload.getString("name"), "Starfire"); + assert.equal(payload.getString("missing", "fallback"), "fallback"); + assert.equal(payload.getInt("tierIndex", -1), 3); + assert.equal(payload.getInt("missing", -1), -1); + assert.equal(payload.getDouble("value", -1), 42.5); + assert.equal(payload.getBoolean("enabled"), true); + assert.equal(payload.getBoolean("missing", true), true); + assert.equal(updated.getBoolean("enabled", true), false); + assert.equal(updated.getString("name"), "Starfire"); + assert.deepEqual(payload.fields, { + value: "42.5", + tierIndex: "3", + enabled: "on", + name: "Starfire", + }); + assert.deepEqual(payload.toRecord(), payload.fields); +}); + +test("CultMesh TS flattens and rehydrates operation invocation records", () => { + const invocation = CultMesh.operationInvocation("aetheria.entity.pilot.move", { + schemaId: "gamecult.aetheria.pilot_move.v1", + routeHint: CultMesh.routeHint("ipc", "local Verse node"), + idempotencyKey: "move:42", + }); + + const record = CultMesh.operationInvocationRecord(invocation); + const restored = CultMesh.operationInvocationFromRecord(record, { + fallbackOperationId: "fallback.operation", + fallbackSchemaId: "fallback.schema", + fallbackRouteHint: CultMesh.routeHint("network", "fallback route"), + }); + const csharpRecord = CultMesh.operationInvocationFromRecord( + { + operationId: "", + schemaId: "", + routeKind: "InProcess", + routeDescription: "", + idempotencyKey: "", + }, + { + fallbackOperationId: "fallback.operation", + fallbackSchemaId: "fallback.schema", + fallbackRouteHint: CultMesh.routeHint("network", "fallback route"), + fallbackIdempotencyKey: "fallback-key", + }, + ); + + assert.deepEqual(record, { + operationId: "aetheria.entity.pilot.move", + schemaId: "gamecult.aetheria.pilot_move.v1", + routeKind: "ipc", + routeDescription: "local Verse node", + idempotencyKey: "move:42", + }); + assert.deepEqual(restored, invocation); + assert.deepEqual(csharpRecord, { + operationId: "fallback.operation", + schemaId: "fallback.schema", + routeHint: { + kind: "in-process", + description: "fallback route", + }, + idempotencyKey: "fallback-key", + }); +}); + +test("CultMesh TS composes named state ref resolvers for surfaces", () => { + const daemon = CultMesh.stateRefResolver( + "aetheria.daemon.refs", + (stateRef: string, context) => + stateRef === "aetheria.daemon/frame/id" + ? `${context.runtimeId}:${context.routeHint.kind}:42` + : "", + { + sources: [ + CultMesh.projectionSource("daemon:aetheria.frame.latest.v1", { + schemaId: "gamecult.aetheria.daemon_frame.v1", + }), + ], + routeHint: CultMesh.routeHint("in-process", "embedded renderer"), + }, + ); + const itemStats = CultMesh.stateRefResolver( + "aetheria.item_stats.refs", + (stateRef: string) => + stateRef === "aetheria.item_stats/laser/damage" ? "12.5" : "", + ); + const resolver = daemon.or(itemStats); + + assert.equal( + resolver.resolve( + "aetheria.daemon/frame/id", + CultMesh.queryContext("unity-raven", { + routeHint: CultMesh.routeHint("network", "remote peer"), + }), + ), + "unity-raven:network:42", + ); + assert.equal(resolver.resolve("aetheria.item_stats/laser/damage"), "12.5"); + assert.deepEqual(resolver.tryResolve("missing"), { + resolved: false, + value: "", + }); + assert.equal(resolver.asFunction()("aetheria.item_stats/laser/damage"), "12.5"); + + assert.deepEqual(CultMesh.describeStateRefResolver(resolver), { + resolverId: "aetheria.daemon.refs|aetheria.item_stats.refs", + routeHint: { + kind: "in-process", + description: "embedded renderer", + }, + sources: [ + { + sourceId: "daemon:aetheria.frame.latest.v1", + schemaId: "gamecult.aetheria.daemon_frame.v1", + description: undefined, + }, + ], + }); +}); + +test("CultMesh TS exposes native slice descriptors", () => { + const view = CultMesh.nativeSliceView( + "aetheria.zone.render", + "gamecult.aetheria.zone_render.v1", + 12, + [ + CultMesh.nativeSliceColumn("position", "CultVec3", 12), + CultMesh.nativeSliceColumn("rotationRadians", "float32", 4), + CultMesh.nativeSliceColumn("renderGroupId", "uint32", 4), + ], + { + route: CultMesh.routeHint("shared-memory", "CultCache slab"), + nativeHandle: "aetheria-zone-render-001", + }, + ); + + assert.equal(CultMesh.denseRowStrideBytes(view), 20); + assert.deepEqual(CultMesh.findNativeSliceColumn(view, "position"), { + name: "position", + valueType: "CultVec3", + elementSizeBytes: 12, + }); + assert.equal(CultMesh.findNativeSliceColumn(view, "missing"), undefined); + + const diagnostic = CultMesh.describeNativeSliceView(view); + assert.equal(diagnostic.viewId, "aetheria.zone.render"); + assert.equal(diagnostic.schemaId, "gamecult.aetheria.zone_render.v1"); + assert.equal(diagnostic.rowCount, 12); + assert.equal(diagnostic.route.kind, "shared-memory"); + assert.equal(diagnostic.nativeHandle, "aetheria-zone-render-001"); + assert.equal(diagnostic.denseRowStrideBytes, 20); + assert.deepEqual(diagnostic.columns.map(column => column.name), [ + "position", + "rotationRadians", + "renderGroupId", + ]); + assert.notEqual(diagnostic.columns, view.columns); +}); + test("CultMesh TS branded facade exposes schema and shard catalogs", () => { const schemaCatalog = CultMesh.createSchemaCatalog(); const schemaUpdates: string[] = []; @@ -615,3 +1738,7 @@ test("CultMesh TS negotiates streaming frame body transports explicitly", () => assert.deepEqual(streamUpdates, ["mimir:kiyo-pro"]); assert.deepEqual(frameUpdates, [42n]); }); + +function delay(milliseconds: number): Promise { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +} diff --git a/src/GameCult.Mesh/docs/cross-runtime-primitives-roadmap.md b/src/GameCult.Mesh/docs/cross-runtime-primitives-roadmap.md new file mode 100644 index 00000000..93fb3f3c --- /dev/null +++ b/src/GameCult.Mesh/docs/cross-runtime-primitives-roadmap.md @@ -0,0 +1,298 @@ +# CultMesh Cross-Runtime Primitives Roadmap + +CultMesh should feel like a cozy typed runtime, not a transport kit with good +manners. Application code should read as native state and native operations in +every runtime, while CultMesh chooses the fastest valid path: in-process, +shared memory, IPC, network, or WASM. + +In this roadmap, "primitive" does not mean "tiny DTO" or local convenience +wrapper. It means a reusable cross-runtime developer affordance promoted into +the shared library layer: the abstraction, metadata, generated bindings, +runtime routing, diagnostics, and fast path that make an obvious state operation +feel native everywhere. A `Rect` value is a primitive; so is a typed viewport +query surface that can execute against an in-process daemon, a shared CultCache +slab, a remote Verse peer, or a browser/WASM host without changing the caller's +API. + +This roadmap is the shared-layer backlog for abstractions that Aetheria, +Brokkr, Odin, Eve, Bifrost, Unity, Electron, Rust, and browser clients should +all be able to use without cloning local facade code. + +## Design Rule + +If two runtimes need the same semantic shape, hoist it into CultMesh or the +adjacent CultLib foundation layer. Do not leave it as an Aetheria-only wrapper, +an Electron preload convention, a Unity bridge helper, or an MCP-specific +adapter. + +CultMesh should be expansive enough to absorb repeated developer-experience +patterns, not just repeated serialization shapes. When Aetheria, Brokkr, Odin, +Bifrost, Eve, Ymir, VoidBot, Unity, TS, Rust, or browser clients all want the +same kind of state pointer, operation, query, watch, authority claim, native +view, or surface binding, that is evidence of a missing shared primitive. The +domain package keeps the nouns and policy; CultMesh owns the managed, cozy +machinery that makes those nouns feel magical and run very fast. + +The application layer may name domain operations: + +```ts +await verse.aetheria.zone(zoneId).pilot(entityId).move(CultMesh.vec2(1, 0)); +``` + +The shared layer must own the reusable primitives that make that sugar possible: +typed state pointers, operation handles, query surfaces, authority claims, +projection recipes, native slab descriptors, watches, and generated binding +metadata. + +The test is whether a downstream runtime can say the domain thing directly. If +the caller has to remember record keys, schema ids, MessagePack slots, endpoint +paths, IPC channel names, local publication files, cache filenames, queue names, +or whether the target is local/remote/WASM, the shared primitive is not cozy +enough yet. + +## Managed Magic Contract + +Every promoted primitive should carry both a high-level developer promise and a +low-level performance promise: + +| Promise | Requirement | +| --- | --- | +| Native-feeling API | Public calls look like typed domain state, operations, queries, or views rather than transport instructions. | +| One semantic surface | C#, TypeScript, Rust, browser/WASM, Unity, Eve/CultUI, and tool runtimes use the same conceptual API even when adapters differ. | +| Fastest valid route | The runtime can choose in-process calls, shared slabs, IPC, RUDP/network, copied snapshots, or WASM views without creating a second application API. | +| Typed safety | State, operation payloads, query parameters, results, authority claims, receipts, and view columns are generated or strongly typed. | +| Reactive by default | Values that can change expose watch/subscription semantics with explicit lifetime and backpressure behavior. | +| Inspectable | Diagnostics can explain the chosen route, authority decision, schema version, cache source, native handle, and copy/fallback cost. | +| Portable fallback | Remote or browser clients keep the same semantic surface when zero-copy locality is unavailable. | + +## Primitive Families + +| Primitive family | Shared responsibility | First consumers | +| --- | --- | --- | +| Typed state pointer | A stable typed reference to Verse state that can resolve, watch, and be embedded in Eve/CultUI surfaces. | Eve, Bifrost MCP, Unity inspectors, RTS panels | +| State binding descriptor | Surface metadata that binds a UI/tool prop to a typed CultMesh state pointer with source schema and route hints. | Eve/CultUI, Bifrost MCP, Unity inspectors, RTS status panels | +| Typed operation handle | Method-shaped invocation with typed request/response, authority claims, idempotency, route hints, and receipts. | Aetheria pilot/commander verbs, Brokkr editor tools | +| Operation binding descriptor | Surface metadata that binds a UI/tool command to a typed CultMesh operation id with request schema and route hints. | Eve/CultUI, Bifrost MCP, Unity controls, RTS command panels | +| Operation invocation descriptor | Concrete UI/tool invocation metadata that preserves operation id, request schema, route hints, and idempotency across renderer boundaries. | Eve/CultUI lowerers, Unity UI Toolkit, RTS panels, Bifrost tools | +| Operation payload | Shared invocation field bag with typed scalar readers, copy-on-write updates, and legacy string-field ingestion while generated request schemas take over. | Eve/CultUI lowerers, Unity UI Toolkit, RTS panels, Bifrost tools | +| Typed query surface | Derived-state query with typed parameters and result, callable locally or remotely without exposing transport. | Aetheria objects/gravity/selection/inventory viewports | +| Projection recipe | Declarative composition of typed documents into a named local projection, cacheable and watchable by clients. | Unity render shell, Electron RTS map, browser CultMesh-only clients | +| Authority scope | Runtime claim, lease, witness, or delegated simulation ownership expressed as typed policy state. | Trusted co-op, future quorum/witness modes | +| Native slice view | Typed SoA/slab view descriptors that map to Unity `NativeArray`, Rust slices, C# spans, or browser typed arrays. | Aetheria render state, Ymir physics state | +| Schema-generated binding | Cross-runtime generated metadata for document slots, enum ids, schema ids, and ergonomic method surfaces. | C#, TypeScript, Rust, WASM | +| Reactive watch | Subscription primitive over typed documents, pointers, queries, and native views with disposal/backpressure semantics. | CultUI, Unity presentation caches, RTS status panels | +| Managed Verse context | Shared `verse.use(schema)` binding context carrying runtime id, route hints, and authority claims into generated domain operations and queries. | Unity client, RTS client, browser-only CultMesh clients, Rust rebuild | +| Verse-bound surface | Operation, query, and live-feed handles bound once to a Verse context so generated facades call domain methods without repeatedly materializing transport contexts. | Aetheria RTS facade, Unity facade generation, Rust/WASM bindings | +| Schema-stamped cache recovery | Directory-backed CultCache records may recover missing hot-manifest catalog entries from schema-version metadata embedded in the cold payload, then resolve by shared schema name. | Aetheria persisted Verse state, long-lived daemon stores, editor/runtime co-deployment | + +## Required Sugar Shape + +The shared primitives should let domain packages expose APIs that look like +state, not transport. + +TypeScript target: + +```ts +const verse = await CultMesh.connectVerse("starbridge"); +const aetheria = verse.use(Aetheria); + +const viewport = await aetheria.zone(zoneId) + .objects() + .visibleTo(controlledUnits) + .within(CultMesh.rectFromBounds(-400, -250, 400, 250)) + .query(); + +await aetheria.entity(ravenId) + .pilot() + .move(CultMesh.vec2(1, 0), { claim: "pilot-control" }); +``` + +C# target: + +```csharp +var verse = await CultMesh.ConnectVerseAsync("starbridge"); +var aetheria = verse.Use(AetheriaRuntime.Schema); + +var viewport = await aetheria.Zone(zoneId) + .Objects() + .VisibleTo(controlledUnits) + .Within(CultRect.FromBounds(-400, -250, 400, 250)) + .QueryAsync(); + +await aetheria.Entity(ravenId) + .Pilot() + .MoveAsync(CultVec2.Right, claim: "pilot-control"); +``` + +Current seed: + +```csharp +var verse = await CultMesh.ConnectVerseAsync( + "starbridge", + "unity-raven", + new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "co-located Verse"), + new[] { new CultMeshAuthorityClaim("pilot-control", shardId: "zone:raven") }); + +var aetheria = verse.Use(context => new AetheriaGeneratedFacade(context)); +await aetheria.Entity(ravenId).Pilot.MoveAsync(CultVec2.Right); +``` + +```ts +const verse = await CultMesh.connectVerse("starbridge", "browser-starfire", { + routeHint: CultMesh.routeHint("network", "remote Verse peer"), + claims: [CultMesh.authorityClaim("commander-control", { shardId: "zone:frontier" })], +}); + +const aetheria = verse.use((context) => new AetheriaGeneratedFacade(context)); +await aetheria.entity(ravenId).pilot.move(CultMesh.vec2(1, 0)); +``` + +Generated facades should split Verse views by locality and bind surfaces once: + +```ts +const queryVerse = verse.withRoute("shared-memory", statePath); +const commandVerse = verse + .withRoute("network", endpoint) + .withClaim("commander-control", { shardId: "aetheria.local" }); + +const objectsViewport = CultMesh.bindQuery(queryVerse, queries.objectsViewport); +const setMoveVector = CultMesh.bindOperation(commandVerse, operations.setMoveVector); + +await objectsViewport.execute(viewport); +await setMoveVector.invoke({ actorEntityKey, directionX: 1, directionY: 0 }); +``` + +That seed is intentionally context-only. Discovery, authority negotiation, +transport selection, native slabs, and WASM adapters can grow behind the same +Verse handle without forcing Unity, Electron, browser, or Rust clients to learn +a second public API. + +Rust/WASM target: + +```rust +let verse = CultMesh::connect_verse("starbridge").await?; +let aetheria = verse.use_schema(aetheria::schema()); + +let viewport = aetheria + .zone(zone_id) + .objects() + .visible_to(&controlled_units) + .within(CultRect::from_bounds(-400.0, -250.0, 400.0, 250.0)) + .query() + .await?; +``` + +These examples are intentionally aspirational. They are the shape the primitive +families must support; Aetheria should not keep bespoke client facades once the +shared layer can express the same semantics. + +## Staged Build + +1. Stabilize handle descriptors. + `CultMeshStatePointer`, `CultMeshOperationHandle`, + `CultMeshQuerySurface`, + `CultMeshProjectionRecipe`, `CultMeshRouteHint`, and + `CultMeshNativeSliceViewDescriptor` are the current seed. Keep them small, + serializable where useful, and mirrored across C# and TypeScript. Bound + operation, query, and live-feed wrappers are the first managed layer above + those descriptors; generated facades should prefer them over ad hoc + `OperationContext` / `QueryContext` plumbing. + +2. Add generated binding descriptors. + Domain packages should generate schema ids, slots, enum ids, state pointer + factories, query handles, and operation handles from the same source + contract. Hand-entered MessagePack slot maps are compatibility debt. + +3. Grow projection recipes. + The seed now names source state, route hints, projection execution, and + query-surface conversion. Next it should add generated source handles, local + cache keys, invalidation/watch behavior, and native-view output where + applicable. This turns Aetheria `ObjectsViewport`, `GravityViewport`, + selection, inventory, stats, station stock, and authority status into + portable CultMesh surfaces. + +4. Add transparent state refs for UI runtimes. + Eve/CultUI surfaces may carry a typed `CultMeshStatePointer` instead of a + JSON blob or string state ref. The UI runtime resolves and watches it using + the local Verse node, with Bifrost/Odin using the same pointer semantics for + MCP tools. State pointers now carry route hints and typed source metadata in + both C# and TypeScript, so a surface catalog can advertise where a pointer + resolves and which schema-backed document/source it depends on instead of + leaking daemon-specific resolver conventions. They also bind to a Verse like + operations, queries, and live feeds: `CultMesh.StatePointer(...)` / + `CultMesh.statePointer(...)` create pointers, and + `CultMesh.bindStatePointer(...)` / `verse.BindStatePointer(...)` produce a + bound pointer that resolves and watches through the Verse query context + instead of requiring UI or tool hosts to rebuild runtime-id and route + plumbing. Mutable document-style handles use the same shared shape: + `CultMeshMutableStatePointer` / `CultMesh.mutableStatePointer(...)` + add typed replacement to the read/watch state pointer contract so tools, + editor surfaces, and authorized clients do not grow private + `Document` wrappers. `CultMeshStateBindingDescriptor` / `CultMesh.stateBinding(...)` + now hoists the repeatable "component prop -> state pointer" shape into the + shared layer, including pointer id, source id, schema id, and route hint. + Eve documents, Unity inspectors, browser panels, and Bifrost tools should + carry this descriptor instead of inventing local `stateRef` string props. + `CultMeshOperationBindingDescriptor` / `CultMesh.operationBinding(...)` + applies the same rule to controls: a UI command advertises a typed operation + id, optional request schema, label, and route hint instead of making + `command` and `transport` strings the canonical API. + +5. Add native slab adapters. + `CultMeshNativeSliceViewDescriptor` must map to Unity `NativeArray`, C# + `ReadOnlySpan`, Rust slices, and TS typed arrays where locality permits. + The descriptor names the column and value type; runtime adapters own handles, + fences, and unavoidable copy accounting. + +6. Add authority-aware routing. + Operation/query invocation should accept authority claims and route hints, + then choose local, shared-memory, IPC, network, or WASM execution without + exposing a second public API for each route. + +7. Add cross-runtime conformance tests. + The same primitive examples must pass in C# and TypeScript first, then Rust. + Aetheria client tests should assert against these shared primitives rather + than against private Unity/Electron bridges. + +8. Harden durable cache ergonomics. + Directory-backed CultCache stores should behave like managed state, not + fragile manifest bookkeeping. A missing catalog entry in the hot manifest + must not strand a readable cold record when the payload carries a schema + version. The C# directory store now infers a minimal catalog entry from a + schema-stamped payload and resolves by schema name, with regression coverage + in `GameCult.Caching.Tests`. The TypeScript and Rust single-file MessagePack + stores now perform the same schema-stamped recovery at the snapshot boundary, + and both cache runtimes normalize recovered schema-name envelopes back to the + registered public document type before hydration. Python now mirrors the + same managed recovery path for stores and raw CultNet replication helpers, + and the TypeScript inspector reports recovered schema names instead of + leaking stale manifest ids. Keep this parity intact before relying on + long-lived cross-runtime Verse stores. + +## Aetheria Pressure Tests + +Aetheria should exercise the roadmap in this order: + +1. Sector map and Starbridge session summary as typed state pointers. +2. Objects/gravity viewport split as typed query surfaces. +3. Selected object, inventory, station stock, and current stats as projection + recipes. +4. Pilot movement, targeting, equipment activation, docking, repair, cooling, + salvage, and commander verbs as typed operation handles. +5. Zone render and Ymir physics views as native slice descriptors. +6. Eve/CultUI panels that embed state pointers and operation handles instead of + local payload maps. +7. Unity and RTS clients consuming the same generated domain sugar. + +## Stop Lines + +- Do not add new public Aetheria command facades unless they can be expressed as + typed CultMesh operation handles. +- Do not add new viewport APIs unless they can be expressed as typed CultMesh + query surfaces or projection recipes. +- Do not add new UI state-ref strings when a typed state pointer would express + the same thing. +- Do not add native memory sharing as a Unity-only bridge; describe the slab in + CultMesh and let Unity be one adapter. +- Do not accept runtime parity by convention. C#, TypeScript, and Rust need + shared generated contracts or explicit conformance tests.