@sproutboat/runtime 0.41.0 → 0.42.0

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.
@@ -3302,6 +3302,10 @@ typedef struct sb_wsc {
3302
3302
  uint64_t rng;
3303
3303
  char *host, *path, *headers, *protocols;
3304
3304
  int port, tls;
3305
+ // Raw TCP (cloudflare:sockets connect()): no WebSocket framing, bytes both ways.
3306
+ int raw, starttls_req, shutdown_req, want_drain;
3307
+ size_t unsent, window;
3308
+ char sni[256];
3305
3309
  } sb_wsc;
3306
3310
  static pthread_mutex_t sb_wsc_lock = PTHREAD_MUTEX_INITIALIZER;
3307
3311
  static sb_wsc* sb_wscs = 0;
@@ -3496,6 +3500,7 @@ static long sb_wsio_pump(sb_wsc* c, sb_wsio* io, unsigned char* buf, size_t cap,
3496
3500
  }
3497
3501
  }
3498
3502
 
3503
+ static int sb_wsio_start_tls(sb_wsio* io, const char* host, char* why, size_t whylen);
3499
3504
  static int sb_wsio_open(sb_wsio* io, const char* host, int port, int tls, char* why, size_t whylen) {
3500
3505
  memset(io, 0, sizeof(*io));
3501
3506
  io->fd = sb_tcp_connect(host, port, why, whylen);
@@ -3506,6 +3511,11 @@ static int sb_wsio_open(sb_wsio* io, const char* host, int port, int tls, char*
3506
3511
  fcntl(io->fd, F_SETFL, fcntl(io->fd, F_GETFL) | O_NONBLOCK);
3507
3512
  io->tls = tls;
3508
3513
  if (!tls) return 0;
3514
+ return sb_wsio_start_tls(io, host, why, whylen);
3515
+ }
3516
+ // Starts TLS on an open fd (connect with secureTransport "on", or startTls()).
3517
+ static int sb_wsio_start_tls(sb_wsio* io, const char* host, char* why, size_t whylen) {
3518
+ io->tls = 1;
3509
3519
  io->sc = (br_ssl_client_context*)malloc(sizeof(br_ssl_client_context));
3510
3520
  io->xc = (br_x509_minimal_context*)malloc(sizeof(br_x509_minimal_context));
3511
3521
  io->iobuf = (unsigned char*)malloc(BR_SSL_BUFSIZE_BIDI);
@@ -3759,6 +3769,91 @@ finish:
3759
3769
  return 0;
3760
3770
  }
3761
3771
 
3772
+ // Raw TCP: the thread for connect(). Events: 1 data, 3 error (reason in data), 4 connected (plain),
3773
+ // 6 end of input, 8 send queue drained (after a write left more than the high-water mark queued),
3774
+ // 9 TLS handshake finished.
3775
+ #define SB_TCP_READ_WINDOW (256u << 10)
3776
+ static void* sb_tcp_thread(void* arg) {
3777
+ sb_wsc* c = (sb_wsc*)arg;
3778
+ sb_wsio io;
3779
+ memset(&io, 0, sizeof(io));
3780
+ io.fd = -1;
3781
+ char why[512] = "";
3782
+ unsigned char buf[16384];
3783
+ unsigned char* wq = 0; size_t wq_len = 0, wq_cap = 0;
3784
+ int tls_waiting = 0, eof = 0, shut = 0;
3785
+ if (sb_wsio_open(&io, c->host, c->port, c->tls, why, sizeof(why)) != 0) { sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
3786
+ if (c->tls) tls_waiting = 1; else sb_wsc_push(c, 4, 0, "", 0);
3787
+ for (;;) {
3788
+ pthread_mutex_lock(&c->lock);
3789
+ int rel = c->released;
3790
+ size_t window = c->unacked;
3791
+ int st = c->starttls_req; c->starttls_req = 0;
3792
+ int sh = c->shutdown_req;
3793
+ char sni[256]; memcpy(sni, c->sni, sizeof(sni));
3794
+ if (c->outq_len) {
3795
+ if (wq_len + c->outq_len > wq_cap) {
3796
+ size_t cap = wq_cap ? wq_cap : 4096;
3797
+ while (cap < wq_len + c->outq_len) cap *= 2;
3798
+ unsigned char* grown = (unsigned char*)realloc(wq, cap);
3799
+ if (grown) { wq = grown; wq_cap = cap; }
3800
+ }
3801
+ if (wq_len + c->outq_len <= wq_cap) { memcpy(wq + wq_len, c->outq, c->outq_len); wq_len += c->outq_len; c->outq_len = 0; }
3802
+ }
3803
+ c->unsent = wq_len;
3804
+ pthread_mutex_unlock(&c->lock);
3805
+ if (rel) {
3806
+ if (wq_len) sb_wsio_pump(c, &io, buf, sizeof(buf), wq, &wq_len, 0, 0);
3807
+ goto finish;
3808
+ }
3809
+ if (st && !io.tls) {
3810
+ if (wq_len) sb_wsio_pump(c, &io, buf, sizeof(buf), wq, &wq_len, 0, 0);
3811
+ if (sb_wsio_start_tls(&io, sni[0] ? sni : c->host, why, sizeof(why)) != 0) { sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
3812
+ tls_waiting = 1;
3813
+ }
3814
+ long n = sb_wsio_pump(c, &io, buf, sizeof(buf), wq, &wq_len, !eof && window < SB_TCP_READ_WINDOW, tls_waiting ? 20 : -1);
3815
+ if (tls_waiting && io.tls && (br_ssl_engine_current_state(&io.sc->eng) & (BR_SSL_SENDAPP | BR_SSL_RECVAPP))) {
3816
+ tls_waiting = 0;
3817
+ sb_wsc_push(c, 9, 0, "", 0);
3818
+ }
3819
+ if (n == -2) continue;
3820
+ if (n < 0) {
3821
+ int err = io.tls ? br_ssl_engine_last_error(&io.sc->eng) : 0;
3822
+ if (err != 0 && err != BR_ERR_IO) { snprintf(why, sizeof(why), "TLS error %d", err); sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
3823
+ if (tls_waiting) { snprintf(why, sizeof(why), "connection closed during the TLS handshake"); sb_wsc_push(c, 3, 0, why, strlen(why)); goto finish; }
3824
+ if (!eof) { eof = 1; sb_wsc_push(c, 6, 0, "", 0); }
3825
+ // keep serving writes until released; with no reader the pump just waits for the wake pipe
3826
+ n = 0;
3827
+ }
3828
+ if (n == 0) sb_wsc_drain_wake(c);
3829
+ else sb_wsc_push(c, 1, 0, (const char*)buf, (size_t)n);
3830
+ if (sh && wq_len == 0 && !shut) {
3831
+ shut = 1;
3832
+ if (io.tls) br_ssl_engine_close(&io.sc->eng); else shutdown(io.fd, SHUT_WR);
3833
+ }
3834
+ int drained = 0;
3835
+ pthread_mutex_lock(&c->lock);
3836
+ c->unsent = wq_len;
3837
+ if (c->want_drain && c->outq_len + wq_len == 0) { c->want_drain = 0; drained = 1; }
3838
+ pthread_mutex_unlock(&c->lock);
3839
+ if (drained) sb_wsc_push(c, 8, 0, "", 0);
3840
+ }
3841
+ finish:
3842
+ sb_wsio_close(&io);
3843
+ free(wq);
3844
+ pthread_mutex_lock(&c->lock);
3845
+ c->finished = 1;
3846
+ int rel2 = c->released;
3847
+ pthread_mutex_unlock(&c->lock);
3848
+ if (rel2) {
3849
+ pthread_mutex_lock(&sb_wsc_lock);
3850
+ for (sb_wsc** at = &sb_wscs; *at; at = &(*at)->next) if (*at == c) { *at = c->next; break; }
3851
+ pthread_mutex_unlock(&sb_wsc_lock);
3852
+ sb_wsc_free(c);
3853
+ }
3854
+ return 0;
3855
+ }
3856
+
3762
3857
  static sb_wsc* sb_wsc_find(unsigned id) {
3763
3858
  pthread_mutex_lock(&sb_wsc_lock);
3764
3859
  sb_wsc* c = sb_wscs;
@@ -3787,6 +3882,52 @@ static void sb_wsc_drain(void) {
3787
3882
 
3788
3883
  // The JS side's ops. data is borrowed for the call.
3789
3884
  static long long sb_wsc_op(int op, unsigned id, const char* data, size_t len, size_t n, char* out, size_t cap) {
3885
+ if (op == 'T') {
3886
+ // data: host \n port \n mode (0 off, 1 on, 2 starttls) \n servername
3887
+ sb_wsc* c = (sb_wsc*)calloc(1, sizeof(sb_wsc));
3888
+ if (!c) return 0;
3889
+ char* copy = (char*)malloc(len + 1);
3890
+ if (!copy) { free(c); return 0; }
3891
+ memcpy(copy, data, len); copy[len] = 0;
3892
+ char* parts[4]; char* cur = copy;
3893
+ for (int i = 0; i < 4; i++) {
3894
+ parts[i] = cur;
3895
+ char* nl = strchr(cur, '\n');
3896
+ if (nl) { *nl = 0; cur = nl + 1; } else cur += strlen(cur);
3897
+ }
3898
+ c->raw = 1; c->window = SB_TCP_READ_WINDOW;
3899
+ c->host = strdup(parts[0]); c->port = atoi(parts[1]); c->tls = atoi(parts[2]) == 1;
3900
+ snprintf(c->sni, sizeof(c->sni), "%s", parts[3]);
3901
+ c->path = strdup(""); c->protocols = strdup(""); c->headers = strdup("");
3902
+ free(copy);
3903
+ if (!c->host || !c->path || !c->protocols || !c->headers || pipe(c->wake) != 0) { free(c->host); free(c->path); free(c->protocols); free(c->headers); free(c); return 0; }
3904
+ 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); }
3905
+ pthread_mutex_init(&c->lock, 0);
3906
+ pthread_mutex_lock(&sb_wsc_lock);
3907
+ int live = 0;
3908
+ for (sb_wsc* k = sb_wscs; k; k = k->next) live++;
3909
+ if (live >= SB_WSC_MAX) { pthread_mutex_unlock(&sb_wsc_lock); sb_wsc_free(c); return 0; }
3910
+ sb_wsc_next = (sb_wsc_next + 1) & 0x7fffffffu;
3911
+ if (!sb_wsc_next) sb_wsc_next = 1;
3912
+ c->id = sb_wsc_next | SB_WSC_BIT;
3913
+ c->next = sb_wscs; sb_wscs = c;
3914
+ pthread_mutex_unlock(&sb_wsc_lock);
3915
+ pthread_attr_t attr;
3916
+ pthread_attr_init(&attr);
3917
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
3918
+ pthread_attr_setstacksize(&attr, 512 * 1024);
3919
+ pthread_t thread;
3920
+ int failed = pthread_create(&thread, &attr, sb_tcp_thread, c);
3921
+ pthread_attr_destroy(&attr);
3922
+ if (failed) {
3923
+ pthread_mutex_lock(&sb_wsc_lock);
3924
+ for (sb_wsc** at = &sb_wscs; *at; at = &(*at)->next) if (*at == c) { *at = c->next; break; }
3925
+ pthread_mutex_unlock(&sb_wsc_lock);
3926
+ sb_wsc_free(c);
3927
+ return 0;
3928
+ }
3929
+ return (long long)c->id;
3930
+ }
3790
3931
  if (op == 'u') {
3791
3932
  // data: host \n port \n tls(0|1) \n path \n protocols \n header lines (to the end)
3792
3933
  sb_wsc* c = (sb_wsc*)calloc(1, sizeof(sb_wsc));
@@ -3836,7 +3977,40 @@ static long long sb_wsc_op(int op, unsigned id, const char* data, size_t len, si
3836
3977
  sb_wsc* c = sb_wsc_find(id);
3837
3978
  if (!c) return op == 'p' || op == 'k' || op == 'w' || op == 'r' ? 0 : -1;
3838
3979
  switch (op) {
3839
- case 't': case 'b': return sb_wsc_frame(c, op == 't' ? 1 : 2, data, len, 0, 0);
3980
+ case 'b': if (c->raw) {
3981
+ // raw bytes: queue them, say how many are still unsent, and ask for a drain event past the mark
3982
+ pthread_mutex_lock(&c->lock);
3983
+ size_t need = c->outq_len + len;
3984
+ if (need > c->outq_cap) {
3985
+ size_t cap = c->outq_cap ? c->outq_cap : 4096;
3986
+ while (cap < need) cap *= 2;
3987
+ unsigned char* grown = (unsigned char*)realloc(c->outq, cap);
3988
+ if (!grown) { pthread_mutex_unlock(&c->lock); return -1; }
3989
+ c->outq = grown; c->outq_cap = cap;
3990
+ }
3991
+ memcpy(c->outq + c->outq_len, data, len);
3992
+ c->outq_len += len;
3993
+ size_t queued = c->outq_len + c->unsent;
3994
+ if (queued > (256u << 10)) c->want_drain = 1;
3995
+ pthread_mutex_unlock(&c->lock);
3996
+ sb_wsc_wake_thread(c);
3997
+ return (long long)queued;
3998
+ }
3999
+ /* fall through */
4000
+ case 't': return sb_wsc_frame(c, op == 't' ? 1 : 2, data, len, 0, 0);
4001
+ case 'S':
4002
+ pthread_mutex_lock(&c->lock);
4003
+ snprintf(c->sni, sizeof(c->sni), "%.*s", (int)(len < sizeof(c->sni) - 1 ? len : sizeof(c->sni) - 1), data ? data : "");
4004
+ c->starttls_req = 1;
4005
+ pthread_mutex_unlock(&c->lock);
4006
+ sb_wsc_wake_thread(c);
4007
+ return 0;
4008
+ case 'H':
4009
+ pthread_mutex_lock(&c->lock);
4010
+ c->shutdown_req = 1;
4011
+ pthread_mutex_unlock(&c->lock);
4012
+ sb_wsc_wake_thread(c);
4013
+ return 0;
3840
4014
  case 'c': {
3841
4015
  pthread_mutex_lock(&c->lock);
3842
4016
  int already = c->close_queued;
package/src/wrap.ts CHANGED
@@ -137,6 +137,20 @@ function hasBindings(b: Bindings): boolean {
137
137
  * leaves the specifier external for this.
138
138
  */
139
139
  export function inlineWorkersImports(source: string): string {
140
+ // `cloudflare:sockets` (compatibility date 2026-11-17): connect() lives on a prelude global.
141
+ // postgres/cf loads it lazily: `await import("cloudflare:sockets")`.
142
+ source = source.replace(/\bimport\(\s*["']cloudflare:sockets["']\s*\)/g, "Promise.resolve(globalThis.__sbSockets)");
143
+ source = source.replace(
144
+ /^import\s*\{([^}]*)\}\s*from\s*["']cloudflare:sockets["'];?[ \t]*$/gm,
145
+ (_match, names: string) => {
146
+ const pairs = names
147
+ .split(",")
148
+ .map((part) => part.trim())
149
+ .filter(Boolean)
150
+ .map((part) => part.replace(/^(\S+)\s+as\s+(\S+)$/, "$1: $2"));
151
+ return `const { ${pairs.join(", ")} } = globalThis.__sbSockets;`;
152
+ },
153
+ );
140
154
  return source.replace(
141
155
  /^import\s*\{([^}]*)\}\s*from\s*["']cloudflare:workers["'];?[ \t]*$/gm,
142
156
  (_match, names: string) => {