Implement main-thread Camino sessions and synchronous CRDT editing

This commit is contained in:
Timothy J. Aveni
2026-09-27 23:57:29 -07:00
parent ad72185a6c
commit 696023b4ba
24 changed files with 3784 additions and 3218 deletions
+22
View File
@@ -16,6 +16,7 @@ export type ValueTypeExpression =
| Extract<ValueType, { kind: "builtin" | "scalar" | "message" }>
| { kind: "parameter"; parameterId: string }
| { kind: "record"; fields: Record<string, ValueTypeExpression> }
| { kind: "variant"; id: string; cases: Record<string, { id: string; value: ValueTypeExpression }> }
| { kind: "list" | "optional" | "change"; value: ValueTypeExpression }
| { kind: "object-ref"; expectation: ObjectTypeExpression }
| { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] };
@@ -195,6 +196,8 @@ export const isStorableType = (
return false;
case "record":
return Object.values(value.fields).every((field) => visit(field, depth + 1));
case "variant":
return Object.values(value.cases).every((entry) => visit(entry.value, depth + 1));
}
};
return visit(type, 0);
@@ -221,6 +224,14 @@ export const canonicalTypeArgument = (argument: ClosedTypeArgument): string => {
return [value.kind, value.name];
case "message":
return ["message", value.descriptorId];
case "variant":
return [
"variant",
value.id,
Object.keys(value.cases)
.sort()
.map((label) => [label, value.cases[label].id, type(value.cases[label].value, depth + 1)]),
];
case "object-ref":
return ["object-ref", target(value.expectation)];
case "change":
@@ -323,6 +334,17 @@ export class TypeSubstitution {
value(expression: ValueTypeExpression, path = "type", depth = 0): ValueType {
this.budget.enter(path, depth);
switch (expression.kind) {
case "variant":
return {
kind: "variant",
id: expression.id,
cases: Object.fromEntries(
Object.entries(expression.cases).map(([label, entry]) => [
label,
{ id: entry.id, value: this.value(entry.value, `${path}.${label}`, depth + 1) },
]),
),
};
case "builtin":
return { kind: "builtin", name: expression.name };
case "scalar":
+3
View File
@@ -65,6 +65,7 @@ export type ValueType =
| { kind: "scalar"; name: ScalarValueTypeName }
| { kind: "message"; descriptorId: string }
| { kind: "record"; fields: Record<string, ValueType> }
| { kind: "variant"; id: string; cases: Record<string, { id: string; value: ValueType }> }
| { kind: "object-ref"; expectation: ObjectExpectation }
| { kind: "optional"; value: ValueType }
| { kind: "list"; value: ValueType }
@@ -270,6 +271,8 @@ export interface DependencyPort {
}
interface PackageExportBase {
/** Returns this declared birth, with unchanged operation input as its initializer. */
preparation?: { port: DependencyPortId; key: string };
/** Required authoritative snapshot export in this package. */
acceptanceExportId?: PackageExportId;
execution?: ExecutionContract;
+54
View File
@@ -148,6 +148,11 @@ const requireText = (issues: CapabilityValidationIssue[], value: string, path: s
const typeLabel = (type: ValueType): string => {
switch (type.kind) {
case "variant":
return `variant:${type.id}{${Object.entries(type.cases)
.sort(([a], [b]) => a.localeCompare(b))
.map(([label, entry]) => `${label}:${entry.id}:${typeLabel(entry.value)}`)
.join(";")}}`;
case "builtin":
case "scalar":
return type.name;
@@ -176,6 +181,19 @@ export const valueTypesEqual = (left: ValueType, right: ValueType): boolean => {
return false;
}
switch (left.kind) {
case "variant": {
const other = right as typeof left;
return (
left.id === other.id &&
Object.keys(left.cases).length === Object.keys(other.cases).length &&
Object.entries(left.cases).every(
([label, entry]) =>
Object.hasOwn(other.cases, label) &&
entry.id === other.cases[label].id &&
valueTypesEqual(entry.value, other.cases[label].value),
)
);
}
case "builtin":
case "scalar":
return left.name === (right as typeof left).name;
@@ -232,6 +250,20 @@ const validateValueType = (
indexes: Pick<ValidationIndexes, "atoms" | "interfaces">,
) => {
switch (type.kind) {
case "variant": {
requireText(issues, type.id, `${path}.id`, "Variant identity");
const entries = Object.entries(type.cases),
ids = new Set<string>();
if (!entries.length || entries.length > 256) issue(issues, "invalid-value-type", path, "Use 1–256 variant cases");
for (const [label, entry] of entries) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(label)) issue(issues, "invalid-value-type", path, "Invalid variant label");
requireText(issues, entry.id, `${path}.${label}.id`, "Case identity");
if (ids.has(entry.id)) issue(issues, "invalid-value-type", path, "Duplicate case identity");
ids.add(entry.id);
validateValueType(issues, entry.value, `${path}.${label}`, indexes);
}
return;
}
case "record":
for (const [name, field] of Object.entries(type.fields)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
@@ -569,6 +601,7 @@ const collectIdentityIndexes = (
if (member.kind === "value") {
const queryValue = (type: ValueType): boolean =>
type.kind === "scalar" ||
(type.kind === "variant" && isStorableType(type)) ||
((type.kind === "optional" || type.kind === "list") && queryValue(type.value)) ||
(type.kind === "record" &&
Object.keys(type.fields).length > 0 &&
@@ -893,6 +926,7 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
type.kind === "object-ref" ||
type.kind === "change" ||
(type.kind === "record" && Object.values(type.fields).some(containsRpcType)) ||
(type.kind === "variant" && Object.values(type.cases).some((entry) => containsRpcType(entry.value))) ||
((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value));
if (
containsRpcType(attachment.valueType) ||
@@ -999,6 +1033,26 @@ const validatePackages = (
const exportPath = `${path}.exports[${exportIndex}]`;
validateValueType(issues, entry.inputType, `${exportPath}.inputType`, indexes);
validateValueType(issues, entry.outputType, `${exportPath}.outputType`, indexes);
if (entry.preparation) {
const port = entry.dependencyPorts.find((port) => port.id === entry.preparation!.port);
if (
entry.execution?.kind !== "transaction" ||
entry.kind === "constructor" ||
(entry.kind === "operation" && entry.mode !== "call") ||
!entry.preparation.key ||
port?.requirement.kind !== "creation" ||
!valueTypesEqual(port.requirement.inputType, entry.inputType) ||
entry.outputType.kind !== "object-ref" ||
(entry.outputType.expectation.kind === "atom" &&
entry.outputType.expectation.atomId !== port.requirement.atomId)
)
issue(
issues,
"invalid-dependency-binding",
exportPath,
"Prepared creation requires a transaction returning its declared creation, with matching captured input types and a fixed key",
);
}
if (entry.acceptanceExportId !== undefined) {
const check = revision.exports.find((candidate) => candidate.id === entry.acceptanceExportId);
if (