@blokjs/trigger-grpc 0.2.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/.env.example +8 -0
- package/CHANGELOG.md +143 -0
- package/README.md +25 -0
- package/__tests__/unit/GRpcTrigger.test.ts +313 -0
- package/__tests__/unit/GrpcClient.test.ts +138 -0
- package/__tests__/unit/GrpcServer.test.ts +127 -0
- package/__tests__/unit/MessageDecode.test.ts +314 -0
- package/__tests__/unit/NanoSDK.test.ts +162 -0
- package/__tests__/unit/Nodes.test.ts +16 -0
- package/buf.gen.yaml +11 -0
- package/buf.yaml +9 -0
- package/dist/GRpcTrigger.d.ts +18 -0
- package/dist/GRpcTrigger.js +219 -0
- package/dist/GRpcTrigger.js.map +1 -0
- package/dist/GrpcClient.d.ts +28 -0
- package/dist/GrpcClient.js +72 -0
- package/dist/GrpcClient.js.map +1 -0
- package/dist/GrpcServer.d.ts +14 -0
- package/dist/GrpcServer.js +54 -0
- package/dist/GrpcServer.js.map +1 -0
- package/dist/MessageDecode.d.ts +12 -0
- package/dist/MessageDecode.js +133 -0
- package/dist/MessageDecode.js.map +1 -0
- package/dist/NanoSDK.d.ts +13 -0
- package/dist/NanoSDK.js +169 -0
- package/dist/NanoSDK.js.map +1 -0
- package/dist/Nodes.d.ts +5 -0
- package/dist/Nodes.js +13 -0
- package/dist/Nodes.js.map +1 -0
- package/dist/Workflows.d.ts +3 -0
- package/dist/Workflows.js +5 -0
- package/dist/Workflows.js.map +1 -0
- package/dist/gen/workflow_pb.d.ts +113 -0
- package/dist/gen/workflow_pb.js +76 -0
- package/dist/gen/workflow_pb.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/opentelemetry_metrics.d.ts +1 -0
- package/dist/opentelemetry_metrics.js +21 -0
- package/dist/opentelemetry_metrics.js.map +1 -0
- package/dist/proto/workflow.proto +33 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +1 -0
- package/dist/types/Message.d.ts +0 -0
- package/dist/types/Message.js +2 -0
- package/dist/types/Message.js.map +1 -0
- package/dist/types/RuntimeWorkflow.d.ts +5 -0
- package/dist/types/RuntimeWorkflow.js +3 -0
- package/dist/types/RuntimeWorkflow.js.map +1 -0
- package/dist/types/Workflows.d.ts +5 -0
- package/dist/types/Workflows.js +3 -0
- package/dist/types/Workflows.js.map +1 -0
- package/localhost+2-key.pem +28 -0
- package/localhost+2.pem +27 -0
- package/package.json +60 -0
- package/proto/workflow.proto +33 -0
- package/src/GRpcTrigger.ts +252 -0
- package/src/GrpcClient.ts +87 -0
- package/src/GrpcServer.ts +63 -0
- package/src/MessageDecode.ts +142 -0
- package/src/NanoSDK.ts +146 -0
- package/src/Nodes.ts +12 -0
- package/src/Workflows.ts +5 -0
- package/src/gen/workflow_pb.ts +142 -0
- package/src/index.ts +22 -0
- package/src/opentelemetry_metrics.ts +26 -0
- package/src/server.ts +8 -0
- package/src/types/Message.ts +0 -0
- package/src/types/RuntimeWorkflow.ts +7 -0
- package/src/types/Workflows.ts +7 -0
- package/tsconfig.json +34 -0
- package/vitest.config.ts +29 -0
- package/workflows/countries.json +31 -0
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blokjs/trigger-grpc",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18.0.0"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"repository": "https://github.com/well-prado/blok.git",
|
|
12
|
+
"author": "Marco A. Castillo Della Sera",
|
|
13
|
+
"license": "Apache-2.0",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "bun --watch run src/index.ts",
|
|
16
|
+
"start": "bun run dist/index.js",
|
|
17
|
+
"reload": "bun --env-file=.env.local run src/index.ts",
|
|
18
|
+
"build": "rimraf ./dist && tsc && copyfiles -u 1 proto/** dist/proto",
|
|
19
|
+
"build:proto": "npx buf generate .",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:dev": "vitest"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@bufbuild/buf": "^1.64.0",
|
|
25
|
+
"@bufbuild/protoc-gen-es": "^2.11.0",
|
|
26
|
+
"@connectrpc/protoc-gen-connect-es": "^1.7.0",
|
|
27
|
+
"@types/node": "^22.15.21",
|
|
28
|
+
"copyfiles": "^2.4.1",
|
|
29
|
+
"rimraf": "^6.1.2",
|
|
30
|
+
"typescript": "^5.8.3",
|
|
31
|
+
"vitest": "^4.0.18"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@bufbuild/protobuf": "^2.11.0",
|
|
35
|
+
"@connectrpc/connect": "^2.1.1",
|
|
36
|
+
"@connectrpc/connect-fastify": "^2.1.1",
|
|
37
|
+
"@connectrpc/connect-node": "^2.1.1",
|
|
38
|
+
"@blokjs/api-call": "workspace:*",
|
|
39
|
+
"@blokjs/helper": "workspace:*",
|
|
40
|
+
"@blokjs/if-else": "workspace:*",
|
|
41
|
+
"@blokjs/runner": "workspace:*",
|
|
42
|
+
"@blokjs/shared": "workspace:*",
|
|
43
|
+
"@opentelemetry/api": "^1.9.0",
|
|
44
|
+
"@opentelemetry/exporter-prometheus": "^0.57.2",
|
|
45
|
+
"@opentelemetry/resources": "^1.30.1",
|
|
46
|
+
"@opentelemetry/sdk-metrics": "^1.30.1",
|
|
47
|
+
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
48
|
+
"fast-xml-parser": "^5.3.4",
|
|
49
|
+
"fastify": "^5.7.2",
|
|
50
|
+
"uuid": "^11.1.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@bufbuild/protoc-gen-es": "2.11.0",
|
|
54
|
+
"@connectrpc/connect": "2.1.1"
|
|
55
|
+
},
|
|
56
|
+
"private": false,
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package blok.workflow.v1;
|
|
4
|
+
|
|
5
|
+
message WorkflowRequest {
|
|
6
|
+
string Name = 1;
|
|
7
|
+
string Message = 2;
|
|
8
|
+
string Encoding = 3;
|
|
9
|
+
string Type = 4;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
message WorkflowResponse {
|
|
13
|
+
string Message = 1;
|
|
14
|
+
string Encoding = 2;
|
|
15
|
+
string Type = 3;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
enum MessageEncoding {
|
|
19
|
+
BASE64 = 0;
|
|
20
|
+
STRING = 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
enum MessageType {
|
|
24
|
+
TEXT = 0;
|
|
25
|
+
JSON = 1;
|
|
26
|
+
XML = 2;
|
|
27
|
+
HTML = 3;
|
|
28
|
+
BINARY = 4;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
service WorkflowService {
|
|
32
|
+
rpc ExecuteWorkflow (WorkflowRequest) returns (WorkflowResponse) {}
|
|
33
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { ConnectRouter } from "@connectrpc/connect";
|
|
2
|
+
import {
|
|
3
|
+
DefaultLogger,
|
|
4
|
+
type GlobalOptions,
|
|
5
|
+
NodeMap,
|
|
6
|
+
type ParamsDictionary,
|
|
7
|
+
TriggerBase,
|
|
8
|
+
type TriggerResponse,
|
|
9
|
+
} from "@blokjs/runner";
|
|
10
|
+
import { type Context, GlobalError } from "@blokjs/shared";
|
|
11
|
+
import { type Span, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
|
|
12
|
+
import fastify from "fastify";
|
|
13
|
+
import { v4 as uuid } from "uuid";
|
|
14
|
+
import MessageDecode from "./MessageDecode";
|
|
15
|
+
import nodes from "./Nodes";
|
|
16
|
+
import workflows from "./Workflows";
|
|
17
|
+
import type RuntimeWorkflow from "./types/RuntimeWorkflow";
|
|
18
|
+
|
|
19
|
+
import { type Step, Workflow } from "@blokjs/helper";
|
|
20
|
+
import type { TriggerOpts } from "@blokjs/helper/dist/types/TriggerOpts";
|
|
21
|
+
import {
|
|
22
|
+
MessageEncoding,
|
|
23
|
+
MessageType,
|
|
24
|
+
type WorkflowRequest,
|
|
25
|
+
type WorkflowResponse,
|
|
26
|
+
WorkflowService,
|
|
27
|
+
} from "./gen/workflow_pb";
|
|
28
|
+
|
|
29
|
+
enum NodeTypes {
|
|
30
|
+
MODULE = "module",
|
|
31
|
+
LOCAL = "local",
|
|
32
|
+
PYTHON3 = "runtime.python3",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default class GRpcTrigger extends TriggerBase {
|
|
36
|
+
private server = fastify({
|
|
37
|
+
http2: true,
|
|
38
|
+
// https: {
|
|
39
|
+
// key: readFileSync("localhost+2-key.pem", "utf8"),
|
|
40
|
+
// cert: readFileSync("localhost+2.pem", "utf8"),
|
|
41
|
+
// }
|
|
42
|
+
});
|
|
43
|
+
private nodeMap: GlobalOptions = <GlobalOptions>{};
|
|
44
|
+
protected tracer = trace.getTracer(
|
|
45
|
+
process.env.PROJECT_NAME || "trigger-grpc-workflow",
|
|
46
|
+
process.env.PROJECT_VERSION || "0.0.1",
|
|
47
|
+
);
|
|
48
|
+
private logger = new DefaultLogger();
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
super();
|
|
52
|
+
this.loadNodes();
|
|
53
|
+
this.loadWorkflows();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getApp() {
|
|
57
|
+
return this.server;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async listen(): Promise<number> {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
loadNodes() {
|
|
65
|
+
this.nodeMap.nodes = new NodeMap();
|
|
66
|
+
const nodeKeys = Object.keys(nodes);
|
|
67
|
+
for (const key of nodeKeys) {
|
|
68
|
+
this.nodeMap.nodes.addNode(key, nodes[key]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
loadWorkflows() {
|
|
73
|
+
this.nodeMap.workflows = workflows;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
processRequest(router: ConnectRouter, trigger: GRpcTrigger) {
|
|
77
|
+
router.service(WorkflowService, {
|
|
78
|
+
executeWorkflow: (request: WorkflowRequest) => trigger.executeWorkflow(request),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async executeWorkflow(request: WorkflowRequest) {
|
|
83
|
+
const start = performance.now();
|
|
84
|
+
const coder = new MessageDecode();
|
|
85
|
+
let name = request.Name;
|
|
86
|
+
const messageContext: Context = coder.requestDecode(request);
|
|
87
|
+
const runtimeWorkflow = messageContext as unknown as RuntimeWorkflow;
|
|
88
|
+
const id: string = (messageContext.request.query?.requestId as string) || (uuid() as string);
|
|
89
|
+
let remoteNodeExecution = false;
|
|
90
|
+
|
|
91
|
+
const defaultMeter = metrics.getMeter("default");
|
|
92
|
+
const workflow_runner_errors = defaultMeter.createCounter("workflow_errors", {
|
|
93
|
+
description: "Workflow runner errors",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return await this.tracer.startActiveSpan(`${name}`, async (span: Span) => {
|
|
97
|
+
try {
|
|
98
|
+
if (runtimeWorkflow !== undefined) {
|
|
99
|
+
const workflowModel = runtimeWorkflow.workflow;
|
|
100
|
+
const node_type = (workflowModel.steps[0] as unknown as ParamsDictionary).type;
|
|
101
|
+
let set_node_type: NodeTypes = NodeTypes.MODULE;
|
|
102
|
+
switch (node_type) {
|
|
103
|
+
case "runtime.python3":
|
|
104
|
+
set_node_type = NodeTypes.PYTHON3;
|
|
105
|
+
break;
|
|
106
|
+
case "local":
|
|
107
|
+
set_node_type = NodeTypes.LOCAL;
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
set_node_type = NodeTypes.MODULE;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const trigger = Object.keys(workflowModel.trigger)[0];
|
|
115
|
+
const trigger_config =
|
|
116
|
+
((workflowModel.trigger as unknown as ParamsDictionary)[trigger] as unknown as TriggerOpts) || {};
|
|
117
|
+
|
|
118
|
+
const step: Step = Workflow({
|
|
119
|
+
name: `Remote Node: ${name}`,
|
|
120
|
+
version: "1.0.0",
|
|
121
|
+
description: "Remote Node",
|
|
122
|
+
})
|
|
123
|
+
.addTrigger((trigger as unknown as "http") || "grpc", trigger_config)
|
|
124
|
+
.addStep({
|
|
125
|
+
name: "node",
|
|
126
|
+
node: name,
|
|
127
|
+
type: set_node_type,
|
|
128
|
+
inputs: ((workflowModel.nodes as unknown as ParamsDictionary).node as unknown as ParamsDictionary).inputs,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
this.nodeMap.workflows[id] = step;
|
|
132
|
+
name = id;
|
|
133
|
+
remoteNodeExecution = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await this.configuration.init(name, this.nodeMap);
|
|
137
|
+
let ctx: Context = this.createContext(undefined, name, id);
|
|
138
|
+
ctx.request = messageContext.request;
|
|
139
|
+
ctx.logger.log(`Workflow: ${name}, Version: ${this.configuration.version}`);
|
|
140
|
+
|
|
141
|
+
const response: TriggerResponse = await this.run(ctx);
|
|
142
|
+
ctx = response.ctx;
|
|
143
|
+
const average = response.metrics;
|
|
144
|
+
|
|
145
|
+
if (ctx.response.contentType === undefined || ctx.response.contentType === "")
|
|
146
|
+
ctx.response.contentType = "application/json";
|
|
147
|
+
|
|
148
|
+
const end = performance.now();
|
|
149
|
+
ctx.logger.log(`Completed in ${(end - start).toFixed(2)}ms`);
|
|
150
|
+
|
|
151
|
+
span.setAttribute("success", true);
|
|
152
|
+
span.setAttribute("Content-Type", ctx.response.contentType as string);
|
|
153
|
+
span.setAttribute("workflow_request_id", `${ctx.id}`);
|
|
154
|
+
span.setAttribute("workflow_elapsed_time", `${end - start}`);
|
|
155
|
+
span.setAttribute("workflow_version", `${this.configuration.version}`);
|
|
156
|
+
span.setAttribute("workflow_name", `${this.configuration.name}`);
|
|
157
|
+
span.setAttribute("workflow_memory_avg_mb", `${average.memory.total}`);
|
|
158
|
+
span.setAttribute("workflow_memory_min_mb", `${average.memory.min}`);
|
|
159
|
+
span.setAttribute("workflow_memory_max_mb", `${average.memory.max}`);
|
|
160
|
+
span.setAttribute("workflow_cpu_percentage", `${average.cpu.average}`);
|
|
161
|
+
span.setAttribute("workflow_cpu_total", `${average.cpu.total}`);
|
|
162
|
+
span.setAttribute("workflow_cpu_usage", `${average.cpu.usage}`);
|
|
163
|
+
span.setAttribute("workflow_cpu_model", `${average.cpu.model}`);
|
|
164
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
165
|
+
|
|
166
|
+
return coder.responseEncode(ctx, request.Encoding, request.Type);
|
|
167
|
+
} catch (e: unknown) {
|
|
168
|
+
span.setAttribute("success", false);
|
|
169
|
+
span.setAttribute("workflow_request_id", `${id}`);
|
|
170
|
+
span.recordException(e as Error);
|
|
171
|
+
let message: WorkflowResponse = <WorkflowResponse>{};
|
|
172
|
+
const base64Key = MessageEncoding[MessageEncoding.BASE64];
|
|
173
|
+
const textKey = MessageType[MessageType.TEXT];
|
|
174
|
+
const jsonKey = MessageType[MessageType.JSON];
|
|
175
|
+
|
|
176
|
+
if (e instanceof GlobalError) {
|
|
177
|
+
const error_context = e as GlobalError;
|
|
178
|
+
|
|
179
|
+
if (error_context.context.message === "{}" && error_context.context.json instanceof DOMException) {
|
|
180
|
+
workflow_runner_errors.add(1, {
|
|
181
|
+
env: process.env.NODE_ENV,
|
|
182
|
+
workflow_version: `${this.configuration.version || "unknown"}`,
|
|
183
|
+
workflow_name: `${name || this.configuration.name}`,
|
|
184
|
+
});
|
|
185
|
+
span.setStatus({
|
|
186
|
+
code: SpanStatusCode.ERROR,
|
|
187
|
+
message: (error_context.context.json as Error).toString(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this.logger.error(`${(error_context.context.json as Error).toString()}`);
|
|
191
|
+
message = {
|
|
192
|
+
Message: coder.responseErrorEncode((error_context.context.json as Error).toString(), base64Key, textKey),
|
|
193
|
+
Encoding: base64Key,
|
|
194
|
+
Type: textKey,
|
|
195
|
+
} as WorkflowResponse;
|
|
196
|
+
} else {
|
|
197
|
+
if (error_context.context.code === undefined) error_context.setCode(500);
|
|
198
|
+
|
|
199
|
+
if (error_context.hasJson()) {
|
|
200
|
+
workflow_runner_errors.add(1, {
|
|
201
|
+
env: process.env.NODE_ENV,
|
|
202
|
+
workflow_version: `${this.configuration.version || "unknown"}`,
|
|
203
|
+
workflow_name: `${name || this.configuration.name}`,
|
|
204
|
+
});
|
|
205
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: JSON.stringify(error_context.context.json) });
|
|
206
|
+
this.logger.error(`${JSON.stringify(error_context.context.json)}`);
|
|
207
|
+
message = {
|
|
208
|
+
Message: coder.responseErrorEncode(JSON.stringify(error_context.context.json), base64Key, textKey),
|
|
209
|
+
Encoding: base64Key,
|
|
210
|
+
Type: jsonKey,
|
|
211
|
+
} as WorkflowResponse;
|
|
212
|
+
} else {
|
|
213
|
+
workflow_runner_errors.add(1, {
|
|
214
|
+
env: process.env.NODE_ENV,
|
|
215
|
+
workflow_version: `${this.configuration.version || "unknown"}`,
|
|
216
|
+
workflow_name: `${name || this.configuration.name}`,
|
|
217
|
+
});
|
|
218
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error_context.message });
|
|
219
|
+
this.logger.error(`${error_context.message}`, error_context.stack?.replace(/\n/g, " "));
|
|
220
|
+
message = {
|
|
221
|
+
Message: coder.responseErrorEncode(error_context.message, base64Key, textKey),
|
|
222
|
+
Encoding: MessageEncoding[MessageEncoding.BASE64],
|
|
223
|
+
Type: textKey,
|
|
224
|
+
} as WorkflowResponse;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
workflow_runner_errors.add(1, {
|
|
229
|
+
env: process.env.NODE_ENV,
|
|
230
|
+
workflow_version: `${this.configuration.version || "unknown"}`,
|
|
231
|
+
workflow_name: `${name || this.configuration.name}`,
|
|
232
|
+
});
|
|
233
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: (e as Error).message });
|
|
234
|
+
this.logger.error(`${(e as Error).message}`, `${(e as Error).stack?.replace(/\n/g, " ")}`);
|
|
235
|
+
|
|
236
|
+
message = {
|
|
237
|
+
Message: coder.responseErrorEncode((e as Error).message, base64Key, textKey),
|
|
238
|
+
Encoding: base64Key,
|
|
239
|
+
Type: textKey,
|
|
240
|
+
} as WorkflowResponse;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return message;
|
|
244
|
+
} finally {
|
|
245
|
+
if (remoteNodeExecution) {
|
|
246
|
+
delete this.nodeMap.workflows[id];
|
|
247
|
+
}
|
|
248
|
+
span.end();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type Transport, createClient } from "@connectrpc/connect";
|
|
2
|
+
import { createGrpcTransport } from "@connectrpc/connect-node";
|
|
3
|
+
import { createConnectTransport } from "@connectrpc/connect-node";
|
|
4
|
+
import { createGrpcWebTransport } from "@connectrpc/connect-node";
|
|
5
|
+
import { type WorkflowRequest, type WorkflowResponse, WorkflowService } from "./gen/workflow_pb";
|
|
6
|
+
|
|
7
|
+
export type RpcOptions = {
|
|
8
|
+
host: string;
|
|
9
|
+
port: number;
|
|
10
|
+
protocol: string;
|
|
11
|
+
httpVersion: HttpVersionEnum;
|
|
12
|
+
transport: TransportEnum;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type CallOptions = {
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export enum TransportEnum {
|
|
20
|
+
GRPC = "grpc",
|
|
21
|
+
GRPC_WEB = "grpc-web",
|
|
22
|
+
CONNECT = "connect",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export enum HttpVersionEnum {
|
|
26
|
+
HTTP1 = "1.1",
|
|
27
|
+
HTTP2 = "2",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type { WorkflowRequest, WorkflowResponse };
|
|
31
|
+
|
|
32
|
+
export default class GrpcClient {
|
|
33
|
+
protected opts: RpcOptions;
|
|
34
|
+
|
|
35
|
+
constructor(options: RpcOptions) {
|
|
36
|
+
this.opts = options;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async call(message: WorkflowRequest, opts?: CallOptions): Promise<WorkflowResponse> {
|
|
40
|
+
const transport = this.transport();
|
|
41
|
+
const client = createClient(WorkflowService, transport);
|
|
42
|
+
return await client.executeWorkflow(message, opts);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
transport(): Transport {
|
|
46
|
+
switch (this.opts.transport) {
|
|
47
|
+
case TransportEnum.GRPC:
|
|
48
|
+
return createGrpcTransport({
|
|
49
|
+
baseUrl: `${this.opts.protocol}://${this.opts.host}:${this.opts.port}/`,
|
|
50
|
+
interceptors: [],
|
|
51
|
+
});
|
|
52
|
+
case TransportEnum.GRPC_WEB:
|
|
53
|
+
return createGrpcWebTransport({
|
|
54
|
+
baseUrl: `${this.opts.protocol}://${this.opts.host}:${this.opts.port}/`,
|
|
55
|
+
httpVersion: this.opts.httpVersion,
|
|
56
|
+
interceptors: [],
|
|
57
|
+
});
|
|
58
|
+
case TransportEnum.CONNECT:
|
|
59
|
+
return createConnectTransport({
|
|
60
|
+
baseUrl: `${this.opts.protocol}://${this.opts.host}:${this.opts.port}/`,
|
|
61
|
+
httpVersion: this.opts.httpVersion,
|
|
62
|
+
interceptors: [],
|
|
63
|
+
});
|
|
64
|
+
default:
|
|
65
|
+
throw new Error("Invalid transport type");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// let client = new GrpcClient({
|
|
71
|
+
// host: "localhost",
|
|
72
|
+
// port: 8433,
|
|
73
|
+
// protocol: "http",
|
|
74
|
+
// httpVersion: HttpVersionEnum.HTTP2,
|
|
75
|
+
// transport: TransportEnum.GRPC,
|
|
76
|
+
// });
|
|
77
|
+
|
|
78
|
+
// client.call({
|
|
79
|
+
// "Name": "countries",
|
|
80
|
+
// "Message": "ewogICAgInJlcXVlc3QiOiB7CiAgICAgICAgImJvZHkiOiB7CiAgICAgICAgICAgICJjb3VudHJ5X25hbWUiOiAiQ2FuYWRhIgogICAgICAgIH0KICAgIH0KfQ==",
|
|
81
|
+
// "Encoding": "BASE64",
|
|
82
|
+
// "Type": "JSON"
|
|
83
|
+
// } as WorkflowRequest).then((response) => {
|
|
84
|
+
// console.log(response);
|
|
85
|
+
// }).catch((error) => {
|
|
86
|
+
// console.error(error);
|
|
87
|
+
// });
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ConnectRouter } from "@connectrpc/connect";
|
|
2
|
+
import { fastifyConnectPlugin } from "@connectrpc/connect-fastify";
|
|
3
|
+
import { DefaultLogger } from "@blokjs/runner";
|
|
4
|
+
import { type Span, metrics, trace } from "@opentelemetry/api";
|
|
5
|
+
import GRpcTrigger from "./GRpcTrigger";
|
|
6
|
+
|
|
7
|
+
export type GrpcServerOptions = {
|
|
8
|
+
host: string;
|
|
9
|
+
port: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default class GrpcServer {
|
|
13
|
+
protected opts: GrpcServerOptions;
|
|
14
|
+
protected tracer = trace.getTracer(
|
|
15
|
+
process.env.PROJECT_NAME || "trigger-grpc-server",
|
|
16
|
+
process.env.PROJECT_VERSION || "0.0.1",
|
|
17
|
+
);
|
|
18
|
+
protected app_cold_start = metrics.getMeter("default").createGauge("initialization", {
|
|
19
|
+
description: "Application cold start",
|
|
20
|
+
});
|
|
21
|
+
protected initializer = 0;
|
|
22
|
+
protected logger = new DefaultLogger();
|
|
23
|
+
|
|
24
|
+
constructor(opts: GrpcServerOptions) {
|
|
25
|
+
this.opts = opts;
|
|
26
|
+
|
|
27
|
+
if (this.opts.host === undefined) {
|
|
28
|
+
this.opts.host = "0.0.0.0";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (this.opts.port === undefined) {
|
|
32
|
+
this.opts.port = 8443;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.initializer = performance.now();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async start() {
|
|
39
|
+
this.tracer.startActiveSpan("initialization", async (span: Span) => {
|
|
40
|
+
const trigger = new GRpcTrigger();
|
|
41
|
+
const server = trigger.getApp();
|
|
42
|
+
const host = process.env.GRPC_HOST || this.opts.host;
|
|
43
|
+
let port: string | number = process.env.GRPC_PORT || this.opts.port;
|
|
44
|
+
if (typeof port === "string") {
|
|
45
|
+
port = Number.parseInt(port, 10);
|
|
46
|
+
}
|
|
47
|
+
await server.register(fastifyConnectPlugin, {
|
|
48
|
+
routes: (router: ConnectRouter) => trigger.processRequest(router, trigger),
|
|
49
|
+
});
|
|
50
|
+
await server.listen({ host, port: port });
|
|
51
|
+
this.logger.log(`Server is listening at ${JSON.stringify(server.addresses())}`);
|
|
52
|
+
|
|
53
|
+
this.initializer = performance.now() - this.initializer;
|
|
54
|
+
this.logger.log(`Server initialized in ${(this.initializer).toFixed(2)}ms`);
|
|
55
|
+
this.app_cold_start.record(this.initializer, {
|
|
56
|
+
pid: process.pid,
|
|
57
|
+
env: process.env.NODE_ENV,
|
|
58
|
+
app: process.env.APP_NAME,
|
|
59
|
+
});
|
|
60
|
+
span.end();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { JsonLikeObject } from "@blokjs/runner";
|
|
2
|
+
import type { Context } from "@blokjs/shared";
|
|
3
|
+
import { XMLBuilder, XMLParser } from "fast-xml-parser";
|
|
4
|
+
import { MessageEncoding, MessageType, type WorkflowRequest, type WorkflowResponse } from "./gen/workflow_pb";
|
|
5
|
+
|
|
6
|
+
export default class MessageDecode {
|
|
7
|
+
requestDecode(request: WorkflowRequest): Context {
|
|
8
|
+
let message: Context = <Context>{};
|
|
9
|
+
|
|
10
|
+
switch (request.Encoding) {
|
|
11
|
+
case MessageEncoding[MessageEncoding.BASE64]: {
|
|
12
|
+
const messageStr = Buffer.from(request.Message, "base64").toString("utf-8");
|
|
13
|
+
message = this.decodeType(messageStr, request.Type);
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
case MessageEncoding[MessageEncoding.STRING]: {
|
|
17
|
+
message = this.decodeType(request.Message, request.Type);
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
default:
|
|
21
|
+
throw new Error(`Unsupported encoding: ${request.Encoding}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return message;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
decodeType(message: string, type: string): Context {
|
|
28
|
+
switch (type) {
|
|
29
|
+
case MessageType[MessageType.JSON]: {
|
|
30
|
+
return JSON.parse(message);
|
|
31
|
+
}
|
|
32
|
+
case MessageType[MessageType.XML]: {
|
|
33
|
+
return new XMLParser().parse(message);
|
|
34
|
+
}
|
|
35
|
+
default:
|
|
36
|
+
throw new Error(`Unsupported type: ${type}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
responseEncode(ctx: Context, encoding: string, type: string): WorkflowResponse {
|
|
41
|
+
let message: string | object | Buffer<ArrayBuffer>;
|
|
42
|
+
const responseType = this.mapContentType(ctx.response.contentType as string);
|
|
43
|
+
switch (encoding) {
|
|
44
|
+
case MessageEncoding[MessageEncoding.BASE64]: {
|
|
45
|
+
if (responseType === MessageType.JSON) {
|
|
46
|
+
message = this.encodeType(ctx.response.data as JsonLikeObject, type);
|
|
47
|
+
message = Buffer.from(message).toString("base64");
|
|
48
|
+
}
|
|
49
|
+
if (responseType === MessageType.XML) {
|
|
50
|
+
message = this.encodeType(ctx.response.data as object, type);
|
|
51
|
+
message = Buffer.from(message).toString("base64");
|
|
52
|
+
} else {
|
|
53
|
+
message = this.encodeType(ctx.response.data as string, type);
|
|
54
|
+
message = Buffer.from(message).toString("base64");
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case MessageEncoding[MessageEncoding.STRING]: {
|
|
59
|
+
if (responseType === MessageType.JSON) {
|
|
60
|
+
message = this.encodeType(ctx.response.data as JsonLikeObject, type);
|
|
61
|
+
}
|
|
62
|
+
if (responseType === MessageType.XML) {
|
|
63
|
+
message = this.encodeType(ctx.response.data as object, type);
|
|
64
|
+
} else {
|
|
65
|
+
message = this.encodeType(ctx.response.data as string, type);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
default:
|
|
70
|
+
throw new Error(`Unsupported encoding: ${encoding}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
Message: message,
|
|
75
|
+
Encoding: encoding,
|
|
76
|
+
Type: MessageType[responseType],
|
|
77
|
+
} as WorkflowResponse;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
responseErrorEncode(e: string | JsonLikeObject, encoding: string, type: string): string {
|
|
81
|
+
let message: string | object | Buffer<ArrayBuffer>;
|
|
82
|
+
switch (encoding) {
|
|
83
|
+
case MessageEncoding[MessageEncoding.BASE64]:
|
|
84
|
+
message = Buffer.from(this.encodeType(e, type)).toString("base64");
|
|
85
|
+
break;
|
|
86
|
+
case MessageEncoding[MessageEncoding.STRING]:
|
|
87
|
+
message = this.encodeType(e, type);
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
throw new Error(`Unsupported encoding: ${encoding}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return message as string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
responseDecode(response: WorkflowResponse): JsonLikeObject {
|
|
97
|
+
let message: JsonLikeObject = {};
|
|
98
|
+
|
|
99
|
+
switch (response.Encoding) {
|
|
100
|
+
case MessageEncoding[MessageEncoding.BASE64]: {
|
|
101
|
+
const messageStr = Buffer.from(response.Message, "base64").toString("utf-8");
|
|
102
|
+
message = this.decodeType(messageStr, response.Type) as unknown as JsonLikeObject;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case MessageEncoding[MessageEncoding.STRING]: {
|
|
106
|
+
message = this.decodeType(response.Message, response.Type) as unknown as JsonLikeObject;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
default:
|
|
110
|
+
throw new Error(`Unsupported encoding: ${response.Encoding}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return message;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
encodeType(message: string | object | Buffer<ArrayBuffer>, type: string): string {
|
|
117
|
+
switch (type) {
|
|
118
|
+
case MessageType[MessageType.JSON]:
|
|
119
|
+
return JSON.stringify(message);
|
|
120
|
+
case MessageType[MessageType.TEXT]:
|
|
121
|
+
case MessageType[MessageType.HTML]:
|
|
122
|
+
return message.toString();
|
|
123
|
+
case MessageType[MessageType.XML]:
|
|
124
|
+
return new XMLBuilder().build(message);
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`Unsupported type: ${type}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
mapContentType(contentType: string): MessageType {
|
|
131
|
+
switch (contentType) {
|
|
132
|
+
case "application/json":
|
|
133
|
+
return MessageType.JSON;
|
|
134
|
+
case "text/html":
|
|
135
|
+
return MessageType.HTML;
|
|
136
|
+
case "text/xml":
|
|
137
|
+
return MessageType.XML;
|
|
138
|
+
default:
|
|
139
|
+
return MessageType.TEXT;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|