@webda/core 4.0.0-beta.6 → 4.0.0-beta.7

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.
Files changed (138) hide show
  1. package/README.md +6 -5
  2. package/lib/application/application.d.ts.map +1 -1
  3. package/lib/application/application.js +6 -5
  4. package/lib/application/application.js.map +1 -1
  5. package/lib/application/iconfiguration.d.ts +31 -0
  6. package/lib/application/iconfiguration.d.ts.map +1 -1
  7. package/lib/application/iconfiguration.js.map +1 -1
  8. package/lib/application/unpackedapplication.d.ts.map +1 -1
  9. package/lib/application/unpackedapplication.js +2 -1
  10. package/lib/application/unpackedapplication.js.map +1 -1
  11. package/lib/bin/cli-phase.d.ts +18 -2
  12. package/lib/bin/cli-phase.d.ts.map +1 -1
  13. package/lib/bin/cli-phase.js +25 -0
  14. package/lib/bin/cli-phase.js.map +1 -1
  15. package/lib/bin/cli.d.ts +8 -0
  16. package/lib/bin/cli.d.ts.map +1 -1
  17. package/lib/bin/cli.js +94 -35
  18. package/lib/bin/cli.js.map +1 -1
  19. package/lib/configurations/configuration.service.d.ts +7 -0
  20. package/lib/configurations/configuration.service.d.ts.map +1 -1
  21. package/lib/configurations/configuration.service.js +18 -5
  22. package/lib/configurations/configuration.service.js.map +1 -1
  23. package/lib/configurations/kubernetesconfiguration.service.d.ts.map +1 -1
  24. package/lib/configurations/kubernetesconfiguration.service.js +1 -4
  25. package/lib/configurations/kubernetesconfiguration.service.js.map +1 -1
  26. package/lib/contexts/httpcontext.d.ts +15 -1
  27. package/lib/contexts/httpcontext.d.ts.map +1 -1
  28. package/lib/contexts/httpcontext.js +61 -19
  29. package/lib/contexts/httpcontext.js.map +1 -1
  30. package/lib/contexts/operationcontext.d.ts +6 -0
  31. package/lib/contexts/operationcontext.d.ts.map +1 -1
  32. package/lib/contexts/operationcontext.js +16 -2
  33. package/lib/contexts/operationcontext.js.map +1 -1
  34. package/lib/contexts/restcontext.d.ts +110 -0
  35. package/lib/contexts/restcontext.d.ts.map +1 -0
  36. package/lib/contexts/restcontext.js +247 -0
  37. package/lib/contexts/restcontext.js.map +1 -0
  38. package/lib/contexts/streamingcontext.d.ts +71 -0
  39. package/lib/contexts/streamingcontext.d.ts.map +1 -0
  40. package/lib/contexts/streamingcontext.js +93 -0
  41. package/lib/contexts/streamingcontext.js.map +1 -0
  42. package/lib/contexts/webcontext.d.ts +20 -0
  43. package/lib/contexts/webcontext.d.ts.map +1 -1
  44. package/lib/contexts/webcontext.js +43 -10
  45. package/lib/contexts/webcontext.js.map +1 -1
  46. package/lib/contexts/websocketcontext.d.ts +39 -0
  47. package/lib/contexts/websocketcontext.d.ts.map +1 -0
  48. package/lib/contexts/websocketcontext.js +67 -0
  49. package/lib/contexts/websocketcontext.js.map +1 -0
  50. package/lib/core/asyncqueue.d.ts +50 -0
  51. package/lib/core/asyncqueue.d.ts.map +1 -0
  52. package/lib/core/asyncqueue.js +96 -0
  53. package/lib/core/asyncqueue.js.map +1 -0
  54. package/lib/core/core.d.ts +18 -0
  55. package/lib/core/core.d.ts.map +1 -1
  56. package/lib/core/core.js +72 -7
  57. package/lib/core/core.js.map +1 -1
  58. package/lib/core/icore.d.ts +14 -1
  59. package/lib/core/icore.d.ts.map +1 -1
  60. package/lib/core/icore.js.map +1 -1
  61. package/lib/core/instancestorage.d.ts +1 -0
  62. package/lib/core/instancestorage.d.ts.map +1 -1
  63. package/lib/core/instancestorage.js +1 -0
  64. package/lib/core/instancestorage.js.map +1 -1
  65. package/lib/core/operations.d.ts +49 -6
  66. package/lib/core/operations.d.ts.map +1 -1
  67. package/lib/core/operations.js +187 -24
  68. package/lib/core/operations.js.map +1 -1
  69. package/lib/deployers/git.d.ts +4 -2
  70. package/lib/deployers/git.d.ts.map +1 -1
  71. package/lib/deployers/git.js +44 -5
  72. package/lib/deployers/git.js.map +1 -1
  73. package/lib/errors/errors.d.ts +15 -0
  74. package/lib/errors/errors.d.ts.map +1 -1
  75. package/lib/errors/errors.js +20 -0
  76. package/lib/errors/errors.js.map +1 -1
  77. package/lib/index.d.ts +5 -0
  78. package/lib/index.d.ts.map +1 -1
  79. package/lib/index.js +5 -0
  80. package/lib/index.js.map +1 -1
  81. package/lib/rest/restoperationstransport.service.d.ts +108 -0
  82. package/lib/rest/restoperationstransport.service.d.ts.map +1 -1
  83. package/lib/rest/restoperationstransport.service.js +348 -9
  84. package/lib/rest/restoperationstransport.service.js.map +1 -1
  85. package/lib/rest/router.service.d.ts +1 -1
  86. package/lib/rest/router.service.d.ts.map +1 -1
  87. package/lib/rest/router.service.js.map +1 -1
  88. package/lib/services/authenticatedencryption.d.ts +25 -0
  89. package/lib/services/authenticatedencryption.d.ts.map +1 -0
  90. package/lib/services/authenticatedencryption.js +56 -0
  91. package/lib/services/authenticatedencryption.js.map +1 -0
  92. package/lib/services/cloudbinary.d.ts.map +1 -1
  93. package/lib/services/cloudbinary.js +3 -2
  94. package/lib/services/cloudbinary.js.map +1 -1
  95. package/lib/services/command.d.ts +7 -1
  96. package/lib/services/command.d.ts.map +1 -1
  97. package/lib/services/command.js +4 -0
  98. package/lib/services/command.js.map +1 -1
  99. package/lib/services/cryptoservice.service.d.ts +405 -25
  100. package/lib/services/cryptoservice.service.d.ts.map +1 -1
  101. package/lib/services/cryptoservice.service.js +779 -72
  102. package/lib/services/cryptoservice.service.js.map +1 -1
  103. package/lib/services/httpserver.service.d.ts.map +1 -1
  104. package/lib/services/httpserver.service.js +15 -9
  105. package/lib/services/httpserver.service.js.map +1 -1
  106. package/lib/services/icryptoservice.d.ts +11 -0
  107. package/lib/services/icryptoservice.d.ts.map +1 -1
  108. package/lib/services/icryptoservice.js +2 -0
  109. package/lib/services/icryptoservice.js.map +1 -1
  110. package/lib/services/mailer.service.d.ts +4 -0
  111. package/lib/services/mailer.service.d.ts.map +1 -1
  112. package/lib/services/mailer.service.js +7 -1
  113. package/lib/services/mailer.service.js.map +1 -1
  114. package/lib/services/operationstransport.d.ts +13 -0
  115. package/lib/services/operationstransport.d.ts.map +1 -1
  116. package/lib/services/operationstransport.js +28 -6
  117. package/lib/services/operationstransport.js.map +1 -1
  118. package/lib/services/pki.encrypter.d.ts +214 -0
  119. package/lib/services/pki.encrypter.d.ts.map +1 -0
  120. package/lib/services/pki.encrypter.js +350 -0
  121. package/lib/services/pki.encrypter.js.map +1 -0
  122. package/lib/services/secretscheck.d.ts +120 -0
  123. package/lib/services/secretscheck.d.ts.map +1 -0
  124. package/lib/services/secretscheck.js +322 -0
  125. package/lib/services/secretscheck.js.map +1 -0
  126. package/lib/services/servicecommands.d.ts +8 -4
  127. package/lib/services/servicecommands.d.ts.map +1 -1
  128. package/lib/services/servicecommands.js +14 -5
  129. package/lib/services/servicecommands.js.map +1 -1
  130. package/lib/session/manager.model.d.ts +3 -1
  131. package/lib/session/manager.model.d.ts.map +1 -1
  132. package/lib/session/manager.model.js +6 -3
  133. package/lib/session/manager.model.js.map +1 -1
  134. package/lib/test/objects.d.ts.map +1 -1
  135. package/lib/test/objects.js +3 -1
  136. package/lib/test/objects.js.map +1 -1
  137. package/package.json +12 -11
  138. package/webda.module.json +135 -41
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,11 @@
1
1
  import { TransformCaseType } from "@webda/utils";
