Generalize live field bindings and use them for Studio canvas gestures

This commit is contained in:
Timothy J. Aveni
2026-09-28 11:12:11 -07:00
parent 696023b4ba
commit 0ace71eae6
13 changed files with 1766 additions and 1586 deletions
+2 -1
View File
@@ -106,7 +106,7 @@ operationMember
; ;
valueMember valueMember
: queryReadContract? VALUE identifier ID stringLiteral COLON valueType : queryReadContract? (VALUE | FIELD) identifier ID stringLiteral COLON valueType
LBRACE valueMemberOperation* RBRACE LBRACE valueMemberOperation* RBRACE
; ;
@@ -568,6 +568,7 @@ INTERFACE: 'interface';
INTERFACES: 'interfaces'; INTERFACES: 'interfaces';
PACKAGE: 'package'; PACKAGE: 'package';
VALUE: 'value'; VALUE: 'value';
FIELD: 'field';
RELATION: 'relation'; RELATION: 'relation';
OPERATION: 'operation'; OPERATION: 'operation';
FUNCTION: 'function'; FUNCTION: 'function';
File diff suppressed because one or more lines are too long
@@ -47,112 +47,113 @@ INTERFACE=46
INTERFACES=47 INTERFACES=47
PACKAGE=48 PACKAGE=48
VALUE=49 VALUE=49
RELATION=50 FIELD=50
OPERATION=51 RELATION=51
FUNCTION=52 OPERATION=52
CONSTRUCTOR=53 FUNCTION=53
CONSTRUCTS=54 CONSTRUCTOR=54
INPUT=55 CONSTRUCTS=55
CONFORM=56 INPUT=56
AS=57 CONFORM=57
BIND=58 AS=58
STATIC=59 BIND=59
TO=60 STATIC=60
PRIVATE=61 TO=61
SHARED=62 PRIVATE=62
STATE=63 SHARED=63
EDGE=64 STATE=64
PROJECTION=65 EDGE=65
WITH=66 PROJECTION=66
USING=67 WITH=67
ACCEPT=68 USING=68
VIA=69 ACCEPT=69
MATERIALIZE=70 VIA=70
IF=71 MATERIALIZE=71
ABSENT=72 IF=72
ON=73 ABSENT=73
POLICY=74 ON=74
DEFAULT=75 POLICY=75
SOURCE=76 DEFAULT=76
REPOSITORY=77 SOURCE=77
COMMIT=78 REPOSITORY=78
REVISION=79 COMMIT=79
SEMANTIC_MAJOR=80 REVISION=80
ON_DELETE=81 SEMANTIC_MAJOR=81
LIFECYCLE=82 ON_DELETE=82
RETAIN_OTHER=83 LIFECYCLE=83
KEYED=84 RETAIN_OTHER=84
PUBLIC_TRAVERSAL=85 KEYED=85
ID=86 PUBLIC_TRAVERSAL=86
DOC=87 ID=87
MODE=88 DOC=88
EMITS=89 MODE=89
RECEIVER=90 EMITS=90
REQUIRES=91 RECEIVER=91
ANY=92 REQUIRES=92
GET=93 ANY=93
SET=94 GET=94
WATCH=95 SET=95
START=96 WATCH=96
STOP=97 START=97
READ=98 STOP=98
WRITE=99 READ=99
RESOLVE=100 WRITE=100
CONNECT=101 RESOLVE=101
DISCONNECT=102 CONNECT=102
CALL=103 DISCONNECT=103
WATCH_START=104 CALL=104
WATCH_STOP=105 WATCH_START=105
SUBSCRIBE=106 WATCH_STOP=106
UNSUBSCRIBE=107 SUBSCRIBE=107
REGISTER=108 UNSUBSCRIBE=108
CRDT=109 REGISTER=109
OPTIONAL_ONE=110 CRDT=110
EXACTLY_ONE=111 OPTIONAL_ONE=111
MANY_UNIQUE=112 EXACTLY_ONE=112
MANY=113 MANY_UNIQUE=113
ORDERED=114 MANY=114
UNIT=115 ORDERED=115
WATCH_HANDLE=116 UNIT=116
MESSAGE=117 WATCH_HANDLE=117
ATOM_REF=118 MESSAGE=118
INTERFACE_REF=119 ATOM_REF=119
OPTIONAL=120 INTERFACE_REF=120
LIST=121 OPTIONAL=121
RECORD=122 LIST=122
BOOL=123 RECORD=123
BYTES=124 BOOL=124
DOUBLE=125 BYTES=125
INT32=126 DOUBLE=126
INT64=127 INT32=127
STRING=128 INT64=128
UINT32=129 STRING=129
UINT64=130 UINT32=130
TRUE=131 UINT64=131
FALSE=132 TRUE=132
NULL=133 FALSE=133
ARROW=134 NULL=134
COLON=135 ARROW=135
SEMI=136 COLON=136
COMMA=137 SEMI=137
DOT=138 COMMA=138
LBRACE=139 DOT=139
RBRACE=140 LBRACE=140
LBRACK=141 RBRACE=141
RBRACK=142 LBRACK=142
LPAREN=143 RBRACK=143
RPAREN=144 LPAREN=144
LT=145 RPAREN=145
GT=146 LT=146
AMP=147 GT=147
EQUAL=148 AMP=148
INTEGER=149 EQUAL=149
JSON_NUMBER=150 INTEGER=150
IDENTIFIER=151 JSON_NUMBER=151
STRING_LITERAL=152 IDENTIFIER=152
LINE_COMMENT=153 STRING_LITERAL=153
BLOCK_COMMENT=154 LINE_COMMENT=154
WS=155 BLOCK_COMMENT=155
WS=156
'edit'=1 'edit'=1
'module'=2 'module'=2
'contract'=3 'contract'=3
@@ -202,102 +203,103 @@ WS=155
'interfaces'=47 'interfaces'=47
'package'=48 'package'=48
'value'=49 'value'=49
'relation'=50 'field'=50
'operation'=51 'relation'=51
'function'=52 'operation'=52
'constructor'=53 'function'=53
'constructs'=54 'constructor'=54
'input'=55 'constructs'=55
'conform'=56 'input'=56
'as'=57 'conform'=57
'bind'=58 'as'=58
'static'=59 'bind'=59
'to'=60 'static'=60
'private'=61 'to'=61
'shared'=62 'private'=62
'state'=63 'shared'=63
'edge'=64 'state'=64
'projection'=65 'edge'=65
'with'=66 'projection'=66
'using'=67 'with'=67
'accept'=68 'using'=68
'via'=69 'accept'=69
'materialize'=70 'via'=70
'if'=71 'materialize'=71
'absent'=72 'if'=72
'on'=73 'absent'=73
'policy'=74 'on'=74
'default'=75 'policy'=75
'source'=76 'default'=76
'repository'=77 'source'=77
'commit'=78 'repository'=78
'revision'=79 'commit'=79
'semantic-major'=80 'revision'=80
'on-delete'=81 'semantic-major'=81
'lifecycle'=82 'on-delete'=82
'retain-other'=83 'lifecycle'=83
'keyed'=84 'retain-other'=84
'public-traversal'=85 'keyed'=85
'id'=86 'public-traversal'=86
'doc'=87 'id'=87
'mode'=88 'doc'=88
'emits'=89 'mode'=89
'receiver'=90 'emits'=90
'requires'=91 'receiver'=91
'any'=92 'requires'=92
'get'=93 'any'=93
'set'=94 'get'=94
'watch'=95 'set'=95
'start'=96 'watch'=96
'stop'=97 'start'=97
'read'=98 'stop'=98
'write'=99 'read'=99
'resolve'=100 'write'=100
'connect'=101 'resolve'=101
'disconnect'=102 'connect'=102
'call'=103 'disconnect'=103
'watch-start'=104 'call'=104
'watch-stop'=105 'watch-start'=105
'subscribe'=106 'watch-stop'=106
'unsubscribe'=107 'subscribe'=107
'register'=108 'unsubscribe'=108
'crdt'=109 'register'=109
'optional-one'=110 'crdt'=110
'exactly-one'=111 'optional-one'=111
'many-unique'=112 'exactly-one'=112
'many'=113 'many-unique'=113
'ordered'=114 'many'=114
'unit'=115 'ordered'=115
'watch-handle'=116 'unit'=116
'message'=117 'watch-handle'=117
'atom-ref'=118 'message'=118
'interface-ref'=119 'atom-ref'=119
'optional'=120 'interface-ref'=120
'list'=121 'optional'=121
'record'=122 'list'=122
'bool'=123 'record'=123
'bytes'=124 'bool'=124
'double'=125 'bytes'=125
'int32'=126 'double'=126
'int64'=127 'int32'=127
'string'=128 'int64'=128
'uint32'=129 'string'=129
'uint64'=130 'uint32'=130
'true'=131 'uint64'=131
'false'=132 'true'=132
'null'=133 'false'=133
'->'=134 'null'=134
':'=135 '->'=135
';'=136 ':'=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
File diff suppressed because one or more lines are too long
@@ -47,112 +47,113 @@ INTERFACE=46
INTERFACES=47 INTERFACES=47
PACKAGE=48 PACKAGE=48
VALUE=49 VALUE=49
RELATION=50 FIELD=50
OPERATION=51 RELATION=51
FUNCTION=52 OPERATION=52
CONSTRUCTOR=53 FUNCTION=53
CONSTRUCTS=54 CONSTRUCTOR=54
INPUT=55 CONSTRUCTS=55
CONFORM=56 INPUT=56
AS=57 CONFORM=57
BIND=58 AS=58
STATIC=59 BIND=59
TO=60 STATIC=60
PRIVATE=61 TO=61
SHARED=62 PRIVATE=62
STATE=63 SHARED=63
EDGE=64 STATE=64
PROJECTION=65 EDGE=65
WITH=66 PROJECTION=66
USING=67 WITH=67
ACCEPT=68 USING=68
VIA=69 ACCEPT=69
MATERIALIZE=70 VIA=70
IF=71 MATERIALIZE=71
ABSENT=72 IF=72
ON=73 ABSENT=73
POLICY=74 ON=74
DEFAULT=75 POLICY=75
SOURCE=76 DEFAULT=76
REPOSITORY=77 SOURCE=77
COMMIT=78 REPOSITORY=78
REVISION=79 COMMIT=79
SEMANTIC_MAJOR=80 REVISION=80
ON_DELETE=81 SEMANTIC_MAJOR=81
LIFECYCLE=82 ON_DELETE=82
RETAIN_OTHER=83 LIFECYCLE=83
KEYED=84 RETAIN_OTHER=84
PUBLIC_TRAVERSAL=85 KEYED=85
ID=86 PUBLIC_TRAVERSAL=86
DOC=87 ID=87
MODE=88 DOC=88
EMITS=89 MODE=89
RECEIVER=90 EMITS=90
REQUIRES=91 RECEIVER=91
ANY=92 REQUIRES=92
GET=93 ANY=93
SET=94 GET=94
WATCH=95 SET=95
START=96 WATCH=96
STOP=97 START=97
READ=98 STOP=98
WRITE=99 READ=99
RESOLVE=100 WRITE=100
CONNECT=101 RESOLVE=101
DISCONNECT=102 CONNECT=102
CALL=103 DISCONNECT=103
WATCH_START=104 CALL=104
WATCH_STOP=105 WATCH_START=105
SUBSCRIBE=106 WATCH_STOP=106
UNSUBSCRIBE=107 SUBSCRIBE=107
REGISTER=108 UNSUBSCRIBE=108
CRDT=109 REGISTER=109
OPTIONAL_ONE=110 CRDT=110
EXACTLY_ONE=111 OPTIONAL_ONE=111
MANY_UNIQUE=112 EXACTLY_ONE=112
MANY=113 MANY_UNIQUE=113
ORDERED=114 MANY=114
UNIT=115 ORDERED=115
WATCH_HANDLE=116 UNIT=116
MESSAGE=117 WATCH_HANDLE=117
ATOM_REF=118 MESSAGE=118
INTERFACE_REF=119 ATOM_REF=119
OPTIONAL=120 INTERFACE_REF=120
LIST=121 OPTIONAL=121
RECORD=122 LIST=122
BOOL=123 RECORD=123
BYTES=124 BOOL=124
DOUBLE=125 BYTES=125
INT32=126 DOUBLE=126
INT64=127 INT32=127
STRING=128 INT64=128
UINT32=129 STRING=129
UINT64=130 UINT32=130
TRUE=131 UINT64=131
FALSE=132 TRUE=132
NULL=133 FALSE=133
ARROW=134 NULL=134
COLON=135 ARROW=135
SEMI=136 COLON=136
COMMA=137 SEMI=137
DOT=138 COMMA=138
LBRACE=139 DOT=139
RBRACE=140 LBRACE=140
LBRACK=141 RBRACE=141
RBRACK=142 LBRACK=142
LPAREN=143 RBRACK=143
RPAREN=144 LPAREN=144
LT=145 RPAREN=145
GT=146 LT=146
AMP=147 GT=147
EQUAL=148 AMP=148
INTEGER=149 EQUAL=149
JSON_NUMBER=150 INTEGER=150
IDENTIFIER=151 JSON_NUMBER=151
STRING_LITERAL=152 IDENTIFIER=152
LINE_COMMENT=153 STRING_LITERAL=153
BLOCK_COMMENT=154 LINE_COMMENT=154
WS=155 BLOCK_COMMENT=155
WS=156
'edit'=1 'edit'=1
'module'=2 'module'=2
'contract'=3 'contract'=3
@@ -202,102 +203,103 @@ WS=155
'interfaces'=47 'interfaces'=47
'package'=48 'package'=48
'value'=49 'value'=49
'relation'=50 'field'=50
'operation'=51 'relation'=51
'function'=52 'operation'=52
'constructor'=53 'function'=53
'constructs'=54 'constructor'=54
'input'=55 'constructs'=55
'conform'=56 'input'=56
'as'=57 'conform'=57
'bind'=58 'as'=58
'static'=59 'bind'=59
'to'=60 'static'=60
'private'=61 'to'=61
'shared'=62 'private'=62
'state'=63 'shared'=63
'edge'=64 'state'=64
'projection'=65 'edge'=65
'with'=66 'projection'=66
'using'=67 'with'=67
'accept'=68 'using'=68
'via'=69 'accept'=69
'materialize'=70 'via'=70
'if'=71 'materialize'=71
'absent'=72 'if'=72
'on'=73 'absent'=73
'policy'=74 'on'=74
'default'=75 'policy'=75
'source'=76 'default'=76
'repository'=77 'source'=77
'commit'=78 'repository'=78
'revision'=79 'commit'=79
'semantic-major'=80 'revision'=80
'on-delete'=81 'semantic-major'=81
'lifecycle'=82 'on-delete'=82
'retain-other'=83 'lifecycle'=83
'keyed'=84 'retain-other'=84
'public-traversal'=85 'keyed'=85
'id'=86 'public-traversal'=86
'doc'=87 'id'=87
'mode'=88 'doc'=88
'emits'=89 'mode'=89
'receiver'=90 'emits'=90
'requires'=91 'receiver'=91
'any'=92 'requires'=92
'get'=93 'any'=93
'set'=94 'get'=94
'watch'=95 'set'=95
'start'=96 'watch'=96
'stop'=97 'start'=97
'read'=98 'stop'=98
'write'=99 'read'=99
'resolve'=100 'write'=100
'connect'=101 'resolve'=101
'disconnect'=102 'connect'=102
'call'=103 'disconnect'=103
'watch-start'=104 'call'=104
'watch-stop'=105 'watch-start'=105
'subscribe'=106 'watch-stop'=106
'unsubscribe'=107 'subscribe'=107
'register'=108 'unsubscribe'=108
'crdt'=109 'register'=109
'optional-one'=110 'crdt'=110
'exactly-one'=111 'optional-one'=111
'many-unique'=112 'exactly-one'=112
'many'=113 'many-unique'=113
'ordered'=114 'many'=114
'unit'=115 'ordered'=115
'watch-handle'=116 'unit'=116
'message'=117 'watch-handle'=117
'atom-ref'=118 'message'=118
'interface-ref'=119 'atom-ref'=119
'optional'=120 'interface-ref'=120
'list'=121 'optional'=121
'record'=122 'list'=122
'bool'=123 'record'=123
'bytes'=124 'bool'=124
'double'=125 'bytes'=125
'int32'=126 'double'=126
'int64'=127 'int32'=127
'string'=128 'int64'=128
'uint32'=129 'string'=129
'uint64'=130 'uint32'=130
'true'=131 'uint64'=131
'false'=132 'true'=132
'null'=133 'false'=133
'->'=134 'null'=134
':'=135 '->'=135
';'=136 ':'=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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2
View File
@@ -529,6 +529,7 @@ const lowerValueMember = (state: LoweringState, context: ValueMemberContext): In
} }
return { return {
kind: "value", kind: "value",
...(context.FIELD() ? { nativeField: true as const } : {}),
...(context.queryReadContract() ...(context.queryReadContract()
? { ? {
queryRead: { queryRead: {
@@ -794,6 +795,7 @@ const lowerInterfaceTemplate = (
const type = types.expression(value.valueType()); const type = types.expression(value.valueType());
return { return {
kind: "value", kind: "value",
...(value.FIELD() ? { nativeField: true as const } : {}),
...(value.queryReadContract() ...(value.queryReadContract()
? { ? {
queryRead: { queryRead: {
+1
View File
@@ -98,6 +98,7 @@ export const instantiateInterface = (
return { return {
...common, ...common,
kind: "value", kind: "value",
...(member.nativeField ? { nativeField: true as const } : {}),
...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}), ...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}),
valueType: substitution.value(member.valueType, member.displayName), valueType: substitution.value(member.valueType, member.displayName),
}; };
+2
View File
@@ -132,6 +132,8 @@ interface InterfaceMemberBase<Type = ValueType> {
export interface ValueInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> { export interface ValueInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
kind: "value"; kind: "value";
/** Every operation resolves directly to the same native state slot. */
nativeField?: true;
queryRead?: QueryReadContract; queryRead?: QueryReadContract;
valueType: Type; valueType: Type;
} }
+45
View File
@@ -615,6 +615,23 @@ const collectIdentityIndexes = (
); );
} }
} }
if (member.kind === "value" && member.nativeField) {
const names = member.operations.map((o) => o.displayName);
if (
!isStorableType(member.valueType) ||
!names.includes("get") ||
names.some((n) => !["get", "set", "edit"].includes(n)) ||
(names.includes("set") && names.includes("edit"))
)
issue(
issues,
"invalid-state-binding",
memberPath,
"Fields require a storable value, a native getter and at most one setter or editor",
);
if (member.queryRead && member.queryRead.execution !== "native")
issue(issues, "invalid-query-contract", memberPath, "Fields cannot have package-backed query reads");
}
requireText(issues, member.id, `${memberPath}.id`, "Member ID"); requireText(issues, member.id, `${memberPath}.id`, "Member ID");
requireText(issues, member.displayName, `${memberPath}.displayName`, "Member name"); requireText(issues, member.displayName, `${memberPath}.displayName`, "Member name");
if (memberIds.has(member.id)) { if (memberIds.has(member.id)) {
@@ -1758,6 +1775,34 @@ const validateConformances = (
} }
} else binding = entry.binding; } else binding = entry.binding;
const member = operationEntry.member; const member = operationEntry.member;
if (member.kind === "value" && member.nativeField) {
const primitive = { get: "read", set: "write", edit: "edit" }[operation.displayName];
const getter = member.operations.find((o) => o.displayName === "get");
let read: Binding | undefined;
try {
if (getter)
read = resolveForwardedBinding(
workspace,
conformance.atomId,
conformance.interfaceRevisionId,
getter.id,
).binding;
} catch {}
if (
binding.kind !== "state" ||
binding.primitive !== primitive ||
read?.kind !== "state" ||
read.primitive !== "read" ||
read.slotId !== binding.slotId
)
issue(
issues,
"invalid-state-binding",
bindingPath,
"Field operations must resolve to the same native state slot",
);
}
if ( if (
member.kind === "value" && member.kind === "value" &&
operation.displayName === "edit" && operation.displayName === "edit" &&
+21 -5
View File
@@ -17,6 +17,7 @@ export interface PublicOperationContract {
execution: ExecutionContract; execution: ExecutionContract;
input: unknown; input: unknown;
output: unknown; output: unknown;
fieldPolicy?: "register" | "crdt";
} }
export function publicOperationContract( export function publicOperationContract(
iface: InterfaceRevision, iface: InterfaceRevision,
@@ -29,7 +30,20 @@ export function publicOperationContract(
"Expected a closed interface call operation", "Expected a closed interface call operation",
); );
const execution = operation.execution ?? { kind: "remote" }; const execution = operation.execution ?? { kind: "remote" };
const member = iface.members.find((m) => m.operations.includes(operation))!;
const fieldPolicy =
member.kind === "value" &&
operation.scope !== "class" &&
execution.kind === "transaction" &&
execution.targets.includes("browser")
? operation.displayName === "edit"
? "crdt"
: member.nativeField && operation.displayName === "set"
? "register"
: undefined
: undefined;
return { return {
...(fieldPolicy ? { fieldPolicy } : {}),
interface: iface.revisionId, interface: iface.revisionId,
operation: operation.id, operation: operation.id,
scope: operation.scope ?? "object", scope: operation.scope ?? "object",
@@ -116,15 +130,17 @@ export function generateInterfaceClientBindings(
} }
const operations = member.operations const operations = member.operations
.filter((op) => op.mode === "call") .filter((op) => op.mode === "call")
.map( .map((op) => {
(op) => const contract = publicOperationContract(iface, op);
`${JSON.stringify(op.displayName)}: defineInterfaceOperation<${tsType(op.inputType)}, ${tsType(op.outputType)}, ${JSON.stringify(publicOperationContract(iface, op).execution)}>(${JSON.stringify(publicOperationContract(iface, op))})`, return contract.fieldPolicy && member.kind === "value"
); ? `${JSON.stringify(op.displayName)}: defineFieldOperation<${tsType(member.valueType)}, ${JSON.stringify(contract.fieldPolicy)}, ${JSON.stringify(contract.execution)}>(${JSON.stringify(contract)})`
: `${JSON.stringify(op.displayName)}: defineInterfaceOperation<${tsType(op.inputType)}, ${tsType(op.outputType)}, ${JSON.stringify(contract.execution)}>(${JSON.stringify(contract)})`;
});
return `${JSON.stringify(member.displayName)}: {${operations.join(",\n")}}`; return `${JSON.stringify(member.displayName)}: {${operations.join(",\n")}}`;
}); });
return `${JSON.stringify(alias)}: {${members.join(",\n")}}`; return `${JSON.stringify(alias)}: {${members.join(",\n")}}`;
}); });
return `// Generated from imported interface contracts. Regenerate; do not edit.\nimport { defineModuleMember, defineInterfaceOperation, type PreparedChange, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const interfaces = {${entries.join(",\n")}} as const;\n`; return `// Generated from imported interface contracts. Regenerate; do not edit.\nimport { defineModuleMember, defineInterfaceOperation, defineFieldOperation, type PreparedChange, type ManagedReference } from ${JSON.stringify(runtimeModule)};\nexport const interfaces = {${entries.join(",\n")}} as const;\n`;
} }
export function generatePackageQueryClientBindings( export function generatePackageQueryClientBindings(
+89
View File
@@ -22,6 +22,95 @@ const parsed = compileCapabilityResourceSource(
assert.ok(parsed.ok && parsed.resource.kind === "interface", JSON.stringify(parsed.diagnostics)); assert.ok(parsed.ok && parsed.resource.kind === "interface", JSON.stringify(parsed.diagnostics));
const iface = parsed.resource.revision; const iface = parsed.resource.revision;
const environment = { interfaces: new Map([["Data", iface]]), interfaceClosure: [iface] }; const environment = { interfaces: new Map([["Data", iface]]), interfaceClosure: [iface] };
test("native fields certify one storage slot; only browser writes get live descriptors", () => {
const parsed = compileCapabilityResourceSource(
`interface Fields id "fields" revision "fields@1" {
queryable field position id "position" : string {
get id "position:get" execution snapshot targets [browser, server];
set id "position:set" execution transaction targets [browser, server];
}
field remote id "remote" : string {
get id "remote:get";
set id "remote:set";
}
value arbitrary id "arbitrary" : string {
get id "arbitrary:get" execution snapshot targets [browser,server];
set id "arbitrary:set" execution transaction targets [browser, server];
}
value text id "text" : string {
get id "text:get";
edit id "text:edit" execution transaction targets [browser, server];
}
}`,
{ source },
);
assert.ok(parsed.ok && parsed.resource.kind === "interface", JSON.stringify(parsed.diagnostics));
const revision = parsed.resource.revision;
const generated = generateInterfaceClientBindings({ Fields: revision });
assert.match(generated, /defineFieldOperation<string, "register",/);
assert.match(generated, /defineFieldOperation<string, "crdt",/);
for (const member of revision.members) {
const operation = member.operations.at(-1)!;
assert.equal(
publicOperationContract(revision, operation).fieldPolicy,
member.displayName === "position" ? "register" : member.displayName === "text" ? "crdt" : undefined,
);
}
const workspace = `workspace W id "w" revision "w@1" commit "${source.commit}" {
import interface Fields; atom A id "a";
conform A as Fields id "a:fields" {
private state First id "first" on A : string policy register default "";
private state Second id "second" on A : string policy register default "";
private state Text id "text" on A : string policy crdt(string);
initialize { text to state Text; };
bind position.get to state First.read;
bind position.set to state First.write;
bind remote.get to state Second.read;
bind remote.set to state Second.write;
bind arbitrary.get to state Second.read;
bind arbitrary.set to state Second.write;
bind text.get to state Text.read;
bind text.edit to state Text.edit;
}
}`;
const env = { interfaces: new Map([["Fields", revision]]) };
const good = compileCapabilitySource(workspace, "workspace.qx", env);
assert.ok(good.ok, JSON.stringify(good.diagnostics));
const bad = compileCapabilitySource(
workspace.replace("bind position.set to state First.write", "bind position.set to state Second.write"),
"workspace.qx",
env,
);
assert.equal(bad.ok, false);
assert.ok(
bad.diagnostics.some((d) => d.message.includes("same native state slot")),
JSON.stringify(bad.diagnostics),
);
const forwarded = workspace
.replace("bind position.get to state First.read", "bind position.get to interface Fields.arbitrary.get")
.replace("bind position.set to state First.write", "bind position.set to interface Fields.arbitrary.set");
const goodForward = compileCapabilitySource(forwarded, "workspace.qx", env);
assert.ok(goodForward.ok, JSON.stringify(goodForward.diagnostics));
const pkg = compileCapabilityResourceSource(
`package Setter id "setter" revision "setter@1" {
operation replaceValue id "write" : string -> unit mode call execution transaction targets [browser,server] receiver any;
}`,
{ source },
);
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
const badForward = compileCapabilitySource(
forwarded
.replace("import interface Fields;", "import interface Fields; import package Setter;")
.replace("bind arbitrary.set to state Second.write", "bind arbitrary.set to package Setter.replaceValue"),
"workspace.qx",
{ ...env, packages: new Map([["Setter", pkg.resource.revision]]) },
);
assert.equal(badForward.ok, false);
assert.ok(
badForward.diagnostics.some((d) => d.message.includes("same native state slot")),
JSON.stringify(badForward.diagnostics),
);
});
const compile = async () => { const compile = async () => {
const pkg = compileCapabilityResourceSource( const pkg = compileCapabilityResourceSource(
`import interface Data; package Views id "views" revision "views@1" { `import interface Data; package Views id "views" revision "views@1" {