Implement main-thread Camino sessions and synchronous CRDT editing

This commit is contained in:
Timothy J. Aveni
2026-09-27 23:57:29 -07:00
parent ad72185a6c
commit 696023b4ba
24 changed files with 3784 additions and 3218 deletions
+13 -2
View File
@@ -189,18 +189,22 @@ fragmentVariable : identifier TO identifier SEMI;
packageOperationExport
: OPERATION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
MODE operationMode executionClause? eventClause? RECEIVER receiverRequirement dependencyBlock? acceptanceClause? SEMI
MODE operationMode executionClause? eventClause? RECEIVER receiverRequirement dependencyBlock? preparationClause? acceptanceClause? SEMI
;
packageFunctionExport
: FUNCTION identifier typeParameters? ID stringLiteral COLON valueType ARROW valueType
executionClause? dependencyBlock? acceptanceClause? SEMI
executionClause? dependencyBlock? preparationClause? acceptanceClause? SEMI
;
acceptanceClause
: ACCEPT USING identifier
;
preparationClause
: PREPARES CREATION identifier USING stringLiteral
;
executionClause
: EXECUTION REMOTE
| EXECUTION (SNAPSHOT | TRANSACTION) TARGETS LBRACK identifierList RBRACK
@@ -418,6 +422,7 @@ valueType
| OPTIONAL LT valueType GT
| LIST LT valueType GT
| RECORD LBRACE recordField* RBRACE
| VARIANT ID stringLiteral LBRACE variantCase+ RBRACE
| identifier typeArguments?
;
@@ -425,6 +430,10 @@ recordField
: identifier COLON valueType SEMI
;
variantCase
: identifier ID stringLiteral COLON valueType SEMI
;
scalarType
: BOOL
| BYTES
@@ -517,6 +526,8 @@ PRESENTATION: 'presentation';
SUBJECT: 'subject';
FOR: 'for';
CREATION: 'creation';
VARIANT: 'variant';
PREPARES: 'prepares';
INITIALIZE: 'initialize';
WORKSPACE: 'workspace';
EXECUTION: 'execution';
+2
View File
@@ -11,6 +11,8 @@ export const generateClientContracts = (
throw new Error("Host contracts require closed interface applications, not generic definitions");
const type = (value: ValueType): string => {
switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin":
return value.name === "unit" ? "undefined" : "string";
case "scalar":
+2
View File
@@ -49,6 +49,8 @@ export const genericImplementationType = (
const value = (type: ValueTypeExpression, scope: Scope, depth = 0): string => {
if (depth > 128) throw new Error("Generic type expansion exceeds depth limit");
switch (type.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "parameter": {
const bound = scope.arguments.get(type.parameterId);
if (bound) return bound;
+3
View File
@@ -112,6 +112,8 @@ export const generateTypeScriptBindings = (
const messages = new Map<string, string>();
const type = (value: ValueType): string => {
switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin":
return value.name === "unit" ? "null" : "QxWatchHandle";
case "scalar":
@@ -340,6 +342,7 @@ export const generateTypeScriptBindings = (
// A used port still fails above if its own required codec is missing.
const hasCodec = (value: ValueType): boolean => {
if (value.kind === "message") return !!options.messages?.[value.descriptorId];
if (value.kind === "variant") return false;
if (value.kind === "record") return Object.values(value.fields).every(hasCodec);
if (value.kind === "list" || value.kind === "optional") return hasCodec(value.value);
return true;
+15
View File
@@ -29,6 +29,17 @@ const expectation = (e: ObjectExpectation) => ({
/** The protocol IR and replica wire schema intentionally share structure, not reference identity. */
export function portableValueSchema(type: ValueType): unknown {
switch (type.kind) {
case "variant":
return {
kind: "variant",
id: type.id,
cases: Object.fromEntries(
Object.entries(type.cases).map(([label, entry]) => [
label,
{ id: entry.id, value: portableValueSchema(entry.value) },
]),
),
};
case "change":
return {
kind: "record",
@@ -65,6 +76,10 @@ export function portableValueSchema(type: ValueType): unknown {
export function tsType(type: ValueType): string {
portableValueSchema(type);
switch (type.kind) {
case "variant":
return `(${Object.entries(type.cases)
.map(([label, entry]) => `{ readonly kind: ${q(label)}; readonly value: ${tsType(entry.value)} }`)
.join(" | ")})`;
case "change":
return `PreparedChange<${tsType(type.value)}>`;
case "scalar":
+3
View File
@@ -15,6 +15,8 @@ export function generateReactBindings(
const declarations: string[] = [];
const type = (value: ValueType): string => {
switch (value.kind) {
case "variant":
throw new Error("Variants require the Camino portable value runtime; legacy RPC bindings are unsupported");
case "builtin":
if (value.name !== "unit") throw new Error(`Unsupported React props builtin ${value.name}`);
return "null";
@@ -85,6 +87,7 @@ export function generateReactBindings(
const browserValue = (value: ValueType, seen: Set<string>): boolean => {
if (value.kind === "message") return false;
if (value.kind === "builtin") return value.name === "unit";
if (value.kind === "variant") return false;
if (value.kind === "record") return Object.values(value.fields).every((field) => browserValue(field, seen));
if (value.kind === "list" || value.kind === "optional") return browserValue(value.value, seen);
if (value.kind === "object-ref" && value.expectation.kind === "interface") {
File diff suppressed because one or more lines are too long
@@ -5,152 +5,154 @@ PRESENTATION=4
SUBJECT=5
FOR=6
CREATION=7
INITIALIZE=8
WORKSPACE=9
EXECUTION=10
SNAPSHOT=11
TRANSACTION=12
REMOTE=13
TARGETS=14
PORTABLE=15
REPLICA=16
LOCAL=17
FROM=18
RESULT_BYTES=19
RPC_CALLS=20
DEADLINE_MS=21
QUERY=22
REFLECTION=23
SPECIALIZE=24
ROOT=25
DOCUMENT=26
FRAGMENTS=27
VIEW=28
MAX=29
ALLOW=30
POLL=31
QUERYABLE=32
RPC=33
QUERY_REASON=34
TYPE=35
OBJECT=36
STORABLE=37
IMPLEMENTS=38
REF=39
FRAGMENT=40
IMPORT=41
EXTERNAL=42
ATOM=43
INTERFACE=44
INTERFACES=45
PACKAGE=46
VALUE=47
RELATION=48
OPERATION=49
FUNCTION=50
CONSTRUCTOR=51
CONSTRUCTS=52
INPUT=53
CONFORM=54
AS=55
BIND=56
STATIC=57
TO=58
PRIVATE=59
SHARED=60
STATE=61
EDGE=62
PROJECTION=63
WITH=64
USING=65
ACCEPT=66
VIA=67
MATERIALIZE=68
IF=69
ABSENT=70
ON=71
POLICY=72
DEFAULT=73
SOURCE=74
REPOSITORY=75
COMMIT=76
REVISION=77
SEMANTIC_MAJOR=78
ON_DELETE=79
LIFECYCLE=80
RETAIN_OTHER=81
KEYED=82
PUBLIC_TRAVERSAL=83
ID=84
DOC=85
MODE=86
EMITS=87
RECEIVER=88
REQUIRES=89
ANY=90
GET=91
SET=92
WATCH=93
START=94
STOP=95
READ=96
WRITE=97
RESOLVE=98
CONNECT=99
DISCONNECT=100
CALL=101
WATCH_START=102
WATCH_STOP=103
SUBSCRIBE=104
UNSUBSCRIBE=105
REGISTER=106
CRDT=107
OPTIONAL_ONE=108
EXACTLY_ONE=109
MANY_UNIQUE=110
MANY=111
ORDERED=112
UNIT=113
WATCH_HANDLE=114
MESSAGE=115
ATOM_REF=116
INTERFACE_REF=117
OPTIONAL=118
LIST=119
RECORD=120
BOOL=121
BYTES=122
DOUBLE=123
INT32=124
INT64=125
STRING=126
UINT32=127
UINT64=128
TRUE=129
FALSE=130
NULL=131
ARROW=132
COLON=133
SEMI=134
COMMA=135
DOT=136
LBRACE=137
RBRACE=138
LBRACK=139
RBRACK=140
LPAREN=141
RPAREN=142
LT=143
GT=144
AMP=145
EQUAL=146
INTEGER=147
JSON_NUMBER=148
IDENTIFIER=149
STRING_LITERAL=150
LINE_COMMENT=151
BLOCK_COMMENT=152
WS=153
VARIANT=8
PREPARES=9
INITIALIZE=10
WORKSPACE=11
EXECUTION=12
SNAPSHOT=13
TRANSACTION=14
REMOTE=15
TARGETS=16
PORTABLE=17
REPLICA=18
LOCAL=19
FROM=20
RESULT_BYTES=21
RPC_CALLS=22
DEADLINE_MS=23
QUERY=24
REFLECTION=25
SPECIALIZE=26
ROOT=27
DOCUMENT=28
FRAGMENTS=29
VIEW=30
MAX=31
ALLOW=32
POLL=33
QUERYABLE=34
RPC=35
QUERY_REASON=36
TYPE=37
OBJECT=38
STORABLE=39
IMPLEMENTS=40
REF=41
FRAGMENT=42
IMPORT=43
EXTERNAL=44
ATOM=45
INTERFACE=46
INTERFACES=47
PACKAGE=48
VALUE=49
RELATION=50
OPERATION=51
FUNCTION=52
CONSTRUCTOR=53
CONSTRUCTS=54
INPUT=55
CONFORM=56
AS=57
BIND=58
STATIC=59
TO=60
PRIVATE=61
SHARED=62
STATE=63
EDGE=64
PROJECTION=65
WITH=66
USING=67
ACCEPT=68
VIA=69
MATERIALIZE=70
IF=71
ABSENT=72
ON=73
POLICY=74
DEFAULT=75
SOURCE=76
REPOSITORY=77
COMMIT=78
REVISION=79
SEMANTIC_MAJOR=80
ON_DELETE=81
LIFECYCLE=82
RETAIN_OTHER=83
KEYED=84
PUBLIC_TRAVERSAL=85
ID=86
DOC=87
MODE=88
EMITS=89
RECEIVER=90
REQUIRES=91
ANY=92
GET=93
SET=94
WATCH=95
START=96
STOP=97
READ=98
WRITE=99
RESOLVE=100
CONNECT=101
DISCONNECT=102
CALL=103
WATCH_START=104
WATCH_STOP=105
SUBSCRIBE=106
UNSUBSCRIBE=107
REGISTER=108
CRDT=109
OPTIONAL_ONE=110
EXACTLY_ONE=111
MANY_UNIQUE=112
MANY=113
ORDERED=114
UNIT=115
WATCH_HANDLE=116
MESSAGE=117
ATOM_REF=118
INTERFACE_REF=119
OPTIONAL=120
LIST=121
RECORD=122
BOOL=123
BYTES=124
DOUBLE=125
INT32=126
INT64=127
STRING=128
UINT32=129
UINT64=130
TRUE=131
FALSE=132
NULL=133
ARROW=134
COLON=135
SEMI=136
COMMA=137
DOT=138
LBRACE=139
RBRACE=140
LBRACK=141
RBRACK=142
LPAREN=143
RPAREN=144
LT=145
GT=146
AMP=147
EQUAL=148
INTEGER=149
JSON_NUMBER=150
IDENTIFIER=151
STRING_LITERAL=152
LINE_COMMENT=153
BLOCK_COMMENT=154
WS=155
'edit'=1
'module'=2
'contract'=3
@@ -158,142 +160,144 @@ WS=153
'subject'=5
'for'=6
'creation'=7
'initialize'=8
'workspace'=9
'execution'=10
'snapshot'=11
'transaction'=12
'remote'=13
'targets'=14
'portable'=15
'replica'=16
'local'=17
'from'=18
'result-bytes'=19
'rpc-calls'=20
'deadline-ms'=21
'query'=22
'reflection'=23
'specialize'=24
'root'=25
'document'=26
'fragments'=27
'view'=28
'max'=29
'allow'=30
'poll'=31
'queryable'=32
'rpc'=33
'query-reason'=34
'type'=35
'object'=36
'storable'=37
'implements'=38
'ref'=39
'fragment'=40
'import'=41
'external'=42
'atom'=43
'interface'=44
'interfaces'=45
'package'=46
'value'=47
'relation'=48
'operation'=49
'function'=50
'constructor'=51
'constructs'=52
'input'=53
'conform'=54
'as'=55
'bind'=56
'static'=57
'to'=58
'private'=59
'shared'=60
'state'=61
'edge'=62
'projection'=63
'with'=64
'using'=65
'accept'=66
'via'=67
'materialize'=68
'if'=69
'absent'=70
'on'=71
'policy'=72
'default'=73
'source'=74
'repository'=75
'commit'=76
'revision'=77
'semantic-major'=78
'on-delete'=79
'lifecycle'=80
'retain-other'=81
'keyed'=82
'public-traversal'=83
'id'=84
'doc'=85
'mode'=86
'emits'=87
'receiver'=88
'requires'=89
'any'=90
'get'=91
'set'=92
'watch'=93
'start'=94
'stop'=95
'read'=96
'write'=97
'resolve'=98
'connect'=99
'disconnect'=100
'call'=101
'watch-start'=102
'watch-stop'=103
'subscribe'=104
'unsubscribe'=105
'register'=106
'crdt'=107
'optional-one'=108
'exactly-one'=109
'many-unique'=110
'many'=111
'ordered'=112
'unit'=113
'watch-handle'=114
'message'=115
'atom-ref'=116
'interface-ref'=117
'optional'=118
'list'=119
'record'=120
'bool'=121
'bytes'=122
'double'=123
'int32'=124
'int64'=125
'string'=126
'uint32'=127
'uint64'=128
'true'=129
'false'=130
'null'=131
'->'=132
':'=133
';'=134
','=135
'.'=136
'{'=137
'}'=138
'['=139
']'=140
'('=141
')'=142
'<'=143
'>'=144
'&'=145
'='=146
'variant'=8
'prepares'=9
'initialize'=10
'workspace'=11
'execution'=12
'snapshot'=13
'transaction'=14
'remote'=15
'targets'=16
'portable'=17
'replica'=18
'local'=19
'from'=20
'result-bytes'=21
'rpc-calls'=22
'deadline-ms'=23
'query'=24
'reflection'=25
'specialize'=26
'root'=27
'document'=28
'fragments'=29
'view'=30
'max'=31
'allow'=32
'poll'=33
'queryable'=34
'rpc'=35
'query-reason'=36
'type'=37
'object'=38
'storable'=39
'implements'=40
'ref'=41
'fragment'=42
'import'=43
'external'=44
'atom'=45
'interface'=46
'interfaces'=47
'package'=48
'value'=49
'relation'=50
'operation'=51
'function'=52
'constructor'=53
'constructs'=54
'input'=55
'conform'=56
'as'=57
'bind'=58
'static'=59
'to'=60
'private'=61
'shared'=62
'state'=63
'edge'=64
'projection'=65
'with'=66
'using'=67
'accept'=68
'via'=69
'materialize'=70
'if'=71
'absent'=72
'on'=73
'policy'=74
'default'=75
'source'=76
'repository'=77
'commit'=78
'revision'=79
'semantic-major'=80
'on-delete'=81
'lifecycle'=82
'retain-other'=83
'keyed'=84
'public-traversal'=85
'id'=86
'doc'=87
'mode'=88
'emits'=89
'receiver'=90
'requires'=91
'any'=92
'get'=93
'set'=94
'watch'=95
'start'=96
'stop'=97
'read'=98
'write'=99
'resolve'=100
'connect'=101
'disconnect'=102
'call'=103
'watch-start'=104
'watch-stop'=105
'subscribe'=106
'unsubscribe'=107
'register'=108
'crdt'=109
'optional-one'=110
'exactly-one'=111
'many-unique'=112
'many'=113
'ordered'=114
'unit'=115
'watch-handle'=116
'message'=117
'atom-ref'=118
'interface-ref'=119
'optional'=120
'list'=121
'record'=122
'bool'=123
'bytes'=124
'double'=125
'int32'=126
'int64'=127
'string'=128
'uint32'=129
'uint64'=130
'true'=131
'false'=132
'null'=133
'->'=134
':'=135
';'=136
','=137
'.'=138
'{'=139
'}'=140
'['=141
']'=142
'('=143
')'=144
'<'=145
'>'=146
'&'=147
'='=148
File diff suppressed because one or more lines are too long
@@ -5,152 +5,154 @@ PRESENTATION=4
SUBJECT=5
FOR=6
CREATION=7
INITIALIZE=8
WORKSPACE=9
EXECUTION=10
SNAPSHOT=11
TRANSACTION=12
REMOTE=13
TARGETS=14
PORTABLE=15
REPLICA=16
LOCAL=17
FROM=18
RESULT_BYTES=19
RPC_CALLS=20
DEADLINE_MS=21
QUERY=22
REFLECTION=23
SPECIALIZE=24
ROOT=25
DOCUMENT=26
FRAGMENTS=27
VIEW=28
MAX=29
ALLOW=30
POLL=31
QUERYABLE=32
RPC=33
QUERY_REASON=34
TYPE=35
OBJECT=36
STORABLE=37
IMPLEMENTS=38
REF=39
FRAGMENT=40
IMPORT=41
EXTERNAL=42
ATOM=43
INTERFACE=44
INTERFACES=45
PACKAGE=46
VALUE=47
RELATION=48
OPERATION=49
FUNCTION=50
CONSTRUCTOR=51
CONSTRUCTS=52
INPUT=53
CONFORM=54
AS=55
BIND=56
STATIC=57
TO=58
PRIVATE=59
SHARED=60
STATE=61
EDGE=62
PROJECTION=63
WITH=64
USING=65
ACCEPT=66
VIA=67
MATERIALIZE=68
IF=69
ABSENT=70
ON=71
POLICY=72
DEFAULT=73
SOURCE=74
REPOSITORY=75
COMMIT=76
REVISION=77
SEMANTIC_MAJOR=78
ON_DELETE=79
LIFECYCLE=80
RETAIN_OTHER=81
KEYED=82
PUBLIC_TRAVERSAL=83
ID=84
DOC=85
MODE=86
EMITS=87
RECEIVER=88
REQUIRES=89
ANY=90
GET=91
SET=92
WATCH=93
START=94
STOP=95
READ=96
WRITE=97
RESOLVE=98
CONNECT=99
DISCONNECT=100
CALL=101
WATCH_START=102
WATCH_STOP=103
SUBSCRIBE=104
UNSUBSCRIBE=105
REGISTER=106
CRDT=107
OPTIONAL_ONE=108
EXACTLY_ONE=109
MANY_UNIQUE=110
MANY=111
ORDERED=112
UNIT=113
WATCH_HANDLE=114
MESSAGE=115
ATOM_REF=116
INTERFACE_REF=117
OPTIONAL=118
LIST=119
RECORD=120
BOOL=121
BYTES=122
DOUBLE=123
INT32=124
INT64=125
STRING=126
UINT32=127
UINT64=128
TRUE=129
FALSE=130
NULL=131
ARROW=132
COLON=133
SEMI=134
COMMA=135
DOT=136
LBRACE=137
RBRACE=138
LBRACK=139
RBRACK=140
LPAREN=141
RPAREN=142
LT=143
GT=144
AMP=145
EQUAL=146
INTEGER=147
JSON_NUMBER=148
IDENTIFIER=149
STRING_LITERAL=150
LINE_COMMENT=151
BLOCK_COMMENT=152
WS=153
VARIANT=8
PREPARES=9
INITIALIZE=10
WORKSPACE=11
EXECUTION=12
SNAPSHOT=13
TRANSACTION=14
REMOTE=15
TARGETS=16
PORTABLE=17
REPLICA=18
LOCAL=19
FROM=20
RESULT_BYTES=21
RPC_CALLS=22
DEADLINE_MS=23
QUERY=24
REFLECTION=25
SPECIALIZE=26
ROOT=27
DOCUMENT=28
FRAGMENTS=29
VIEW=30
MAX=31
ALLOW=32
POLL=33
QUERYABLE=34
RPC=35
QUERY_REASON=36
TYPE=37
OBJECT=38
STORABLE=39
IMPLEMENTS=40
REF=41
FRAGMENT=42
IMPORT=43
EXTERNAL=44
ATOM=45
INTERFACE=46
INTERFACES=47
PACKAGE=48
VALUE=49
RELATION=50
OPERATION=51
FUNCTION=52
CONSTRUCTOR=53
CONSTRUCTS=54
INPUT=55
CONFORM=56
AS=57
BIND=58
STATIC=59
TO=60
PRIVATE=61
SHARED=62
STATE=63
EDGE=64
PROJECTION=65
WITH=66
USING=67
ACCEPT=68
VIA=69
MATERIALIZE=70
IF=71
ABSENT=72
ON=73
POLICY=74
DEFAULT=75
SOURCE=76
REPOSITORY=77
COMMIT=78
REVISION=79
SEMANTIC_MAJOR=80
ON_DELETE=81
LIFECYCLE=82
RETAIN_OTHER=83
KEYED=84
PUBLIC_TRAVERSAL=85
ID=86
DOC=87
MODE=88
EMITS=89
RECEIVER=90
REQUIRES=91
ANY=92
GET=93
SET=94
WATCH=95
START=96
STOP=97
READ=98
WRITE=99
RESOLVE=100
CONNECT=101
DISCONNECT=102
CALL=103
WATCH_START=104
WATCH_STOP=105
SUBSCRIBE=106
UNSUBSCRIBE=107
REGISTER=108
CRDT=109
OPTIONAL_ONE=110
EXACTLY_ONE=111
MANY_UNIQUE=112
MANY=113
ORDERED=114
UNIT=115
WATCH_HANDLE=116
MESSAGE=117
ATOM_REF=118
INTERFACE_REF=119
OPTIONAL=120
LIST=121
RECORD=122
BOOL=123
BYTES=124
DOUBLE=125
INT32=126
INT64=127
STRING=128
UINT32=129
UINT64=130
TRUE=131
FALSE=132
NULL=133
ARROW=134
COLON=135
SEMI=136
COMMA=137
DOT=138
LBRACE=139
RBRACE=140
LBRACK=141
RBRACK=142
LPAREN=143
RPAREN=144
LT=145
GT=146
AMP=147
EQUAL=148
INTEGER=149
JSON_NUMBER=150
IDENTIFIER=151
STRING_LITERAL=152
LINE_COMMENT=153
BLOCK_COMMENT=154
WS=155
'edit'=1
'module'=2
'contract'=3
@@ -158,142 +160,144 @@ WS=153
'subject'=5
'for'=6
'creation'=7
'initialize'=8
'workspace'=9
'execution'=10
'snapshot'=11
'transaction'=12
'remote'=13
'targets'=14
'portable'=15
'replica'=16
'local'=17
'from'=18
'result-bytes'=19
'rpc-calls'=20
'deadline-ms'=21
'query'=22
'reflection'=23
'specialize'=24
'root'=25
'document'=26
'fragments'=27
'view'=28
'max'=29
'allow'=30
'poll'=31
'queryable'=32
'rpc'=33
'query-reason'=34
'type'=35
'object'=36
'storable'=37
'implements'=38
'ref'=39
'fragment'=40
'import'=41
'external'=42
'atom'=43
'interface'=44
'interfaces'=45
'package'=46
'value'=47
'relation'=48
'operation'=49
'function'=50
'constructor'=51
'constructs'=52
'input'=53
'conform'=54
'as'=55
'bind'=56
'static'=57
'to'=58
'private'=59
'shared'=60
'state'=61
'edge'=62
'projection'=63
'with'=64
'using'=65
'accept'=66
'via'=67
'materialize'=68
'if'=69
'absent'=70
'on'=71
'policy'=72
'default'=73
'source'=74
'repository'=75
'commit'=76
'revision'=77
'semantic-major'=78
'on-delete'=79
'lifecycle'=80
'retain-other'=81
'keyed'=82
'public-traversal'=83
'id'=84
'doc'=85
'mode'=86
'emits'=87
'receiver'=88
'requires'=89
'any'=90
'get'=91
'set'=92
'watch'=93
'start'=94
'stop'=95
'read'=96
'write'=97
'resolve'=98
'connect'=99
'disconnect'=100
'call'=101
'watch-start'=102
'watch-stop'=103
'subscribe'=104
'unsubscribe'=105
'register'=106
'crdt'=107
'optional-one'=108
'exactly-one'=109
'many-unique'=110
'many'=111
'ordered'=112
'unit'=113
'watch-handle'=114
'message'=115
'atom-ref'=116
'interface-ref'=117
'optional'=118
'list'=119
'record'=120
'bool'=121
'bytes'=122
'double'=123
'int32'=124
'int64'=125
'string'=126
'uint32'=127
'uint64'=128
'true'=129
'false'=130
'null'=131
'->'=132
':'=133
';'=134
','=135
'.'=136
'{'=137
'}'=138
'['=139
']'=140
'('=141
')'=142
'<'=143
'>'=144
'&'=145
'='=146
'variant'=8
'prepares'=9
'initialize'=10
'workspace'=11
'execution'=12
'snapshot'=13
'transaction'=14
'remote'=15
'targets'=16
'portable'=17
'replica'=18
'local'=19
'from'=20
'result-bytes'=21
'rpc-calls'=22
'deadline-ms'=23
'query'=24
'reflection'=25
'specialize'=26
'root'=27
'document'=28
'fragments'=29
'view'=30
'max'=31
'allow'=32
'poll'=33
'queryable'=34
'rpc'=35
'query-reason'=36
'type'=37
'object'=38
'storable'=39
'implements'=40
'ref'=41
'fragment'=42
'import'=43
'external'=44
'atom'=45
'interface'=46
'interfaces'=47
'package'=48
'value'=49
'relation'=50
'operation'=51
'function'=52
'constructor'=53
'constructs'=54
'input'=55
'conform'=56
'as'=57
'bind'=58
'static'=59
'to'=60
'private'=61
'shared'=62
'state'=63
'edge'=64
'projection'=65
'with'=66
'using'=67
'accept'=68
'via'=69
'materialize'=70
'if'=71
'absent'=72
'on'=73
'policy'=74
'default'=75
'source'=76
'repository'=77
'commit'=78
'revision'=79
'semantic-major'=80
'on-delete'=81
'lifecycle'=82
'retain-other'=83
'keyed'=84
'public-traversal'=85
'id'=86
'doc'=87
'mode'=88
'emits'=89
'receiver'=90
'requires'=91
'any'=92
'get'=93
'set'=94
'watch'=95
'start'=96
'stop'=97
'read'=98
'write'=99
'resolve'=100
'connect'=101
'disconnect'=102
'call'=103
'watch-start'=104
'watch-stop'=105
'subscribe'=106
'unsubscribe'=107
'register'=108
'crdt'=109
'optional-one'=110
'exactly-one'=111
'many-unique'=112
'many'=113
'ordered'=114
'unit'=115
'watch-handle'=116
'message'=117
'atom-ref'=118
'interface-ref'=119
'optional'=120
'list'=121
'record'=122
'bool'=123
'bytes'=124
'double'=125
'int32'=126
'int64'=127
'string'=128
'uint32'=129
'uint64'=130
'true'=131
'false'=132
'null'=133
'->'=134
':'=135
';'=136
','=137
'.'=138
'{'=139
'}'=140
'['=141
']'=142
'('=143
')'=144
'<'=145
'>'=146
'&'=147
'='=148
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -39,6 +39,7 @@ import { FragmentVariableContext } from "./QuixosCapabilityParser.js";
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
import { AcceptanceClauseContext } from "./QuixosCapabilityParser.js";
import { PreparationClauseContext } from "./QuixosCapabilityParser.js";
import { ExecutionClauseContext } from "./QuixosCapabilityParser.js";
import { SelectedOperationsContext } from "./QuixosCapabilityParser.js";
import { PackageConstructorExportContext } from "./QuixosCapabilityParser.js";
@@ -77,6 +78,7 @@ import { DependencyBindingContext } from "./QuixosCapabilityParser.js";
import { ConstructorBindingDeclContext } from "./QuixosCapabilityParser.js";
import { ValueTypeContext } from "./QuixosCapabilityParser.js";
import { RecordFieldContext } from "./QuixosCapabilityParser.js";
import { VariantCaseContext } from "./QuixosCapabilityParser.js";
import { ScalarTypeContext } from "./QuixosCapabilityParser.js";
import { CardinalityContext } from "./QuixosCapabilityParser.js";
import { JsonLiteralContext } from "./QuixosCapabilityParser.js";
@@ -317,6 +319,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitAcceptanceClause?: (ctx: AcceptanceClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.preparationClause`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPreparationClause?: (ctx: PreparationClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.executionClause`.
* @param ctx the parse tree
@@ -545,6 +553,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitRecordField?: (ctx: RecordFieldContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.variantCase`.
* @param ctx the parse tree
* @return the visitor result
*/
visitVariantCase?: (ctx: VariantCaseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.scalarType`.
* @param ctx the parse tree
+19
View File
@@ -110,6 +110,7 @@ export class GenericSourceTypes {
}
if (type.kind === "list" || type.kind === "optional") return storable(type.value, depth + 1);
if (type.kind === "record") return Object.values(type.fields).every((field) => storable(field, depth + 1));
if (type.kind === "variant") return Object.values(type.cases).every((entry) => storable(entry.value, depth + 1));
if (type.kind === "alias") {
const alias = aliases.get(type.definitionId);
if (!alias || alias.parameters.length !== type.arguments.length) return false;
@@ -301,6 +302,24 @@ export class GenericSourceTypes {
}
expression(context: ValueTypeContext): ValueTypeExpression {
if (context.VARIANT()) {
const entries = context.variantCase().map(
(entry) =>
[
entry.identifier().getText(),
{
id: literal(entry.stringLiteral()),
value: this.expression(entry.valueType()),
},
] as const,
);
if (
new Set(entries.map(([label]) => label)).size !== entries.length ||
new Set(entries.map(([, entry]) => entry.id)).size !== entries.length
)
throw new GenericTypeError("duplicate-variant-case", "variant", "Variant labels and case IDs must be unique");
return { kind: "variant", id: literal(context.stringLiteral()!), cases: Object.fromEntries(entries) };
}
if (context.scalarType())
return {
kind: "scalar",
+21
View File
@@ -316,6 +316,7 @@ const lowerValueType = (state: LoweringState, context: ValueTypeContext | null):
throw new Error("Missing value type after a successful parse");
}
if (
context.VARIANT() ||
context.REF() ||
context.typeArguments() ||
(context.identifier() && !context.ATOM_REF() && !context.INTERFACE_REF())
@@ -1658,6 +1659,13 @@ const lowerPackage = (
const operation = exportContext.packageOperationExport();
const generic = operation ?? exportContext.packageFunctionExport();
if (generic?.typeParameters()) {
if (generic.preparationClause())
loweringIssue(
state,
generic,
"unsupported-generic-preparation",
"Creation preparation currently requires a closed export",
);
if (generic.acceptanceClause())
loweringIssue(
state,
@@ -1689,6 +1697,19 @@ const lowerPackage = (
// Resolve after all exports have been declared, so checks can follow bodies.
for (const source of context.packageExport()) {
const declaration = source.packageOperationExport() ?? source.packageFunctionExport();
const preparation = declaration?.preparationClause();
if (declaration && preparation && !declaration.typeParameters()) {
const entry = exports.find((entry) => entry.displayName === identifier(declaration.identifier()));
const port = entry?.dependencyPorts.find((port) => port.displayName === identifier(preparation.identifier()));
if (!entry || port?.requirement.kind !== "creation")
loweringIssue(
state,
preparation,
"invalid-creation-preparation",
"Preparation must name a creation port of this export",
);
else entry.preparation = { port: port.id, key: stringValue(preparation.stringLiteral()) };
}
const clause = declaration?.acceptanceClause();
if (!declaration || !clause || declaration.typeParameters()) continue;
const entry = exports.find((entry) => entry.displayName === identifier(declaration.identifier()));
+22
View File
@@ -16,6 +16,7 @@ export type ValueTypeExpression =
| Extract<ValueType, { kind: "builtin" | "scalar" | "message" }>
| { kind: "parameter"; parameterId: string }
| { kind: "record"; fields: Record<string, ValueTypeExpression> }
| { kind: "variant"; id: string; cases: Record<string, { id: string; value: ValueTypeExpression }> }
| { kind: "list" | "optional" | "change"; value: ValueTypeExpression }
| { kind: "object-ref"; expectation: ObjectTypeExpression }
| { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] };
@@ -195,6 +196,8 @@ export const isStorableType = (
return false;
case "record":
return Object.values(value.fields).every((field) => visit(field, depth + 1));
case "variant":
return Object.values(value.cases).every((entry) => visit(entry.value, depth + 1));
}
};
return visit(type, 0);
@@ -221,6 +224,14 @@ export const canonicalTypeArgument = (argument: ClosedTypeArgument): string => {
return [value.kind, value.name];
case "message":
return ["message", value.descriptorId];
case "variant":
return [
"variant",
value.id,
Object.keys(value.cases)
.sort()
.map((label) => [label, value.cases[label].id, type(value.cases[label].value, depth + 1)]),
];
case "object-ref":
return ["object-ref", target(value.expectation)];
case "change":
@@ -323,6 +334,17 @@ export class TypeSubstitution {
value(expression: ValueTypeExpression, path = "type", depth = 0): ValueType {
this.budget.enter(path, depth);
switch (expression.kind) {
case "variant":
return {
kind: "variant",
id: expression.id,
cases: Object.fromEntries(
Object.entries(expression.cases).map(([label, entry]) => [
label,
{ id: entry.id, value: this.value(entry.value, `${path}.${label}`, depth + 1) },
]),
),
};
case "builtin":
return { kind: "builtin", name: expression.name };
case "scalar":
+3
View File
@@ -65,6 +65,7 @@ export type ValueType =
| { kind: "scalar"; name: ScalarValueTypeName }
| { kind: "message"; descriptorId: string }
| { kind: "record"; fields: Record<string, ValueType> }
| { kind: "variant"; id: string; cases: Record<string, { id: string; value: ValueType }> }
| { kind: "object-ref"; expectation: ObjectExpectation }
| { kind: "optional"; value: ValueType }
| { kind: "list"; value: ValueType }
@@ -270,6 +271,8 @@ export interface DependencyPort {
}
interface PackageExportBase {
/** Returns this declared birth, with unchanged operation input as its initializer. */
preparation?: { port: DependencyPortId; key: string };
/** Required authoritative snapshot export in this package. */
acceptanceExportId?: PackageExportId;
execution?: ExecutionContract;
+54
View File
@@ -148,6 +148,11 @@ const requireText = (issues: CapabilityValidationIssue[], value: string, path: s
const typeLabel = (type: ValueType): string => {
switch (type.kind) {
case "variant":
return `variant:${type.id}{${Object.entries(type.cases)
.sort(([a], [b]) => a.localeCompare(b))
.map(([label, entry]) => `${label}:${entry.id}:${typeLabel(entry.value)}`)
.join(";")}}`;
case "builtin":
case "scalar":
return type.name;
@@ -176,6 +181,19 @@ export const valueTypesEqual = (left: ValueType, right: ValueType): boolean => {
return false;
}
switch (left.kind) {
case "variant": {
const other = right as typeof left;
return (
left.id === other.id &&
Object.keys(left.cases).length === Object.keys(other.cases).length &&
Object.entries(left.cases).every(
([label, entry]) =>
Object.hasOwn(other.cases, label) &&
entry.id === other.cases[label].id &&
valueTypesEqual(entry.value, other.cases[label].value),
)
);
}
case "builtin":
case "scalar":
return left.name === (right as typeof left).name;
@@ -232,6 +250,20 @@ const validateValueType = (
indexes: Pick<ValidationIndexes, "atoms" | "interfaces">,
) => {
switch (type.kind) {
case "variant": {
requireText(issues, type.id, `${path}.id`, "Variant identity");
const entries = Object.entries(type.cases),
ids = new Set<string>();
if (!entries.length || entries.length > 256) issue(issues, "invalid-value-type", path, "Use 1–256 variant cases");
for (const [label, entry] of entries) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(label)) issue(issues, "invalid-value-type", path, "Invalid variant label");
requireText(issues, entry.id, `${path}.${label}.id`, "Case identity");
if (ids.has(entry.id)) issue(issues, "invalid-value-type", path, "Duplicate case identity");
ids.add(entry.id);
validateValueType(issues, entry.value, `${path}.${label}`, indexes);
}
return;
}
case "record":
for (const [name, field] of Object.entries(type.fields)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
@@ -569,6 +601,7 @@ const collectIdentityIndexes = (
if (member.kind === "value") {
const queryValue = (type: ValueType): boolean =>
type.kind === "scalar" ||
(type.kind === "variant" && isStorableType(type)) ||
((type.kind === "optional" || type.kind === "list") && queryValue(type.value)) ||
(type.kind === "record" &&
Object.keys(type.fields).length > 0 &&
@@ -893,6 +926,7 @@ const validateAttachments = (issues: CapabilityValidationIssue[], indexes: Valid
type.kind === "object-ref" ||
type.kind === "change" ||
(type.kind === "record" && Object.values(type.fields).some(containsRpcType)) ||
(type.kind === "variant" && Object.values(type.cases).some((entry) => containsRpcType(entry.value))) ||
((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value));
if (
containsRpcType(attachment.valueType) ||
@@ -999,6 +1033,26 @@ const validatePackages = (
const exportPath = `${path}.exports[${exportIndex}]`;
validateValueType(issues, entry.inputType, `${exportPath}.inputType`, indexes);
validateValueType(issues, entry.outputType, `${exportPath}.outputType`, indexes);
if (entry.preparation) {
const port = entry.dependencyPorts.find((port) => port.id === entry.preparation!.port);
if (
entry.execution?.kind !== "transaction" ||
entry.kind === "constructor" ||
(entry.kind === "operation" && entry.mode !== "call") ||
!entry.preparation.key ||
port?.requirement.kind !== "creation" ||
!valueTypesEqual(port.requirement.inputType, entry.inputType) ||
entry.outputType.kind !== "object-ref" ||
(entry.outputType.expectation.kind === "atom" &&
entry.outputType.expectation.atomId !== port.requirement.atomId)
)
issue(
issues,
"invalid-dependency-binding",
exportPath,
"Prepared creation requires a transaction returning its declared creation, with matching captured input types and a fixed key",
);
}
if (entry.acceptanceExportId !== undefined) {
const check = revision.exports.find((candidate) => candidate.id === entry.acceptanceExportId);
if (
+8 -1
View File
@@ -9,10 +9,15 @@ import type {
} from "../capability-model/types.js";
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
import { portableValueSchema } from "../bindings/portable.js";
function preparedInputPaths(type: ValueType, path: (string | null)[] = []): (string | null)[][] {
type PreparedStep = string | null | { case: string; id: string };
function preparedInputPaths(type: ValueType, path: PreparedStep[] = []): PreparedStep[][] {
if (type.kind === "change") return [path];
if (type.kind === "optional") return preparedInputPaths(type.value, path);
if (type.kind === "list") return preparedInputPaths(type.value, [...path, null]);
if (type.kind === "variant")
return Object.entries(type.cases).flatMap(([label, entry]) =>
preparedInputPaths(entry.value, [...path, { case: label, id: entry.id }]),
);
if (type.kind === "record")
return Object.entries(type.fields).flatMap(([key, value]) => preparedInputPaths(value, [...path, key]));
return [];
@@ -210,6 +215,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
package: binding.packageRevisionId,
export: binding.exportId,
ports,
...(entry.preparation ? { preparation: entry.preparation } : {}),
...(check
? { acceptance: { export: check.id, ports: compilePorts(check, binding.acceptanceDependencies ?? []) } }
: {}),
@@ -237,6 +243,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
revision: pkg.revisionId,
exports: pkg.exports.map((entry) => ({
id: entry.id,
...(entry.preparation ? { preparation: entry.preparation } : {}),
...(entry.acceptanceExportId ? { acceptance: entry.acceptanceExportId } : {}),
effect: entry.execution?.kind ?? "remote",
targets: entry.execution && entry.execution.kind !== "remote" ? entry.execution.targets : [],
+3
View File
@@ -142,6 +142,9 @@ export function resolvePlannedOperation(
/** Bound argument/state codec subset. A parsed type alone does not mean a runtime implements it. */
function checkValue(type: ValueType, path: string, allowChange = false): void {
switch (type.kind) {
case "variant":
for (const [label, entry] of Object.entries(type.cases)) checkValue(entry.value, `${path}.${label}`, allowChange);
return;
case "scalar":
case "object-ref":
return;
+21
View File
@@ -126,8 +126,29 @@ export function querySchema(
);
return views[0]!.interfaceRevisionId;
};
const variants = new Map<string, GraphQLScalarType>();
const scalar = (type: ValueType): GraphQLOutputType & GraphQLInputType => {
if (type.kind === "optional") return scalar(type.value);
if (type.kind === "variant") {
const name = `QxVariant_${createHash("sha256").update(JSON.stringify(type)).digest("hex").slice(0, 16)}`;
let result = variants.get(name);
if (!result) {
result = new GraphQLScalarType({
name,
description: "Whole checked Camino variant value. Branch selection and predicates are not supported.",
serialize: (value) => value,
parseValue() {
throw new QueryCompileError("QUERY_TYPE", "Variant query inputs are not supported");
},
parseLiteral() {
throw new QueryCompileError("QUERY_TYPE", "Variant query inputs are not supported");
},
});
variants.set(name, result);
scalarTypes.set(name, type);
}
return result;
}
if (type.kind !== "scalar")
throw new QueryCompileError("QUERY_TYPE", "Only scalar/optional scalar query fields are supported");
return queryScalars[type.name];
+17
View File
@@ -154,3 +154,20 @@ await writeFile("dist/portable-reader/compound-crdt/kernel.json", JSON.stringify
const { emitExecutionScaffold } = await import("./execution-scaffold-fixture.js");
const executionScaffold = await emitExecutionScaffold("dist/portable-reader/execution-scaffold/source");
await writeFile("dist/portable-reader/execution-scaffold/plan.json", JSON.stringify(executionScaffold));
// Native variant fixture shared by generated-type and browser editing qualification.
const { readFile } = await import("node:fs/promises");
const { compileCapabilityResourceSource } = await import("../src/capability-language/index.js");
const { portableValueSchema, tsType } = await import("../src/bindings/portable.js");
const variantSource = compileCapabilityResourceSource(await readFile("test/fixtures/canvas-content.qx", "utf8"), {
source: { repository: "https://example.test/canvas-content.git", commit: "a".repeat(40) },
});
if (!variantSource.ok || variantSource.resource.kind !== "interface") throw Error("Invalid variant fixture");
const entries = variantSource.resource.revision.members[0];
if (entries.kind !== "value") throw Error("Expected entries value");
await mkdir("dist/portable-reader/variants", { recursive: true });
await writeFile("dist/portable-reader/variants/schema.json", JSON.stringify(portableValueSchema(entries.valueType)));
await writeFile(
"dist/portable-reader/variants/entries.ts",
"export type Entries = " + tsType(entries.valueType) + ";\n",
);
+12
View File
@@ -0,0 +1,12 @@
type Point = record { x:double; y:double; };
type Entry = variant id "canvas:entry@1" {
paragraph id "canvas:paragraph" : record { text:string; };
stroke id "canvas:stroke" : record { points:list<Point>; };
image id "canvas:image" : record { asset:string; };
};
interface CanvasContent id "canvas" revision "canvas@1" {
queryable value entries id "entries" : list<Entry> {
get id "entries:get" execution snapshot targets [browser,server];
edit id "entries:edit" execution transaction targets [browser,server];
}
}
+82
View File
@@ -0,0 +1,82 @@
import { readFileSync } from "node:fs";
import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
import { compileQuery } from "../src/query/compile.js";
import { defaultQueryBudgets } from "../src/query/types.js";
import { portableValueSchema, tsType } from "../src/bindings/portable.js";
import { isStorableType, TypeSubstitution } from "../src/capability-model/index.js";
const source = { repository: "https://example.test/variants.git", commit: "a".repeat(40) };
const declaration = readFileSync("test/fixtures/canvas-content.qx", "utf8");
const result = compileCapabilityResourceSource(declaration, { source });
assert.ok(result.ok, JSON.stringify(result.diagnostics));
if (result.resource.kind !== "interface") throw Error("Expected interface");
const iface = result.resource.revision;
const entries = iface.members[0];
if (entries.kind !== "value") throw Error("Expected value");
test("native variant aliases lower into nominal cases with typed payloads", () => {
assert.equal(entries.valueType.kind, "list");
assert.ok(isStorableType(entries.valueType));
const schema = portableValueSchema(entries.valueType) as any;
assert.equal(schema.value.id, "canvas:entry@1");
assert.equal(schema.value.cases.stroke.id, "canvas:stroke");
assert.match(tsType(entries.valueType), /kind: "stroke"/);
assert.match(tsType(entries.valueType), /ReadonlyArray<.*"x": number/);
const substitution = new TypeSubstitution({
arguments: new Map([["T", { kind: "value", type: { kind: "scalar", name: "string" } }]]),
applyInterface: () => {
throw Error("unused");
},
implementsInterface: () => false,
});
assert.deepEqual(
substitution.value({
kind: "variant",
id: "box",
cases: { some: { id: "box:some", value: { kind: "parameter", parameterId: "T" } } },
}),
{ kind: "variant", id: "box", cases: { some: { id: "box:some", value: { kind: "scalar", name: "string" } } } },
);
});
test("variant query scalars keep their checked payload type and reject branch selection", async () => {
const query = {
id: "canvas-query",
displayName: "Canvas",
root: { kind: "object" as const, interface: iface.revisionId },
document: "canvas.graphql",
operation: "Canvas",
fragments: [],
views: [],
allowances: [],
budgets: { ...defaultQueryBudgets },
watch: true,
};
const checked = await compileQuery(query, [iface], async () => `query Canvas { root { entries } }`);
assert.equal(checked.output.kind, "record");
if (checked.output.kind !== "record") return;
const root = checked.output.fields.root;
assert.equal(root.kind, "record");
if (root.kind !== "record") return;
assert.deepEqual(root.fields.entries, entries.valueType);
await assert.rejects(compileQuery(query, [iface], async () => `query Canvas { root { entries { kind value } } }`));
});
test("unknown and duplicate cases fail; reference payloads remain nonstorable", () => {
const duplicate = compileCapabilityResourceSource(declaration.replace('"canvas:image"', '"canvas:stroke"'), {
source,
});
assert.equal(duplicate.ok, false);
assert.ok(
!isStorableType({
kind: "variant",
id: "transport",
cases: {
ref: {
id: "ref",
value: { kind: "object-ref", expectation: { kind: "interface", interfaceRevisionId: iface.revisionId } },
},
},
}),
);
});