@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/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
- `static QByteArray encodeDragDropPayload_${helper.typeName}(const ${helper.cppType}& payload);`,
1188
- `static std::optional<${helper.cppType}> decodeDragDropPayload_${helper.typeName}(const QByteArray& rawPayload);`
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.map((p) => `const ${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])}& ${p.name}`).join(", ");
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.map((p) => `const ${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])}& ${p.name}`).join(", ");
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 ? cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]) : "void";
1207
- const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
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 setInterfaceDecl = setMembers.length > 0
2245
- ? `export interface ${setInterfaceName} {\n${setMembers.join("\n")}\n}`
2246
- : `export interface ${setInterfaceName} {}`;
2247
- const onSlotInterfaceDecl = onSlotMembers.length > 0
2248
- ? `export interface ${onSlotInterfaceName} {\n${onSlotMembers.join("\n")}\n}`
2249
- : `export interface ${onSlotInterfaceName} {}`;
2250
- const classMemberBlock = classMembers.length > 0 ? `\n${classMembers.join("\n")}` : "";
2251
- return `${setInterfaceDecl}
2252
-
2253
- ${onSlotInterfaceDecl}
2254
-
2255
- export declare class ${serviceName} {
2256
- readonly set: ${setInterfaceName};
2257
- readonly onSlot: ${onSlotInterfaceName};${classMemberBlock}
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 renderNodeExpressWsPackage(spec) {
3153
- return JSON.stringify({
3154
- name: `${spec.widgetName.toLowerCase()}-node-express-ws-generated`,
3155
- version: "0.1.0",
3156
- private: true,
3157
- types: "types/index.d.ts",
3158
- main: "index.ts",
3159
- exports: {
3160
- ".": {
3161
- types: "./types/index.d.ts",
3162
- default: "./index.ts"
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
- anqst: {
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
- else if (member.kind === "Emitter") {
3210
- lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}${prefixedArgs}): void | Promise<void>;`);
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 if (member.kind === "Input" && member.payloadTypeText) {
3213
- lines.push(` ${member.name}(bridge: ${handlerBridgeTypeName}, value: ${mapTypeTextToTs(member.payloadTypeText)}): void | Promise<void>;`);
3277
+ else {
3278
+ onSlotMembers.push(" return result;");
3214
3279
  }
3280
+ onSlotMembers.push(" });");
3281
+ onSlotMembers.push(" },");
3282
+ continue;
3215
3283
  }
3216
- return `export interface ${service.name}NodeHandlers {\n${lines.join("\n")}\n}`;
3217
- })
3218
- .join("\n\n");
3219
- const implementationFields = spec.services.map((service) => ` ${service.name}: ${service.name}NodeHandlers;`).join("\n");
3220
- const slotHelpers = spec.services
3221
- .flatMap((service) => service.members
3222
- .filter((member) => member.kind === "Slot")
3223
- .map((member) => {
3224
- const ret = mapTypeTextToTs(member.payloadTypeText ?? "void");
3225
- const args = nodeParamArgs(member);
3226
- const paramSites = member.parameters.map((p) => (0, boundary_codecs_1.getBoundaryParameterSite)(codecCatalog, service.name, member.name, p.name));
3227
- const payloadSite = (0, boundary_codecs_1.getBoundaryPayloadSite)(codecCatalog, service.name, member.name);
3228
- const encodedArgs = member.parameters.length > 0
3229
- ? `[${member.parameters.map((p, index) => `${paramSites[index] ? `encode${paramSites[index].codecId}(${p.name})` : p.name}`).join(", ")}]`
3230
- : "[]";
3231
- return ` ${service.name}_${member.name}(${args}${args ? ", " : ""}timeoutMs = this.defaultSlotTimeoutMs): Promise<${ret}> {
3232
- return this.invokeSlot("${service.name}", "${member.name}", ${encodedArgs}, timeoutMs).then((value) => ${payloadSite ? `decode${payloadSite.codecId}(value)` : `value as ${ret}`});
3233
- }`;
3234
- }))
3235
- .join("\n");
3236
- const outputHelpers = spec.services
3237
- .flatMap((service) => service.members
3238
- .filter((member) => member.kind === "Output" && member.payloadTypeText)
3239
- .map((member) => {
3240
- const typeText = mapTypeTextToTs(member.payloadTypeText);
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
- return ` ${member.name}: {\n set(value: ${typeText}): void;\n };`;
3270
- });
3271
- 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}`;
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
- return ` ${member.name}: {\n set: (value: ${typeText}) => session.set${service.name}_${nodeCap(member.name)}(value)\n },`;
3301
- })
3302
- .join("\n");
3303
- return ` ${service.name}: {\n${slotMembers}\n signal: {\n${signalMembers}\n },\n property: {\n${propertyMembers}\n }\n },`;
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 = { emitQWidget: true, emitAngularService: true, emitNodeExpressWs: false }) {
3940
- const frontendDir = `frontend/${(0, layout_1.generatedFrontendDirName)(spec.widgetName)}`;
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 (options.emitAngularService) {
3946
- outputs[`${frontendDir}/package.json`] = renderNpmPackage(spec);
3947
- outputs[`${frontendDir}/index.ts`] = renderTsIndex();
3948
- outputs[`${frontendDir}/services.ts`] = renderTsServices(spec, codecCatalog);
3949
- outputs[`${frontendDir}/types.ts`] = renderTsTypes(spec);
3950
- outputs[`${frontendDir}/index.js`] = renderJsIndex();
3951
- outputs[`${frontendDir}/services.js`] = renderJsServices();
3952
- outputs[`${frontendDir}/types.js`] = renderJsTypes();
3953
- outputs[`${frontendDir}/types/index.d.ts`] = renderTypeRootIndexDts(spec);
3954
- outputs[`${frontendDir}/types/services.d.ts`] = renderTypeServicesDts(spec);
3955
- outputs[`${frontendDir}/types/types.d.ts`] = renderTypeTypesDts(spec);
3956
- }
3957
- if (options.emitQWidget) {
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 (options.emitNodeExpressWs) {
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);