@scriptc/runtime 0.0.12 → 0.0.14

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) */
@@ -2679,8 +2911,23 @@ static ScrHttpClientReq *scr_http_request_impl(ScrStr *host /*borrowed*/, double
2679
2911
  if (cb) scr_net_ls_add(&c->resp_ls, cb, (void *)fn, true);
2680
2912
 
2681
2913
  /* dial — or take the caller's pre-made socket (createConnection); dial
2682
- * failures are the async 'error' either way, the net story */
2683
- c->sock = presock != NULL ? presock : scr_net_connect(port, host, NULL);
2914
+ * failures are the async 'error' either way, the net story.
2915
+ *
2916
+ * The hostname resolves HERE, at the client dial, the way the island's
2917
+ * client and the native fetch resolve at theirs: node:net's own connect
2918
+ * surface stays resolver-less on purpose (Node's async lookup semantics
2919
+ * are not this), so a client that dials by name has to ask. Only the
2920
+ * dial takes the address — c->host keeps the ORIGINAL name, which is
2921
+ * what the Host header carries and what the caller built the TLS
2922
+ * client's SNI from. An unresolvable name comes back unchanged and the
2923
+ * dial delivers Node's deferred ENOTFOUND. */
2924
+ if (presock != NULL) {
2925
+ c->sock = presock;
2926
+ } else {
2927
+ ScrStr *dial = scr_net_blocking_lookup(host);
2928
+ c->sock = scr_net_connect(port, dial, NULL);
2929
+ scr_str_release(dial);
2930
+ }
2684
2931
  ScrHttpConn *conn = calloc(1, sizeof *conn);
2685
2932
  if (!conn) scr_http_oom();
2686
2933
  conn->sock = c->sock; /* borrowed */
@@ -2744,6 +2991,300 @@ ScrHttpClientReq *scr_http_request(ScrStr *host /*borrowed*/, double port,
2744
2991
  fn, 80, NULL, NULL);
2745
2992
  }
2746
2993
 
