@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_http2.c CHANGED
@@ -2192,6 +2192,7 @@ static void scr_h2_compat_dispatch(ScrH2Session *s, ScrH2Stream *st, const ScrH2
2192
2192
  scr_http_h2_req_header(req, name, value);
2193
2193
  }
2194
2194
  ScrHttpRes *res = scr_http_h2_res_new(s->sock, st);
2195
+ scr_http_res_set_req(res, req); /* res.req — the compat pair's backref */
2195
2196
  st->compat_req = scr_http_req_retain(req);
2196
2197
  st->compat_res = scr_http_res_retain(res);
2197
2198
  /* the req owns the READ side: the stream's own 'end' bookkeeping is
@@ -2302,6 +2303,55 @@ static ScrH2SrvCtx *scr_h2_server_ctx(ScrNetServer *s) {
2302
2303
  return ctx;
2303
2304
  }
2304
2305
 
2306
+ /* createSecureServer(options, handler): the eager COMPAT handler is the
2307
+ * first 'request' listener over the ALPN=h2 server — the
2308
+ * create_server_req route, secured. */
2309
+ ScrNetServer *scr_http2_create_secure_server_req(const char *cert, size_t cert_len,
2310
+ const char *key, size_t key_len,
2311
+ ScrClosure *handler /*moves, nullable*/,
2312
+ ScrHttpReqFn fn) {
2313
+ ScrNetServer *s = scr_http2_create_secure_server(cert, cert_len, key, key_len);
2314
+ if (handler != NULL) {
2315
+ ScrH2SrvCtx *ctx = scr_h2_server_ctx(s);
2316
+ if (ctx != NULL) scr_net_ls_add(&ctx->request_ls, handler, (void *)fn, false);
2317
+ else scr_closure_release(handler);
2318
+ }
2319
+ return s;
2320
+ }
2321
+
2322
+ /* createSecureServer with a RUNTIME options record (the divergence-66
2323
+ * stance): allowHTTP1 picks the flavor — truthy is the https/HTTP-1.1
2324
+ * compatibility server, absent or falsy the ALPN=h2 server — cert/key
2325
+ * ride the shared TLS server walk (scr_tls.c: out-of-bounds members
2326
+ * throw the catchable runtime fence), and the h2 session-tuning keys
2327
+ * drop exactly like the literal walk ignores them. NULL + pending
2328
+ * exception when the walk fences. */
2329
+ ScrNetServer *scr_http2_create_secure_server_dyn(const ScrDyn *opts /*borrowed*/,
2330
+ ScrClosure *handler /*moves, nullable*/,
2331
+ ScrHttpReqFn fn) {
2332
+ bool allow_http1 = false;
2333
+ if (opts != NULL && opts->kind == SCR_DYN_OBJ) {
2334
+ for (size_t i = 0; i < opts->v.obj.len; i++) {
2335
+ if (strcmp(opts->v.obj.entries[i].key, "allowHTTP1") == 0) {
2336
+ allow_http1 = scr_dyn_truthy(opts->v.obj.entries[i].value);
2337
+ }
2338
+ }
2339
+ }
2340
+ ScrBytes *cert, *key;
2341
+ if (!scr_tls_srv_opts_walk(opts, "http2.createSecureServer", &cert, &key)) {
2342
+ scr_closure_release(handler);
2343
+ return NULL;
2344
+ }
2345
+ ScrNetServer *s = allow_http1
2346
+ ? scr_https_create_server((const char *)cert->data, cert->len,
2347
+ (const char *)key->data, key->len, handler, fn)
2348
+ : scr_http2_create_secure_server_req((const char *)cert->data, cert->len,
2349
+ (const char *)key->data, key->len, handler, fn);
2350
+ scr_bytes_release(cert);
2351
+ scr_bytes_release(key);
2352
+ return s;
2353
+ }
2354
+
2305
2355
  void scr_http2_server_on_stream(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrH2StreamFn fn,
2306
2356
  bool once) {
2307
2357
  ScrH2SrvCtx *ctx = scr_h2_server_ctx(s);
@@ -2390,7 +2440,12 @@ ScrH2Session *scr_http2_connect(ScrStr *url /*borrowed*/, bool reject_unauthoriz
2390
2440
  /* stash the authority in the session's hblock buf slot? No — its own
2391
2441
  * field: reuse rbuf? Neither: a dedicated malloc. */
2392
2442
  ScrStr *host_str = scr_str_new(host, hlen);
2393
- ScrNetSocket *sock = scr_net_connect(port, host_str, NULL);
2443
+ /* Resolve for the dial only (the http client's rule): the TLS wrap below
2444
+ * takes host_str, the NAME, because that is what SNI and verification
2445
+ * must see. */
2446
+ ScrStr *dial_addr = scr_net_blocking_lookup(host_str);
2447
+ ScrNetSocket *sock = scr_net_connect(port, dial_addr, NULL);
2448
+ scr_str_release(dial_addr);
2394
2449
  if (secure) scr_tls_h2_client_wrap(sock, host_str, reject_unauthorized, ca, ca_len);
2395
2450
  scr_str_release(host_str);
2396
2451
  s->sock = sock; /* scr_net_connect returned +1 — the session takes it */
package/src/scr_json.c CHANGED
@@ -1527,6 +1527,27 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
1527
1527
  * ignore silently — the loud choice, SEMANTICS.md). Receiver, key, and
1528
1528
  * value are all BORROWED (the member retains the value in). */
1529
1529
  static const char *scr_dyn_kind_name(const ScrDyn *d);
1530
+ /* `key in v` with a RUNTIME key (the compile-time dynHasKey fold, per
1531
+ * value): OBJ answers own-member presence, ARR answers 'length' or a
1532
+ * valid dense index, every other kind false (tsc admits `in` only on
1533
+ * object-typed operands). Borrows both; never throws. */
1534
+ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
1535
+ if (v->kind == SCR_DYN_OBJ) return scr_dyn_obj_get(v, key->data, key->len) != NULL;
1536
+ if (v->kind == SCR_DYN_ARR) {
1537
+ if (key->len == 6 && memcmp(key->data, "length", 6) == 0) return true;
1538
+ if (key->len == 0 || key->len > 15) return false;
1539
+ size_t idx = 0;
1540
+ for (size_t i = 0; i < key->len; i++) {
1541
+ char c = key->data[i];
1542
+ if (c < '0' || c > '9') return false;
1543
+ if (i > 0 && idx == 0) return false; /* a leading zero is no canonical index */
1544
+ idx = idx * 10 + (size_t)(c - '0');
1545
+ }
1546
+ return idx < v->v.arr.len;
1547
+ }
1548
+ return false;
1549
+ }
1550
+
1530
1551
  void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
