Skip to content

Commit 808564b

Browse files
authored
add wsSend to connection (#70)
1 parent 921796a commit 808564b

16 files changed

Lines changed: 403 additions & 834 deletions

File tree

.llms/rules/rules.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

CLAUDE.md

Lines changed: 293 additions & 0 deletions
Large diffs are not rendered by default.

c_bridges/lws-bridge.c

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,26 @@ static size_t base64_encode(const unsigned char *in, size_t len, char *out) {
136136

137137
/* ---- uv helpers ---- */
138138

139+
static void ws_format_conn_id(uv_tcp_t *h, char *buf, size_t sz) {
140+
snprintf(buf, sz, "%p", (void *)h);
141+
}
142+
139143
static void on_close(uv_handle_t *handle) {
140144
http_conn_t *conn = (http_conn_t *)handle;
141145
if (conn->type == CONN_WEBSOCKET) {
142146
ws_track_remove(&conn->handle);
143147
if (g_ws_handler) {
144-
char *evt_mem = (char *)GC_malloc(16);
148+
char *evt_mem = (char *)GC_malloc(24);
145149
char **evt = (char **)evt_mem;
146150
char *empty = (char *)GC_malloc_atomic(1);
147151
empty[0] = '\0';
148152
evt[0] = empty;
149153
char *close_str = (char *)GC_malloc_atomic(6);
150154
memcpy(close_str, "close", 6);
151155
evt[1] = close_str;
156+
char *conn_id_str = (char *)GC_malloc_atomic(20);
157+
ws_format_conn_id(&conn->handle, conn_id_str, 20);
158+
evt[2] = conn_id_str;
152159
g_ws_handler(evt_mem);
153160
}
154161
}
@@ -470,14 +477,17 @@ static void ws_handle_upgrade(http_conn_t *conn) {
470477
send_raw(&conn->handle, response, (size_t)rlen);
471478

472479
if (g_ws_handler) {
473-
char *evt_mem = (char *)GC_malloc(16);
480+
char *evt_mem = (char *)GC_malloc(24);
474481
char **evt = (char **)evt_mem;
475482
char *empty = (char *)GC_malloc_atomic(1);
476483
empty[0] = '\0';
477484
evt[0] = empty;
478485
char *open_str = (char *)GC_malloc_atomic(5);
479486
memcpy(open_str, "open", 5);
480487
evt[1] = open_str;
488+
char *conn_id_str = (char *)GC_malloc_atomic(20);
489+
ws_format_conn_id(&conn->handle, conn_id_str, 20);
490+
evt[2] = conn_id_str;
481491

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

529-
char *evt_mem = (char *)GC_malloc(16);
539+
char *evt_mem = (char *)GC_malloc(24);
530540
char **evt = (char **)evt_mem;
531541
evt[0] = data;
532542
char *msg_str = (char *)GC_malloc_atomic(8);
533543
memcpy(msg_str, "message", 8);
534544
evt[1] = msg_str;
545+
char *conn_id_str = (char *)GC_malloc_atomic(20);
546+
ws_format_conn_id(&conn->handle, conn_id_str, 20);
547+
evt[2] = conn_id_str;
535548

536549
char *reply = g_ws_handler(evt_mem);
537550
if (reply && reply[0]) {
@@ -787,3 +800,14 @@ void lws_bridge_ws_broadcast(const char *data, int len) {
787800
ws_send_frame(g_ws_conns[i], 0x1, data, (size_t)len);
788801
}
789802
}
803+
804+
void lws_bridge_ws_send_to(const char *conn_id, const char *data, int len) {
805+
unsigned long long val = strtoull(conn_id, NULL, 0);
806+
uv_tcp_t *handle = (uv_tcp_t *)(uintptr_t)val;
807+
for (int i = 0; i < g_ws_conn_count; i++) {
808+
if (g_ws_conns[i] == handle) {
809+
ws_send_frame(handle, 0x1, data, (size_t)len);
810+
return;
811+
}
812+
}
813+
}

c_bridges/lws-bridge.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ typedef char* (*lws_bridge_ws_handler)(void *event);
2525
int lws_bridge_serve(int port, lws_bridge_http_handler http_handler, lws_bridge_ws_handler ws_handler);
2626
void lws_bridge_ws_send(void *wsi, const char *data, int len);
2727
void lws_bridge_ws_broadcast(const char *data, int len);
28+
void lws_bridge_ws_send_to(const char *conn_id, const char *data, int len);
2829

2930
#endif

docs/stdlib/http-server.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Any HTTP request with an `Upgrade: websocket` header is automatically upgraded w
2727
interface WsEvent {
2828
data: string;
2929
event: string; // "open", "message", "close"
30+
connId: string; // hex pointer identifying this connection
3031
}
3132

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

52+
## `wsSend(connId, message)`
53+
54+
Send a message to a specific WebSocket connection identified by its `connId`. The `connId` is available on every `WsEvent`.
55+
56+
```typescript
57+
function wsHandler(event: WsEvent): string {
58+
if (event.event == "message") {
59+
wsSend(event.connId, "echo: " + event.data); // reply to sender only
60+
return "";
61+
}
62+
return "";
63+
}
64+
```
65+
5166
## HttpRequest Object
5267

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

7894
## Example
7995

@@ -114,6 +130,7 @@ A chat server with HTTP homepage and WebSocket messaging:
114130
interface WsEvent {
115131
data: string;
116132
event: string;
133+
connId: string;
117134
}
118135

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

230248
## Transparent Compression

examples/websocket/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const port = parseInt(parser.getOption("port"));
1010
interface WsEvent {
1111
data: string;
1212
event: string;
13+
connId: string;
1314
}
1415

1516
interface HttpRequest {

src/codegen/expressions/calls.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ export class CallExpressionGenerator {
9292
return this.ctx.generateWsBroadcast(expr, params);
9393
}
9494

95+
// Handle wsSend(connId, msg) - send message to a specific WebSocket connection
96+
if (expr.name === "wsSend") {
97+
return this.ctx.generateWsSend(expr, params);
98+
}
99+
95100
// Handle setTimeout() - libuv timer (one-shot)
96101
if (expr.name === "setTimeout") {
97102
return this.generateSetTimeout(expr, params);

src/codegen/expressions/method-calls.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export interface MethodCallGeneratorContext {
195195
setUsesConsoleTime(value: boolean): void;
196196
setUsesCrypto(value: boolean): void;
197197
setUsesJson(value: boolean): void;
198-
setUsesMongoose(value: boolean): void;
198+
setUsesHttpServer(value: boolean): void;
199199
setUsesMultipart(value: boolean): void;
200200
setUsesTestRunner(value: boolean): void;
201201
classGenGetFieldInfo(

src/codegen/infrastructure/generator-context.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,7 @@ export interface IGeneratorContext {
767767
setUsesArraySort(value: boolean): void;
768768
setUsesCrypto(value: boolean): void;
769769
setUsesJson(value: boolean): void;
770-
setUsesMongoose(value: boolean): void;
770+
setUsesHttpServer(value: boolean): void;
771771
setUsesMultipart(value: boolean): void;
772772
setUsesRegex(value: boolean): void;
773773
setUsesTestRunner(value: boolean): void;
@@ -802,6 +802,11 @@ export interface IGeneratorContext {
802802
*/
803803
generateWsBroadcast(expr: CallNode, params: string[]): string;
804804

805+
/**
806+
* Generate WebSocket targeted send to a specific connection
807+
*/
808+
generateWsSend(expr: CallNode, params: string[]): string;
809+
805810
/**
806811
* Look up an interface definition by name from the AST
807812
*/
@@ -980,7 +985,7 @@ export class MockGeneratorContext implements IGeneratorContext {
980985
public usesUvHrtime: number = 0;
981986
public usesCrypto: number = 0;
982987
public usesJson: number = 0;
983-
public usesMongoose: number = 0;
988+
public usesHttpServer: number = 0;
984989
public usesRegex: number = 0;
985990
public usesTestRunner: number = 0;
986991
public usesAsyncFs: number = 0;
@@ -1191,8 +1196,8 @@ export class MockGeneratorContext implements IGeneratorContext {
11911196
setUsesJson(value: boolean): void {
11921197
this.usesJson = value ? 1 : 0;
11931198
}
1194-
setUsesMongoose(value: boolean): void {
1195-
this.usesMongoose = value ? 1 : 0;
1199+
setUsesHttpServer(value: boolean): void {
1200+
this.usesHttpServer = value ? 1 : 0;
11961201
}
11971202
setUsesMultipart(value: boolean): void {
11981203
// no-op in mock
@@ -1695,6 +1700,10 @@ export class MockGeneratorContext implements IGeneratorContext {
16951700
return "0.0";
16961701
}
16971702

1703+
generateWsSend(_expr: CallNode, _params: string[]): string {
1704+
return "0.0";
1705+
}
1706+
16981707
getInterfaceFromAST(
16991708
name: string,
17001709
): { name: string; fields: { name: string; type: string }[] } | null {

src/codegen/llvm-generator.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
210210
public usesArraySort: number = 0;
211211
public usesCrypto: number = 0;
212212
public usesJson: number = 0;
213-
public usesMongoose: number = 0;
213+
public usesHttpServer: number = 0;
214214
public usesMultipart: number = 0;
215215
public usesRegex: number = 0;
216216
public usesTestRunner: number = 0;
@@ -1035,11 +1035,11 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
10351035
public getUsesJson(): boolean {
10361036
return this.usesJson !== 0;
10371037
}
1038-
public setUsesMongoose(value: boolean): void {
1039-
this.usesMongoose = value ? 1 : 0;
1038+
public setUsesHttpServer(value: boolean): void {
1039+
this.usesHttpServer = value ? 1 : 0;
10401040
}
1041-
public getUsesMongoose(): boolean {
1042-
return this.usesMongoose !== 0;
1041+
public getUsesHttpServer(): boolean {
1042+
return this.usesHttpServer !== 0;
10431043
}
10441044
public setUsesMultipart(value: boolean): void {
10451045
this.usesMultipart = value ? 1 : 0;
@@ -1449,7 +1449,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
14491449
this.usesArraySort = 0;
14501450
this.usesCrypto = 0;
14511451
this.usesJson = 0;
1452-
this.usesMongoose = 0;
1452+
this.usesHttpServer = 0;
14531453
this.usesMultipart = 0;
14541454
this.usesRegex = 0;
14551455
this.usesTestRunner = 0;
@@ -2478,6 +2478,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
24782478
if (wsHandler) {
24792479
irParts.push("\n");
24802480
irParts.push(this.httpServerGen.generateWsBroadcastFunction());
2481+
irParts.push(this.httpServerGen.generateWsSendToFunction());
24812482
}
24822483
}
24832484

@@ -2553,7 +2554,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
25532554
this.usesPromises ||
25542555
this.usesCurl ||
25552556
this.usesUvHrtime ||
2556-
this.usesMongoose ||
2557+
this.usesHttpServer ||
25572558
this.usesAsyncFs;
25582559
const needsPromise = this.usesPromises || this.usesCurl || this.usesAsyncFs;
25592560

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

2631-
if (this.usesMongoose) {
2632+
if (this.usesHttpServer) {
26322633
const httpServerDecls = this.httpServerGen.generateDeclarations();
26332634
if (httpServerDecls) {
26342635
finalParts.push(this.filterDuplicateDeclarations(httpServerDecls));
@@ -3573,7 +3574,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext {
35733574

35743575
// Track handler for http server event handler generation
35753576
this.httpHandlers.push(handlerName);
3576-
this.usesMongoose = 1;
3577+
this.usesHttpServer = 1;
35773578

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

3617+
public generateWsSend(expr: CallNode, params: string[]): string {
3618+
if (expr.args.length < 2) {
3619+
return this.emitError("wsSend() requires 2 arguments: connId, message", expr.loc);
3620+
}
3621+
const connIdValue = this.generateExpression(expr.args[0], params);
3622+
const msgValue = this.generateExpression(expr.args[1], params);
3623+
const len = this.nextTemp();
3624+
this.emit(`${len} = call i64 @strlen(i8* ${msgValue})`);
3625+
this.emit(`call void @__ws_send_to(i8* ${connIdValue}, i8* ${msgValue}, i64 ${len})`);
3626+
return "0.0";
3627+
}
3628+
36163629
public getInterfaceFromAST(
36173630
name: string,
36183631
): { name: string; fields: { name: string; type: string }[] } | null {

0 commit comments

Comments
 (0)