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
+10
View File
@@ -288,6 +288,8 @@ stateDecl
storagePolicy storagePolicy
: REGISTER : REGISTER
| CRDT LPAREN valueType RPAREN | CRDT LPAREN valueType RPAREN
| JOURNAL jsonObject
| SUBSCRIPTION LPAREN valueType RPAREN
; ;
edgeDecl edgeDecl
@@ -367,6 +369,8 @@ operationName
operationProvider operationProvider
: LIFECYCLE DOT identifier : LIFECYCLE DOT identifier
| JOURNAL identifier DOT identifier
| SUBSCRIPTION identifier DOT identifier
| STATE identifier DOT statePrimitive | STATE identifier DOT statePrimitive
| EDGE identifier DOT identifier DOT edgePrimitive | EDGE identifier DOT identifier DOT edgePrimitive
| INTERFACE identifier typeArguments? DOT memberOperationRef | INTERFACE identifier typeArguments? DOT memberOperationRef
@@ -512,6 +516,10 @@ identifier
| DEADLINE_MS | DEADLINE_MS
| PRESENTATION | PRESENTATION
| SUBJECT | SUBJECT
| MESSAGE
| JOURNAL
| SUBSCRIPTION
| CONNECT
| FOR | FOR
; ;
@@ -602,6 +610,8 @@ REVISION: 'revision';
SEMANTIC_MAJOR: 'semantic-major'; SEMANTIC_MAJOR: 'semantic-major';
ON_DELETE: 'on-delete'; ON_DELETE: 'on-delete';
LIFECYCLE: 'lifecycle'; LIFECYCLE: 'lifecycle';
JOURNAL: 'journal';
SUBSCRIPTION: 'subscription';
RETAIN_OTHER: 'retain-other'; RETAIN_OTHER: 'retain-other';
KEYED: 'keyed'; KEYED: 'keyed';
PUBLIC_TRAVERSAL: 'public-traversal'; PUBLIC_TRAVERSAL: 'public-traversal';
+1 -1
View File
@@ -203,7 +203,7 @@ function methods(
.map(({ member, op }) => { .map(({ member, op }) => {
const execution: ExecutionContract = op.execution ?? { kind: "remote" }; const execution: ExecutionContract = op.execution ?? { kind: "remote" };
if ( if (
(execution.kind === "remote" ? !remote : !execution.targets.includes(target)) || (execution.kind === "remote" && !remote) ||
op.mode !== "call" || op.mode !== "call" ||
(op.scope === "class" && (remote || req.receiver === "input")) (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 SEMANTIC_MAJOR=81
ON_DELETE=82 ON_DELETE=82
LIFECYCLE=83 LIFECYCLE=83
RETAIN_OTHER=84 JOURNAL=84
KEYED=85 SUBSCRIPTION=85
PUBLIC_TRAVERSAL=86 RETAIN_OTHER=86
ID=87 KEYED=87
DOC=88 PUBLIC_TRAVERSAL=88
MODE=89 ID=89
EMITS=90 DOC=90
RECEIVER=91 MODE=91
REQUIRES=92 EMITS=92
ANY=93 RECEIVER=93
GET=94 REQUIRES=94
SET=95 ANY=95
WATCH=96 GET=96
START=97 SET=97
STOP=98 WATCH=98
READ=99 START=99
WRITE=100 STOP=100
RESOLVE=101 READ=101
CONNECT=102 WRITE=102
DISCONNECT=103 RESOLVE=103
CALL=104 CONNECT=104
WATCH_START=105 DISCONNECT=105
WATCH_STOP=106 CALL=106
SUBSCRIBE=107 WATCH_START=107
UNSUBSCRIBE=108 WATCH_STOP=108
REGISTER=109 SUBSCRIBE=109
CRDT=110 UNSUBSCRIBE=110
OPTIONAL_ONE=111 REGISTER=111
EXACTLY_ONE=112 CRDT=112
MANY_UNIQUE=113 OPTIONAL_ONE=113
MANY=114 EXACTLY_ONE=114
ORDERED=115 MANY_UNIQUE=115
UNIT=116 MANY=116
WATCH_HANDLE=117 ORDERED=117
MESSAGE=118 UNIT=118
ATOM_REF=119 WATCH_HANDLE=119
INTERFACE_REF=120 MESSAGE=120
OPTIONAL=121 ATOM_REF=121
LIST=122 INTERFACE_REF=122
RECORD=123 OPTIONAL=123
BOOL=124 LIST=124
BYTES=125 RECORD=125
DOUBLE=126 BOOL=126
INT32=127 BYTES=127
INT64=128 DOUBLE=128
STRING=129 INT32=129
UINT32=130 INT64=130
UINT64=131 STRING=131
TRUE=132 UINT32=132
FALSE=133 UINT64=133
NULL=134 TRUE=134
ARROW=135 FALSE=135
COLON=136 NULL=136
SEMI=137 ARROW=137
COMMA=138 COLON=138
DOT=139 SEMI=139
LBRACE=140 COMMA=140
RBRACE=141 DOT=141
LBRACK=142 LBRACE=142
RBRACK=143 RBRACE=143
LPAREN=144 LBRACK=144
RPAREN=145 RBRACK=145
LT=146 LPAREN=146
GT=147 RPAREN=147
AMP=148 LT=148
EQUAL=149 GT=149
INTEGER=150 AMP=150
JSON_NUMBER=151 EQUAL=151
IDENTIFIER=152 INTEGER=152
STRING_LITERAL=153 JSON_NUMBER=153
LINE_COMMENT=154 IDENTIFIER=154
BLOCK_COMMENT=155 STRING_LITERAL=155
WS=156 LINE_COMMENT=156
BLOCK_COMMENT=157
WS=158
'edit'=1 'edit'=1
'module'=2 'module'=2
'contract'=3 'contract'=3
@@ -237,69 +239,71 @@ WS=156
'semantic-major'=81 'semantic-major'=81
'on-delete'=82 'on-delete'=82
'lifecycle'=83 'lifecycle'=83
'retain-other'=84 'journal'=84
'keyed'=85 'subscription'=85
'public-traversal'=86 'retain-other'=86
'id'=87 'keyed'=87
'doc'=88 'public-traversal'=88
'mode'=89 'id'=89
'emits'=90 'doc'=90
'receiver'=91 'mode'=91
'requires'=92 'emits'=92
'any'=93 'receiver'=93
'get'=94 'requires'=94
'set'=95 'any'=95
'watch'=96 'get'=96
'start'=97 'set'=97
'stop'=98 'watch'=98
'read'=99 'start'=99
'write'=100 'stop'=100
'resolve'=101 'read'=101
'connect'=102 'write'=102
'disconnect'=103 'resolve'=103
'call'=104 'connect'=104
'watch-start'=105 'disconnect'=105
'watch-stop'=106 'call'=106
'subscribe'=107 'watch-start'=107
'unsubscribe'=108 'watch-stop'=108
'register'=109 'subscribe'=109
'crdt'=110 'unsubscribe'=110
'optional-one'=111 'register'=111
'exactly-one'=112 'crdt'=112
'many-unique'=113 'optional-one'=113
'many'=114 'exactly-one'=114
'ordered'=115 'many-unique'=115
'unit'=116 'many'=116
'watch-handle'=117 'ordered'=117
'message'=118 'unit'=118
'atom-ref'=119 'watch-handle'=119
'interface-ref'=120 'message'=120
'optional'=121 'atom-ref'=121
'list'=122 'interface-ref'=122
'record'=123 'optional'=123
'bool'=124 'list'=124
'bytes'=125 'record'=125
'double'=126 'bool'=126
'int32'=127 'bytes'=127
'int64'=128 'double'=128
'string'=129 'int32'=129
'uint32'=130 'int64'=130
'uint64'=131 'string'=131
'true'=132 'uint32'=132
'false'=133 'uint64'=133
'null'=134 'true'=134
'->'=135 'false'=135
':'=136 'null'=136
';'=137 '->'=137
','=138 ':'=138
'.'=139 ';'=139
'{'=140 ','=140
'}'=141 '.'=141
'['=142 '{'=142
']'=143 '}'=143
'('=144 '['=144
')'=145 ']'=145
'<'=146 '('=146
'>'=147 ')'=147
'&'=148 '<'=148
'='=149 '>'=149
'&'=150
'='=151
File diff suppressed because one or more lines are too long
@@ -81,79 +81,81 @@ REVISION=80
SEMANTIC_MAJOR=81 SEMANTIC_MAJOR=81
ON_DELETE=82 ON_DELETE=82
LIFECYCLE=83 LIFECYCLE=83
RETAIN_OTHER=84 JOURNAL=84
KEYED=85 SUBSCRIPTION=85
PUBLIC_TRAVERSAL=86 RETAIN_OTHER=86
ID=87 KEYED=87
DOC=88 PUBLIC_TRAVERSAL=88
MODE=89 ID=89
EMITS=90 DOC=90
RECEIVER=91 MODE=91
REQUIRES=92 EMITS=92
ANY=93 RECEIVER=93
GET=94 REQUIRES=94
SET=95 ANY=95
WATCH=96 GET=96
START=97 SET=97
STOP=98 WATCH=98
READ=99 START=99
WRITE=100 STOP=100
RESOLVE=101 READ=101
CONNECT=102 WRITE=102
DISCONNECT=103 RESOLVE=103
CALL=104 CONNECT=104
WATCH_START=105 DISCONNECT=105
WATCH_STOP=106 CALL=106
SUBSCRIBE=107 WATCH_START=107
UNSUBSCRIBE=108 WATCH_STOP=108
REGISTER=109 SUBSCRIBE=109
CRDT=110 UNSUBSCRIBE=110
OPTIONAL_ONE=111 REGISTER=111
EXACTLY_ONE=112 CRDT=112
MANY_UNIQUE=113 OPTIONAL_ONE=113
MANY=114 EXACTLY_ONE=114
ORDERED=115 MANY_UNIQUE=115
UNIT=116 MANY=116
WATCH_HANDLE=117 ORDERED=117
MESSAGE=118 UNIT=118
ATOM_REF=119 WATCH_HANDLE=119
INTERFACE_REF=120 MESSAGE=120
OPTIONAL=121 ATOM_REF=121
LIST=122 INTERFACE_REF=122
RECORD=123 OPTIONAL=123
BOOL=124 LIST=124
BYTES=125 RECORD=125
DOUBLE=126 BOOL=126
INT32=127 BYTES=127
INT64=128 DOUBLE=128
STRING=129 INT32=129
UINT32=130 INT64=130
UINT64=131 STRING=131
TRUE=132 UINT32=132
FALSE=133 UINT64=133
NULL=134 TRUE=134
ARROW=135 FALSE=135
COLON=136 NULL=136
SEMI=137 ARROW=137
COMMA=138 COLON=138
DOT=139 SEMI=139
LBRACE=140 COMMA=140
RBRACE=141 DOT=141
LBRACK=142 LBRACE=142
RBRACK=143 RBRACE=143
LPAREN=144 LBRACK=144
RPAREN=145 RBRACK=145
LT=146 LPAREN=146
GT=147 RPAREN=147
AMP=148 LT=148
EQUAL=149 GT=149
INTEGER=150 AMP=150
JSON_NUMBER=151 EQUAL=151
IDENTIFIER=152 INTEGER=152
STRING_LITERAL=153 JSON_NUMBER=153
LINE_COMMENT=154 IDENTIFIER=154
BLOCK_COMMENT=155 STRING_LITERAL=155
WS=156 LINE_COMMENT=156
BLOCK_COMMENT=157
WS=158
'edit'=1 'edit'=1
'module'=2 'module'=2
'contract'=3 'contract'=3
@@ -237,69 +239,71 @@ WS=156
'semantic-major'=81 'semantic-major'=81
'on-delete'=82 'on-delete'=82
'lifecycle'=83 'lifecycle'=83
'retain-other'=84 'journal'=84
'keyed'=85 'subscription'=85
'public-traversal'=86 'retain-other'=86
'id'=87 'keyed'=87
'doc'=88 'public-traversal'=88
'mode'=89 'id'=89
'emits'=90 'doc'=90
'receiver'=91 'mode'=91
'requires'=92 'emits'=92
'any'=93 'receiver'=93
'get'=94 'requires'=94
'set'=95 'any'=95
'watch'=96 'get'=96
'start'=97 'set'=97
'stop'=98 'watch'=98
'read'=99 'start'=99
'write'=100 'stop'=100
'resolve'=101 'read'=101
'connect'=102 'write'=102
'disconnect'=103 'resolve'=103
'call'=104 'connect'=104
'watch-start'=105 'disconnect'=105
'watch-stop'=106 'call'=106
'subscribe'=107 'watch-start'=107
'unsubscribe'=108 'watch-stop'=108
'register'=109 'subscribe'=109
'crdt'=110 'unsubscribe'=110
'optional-one'=111 'register'=111
'exactly-one'=112 'crdt'=112
'many-unique'=113 'optional-one'=113
'many'=114 'exactly-one'=114
'ordered'=115 'many-unique'=115
'unit'=116 'many'=116
'watch-handle'=117 'ordered'=117
'message'=118 'unit'=118
'atom-ref'=119 'watch-handle'=119
'interface-ref'=120 'message'=120
'optional'=121 'atom-ref'=121
'list'=122 'interface-ref'=122
'record'=123 'optional'=123
'bool'=124 'list'=124
'bytes'=125 'record'=125
'double'=126 'bool'=126
'int32'=127 'bytes'=127
'int64'=128 'double'=128
'string'=129 'int32'=129
'uint32'=130 'int64'=130
'uint64'=131 'string'=131
'true'=132 'uint32'=132
'false'=133 'uint64'=133
'null'=134 'true'=134
'->'=135 'false'=135
':'=136 'null'=136
';'=137 '->'=137
','=138 ':'=138
'.'=139 ';'=139
'{'=140 ','=140
'}'=141 '.'=141
'['=142 '{'=142
']'=143 '}'=143
'('=144 '['=144
')'=145 ']'=145
'<'=146 '('=146
'>'=147 ')'=147
'&'=148 '<'=148
'='=149 '>'=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()), valueType: lowerValueType(state, context.valueType()),
storagePolicy: policy.REGISTER() storagePolicy: policy.REGISTER()
? { kind: "register" } ? { kind: "register" }
: { : policy.JOURNAL()
kind: "crdt-document", ? { kind: "journal", limits: JSON.parse(text(policy.jsonObject())) }
updateType: lowerValueType(state, policy.valueType()), : policy.SUBSCRIPTION()
}, ? { kind: "subscription", payload: lowerValueType(state, policy.valueType()) }
: {
kind: "crdt-document",
updateType: lowerValueType(state, policy.valueType()),
},
defaultValue, 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 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 // Concrete attachments are registered before rules, so unused rules can
// also check public traversal without waiting for an application. // also check public traversal without waiting for an application.
if (provider.EDGE() && !privateStorage.has(name)) { if (provider.EDGE() && !privateStorage.has(name)) {
@@ -2435,6 +2439,74 @@ const lowerConformanceBody = (
return []; return [];
} }
const provider = bindingContext.operationProvider(); 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 (provider.LIFECYCLE()) {
if (identifier(provider.identifier(0)) !== "delete") { if (identifier(provider.identifier(0)) !== "delete") {
loweringIssue(state, provider, "invalid-lifecycle-binding", "Only lifecycle.delete is supported"); 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) => { const binding = (parent: GraphNode, value: Binding, atomId: string, context: unknown) => {
if (value.kind === "lifecycle") return; 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") { if (value.kind !== "package") {
dependency(parent, value, atomId); dependency(parent, value, atomId);
return; return;
+22 -3
View File
@@ -25,6 +25,7 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
requested: ExecutionContract | undefined, requested: ExecutionContract | undefined,
provided: ExecutionContract | undefined, provided: ExecutionContract | undefined,
path: string, path: string,
requireTargets = true,
) => { ) => {
if (!requested || requested.kind === "remote") return; if (!requested || requested.kind === "remote") return;
if (!provided || provided.kind === "remote") { if (!provided || provided.kind === "remote") {
@@ -33,8 +34,9 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
} }
if (requested.kind === "snapshot" && provided.kind !== "snapshot") if (requested.kind === "snapshot" && provided.kind !== "snapshot")
fail(path, "Snapshot execution cannot perform a transaction"); fail(path, "Snapshot execution cannot perform a transaction");
for (const target of requested.targets ?? []) if (requireTargets)
if (!provided.targets?.includes(target)) fail(path, `Dependency does not provide execution target ${target}`); 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 revision of workspace.interfaceImports)
for (const member of revision.template?.members ?? revision.members) { 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 (entry.execution && entry.execution.kind !== "remote") {
if (!selected) fail(portPath, "Portable interface ports must select their required operations"); if (!selected) fail(portPath, "Portable interface ports must select their required operations");
for (const id of selected ?? []) 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; if (!entry.execution || entry.execution.kind === "remote") continue;
@@ -150,6 +155,20 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
continue; continue;
} }
if (!operation.execution || operation.execution.kind === "remote") 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") { if (binding.kind === "package") {
const provider = packages.get(binding.packageRevisionId)?.exports.find((e) => e.id === binding.exportId); const provider = packages.get(binding.packageRevisionId)?.exports.find((e) => e.id === binding.exportId);
supports(operation.execution, provider?.execution, path); 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`); fail(ownerKey(owner), `Initialization input ${field.input} must name an attachment owned by this conformance`);
return; 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; if (binding.kind !== "edge") return;
const index = attachment.endpoints.findIndex((e) => e.projectionId === binding.projectionId); const index = attachment.endpoints.findIndex((e) => e.projectionId === binding.projectionId);
const endpoint = attachment.endpoints[index]; const endpoint = attachment.endpoints[index];
@@ -102,7 +108,12 @@ export function validateInitialization(workspace: WorkspaceRevision): Capability
...workspace.conformances.flatMap((o) => o.privateAttachments), ...workspace.conformances.flatMap((o) => o.privateAttachments),
].filter((a) => a.kind === "state" && a.attachedTo === binding.atomId); ].filter((a) => a.kind === "state" && a.attachedTo === binding.atomId);
for (const state of states) 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`); 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 }[]; 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 { export interface StateSlotDefinition {
kind: "state"; kind: "state";
@@ -417,6 +429,8 @@ export interface RelationshipMaterialization {
} }
export type Binding = export type Binding =
| { kind: "journal"; slotId: SlotId; primitive: JournalPrimitive }
| { kind: "subscription"; slotId: SlotId; primitive: SubscriptionPrimitive }
| { kind: "lifecycle"; primitive: "delete" } | { kind: "lifecycle"; primitive: "delete" }
| { | {
kind: "interface"; kind: "interface";
+145 -32
View File
@@ -1,4 +1,10 @@
import { isStorableType } from "./generics.js"; import { isStorableType } from "./generics.js";
import {
journalPayloadType,
journalPrimitiveSignature,
subscriptionContract,
subscriptionStatusType,
} from "./journals.js";
import { validateInitialization } from "./initialization.js"; import { validateInitialization } from "./initialization.js";
import { validatePresentations } from "./presentations.js"; import { validatePresentations } from "./presentations.js";
import { validateModules } from "./modules.js"; import { validateModules } from "./modules.js";
@@ -511,15 +517,32 @@ const atomSatisfiesConstraint = (
? constraint.atomId === atomId ? constraint.atomId === atomId
: conformances.has(conformanceKey(atomId, constraint.interfaceRevisionId)); : 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 = ( const constraintSatisfiesConstraint = (
actual: EdgeEndpointConstraint, actual: EdgeEndpointConstraint,
required: EdgeEndpointConstraint, required: EdgeEndpointConstraint,
conformances: ReadonlyMap<string, Conformance>, indexes: ValidationIndexes,
) => ) =>
constraintsEqual(actual, required) || constraintsEqual(actual, required) ||
(actual.kind === "atom" && (actual.kind === "atom" &&
required.kind === "interface" && 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 = ( const canAccessAttachment = (
owner: AttachmentOwner, owner: AttachmentOwner,
@@ -858,24 +881,12 @@ const validateInterfaces = (
issues: CapabilityValidationIssue[], issues: CapabilityValidationIssue[],
indexes: ValidationIndexes, 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 [packageIndex, pkg] of workspace.packageImports.entries()) {
for (const obligation of pkg.argumentRequirements ?? []) { for (const obligation of pkg.argumentRequirements ?? []) {
const satisfied = const satisfied =
obligation.target.kind === "atom" obligation.target.kind === "atom"
? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required)) ? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required))
: implies(obligation.target.interfaceRevisionId, obligation.required); : interfaceImplies(indexes, obligation.target.interfaceRevisionId, obligation.required);
if (!satisfied) if (!satisfied)
issue( issue(
issues, issues,
@@ -890,7 +901,7 @@ const validateInterfaces = (
for (const required of revision.requiredInterfaces ?? []) { for (const required of revision.requiredInterfaces ?? []) {
if (!indexes.interfaces.has(required)) if (!indexes.interfaces.has(required))
issue(issues, "unresolved-reference", path, `Unknown prerequisite interface ${required}`); issue(issues, "unresolved-reference", path, `Unknown prerequisite interface ${required}`);
else if (implies(required, revision.revisionId)) else if (interfaceImplies(indexes, required, revision.revisionId))
issue( issue(
issues, issues,
"cyclic-conformance-requirement", "cyclic-conformance-requirement",
@@ -902,7 +913,7 @@ const validateInterfaces = (
const satisfied = const satisfied =
obligation.target.kind === "atom" obligation.target.kind === "atom"
? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required)) ? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required))
: implies(obligation.target.interfaceRevisionId, obligation.required); : interfaceImplies(indexes, obligation.target.interfaceRevisionId, obligation.required);
if (!satisfied) if (!satisfied)
issue(issues, "unsatisfied-interface", path, `Generic argument does not implement ${obligation.required}`); 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 === "variant" && Object.values(type.cases).some((entry) => containsRpcType(entry.value))) ||
((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value)); ((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value));
if ( if (
containsRpcType(attachment.valueType) || (!["journal", "subscription"].includes(attachment.storagePolicy.kind) &&
containsRpcType(attachment.valueType)) ||
(attachment.storagePolicy.kind === "crdt-document" && containsRpcType(attachment.storagePolicy.updateType)) (attachment.storagePolicy.kind === "crdt-document" && containsRpcType(attachment.storagePolicy.updateType))
) { ) {
issue( issue(
@@ -959,6 +971,43 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
if (attachment.storagePolicy.kind === "crdt-document") { if (attachment.storagePolicy.kind === "crdt-document") {
validateValueType(issues, attachment.storagePolicy.updateType, `${path}.storagePolicy.updateType`, indexes); 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 (owner.kind === "conformance") {
if (attachment.attachedTo !== owner.atomId) { if (attachment.attachedTo !== owner.atomId) {
issue( issue(
@@ -1413,6 +1462,13 @@ const validateBoundDependencies = (
break; break;
} }
validateAttachmentAccess(issues, attachment, params.conformance, `${path}.binding.slotId`); 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 const traversal = stateBinding.via
? validateTraversal( ? validateTraversal(
issues, issues,
@@ -1519,11 +1575,7 @@ const validateBoundDependencies = (
); );
} }
if ( if (
!constraintSatisfiesConstraint( !constraintSatisfiesConstraint(projection.target.constraint, requirement.target, params.indexes) ||
projection.target.constraint,
requirement.target,
params.indexes.conformances,
) ||
requirement.cardinality !== projection.endpoint.cardinality || requirement.cardinality !== projection.endpoint.cardinality ||
requirement.keyType !== projection.endpoint.keyType requirement.keyType !== projection.endpoint.keyType
) { ) {
@@ -1822,7 +1874,9 @@ const validateConformances = (
) { ) {
const native = const native =
member.kind === "value" 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"; : binding.kind === "edge" && binding.primitive === "resolve";
if (!native && (member.queryRead.execution === "native" || binding.kind !== "package")) if (!native && (member.queryRead.execution === "native" || binding.kind !== "package"))
issue( issue(
@@ -1878,6 +1932,67 @@ const validateConformances = (
); );
continue; 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") { if (binding.kind === "state") {
const attachment = findAttachment(indexes, "state", binding.slotId); const attachment = findAttachment(indexes, "state", binding.slotId);
if (!attachment || attachment.attachment.kind !== "state") { if (!attachment || attachment.attachment.kind !== "state") {
@@ -1890,6 +2005,8 @@ const validateConformances = (
continue; continue;
} }
validateAttachmentAccess(issues, attachment, conformance, `${bindingPath}.binding.slotId`); 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) { if (attachment.attachment.attachedTo !== conformance.atomId) {
issue( issue(
issues, issues,
@@ -1990,11 +2107,7 @@ const validateConformances = (
"Edge primitives may only implement relationship members", "Edge primitives may only implement relationship members",
); );
} else if ( } else if (
!constraintSatisfiesConstraint( !constraintSatisfiesConstraint(projection.target.constraint, operationEntry.member.target, indexes) ||
projection.target.constraint,
operationEntry.member.target,
indexes.conformances,
) ||
operationEntry.member.cardinality !== projection.endpoint.cardinality || operationEntry.member.cardinality !== projection.endpoint.cardinality ||
(operationEntry.member.keyType !== undefined && operationEntry.member.keyType !== projection.endpoint.keyType) (operationEntry.member.keyType !== undefined && operationEntry.member.keyType !== projection.endpoint.keyType)
) { ) {
@@ -2002,7 +2115,7 @@ const validateConformances = (
issues, issues,
"invalid-edge-binding", "invalid-edge-binding",
`${bindingPath}.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)) { if (!signaturesMatch(operation, expected)) {
@@ -2531,7 +2644,7 @@ export const resolveOperationPlan = (
attachment: owned as OwnedAttachment & { attachment: EdgeDefinition }, 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 packageRevision = plan.packages.get(binding.packageRevisionId);
const packageExport = packageRevision?.exports.find( const packageExport = packageRevision?.exports.find(
(entry): entry is PackageOperationExport | PackageFunctionExport => (entry): entry is PackageOperationExport | PackageFunctionExport =>
@@ -2679,7 +2792,7 @@ export const computeCapabilityClosure = (
queued.push({ atomId: root.atomId, interfaceRevisionId: member.subject.interfaceRevisionId }); queued.push({ atomId: root.atomId, interfaceRevisionId: member.subject.interfaceRevisionId });
} }
for (const binding of conformance.operationBindings.values()) { 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); attachments.add(binding.slotId);
} else if (binding.kind === "edge") { } else if (binding.kind === "edge") {
attachments.add(binding.edgeTypeId); attachments.add(binding.edgeTypeId);
+105 -2
View File
@@ -9,6 +9,7 @@ import type {
} from "../capability-model/types.js"; } from "../capability-model/types.js";
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js"; import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
import { portableValueSchema } from "../bindings/portable.js"; import { portableValueSchema } from "../bindings/portable.js";
import { subscriptionContract } from "../capability-model/journals.js";
type PreparedStep = string | null | { case: string; id: string }; type PreparedStep = string | null | { case: string; id: string };
function preparedInputPaths(type: ValueType, path: PreparedStep[] = []): PreparedStep[][] { function preparedInputPaths(type: ValueType, path: PreparedStep[] = []): PreparedStep[][] {
if (type.kind === "change") return [path]; if (type.kind === "change") return [path];
@@ -73,10 +74,105 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
if (!shape) if (!shape)
throw new ExecutionPlanError("UNSUPPORTED_STORAGE_CODEC", storage.id, "Unsupported canonical value type"); throw new ExecutionPlanError("UNSUPPORTED_STORAGE_CODEC", storage.id, "Unsupported canonical value type");
ownerBySlot.set(storage.id, owner.id); 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({ slots.push({
id: storage.id, id: storage.id,
schema: shape, 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 } : {}), ...(Object.hasOwn(storage, "defaultValue") ? { defaultLiteral: storage.defaultValue } : {}),
}); });
} else lowerEdge(storage, owner.id); } else lowerEdge(storage, owner.id);
@@ -109,6 +205,13 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
let target: unknown; let target: unknown;
let unsupported: string | null = null; let unsupported: string | null = null;
if (binding.kind === "lifecycle") target = { kind: "lifecycle", primitive: binding.primitive }; 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") else if (binding.kind === "state")
target = { kind: "state", owner: slotOwner(binding.slotId), slot: binding.slotId, primitive: binding.primitive }; target = { kind: "state", owner: slotOwner(binding.slotId), slot: binding.slotId, primitive: binding.primitive };
else if (binding.kind === "edge") else if (binding.kind === "edge")
@@ -231,7 +334,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
operation: operation.operationId, operation: operation.operationId,
scope: operation.scope, scope: operation.scope,
effect: operation.execution.kind, effect: operation.execution.kind,
targets: operation.execution.kind === "remote" ? [] : operation.execution.targets, targets: operation.availableTargets,
input, input,
...(inputChanges.length ? { inputChanges } : {}), ...(inputChanges.length ? { inputChanges } : {}),
output, output,
+45 -1
View File
@@ -45,6 +45,8 @@ export interface PlannedOperation {
operationId: string; operationId: string;
scope: "object" | "class"; scope: "object" | "class";
execution: ExecutionContract; execution: ExecutionContract;
/** Installed implementation closure; the public execution contract above remains required. */
availableTargets: string[];
input: ValueType; input: ValueType;
output: ValueType; output: ValueType;
/** Concrete target after public forwarding. Never chosen by a runtime caller. */ /** Concrete target after public forwarding. Never chosen by a runtime caller. */
@@ -240,6 +242,7 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
operationId: operation, operationId: operation,
scope: publicContract.scope === "class" ? "class" : "object", scope: publicContract.scope === "class" ? "class" : "object",
execution: publicContract.execution ?? { kind: "remote" }, execution: publicContract.execution ?? { kind: "remote" },
availableTargets: [],
input: publicContract.inputType, input: publicContract.inputType,
output: publicContract.outputType, output: publicContract.outputType,
binding, binding,
@@ -251,7 +254,8 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
checkValue(planned.input, `${key}/input`, true); checkValue(planned.input, `${key}/input`, true);
checkValue(planned.output, `${key}/output`, 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") else if (binding.kind === "edge")
planned.dependencies.push({ kind: "edge", edge: binding.edgeTypeId, projection: binding.projectionId }); planned.dependencies.push({ kind: "edge", edge: binding.edgeTypeId, projection: binding.projectionId });
else if (binding.kind === "package") { else if (binding.kind === "package") {
@@ -338,6 +342,46 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
}; };
walk(root.key, [root.key]); 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) => ({ const plannedQueries: PlannedQuery[] = queries.map((query) => ({
id: queryKey(query.packageRevisionId, query.checked.declaration.id), id: queryKey(query.packageRevisionId, query.checked.declaration.id),
query, query,
+5
View File
@@ -98,6 +98,11 @@ export function linkQueries(workspace: WorkspaceRevision): LinkedQuery[] {
`Package query field ${effect.memberId} needs query-reason`, `Package query field ${effect.memberId} needs query-reason`,
); );
} else if (binding.kind === "state" && binding.primitive === "read") needed.add(binding.slotId); } 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 if (binding.kind === "edge" && binding.primitive === "resolve") needed.add(binding.edgeTypeId);
else else
throw new QueryCompileError( throw new QueryCompileError(
+4 -1
View File
@@ -116,7 +116,10 @@ export const queryRuntimePlan = (linked: LinkedQuery, workspace: WorkspaceRevisi
"many-unique": Cardinality.MANY_UNIQUE, "many-unique": Cardinality.MANY_UNIQUE,
}[member.cardinality] }[member.cardinality]
: undefined, : 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 : "", edgeTypeId: field.binding.kind === "edge" ? field.binding.edgeTypeId : "",
projectionId: field.binding.kind === "edge" ? field.binding.projectionId : "", projectionId: field.binding.kind === "edge" ? field.binding.projectionId : "",
rpc: field.binding.kind === "package", rpc: field.binding.kind === "package",
+8 -2
View File
@@ -51,10 +51,9 @@ test("portable contracts preserve exact selected operations and targets", () =>
}); });
}); });
test("portable dependency diagnostics reject remote defaults, missing targets, and broad acquisition", () => { test("portable dependency diagnostics reject remote defaults and broad acquisition", () => {
for (const [selection, execution, message] of [ for (const [selection, execution, message] of [
["using [refresh.call]", "snapshot targets [browser]", "remote-only"], ["using [refresh.call]", "snapshot targets [browser]", "remote-only"],
["using [label.get]", "snapshot targets [arduino]", "arduino"],
["", "snapshot targets [browser]", "select"], ["", "snapshot targets [browser]", "select"],
["using [label.get, label.get]", "snapshot targets [browser]", "duplicates"], ["using [label.get, label.get]", "snapshot targets [browser]", "duplicates"],
["using [label.set]", "snapshot targets [browser]", "Unknown"], ["using [label.set]", "snapshot targets [browser]", "Unknown"],
@@ -65,6 +64,13 @@ test("portable dependency diagnostics reject remote defaults, missing targets, a
} }
}); });
test("package code targets may exceed a dependency's public minimum", () => {
const result = program("using [label.get]", "snapshot targets [arduino]");
assert.ok(result.ok, JSON.stringify(result.diagnostics));
// A workspace still has to provide the full installed closure before an
// operation can promise this target; package code alone is insufficient.
});
test("portable query getters and target declarations are checked", () => { test("portable query getters and target declarations are checked", () => {
assert.equal(data("remote").ok, false); assert.equal(data("remote").ok, false);
assert.equal(data("transaction targets [browser]").ok, false); assert.equal(data("transaction targets [browser]").ok, false);
+23
View File
@@ -16,11 +16,34 @@ import {
} from "../src/execution-world/index.js"; } from "../src/execution-world/index.js";
import { buildArtifactFixture as buildFixture } from "./artifact-fixture.js"; import { buildArtifactFixture as buildFixture } from "./artifact-fixture.js";
import { compileKernelProgram } from "../src/execution-world/kernel.js";
const resign = (implementation: ArtifactImplementation) => { const resign = (implementation: ArtifactImplementation) => {
const { contentDigest, ...body } = implementation; const { contentDigest, ...body } = implementation;
implementation.contentDigest = implementationDigest(body); implementation.contentDigest = implementationDigest(body);
}; };
test("installed targets can exceed the public requirement without changing that contract", async () => {
const workspace = await loadReaderWorkspace((name, text) =>
name === "DocumentFactory.qx" ? text.replaceAll("targets [browser, server]", "targets [server]") : text,
);
const plan = compileExecutionPlan(workspace);
const factory = plan.definition.operations.find(
(op) => op.binding.kind === "package" && op.binding.exportId === "reader:create-document",
)!;
assert.deepEqual(factory.execution, { kind: "transaction", targets: ["server"] });
assert.deepEqual(factory.availableTargets, ["browser", "server"]);
const kernel = compileKernelProgram(plan) as { operations: { key: string; targets: string[] }[] };
assert.deepEqual(kernel.operations.find((op) => op.key === factory.key)!.targets, ["browser", "server"]);
const provider = workspace.packageImports
.flatMap((pkg) => pkg.exports)
.find((entry) => entry.id === "reader:create-document")!;
provider.execution = { kind: "transaction", targets: ["server"] };
const authoritative = compileExecutionPlan(workspace).definition.operations.find((op) => op.key === factory.key)!;
assert.deepEqual(authoritative.availableTargets, ["server"]);
assert.deepEqual(authoritative.execution, factory.execution);
});
test("reader execution plan closes selector storage and traversed interface dependencies", async () => { test("reader execution plan closes selector storage and traversed interface dependencies", async () => {
const workspace = await loadReaderWorkspace(); const workspace = await loadReaderWorkspace();
const plan = compileExecutionPlan(workspace); const plan = compileExecutionPlan(workspace);
+5
View File
@@ -0,0 +1,5 @@
interface EventHandler<value Payload,value Parameters> id "interface:org.quixos.events:handler" revision "interface:org.quixos.events:handler@1" {
operation handle id "events:handle" : record {event:Payload;parameters:Parameters;} -> unit {
call id "events:handle:call" execution transaction targets [server];
}
}
+3
View File
@@ -0,0 +1,3 @@
import interface EventStream;
// Opt into the standard workspace journal conformance rule.
interface EventStreamRecord<value Payload> id "interface:org.quixos.events:stream-record" revision "interface:org.quixos.events:stream-record@1" requires EventStream<Payload> {}
+13
View File
@@ -0,0 +1,13 @@
// Required server support; a checked installed implementation may additionally predict locally.
interface EventStream<value Payload> id "interface:org.quixos.events:stream" revision "interface:org.quixos.events:stream@1" {
operation publish id "events:publish" : Payload -> record {publication:string;} {
call id "events:publish:call" execution transaction targets [server];
}
queryable value head id "events:head" : optional<uint64> {
get id "events:head:get" execution snapshot targets [server];
}
operation readPage id "events:read-page" : record {after:optional<string>;limit:uint32;}
-> record {entries:list<record {publication:string;position:uint64;payload:Payload;}>;next:optional<string>;asOfHead:optional<uint64>;} {
call id "events:read-page:call" execution snapshot targets [server];
}
}
+2
View File
@@ -0,0 +1,2 @@
import interface EventSubscription;
interface EventSubscriptionRecord<value Payload,value Parameters> id "interface:org.quixos.events:subscription-record" revision "interface:org.quixos.events:subscription-record@1" requires EventSubscription<Payload,Parameters> {}
+18
View File
@@ -0,0 +1,18 @@
import interface EventStream;
import interface EventHandler;
type StartPosition = variant id "org.quixos.events:start@1" {
future id "events:start:future" : unit;
retained id "events:start:retained" : unit;
afterCursor id "events:start:after" : string;
};
interface EventSubscription<value Payload,value Parameters> id "interface:org.quixos.events:subscription" revision "interface:org.quixos.events:subscription@1" {
operation attach id "events:attach" : record {source:interface-ref<EventStream<Payload>>;handler:interface-ref<EventHandler<Payload,Parameters>>;parameters:Parameters;start:StartPosition;} -> unit {
call id "events:attach:call" execution transaction targets [server];
}
queryable value status id "events:status" : record {status:string;generation:uint64;cursor:uint64;failure:optional<record {publication:string;code:string;message:string;}>;} {
get id "events:status:get" execution snapshot targets [server];
}
operation pause id "events:pause" : unit -> unit {call id "events:pause:call" execution transaction targets [server];}
operation resume id "events:resume" : unit -> unit {call id "events:resume:call" execution transaction targets [server];}
operation retire id "events:retire" : unit -> unit {call id "events:retire:call" execution transaction targets [server];}
}
+21
View File
@@ -0,0 +1,21 @@
fragment {
import interface EventStream;
import interface EventHandler;
import interface EventSubscription;
import interface EventStreamRecord;
import interface EventSubscriptionRecord;
conform<value Payload> EventStreamRecord<Payload> as EventStream<Payload> id "events:standard-stream" {
private state Entries id "events:entries" on Self : Payload policy journal {"maxEntries":128,"maxBytes":1048576,"maxPublicationBytes":262144,"maxProtectedSubscriptions":32};
bind publish.call to journal Entries.publish;
bind head.get to journal Entries.head;
bind readPage.call to journal Entries.readPage;
}
conform<value Payload,value Parameters> EventSubscriptionRecord<Payload,Parameters> as EventSubscription<Payload,Parameters> id "events:standard-subscription" {
private state Delivery id "events:delivery" on Self : Parameters policy subscription(Payload);
bind attach.call to subscription Delivery.attach;
bind status.get to subscription Delivery.status;
bind pause.call to subscription Delivery.pause;
bind resume.call to subscription Delivery.resume;
bind retire.call to subscription Delivery.retire;
}
}
+29
View File
@@ -603,3 +603,32 @@ test("prerequisites require explicit conformances and remain in the capability c
applied.application!.arguments = [{ kind: "value", type: valueType.int64 }]; applied.application!.arguments = [{ kind: "value", type: valueType.int64 }];
assert.equal(compileWorkspaceRevision(forged).ok, false); assert.equal(compileWorkspaceRevision(forged).ok, false);
}); });
test("edge read targets honor declared interface prerequisites without inferring unrelated conformances", () => {
const interfaces = new Map();
for (const text of [
'interface Base id "base" revision "base@1" {}',
'import interface Base; interface Detailed id "detailed" revision "detailed@1" requires Base {}',
'import interface Base; interface Holder id "holder" revision "holder@1" {queryable relation item id "item" : optional-one interface Base {resolve id "item:resolve" execution snapshot targets [browser,server];}}',
]) {
const parsed = compileCapabilityResourceSource(text, { source, environment: { interfaces } });
assert.ok(parsed.ok, JSON.stringify(parsed.diagnostics));
if (parsed.resource.kind !== "interface") throw Error("interface expected");
interfaces.set(parsed.resource.revision.displayName, parsed.resource.revision);
}
const assembly = `workspace W id "w" revision "w@1" commit "${source.commit}" {
import interface Base;import interface Detailed;import interface Holder;
atom Item id "item";atom Box id "box";
conform Item as Base id "item-base" {} conform Item as Detailed id "item-detailed" {}
conform Box as Holder id "box-holder" {
private edge ItemEdge id "items" {atom Self projection item id "item" optional-one;interface Detailed projection holders id "holders" many;}
bind item.resolve to edge ItemEdge.item.resolve;
}
}`;
const valid = compileCapabilitySource(assembly, "workspace.qx", { interfaces });
assert.ok(valid.ok, JSON.stringify(valid.diagnostics));
const unrelated = new Map(interfaces);
unrelated.set("Detailed", { ...interfaces.get("Detailed"), requiredInterfaces: [] });
const invalid = compileCapabilitySource(assembly, "workspace.qx", { interfaces: unrelated });
assert.equal(invalid.ok, false, "installed co-occurrence alone is not proof of a declared interface implication");
});
+186
View File
@@ -0,0 +1,186 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
import { formatQx } from "../src/capability-language/source.js";
import { compileExecutionPlan, compileKernelProgram } from "../src/execution-world/index.js";
import type { InterfaceRevision } from "../src/capability-model/types.js";
import { readFileSync } from "node:fs";
const source = { repository: "https://example.test/journals", commit: "a".repeat(40) };
test("standard event contracts instantiate generic native conformance rules", () => {
const interfaces = new Map<string, InterfaceRevision>();
for (const [name, slug] of [
["EventStream", "event-stream"],
["EventHandler", "event-handler"],
["EventSubscription", "event-subscription"],
["EventStreamRecord", "event-stream-record"],
["EventSubscriptionRecord", "event-subscription-record"],
]) {
const text = readFileSync(new URL(`../../test/fixtures/events/${slug}.qx`, import.meta.url), "utf8");
const result = compileCapabilityResourceSource(text, {
source,
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
});
assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics));
interfaces.set(name!, result.resource.revision);
}
const rules = readFileSync(new URL("../../test/fixtures/events/rules.qx", import.meta.url), "utf8")
.replace(/^fragment\s*\{/, "")
.replace(/}\s*$/, "");
const result = compileCapabilitySource(
`workspace Events id "events" revision "events@1" commit "${source.commit}" {
${rules}
atom TextStream id "text-stream";
conform TextStream as EventStreamRecord<string> id "text-marker" {}
atom TextSubscription id "text-subscription";
conform TextSubscription as EventSubscriptionRecord<string,unit> id "text-subscription-marker" {}
atom NumberStream id "number-stream";
conform NumberStream as EventStreamRecord<uint32> id "number-marker" {}
}`,
"events.qx",
{ interfaces, interfaceClosure: [...interfaces.values()] },
);
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const kernel = compileKernelProgram(compileExecutionPlan(result.workspace)) as {
model: { owners: { slots: { policy: string }[] }[] };
};
assert.equal(
kernel.model.owners.flatMap((owner) => owner.slots).filter((slot) => slot.policy === "journal").length,
2,
);
assert.equal(
kernel.model.owners.flatMap((owner) => owner.slots).filter((slot) => slot.policy === "subscription").length,
1,
);
});
const contract = `external atom Document id "document";
type Event = record { document: atom-ref<Document>; title: string; };
interface Stream id "stream" revision "stream@1" {
operation publish id "publish" : Event -> record { publication: string; } {
call id "publish:call" execution transaction targets [server];
}
queryable value head id "head" : optional<uint64> { get id "head:get" execution snapshot targets [server]; }
operation readPage id "page" : record { after: optional<string>; limit: uint32; }
-> record { entries: list<record { publication: string; position: uint64; payload: Event; }>; next: optional<string>; asOfHead: optional<uint64>; } {
call id "page:call" execution snapshot targets [server];
}
}`;
const assembly = `workspace Journals id "journals" revision "journals@1" commit "${source.commit}" {
import interface Stream;
atom Document id "document";
atom Journal id "journal-object";
conform Journal as Stream id "journal-owner" {
private state Entries id "entries" on Journal : record { document: atom-ref<Document>; title: string; }
policy journal { "maxEntries": 32, "maxBytes": 65536, "maxPublicationBytes": 8192, "maxProtectedSubscriptions": 8 };
bind publish.call to journal Entries.publish;
bind head.get to journal Entries.head;
bind readPage.call to journal Entries.readPage;
}
}`;
function compile(text = assembly, iface = contract) {
const resource = compileCapabilityResourceSource(iface, { source });
assert.ok(resource.ok && resource.resource.kind === "interface", JSON.stringify(resource.diagnostics));
return compileCapabilitySource(formatQx(text), "journal.qx", {
interfaces: new Map([["Stream", resource.resource.revision]]),
});
}
test("journal bindings typecheck and lower managed reference pins", () => {
const result = compile();
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const kernel = compileKernelProgram(compileExecutionPlan(result.workspace)) as {
model: { edges: unknown[]; owners: { slots: { policy: string; journal: { pins: unknown[] } }[] }[] };
operations: { binding: { kind: string; primitive: string } }[];
};
assert.equal(kernel.model.owners[0]!.slots[0]!.policy, "journal");
assert.equal(kernel.model.owners[0]!.slots[0]!.journal.pins.length, 1);
assert.equal(kernel.model.edges.length, 1);
assert.deepEqual(kernel.operations.map((op) => op.binding.primitive).sort(), ["head", "publish", "readPage"]);
assert.ok(kernel.operations.every((op) => op.binding.kind === "journal"));
});
test("journal declarations reject state access, bad bounds, seeding and signature substitution", () => {
for (const [ws, iface] of [
[assembly.replace("journal Entries.head", "state Entries.read"), contract],
[assembly.replace('"maxEntries": 32', '"maxEntries": 0'), contract],
[assembly.replace("bind publish.call", "initialize { seed to state Entries; }; bind publish.call"), contract],
[assembly, contract.replace("publication: string", "publication: uint32")],
[assembly, contract.replace("execution transaction", "execution snapshot")],
]) {
const result = compile(ws, iface);
assert.equal(result.ok, false, `${ws}\n${iface}`);
}
});
test("subscription storage infers its typed handler and retains configuration references", () => {
const interfaces = new Map<string, InterfaceRevision>();
const resource = (name: string, text: string) => {
const result = compileCapabilityResourceSource(text, {
source,
environment: { interfaces, interfaceClosure: [...interfaces.values()] },
});
assert.ok(result.ok && result.resource.kind === "interface", JSON.stringify(result.diagnostics));
interfaces.set(name, result.resource.revision);
};
resource("Stream", contract);
resource(
"Receiver",
`external atom Document id "document";
interface Receiver id "receiver" revision "receiver@1" {
operation receive id "receive" : record { event: record { document: atom-ref<Document>; title: string; }; parameters: string; } -> unit {
call id "receive:call" execution transaction targets [server];
}
}`,
);
resource(
"Subscription",
`external atom Document id "document";
import interface Stream;
import interface Receiver;
type Start = variant id "start" { future id "future" : unit; retained id "retained" : unit; afterCursor id "after" : string; };
interface Subscription id "subscription" revision "subscription@1" {
operation attach id "attach" : record { source: interface-ref<Stream>; handler: interface-ref<Receiver>; parameters: string; start: Start; } -> unit {
call id "attach:call" execution transaction targets [server];
}
queryable value status id "status" : record { status: string; generation: uint64; cursor: uint64; failure: optional<record { publication: string; code: string; message: string; }>; } {
get id "status:get" execution snapshot targets [server];
}
operation retire id "retire" : unit -> unit { call id "retire:call" execution transaction targets [server]; }
}`,
);
const ws = assembly
.replace(
"import interface Stream;",
"import interface Stream; import interface Receiver; import interface Subscription;",
)
.replace(
/\n}$/,
`
atom Delivery id "delivery";
conform Delivery as Subscription id "delivery-owner" {
private state DeliveryState id "delivery-state" on Delivery : string policy subscription(record { document: atom-ref<Document>; title: string; });
bind attach.call to subscription DeliveryState.attach;
bind status.get to subscription DeliveryState.status;
bind retire.call to subscription DeliveryState.retire;
}
}`,
);
const result = compileCapabilitySource(formatQx(ws), "subscriptions.qx", {
interfaces,
interfaceClosure: [...interfaces.values()],
});
assert.ok(result.ok, JSON.stringify(result.diagnostics));
const kernel = compileKernelProgram(compileExecutionPlan(result.workspace)) as {
model: {
owners: { slots: { policy: string; subscription?: { operation: string; pins: unknown[]; active: unknown } }[] }[];
};
};
const slot = kernel.model.owners.flatMap((owner) => owner.slots).find((slot) => slot.policy === "subscription")!;
assert.equal(slot.subscription!.operation, "receive:call");
assert.equal(slot.subscription!.pins.length, 2);
assert.ok(slot.subscription!.active);
const bad = compileCapabilitySource(ws.replace("on Delivery : string", "on Delivery : uint32"), "bad.qx", {
interfaces,
interfaceClosure: [...interfaces.values()],
});
assert.equal(bad.ok, false, "remembered parameter type must agree with attachment and handler");
});
+8 -2
View File
@@ -82,9 +82,15 @@ test("closed generic specialization preserves reference-selected interface ports
); );
}); });
test("portable input-selected reads require the dependency's declared target support", () => { test("portable input-selected reads use installed support beyond the public minimum", () => {
const portable = pkg.replaceAll("targets [server]", "targets [browser, server]"); const portable = pkg.replaceAll("targets [server]", "targets [browser, server]");
assert.equal(compile(portable).ok, false); const optional = compile(portable);
assert.ok(optional.ok, JSON.stringify(optional.diagnostics));
const plan = compileExecutionPlan(optional.workspace);
assert.deepEqual(plan.definition.operations.find((op) => op.operationId === "peek:call")!.availableTargets, [
"browser",
"server",
]);
const result = compile(portable, workspace, iface.replaceAll("targets [server]", "targets [browser, server]")); const result = compile(portable, workspace, iface.replaceAll("targets [server]", "targets [browser, server]"));
assert.ok(result.ok, JSON.stringify(result.diagnostics)); assert.ok(result.ok, JSON.stringify(result.diagnostics));
}); });