1531
1552
  if (recv->kind == SCR_DYN_OBJ) {
1532
1553
  scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value));
package/src/scr_net.c CHANGED
@@ -464,12 +464,34 @@ struct ScrNetSocket {
464
464
  * dial_ips[dial_i]; the LAST failure's message surfaces (Node's
465
465
  * autoSelectFamily aggregate is the documented divergence) */
466
466
  bool lookup_wait;
467
+ /* a deferred dial (the http agent's maxSockets queue): registered and
468
+ * "connecting" (writes buffer), the actual dial starts on
469
+ * scr_net_sock_dial_start */
470
+ bool dial_deferred;
467
471
  ScrStr **dial_ips;
468
472
  size_t dial_n, dial_i;
469
473
  /* the in-flight dial chain's LAST failure — surfaced (moved into
470
474
  * pending_err) only when the list exhausts; a later success drops it */
471
475
  ScrStr *dial_err;
472
476
  ScrNetLs data_ls, end_ls, close_ls, err_ls, conn_ls, timeout_ls, readable_ls;
477
+ /* Flow control (pause()/resume()): user_paused holds reads OFF even
478
+ * with consumers (kernel/TCP backpressure is the buffer); flowing
479
+ * counts as a consumer even with no 'data' listener
480
+ * (resume()'s flow-and-discard drain, how Node reaches 'end' on an
481
+ * unconsumed stream). resume() clears user_paused and sets flowing. */
482
+ bool user_paused;
483
+ bool flowing;
484
+ /* destroySoon(): end() now, destroy once the FIN actually went out */
485
+ bool destroy_on_finish;
486
+ /* socket.bytesWritten: every byte accepted by the write paths (user
487
+ * writes, protocol heads, pipe deliveries — plaintext on TLS sockets) */
488
+ size_t bytes_written;
489
+ /* end(callback)/write(chunk, callback): 'finish'-shaped callbacks fire
490
+ * from the SWEEP (never the registering stack — Node defers them too).
491
+ * finish_ls waits for the FIN (wr_done); wcb_ls waits for the write
492
+ * buffer to drain (this surface's flush moment). */
493
+ ScrNetLs finish_ls, wcb_ls;
494
+ bool finish_pending; /* wr_done seen: fire finish_ls at the next sweep */
473
495
  /* a server whose 'connection' waits for this socket's handshake — the
474
496
  * TLS defer target (+1; the socket's own `server` backref stays the
475
497
  * ACCEPTING server, which may be the demux wrapper) */