2994
+ /* ── the http Agent ──────────────────────────────────────────────────────
2995
+ *
2996
+ * Options, getName, destroy, and REAL maxSockets accounting over the
2997
+ * one-dial-per-request connection model: an over-limit request's socket
2998
+ * defers its dial (scr_net_connect_deferred) and queues; a dying
2999
+ * active connection frees the slot and starts the next dial. What the
3000
+ * runtime cannot express stays a NAMED fence: keep-alive socket POOLING
3001
+ * (keepAlive: true) fences at construction — agent.freeSockets is
3002
+ * always empty; the runtime does not emulate a pool.
3003
+ *
3004
+ * Ownership: the agent registers (+1, atexit-swept); entries hold their
3005
+ * client +1 and the client holds the agent +1 — the cycle breaks when
3006
+ * the client's socket dies (scr_http_client_agent_detach) or at the
3007
+ * atexit sweep. */
3008
+
3009
+ typedef struct ScrHttpAgentEnt {
3010
+ ScrStr *name; /* getName's shape: "host:port:" */
3011
+ ScrHttpClientReq *client; /* +1 while listed */
3012
+ bool queued; /* waiting for a slot: the dial is deferred */
3013
+ struct ScrHttpAgentEnt *next;
3014
+ } ScrHttpAgentEnt;
3015
+
3016
+ typedef struct ScrHttpAgent {
3017
+ size_t rc;
3018
+ bool secure; /* https.Agent: protocol/defaultPort answers */
3019
+ bool keep_alive; /* always false here (true fences at construction) */
3020
+ double ka_msecs;
3021
+ double max_sockets; /* INFINITY = Node's default */
3022
+ double max_free;
3023
+ double timeout_ms; /* < 0 = unset */
3024
+ double default_port; /* settable (agent.defaultPort = p) */
3025
+ bool destroyed;
3026
+ ScrHttpAgentEnt *ents; /* append order — Node's FIFO queue */
3027
+ bool in_registry;
3028
+ struct ScrHttpAgent *next;
3029
+ } ScrHttpAgent;
3030
+
3031
+ static ScrHttpAgent *scr_http_agents = NULL; /* registry: +1 each, atexit-swept */
3032
+
3033
+ static ScrHttpAgent *scr_http_agent_retain(ScrHttpAgent *a) {
3034
+ a->rc++;
3035
+ return a;
3036
+ }
3037
+
3038
+ static void scr_http_agent_release(ScrHttpAgent *a) {
3039
+ if (!a || --a->rc > 0) return;
3040
+ ScrHttpAgentEnt *e = a->ents;
3041
+ while (e) {
3042
+ ScrHttpAgentEnt *next = e->next;
3043
+ scr_str_release(e->name);
3044
+ scr_http_client_release(e->client);
3045
+ free(e);
3046
+ e = next;
3047
+ }
3048
+ free(a);
3049
+ }
3050
+
3051
+ static struct ScrHttpAgent *scr_http_agent_release_p(struct ScrHttpAgent *ag) {
3052
+ scr_http_agent_release(ag);
3053
+ return NULL;
3054
+ }
3055
+
3056
+ static void *scr_http_agent_retain_v(void *p) { return scr_http_agent_retain((ScrHttpAgent *)p); }
3057
+ static void scr_http_agent_release_v(void *p) { scr_http_agent_release((ScrHttpAgent *)p); }
3058
+
3059
+ /* Actives (dialed, not settled) under a name. */
3060
+ static size_t scr_http_agent_active(const ScrHttpAgent *a, const ScrStr *name) {
3061
+ size_t n = 0;
3062
+ for (const ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
3063
+ if (!e->queued && e->name->len == name->len &&
3064
+ memcmp(e->name->data, name->data, name->len) == 0) n++;
3065
+ }
3066
+ return n;
3067
+ }
3068
+
3069
+ /* The settling/dying client leaves the lists; a freed slot starts the
3070
+ * first queued dial for the same name (Node's FIFO). */
3071
+ static void scr_http_agent_client_done(struct ScrHttpAgent *ag, struct ScrHttpClientReq *c) {
3072
+ ScrHttpAgentEnt **link = &ag->ents;
3073
+ ScrStr *name = NULL;
3074
+ while (*link && (*link)->client != c) link = &(*link)->next;
3075
+ if (*link) {
3076
+ ScrHttpAgentEnt *e = *link;
3077
+ *link = e->next;
3078
+ name = e->name; /* ownership moves out for the pump below */
3079
+ scr_http_client_release(e->client);
3080
+ free(e);
3081
+ }
3082
+ if (name != NULL && !ag->destroyed) {
3083
+ for (ScrHttpAgentEnt *e = ag->ents; e; e = e->next) {
3084
+ if (e->queued && e->name->len == name->len &&
3085
+ memcmp(e->name->data, name->data, name->len) == 0) {
3086
+ if (scr_http_agent_active(ag, name) < ag->max_sockets) {
3087
+ e->queued = false;
3088
+ if (e->client->sock) scr_net_sock_dial_start(e->client->sock);
3089
+ }
3090
+ break;
3091
+ }
3092
+ }
3093
+ }
3094
+ scr_str_release(name);
3095
+ }
3096
+
3097
+ /* getName's string builder — Node's exact shape:
3098
+ * host:port:localAddress[:family][:socketPath]. */
3099
+ static ScrStr *scr_http_agent_name(const char *host, size_t host_len, const char *port,
3100
+ size_t port_len, const char *laddr, size_t laddr_len,
3101
+ int family, const char *spath, size_t spath_len) {
3102
+ ScrJsonBuf b;
3103
+ scr_jb_init(&b);
3104
+ if (host_len == 0) { scr_jb_puts(&b, "localhost"); }
3105
+ else for (size_t i = 0; i < host_len; i++) scr_jb_putc(&b, host[i]);
3106
+ scr_jb_putc(&b, ':');
3107
+ for (size_t i = 0; i < port_len; i++) scr_jb_putc(&b, port[i]);
3108
+ scr_jb_putc(&b, ':');
3109
+ for (size_t i = 0; i < laddr_len; i++) scr_jb_putc(&b, laddr[i]);
3110
+ if (family == 4 || family == 6) {
3111
+ scr_jb_putc(&b, ':');
3112
+ scr_jb_putc(&b, family == 4 ? '4' : '6');
3113
+ }
3114
+ if (spath_len > 0) {
3115
+ scr_jb_putc(&b, ':');
3116
+ for (size_t i = 0; i < spath_len; i++) scr_jb_putc(&b, spath[i]);
3117
+ }
3118
+ return scr_jb_finish(&b);
3119
+ }
3120
+
3121
+ ScrDyn *scr_http_agent_new(bool secure, bool keep_alive, double ka_msecs,
3122
+ double max_sockets, double max_free, double timeout_ms,
3123
+ double port /* < 0 = unset */) {
3124
+ if (keep_alive) {
3125
+ static const char msg[] =
3126
+ "an http Agent with keepAlive: true (socket pooling and reuse — compiled clients "
3127
+ "dial one connection per request and close it with the response) is not supported "
3128
+ "yet — construct the Agent without keepAlive, or drop the agent option";
3129
+ scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
3130
+ return NULL;
3131
+ }
3132
+ scr_http_install();
3133
+ ScrHttpAgent *a = calloc(1, sizeof *a);
3134
+ if (!a) scr_http_oom();
3135
+ a->rc = 1;
3136
+ a->secure = secure;
3137
+ a->keep_alive = false;
3138
+ a->ka_msecs = ka_msecs >= 0 ? ka_msecs : 1000;
3139
+ a->max_sockets = max_sockets >= 0 ? max_sockets : (double)INFINITY;
3140
+ a->max_free = max_free >= 0 ? max_free : 256;
3141
+ a->timeout_ms = timeout_ms;
3142
+ /* an agent-options `port` merges under portless request options (Node
3143
+ * merges agent options into each connection) — the settable
3144
+ * defaultPort carries it */
3145
+ a->default_port = port >= 0 ? port : secure ? 443 : 80;
3146
+ /* registry (+1): the atexit sweep breaks agent↔client edges a program
3147
+ * legitimately leaves in flight */
3148
+ a->in_registry = true;
3149
+ a->next = scr_http_agents;
3150
+ scr_http_agents = scr_http_agent_retain(a);
3151
+ ScrDyn *d = scr_dyn_new_handle(a, SCR_DYNH_HTTP_AGENT);
3152
+ scr_http_agent_release(a); /* the handle's retain holds it */
3153
+ return d;
3154
+ }
3155
+
3156
+ /* agent.destroy(): tears down every listed connection (actives destroy
3157
+ * their sockets, queued dials never start) — Node destroys in-use
3158
+ * sockets too; there is no free pool here. */
3159
+ static void scr_http_agent_destroy(ScrHttpAgent *a) {
3160
+ a->destroyed = true;
3161
+ /* destroying sockets detaches entries re-entrantly — walk a snapshot */
3162
+ for (;;) {
3163
+ ScrHttpClientReq *victim = NULL;
3164
+ for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
3165
+ if (e->client->sock && !e->client->close_emitted) {
3166
+ victim = e->client;
3167
+ break;
3168
+ }
3169
+ }
3170
+ if (!victim) break;
3171
+ scr_http_client_agent_detach(victim); /* leaves the list first */
3172
+ scr_net_sock_destroy(victim->sock);
3173
+ }
3174
+ }
3175
+
3176
+ ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double port,
3177
+ ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
3178
+ double timeout_ms, ScrArr *header_pairs /*borrowed*/,
3179
+ bool auto_end, const ScrDyn *agent /*borrowed*/,
3180
+ ScrClosure *cb /*moves, nullable*/,
3181
+ ScrHttpRespFn fn, int default_port,
3182
+ void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx) {
3183
+ /* the null/undefined arms: the default path (port < 0 = no port option) */
3184
+ if (agent == NULL || agent->kind == SCR_DYN_UNDEF || agent->kind == SCR_DYN_NULL) {
3185
+ double p = port >= 0 ? port : (double)default_port;
3186
+ return scr_http_request_impl(host, p, path, method, timeout_ms, header_pairs, auto_end,
3187
+ cb, fn, default_port, wrap, wrap_ctx, NULL);
3188
+ }
3189
+ /* agent: false — Node's one-shot Agent: exactly this client's model,
3190
+ * plus its Connection: close request header (unless the caller set one) */
3191
+ if (agent->kind == SCR_DYN_BOOL && !agent->v.b) {
3192
+ double p = port >= 0 ? port : (double)default_port;
3193
+ bool has_conn = false;
3194
+ size_t npairs = (size_t)scr_arr_len(header_pairs);
3195
+ for (size_t i = 0; i + 1 < npairs && !has_conn; i += 2) {
3196
+ ScrStr *n = (ScrStr *)scr_arr_get_ref(header_pairs, (double)i);
3197
+ if (n->len == 10) {
3198
+ bool eq = true;
3199
+ for (size_t j = 0; j < 10 && eq; j++) {
3200
+ if (tolower((unsigned char)n->data[j]) != "connection"[j]) eq = false;
3201
+ }
3202
+ has_conn = eq;
3203
+ }
3204
+ scr_str_release(n);
3205
+ }
3206
+ ScrArr *pairs = header_pairs;
3207
+ ScrArr *copy = NULL;
3208
+ if (!has_conn) {
3209
+ copy = scr_arr_new(SCR_ELEM_STR, npairs + 2);
3210
+ for (size_t i = 0; i < npairs; i++) scr_arr_push_ref(copy, scr_arr_get_ref(header_pairs, (double)i));
3211
+ scr_arr_push_ref(copy, scr_str_new("Connection", 10));
3212
+ scr_arr_push_ref(copy, scr_str_new("close", 5));
3213
+ pairs = copy;
3214
+ }
3215
+ ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, pairs,
3216
+ auto_end, cb, fn, default_port, wrap, wrap_ctx, NULL);
3217
+ if (copy) scr_arr_release(copy);
3218
+ return c;
3219
+ }
3220
+ if (agent->kind != SCR_DYN_HANDLE || agent->v.handle.tag != SCR_DYNH_HTTP_AGENT) {
3221
+ if (cb) scr_closure_release(cb);
3222
+ scr_dyn_arg_type_fail("options.agent", "an instance of http.Agent, false, null, or undefined", agent);
3223
+ return NULL;
3224
+ }
3225
+ ScrHttpAgent *ag = (ScrHttpAgent *)agent->v.handle.ptr;
3226
+ double p = port >= 0 ? port
3227
+ : ag->default_port > 0 ? ag->default_port
3228
+ : (double)default_port;
3229
+ char portbuf[16];
3230
+ int portn = snprintf(portbuf, sizeof portbuf, "%d", (int)p);
3231
+ ScrStr *name = scr_http_agent_name(host->data, host->len, portbuf, (size_t)portn,
3232
+ NULL, 0, 0, NULL, 0);
3233
+ bool queue = scr_http_agent_active(ag, name) >= ag->max_sockets;
3234
+ ScrNetSocket *presock = queue ? scr_net_connect_deferred(p, host) : NULL;
3235
+ ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, header_pairs,
3236
+ auto_end, cb, fn, default_port, wrap, wrap_ctx,
3237
+ presock);
3238
+ if (c == NULL) { /* the method-token throw: nothing registered */
3239
+ scr_str_release(name);
3240
+ return NULL;
3241
+ }
3242
+ if (ag->timeout_ms >= 0 && timeout_ms <= 0) scr_net_sock_set_timeout(c->sock, ag->timeout_ms);
3243
+ c->agent = scr_http_agent_retain(ag);
3244
+ ScrHttpAgentEnt *e = calloc(1, sizeof *e);
3245
+ if (!e) scr_http_oom();
3246
+ e->name = name; /* ownership moves */
3247
+ e->client = scr_http_client_retain(c);
3248
+ e->queued = queue;
3249
+ ScrHttpAgentEnt **link = &ag->ents;
3250
+ while (*link) link = &(*link)->next;
3251
+ *link = e;
3252
+ return c;
3253
+ }
3254
+
3255
+ ScrHttpClientReq *scr_http_request_agent(ScrStr *host /*borrowed*/, double port,
3256
+ ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
3257
+ double timeout_ms, ScrArr *header_pairs /*borrowed*/,
3258
+ bool auto_end, const ScrDyn *agent /*borrowed*/,
3259
+ ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) {
3260
+ return scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, auto_end,
3261
+ agent, cb, fn, 80, NULL, NULL);
3262
+ }
3263
+
3264
+ /* The atexit sweep: break agent↔client edges a program leaves in flight
3265
+ * (queued dials that never ran) so the RC audit sees a clean heap. */
3266
+ static void scr_http_agents_cleanup(void) {
3267
+ while (scr_http_agents) {
3268
+ ScrHttpAgent *a = scr_http_agents;
3269
+ scr_http_agents = a->next;
3270
+ a->in_registry = false;
3271
+ ScrHttpAgentEnt *e = a->ents;
3272
+ a->ents = NULL;
3273
+ while (e) {
3274
+ ScrHttpAgentEnt *next = e->next;
3275
+ if (e->client->agent) {
3276
+ scr_http_agent_release(e->client->agent);
3277
+ e->client->agent = NULL;
3278
+ }
3279
+ scr_str_release(e->name);
3280
+ scr_http_client_release(e->client);
3281
+ free(e);
3282
+ e = next;
3283
+ }
3284
+ scr_http_agent_release(a);
3285
+ }
3286
+ }
3287
+
2747
3288
  /* Exit-time registry cleanup (the net-unit precedent): clients a program
2748
3289
  * legitimately leaves in flight at exit release their listeners and
2749
3290
  * registry references so the RC audit sees a clean heap. */
