@dusted/anqst 0.1.2 → 1.0.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.
@@ -55,26 +55,168 @@ function parseMemberKindFromAnQstType(typeNode) {
55
55
  if (!typeName.startsWith("AnQst."))
56
56
  return null;
57
57
  const kind = typeName.slice("AnQst.".length);
58
- if (!["Call", "Slot", "Emitter", "Output", "Input"].includes(kind))
58
+ if (!["Call", "Slot", "Emitter", "Output", "Input", "DropTarget", "HoverTarget"].includes(kind))
59
59
  return null;
60
60
  if (kind === "Emitter")
61
61
  return { kind, payload: null };
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", "DropTarget", "HoverTarget"].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" || kind === "HoverTarget") ? (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
+ const DEFAULT_HOVER_RATE_HZ = 60;
160
+ const DEFAULT_HOVER_THROTTLE_MS = Math.round(1000 / DEFAULT_HOVER_RATE_HZ);
161
+ function resolveHoverThrottleMs(source, serviceName, memberName, kind, configTypeNode, warnings, memberLoc) {
162
+ if (kind !== "HoverTarget")
163
+ return 0;
164
+ if (!configTypeNode)
165
+ return DEFAULT_HOVER_THROTTLE_MS;
166
+ if (!typescript_1.default.isTypeLiteralNode(configTypeNode)) {
167
+ throw new errors_1.VerifyError(`HoverTarget config for '${memberName}' must be an inline object literal type.`, locFromNode(source, configTypeNode));
168
+ }
169
+ let maxRateHz = null;
170
+ const memberPath = `${serviceName}.${memberName}`;
171
+ for (const prop of configTypeNode.members) {
172
+ if (!typescript_1.default.isPropertySignature(prop) || !prop.name) {
173
+ throw new errors_1.VerifyError(`HoverTarget config for '${memberName}' only supports named properties.`, locFromNode(source, prop));
174
+ }
175
+ if (!typescript_1.default.isIdentifier(prop.name)) {
176
+ throw new errors_1.VerifyError(`HoverTarget config for '${memberName}' only supports identifier keys.`, locFromNode(source, prop.name));
177
+ }
178
+ const key = prop.name.text;
179
+ if (!prop.type) {
180
+ throw new errors_1.VerifyError(`HoverTarget config key '${key}' in '${memberName}' must declare a numeric literal value.`, locFromNode(source, prop));
181
+ }
182
+ if (key !== "maxRateHz") {
183
+ warnings.push({
184
+ severity: "warn",
185
+ message: `Unknown HoverTarget config key '${key}' ignored for '${memberPath}'.`,
186
+ loc: locFromNode(source, prop.name),
187
+ memberPath
188
+ });
189
+ continue;
190
+ }
191
+ const numericValue = parseNumericLiteralType(prop.type);
192
+ if (numericValue === null || !Number.isFinite(numericValue)) {
193
+ throw new errors_1.VerifyError(`HoverTarget config key '${key}' in '${memberName}' must be a numeric literal >= 0.`, locFromNode(source, prop.type));
194
+ }
195
+ if (numericValue < 0) {
196
+ throw new errors_1.VerifyError(`HoverTarget config key '${key}' in '${memberName}' must be >= 0.`, locFromNode(source, prop.type));
197
+ }
198
+ maxRateHz = numericValue;
199
+ }
200
+ if (maxRateHz === null)
201
+ return DEFAULT_HOVER_THROTTLE_MS;
202
+ return maxRateHz === 0 ? 0 : Math.round(1000 / maxRateHz);
203
+ }
204
+ function parseServiceMember(source, serviceName, member, warnings) {
66
205
  if (typescript_1.default.isMethodSignature(member)) {
67
206
  if (member.questionToken)
68
207
  throw new errors_1.VerifyError("Optional service methods are not allowed.", locFromNode(source, member));
69
208
  const returnType = member.type;
70
209
  if (!returnType)
71
210
  throw new errors_1.VerifyError("Service method must declare return type.", locFromNode(source, member));
72
- const parsed = parseMemberKindFromAnQstType(returnType);
211
+ const parsed = parseMemberKindWithConfig(returnType);
73
212
  if (!parsed)
74
213
  throw new errors_1.VerifyError(`Unsupported service method return type '${returnType.getText()}'.`, locFromNode(source, member));
75
- if (parsed.kind === "Input" || parsed.kind === "Output") {
214
+ if (parsed.kind === "Input" || parsed.kind === "Output" || parsed.kind === "DropTarget" || parsed.kind === "HoverTarget") {
76
215
  throw new errors_1.VerifyError(`${parsed.kind} must be declared as property, not method.`, locFromNode(source, member));
77
216
  }
217
+ if (parsed.kind === "Emitter" && parsed.configTypeNode !== null) {
218
+ throw new errors_1.VerifyError(`Emitter '${member.name.getText(source)}' does not support config parameters; use plain AnQst.Emitter.`, locFromNode(source, parsed.configTypeNode));
219
+ }
78
220
  if (!member.name || !typescript_1.default.isIdentifier(member.name)) {
79
221
  throw new errors_1.VerifyError("Only identifier service method names are supported.", locFromNode(source, member));
80
222
  }
@@ -87,11 +229,14 @@ function parseServiceMember(source, member) {
87
229
  throw new errors_1.VerifyError("Service parameters must declare type.", locFromNode(source, param));
88
230
  return { name: param.name.text, typeText: param.type.getText() };
89
231
  });
232
+ const timeoutMs = resolveMemberTimeoutMs(source, serviceName, member.name.text, parsed.kind, parsed.configTypeNode, warnings, locFromNode(source, member));
90
233
  return {
91
234
  kind: parsed.kind,
92
235
  name: member.name.text,
93
236
  payloadTypeText: parsed.payload,
94
237
  parameters,
238
+ timeoutMs,
239
+ hoverThrottleMs: 0,
95
240
  loc: locFromNode(source, member)
96
241
  };
97
242
  }
@@ -100,20 +245,24 @@ function parseServiceMember(source, member) {
100
245
  throw new errors_1.VerifyError("Service property must declare type.", locFromNode(source, member));
101
246
  if (member.questionToken)
102
247
  throw new errors_1.VerifyError("Optional service properties are not allowed.", locFromNode(source, member));
103
- const parsed = parseMemberKindFromAnQstType(member.type);
248
+ const parsed = parseMemberKindWithConfig(member.type);
104
249
  if (!parsed)
105
250
  throw new errors_1.VerifyError(`Unsupported service property type '${member.type.getText()}'.`, locFromNode(source, member));
106
- if (parsed.kind !== "Input" && parsed.kind !== "Output") {
251
+ if (parsed.kind !== "Input" && parsed.kind !== "Output" && parsed.kind !== "DropTarget" && parsed.kind !== "HoverTarget") {
107
252
  throw new errors_1.VerifyError(`${parsed.kind} must be declared as method, not property.`, locFromNode(source, member));
108
253
  }
109
254
  if (!member.name || !typescript_1.default.isIdentifier(member.name)) {
110
255
  throw new errors_1.VerifyError("Only identifier service property names are supported.", locFromNode(source, member));
111
256
  }
257
+ const timeoutMs = resolveMemberTimeoutMs(source, serviceName, member.name.text, parsed.kind, parsed.configTypeNode, warnings, locFromNode(source, member));
258
+ const hoverThrottleMs = resolveHoverThrottleMs(source, serviceName, member.name.text, parsed.kind, parsed.configTypeNode, warnings, locFromNode(source, member));
112
259
  return {
113
260
  kind: parsed.kind,
114
261
  name: member.name.text,
115
262
  payloadTypeText: parsed.payload,
116
263
  parameters: [],
264
+ timeoutMs,
265
+ hoverThrottleMs,
117
266
  loc: locFromNode(source, member)
118
267
  };
119
268
  }
@@ -228,12 +377,13 @@ function parseSpecFileAst(specFilePath) {
228
377
  throw new errors_1.VerifyError("Namespace body must be a block.", locFromNode(source, ns));
229
378
  const services = [];
230
379
  const namespaceTypeDecls = [];
380
+ const warnings = [];
231
381
  let supportsDevelopmentModeTransport = false;
232
382
  for (const stmt of ns.body.statements) {
233
383
  if (typescript_1.default.isInterfaceDeclaration(stmt)) {
234
384
  const baseType = serviceBaseType(stmt);
235
385
  if (baseType !== null) {
236
- const members = stmt.members.map((member) => parseServiceMember(source, member));
386
+ const members = stmt.members.map((member) => parseServiceMember(source, stmt.name.text, member, warnings));
237
387
  if (baseType === "AngularHTTPBaseServerClass") {
238
388
  supportsDevelopmentModeTransport = true;
239
389
  }
@@ -257,7 +407,8 @@ function parseSpecFileAst(specFilePath) {
257
407
  namespaceTypeDecls,
258
408
  importedTypeDecls: importInfo.importedTypeDecls,
259
409
  importedTypeSymbols: importInfo.importedTypeSymbols,
260
- specImports: importInfo.specImports
410
+ specImports: importInfo.specImports,
411
+ warnings
261
412
  };
262
413
  }
263
414
  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,12 +1,13 @@
1
1
  {
2
2
  "name": "@dusted/anqst",
3
- "version": "0.1.2",
3
+ "version": "1.0.0",
4
4
  "description": "Opinionated backend generator for webapps.",
5
5
  "keywords": [
6
6
  "nodejs",
7
7
  "qt",
8
8
  "angular",
9
- "webapp"
9
+ "webapp",
10
+ "apigenerator"
10
11
  ],
11
12
  "homepage": "https://github.com/DusteDdk/AnQst#readme",
12
13
  "bugs": {