2
2
  import { OperationsTransport, OperationsTransportParameters } from "../services/operationstransport.js";
3
3
  import { OperationDefinition } from "../core/icore.js";
4
+ import { useInstanceStorage } from "../core/instancestorage.js";
4
5
  import { WebContext } from "../contexts/webcontext.js";
6
+ import type { IncomingMessage, Server, ServerResponse } from "node:http";
7
+ import type { Duplex } from "node:stream";
8
+ import { type WebSocket } from "ws";
5
9
  import type { ModelClass } from "@webda/models";
6
10
  import type { ModelAction } from "../models/types.js";
7
11
  import type { ModelGraphBehaviorDefinition, ModelMetadata } from "@webda/compiler";
@@ -42,6 +46,31 @@ export declare class RESTOperationsTransportParameters extends OperationsTranspo
42
46
  * @default "PUT"
43
47
  */
44
48
  queryMethod?: "PUT" | "GET";
49
+ /**
50
+ * Largest message (bytes) a client may send on a bidirectional WebSocket operation; a bigger one closes the socket
51
+ *
52
+ * @default 1048576
53
+ */
54
+ webSocketMaxPayload?: number;
55
+ /**
56
+ * Most unconsumed messages a bidirectional operation may have queued; above it the socket closes with 4413
57
+ *
58
+ * @default 1000
59
+ */
60
+ webSocketMaxQueuedMessages?: number;
61
+ /**
62
+ * Most bytes of unconsumed messages a bidirectional operation may have queued; above it the socket closes with 4413
63
+ *
64
+ * @default 16777216
65
+ */
66
+ webSocketMaxQueuedBytes?: number;
67
+ /**
68
+ * Milliseconds between two keep-alive comments on an open `text/event-stream` response of a server-streaming
69
+ * operation (0 disables them)
70
+ *
71
+ * @default 20000
72
+ */
73
+ streamKeepAliveInterval?: number;
45
74
  /**
46
75
  * Load parameters with defaults
47
76
  * @param params - the service parameters
@@ -49,6 +78,13 @@ export declare class RESTOperationsTransportParameters extends OperationsTranspo
49
78
  */
