Implement durable typed journals, subscriptions, and Reader document connections

Add checked native journal/subscription bindings, managed-reference retention,
bounded history and backpressure, atomic delivery receipts, and fenced hosting.
Preserve source publication prediction and replica reconciliation; subscription
attachment, controls, and delivery remain authoritative in this initial version.

Connect independent manual/Gutenberg document producers to Reader bookshelves
through typed adapters. Add connection inspection, shared provider selection,
authoring documentation, and restart/rejection/fan-out integration coverage.

Verified compiler suite (287 passed, two existing skips), Nix runtime and
PostgreSQL qualification, five example assemblies, headless Reader integration,
Chromium Reader integration, resource pins, formatting, and lint.
No pinned workspace runtime was deployed.
This commit is contained in:
Timothy J. Aveni
2026-09-28 19:35:51 -07:00
parent 0ace71eae6
commit 1e72713ae2
30 changed files with 2949 additions and 2033 deletions
+1 -1
View File
@@ -203,7 +203,7 @@ function methods(
.map(({ member, op }) => {
const execution: ExecutionContract = op.execution ?? { kind: "remote" };
if (
(execution.kind === "remote" ? !remote : !execution.targets.includes(target)) ||
(execution.kind === "remote" && !remote) ||
op.mode !== "call" ||
(op.scope === "class" && (remote || req.receiver === "input"))
)
File diff suppressed because one or more lines are too long
@@ -81,79 +81,81 @@ REVISION=80
SEMANTIC_MAJOR=81
ON_DELETE=82
LIFECYCLE=83
RETAIN_OTHER=84
KEYED=85
PUBLIC_TRAVERSAL=86
ID=87
DOC=88
MODE=89
EMITS=90
RECEIVER=91
REQUIRES=92
ANY=93
GET=94
SET=95
WATCH=96
START=97
STOP=98
READ=99
WRITE=100
RESOLVE=101
CONNECT=102
DISCONNECT=103
CALL=104
WATCH_START=105
WATCH_STOP=106
SUBSCRIBE=107
UNSUBSCRIBE=108
REGISTER=109
CRDT=110
OPTIONAL_ONE=111
EXACTLY_ONE=112
MANY_UNIQUE=113
MANY=114
ORDERED=115
UNIT=116
WATCH_HANDLE=117
MESSAGE=118
ATOM_REF=119
INTERFACE_REF=120
OPTIONAL=121
LIST=122
RECORD=123
BOOL=124
BYTES=125
DOUBLE=126
INT32=127
INT64=128
STRING=129
UINT32=130
UINT64=131
TRUE=132
FALSE=133
NULL=134
ARROW=135
COLON=136
SEMI=137
COMMA=138
DOT=139
LBRACE=140
RBRACE=141
LBRACK=142
RBRACK=143
LPAREN=144
RPAREN=145
LT=146
GT=147
AMP=148
EQUAL=149
INTEGER=150
JSON_NUMBER=151
IDENTIFIER=152
STRING_LITERAL=153
LINE_COMMENT=154
BLOCK_COMMENT=155
WS=156
JOURNAL=84
SUBSCRIPTION=85
RETAIN_OTHER=86
KEYED=87
PUBLIC_TRAVERSAL=88
ID=89
DOC=90
MODE=91
EMITS=92
RECEIVER=93
REQUIRES=94
ANY=95
GET=96
SET=97
WATCH=98
START=99
STOP=100
READ=101
WRITE=102
RESOLVE=103
CONNECT=104
DISCONNECT=105
CALL=106
WATCH_START=107
WATCH_STOP=108
SUBSCRIBE=109
UNSUBSCRIBE=110
REGISTER=111
CRDT=112
OPTIONAL_ONE=113
EXACTLY_ONE=114
MANY_UNIQUE=115
MANY=116
ORDERED=117
UNIT=118
WATCH_HANDLE=119
MESSAGE=120
ATOM_REF=121
INTERFACE_REF=122
OPTIONAL=123
LIST=124
RECORD=125
BOOL=126
BYTES=127
DOUBLE=128
INT32=129
INT64=130
STRING=131
UINT32=132
UINT64=133
TRUE=134
FALSE=135
NULL=136
ARROW=137
COLON=138
SEMI=139
COMMA=140
DOT=141
LBRACE=142
RBRACE=143
LBRACK=144
RBRACK=145
LPAREN=146
RPAREN=147
LT=148
GT=149
AMP=150
EQUAL=151
INTEGER=152
JSON_NUMBER=153
IDENTIFIER=154
STRING_LITERAL=155
LINE_COMMENT=156
BLOCK_COMMENT=157
WS=158
'edit'=1
'module'=2
'contract'=3
@@ -237,69 +239,71 @@ WS=156
'semantic-major'=81
'on-delete'=82
'lifecycle'=83
'retain-other'=84
'keyed'=85
'public-traversal'=86
'id'=87
'doc'=88
'mode'=89
'emits'=90
'receiver'=91
'requires'=92
'any'=93
'get'=94
'set'=95
'watch'=96
'start'=97
'stop'=98
'read'=99
'write'=100
'resolve'=101
'connect'=102
'disconnect'=103
'call'=104
'watch-start'=105
'watch-stop'=106
'subscribe'=107
'unsubscribe'=108
'register'=109
'crdt'=110
'optional-one'=111
'exactly-one'=112
'many-unique'=113
'many'=114
'ordered'=115
'unit'=116
'watch-handle'=117
'message'=118
'atom-ref'=119
'interface-ref'=120
'optional'=121
'list'=122
'record'=123
'bool'=124
'bytes'=125
'double'=126
'int32'=127
'int64'=128
'string'=129
'uint32'=130
'uint64'=131
'true'=132
'false'=133
'null'=134
'->'=135
':'=136
';'=137
','=138
'.'=139
'{'=140
'}'=141
'['=142
']'=143
'('=144
')'=145
'<'=146
'>'=147
'&'=148
'='=149
'journal'=84
'subscription'=85
'retain-other'=86
'keyed'=87
'public-traversal'=88
'id'=89
'doc'=90
'mode'=91
'emits'=92
'receiver'=93
'requires'=94
'any'=95
'get'=96
'set'=97
'watch'=98
'start'=99
'stop'=100
'read'=101
'write'=102
'resolve'=103
'connect'=104
'disconnect'=105
'call'=106
'watch-start'=107
'watch-stop'=108
'subscribe'=109
'unsubscribe'=110
'register'=111
'crdt'=112
'optional-one'=113
'exactly-one'=114
'many-unique'=115
'many'=116
'ordered'=117
'unit'=118
'watch-handle'=119
'message'=120
'atom-ref'=121
'interface-ref'=122
'optional'=123
'list'=124
'record'=125
'bool'=126
'bytes'=127
'double'=128
'int32'=129
'int64'=130
'string'=131
'uint32'=132
'uint64'=133
'true'=134
'false'=135
'null'=136
'->'=137
':'=138
';'=139
','=140
'.'=141
'{'=142
'}'=143
'['=144
']'=145
'('=146
')'=147
'<'=148
'>'=149
'&'=150
'='=151
File diff suppressed because one or more lines are too long
@@ -81,79 +81,81 @@ REVISION=80
SEMANTIC_MAJOR=81
ON_DELETE=82
LIFECYCLE=83
RETAIN_OTHER=84
KEYED=85
PUBLIC_TRAVERSAL=86
ID=87
DOC=88
MODE=89
EMITS=90
RECEIVER=91
REQUIRES=92
ANY=93
GET=94
SET=95
WATCH=96
START=97
STOP=98
READ=99
WRITE=100
RESOLVE=101
CONNECT=102
DISCONNECT=103
CALL=104
WATCH_START=105
WATCH_STOP=106
SUBSCRIBE=107
UNSUBSCRIBE=108
REGISTER=109
CRDT=110
OPTIONAL_ONE=111
EXACTLY_ONE=112
MANY_UNIQUE=113
MANY=114
ORDERED=115
UNIT=116
WATCH_HANDLE=117
MESSAGE=118
ATOM_REF=119
INTERFACE_REF=120
OPTIONAL=121
LIST=122
RECORD=123
BOOL=124
BYTES=125
DOUBLE=126
INT32=127
INT64=128
STRING=129
UINT32=130
UINT64=131
TRUE=132
FALSE=133
NULL=134
ARROW=135
COLON=136
SEMI=137
COMMA=138
DOT=139
LBRACE=140
RBRACE=141
LBRACK=142
RBRACK=143
LPAREN=144
RPAREN=145
LT=146
GT=147
AMP=148
EQUAL=149
INTEGER=150
JSON_NUMBER=151
IDENTIFIER=152
STRING_LITERAL=153
LINE_COMMENT=154
BLOCK_COMMENT=155
WS=156
JOURNAL=84
SUBSCRIPTION=85
RETAIN_OTHER=86
KEYED=87
PUBLIC_TRAVERSAL=88
ID=89
DOC=90
MODE=91
EMITS=92
RECEIVER=93
REQUIRES=94
ANY=95
GET=96
SET=97
WATCH=98
START=99
STOP=100
READ=101
WRITE=102
RESOLVE=103
CONNECT=104
DISCONNECT=105
CALL=106
WATCH_START=107
WATCH_STOP=108
SUBSCRIBE=109
UNSUBSCRIBE=110
REGISTER=111
CRDT=112
OPTIONAL_ONE=113
EXACTLY_ONE=114
MANY_UNIQUE=115
MANY=116
ORDERED=117
UNIT=118
WATCH_HANDLE=119
MESSAGE=120
ATOM_REF=121
INTERFACE_REF=122
OPTIONAL=123
LIST=124
RECORD=125
BOOL=126
BYTES=127
DOUBLE=128
INT32=129
INT64=130
STRING=131
UINT32=132
UINT64=133
TRUE=134
FALSE=135
NULL=136
ARROW=137
COLON=138
SEMI=139
COMMA=140
DOT=141
LBRACE=142
RBRACE=143
LBRACK=144
RBRACK=145
LPAREN=146
RPAREN=147
LT=148
GT=149
AMP=150
EQUAL=151
INTEGER=152
JSON_NUMBER=153
IDENTIFIER=154
STRING_LITERAL=155
LINE_COMMENT=156
BLOCK_COMMENT=157
WS=158
'edit'=1
'module'=2
'contract'=3
@@ -237,69 +239,71 @@ WS=156
'semantic-major'=81
'on-delete'=82
'lifecycle'=83
'retain-other'=84
'keyed'=85
'public-traversal'=86
'id'=87
'doc'=88
'mode'=89
'emits'=90
'receiver'=91
'requires'=92
'any'=93
'get'=94
'set'=95
'watch'=96
'start'=97
'stop'=98
'read'=99
'write'=100
'resolve'=101
'connect'=102
'disconnect'=103
'call'=104
'watch-start'=105
'watch-stop'=106
'subscribe'=107
'unsubscribe'=108
'register'=109
'crdt'=110
'optional-one'=111
'exactly-one'=112
'many-unique'=113
'many'=114
'ordered'=115
'unit'=116
'watch-handle'=117
'message'=118
'atom-ref'=119
'interface-ref'=120
'optional'=121
'list'=122
'record'=123
'bool'=124
'bytes'=125
'double'=126
'int32'=127
'int64'=128
'string'=129
'uint32'=130
'uint64'=131
'true'=132
'false'=133
'null'=134
'->'=135
':'=136
';'=137
','=138
'.'=139
'{'=140
'}'=141
'['=142
']'=143
'('=144
')'=145
'<'=146
'>'=147
'&'=148
'='=149
'journal'=84
'subscription'=85
'retain-other'=86
'keyed'=87
'public-traversal'=88
'id'=89
'doc'=90
'mode'=91
'emits'=92
'receiver'=93
'requires'=94
'any'=95
'get'=96
'set'=97
'watch'=98
'start'=99
'stop'=100
'read'=101
'write'=102
'resolve'=103
'connect'=104
'disconnect'=105
'call'=106
'watch-start'=107
'watch-stop'=108
'subscribe'=109
'unsubscribe'=110
'register'=111
'crdt'=112
'optional-one'=113
'exactly-one'=114
'many-unique'=115
'many'=116
'ordered'=117
'unit'=118
'watch-handle'=119
'message'=120
'atom-ref'=121
'interface-ref'=122
'optional'=123
'list'=124
'record'=125
'bool'=126
'bytes'=127
'double'=128
'int32'=129
'int64'=130
'string'=131
'uint32'=132
'uint64'=133
'true'=134
'false'=135
'null'=136
'->'=137
':'=138
';'=139
','=140
'.'=141
'{'=142
'}'=143
'['=144
']'=145
'('=146
')'=147
'<'=148
'>'=149
'&'=150
'='=151
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+78 -6
View File
@@ -1789,10 +1789,14 @@ const lowerState = (state: LoweringState, context: StateDeclContext): StateSlotD
valueType: lowerValueType(state, context.valueType()),
storagePolicy: policy.REGISTER()
? { kind: "register" }
: {
kind: "crdt-document",
updateType: lowerValueType(state, policy.valueType()),
},
: policy.JOURNAL()
? { kind: "journal", limits: JSON.parse(text(policy.jsonObject())) }
: policy.SUBSCRIPTION()
? { kind: "subscription", payload: lowerValueType(state, policy.valueType()) }
: {
kind: "crdt-document",
updateType: lowerValueType(state, policy.valueType()),
},
defaultValue,
};
};
@@ -2239,9 +2243,9 @@ const lowerConformanceRule = (state: LoweringState, context: ConformanceDeclCont
);
}
}
} else if (provider.STATE() || provider.EDGE()) {
} else if (provider.STATE() || provider.JOURNAL() || provider.SUBSCRIPTION() || provider.EDGE()) {
const name = identifier(provider.identifier(0));
const expected = provider.STATE() ? "state" : "edge";
const expected = provider.STATE() || provider.JOURNAL() || provider.SUBSCRIPTION() ? "state" : "edge";
// Concrete attachments are registered before rules, so unused rules can
// also check public traversal without waiting for an application.
if (provider.EDGE() && !privateStorage.has(name)) {
@@ -2435,6 +2439,74 @@ const lowerConformanceBody = (
return [];
}
const provider = bindingContext.operationProvider();
if (provider.SUBSCRIPTION()) {
const attachment = requireSymbol(
state,
state.attachments,
identifier(provider.identifier(0)),
provider,
"attachment",
);
const primitive = identifier(provider.identifier(1));
if (
!attachment ||
attachment.attachment.kind !== "state" ||
attachment.attachment.storagePolicy.kind !== "subscription" ||
!["attach", "status", "pause", "resume", "retire"].includes(primitive)
) {
loweringIssue(
state,
provider,
"invalid-subscription-binding",
"Expected native subscription storage and a public subscription primitive",
);
return [];
}
return [
{
operationId,
binding: {
kind: "subscription",
slotId: attachment.attachment.id,
primitive: primitive as "attach" | "status" | "pause" | "resume" | "retire",
},
},
];
}
if (provider.JOURNAL()) {
const attachment = requireSymbol(
state,
state.attachments,
identifier(provider.identifier(0)),
provider,
"attachment",
);
const primitive = identifier(provider.identifier(1));
if (
!attachment ||
attachment.attachment.kind !== "state" ||
attachment.attachment.storagePolicy.kind !== "journal" ||
!["publish", "readPage", "head"].includes(primitive)
) {
loweringIssue(
state,
provider,
"invalid-journal-binding",
"Expected a native journal and publish, readPage or head",
);
return [];
}
return [
{
operationId,
binding: {
kind: "journal",
slotId: attachment.attachment.id,
primitive: primitive as "publish" | "readPage" | "head",
},
},
];
}
if (provider.LIFECYCLE()) {
if (identifier(provider.identifier(0)) !== "delete") {
loweringIssue(state, provider, "invalid-lifecycle-binding", "Only lifecycle.delete is supported");
+4
View File
@@ -196,6 +196,10 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
};
const binding = (parent: GraphNode, value: Binding, atomId: string, context: unknown) => {
if (value.kind === "lifecycle") return;
if (value.kind === "journal" || value.kind === "subscription") {
dependency(parent, { kind: "state", slotId: value.slotId }, atomId);
return;
}
if (value.kind !== "package") {
dependency(parent, value, atomId);
return;
+22 -3
View File
@@ -25,6 +25,7 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
requested: ExecutionContract | undefined,
provided: ExecutionContract | undefined,
path: string,
requireTargets = true,
) => {
if (!requested || requested.kind === "remote") return;
if (!provided || provided.kind === "remote") {
@@ -33,8 +34,9 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
}
if (requested.kind === "snapshot" && provided.kind !== "snapshot")
fail(path, "Snapshot execution cannot perform a transaction");
for (const target of requested.targets ?? [])
if (!provided.targets?.includes(target)) fail(path, `Dependency does not provide execution target ${target}`);
if (requireTargets)
for (const target of requested.targets ?? [])
if (!provided.targets?.includes(target)) fail(path, `Dependency does not provide execution target ${target}`);
};
for (const revision of workspace.interfaceImports)
for (const member of revision.template?.members ?? revision.members) {
@@ -82,7 +84,10 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
if (entry.execution && entry.execution.kind !== "remote") {
if (!selected) fail(portPath, "Portable interface ports must select their required operations");
for (const id of selected ?? [])
if (contract) supports(entry.execution, operations(id, contract)?.execution, portPath);
// Package targets describe available code. Required public target
// guarantees are checked over the installed operation closure;
// an optional implementation target must not strengthen a dependency.
if (contract) supports(entry.execution, operations(id, contract)?.execution, portPath, false);
}
}
if (!entry.execution || entry.execution.kind === "remote") continue;
@@ -150,6 +155,20 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
continue;
}
if (!operation.execution || operation.execution.kind === "remote") continue;
if (binding.kind === "journal") {
const expected = binding.primitive === "publish" ? "transaction" : "snapshot";
if (operation.execution.kind !== expected) fail(path, "Native journal operation requires the matching effect");
continue;
}
if (binding.kind === "subscription") {
const expected = binding.primitive === "status" ? "snapshot" : "transaction";
if (
operation.execution.kind !== expected ||
(binding.primitive !== "status" && operation.execution.targets.some((target) => target !== "server"))
)
fail(path, "Subscription controls require authoritative execution; status is a portable snapshot");
continue;
}
if (binding.kind === "package") {
const provider = packages.get(binding.packageRevisionId)?.exports.find((e) => e.id === binding.exportId);
supports(operation.execution, provider?.execution, path);
+13 -2
View File
@@ -24,7 +24,13 @@ export function validateInitialization(workspace: WorkspaceRevision): Capability
fail(ownerKey(owner), `Initialization input ${field.input} must name an attachment owned by this conformance`);
return;
}
if (attachment.kind === "state") return attachment.valueType;
if (attachment.kind === "state") {
if (["journal", "subscription"].includes(attachment.storagePolicy.kind)) {
fail(ownerKey(owner), "Native journals start empty and cannot be seeded as state");
return;
}
return attachment.valueType;
}
if (binding.kind !== "edge") return;
const index = attachment.endpoints.findIndex((e) => e.projectionId === binding.projectionId);
const endpoint = attachment.endpoints[index];
@@ -102,7 +108,12 @@ export function validateInitialization(workspace: WorkspaceRevision): Capability
...workspace.conformances.flatMap((o) => o.privateAttachments),
].filter((a) => a.kind === "state" && a.attachedTo === binding.atomId);
for (const state of states)
if (state.kind === "state" && state.defaultValue === undefined && !initialized.has(state.id))
if (
state.kind === "state" &&
!["journal", "subscription"].includes(state.storagePolicy.kind) &&
state.defaultValue === undefined &&
!initialized.has(state.id)
)
fail(portPath, `Required state ${state.id} has no default or declared seed mapping`);
}
};
+131
View File
@@ -0,0 +1,131 @@
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),
},
},
};
}
+15 -1
View File
@@ -196,7 +196,19 @@ export interface InterfaceRevision {
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
}
export type StoragePolicy = { kind: "register" } | { kind: "crdt-document"; updateType: ValueType };
export interface JournalStorageLimits {
maxEntries: number;
maxBytes: number;
maxPublicationBytes: number;
maxProtectedSubscriptions: number;
}
export type StoragePolicy =
| { kind: "register" }
| { kind: "crdt-document"; updateType: ValueType }
| { kind: "journal"; limits: JournalStorageLimits }
| { kind: "subscription"; payload: ValueType };
export type JournalPrimitive = "publish" | "readPage" | "head";
export type SubscriptionPrimitive = "attach" | "status" | "pause" | "resume" | "retire";
export interface StateSlotDefinition {
kind: "state";
@@ -417,6 +429,8 @@ export interface RelationshipMaterialization {
}
export type Binding =
| { kind: "journal"; slotId: SlotId; primitive: JournalPrimitive }
| { kind: "subscription"; slotId: SlotId; primitive: SubscriptionPrimitive }
| { kind: "lifecycle"; primitive: "delete" }
| {
kind: "interface";
+145 -32
View File
@@ -1,4 +1,10 @@
import { isStorableType } from "./generics.js";
import {
journalPayloadType,
journalPrimitiveSignature,
subscriptionContract,
subscriptionStatusType,
} from "./journals.js";
import { validateInitialization } from "./initialization.js";
import { validatePresentations } from "./presentations.js";
import { validateModules } from "./modules.js";
@@ -511,15 +517,32 @@ const atomSatisfiesConstraint = (
? constraint.atomId === atomId
: conformances.has(conformanceKey(atomId, constraint.interfaceRevisionId));
const interfaceImplies = (
indexes: ValidationIndexes,
actual: InterfaceRevisionId,
required: InterfaceRevisionId,
visited = new Set<string>(),
): boolean => {
if (actual === required) return true;
if (visited.has(actual)) return false;
visited.add(actual);
return (indexes.interfaces.get(actual)?.revision.requiredInterfaces ?? []).some((entry) =>
interfaceImplies(indexes, entry, required, visited),
);
};
const constraintSatisfiesConstraint = (
actual: EdgeEndpointConstraint,
required: EdgeEndpointConstraint,
conformances: ReadonlyMap<string, Conformance>,
indexes: ValidationIndexes,
) =>
constraintsEqual(actual, required) ||
(actual.kind === "atom" &&
required.kind === "interface" &&
atomSatisfiesConstraint(actual.atomId, required, conformances));
atomSatisfiesConstraint(actual.atomId, required, indexes.conformances)) ||
(actual.kind === "interface" &&
required.kind === "interface" &&
interfaceImplies(indexes, actual.interfaceRevisionId, required.interfaceRevisionId));
const canAccessAttachment = (
owner: AttachmentOwner,
@@ -858,24 +881,12 @@ const validateInterfaces = (
issues: CapabilityValidationIssue[],
indexes: ValidationIndexes,
) => {
const implies = (
actual: InterfaceRevisionId,
required: InterfaceRevisionId,
visited = new Set<string>(),
): boolean => {
if (actual === required) return true;
if (visited.has(actual)) return false;
visited.add(actual);
return (indexes.interfaces.get(actual)?.revision.requiredInterfaces ?? []).some((entry) =>
implies(entry, required, visited),
);
};
for (const [packageIndex, pkg] of workspace.packageImports.entries()) {
for (const obligation of pkg.argumentRequirements ?? []) {
const satisfied =
obligation.target.kind === "atom"
? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required))
: implies(obligation.target.interfaceRevisionId, obligation.required);
: interfaceImplies(indexes, obligation.target.interfaceRevisionId, obligation.required);
if (!satisfied)
issue(
issues,
@@ -890,7 +901,7 @@ const validateInterfaces = (
for (const required of revision.requiredInterfaces ?? []) {
if (!indexes.interfaces.has(required))
issue(issues, "unresolved-reference", path, `Unknown prerequisite interface ${required}`);
else if (implies(required, revision.revisionId))
else if (interfaceImplies(indexes, required, revision.revisionId))
issue(
issues,
"cyclic-conformance-requirement",
@@ -902,7 +913,7 @@ const validateInterfaces = (
const satisfied =
obligation.target.kind === "atom"
? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required))
: implies(obligation.target.interfaceRevisionId, obligation.required);
: interfaceImplies(indexes, obligation.target.interfaceRevisionId, obligation.required);
if (!satisfied)
issue(issues, "unsatisfied-interface", path, `Generic argument does not implement ${obligation.required}`);
}
@@ -946,7 +957,8 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
(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) ||
(!["journal", "subscription"].includes(attachment.storagePolicy.kind) &&
containsRpcType(attachment.valueType)) ||
(attachment.storagePolicy.kind === "crdt-document" && containsRpcType(attachment.storagePolicy.updateType))
) {
issue(
@@ -959,6 +971,43 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
if (attachment.storagePolicy.kind === "crdt-document") {
validateValueType(issues, attachment.storagePolicy.updateType, `${path}.storagePolicy.updateType`, indexes);
}
if (attachment.storagePolicy.kind === "journal") {
if (!journalPayloadType(attachment.valueType) || attachment.defaultValue !== undefined)
issue(issues, "invalid-attachment", path, "Journals require finite typed payloads and start empty");
const limits = attachment.storagePolicy.limits;
const ceilings = {
maxEntries: 10000,
maxBytes: 1048576,
maxPublicationBytes: 262144,
maxProtectedSubscriptions: 256,
};
if (
!limits ||
Object.keys(limits).length !== 4 ||
Object.entries(ceilings).some(
([key, ceiling]) =>
!Number.isSafeInteger(limits[key as keyof typeof limits]) ||
limits[key as keyof typeof limits] < 1 ||
limits[key as keyof typeof limits] > ceiling,
) ||
limits.maxPublicationBytes > limits.maxBytes
)
issue(issues, "invalid-attachment", path, "Invalid bounded journal limits");
}
if (attachment.storagePolicy.kind === "subscription") {
validateValueType(issues, attachment.storagePolicy.payload, `${path}.storagePolicy.payload`, indexes);
if (
!journalPayloadType(attachment.valueType) ||
!journalPayloadType(attachment.storagePolicy.payload) ||
attachment.defaultValue !== undefined
)
issue(
issues,
"invalid-attachment",
path,
"Subscriptions require finite typed payloads/parameters and start unattached",
);
}
if (owner.kind === "conformance") {
if (attachment.attachedTo !== owner.atomId) {
issue(
@@ -1413,6 +1462,13 @@ const validateBoundDependencies = (
break;
}
validateAttachmentAccess(issues, attachment, params.conformance, `${path}.binding.slotId`);
if (["journal", "subscription"].includes(attachment.attachment.storagePolicy.kind))
issue(
issues,
"invalid-dependency-binding",
path,
"Journal storage requires checked journal interface calls, not ordinary state ports",
);
const traversal = stateBinding.via
? validateTraversal(
issues,
@@ -1519,11 +1575,7 @@ const validateBoundDependencies = (
);
}
if (
!constraintSatisfiesConstraint(
projection.target.constraint,
requirement.target,
params.indexes.conformances,
) ||
!constraintSatisfiesConstraint(projection.target.constraint, requirement.target, params.indexes) ||
requirement.cardinality !== projection.endpoint.cardinality ||
requirement.keyType !== projection.endpoint.keyType
) {
@@ -1822,7 +1874,9 @@ const validateConformances = (
) {
const native =
member.kind === "value"
? binding.kind === "state" && binding.primitive === "read"
? (binding.kind === "state" && binding.primitive === "read") ||
(binding.kind === "journal" && binding.primitive === "head") ||
(binding.kind === "subscription" && binding.primitive === "status")
: binding.kind === "edge" && binding.primitive === "resolve";
if (!native && (member.queryRead.execution === "native" || binding.kind !== "package"))
issue(
@@ -1878,6 +1932,67 @@ const validateConformances = (
);
continue;
}
if (binding.kind === "journal") {
const attachment = findAttachment(indexes, "state", binding.slotId);
if (
!attachment ||
attachment.attachment.kind !== "state" ||
attachment.attachment.storagePolicy.kind !== "journal"
) {
issue(issues, "invalid-state-binding", bindingPath, "Expected native journal storage");
continue;
}
validateAttachmentAccess(issues, attachment, conformance, bindingPath);
if (
attachment.attachment.attachedTo !== conformance.atomId ||
!["publish", "readPage", "head"].includes(binding.primitive) ||
!signaturesMatch(operation, journalPrimitiveSignature(attachment.attachment.valueType, binding.primitive))
)
issue(
issues,
"invalid-state-binding",
bindingPath,
"Journal primitive does not match this object operation's signature",
);
continue;
}
if (binding.kind === "subscription") {
const attachment = findAttachment(indexes, "state", binding.slotId);
if (
!attachment ||
attachment.attachment.kind !== "state" ||
attachment.attachment.storagePolicy.kind !== "subscription"
) {
issue(issues, "invalid-state-binding", bindingPath, "Expected native subscription storage");
continue;
}
validateAttachmentAccess(issues, attachment, conformance, bindingPath);
const slot = attachment.attachment;
if (slot.storagePolicy.kind !== "subscription") continue;
if (slot.attachedTo !== conformance.atomId)
issue(issues, "invalid-state-binding", bindingPath, "Subscription belongs to another atom");
if (binding.primitive === "attach") {
try {
subscriptionContract(
slot.storagePolicy.payload,
slot.valueType,
operation,
[...indexes.interfaces.values()].map((value) => value.revision),
);
} catch (error) {
issue(issues, "invalid-state-binding", bindingPath, String(error));
}
} else if (
!["status", "pause", "resume", "retire"].includes(binding.primitive) ||
!signaturesMatch(operation, {
mode: "call",
inputType: valueType.unit,
outputType: binding.primitive === "status" ? subscriptionStatusType : valueType.unit,
})
)
issue(issues, "invalid-state-binding", bindingPath, "Subscription primitive signature mismatch");
continue;
}
if (binding.kind === "state") {
const attachment = findAttachment(indexes, "state", binding.slotId);
if (!attachment || attachment.attachment.kind !== "state") {
@@ -1890,6 +2005,8 @@ const validateConformances = (
continue;
}
validateAttachmentAccess(issues, attachment, conformance, `${bindingPath}.binding.slotId`);
if (["journal", "subscription"].includes(attachment.attachment.storagePolicy.kind))
issue(issues, "invalid-state-binding", bindingPath, "Use native journal primitives, not state access");
if (attachment.attachment.attachedTo !== conformance.atomId) {
issue(
issues,
@@ -1990,11 +2107,7 @@ const validateConformances = (
"Edge primitives may only implement relationship members",
);
} else if (
!constraintSatisfiesConstraint(
projection.target.constraint,
operationEntry.member.target,
indexes.conformances,
) ||
!constraintSatisfiesConstraint(projection.target.constraint, operationEntry.member.target, indexes) ||
operationEntry.member.cardinality !== projection.endpoint.cardinality ||
(operationEntry.member.keyType !== undefined && operationEntry.member.keyType !== projection.endpoint.keyType)
) {
@@ -2002,7 +2115,7 @@ const validateConformances = (
issues,
"invalid-edge-binding",
`${bindingPath}.binding`,
"Edge projection target or cardinality does not match relationship member",
`Edge projection target or cardinality does not match ${conformance.atomId}/${conformance.interfaceRevisionId}/${operationEntry.member.displayName}`,
);
}
if (!signaturesMatch(operation, expected)) {
@@ -2531,7 +2644,7 @@ export const resolveOperationPlan = (
attachment: owned as OwnedAttachment & { attachment: EdgeDefinition },
};
}
if (binding.kind === "lifecycle") return undefined; // Old runtime has no lifecycle provider.
if (binding.kind === "lifecycle" || binding.kind === "journal" || binding.kind === "subscription") return undefined; // These native providers require the current kernel.
const packageRevision = plan.packages.get(binding.packageRevisionId);
const packageExport = packageRevision?.exports.find(
(entry): entry is PackageOperationExport | PackageFunctionExport =>
@@ -2679,7 +2792,7 @@ export const computeCapabilityClosure = (
queued.push({ atomId: root.atomId, interfaceRevisionId: member.subject.interfaceRevisionId });
}
for (const binding of conformance.operationBindings.values()) {
if (binding.kind === "state") {
if (binding.kind === "state" || binding.kind === "journal" || binding.kind === "subscription") {
attachments.add(binding.slotId);
} else if (binding.kind === "edge") {
attachments.add(binding.edgeTypeId);
+105 -2
View File
@@ -9,6 +9,7 @@ import type {
} from "../capability-model/types.js";
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
import { portableValueSchema } from "../bindings/portable.js";
import { subscriptionContract } from "../capability-model/journals.js";
type PreparedStep = string | null | { case: string; id: string };
function preparedInputPaths(type: ValueType, path: PreparedStep[] = []): PreparedStep[][] {
if (type.kind === "change") return [path];
@@ -73,10 +74,105 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
if (!shape)
throw new ExecutionPlanError("UNSUPPORTED_STORAGE_CODEC", storage.id, "Unsupported canonical value type");
ownerBySlot.set(storage.id, owner.id);
const pinsFor = (payload: ValueType) => {
const expectations = new Map<string, { kind: "atom" | "interface"; id: string }>();
const walk = (type: ValueType): void => {
if (type.kind === "object-ref") {
const expectation = {
kind: type.expectation.kind,
id: type.expectation.kind === "atom" ? type.expectation.atomId : type.expectation.interfaceRevisionId,
};
expectations.set(JSON.stringify(expectation), expectation);
} else if (type.kind === "list" || type.kind === "optional") walk(type.value);
else if (type.kind === "record") Object.values(type.fields).forEach(walk);
else if (type.kind === "variant") Object.values(type.cases).forEach((entry) => walk(entry.value));
};
walk(payload);
const pins = [...expectations.values()].map((expectation) => {
const id = `journal-pin:${hexDigest([storage.id, expectation])}`;
const projection = `${id}:source`;
edges.push({
id,
owner: owner.id,
a: {
projection,
constraint: { kind: "atom", id: owner.atom },
cardinality: "many",
keyType: "string",
onDelete: "detach",
},
b: { projection: `${id}:target`, constraint: expectation, cardinality: "many", onDelete: "restrict" },
});
return { edge: id, projection };
});
return pins;
};
let journal: unknown, subscription: unknown;
if (storage.storagePolicy.kind === "journal") {
journal = { limits: storage.storagePolicy.limits, pins: pinsFor(storage.valueType) };
}
if (storage.storagePolicy.kind === "subscription") {
const bindings = plan.definition.operations.filter(
(operation) =>
operation.atomId === owner.atom &&
operation.binding.kind === "subscription" &&
operation.binding.slotId === storage.id &&
operation.binding.primitive === "attach",
);
if (bindings.length !== 1)
throw new ExecutionPlanError(
"SUBSCRIPTION_ATTACHMENT_REQUIRED",
storage.id,
"Native subscription storage requires one checked attachment operation",
);
const binding = bindings[0]!;
const operation = workspace.interfaceImports
.find((iface) => iface.revisionId === binding.interfaceRevisionId)!
.members.flatMap((member) => member.operations)
.find((operation) => operation.id === binding.operationId)!;
const contract = subscriptionContract(
storage.storagePolicy.payload,
storage.valueType,
operation,
workspace.interfaceImports,
);
const expectation = (value: typeof contract.source) => ({
kind: value.kind,
id: value.kind === "atom" ? value.atomId : value.interfaceRevisionId,
});
const source = expectation(contract.source),
handler = expectation(contract.handler);
const configuration: ValueType = {
kind: "record",
fields: {
source: { kind: "object-ref", expectation: contract.source },
handler: { kind: "object-ref", expectation: contract.handler },
parameters: storage.valueType,
},
};
const edge = `subscription-active:${hexDigest(storage.id)}`,
projection = `${edge}:subscription`;
edges.push({
id: edge,
owner: owner.id,
a: { projection, constraint: { kind: "atom", id: owner.atom }, cardinality: "many", onDelete: "restrict" },
b: { projection: `${edge}:stream`, constraint: source, cardinality: "many", onDelete: "restrict" },
});
subscription = {
payload: schema(storage.storagePolicy.payload),
source,
handler,
operation: contract.operation,
pins: pinsFor(configuration),
active: { edge, projection },
};
}
slots.push({
id: storage.id,
schema: shape,
policy: storage.storagePolicy.kind === "register" ? "register" : "crdt",
policy: storage.storagePolicy.kind === "crdt-document" ? "crdt" : storage.storagePolicy.kind,
...(journal ? { journal } : {}),
...(subscription ? { subscription } : {}),
...(Object.hasOwn(storage, "defaultValue") ? { defaultLiteral: storage.defaultValue } : {}),
});
} else lowerEdge(storage, owner.id);
@@ -109,6 +205,13 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
let target: unknown;
let unsupported: string | null = null;
if (binding.kind === "lifecycle") target = { kind: "lifecycle", primitive: binding.primitive };
else if (binding.kind === "journal" || binding.kind === "subscription")
target = {
kind: binding.kind,
owner: slotOwner(binding.slotId),
slot: binding.slotId,
primitive: binding.primitive,
};
else if (binding.kind === "state")
target = { kind: "state", owner: slotOwner(binding.slotId), slot: binding.slotId, primitive: binding.primitive };
else if (binding.kind === "edge")
@@ -231,7 +334,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
operation: operation.operationId,
scope: operation.scope,
effect: operation.execution.kind,
targets: operation.execution.kind === "remote" ? [] : operation.execution.targets,
targets: operation.availableTargets,
input,
...(inputChanges.length ? { inputChanges } : {}),
output,
+45 -1
View File
@@ -45,6 +45,8 @@ export interface PlannedOperation {
operationId: string;
scope: "object" | "class";
execution: ExecutionContract;
/** Installed implementation closure; the public execution contract above remains required. */
availableTargets: string[];
input: ValueType;
output: ValueType;
/** Concrete target after public forwarding. Never chosen by a runtime caller. */
@@ -240,6 +242,7 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
operationId: operation,
scope: publicContract.scope === "class" ? "class" : "object",
execution: publicContract.execution ?? { kind: "remote" },
availableTargets: [],
input: publicContract.inputType,
output: publicContract.outputType,
binding,
@@ -251,7 +254,8 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
checkValue(planned.input, `${key}/input`, true);
checkValue(planned.output, `${key}/output`, true);
}
if (binding.kind === "state") planned.dependencies.push({ kind: "state", slot: binding.slotId });
if (binding.kind === "state" || binding.kind === "journal" || binding.kind === "subscription")
planned.dependencies.push({ kind: "state", slot: binding.slotId });
else if (binding.kind === "edge")
planned.dependencies.push({ kind: "edge", edge: binding.edgeTypeId, projection: binding.projectionId });
else if (binding.kind === "package") {
@@ -338,6 +342,46 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
};
walk(root.key, [root.key]);
}
// Additional implementation targets are opportunities, not new public requirements.
// Start with provider support and remove targets until the complete installed
// dependency closure agrees. This also handles recursive call graphs without
// mistaking an unfinished DFS for proof of availability.
for (const node of operationMap.values()) {
if (node.execution.kind === "remote") continue;
const binding = node.binding;
const provider =
binding.kind === "package"
? packages.get(binding.packageRevisionId)!.exports.find((entry) => entry.id === binding.exportId)!.execution
: binding.kind === "lifecycle" || (binding.kind === "subscription" && binding.primitive !== "status")
? { kind: "transaction" as const, targets: ["server" as const] }
: { kind: node.execution.kind, targets: ["browser" as const, "server" as const] };
if (provider && provider.kind !== "remote") node.availableTargets = [...provider.targets].sort();
}
let targetsChanged = true;
while (targetsChanged) {
targetsChanged = false;
for (const node of operationMap.values()) {
const available = node.availableTargets.filter((target) =>
node.dependencies.every((dependency) => {
if (dependency.kind === "operation")
return operationMap.get(dependency.operation)!.availableTargets.includes(target);
if (dependency.kind === "query") {
const query = queryMap.get(dependency.query)!;
return (
node.execution.kind === "snapshot" &&
query.checked.declaration.placement?.kind === "local" &&
queryOperations(query).every((key) => operationMap.get(key)!.availableTargets.includes(target))
);
}
return true;
}),
);
if (available.length !== node.availableTargets.length) {
node.availableTargets = available;
targetsChanged = true;
}
}
}
const plannedQueries: PlannedQuery[] = queries.map((query) => ({
id: queryKey(query.packageRevisionId, query.checked.declaration.id),
query,
+5
View File
@@ -98,6 +98,11 @@ export function linkQueries(workspace: WorkspaceRevision): LinkedQuery[] {
`Package query field ${effect.memberId} needs query-reason`,
);
} else if (binding.kind === "state" && binding.primitive === "read") needed.add(binding.slotId);
else if (
(binding.kind === "journal" && binding.primitive === "head") ||
(binding.kind === "subscription" && binding.primitive === "status")
)
needed.add(binding.slotId);
else if (binding.kind === "edge" && binding.primitive === "resolve") needed.add(binding.edgeTypeId);
else
throw new QueryCompileError(
+4 -1
View File
@@ -116,7 +116,10 @@ export const queryRuntimePlan = (linked: LinkedQuery, workspace: WorkspaceRevisi
"many-unique": Cardinality.MANY_UNIQUE,
}[member.cardinality]
: undefined,
slotId: field.binding.kind === "state" ? field.binding.slotId : "",
slotId:
["state", "journal", "subscription"].includes(field.binding.kind) && "slotId" in field.binding
? field.binding.slotId
: "",
edgeTypeId: field.binding.kind === "edge" ? field.binding.edgeTypeId : "",
projectionId: field.binding.kind === "edge" ? field.binding.projectionId : "",
rpc: field.binding.kind === "package",