@scriptc/runtime 0.0.12 → 0.0.13

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/src/scr_http.c CHANGED
@@ -49,6 +49,7 @@
49
49
  #include "scr_runtime.h"
50
50
 
51
51
  #include <ctype.h>
52
+ #include <math.h> /* INFINITY — the Agent's maxSockets default */
52
53
  #include <stdio.h>
53
54
  #include <stdlib.h>
54
55
  #include <string.h>
@@ -148,6 +149,16 @@ struct ScrHttpReq {
148
149
  bool close_queued;
149
150
  bool close_emitted; /* settled: err/close listeners dropped */
150
151
  bool join_dup; /* joinDuplicateHeaders: repeated names read joined ", " */
152
+ /* pause(): delivery holds — arrived body bytes buffer in pend (the
153
+ * parser keeps consuming; memory is the buffer, a documented bound)
154
+ * and 'end' defers (end_pending) until resume() drains through the
155
+ * emit queue, never the resuming stack. */
156
+ bool paused;
157
+ bool end_pending;
158
+ bool drain_queued;
159
+ char *pend;
160
+ size_t pend_len, pend_cap;
161
+ bool destroyed; /* req.destroy()/the teardown ran — req.destroyed */
151
162
  ScrNetLs aborted_ls; /* req.on('aborted') — the h2 compat event */
152
163
  };
153
164
 
@@ -211,6 +222,7 @@ void scr_http_req_release(ScrHttpReq *r) {
211
222
  scr_http_res_release(r->pipe_res);
212
223
  scr_http_client_release(r->pipe_client);
213
224
  scr_net_sock_release(r->pipe_sock);
225
+ free(r->pend);
214
226
  if (r->sock) scr_net_sock_release(r->sock);
215
227
  if (r->h2_stream) scr_http_h2_ops->release(r->h2_stream);
216
228
  #ifdef SCR_RC_AUDIT
@@ -414,14 +426,53 @@ ScrNetSocket *scr_http_req_socket(ScrHttpReq *r) {
414
426
  return r->sock ? scr_net_sock_retain(r->sock) : NULL;
415
427
  }
416
428
 
417
- /* resume(): a flow-control no-op this parser always consumes body
418
- * bytes and fires 'end' regardless of consumers (SEMANTICS.md; Node
419
- * requires the stream to flow). */
420
- void scr_http_req_resume(ScrHttpReq *r) { (void)r; }
429
+ /* the deferred-emit queue lives below (the proto sweep) — resume()'s
430
+ * drain rides it; the parser's deliver and the cork flush live below
431
+ * their callers too */
432
+ static void scr_http_emit_push(int kind, void *h /*moves +1*/);
433
+ static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n);
434
+ static void scr_http_res_cork_flush(ScrHttpRes *r);
435
+ #define SCR_HTTP_EMIT_REQ_DRAIN_K 6
436
+
437
+ /* resume(): a flow-control no-op unless pause() held delivery — then the
438
+ * buffered bytes (and a deferred 'end') drain through the emit queue,
439
+ * never the resuming stack. This parser always consumes body bytes
440
+ * (SEMANTICS.md; Node requires the stream to flow). */
441
+ void scr_http_req_resume(ScrHttpReq *r) {
442
+ if (!r->paused) return;
443
+ r->paused = false;
444
+ if ((r->pend_len > 0 || r->end_pending) && !r->drain_queued) {
445
+ r->drain_queued = true;
446
+ scr_http_emit_push(SCR_HTTP_EMIT_REQ_DRAIN_K, scr_http_req_retain(r));
447
+ }
448
+ }
449
+
450
+ /* pause(): delivery holds until resume() (scr_http_req_deliver buffers;
451
+ * a completed body's 'end' waits too). */
452
+ void scr_http_req_pause(ScrHttpReq *r) {
453
+ if (!r->ended) r->paused = true;
454
+ }
455
+
456
+ /* req.setTimeout(ms[, cb]): the underlying socket's idle timer — the cb
457
+ * registers once('timeout') there, Node's delegation. Finished/destroyed
458
+ * messages skip (Node no-ops once the stream is done). */
459
+ void scr_http_req_set_timeout(ScrHttpReq *r, double ms, ScrClosure *cb /*moves, nullable*/) {
460
+ if (r->ended || r->destroyed || r->close_emitted || !r->sock) {
461
+ /* the message is done — Node no-ops there (never arms, never fires) */
462
+ if (cb) scr_closure_release(cb);
463
+ return;
464
+ }
465
+ scr_net_sock_set_timeout(r->sock, ms);
466
+ if (cb) scr_net_sock_on_timeout(r->sock, cb, true);
467
+ }
468
+
469
+ bool scr_http_req_destroyed_flag(ScrHttpReq *r) { return r->destroyed; }
470
+ bool scr_http_req_readable(ScrHttpReq *r) { return !r->ended && !r->destroyed; }
421
471
 
422
472
  /* destroy(): tears the underlying connection down NOW; the teardown
423
473
  * events flow through the socket's native hooks like a peer close. */