50
79
  load(params?: any): this;
51
80
  }
81
+ /**
82
+ * Truncate a string to a number of UTF-8 bytes without cutting a character (a close reason holds 123 bytes)
83
+ * @param text - the text
84
+ * @param max - the maximum byte length
85
+ * @returns the truncated text
86
+ */
87
+ export declare function truncateUtf8(text: string, max: number): string;
52
88
  /**
53
89
  * REST transport that exposes model operations as RESTful HTTP routes.
54
90
  *
@@ -63,6 +99,13 @@ export declare class RESTOperationsTransport<T extends RESTOperationsTransportPa
63
99
  * OpenAPI cache
64
100
  */
65
101
  openapiContent: string;
102
+ /** Bidirectional operations served as WebSocket upgrades: path → operation id */
103
+ webSocketRoutes: Map<string, string>;
104
+ /** Shared WebSocket server (noServer: upgrades come from the HttpServer) */
105
+ private webSocketServer?;
106
+ /** HTTP servers the upgrade listener is already attached to */
107
+ private webSocketServers;
108
+ private webSocketEvents;
66
109
  /**
67
110
  * Transform name using configured casing
68
111
  * @param name - the name to transform
@@ -74,6 +117,45 @@ export declare class RESTOperationsTransport<T extends RESTOperationsTransportPa
74
117
  * @returns this for chaining
75
118
  */
76
119
  resolve(): this;
120
+ /**
121
+ * Find the bidirectional operation served on a request URL, the way the router matches HTTP paths: relative to the
122
+ * HttpContext prefix, without the query string, with or without the global route prefix
123
+ * @param uri - the request URL
124
+ * @param prefix - the HttpContext prefix (a gateway stage...)
125
+ * @returns the operation id, if any
126
+ */
127
+ webSocketOperationOf(uri: string, prefix?: string): string | undefined;
128
+ /**
129
+ * Serve the bidirectional operations' upgrades on an HTTP server (once per server, only if there are any)
130
+ * @param server - the server emitted by Webda.Init.Http
131
+ * @param storage - the instance storage to run requests in
132
+ */
133
+ protected attachWebSockets(server: Server, storage: ReturnType<typeof useInstanceStorage>): void;
134
+ /**
135
+ * Answer an upgrade with an HTTP error
136
+ * @param socket - the raw socket
137
+ * @param status - HTTP status
138
+ * @param message - reason phrase
139
+ */
140
+ private refuseUpgrade;
141
+ /**
142
+ * Check the caller like a normal request (Webda.Request, router request filters, permission) and accept the WebSocket
143
+ * @param opId - the bidirectional operation
144
+ * @param server - the HTTP server
145
+ * @param req - the upgrade request
146
+ * @param socket - the raw socket
147
+ * @param head - first packet of the upgraded stream
148
+ * @returns a promise settled once the upgrade is accepted or refused
149
+ */
150
+ protected upgradeOperation(opId: string, server: Server, req: IncomingMessage, socket: Duplex, head: Buffer): Promise<void>;
151
+ /**
152
+ * Run a bidirectional operation over an accepted WebSocket: messages in, chunks out
153
+ * @param opId - the operation
154
+ * @param httpContext - the upgrade request
155
+ * @param ws - the socket
156
+ * @param session - the session loaded when the upgrade was authorized
157
+ */
158
+ protected runWebSocketOperation(opId: string, httpContext: any, ws: WebSocket, session?: any): Promise<void>;
77
159
  /**
78
160
  * Override initTransport to walk the model tree instead of flat iteration.
79
161
  * For each model, we look up its operations and register REST routes.
@@ -85,6 +167,32 @@ export declare class RESTOperationsTransport<T extends RESTOperationsTransportPa
85
167
  * @param operations - the filtered operations map
86
168
  */