@@ -2854,12 +3395,43 @@ static bool scr_http_dynh_chunk_ok(const ScrDyn *chunk) {
2854
3395
  return false;
2855
3396
  }
2856
3397
 
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;
3398
+ /* An optional trailing encoding argument on write/end: utf8 spellings
3399
+ * pass through (string chunks already carry utf8 bytes); every other
3400
+ * real Node encoding decodes the STRING chunk through Buffer.from's
3401
+ * decoder into *out (+1; buffer chunks ignore the encoding, Node);
3402
+ * unknown names throw Node's ERR_UNKNOWN_ENCODING. False = exception
3403
+ * pending. */
3404
+ static bool scr_http_dynh_encode(const ScrDyn *chunk, const ScrDyn *enc, ScrBytes **out) {
3405
+ *out = NULL;
3406
+ if (enc->kind != SCR_DYN_STR || !scr_bytes_is_encoding(enc->v.str)) {
3407
+ ScrJsonBuf b;
3408
+ scr_jb_init(&b);
3409
+ scr_jb_puts(&b, "Unknown encoding: ");
3410
+ if (enc->kind == SCR_DYN_STR) {
3411
+ for (size_t i = 0; i < enc->v.str->len && i < 64; i++) scr_jb_putc(&b, enc->v.str->data[i]);
3412
+ }
3413
+ ScrStr *msg = scr_jb_finish(&b);
3414
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_UNKNOWN_ENCODING");
3415
+ scr_str_release(msg);
3416
+ return false;
3417
+ }
3418
+ if (chunk->kind != SCR_DYN_STR) return true; /* buffers carry their bytes */
3419
+ /* lowercase + alias normalization down to scr_bytes_from_str's arms */
3420
+ char low[10];
3421
+ size_t n = enc->v.str->len < 9 ? enc->v.str->len : 9;
3422
+ for (size_t i = 0; i < n; i++) {
3423
+ char c = enc->v.str->data[i];
3424
+ low[i] = c >= 'A' && c <= 'Z' ? (char)(c + 32) : c;
3425
+ }
3426
+ low[n] = 0;
3427
+ const char *canon = low;
3428
+ if (strcmp(low, "utf8") == 0 || strcmp(low, "utf-8") == 0) return true; /* passthrough */
3429
+ if (strcmp(low, "binary") == 0) canon = "latin1";
3430
+ else if (strcmp(low, "ucs2") == 0 || strcmp(low, "ucs-2") == 0 || strcmp(low, "utf-16le") == 0) canon = "utf16le";
3431
+ ScrStr *cs = scr_str_new(canon, strlen(canon));
3432
+ *out = scr_bytes_from_str(chunk->v.str, cs);
3433
+ scr_str_release(cs);
3434
+ return true;
2863
3435
  }
2864
3436
 
2865
3437
  /* ── IncomingMessage (SCR_DYNH_HTTP_REQ) ─────────────────────────────── */