@@ -564,6 +586,8 @@ void scr_net_sock_release(ScrNetSocket *s) {
564
586
  scr_net_ls_drop(&s->conn_ls);
565
587
  scr_net_ls_drop(&s->timeout_ls);
566
588
  scr_net_ls_drop(&s->readable_ls);
589
+ scr_net_ls_drop(&s->finish_ls);
590
+ scr_net_ls_drop(&s->wcb_ls);
567
591
  scr_str_release(s->pending_err);
568
592
  scr_str_release(s->remote_cache);
569
593
  for (size_t i = 0; i < s->dial_n; i++) scr_str_release(s->dial_ips[i]);
@@ -751,8 +775,9 @@ static ScrNetSocket *scr_net_sock_new(void) {
751
775
  /* Consumer-driven read arming (the stdin discipline: never read a byte
752
776
  * nobody consumes — the socket buffer is the backpressure). */
753
777
  static void scr_net_sock_update_read(ScrNetSocket *s) {
754
- bool want = s->fd >= 0 && !s->connecting && !s->rd_eof &&
778
+ bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused &&
755
779
  (s->data_ls.n > 0 || s->pipe_dst != NULL || s->native_data != NULL ||
780
+ s->flowing /* resume(): flow (and discard sans listeners) */ ||
756
781
  s->readable_ls.n > 0 /* paused-mode consumer: bytes buffer */ ||
757
782
  s->tops != NULL /* a transport is ALWAYS a consumer: the TLS
758
783
  * layer must process protocol frames (the
@@ -804,6 +829,13 @@ static void scr_net_sock_maybe_finish_write(ScrNetSocket *s) {
804
829
  if (s->tops && s->t_est) s->tops->shutdown_write(s->tctx);
805
830
  shutdown(s->fd, SHUT_WR);
806
831
  s->wr_done = true;
832
+ if (s->finish_ls.n > 0) s->finish_pending = true; /* end(cb): the sweep fires it */
833
+ if (s->destroy_on_finish) {
834
+ /* destroySoon(): the FIN is out — tear down now (Node's 'finish'-then-
835
+ * destroy). The close emits through the ordinary sweep. */
836
+ scr_net_sock_close_fd(s);
837
+ return;
838
+ }
807
839
  if (s->rd_eof) scr_net_sock_close_fd(s);
808
840
  else scr_net_sock_update_read(s); /* the drain-to-EOF mode arms here */
809
841
  }
@@ -833,8 +865,15 @@ static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len) {
833
865
  * dead or ended write half (Node fires 'error' there; this slice drops —
834
866
  * SEMANTICS.md documents the bound). */
835
867
  static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len) {
836
- if (s->fd < 0 || s->wr_ending || s->close_emitted || len == 0) return;
837
- if (s->connecting || s->wlen > s->whead || (s->tops && !s->t_est)) {
868
+ /* A socket whose dial hasn't STARTED yet (the agent's deferred dial,
869
+ * the caller-lookup wait) has no fd but is logically connecting —
870
+ * bytes buffer and flush at establishment like any mid-connect write. */
871
+ bool pre_dial = s->fd < 0 && (s->dial_deferred || s->lookup_wait) &&
872
+ !s->wr_ending && !s->close_emitted;
873
+ if (!pre_dial && (s->fd < 0 || s->wr_ending || s->close_emitted)) return;
874
+ if (len == 0) return;
875
+ s->bytes_written += len; /* accepted bytes (Node counts buffered ones too) */
876
+ if (pre_dial || s->connecting || s->wlen > s->whead || (s->tops && !s->t_est)) {
838
877
  /* mid-connect and mid-handshake bytes buffer; the flush after
839
878
  * 'connect'/establishment sends them */
840
879
  scr_net_sock_buffer(s, data, len);
@@ -959,8 +998,8 @@ static void scr_net_sock_transport_pump(ScrNetSocket *s);
959
998
  static void scr_net_sock_read(ScrNetSocket *s) {
960
999
  if (s->tops && !s->t_est) return; /* readiness feeds the handshake pump instead */
961
1000
  for (;;) {
962
- if (s->fd < 0) return;
963
- bool flowing = s->data_ls.n > 0 || s->pipe_dst || s->native_data;
1001
+ if (s->fd < 0 || s->user_paused) return;
1002
+ bool flowing = s->data_ls.n > 0 || s->pipe_dst || s->native_data || s->flowing;
964
1003
  /* buffered (peeked/unshifted) bytes first — they precede the
965
1004
  * kernel's in the stream; a TLS engine drains them through its bio */
966
1005
  if (!s->tops && flowing && s->rlen > s->rhead) {
@@ -1532,18 +1571,13 @@ ScrStr *scr_net_blocking_lookup(ScrStr *host /*borrowed*/) {
1532
1571
  return scr_str_new(ip, strlen(ip));
1533
1572
  }
1534
1573
 
1535
- ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/,
1536
- ScrClosure *cb /*moves, nullable*/) {
1537
- ScrNetSocket *s = scr_net_sock_new();
1538
- if (cb) scr_net_ls_add(&s->conn_ls, cb, NULL, true);
1539
- s->peer_port = (int)port;
1540
- const char *h = host && host->len > 0 ? host->data : "localhost";
1541
- if (strcmp(h, "localhost") == 0) h = "127.0.0.1";
1542
- snprintf(s->peer_ip, sizeof s->peer_ip, "%s", h);
1543
- if (!scr_net_poller_init()) {
1544
- fputs("scriptc: event poller init failed\n", stderr);
1545
- abort();
1546
- }
1574
+ /* The dial itself (peer_ip/peer_port already set): the sockaddr arms,
1575
+ * socket(), and the nonblocking connect(). Shared by the immediate dial
1576
+ * (scr_net_connect) and the DEFERRED one (the http agent's maxSockets
1577
+ * queue — the socket exists, buffers writes, and dials when a slot
1578
+ * frees). */
1579
+ static void scr_net_sock_dial_peer(ScrNetSocket *s) {
1580
+ const char *h = s->peer_ip;
1547
1581
  struct sockaddr_in a4;
1548
1582
  struct sockaddr_in6 a6;
1549
1583
  struct sockaddr *sa = NULL;
@@ -1569,8 +1603,8 @@ ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/,
1569
1603
  s->pending_err = scr_str_new(msg, strlen(msg));
1570
1604
  s->had_error = true;
1571
1605
  s->emit_close = true;
1572
- scr_net_sock_register(s);
1573
- return s;
1606
+ s->connecting = false;
1607
+ return;
1574
1608
  }
1575
1609
  int fd = socket(sa->sa_family, SOCK_STREAM, 0);
1576
1610
  if (fd < 0) {
@@ -1596,8 +1630,24 @@ ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/,
1596
1630
  s->peer_port);
1597
1631
  s->pending_err = scr_str_new(msg, strlen(msg));
1598
1632
  s->had_error = true;
1633
+ s->connecting = false;
1599
1634
  scr_net_sock_close_fd(s);
1600
1635
  }
1636
+ }
1637
+
1638
+ ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/,
1639
+ ScrClosure *cb /*moves, nullable*/) {
1640
+ ScrNetSocket *s = scr_net_sock_new();
1641
+ if (cb) scr_net_ls_add(&s->conn_ls, cb, NULL, true);
1642
+ s->peer_port = (int)port;
1643
+ const char *h = host && host->len > 0 ? host->data : "localhost";
1644
+ if (strcmp(h, "localhost") == 0) h = "127.0.0.1";
1645
+ snprintf(s->peer_ip, sizeof s->peer_ip, "%s", h);
1646
+ if (!scr_net_poller_init()) {
1647
+ fputs("scriptc: event poller init failed\n", stderr);
1648
+ abort();
1649
+ }
1650
+ scr_net_sock_dial_peer(s);
1601
1651
  scr_net_sock_register(s);
1602
1652
  return s;
1603
1653
  }
@@ -1713,6 +1763,31 @@ void scr_net_connect_opts_chk(const ScrDyn *opts, const ScrStr *fence) {
1713
1763
  scr_throw_lowering_fence(fence);
1714
1764
  }
1715
1765
 
1766
+ /* A dial the caller starts LATER (the http agent's maxSockets queue):
1767
+ * the socket exists, registers, and buffers writes as "connecting" —
1768
+ * scr_net_sock_dial_start runs the actual dial when a slot frees. */
1769
+ ScrNetSocket *scr_net_connect_deferred(double port, ScrStr *host /*borrowed, nullable*/) {
1770
+ ScrNetSocket *s = scr_net_sock_new();
1771
+ s->peer_port = (int)port;
1772
+ const char *h = host && host->len > 0 ? host->data : "localhost";
1773
+ if (strcmp(h, "localhost") == 0) h = "127.0.0.1";
1774
+ snprintf(s->peer_ip, sizeof s->peer_ip, "%s", h);
1775
+ if (!scr_net_poller_init()) {
1776
+ fputs("scriptc: event poller init failed\n", stderr);
1777
+ abort();
1778
+ }
1779
+ s->connecting = true; /* logically connecting while the dial waits */
1780
+ s->dial_deferred = true;
1781
+ scr_net_sock_register(s);
1782
+ return s;
1783
+ }
1784
+
1785
+ void scr_net_sock_dial_start(ScrNetSocket *s) {
1786
+ if (!s->dial_deferred || s->close_emitted || s->emit_close || s->fd >= 0) return;
1787
+ s->dial_deferred = false;
1788
+ scr_net_sock_dial_peer(s);
1789
+ }
1790
+
1716
1791
  /* ── the caller-lookup dial (net.connect with a lookup option) ─────────
1717
1792
  *
1718
1793
  * portless's createLoopbackConnection: connect({ host, port,
@@ -1891,6 +1966,78 @@ void scr_net_sock_end_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) {
1891
1966
  scr_net_sock_end(s);
1892
1967
  }
1893
1968
 
1969
+ /* socket.pause(): reads stay off — the kernel buffer (and TCP flow
1970
+ * control) holds arrived bytes. Delivery restarts on
1971
+ * resume(), always from the poller/sweep, never this stack. Answers the
1972
+ * socket (+1) — Node's chaining. */
1973
+ ScrNetSocket *scr_net_sock_pause(ScrNetSocket *s) {
1974
+ s->user_paused = true;
1975
+ scr_net_sock_update_read(s);
1976
+ return scr_net_sock_retain(s);
1977
+ }
1978
+
1979
+ /* socket.resume(): flowing mode — a consumer even with no 'data'
1980
+ * listener (arrived bytes discard, so 'end' can be reached, Node's
1981
+ * resumed-but-unconsumed stream). Buffered bytes deliver from the next
1982
+ * sweep (flags_pending sees the flowing consumer), not this stack.
1983
+ * Answers the socket (+1) — Node's chaining. */
1984
+ ScrNetSocket *scr_net_sock_resume(ScrNetSocket *s) {
1985
+ s->user_paused = false;
1986
+ s->flowing = true;
1987
+ scr_net_sock_update_read(s);
1988
+ return scr_net_sock_retain(s);
1989
+ }
1990
+
1991
+ /* socket.setNoDelay(enable): TCP_NODELAY on the live fd (client dials
1992
+ * already set it — Node's default there is off, a documented divergence
1993
+ * in the dial path; this call makes the state explicit either way).
1994
+ * Answers the socket (+1) — Node's chaining. */
1995
+ ScrNetSocket *scr_net_sock_set_nodelay(ScrNetSocket *s, bool enable) {
1996
+ if (s->fd >= 0) {
1997
+ int v = enable ? 1 : 0;
1998
+ setsockopt(s->fd, IPPROTO_TCP, TCP_NODELAY, &v, sizeof v);
1999
+ }
2000
+ return scr_net_sock_retain(s);
2001
+ }
2002
+
2003
+ /* socket.destroySoon(): end the write half now, destroy once the FIN is
2004
+ * actually out (buffered bytes flush first — Node's 'finish'-then-destroy). */
2005
+ void scr_net_sock_destroy_soon(ScrNetSocket *s) {
2006
+ if (s->fd < 0) return;
2007
+ if (s->wr_done) {
2008
+ scr_net_sock_close_fd(s);
2009
+ return;
2010
+ }
2011
+ s->destroy_on_finish = true;
2012
+ s->wr_ending = true;
2013
+ scr_net_sock_maybe_finish_write(s);
2014
+ }
2015
+
2016
+ /* end(callback): fires once the FIN went out ('finish'), from the sweep. */
2017
+ void scr_net_sock_on_finish(ScrNetSocket *s, ScrClosure *cb /*moves*/) {
2018
+ if (s->close_emitted) {
2019
+ scr_closure_release(cb);
2020
+ return;
2021
+ }
2022
+ scr_net_ls_add(&s->finish_ls, cb, NULL, true);
2023
+ if (s->wr_done) s->finish_pending = true; /* already finished: next sweep */
2024
+ }
2025
+
2026
+ /* write(chunk, callback): fires when the write buffer drains (this
2027
+ * surface's flush moment), from the sweep. */
2028
+ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) {
2029
+ if (s->close_emitted) {
2030
+ scr_closure_release(cb);
2031
+ return;
2032
+ }
2033
+ scr_net_ls_add(&s->wcb_ls, cb, NULL, true);
2034
+ }
2035
+
2036
+ double scr_net_sock_bytes_written(ScrNetSocket *s) { return (double)s->bytes_written; }
2037
+
2038
+ /* socket.readable: true until the read half is done (peer FIN / destroy). */
2039
+ bool scr_net_sock_readable(ScrNetSocket *s) { return s->fd >= 0 && !s->rd_eof; }
2040
+
1894
2041
  /* socket.setTimeout(ms): ms > 0 arms the idle timer NOW (Node starts the
1895
2042
  * clock at the call — connecting time counts), ms <= 0 disables. */
1896
2043
  void scr_net_sock_set_timeout(ScrNetSocket *s, double ms) {
@@ -2151,6 +2298,19 @@ void scr_net_data_thunk_dyn(ScrClosure *cb, ScrBytes *chunk) {
2151
2298
  ((void (*)(ScrClosure *, ScrDyn *))cb->fn)(cb, d);
2152
2299
  }
2153
2300
 
2301
+ /* The string-TYPED data listener ((chunk: string) => void — the
2302
+ * setEncoding('utf8') shape): the chunk decodes as UTF-8 (WHATWG
2303
+ * replacement, Buffer.toString's algorithm) into the string the
2304
+ * annotation promises. Without setEncoding Node would hand a Buffer —
2305
+ * a string-typed listener there mistypes its own input, and the decode
2306
+ * is the honest reading of the annotation. */
2307
+ void scr_net_data_thunk_str(ScrClosure *cb, ScrBytes *chunk) {
2308
+ ScrStr *enc = scr_str_new("utf8", 4);
2309
+ ScrStr *text = scr_bytes_to_str(chunk, enc);
2310
+ scr_str_release(enc);
2311
+ ((void (*)(ScrClosure *, ScrStr *))cb->fn)(cb, text);
2312
+ }
2313
+
2154
2314
  /* The connection-handler adapters: the socket arrives +1 from the firing
2155
2315
  * site; the zero-param shape releases it. */
2156
2316
  void scr_net_conn_thunk0(ScrClosure *cb, ScrNetSocket *sock) {
@@ -2351,10 +2511,13 @@ static bool scr_net_flags_pending(void) {
2351
2511
  if (scr_net_proto_pending()) return true;
2352
2512
  for (ScrNetSocket *s = scr_net_socks; s; s = s->next) {
2353
2513
  if (s->pending_err || (s->emit_close && !s->close_emitted)) return true;
2514
+ if (s->finish_pending && s->finish_ls.n > 0) return true;
2515
+ if (s->wcb_ls.n > 0 && (s->wlen == s->whead || s->fd < 0)) return true;
2354
2516
  /* bytes already out of the kernel with a consumer waiting — decrypted
2355
2517
  * plaintext inside a transport, or unshifted bytes in the receive
2356
2518
  * buffer: the poller can't re-signal them, the sweep must deliver */
2357
- bool consumers = s->data_ls.n > 0 || s->pipe_dst || s->native_data;
2519
+ bool consumers = (s->data_ls.n > 0 || s->pipe_dst || s->native_data || s->flowing) &&
2520
+ !s->user_paused;
2358
2521
  if (consumers && s->fd >= 0) {
2359
2522
  if (s->tops && s->t_est && s->tops->pending && s->tops->pending(s->tctx)) return true;
2360
2523
  if (!s->tops && s->rlen > s->rhead) return true;
@@ -2393,7 +2556,8 @@ static void scr_net_sweep(void) {
2393
2556
  while (sock) {
2394
2557
  ScrNetSocket *next = sock->next; /* may unregister below */
2395
2558
  scr_net_sock_retain(sock); /* callbacks may drop every other ref */
2396
- if (sock->fd >= 0 && (sock->data_ls.n > 0 || sock->pipe_dst || sock->native_data) &&
2559
+ if (sock->fd >= 0 && !sock->user_paused &&
2560
+ (sock->data_ls.n > 0 || sock->pipe_dst || sock->native_data || sock->flowing) &&
2397
2561
  ((sock->tops && sock->t_est && sock->tops->pending && sock->tops->pending(sock->tctx)) ||
2398
2562
  (!sock->tops && sock->rlen > sock->rhead))) {
2399
2563
  /* deliver bytes the kernel can't re-signal (see flags_pending) */
@@ -2403,6 +2567,26 @@ static void scr_net_sweep(void) {
2403
2567
  return;
2404
2568
  }
2405
2569
  }
2570
+ if (sock->wcb_ls.n > 0 && (sock->wlen == sock->whead || sock->fd < 0)) {
2571
+ /* write(chunk, cb): the buffer drained (this surface's flush
2572
+ * moment) — the callbacks fire off the sweep, never the writing
2573
+ * stack. A dead socket fires them too (the buffered bytes are
2574
+ * gone either way; Node errors them — documented divergence,
2575
+ * the no-backpressure stance). */
2576
+ scr_net_fire0_this(&sock->wcb_ls, sock, SCR_DYNH_NET_SOCKET);
2577
+ if (scr_exc_pending()) {
2578
+ scr_net_sock_release(sock);
2579
+ return;
2580
+ }
2581
+ }
2582
+ if (sock->finish_pending) {
2583
+ sock->finish_pending = false;
2584
+ scr_net_fire0_this(&sock->finish_ls, sock, SCR_DYNH_NET_SOCKET);
2585
+ if (scr_exc_pending()) {
2586
+ scr_net_sock_release(sock);
2587
+ return;
2588
+ }
2589
+ }
2406
2590
  if (sock->pending_err) {
2407
2591
  ScrStr *msg = sock->pending_err;
2408
2592
  sock->pending_err = NULL;
@@ -2454,6 +2638,8 @@ static void scr_net_sweep(void) {
2454
2638
  scr_net_ls_drop(&sock->conn_ls);
2455
2639
  scr_net_ls_drop(&sock->timeout_ls);
2456
2640
  scr_net_ls_drop(&sock->readable_ls);
2641
+ scr_net_ls_drop(&sock->finish_ls);
2642
+ scr_net_ls_drop(&sock->wcb_ls);
2457
2643
  if (sock->pipe_dst) {
2458
2644
  scr_net_sock_release(sock->pipe_dst);
2459
2645
  sock->pipe_dst = NULL;
@@ -2718,6 +2904,9 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho
2718
2904
  scr_net_sock_on_timeout(s, scr_dyn_listener_closure0(cb), once);
2719
2905
  } else if (scr_net_dynh_name_is(name, "readable")) {
2720
2906
  scr_net_sock_on_readable(s, scr_dyn_listener_closure0(cb), once);
2907
+ } else if (scr_net_dynh_name_is(name, "finish")) {
2908
+ /* fires once when the FIN goes out — once either way */
2909
+ scr_net_sock_on_finish(s, scr_dyn_listener_closure0(cb));
2721
2910
  } else if (scr_net_dynh_name_is(name, "drain")) {
2722
2911
  /* This surface never backpressures (write answers true) — an
2723
2912
  * accepted, never-fired registration is the consistent answer. */
@@ -2743,28 +2932,51 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho
2743
2932
  scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
2744
2933
  return NULL;
2745
2934
  }
2746
- if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) {
2747
- scr_net_dynh_unsupported("write", "write(chunk, callback) is not modeled");
2748
- return NULL;
2749
- }
2750
2935
  if (chunk->kind == SCR_DYN_STR) scr_net_sock_write_str(s, chunk->v.str);
2751
2936
  else scr_net_sock_write_bytes(s, chunk->v.bytes);
2937
+ if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) {
2938
+ /* write(chunk, cb): fires when the buffer drains (sweep-deferred) */
2939
+ scr_net_sock_on_write_flush(s, scr_dyn_listener_closure0(args[1]));
2940
+ }
2752
2941
  return scr_dyn_new_bool(true); /* backpressure is not modeled (SEMANTICS.md) */
2753
2942
  }
2754
2943
  if (strcmp(method, "end") == 0) {
2755
2944
  const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
2945
+ const ScrDyn *cb = NULL;
2946
+ if (chunk->kind == SCR_DYN_FUNC) { /* end(callback) */
2947
+ cb = chunk;
2948
+ chunk = scr_dyn_undefined();
2949
+ } else if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) {
2950
+ cb = args[1]; /* end(chunk, callback) */
2951
+ }
2952
+ if (cb != NULL) scr_net_sock_on_finish(s, scr_dyn_listener_closure0(cb));
2756
2953
  if (chunk->kind == SCR_DYN_STR) scr_net_sock_end_str(s, chunk->v.str);
2757
2954
  else if (chunk->kind == SCR_DYN_BYTES) scr_net_sock_end_bytes(s, chunk->v.bytes);
2758
2955
  else if (chunk->kind == SCR_DYN_UNDEF || argc == 0) scr_net_sock_end(s);
2759
- else if (chunk->kind == SCR_DYN_FUNC) {
2760
- scr_net_dynh_unsupported("end", "end(callback) is not modeled");
2761
- return NULL;
2762
- } else {
2956
+ else {
2763
2957
  scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
2764
2958
  return NULL;
2765
2959
  }
2766
2960
  return scr_dyn_retain(self);
2767
2961
  }
2962
+ if (strcmp(method, "pause") == 0) {
2963
+ scr_net_sock_release(scr_net_sock_pause(s)); /* the chaining +1; self answers */
2964
+ return scr_dyn_retain(self);
2965
+ }
2966
+ if (strcmp(method, "resume") == 0) {
2967
+ scr_net_sock_release(scr_net_sock_resume(s));
2968
+ return scr_dyn_retain(self);
2969
+ }
2970
+ if (strcmp(method, "setNoDelay") == 0) {
2971
+ /* setNoDelay([enable]) — missing/undefined means true, Node */
2972
+ bool enable = argc == 0 || args[0]->kind == SCR_DYN_UNDEF || scr_dyn_truthy(args[0]);
2973
+ scr_net_sock_release(scr_net_sock_set_nodelay(s, enable));
2974
+ return scr_dyn_retain(self);
2975
+ }
2976
+ if (strcmp(method, "destroySoon") == 0) {
2977
+ scr_net_sock_destroy_soon(s);
2978
+ return scr_dyn_retain(scr_dyn_undefined());
2979
+ }
2768
2980
  if (strcmp(method, "destroy") == 0) {
2769
2981
  if (argc > 0 && args[0]->kind != SCR_DYN_UNDEF) {
2770
2982
  scr_net_dynh_unsupported("destroy", "destroy(error) carries a payload this surface does not model");
@@ -2826,10 +3038,10 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho
2826
3038
  if (scr_net_dynh_tls->invoke(s, method, args, argc, &out)) return out;
2827
3039
  }
2828
3040
  {
2829
- static const char *const known[] = { "connect", "setNoDelay", "setKeepAlive",
2830
- "address", "pause", "resume", "ref", "unref", "cork", "uncork", "read", "unpipe",
3041
+ static const char *const known[] = { "connect", "setKeepAlive",
3042
+ "address", "ref", "unref", "cork", "uncork", "read", "unpipe",
2831
3043
  "off", "removeListener", "removeAllListeners", "emit", "prependListener",
2832
- "prependOnceListener", "listenerCount", "listeners", "resetAndDestroy", "destroySoon", NULL };
3044
+ "prependOnceListener", "listenerCount", "listeners", "resetAndDestroy", NULL };
2833
3045
  for (size_t i = 0; known[i]; i++) {
2834
3046
  if (strcmp(method, known[i]) == 0) {
2835
3047
  scr_net_dynh_unsupported(method, NULL);
@@ -2862,14 +3074,21 @@ static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) {
2862
3074
  * undefined — the static lane's false→undefined mapping. */
2863
3075
  return scr_net_sock_encrypted(s) ? scr_dyn_new_bool(true) : NULL;
2864
3076
  }
3077
+ if (strcmp(key, "bytesWritten") == 0) return scr_dyn_new_num(scr_net_sock_bytes_written(s));
3078
+ if (strcmp(key, "readable") == 0) return scr_dyn_new_bool(scr_net_sock_readable(s));
3079
+ if (strcmp(key, "writableHighWaterMark") == 0 || strcmp(key, "readableHighWaterMark") == 0) {
3080
+ /* Node's default stream highWaterMark — a constant here (backpressure
3081
+ * is not modeled; SEMANTICS.md) */
3082
+ return scr_dyn_new_num(16384);
3083
+ }
2865
3084
  if (scr_net_dynh_tls != NULL) {
2866
3085
  ScrDyn *out = NULL;
2867
3086
  if (scr_net_dynh_tls->get(s, key, &out)) return out;
2868
3087
  }
2869
3088
  {
2870
3089
  static const char *const known[] = { "remotePort", "remoteFamily", "localAddress",
2871
- "localPort", "localFamily", "bytesRead", "bytesWritten", "connecting", "pending",
2872
- "readyState", "bufferSize", "timeout", "readable", "closed", "errored", NULL };
3090
+ "localPort", "localFamily", "bytesRead", "connecting", "pending",
3091
+ "readyState", "bufferSize", "timeout", "closed", "errored", NULL };
2873
3092
  for (size_t i = 0; known[i]; i++) {
2874
3093
  if (strcmp(key, known[i]) == 0) {
2875
3094
  scr_net_dynh_unsupported(key, NULL);