@grest-ts/ipc 0.0.14 → 0.0.17
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/README.md +185 -0
- package/package.json +7 -8
package/README.md
CHANGED
|
@@ -3,3 +3,188 @@
|
|
|
3
3
|
> [Documentation](https://github.com/grest-ts/grest-ts#readme) | [All packages](https://github.com/grest-ts/grest-ts#package-reference)
|
|
4
4
|
<!-- GREST-TS-BANNER-END -->
|
|
5
5
|
|
|
6
|
+
# IPC Package (@grest-ts/ipc)
|
|
7
|
+
|
|
8
|
+
> **Internal package.** This is used by the framework internals (discovery, testkit). You should not need to use it directly in application code.
|
|
9
|
+
|
|
10
|
+
Type-safe inter-process communication over WebSocket with built-in HTTP routing and proxying. Provides the transport layer for local service discovery and the test framework's runtime communication.
|
|
11
|
+
|
|
12
|
+
## What it provides
|
|
13
|
+
|
|
14
|
+
- **IPCServer** - HTTP server with WebSocket support, route interception, and proxy routing
|
|
15
|
+
- **IPCClient** - WebSocket client that connects to an IPCServer
|
|
16
|
+
- **IPCSocket** - Low-level WebSocket wrapper with request-response messaging and fire-and-forget messages
|
|
17
|
+
- **Type-safe request definitions** - Branded string types (`IPCServerRequest`, `IPCClientRequest`) that enforce payload/response types at compile time
|
|
18
|
+
|
|
19
|
+
## How it is used
|
|
20
|
+
|
|
21
|
+
### 1. Service Discovery
|
|
22
|
+
|
|
23
|
+
The discovery package uses IPC for service registration and lookup between runtimes running locally.
|
|
24
|
+
|
|
25
|
+
**Defining request types:**
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import {IPCServer} from "@grest-ts/ipc"
|
|
29
|
+
|
|
30
|
+
export const GGDiscoveryIPC = {
|
|
31
|
+
discoveryServer: {
|
|
32
|
+
register: IPCServer.defineRequest<GGServiceDiscoveryEntry[], void>("discovery/register"),
|
|
33
|
+
discoverApi: IPCServer.defineRequest<string, DiscoverApiResult>("discovery/discoverApi"),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Server side** - the discovery server registers handlers on the IPCServer:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
constructor(server: IPCServer) {
|
|
42
|
+
server.onFrameworkMessage(GGDiscoveryIPC.discoveryServer.register, async (routes) => {
|
|
43
|
+
routes.forEach(route => this.addRoute(route))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
server.onFrameworkMessage(GGDiscoveryIPC.discoveryServer.discoverApi, async (apiName) => {
|
|
47
|
+
const route = this.getRoute(apiName)
|
|
48
|
+
if (route) return {success: true, url: this.server.getUrl()}
|
|
49
|
+
return {success: false, error: "Service not registered"}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Route unhandled HTTP/WebSocket traffic to actual services
|
|
53
|
+
server.setRouteProxyResolver((path) => {
|
|
54
|
+
return this.matchRoute(path)?.baseUrl || undefined
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Client side** - services connect and register themselves:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const client = new IPCClient(port)
|
|
63
|
+
await client.connect()
|
|
64
|
+
|
|
65
|
+
// Register routes
|
|
66
|
+
await client.sendFrameworkRequest(GGDiscoveryIPC.discoveryServer.register, entries)
|
|
67
|
+
|
|
68
|
+
// Discover another service
|
|
69
|
+
const result = await client.sendFrameworkRequest(GGDiscoveryIPC.discoveryServer.discoverApi, "my-api")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Test Framework (Runner <-> Worker communication)
|
|
73
|
+
|
|
74
|
+
The testkit uses IPC for bidirectional communication between the test runner process and runtime worker processes.
|
|
75
|
+
|
|
76
|
+
**Test runner (server side)** - sends commands to workers and receives registrations:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Runner creates the server
|
|
80
|
+
const ipcServer = new IPCServer(port)
|
|
81
|
+
await ipcServer.start()
|
|
82
|
+
|
|
83
|
+
// Handle worker registrations
|
|
84
|
+
ipcServer.onFrameworkMessage(TestableIPC.server.registerKeys, async (payload) => {
|
|
85
|
+
runtime.registerLocatorKeys(payload.keys)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Send command to a specific worker by runtimeId
|
|
89
|
+
await ipcServer.sendFrameworkMessage(runtimeId, GGConfigIPC.worker.update, {
|
|
90
|
+
storeName: "myStore",
|
|
91
|
+
keyName: "myKey",
|
|
92
|
+
value: newValue
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Runtime worker (client side)** - connects with a `runtimeId` and handles commands from the runner:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const client = new IPCClient(config.testRouterPort)
|
|
100
|
+
|
|
101
|
+
// runtimeId allows the server to target this specific worker
|
|
102
|
+
await client.connect(config.runtimeId)
|
|
103
|
+
|
|
104
|
+
// Handle commands from test runner
|
|
105
|
+
client.onFrameworkRequest(GGConfigIPC.worker.update, async (payload) => {
|
|
106
|
+
await getStore(payload.storeName).updateValueOverride(
|
|
107
|
+
GGConfigKey.getKey(payload.keyName),
|
|
108
|
+
payload.value
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Send data back to the runner
|
|
113
|
+
await client.sendFrameworkRequest(TestableIPC.server.registerKeys, {
|
|
114
|
+
runtimeId: config.runtimeId,
|
|
115
|
+
keys: runtime.scope.getKeys()
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 3. HTTP Interception (Test Mocking/Spying)
|
|
120
|
+
|
|
121
|
+
The HTTP testkit uses the IPCServer's HTTP routing to intercept requests during tests - either mocking responses or spying on traffic while proxying to real services.
|
|
122
|
+
|
|
123
|
+
**Mock mode** - intercept and return custom responses:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// Route discovery traffic for this API to the test server
|
|
127
|
+
discoveryServer.addRoute({api: "user-api", baseUrl: server.getUrl(), pathPrefix: "/users"})
|
|
128
|
+
|
|
129
|
+
// Register a mock handler
|
|
130
|
+
server.interceptHttp("GET", "/users/:id", async (body, pathParams) => {
|
|
131
|
+
return {success: true, statusCode: 200, data: {id: pathParams.id, name: "Mock User"}}
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Spy mode** - observe traffic while forwarding to the real service:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
server.interceptHttp("POST", "/orders", async (body, _pathParams, headers) => {
|
|
139
|
+
await interceptor.onRequest(body) // observe the request
|
|
140
|
+
const response = await fetch(targetUrl, {...}) // forward to real service
|
|
141
|
+
await interceptor.onResponse(response) // observe the response
|
|
142
|
+
return response
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 4. Leader Election
|
|
147
|
+
|
|
148
|
+
The resilient discovery client uses IPC's port-binding behavior for leader election. The first instance to successfully start the IPCServer on a known port becomes the leader; others become followers that connect as clients.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const server = new IPCServer(knownPort)
|
|
152
|
+
if (await server.start()) {
|
|
153
|
+
// Port was available - this instance is the leader
|
|
154
|
+
this.isLeader = true
|
|
155
|
+
} else {
|
|
156
|
+
// Port already taken - connect as follower
|
|
157
|
+
await client.connect()
|
|
158
|
+
client.onClose(async () => {
|
|
159
|
+
// Leader died - try to become the new leader
|
|
160
|
+
await this.becomeLeaderOrFollower()
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Defining Request Types
|
|
166
|
+
|
|
167
|
+
Requests are defined as branded strings with phantom type parameters for compile-time safety.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// For requests sent TO the server (client -> server)
|
|
171
|
+
const myRequest = IPCServer.defineRequest<RequestPayload, ResponsePayload>("my/request")
|
|
172
|
+
|
|
173
|
+
// For requests sent TO the client (server -> client)
|
|
174
|
+
const myCommand = IPCClient.defineRequest<CommandPayload, CommandResult>("my/command")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Architecture
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
IPCServer
|
|
181
|
+
├── HttpHandler - Route matching (find-my-way), request handling, HTTP proxying (http-proxy)
|
|
182
|
+
├── SocketHandler - WebSocket upgrade handling, client tracking by clientId/runtimeId, WS proxying
|
|
183
|
+
│ └── IPCSocket - Per-connection message framing, request-response correlation, timeouts
|
|
184
|
+
└── http.Server - Underlying Node.js HTTP server (handles both HTTP and WS upgrade)
|
|
185
|
+
|
|
186
|
+
IPCClient
|
|
187
|
+
└── IPCSocket - WebSocket connection to server, message/request handlers
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Message protocol:** Messages are framed as `type:id:path:data` over WebSocket text frames, where type is `m` (fire-and-forget), `r` (request), or `s` (response). Data is JSON-serialized.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grest-ts/ipc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Service internal process communications library. For local testing only.",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"url": "https://github.com/grest-ts/grest-ts.git",
|
|
26
26
|
"directory": "packages/ipc"
|
|
27
27
|
},
|
|
28
|
-
"homepage": "https://
|
|
28
|
+
"homepage": "https://grest-ts.com/packages/ipc",
|
|
29
29
|
"bugs": {
|
|
30
30
|
"url": "https://github.com/grest-ts/grest-ts/issues"
|
|
31
31
|
},
|
|
@@ -44,17 +44,16 @@
|
|
|
44
44
|
"node": ">=25"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@grest-ts/common": "0.0.
|
|
48
|
-
"@grest-ts/context": "0.0.
|
|
49
|
-
"@grest-ts/locator": "0.0.
|
|
50
|
-
"@grest-ts/logger": "0.0.
|
|
51
|
-
"@grest-ts/trace": "0.0.
|
|
47
|
+
"@grest-ts/common": "0.0.17",
|
|
48
|
+
"@grest-ts/context": "0.0.17",
|
|
49
|
+
"@grest-ts/locator": "0.0.17",
|
|
50
|
+
"@grest-ts/logger": "0.0.17",
|
|
51
|
+
"@grest-ts/trace": "0.0.17",
|
|
52
52
|
"find-my-way": "^9.4.0",
|
|
53
53
|
"http-proxy": "^1.18.1",
|
|
54
54
|
"ws": "^8.19.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@grest-ts/x-packager": "0.0.14",
|
|
58
57
|
"@types/http-proxy": "^1.17.17"
|
|
59
58
|
}
|
|
60
59
|
}
|