87
169
  protected exposeServiceOperations(operations: Record<string, OperationDefinition>): void;
170
+ /**
171
+ * The OpenAPI responses of a server-streaming operation
172
+ * @returns the 200 response with both stream formats
173
+ */
174
+ protected streamedResponses(): Record<string, any>;
175
+ /**
176
+ * Run an operation for a route: a server-streaming one is streamed to the client as its generator yields, any other
177
+ * is called and its result flushed with the response
178
+ * @param context - the request context
179
+ * @param operationId - the operation
180
+ * @returns a promise settled once the operation is done (a stream: once the response is over)
181
+ */
182
+ protected runOperation(context: WebContext, operationId: string): Promise<void>;
183
+ /**
184
+ * Stream a server-streaming operation: NDJSON, or server-sent events when the client accepts them
185
+ *
186
+ * The streaming context is built here, from the request's context, rather than by the HttpServer: only these routes
187
+ * need it, and permissions, validation and events still go through `callOperation` on the request's input. Until the
188
+ * first chunk an error is a normal HTTP error (thrown to the HttpServer); after it, the error is the last event or
189
+ * line of the stream.
190
+ * @param context - the request context
191
+ * @param operationId - the operation
192
+ * @param response - the HTTP response
193
+ * @returns a promise settled once the response is over
194
+ */
195
+ protected streamOperation(context: WebContext, operationId: string, response: ServerResponse): Promise<void>;
88
196
  /**
89
197
  * Recursively walk a model and its children, registering routes at each level
90
198
  * @param model - the model class
@@ -1 +1 @@
1
- {"version":3,"file":"restoperationstransport.service.d.ts","sourceRoot":"","sources":["../../src/rest/restoperationstransport.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"restoperationstransport.service.d.ts","sourceRoot":"","sources":["../../src/rest/restoperationstransport.service.ts"],"names":[],"mappings":"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"}
@@ -3,11 +3,28 @@ import { TransformCase } from "@webda/utils";
3
3
  import { OperationsTransport, OperationsTransportParameters } from "../services/operationstransport.js";
4
4
  import * as WebdaError from "../errors/errors.js";
5
5
  import { useRouter } from "./hooks.js";
6
- import { useApplication } from "../application/hooks.js";
6
+ import { useLog } from "@webda/workout";
7
+ import { HttpContext } from "../contexts/httpcontext.js";
8
+ import { runWithContext } from "../contexts/execution.js";
9
+ import { emitCoreEvent, useCoreEvents } from "../events/events.js";
10
+ import { useApplication, useParameters } from "../application/hooks.js";
7
11
  import { useCore, useModelMetadata } from "../core/hooks.js";
8
- import { useInstanceStorage } from "../core/instancestorage.js";
9
- import { callOperation } from "../core/operations.js";
12
+ import { runWithInstanceStorage, useInstanceStorage } from "../core/instancestorage.js";
13
+ import { callOperation, canCallOperation, getOperationStreaming } from "../core/operations.js";
14
+ import { AsyncQueue } from "../core/asyncqueue.js";
15
+ import { WebSocketOperationContext } from "../contexts/websocketcontext.js";
16
+ import { RestStreamingOperationContext } from "../contexts/restcontext.js";
17
+ import { WebSocketServer } from "ws";
10
18
  import { hasSchema } from "../schemas/hooks.js";
19
+ /**
20
+ * @param data - a raw WebSocket message
21
+ * @returns its length in bytes
22
+ */
23
+ function rawLength(data) {
24
+ if (Array.isArray(data))
25
+ return data.reduce((sum, chunk) => sum + chunk.length, 0);
26
+ return data instanceof ArrayBuffer ? data.byteLength : data.length;
27
+ }
11
28
  /**
12
29
  * Swagger static html
13
30
  */
@@ -87,6 +104,31 @@ export class RESTOperationsTransportParameters extends OperationsTransportParame
87
104
  * @default "PUT"
88
105
  */
89
106
  queryMethod;
