Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .llms/rules/rules.md

This file was deleted.

293 changes: 293 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

30 changes: 27 additions & 3 deletions c_bridges/lws-bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,26 @@ static size_t base64_encode(const unsigned char *in, size_t len, char *out) {

/* ---- uv helpers ---- */

static void ws_format_conn_id(uv_tcp_t *h, char *buf, size_t sz) {
snprintf(buf, sz, "%p", (void *)h);
}

static void on_close(uv_handle_t *handle) {
http_conn_t *conn = (http_conn_t *)handle;
if (conn->type == CONN_WEBSOCKET) {
ws_track_remove(&conn->handle);
if (g_ws_handler) {
char *evt_mem = (char *)GC_malloc(16);
char *evt_mem = (char *)GC_malloc(24);
char **evt = (char **)evt_mem;
char *empty = (char *)GC_malloc_atomic(1);
empty[0] = '\0';
evt[0] = empty;
char *close_str = (char *)GC_malloc_atomic(6);
memcpy(close_str, "close", 6);
evt[1] = close_str;
char *conn_id_str = (char *)GC_malloc_atomic(20);
ws_format_conn_id(&conn->handle, conn_id_str, 20);
evt[2] = conn_id_str;
g_ws_handler(evt_mem);
}
}
Expand Down Expand Up @@ -470,14 +477,17 @@ static void ws_handle_upgrade(http_conn_t *conn) {
send_raw(&conn->handle, response, (size_t)rlen);

if (g_ws_handler) {
char *evt_mem = (char *)GC_malloc(16);
char *evt_mem = (char *)GC_malloc(24);
char **evt = (char **)evt_mem;
char *empty = (char *)GC_malloc_atomic(1);
empty[0] = '\0';
evt[0] = empty;
char *open_str = (char *)GC_malloc_atomic(5);
memcpy(open_str, "open", 5);
evt[1] = open_str;
char *conn_id_str = (char *)GC_malloc_atomic(20);
ws_format_conn_id(&conn->handle, conn_id_str, 20);
evt[2] = conn_id_str;

char *reply = g_ws_handler(evt_mem);
if (reply && reply[0]) {
Expand Down Expand Up @@ -526,12 +536,15 @@ static void ws_process_frame(http_conn_t *conn) {
memcpy(data, payload, payload_len);
data[payload_len] = '\0';

char *evt_mem = (char *)GC_malloc(16);
char *evt_mem = (char *)GC_malloc(24);
char **evt = (char **)evt_mem;
evt[0] = data;
char *msg_str = (char *)GC_malloc_atomic(8);
memcpy(msg_str, "message", 8);
evt[1] = msg_str;
char *conn_id_str = (char *)GC_malloc_atomic(20);
ws_format_conn_id(&conn->handle, conn_id_str, 20);
evt[2] = conn_id_str;

char *reply = g_ws_handler(evt_mem);
if (reply && reply[0]) {
Expand Down Expand Up @@ -787,3 +800,14 @@ void lws_bridge_ws_broadcast(const char *data, int len) {
ws_send_frame(g_ws_conns[i], 0x1, data, (size_t)len);
}
}

void lws_bridge_ws_send_to(const char *conn_id, const char *data, int len) {
unsigned long long val = strtoull(conn_id, NULL, 0);
uv_tcp_t *handle = (uv_tcp_t *)(uintptr_t)val;
for (int i = 0; i < g_ws_conn_count; i++) {
if (g_ws_conns[i] == handle) {
ws_send_frame(handle, 0x1, data, (size_t)len);
return;
}
}
}
1 change: 1 addition & 0 deletions c_bridges/lws-bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ typedef char* (*lws_bridge_ws_handler)(void *event);
int lws_bridge_serve(int port, lws_bridge_http_handler http_handler, lws_bridge_ws_handler ws_handler);
void lws_bridge_ws_send(void *wsi, const char *data, int len);
void lws_bridge_ws_broadcast(const char *data, int len);
void lws_bridge_ws_send_to(const char *conn_id, const char *data, int len);

#endif
18 changes: 18 additions & 0 deletions docs/stdlib/http-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Any HTTP request with an `Upgrade: websocket` header is automatically upgraded w
interface WsEvent {
data: string;
event: string; // "open", "message", "close"
connId: string; // hex pointer identifying this connection
}

function wsHandler(event: WsEvent): string {
Expand All @@ -48,6 +49,20 @@ Send a message to all connected WebSocket clients. Only available when a wsHandl
wsBroadcast("hello everyone");
```

## `wsSend(connId, message)`

Send a message to a specific WebSocket connection identified by its `connId`. The `connId` is available on every `WsEvent`.

```typescript
function wsHandler(event: WsEvent): string {
if (event.event == "message") {
wsSend(event.connId, "echo: " + event.data); // reply to sender only
return "";
}
return "";
}
```

## HttpRequest Object

| Property | Type | Description |
Expand All @@ -74,6 +89,7 @@ If `headers` contains a `Content-Type:` line, it overrides the auto-detected con
|----------|------|-------------|
| `data` | `string` | Message data (empty for open/close events) |
| `event` | `string` | Event type: `"open"`, `"message"`, or `"close"` |
| `connId` | `string` | Hex pointer identifying this connection (use with `wsSend`) |

## Example

Expand Down Expand Up @@ -114,6 +130,7 @@ A chat server with HTTP homepage and WebSocket messaging:
interface WsEvent {
data: string;
event: string;
connId: string;
}

interface HttpRequest {
Expand Down Expand Up @@ -225,6 +242,7 @@ function handleRequest(req: HttpRequest): HttpResponse {
|-----|---------|
| `httpServe()` | libuv TCP + picohttpparser (zero-copy HTTP parsing) |
| `wsBroadcast()` | `lws_bridge_ws_broadcast()` to all tracked connections |
| `wsSend()` | `lws_bridge_ws_send_to()` — parses hex connId, sends to matching handle |
| WebSocket upgrade | embedded SHA-1 + base64 handshake + frame parser |

## Transparent Compression
Expand Down
1 change: 1 addition & 0 deletions examples/websocket/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const port = parseInt(parser.getOption("port"));
interface WsEvent {
data: string;
event: string;
connId: string;
}

interface HttpRequest {
Expand Down
5 changes: 5 additions & 0 deletions src/codegen/expressions/calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ export class CallExpressionGenerator {
return this.ctx.generateWsBroadcast(expr, params);
}

// Handle wsSend(connId, msg) - send message to a specific WebSocket connection
if (expr.name === "wsSend") {
return this.ctx.generateWsSend(expr, params);
}

// Handle setTimeout() - libuv timer (one-shot)
if (expr.name === "setTimeout") {
return this.generateSetTimeout(expr, params);
Expand Down
2 changes: 1 addition & 1 deletion src/codegen/expressions/method-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export interface MethodCallGeneratorContext {
setUsesConsoleTime(value: boolean): void;
setUsesCrypto(value: boolean): void;
setUsesJson(value: boolean): void;
setUsesMongoose(value: boolean): void;
setUsesHttpServer(value: boolean): void;
setUsesMultipart(value: boolean): void;
setUsesTestRunner(value: boolean): void;
classGenGetFieldInfo(
Expand Down
17 changes: 13 additions & 4 deletions src/codegen/infrastructure/generator-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ export interface IGeneratorContext {
setUsesArraySort(value: boolean): void;
setUsesCrypto(value: boolean): void;
setUsesJson(value: boolean): void;
setUsesMongoose(value: boolean): void;
setUsesHttpServer(value: boolean): void;
setUsesMultipart(value: boolean): void;
setUsesRegex(value: boolean): void;
setUsesTestRunner(value: boolean): void;
Expand Down Expand Up @@ -802,6 +802,11 @@ export interface IGeneratorContext {
*/
generateWsBroadcast(expr: CallNode, params: string[]): string;

/**
* Generate WebSocket targeted send to a specific connection
*/
generateWsSend(expr: CallNode, params: string[]): string;

/**
* Look up an interface definition by name from the AST
*/
Expand Down Expand Up @@ -980,7 +985,7 @@ export class MockGeneratorContext implements IGeneratorContext {
public usesUvHrtime: number = 0;
public usesCrypto: number = 0;
public usesJson: number = 0;
public usesMongoose: number = 0;
public usesHttpServer: number = 0;
public usesRegex: number = 0;
public usesTestRunner: number = 0;
public usesAsyncFs: number = 0;
Expand Down Expand Up @@ -1191,8 +1196,8 @@ export class MockGeneratorContext implements IGeneratorContext {
setUsesJson(value: boolean): void {
this.usesJson = value ? 1 : 0;
}
setUsesMongoose(value: boolean): void {
this.usesMongoose = value ? 1 : 0;
setUsesHttpServer(value: boolean): void {
this.usesHttpServer = value ? 1 : 0;
}
setUsesMultipart(value: boolean): void {
// no-op in mock
Expand Down Expand Up @@ -1695,6 +1700,10 @@ export class MockGeneratorContext implements IGeneratorContext {
return "0.0";
}

generateWsSend(_expr: CallNode, _params: string[]): string {
return "0.0";
}

getInterfaceFromAST(
name: string,
): { name: string; fields: { name: string; type: string }[] } | null {
Expand Down
31 changes: 22 additions & 9 deletions src/codegen/llvm-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
public usesArraySort: number = 0;
public usesCrypto: number = 0;
public usesJson: number = 0;
public usesMongoose: number = 0;
public usesHttpServer: number = 0;
public usesMultipart: number = 0;
public usesRegex: number = 0;
public usesTestRunner: number = 0;
Expand Down Expand Up @@ -1035,11 +1035,11 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
public getUsesJson(): boolean {
return this.usesJson !== 0;
}
public setUsesMongoose(value: boolean): void {
this.usesMongoose = value ? 1 : 0;
public setUsesHttpServer(value: boolean): void {
this.usesHttpServer = value ? 1 : 0;
}
public getUsesMongoose(): boolean {
return this.usesMongoose !== 0;
public getUsesHttpServer(): boolean {
return this.usesHttpServer !== 0;
}
public setUsesMultipart(value: boolean): void {
this.usesMultipart = value ? 1 : 0;
Expand Down Expand Up @@ -1449,7 +1449,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
this.usesArraySort = 0;
this.usesCrypto = 0;
this.usesJson = 0;
this.usesMongoose = 0;
this.usesHttpServer = 0;
this.usesMultipart = 0;
this.usesRegex = 0;
this.usesTestRunner = 0;
Expand Down Expand Up @@ -2478,6 +2478,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
if (wsHandler) {
irParts.push("\n");
irParts.push(this.httpServerGen.generateWsBroadcastFunction());
irParts.push(this.httpServerGen.generateWsSendToFunction());
}
}

Expand Down Expand Up @@ -2553,7 +2554,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
this.usesPromises ||
this.usesCurl ||
this.usesUvHrtime ||
this.usesMongoose ||
this.usesHttpServer ||
this.usesAsyncFs;
const needsPromise = this.usesPromises || this.usesCurl || this.usesAsyncFs;

Expand Down Expand Up @@ -2628,7 +2629,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
finalParts.push("\n");
}

if (this.usesMongoose) {
if (this.usesHttpServer) {
const httpServerDecls = this.httpServerGen.generateDeclarations();
if (httpServerDecls) {
finalParts.push(this.filterDuplicateDeclarations(httpServerDecls));
Expand Down Expand Up @@ -3573,7 +3574,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {

// Track handler for http server event handler generation
this.httpHandlers.push(handlerName);
this.usesMongoose = 1;
this.usesHttpServer = 1;

if (expr.args.length >= 3) {
const wsHandlerArg = expr.args[2];
Expand Down Expand Up @@ -3613,6 +3614,18 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
return "0.0";
}

public generateWsSend(expr: CallNode, params: string[]): string {
if (expr.args.length < 2) {
return this.emitError("wsSend() requires 2 arguments: connId, message", expr.loc);
}
const connIdValue = this.generateExpression(expr.args[0], params);
const msgValue = this.generateExpression(expr.args[1], params);
const len = this.nextTemp();
this.emit(`${len} = call i64 @strlen(i8* ${msgValue})`);
this.emit(`call void @__ws_send_to(i8* ${connIdValue}, i8* ${msgValue}, i64 ${len})`);
return "0.0";
}

public getInterfaceFromAST(
name: string,
): { name: string; fields: { name: string; type: string }[] } | null {
Expand Down
12 changes: 12 additions & 0 deletions src/codegen/stdlib/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class HttpServerGenerator {
"declare i32 @lws_bridge_serve(i32, void (%struct.lws_bridge_request*, %struct.lws_bridge_response*)*, i8* (i8*)*)\n";
ir += "declare void @lws_bridge_ws_send(i8*, i8*, i32)\n";
ir += "declare void @lws_bridge_ws_broadcast(i8*, i32)\n";
ir += "declare void @lws_bridge_ws_send_to(i8*, i8*, i32)\n";
ir += "\n";

return ir;
Expand Down Expand Up @@ -127,6 +128,17 @@ export class HttpServerGenerator {
return ir;
}

generateWsSendToFunction(): string {
let ir = "; WebSocket targeted send to specific connection (via lws-bridge)\n";
ir += "define void @__ws_send_to(i8* %conn_id, i8* %msg, i64 %len) {\n";
ir += "entry:\n";
ir += " %len32 = trunc i64 %len to i32\n";
ir += " call void @lws_bridge_ws_send_to(i8* %conn_id, i8* %msg, i32 %len32)\n";
ir += " ret void\n";
ir += "}\n\n";
return ir;
}

generateWsBroadcastFunction(): string {
let ir = "; WebSocket broadcast to all connected clients (via lws-bridge)\n";
ir += "define void @__ws_broadcast(i8* %msg, i64 %len) {\n";
Expand Down
Loading
Loading