@@ -2883,6 +3455,9 @@ static ScrDyn *scr_http_dynh_req_invoke(void *h, ScrDyn *self, const char *metho
2883
3455
  scr_http_req_on_close(r, scr_dyn_listener_closure0(cb), once);
2884
3456
  } else if (scr_http_dynh_name_is(name, "aborted")) {
2885
3457
  scr_http_req_on_aborted(r, scr_dyn_listener_closure0(cb), once);
3458
+ } else if (scr_http_dynh_name_is(name, "timeout") && r->sock != NULL) {
3459
+ /* the socket's idle timer — req.setTimeout's event, Node's delegation */
3460
+ scr_net_sock_on_timeout(r->sock, scr_dyn_listener_closure0(cb), once);
2886
3461
  } else {
2887
3462
  scr_http_dynh_event_unsupported("IncomingMessage", name);
2888
3463
  return NULL;
@@ -2893,6 +3468,25 @@ static ScrDyn *scr_http_dynh_req_invoke(void *h, ScrDyn *self, const char *metho
2893
3468
  scr_http_req_resume(r);
2894
3469
  return scr_dyn_retain(self);
2895
3470
  }
3471
+ if (strcmp(method, "pause") == 0) {
3472
+ scr_http_req_pause(r);
3473
+ return scr_dyn_retain(self);
3474
+ }
3475
+ if (strcmp(method, "setTimeout") == 0) {
3476
+ const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
3477
+ if (ms->kind != SCR_DYN_NUM) {
3478
+ scr_dyn_arg_type_fail("msecs", "of type number", ms);
3479
+ return NULL;
3480
+ }
3481
+ ScrClosure *cb = NULL;
3482
+ if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) cb = scr_dyn_listener_closure0(args[1]);
3483
+ else if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) {
3484
+ scr_dyn_check_listener(args[1], "callback");
3485
+ return NULL;
3486
+ }
3487
+ scr_http_req_set_timeout(r, ms->v.num, cb);
3488
+ return scr_dyn_retain(self);
3489
+ }
2896
3490
  if (strcmp(method, "setEncoding") == 0) {
2897
3491
  const ScrDyn *enc = argc > 0 ? args[0] : scr_dyn_undefined();
2898
3492
  if (enc->kind != SCR_DYN_STR) {
@@ -2932,7 +3526,7 @@ static ScrDyn *scr_http_dynh_req_invoke(void *h, ScrDyn *self, const char *metho
2932
3526
  }
2933
3527
  {
2934
3528
  /* Real prototype members without a modeled dispatch: loud. */
2935
- static const char *const known[] = { "pause", "unpipe", "setTimeout",
3529
+ static const char *const known[] = { "unpipe",
2936
3530
  "read", "push", "off", "removeListener", "removeAllListeners", "emit",
2937
3531
  "prependListener", "prependOnceListener", "listenerCount", "listeners",
2938
3532
  "isPaused", "wrap", "cork", "uncork", NULL };
@@ -3028,12 +3622,15 @@ static ScrDyn *scr_http_dynh_req_get(void *h, const char *key, size_t key_len) {
3028
3622
  }
3029
3623
  if (strcmp(key, "aborted") == 0) return scr_dyn_new_bool(r->aborted);
3030
3624
  if (strcmp(key, "complete") == 0) return scr_dyn_new_bool(r->ended);
3625
+ if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_req_destroyed_flag(r));
3626
+ if (strcmp(key, "readable") == 0) return scr_dyn_new_bool(scr_http_req_readable(r));
3627
+ if (strcmp(key, "readableEnded") == 0) return scr_dyn_new_bool(r->ended);
3628
+ if (strcmp(key, "closed") == 0) return scr_dyn_new_bool(r->close_emitted);
3031
3629
  {
3032
3630
  /* Real instance properties without a modeled read: loud, never a
3033
3631
  * silent undefined where Node has a value. */
3034
3632
  static const char *const known[] = {
3035
- "trailers", "rawTrailers", "readable", "readableEnded",
3036
- "closed", "destroyed", NULL };
3633
+ "trailers", "rawTrailers", NULL };
3037
3634
  if (scr_http_dynh_in(key, known)) {
3038
3635
  scr_http_dynh_unsupported("IncomingMessage", key, NULL);
3039
3636
  return NULL;
@@ -3088,6 +3685,55 @@ static ScrArr *scr_http_dynh_pairs(const ScrDyn *headers) {
3088
3685
  return pairs;
3089
3686
  }
3090
3687
 
3688
+ /* writeHead's RAW-array form over a dyn ARR: [k0, v0, k1, v1, ...]
3689
+ * flattens into the same pairs feed; an odd length throws Node's
3690
+ * ERR_INVALID_ARG_VALUE; value slots may be arrays (consecutive
3691
+ * same-name lines) or numbers (String(n)). NULL = exception pending. */
3692
+ static ScrArr *scr_http_dynh_flat_pairs(const ScrDyn *list) {
3693
+ size_t n = list->v.arr.len;
3694
+ if (n % 2 != 0) {
3695
+ static const char msg[] = "The argument 'headers' is invalid.";
3696
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_VALUE");
3697
+ return NULL;
3698
+ }
3699
+ ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, n);
3700
+ for (size_t i = 0; i + 1 < n; i += 2) {
3701
+ const ScrDyn *k = list->v.arr.items[i];
3702
+ const ScrDyn *v = list->v.arr.items[i + 1];
3703
+ if (k->kind != SCR_DYN_STR) {
3704
+ scr_arr_release(pairs);
3705
+ scr_dyn_arg_type_fail("name", "of type string", k);
3706
+ return NULL;
3707
+ }
3708
+ size_t reps = v->kind == SCR_DYN_ARR ? v->v.arr.len : 1;
3709
+ for (size_t rep = 0; rep < reps; rep++) {
3710
+ const ScrDyn *one = v->kind == SCR_DYN_ARR ? v->v.arr.items[rep] : v;
3711
+ ScrStr *vs;
3712
+ if (one->kind == SCR_DYN_STR) {
3713
+ vs = scr_str_retain(one->v.str);
3714
+ } else if (one->kind == SCR_DYN_NUM) {
3715
+ vs = scr_f64_to_scrstr(one->v.num);
3716
+ } else {
3717
+ ScrJsonBuf b;
3718
+ scr_jb_init(&b);
3719
+ scr_jb_puts(&b, "Invalid value \"");
3720
+ scr_jb_puts(&b, one->kind == SCR_DYN_UNDEF ? "undefined" : "object");
3721
+ scr_jb_puts(&b, "\" for header \"");
3722
+ for (size_t c = 0; c < k->v.str->len; c++) scr_jb_putc(&b, k->v.str->data[c]);
3723
+ scr_jb_putc(&b, '"');
3724
+ ScrStr *msg = scr_jb_finish(&b);
3725
+ scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_HTTP_INVALID_HEADER_VALUE");
3726
+ scr_str_release(msg);
3727
+ scr_arr_release(pairs);
3728
+ return NULL;
3729
+ }
3730
+ scr_arr_push_ref(pairs, scr_str_new(k->v.str->data, k->v.str->len));
3731
+ scr_arr_push_ref(pairs, vs);
3732
+ }
3733
+ }
3734
+ return pairs;
3735
+ }
3736
+
3091
3737
  static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *method,
3092
3738
  ScrDyn *const *args, size_t argc, const char *what) {
3093
3739
  ScrHttpRes *r = (ScrHttpRes *)h;
@@ -3105,6 +3751,9 @@ static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *metho
3105
3751
  /* This surface never backpressures (write answers true), so Node's
3106
3752
  * contract says 'drain' never fires — an accepted, never-fired
3107
3753
  * registration is the consistent answer (SEMANTICS.md). */
3754
+ } else if (scr_http_dynh_name_is(name, "timeout") && r->sock != NULL) {
3755
+ /* the socket's idle timer — res.setTimeout's event, Node's delegation */
3756
+ scr_net_sock_on_timeout(r->sock, scr_dyn_listener_closure0(cb), once);
3108
3757
  } else {
3109
3758
  scr_http_dynh_event_unsupported("ServerResponse", name);
3110
3759
  return NULL;
@@ -3117,41 +3766,117 @@ static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *metho
3117
3766
  size_t i = 0;
3118
3767
  const ScrDyn *chunk = NULL;
3119
3768
  const ScrDyn *cb = NULL;
3769
+ ScrBytes *decoded = NULL;
3120
3770
  if (i < argc && args[i]->kind == SCR_DYN_FUNC) {
3121
3771
  cb = args[i++];
3122
3772
  } else if (i < argc && args[i]->kind != SCR_DYN_UNDEF && args[i]->kind != SCR_DYN_NULL) {
3123
3773
  if (!scr_http_dynh_chunk_ok(args[i])) return NULL;
3124
3774
  chunk = args[i++];
3125
3775
  if (i < argc && args[i]->kind == SCR_DYN_STR) { /* encoding */
3126
- if (!scr_http_dynh_enc_ok(args[i], "ServerResponse", "end")) return NULL;
3776
+ if (!scr_http_dynh_encode(chunk, args[i], &decoded)) return NULL;
3127
3777
  i++;
3128
3778
  }
3129
3779
  if (i < argc && args[i]->kind == SCR_DYN_FUNC) cb = args[i++];
3130
3780
  }
3131
3781
  if (i < argc && args[i]->kind != SCR_DYN_UNDEF) {
3782
+ scr_bytes_release(decoded);
3132
3783
  scr_http_dynh_unsupported("ServerResponse", "end", "this argument shape is not modeled");
3133
3784
  return NULL;
3134
3785
  }
3135
3786
  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);
3787
+ if (decoded != NULL) {
3788
+ scr_http_res_end_bytes(r, decoded);
3789
+ scr_bytes_release(decoded);
3790
+ } else if (chunk == NULL) {
3791
+ scr_http_res_end(r);
3792
+ } else if (chunk->kind == SCR_DYN_STR) {
3793
+ scr_http_res_end_str(r, chunk->v.str);
3794
+ } else {
3795
+ scr_http_res_end_bytes(r, chunk->v.bytes);
3796
+ }
3139
3797
  return scr_dyn_retain(self);
3140
3798
  }
3141
3799
  if (strcmp(method, "write") == 0) {
3142
3800
  const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
3143
3801
  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;
3802
+ ScrBytes *decoded = NULL;
3803
+ size_t i = 1;
3804
+ if (i < argc && args[i]->kind == SCR_DYN_STR) { /* encoding */
3805
+ if (!scr_http_dynh_encode(chunk, args[i], &decoded)) return NULL;
3806
+ i++;
3807
+ }
3808
+ if (i < argc && args[i]->kind == SCR_DYN_FUNC) {
3809
+ /* write(chunk[, enc], cb): fires from the queue once the chunk
3810
+ * entered the socket buffer — this surface's flush moment */
3811
+ scr_http_res_on_write_flush(r, scr_dyn_listener_closure0(args[i]));
3812
+ i++;
3813
+ }
3814
+ if (decoded != NULL) {
3815
+ scr_http_res_write_bytes(r, decoded);
3816
+ scr_bytes_release(decoded);
3817
+ } else if (chunk->kind == SCR_DYN_STR) {
3818
+ scr_http_res_write_str(r, chunk->v.str);
3819
+ } else {
3820
+ scr_http_res_write_bytes(r, chunk->v.bytes);
3148
3821
  }
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
3822
  /* Always-true: this surface buffers without a highWaterMark verdict
3152
3823
  * (SEMANTICS.md — backpressure is not modeled). */
3153
3824
  return scr_dyn_new_bool(true);
3154
3825
  }
3826
+ if (strcmp(method, "flushHeaders") == 0) {
3827
+ scr_http_res_flush_headers(r);
3828
+ return scr_dyn_retain(scr_dyn_undefined());
3829
+ }
3830
+ if (strcmp(method, "cork") == 0) {
3831
+ scr_http_res_cork(r);
3832
+ return scr_dyn_retain(scr_dyn_undefined());
3833
+ }
3834
+ if (strcmp(method, "uncork") == 0) {
3835
+ scr_http_res_uncork(r);
3836
+ return scr_dyn_retain(scr_dyn_undefined());
3837
+ }
3838
+ if (strcmp(method, "setTimeout") == 0) {
3839
+ const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
3840
+ if (ms->kind != SCR_DYN_NUM) {
3841
+ scr_dyn_arg_type_fail("msecs", "of type number", ms);
3842
+ return NULL;
3843
+ }
3844
+ ScrClosure *tcb = NULL;
3845
+ if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) tcb = scr_dyn_listener_closure0(args[1]);
3846
+ else if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) {
3847
+ scr_dyn_check_listener(args[1], "callback");
3848
+ return NULL;
3849
+ }
3850
+ scr_http_res_set_timeout(r, ms->v.num, tcb);
3851
+ return scr_dyn_retain(self);
3852
+ }
3853
+ if (strcmp(method, "getHeaders") == 0) {
3854
+ /* the snapshot object — lowercased names; repeated names read as an
3855
+ * array in set order (Node's outgoing shape) */
3856
+ ScrDyn *obj = scr_dyn_new_obj();
3857
+ for (size_t k = 0; k < r->nheaders; k++) {
3858
+ const ScrStr *n = r->hnames[k];
3859
+ char lower[256];
3860
+ size_t nl = n->len < sizeof lower ? n->len : sizeof lower - 1;
3861
+ for (size_t c = 0; c < nl; c++) {
3862
+ char ch = n->data[c];
3863
+ lower[c] = ch >= 'A' && ch <= 'Z' ? (char)(ch + 32) : ch;
3864
+ }
3865
+ ScrDyn *prev = scr_dyn_obj_get(obj, lower, nl);
3866
+ ScrDyn *val = scr_dyn_new_str(r->hvalues[k]);
3867
+ if (prev == NULL) {
3868
+ scr_dyn_obj_set(obj, lower, nl, val); /* moves */
3869
+ } else if (prev->kind == SCR_DYN_ARR) {
3870
+ scr_dyn_arr_push(prev, val); /* moves */
3871
+ } else {
3872
+ ScrDyn *arr = scr_dyn_new_arr();
3873
+ scr_dyn_arr_push(arr, scr_dyn_retain(prev));
3874
+ scr_dyn_arr_push(arr, val);
3875
+ scr_dyn_obj_set(obj, lower, nl, arr); /* moves; releases prev */
3876
+ }
3877
+ }
3878
+ return obj;
3879
+ }
3155
3880
  if (strcmp(method, "writeHead") == 0) {
3156
3881
  const ScrDyn *st = argc > 0 ? args[0] : scr_dyn_undefined();
3157
3882
  if (st->kind != SCR_DYN_NUM) {
@@ -3169,8 +3894,21 @@ static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *metho
3169
3894
  scr_http_res_write_head_pairs(r, st->v.num, pairs);
3170
3895
  scr_arr_release(pairs);
3171
3896
  i++;
3897
+ } else if (i < argc && args[i]->kind == SCR_DYN_ARR) {
3898
+ /* the RAW-array form: [k0, v0, k1, v1, ...] — an even length is
3899
+ * Node's contract (ERR_INVALID_ARG_VALUE otherwise), values may be
3900
+ * arrays (consecutive same-name lines, the setHeader expansion),
3901
+ * and per NAME the list overrides what setHeader() stored while
3902
+ * other names survive — the object form's writeHead-wins merge
3903
+ * (oracle-pinned: a res with setHeader('a') plus writeHead(200,
3904
+ * ['test', ...]) sends both). */
3905
+ ScrArr *pairs = scr_http_dynh_flat_pairs(args[i]);
3906
+ if (!pairs) return NULL;
3907
+ scr_http_res_write_head_pairs(r, st->v.num, pairs);
3908
+ scr_arr_release(pairs);
3909
+ i++;
3172
3910
  } else if (i < argc && args[i]->kind != SCR_DYN_UNDEF) {
3173
- scr_http_dynh_unsupported("ServerResponse", "writeHead", "only the header-object form is modeled");
3911
+ scr_http_dynh_unsupported("ServerResponse", "writeHead", "only the header-object and raw-array forms are modeled");
3174
3912
  return NULL;
3175
3913
  } else {
3176
3914
  scr_http_res_write_head(r, st->v.num);
@@ -3250,8 +3988,8 @@ static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *metho
3250
3988
  return d;
3251
3989
  }
3252
3990
  {
3253
- static const char *const known[] = { "writeContinue", "writeEarlyHints", "cork", "uncork",
3254
- "flushHeaders", "getHeaders", "getHeaderNames", "appendHeader", "setTimeout",
3991
+ static const char *const known[] = { "writeContinue", "writeEarlyHints",
3992
+ "getHeaderNames", "appendHeader",
3255
3993
  "addTrailers", "off", "removeListener", "removeAllListeners", "emit",
3256
3994
  "prependListener", "prependOnceListener", "listenerCount", "listeners", "setDefaultEncoding", NULL };
3257
3995
  if (scr_http_dynh_in(method, known)) {
@@ -3283,10 +4021,23 @@ static ScrDyn *scr_http_dynh_res_get(void *h, const char *key, size_t key_len) {
3283
4021
  if (!r->sock) return scr_dyn_new_null(); /* destroyed: Node nulls it */
3284
4022
  return scr_dyn_new_handle(r->sock, SCR_DYNH_NET_SOCKET);
3285
4023
  }
4024
+ if (strcmp(key, "req") == 0) {
4025
+ if (r->req_cleared || r->req_ref == NULL) return scr_dyn_new_null();
4026
+ return scr_dyn_new_handle(r->req_ref, SCR_DYNH_HTTP_REQ);
4027
+ }
4028
+ if (strcmp(key, "writableCorked") == 0) return scr_dyn_new_num(scr_http_res_writable_corked(r));
4029
+ if (strcmp(key, "writableFinished") == 0) return scr_dyn_new_bool(r->finished);
4030
+ if (strcmp(key, "writableHighWaterMark") == 0) {
4031
+ /* Node's default socket highWaterMark — a constant here (backpressure
4032
+ * is not modeled; SEMANTICS.md) */
4033
+ return scr_dyn_new_num(16384);
4034
+ }
4035
+ if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_res_destroyed_flag(r));
4036
+ if (strcmp(key, "closed") == 0) return scr_dyn_new_bool(r->close_emitted);
3286
4037
  {
3287
- static const char *const known[] = { "req", "chunkedEncoding", "sendDate",
3288
- "strictContentLength", "writableFinished", "writableLength", "writableHighWaterMark",
3289
- "writableCorked", "writableObjectMode", "closed", "destroyed", "errored", NULL };
4038
+ static const char *const known[] = { "chunkedEncoding", "sendDate",
4039
+ "strictContentLength", "writableLength",
4040
+ "writableObjectMode", "errored", NULL };
3290
4041
  if (scr_http_dynh_in(key, known)) {
3291
4042
  scr_http_dynh_unsupported("ServerResponse", key, NULL);
3292
4043
  return NULL;
@@ -3320,6 +4071,14 @@ static bool scr_http_dynh_res_set(void *h, const char *key, size_t key_len, cons
3320
4071
  r->no_date = !scr_dyn_truthy(value);
3321
4072
  return true;
3322
4073
  }
4074
+ if (strcmp(key, "req") == 0 &&
4075
+ (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF)) {
4076
+ /* res.req is a plain property in Node — a null/undefined write
4077
+ * clears the read; other writes raise the named fence (the handle
4078
+ * cannot carry arbitrary values). */
4079
+ r->req_cleared = true;
4080
+ return true;
4081
+ }
3323
4082
  return false;
3324
4083
  }
3325
4084
 
@@ -3392,9 +4151,311 @@ static bool scr_http_dynh_server_on(ScrNetServer *s, const char *event, const Sc
3392
4151
  return false;
3393
4152
  }
3394
4153
 
4154
+ /* ── ClientRequest (SCR_DYNH_HTTP_CLIENT) ────────────────────────────── */
4155
+
4156
+ /* The 'response' fire for a DYN listener (the reqres adapter's shape):
4157
+ * res arrives +1, boxes, and the checked-dynamic call runs. */
4158
+ static void scr_http_dynh_fire_resp(ScrClosure *cb, ScrHttpReq *res /* +1 */) {
4159
+ ScrDyn *fn = scr_dyn_listener_fn(cb);
4160
+ ScrDyn *dres = scr_dyn_new_handle(res, SCR_DYNH_HTTP_REQ);
4161
+ ScrDyn *args1[1] = { dres };
4162
+ ScrDyn *r = scr_dyn_call(fn, args1, 1, "listener");
4163
+ scr_dyn_release(r);
4164
+ scr_dyn_release(dres);
4165
+ scr_dyn_release(fn);
4166
+ scr_http_req_release(res);
4167
+ }
4168
+
4169
+ static ScrDyn *scr_http_dynh_client_invoke(void *h, ScrDyn *self, const char *method,
4170
+ ScrDyn *const *args, size_t argc, const char *what) {
4171
+ ScrHttpClientReq *c = (ScrHttpClientReq *)h;
4172
+ bool once = false;
4173
+ if (scr_http_dynh_reg(method, &once)) {
4174
+ const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
4175
+ const ScrDyn *cb = argc > 1 ? args[1] : scr_dyn_undefined();
4176
+ scr_dyn_check_listener(cb, "listener");
4177
+ if (scr_exc_pending()) return NULL;
4178
+ if (scr_http_dynh_name_is(name, "response")) {
4179
+ scr_http_client_on_response(c, scr_dyn_listener_closure_fn(cb, (void *)&scr_http_dynh_fire_resp),
4180
+ (ScrHttpRespFn)&scr_http_dynh_fire_resp, once);
4181
+ } else if (scr_http_dynh_name_is(name, "error")) {
4182
+ scr_http_client_on_error(c, scr_dyn_listener_closure_err(cb), (ScrChildErrFn)&scr_dyn_listener_fire_err, once);
4183
+ } else if (scr_http_dynh_name_is(name, "close")) {
4184
+ scr_http_client_on_close(c, scr_dyn_listener_closure0(cb), once);
4185
+ } else if (scr_http_dynh_name_is(name, "timeout")) {
4186
+ scr_http_client_on_timeout(c, scr_dyn_listener_closure0(cb), once);
4187
+ } else {
4188
+ scr_http_dynh_event_unsupported("ClientRequest", name);
4189
+ return NULL;
4190
+ }
4191
+ return scr_dyn_retain(self);
4192
+ }
4193
+ if (strcmp(method, "end") == 0) {
4194
+ const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
4195
+ if (chunk->kind == SCR_DYN_STR) scr_http_client_end_str(c, chunk->v.str);
4196
+ else if (chunk->kind == SCR_DYN_BYTES) scr_http_client_end_bytes(c, chunk->v.bytes);
4197
+ else if (chunk->kind == SCR_DYN_UNDEF || chunk->kind == SCR_DYN_NULL) scr_http_client_end(c);
4198
+ else {
4199
+ scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
4200
+ return NULL;
4201
+ }
4202
+ return scr_dyn_retain(self);
4203
+ }
4204
+ if (strcmp(method, "write") == 0) {
4205
+ const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
4206
+ if (!scr_http_dynh_chunk_ok(chunk)) return NULL;
4207
+ if (chunk->kind == SCR_DYN_STR) scr_http_client_write_str(c, chunk->v.str);
4208
+ else scr_http_client_write_bytes(c, chunk->v.bytes);
4209
+ return scr_dyn_new_bool(true);
4210
+ }
4211
+ if (strcmp(method, "destroy") == 0) {
4212
+ scr_http_client_destroy(c);
4213
+ return scr_dyn_retain(self);
4214
+ }
4215
+ if (strcmp(method, "setTimeout") == 0) {
4216
+ const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
4217
+ if (ms->kind != SCR_DYN_NUM) {
4218
+ scr_dyn_arg_type_fail("msecs", "of type number", ms);
4219
+ return NULL;
4220
+ }
4221
+ scr_http_client_set_timeout(c, ms->v.num);
4222
+ if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) {
4223
+ scr_http_client_on_timeout(c, scr_dyn_listener_closure0(args[1]), true);
4224
+ }
4225
+ return scr_dyn_retain(self);
4226
+ }
4227
+ if (strcmp(method, "flushHeaders") == 0) {
4228
+ if (!c->head_sent && !c->destroyed) scr_http_client_send_head(c, -1);
4229
+ return scr_dyn_retain(scr_dyn_undefined());
4230
+ }
4231
+ if (strcmp(method, "toString") == 0) {
4232
+ ScrStr *s = scr_str_new("[object Object]", 15);
4233
+ ScrDyn *d = scr_dyn_new_str(s);
4234
+ scr_str_release(s);
4235
+ return d;
4236
+ }
4237
+ {
4238
+ static const char *const known[] = { "abort", "setNoDelay", "setSocketKeepAlive",
4239
+ "getHeader", "setHeader", "removeHeader", "getHeaders", "getHeaderNames", "hasHeader",
4240
+ "cork", "uncork", "pipe", "off", "removeListener", "removeAllListeners", "emit",
4241
+ "prependListener", "prependOnceListener", "listenerCount", "listeners", NULL };
4242
+ if (scr_http_dynh_in(method, known)) {
4243
+ scr_http_dynh_unsupported("ClientRequest", method, NULL);
4244
+ return NULL;
4245
+ }
4246
+ }
4247
+ scr_http_dynh_not_fn(what);
4248
+ return NULL;
4249
+ }
4250
+
4251
+ static ScrDyn *scr_http_dynh_client_get(void *h, const char *key, size_t key_len) {
4252
+ ScrHttpClientReq *c = (ScrHttpClientReq *)h;
4253
+ (void)key_len;
4254
+ if (strcmp(key, "socket") == 0 || strcmp(key, "connection") == 0) {
4255
+ if (!c->sock) return scr_dyn_new_null();
4256
+ return scr_dyn_new_handle(c->sock, SCR_DYNH_NET_SOCKET);
4257
+ }
4258
+ if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_client_destroyed(c));
4259
+ if (strcmp(key, "writableEnded") == 0) return scr_dyn_new_bool(c->ended);
4260
+ if (strcmp(key, "path") == 0) {
4261
+ ScrDyn *d = scr_dyn_new_str(c->path);
4262
+ return d;
4263
+ }
4264
+ if (strcmp(key, "method") == 0) return scr_dyn_new_str(c->method);
4265
+ {
4266
+ static const char *const known[] = { "aborted", "host", "protocol", "res",
4267
+ "reusedSocket", "maxHeadersCount", "headersSent", "writableFinished", NULL };
4268
+ if (scr_http_dynh_in(key, known)) {
4269
+ scr_http_dynh_unsupported("ClientRequest", key, NULL);
4270
+ return NULL;
4271
+ }
4272
+ }
4273
+ return NULL;
4274
+ }
4275
+
4276
+ static bool scr_http_dynh_client_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
4277
+ (void)h; (void)key; (void)key_len; (void)value;
4278
+ return false;
4279
+ }
4280
+
4281
+ static const ScrDynHandleOps scr_http_dynh_client_ops = {
4282
+ "ClientRequest",
4283
+ &scr_http_client_retain_v,
4284
+ &scr_http_client_release_v,
4285
+ &scr_http_dynh_client_invoke,
4286
+ &scr_http_dynh_client_get,
4287
+ &scr_http_dynh_client_set,
4288
+ NULL,
4289
+ };
4290
+
4291
+ /* ── Agent (SCR_DYNH_HTTP_AGENT) ─────────────────────────────────────── */
4292
+
4293
+ /* A string-ish option field out of a dyn OBJ: STR answers its bytes,
4294
+ * NUM formats (the port), everything else reads as absent. */
4295
+ static ScrStr *scr_http_dynh_opt_str(const ScrDyn *opts, const char *key) {
4296
+ if (opts == NULL || opts->kind != SCR_DYN_OBJ) return NULL;
4297
+ const ScrDyn *v = scr_dyn_obj_get(opts, key, strlen(key));
4298
+ if (v == NULL) return NULL;
4299
+ if (v->kind == SCR_DYN_STR) return scr_str_retain(v->v.str);
4300
+ if (v->kind == SCR_DYN_NUM) return scr_f64_to_scrstr(v->v.num);
4301
+ return NULL;
4302
+ }
4303
+
4304
+ static ScrDyn *scr_http_dynh_agent_invoke(void *h, ScrDyn *self, const char *method,
4305
+ ScrDyn *const *args, size_t argc, const char *what) {
4306
+ ScrHttpAgent *a = (ScrHttpAgent *)h;
4307
+ bool once = false;
4308
+ if (scr_http_dynh_reg(method, &once)) {
4309
+ /* Agent events ('free'/'timeout') ride the pooling the runtime does
4310
+ * not have — a named error, never an inert listener. */
4311
+ const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
4312
+ scr_http_dynh_event_unsupported("Agent", name);
4313
+ return NULL;
4314
+ }
4315
+ if (strcmp(method, "getName") == 0) {
4316
+ const ScrDyn *opts = argc > 0 ? args[0] : scr_dyn_undefined();
4317
+ ScrStr *host = scr_http_dynh_opt_str(opts, "host");
4318
+ ScrStr *port = scr_http_dynh_opt_str(opts, "port");
4319
+ ScrStr *laddr = scr_http_dynh_opt_str(opts, "localAddress");
4320
+ ScrStr *spath = scr_http_dynh_opt_str(opts, "socketPath");
4321
+ int family = 0;
4322
+ if (opts != NULL && opts->kind == SCR_DYN_OBJ) {
4323
+ const ScrDyn *f = scr_dyn_obj_get(opts, "family", 6);
4324
+ if (f != NULL && f->kind == SCR_DYN_NUM && (f->v.num == 4 || f->v.num == 6)) {
4325
+ family = (int)f->v.num;
4326
+ }
4327
+ }
4328
+ ScrStr *name = scr_http_agent_name(host ? host->data : NULL, host ? host->len : 0,
4329
+ port ? port->data : NULL, port ? port->len : 0,
4330
+ laddr ? laddr->data : NULL, laddr ? laddr->len : 0,
4331
+ family, spath ? spath->data : NULL,
4332
+ spath ? spath->len : 0);
4333
+ scr_str_release(host);
4334
+ scr_str_release(port);
4335
+ scr_str_release(laddr);
4336
+ scr_str_release(spath);
4337
+ ScrDyn *d = scr_dyn_new_str(name);
4338
+ scr_str_release(name);
4339
+ return d;
4340
+ }
4341
+ if (strcmp(method, "destroy") == 0) {
4342
+ scr_http_agent_destroy(a);
4343
+ return scr_dyn_retain(scr_dyn_undefined());
4344
+ }
4345
+ if (strcmp(method, "toString") == 0) {
4346
+ ScrStr *s = scr_str_new("[object Object]", 15);
4347
+ ScrDyn *d = scr_dyn_new_str(s);
4348
+ scr_str_release(s);
4349
+ return d;
4350
+ }
4351
+ {
4352
+ static const char *const known[] = { "createConnection", "createSocket", "keepSocketAlive",
4353
+ "reuseSocket", "addRequest", "removeSocket", "off", "removeListener",
4354
+ "removeAllListeners", "emit", "prependListener", "prependOnceListener",
4355
+ "listenerCount", "listeners", NULL };
4356
+ if (scr_http_dynh_in(method, known)) {
4357
+ scr_http_dynh_unsupported("Agent", method, NULL);
4358
+ return NULL;
4359
+ }
4360
+ }
4361
+ scr_http_dynh_not_fn(what);
4362
+ (void)self;
4363
+ return NULL;
4364
+ }
4365
+
4366
+ /* The sockets/requests snapshots: name → array (fresh objects per read;
4367
+ * the tests read lengths, membership, and key presence — empty buckets
4368
+ * are OMITTED, Node deletes drained keys). */
4369
+ static ScrDyn *scr_http_dynh_agent_table(ScrHttpAgent *a, bool queued) {
4370
+ ScrDyn *obj = scr_dyn_new_obj();
4371
+ for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
4372
+ if (e->queued != queued) continue;
4373
+ ScrDyn *item = queued
4374
+ ? scr_dyn_new_handle(e->client, SCR_DYNH_HTTP_CLIENT)
4375
+ : e->client->sock ? scr_dyn_new_handle(e->client->sock, SCR_DYNH_NET_SOCKET) : NULL;
4376
+ if (item == NULL) continue;
4377
+ ScrDyn *arr = scr_dyn_obj_get(obj, e->name->data, e->name->len); /* borrowed */
4378
+ if (arr == NULL || arr->kind != SCR_DYN_ARR) {
4379
+ ScrDyn *fresh = scr_dyn_new_arr();
4380
+ scr_dyn_arr_push(fresh, item); /* moves */
4381
+ scr_dyn_obj_set(obj, e->name->data, e->name->len, fresh); /* moves */
4382
+ } else {
4383
+ scr_dyn_arr_push(arr, item); /* moves */
4384
+ }
4385
+ }
4386
+ return obj;
4387
+ }
4388
+
4389
+ static ScrDyn *scr_http_dynh_agent_get(void *h, const char *key, size_t key_len) {
4390
+ ScrHttpAgent *a = (ScrHttpAgent *)h;
4391
+ (void)key_len;
4392
+ if (strcmp(key, "maxSockets") == 0) return scr_dyn_new_num(a->max_sockets);
4393
+ if (strcmp(key, "maxFreeSockets") == 0) return scr_dyn_new_num(a->max_free);
4394
+ if (strcmp(key, "keepAlive") == 0) return scr_dyn_new_bool(a->keep_alive);
4395
+ if (strcmp(key, "keepAliveMsecs") == 0) return scr_dyn_new_num(a->ka_msecs);
4396
+ if (strcmp(key, "defaultPort") == 0) return scr_dyn_new_num(a->default_port);
4397
+ if (strcmp(key, "protocol") == 0) {
4398
+ ScrStr *s = scr_str_new(a->secure ? "https:" : "http:", a->secure ? 6 : 5);
4399
+ ScrDyn *d = scr_dyn_new_str(s);
4400
+ scr_str_release(s);
4401
+ return d;
4402
+ }
4403
+ if (strcmp(key, "sockets") == 0) return scr_http_dynh_agent_table(a, false);
4404
+ if (strcmp(key, "requests") == 0) return scr_http_dynh_agent_table(a, true);
4405
+ if (strcmp(key, "freeSockets") == 0) {
4406
+ /* Always empty: the runtime pools nothing (keepAlive fences). */
4407
+ return scr_dyn_new_obj();
4408
+ }
4409
+ if (strcmp(key, "totalSocketCount") == 0) {
4410
+ size_t n = 0;
4411
+ for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
4412
+ if (!e->queued) n++;
4413
+ }
4414
+ return scr_dyn_new_num((double)n);
4415
+ }
4416
+ {
4417
+ static const char *const known[] = { "options", "maxTotalSockets", "scheduling", NULL };
4418
+ if (scr_http_dynh_in(key, known)) {
4419
+ scr_http_dynh_unsupported("Agent", key, NULL);
4420
+ return NULL;
4421
+ }
4422
+ }
4423
+ return NULL;
4424
+ }
4425
+
4426
+ static bool scr_http_dynh_agent_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
4427
+ ScrHttpAgent *a = (ScrHttpAgent *)h;
4428
+ (void)key_len;
4429
+ if (strcmp(key, "defaultPort") == 0 || strcmp(key, "maxSockets") == 0 ||
4430
+ strcmp(key, "maxFreeSockets") == 0 || strcmp(key, "keepAliveMsecs") == 0) {
4431
+ if (value->kind != SCR_DYN_NUM) {
4432
+ scr_dyn_arg_type_fail(key, "of type number", value);
4433
+ return true; /* handled: the exception is pending */
4434
+ }
4435
+ if (strcmp(key, "defaultPort") == 0) a->default_port = value->v.num;
4436
+ else if (strcmp(key, "maxSockets") == 0) a->max_sockets = value->v.num;
4437
+ else if (strcmp(key, "maxFreeSockets") == 0) a->max_free = value->v.num;
4438
+ else a->ka_msecs = value->v.num;
4439
+ return true;
4440
+ }
4441
+ return false; /* createSocket/createConnection installs: the named fence */
4442
+ }
4443
+
4444
+ static const ScrDynHandleOps scr_http_dynh_agent_ops = {
4445
+ "Agent",
4446
+ &scr_http_agent_retain_v,
4447
+ &scr_http_agent_release_v,
4448
+ &scr_http_dynh_agent_invoke,
4449
+ &scr_http_dynh_agent_get,
4450
+ &scr_http_dynh_agent_set,
4451
+ NULL,
4452
+ };
4453
+
3395
4454
  void scr_http_dyn_install(void) {
3396
4455
  scr_dyn_handle_install(SCR_DYNH_HTTP_REQ, &scr_http_dynh_req_ops);
3397
4456
  scr_dyn_handle_install(SCR_DYNH_HTTP_RES, &scr_http_dynh_res_ops);
4457
+ scr_dyn_handle_install(SCR_DYNH_HTTP_CLIENT, &scr_http_dynh_client_ops);
4458
+ scr_dyn_handle_install(SCR_DYNH_HTTP_AGENT, &scr_http_dynh_agent_ops);
3398
4459
  scr_net_set_dynh_http_on(&scr_http_dynh_server_on);
3399
4460
  }
