132 lines
4.8 KiB
TypeScript
132 lines
4.8 KiB
TypeScript
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<InterfaceOperation, "mode" | "inputType" | "outputType"> {
|
||
|
|
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),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|