@grest-ts/ipc 0.0.5 → 0.0.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.
@@ -1,107 +1,107 @@
1
- import * as http from 'http';
2
- import {GGLog} from "@grest-ts/logger";
3
- import {HttpMethod} from "@grest-ts/common";
4
- import httpProxy from "http-proxy";
5
- import findMyWay, {HTTPMethod} from "find-my-way";
6
-
7
- /** HTTP route handler - receives body, path params, and headers. Returns response data (or void for empty 200) */
8
- export type HttpRouteHandler = (body: any, pathParams?: Record<string, string>, headers?: Record<string, string>) => Promise<any>;
9
-
10
- /** Default HTTP handler for unmatched routes (proxying) - gets raw req/res */
11
- export type DefaultHttpHandler = (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void>;
12
-
13
- interface RouteStore {
14
- handler: HttpRouteHandler;
15
- }
16
-
17
- export class HttpHandler {
18
-
19
- private router = findMyWay<findMyWay.HTTPVersion.V1>();
20
- private routeResolver: (path: string) => string
21
- private readonly proxy: httpProxy;
22
-
23
- constructor() {
24
- this.proxy = httpProxy.createProxyServer({});
25
- }
26
-
27
- public setRouteResolver(routeResolver: (path: string) => string) {
28
- this.routeResolver = routeResolver
29
- }
30
-
31
- public addRoute(method: HttpMethod, path: string, handler: HttpRouteHandler): void {
32
- const store: RouteStore = {handler};
33
- this.router.on(method as HTTPMethod, path, () => {
34
- }, store);
35
- }
36
-
37
- public removeRoute(method: HttpMethod, path: string): void {
38
- this.router.off(method as HTTPMethod, path);
39
- }
40
-
41
- public async handleRequest(
42
- req: http.IncomingMessage,
43
- res: http.ServerResponse
44
- ): Promise<void> {
45
- const method = (req.method || 'GET') as HttpMethod;
46
- const url = req.url || '/';
47
- // Strip query string for route matching
48
- const path = url.split('?')[0];
49
-
50
- // Try to match a route
51
- const result = this.router.find(method as HTTPMethod, path);
52
- if (result?.store) {
53
- const store = result.store as RouteStore;
54
- const body = await this.readJsonBody(req);
55
- // Normalize headers to Record<string, string>
56
- const headers: Record<string, string> = {};
57
- for (const [key, value] of Object.entries(req.headers)) {
58
- if (value !== undefined) {
59
- headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;
60
- }
61
- }
62
- const pathParams = result.params as Record<string, string> || {};
63
- try {
64
- const responseData = await store.handler(body, pathParams, headers);
65
- this.sendResponse(res, 200, responseData);
66
- } catch (err: any) {
67
- GGLog.error(this, `HTTP route error [${method} ${path}]: ${err.message}`, err);
68
- this.sendResponse(res, 500, {error: err.message});
69
- }
70
- return;
71
- }
72
-
73
- const routeToUrl = this.routeResolver(path)
74
- if (routeToUrl) {
75
- GGLog.debug(this, `Proxying ${req.method} ${req.url} -> ${routeToUrl}`);
76
- this.proxy.web(req, res, {target: routeToUrl}, (err: Error) => {
77
- GGLog.error(this, "Proxy error", err);
78
- this.sendResponse(res, 502, {issue: "Proxy error: " + err.message + " (Did the service crash?)"});
79
- });
80
- return;
81
- }
82
-
83
- this.sendResponse(res, 404, {error: `No route for ${method} ${path}! Did you forget to start a service that defines this path?`});
84
- }
85
-
86
- private async readJsonBody(req: http.IncomingMessage): Promise<any> {
87
- return new Promise((resolve) => {
88
- let bodyStr = '';
89
- req.on('data', (chunk: Buffer) => {
90
- bodyStr += chunk.toString();
91
- });
92
- req.on('end', () => {
93
- try {
94
- resolve(bodyStr ? JSON.parse(bodyStr) : undefined);
95
- } catch {
96
- resolve(undefined);
97
- }
98
- });
99
- req.on('error', () => resolve(undefined));
100
- });
101
- }
102
-
103
- private sendResponse(res: http.ServerResponse, status: number, data?: any): void {
104
- res.writeHead(status, {'Content-Type': 'application/json'});
105
- res.end(data !== undefined ? JSON.stringify(data) : '');
106
- }
107
- }
1
+ import * as http from 'http';
2
+ import {GGLog} from "@grest-ts/logger";
3
+ import {HttpMethod} from "@grest-ts/common";
4
+ import httpProxy from "http-proxy";
5
+ import findMyWay, {HTTPMethod} from "find-my-way";
6
+
7
+ /** HTTP route handler - receives body, path params, and headers. Returns response data (or void for empty 200) */
8
+ export type HttpRouteHandler = (body: any, pathParams?: Record<string, string>, headers?: Record<string, string>) => Promise<any>;
9
+
10
+ /** Default HTTP handler for unmatched routes (proxying) - gets raw req/res */
11
+ export type DefaultHttpHandler = (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void>;
12
+
13
+ interface RouteStore {
14
+ handler: HttpRouteHandler;
15
+ }
16
+
17
+ export class HttpHandler {
18
+
19
+ private router = findMyWay<findMyWay.HTTPVersion.V1>();
20
+ private routeResolver: (path: string) => string
21
+ private readonly proxy: httpProxy;
22
+
23
+ constructor() {
24
+ this.proxy = httpProxy.createProxyServer({});
25
+ }
26
+
27
+ public setRouteResolver(routeResolver: (path: string) => string) {
28
+ this.routeResolver = routeResolver
29
+ }
30
+
31
+ public addRoute(method: HttpMethod, path: string, handler: HttpRouteHandler): void {
32
+ const store: RouteStore = {handler};
33
+ this.router.on(method as HTTPMethod, path, () => {
34
+ }, store);
35
+ }
36
+
37
+ public removeRoute(method: HttpMethod, path: string): void {
38
+ this.router.off(method as HTTPMethod, path);
39
+ }
40
+
41
+ public async handleRequest(
42
+ req: http.IncomingMessage,
43
+ res: http.ServerResponse
44
+ ): Promise<void> {
45
+ const method = (req.method || 'GET') as HttpMethod;
46
+ const url = req.url || '/';
47
+ // Strip query string for route matching
48
+ const path = url.split('?')[0];
49
+
50
+ // Try to match a route
51
+ const result = this.router.find(method as HTTPMethod, path);
52
+ if (result?.store) {
53
+ const store = result.store as RouteStore;
54
+ const body = await this.readJsonBody(req);
55
+ // Normalize headers to Record<string, string>
56
+ const headers: Record<string, string> = {};
57
+ for (const [key, value] of Object.entries(req.headers)) {
58
+ if (value !== undefined) {
59
+ headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;
60
+ }
61
+ }
62
+ const pathParams = result.params as Record<string, string> || {};
63
+ try {
64
+ const responseData = await store.handler(body, pathParams, headers);
65
+ this.sendResponse(res, 200, responseData);
66
+ } catch (err: any) {
67
+ GGLog.error(this, `HTTP route error [${method} ${path}]: ${err.message}`, err);
68
+ this.sendResponse(res, 500, {error: err.message});
69
+ }
70
+ return;
71
+ }
72
+
73
+ const routeToUrl = this.routeResolver(path)
74
+ if (routeToUrl) {
75
+ GGLog.debug(this, `Proxying ${req.method} ${req.url} -> ${routeToUrl}`);
76
+ this.proxy.web(req, res, {target: routeToUrl}, (err: Error) => {
77
+ GGLog.error(this, "Proxy error", err);
78
+ this.sendResponse(res, 502, {issue: "Proxy error: " + err.message + " (Did the service crash?)"});
79
+ });
80
+ return;
81
+ }
82
+
83
+ this.sendResponse(res, 404, {error: `No route for ${method} ${path}! Did you forget to start a service that defines this path?`});
84
+ }
85
+
86
+ private async readJsonBody(req: http.IncomingMessage): Promise<any> {
87
+ return new Promise((resolve) => {
88
+ let bodyStr = '';
89
+ req.on('data', (chunk: Buffer) => {
90
+ bodyStr += chunk.toString();
91
+ });
92
+ req.on('end', () => {
93
+ try {
94
+ resolve(bodyStr ? JSON.parse(bodyStr) : undefined);
95
+ } catch {
96
+ resolve(undefined);
97
+ }
98
+ });
99
+ req.on('error', () => resolve(undefined));
100
+ });
101
+ }
102
+
103
+ private sendResponse(res: http.ServerResponse, status: number, data?: any): void {
104
+ res.writeHead(status, {'Content-Type': 'application/json'});
105
+ res.end(data !== undefined ? JSON.stringify(data) : '');
106
+ }
107
+ }
@@ -1,159 +1,159 @@
1
- import * as http from 'http';
2
- import * as net from 'net';
3
- import {GGLog} from "@grest-ts/logger";
4
- import {Duplex} from "stream";
5
- import {HttpMethod} from "@grest-ts/common";
6
- import {HttpHandler} from "./HttpHandler";
7
- import {SocketHandler, SocketMessage} from "./SocketHandler";
8
- import {IPCClientRequest, IPCServerRequest} from "../common/IPCTypes";
9
- import {GGLocator, GGLocatorScope} from "@grest-ts/locator";
10
- import {GGContext} from "@grest-ts/context";
11
- import {GG_TRACE} from "@grest-ts/trace";
12
-
13
- export class IPCServer {
14
-
15
- private readonly httpServer: http.Server;
16
- private readonly httpHandler: HttpHandler;
17
- private readonly socketHandler: SocketHandler;
18
-
19
- private readonly expectedPort: number;
20
- private port: number = 0;
21
- private url: string = '';
22
-
23
- private readonly connections: Set<net.Socket> = new Set();
24
-
25
- private readonly scope: GGLocatorScope;
26
-
27
- constructor(port: number = 0) {
28
- this.expectedPort = port;
29
- this.scope = GGLocator.getScope();
30
-
31
- // Create handlers
32
- this.httpHandler = new HttpHandler();
33
- this.socketHandler = new SocketHandler();
34
-
35
- // Create HTTP server
36
- this.httpServer = http.createServer(async (req, res) => {
37
- await new GGContext("LocalRouter").run(async () => {
38
- GG_TRACE.init();
39
- await this.httpHandler.handleRequest(req, res);
40
- });
41
- });
42
-
43
- // Handle WebSocket upgrades
44
- // Note: Incoming TCP connections start with no async context (they originate from libuv/OS layer).
45
- this.httpServer.on('upgrade', async (req, socket, head) => {
46
- this.scope.ensureEntered();
47
- await new GGContext("LocalRouter").run(async () => {
48
- GG_TRACE.init();
49
- await this.socketHandler.handleUpgrade(req, socket as Duplex, head);
50
- });
51
- });
52
-
53
- // Track connections for cleanup
54
- this.httpServer.on('connection', (conn) => {
55
- this.connections.add(conn);
56
- conn.on('close', () => this.connections.delete(conn));
57
- });
58
- }
59
-
60
- public static defineRequest<Req, Res>(name: string): IPCServerRequest<Req, Res> {
61
- return name as IPCServerRequest<Req, Res>;
62
- }
63
-
64
- // -----------------------------------------------
65
- // Lifecycle
66
- // -----------------------------------------------
67
-
68
- public async start(): Promise<boolean> {
69
- return new Promise<boolean>((resolve, reject) => {
70
- const errorHandler = (err: any) => {
71
- if (err.code === 'EADDRINUSE') {
72
- resolve(false);
73
- } else {
74
- reject(err);
75
- }
76
- };
77
-
78
- this.httpServer.once('error', errorHandler);
79
-
80
- this.httpServer.listen(this.expectedPort, '0.0.0.0', () => {
81
- this.scope.ensureEntered();
82
- this.port = (this.httpServer.address() as any).port;
83
- this.url = `http://localhost:${this.port}`;
84
- this.httpServer.removeListener('error', errorHandler);
85
- GGLog.info(this, `Internal server started on port ${this.port}`);
86
- resolve(true);
87
- });
88
- });
89
- }
90
-
91
- public async teardown(): Promise<void> {
92
- // Teardown socket handler (closes clients and WSS)
93
- await this.socketHandler.teardown();
94
-
95
- // Close all HTTP connections
96
- this.connections.forEach(conn => conn.destroy());
97
- this.connections.clear();
98
-
99
- // Close the server
100
- await new Promise<void>((resolve, reject) => {
101
- this.httpServer.close((err) => {
102
- this.scope.ensureEntered();
103
- if (err) reject(err);
104
- else resolve();
105
- });
106
- });
107
-
108
- GGLog.info(this, 'Internal server stopped');
109
- }
110
-
111
- // -----------------------------------------------
112
- // Handling http/socket requests that services send between each other. Can intercept http here.
113
- // -----------------------------------------------
114
-
115
- public interceptHttp(method: HttpMethod, path: string, handler: (body: any, pathParams?: Record<string, string>, headers?: Record<string, string>) => Promise<any>): void {
116
- this.httpHandler.addRoute(method, path, handler);
117
- }
118
-
119
- public removeInterceptHttp(method: HttpMethod, path: string): void {
120
- this.httpHandler.removeRoute(method, path);
121
- }
122
-
123
- public setRouteProxyResolver(resolver: (path: string) => string) {
124
- this.httpHandler.setRouteResolver(resolver);
125
- this.socketHandler.setRouteResolver(resolver);
126
- }
127
-
128
- // -----------------------------------------------
129
- // Grest Framework internal messaging between either local discovery server or local test server.
130
- // -----------------------------------------------
131
-
132
- public onFrameworkMessage<Req, Res>(
133
- type: IPCServerRequest<Req, Res>,
134
- handler: (payload: Req, msg: SocketMessage) => Promise<Res>
135
- ): void {
136
- this.socketHandler.onMessage(type, async (msg) => handler(msg.data as Req, msg));
137
- }
138
-
139
- public async sendFrameworkMessage<Req, Res>(
140
- runtimeId: string,
141
- type: IPCClientRequest<Req, Res>,
142
- payload: Req,
143
- timeoutMs?: number
144
- ): Promise<Res> {
145
- return this.socketHandler.request<Res>(runtimeId, type, payload, timeoutMs);
146
- }
147
-
148
- // -----------------------------------------------
149
- // Accessors
150
- // -----------------------------------------------
151
-
152
- public getPort(): number {
153
- return this.port;
154
- }
155
-
156
- public getUrl(): string {
157
- return this.url;
158
- }
159
- }
1
+ import * as http from 'http';
2
+ import * as net from 'net';
3
+ import {GGLog} from "@grest-ts/logger";
4
+ import {Duplex} from "stream";
5
+ import {HttpMethod} from "@grest-ts/common";
6
+ import {HttpHandler} from "./HttpHandler";
7
+ import {SocketHandler, SocketMessage} from "./SocketHandler";
8
+ import {IPCClientRequest, IPCServerRequest} from "../common/IPCTypes";
9
+ import {GGLocator, GGLocatorScope} from "@grest-ts/locator";
10
+ import {GGContext} from "@grest-ts/context";
11
+ import {GG_TRACE} from "@grest-ts/trace";
12
+
13
+ export class IPCServer {
14
+
15
+ private readonly httpServer: http.Server;
16
+ private readonly httpHandler: HttpHandler;
17
+ private readonly socketHandler: SocketHandler;
18
+
19
+ private readonly expectedPort: number;
20
+ private port: number = 0;
21
+ private url: string = '';
22
+
23
+ private readonly connections: Set<net.Socket> = new Set();
24
+
25
+ private readonly scope: GGLocatorScope;
26
+
27
+ constructor(port: number = 0) {
28
+ this.expectedPort = port;
29
+ this.scope = GGLocator.getScope();
30
+
31
+ // Create handlers
32
+ this.httpHandler = new HttpHandler();
33
+ this.socketHandler = new SocketHandler();
34
+
35
+ // Create HTTP server
36
+ this.httpServer = http.createServer(async (req, res) => {
37
+ await new GGContext("LocalRouter").run(async () => {
38
+ GG_TRACE.init();
39
+ await this.httpHandler.handleRequest(req, res);
40
+ });
41
+ });
42
+
43
+ // Handle WebSocket upgrades
44
+ // Note: Incoming TCP connections start with no async context (they originate from libuv/OS layer).
45
+ this.httpServer.on('upgrade', async (req, socket, head) => {
46
+ this.scope.ensureEntered();
47
+ await new GGContext("LocalRouter").run(async () => {
48
+ GG_TRACE.init();
49
+ await this.socketHandler.handleUpgrade(req, socket as Duplex, head);
50
+ });
51
+ });
52
+
53
+ // Track connections for cleanup
54
+ this.httpServer.on('connection', (conn) => {
55
+ this.connections.add(conn);
56
+ conn.on('close', () => this.connections.delete(conn));
57
+ });
58
+ }
59
+
60
+ public static defineRequest<Req, Res>(name: string): IPCServerRequest<Req, Res> {
61
+ return name as IPCServerRequest<Req, Res>;
62
+ }
63
+
64
+ // -----------------------------------------------
65
+ // Lifecycle
66
+ // -----------------------------------------------
67
+
68
+ public async start(): Promise<boolean> {
69
+ return new Promise<boolean>((resolve, reject) => {
70
+ const errorHandler = (err: any) => {
71
+ if (err.code === 'EADDRINUSE') {
72
+ resolve(false);
73
+ } else {
74
+ reject(err);
75
+ }
76
+ };
77
+
78
+ this.httpServer.once('error', errorHandler);
79
+
80
+ this.httpServer.listen(this.expectedPort, '0.0.0.0', () => {
81
+ this.scope.ensureEntered();
82
+ this.port = (this.httpServer.address() as any).port;
83
+ this.url = `http://localhost:${this.port}`;
84
+ this.httpServer.removeListener('error', errorHandler);
85
+ GGLog.info(this, `Internal server started on port ${this.port}`);
86
+ resolve(true);
87
+ });
88
+ });
89
+ }
90
+
91
+ public async teardown(): Promise<void> {
92
+ // Teardown socket handler (closes clients and WSS)
93
+ await this.socketHandler.teardown();
94
+
95
+ // Close all HTTP connections
96
+ this.connections.forEach(conn => conn.destroy());
97
+ this.connections.clear();
98
+
99
+ // Close the server
100
+ await new Promise<void>((resolve, reject) => {
101
+ this.httpServer.close((err) => {
102
+ this.scope.ensureEntered();
103
+ if (err) reject(err);
104
+ else resolve();
105
+ });
106
+ });
107
+
108
+ GGLog.info(this, 'Internal server stopped');
109
+ }
110
+
111
+ // -----------------------------------------------
112
+ // Handling http/socket requests that services send between each other. Can intercept http here.
113
+ // -----------------------------------------------
114
+
115
+ public interceptHttp(method: HttpMethod, path: string, handler: (body: any, pathParams?: Record<string, string>, headers?: Record<string, string>) => Promise<any>): void {
116
+ this.httpHandler.addRoute(method, path, handler);
117
+ }
118
+
119
+ public removeInterceptHttp(method: HttpMethod, path: string): void {
120
+ this.httpHandler.removeRoute(method, path);
121
+ }
122
+
123
+ public setRouteProxyResolver(resolver: (path: string) => string) {
124
+ this.httpHandler.setRouteResolver(resolver);
125
+ this.socketHandler.setRouteResolver(resolver);
126
+ }
127
+
128
+ // -----------------------------------------------
129
+ // Grest Framework internal messaging between either local discovery server or local test server.
130
+ // -----------------------------------------------
131
+
132
+ public onFrameworkMessage<Req, Res>(
133
+ type: IPCServerRequest<Req, Res>,
134
+ handler: (payload: Req, msg: SocketMessage) => Promise<Res>
135
+ ): void {
136
+ this.socketHandler.onMessage(type, async (msg) => handler(msg.data as Req, msg));
137
+ }
138
+
139
+ public async sendFrameworkMessage<Req, Res>(
140
+ runtimeId: string,
141
+ type: IPCClientRequest<Req, Res>,
142
+ payload: Req,
143
+ timeoutMs?: number
144
+ ): Promise<Res> {
145
+ return this.socketHandler.request<Res>(runtimeId, type, payload, timeoutMs);
146
+ }
147
+
148
+ // -----------------------------------------------
149
+ // Accessors
150
+ // -----------------------------------------------
151
+
152
+ public getPort(): number {
153
+ return this.port;
154
+ }
155
+
156
+ public getUrl(): string {
157
+ return this.url;
158
+ }
159
+ }