@zap-js/client 0.0.2 → 0.0.4
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 +310 -24
- package/bin/zap +0 -0
- package/bin/zap-codegen +0 -0
- package/dist/cli/commands/build.d.ts +11 -0
- package/dist/cli/commands/build.js +282 -0
- package/dist/cli/commands/codegen.d.ts +8 -0
- package/dist/cli/commands/codegen.js +95 -0
- package/dist/cli/commands/dev.d.ts +20 -0
- package/dist/cli/commands/dev.js +78 -0
- package/dist/cli/commands/new.d.ts +9 -0
- package/dist/cli/commands/new.js +307 -0
- package/dist/cli/commands/routes-old.d.ts +9 -0
- package/dist/cli/commands/routes-old.js +106 -0
- package/dist/cli/commands/routes.d.ts +11 -0
- package/dist/cli/commands/routes.js +280 -0
- package/dist/cli/commands/serve.d.ts +17 -0
- package/dist/cli/commands/serve.js +386 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +76 -0
- package/dist/cli/utils/index.d.ts +2 -0
- package/dist/cli/utils/index.js +2 -0
- package/dist/cli/utils/logger.d.ts +84 -0
- package/dist/cli/utils/logger.js +181 -0
- package/dist/cli/utils/port-finder.d.ts +8 -0
- package/dist/cli/utils/port-finder.js +48 -0
- package/dist/dev-server/codegen-runner.d.ts +41 -0
- package/dist/dev-server/codegen-runner.js +172 -0
- package/dist/dev-server/hot-reload.d.ts +72 -0
- package/dist/dev-server/hot-reload.js +280 -0
- package/dist/dev-server/index.d.ts +8 -0
- package/dist/dev-server/index.js +8 -0
- package/dist/dev-server/route-scanner.d.ts +71 -0
- package/dist/dev-server/route-scanner.js +114 -0
- package/dist/dev-server/rust-builder.d.ts +66 -0
- package/dist/dev-server/rust-builder.js +286 -0
- package/dist/dev-server/server.d.ts +147 -0
- package/dist/dev-server/server.js +658 -0
- package/dist/dev-server/vite-proxy.d.ts +56 -0
- package/dist/dev-server/vite-proxy.js +212 -0
- package/dist/dev-server/watcher.d.ts +48 -0
- package/dist/dev-server/watcher.js +127 -0
- package/dist/router/codegen-enhanced.d.ts +5 -0
- package/dist/router/codegen-enhanced.js +275 -0
- package/dist/router/codegen.d.ts +17 -0
- package/dist/router/codegen.js +654 -0
- package/dist/router/index.d.ts +16 -0
- package/dist/router/index.js +19 -0
- package/dist/router/scanner.d.ts +86 -0
- package/dist/router/scanner.js +689 -0
- package/dist/router/ssg.d.ts +115 -0
- package/dist/router/ssg.js +202 -0
- package/dist/router/types.d.ts +124 -0
- package/dist/router/types.js +9 -0
- package/dist/router/watch.d.ts +38 -0
- package/dist/router/watch.js +135 -0
- package/dist/runtime/csrf.d.ts +146 -0
- package/dist/runtime/csrf.js +166 -0
- package/dist/runtime/error-boundary.d.ts +129 -0
- package/dist/runtime/error-boundary.js +287 -0
- package/dist/runtime/hooks.d.ts +83 -0
- package/dist/runtime/hooks.js +96 -0
- package/dist/runtime/index.d.ts +229 -0
- package/dist/runtime/index.js +449 -0
- package/dist/runtime/ipc-client.d.ts +144 -0
- package/dist/runtime/ipc-client.js +621 -0
- package/dist/runtime/logger.d.ts +71 -0
- package/dist/runtime/logger.js +164 -0
- package/dist/runtime/middleware.d.ts +66 -0
- package/dist/runtime/middleware.js +114 -0
- package/dist/runtime/process-manager.d.ts +51 -0
- package/dist/runtime/process-manager.js +207 -0
- package/dist/runtime/router-simple.d.ts +98 -0
- package/dist/runtime/router-simple.js +330 -0
- package/dist/runtime/router.d.ts +103 -0
- package/dist/runtime/router.js +435 -0
- package/dist/runtime/rpc-client.d.ts +35 -0
- package/dist/runtime/rpc-client.js +140 -0
- package/dist/runtime/streaming-utils.d.ts +86 -0
- package/dist/runtime/streaming-utils.js +150 -0
- package/dist/runtime/types.d.ts +465 -0
- package/dist/runtime/types.js +60 -0
- package/dist/runtime/websockets-utils.d.ts +50 -0
- package/dist/runtime/websockets-utils.js +92 -0
- package/package.json +30 -20
- package/index.js +0 -29
- package/internal/cli/package.json +0 -46
- package/internal/cli/tsconfig.tsbuildinfo +0 -1
- package/internal/dev-server/node_modules/ora/index.d.ts +0 -332
- package/internal/dev-server/node_modules/ora/index.js +0 -416
- package/internal/dev-server/node_modules/ora/license +0 -9
- package/internal/dev-server/node_modules/ora/node_modules/string-width/index.d.ts +0 -36
- package/internal/dev-server/node_modules/ora/node_modules/string-width/index.js +0 -65
- package/internal/dev-server/node_modules/ora/node_modules/string-width/license +0 -9
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/LICENSE-MIT.txt +0 -20
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/README.md +0 -107
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.d.ts +0 -3
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.js +0 -4
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.mjs +0 -4
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/package.json +0 -46
- package/internal/dev-server/node_modules/ora/node_modules/string-width/package.json +0 -60
- package/internal/dev-server/node_modules/ora/node_modules/string-width/readme.md +0 -62
- package/internal/dev-server/node_modules/ora/package.json +0 -66
- package/internal/dev-server/node_modules/ora/readme.md +0 -325
- package/internal/dev-server/package.json +0 -41
- package/internal/router/package.json +0 -28
- package/internal/runtime/package.json +0 -41
- package/internal/runtime/src/error-boundary.tsx +0 -476
- package/internal/runtime/src/router-simple.tsx +0 -640
- package/internal/runtime/src/router.tsx +0 -771
- package/internal/runtime/tsconfig.tsbuildinfo +0 -1
- package/src/errors.js +0 -33
- package/src/logger.js +0 -10
- package/src/middleware.js +0 -32
- package/src/router.js +0 -41
- package/src/types.js +0 -39
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import { createServer, createConnection } from "net";
|
|
2
|
+
import { unlinkSync, existsSync } from "fs";
|
|
3
|
+
import { EventEmitter } from "events";
|
|
4
|
+
import { encode, decode } from "@msgpack/msgpack";
|
|
5
|
+
import { isAsyncIterable } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Serialize an IPC message to bytes
|
|
8
|
+
*/
|
|
9
|
+
function serializeMessage(msg, encoding) {
|
|
10
|
+
if (encoding === "msgpack") {
|
|
11
|
+
return Buffer.from(encode(msg));
|
|
12
|
+
}
|
|
13
|
+
return Buffer.from(JSON.stringify(msg));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Deserialize an IPC message from bytes (auto-detects encoding)
|
|
17
|
+
*/
|
|
18
|
+
function deserializeMessage(data) {
|
|
19
|
+
if (data.length === 0) {
|
|
20
|
+
throw new Error("Empty message");
|
|
21
|
+
}
|
|
22
|
+
// Auto-detect: JSON starts with '{' (0x7B), MessagePack maps start with 0x80-0xBF
|
|
23
|
+
const firstByte = data[0];
|
|
24
|
+
if (firstByte === 0x7b) {
|
|
25
|
+
// '{' character = JSON
|
|
26
|
+
return JSON.parse(data.toString("utf-8"));
|
|
27
|
+
}
|
|
28
|
+
// MessagePack
|
|
29
|
+
return decode(data);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Write a length-prefixed message to a socket
|
|
33
|
+
*/
|
|
34
|
+
function writeFramedMessage(socket, msg, encoding) {
|
|
35
|
+
const payload = serializeMessage(msg, encoding);
|
|
36
|
+
const length = payload.length;
|
|
37
|
+
// 4-byte big-endian length prefix
|
|
38
|
+
const lengthBuf = Buffer.alloc(4);
|
|
39
|
+
lengthBuf.writeUInt32BE(length, 0);
|
|
40
|
+
// ATOMIC: Single write with combined buffer to prevent frame corruption
|
|
41
|
+
const frame = Buffer.concat([lengthBuf, payload]);
|
|
42
|
+
socket.write(frame);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* FrameReader - reads length-prefixed frames from a socket
|
|
46
|
+
*/
|
|
47
|
+
class FrameReader {
|
|
48
|
+
constructor(onFrame) {
|
|
49
|
+
this.buffer = Buffer.alloc(0);
|
|
50
|
+
this.onFrame = onFrame;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Process incoming data chunks
|
|
54
|
+
*/
|
|
55
|
+
push(chunk) {
|
|
56
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
57
|
+
// Process complete frames
|
|
58
|
+
while (this.buffer.length >= 4) {
|
|
59
|
+
const length = this.buffer.readUInt32BE(0);
|
|
60
|
+
// Check for unreasonably large messages (100MB limit)
|
|
61
|
+
if (length > 100 * 1024 * 1024) {
|
|
62
|
+
throw new Error(`Message too large: ${length} bytes`);
|
|
63
|
+
}
|
|
64
|
+
// Wait for complete frame
|
|
65
|
+
if (this.buffer.length < 4 + length) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
// Extract frame
|
|
69
|
+
const frame = this.buffer.subarray(4, 4 + length);
|
|
70
|
+
this.buffer = this.buffer.subarray(4 + length);
|
|
71
|
+
// Emit frame
|
|
72
|
+
this.onFrame(frame);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reset the buffer
|
|
77
|
+
*/
|
|
78
|
+
reset() {
|
|
79
|
+
this.buffer = Buffer.alloc(0);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* WebSocket connection implementation for TypeScript handlers
|
|
84
|
+
*/
|
|
85
|
+
class WsConnectionImpl {
|
|
86
|
+
constructor(id, path, headers, handlerId, server) {
|
|
87
|
+
this.id = id;
|
|
88
|
+
this.path = path;
|
|
89
|
+
this.headers = headers;
|
|
90
|
+
this.handlerId = handlerId;
|
|
91
|
+
this.server = server;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Send a text message to the client
|
|
95
|
+
*/
|
|
96
|
+
send(data) {
|
|
97
|
+
this.server.sendWsMessage(this.id, data, false);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Send binary data to the client
|
|
101
|
+
*/
|
|
102
|
+
sendBinary(data) {
|
|
103
|
+
// Base64 encode binary data for IPC transport
|
|
104
|
+
const encoded = Buffer.from(data).toString("base64");
|
|
105
|
+
this.server.sendWsMessage(this.id, encoded, true);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Close the connection
|
|
109
|
+
*/
|
|
110
|
+
close(code, reason) {
|
|
111
|
+
this.server.closeWsConnection(this.id, code, reason);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* IpcServer
|
|
116
|
+
*
|
|
117
|
+
* Listens on a Unix socket for IPC messages from the Rust backend.
|
|
118
|
+
* The Rust server sends handler invocation requests, which we dispatch
|
|
119
|
+
* to the registered TypeScript handlers and send responses back.
|
|
120
|
+
*
|
|
121
|
+
* Protocol: Length-prefixed MessagePack (default) with JSON fallback
|
|
122
|
+
* Frame format: [4-byte big-endian length][payload]
|
|
123
|
+
*/
|
|
124
|
+
export class IpcServer {
|
|
125
|
+
constructor(socketPath, encoding = "msgpack") {
|
|
126
|
+
this.server = null;
|
|
127
|
+
this.handlers = new Map();
|
|
128
|
+
this.wsHandlers = new Map();
|
|
129
|
+
this.wsConnections = new Map();
|
|
130
|
+
this.currentSocket = null;
|
|
131
|
+
this.socketPath = socketPath;
|
|
132
|
+
this.encoding = encoding;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Register a handler function for a specific handler ID
|
|
136
|
+
*/
|
|
137
|
+
registerHandler(handlerId, handler) {
|
|
138
|
+
this.handlers.set(handlerId, handler);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Register a WebSocket handler for a specific handler ID
|
|
142
|
+
*/
|
|
143
|
+
registerWsHandler(handlerId, handler) {
|
|
144
|
+
this.wsHandlers.set(handlerId, handler);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Start the IPC server listening on the Unix socket
|
|
148
|
+
*/
|
|
149
|
+
async start() {
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
try {
|
|
152
|
+
// Clean up old socket file if it exists
|
|
153
|
+
if (existsSync(this.socketPath)) {
|
|
154
|
+
try {
|
|
155
|
+
unlinkSync(this.socketPath);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Ignore if we can't delete it
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Create Unix domain socket server
|
|
162
|
+
this.server = createServer((socket) => {
|
|
163
|
+
this.handleConnection(socket);
|
|
164
|
+
});
|
|
165
|
+
this.server.on("error", (err) => {
|
|
166
|
+
console.error(`[IPC] Server error:`, err);
|
|
167
|
+
reject(err);
|
|
168
|
+
});
|
|
169
|
+
this.server.listen(this.socketPath, () => {
|
|
170
|
+
console.log(`[IPC] IPC server listening on ${this.socketPath} (${this.encoding})`);
|
|
171
|
+
resolve();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
reject(error);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Handle a new IPC connection from the Rust server
|
|
181
|
+
*/
|
|
182
|
+
handleConnection(socket) {
|
|
183
|
+
console.log(`[IPC] Client connected`);
|
|
184
|
+
this.currentSocket = socket;
|
|
185
|
+
const frameReader = new FrameReader((frame) => {
|
|
186
|
+
void this.handleFrame(frame, socket);
|
|
187
|
+
});
|
|
188
|
+
socket.on("data", (chunk) => {
|
|
189
|
+
try {
|
|
190
|
+
frameReader.push(chunk);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
console.error(`[IPC] Frame error:`, error);
|
|
194
|
+
socket.destroy();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
socket.on("close", () => {
|
|
198
|
+
console.log(`[IPC] Client disconnected`);
|
|
199
|
+
if (this.currentSocket === socket) {
|
|
200
|
+
this.currentSocket = null;
|
|
201
|
+
}
|
|
202
|
+
// Clean up any WebSocket connections for this socket
|
|
203
|
+
this.wsConnections.clear();
|
|
204
|
+
});
|
|
205
|
+
socket.on("error", (error) => {
|
|
206
|
+
console.error(`[IPC] Connection error:`, error);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Handle a complete frame
|
|
211
|
+
*/
|
|
212
|
+
async handleFrame(frame, socket) {
|
|
213
|
+
try {
|
|
214
|
+
const message = deserializeMessage(frame);
|
|
215
|
+
await this.processMessage(message, socket);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
219
|
+
console.error(`[IPC] Error processing message:`, errorMessage);
|
|
220
|
+
const errorResponse = {
|
|
221
|
+
type: "error",
|
|
222
|
+
code: "HANDLER_ERROR",
|
|
223
|
+
message: errorMessage,
|
|
224
|
+
status: 500,
|
|
225
|
+
digest: crypto.randomUUID(),
|
|
226
|
+
};
|
|
227
|
+
writeFramedMessage(socket, errorResponse, this.encoding);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Process an incoming IPC message
|
|
232
|
+
*/
|
|
233
|
+
async processMessage(message, socket) {
|
|
234
|
+
console.log(`[IPC] Received message type: ${message.type}`);
|
|
235
|
+
if (message.type === "invoke_handler") {
|
|
236
|
+
const invokeMsg = message;
|
|
237
|
+
const { handler_id, request } = invokeMsg;
|
|
238
|
+
console.log(`[IPC] Looking for handler: ${handler_id}`);
|
|
239
|
+
console.log(`[IPC] Available handlers: ${Array.from(this.handlers.keys()).join(', ')}`);
|
|
240
|
+
const handler = this.handlers.get(handler_id);
|
|
241
|
+
if (!handler) {
|
|
242
|
+
console.error(`[IPC] Handler NOT FOUND: ${handler_id}`);
|
|
243
|
+
writeFramedMessage(socket, {
|
|
244
|
+
type: "error",
|
|
245
|
+
code: "HANDLER_NOT_FOUND",
|
|
246
|
+
message: `Handler ${handler_id} not found`,
|
|
247
|
+
status: 404,
|
|
248
|
+
digest: crypto.randomUUID(),
|
|
249
|
+
}, this.encoding);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
console.log(`[IPC] Invoking handler: ${handler_id} for ${request.method} ${request.path}`);
|
|
254
|
+
const result = handler(request);
|
|
255
|
+
// Check if this is a streaming response (async iterable)
|
|
256
|
+
if (isAsyncIterable(result)) {
|
|
257
|
+
await this.handleStreamingResponse(result, handler_id, socket);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Regular response - await the promise
|
|
261
|
+
const response = await result;
|
|
262
|
+
console.log(`[IPC] Handler result:`, JSON.stringify(response, null, 2));
|
|
263
|
+
writeFramedMessage(socket, {
|
|
264
|
+
type: "handler_response",
|
|
265
|
+
handler_id,
|
|
266
|
+
status: response.status || 200,
|
|
267
|
+
headers: response.headers || { "content-type": "application/json" },
|
|
268
|
+
body: response.body || "{}",
|
|
269
|
+
}, this.encoding);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
274
|
+
console.error(`[IPC] Error executing handler ${handler_id}:`, errorMessage);
|
|
275
|
+
writeFramedMessage(socket, {
|
|
276
|
+
type: "error",
|
|
277
|
+
code: "HANDLER_EXECUTION_ERROR",
|
|
278
|
+
message: errorMessage,
|
|
279
|
+
status: 500,
|
|
280
|
+
digest: crypto.randomUUID(),
|
|
281
|
+
}, this.encoding);
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Health check message
|
|
286
|
+
if (message.type === "health_check") {
|
|
287
|
+
console.log(`[IPC] Health check received`);
|
|
288
|
+
writeFramedMessage(socket, { type: "health_check_response" }, this.encoding);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
// WebSocket connect message - new client connected
|
|
292
|
+
if (message.type === "ws_connect") {
|
|
293
|
+
const { connection_id, handler_id, path, headers } = message;
|
|
294
|
+
console.log(`[IPC] WebSocket connect: ${connection_id} for handler ${handler_id}`);
|
|
295
|
+
const wsHandler = this.wsHandlers.get(handler_id);
|
|
296
|
+
if (!wsHandler) {
|
|
297
|
+
console.error(`[IPC] WebSocket handler NOT FOUND: ${handler_id}`);
|
|
298
|
+
writeFramedMessage(socket, {
|
|
299
|
+
type: "error",
|
|
300
|
+
code: "WS_HANDLER_NOT_FOUND",
|
|
301
|
+
message: `WebSocket handler ${handler_id} not found`,
|
|
302
|
+
status: 404,
|
|
303
|
+
digest: crypto.randomUUID(),
|
|
304
|
+
}, this.encoding);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// Create connection object and store it
|
|
308
|
+
const connection = new WsConnectionImpl(connection_id, path, headers, handler_id, this);
|
|
309
|
+
this.wsConnections.set(connection_id, connection);
|
|
310
|
+
// Call onConnect if defined
|
|
311
|
+
try {
|
|
312
|
+
if (wsHandler.onConnect) {
|
|
313
|
+
await wsHandler.onConnect(connection);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
318
|
+
console.error(`[IPC] WebSocket onConnect error:`, errorMessage);
|
|
319
|
+
// Close the connection on error
|
|
320
|
+
this.closeWsConnection(connection_id, 1011, errorMessage);
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// WebSocket message - message from client
|
|
325
|
+
if (message.type === "ws_message") {
|
|
326
|
+
const { connection_id, data, binary } = message;
|
|
327
|
+
console.log(`[IPC] WebSocket message from ${connection_id}: ${data.length} bytes (binary: ${binary})`);
|
|
328
|
+
const connection = this.wsConnections.get(connection_id);
|
|
329
|
+
if (!connection) {
|
|
330
|
+
console.error(`[IPC] WebSocket connection NOT FOUND: ${connection_id}`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Use handler_id from the connection (set during connect)
|
|
334
|
+
const wsHandler = this.wsHandlers.get(connection.handlerId);
|
|
335
|
+
if (!wsHandler || !wsHandler.onMessage) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
let messageData;
|
|
340
|
+
if (binary) {
|
|
341
|
+
// Decode base64 binary data
|
|
342
|
+
messageData = Buffer.from(data, "base64");
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
messageData = data;
|
|
346
|
+
}
|
|
347
|
+
await wsHandler.onMessage(connection, messageData);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
351
|
+
console.error(`[IPC] WebSocket onMessage error:`, errorMessage);
|
|
352
|
+
if (wsHandler.onError) {
|
|
353
|
+
try {
|
|
354
|
+
await wsHandler.onError(connection, error instanceof Error ? error : new Error(errorMessage));
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Ignore errors in error handler
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// WebSocket close - client disconnected
|
|
364
|
+
if (message.type === "ws_close") {
|
|
365
|
+
const { connection_id, code, reason } = message;
|
|
366
|
+
console.log(`[IPC] WebSocket close: ${connection_id} (code: ${code}, reason: ${reason})`);
|
|
367
|
+
const connection = this.wsConnections.get(connection_id);
|
|
368
|
+
if (!connection) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
// Use handler_id from the connection (set during connect)
|
|
372
|
+
const wsHandler = this.wsHandlers.get(connection.handlerId);
|
|
373
|
+
if (wsHandler && wsHandler.onClose) {
|
|
374
|
+
try {
|
|
375
|
+
await wsHandler.onClose(connection, code, reason);
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
379
|
+
console.error(`[IPC] WebSocket onClose error:`, errorMessage);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Remove from connections map
|
|
383
|
+
this.wsConnections.delete(connection_id);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
console.error(`[IPC] Unknown message type: ${message.type}`);
|
|
387
|
+
writeFramedMessage(socket, {
|
|
388
|
+
type: "error",
|
|
389
|
+
code: "UNKNOWN_MESSAGE_TYPE",
|
|
390
|
+
message: `Unknown message type: ${message.type}`,
|
|
391
|
+
status: 400,
|
|
392
|
+
digest: crypto.randomUUID(),
|
|
393
|
+
}, this.encoding);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Handle a streaming response from a handler
|
|
397
|
+
*/
|
|
398
|
+
async handleStreamingResponse(stream, handlerId, socket) {
|
|
399
|
+
const streamId = `stream_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
400
|
+
console.log(`[IPC] Starting streaming response: ${streamId}`);
|
|
401
|
+
// Send stream start message
|
|
402
|
+
writeFramedMessage(socket, {
|
|
403
|
+
type: "stream_start",
|
|
404
|
+
stream_id: streamId,
|
|
405
|
+
status: 200,
|
|
406
|
+
headers: { "content-type": "text/event-stream" },
|
|
407
|
+
}, this.encoding);
|
|
408
|
+
try {
|
|
409
|
+
// Stream chunks
|
|
410
|
+
for await (const chunk of stream) {
|
|
411
|
+
let data;
|
|
412
|
+
if (chunk.bytes) {
|
|
413
|
+
// Binary data - base64 encode
|
|
414
|
+
data = Buffer.from(chunk.bytes).toString("base64");
|
|
415
|
+
}
|
|
416
|
+
else if (chunk.data) {
|
|
417
|
+
// String data - base64 encode for consistency
|
|
418
|
+
data = Buffer.from(chunk.data, "utf-8").toString("base64");
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
continue; // Skip empty chunks
|
|
422
|
+
}
|
|
423
|
+
writeFramedMessage(socket, {
|
|
424
|
+
type: "stream_chunk",
|
|
425
|
+
stream_id: streamId,
|
|
426
|
+
data,
|
|
427
|
+
}, this.encoding);
|
|
428
|
+
}
|
|
429
|
+
// Send stream end message
|
|
430
|
+
writeFramedMessage(socket, {
|
|
431
|
+
type: "stream_end",
|
|
432
|
+
stream_id: streamId,
|
|
433
|
+
}, this.encoding);
|
|
434
|
+
console.log(`[IPC] Streaming response completed: ${streamId}`);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
438
|
+
console.error(`[IPC] Streaming error for ${handlerId}:`, errorMessage);
|
|
439
|
+
// Send error message
|
|
440
|
+
writeFramedMessage(socket, {
|
|
441
|
+
type: "error",
|
|
442
|
+
code: "STREAM_ERROR",
|
|
443
|
+
message: errorMessage,
|
|
444
|
+
status: 500,
|
|
445
|
+
digest: crypto.randomUUID(),
|
|
446
|
+
}, this.encoding);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Send a message to a WebSocket client via the Rust server
|
|
451
|
+
*/
|
|
452
|
+
sendWsMessage(connectionId, data, binary) {
|
|
453
|
+
if (!this.currentSocket) {
|
|
454
|
+
console.error(`[IPC] Cannot send WebSocket message: no active socket`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
writeFramedMessage(this.currentSocket, {
|
|
458
|
+
type: "ws_send",
|
|
459
|
+
connection_id: connectionId,
|
|
460
|
+
data,
|
|
461
|
+
binary,
|
|
462
|
+
}, this.encoding);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Close a WebSocket connection via the Rust server
|
|
466
|
+
*/
|
|
467
|
+
closeWsConnection(connectionId, code, reason) {
|
|
468
|
+
// Remove from connections map
|
|
469
|
+
const connection = this.wsConnections.get(connectionId);
|
|
470
|
+
const handlerId = connection?.handlerId || "";
|
|
471
|
+
if (connection) {
|
|
472
|
+
this.wsConnections.delete(connectionId);
|
|
473
|
+
}
|
|
474
|
+
if (!this.currentSocket) {
|
|
475
|
+
console.error(`[IPC] Cannot close WebSocket connection: no active socket`);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
writeFramedMessage(this.currentSocket, {
|
|
479
|
+
type: "ws_close",
|
|
480
|
+
connection_id: connectionId,
|
|
481
|
+
handler_id: handlerId,
|
|
482
|
+
code,
|
|
483
|
+
reason,
|
|
484
|
+
}, this.encoding);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Stop the IPC server
|
|
488
|
+
*/
|
|
489
|
+
async stop() {
|
|
490
|
+
// Close all WebSocket connections
|
|
491
|
+
for (const [connectionId] of this.wsConnections) {
|
|
492
|
+
this.closeWsConnection(connectionId, 1001, "Server shutting down");
|
|
493
|
+
}
|
|
494
|
+
this.wsConnections.clear();
|
|
495
|
+
if (this.server) {
|
|
496
|
+
return new Promise((resolve) => {
|
|
497
|
+
this.server.close(() => {
|
|
498
|
+
// Clean up socket file
|
|
499
|
+
if (existsSync(this.socketPath)) {
|
|
500
|
+
try {
|
|
501
|
+
unlinkSync(this.socketPath);
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Ignore
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
resolve();
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* IpcClient
|
|
515
|
+
*
|
|
516
|
+
* Connects to a Unix socket to communicate with the Rust server.
|
|
517
|
+
* Used for RPC calls from TypeScript to Rust.
|
|
518
|
+
*
|
|
519
|
+
* Protocol: Length-prefixed MessagePack (default) with JSON fallback
|
|
520
|
+
*/
|
|
521
|
+
export class IpcClient extends EventEmitter {
|
|
522
|
+
constructor(socketPath, encoding = "msgpack") {
|
|
523
|
+
super();
|
|
524
|
+
this.socket = null;
|
|
525
|
+
this.connected = false;
|
|
526
|
+
this.frameReader = null;
|
|
527
|
+
this.socketPath = socketPath;
|
|
528
|
+
this.encoding = encoding;
|
|
529
|
+
this.connect();
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Connect to the Unix socket
|
|
533
|
+
*/
|
|
534
|
+
connect() {
|
|
535
|
+
this.socket = createConnection(this.socketPath);
|
|
536
|
+
this.socket.on("connect", () => {
|
|
537
|
+
this.connected = true;
|
|
538
|
+
this.emit("connect");
|
|
539
|
+
// Set up frame reader for length-prefixed messages
|
|
540
|
+
this.frameReader = new FrameReader((frame) => {
|
|
541
|
+
try {
|
|
542
|
+
const message = deserializeMessage(frame);
|
|
543
|
+
this.emit("message", message);
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
this.emit("error", new Error(`Failed to deserialize message: ${error}`));
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
this.socket.on("data", (chunk) => {
|
|
551
|
+
if (this.frameReader) {
|
|
552
|
+
try {
|
|
553
|
+
this.frameReader.push(chunk);
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
this.emit("error", error);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
this.socket.on("error", (err) => {
|
|
561
|
+
this.connected = false;
|
|
562
|
+
this.emit("error", err);
|
|
563
|
+
});
|
|
564
|
+
this.socket.on("close", () => {
|
|
565
|
+
this.connected = false;
|
|
566
|
+
this.emit("close");
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Send a message to the server
|
|
571
|
+
*/
|
|
572
|
+
send(message) {
|
|
573
|
+
if (!this.socket || !this.connected) {
|
|
574
|
+
throw new Error("IPC client not connected");
|
|
575
|
+
}
|
|
576
|
+
writeFramedMessage(this.socket, message, this.encoding);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Send a message and wait for response
|
|
580
|
+
*/
|
|
581
|
+
async sendRecv(message) {
|
|
582
|
+
return new Promise((resolve, reject) => {
|
|
583
|
+
const timeout = setTimeout(() => {
|
|
584
|
+
reject(new Error("IPC request timeout"));
|
|
585
|
+
}, 30000);
|
|
586
|
+
const handler = (response) => {
|
|
587
|
+
clearTimeout(timeout);
|
|
588
|
+
this.removeListener("message", handler);
|
|
589
|
+
resolve(response);
|
|
590
|
+
};
|
|
591
|
+
this.on("message", handler);
|
|
592
|
+
this.send(message);
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Close the connection
|
|
597
|
+
*/
|
|
598
|
+
async close() {
|
|
599
|
+
this.frameReader = null;
|
|
600
|
+
if (this.socket) {
|
|
601
|
+
return new Promise((resolve) => {
|
|
602
|
+
this.socket.once("close", resolve);
|
|
603
|
+
this.socket.end();
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Check if connected
|
|
609
|
+
*/
|
|
610
|
+
isConnected() {
|
|
611
|
+
return this.connected;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Get the encoding being used
|
|
615
|
+
*/
|
|
616
|
+
getEncoding() {
|
|
617
|
+
return this.encoding;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Export serialization utilities for testing
|
|
621
|
+
export { serializeMessage, deserializeMessage, FrameReader };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured JSON Logger for ZapJS
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent JSON logging with request context.
|
|
5
|
+
* Used across the TypeScript runtime for observability.
|
|
6
|
+
*/
|
|
7
|
+
export interface LogContext {
|
|
8
|
+
request_id?: string;
|
|
9
|
+
handler_id?: string;
|
|
10
|
+
method?: string;
|
|
11
|
+
path?: string;
|
|
12
|
+
duration_ms?: number;
|
|
13
|
+
status?: number;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
|
|
17
|
+
declare class Logger {
|
|
18
|
+
private jsonFormat;
|
|
19
|
+
private minLevel;
|
|
20
|
+
constructor();
|
|
21
|
+
private shouldLog;
|
|
22
|
+
private formatEntry;
|
|
23
|
+
private log;
|
|
24
|
+
trace(message: string, context?: LogContext): void;
|
|
25
|
+
debug(message: string, context?: LogContext): void;
|
|
26
|
+
info(message: string, context?: LogContext): void;
|
|
27
|
+
warn(message: string, context?: LogContext): void;
|
|
28
|
+
error(message: string, context?: LogContext, error?: Error): void;
|
|
29
|
+
/**
|
|
30
|
+
* Create a child logger with pre-set context
|
|
31
|
+
*/
|
|
32
|
+
child(baseContext: LogContext): ChildLogger;
|
|
33
|
+
/**
|
|
34
|
+
* Set JSON format mode
|
|
35
|
+
*/
|
|
36
|
+
setJsonFormat(enabled: boolean): void;
|
|
37
|
+
/**
|
|
38
|
+
* Set minimum log level
|
|
39
|
+
*/
|
|
40
|
+
setMinLevel(level: LogLevel): void;
|
|
41
|
+
/**
|
|
42
|
+
* Get current configuration
|
|
43
|
+
*/
|
|
44
|
+
getConfig(): {
|
|
45
|
+
jsonFormat: boolean;
|
|
46
|
+
minLevel: LogLevel;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Child logger with inherited base context
|
|
51
|
+
*/
|
|
52
|
+
declare class ChildLogger {
|
|
53
|
+
private parent;
|
|
54
|
+
private baseContext;
|
|
55
|
+
constructor(parent: Logger, baseContext: LogContext);
|
|
56
|
+
private mergeContext;
|
|
57
|
+
trace(message: string, context?: LogContext): void;
|
|
58
|
+
debug(message: string, context?: LogContext): void;
|
|
59
|
+
info(message: string, context?: LogContext): void;
|
|
60
|
+
warn(message: string, context?: LogContext): void;
|
|
61
|
+
error(message: string, context?: LogContext, error?: Error): void;
|
|
62
|
+
/**
|
|
63
|
+
* Create a further nested child logger
|
|
64
|
+
*/
|
|
65
|
+
child(additionalContext: LogContext): ChildLogger;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Global logger instance
|
|
69
|
+
*/
|
|
70
|
+
export declare const logger: Logger;
|
|
71
|
+
export { Logger, ChildLogger };
|