Implement main-thread Camino sessions and synchronous CRDT editing
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user