@sproutboat/runtime 0.41.0 → 0.42.1

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.
@@ -0,0 +1,42 @@
1
+ import { expect, test } from "bun:test";
2
+ import { inlineWorkersImports } from "./wrap";
3
+ import { SOCKETS_DATE, validateHttpSyncSource } from "./source";
4
+
5
+ test("cloudflare:sockets imports become the prelude's connect", () => {
6
+ const out = inlineWorkersImports('import { connect } from "cloudflare:sockets";\nconst x = 1;');
7
+ expect(out).toContain("const { connect } = globalThis.__sbSockets;");
8
+ expect(inlineWorkersImports('import { connect as dial } from "cloudflare:sockets";')).toContain(
9
+ "const { connect: dial } = globalThis.__sbSockets;",
10
+ );
11
+ });
12
+
13
+ test("a lazy import of cloudflare:sockets (postgres/cf, pg-cloudflare) resolves to the same object", () => {
14
+ const out = inlineWorkersImports('const { connect } = await import("cloudflare:sockets");');
15
+ expect(out).toBe("const { connect } = await Promise.resolve(globalThis.__sbSockets);");
16
+ });
17
+
18
+ const handler = "export default { fetch() { return new Response(Buffer.from('a').toString() + process.nextTick); } };";
19
+
20
+ test("Buffer and process in a bundle are refused before the sockets date and allowed from it", () => {
21
+ expect(validateHttpSyncSource(handler, "2026-09-14").ok).toBe(false);
22
+ expect(validateHttpSyncSource(handler, SOCKETS_DATE)).toEqual({ ok: true });
23
+ });
24
+
25
+ test("a node: import specifier is still refused from the sockets date", () => {
26
+ const withNode = "export default { fetch() { return new Response('node:fs'.length + \"\"); } };".replace(
27
+ "'node:fs'",
28
+ '"node:fs"',
29
+ );
30
+ expect(validateHttpSyncSource(withNode, SOCKETS_DATE).ok).toBe(false);
31
+ });
32
+
33
+ test("cloudflare:sockets needs the sockets date", () => {
34
+ const app =
35
+ 'import { connect } from "cloudflare:sockets";\nexport default { fetch() { return new Response(String(typeof connect)); } };';
36
+ const before = validateHttpSyncSource(app, "2026-11-16");
37
+ expect(before.ok).toBe(false);
38
+ expect(before.ok ? [] : before.errors).toContain(
39
+ `cloudflare:sockets needs compatibility_date ${SOCKETS_DATE} or later`,
40
+ );
41
+ expect(validateHttpSyncSource(app, SOCKETS_DATE)).toEqual({ ok: true });
42
+ });
package/src/source.ts CHANGED
@@ -67,6 +67,14 @@ function usesNodeGlobalUnguarded(module: string): boolean {
67
67
  /** `new WebSocket(url)` connects out from this compatibility date (earlier, only WebSocketPair makes one). */
68
68
  export const OUTBOUND_WEBSOCKET_DATE = "2026-11-01";
69
69
 
70
+ /**
71
+ * From this date `cloudflare:sockets` connect() exists, and so does the bundled Buffer and process
72
+ * stub a database driver brings: the bundler inlines both, so `Buffer.from` and `process.nextTick`
73
+ * in the output are the bundle's own code, not a host API.
74
+ */
75
+ export const SOCKETS_DATE = "2026-11-17";
76
+ const driverAllowed = (message: string) => message === "Node, Bun, and Deno APIs are not supported";
77
+
70
78
  export function validateHttpSyncSource(module: string, compatibilityDate?: string): SourceValidation {
71
79
  // `cloudflare:workers` is the one import allowed past bundling: the compiler turns it
72
80
  // into globals the same way (see inlineWorkersImports).
@@ -85,8 +93,15 @@ export function validateHttpSyncSource(module: string, compatibilityDate?: strin
85
93
  "handler must default-export an object with fetch(request), scheduled() or queue(), or a WorkerEntrypoint class",
86
94
  );
87
95
  }
96
+ if (
97
+ /["']cloudflare:sockets["']/.test(module) &&
98
+ !(compatibilityDate !== undefined && compatibilityDate >= SOCKETS_DATE)
99
+ )
100
+ errors.push(`cloudflare:sockets needs compatibility_date ${SOCKETS_DATE} or later`);
88
101
  const guardedGlobals = compatibilityDate !== undefined && compatibilityDate >= GUARDED_GLOBALS_DATE;
102
+ const sockets = compatibilityDate !== undefined && compatibilityDate >= SOCKETS_DATE;
89
103
  for (const [pattern, message] of alwaysForbidden) {
104
+ if (sockets && driverAllowed(message) && !/['"`]node:/.test(source)) continue;
90
105
  if (pattern === NODE_GLOBAL_USE && guardedGlobals ? !usesNodeGlobalUnguarded(source) : !pattern.test(source))
91
106
  continue;
92
107
  errors.push(message);
@@ -786,10 +786,13 @@ const char* sb_async_reply_json(u32 id, size_t* len) {
786
786
  return sb_async_current->json;
787
787
  }
788
788
 
789
+ // Set by the TCP block below (a constructor there): fires the parked timers of connect() sockets.
790
+ static void (*sb_wsc_drain_hook)(void) = 0;
789
791
  // Host, on its loop: fires the callback for each reply frame, oldest first. Its
790
792
  // bytes go where __sbTakeBin reads them. A final frame also drops the placeholder
791
793
  // timer; an earlier frame of a streamed response leaves it for the next one.
792
794
  void sb_async_drain(void) {
795
+ if (sb_wsc_drain_hook) sb_wsc_drain_hook();
793
796
  pthread_mutex_lock(&sb_async_lock);
794
797
  sb_async_delivery* ready = sb_async_ready;
795
798
  sb_async_ready = 0;
@@ -816,6 +819,474 @@ void sb_async_drain(void) {
816
819
  free(d);
817
820
  }
818
821
  }
822
+ // TCP:BEGIN
823
+ // cloudflare:sockets connect() (compatibility date 2026-11-17) through the broker. One detached
824
+ // I/O thread per socket opens a second loopback connection to the broker, sends a tcp.open frame
825
+ // (token, host, port, mode, server name) and, once the broker has acked it, exchanges typed
826
+ // frames [u8 type][u32 LE length][payload] with it; the broker resolves and vets the address,
827
+ // connects and does the TLS (see wire/src/tcp.ts). The JS side speaks the same ops as the
828
+ // embedded transport (sb_wsc_hook; ids here have the top bit set): 'T' open, 'b' queue bytes
829
+ // (returns how many are still unsent), 'S' start TLS, 'H' shut the write side, 'p' 'i' take an
830
+ // event, 'k' acknowledge bytes dispatched, 'w' park a timer for the next event, 'r' release.
831
+ // Event kinds: 1 data, 3 error (data is the reason), 4 connected, 6 end of input, 8 send queue
832
+ // drained, 9 TLS handshake done. Threads touch only their own malloc'd state, never the JS heap;
833
+ // the loop is woken through the host's waker and sb_async_drain() fires the parked timers.
834
+ // Backpressure is TCP's: the thread stops reading the broker while the JS side is behind, and a
835
+ // broker that cannot write to the origin stops reading from the thread, so the thread's writes
836
+ // block in the kernel and 'b' reports a growing unsent count.
837
+ #define SB_TC_BIT 0x80000000u
838
+ #define SB_TC_MAX 16
839
+ #define SB_TC_WINDOW (256u << 10)
840
+ #define SB_TC_FRAME (64u << 10)
841
+ #define SB_TC_MAXFRAME (16u << 20)
842
+ typedef struct sb_tc_ev { struct sb_tc_ev* next; int kind; size_t len; char* data; } sb_tc_ev;
843
+ typedef struct sb_tc {
844
+ struct sb_tc* next;
845
+ unsigned id;
846
+ pthread_mutex_t lock;
847
+ int wake[2];
848
+ sb_tc_ev *evh, *evt;
849
+ unsigned waiter;
850
+ int wake_pending;
851
+ size_t unacked, unsent;
852
+ unsigned char* outq; size_t outq_len, outq_cap;
853
+ size_t starttls_at;
854
+ int released, finished, starttls_req, shutdown_req, want_drain;
855
+ int port, mode;
856
+ char *host;
857
+ char sni[256];
858
+ char token[256];
859
+ } sb_tc;
860
+ static pthread_mutex_t sb_tc_lock = PTHREAD_MUTEX_INITIALIZER;
861
+ static sb_tc* sb_tcs = 0;
862
+ static unsigned sb_tc_next = 0;
863
+
864
+ static void sb_tc_signal(void) { if (sb_async_waker) sb_async_waker(); }
865
+ static void sb_tc_push(sb_tc* c, int kind, const char* data, size_t len) {
866
+ sb_tc_ev* e = (sb_tc_ev*)calloc(1, sizeof(sb_tc_ev));
867
+ if (!e) return;
868
+ e->kind = kind; e->len = len;
869
+ if (len) {
870
+ e->data = (char*)malloc(len);
871
+ if (!e->data) { free(e); return; }
872
+ memcpy(e->data, data, len);
873
+ }
874
+ pthread_mutex_lock(&c->lock);
875
+ if (c->evt) c->evt->next = e; else c->evh = e;
876
+ c->evt = e;
877
+ if (kind == 1) c->unacked += len;
878
+ if (c->waiter) c->wake_pending = 1;
879
+ pthread_mutex_unlock(&c->lock);
880
+ sb_tc_signal();
881
+ }
882
+ static void sb_tc_wake_thread(sb_tc* c) {
883
+ char one = 1;
884
+ while (write(c->wake[1], &one, 1) < 0 && errno == EINTR) {}
885
+ }
886
+ static void sb_tc_free(sb_tc* c) {
887
+ while (c->evh) { sb_tc_ev* e = c->evh; c->evh = e->next; free(e->data); free(e); }
888
+ free(c->outq); free(c->host);
889
+ if (c->wake[0] >= 0) close(c->wake[0]);
890
+ if (c->wake[1] >= 0) close(c->wake[1]);
891
+ pthread_mutex_destroy(&c->lock);
892
+ free(c);
893
+ }
894
+ static void sb_tc_unlink(sb_tc* c) {
895
+ pthread_mutex_lock(&sb_tc_lock);
896
+ for (sb_tc** at = &sb_tcs; *at; at = &(*at)->next) if (*at == c) { *at = c->next; break; }
897
+ pthread_mutex_unlock(&sb_tc_lock);
898
+ }
899
+
900
+ // Appends one typed frame to the thread's write buffer.
901
+ static int sb_tc_put(unsigned char** wb, size_t* len, size_t* cap, int type, const unsigned char* data, size_t n) {
902
+ if (*len + 5 + n > *cap) {
903
+ size_t grown = *cap ? *cap : 4096;
904
+ while (grown < *len + 5 + n) grown *= 2;
905
+ unsigned char* more = (unsigned char*)realloc(*wb, grown);
906
+ if (!more) return -1;
907
+ *wb = more; *cap = grown;
908
+ }
909
+ unsigned char* at = *wb + *len;
910
+ at[0] = (unsigned char)type;
911
+ at[1] = (unsigned char)(n & 0xff); at[2] = (unsigned char)((n >> 8) & 0xff);
912
+ at[3] = (unsigned char)((n >> 16) & 0xff); at[4] = (unsigned char)((n >> 24) & 0xff);
913
+ if (n) memcpy(at + 5, data, n);
914
+ *len += 5 + n;
915
+ return 0;
916
+ }
917
+
918
+ // s as the inside of a JSON string.
919
+ static size_t sb_tc_json(char* out, const char* s) {
920
+ size_t o = 0;
921
+ for (; *s; s++) {
922
+ unsigned char ch = (unsigned char)*s;
923
+ if (ch == '"' || ch == '\\') { out[o++] = '\\'; out[o++] = (char)ch; }
924
+ else if (ch < 0x20) o += (size_t)sprintf(out + o, "\\u%04x", ch);
925
+ else out[o++] = (char)ch;
926
+ }
927
+ out[o] = 0;
928
+ return o;
929
+ }
930
+
931
+ // The opening frame: [u32 length][0x01][u32 json length][json]. Returns its length.
932
+ static unsigned char* sb_tc_open_frame(sb_tc* c, size_t* out_len) {
933
+ size_t cap = 512 + (strlen(c->host) + strlen(c->sni) + strlen(c->token)) * 6;
934
+ char* json = (char*)malloc(cap);
935
+ if (!json) return 0;
936
+ size_t o = (size_t)sprintf(json, "{\"v\":1,\"token\":\"");
937
+ o += sb_tc_json(json + o, c->token);
938
+ o += (size_t)sprintf(json + o, "\",\"op\":\"tcp.open\",\"host\":\"");
939
+ o += sb_tc_json(json + o, c->host);
940
+ o += (size_t)sprintf(json + o, "\",\"port\":%d,\"mode\":\"%s\",\"servername\":\"", c->port, c->mode == 1 ? "on" : c->mode == 2 ? "starttls" : "off");
941
+ o += sb_tc_json(json + o, c->sni[0] ? c->sni : c->host);
942
+ o += (size_t)sprintf(json + o, "\"}");
943
+ unsigned char* frame = (unsigned char*)malloc(9 + o);
944
+ if (!frame) { free(json); return 0; }
945
+ size_t payload = 5 + o;
946
+ frame[0] = (unsigned char)(payload & 0xff); frame[1] = (unsigned char)((payload >> 8) & 0xff);
947
+ frame[2] = (unsigned char)((payload >> 16) & 0xff); frame[3] = (unsigned char)((payload >> 24) & 0xff);
948
+ frame[4] = 1;
949
+ frame[5] = (unsigned char)(o & 0xff); frame[6] = (unsigned char)((o >> 8) & 0xff);
950
+ frame[7] = (unsigned char)((o >> 16) & 0xff); frame[8] = (unsigned char)((o >> 24) & 0xff);
951
+ memcpy(frame + 9, json, o);
952
+ free(json);
953
+ *out_len = 9 + o;
954
+ return frame;
955
+ }
956
+
957
+ static void* sb_tc_thread(void* arg) {
958
+ sb_tc* c = (sb_tc*)arg;
959
+ int fd = -1;
960
+ unsigned char *wb = 0, *rb = 0;
961
+ size_t wb_len = 0, wb_cap = 0, woff = 0, rb_len = 0, rb_cap = 0;
962
+ int acked = 0, connected = 0, eof = 0, shut = 0;
963
+ long long started = sb_now_ms();
964
+ char why[300] = "";
965
+ fd = sb_broker_open();
966
+ if (fd < 0) { snprintf(why, sizeof(why), "could not reach the broker (%d)", fd); sb_tc_push(c, 3, why, strlen(why)); goto finish; }
967
+ sb_io_nonblock(fd);
968
+ {
969
+ size_t flen = 0;
970
+ unsigned char* frame = sb_tc_open_frame(c, &flen);
971
+ if (!frame) { snprintf(why, sizeof(why), "out of memory"); sb_tc_push(c, 3, why, strlen(why)); goto finish; }
972
+ wb = frame; wb_len = flen; wb_cap = flen;
973
+ }
974
+ for (;;) {
975
+ pthread_mutex_lock(&c->lock);
976
+ int rel = c->released;
977
+ size_t window = c->unacked;
978
+ int st = c->starttls_req; c->starttls_req = 0;
979
+ size_t st_at = c->starttls_at;
980
+ int sh = c->shutdown_req;
981
+ char sni[256]; memcpy(sni, c->sni, sizeof(sni));
982
+ unsigned char* chunk = c->outq; size_t chunk_len = c->outq_len;
983
+ c->outq = 0; c->outq_len = c->outq_cap = 0;
984
+ c->unsent = (wb_len - woff) + chunk_len;
985
+ pthread_mutex_unlock(&c->lock);
986
+ // Frame what the JS side queued; START_TLS sits where it was asked for among the bytes.
987
+ {
988
+ size_t at = 0;
989
+ if (st && st_at > chunk_len) st_at = chunk_len;
990
+ while (at < chunk_len || st) {
991
+ if (st && at >= st_at) { sb_tc_put(&wb, &wb_len, &wb_cap, 2, (const unsigned char*)sni, strlen(sni)); st = 0; continue; }
992
+ size_t stop = st ? st_at : chunk_len;
993
+ size_t n = stop - at < SB_TC_FRAME ? stop - at : SB_TC_FRAME;
994
+ if (sb_tc_put(&wb, &wb_len, &wb_cap, 1, chunk + at, n) != 0) { free(chunk); snprintf(why, sizeof(why), "out of memory"); sb_tc_push(c, 3, why, strlen(why)); goto finish; }
995
+ at += n;
996
+ }
997
+ free(chunk);
998
+ }
999
+ if (sh && !shut) { shut = 1; sb_tc_put(&wb, &wb_len, &wb_cap, 3, 0, 0); }
1000
+ if (rel) {
1001
+ // close(): what was queued still goes out before the connection does
1002
+ sb_tc_put(&wb, &wb_len, &wb_cap, 7, 0, 0);
1003
+ long long give_up = sb_now_ms() + 5000;
1004
+ while (woff < wb_len && sb_now_ms() < give_up) {
1005
+ ssize_t n = write(fd, wb + woff, wb_len - woff);
1006
+ if (n > 0) { woff += (size_t)n; continue; }
1007
+ if (n < 0 && errno == EINTR) continue;
1008
+ if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { struct pollfd p; p.fd = fd; p.events = POLLOUT; p.revents = 0; poll(&p, 1, 200); continue; }
1009
+ break;
1010
+ }
1011
+ goto finish;
1012
+ }
1013
+ struct pollfd p[2];
1014
+ // Nothing to read or write: leave the socket out, or its HUP would spin the loop.
1015
+ short want = (short)(((!eof && window < SB_TC_WINDOW) ? POLLIN : 0) | (woff < wb_len ? POLLOUT : 0));
1016
+ p[0].fd = want ? fd : -1; p[0].events = want; p[0].revents = 0;
1017
+ p[1].fd = c->wake[0]; p[1].events = POLLIN; p[1].revents = 0;
1018
+ int rc;
1019
+ do { rc = poll(p, 2, connected ? -1 : 1000); } while (rc < 0 && errno == EINTR);
1020
+ if (rc < 0) { snprintf(why, sizeof(why), "poll failed"); sb_tc_push(c, 3, why, strlen(why)); goto finish; }
1021
+ if (!connected && !eof && sb_now_ms() - started > 40000) { snprintf(why, sizeof(why), "the broker did not answer"); sb_tc_push(c, 3, why, strlen(why)); goto finish; }
1022
+ if (p[1].revents & POLLIN) { char buf[64]; while (read(c->wake[0], buf, sizeof(buf)) > 0) {} }
1023
+ if (p[0].revents & POLLOUT) {
1024
+ ssize_t n = write(fd, wb + woff, wb_len - woff);
1025
+ if (n > 0) woff += (size_t)n;
1026
+ else if (n < 0 && errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) { if (!eof) { eof = 1; if (connected) sb_tc_push(c, 6, "", 0); else { snprintf(why, sizeof(why), "the broker closed the connection"); sb_tc_push(c, 3, why, strlen(why)); goto finish; } } wb_len = woff = 0; }
1027
+ if (woff == wb_len) {
1028
+ woff = wb_len = 0;
1029
+ if (wb_cap > (1u << 20)) { free(wb); wb = 0; wb_cap = 0; }
1030
+ }
1031
+ }
1032
+ if (p[0].revents & (POLLIN | POLLHUP | POLLERR)) {
1033
+ if (rb_len + 65536 > rb_cap) {
1034
+ size_t grown = rb_cap ? rb_cap : 131072;
1035
+ while (grown < rb_len + 65536) grown *= 2;
1036
+ unsigned char* more = (unsigned char*)realloc(rb, grown);
1037
+ if (!more) { snprintf(why, sizeof(why), "out of memory"); sb_tc_push(c, 3, why, strlen(why)); goto finish; }
1038
+ rb = more; rb_cap = grown;
1039
+ }
1040
+ if (!eof && window < SB_TC_WINDOW) {
1041
+ ssize_t n = read(fd, rb + rb_len, rb_cap - rb_len);
1042
+ if (n > 0) rb_len += (size_t)n;
1043
+ else if (n == 0 || (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK)) {
1044
+ // the broker is gone
1045
+ if (!connected) { snprintf(why, sizeof(why), "the broker closed the connection"); sb_tc_push(c, 3, why, strlen(why)); goto finish; }
1046
+ sb_tc_push(c, 6, "", 0);
1047
+ goto finish;
1048
+ }
1049
+ }
1050
+ }
1051
+ // Parse what has arrived.
1052
+ {
1053
+ size_t at = 0;
1054
+ for (;;) {
1055
+ if (!acked) {
1056
+ if (rb_len - at < 4) break;
1057
+ size_t len = (size_t)rb[at] | ((size_t)rb[at + 1] << 8) | ((size_t)rb[at + 2] << 16) | ((size_t)rb[at + 3] << 24);
1058
+ if (len > 65536) { snprintf(why, sizeof(why), "bad reply from the broker"); sb_tc_push(c, 3, why, strlen(why)); goto finish; }
1059
+ if (rb_len - at < 4 + len) break;
1060
+ char* json = (char*)malloc(len + 1);
1061
+ if (!json) goto finish;
1062
+ memcpy(json, rb + at + 4, len); json[len] = 0;
1063
+ at += 4 + len;
1064
+ if (strstr(json, "\"ok\":true")) acked = 1;
1065
+ else {
1066
+ const char* e = strstr(json, "\"error\":\"");
1067
+ size_t w = 0;
1068
+ if (e) { e += 9; while (*e && *e != '"' && w + 1 < sizeof(why)) { if (*e == '\\' && e[1]) e++; why[w++] = *e++; } }
1069
+ why[w] = 0;
1070
+ if (!w) snprintf(why, sizeof(why), "the broker refused the connection");
1071
+ free(json);
1072
+ sb_tc_push(c, 3, why, strlen(why));
1073
+ goto finish;
1074
+ }
1075
+ free(json);
1076
+ continue;
1077
+ }
1078
+ if (rb_len - at < 5) break;
1079
+ int type = rb[at];
1080
+ size_t len = (size_t)rb[at + 1] | ((size_t)rb[at + 2] << 8) | ((size_t)rb[at + 3] << 16) | ((size_t)rb[at + 4] << 24);
1081
+ if (len > SB_TC_MAXFRAME) { snprintf(why, sizeof(why), "bad frame from the broker"); sb_tc_push(c, 3, why, strlen(why)); goto finish; }
1082
+ if (rb_len - at < 5 + len) {
1083
+ if (5 + len > rb_cap - at) {
1084
+ // a frame bigger than the buffer: make room for all of it
1085
+ size_t need = at + 5 + len;
1086
+ size_t grown = rb_cap;
1087
+ while (grown < need) grown *= 2;
1088
+ unsigned char* more = (unsigned char*)realloc(rb, grown);
1089
+ if (!more) { snprintf(why, sizeof(why), "out of memory"); sb_tc_push(c, 3, why, strlen(why)); goto finish; }
1090
+ rb = more; rb_cap = grown;
1091
+ }
1092
+ break;
1093
+ }
1094
+ const unsigned char* payload = rb + at + 5;
1095
+ at += 5 + len;
1096
+ if (type == 1) sb_tc_push(c, 1, (const char*)payload, len);
1097
+ else if (type == 4) { sb_tc_push(c, 3, (const char*)payload, len); goto finish; }
1098
+ else if (type == 5) { connected = 1; sb_tc_push(c, 4, "", 0); }
1099
+ else if (type == 6) sb_tc_push(c, 9, "", 0);
1100
+ else if (type == 8) { if (!eof) { eof = 1; sb_tc_push(c, 6, "", 0); } }
1101
+ else if (type == 7) { if (!eof) { eof = 1; sb_tc_push(c, 6, "", 0); } goto finish; }
1102
+ }
1103
+ if (at > 0) {
1104
+ if (at < rb_len) memmove(rb, rb + at, rb_len - at);
1105
+ rb_len -= at;
1106
+ }
1107
+ }
1108
+ int drained = 0;
1109
+ pthread_mutex_lock(&c->lock);
1110
+ c->unsent = wb_len - woff;
1111
+ if (c->want_drain && c->outq_len + c->unsent == 0) { c->want_drain = 0; drained = 1; }
1112
+ pthread_mutex_unlock(&c->lock);
1113
+ if (drained) sb_tc_push(c, 8, "", 0);
1114
+ }
1115
+ finish:
1116
+ if (fd >= 0) close(fd);
1117
+ free(wb); free(rb);
1118
+ pthread_mutex_lock(&c->lock);
1119
+ c->finished = 1;
1120
+ int rel2 = c->released;
1121
+ int wanted = c->want_drain; c->want_drain = 0;
1122
+ pthread_mutex_unlock(&c->lock);
1123
+ if (wanted) sb_tc_push(c, 8, "", 0);
1124
+ if (rel2) {
1125
+ sb_tc_unlink(c);
1126
+ sb_tc_free(c);
1127
+ }
1128
+ return 0;
1129
+ }
1130
+
1131
+ static sb_tc* sb_tc_find(unsigned id) {
1132
+ pthread_mutex_lock(&sb_tc_lock);
1133
+ sb_tc* c = sb_tcs;
1134
+ while (c && c->id != id) c = c->next;
1135
+ pthread_mutex_unlock(&sb_tc_lock);
1136
+ return c;
1137
+ }
1138
+
1139
+ // Host: fires the timers parked for sockets that have events.
1140
+ static void sb_tc_drain(void) {
1141
+ for (;;) {
1142
+ unsigned timer = 0;
1143
+ pthread_mutex_lock(&sb_tc_lock);
1144
+ for (sb_tc* c = sb_tcs; c; c = c->next) {
1145
+ pthread_mutex_lock(&c->lock);
1146
+ if (c->waiter && (c->wake_pending || c->evh)) { timer = c->waiter; c->waiter = 0; c->wake_pending = 0; }
1147
+ pthread_mutex_unlock(&c->lock);
1148
+ if (timer) break;
1149
+ }
1150
+ pthread_mutex_unlock(&sb_tc_lock);
1151
+ if (!timer) return;
1152
+ porf_native_fetch_timer_clear(timer);
1153
+ porf_native_fetch_fire_timer(timer);
1154
+ }
1155
+ }
1156
+
1157
+ // The JS side's ops. data is borrowed for the call.
1158
+ static long long sb_tc_op(int op, unsigned id, const char* data, size_t len, size_t n, char* out, size_t cap) {
1159
+ if (op == 'T') {
1160
+ // data: host \n port \n mode (0 off, 1 on, 2 starttls) \n servername
1161
+ sb_tc* c = (sb_tc*)calloc(1, sizeof(sb_tc));
1162
+ if (!c) return 0;
1163
+ c->wake[0] = c->wake[1] = -1;
1164
+ char* copy = (char*)malloc(len + 1);
1165
+ if (!copy) { free(c); return 0; }
1166
+ memcpy(copy, data, len); copy[len] = 0;
1167
+ char* parts[4]; char* cur = copy;
1168
+ for (int i = 0; i < 4; i++) {
1169
+ parts[i] = cur;
1170
+ char* nl = strchr(cur, '\n');
1171
+ if (nl) { *nl = 0; cur = nl + 1; } else cur += strlen(cur);
1172
+ }
1173
+ c->host = strdup(parts[0]); c->port = atoi(parts[1]); c->mode = atoi(parts[2]);
1174
+ snprintf(c->sni, sizeof(c->sni), "%s", parts[3]);
1175
+ const char* tok = getenv("SB_BROKER_TOKEN");
1176
+ snprintf(c->token, sizeof(c->token), "%s", tok ? tok : "");
1177
+ c->starttls_at = 0;
1178
+ free(copy);
1179
+ if (!c->host || pipe(c->wake) != 0) { sb_tc_free(c); return 0; }
1180
+ for (int k = 0; k < 2; k++) { fcntl(c->wake[k], F_SETFD, FD_CLOEXEC); fcntl(c->wake[k], F_SETFL, fcntl(c->wake[k], F_GETFL) | O_NONBLOCK); }
1181
+ pthread_mutex_init(&c->lock, 0);
1182
+ pthread_mutex_lock(&sb_tc_lock);
1183
+ int live = 0;
1184
+ for (sb_tc* k = sb_tcs; k; k = k->next) live++;
1185
+ if (live >= SB_TC_MAX) { pthread_mutex_unlock(&sb_tc_lock); sb_tc_free(c); return 0; }
1186
+ sb_tc_next = (sb_tc_next + 1) & 0x7fffffffu;
1187
+ if (!sb_tc_next) sb_tc_next = 1;
1188
+ c->id = sb_tc_next | SB_TC_BIT;
1189
+ c->next = sb_tcs; sb_tcs = c;
1190
+ pthread_mutex_unlock(&sb_tc_lock);
1191
+ pthread_attr_t attr;
1192
+ pthread_attr_init(&attr);
1193
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
1194
+ pthread_attr_setstacksize(&attr, 256 * 1024);
1195
+ pthread_t thread;
1196
+ int failed = pthread_create(&thread, &attr, sb_tc_thread, c);
1197
+ pthread_attr_destroy(&attr);
1198
+ if (failed) { sb_tc_unlink(c); sb_tc_free(c); return 0; }
1199
+ return (long long)c->id;
1200
+ }
1201
+ // Outbound WebSockets are not carried on this transport.
1202
+ if (op == 'u') return 0;
1203
+ sb_tc* c = sb_tc_find(id);
1204
+ if (!c) return op == 'p' || op == 'k' || op == 'w' || op == 'r' ? 0 : -1;
1205
+ switch (op) {
1206
+ case 'b': {
1207
+ pthread_mutex_lock(&c->lock);
1208
+ if (c->finished) { pthread_mutex_unlock(&c->lock); return 0; }
1209
+ size_t need = c->outq_len + len;
1210
+ if (need > c->outq_cap) {
1211
+ size_t grown = c->outq_cap ? c->outq_cap : 4096;
1212
+ while (grown < need) grown *= 2;
1213
+ unsigned char* more = (unsigned char*)realloc(c->outq, grown);
1214
+ if (!more) { pthread_mutex_unlock(&c->lock); return -1; }
1215
+ c->outq = more; c->outq_cap = grown;
1216
+ }
1217
+ memcpy(c->outq + c->outq_len, data, len);
1218
+ c->outq_len += len;
1219
+ size_t queued = c->outq_len + c->unsent;
1220
+ if (queued > SB_TC_WINDOW) c->want_drain = 1;
1221
+ pthread_mutex_unlock(&c->lock);
1222
+ sb_tc_wake_thread(c);
1223
+ return (long long)queued;
1224
+ }
1225
+ case 'S':
1226
+ pthread_mutex_lock(&c->lock);
1227
+ snprintf(c->sni, sizeof(c->sni), "%.*s", (int)(len < sizeof(c->sni) - 1 ? len : sizeof(c->sni) - 1), data ? data : "");
1228
+ c->starttls_req = 1;
1229
+ c->starttls_at = c->outq_len;
1230
+ pthread_mutex_unlock(&c->lock);
1231
+ sb_tc_wake_thread(c);
1232
+ return 0;
1233
+ case 'H':
1234
+ pthread_mutex_lock(&c->lock);
1235
+ c->shutdown_req = 1;
1236
+ pthread_mutex_unlock(&c->lock);
1237
+ sb_tc_wake_thread(c);
1238
+ return 0;
1239
+ case 'p': {
1240
+ if (!out || !cap) return -1;
1241
+ out[0] = 0;
1242
+ pthread_mutex_lock(&c->lock);
1243
+ if (c->evh) snprintf(out, cap, "%d %zu %u", c->evh->kind, c->evh->len, 0u);
1244
+ pthread_mutex_unlock(&c->lock);
1245
+ return 0;
1246
+ }
1247
+ case 'i': {
1248
+ pthread_mutex_lock(&c->lock);
1249
+ sb_tc_ev* e = c->evh;
1250
+ if (!e) { pthread_mutex_unlock(&c->lock); return -1; }
1251
+ c->evh = e->next; if (!c->evh) c->evt = 0;
1252
+ pthread_mutex_unlock(&c->lock);
1253
+ size_t got = e->len < cap ? e->len : cap;
1254
+ if (got) memcpy(out, e->data, got);
1255
+ free(e->data); free(e);
1256
+ return (long long)got;
1257
+ }
1258
+ case 'k':
1259
+ pthread_mutex_lock(&c->lock);
1260
+ c->unacked -= n < c->unacked ? n : c->unacked;
1261
+ pthread_mutex_unlock(&c->lock);
1262
+ sb_tc_wake_thread(c);
1263
+ return 0;
1264
+ case 'w':
1265
+ pthread_mutex_lock(&c->lock);
1266
+ c->waiter = (unsigned)n;
1267
+ if (c->evh) c->wake_pending = 1;
1268
+ pthread_mutex_unlock(&c->lock);
1269
+ if (c->evh) sb_tc_signal();
1270
+ return 0;
1271
+ case 'r': {
1272
+ pthread_mutex_lock(&c->lock);
1273
+ c->released = 1;
1274
+ int done = c->finished;
1275
+ pthread_mutex_unlock(&c->lock);
1276
+ if (done) { sb_tc_unlink(c); sb_tc_free(c); }
1277
+ else sb_tc_wake_thread(c);
1278
+ return 0;
1279
+ }
1280
+ }
1281
+ return -1;
1282
+ }
1283
+ // 1 when something will run the loop when an event arrives (the host's waker).
1284
+ static int sb_tc_wakeable(void) { return sb_async_waker != 0; }
1285
+ extern long long (*sb_wsc_hook)(int op, unsigned int id, const char* data, size_t length, size_t n, char* out, size_t cap);
1286
+ extern int (*sb_wsc_wakeable_hook)(void);
1287
+ static void sb_tc_install(void) __attribute__((constructor));
1288
+ static void sb_tc_install(void) { sb_wsc_drain_hook = sb_tc_drain; sb_wsc_hook = sb_tc_op; sb_wsc_wakeable_hook = sb_tc_wakeable; }
1289
+ // TCP:END
819
1290
  `;
820
1291
 
821
1292
  // One request string in, one reply string out. `reqJson` is a parameter, so the