107
+ /**
108
+ * Largest message (bytes) a client may send on a bidirectional WebSocket operation; a bigger one closes the socket
109
+ *
110
+ * @default 1048576
111
+ */
112
+ webSocketMaxPayload;
113
+ /**
114
+ * Most unconsumed messages a bidirectional operation may have queued; above it the socket closes with 4413
115
+ *
116
+ * @default 1000
117
+ */
118
+ webSocketMaxQueuedMessages;
119
+ /**
120
+ * Most bytes of unconsumed messages a bidirectional operation may have queued; above it the socket closes with 4413
121
+ *
122
+ * @default 16777216
123
+ */
124
+ webSocketMaxQueuedBytes;
125
+ /**
126
+ * Milliseconds between two keep-alive comments on an open `text/event-stream` response of a server-streaming
127
+ * operation (0 disables them)
128
+ *
129
+ * @default 20000
130
+ */
131
+ streamKeepAliveInterval;
90
132
  /**
91
133
  * Load parameters with defaults
92
134
  * @param params - the service parameters
@@ -96,6 +138,10 @@ export class RESTOperationsTransportParameters extends OperationsTransportParame
96
138
  super.load(params);
97
139
  this.nameTransformer ??= "camelCase";
98
140
  this.queryMethod ??= "PUT";
141
+ this.webSocketMaxPayload ??= 1024 * 1024;
142
+ this.webSocketMaxQueuedMessages ??= 1000;
143
+ this.webSocketMaxQueuedBytes ??= 16 * 1024 * 1024;
144
+ this.streamKeepAliveInterval ??= 20000;
99
145
  // Ensure url ends with /
100
146
  if (this.url && !this.url.endsWith("/")) {
101
147
  this.url += "/";
@@ -103,6 +149,21 @@ export class RESTOperationsTransportParameters extends OperationsTransportParame
103
149
  return this;
104
150
  }
105
151
  }
152
+ /**
153
+ * Truncate a string to a number of UTF-8 bytes without cutting a character (a close reason holds 123 bytes)
154
+ * @param text - the text
155
+ * @param max - the maximum byte length
156
+ * @returns the truncated text
157
+ */
158
+ export function truncateUtf8(text, max) {
159
+ const buffer = Buffer.from(text, "utf8");
160
+ if (buffer.length <= max)
161
+ return text;
162
+ let end = max;
163
+ while (end > 0 && (buffer[end] & 0xc0) === 0x80)
164
+ end--;
165
+ return buffer.subarray(0, end).toString("utf8");
166
+ }
106
167
  /**
107
168
  * REST transport that exposes model operations as RESTful HTTP routes.
108
169
  *
@@ -117,6 +178,13 @@ export class RESTOperationsTransport extends OperationsTransport {
117
178
  * OpenAPI cache
118
179
  */
119
180
  openapiContent;
