@dusted/anqst 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -4
- package/dist/src/app.js +47 -15
- package/dist/src/emit.js +1463 -200
- package/dist/src/layout.js +9 -3
- package/dist/src/project.js +3 -3
- package/package.json +1 -1
- package/spec/AnQst-Spec-DSL.d.ts +22 -24
package/dist/src/emit.js
CHANGED
|
@@ -277,6 +277,52 @@ function sanitizeIdentifier(value) {
|
|
|
277
277
|
const withFallback = trimmed.length > 0 ? trimmed : "Codec";
|
|
278
278
|
return /^[0-9]/.test(withFallback) ? `T_${withFallback}` : withFallback;
|
|
279
279
|
}
|
|
280
|
+
/** Qualify generated struct/enum names in public widget headers so TUs that include multiple widgets
|
|
281
|
+
* (each with `using namespace WidgetName`) do not get ambiguous unqualified types (e.g. two `Magic`). */
|
|
282
|
+
const CPP_WIDGET_HEADER_PRIMITIVES = new Set([
|
|
283
|
+
"void",
|
|
284
|
+
"bool",
|
|
285
|
+
"double",
|
|
286
|
+
"float",
|
|
287
|
+
"QString",
|
|
288
|
+
"QByteArray",
|
|
289
|
+
"QVariantMap",
|
|
290
|
+
"QStringList",
|
|
291
|
+
"qint64",
|
|
292
|
+
"quint64",
|
|
293
|
+
"qint32",
|
|
294
|
+
"quint32",
|
|
295
|
+
"qint16",
|
|
296
|
+
"quint16",
|
|
297
|
+
"qint8",
|
|
298
|
+
"quint8",
|
|
299
|
+
"int8_t",
|
|
300
|
+
"uint8_t",
|
|
301
|
+
"int16_t",
|
|
302
|
+
"uint16_t",
|
|
303
|
+
"int32_t",
|
|
304
|
+
"uint32_t"
|
|
305
|
+
]);
|
|
306
|
+
function qualifyCppTypeForWidgetHeaderPublicApi(widgetName, cppType) {
|
|
307
|
+
const trimmed = cppType.trim();
|
|
308
|
+
const listInner = /^QList<(.+)>$/.exec(trimmed);
|
|
309
|
+
if (listInner) {
|
|
310
|
+
return `QList<${qualifyCppTypeForWidgetHeaderPublicApi(widgetName, listInner[1])}>`;
|
|
311
|
+
}
|
|
312
|
+
if (trimmed.includes("::")) {
|
|
313
|
+
return trimmed;
|
|
314
|
+
}
|
|
315
|
+
if (CPP_WIDGET_HEADER_PRIMITIVES.has(trimmed)) {
|
|
316
|
+
return trimmed;
|
|
317
|
+
}
|
|
318
|
+
if (trimmed.startsWith("std::")) {
|
|
319
|
+
return trimmed;
|
|
320
|
+
}
|
|
321
|
+
if (/^(qint|quint)[0-9]{1,2}$/.test(trimmed)) {
|
|
322
|
+
return trimmed;
|
|
323
|
+
}
|
|
324
|
+
return `${widgetName}::${trimmed}`;
|
|
325
|
+
}
|
|
280
326
|
function variantToCppExpression(cppType, expr) {
|
|
281
327
|
if (cppType === "QString")
|
|
282
328
|
return `${expr}.toString()`;
|
|
@@ -1183,32 +1229,44 @@ function renderWidgetHeader(spec, cppTypes, cppCodecCatalog) {
|
|
|
1183
1229
|
const properties = [];
|
|
1184
1230
|
const fields = [];
|
|
1185
1231
|
const publicSlots = [];
|
|
1186
|
-
const dragDropHelperMethods = dragDropPayloadHelpers.flatMap((helper) =>
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1232
|
+
const dragDropHelperMethods = dragDropPayloadHelpers.flatMap((helper) => {
|
|
1233
|
+
const qPayload = qualifyCppTypeForWidgetHeaderPublicApi(spec.widgetName, helper.cppType);
|
|
1234
|
+
return [
|
|
1235
|
+
`static QByteArray encodeDragDropPayload_${helper.typeName}(const ${qPayload}& payload);`,
|
|
1236
|
+
`static std::optional<${qPayload}> decodeDragDropPayload_${helper.typeName}(const QByteArray& rawPayload);`
|
|
1237
|
+
];
|
|
1238
|
+
});
|
|
1239
|
+
const qType = (t) => qualifyCppTypeForWidgetHeaderPublicApi(spec.widgetName, t);
|
|
1190
1240
|
for (const service of spec.services) {
|
|
1191
1241
|
for (const member of service.members) {
|
|
1192
1242
|
const memberPascal = pascalCase(member.name);
|
|
1193
1243
|
if (member.kind === "Call" && member.payloadTypeText) {
|
|
1194
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1195
|
-
const args = member.parameters
|
|
1244
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
1245
|
+
const args = member.parameters
|
|
1246
|
+
.map((p) => `const ${qType(cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]))}& ${p.name}`)
|
|
1247
|
+
.join(", ");
|
|
1196
1248
|
callbackAliases.push(`using ${memberPascal}Handler = std::function<${cppType}(${args})>;`);
|
|
1197
1249
|
handleMethods.push(` void ${member.name}(const ${memberPascal}Handler& handler) const;`);
|
|
1198
1250
|
callSetterMethods.push(`void set${memberPascal}CallHandler(const ${memberPascal}Handler& handler);`);
|
|
1199
1251
|
fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
|
|
1200
1252
|
}
|
|
1201
1253
|
else if (member.kind === "Emitter") {
|
|
1202
|
-
const args = member.parameters
|
|
1254
|
+
const args = member.parameters
|
|
1255
|
+
.map((p) => `const ${qType(cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]))}& ${p.name}`)
|
|
1256
|
+
.join(", ");
|
|
1203
1257
|
signals.push(`void ${member.name}(${args});`);
|
|
1204
1258
|
}
|
|
1205
1259
|
else if (member.kind === "Slot") {
|
|
1206
|
-
const ret = member.payloadTypeText
|
|
1207
|
-
|
|
1260
|
+
const ret = member.payloadTypeText
|
|
1261
|
+
? qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]))
|
|
1262
|
+
: "void";
|
|
1263
|
+
const args = member.parameters
|
|
1264
|
+
.map((p) => `${qType(cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]))} ${p.name}`)
|
|
1265
|
+
.join(", ");
|
|
1208
1266
|
slotMethods.push(`${ret} slot_${member.name}(${args});`);
|
|
1209
1267
|
}
|
|
1210
1268
|
else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
|
|
1211
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1269
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
1212
1270
|
const cap = member.name.charAt(0).toUpperCase() + member.name.slice(1);
|
|
1213
1271
|
properties.push(`Q_PROPERTY(${cppType} ${member.name} READ ${member.name} WRITE set${cap} NOTIFY ${member.name}Changed)`);
|
|
1214
1272
|
publicMethods.push(`${cppType} ${member.name}() const;`);
|
|
@@ -1225,11 +1283,11 @@ function renderWidgetHeader(spec, cppTypes, cppCodecCatalog) {
|
|
|
1225
1283
|
}
|
|
1226
1284
|
}
|
|
1227
1285
|
else if (member.kind === "DropTarget" && member.payloadTypeText) {
|
|
1228
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1286
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
1229
1287
|
signals.push(`void ${member.name}(const ${cppType}& payload, double x, double y);`);
|
|
1230
1288
|
}
|
|
1231
1289
|
else if (member.kind === "HoverTarget" && member.payloadTypeText) {
|
|
1232
|
-
const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
|
|
1290
|
+
const cppType = qType(cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]));
|
|
1233
1291
|
signals.push(`void ${member.name}(const ${cppType}& payload, double x, double y);`);
|
|
1234
1292
|
signals.push(`void ${member.name}Left();`);
|
|
1235
1293
|
}
|
|
@@ -2012,7 +2070,8 @@ function renderNpmPackage(spec) {
|
|
|
2012
2070
|
anqst: {
|
|
2013
2071
|
widget: spec.widgetName,
|
|
2014
2072
|
services: spec.services.map((s) => s.name),
|
|
2015
|
-
supportsDevelopmentModeTransport: spec.supportsDevelopmentModeTransport
|
|
2073
|
+
supportsDevelopmentModeTransport: spec.supportsDevelopmentModeTransport,
|
|
2074
|
+
outputContractVersion: 2
|
|
2016
2075
|
}
|
|
2017
2076
|
}, null, 2);
|
|
2018
2077
|
}
|
|
@@ -2041,6 +2100,19 @@ function slotHandlerReturnType(tsRet) {
|
|
|
2041
2100
|
}
|
|
2042
2101
|
return `${tsRet} | Promise<${tsRet}> | Error`;
|
|
2043
2102
|
}
|
|
2103
|
+
/** Angular and vanilla: emit `readonly set` / `readonly onSlot` only when the spec provides members for that namespace. */
|
|
2104
|
+
function formatTsServiceSetAndOnSlotObjectLiterals(setMembers, onSlotMembers) {
|
|
2105
|
+
const blocks = [];
|
|
2106
|
+
if (setMembers.length > 0) {
|
|
2107
|
+
blocks.push(` readonly set = {\n${setMembers.join("\n")}\n };`);
|
|
2108
|
+
}
|
|
2109
|
+
if (onSlotMembers.length > 0) {
|
|
2110
|
+
blocks.push(` readonly onSlot = {\n${onSlotMembers.join("\n")}\n };`);
|
|
2111
|
+
}
|
|
2112
|
+
if (blocks.length === 0)
|
|
2113
|
+
return "";
|
|
2114
|
+
return `\n${blocks.join("\n")}`;
|
|
2115
|
+
}
|
|
2044
2116
|
function renderTsService(spec, serviceName, codecCatalog) {
|
|
2045
2117
|
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
2046
2118
|
const fieldLines = [];
|
|
@@ -2191,14 +2263,7 @@ function renderTsService(spec, serviceName, codecCatalog) {
|
|
|
2191
2263
|
export class ${serviceName} {
|
|
2192
2264
|
private readonly _bridge = inject(AnQstBridgeRuntime);
|
|
2193
2265
|
${fieldLines.join("\n")}
|
|
2194
|
-
${constructorLines.join("\n")}
|
|
2195
|
-
readonly set = {
|
|
2196
|
-
${setMembers.join("\n")}
|
|
2197
|
-
};
|
|
2198
|
-
readonly onSlot = {
|
|
2199
|
-
${onSlotMembers.join("\n")}
|
|
2200
|
-
};
|
|
2201
|
-
${methodLines.join("\n")}
|
|
2266
|
+
${constructorLines.join("\n")}${formatTsServiceSetAndOnSlotObjectLiterals(setMembers, onSlotMembers)}${methodLines.length > 0 ? `\n${methodLines.join("\n")}` : ""}
|
|
2202
2267
|
}
|
|
2203
2268
|
`;
|
|
2204
2269
|
}
|
|
@@ -2241,25 +2306,29 @@ function renderTsServiceDts(spec, serviceName) {
|
|
|
2241
2306
|
classMembers.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null;`);
|
|
2242
2307
|
}
|
|
2243
2308
|
}
|
|
2244
|
-
const
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
readonly onSlot: ${onSlotInterfaceName}
|
|
2309
|
+
const interfaceBlocks = [];
|
|
2310
|
+
if (setMembers.length > 0) {
|
|
2311
|
+
interfaceBlocks.push(`export interface ${setInterfaceName} {\n${setMembers.join("\n")}\n}`);
|
|
2312
|
+
}
|
|
2313
|
+
if (onSlotMembers.length > 0) {
|
|
2314
|
+
interfaceBlocks.push(`export interface ${onSlotInterfaceName} {\n${onSlotMembers.join("\n")}\n}`);
|
|
2315
|
+
}
|
|
2316
|
+
const interfaceSection = interfaceBlocks.length > 0 ? `${interfaceBlocks.join("\n\n")}\n\n` : "";
|
|
2317
|
+
const namespaceLines = [];
|
|
2318
|
+
if (setMembers.length > 0) {
|
|
2319
|
+
namespaceLines.push(` readonly set: ${setInterfaceName};`);
|
|
2320
|
+
}
|
|
2321
|
+
if (onSlotMembers.length > 0) {
|
|
2322
|
+
namespaceLines.push(` readonly onSlot: ${onSlotInterfaceName};`);
|
|
2323
|
+
}
|
|
2324
|
+
const declareBodyLines = [...namespaceLines, ...classMembers];
|
|
2325
|
+
return `${interfaceSection}export declare class ${serviceName} {
|
|
2326
|
+
${declareBodyLines.join("\n")}
|
|
2258
2327
|
}`;
|
|
2259
2328
|
}
|
|
2260
2329
|
function renderTsServices(spec, codecCatalog) {
|
|
2261
2330
|
const serviceClasses = spec.services.map((s) => renderTsService(spec, s.name, codecCatalog)).join("\n");
|
|
2262
|
-
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/services.ts`).trim();
|
|
2331
|
+
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/services.ts`).trim();
|
|
2263
2332
|
const localTypeImports = renderLocalTypeImports(spec).trim();
|
|
2264
2333
|
const typeImports = [externalTypeImports, localTypeImports].filter((s) => s.length > 0).join("\n");
|
|
2265
2334
|
const typeImportsBlock = typeImports.length > 0 ? `${typeImports}\n\n` : "";
|
|
@@ -3077,13 +3146,13 @@ ${serviceClasses}
|
|
|
3077
3146
|
`;
|
|
3078
3147
|
}
|
|
3079
3148
|
function renderTsTypes(spec) {
|
|
3080
|
-
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types.ts`).trim();
|
|
3149
|
+
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/types.ts`).trim();
|
|
3081
3150
|
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
3082
3151
|
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
3083
3152
|
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
3084
3153
|
}
|
|
3085
3154
|
function renderTypeServicesDts(spec) {
|
|
3086
|
-
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types/services.d.ts`).trim();
|
|
3155
|
+
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/types/services.d.ts`).trim();
|
|
3087
3156
|
const localTypeImports = renderLocalTypeImports(spec).trim();
|
|
3088
3157
|
const bridgeDiagnosticsDecl = `export type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
|
|
3089
3158
|
|
|
@@ -3120,7 +3189,7 @@ export declare class AnQstBridgeDiagnostics {
|
|
|
3120
3189
|
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
3121
3190
|
}
|
|
3122
3191
|
function renderTypeTypesDts(spec) {
|
|
3123
|
-
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}/types/types.d.ts`).trim();
|
|
3192
|
+
const typeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}/types/types.d.ts`).trim();
|
|
3124
3193
|
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
3125
3194
|
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
3126
3195
|
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
@@ -3149,158 +3218,1329 @@ function renderJsServices() {
|
|
|
3149
3218
|
function renderJsTypes() {
|
|
3150
3219
|
return renderJsModule();
|
|
3151
3220
|
}
|
|
3152
|
-
function
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3221
|
+
function renderVanillaServiceTs(spec, serviceName, codecCatalog) {
|
|
3222
|
+
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
3223
|
+
const fieldLines = [];
|
|
3224
|
+
const methodLines = [];
|
|
3225
|
+
const setMembers = [];
|
|
3226
|
+
const onSlotMembers = [];
|
|
3227
|
+
const constructorBodyLines = [];
|
|
3228
|
+
for (const m of members) {
|
|
3229
|
+
const args = m.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
3230
|
+
const paramSites = m.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, serviceName, m.name, p.name));
|
|
3231
|
+
const encodedValueArray = paramSites.length > 0
|
|
3232
|
+
? `[${m.parameters.map((p, index) => `${paramSites[index] ? `encode${paramSites[index].codecId}(${p.name})` : p.name}`).join(", ")}]`
|
|
3233
|
+
: "[]";
|
|
3234
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, serviceName, m.name);
|
|
3235
|
+
if (m.kind === "Call") {
|
|
3236
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
3237
|
+
if (payloadSite) {
|
|
3238
|
+
methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { const result = await this._bridge.call<unknown>("${serviceName}", "${m.name}", ${encodedValueArray}); return decode${payloadSite.codecId}(result); }`);
|
|
3163
3239
|
}
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
widget: spec.widgetName,
|
|
3167
|
-
services: spec.services.map((s) => s.name),
|
|
3168
|
-
target: "node_express_ws"
|
|
3169
|
-
}
|
|
3170
|
-
}, null, 2);
|
|
3171
|
-
}
|
|
3172
|
-
function nodeParamTuple(member) {
|
|
3173
|
-
if (member.parameters.length === 0)
|
|
3174
|
-
return "[]";
|
|
3175
|
-
return `[${member.parameters.map((p) => mapTypeTextToTs(p.typeText)).join(", ")}]`;
|
|
3176
|
-
}
|
|
3177
|
-
function nodeParamArgs(member) {
|
|
3178
|
-
return member.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
3179
|
-
}
|
|
3180
|
-
function nodeParamValues(member) {
|
|
3181
|
-
if (member.parameters.length === 0)
|
|
3182
|
-
return "[]";
|
|
3183
|
-
return `[${member.parameters.map((p) => p.name).join(", ")}]`;
|
|
3184
|
-
}
|
|
3185
|
-
function nodeCap(value) {
|
|
3186
|
-
return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
3187
|
-
}
|
|
3188
|
-
function renderNodeExpressWsTypes(spec) {
|
|
3189
|
-
const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/types/index.d.ts`).trim();
|
|
3190
|
-
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
3191
|
-
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
3192
|
-
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
3193
|
-
}
|
|
3194
|
-
function renderNodeExpressWsIndex(spec, codecCatalog) {
|
|
3195
|
-
const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
|
|
3196
|
-
const typeDecls = renderTypeDeclarations(spec, true);
|
|
3197
|
-
const handlerBridgeTypeName = `${spec.widgetName}HandlerBridge`;
|
|
3198
|
-
const sessionBridgeTypeName = `${spec.widgetName}SessionBridge`;
|
|
3199
|
-
const handlerInterfaces = spec.services
|
|
3200
|
-
.map((service) => {
|
|
3201
|
-
const lines = [];
|
|
3202
|
-
for (const member of service.members) {
|
|
3203
|
-
const args = nodeParamArgs(member);
|
|
3204
|
-
const prefixedArgs = args.length > 0 ? `, ${args}` : "";
|
|
3205
|
-
if (member.kind === "Call" && member.payloadTypeText) {
|
|
3206
|
-
const ret = mapTypeTextToTs(member.payloadTypeText);
|
|
3207
|
-
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): ${ret} | Promise<${ret}>;`);
|
|
3240
|
+
else {
|
|
3241
|
+
methodLines.push(` async ${m.name}(${args}): Promise<${ret}> { return this._bridge.call<${ret}>("${serviceName}", "${m.name}", ${encodedValueArray}); }`);
|
|
3208
3242
|
}
|
|
3209
|
-
|
|
3210
|
-
|
|
3243
|
+
continue;
|
|
3244
|
+
}
|
|
3245
|
+
if (m.kind === "Emitter") {
|
|
3246
|
+
methodLines.push(` ${m.name}(${args}): void {`);
|
|
3247
|
+
methodLines.push(` let encodedArgs: unknown[];`);
|
|
3248
|
+
methodLines.push(` try {`);
|
|
3249
|
+
methodLines.push(` encodedArgs = ${encodedValueArray};`);
|
|
3250
|
+
methodLines.push(` } catch (error) {`);
|
|
3251
|
+
methodLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3252
|
+
methodLines.push(` code: "SerializationError",`);
|
|
3253
|
+
methodLines.push(` severity: "error",`);
|
|
3254
|
+
methodLines.push(` category: "bridge",`);
|
|
3255
|
+
methodLines.push(` recoverable: true,`);
|
|
3256
|
+
methodLines.push(` message: \`Failed to serialize Emitter ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3257
|
+
methodLines.push(` service: "${serviceName}",`);
|
|
3258
|
+
methodLines.push(` member: "${m.name}",`);
|
|
3259
|
+
methodLines.push(` context: { interaction: "Emitter" }`);
|
|
3260
|
+
methodLines.push(` });`);
|
|
3261
|
+
methodLines.push(` return;`);
|
|
3262
|
+
methodLines.push(` }`);
|
|
3263
|
+
methodLines.push(` this._bridge.emit("${serviceName}", "${m.name}", encodedArgs);`);
|
|
3264
|
+
methodLines.push(` }`);
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
if (m.kind === "Slot") {
|
|
3268
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
3269
|
+
const decodedArgs = m.parameters.map((p, index) => `${paramSites[index] ? `decode${paramSites[index].codecId}(wireArgs[${index}])` : `wireArgs[${index}] as ${mapTypeTextToTs(p.typeText)}`}`).join(", ");
|
|
3270
|
+
onSlotMembers.push(` ${m.name}: (handler: (${args}) => ${slotHandlerReturnType(ret)}): void => {`);
|
|
3271
|
+
onSlotMembers.push(` this._bridge.registerSlot("${serviceName}", "${m.name}", (...wireArgs: unknown[]) => {`);
|
|
3272
|
+
onSlotMembers.push(` const result = handler(${decodedArgs});`);
|
|
3273
|
+
if (payloadSite) {
|
|
3274
|
+
onSlotMembers.push(` if (result instanceof Promise) return result.then((value) => value instanceof Error ? value : encode${payloadSite.codecId}(value));`);
|
|
3275
|
+
onSlotMembers.push(` return result instanceof Error ? result : encode${payloadSite.codecId}(result);`);
|
|
3211
3276
|
}
|
|
3212
|
-
else
|
|
3213
|
-
|
|
3277
|
+
else {
|
|
3278
|
+
onSlotMembers.push(" return result;");
|
|
3214
3279
|
}
|
|
3280
|
+
onSlotMembers.push(" });");
|
|
3281
|
+
onSlotMembers.push(" },");
|
|
3282
|
+
continue;
|
|
3215
3283
|
}
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
|
|
3242
|
-
return ` set${service.name}_${nodeCap(member.name)}(value: ${typeText}): void {
|
|
3243
|
-
this.setOutputValue("${service.name}", "${member.name}", ${payloadSite ? `encode${payloadSite.codecId}(value)` : "value"});
|
|
3244
|
-
}`;
|
|
3245
|
-
}))
|
|
3246
|
-
.join("\n");
|
|
3247
|
-
const sessionServiceInterfaces = spec.services
|
|
3248
|
-
.map((service) => {
|
|
3249
|
-
const slotLines = service.members
|
|
3250
|
-
.filter((member) => member.kind === "Slot")
|
|
3251
|
-
.map((member) => {
|
|
3252
|
-
const ret = mapTypeTextToTs(member.payloadTypeText ?? "void");
|
|
3253
|
-
const args = nodeParamArgs(member);
|
|
3254
|
-
return ` ${member.name}(${args}${args.length > 0 ? ", " : ""}timeoutMs?: number): Promise<${ret}>;`;
|
|
3255
|
-
});
|
|
3256
|
-
const signalMembers = service.members
|
|
3257
|
-
.filter((member) => member.kind === "Emitter")
|
|
3258
|
-
.map((member) => {
|
|
3259
|
-
const args = nodeParamArgs(member);
|
|
3260
|
-
return ` ${member.name}(handler: (${args}) => void): () => void;`;
|
|
3261
|
-
});
|
|
3262
|
-
const propertyMembers = service.members
|
|
3263
|
-
.filter((member) => (member.kind === "Input" || member.kind === "Output") && member.payloadTypeText)
|
|
3264
|
-
.map((member) => {
|
|
3265
|
-
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
3266
|
-
if (member.kind === "Input") {
|
|
3267
|
-
return ` ${member.name}: {\n get(): Promise<${typeText}>;\n on(handler: (value: ${typeText}) => void): () => void;\n };`;
|
|
3284
|
+
if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
|
|
3285
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
3286
|
+
fieldLines.push(` private readonly _${m.name} = createValueCell<${tsType} | undefined>(undefined);`);
|
|
3287
|
+
methodLines.push(` ${m.name}(): ${tsType} | undefined { return this._${m.name}.get(); }`);
|
|
3288
|
+
if (m.kind === "Input") {
|
|
3289
|
+
setMembers.push(` ${m.name}: (value: ${tsType}): void => {`);
|
|
3290
|
+
setMembers.push(` let encodedValue: unknown;`);
|
|
3291
|
+
setMembers.push(` try {`);
|
|
3292
|
+
setMembers.push(` encodedValue = ${payloadSite ? `encode${payloadSite.codecId}(value)` : "value"};`);
|
|
3293
|
+
setMembers.push(` } catch (error) {`);
|
|
3294
|
+
setMembers.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3295
|
+
setMembers.push(` code: "SerializationError",`);
|
|
3296
|
+
setMembers.push(` severity: "error",`);
|
|
3297
|
+
setMembers.push(` category: "bridge",`);
|
|
3298
|
+
setMembers.push(` recoverable: true,`);
|
|
3299
|
+
setMembers.push(` message: \`Failed to serialize Input ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3300
|
+
setMembers.push(` service: "${serviceName}",`);
|
|
3301
|
+
setMembers.push(` member: "${m.name}",`);
|
|
3302
|
+
setMembers.push(` context: { interaction: "Input" }`);
|
|
3303
|
+
setMembers.push(` });`);
|
|
3304
|
+
setMembers.push(` return;`);
|
|
3305
|
+
setMembers.push(` }`);
|
|
3306
|
+
setMembers.push(` this._${m.name}.set(value);`);
|
|
3307
|
+
setMembers.push(` this._bridge.setInput("${serviceName}", "${m.name}", encodedValue);`);
|
|
3308
|
+
setMembers.push(" },");
|
|
3268
3309
|
}
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
})
|
|
3273
|
-
.join("\n\n");
|
|
3274
|
-
const widgetServiceFields = spec.services.map((service) => ` ${service.name}: ${service.name}SessionBridgeService;`).join("\n");
|
|
3275
|
-
const sessionBridgeFactory = spec.services
|
|
3276
|
-
.map((service) => {
|
|
3277
|
-
const slotMembers = service.members
|
|
3278
|
-
.filter((member) => member.kind === "Slot")
|
|
3279
|
-
.map((member) => {
|
|
3280
|
-
const args = member.parameters.map((p) => p.name).join(", ");
|
|
3281
|
-
const typedArgs = nodeParamArgs(member);
|
|
3282
|
-
return ` ${member.name}: (${typedArgs}${typedArgs.length > 0 ? ", " : ""}timeoutMs = defaultSlotTimeoutMs) => session.${service.name}_${member.name}(${args}${args.length > 0 ? ", " : ""}timeoutMs),`;
|
|
3283
|
-
})
|
|
3284
|
-
.join("\n");
|
|
3285
|
-
const signalMembers = service.members
|
|
3286
|
-
.filter((member) => member.kind === "Emitter")
|
|
3287
|
-
.map((member) => {
|
|
3288
|
-
const args = nodeParamArgs(member);
|
|
3289
|
-
return ` ${member.name}: (handler: (${args}) => void) => session.onSignal("${service.name}", "${member.name}", handler as (...args: unknown[]) => void),`;
|
|
3290
|
-
})
|
|
3291
|
-
.join("\n");
|
|
3292
|
-
const propertyMembers = service.members
|
|
3293
|
-
.filter((member) => (member.kind === "Input" || member.kind === "Output") && member.payloadTypeText)
|
|
3294
|
-
.map((member) => {
|
|
3295
|
-
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
3296
|
-
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
|
|
3297
|
-
if (member.kind === "Input") {
|
|
3298
|
-
return ` ${member.name}: {\n get: () => session.readInput("${service.name}", "${member.name}").then((value) => ${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${typeText}`}),\n on: (handler: (value: ${typeText}) => void) => session.onInput("${service.name}", "${member.name}", (value) => handler(${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${typeText}`}))\n },`;
|
|
3310
|
+
if (m.kind === "Output") {
|
|
3311
|
+
constructorBodyLines.push(` this._bridge.onOutput("${serviceName}", "${m.name}", (value) => {`);
|
|
3312
|
+
constructorBodyLines.push(` this._${m.name}.set(${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${tsType}`});`);
|
|
3313
|
+
constructorBodyLines.push(` });`);
|
|
3299
3314
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
.
|
|
3303
|
-
|
|
3315
|
+
}
|
|
3316
|
+
if (m.kind === "DropTarget" && m.payloadTypeText) {
|
|
3317
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
3318
|
+
const typeName = m.payloadTypeText.replace(/\s/g, "");
|
|
3319
|
+
fieldLines.push(` private readonly _${m.name} = createValueCell<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
|
|
3320
|
+
methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}.get(); }`);
|
|
3321
|
+
constructorBodyLines.push(` this._bridge.onDrop("${serviceName}", "${m.name}", (payload, x, y) => {`);
|
|
3322
|
+
constructorBodyLines.push(` try {`);
|
|
3323
|
+
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decodeDragDropPayload_${typeName}(payload)` : `payload as ${tsType}`}, x, y });`);
|
|
3324
|
+
constructorBodyLines.push(` } catch (error) {`);
|
|
3325
|
+
constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3326
|
+
constructorBodyLines.push(` code: "DeserializationError",`);
|
|
3327
|
+
constructorBodyLines.push(` severity: "error",`);
|
|
3328
|
+
constructorBodyLines.push(` category: "bridge",`);
|
|
3329
|
+
constructorBodyLines.push(` recoverable: true,`);
|
|
3330
|
+
constructorBodyLines.push(` message: \`Failed to deserialize DropTarget ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3331
|
+
constructorBodyLines.push(` service: "${serviceName}",`);
|
|
3332
|
+
constructorBodyLines.push(` member: "${m.name}",`);
|
|
3333
|
+
constructorBodyLines.push(` context: { interaction: "DropTarget" }`);
|
|
3334
|
+
constructorBodyLines.push(` });`);
|
|
3335
|
+
constructorBodyLines.push(` }`);
|
|
3336
|
+
constructorBodyLines.push(` });`);
|
|
3337
|
+
}
|
|
3338
|
+
if (m.kind === "HoverTarget" && m.payloadTypeText) {
|
|
3339
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
3340
|
+
const typeName = m.payloadTypeText.replace(/\s/g, "");
|
|
3341
|
+
fieldLines.push(` private readonly _${m.name} = createValueCell<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
|
|
3342
|
+
methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}.get(); }`);
|
|
3343
|
+
constructorBodyLines.push(` this._bridge.onHover("${serviceName}", "${m.name}", (payload, x, y) => {`);
|
|
3344
|
+
constructorBodyLines.push(` try {`);
|
|
3345
|
+
constructorBodyLines.push(` this._${m.name}.set({ payload: ${payloadSite ? `decodeDragDropPayload_${typeName}(payload)` : `payload as ${tsType}`}, x, y });`);
|
|
3346
|
+
constructorBodyLines.push(` } catch (error) {`);
|
|
3347
|
+
constructorBodyLines.push(` this._bridge.reportFrontendDiagnostic({`);
|
|
3348
|
+
constructorBodyLines.push(` code: "DeserializationError",`);
|
|
3349
|
+
constructorBodyLines.push(` severity: "error",`);
|
|
3350
|
+
constructorBodyLines.push(` category: "bridge",`);
|
|
3351
|
+
constructorBodyLines.push(` recoverable: true,`);
|
|
3352
|
+
constructorBodyLines.push(` message: \`Failed to deserialize HoverTarget ${serviceName}.${m.name}: \${errorMessage(error)}\`,`);
|
|
3353
|
+
constructorBodyLines.push(` service: "${serviceName}",`);
|
|
3354
|
+
constructorBodyLines.push(` member: "${m.name}",`);
|
|
3355
|
+
constructorBodyLines.push(` context: { interaction: "HoverTarget" }`);
|
|
3356
|
+
constructorBodyLines.push(` });`);
|
|
3357
|
+
constructorBodyLines.push(` }`);
|
|
3358
|
+
constructorBodyLines.push(` });`);
|
|
3359
|
+
constructorBodyLines.push(` this._bridge.onHoverLeft("${serviceName}", "${m.name}", () => this._${m.name}.set(null));`);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
const constructorLines = [
|
|
3363
|
+
" constructor(private readonly _bridge: AnQstBridgeRuntime) {",
|
|
3364
|
+
...constructorBodyLines,
|
|
3365
|
+
" }",
|
|
3366
|
+
];
|
|
3367
|
+
return `class ${serviceName} {
|
|
3368
|
+
${fieldLines.join("\n")}
|
|
3369
|
+
${constructorLines.join("\n")}${formatTsServiceSetAndOnSlotObjectLiterals(setMembers, onSlotMembers)}${methodLines.length > 0 ? `\n${methodLines.join("\n")}` : ""}
|
|
3370
|
+
}
|
|
3371
|
+
`;
|
|
3372
|
+
}
|
|
3373
|
+
function renderVanillaBrowserTs(spec, codecCatalog) {
|
|
3374
|
+
const localTypeDecls = renderTypeDeclarations(spec).trim();
|
|
3375
|
+
const localTypesBlock = localTypeDecls.length > 0 ? `${localTypeDecls}\n\n` : "";
|
|
3376
|
+
const dragDropHelperBlock = renderTsDragDropPayloadHelpers(spec, codecCatalog).trim();
|
|
3377
|
+
const dragDropHelpers = dragDropHelperBlock.length > 0 ? `\n// Drag/drop payload helpers\n${dragDropHelperBlock}\n` : "";
|
|
3378
|
+
const serviceClasses = spec.services.map((s) => renderVanillaServiceTs(spec, s.name, codecCatalog)).join("\n");
|
|
3379
|
+
const frontendServices = spec.services.length > 0
|
|
3380
|
+
? spec.services.map((s) => ` ${s.name}: ${s.name};`).join("\n")
|
|
3381
|
+
: "";
|
|
3382
|
+
const frontendServiceFactories = spec.services.length > 0
|
|
3383
|
+
? spec.services.map((s) => ` ${s.name}: new ${s.name}(bridge)`).join(",\n")
|
|
3384
|
+
: "";
|
|
3385
|
+
return `${localTypesBlock}// Boundary codec plan helpers
|
|
3386
|
+
${(0, boundary_codecs_1.renderTsBoundaryCodecHelpers)(codecCatalog)}
|
|
3387
|
+
${dragDropHelpers}
|
|
3388
|
+
|
|
3389
|
+
type SlotHandler = (...args: unknown[]) => unknown;
|
|
3390
|
+
type OutputHandler = (value: unknown) => void;
|
|
3391
|
+
type SlotInvocationListener = (requestId: string, service: string, member: string, args: unknown[]) => void;
|
|
3392
|
+
type OutputListener = (service: string, member: string, value: unknown) => void;
|
|
3393
|
+
type DropListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
|
|
3394
|
+
type HoverListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
|
|
3395
|
+
type HoverLeftListener = (service: string, member: string) => void;
|
|
3396
|
+
type HostDiagnosticListener = (payload: unknown) => void;
|
|
3397
|
+
type DisconnectListener = () => void;
|
|
3398
|
+
|
|
3399
|
+
type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
|
|
3400
|
+
type AnQstBridgeSource = "frontend" | "host";
|
|
3401
|
+
type AnQstBridgeTransport = "qt-webchannel" | "dev-websocket";
|
|
3402
|
+
type AnQstBridgeState = "starting" | "ready" | "failed" | "disconnected";
|
|
3403
|
+
|
|
3404
|
+
interface AnQstBridgeDiagnostic {
|
|
3405
|
+
code: string;
|
|
3406
|
+
severity: AnQstBridgeSeverity;
|
|
3407
|
+
category: string;
|
|
3408
|
+
recoverable: boolean;
|
|
3409
|
+
message: string;
|
|
3410
|
+
timestamp: string;
|
|
3411
|
+
source: AnQstBridgeSource;
|
|
3412
|
+
transport?: AnQstBridgeTransport;
|
|
3413
|
+
service?: string;
|
|
3414
|
+
member?: string;
|
|
3415
|
+
requestId?: string;
|
|
3416
|
+
context?: Record<string, unknown>;
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
interface HostBridgeApi {
|
|
3420
|
+
anQstBridge_call(service: string, member: string, args: unknown[], callback: (result: unknown) => void): void;
|
|
3421
|
+
anQstBridge_emit(service: string, member: string, args: unknown[]): void;
|
|
3422
|
+
anQstBridge_setInput(service: string, member: string, value: unknown): void;
|
|
3423
|
+
anQstBridge_registerSlot(service: string, member: string): void;
|
|
3424
|
+
anQstBridge_resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
|
|
3425
|
+
anQstBridge_outputUpdated: { connect: (cb: (service: string, member: string, value: unknown) => void) => void };
|
|
3426
|
+
anQstBridge_slotInvocationRequested: {
|
|
3427
|
+
connect: (cb: (requestId: string, service: string, member: string, args: unknown[]) => void) => void;
|
|
3428
|
+
};
|
|
3429
|
+
anQstBridge_hostDiagnostic?: { connect: (cb: (payload: unknown) => void) => void };
|
|
3430
|
+
anQstBridge_dropReceived: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
|
|
3431
|
+
anQstBridge_hoverUpdated: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
|
|
3432
|
+
anQstBridge_hoverLeft: { connect: (cb: (service: string, member: string) => void) => void };
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
interface QWebChannelCtor {
|
|
3436
|
+
new (
|
|
3437
|
+
transport: unknown,
|
|
3438
|
+
initCallback: (channel: { objects: Record<string, HostBridgeApi | undefined> }) => void
|
|
3439
|
+
): unknown;
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
interface BridgeAdapter {
|
|
3443
|
+
readonly transport: AnQstBridgeTransport;
|
|
3444
|
+
call<T>(service: string, member: string, args: unknown[]): Promise<T>;
|
|
3445
|
+
emit(service: string, member: string, args: unknown[]): void;
|
|
3446
|
+
setInput(service: string, member: string, value: unknown): void;
|
|
3447
|
+
registerSlot(service: string, member: string): void;
|
|
3448
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
|
|
3449
|
+
onOutput(handler: OutputListener): void;
|
|
3450
|
+
onSlotInvocation(handler: SlotInvocationListener): void;
|
|
3451
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void;
|
|
3452
|
+
onDisconnected(handler: DisconnectListener): void;
|
|
3453
|
+
onDrop(handler: DropListener): void;
|
|
3454
|
+
onHover(handler: HoverListener): void;
|
|
3455
|
+
onHoverLeft(handler: HoverLeftListener): void;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
interface ValueCell<T> {
|
|
3459
|
+
get(): T;
|
|
3460
|
+
set(value: T): void;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
function createValueCell<T>(initial: T): ValueCell<T> {
|
|
3464
|
+
let current = initial;
|
|
3465
|
+
return {
|
|
3466
|
+
get(): T {
|
|
3467
|
+
return current;
|
|
3468
|
+
},
|
|
3469
|
+
set(value: T): void {
|
|
3470
|
+
current = value;
|
|
3471
|
+
}
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
function errorMessage(error: unknown): string {
|
|
3476
|
+
if (error instanceof Error && typeof error.message === "string" && error.message.length > 0) {
|
|
3477
|
+
return error.message;
|
|
3478
|
+
}
|
|
3479
|
+
return String(error);
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
function normalizeSeverity(value: unknown): AnQstBridgeSeverity {
|
|
3483
|
+
if (value === "info" || value === "warn" || value === "error" || value === "fatal") {
|
|
3484
|
+
return value;
|
|
3485
|
+
}
|
|
3486
|
+
return "error";
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
3490
|
+
if (value === null || typeof value !== "object") {
|
|
3491
|
+
return undefined;
|
|
3492
|
+
}
|
|
3493
|
+
return value as Record<string, unknown>;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
function readString(record: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
3497
|
+
const value = record?.[key];
|
|
3498
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
function readBoolean(record: Record<string, unknown> | undefined, key: string): boolean | undefined {
|
|
3502
|
+
const value = record?.[key];
|
|
3503
|
+
return typeof value === "boolean" ? value : undefined;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
function readContext(record: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
3507
|
+
const context = asRecord(record?.["context"]);
|
|
3508
|
+
return context === undefined ? undefined : context;
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
function normalizeHostDiagnostic(payload: unknown, transport: AnQstBridgeTransport): Omit<AnQstBridgeDiagnostic, "timestamp"> {
|
|
3512
|
+
const row = asRecord(payload);
|
|
3513
|
+
if (row === undefined) {
|
|
3514
|
+
return {
|
|
3515
|
+
code: "HostDiagnosticMalformed",
|
|
3516
|
+
severity: "error",
|
|
3517
|
+
category: "bridge",
|
|
3518
|
+
recoverable: true,
|
|
3519
|
+
message: "Host emitted a malformed diagnostic payload.",
|
|
3520
|
+
source: "host",
|
|
3521
|
+
transport
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
const context = readContext(row);
|
|
3526
|
+
return {
|
|
3527
|
+
code: readString(row, "code") ?? "HostDiagnostic",
|
|
3528
|
+
severity: normalizeSeverity(row["severity"]),
|
|
3529
|
+
category: readString(row, "category") ?? "bridge",
|
|
3530
|
+
recoverable: readBoolean(row, "recoverable") ?? true,
|
|
3531
|
+
message: readString(row, "message") ?? "Host emitted a diagnostic payload.",
|
|
3532
|
+
source: "host",
|
|
3533
|
+
transport,
|
|
3534
|
+
service: readString(row, "service") ?? readString(context, "service"),
|
|
3535
|
+
member: readString(row, "member") ?? readString(context, "member"),
|
|
3536
|
+
requestId: readString(row, "requestId") ?? readString(context, "requestId"),
|
|
3537
|
+
context
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
function isBridgeCallError(value: unknown): value is {
|
|
3542
|
+
code: unknown;
|
|
3543
|
+
message: unknown;
|
|
3544
|
+
service: unknown;
|
|
3545
|
+
member: unknown;
|
|
3546
|
+
requestId: unknown;
|
|
3547
|
+
} {
|
|
3548
|
+
if (value === null || typeof value !== "object") return false;
|
|
3549
|
+
const row = value as Record<string, unknown>;
|
|
3550
|
+
return (
|
|
3551
|
+
Object.prototype.hasOwnProperty.call(row, "code")
|
|
3552
|
+
&& Object.prototype.hasOwnProperty.call(row, "message")
|
|
3553
|
+
&& Object.prototype.hasOwnProperty.call(row, "service")
|
|
3554
|
+
&& Object.prototype.hasOwnProperty.call(row, "member")
|
|
3555
|
+
&& Object.prototype.hasOwnProperty.call(row, "requestId")
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
class QtWebChannelAdapter implements BridgeAdapter {
|
|
3560
|
+
readonly transport = "qt-webchannel" as const;
|
|
3561
|
+
|
|
3562
|
+
private constructor(private readonly host: HostBridgeApi) {}
|
|
3563
|
+
|
|
3564
|
+
static async create(): Promise<QtWebChannelAdapter> {
|
|
3565
|
+
const anyWindow = window as unknown as {
|
|
3566
|
+
qt?: { webChannelTransport?: unknown };
|
|
3567
|
+
QWebChannel?: QWebChannelCtor;
|
|
3568
|
+
};
|
|
3569
|
+
if (typeof anyWindow.QWebChannel !== "function" || anyWindow.qt?.webChannelTransport === undefined) {
|
|
3570
|
+
throw new Error("Qt WebChannel transport is unavailable.");
|
|
3571
|
+
}
|
|
3572
|
+
return await new Promise<QtWebChannelAdapter>((resolve, reject) => {
|
|
3573
|
+
try {
|
|
3574
|
+
const QWebChannel = anyWindow.QWebChannel as QWebChannelCtor;
|
|
3575
|
+
new QWebChannel(anyWindow.qt!.webChannelTransport, (channel) => {
|
|
3576
|
+
try {
|
|
3577
|
+
const host = channel.objects["${spec.widgetName}Bridge"];
|
|
3578
|
+
if (host === undefined) {
|
|
3579
|
+
reject(new Error("${spec.widgetName}Bridge bridge object is unavailable."));
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
resolve(new QtWebChannelAdapter(host));
|
|
3583
|
+
} catch (error) {
|
|
3584
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
3585
|
+
}
|
|
3586
|
+
});
|
|
3587
|
+
} catch (error) {
|
|
3588
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
3589
|
+
}
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
3594
|
+
return new Promise<T>((resolve, reject) => {
|
|
3595
|
+
this.host.anQstBridge_call(service, member, args, (result) => {
|
|
3596
|
+
if (isBridgeCallError(result)) {
|
|
3597
|
+
reject(result);
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
resolve(result as T);
|
|
3601
|
+
});
|
|
3602
|
+
});
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
3606
|
+
this.host.anQstBridge_emit(service, member, args);
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
3610
|
+
this.host.anQstBridge_setInput(service, member, value);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
registerSlot(service: string, member: string): void {
|
|
3614
|
+
this.host.anQstBridge_registerSlot(service, member);
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void {
|
|
3618
|
+
this.host.anQstBridge_resolveSlot(requestId, ok, payload, error);
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
onOutput(handler: OutputListener): void {
|
|
3622
|
+
this.host.anQstBridge_outputUpdated.connect(handler);
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
onSlotInvocation(handler: SlotInvocationListener): void {
|
|
3626
|
+
this.host.anQstBridge_slotInvocationRequested.connect(handler);
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void {
|
|
3630
|
+
this.host.anQstBridge_hostDiagnostic?.connect(handler);
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
onDisconnected(_handler: DisconnectListener): void {
|
|
3634
|
+
// QWebChannel does not expose a deterministic disconnect event here.
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
onDrop(handler: DropListener): void {
|
|
3638
|
+
this.host.anQstBridge_dropReceived.connect(handler);
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
onHover(handler: HoverListener): void {
|
|
3642
|
+
this.host.anQstBridge_hoverUpdated.connect(handler);
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
onHoverLeft(handler: HoverLeftListener): void {
|
|
3646
|
+
this.host.anQstBridge_hoverLeft.connect(handler);
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
class WebSocketBridgeAdapter implements BridgeAdapter {
|
|
3651
|
+
readonly transport = "dev-websocket" as const;
|
|
3652
|
+
private readonly pending = new Map<string, {
|
|
3653
|
+
service: string;
|
|
3654
|
+
member: string;
|
|
3655
|
+
requestId: string;
|
|
3656
|
+
resolve: (result: unknown) => void;
|
|
3657
|
+
reject: (error: unknown) => void;
|
|
3658
|
+
}>();
|
|
3659
|
+
private readonly outputListeners: OutputListener[] = [];
|
|
3660
|
+
private readonly slotListeners: SlotInvocationListener[] = [];
|
|
3661
|
+
private readonly hostDiagnosticListeners: HostDiagnosticListener[] = [];
|
|
3662
|
+
private readonly disconnectListeners: DisconnectListener[] = [];
|
|
3663
|
+
private readonly dropListeners: DropListener[] = [];
|
|
3664
|
+
private readonly hoverListeners: HoverListener[] = [];
|
|
3665
|
+
private readonly hoverLeftListeners: HoverLeftListener[] = [];
|
|
3666
|
+
private requestCounter = 0;
|
|
3667
|
+
|
|
3668
|
+
private constructor(private readonly socket: WebSocket) {
|
|
3669
|
+
this.socket.addEventListener("message", (event) => {
|
|
3670
|
+
const raw = typeof event.data === "string" ? event.data : String(event.data);
|
|
3671
|
+
const message = JSON.parse(raw) as Record<string, unknown>;
|
|
3672
|
+
const type = String(message["type"] ?? "");
|
|
3673
|
+
if (type === "callResult") {
|
|
3674
|
+
const requestId = String(message["requestId"] ?? "");
|
|
3675
|
+
const pending = this.pending.get(requestId);
|
|
3676
|
+
if (pending) {
|
|
3677
|
+
this.pending.delete(requestId);
|
|
3678
|
+
const result = message["result"];
|
|
3679
|
+
if (isBridgeCallError(result)) {
|
|
3680
|
+
pending.reject(result);
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
3683
|
+
pending.resolve(result);
|
|
3684
|
+
}
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
if (type === "outputUpdated") {
|
|
3688
|
+
const service = String(message["service"] ?? "");
|
|
3689
|
+
const member = String(message["member"] ?? "");
|
|
3690
|
+
for (const listener of this.outputListeners) {
|
|
3691
|
+
listener(service, member, message["value"]);
|
|
3692
|
+
}
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
if (type === "slotInvocationRequested") {
|
|
3696
|
+
const requestId = String(message["requestId"] ?? "");
|
|
3697
|
+
const service = String(message["service"] ?? "");
|
|
3698
|
+
const member = String(message["member"] ?? "");
|
|
3699
|
+
const args = Array.isArray(message["args"]) ? (message["args"] as unknown[]) : [];
|
|
3700
|
+
for (const listener of this.slotListeners) {
|
|
3701
|
+
listener(requestId, service, member, args);
|
|
3702
|
+
}
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
if (type === "dropReceived") {
|
|
3706
|
+
const service = String(message["service"] ?? "");
|
|
3707
|
+
const member = String(message["member"] ?? "");
|
|
3708
|
+
const x = Number(message["x"] ?? 0);
|
|
3709
|
+
const y = Number(message["y"] ?? 0);
|
|
3710
|
+
for (const listener of this.dropListeners) {
|
|
3711
|
+
listener(service, member, message["payload"], x, y);
|
|
3712
|
+
}
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
if (type === "hoverUpdated") {
|
|
3716
|
+
const service = String(message["service"] ?? "");
|
|
3717
|
+
const member = String(message["member"] ?? "");
|
|
3718
|
+
const x = Number(message["x"] ?? 0);
|
|
3719
|
+
const y = Number(message["y"] ?? 0);
|
|
3720
|
+
for (const listener of this.hoverListeners) {
|
|
3721
|
+
listener(service, member, message["payload"], x, y);
|
|
3722
|
+
}
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
if (type === "hoverLeft") {
|
|
3726
|
+
const service = String(message["service"] ?? "");
|
|
3727
|
+
const member = String(message["member"] ?? "");
|
|
3728
|
+
for (const listener of this.hoverLeftListeners) {
|
|
3729
|
+
listener(service, member);
|
|
3730
|
+
}
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
if (type === "hostError") {
|
|
3734
|
+
for (const listener of this.hostDiagnosticListeners) {
|
|
3735
|
+
listener(message["payload"]);
|
|
3736
|
+
}
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
if (type === "widgetReattached") {
|
|
3740
|
+
document.body.textContent = "Widget Reattached";
|
|
3741
|
+
this.socket.close();
|
|
3742
|
+
}
|
|
3743
|
+
});
|
|
3744
|
+
this.socket.addEventListener("close", () => {
|
|
3745
|
+
for (const pending of this.pending.values()) {
|
|
3746
|
+
pending.reject({
|
|
3747
|
+
code: "BridgeDisconnectedError",
|
|
3748
|
+
message: "Bridge disconnected before call completion.",
|
|
3749
|
+
service: pending.service,
|
|
3750
|
+
member: pending.member,
|
|
3751
|
+
requestId: pending.requestId
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
this.pending.clear();
|
|
3755
|
+
for (const listener of this.disconnectListeners) {
|
|
3756
|
+
listener();
|
|
3757
|
+
}
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
static async create(): Promise<WebSocketBridgeAdapter> {
|
|
3762
|
+
const configResponse = await fetch("/anqst-dev-config.json", { cache: "no-store" });
|
|
3763
|
+
if (!configResponse.ok) {
|
|
3764
|
+
throw new Error("AnQst host bootstrap missing: unable to read /anqst-dev-config.json");
|
|
3765
|
+
}
|
|
3766
|
+
const config = (await configResponse.json()) as { wsUrl?: string; wsPath?: string };
|
|
3767
|
+
let wsUrl = config.wsUrl;
|
|
3768
|
+
if (!wsUrl && config.wsPath) {
|
|
3769
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
3770
|
+
wsUrl = protocol + "//" + window.location.host + config.wsPath;
|
|
3771
|
+
}
|
|
3772
|
+
if (!wsUrl) {
|
|
3773
|
+
throw new Error("AnQst host bootstrap missing: wsUrl/wsPath is unavailable.");
|
|
3774
|
+
}
|
|
3775
|
+
if (wsUrl.startsWith("http://")) {
|
|
3776
|
+
wsUrl = "ws://" + wsUrl.slice("http://".length);
|
|
3777
|
+
} else if (wsUrl.startsWith("https://")) {
|
|
3778
|
+
wsUrl = "wss://" + wsUrl.slice("https://".length);
|
|
3779
|
+
}
|
|
3780
|
+
return await new Promise<WebSocketBridgeAdapter>((resolve, reject) => {
|
|
3781
|
+
const socket = new WebSocket(wsUrl!);
|
|
3782
|
+
socket.addEventListener("open", () => resolve(new WebSocketBridgeAdapter(socket)));
|
|
3783
|
+
socket.addEventListener("error", () => reject(new Error("Failed to connect to AnQst WebSocket bridge.")));
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
3788
|
+
const requestId = \`req-\${++this.requestCounter}\`;
|
|
3789
|
+
const payload = { type: "call", requestId, service, member, args };
|
|
3790
|
+
return await new Promise<T>((resolve, reject) => {
|
|
3791
|
+
this.pending.set(requestId, {
|
|
3792
|
+
service,
|
|
3793
|
+
member,
|
|
3794
|
+
requestId,
|
|
3795
|
+
resolve: (value) => resolve(value as T),
|
|
3796
|
+
reject
|
|
3797
|
+
});
|
|
3798
|
+
this.socket.send(JSON.stringify(payload));
|
|
3799
|
+
});
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
3803
|
+
this.socket.send(JSON.stringify({ type: "emit", service, member, args }));
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
3807
|
+
this.socket.send(JSON.stringify({ type: "setInput", service, member, value }));
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
registerSlot(service: string, member: string): void {
|
|
3811
|
+
this.socket.send(JSON.stringify({ type: "registerSlot", service, member }));
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void {
|
|
3815
|
+
this.socket.send(JSON.stringify({ type: "resolveSlot", requestId, ok, payload, error }));
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
onOutput(handler: OutputListener): void {
|
|
3819
|
+
this.outputListeners.push(handler);
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
onSlotInvocation(handler: SlotInvocationListener): void {
|
|
3823
|
+
this.slotListeners.push(handler);
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
onHostDiagnostic(handler: HostDiagnosticListener): void {
|
|
3827
|
+
this.hostDiagnosticListeners.push(handler);
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
onDisconnected(handler: DisconnectListener): void {
|
|
3831
|
+
this.disconnectListeners.push(handler);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
onDrop(handler: DropListener): void {
|
|
3835
|
+
this.dropListeners.push(handler);
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
onHover(handler: HoverListener): void {
|
|
3839
|
+
this.hoverListeners.push(handler);
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
onHoverLeft(handler: HoverLeftListener): void {
|
|
3843
|
+
this.hoverLeftListeners.push(handler);
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
class AnQstBridgeRuntime {
|
|
3848
|
+
private static readonly maxDiagnostics = 50;
|
|
3849
|
+
private adapter: BridgeAdapter | null = null;
|
|
3850
|
+
private readonly slotHandlers = new Map<string, SlotHandler>();
|
|
3851
|
+
private readonly outputHandlers = new Map<string, OutputHandler[]>();
|
|
3852
|
+
private readonly dropHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
|
|
3853
|
+
private readonly hoverHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
|
|
3854
|
+
private readonly hoverLeftHandlers = new Map<string, (() => void)[]>();
|
|
3855
|
+
private readonly diagnosticListeners = new Set<(diagnostic: AnQstBridgeDiagnostic) => void>();
|
|
3856
|
+
private readonly _diagnostics = createValueCell<readonly AnQstBridgeDiagnostic[]>([]);
|
|
3857
|
+
private readonly _state = createValueCell<AnQstBridgeState>("starting");
|
|
3858
|
+
private readonly startup = this.init().catch((error) => {
|
|
3859
|
+
this._state.set("failed");
|
|
3860
|
+
this.reportFrontendDiagnostic({
|
|
3861
|
+
code: "BridgeBootstrapError",
|
|
3862
|
+
severity: "fatal",
|
|
3863
|
+
category: "bridge",
|
|
3864
|
+
recoverable: false,
|
|
3865
|
+
message: \`Failed to initialize bridge: \${errorMessage(error)}\`
|
|
3866
|
+
});
|
|
3867
|
+
throw error;
|
|
3868
|
+
});
|
|
3869
|
+
|
|
3870
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[] {
|
|
3871
|
+
return this._diagnostics.get();
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
state(): AnQstBridgeState {
|
|
3875
|
+
return this._state.get();
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
subscribeDiagnostics(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void {
|
|
3879
|
+
this.diagnosticListeners.add(listener);
|
|
3880
|
+
return () => this.diagnosticListeners.delete(listener);
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
async ready(): Promise<void> {
|
|
3884
|
+
return this.startup;
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
reportFrontendDiagnostic(diagnostic: Omit<AnQstBridgeDiagnostic, "timestamp" | "source">): void {
|
|
3888
|
+
this.pushDiagnostic({
|
|
3889
|
+
...diagnostic,
|
|
3890
|
+
source: "frontend",
|
|
3891
|
+
transport: diagnostic.transport ?? this.adapter?.transport,
|
|
3892
|
+
timestamp: new Date().toISOString()
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
|
|
3897
|
+
const adapter = await this.requireAdapter();
|
|
3898
|
+
return adapter.call<T>(service, member, args);
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
emit(service: string, member: string, args: unknown[]): void {
|
|
3902
|
+
this.publishNonCall("Emitter", service, member, (adapter) => adapter.emit(service, member, args));
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
setInput(service: string, member: string, value: unknown): void {
|
|
3906
|
+
this.publishNonCall("Input", service, member, (adapter) => adapter.setInput(service, member, value));
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
registerSlot(service: string, member: string, handler: SlotHandler): void {
|
|
3910
|
+
const key = this.key(service, member);
|
|
3911
|
+
this.slotHandlers.set(key, handler);
|
|
3912
|
+
if (this.adapter !== null) {
|
|
3913
|
+
try {
|
|
3914
|
+
this.adapter.registerSlot(service, member);
|
|
3915
|
+
} catch (error) {
|
|
3916
|
+
this.reportFrontendDiagnostic({
|
|
3917
|
+
code: "BridgePublishError",
|
|
3918
|
+
severity: "error",
|
|
3919
|
+
category: "bridge",
|
|
3920
|
+
recoverable: true,
|
|
3921
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
3922
|
+
service,
|
|
3923
|
+
member,
|
|
3924
|
+
context: { interaction: "Slot" }
|
|
3925
|
+
});
|
|
3926
|
+
}
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
this.ready()
|
|
3930
|
+
.then(() => {
|
|
3931
|
+
try {
|
|
3932
|
+
this.requireAdapterSync().registerSlot(service, member);
|
|
3933
|
+
} catch (error) {
|
|
3934
|
+
this.reportFrontendDiagnostic({
|
|
3935
|
+
code: "BridgePublishError",
|
|
3936
|
+
severity: "error",
|
|
3937
|
+
category: "bridge",
|
|
3938
|
+
recoverable: true,
|
|
3939
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
3940
|
+
service,
|
|
3941
|
+
member,
|
|
3942
|
+
context: { interaction: "Slot" }
|
|
3943
|
+
});
|
|
3944
|
+
}
|
|
3945
|
+
})
|
|
3946
|
+
.catch((error) => {
|
|
3947
|
+
this.reportFrontendDiagnostic({
|
|
3948
|
+
code: "BridgePublishError",
|
|
3949
|
+
severity: "error",
|
|
3950
|
+
category: "bridge",
|
|
3951
|
+
recoverable: true,
|
|
3952
|
+
message: \`Failed to register Slot \${service}.\${member}: \${errorMessage(error)}\`,
|
|
3953
|
+
service,
|
|
3954
|
+
member,
|
|
3955
|
+
context: { interaction: "Slot" }
|
|
3956
|
+
});
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
onOutput(service: string, member: string, handler: OutputHandler): void {
|
|
3961
|
+
const key = this.key(service, member);
|
|
3962
|
+
const existing = this.outputHandlers.get(key) ?? [];
|
|
3963
|
+
existing.push(handler);
|
|
3964
|
+
this.outputHandlers.set(key, existing);
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
onDrop(service: string, member: string, handler: (payload: unknown, x: number, y: number) => void): void {
|
|
3968
|
+
const key = this.key(service, member);
|
|
3969
|
+
const existing = this.dropHandlers.get(key) ?? [];
|
|
3970
|
+
existing.push(handler);
|
|
3971
|
+
this.dropHandlers.set(key, existing);
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
onHover(service: string, member: string, handler: (payload: unknown, x: number, y: number) => void): void {
|
|
3975
|
+
const key = this.key(service, member);
|
|
3976
|
+
const existing = this.hoverHandlers.get(key) ?? [];
|
|
3977
|
+
existing.push(handler);
|
|
3978
|
+
this.hoverHandlers.set(key, existing);
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
onHoverLeft(service: string, member: string, handler: () => void): void {
|
|
3982
|
+
const key = this.key(service, member);
|
|
3983
|
+
const existing = this.hoverLeftHandlers.get(key) ?? [];
|
|
3984
|
+
existing.push(handler);
|
|
3985
|
+
this.hoverLeftHandlers.set(key, existing);
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
private requireAdapterSync(): BridgeAdapter {
|
|
3989
|
+
if (this.adapter === null) {
|
|
3990
|
+
throw new Error("AnQst bridge is not ready.");
|
|
3991
|
+
}
|
|
3992
|
+
return this.adapter;
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
private async requireAdapter(): Promise<BridgeAdapter> {
|
|
3996
|
+
await this.startup;
|
|
3997
|
+
return this.requireAdapterSync();
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
private pushDiagnostic(diagnostic: AnQstBridgeDiagnostic): void {
|
|
4001
|
+
const previous = this._diagnostics.get();
|
|
4002
|
+
const trimmed = previous.length >= AnQstBridgeRuntime.maxDiagnostics
|
|
4003
|
+
? previous.slice(previous.length - (AnQstBridgeRuntime.maxDiagnostics - 1))
|
|
4004
|
+
: previous;
|
|
4005
|
+
const next = [...trimmed, diagnostic];
|
|
4006
|
+
this._diagnostics.set(next);
|
|
4007
|
+
for (const listener of this.diagnosticListeners) {
|
|
4008
|
+
listener(diagnostic);
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
private publishNonCall(
|
|
4013
|
+
interaction: "Emitter" | "Input",
|
|
4014
|
+
service: string,
|
|
4015
|
+
member: string,
|
|
4016
|
+
publish: (adapter: BridgeAdapter) => void
|
|
4017
|
+
): void {
|
|
4018
|
+
if (this.adapter !== null) {
|
|
4019
|
+
try {
|
|
4020
|
+
publish(this.adapter);
|
|
4021
|
+
} catch (error) {
|
|
4022
|
+
this.reportFrontendDiagnostic({
|
|
4023
|
+
code: "BridgePublishError",
|
|
4024
|
+
severity: "error",
|
|
4025
|
+
category: "bridge",
|
|
4026
|
+
recoverable: true,
|
|
4027
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4028
|
+
service,
|
|
4029
|
+
member,
|
|
4030
|
+
context: { interaction }
|
|
4031
|
+
});
|
|
4032
|
+
}
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
this.ready()
|
|
4037
|
+
.then(() => {
|
|
4038
|
+
try {
|
|
4039
|
+
publish(this.requireAdapterSync());
|
|
4040
|
+
} catch (error) {
|
|
4041
|
+
this.reportFrontendDiagnostic({
|
|
4042
|
+
code: "BridgePublishError",
|
|
4043
|
+
severity: "error",
|
|
4044
|
+
category: "bridge",
|
|
4045
|
+
recoverable: true,
|
|
4046
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4047
|
+
service,
|
|
4048
|
+
member,
|
|
4049
|
+
context: { interaction }
|
|
4050
|
+
});
|
|
4051
|
+
}
|
|
4052
|
+
})
|
|
4053
|
+
.catch((error) => {
|
|
4054
|
+
this.reportFrontendDiagnostic({
|
|
4055
|
+
code: "BridgePublishError",
|
|
4056
|
+
severity: "error",
|
|
4057
|
+
category: "bridge",
|
|
4058
|
+
recoverable: true,
|
|
4059
|
+
message: \`Failed to publish \${interaction} \${service}.\${member}: \${errorMessage(error)}\`,
|
|
4060
|
+
service,
|
|
4061
|
+
member,
|
|
4062
|
+
context: { interaction }
|
|
4063
|
+
});
|
|
4064
|
+
});
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
private async init(): Promise<void> {
|
|
4068
|
+
const anyWindow = window as unknown as { qt?: { webChannelTransport?: unknown }; QWebChannel?: QWebChannelCtor };
|
|
4069
|
+
if (typeof anyWindow.QWebChannel === "function" && anyWindow.qt?.webChannelTransport !== undefined) {
|
|
4070
|
+
this.adapter = await QtWebChannelAdapter.create();
|
|
4071
|
+
} else {
|
|
4072
|
+
this.adapter = await WebSocketBridgeAdapter.create();
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
const adapter = this.adapter;
|
|
4076
|
+
adapter.onHostDiagnostic((payload) => {
|
|
4077
|
+
this.pushDiagnostic({
|
|
4078
|
+
...normalizeHostDiagnostic(payload, adapter.transport),
|
|
4079
|
+
timestamp: new Date().toISOString()
|
|
4080
|
+
});
|
|
4081
|
+
});
|
|
4082
|
+
adapter.onDisconnected(() => {
|
|
4083
|
+
this._state.set("disconnected");
|
|
4084
|
+
this.reportFrontendDiagnostic({
|
|
4085
|
+
code: "BridgeDisconnectedError",
|
|
4086
|
+
severity: "error",
|
|
4087
|
+
category: "bridge",
|
|
4088
|
+
recoverable: true,
|
|
4089
|
+
message: "Bridge disconnected.",
|
|
4090
|
+
transport: adapter.transport
|
|
4091
|
+
});
|
|
4092
|
+
});
|
|
4093
|
+
|
|
4094
|
+
adapter.onOutput((service, member, value) => {
|
|
4095
|
+
const key = this.key(service, member);
|
|
4096
|
+
for (const outputHandler of this.outputHandlers.get(key) ?? []) {
|
|
4097
|
+
outputHandler(value);
|
|
4098
|
+
}
|
|
4099
|
+
});
|
|
4100
|
+
adapter.onSlotInvocation(async (requestId, service, member, args) => {
|
|
4101
|
+
const key = this.key(service, member);
|
|
4102
|
+
const handler = this.slotHandlers.get(key);
|
|
4103
|
+
if (handler === undefined) {
|
|
4104
|
+
this.reportFrontendDiagnostic({
|
|
4105
|
+
code: "HandlerNotRegisteredError",
|
|
4106
|
+
severity: "error",
|
|
4107
|
+
category: "bridge",
|
|
4108
|
+
recoverable: true,
|
|
4109
|
+
message: \`No slot handler registered for \${service}.\${member}.\`,
|
|
4110
|
+
service,
|
|
4111
|
+
member,
|
|
4112
|
+
requestId,
|
|
4113
|
+
context: { interaction: "Slot" }
|
|
4114
|
+
});
|
|
4115
|
+
adapter.resolveSlot(requestId, false, undefined, "No slot handler registered.");
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
try {
|
|
4119
|
+
const result = await Promise.resolve(handler(...args));
|
|
4120
|
+
if (result instanceof Error) {
|
|
4121
|
+
this.reportFrontendDiagnostic({
|
|
4122
|
+
code: "SlotRequestFailed",
|
|
4123
|
+
severity: "error",
|
|
4124
|
+
category: "bridge",
|
|
4125
|
+
recoverable: true,
|
|
4126
|
+
message: result.message.length > 0
|
|
4127
|
+
? result.message
|
|
4128
|
+
: \`Slot \${service}.\${member} returned an Error.\`,
|
|
4129
|
+
service,
|
|
4130
|
+
member,
|
|
4131
|
+
requestId,
|
|
4132
|
+
context: { interaction: "Slot" }
|
|
4133
|
+
});
|
|
4134
|
+
adapter.resolveSlot(requestId, false, undefined, result.message);
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
adapter.resolveSlot(requestId, true, result, "");
|
|
4138
|
+
} catch (error) {
|
|
4139
|
+
const message = errorMessage(error);
|
|
4140
|
+
this.reportFrontendDiagnostic({
|
|
4141
|
+
code: "SlotHandlerError",
|
|
4142
|
+
severity: "error",
|
|
4143
|
+
category: "bridge",
|
|
4144
|
+
recoverable: true,
|
|
4145
|
+
message: \`Slot handler \${service}.\${member} threw: \${message}\`,
|
|
4146
|
+
service,
|
|
4147
|
+
member,
|
|
4148
|
+
requestId,
|
|
4149
|
+
context: { interaction: "Slot" }
|
|
4150
|
+
});
|
|
4151
|
+
adapter.resolveSlot(requestId, false, undefined, message);
|
|
4152
|
+
}
|
|
4153
|
+
});
|
|
4154
|
+
adapter.onDrop((service, member, payload, x, y) => {
|
|
4155
|
+
const key = this.key(service, member);
|
|
4156
|
+
for (const handler of this.dropHandlers.get(key) ?? []) {
|
|
4157
|
+
handler(payload, x, y);
|
|
4158
|
+
}
|
|
4159
|
+
});
|
|
4160
|
+
adapter.onHover((service, member, payload, x, y) => {
|
|
4161
|
+
const key = this.key(service, member);
|
|
4162
|
+
for (const handler of this.hoverHandlers.get(key) ?? []) {
|
|
4163
|
+
handler(payload, x, y);
|
|
4164
|
+
}
|
|
4165
|
+
});
|
|
4166
|
+
adapter.onHoverLeft((service, member) => {
|
|
4167
|
+
const key = this.key(service, member);
|
|
4168
|
+
for (const handler of this.hoverLeftHandlers.get(key) ?? []) {
|
|
4169
|
+
handler();
|
|
4170
|
+
}
|
|
4171
|
+
});
|
|
4172
|
+
for (const key of this.slotHandlers.keys()) {
|
|
4173
|
+
const parts = key.split("::");
|
|
4174
|
+
if (parts.length === 2) {
|
|
4175
|
+
adapter.registerSlot(parts[0], parts[1]);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
this._state.set("ready");
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
private key(service: string, member: string): string {
|
|
4182
|
+
return \`\${service}::\${member}\`;
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
class AnQstBridgeDiagnostics {
|
|
4187
|
+
constructor(private readonly _bridge: AnQstBridgeRuntime) {}
|
|
4188
|
+
|
|
4189
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[] {
|
|
4190
|
+
return this._bridge.diagnostics();
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
state(): AnQstBridgeState {
|
|
4194
|
+
return this._bridge.state();
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
subscribe(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void {
|
|
4198
|
+
return this._bridge.subscribeDiagnostics(listener);
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
${serviceClasses}
|
|
4203
|
+
interface ${spec.widgetName}FrontendServices {
|
|
4204
|
+
${frontendServices}
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
interface ${spec.widgetName}Frontend {
|
|
4208
|
+
diagnostics: AnQstBridgeDiagnostics;
|
|
4209
|
+
services: ${spec.widgetName}FrontendServices;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
async function createFrontend(): Promise<${spec.widgetName}Frontend> {
|
|
4213
|
+
const bridge = new AnQstBridgeRuntime();
|
|
4214
|
+
await bridge.ready();
|
|
4215
|
+
return {
|
|
4216
|
+
diagnostics: new AnQstBridgeDiagnostics(bridge),
|
|
4217
|
+
services: {
|
|
4218
|
+
${frontendServiceFactories}
|
|
4219
|
+
}
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
(function bootstrapAnQstGenerated(global: typeof globalThis & { AnQstGenerated?: { widgets?: Record<string, unknown> } }) {
|
|
4224
|
+
const root = global.AnQstGenerated ?? (global.AnQstGenerated = {});
|
|
4225
|
+
const widgets = root.widgets ?? (root.widgets = {});
|
|
4226
|
+
widgets["${spec.widgetName}"] = {
|
|
4227
|
+
createFrontend
|
|
4228
|
+
};
|
|
4229
|
+
})(window as typeof globalThis & { AnQstGenerated?: { widgets?: Record<string, unknown> } });
|
|
4230
|
+
`;
|
|
4231
|
+
}
|
|
4232
|
+
function renderVanillaServiceDts(spec, serviceName) {
|
|
4233
|
+
const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
|
|
4234
|
+
const setMembers = [];
|
|
4235
|
+
const onSlotMembers = [];
|
|
4236
|
+
const classMembers = [];
|
|
4237
|
+
const setInterfaceName = `${serviceName}Set`;
|
|
4238
|
+
const onSlotInterfaceName = `${serviceName}OnSlot`;
|
|
4239
|
+
for (const m of members) {
|
|
4240
|
+
const args = m.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
4241
|
+
if (m.kind === "Call") {
|
|
4242
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
4243
|
+
classMembers.push(` ${m.name}(${args}): Promise<${ret}>;`);
|
|
4244
|
+
continue;
|
|
4245
|
+
}
|
|
4246
|
+
if (m.kind === "Emitter") {
|
|
4247
|
+
classMembers.push(` ${m.name}(${args}): void;`);
|
|
4248
|
+
continue;
|
|
4249
|
+
}
|
|
4250
|
+
if (m.kind === "Slot") {
|
|
4251
|
+
const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
|
|
4252
|
+
onSlotMembers.push(` ${m.name}(handler: (${args}) => ${slotHandlerReturnType(ret)}): void;`);
|
|
4253
|
+
continue;
|
|
4254
|
+
}
|
|
4255
|
+
if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
|
|
4256
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
4257
|
+
classMembers.push(` ${m.name}(): ${tsType} | undefined;`);
|
|
4258
|
+
if (m.kind === "Input") {
|
|
4259
|
+
setMembers.push(` ${m.name}(value: ${tsType}): void;`);
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
if (m.kind === "DropTarget" && m.payloadTypeText) {
|
|
4263
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
4264
|
+
classMembers.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null;`);
|
|
4265
|
+
}
|
|
4266
|
+
if (m.kind === "HoverTarget" && m.payloadTypeText) {
|
|
4267
|
+
const tsType = mapTypeTextToTs(m.payloadTypeText);
|
|
4268
|
+
classMembers.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null;`);
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
const interfaceBlocks = [];
|
|
4272
|
+
if (setMembers.length > 0) {
|
|
4273
|
+
interfaceBlocks.push(`interface ${setInterfaceName} {\n${setMembers.join("\n")}\n}`);
|
|
4274
|
+
}
|
|
4275
|
+
if (onSlotMembers.length > 0) {
|
|
4276
|
+
interfaceBlocks.push(`interface ${onSlotInterfaceName} {\n${onSlotMembers.join("\n")}\n}`);
|
|
4277
|
+
}
|
|
4278
|
+
const interfaceSection = interfaceBlocks.length > 0 ? `${interfaceBlocks.join("\n\n")}\n\n` : "";
|
|
4279
|
+
const namespaceLines = [];
|
|
4280
|
+
if (setMembers.length > 0) {
|
|
4281
|
+
namespaceLines.push(` readonly set: ${setInterfaceName};`);
|
|
4282
|
+
}
|
|
4283
|
+
if (onSlotMembers.length > 0) {
|
|
4284
|
+
namespaceLines.push(` readonly onSlot: ${onSlotInterfaceName};`);
|
|
4285
|
+
}
|
|
4286
|
+
const declareBodyLines = [...namespaceLines, ...classMembers];
|
|
4287
|
+
return `${interfaceSection}declare class ${serviceName} {
|
|
4288
|
+
${declareBodyLines.join("\n")}
|
|
4289
|
+
}`;
|
|
4290
|
+
}
|
|
4291
|
+
function renderVanillaIndexDts(spec) {
|
|
4292
|
+
const externalTypeImports = renderRequiredTypeImports(spec, `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "VanillaTS")}/index.d.ts`).trim();
|
|
4293
|
+
// Export widget namespace types so other packages can `import type { ... }` from this declaration file
|
|
4294
|
+
// (e.g. a second widget spec that reuses structs generated for the first widget).
|
|
4295
|
+
const localTypeDecls = renderTypeDeclarations(spec, true).trim();
|
|
4296
|
+
const serviceDecls = spec.services.map((s) => renderVanillaServiceDts(spec, s.name)).join("\n\n");
|
|
4297
|
+
const servicesShape = spec.services.length > 0
|
|
4298
|
+
? spec.services.map((s) => ` ${s.name}: ${s.name};`).join("\n")
|
|
4299
|
+
: "";
|
|
4300
|
+
const sections = [externalTypeImports, localTypeDecls].filter((s) => s.length > 0);
|
|
4301
|
+
const prelude = sections.length > 0 ? `${sections.join("\n\n")}\n\n` : "";
|
|
4302
|
+
return `export {};
|
|
4303
|
+
${prelude}type AnQstBridgeSeverity = "info" | "warn" | "error" | "fatal";
|
|
4304
|
+
|
|
4305
|
+
type AnQstBridgeSource = "frontend" | "host";
|
|
4306
|
+
|
|
4307
|
+
type AnQstBridgeTransport = "qt-webchannel" | "dev-websocket";
|
|
4308
|
+
|
|
4309
|
+
type AnQstBridgeState = "starting" | "ready" | "failed" | "disconnected";
|
|
4310
|
+
|
|
4311
|
+
interface AnQstBridgeDiagnostic {
|
|
4312
|
+
code: string;
|
|
4313
|
+
severity: AnQstBridgeSeverity;
|
|
4314
|
+
category: string;
|
|
4315
|
+
recoverable: boolean;
|
|
4316
|
+
message: string;
|
|
4317
|
+
timestamp: string;
|
|
4318
|
+
source: AnQstBridgeSource;
|
|
4319
|
+
transport?: AnQstBridgeTransport;
|
|
4320
|
+
service?: string;
|
|
4321
|
+
member?: string;
|
|
4322
|
+
requestId?: string;
|
|
4323
|
+
context?: Record<string, unknown>;
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
declare class AnQstBridgeDiagnostics {
|
|
4327
|
+
diagnostics(): readonly AnQstBridgeDiagnostic[];
|
|
4328
|
+
state(): AnQstBridgeState;
|
|
4329
|
+
subscribe(listener: (diagnostic: AnQstBridgeDiagnostic) => void): () => void;
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
${serviceDecls}
|
|
4333
|
+
|
|
4334
|
+
interface ${spec.widgetName}FrontendServices {
|
|
4335
|
+
${servicesShape}
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
interface ${spec.widgetName}Frontend {
|
|
4339
|
+
diagnostics: AnQstBridgeDiagnostics;
|
|
4340
|
+
services: ${spec.widgetName}FrontendServices;
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
interface ${spec.widgetName}Global {
|
|
4344
|
+
createFrontend(): Promise<${spec.widgetName}Frontend>;
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
interface AnQstGeneratedWidgets {
|
|
4348
|
+
${spec.widgetName}: ${spec.widgetName}Global;
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
interface AnQstGeneratedRoot {
|
|
4352
|
+
widgets: AnQstGeneratedWidgets;
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4355
|
+
declare global {
|
|
4356
|
+
interface Window {
|
|
4357
|
+
AnQstGenerated: AnQstGeneratedRoot;
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
var AnQstGenerated: AnQstGeneratedRoot;
|
|
4361
|
+
}
|
|
4362
|
+
`;
|
|
4363
|
+
}
|
|
4364
|
+
function transpileBrowserTsToJs(source) {
|
|
4365
|
+
return typescript_1.default.transpileModule(source, {
|
|
4366
|
+
compilerOptions: {
|
|
4367
|
+
target: typescript_1.default.ScriptTarget.ES2018,
|
|
4368
|
+
module: typescript_1.default.ModuleKind.None,
|
|
4369
|
+
importsNotUsedAsValues: typescript_1.default.ImportsNotUsedAsValues.Remove
|
|
4370
|
+
}
|
|
4371
|
+
}).outputText;
|
|
4372
|
+
}
|
|
4373
|
+
function renderVanillaPackage(spec, target) {
|
|
4374
|
+
const packageJson = {
|
|
4375
|
+
name: `${spec.widgetName.toLowerCase()}-${target.toLowerCase()}-generated`,
|
|
4376
|
+
version: "0.1.0",
|
|
4377
|
+
private: true,
|
|
4378
|
+
main: "index.js",
|
|
4379
|
+
anqst: {
|
|
4380
|
+
widget: spec.widgetName,
|
|
4381
|
+
services: spec.services.map((s) => s.name),
|
|
4382
|
+
target,
|
|
4383
|
+
supportsDevelopmentModeTransport: spec.supportsDevelopmentModeTransport,
|
|
4384
|
+
outputContractVersion: 2
|
|
4385
|
+
}
|
|
4386
|
+
};
|
|
4387
|
+
if (target === "VanillaTS") {
|
|
4388
|
+
packageJson.types = "index.d.ts";
|
|
4389
|
+
}
|
|
4390
|
+
return JSON.stringify(packageJson, null, 2);
|
|
4391
|
+
}
|
|
4392
|
+
function renderNodeExpressWsPackage(spec) {
|
|
4393
|
+
return JSON.stringify({
|
|
4394
|
+
name: `${spec.widgetName.toLowerCase()}-node-express-ws-generated`,
|
|
4395
|
+
version: "0.1.0",
|
|
4396
|
+
private: true,
|
|
4397
|
+
types: "types/index.d.ts",
|
|
4398
|
+
main: "index.ts",
|
|
4399
|
+
exports: {
|
|
4400
|
+
".": {
|
|
4401
|
+
types: "./types/index.d.ts",
|
|
4402
|
+
default: "./index.ts"
|
|
4403
|
+
}
|
|
4404
|
+
},
|
|
4405
|
+
anqst: {
|
|
4406
|
+
widget: spec.widgetName,
|
|
4407
|
+
services: spec.services.map((s) => s.name),
|
|
4408
|
+
target: "node_express_ws"
|
|
4409
|
+
}
|
|
4410
|
+
}, null, 2);
|
|
4411
|
+
}
|
|
4412
|
+
function nodeParamTuple(member) {
|
|
4413
|
+
if (member.parameters.length === 0)
|
|
4414
|
+
return "[]";
|
|
4415
|
+
return `[${member.parameters.map((p) => mapTypeTextToTs(p.typeText)).join(", ")}]`;
|
|
4416
|
+
}
|
|
4417
|
+
function nodeParamArgs(member) {
|
|
4418
|
+
return member.parameters.map((p) => `${p.name}: ${mapTypeTextToTs(p.typeText)}`).join(", ");
|
|
4419
|
+
}
|
|
4420
|
+
function nodeParamValues(member) {
|
|
4421
|
+
if (member.parameters.length === 0)
|
|
4422
|
+
return "[]";
|
|
4423
|
+
return `[${member.parameters.map((p) => p.name).join(", ")}]`;
|
|
4424
|
+
}
|
|
4425
|
+
function nodeCap(value) {
|
|
4426
|
+
return value.length === 0 ? value : `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
4427
|
+
}
|
|
4428
|
+
function renderNodeExpressWsTypes(spec) {
|
|
4429
|
+
const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/types/index.d.ts`).trim();
|
|
4430
|
+
const typeDecls = renderTypeDeclarations(spec, true).trim();
|
|
4431
|
+
const sections = [typeImports, typeDecls].filter((s) => s.length > 0);
|
|
4432
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
4433
|
+
}
|
|
4434
|
+
function renderNodeExpressWsIndex(spec, codecCatalog) {
|
|
4435
|
+
const typeImports = renderRequiredTypeImports(spec, `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}/index.ts`);
|
|
4436
|
+
const typeDecls = renderTypeDeclarations(spec, true);
|
|
4437
|
+
const handlerBridgeTypeName = `${spec.widgetName}HandlerBridge`;
|
|
4438
|
+
const sessionBridgeTypeName = `${spec.widgetName}SessionBridge`;
|
|
4439
|
+
const handlerInterfaces = spec.services
|
|
4440
|
+
.map((service) => {
|
|
4441
|
+
const lines = [];
|
|
4442
|
+
for (const member of service.members) {
|
|
4443
|
+
const args = nodeParamArgs(member);
|
|
4444
|
+
const prefixedArgs = args.length > 0 ? `, ${args}` : "";
|
|
4445
|
+
if (member.kind === "Call" && member.payloadTypeText) {
|
|
4446
|
+
const ret = mapTypeTextToTs(member.payloadTypeText);
|
|
4447
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): ${ret} | Promise<${ret}>;`);
|
|
4448
|
+
}
|
|
4449
|
+
else if (member.kind === "Emitter") {
|
|
4450
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): void | Promise<void>;`);
|
|
4451
|
+
}
|
|
4452
|
+
else if (member.kind === "Input" && member.payloadTypeText) {
|
|
4453
|
+
lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}, value: ${mapTypeTextToTs(member.payloadTypeText)}): void | Promise<void>;`);
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
return `export interface ${service.name}NodeHandlers {\n${lines.join("\n")}\n}`;
|
|
4457
|
+
})
|
|
4458
|
+
.join("\n\n");
|
|
4459
|
+
const implementationFields = spec.services.map((service) => ` ${service.name}: ${service.name}NodeHandlers;`).join("\n");
|
|
4460
|
+
const slotHelpers = spec.services
|
|
4461
|
+
.flatMap((service) => service.members
|
|
4462
|
+
.filter((member) => member.kind === "Slot")
|
|
4463
|
+
.map((member) => {
|
|
4464
|
+
const ret = mapTypeTextToTs(member.payloadTypeText ?? "void");
|
|
4465
|
+
const args = nodeParamArgs(member);
|
|
4466
|
+
const paramSites = member.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, service.name, member.name, p.name));
|
|
4467
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
|
|
4468
|
+
const encodedArgs = member.parameters.length > 0
|
|
4469
|
+
? `[${member.parameters.map((p, index) => `${paramSites[index] ? `encode${paramSites[index].codecId}(${p.name})` : p.name}`).join(", ")}]`
|
|
4470
|
+
: "[]";
|
|
4471
|
+
return ` ${service.name}_${member.name}(${args}${args ? ", " : ""}timeoutMs = this.defaultSlotTimeoutMs): Promise<${ret}> {
|
|
4472
|
+
return this.invokeSlot("${service.name}", "${member.name}", ${encodedArgs}, timeoutMs).then((value) => ${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${ret}`});
|
|
4473
|
+
}`;
|
|
4474
|
+
}))
|
|
4475
|
+
.join("\n");
|
|
4476
|
+
const outputHelpers = spec.services
|
|
4477
|
+
.flatMap((service) => service.members
|
|
4478
|
+
.filter((member) => member.kind === "Output" && member.payloadTypeText)
|
|
4479
|
+
.map((member) => {
|
|
4480
|
+
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
4481
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
|
|
4482
|
+
return ` set${service.name}_${nodeCap(member.name)}(value: ${typeText}): void {
|
|
4483
|
+
this.setOutputValue("${service.name}", "${member.name}", ${payloadSite ? `encode${payloadSite.codecId}(value)` : "value"});
|
|
4484
|
+
}`;
|
|
4485
|
+
}))
|
|
4486
|
+
.join("\n");
|
|
4487
|
+
const sessionServiceInterfaces = spec.services
|
|
4488
|
+
.map((service) => {
|
|
4489
|
+
const slotLines = service.members
|
|
4490
|
+
.filter((member) => member.kind === "Slot")
|
|
4491
|
+
.map((member) => {
|
|
4492
|
+
const ret = mapTypeTextToTs(member.payloadTypeText ?? "void");
|
|
4493
|
+
const args = nodeParamArgs(member);
|
|
4494
|
+
return ` ${member.name}(${args}${args.length > 0 ? ", " : ""}timeoutMs?: number): Promise<${ret}>;`;
|
|
4495
|
+
});
|
|
4496
|
+
const signalMembers = service.members
|
|
4497
|
+
.filter((member) => member.kind === "Emitter")
|
|
4498
|
+
.map((member) => {
|
|
4499
|
+
const args = nodeParamArgs(member);
|
|
4500
|
+
return ` ${member.name}(handler: (${args}) => void): () => void;`;
|
|
4501
|
+
});
|
|
4502
|
+
const propertyMembers = service.members
|
|
4503
|
+
.filter((member) => (member.kind === "Input" || member.kind === "Output") && member.payloadTypeText)
|
|
4504
|
+
.map((member) => {
|
|
4505
|
+
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
4506
|
+
if (member.kind === "Input") {
|
|
4507
|
+
return ` ${member.name}: {\n get(): Promise<${typeText}>;\n on(handler: (value: ${typeText}) => void): () => void;\n };`;
|
|
4508
|
+
}
|
|
4509
|
+
return ` ${member.name}: {\n set(value: ${typeText}): void;\n };`;
|
|
4510
|
+
});
|
|
4511
|
+
return `export interface ${service.name}SessionBridgeService {\n${slotLines.join("\n")}\n signal: {\n${signalMembers.join("\n")}\n };\n property: {\n${propertyMembers.join("\n")}\n };\n}`;
|
|
4512
|
+
})
|
|
4513
|
+
.join("\n\n");
|
|
4514
|
+
const widgetServiceFields = spec.services.map((service) => ` ${service.name}: ${service.name}SessionBridgeService;`).join("\n");
|
|
4515
|
+
const sessionBridgeFactory = spec.services
|
|
4516
|
+
.map((service) => {
|
|
4517
|
+
const slotMembers = service.members
|
|
4518
|
+
.filter((member) => member.kind === "Slot")
|
|
4519
|
+
.map((member) => {
|
|
4520
|
+
const args = member.parameters.map((p) => p.name).join(", ");
|
|
4521
|
+
const typedArgs = nodeParamArgs(member);
|
|
4522
|
+
return ` ${member.name}: (${typedArgs}${typedArgs.length > 0 ? ", " : ""}timeoutMs = defaultSlotTimeoutMs) => session.${service.name}_${member.name}(${args}${args.length > 0 ? ", " : ""}timeoutMs),`;
|
|
4523
|
+
})
|
|
4524
|
+
.join("\n");
|
|
4525
|
+
const signalMembers = service.members
|
|
4526
|
+
.filter((member) => member.kind === "Emitter")
|
|
4527
|
+
.map((member) => {
|
|
4528
|
+
const args = nodeParamArgs(member);
|
|
4529
|
+
return ` ${member.name}: (handler: (${args}) => void) => session.onSignal("${service.name}", "${member.name}", handler as (...args: unknown[]) => void),`;
|
|
4530
|
+
})
|
|
4531
|
+
.join("\n");
|
|
4532
|
+
const propertyMembers = service.members
|
|
4533
|
+
.filter((member) => (member.kind === "Input" || member.kind === "Output") && member.payloadTypeText)
|
|
4534
|
+
.map((member) => {
|
|
4535
|
+
const typeText = mapTypeTextToTs(member.payloadTypeText);
|
|
4536
|
+
const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
|
|
4537
|
+
if (member.kind === "Input") {
|
|
4538
|
+
return ` ${member.name}: {\n get: () => session.readInput("${service.name}", "${member.name}").then((value) => ${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${typeText}`}),\n on: (handler: (value: ${typeText}) => void) => session.onInput("${service.name}", "${member.name}", (value) => handler(${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${typeText}`}))\n },`;
|
|
4539
|
+
}
|
|
4540
|
+
return ` ${member.name}: {\n set: (value: ${typeText}) => session.set${service.name}_${nodeCap(member.name)}(value)\n },`;
|
|
4541
|
+
})
|
|
4542
|
+
.join("\n");
|
|
4543
|
+
return ` ${service.name}: {\n${slotMembers}\n signal: {\n${signalMembers}\n },\n property: {\n${propertyMembers}\n }\n },`;
|
|
3304
4544
|
})
|
|
3305
4545
|
.join("\n");
|
|
3306
4546
|
const callDispatch = spec.services
|
|
@@ -3936,25 +5176,48 @@ function generatedCppLibraryDirName(widgetName) {
|
|
|
3936
5176
|
function generatedNodeExpressWsDirName(widgetName) {
|
|
3937
5177
|
return (0, layout_1.generatedNodeExpressDirName)(widgetName);
|
|
3938
5178
|
}
|
|
3939
|
-
function generateOutputs(spec, options = {
|
|
3940
|
-
const
|
|
5179
|
+
function generateOutputs(spec, options = {}) {
|
|
5180
|
+
const useDefaultBrowserTargets = Object.keys(options).length === 0;
|
|
5181
|
+
const normalizedOptions = {
|
|
5182
|
+
emitQWidget: options.emitQWidget ?? true,
|
|
5183
|
+
emitAngularService: options.emitAngularService ?? true,
|
|
5184
|
+
emitVanillaTS: options.emitVanillaTS ?? useDefaultBrowserTargets,
|
|
5185
|
+
emitVanillaJS: options.emitVanillaJS ?? useDefaultBrowserTargets,
|
|
5186
|
+
emitNodeExpressWs: options.emitNodeExpressWs ?? false
|
|
5187
|
+
};
|
|
5188
|
+
const angularFrontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "AngularService")}`;
|
|
5189
|
+
const vanillaTsFrontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "VanillaTS")}`;
|
|
5190
|
+
const vanillaJsFrontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName, "VanillaJS")}`;
|
|
3941
5191
|
const cppDir = `backend/cpp/qt/${generatedCppLibraryDirName(spec.widgetName)}`;
|
|
3942
5192
|
const nodeDir = `backend/node/express/${generatedNodeExpressWsDirName(spec.widgetName)}`;
|
|
3943
5193
|
const outputs = {};
|
|
3944
5194
|
const codecCatalog = (0, boundary_codecs_1.buildBoundaryCodecCatalog)(spec);
|
|
3945
|
-
if (
|
|
3946
|
-
outputs[`${
|
|
3947
|
-
outputs[`${
|
|
3948
|
-
outputs[`${
|
|
3949
|
-
outputs[`${
|
|
3950
|
-
outputs[`${
|
|
3951
|
-
outputs[`${
|
|
3952
|
-
outputs[`${
|
|
3953
|
-
outputs[`${
|
|
3954
|
-
outputs[`${
|
|
3955
|
-
outputs[`${
|
|
3956
|
-
}
|
|
3957
|
-
if (
|
|
5195
|
+
if (normalizedOptions.emitAngularService) {
|
|
5196
|
+
outputs[`${angularFrontendDir}/package.json`] = renderNpmPackage(spec);
|
|
5197
|
+
outputs[`${angularFrontendDir}/index.ts`] = renderTsIndex();
|
|
5198
|
+
outputs[`${angularFrontendDir}/services.ts`] = renderTsServices(spec, codecCatalog);
|
|
5199
|
+
outputs[`${angularFrontendDir}/types.ts`] = renderTsTypes(spec);
|
|
5200
|
+
outputs[`${angularFrontendDir}/index.js`] = renderJsIndex();
|
|
5201
|
+
outputs[`${angularFrontendDir}/services.js`] = renderJsServices();
|
|
5202
|
+
outputs[`${angularFrontendDir}/types.js`] = renderJsTypes();
|
|
5203
|
+
outputs[`${angularFrontendDir}/types/index.d.ts`] = renderTypeRootIndexDts(spec);
|
|
5204
|
+
outputs[`${angularFrontendDir}/types/services.d.ts`] = renderTypeServicesDts(spec);
|
|
5205
|
+
outputs[`${angularFrontendDir}/types/types.d.ts`] = renderTypeTypesDts(spec);
|
|
5206
|
+
}
|
|
5207
|
+
if (normalizedOptions.emitVanillaTS || normalizedOptions.emitVanillaJS) {
|
|
5208
|
+
const vanillaBrowserTs = renderVanillaBrowserTs(spec, codecCatalog);
|
|
5209
|
+
const vanillaBrowserJs = transpileBrowserTsToJs(vanillaBrowserTs);
|
|
5210
|
+
if (normalizedOptions.emitVanillaTS) {
|
|
5211
|
+
outputs[`${vanillaTsFrontendDir}/package.json`] = renderVanillaPackage(spec, "VanillaTS");
|
|
5212
|
+
outputs[`${vanillaTsFrontendDir}/index.js`] = vanillaBrowserJs;
|
|
5213
|
+
outputs[`${vanillaTsFrontendDir}/index.d.ts`] = renderVanillaIndexDts(spec);
|
|
5214
|
+
}
|
|
5215
|
+
if (normalizedOptions.emitVanillaJS) {
|
|
5216
|
+
outputs[`${vanillaJsFrontendDir}/package.json`] = renderVanillaPackage(spec, "VanillaJS");
|
|
5217
|
+
outputs[`${vanillaJsFrontendDir}/index.js`] = vanillaBrowserJs;
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
if (normalizedOptions.emitQWidget) {
|
|
3958
5221
|
const cppTypes = buildCppTypeContext(spec);
|
|
3959
5222
|
outputs[`${cppDir}/CMakeLists.txt`] = renderCMake(spec);
|
|
3960
5223
|
outputs[`${cppDir}/${spec.widgetName}.qrc`] = renderEmbeddedQrc(spec.widgetName, []);
|
|
@@ -3963,7 +5226,7 @@ function generateOutputs(spec, options = { emitQWidget: true, emitAngularService
|
|
|
3963
5226
|
outputs[`${cppDir}/include/${spec.widgetName}Types.h`] = renderTypesHeader(spec, cppTypes);
|
|
3964
5227
|
outputs[`${cppDir}/${spec.widgetName}.cpp`] = renderCppStub(spec, cppTypes, codecCatalog);
|
|
3965
5228
|
}
|
|
3966
|
-
if (
|
|
5229
|
+
if (normalizedOptions.emitNodeExpressWs) {
|
|
3967
5230
|
outputs[`${nodeDir}/package.json`] = renderNodeExpressWsPackage(spec);
|
|
3968
5231
|
outputs[`${nodeDir}/index.ts`] = renderNodeExpressWsIndex(spec, codecCatalog);
|
|
3969
5232
|
outputs[`${nodeDir}/types/index.d.ts`] = renderNodeExpressWsTypes(spec);
|