@dusted/anqst 0.1.2 → 0.1.3

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.
@@ -62,19 +62,116 @@ function parseMemberKindFromAnQstType(typeNode) {
62
62
  const arg = typeNode.typeArguments?.[0];
63
63
  return { kind, payload: arg ? arg.getText() : null };
64
64
  }
65
- function parseServiceMember(source, member) {
65
+ const DEFAULT_MEMBER_TIMEOUT_MS = 120000;
66
+ const MAX_MEMBER_TIMEOUT_MS = 2147483647;
67
+ function parseMemberKindWithConfig(typeNode) {
68
+ if (!typescript_1.default.isTypeReferenceNode(typeNode))
69
+ return null;
70
+ const typeName = qNameToText(typeNode.typeName);
71
+ if (!typeName.startsWith("AnQst."))
72
+ return null;
73
+ const kind = typeName.slice("AnQst.".length);
74
+ if (!["Call", "Slot", "Emitter", "Output", "Input"].includes(kind))
75
+ return null;
76
+ const typeArgs = typeNode.typeArguments ?? [];
77
+ if (kind === "Emitter") {
78
+ return {
79
+ kind,
80
+ payload: null,
81
+ configTypeNode: typeArgs[0] ?? null
82
+ };
83
+ }
84
+ return {
85
+ kind,
86
+ payload: typeArgs[0] ? typeArgs[0].getText() : null,
87
+ configTypeNode: kind === "Call" ? (typeArgs[1] ?? null) : null
88
+ };
89
+ }
90
+ function parseNumericLiteralType(node) {
91
+ if (!typescript_1.default.isLiteralTypeNode(node))
92
+ return null;
93
+ if (typescript_1.default.isNumericLiteral(node.literal)) {
94
+ return Number(node.literal.text);
95
+ }
96
+ if (typescript_1.default.isPrefixUnaryExpression(node.literal)
97
+ && node.literal.operator === typescript_1.default.SyntaxKind.MinusToken
98
+ && typescript_1.default.isNumericLiteral(node.literal.operand)) {
99
+ return -Number(node.literal.operand.text);
100
+ }
101
+ return null;
102
+ }
103
+ function resolveMemberTimeoutMs(source, serviceName, memberName, kind, configTypeNode, warnings, memberLoc) {
104
+ if (kind !== "Call")
105
+ return DEFAULT_MEMBER_TIMEOUT_MS;
106
+ if (!configTypeNode)
107
+ return DEFAULT_MEMBER_TIMEOUT_MS;
108
+ if (!typescript_1.default.isTypeLiteralNode(configTypeNode)) {
109
+ throw new errors_1.VerifyError(`${kind} config for '${memberName}' must be an inline object literal type.`, locFromNode(source, configTypeNode));
110
+ }
111
+ let timeoutSeconds = null;
112
+ let timeoutMilliseconds = null;
113
+ const memberPath = `${serviceName}.${memberName}`;
114
+ for (const prop of configTypeNode.members) {
115
+ if (!typescript_1.default.isPropertySignature(prop) || !prop.name) {
116
+ throw new errors_1.VerifyError(`${kind} config for '${memberName}' only supports named properties.`, locFromNode(source, prop));
117
+ }
118
+ if (!typescript_1.default.isIdentifier(prop.name)) {
119
+ throw new errors_1.VerifyError(`${kind} config for '${memberName}' only supports identifier keys.`, locFromNode(source, prop.name));
120
+ }
121
+ const key = prop.name.text;
122
+ if (!prop.type) {
123
+ throw new errors_1.VerifyError(`${kind} config key '${key}' in '${memberName}' must declare a numeric literal value.`, locFromNode(source, prop));
124
+ }
125
+ if (key !== "timeoutSeconds" && key !== "timeoutMilliseconds") {
126
+ warnings.push({
127
+ severity: "warn",
128
+ message: `Unknown ${kind} config key '${key}' ignored for '${memberPath}'.`,
129
+ loc: locFromNode(source, prop.name),
130
+ memberPath
131
+ });
132
+ continue;
133
+ }
134
+ const numericValue = parseNumericLiteralType(prop.type);
135
+ if (numericValue === null || !Number.isInteger(numericValue)) {
136
+ throw new errors_1.VerifyError(`${kind} config key '${key}' in '${memberName}' must be an integer literal >= 0.`, locFromNode(source, prop.type));
137
+ }
138
+ if (numericValue < 0) {
139
+ throw new errors_1.VerifyError(`${kind} config key '${key}' in '${memberName}' must be >= 0.`, locFromNode(source, prop.type));
140
+ }
141
+ if (key === "timeoutSeconds")
142
+ timeoutSeconds = numericValue;
143
+ if (key === "timeoutMilliseconds")
144
+ timeoutMilliseconds = numericValue;
145
+ }
146
+ if (timeoutSeconds !== null && timeoutMilliseconds !== null) {
147
+ throw new errors_1.VerifyError(`${kind} config for '${memberName}' must specify only one of 'timeoutSeconds' or 'timeoutMilliseconds'.`, memberLoc);
148
+ }
149
+ const effectiveMs = timeoutMilliseconds !== null
150
+ ? timeoutMilliseconds
151
+ : timeoutSeconds !== null
152
+ ? timeoutSeconds * 1000
153
+ : DEFAULT_MEMBER_TIMEOUT_MS;
154
+ if (effectiveMs > MAX_MEMBER_TIMEOUT_MS) {
155
+ throw new errors_1.VerifyError(`${kind} timeout for '${memberName}' exceeds max supported value (${MAX_MEMBER_TIMEOUT_MS} ms).`, memberLoc);
156
+ }
157
+ return effectiveMs;
158
+ }
159
+ function parseServiceMember(source, serviceName, member, warnings) {
66
160
  if (typescript_1.default.isMethodSignature(member)) {
67
161
  if (member.questionToken)
68
162
  throw new errors_1.VerifyError("Optional service methods are not allowed.", locFromNode(source, member));
69
163
  const returnType = member.type;
70
164
  if (!returnType)
71
165
  throw new errors_1.VerifyError("Service method must declare return type.", locFromNode(source, member));
72
- const parsed = parseMemberKindFromAnQstType(returnType);
166
+ const parsed = parseMemberKindWithConfig(returnType);
73
167
  if (!parsed)
74
168
  throw new errors_1.VerifyError(`Unsupported service method return type '${returnType.getText()}'.`, locFromNode(source, member));
75
169
  if (parsed.kind === "Input" || parsed.kind === "Output") {
76
170
  throw new errors_1.VerifyError(`${parsed.kind} must be declared as property, not method.`, locFromNode(source, member));
77
171
  }
172
+ if (parsed.kind === "Emitter" && parsed.configTypeNode !== null) {
173
+ throw new errors_1.VerifyError(`Emitter '${member.name.getText(source)}' does not support config parameters; use plain AnQst.Emitter.`, locFromNode(source, parsed.configTypeNode));
174
+ }
78
175
  if (!member.name || !typescript_1.default.isIdentifier(member.name)) {
79
176
  throw new errors_1.VerifyError("Only identifier service method names are supported.", locFromNode(source, member));
80
177
  }
@@ -87,11 +184,13 @@ function parseServiceMember(source, member) {
87
184
  throw new errors_1.VerifyError("Service parameters must declare type.", locFromNode(source, param));
88
185
  return { name: param.name.text, typeText: param.type.getText() };
89
186
  });
187
+ const timeoutMs = resolveMemberTimeoutMs(source, serviceName, member.name.text, parsed.kind, parsed.configTypeNode, warnings, locFromNode(source, member));
90
188
  return {
91
189
  kind: parsed.kind,
92
190
  name: member.name.text,
93
191
  payloadTypeText: parsed.payload,
94
192
  parameters,
193
+ timeoutMs,
95
194
  loc: locFromNode(source, member)
96
195
  };
97
196
  }
@@ -100,7 +199,7 @@ function parseServiceMember(source, member) {
100
199
  throw new errors_1.VerifyError("Service property must declare type.", locFromNode(source, member));
101
200
  if (member.questionToken)
102
201
  throw new errors_1.VerifyError("Optional service properties are not allowed.", locFromNode(source, member));
103
- const parsed = parseMemberKindFromAnQstType(member.type);
202
+ const parsed = parseMemberKindWithConfig(member.type);
104
203
  if (!parsed)
105
204
  throw new errors_1.VerifyError(`Unsupported service property type '${member.type.getText()}'.`, locFromNode(source, member));
106
205
  if (parsed.kind !== "Input" && parsed.kind !== "Output") {
@@ -109,11 +208,13 @@ function parseServiceMember(source, member) {
109
208
  if (!member.name || !typescript_1.default.isIdentifier(member.name)) {
110
209
  throw new errors_1.VerifyError("Only identifier service property names are supported.", locFromNode(source, member));
111
210
  }
211
+ const timeoutMs = resolveMemberTimeoutMs(source, serviceName, member.name.text, parsed.kind, parsed.configTypeNode, warnings, locFromNode(source, member));
112
212
  return {
113
213
  kind: parsed.kind,
114
214
  name: member.name.text,
115
215
  payloadTypeText: parsed.payload,
116
216
  parameters: [],
217
+ timeoutMs,
117
218
  loc: locFromNode(source, member)
118
219
  };
119
220
  }
@@ -228,12 +329,13 @@ function parseSpecFileAst(specFilePath) {
228
329
  throw new errors_1.VerifyError("Namespace body must be a block.", locFromNode(source, ns));
229
330
  const services = [];
230
331
  const namespaceTypeDecls = [];
332
+ const warnings = [];
231
333
  let supportsDevelopmentModeTransport = false;
232
334
  for (const stmt of ns.body.statements) {
233
335
  if (typescript_1.default.isInterfaceDeclaration(stmt)) {
234
336
  const baseType = serviceBaseType(stmt);
235
337
  if (baseType !== null) {
236
- const members = stmt.members.map((member) => parseServiceMember(source, member));
338
+ const members = stmt.members.map((member) => parseServiceMember(source, stmt.name.text, member, warnings));
237
339
  if (baseType === "AngularHTTPBaseServerClass") {
238
340
  supportsDevelopmentModeTransport = true;
239
341
  }
@@ -257,7 +359,8 @@ function parseSpecFileAst(specFilePath) {
257
359
  namespaceTypeDecls,
258
360
  importedTypeDecls: importInfo.importedTypeDecls,
259
361
  importedTypeSymbols: importInfo.importedTypeSymbols,
260
- specImports: importInfo.specImports
362
+ specImports: importInfo.specImports,
363
+ warnings
261
364
  };
262
365
  }
263
366
  function parseSpecFile(specFilePath) {
@@ -194,9 +194,14 @@ function verifySpecSemantics(spec) {
194
194
  reachableGeneratedTypes: reachable.size,
195
195
  serviceCount: spec.services.length
196
196
  };
197
+ const warnings = [...spec.warnings];
198
+ const warningSummary = warnings.length === 0
199
+ ? ""
200
+ : `\nWarnings:\n${warnings.map((w) => ` [warn] ${w.loc.file}:${w.loc.line}:${w.loc.column} ${w.memberPath} - ${w.message}`).join("\n")}`;
197
201
  return {
198
202
  stats,
199
- message: `AnQst spec valid:\n ${stats.namespaceDeclaredTypes} types.\n ${stats.serviceCount} services.`
203
+ message: `AnQst spec valid:\n ${stats.namespaceDeclaredTypes} types.\n ${stats.serviceCount} services.${warningSummary}`,
204
+ warnings
200
205
  };
201
206
  }
202
207
  function verifySpec(spec) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dusted/anqst",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Opinionated backend generator for webapps.",
5
5
  "keywords": [
6
6
  "nodejs",
@@ -10,20 +10,20 @@
10
10
  * - This is not an input file to the generator, it is the description of the language that describes the widget.
11
11
  * Not for use by TypeScript application implementation.
12
12
  * @example
13
- * package.json:
14
- * - "AnQst": "./AnQst/MyUserMgmtWidget.settings.json"
13
+ * package.json:
14
+ * - "AnQst": "./AnQst/MyUserMgmtWidget.settings.json"
15
15
  */
16
16
 
17
17
  export namespace AnQst {
18
18
 
19
19
  /**
20
20
  * Declare service `InterfaceName`
21
- *
21
+ *
22
22
  * @remarks
23
23
  * Multiple allowed.
24
24
  * Affords developers of advanced widgets the ability to create domain-informed categories.
25
25
  * - Duplicate method declarations with identical parameter lists are invalid in normative AnQst-Spec input.
26
- *
26
+ *
27
27
  * @example
28
28
  * export interface UserService extends Widget.Service { }
29
29
  * // Generates UserService.
@@ -44,6 +44,7 @@ export namespace AnQst {
44
44
  */
45
45
  interface AngularHTTPBaseServerClass extends Service { }
46
46
 
47
+ type CallConfig = { timeoutSeconds: number } | { timeoutMilliseconds: number } | {};
47
48
 
48
49
  /**
49
50
  * Declare non-blocking service method `MethodName`(`MethodArguments`): Promise<`MethodReturnType`>.
@@ -52,27 +53,38 @@ export namespace AnQst {
52
53
  * - Flow:
53
54
  * - Widget: Call to service method `MethodName`(`MethodArguments`).
54
55
  * - Widget: Returns Promise<`MethodReturnType`>.
55
- * - Parent: Registered `MethodReturnType` `MethodName`_Handler(`MethodArguments`) is called.
56
- * - Parent: Handler returns result.
57
- * - Widget: Promise resolves with result.
56
+ * - Parent: Registered callback receives args and returns `T` synchronously.
57
+ * - Widget: Promise resolves with payload `T` or rejects with plain error object.
58
+ * - Optional config supports timeout tuning:
59
+ * - `AnQst.Call<T, { timeoutSeconds: N }>`
60
+ * - `AnQst.Call<T, { timeoutMilliseconds: N }>`
61
+ * - Exactly one timeout key is allowed. Value must be integer >= 0.
62
+ * - Default timeout is 120s. `0` means wait forever.
58
63
  * @example
59
64
  * // AnQst spec:
60
65
  * getUserById(userId: string): AnQst.Call<User>
61
66
  * //Angular app:
62
67
  * const user: User = await this.userService.getUserById("abc");
63
68
  */
64
- interface Call<T> { dummy: T }
69
+ interface Call<T, Config extends CallConfig = {}> { dummy: T; config?: Config }
65
70
 
66
- /**
67
- * Declare blocking service method onSlot.`MethodName`( handler(`MethodArguments`):`MethodReturnType` ): void
71
+ /**
72
+ * Declare blocking service method onSlot.`MethodName`( handler(`MethodArguments`):`MethodReturnType` ): void
68
73
  * @remarks
69
74
  * - **Parent** -> Widget
70
75
  * - Impl note: Autogenerated stub handler queues until handler is set (set method calls spools queue through handler)
71
76
  * - Flow:
72
77
  * - Parent: Call to generated widget method `MethodName`(`MethodArguments`).
73
- * - Widget: Registered handler(`MethodArguments`): `MethodReturnType` is called.
74
- * - Widget: Handler returns result.
78
+ * - Widget: Registered handler(`MethodArguments`) is called.
79
+ * - Widget: Handler return forms:
80
+ * - `T` -> success payload
81
+ * - `Promise<T>` -> awaited, success payload on resolve
82
+ * - `Error` -> failure
83
+ * - throw -> failure
84
+ * - rejected promise -> failure
75
85
  * - Parent: `MethodName` call returns with result.
86
+ * - Generated C++ Slot methods do not expose `ok/error` out parameters.
87
+ * - Default Slot timeout is 1000ms.
76
88
  * Note: One active handler, calling will replace existing and is valid and allowed.
77
89
  * @example
78
90
  * // AnQst spec:
@@ -95,6 +107,7 @@ export namespace AnQst {
95
107
  * - Widget: Call to service method `MethodName`(`MethodArguments`).
96
108
  * - Widget: Returns void.
97
109
  * - Parent: Might have something connected to the signal, might not.
110
+ * - If no listener is connected, event is dropped.
98
111
  * @example
99
112
  * // AnQst spec:
100
113
  * complain(whine: string): AnQst.Emitter;
@@ -144,20 +157,20 @@ export namespace AnQst {
144
157
 
145
158
  /**
146
159
  * AnQst-Spec type mapping overview and control.
147
- *
160
+ *
148
161
  * @remarks
149
162
  * Any Type that be mapped between TypeScript and Qt, C++ standard types or
150
163
  * Plain Old Data (POD) types (in that order of preference) will be mapped by default.
151
- *
164
+ *
152
165
  * Canonical mapping directive namespace is AnQst.Type.<type>.
153
166
  * To express advisory mapping preference, use AnQst.Type.<type>.
154
167
  * Advisory means generator SHOULD honor it, but MAY fall back to inferred/default mapping and emit diagnostic.
155
168
  * Array forms are equivalent: T[] == Array<T>, and AnQst.Type.X[] == Array<AnQst.Type.X>.
156
- *
169
+ *
157
170
  * TypeScript definitions+classes and C++ structs are generated for each
158
171
  * structured TypeScript type ( type = {...} or interface { ... } )
159
172
  * referenced in an AnQst spec.
160
- *
173
+ *
161
174
  */
162
175
  enum Type {
163
176
  object = "JavaScript Object <-> QVariantMap (JSON.stringify/parse semantics)",
@@ -195,13 +208,13 @@ export namespace AnQst {
195
208
 
196
209
  /**
197
210
  * These are explicitly forbidden argument/return types.
198
- *
211
+ *
199
212
  * @remarks
200
213
  * - They may not be referenced in AnQst specs or imported types.
201
214
  * - Service methods cannot accept them as arguments
202
215
  * - Service methods cannot return them.
203
216
  * - Service properties of their type cannot be declared.
204
- *
217
+ *
205
218
  * - For Objects/Maps/Sets use AnQst.Type.<Type> instead.
206
219
  */
207
220
  export enum ForbiddenType {
@@ -213,5 +226,24 @@ export namespace AnQst {
213
226
  any = "Passing 'any' type across the boundary is not allowed",
214
227
  }
215
228
 
229
+ /**
230
+ * JS/TS Error instances can be returned, but they have special meaning.
231
+ *
232
+ * @remarks
233
+ * AnQst is opinionated, Errors are not for control flow. They are for signalling unrecoverable and unhandled circumstance.
234
+ * Use: To indicate unrecoverable program error/wrong use.
235
+ * Don't use: To communicate expected and/or ignorable/handable situations, define a domain-specific transport type instad.
236
+ * When else encountered: Unhandled Errors.
237
+ * Effect: The receiving end throws an exception with message `<service>.<member> emitted error: <WidgetStackTrace>`
238
+ * AnQst internal behavior:
239
+ * When AnQst type translation/mapping encounters a true Error object instance, the Error object is not transported, instead
240
+ * the sending AnQst code signals to the receiving AnQst code a message, and the receiving AnQst code throws a runtime exception on
241
+ * the call or callback site in the Parent.
242
+
243
+ */
244
+ export enum ExceptionalType {
245
+ Error = "AnQst will not transport an Error object, but will cause an exception to be thrown on reception site."
246
+ }
247
+
216
248
  }
217
249