import { valueType, type JournalPrimitive, type InterfaceOperation, type InterfaceRevision, type ObjectExpectation, type OperationId, type ValueType, } from "./types.js"; import { valueTypesEqual } from "./validation.js"; export const subscriptionStatusType: ValueType = { kind: "record", fields: { status: valueType.string, generation: valueType.uint64, cursor: valueType.uint64, failure: { kind: "optional", value: { kind: "record", fields: { publication: valueType.string, code: valueType.string, message: valueType.string }, }, }, }, }; /** Native subscription configuration is selected by the checked attach contract. * No library/interface ID or handler method name is reserved by Camino. */ export function subscriptionContract( payload: ValueType, parameters: ValueType, attach: InterfaceOperation, interfaces: readonly InterfaceRevision[], ): { source: ObjectExpectation; handler: ObjectExpectation; operation: OperationId } { const fail = (message: string): never => { throw new Error(message); }; if ( attach.mode !== "call" || attach.scope === "class" || !valueTypesEqual(attach.outputType, valueType.unit) || attach.inputType.kind !== "record" ) return fail("Subscription attachment must be an object call returning unit"); const fields = attach.inputType.fields; if ( Object.keys(fields).sort().join(",") !== "handler,parameters,source,start" || fields.source?.kind !== "object-ref" || fields.handler?.kind !== "object-ref" || fields.handler.expectation.kind !== "interface" || !valueTypesEqual(fields.parameters!, parameters) ) return fail("Attachment needs typed source/handler references, matching parameters, and a start policy"); const start = fields.start; if ( start?.kind !== "variant" || Object.keys(start.cases).sort().join(",") !== "afterCursor,future,retained" || !valueTypesEqual(start.cases.future!.value, valueType.unit) || !valueTypesEqual(start.cases.retained!.value, valueType.unit) || !valueTypesEqual(start.cases.afterCursor!.value, valueType.string) ) return fail("Subscription start requires future:unit, retained:unit and afterCursor:string variant cases"); const handler = fields.handler.expectation; const expected: ValueType = { kind: "record", fields: { event: payload, parameters } }; const candidates = interfaces .find((iface) => iface.revisionId === handler.interfaceRevisionId) ?.members.flatMap((member) => member.operations) .filter( (operation) => operation.mode === "call" && operation.scope !== "class" && operation.execution?.kind === "transaction" && operation.execution.targets.includes("server") && valueTypesEqual(operation.inputType, expected) && valueTypesEqual(operation.outputType, valueType.unit), ) ?? []; if (candidates.length !== 1) return fail("Handler interface must select one unambiguous server transaction from {event, parameters} to unit"); return { source: fields.source.expectation, handler, operation: candidates[0]!.id }; } /** Journals persist checked values, including managed references. They cannot * persist process handles, opaque messages or edit commands. */ export function journalPayloadType(type: ValueType): boolean { switch (type.kind) { case "scalar": case "object-ref": return true; case "builtin": return type.name === "unit"; case "list": case "optional": return journalPayloadType(type.value); case "record": return Object.values(type.fields).every(journalPayloadType); case "variant": return Object.values(type.cases).every((entry) => journalPayloadType(entry.value)); default: return false; } } export function journalPrimitiveSignature( payload: ValueType, primitive: JournalPrimitive, ): Pick { const optional = (value: ValueType): ValueType => ({ kind: "optional", value }); if (primitive === "publish") return { mode: "call", inputType: payload, outputType: { kind: "record", fields: { publication: valueType.string } }, }; if (primitive === "head") return { mode: "call", inputType: valueType.unit, outputType: optional(valueType.uint64) }; return { mode: "call", inputType: { kind: "record", fields: { after: optional(valueType.string), limit: valueType.uint32 } }, outputType: { kind: "record", fields: { entries: { kind: "list", value: { kind: "record", fields: { publication: valueType.string, position: valueType.uint64, payload } }, }, next: optional(valueType.string), asOfHead: optional(valueType.uint64), }, }, }; }