181
+ /** Bidirectional operations served as WebSocket upgrades: path → operation id */
182
+ webSocketRoutes = new Map();
183
+ /** Shared WebSocket server (noServer: upgrades come from the HttpServer) */
184
+ webSocketServer;
185
+ /** HTTP servers the upgrade listener is already attached to */
186
+ webSocketServers = new WeakSet();
187
+ webSocketEvents = false;
120
188
  /**
121
189
  * Transform name using configured casing
122
190
  * @param name - the name to transform
@@ -135,8 +203,162 @@ export class RESTOperationsTransport extends OperationsTransport {
135
203
  if (this.parameters.exposeOpenAPI) {
136
204
  this.addRoute(".", ["GET"], this.openapi, { hidden: true });
137
205
  }
206
+ // Bidirectional operations: accept WebSocket upgrades on every HTTP server once it listens
207
+ if (!this.webSocketEvents) {
208
+ this.webSocketEvents = true;
209
+ const storage = useInstanceStorage();
210
+ useCoreEvents("Webda.Init.Http", (server) => this.attachWebSockets(server, storage));
211
+ }
138
212
  return this;
139
213
  }
214
+ /**
215
+ * Find the bidirectional operation served on a request URL, the way the router matches HTTP paths: relative to the
216
+ * HttpContext prefix, without the query string, with or without the global route prefix
217
+ * @param uri - the request URL
218
+ * @param prefix - the HttpContext prefix (a gateway stage...)
219
+ * @returns the operation id, if any
220
+ */
221
+ webSocketOperationOf(uri, prefix = "") {
222
+ const context = new HttpContext("localhost", "GET", uri);
223
+ if (prefix)
224
+ context.setPrefix(prefix);
225
+ let path = context.getRelativeUri().split("?")[0];
226
+ const routePrefix = useParameters().routePrefix || "";
227
+ if (routePrefix && path.startsWith(routePrefix))
228
+ path = path.substring(routePrefix.length) || "/";
229
+ return this.webSocketRoutes.get(path);
230
+ }
231
+ /**
232
+ * Serve the bidirectional operations' upgrades on an HTTP server (once per server, only if there are any)
233
+ * @param server - the server emitted by Webda.Init.Http
234
+ * @param storage - the instance storage to run requests in
235
+ */
236
+ attachWebSockets(server, storage) {
237
+ if (this.webSocketRoutes.size === 0 || this.webSocketServers.has(server))
238
+ return;
239
+ this.webSocketServers.add(server);
240
+ server.on("upgrade", (req, socket, head) => {
241
+ runWithInstanceStorage(storage, () => {
242
+ const opId = this.webSocketOperationOf(req.url ?? "/");
243
+ // Not ours (GraphQL subscriptions...): leave the socket to the other listeners
244
+ if (!opId)
245
+ return;
246
+ this.upgradeOperation(opId, server, req, socket, head).catch(err => {
247
+ useLog("ERROR", "WebSocket upgrade failed", opId, err);
248
+ socket.destroy();
249
+ });
250
+ });
251
+ });
252
+ }
253
+ /**
254
+ * Answer an upgrade with an HTTP error
255
+ * @param socket - the raw socket
256
+ * @param status - HTTP status
257
+ * @param message - reason phrase
258
+ */
259
+ refuseUpgrade(socket, status, message) {
260
+ socket.end(`HTTP/1.1 ${status} ${message}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
261
+ }
262
+ /**
263
+ * Check the caller like a normal request (Webda.Request, router request filters, permission) and accept the WebSocket
264
+ * @param opId - the bidirectional operation
265
+ * @param server - the HTTP server
266
+ * @param req - the upgrade request
267
+ * @param socket - the raw socket
268
+ * @param head - first packet of the upgraded stream
269
+ * @returns a promise settled once the upgrade is accepted or refused
270
+ */
271
+ async upgradeOperation(opId, server, req, socket, head) {
272
+ const http = Object.values(useCore().getServices()).find(s => s.server === server && typeof s.getContextFromRequest === "function");
273
+ const base = http && (await http.getContextFromRequest(req));
274
+ if (!base)
275
+ return this.refuseUpgrade(socket, 400, "Bad Request");
276
+ await base.init();
277
+ const allowed = await runWithContext(base, async () => {
278
+ try {
279
+ emitCoreEvent("Webda.Request", { context: base });
280
+ }
281
+ catch {
282
+ // listener error
283
+ }
284
+ return (await useRouter().checkRequest(base)) && canCallOperation(base, opId);
285
+ });
286
+ if (!allowed)
287
+ return this.refuseUpgrade(socket, 403, "Forbidden");
288
+ const storage = useInstanceStorage();
289
+ this.webSocketServer ??= new WebSocketServer({
290
+ noServer: true,
291
+ maxPayload: this.parameters.webSocketMaxPayload
292
+ });
293
+ this.webSocketServer.handleUpgrade(req, socket, head, ws => {
294
+ runWithInstanceStorage(storage, () => {
295
+ this.runWebSocketOperation(opId, base.getHttpContext(), ws, base.getSession()).catch(err => {
296
+ useLog("ERROR", "WebSocket operation failed", opId, err);
297
+ ws.close(1011, "Internal server error");
298
+ });
299
+ });
300
+ });
301
+ }
302
+ /**
303
+ * Run a bidirectional operation over an accepted WebSocket: messages in, chunks out
304
+ * @param opId - the operation
305
+ * @param httpContext - the upgrade request
306
+ * @param ws - the socket
307
+ * @param session - the session loaded when the upgrade was authorized
308
+ */
309
+ async runWebSocketOperation(opId, httpContext, ws, session) {
310
+ const ctx = new WebSocketOperationContext(httpContext, ws, session);
311
+ await ctx.init();
312
+ const input = new AsyncQueue();
313
+ ctx.setExtension("operationInputStream", input);
314
+ const maxQueued = this.parameters.webSocketMaxQueuedMessages;
315
+ const maxQueuedBytes = this.parameters.webSocketMaxQueuedBytes;
316
+ ws.on("message", data => {
317
+ let message;
318
+ try {
319
+ message = JSON.parse(String(data));
320
+ }
321
+ catch {
322
+ ws.close(4400, "Invalid JSON message");
323
+ return;
324
+ }
325
+ input.push(message, rawLength(data));
326
+ if (input.pending > maxQueued || input.pendingBytes > maxQueuedBytes) {
327
+ // The operation does not keep up: stop it rather than buffering without limit
328
+ const bytes = input.pendingBytes > maxQueuedBytes;
329
+ ctx.cancel();
330
+ input.end(true);
331
+ ws.close(4413, bytes ? "Too many queued bytes" : "Too many queued messages");
332
+ }
333
+ });
334
+ ws.on("error", err => useLog("DEBUG", "WebSocket error", opId, err));
335
+ // The client is gone: the operation's writes throw and its pending read ends so its finally blocks run
336
+ ws.on("close", () => {
337
+ ctx.cancel();
338
+ input.end();
339
+ });
340
+ try {
341
+ await callOperation(ctx, opId);
342
+ if (ctx.isCancelled)
343
+ return;
344
+ const output = ctx.getExtension("operationStreaming") ? undefined : ctx.getOutput();
345
+ if (output)
346
+ ws.send(output);
347
+ ws.close(1000);
348
+ }
349
+ catch (err) {
350
+ // Only a client that went away is a silent cancel: an operation raising it while connected is an error
351
+ if (ctx.isCancelled)
352
+ return;
353
+ const status = typeof err?.getResponseCode === "function" ? err.getResponseCode() : undefined;
354
+ if (status >= 400 && status < 500) {
355
+ ws.close(4000 + status, truncateUtf8(String(err.message ?? "Error"), 123));
356
+ return;
357
+ }
358
+ useLog("ERROR", `[WebSocket ${opId}] operation threw:`, err);
359
+ ws.close(status ? 4000 + status : 1011, "Internal server error");
360
+ }
361
+ }
140
362
  /**
141
363
  * Override initTransport to walk the model tree instead of flat iteration.
142
364
  * For each model, we look up its operations and register REST routes.
@@ -187,6 +409,9 @@ export class RESTOperationsTransport extends OperationsTransport {
187
409
  for (const [opId, op] of Object.entries(operations)) {
188
410
  if (op.hidden)
189
411
  continue;
412
+ // A client stream cannot be carried by a plain HTTP request: reachable over gRPC only
413
+ if (getOperationStreaming(op) === "client")
414
+ continue;
190
415
  // Skip if this operation was already handled by model tree walk
191
416
  if (op.context?.model)
192
417
  continue;
@@ -202,18 +427,121 @@ export class RESTOperationsTransport extends OperationsTransport {
202
427
  path = `${this.parameters.url}${opId.toLowerCase().replace(/\./g, "/")}`;
203
428
  methods = ["PUT"];
204
429
  }
430
+ if (getOperationStreaming(op) === "bidi") {
431
+ // Served as a WebSocket upgrade on this path; a plain request cannot carry a bidirectional stream
432
+ if (path.includes("{")) {
433
+ this.log("WARN", `${opId} is bidirectional but its path ${path} has parameters: not served over WebSocket`);
434
+ }
435
+ else {
436
+ this.webSocketRoutes.set(path, opId);
437
+ }
438
+ this.addRoute(path, [...new Set(["GET", ...methods])], async () => {
439
+ throw new WebdaError.HttpError("Upgrade Required: open a WebSocket on this URL", 426);
440
+ }, { get: { tags: op.tags || [], summary: `${op.summary || opId} (WebSocket)`, operationId: opId } });
441
+ continue;
442
+ }
205
443
  const openapi = {
206
444
  [methods[0].toLowerCase()]: {
207
445
  tags: op.tags || [],
208
446
  summary: op.summary || opId,
209
- operationId: opId
447
+ operationId: opId,
448
+ ...(getOperationStreaming(op) === "server" ? { responses: this.streamedResponses() } : {})
210
449
  }
211
450
  };
212
451
  this.addRoute(path, methods, async (context) => {
213
- await callOperation(context, opId);
452
+ await this.runOperation(context, opId);
214
453
  }, openapi);
215
454
  }
216
455
  }
456
+ /**
457
+ * The OpenAPI responses of a server-streaming operation
458
+ * @returns the 200 response with both stream formats
459
+ */
460
+ streamedResponses() {
461
+ return {
462
+ "200": {
463
+ description: "Stream of chunks: one JSON line each (application/x-ndjson), or one `data:` event each when `Accept` is " +
464
+ "text/event-stream, ended by an `end` event. An error after the first chunk is a last `error` event or " +
465
+ '`{"error": {"message", "code"}}` line.',
466
+ content: {
467
+ "application/x-ndjson": { schema: { type: "string", description: "One JSON value per line" } },
468
+ "text/event-stream": { schema: { type: "string", description: "Server-sent events with a JSON `data`" } }
469
+ }
470
+ }
471
+ };
472
+ }
473
+ /**
474
+ * Run an operation for a route: a server-streaming one is streamed to the client as its generator yields, any other
475
+ * is called and its result flushed with the response
476
+ * @param context - the request context
477
+ * @param operationId - the operation
478
+ * @returns a promise settled once the operation is done (a stream: once the response is over)
479
+ */
480
+ async runOperation(context, operationId) {
481
+ const response = context._stream;
482
+ if (getOperationStreaming(useInstanceStorage().operations?.[operationId]) !== "server" ||
483
+ typeof response?.writeHead !== "function") {
484
+ return callOperation(context, operationId);
485
+ }
486
+ return this.streamOperation(context, operationId, response);
487
+ }
488
+ /**
489
+ * Stream a server-streaming operation: NDJSON, or server-sent events when the client accepts them
490
+ *
491
+ * The streaming context is built here, from the request's context, rather than by the HttpServer: only these routes
492
+ * need it, and permissions, validation and events still go through `callOperation` on the request's input. Until the
493
+ * first chunk an error is a normal HTTP error (thrown to the HttpServer); after it, the error is the last event or
494
+ * line of the stream.
495
+ * @param context - the request context
496
+ * @param operationId - the operation
497
+ * @param response - the HTTP response
498
+ * @returns a promise settled once the response is over
499
+ */
500
+ async streamOperation(context, operationId, response) {
501
+ const accept = context.getHttpContext().getUniqueHeader("accept", "") ?? "";
502
+ const format = accept.includes("text/event-stream") ? "sse" : "ndjson";
503
+ const stream = new RestStreamingOperationContext(context.getHttpContext(), response, format, context.getSession(), this.parameters.streamKeepAliveInterval, context.getResponseHeaders(), context.getSetCookieHeaders());
504
+ await stream.init();
505
+ stream.setParameters(context.getParameters());
506
+ stream.startKeepAlive();
507
+ try {
508
+ await callOperation(stream, operationId);
509
+ }
510
+ catch (err) {
511
+ stream.stopKeepAlive();
512
+ // The client went away: nothing to tell
513
+ if (stream.isCancelled || stream.disconnected)
514
+ return stream.finish();
515
+ // Nothing sent yet: a normal HTTP error response
516
+ if (!stream.hasStarted) {
517
+ stream.abortBeforeStart();
518
+ throw err;
519
+ }
520
+ const status = typeof err?.getResponseCode === "function" ? err.getResponseCode() : undefined;
521
+ const clientError = status >= 400 && status < 500;
522
+ if (!clientError)
523
+ useLog("ERROR", `[REST ${operationId}] streamed operation threw:`, err);
524
+ await stream.writeError(clientError ? String(err.message ?? "Error") : "Internal server error", status || 500);
525
+ return stream.finish();
526
+ }
527
+ stream.stopKeepAlive();
528
+ if (stream.isCancelled)
529
+ return stream.finish();
530
+ if (!stream.getExtension("operationStreaming")) {
531
+ // Not a generator after all: a normal response, with the status and headers the operation set
532
+ if (stream.statusCode && stream.statusCode !== 204)
533
+ context.statusCode = stream.statusCode;
534
+ for (const [name, value] of Object.entries(stream.getResponseHeaders()))
535
+ context.setHeader(name, value);
536
+ const output = stream.getOutput();
537
+ if (output !== undefined) {
538
+ context.setHeader("Content-type", "application/json");
539
+ context.write(output);
540
+ }
541
+ return;
542
+ }
543
+ return stream.finish(true);
544
+ }
217
545
  /**
218
546
  * Recursively walk a model and its children, registering routes at each level
219
547
  * @param model - the model class
@@ -637,6 +965,13 @@ export class RESTOperationsTransport extends OperationsTransport {
637
965
  ...(action.openapi?.[method.toLowerCase()] ?? {})
638
966
  };
639
967
  });
968
+ if (getOperationStreaming(useInstanceStorage().operations?.[operationId]) === "server") {
969
+ Object.keys(openapi)
970
+ .filter(k => ["get", "post", "put", "patch", "delete"].includes(k))
971
+ .forEach(k => {
972
+ openapi[k].responses = { ...this.streamedResponses(), ...openapi[k].responses };
973
+ });
974
+ }
640
975
  if (hasSchema(`${identifier}.${actionName}.input`)) {
641
976
  Object.keys(openapi)
642
977
  .filter(k => ["get", "post", "put", "patch", "delete"].includes(k))
@@ -652,7 +987,8 @@ export class RESTOperationsTransport extends OperationsTransport {
652
987
  };
653
988
  });
654
989
  }
655
- if (hasSchema(`${identifier}.${actionName}.output`)) {
990
+ if (hasSchema(`${identifier}.${actionName}.output`) &&
991
+ getOperationStreaming(useInstanceStorage().operations?.[operationId]) !== "server") {
656
992
  Object.keys(openapi)
657
993
  .filter(k => ["get", "post", "put", "patch", "delete"].includes(k))
658
994
  .forEach(k => {
@@ -672,7 +1008,7 @@ export class RESTOperationsTransport extends OperationsTransport {
672
1008
  context.getParameters()[injectAttribute] = context.parameter(`pid.${depth - 1}`);
673
1009
  context.getParameters()[`pid.${depth - 1}`] = undefined;
674
1010
  }
675
- await callOperation(context, operationId);
1011
+ await this.runOperation(context, operationId);
676
1012
  }, openapi);
677
1013
  }
678
1014
  /**
@@ -745,7 +1081,10 @@ export class RESTOperationsTransport extends OperationsTransport {
745
1081
  }
746
1082
  };
747
1083
  }
748
- if (hasSchema(outputSchema)) {
1084
+ if (getOperationStreaming(op) === "server") {
1085
+ openapi[methodKey].responses = this.streamedResponses();
1086
+ }
1087
+ else if (hasSchema(outputSchema)) {
749
1088
  openapi[methodKey].responses = {
750
1089
  "200": {
751
1090
  description: "Operation success",
@@ -759,7 +1098,7 @@ export class RESTOperationsTransport extends OperationsTransport {
759
1098
  }
760
1099
  };
761
1100
  }
762
- this.addRoute(fullPath, [httpMethod], async (context) => callOperation(context, operationId), openapi);
1101
+ this.addRoute(fullPath, [httpMethod], async (context) => this.runOperation(context, operationId), openapi);
763
1102
  });
764
1103
  }
765
1104
  /**