@webda/grpc 4.0.0-beta.6 → 4.0.0-beta.8
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/.webda/cache +4 -4
- package/CHANGELOG.md +43 -0
- package/README.md +24 -10
- package/lib/grpc-context.d.ts +55 -0
- package/lib/grpc-context.d.ts.map +1 -0
- package/lib/grpc-context.js +83 -0
- package/lib/grpc-context.js.map +1 -0
- package/lib/grpc-stream.d.ts +13 -4
- package/lib/grpc-stream.d.ts.map +1 -1
- package/lib/grpc-stream.js +67 -23
- package/lib/grpc-stream.js.map +1 -1
- package/lib/grpcservice.service.d.ts +43 -25
- package/lib/grpcservice.service.d.ts.map +1 -1
- package/lib/grpcservice.service.js +157 -170
- package/lib/grpcservice.service.js.map +1 -1
- package/lib/proto-generator.js +1 -1
- package/lib/proto-generator.js.map +1 -1
- package/package.json +8 -5
- package/src/connect.e2e.spec.ts +114 -0
- package/src/grpc-context.ts +93 -0
- package/src/grpc-live.spec.ts +377 -0
- package/src/grpc-stream.spec.ts +122 -1
- package/src/grpc-stream.ts +64 -25
- package/src/grpcservice.service.ts +169 -199
- package/src/grpcservice.spec.ts +29 -442
- package/src/proto-generator.spec.ts +17 -0
- package/src/proto-generator.ts +1 -1
- package/src/query-statements.spec.ts +39 -13
- package/test/fixture.ts +166 -0
- package/tsconfig.json +1 -1
- package/webda.module.json +10 -0
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AsyncQueue,
|
|
3
|
+
BuildCommand,
|
|
4
|
+
HttpContext,
|
|
5
|
+
OperationDefinition,
|
|
2
6
|
OperationsTransport,
|
|
3
7
|
OperationsTransportParameters,
|
|
4
|
-
|
|
8
|
+
WebdaError,
|
|
5
9
|
callOperation,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
emitCoreEvent,
|
|
11
|
+
getOperationStreaming,
|
|
12
|
+
runWithContext,
|
|
13
|
+
runWithInstanceStorage,
|
|
9
14
|
useApplication,
|
|
10
15
|
useCore,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
runWithInstanceStorage,
|
|
14
|
-
useInstanceStorage
|
|
16
|
+
useInstanceStorage,
|
|
17
|
+
useRouter
|
|
15
18
|
} from "@webda/core";
|
|
16
19
|
import { useLog } from "@webda/workout";
|
|
17
20
|
import * as protoLoader from "@grpc/proto-loader";
|
|
18
21
|
import { existsSync, writeFileSync, mkdirSync } from "node:fs";
|
|
19
22
|
import { join, dirname } from "node:path";
|
|
23
|
+
import { GrpcOperationContext, toGrpcMessage } from "./grpc-context.js";
|
|
20
24
|
import { GrpcStream, GrpcStatus } from "./grpc-stream.js";
|
|
21
25
|
import { generateProto } from "./proto-generator.js";
|
|
22
26
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
@@ -29,13 +33,29 @@ export class GrpcServiceParameters extends OperationsTransportParameters {
|
|
|
29
33
|
* Path to the generated .proto file
|
|
30
34
|
* @default ".webda/app.proto"
|
|
31
35
|
*/
|
|
32
|
-
protoFile
|
|
36
|
+
protoFile: string;
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
39
|
* Protobuf package name
|
|
36
40
|
* @default "webda"
|
|
37
41
|
*/
|
|
38
|
-
packageName
|
|
42
|
+
packageName: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Most unconsumed messages a client or bidirectional operation may have queued; above it the call fails with
|
|
46
|
+
* RESOURCE_EXHAUSTED
|
|
47
|
+
*
|
|
48
|
+
* @default 1000
|
|
49
|
+
*/
|
|
50
|
+
maxQueuedMessages: number;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Most bytes of unconsumed messages a client or bidirectional operation may have queued; above it the call fails
|
|
54
|
+
* with RESOURCE_EXHAUSTED
|
|
55
|
+
*
|
|
56
|
+
* @default 16777216
|
|
57
|
+
*/
|
|
58
|
+
maxQueuedBytes: number;
|
|
39
59
|
|
|
40
60
|
/**
|
|
41
61
|
* Load and apply default parameter values.
|
|
@@ -46,6 +66,8 @@ export class GrpcServiceParameters extends OperationsTransportParameters {
|
|
|
46
66
|
super.load(params);
|
|
47
67
|
this.protoFile ??= ".webda/app.proto";
|
|
48
68
|
this.packageName ??= "webda";
|
|
69
|
+
this.maxQueuedMessages ??= 1000;
|
|
70
|
+
this.maxQueuedBytes ??= 16 * 1024 * 1024;
|
|
49
71
|
return this;
|
|
50
72
|
}
|
|
51
73
|
}
|
|
@@ -61,9 +83,7 @@ export class GrpcServiceParameters extends OperationsTransportParameters {
|
|
|
61
83
|
*
|
|
62
84
|
* @WebdaModda
|
|
63
85
|
*/
|
|
64
|
-
export class GrpcService<
|
|
65
|
-
T extends GrpcServiceParameters = GrpcServiceParameters
|
|
66
|
-
> extends OperationsTransport<T> {
|
|
86
|
+
export class GrpcService<T extends GrpcServiceParameters = GrpcServiceParameters> extends OperationsTransport<T> {
|
|
67
87
|
/** Loaded protobuf definitions */
|
|
68
88
|
private definitions: protoLoader.PackageDefinition;
|
|
69
89
|
/** Map of gRPC path → operation ID */
|
|
@@ -71,6 +91,14 @@ export class GrpcService<
|
|
|
71
91
|
/** Map of gRPC path → method definition */
|
|
72
92
|
private rpcMethods: Map<string, protoLoader.MethodDefinition<any, any>> = new Map();
|
|
73
93
|
|
|
94
|
+
/**
|
|
95
|
+
* @param params - raw service parameters
|
|
96
|
+
* @returns the loaded parameters (with the operations include/exclude filter)
|
|
97
|
+
*/
|
|
98
|
+
static createConfiguration(params: any = {}): GrpcServiceParameters {
|
|
99
|
+
return new GrpcServiceParameters().load(params);
|
|
100
|
+
}
|
|
101
|
+
|
|
74
102
|
/**
|
|
75
103
|
* Generate a .proto file from the current operation registry.
|
|
76
104
|
*
|
|
@@ -124,6 +152,35 @@ export class GrpcService<
|
|
|
124
152
|
return prefixes.size;
|
|
125
153
|
}
|
|
126
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Load the .proto file and map its RPC methods to operations (also callable after registering operations late)
|
|
157
|
+
*/
|
|
158
|
+
loadDefinitions(): void {
|
|
159
|
+
const protoFile = this.parameters.protoFile;
|
|
160
|
+
if (!existsSync(protoFile)) {
|
|
161
|
+
useLog("WARN", `Proto file not found: ${this.parameters.protoFile}. Run 'webdac build' to generate it.`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
this.definitions = protoLoader.loadSync(protoFile, {
|
|
166
|
+
keepCase: true,
|
|
167
|
+
longs: String,
|
|
168
|
+
enums: String,
|
|
169
|
+
defaults: true,
|
|
170
|
+
oneofs: true
|
|
171
|
+
});
|
|
172
|
+
this.rpcToOperation.clear();
|
|
173
|
+
this.rpcMethods.clear();
|
|
174
|
+
this.buildRpcMap();
|
|
175
|
+
useLog(
|
|
176
|
+
"INFO",
|
|
177
|
+
`Loaded gRPC definitions from ${this.parameters.protoFile} — ${this.rpcToOperation.size} RPC methods mapped`
|
|
178
|
+
);
|
|
179
|
+
} catch (err: any) {
|
|
180
|
+
useLog("WARN", `Failed to load proto file: ${err.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
127
184
|
/**
|
|
128
185
|
* Initialize the gRPC transport: load proto, build RPC map, hook into HTTP/2.
|
|
129
186
|
* @returns a promise resolving to this service instance once initialization is complete
|
|
@@ -131,30 +188,7 @@ export class GrpcService<
|
|
|
131
188
|
async init(): Promise<this> {
|
|
132
189
|
await super.init();
|
|
133
190
|
|
|
134
|
-
|
|
135
|
-
if (existsSync(this.parameters.protoFile)) {
|
|
136
|
-
try {
|
|
137
|
-
this.definitions = protoLoader.loadSync(this.parameters.protoFile, {
|
|
138
|
-
keepCase: true,
|
|
139
|
-
longs: String,
|
|
140
|
-
enums: String,
|
|
141
|
-
defaults: true,
|
|
142
|
-
oneofs: true
|
|
143
|
-
});
|
|
144
|
-
this.buildRpcMap();
|
|
145
|
-
useLog(
|
|
146
|
-
"INFO",
|
|
147
|
-
`Loaded gRPC definitions from ${this.parameters.protoFile} — ${this.rpcToOperation.size} RPC methods mapped`
|
|
148
|
-
);
|
|
149
|
-
} catch (err: any) {
|
|
150
|
-
useLog("WARN", `Failed to load proto file: ${err.message}`);
|
|
151
|
-
}
|
|
152
|
-
} else {
|
|
153
|
-
useLog(
|
|
154
|
-
"WARN",
|
|
155
|
-
`Proto file not found: ${this.parameters.protoFile}. Run 'webdac build' to generate it.`
|
|
156
|
-
);
|
|
157
|
-
}
|
|
191
|
+
this.loadDefinitions();
|
|
158
192
|
|
|
159
193
|
// Plug the gRPC dispatcher into every HttpServer instance in the app.
|
|
160
194
|
// Serving both REST (HTTP/1.1 or TLS+ALPN) and plaintext gRPC (h2c) means
|
|
@@ -226,7 +260,21 @@ export class GrpcService<
|
|
|
226
260
|
}
|
|
227
261
|
|
|
228
262
|
/**
|
|
229
|
-
*
|
|
263
|
+
* The request line and metadata of a gRPC call as an HttpContext (HTTP/2 pseudo-headers removed)
|
|
264
|
+
* @param req - the HTTP/2 request
|
|
265
|
+
* @returns the http context
|
|
266
|
+
*/
|
|
267
|
+
private httpContextOf(req: IncomingMessage): HttpContext {
|
|
268
|
+
const raw = req.headers as Record<string, string | string[]>;
|
|
269
|
+
const authority = (raw[":authority"] as string) || (raw.host as string) || "localhost";
|
|
270
|
+
const [hostname, port] = authority.split(":");
|
|
271
|
+
const headers = Object.fromEntries(Object.entries(raw).filter(([key]) => !key.startsWith(":")));
|
|
272
|
+
return new HttpContext(hostname, "POST", req.url || "/", "http", port || "80", headers as any);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Handle an incoming gRPC request: one path for the four modes. Incoming messages feed the operation (as its
|
|
277
|
+
* input, or as the stream in `operationInputStream`), streamed output is sent as it is produced.
|
|
230
278
|
*
|
|
231
279
|
* Called by the HttpServer when content-type is application/grpc.
|
|
232
280
|
*
|
|
@@ -235,7 +283,7 @@ export class GrpcService<
|
|
|
235
283
|
* @returns a promise that resolves when the response has been fully sent
|
|
236
284
|
*/
|
|
237
285
|
async handleGrpcRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
238
|
-
const grpcPath = req.url;
|
|
286
|
+
const grpcPath = req.url ?? "";
|
|
239
287
|
const opId = this.rpcToOperation.get(grpcPath);
|
|
240
288
|
const methodDef = this.rpcMethods.get(grpcPath);
|
|
241
289
|
|
|
@@ -250,179 +298,101 @@ export class GrpcService<
|
|
|
250
298
|
}
|
|
251
299
|
|
|
252
300
|
const stream = new GrpcStream(req, res, methodDef);
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
const
|
|
301
|
+
const streaming = getOperationStreaming(this.getOperations()[opId]);
|
|
302
|
+
const ctx = new GrpcOperationContext(this.httpContextOf(req), stream, res);
|
|
303
|
+
const input = new AsyncQueue<unknown>();
|
|
304
|
+
// Handlers first: data may arrive while the session loads
|
|
305
|
+
stream.onMessage((message, size) => {
|
|
306
|
+
input.push(this.cleanMessage(message), size);
|
|
307
|
+
const bytes = input.pendingBytes > this.parameters.maxQueuedBytes;
|
|
308
|
+
if ((bytes || input.pending > this.parameters.maxQueuedMessages) && !finished()) {
|
|
309
|
+
// The operation does not keep up: stop it rather than buffering without limit
|
|
310
|
+
stream.sendError(GrpcStatus.RESOURCE_EXHAUSTED, bytes ? "Too many queued bytes" : "Too many queued messages");
|
|
311
|
+
ctx.cancel();
|
|
312
|
+
input.end(true);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
stream.onEnd(() => input.end());
|
|
316
|
+
stream.onCancel(() => {
|
|
317
|
+
ctx.cancel();
|
|
318
|
+
input.end();
|
|
319
|
+
});
|
|
320
|
+
// Whatever ends the response (client gone, or the stream failing on a bad frame): stop the operation
|
|
321
|
+
res.on("close", () => {
|
|
322
|
+
ctx.cancel();
|
|
323
|
+
input.end();
|
|
324
|
+
});
|
|
325
|
+
// The stream may fail itself (bad frame, ...): it then ends the response, nothing more must be written
|
|
326
|
+
const finished = () => ctx.isCancelled || (res as any).writableEnded;
|
|
256
327
|
|
|
257
328
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
329
|
+
await ctx.init();
|
|
330
|
+
const allowed = await runWithContext(ctx, async () => {
|
|
331
|
+
try {
|
|
332
|
+
emitCoreEvent("Webda.Request", { context: ctx });
|
|
333
|
+
} catch {
|
|
334
|
+
// listener error
|
|
335
|
+
}
|
|
336
|
+
return useRouter().checkRequest(ctx);
|
|
337
|
+
});
|
|
338
|
+
if (!allowed) {
|
|
339
|
+
if (!finished()) stream.sendError(GrpcStatus.PERMISSION_DENIED, "Forbidden");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (streaming === "client" || streaming === "bidi") {
|
|
343
|
+
ctx.setExtension("operationInputStream", input);
|
|
344
|
+
} else {
|
|
345
|
+
const first = await input.next();
|
|
346
|
+
if (finished()) return;
|
|
347
|
+
ctx.setMessage(first.done ? {} : first.value);
|
|
348
|
+
}
|
|
349
|
+
if (finished()) return;
|
|
350
|
+
await callOperation(ctx, opId);
|
|
351
|
+
if (finished()) return;
|
|
352
|
+
if (ctx.getExtension("operationStreaming")) {
|
|
353
|
+
stream.end(GrpcStatus.OK);
|
|
264
354
|
} else {
|
|
265
|
-
|
|
266
|
-
await this.handleUnary(stream, opId, methodDef);
|
|
355
|
+
stream.sendUnary(this.unaryResponse(ctx.getOutput()));
|
|
267
356
|
}
|
|
268
|
-
} catch (err) {
|
|
269
|
-
|
|
270
|
-
|
|
357
|
+
} catch (err: any) {
|
|
358
|
+
if (finished()) return;
|
|
359
|
+
if (err instanceof WebdaError.OperationCancelledError) {
|
|
360
|
+
// Raised by the operation itself while the client is still there
|
|
361
|
+
stream.sendError(GrpcStatus.CANCELLED, err.message || "Operation cancelled");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (!(err?.getResponseCode?.() < 500)) useLog("ERROR", `[gRPC ${opId}] handler threw:`, err);
|
|
365
|
+
// Only client errors (4xx) carry their message: anything else may leak internals
|
|
366
|
+
const code = err?.getResponseCode?.();
|
|
367
|
+
const visible = typeof code === "number" && code >= 400 && code < 500;
|
|
368
|
+
stream.sendError(this.errorToGrpcStatus(err), visible ? err.message || "Bad request" : "Internal server error");
|
|
271
369
|
}
|
|
272
370
|
}
|
|
273
371
|
|
|
274
372
|
/**
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* @param
|
|
278
|
-
* @
|
|
279
|
-
* @returns a promise that resolves when the unary response has been sent
|
|
280
|
-
*/
|
|
281
|
-
private handleUnary(
|
|
282
|
-
stream: GrpcStream,
|
|
283
|
-
opId: string,
|
|
284
|
-
methodDef: protoLoader.MethodDefinition<any, any>
|
|
285
|
-
): Promise<void> {
|
|
286
|
-
// AsyncLocalStorage doesn't always propagate through the HTTP/2 'data'
|
|
287
|
-
// event that drives GrpcStream.onMessage, so capture the storage here and
|
|
288
|
-
// re-enter it inside the callback.
|
|
289
|
-
const instanceStorage = useInstanceStorage();
|
|
290
|
-
return new Promise<void>((resolve, reject) => {
|
|
291
|
-
stream.onMessage(message => {
|
|
292
|
-
runWithInstanceStorage(instanceStorage, async () => {
|
|
293
|
-
try {
|
|
294
|
-
const ctx = new SimpleOperationContext();
|
|
295
|
-
await ctx.init();
|
|
296
|
-
// Proto3 `optional` fields deserialize with an extra `_field: "field"`
|
|
297
|
-
// marker (synthetic oneof tracking). Drop those so the payload matches
|
|
298
|
-
// the backing JSON schema which forbids additional properties.
|
|
299
|
-
const cleaned =
|
|
300
|
-
typeof message === "object" && message !== null && !Array.isArray(message)
|
|
301
|
-
? Object.fromEntries(Object.entries(message).filter(([k]) => !k.startsWith("_")))
|
|
302
|
-
: message;
|
|
303
|
-
ctx.setInput(Buffer.from(JSON.stringify(cleaned)));
|
|
304
|
-
await callOperation(ctx, opId);
|
|
305
|
-
const output = ctx.getOutput();
|
|
306
|
-
// Operation return values may be plain strings/numbers (e.g. version
|
|
307
|
-
// strings, publish ids). Wrap them in a `{value: ...}` object so the
|
|
308
|
-
// proto response is always a valid message shape.
|
|
309
|
-
let response: any = {};
|
|
310
|
-
if (output !== undefined && output !== null && output !== "") {
|
|
311
|
-
try {
|
|
312
|
-
response = JSON.parse(output);
|
|
313
|
-
if (typeof response !== "object" || response === null) {
|
|
314
|
-
response = { value: response };
|
|
315
|
-
}
|
|
316
|
-
} catch {
|
|
317
|
-
response = { value: output };
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
stream.sendUnary(response);
|
|
321
|
-
resolve();
|
|
322
|
-
} catch (err: any) {
|
|
323
|
-
useLog("ERROR", `[gRPC ${opId}] handler threw:`, err);
|
|
324
|
-
const status = this.errorToGrpcStatus(err);
|
|
325
|
-
stream.sendError(status, err.message);
|
|
326
|
-
resolve(); // Don't reject — error was sent via gRPC status
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Handle server streaming (single request → stream of responses).
|
|
335
|
-
* Maps to AsyncGenerator operations.
|
|
336
|
-
* @param stream - the active GrpcStream for this request
|
|
337
|
-
* @param opId - the Webda operation ID to invoke
|
|
338
|
-
* @param methodDef - the protobuf method definition with serializer functions
|
|
339
|
-
* @returns a promise that resolves when all response frames have been sent
|
|
373
|
+
* Proto3 `optional` fields deserialize with an extra `_field: "field"` marker (synthetic oneof tracking): drop
|
|
374
|
+
* those so the payload matches the backing JSON schema
|
|
375
|
+
* @param message - decoded message
|
|
376
|
+
* @returns the message without markers
|
|
340
377
|
*/
|
|
341
|
-
private
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
): Promise<void> {
|
|
346
|
-
return new Promise<void>((resolve, reject) => {
|
|
347
|
-
stream.onMessage(async message => {
|
|
348
|
-
try {
|
|
349
|
-
const ctx = new SimpleOperationContext();
|
|
350
|
-
await ctx.init();
|
|
351
|
-
ctx.setInput(Buffer.from(JSON.stringify(message)));
|
|
352
|
-
await callOperation(ctx, opId);
|
|
353
|
-
// callOperation writes chunks to ctx for AsyncGenerators
|
|
354
|
-
// For now, send the full output as a single response
|
|
355
|
-
const output = ctx.getOutput();
|
|
356
|
-
if (output) {
|
|
357
|
-
// Try to parse as NDJSON (multiple JSON objects)
|
|
358
|
-
const lines = output.split("\n").filter(Boolean);
|
|
359
|
-
for (const line of lines) {
|
|
360
|
-
try {
|
|
361
|
-
stream.send(JSON.parse(line));
|
|
362
|
-
} catch {
|
|
363
|
-
stream.send({ data: line });
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
stream.end(GrpcStatus.OK);
|
|
368
|
-
resolve();
|
|
369
|
-
} catch (err: any) {
|
|
370
|
-
const status = this.errorToGrpcStatus(err);
|
|
371
|
-
stream.sendError(status, err.message);
|
|
372
|
-
resolve();
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
});
|
|
378
|
+
private cleanMessage(message: unknown): unknown {
|
|
379
|
+
return typeof message === "object" && message !== null && !Array.isArray(message)
|
|
380
|
+
? Object.fromEntries(Object.entries(message).filter(([key]) => !key.startsWith("_")))
|
|
381
|
+
: message;
|
|
376
382
|
}
|
|
377
383
|
|
|
378
384
|
/**
|
|
379
|
-
*
|
|
380
|
-
* @param
|
|
381
|
-
* @
|
|
382
|
-
* @param streaming - streaming mode, either "client" or "bidi"
|
|
383
|
-
* @returns a promise that resolves when the operation completes and the response is sent
|
|
385
|
+
* The unary response for an operation output (a plain string/number is wrapped as `{ value }`)
|
|
386
|
+
* @param output - the context output
|
|
387
|
+
* @returns the response message
|
|
384
388
|
*/
|
|
385
|
-
private
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
stream.onMessage(message => {
|
|
394
|
-
messages.push(message);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
stream.onEnd(async () => {
|
|
398
|
-
try {
|
|
399
|
-
// For client streaming: process all collected messages
|
|
400
|
-
const ctx = new SimpleOperationContext();
|
|
401
|
-
await ctx.init();
|
|
402
|
-
ctx.setInput(Buffer.from(JSON.stringify(messages)));
|
|
403
|
-
await callOperation(ctx, opId);
|
|
404
|
-
const output = ctx.getOutput();
|
|
405
|
-
const response = output ? JSON.parse(output) : {};
|
|
406
|
-
|
|
407
|
-
if (streaming === "bidi") {
|
|
408
|
-
// Bidi: send response for each input message
|
|
409
|
-
stream.send(response);
|
|
410
|
-
} else {
|
|
411
|
-
// Client streaming: single response
|
|
412
|
-
stream.sendUnary(response);
|
|
413
|
-
}
|
|
414
|
-
resolve();
|
|
415
|
-
} catch (err: any) {
|
|
416
|
-
const status = this.errorToGrpcStatus(err);
|
|
417
|
-
stream.sendError(status, err.message);
|
|
418
|
-
resolve();
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
stream.onCancel(() => {
|
|
423
|
-
resolve();
|
|
424
|
-
});
|
|
425
|
-
});
|
|
389
|
+
private unaryResponse(output: string | undefined): Record<string, unknown> {
|
|
390
|
+
if (output === undefined || output === null || output === "") return {};
|
|
391
|
+
try {
|
|
392
|
+
return toGrpcMessage(JSON.parse(output));
|
|
393
|
+
} catch {
|
|
394
|
+
return { value: output };
|
|
395
|
+
}
|
|
426
396
|
}
|
|
427
397
|
|
|
428
398
|
/**
|