3400
4461
 
@@ -3435,41 +4496,54 @@ void scr_http_client_end_dynv(ScrHttpClientReq *c, const ScrDyn *d) {
3435
4496
  * result +1 or NULL with the exception pending. */
3436
4497
  #include "scr_url_internal.h"
3437
4498
 
3438
- ScrHttpClientReq *scr_http_request_url(ScrStr *url /*borrowed*/, ScrStr *method /*borrowed*/,
3439
- bool auto_end, ScrClosure *cb /*moves, nullable*/,
3440
- ScrHttpRespFn fn) {
4499
+ bool scr_http_url_parts(ScrStr *url /*borrowed*/, bool secure, ScrStr **host_out /*+1*/,
4500
+ double *port_out, ScrStr **path_out /*+1*/) {
3441
4501
  ScrUrl *u = scr_url_new(url);
3442
- if (!u) {
3443
- if (cb) scr_closure_release(cb);
3444
- return NULL; /* Invalid URL pending */
3445
- }
3446
- if (!(u->scheme->len == 4 && memcmp(u->scheme->data, "http", 4) == 0)) {
4502
+ if (!u) return false; /* Invalid URL pending */
4503
+ /* The scheme is checked against the MODULE the call came from, not read
4504
+ * off the URL: http.get('https://…') is Node's ERR_INVALID_PROTOCOL, not
4505
+ * a silent upgrade. Hence `secure` as a parameter. */
4506
+ const char *want = secure ? "https" : "http";
4507
+ const size_t wantlen = secure ? 5 : 4;
4508
+ if (!(u->scheme->len == wantlen && memcmp(u->scheme->data, want, wantlen) == 0)) {
3447
4509
  char msg[128];
3448
- int n = snprintf(msg, sizeof msg, "Protocol \"%.*s:\" not supported. Expected \"http:\"",
3449
- (int)(u->scheme->len < 32 ? u->scheme->len : 32), u->scheme->data);
4510
+ int n = snprintf(msg, sizeof msg, "Protocol \"%.*s:\" not supported. Expected \"%s:\"",
4511
+ (int)(u->scheme->len < 32 ? u->scheme->len : 32), u->scheme->data, want);
3450
4512
  scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_INVALID_PROTOCOL");
3451
4513
  scr_url_release(u);
3452
- if (cb) scr_closure_release(cb);
3453
- return NULL;
4514
+ return false;
3454
4515
  }
3455
- double port = 80;
4516
+ double port = secure ? 443 : 80;
3456
4517
  if (u->port->len > 0) {
3457
4518
  port = 0;
3458
4519
  for (size_t i = 0; i < u->port->len; i++) port = port * 10 + (u->port->data[i] - '0');
3459
4520
  }
3460
4521
  /* IPv6 authority arrives bracketed ("[::1]") — the dial wants the bare
3461
4522
  * address, like Node's client strips them. */
3462
- ScrStr *host;
3463
4523
  if (u->host->len >= 2 && u->host->data[0] == '[' && u->host->data[u->host->len - 1] == ']') {
3464
- host = scr_str_new(u->host->data + 1, u->host->len - 2);
4524
+ *host_out = scr_str_new(u->host->data + 1, u->host->len - 2);
3465
4525
  } else {
3466
- host = scr_str_retain(u->host);
4526
+ *host_out = scr_str_retain(u->host);
3467
4527
  }
3468
- ScrStr *path;
3469
4528
  if (u->query->len > 0) {
3470
- path = scr_str_concat(u->path, u->query);
4529
+ *path_out = scr_str_concat(u->path, u->query);
3471
4530
  } else {
3472
- path = u->path->len > 0 ? scr_str_retain(u->path) : scr_str_new("/", 1);
4531
+ *path_out = u->path->len > 0 ? scr_str_retain(u->path) : scr_str_new("/", 1);
4532
+ }
4533
+ *port_out = port;
4534
+ scr_url_release(u);
4535
+ return true;
4536
+ }
4537
+
4538
+ ScrHttpClientReq *scr_http_request_url(ScrStr *url /*borrowed*/, ScrStr *method /*borrowed*/,
4539
+ bool auto_end, ScrClosure *cb /*moves, nullable*/,
4540
+ ScrHttpRespFn fn) {
4541
+ ScrStr *host;
4542
+ ScrStr *path;
4543
+ double port;
4544
+ if (!scr_http_url_parts(url, false, &host, &port, &path)) {
4545
+ if (cb) scr_closure_release(cb);
4546
+ return NULL; /* Invalid URL / ERR_INVALID_PROTOCOL pending */
3473
4547
  }
3474
4548
  ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, 0); /* request_impl reads the pairs array */
3475
4549
  ScrHttpClientReq *c = scr_http_request_ex(host, port, path, method, 0, pairs, auto_end,
@@ -3477,6 +4551,5 @@ ScrHttpClientReq *scr_http_request_url(ScrStr *url /*borrowed*/, ScrStr *method
3477
4551
  scr_arr_release(pairs);
3478
4552
  scr_str_release(host);
3479
4553
  scr_str_release(path);
3480
- scr_url_release(u);
3481
4554
  return c;
3482
4555
  }