424
474
  void scr_http_req_destroy(ScrHttpReq *r) {
475
+ r->destroyed = true;
425
476
  if (r->h2_stream != NULL) {
426
477
  scr_http_h2_ops->destroy(r->h2_stream);
427
478
  return;
@@ -460,6 +511,11 @@ void scr_http_req_on_aborted(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once)
460
511
  * req cannot cycle past the body). */
461
512
  static void scr_http_req_finish(ScrHttpReq *r, bool fire) {
462
513
  if (r->ended) return;
514
+ if (fire && r->paused) {
515
+ /* pause() holds 'end' too — resume()'s drain finishes the body */
516
+ r->end_pending = true;
517
+ return;
518
+ }
463
519
  r->ended = true;
464
520
  if (fire) scr_net_fire0_this(&r->end_ls, r, SCR_DYNH_HTTP_REQ);
465
521
  scr_net_ls_drop(&r->data_ls);
@@ -503,9 +559,24 @@ struct ScrHttpRes {
503
559
  bool finished;
504
560
  bool no_date; /* res.sendDate = false: suppress the implicit Date header */
505
561
  bool keep_alive; /* the REQUEST's verdict; Connection: close overrides */
562
+ bool destroyed; /* res.destroy()/teardown — res.destroyed (true in 'close') */
563
+ /* cork()/uncork(): corked counts the nesting (res.writableCorked);
564
+ * writes while corked coalesce in cork_buf and flush as ONE write when
565
+ * the count reaches zero (or at end()) — Node's coalescing, and the
566
+ * h2 lane's single DATA frame. */
567
+ int corked;
568
+ char *cork_buf;
569
+ size_t cork_len, cork_cap;
570
+ /* res.req: the paired request (+1; req never points back — no cycle).
571
+ * A `res.req = null` write clears the READ (req_cleared) — Node's
572
+ * plain-property write. */
573
+ ScrHttpReq *req_ref;
574
+ bool req_cleared;
506
575
  ScrNetLs close_ls;
507
576
  ScrNetLs finish_ls; /* res.end(cb) — fires deferred once the body went out */
577
+ ScrNetLs wcb_ls; /* res.write(chunk, cb) — fires from the queue */
508
578
  bool finish_queued;
579
+ bool wcb_queued;
509
580
  bool close_queued;
510
581
  bool close_emitted;
511
582
  };
@@ -527,6 +598,9 @@ void scr_http_res_release(ScrHttpRes *r) {
527
598
  scr_str_release(r->status_msg);
528
599
  scr_net_ls_drop(&r->close_ls);
529
600
  scr_net_ls_drop(&r->finish_ls);
601
+ scr_net_ls_drop(&r->wcb_ls);
602
+ free(r->cork_buf);
603
+ if (r->req_ref) scr_http_req_release(r->req_ref);
530
604
  if (r->sock) scr_net_sock_release(r->sock);
531
605
  if (r->h2_stream) scr_http_h2_ops->release(r->h2_stream);
532
606
  #ifdef SCR_RC_AUDIT
@@ -761,8 +835,28 @@ static void scr_http_res_send_head(ScrHttpRes *r, long long body_len) {
761
835
  free(b.data);
762
836
  }
763
837
 
838
+ /* Writes while corked coalesce here and flush as ONE write when the
839
+ * cork count reaches zero (or at end()) — Node's coalescing, and one
840
+ * DATA frame on the h2 lane. */
841
+ static void scr_http_res_cork_buffer(ScrHttpRes *r, const char *data, size_t len) {
842
+ if (len == 0) return;
843
+ if (r->cork_len + len > r->cork_cap) {
844
+ size_t cap = r->cork_cap ? r->cork_cap : 1024;
845
+ while (cap < r->cork_len + len) cap *= 2;
846
+ r->cork_buf = realloc(r->cork_buf, cap);
847
+ if (!r->cork_buf) scr_http_oom();
848
+ r->cork_cap = cap;
849
+ }
850
+ memcpy(r->cork_buf + r->cork_len, data, len);
851
+ r->cork_len += len;
852
+ }
853
+
764
854
  static void scr_http_res_write_raw(ScrHttpRes *r, const char *data, size_t len) {
765
855
  if (r->finished) return;
856
+ if (r->corked > 0) {
857
+ scr_http_res_cork_buffer(r, data, len);
858
+ return;
859
+ }
766
860
  if (!r->head_sent) scr_http_res_send_head(r, -1); /* streaming: chunked */
767
861
  if (r->h2_stream != NULL) {
768
862
  scr_http_h2_ops->write(r->h2_stream, data, len);
@@ -790,11 +884,65 @@ void scr_http_res_write_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) {
790
884
  scr_http_res_write_raw(r, (const char *)data->data, data->len);
791
885
  }
792
886
 
887
+ /* res.flushHeaders(): the head goes out NOW (streaming framing — chunked
888
+ * unless the user fixed the length; the h2 lane's HEADERS frame). */
889
+ void scr_http_res_flush_headers(ScrHttpRes *r) {
890
+ if (!r->head_sent && !r->finished) scr_http_res_send_head(r, -1);
891
+ }
892
+
893
+ /* cork()/uncork(): the count IS res.writableCorked; the last uncork
894
+ * flushes the coalesced bytes as one write. */
895
+ void scr_http_res_cork(ScrHttpRes *r) { r->corked++; }
896
+
897
+ void scr_http_res_uncork(ScrHttpRes *r) {
898
+ if (r->corked == 0) return;
899
+ if (--r->corked == 0) scr_http_res_cork_flush(r);
900
+ }
901
+
902
+ double scr_http_res_writable_corked(ScrHttpRes *r) { return (double)r->corked; }
903
+
904
+ bool scr_http_res_destroyed_flag(ScrHttpRes *r) { return r->destroyed || r->close_emitted; }
905
+
906
+ /* res.req — the paired request; a res.req = null write cleared it. */
907
+ void scr_http_res_set_req(ScrHttpRes *r, ScrHttpReq *req /*borrowed*/) {
908
+ if (r->req_ref) scr_http_req_release(r->req_ref);
909
+ r->req_ref = req ? scr_http_req_retain(req) : NULL;
910
+ }
911
+
912
+ /* res.setTimeout(ms[, cb]): the socket's idle timer, Node's delegation. */
913
+ void scr_http_res_set_timeout(ScrHttpRes *r, double ms, ScrClosure *cb /*moves, nullable*/) {
914
+ if (r->finished || r->close_emitted || !r->sock) {
915
+ if (cb) scr_closure_release(cb);
916
+ return;
917
+ }
918
+ scr_net_sock_set_timeout(r->sock, ms);
919
+ if (cb) scr_net_sock_on_timeout(r->sock, cb, true);
920
+ }
921
+
793
922
  static void scr_http_conn_response_finished(struct ScrHttpConn *conn, bool keep_alive);
794
923
  static void scr_http_queue_res_finish(ScrHttpRes *res);
795
924
 
925
+ /* Flush corked bytes as one write (corked already zero, or forced by
926
+ * end()). */
927
+ static void scr_http_res_cork_flush(ScrHttpRes *r) {
928
+ if (r->cork_len == 0) return;
929
+ char *held = r->cork_buf;
930
+ size_t held_len = r->cork_len;
931
+ r->cork_buf = NULL;
932
+ r->cork_len = r->cork_cap = 0;
933
+ scr_http_res_write_raw(r, held, held_len);
934
+ free(held);
935
+ }
936
+
796
937
  static void scr_http_res_end_raw(ScrHttpRes *r, const char *data, size_t len) {
797
938
  if (r->finished) return;
939
+ if (r->corked > 0 || r->cork_len > 0) {
940
+ /* end() flushes every cork level (Node) — the body streamed, so the
941
+ * framing below takes the already-committed streaming path */
942
+ r->corked = 0;
943
+ scr_http_res_cork_flush(r);
944
+ if (r->finished) return; /* a teardown inside the flush */
945
+ }
798
946
  if (r->h2_stream != NULL) {
799
947
  if (!r->head_sent) scr_http_res_send_head(r, (long long)len);
800
948
  scr_http_h2_ops->end(r->h2_stream, data, len);
@@ -923,6 +1071,7 @@ void scr_http_res_write_head_pairs(ScrHttpRes *r, double status, ScrArr *pairs /
923
1071
  * point — portless aborts a proxied response with it); 'close' follows
924
1072
  * through the teardown path. Node-matched: no 'error' from a destroy. */
925
1073
  void scr_http_res_destroy(ScrHttpRes *r) {
1074
+ r->destroyed = true;
926
1075
  if (r->h2_stream != NULL) {
927
1076
  /* h2 compat: destroy the STREAM (RST), never the shared session */
928
1077
  scr_http_h2_ops->destroy(r->h2_stream);
@@ -956,7 +1105,9 @@ enum {
956
1105
  SCR_HTTP_EMIT_REQ_CLOSE = 2, /* ScrHttpReq */
957
1106
  SCR_HTTP_EMIT_REQ_ABORTED = 3, /* ScrHttpReq: 'error' ("aborted") */
958
1107
  SCR_HTTP_EMIT_CLIENT_CLOSE = 4, /* ScrHttpClientReq */
959
- SCR_HTTP_EMIT_RES_FINISH = 5 /* ScrHttpRes: the end(cb) callbacks */
1108
+ SCR_HTTP_EMIT_RES_FINISH = 5, /* ScrHttpRes: the end(cb) callbacks */
1109
+ SCR_HTTP_EMIT_REQ_DRAIN = SCR_HTTP_EMIT_REQ_DRAIN_K, /* ScrHttpReq: resume()'s drain */
1110
+ SCR_HTTP_EMIT_RES_WCB = 7 /* ScrHttpRes: write(chunk, cb) callbacks */
960
1111
  };
961
1112
 
962
1113
  typedef struct {
@@ -1007,8 +1158,10 @@ static void scr_http_emit_push(int kind, void *h /*moves +1*/) {
1007
1158
  static void scr_http_emit_release(ScrHttpEmit *e) {
1008
1159
  switch (e->kind) {
1009
1160
  case SCR_HTTP_EMIT_RES_CLOSE:
1161
+ case SCR_HTTP_EMIT_RES_WCB:
1010
1162
  case SCR_HTTP_EMIT_RES_FINISH: scr_http_res_release((ScrHttpRes *)e->h); break;
1011
1163
  case SCR_HTTP_EMIT_REQ_CLOSE:
1164
+ case SCR_HTTP_EMIT_REQ_DRAIN:
1012
1165
  case SCR_HTTP_EMIT_REQ_ABORTED: scr_http_req_release((ScrHttpReq *)e->h); break;
1013
1166
  case SCR_HTTP_EMIT_CLIENT_CLOSE:
1014
1167
  scr_http_client_release_internal((struct ScrHttpClientReq *)e->h);
@@ -1051,6 +1204,17 @@ void scr_http_res_on_finish(ScrHttpRes *r, ScrClosure *cb /*moves*/) {
1051
1204
  if (r->finished) scr_http_queue_res_finish(r);
1052
1205
  }
1053
1206
 
1207
+ /* res.write(chunk, cb): the callback fires from the queue (this surface
1208
+ * flushes synchronously into the socket buffer — the deferral keeps the
1209
+ * cb off the writing stack, Node's contract). */
1210
+ void scr_http_res_on_write_flush(ScrHttpRes *r, ScrClosure *cb /*moves*/) {
1211
+ scr_net_ls_add(&r->wcb_ls, cb, NULL, true);
1212
+ if (!r->wcb_queued) {
1213
+ r->wcb_queued = true;
1214
+ scr_http_emit_push(SCR_HTTP_EMIT_RES_WCB, scr_http_res_retain(r));
1215
+ }
1216
+ }
1217
+
1054
1218
  static bool scr_http_proto_pending(void) { return scr_http_emits_head < scr_http_emits_len; }
1055
1219
 
1056
1220
  static void scr_http_proto_sweep(void) {
@@ -1061,15 +1225,23 @@ static void scr_http_proto_sweep(void) {
1061
1225
  ScrHttpRes *res = (ScrHttpRes *)e.h;
1062
1226
  if (!res->close_emitted) {
1063
1227
  res->close_emitted = true;
1228
+ res->destroyed = true; /* Node: destroyed reads true inside 'close' */
1064
1229
  scr_net_fire0_this(&res->close_ls, res, SCR_DYNH_HTTP_RES);
1065
1230
  scr_net_ls_drop(&res->close_ls);
1066
1231
  }
1067
1232
  break;
1068
1233
  }
1234
+ case SCR_HTTP_EMIT_RES_WCB: {
1235
+ ScrHttpRes *res = (ScrHttpRes *)e.h;
1236
+ res->wcb_queued = false; /* later write(chunk, cb)s re-queue */
1237
+ scr_net_fire0_this(&res->wcb_ls, res, SCR_DYNH_HTTP_RES);
1238
+ break;
1239
+ }
1069
1240
  case SCR_HTTP_EMIT_REQ_CLOSE: {
1070
1241
  ScrHttpReq *req = (ScrHttpReq *)e.h;
1071
1242
  if (!req->close_emitted) {
1072
1243
  req->close_emitted = true;
1244
+ req->destroyed = true; /* Node: destroyed reads true inside 'close' */
1073
1245
  scr_net_fire0_this(&req->close_ls, req, SCR_DYNH_HTTP_REQ);
1074
1246
  scr_net_ls_drop(&req->err_ls);
1075
1247
  scr_net_ls_drop(&req->close_ls);
@@ -1077,6 +1249,26 @@ static void scr_http_proto_sweep(void) {
1077
1249
  }
1078
1250
  break;
1079
1251
  }
1252
+ case SCR_HTTP_EMIT_REQ_DRAIN: {
1253
+ /* resume() after pause(): the held bytes deliver as one chunk (the
1254
+ * reassembled body is the contract), then a deferred 'end'. A
1255
+ * re-pause mid-drain re-buffers — the swap keeps the walk sound. */
1256
+ ScrHttpReq *req = (ScrHttpReq *)e.h;
1257
+ req->drain_queued = false;
1258
+ if (!req->paused) {
1259
+ char *held = req->pend;
1260
+ size_t held_len = req->pend_len;
1261
+ req->pend = NULL;
1262
+ req->pend_len = req->pend_cap = 0;
1263
+ if (held_len > 0) scr_http_req_deliver(req, held, held_len);
1264
+ free(held);
1265
+ if (!scr_exc_pending() && !req->paused && req->end_pending) {
1266
+ req->end_pending = false;
1267
+ scr_http_req_finish(req, true);
1268
+ }
1269
+ }
1270
+ break;
1271
+ }
1080
1272
  case SCR_HTTP_EMIT_REQ_ABORTED: {
1081
1273
  ScrHttpReq *req = (ScrHttpReq *)e.h;
1082
1274
  if (!req->close_emitted) {
@@ -1103,6 +1295,7 @@ static void scr_http_proto_sweep(void) {
1103
1295
  }
1104
1296
 
1105
1297
  static void scr_http_clients_cleanup(void);
1298
+ static void scr_http_agents_cleanup(void);
1106
1299
 
1107
1300
  static void scr_http_cleanup_atexit(void) {
1108
1301
  scr_http_exiting = true;
@@ -1113,6 +1306,7 @@ static void scr_http_cleanup_atexit(void) {
1113
1306
  free(scr_http_emits);
1114
1307
  scr_http_emits = NULL;
1115
1308
  scr_http_emits_head = scr_http_emits_len = scr_http_emits_cap = 0;
1309
+ scr_http_agents_cleanup(); /* break agent↔client edges first */
1116
1310
  scr_http_clients_cleanup();
1117
1311
  }
1118
1312
 
@@ -1274,6 +1468,20 @@ static void scr_http_conn_bad_request(ScrHttpConn *conn) {
1274
1468
  * feed, and the h2 compat DATA-frame feed). */
1275
1469
  static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n) {
1276
1470
  if (!r || n == 0 || r->ended) return;
1471
+ if (r->paused) {
1472
+ /* req.pause(): the parser keeps consuming (memory is the buffer —
1473
+ * a documented bound), delivery waits for resume()'s drain */
1474
+ if (r->pend_len + n > r->pend_cap) {
1475
+ size_t cap = r->pend_cap ? r->pend_cap : 4096;
1476
+ while (cap < r->pend_len + n) cap *= 2;
1477
+ r->pend = realloc(r->pend, cap);
1478
+ if (!r->pend) scr_http_oom();
1479
+ r->pend_cap = cap;
1480
+ }
1481
+ memcpy(r->pend + r->pend_len, data, n);
1482
+ r->pend_len += n;
1483
+ return;
1484
+ }
1277
1485
  bool piped = r->pipe_res != NULL || r->pipe_client != NULL || r->pipe_sock != NULL;
1278
1486
  if (r->data_ls.n == 0 && !piped) return;
1279
1487
  ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n);
@@ -1495,6 +1703,7 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len) {
1495
1703
  res->sock = scr_net_sock_retain(conn->sock);
1496
1704
  res->conn = conn;
1497
1705
  res->keep_alive = req_keep_alive;
1706
+ res->req_ref = scr_http_req_retain(req); /* res.req — req never points back */
1498
1707
  conn->req = req; /* conn's +1 */
1499
1708
  conn->res = res;
1500
1709
  conn->close_after = !req_keep_alive;
@@ -2037,6 +2246,9 @@ struct ScrHttpClientReq {
2037
2246
  bool close_emitted; /* settled: listeners dropped, off the registry */
2038
2247
  ScrHttpReq *res; /* +1 once the head parses */
2039
2248
  ScrNetLs resp_ls, err_ls, timeout_ls, close_ls, upgrade_ls;
2249
+ /* the owning Agent (+1; the agent's entry holds this client +1 too —
2250
+ * the cycle breaks at settle, or at the atexit agent sweep) */
2251
+ struct ScrHttpAgent *agent;
2040
2252
  bool in_registry;
2041
2253
  struct ScrHttpClientReq *next;
2042
2254
  };
@@ -2102,12 +2314,19 @@ static void scr_http_client_unregister(ScrHttpClientReq *c) {
2102
2314
  }
2103
2315
  }
2104
2316
 
2317
+ /* Agent bookkeeping (implementation below, with the Agent unit): the
2318
+ * settling client leaves its agent's lists and frees a slot. */
2319
+ static void scr_http_agent_client_done(struct ScrHttpAgent *ag, struct ScrHttpClientReq *c);
2320
+ static struct ScrHttpAgent *scr_http_agent_release_p(struct ScrHttpAgent *ag);
2321
+ static void scr_http_client_agent_detach(struct ScrHttpClientReq *c);
2322
+
2105
2323
  /* The queue's CLIENT_CLOSE emit: 'close' fires, the handle settles
2106
2324
  * (listeners drop — the cycle story) and leaves the registry. */
2107
2325
  static void scr_http_client_settle(struct ScrHttpClientReq *c) {
2108
2326
  if (c->close_emitted) return;
2109
2327
  c->close_emitted = true;
2110
2328
  c->destroyed = true;
2329
+ scr_http_client_agent_detach(c); /* usually already detached at socket close */
2111
2330
  scr_net_fire0(&c->close_ls);
2112
2331
  scr_net_ls_drop(&c->resp_ls);
2113
2332
  scr_net_ls_drop(&c->err_ls);
@@ -2558,9 +2777,22 @@ static void scr_http_client_eof(ScrHttpConn *conn) {
2558
2777
  scr_http_client_premature(conn);
2559
2778
  }
2560
2779
 
2780
+ /* Detach a client from its agent (the socket died, or the handle
2781
+ * settled): the entry leaves the lists BEFORE the socket's own 'close'
2782
+ * listeners run — Node's agent removes its socket first too, so
2783
+ * agent.sockets no longer names it inside a user 'close' handler. */
2784
+ static void scr_http_client_agent_detach(ScrHttpClientReq *c) {
2785
+ if (!c->agent) return;
2786
+ struct ScrHttpAgent *ag = c->agent;
2787
+ c->agent = NULL;
2788
+ scr_http_agent_client_done(ag, c); /* frees the slot; may dial the next */
2789
+ scr_http_agent_release_p(ag);
2790
+ }
2791
+
2561
2792
  static void scr_http_client_closed(ScrHttpConn *conn) {
2562
2793
  ScrHttpClientReq *c = conn->client;
2563
2794
  if (!c) return;
2795
+ scr_http_client_agent_detach(c);
2564
2796
  scr_http_client_premature(conn);
2565
2797
  /* the connection is gone: break the ctx→client edge (the client stays
2566
2798
  * alive through user refs / the queue until its 'close' settles) */
@@ -2744,6 +2976,300 @@ ScrHttpClientReq *scr_http_request(ScrStr *host /*borrowed*/, double port,
2744
2976
  fn, 80, NULL, NULL);
2745
2977
  }
2746
2978
 
2979
+ /* ── the http Agent ──────────────────────────────────────────────────────
2980
+ *
2981
+ * Options, getName, destroy, and REAL maxSockets accounting over the
2982
+ * one-dial-per-request connection model: an over-limit request's socket
2983
+ * defers its dial (scr_net_connect_deferred) and queues; a dying
2984
+ * active connection frees the slot and starts the next dial. What the
2985
+ * runtime cannot express stays a NAMED fence: keep-alive socket POOLING
2986
+ * (keepAlive: true) fences at construction — agent.freeSockets is
2987
+ * always empty; the runtime does not emulate a pool.
2988
+ *
2989
+ * Ownership: the agent registers (+1, atexit-swept); entries hold their
2990
+ * client +1 and the client holds the agent +1 — the cycle breaks when
2991
+ * the client's socket dies (scr_http_client_agent_detach) or at the
2992
+ * atexit sweep. */
2993
+
2994
+ typedef struct ScrHttpAgentEnt {
2995
+ ScrStr *name; /* getName's shape: "host:port:" */
2996
+ ScrHttpClientReq *client; /* +1 while listed */
2997
+ bool queued; /* waiting for a slot: the dial is deferred */
2998
+ struct ScrHttpAgentEnt *next;
2999
+ } ScrHttpAgentEnt;
3000
+
3001
+ typedef struct ScrHttpAgent {
3002
+ size_t rc;
3003
+ bool secure; /* https.Agent: protocol/defaultPort answers */
3004
+ bool keep_alive; /* always false here (true fences at construction) */
3005
+ double ka_msecs;
3006
+ double max_sockets; /* INFINITY = Node's default */
3007
+ double max_free;
3008
+ double timeout_ms; /* < 0 = unset */
3009
+ double default_port; /* settable (agent.defaultPort = p) */
3010
+ bool destroyed;
3011
+ ScrHttpAgentEnt *ents; /* append order — Node's FIFO queue */
3012
+ bool in_registry;
3013
+ struct ScrHttpAgent *next;
3014
+ } ScrHttpAgent;
3015
+
3016
+ static ScrHttpAgent *scr_http_agents = NULL; /* registry: +1 each, atexit-swept */
3017
+
3018
+ static ScrHttpAgent *scr_http_agent_retain(ScrHttpAgent *a) {
3019
+ a->rc++;
3020
+ return a;
3021
+ }
3022
+
3023
+ static void scr_http_agent_release(ScrHttpAgent *a) {
3024
+ if (!a || --a->rc > 0) return;
3025
+ ScrHttpAgentEnt *e = a->ents;
3026
+ while (e) {
3027
+ ScrHttpAgentEnt *next = e->next;
3028
+ scr_str_release(e->name);
3029
+ scr_http_client_release(e->client);
3030
+ free(e);
3031
+ e = next;
3032
+ }
3033
+ free(a);
3034
+ }
3035
+
3036
+ static struct ScrHttpAgent *scr_http_agent_release_p(struct ScrHttpAgent *ag) {
3037
+ scr_http_agent_release(ag);
3038
+ return NULL;
3039
+ }
3040
+
3041
+ static void *scr_http_agent_retain_v(void *p) { return scr_http_agent_retain((ScrHttpAgent *)p); }
3042
+ static void scr_http_agent_release_v(void *p) { scr_http_agent_release((ScrHttpAgent *)p); }
3043
+
3044
+ /* Actives (dialed, not settled) under a name. */
3045
+ static size_t scr_http_agent_active(const ScrHttpAgent *a, const ScrStr *name) {
3046
+ size_t n = 0;
3047
+ for (const ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
3048
+ if (!e->queued && e->name->len == name->len &&
3049
+ memcmp(e->name->data, name->data, name->len) == 0) n++;
3050
+ }
3051
+ return n;
3052
+ }
3053
+
3054
+ /* The settling/dying client leaves the lists; a freed slot starts the
3055
+ * first queued dial for the same name (Node's FIFO). */
3056
+ static void scr_http_agent_client_done(struct ScrHttpAgent *ag, struct ScrHttpClientReq *c) {
3057
+ ScrHttpAgentEnt **link = &ag->ents;
3058
+ ScrStr *name = NULL;
3059
+ while (*link && (*link)->client != c) link = &(*link)->next;
3060
+ if (*link) {
3061
+ ScrHttpAgentEnt *e = *link;
3062
+ *link = e->next;
3063
+ name = e->name; /* ownership moves out for the pump below */
3064
+ scr_http_client_release(e->client);
3065
+ free(e);
3066
+ }
3067
+ if (name != NULL && !ag->destroyed) {
3068
+ for (ScrHttpAgentEnt *e = ag->ents; e; e = e->next) {
3069
+ if (e->queued && e->name->len == name->len &&
3070
+ memcmp(e->name->data, name->data, name->len) == 0) {
3071
+ if (scr_http_agent_active(ag, name) < ag->max_sockets) {
3072
+ e->queued = false;
3073
+ if (e->client->sock) scr_net_sock_dial_start(e->client->sock);
3074
+ }
3075
+ break;
3076
+ }
3077
+ }
3078
+ }
3079
+ scr_str_release(name);
3080
+ }
3081
+
3082
+ /* getName's string builder — Node's exact shape:
3083
+ * host:port:localAddress[:family][:socketPath]. */
3084
+ static ScrStr *scr_http_agent_name(const char *host, size_t host_len, const char *port,
3085
+ size_t port_len, const char *laddr, size_t laddr_len,
3086
+ int family, const char *spath, size_t spath_len) {
3087
+ ScrJsonBuf b;
3088
+ scr_jb_init(&b);
3089
+ if (host_len == 0) { scr_jb_puts(&b, "localhost"); }
3090
+ else for (size_t i = 0; i < host_len; i++) scr_jb_putc(&b, host[i]);
3091
+ scr_jb_putc(&b, ':');
3092
+ for (size_t i = 0; i < port_len; i++) scr_jb_putc(&b, port[i]);
3093
+ scr_jb_putc(&b, ':');
3094
+ for (size_t i = 0; i < laddr_len; i++) scr_jb_putc(&b, laddr[i]);
3095
+ if (family == 4 || family == 6) {
3096
+ scr_jb_putc(&b, ':');
3097
+ scr_jb_putc(&b, family == 4 ? '4' : '6');
3098
+ }
3099
+ if (spath_len > 0) {
3100
+ scr_jb_putc(&b, ':');
3101
+ for (size_t i = 0; i < spath_len; i++) scr_jb_putc(&b, spath[i]);
3102
+ }
3103
+ return scr_jb_finish(&b);
3104
+ }
3105
+
3106
+ ScrDyn *scr_http_agent_new(bool secure, bool keep_alive, double ka_msecs,
3107
+ double max_sockets, double max_free, double timeout_ms,
3108
+ double port /* < 0 = unset */) {
3109
+ if (keep_alive) {
3110
+ static const char msg[] =
3111
+ "an http Agent with keepAlive: true (socket pooling and reuse — compiled clients "
3112
+ "dial one connection per request and close it with the response) is not supported "
3113
+ "yet — construct the Agent without keepAlive, or drop the agent option";
3114
+ scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
3115
+ return NULL;
3116
+ }
3117
+ scr_http_install();
3118
+ ScrHttpAgent *a = calloc(1, sizeof *a);
3119
+ if (!a) scr_http_oom();
3120
+ a->rc = 1;
3121
+ a->secure = secure;
3122
+ a->keep_alive = false;
3123
+ a->ka_msecs = ka_msecs >= 0 ? ka_msecs : 1000;
3124
+ a->max_sockets = max_sockets >= 0 ? max_sockets : (double)INFINITY;
3125
+ a->max_free = max_free >= 0 ? max_free : 256;
3126
+ a->timeout_ms = timeout_ms;
3127
+ /* an agent-options `port` merges under portless request options (Node
3128
+ * merges agent options into each connection) — the settable
3129
+ * defaultPort carries it */
3130
+ a->default_port = port >= 0 ? port : secure ? 443 : 80;
3131
+ /* registry (+1): the atexit sweep breaks agent↔client edges a program
3132
+ * legitimately leaves in flight */
3133
+ a->in_registry = true;
3134
+ a->next = scr_http_agents;
3135
+ scr_http_agents = scr_http_agent_retain(a);
3136
+ ScrDyn *d = scr_dyn_new_handle(a, SCR_DYNH_HTTP_AGENT);
3137
+ scr_http_agent_release(a); /* the handle's retain holds it */
3138
+ return d;
3139
+ }
3140
+
3141
+ /* agent.destroy(): tears down every listed connection (actives destroy
3142
+ * their sockets, queued dials never start) — Node destroys in-use
3143
+ * sockets too; there is no free pool here. */
3144
+ static void scr_http_agent_destroy(ScrHttpAgent *a) {
3145
+ a->destroyed = true;
3146
+ /* destroying sockets detaches entries re-entrantly — walk a snapshot */
3147
+ for (;;) {
3148
+ ScrHttpClientReq *victim = NULL;
3149
+ for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
3150
+ if (e->client->sock && !e->client->close_emitted) {
3151
+ victim = e->client;
3152
+ break;
3153
+ }
3154
+ }
3155
+ if (!victim) break;
3156
+ scr_http_client_agent_detach(victim); /* leaves the list first */
3157
+ scr_net_sock_destroy(victim->sock);
3158
+ }
3159
+ }
3160
+
3161
+ ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double port,
3162
+ ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
3163
+ double timeout_ms, ScrArr *header_pairs /*borrowed*/,
3164
+ bool auto_end, const ScrDyn *agent /*borrowed*/,
3165
+ ScrClosure *cb /*moves, nullable*/,
3166
+ ScrHttpRespFn fn, int default_port,
3167
+ void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx) {
3168
+ /* the null/undefined arms: the default path (port < 0 = no port option) */
3169
+ if (agent == NULL || agent->kind == SCR_DYN_UNDEF || agent->kind == SCR_DYN_NULL) {
3170
+ double p = port >= 0 ? port : (double)default_port;
3171
+ return scr_http_request_impl(host, p, path, method, timeout_ms, header_pairs, auto_end,
3172
+ cb, fn, default_port, wrap, wrap_ctx, NULL);
3173
+ }
3174
+ /* agent: false — Node's one-shot Agent: exactly this client's model,
3175
+ * plus its Connection: close request header (unless the caller set one) */
3176
+ if (agent->kind == SCR_DYN_BOOL && !agent->v.b) {
3177
+ double p = port >= 0 ? port : (double)default_port;
3178
+ bool has_conn = false;
3179
+ size_t npairs = (size_t)scr_arr_len(header_pairs);
3180
+ for (size_t i = 0; i + 1 < npairs && !has_conn; i += 2) {
3181
+ ScrStr *n = (ScrStr *)scr_arr_get_ref(header_pairs, (double)i);
3182
+ if (n->len == 10) {
3183
+ bool eq = true;
3184
+ for (size_t j = 0; j < 10 && eq; j++) {
3185
+ if (tolower((unsigned char)n->data[j]) != "connection"[j]) eq = false;
3186
+ }
3187
+ has_conn = eq;
3188
+ }
3189
+ scr_str_release(n);
3190
+ }
3191
+ ScrArr *pairs = header_pairs;
3192
+ ScrArr *copy = NULL;
3193
+ if (!has_conn) {
3194
+ copy = scr_arr_new(SCR_ELEM_STR, npairs + 2);
3195
+ for (size_t i = 0; i < npairs; i++) scr_arr_push_ref(copy, scr_arr_get_ref(header_pairs, (double)i));
3196
+ scr_arr_push_ref(copy, scr_str_new("Connection", 10));
3197
+ scr_arr_push_ref(copy, scr_str_new("close", 5));
3198
+ pairs = copy;
3199
+ }
3200
+ ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, pairs,
3201
+ auto_end, cb, fn, default_port, wrap, wrap_ctx, NULL);
3202
+ if (copy) scr_arr_release(copy);
3203
+ return c;
3204
+ }
3205
+ if (agent->kind != SCR_DYN_HANDLE || agent->v.handle.tag != SCR_DYNH_HTTP_AGENT) {
3206
+ if (cb) scr_closure_release(cb);
3207
+ scr_dyn_arg_type_fail("options.agent", "an instance of http.Agent, false, null, or undefined", agent);
3208
+ return NULL;
3209
+ }
3210
+ ScrHttpAgent *ag = (ScrHttpAgent *)agent->v.handle.ptr;
3211
+ double p = port >= 0 ? port
3212
+ : ag->default_port > 0 ? ag->default_port
3213
+ : (double)default_port;
3214
+ char portbuf[16];
3215
+ int portn = snprintf(portbuf, sizeof portbuf, "%d", (int)p);
3216
+ ScrStr *name = scr_http_agent_name(host->data, host->len, portbuf, (size_t)portn,
3217
+ NULL, 0, 0, NULL, 0);
3218
+ bool queue = scr_http_agent_active(ag, name) >= ag->max_sockets;
3219
+ ScrNetSocket *presock = queue ? scr_net_connect_deferred(p, host) : NULL;
3220
+ ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, header_pairs,
3221
+ auto_end, cb, fn, default_port, wrap, wrap_ctx,
3222
+ presock);
3223
+ if (c == NULL) { /* the method-token throw: nothing registered */
3224
+ scr_str_release(name);
3225
+ return NULL;
3226
+ }
3227
+ if (ag->timeout_ms >= 0 && timeout_ms <= 0) scr_net_sock_set_timeout(c->sock, ag->timeout_ms);
3228
+ c->agent = scr_http_agent_retain(ag);
3229
+ ScrHttpAgentEnt *e = calloc(1, sizeof *e);
3230
+ if (!e) scr_http_oom();
3231
+ e->name = name; /* ownership moves */
3232
+ e->client = scr_http_client_retain(c);
3233
+ e->queued = queue;
3234
+ ScrHttpAgentEnt **link = &ag->ents;
3235
+ while (*link) link = &(*link)->next;
3236
+ *link = e;
3237
+ return c;
3238
+ }
3239
+
3240
+ ScrHttpClientReq *scr_http_request_agent(ScrStr *host /*borrowed*/, double port,
3241
+ ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
3242
+ double timeout_ms, ScrArr *header_pairs /*borrowed*/,
3243
+ bool auto_end, const ScrDyn *agent /*borrowed*/,
3244
+ ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) {
3245
+ return scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, auto_end,
3246
+ agent, cb, fn, 80, NULL, NULL);
3247
+ }
3248
+
3249
+ /* The atexit sweep: break agent↔client edges a program leaves in flight
3250
+ * (queued dials that never ran) so the RC audit sees a clean heap. */
3251
+ static void scr_http_agents_cleanup(void) {
3252
+ while (scr_http_agents) {
3253
+ ScrHttpAgent *a = scr_http_agents;
3254
+ scr_http_agents = a->next;
3255
+ a->in_registry = false;
3256
+ ScrHttpAgentEnt *e = a->ents;
3257
+ a->ents = NULL;
3258
+ while (e) {
3259
+ ScrHttpAgentEnt *next = e->next;
3260
+ if (e->client->agent) {
3261
+ scr_http_agent_release(e->client->agent);
3262
+ e->client->agent = NULL;
3263
+ }
3264
+ scr_str_release(e->name);
3265
+ scr_http_client_release(e->client);
3266
+ free(e);
3267
+ e = next;
3268
+ }
3269
+ scr_http_agent_release(a);
3270
+ }
3271
+ }
3272
+
2747
3273
  /* Exit-time registry cleanup (the net-unit precedent): clients a program
2748
3274
  * legitimately leaves in flight at exit release their listeners and
2749
3275
  * registry references so the RC audit sees a clean heap. */
@@ -2854,12 +3380,43 @@ static bool scr_http_dynh_chunk_ok(const ScrDyn *chunk) {
2854
3380
  return false;
2855
3381
  }
2856
3382
 
2857
- /* An optional trailing encoding argument: utf8 spellings pass (chunks
2858
- * already carry utf8 bytes), anything else fences loudly. */
2859
- static bool scr_http_dynh_enc_ok(const ScrDyn *enc, const char *cls, const char *member) {
2860
- if (scr_http_dynh_name_is(enc, "utf8") || scr_http_dynh_name_is(enc, "utf-8")) return true;
2861
- scr_http_dynh_unsupported(cls, member, "only the 'utf8' encoding is modeled");
2862
- return false;
3383
+ /* An optional trailing encoding argument on write/end: utf8 spellings
3384
+ * pass through (string chunks already carry utf8 bytes); every other
3385
+ * real Node encoding decodes the STRING chunk through Buffer.from's
3386
+ * decoder into *out (+1; buffer chunks ignore the encoding, Node);
3387
+ * unknown names throw Node's ERR_UNKNOWN_ENCODING. False = exception
3388
+ * pending. */
3389
+ static bool scr_http_dynh_encode(const ScrDyn *chunk, const ScrDyn *enc, ScrBytes **out) {
3390
+ *out = NULL;
3391
+ if (enc->kind != SCR_DYN_STR || !scr_bytes_is_encoding(enc->v.str)) {
3392
+ ScrJsonBuf b;
3393
+ scr_jb_init(&b);
3394
+ scr_jb_puts(&b, "Unknown encoding: ");
3395
+ if (enc->kind == SCR_DYN_STR) {
3396
+ for (size_t i = 0; i < enc->v.str->len && i < 64; i++) scr_jb_putc(&b, enc->v.str->data[i]);
3397
+ }
3398
+ ScrStr *msg = scr_jb_finish(&b);
3399
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_UNKNOWN_ENCODING");
3400
+ scr_str_release(msg);
3401
+ return false;
3402
+ }
3403
+ if (chunk->kind != SCR_DYN_STR) return true; /* buffers carry their bytes */
3404
+ /* lowercase + alias normalization down to scr_bytes_from_str's arms */
3405
+ char low[10];
3406
+ size_t n = enc->v.str->len < 9 ? enc->v.str->len : 9;
3407
+ for (size_t i = 0; i < n; i++) {
3408
+ char c = enc->v.str->data[i];
3409
+ low[i] = c >= 'A' && c <= 'Z' ? (char)(c + 32) : c;
3410
+ }
3411
+ low[n] = 0;
3412
+ const char *canon = low;
3413
+ if (strcmp(low, "utf8") == 0 || strcmp(low, "utf-8") == 0) return true; /* passthrough */
3414
+ if (strcmp(low, "binary") == 0) canon = "latin1";
3415
+ else if (strcmp(low, "ucs2") == 0 || strcmp(low, "ucs-2") == 0 || strcmp(low, "utf-16le") == 0) canon = "utf16le";
3416
+ ScrStr *cs = scr_str_new(canon, strlen(canon));
3417
+ *out = scr_bytes_from_str(chunk->v.str, cs);
3418
+ scr_str_release(cs);
3419
+ return true;
2863
3420
  }
2864
3421
 
2865
3422
  /* ── IncomingMessage (SCR_DYNH_HTTP_REQ) ─────────────────────────────── */
@@ -2883,6 +3440,9 @@ static ScrDyn *scr_http_dynh_req_invoke(void *h, ScrDyn *self, const char *metho
2883
3440
  scr_http_req_on_close(r, scr_dyn_listener_closure0(cb), once);
2884
3441
  } else if (scr_http_dynh_name_is(name, "aborted")) {
2885
3442
  scr_http_req_on_aborted(r, scr_dyn_listener_closure0(cb), once);
3443
+ } else if (scr_http_dynh_name_is(name, "timeout") && r->sock != NULL) {
3444
+ /* the socket's idle timer — req.setTimeout's event, Node's delegation */
3445
+ scr_net_sock_on_timeout(r->sock, scr_dyn_listener_closure0(cb), once);
2886
3446
  } else {
2887
3447
  scr_http_dynh_event_unsupported("IncomingMessage", name);
2888
3448
  return NULL;
@@ -2893,6 +3453,25 @@ static ScrDyn *scr_http_dynh_req_invoke(void *h, ScrDyn *self, const char *metho
2893
3453
  scr_http_req_resume(r);
2894
3454
  return scr_dyn_retain(self);
2895
3455
  }
3456
+ if (strcmp(method, "pause") == 0) {
3457
+ scr_http_req_pause(r);
3458
+ return scr_dyn_retain(self);
3459
+ }
3460
+ if (strcmp(method, "setTimeout") == 0) {
3461
+ const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
3462
+ if (ms->kind != SCR_DYN_NUM) {
3463
+ scr_dyn_arg_type_fail("msecs", "of type number", ms);
3464
+ return NULL;
3465
+ }
3466
+ ScrClosure *cb = NULL;
3467
+ if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) cb = scr_dyn_listener_closure0(args[1]);
3468
+ else if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) {
3469
+ scr_dyn_check_listener(args[1], "callback");
3470
+ return NULL;
3471
+ }
3472
+ scr_http_req_set_timeout(r, ms->v.num, cb);
3473
+ return scr_dyn_retain(self);
3474
+ }
2896
3475
  if (strcmp(method, "setEncoding") == 0) {
2897
3476
  const ScrDyn *enc = argc > 0 ? args[0] : scr_dyn_undefined();
2898
3477
  if (enc->kind != SCR_DYN_STR) {
@@ -2932,7 +3511,7 @@ static ScrDyn *scr_http_dynh_req_invoke(void *h, ScrDyn *self, const char *metho
2932
3511
  }
2933
3512
  {
2934
3513
  /* Real prototype members without a modeled dispatch: loud. */
2935
- static const char *const known[] = { "pause", "unpipe", "setTimeout",
3514
+ static const char *const known[] = { "unpipe",
2936
3515
  "read", "push", "off", "removeListener", "removeAllListeners", "emit",
2937
3516
  "prependListener", "prependOnceListener", "listenerCount", "listeners",
2938
3517
  "isPaused", "wrap", "cork", "uncork", NULL };
@@ -3028,12 +3607,15 @@ static ScrDyn *scr_http_dynh_req_get(void *h, const char *key, size_t key_len) {
3028
3607
  }
3029
3608
  if (strcmp(key, "aborted") == 0) return scr_dyn_new_bool(r->aborted);
3030
3609
  if (strcmp(key, "complete") == 0) return scr_dyn_new_bool(r->ended);
3610
+ if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_req_destroyed_flag(r));
3611
+ if (strcmp(key, "readable") == 0) return scr_dyn_new_bool(scr_http_req_readable(r));
3612
+ if (strcmp(key, "readableEnded") == 0) return scr_dyn_new_bool(r->ended);
3613
+ if (strcmp(key, "closed") == 0) return scr_dyn_new_bool(r->close_emitted);
3031
3614
  {
3032
3615
  /* Real instance properties without a modeled read: loud, never a
3033
3616
  * silent undefined where Node has a value. */
3034
3617
  static const char *const known[] = {
3035
- "trailers", "rawTrailers", "readable", "readableEnded",
3036
- "closed", "destroyed", NULL };
3618
+ "trailers", "rawTrailers", NULL };
3037
3619
  if (scr_http_dynh_in(key, known)) {
3038
3620
  scr_http_dynh_unsupported("IncomingMessage", key, NULL);
3039
3621
  return NULL;
@@ -3088,6 +3670,55 @@ static ScrArr *scr_http_dynh_pairs(const ScrDyn *headers) {
3088
3670
  return pairs;
3089
3671
  }
3090
3672
 
3673
+ /* writeHead's RAW-array form over a dyn ARR: [k0, v0, k1, v1, ...]
3674
+ * flattens into the same pairs feed; an odd length throws Node's
3675
+ * ERR_INVALID_ARG_VALUE; value slots may be arrays (consecutive
3676
+ * same-name lines) or numbers (String(n)). NULL = exception pending. */
3677
+ static ScrArr *scr_http_dynh_flat_pairs(const ScrDyn *list) {
3678
+ size_t n = list->v.arr.len;
3679
+ if (n % 2 != 0) {
3680
+ static const char msg[] = "The argument 'headers' is invalid.";
3681
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_VALUE");
3682
+ return NULL;
3683
+ }
3684
+ ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, n);
3685
+ for (size_t i = 0; i + 1 < n; i += 2) {
3686
+ const ScrDyn *k = list->v.arr.items[i];
3687
+ const ScrDyn *v = list->v.arr.items[i + 1];
3688
+ if (k->kind != SCR_DYN_STR) {
3689
+ scr_arr_release(pairs);
3690
+ scr_dyn_arg_type_fail("name", "of type string", k);
3691
+ return NULL;
3692
+ }
3693
+ size_t reps = v->kind == SCR_DYN_ARR ? v->v.arr.len : 1;
3694
+ for (size_t rep = 0; rep < reps; rep++) {
3695
+ const ScrDyn *one = v->kind == SCR_DYN_ARR ? v->v.arr.items[rep] : v;
3696
+ ScrStr *vs;
3697
+ if (one->kind == SCR_DYN_STR) {
3698
+ vs = scr_str_retain(one->v.str);
3699
+ } else if (one->kind == SCR_DYN_NUM) {
3700
+ vs = scr_f64_to_scrstr(one->v.num);
3701
+ } else {
3702
+ ScrJsonBuf b;
3703
+ scr_jb_init(&b);
3704
+ scr_jb_puts(&b, "Invalid value \"");
3705
+ scr_jb_puts(&b, one->kind == SCR_DYN_UNDEF ? "undefined" : "object");
3706
+ scr_jb_puts(&b, "\" for header \"");
3707
+ for (size_t c = 0; c < k->v.str->len; c++) scr_jb_putc(&b, k->v.str->data[c]);
3708
+ scr_jb_putc(&b, '"');
3709
+ ScrStr *msg = scr_jb_finish(&b);
3710
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_HTTP_INVALID_HEADER_VALUE");
3711
+ scr_str_release(msg);
3712
+ scr_arr_release(pairs);
3713
+ return NULL;
3714
+ }
3715
+ scr_arr_push_ref(pairs, scr_str_new(k->v.str->data, k->v.str->len));
3716
+ scr_arr_push_ref(pairs, vs);
3717
+ }
3718
+ }
3719
+ return pairs;
3720
+ }
3721
+
3091
3722
  static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *method,
3092
3723
  ScrDyn *const *args, size_t argc, const char *what) {
3093
3724
  ScrHttpRes *r = (ScrHttpRes *)h;
@@ -3105,6 +3736,9 @@ static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *metho
3105
3736
  /* This surface never backpressures (write answers true), so Node's
3106
3737
  * contract says 'drain' never fires — an accepted, never-fired
3107
3738
  * registration is the consistent answer (SEMANTICS.md). */
3739
+ } else if (scr_http_dynh_name_is(name, "timeout") && r->sock != NULL) {
3740
+ /* the socket's idle timer — res.setTimeout's event, Node's delegation */
3741
+ scr_net_sock_on_timeout(r->sock, scr_dyn_listener_closure0(cb), once);
3108
3742
  } else {
3109
3743
  scr_http_dynh_event_unsupported("ServerResponse", name);
3110
3744
  return NULL;
@@ -3117,41 +3751,117 @@ static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *metho
3117
3751
  size_t i = 0;
3118
3752
  const ScrDyn *chunk = NULL;
3119
3753
  const ScrDyn *cb = NULL;
3754
+ ScrBytes *decoded = NULL;
3120
3755
  if (i < argc && args[i]->kind == SCR_DYN_FUNC) {
3121
3756
  cb = args[i++];
3122
3757
  } else if (i < argc && args[i]->kind != SCR_DYN_UNDEF && args[i]->kind != SCR_DYN_NULL) {
3123
3758
  if (!scr_http_dynh_chunk_ok(args[i])) return NULL;
3124
3759
  chunk = args[i++];
3125
3760
  if (i < argc && args[i]->kind == SCR_DYN_STR) { /* encoding */
3126
- if (!scr_http_dynh_enc_ok(args[i], "ServerResponse", "end")) return NULL;
3761
+ if (!scr_http_dynh_encode(chunk, args[i], &decoded)) return NULL;
3127
3762
  i++;
3128
3763
  }
3129
3764
  if (i < argc && args[i]->kind == SCR_DYN_FUNC) cb = args[i++];
3130
3765
  }
3131
3766
  if (i < argc && args[i]->kind != SCR_DYN_UNDEF) {
3767
+ scr_bytes_release(decoded);
3132
3768
  scr_http_dynh_unsupported("ServerResponse", "end", "this argument shape is not modeled");
3133
3769
  return NULL;
3134
3770
  }
3135
3771
  if (cb) scr_http_res_on_finish(r, scr_dyn_listener_closure0(cb));
3136
- if (chunk == NULL) scr_http_res_end(r);
3137
- else if (chunk->kind == SCR_DYN_STR) scr_http_res_end_str(r, chunk->v.str);
3138
- else scr_http_res_end_bytes(r, chunk->v.bytes);
3772
+ if (decoded != NULL) {
3773
+ scr_http_res_end_bytes(r, decoded);
3774
+ scr_bytes_release(decoded);
3775
+ } else if (chunk == NULL) {
3776
+ scr_http_res_end(r);
3777
+ } else if (chunk->kind == SCR_DYN_STR) {
3778
+ scr_http_res_end_str(r, chunk->v.str);
3779
+ } else {
3780
+ scr_http_res_end_bytes(r, chunk->v.bytes);
3781
+ }
3139
3782
  return scr_dyn_retain(self);
3140
3783
  }
3141
3784
  if (strcmp(method, "write") == 0) {
3142
3785
  const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
3143
3786
  if (!scr_http_dynh_chunk_ok(chunk)) return NULL;
3144
- if (argc > 1 && args[1]->kind == SCR_DYN_STR && !scr_http_dynh_enc_ok(args[1], "ServerResponse", "write")) return NULL;
3145
- if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) {
3146
- scr_http_dynh_unsupported("ServerResponse", "write", "write(chunk, callback) is not modeled");
3147
- return NULL;
3787
+ ScrBytes *decoded = NULL;
3788
+ size_t i = 1;
3789
+ if (i < argc && args[i]->kind == SCR_DYN_STR) { /* encoding */
3790
+ if (!scr_http_dynh_encode(chunk, args[i], &decoded)) return NULL;
3791
+ i++;
3792
+ }
3793
+ if (i < argc && args[i]->kind == SCR_DYN_FUNC) {
3794
+ /* write(chunk[, enc], cb): fires from the queue once the chunk
3795
+ * entered the socket buffer — this surface's flush moment */
3796
+ scr_http_res_on_write_flush(r, scr_dyn_listener_closure0(args[i]));
3797
+ i++;
3798
+ }
3799
+ if (decoded != NULL) {
3800
+ scr_http_res_write_bytes(r, decoded);
3801
+ scr_bytes_release(decoded);
3802
+ } else if (chunk->kind == SCR_DYN_STR) {
3803
+ scr_http_res_write_str(r, chunk->v.str);
3804
+ } else {
3805
+ scr_http_res_write_bytes(r, chunk->v.bytes);
3148
3806
  }
3149
- if (chunk->kind == SCR_DYN_STR) scr_http_res_write_str(r, chunk->v.str);
3150
- else scr_http_res_write_bytes(r, chunk->v.bytes);
3151
3807
  /* Always-true: this surface buffers without a highWaterMark verdict
3152
3808
  * (SEMANTICS.md — backpressure is not modeled). */
3153
3809
  return scr_dyn_new_bool(true);
3154
3810
  }
3811
+ if (strcmp(method, "flushHeaders") == 0) {
3812
+ scr_http_res_flush_headers(r);
3813
+ return scr_dyn_retain(scr_dyn_undefined());
3814
+ }
3815
+ if (strcmp(method, "cork") == 0) {
3816
+ scr_http_res_cork(r);
3817
+ return scr_dyn_retain(scr_dyn_undefined());
3818
+ }
3819
+ if (strcmp(method, "uncork") == 0) {
3820
+ scr_http_res_uncork(r);
3821
+ return scr_dyn_retain(scr_dyn_undefined());
3822
+ }
3823
+ if (strcmp(method, "setTimeout") == 0) {
3824
+ const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
3825
+ if (ms->kind != SCR_DYN_NUM) {
3826
+ scr_dyn_arg_type_fail("msecs", "of type number", ms);
3827
+ return NULL;
3828
+ }
3829
+ ScrClosure *tcb = NULL;
3830
+ if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) tcb = scr_dyn_listener_closure0(args[1]);
3831
+ else if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) {
3832
+ scr_dyn_check_listener(args[1], "callback");
3833
+ return NULL;
3834
+ }
3835
+ scr_http_res_set_timeout(r, ms->v.num, tcb);
3836
+ return scr_dyn_retain(self);
3837
+ }
3838
+ if (strcmp(method, "getHeaders") == 0) {
3839
+ /* the snapshot object — lowercased names; repeated names read as an
3840
+ * array in set order (Node's outgoing shape) */
3841
+ ScrDyn *obj = scr_dyn_new_obj();
3842
+ for (size_t k = 0; k < r->nheaders; k++) {
3843
+ const ScrStr *n = r->hnames[k];
3844
+ char lower[256];
3845
+ size_t nl = n->len < sizeof lower ? n->len : sizeof lower - 1;
3846
+ for (size_t c = 0; c < nl; c++) {
3847
+ char ch = n->data[c];
3848
+ lower[c] = ch >= 'A' && ch <= 'Z' ? (char)(ch + 32) : ch;
3849
+ }
3850
+ ScrDyn *prev = scr_dyn_obj_get(obj, lower, nl);
3851
+ ScrDyn *val = scr_dyn_new_str(r->hvalues[k]);
3852
+ if (prev == NULL) {
3853
+ scr_dyn_obj_set(obj, lower, nl, val); /* moves */
3854
+ } else if (prev->kind == SCR_DYN_ARR) {
3855
+ scr_dyn_arr_push(prev, val); /* moves */
3856
+ } else {
3857
+ ScrDyn *arr = scr_dyn_new_arr();
3858
+ scr_dyn_arr_push(arr, scr_dyn_retain(prev));
3859
+ scr_dyn_arr_push(arr, val);
3860
+ scr_dyn_obj_set(obj, lower, nl, arr); /* moves; releases prev */
3861
+ }
3862
+ }
3863
+ return obj;
3864
+ }
3155
3865
  if (strcmp(method, "writeHead") == 0) {
3156
3866
  const ScrDyn *st = argc > 0 ? args[0] : scr_dyn_undefined();
3157
3867
  if (st->kind != SCR_DYN_NUM) {
@@ -3169,8 +3879,21 @@ static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *metho
3169
3879
  scr_http_res_write_head_pairs(r, st->v.num, pairs);
3170
3880
  scr_arr_release(pairs);
3171
3881
  i++;
3882
+ } else if (i < argc && args[i]->kind == SCR_DYN_ARR) {
3883
+ /* the RAW-array form: [k0, v0, k1, v1, ...] — an even length is
3884
+ * Node's contract (ERR_INVALID_ARG_VALUE otherwise), values may be
3885
+ * arrays (consecutive same-name lines, the setHeader expansion),
3886
+ * and per NAME the list overrides what setHeader() stored while
3887
+ * other names survive — the object form's writeHead-wins merge
3888
+ * (oracle-pinned: a res with setHeader('a') plus writeHead(200,
3889
+ * ['test', ...]) sends both). */
3890
+ ScrArr *pairs = scr_http_dynh_flat_pairs(args[i]);
3891
+ if (!pairs) return NULL;
3892
+ scr_http_res_write_head_pairs(r, st->v.num, pairs);
3893
+ scr_arr_release(pairs);
3894
+ i++;
3172
3895
  } else if (i < argc && args[i]->kind != SCR_DYN_UNDEF) {
3173
- scr_http_dynh_unsupported("ServerResponse", "writeHead", "only the header-object form is modeled");
3896
+ scr_http_dynh_unsupported("ServerResponse", "writeHead", "only the header-object and raw-array forms are modeled");
3174
3897
  return NULL;
3175
3898
  } else {
3176
3899
  scr_http_res_write_head(r, st->v.num);
@@ -3250,8 +3973,8 @@ static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *metho
3250
3973
  return d;
3251
3974
  }
3252
3975
  {
3253
- static const char *const known[] = { "writeContinue", "writeEarlyHints", "cork", "uncork",
3254
- "flushHeaders", "getHeaders", "getHeaderNames", "appendHeader", "setTimeout",
3976
+ static const char *const known[] = { "writeContinue", "writeEarlyHints",
3977
+ "getHeaderNames", "appendHeader",
3255
3978
  "addTrailers", "off", "removeListener", "removeAllListeners", "emit",
3256
3979
  "prependListener", "prependOnceListener", "listenerCount", "listeners", "setDefaultEncoding", NULL };
3257
3980
  if (scr_http_dynh_in(method, known)) {
@@ -3283,10 +4006,23 @@ static ScrDyn *scr_http_dynh_res_get(void *h, const char *key, size_t key_len) {
3283
4006
  if (!r->sock) return scr_dyn_new_null(); /* destroyed: Node nulls it */
3284
4007
  return scr_dyn_new_handle(r->sock, SCR_DYNH_NET_SOCKET);
3285
4008
  }
4009
+ if (strcmp(key, "req") == 0) {
4010
+ if (r->req_cleared || r->req_ref == NULL) return scr_dyn_new_null();
4011
+ return scr_dyn_new_handle(r->req_ref, SCR_DYNH_HTTP_REQ);
4012
+ }
4013
+ if (strcmp(key, "writableCorked") == 0) return scr_dyn_new_num(scr_http_res_writable_corked(r));
4014
+ if (strcmp(key, "writableFinished") == 0) return scr_dyn_new_bool(r->finished);
4015
+ if (strcmp(key, "writableHighWaterMark") == 0) {
4016
+ /* Node's default socket highWaterMark — a constant here (backpressure
4017
+ * is not modeled; SEMANTICS.md) */
4018
+ return scr_dyn_new_num(16384);
4019
+ }
4020
+ if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_res_destroyed_flag(r));
4021
+ if (strcmp(key, "closed") == 0) return scr_dyn_new_bool(r->close_emitted);
3286
4022
  {
3287
- static const char *const known[] = { "req", "chunkedEncoding", "sendDate",
3288
- "strictContentLength", "writableFinished", "writableLength", "writableHighWaterMark",
3289
- "writableCorked", "writableObjectMode", "closed", "destroyed", "errored", NULL };
4023
+ static const char *const known[] = { "chunkedEncoding", "sendDate",
4024
+ "strictContentLength", "writableLength",
4025
+ "writableObjectMode", "errored", NULL };
3290
4026
  if (scr_http_dynh_in(key, known)) {
3291
4027
  scr_http_dynh_unsupported("ServerResponse", key, NULL);
3292
4028
  return NULL;
@@ -3320,6 +4056,14 @@ static bool scr_http_dynh_res_set(void *h, const char *key, size_t key_len, cons
3320
4056
  r->no_date = !scr_dyn_truthy(value);
3321
4057
  return true;
3322
4058
  }
4059
+ if (strcmp(key, "req") == 0 &&
4060
+ (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF)) {
4061
+ /* res.req is a plain property in Node — a null/undefined write
4062
+ * clears the read; other writes raise the named fence (the handle
4063
+ * cannot carry arbitrary values). */
4064
+ r->req_cleared = true;
4065
+ return true;
4066
+ }
3323
4067
  return false;
3324
4068
  }
3325
4069
 
@@ -3392,9 +4136,311 @@ static bool scr_http_dynh_server_on(ScrNetServer *s, const char *event, const Sc
3392
4136
  return false;
3393
4137
  }
3394
4138
 
4139
+ /* ── ClientRequest (SCR_DYNH_HTTP_CLIENT) ────────────────────────────── */
4140
+
4141
+ /* The 'response' fire for a DYN listener (the reqres adapter's shape):
4142
+ * res arrives +1, boxes, and the checked-dynamic call runs. */
4143
+ static void scr_http_dynh_fire_resp(ScrClosure *cb, ScrHttpReq *res /* +1 */) {
4144
+ ScrDyn *fn = scr_dyn_listener_fn(cb);
4145
+ ScrDyn *dres = scr_dyn_new_handle(res, SCR_DYNH_HTTP_REQ);
4146
+ ScrDyn *args1[1] = { dres };
4147
+ ScrDyn *r = scr_dyn_call(fn, args1, 1, "listener");
4148
+ scr_dyn_release(r);
4149
+ scr_dyn_release(dres);
4150
+ scr_dyn_release(fn);
4151
+ scr_http_req_release(res);
4152
+ }
4153
+
4154
+ static ScrDyn *scr_http_dynh_client_invoke(void *h, ScrDyn *self, const char *method,
4155
+ ScrDyn *const *args, size_t argc, const char *what) {
4156
+ ScrHttpClientReq *c = (ScrHttpClientReq *)h;
4157
+ bool once = false;
4158
+ if (scr_http_dynh_reg(method, &once)) {
4159
+ const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
4160
+ const ScrDyn *cb = argc > 1 ? args[1] : scr_dyn_undefined();
4161
+ scr_dyn_check_listener(cb, "listener");
4162
+ if (scr_exc_pending()) return NULL;
4163
+ if (scr_http_dynh_name_is(name, "response")) {
4164
+ scr_http_client_on_response(c, scr_dyn_listener_closure_fn(cb, (void *)&scr_http_dynh_fire_resp),
4165
+ (ScrHttpRespFn)&scr_http_dynh_fire_resp, once);
4166
+ } else if (scr_http_dynh_name_is(name, "error")) {
4167
+ scr_http_client_on_error(c, scr_dyn_listener_closure_err(cb), (ScrChildErrFn)&scr_dyn_listener_fire_err, once);
4168
+ } else if (scr_http_dynh_name_is(name, "close")) {
4169
+ scr_http_client_on_close(c, scr_dyn_listener_closure0(cb), once);
4170
+ } else if (scr_http_dynh_name_is(name, "timeout")) {
4171
+ scr_http_client_on_timeout(c, scr_dyn_listener_closure0(cb), once);
4172
+ } else {
4173
+ scr_http_dynh_event_unsupported("ClientRequest", name);
4174
+ return NULL;
4175
+ }
4176
+ return scr_dyn_retain(self);
4177
+ }
4178
+ if (strcmp(method, "end") == 0) {
4179
+ const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
4180
+ if (chunk->kind == SCR_DYN_STR) scr_http_client_end_str(c, chunk->v.str);
4181
+ else if (chunk->kind == SCR_DYN_BYTES) scr_http_client_end_bytes(c, chunk->v.bytes);
4182
+ else if (chunk->kind == SCR_DYN_UNDEF || chunk->kind == SCR_DYN_NULL) scr_http_client_end(c);
4183
+ else {
4184
+ scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
4185
+ return NULL;
4186
+ }
4187
+ return scr_dyn_retain(self);
4188
+ }
4189
+ if (strcmp(method, "write") == 0) {
4190
+ const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
4191
+ if (!scr_http_dynh_chunk_ok(chunk)) return NULL;
4192
+ if (chunk->kind == SCR_DYN_STR) scr_http_client_write_str(c, chunk->v.str);
4193
+ else scr_http_client_write_bytes(c, chunk->v.bytes);
4194
+ return scr_dyn_new_bool(true);
4195
+ }
4196
+ if (strcmp(method, "destroy") == 0) {
4197
+ scr_http_client_destroy(c);
4198
+ return scr_dyn_retain(self);
4199
+ }
4200
+ if (strcmp(method, "setTimeout") == 0) {
4201
+ const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
4202
+ if (ms->kind != SCR_DYN_NUM) {
4203
+ scr_dyn_arg_type_fail("msecs", "of type number", ms);
4204
+ return NULL;
4205
+ }
4206
+ scr_http_client_set_timeout(c, ms->v.num);
4207
+ if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) {
4208
+ scr_http_client_on_timeout(c, scr_dyn_listener_closure0(args[1]), true);
4209
+ }
4210
+ return scr_dyn_retain(self);
4211
+ }
4212
+ if (strcmp(method, "flushHeaders") == 0) {
4213
+ if (!c->head_sent && !c->destroyed) scr_http_client_send_head(c, -1);
4214
+ return scr_dyn_retain(scr_dyn_undefined());
4215
+ }
4216
+ if (strcmp(method, "toString") == 0) {
4217
+ ScrStr *s = scr_str_new("[object Object]", 15);
4218
+ ScrDyn *d = scr_dyn_new_str(s);
4219
+ scr_str_release(s);
4220
+ return d;
4221
+ }
4222
+ {
4223
+ static const char *const known[] = { "abort", "setNoDelay", "setSocketKeepAlive",
4224
+ "getHeader", "setHeader", "removeHeader", "getHeaders", "getHeaderNames", "hasHeader",
4225
+ "cork", "uncork", "pipe", "off", "removeListener", "removeAllListeners", "emit",
4226
+ "prependListener", "prependOnceListener", "listenerCount", "listeners", NULL };
4227
+ if (scr_http_dynh_in(method, known)) {
4228
+ scr_http_dynh_unsupported("ClientRequest", method, NULL);
4229
+ return NULL;
4230
+ }
4231
+ }
4232
+ scr_http_dynh_not_fn(what);
4233
+ return NULL;
4234
+ }
4235
+
4236
+ static ScrDyn *scr_http_dynh_client_get(void *h, const char *key, size_t key_len) {
4237
+ ScrHttpClientReq *c = (ScrHttpClientReq *)h;
4238
+ (void)key_len;
4239
+ if (strcmp(key, "socket") == 0 || strcmp(key, "connection") == 0) {
4240
+ if (!c->sock) return scr_dyn_new_null();
4241
+ return scr_dyn_new_handle(c->sock, SCR_DYNH_NET_SOCKET);
4242
+ }
4243
+ if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_client_destroyed(c));
4244
+ if (strcmp(key, "writableEnded") == 0) return scr_dyn_new_bool(c->ended);
4245
+ if (strcmp(key, "path") == 0) {
4246
+ ScrDyn *d = scr_dyn_new_str(c->path);
4247
+ return d;
4248
+ }
4249
+ if (strcmp(key, "method") == 0) return scr_dyn_new_str(c->method);
4250
+ {
4251
+ static const char *const known[] = { "aborted", "host", "protocol", "res",
4252
+ "reusedSocket", "maxHeadersCount", "headersSent", "writableFinished", NULL };
4253
+ if (scr_http_dynh_in(key, known)) {
4254
+ scr_http_dynh_unsupported("ClientRequest", key, NULL);
4255
+ return NULL;
4256
+ }
4257
+ }
4258
+ return NULL;
4259
+ }
4260
+
4261
+ static bool scr_http_dynh_client_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
4262
+ (void)h; (void)key; (void)key_len; (void)value;
4263
+ return false;
4264
+ }
4265
+
4266
+ static const ScrDynHandleOps scr_http_dynh_client_ops = {
4267
+ "ClientRequest",
4268
+ &scr_http_client_retain_v,
4269
+ &scr_http_client_release_v,
4270
+ &scr_http_dynh_client_invoke,
4271
+ &scr_http_dynh_client_get,
4272
+ &scr_http_dynh_client_set,
4273
+ NULL,
4274
+ };
4275
+
4276
+ /* ── Agent (SCR_DYNH_HTTP_AGENT) ─────────────────────────────────────── */
4277
+
4278
+ /* A string-ish option field out of a dyn OBJ: STR answers its bytes,
4279
+ * NUM formats (the port), everything else reads as absent. */
4280
+ static ScrStr *scr_http_dynh_opt_str(const ScrDyn *opts, const char *key) {
4281
+ if (opts == NULL || opts->kind != SCR_DYN_OBJ) return NULL;
4282
+ const ScrDyn *v = scr_dyn_obj_get(opts, key, strlen(key));
4283
+ if (v == NULL) return NULL;
4284
+ if (v->kind == SCR_DYN_STR) return scr_str_retain(v->v.str);
4285
+ if (v->kind == SCR_DYN_NUM) return scr_f64_to_scrstr(v->v.num);
4286
+ return NULL;
4287
+ }
4288
+
4289
+ static ScrDyn *scr_http_dynh_agent_invoke(void *h, ScrDyn *self, const char *method,
4290
+ ScrDyn *const *args, size_t argc, const char *what) {
4291
+ ScrHttpAgent *a = (ScrHttpAgent *)h;
4292
+ bool once = false;
4293
+ if (scr_http_dynh_reg(method, &once)) {
4294
+ /* Agent events ('free'/'timeout') ride the pooling the runtime does
4295
+ * not have — a named error, never an inert listener. */
4296
+ const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
4297
+ scr_http_dynh_event_unsupported("Agent", name);
4298
+ return NULL;
4299
+ }
4300
+ if (strcmp(method, "getName") == 0) {
4301
+ const ScrDyn *opts = argc > 0 ? args[0] : scr_dyn_undefined();
4302
+ ScrStr *host = scr_http_dynh_opt_str(opts, "host");
4303
+ ScrStr *port = scr_http_dynh_opt_str(opts, "port");
4304
+ ScrStr *laddr = scr_http_dynh_opt_str(opts, "localAddress");
4305
+ ScrStr *spath = scr_http_dynh_opt_str(opts, "socketPath");
4306
+ int family = 0;
4307
+ if (opts != NULL && opts->kind == SCR_DYN_OBJ) {
4308
+ const ScrDyn *f = scr_dyn_obj_get(opts, "family", 6);
4309
+ if (f != NULL && f->kind == SCR_DYN_NUM && (f->v.num == 4 || f->v.num == 6)) {
4310
+ family = (int)f->v.num;
4311
+ }
4312
+ }
4313
+ ScrStr *name = scr_http_agent_name(host ? host->data : NULL, host ? host->len : 0,
4314
+ port ? port->data : NULL, port ? port->len : 0,
4315
+ laddr ? laddr->data : NULL, laddr ? laddr->len : 0,
4316
+ family, spath ? spath->data : NULL,
4317
+ spath ? spath->len : 0);
4318
+ scr_str_release(host);
4319
+ scr_str_release(port);
4320
+ scr_str_release(laddr);
4321
+ scr_str_release(spath);
4322
+ ScrDyn *d = scr_dyn_new_str(name);
4323
+ scr_str_release(name);
4324
+ return d;
4325
+ }
4326
+ if (strcmp(method, "destroy") == 0) {
4327
+ scr_http_agent_destroy(a);
4328
+ return scr_dyn_retain(scr_dyn_undefined());
4329
+ }
4330
+ if (strcmp(method, "toString") == 0) {
4331
+ ScrStr *s = scr_str_new("[object Object]", 15);
4332
+ ScrDyn *d = scr_dyn_new_str(s);
4333
+ scr_str_release(s);
4334
+ return d;
4335
+ }
4336
+ {
4337
+ static const char *const known[] = { "createConnection", "createSocket", "keepSocketAlive",
4338
+ "reuseSocket", "addRequest", "removeSocket", "off", "removeListener",
4339
+ "removeAllListeners", "emit", "prependListener", "prependOnceListener",
4340
+ "listenerCount", "listeners", NULL };
4341
+ if (scr_http_dynh_in(method, known)) {
4342
+ scr_http_dynh_unsupported("Agent", method, NULL);
4343
+ return NULL;
4344
+ }
4345
+ }
4346
+ scr_http_dynh_not_fn(what);
4347
+ (void)self;
4348
+ return NULL;
4349
+ }
4350
+
4351
+ /* The sockets/requests snapshots: name → array (fresh objects per read;
4352
+ * the tests read lengths, membership, and key presence — empty buckets
4353
+ * are OMITTED, Node deletes drained keys). */
4354
+ static ScrDyn *scr_http_dynh_agent_table(ScrHttpAgent *a, bool queued) {
4355
+ ScrDyn *obj = scr_dyn_new_obj();
4356
+ for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
4357
+ if (e->queued != queued) continue;
4358
+ ScrDyn *item = queued
4359
+ ? scr_dyn_new_handle(e->client, SCR_DYNH_HTTP_CLIENT)
4360
+ : e->client->sock ? scr_dyn_new_handle(e->client->sock, SCR_DYNH_NET_SOCKET) : NULL;
4361
+ if (item == NULL) continue;
4362
+ ScrDyn *arr = scr_dyn_obj_get(obj, e->name->data, e->name->len); /* borrowed */
4363
+ if (arr == NULL || arr->kind != SCR_DYN_ARR) {
4364
+ ScrDyn *fresh = scr_dyn_new_arr();
4365
+ scr_dyn_arr_push(fresh, item); /* moves */
4366
+ scr_dyn_obj_set(obj, e->name->data, e->name->len, fresh); /* moves */
4367
+ } else {
4368
+ scr_dyn_arr_push(arr, item); /* moves */
4369
+ }
4370
+ }
4371
+ return obj;
4372
+ }
4373
+
4374
+ static ScrDyn *scr_http_dynh_agent_get(void *h, const char *key, size_t key_len) {
4375
+ ScrHttpAgent *a = (ScrHttpAgent *)h;
4376
+ (void)key_len;
4377
+ if (strcmp(key, "maxSockets") == 0) return scr_dyn_new_num(a->max_sockets);
4378
+ if (strcmp(key, "maxFreeSockets") == 0) return scr_dyn_new_num(a->max_free);
4379
+ if (strcmp(key, "keepAlive") == 0) return scr_dyn_new_bool(a->keep_alive);
4380
+ if (strcmp(key, "keepAliveMsecs") == 0) return scr_dyn_new_num(a->ka_msecs);
4381
+ if (strcmp(key, "defaultPort") == 0) return scr_dyn_new_num(a->default_port);
4382
+ if (strcmp(key, "protocol") == 0) {
4383
+ ScrStr *s = scr_str_new(a->secure ? "https:" : "http:", a->secure ? 6 : 5);
4384
+ ScrDyn *d = scr_dyn_new_str(s);
4385
+ scr_str_release(s);
4386
+ return d;
4387
+ }
4388
+ if (strcmp(key, "sockets") == 0) return scr_http_dynh_agent_table(a, false);
4389
+ if (strcmp(key, "requests") == 0) return scr_http_dynh_agent_table(a, true);
4390
+ if (strcmp(key, "freeSockets") == 0) {
4391
+ /* Always empty: the runtime pools nothing (keepAlive fences). */
4392
+ return scr_dyn_new_obj();
4393
+ }
4394
+ if (strcmp(key, "totalSocketCount") == 0) {
4395
+ size_t n = 0;
4396
+ for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
4397
+ if (!e->queued) n++;
4398
+ }
4399
+ return scr_dyn_new_num((double)n);
4400
+ }
4401
+ {
4402
+ static const char *const known[] = { "options", "maxTotalSockets", "scheduling", NULL };
4403
+ if (scr_http_dynh_in(key, known)) {
4404
+ scr_http_dynh_unsupported("Agent", key, NULL);
4405
+ return NULL;
4406
+ }
4407
+ }
4408
+ return NULL;
4409
+ }
4410
+
4411
+ static bool scr_http_dynh_agent_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
4412
+ ScrHttpAgent *a = (ScrHttpAgent *)h;
4413
+ (void)key_len;
4414
+ if (strcmp(key, "defaultPort") == 0 || strcmp(key, "maxSockets") == 0 ||
4415
+ strcmp(key, "maxFreeSockets") == 0 || strcmp(key, "keepAliveMsecs") == 0) {
4416
+ if (value->kind != SCR_DYN_NUM) {
4417
+ scr_dyn_arg_type_fail(key, "of type number", value);
4418
+ return true; /* handled: the exception is pending */
4419
+ }
4420
+ if (strcmp(key, "defaultPort") == 0) a->default_port = value->v.num;
4421
+ else if (strcmp(key, "maxSockets") == 0) a->max_sockets = value->v.num;
4422
+ else if (strcmp(key, "maxFreeSockets") == 0) a->max_free = value->v.num;
4423
+ else a->ka_msecs = value->v.num;
4424
+ return true;
4425
+ }
4426
+ return false; /* createSocket/createConnection installs: the named fence */
4427
+ }
4428
+
4429
+ static const ScrDynHandleOps scr_http_dynh_agent_ops = {
4430
+ "Agent",
4431
+ &scr_http_agent_retain_v,
4432
+ &scr_http_agent_release_v,
4433
+ &scr_http_dynh_agent_invoke,
4434
+ &scr_http_dynh_agent_get,
4435
+ &scr_http_dynh_agent_set,
4436
+ NULL,
4437
+ };
4438
+
3395
4439
  void scr_http_dyn_install(void) {
3396
4440
  scr_dyn_handle_install(SCR_DYNH_HTTP_REQ, &scr_http_dynh_req_ops);
3397
4441
  scr_dyn_handle_install(SCR_DYNH_HTTP_RES, &scr_http_dynh_res_ops);
4442
+ scr_dyn_handle_install(SCR_DYNH_HTTP_CLIENT, &scr_http_dynh_client_ops);
4443
+ scr_dyn_handle_install(SCR_DYNH_HTTP_AGENT, &scr_http_dynh_agent_ops);
3398
4444
  scr_net_set_dynh_http_on(&scr_http_dynh_server_on);
3399
4445
  }
3400
4446