@sproutboat/runtime 0.19.2 → 0.20.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.
@@ -11,14 +11,20 @@
11
11
  // If SB_BROKER_PORT is unset the shims below are never installed (compile.ts
12
12
  // only emits the __sbInstallBindings call when the project declares bindings),
13
13
  // so a plain sprout is byte-for-byte unchanged.
14
- // ponytail: text values only; still AF_INET loopback, not AF_UNIX. A failed
15
- // exchange reconnects and resends once — a broker crash between "request applied"
16
- // and "reply read" can double-apply a non-idempotent op (queue.send, INSERT);
17
- // the old fresh-connection-per-call path just failed the call there instead.
18
- // Binary values + AF_UNIX = v2.
14
+ // ponytail: still AF_INET loopback, not AF_UNIX. A failed v0 exchange reconnects
15
+ // and resends; the broker deduplicates its writes (queue.send, INSERT) by request
16
+ // id. v1 exchanges (fetch, service.fetch, R2) resend only when the broker cannot
17
+ // have acted: the request never fully left, or a reused connection had gone
18
+ // stale. On a host with an event loop, fetch and service.fetch run off it, on
19
+ // one I/O thread (sb_async_submit_v1).
19
20
 
20
21
  // oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
21
22
  Porffor.c`
23
+ #include <fcntl.h>
24
+ #include <poll.h>
25
+ #include <pthread.h>
26
+ #include <time.h>
27
+
22
28
  // baronunread/sproutboat#176 — defined in render.js, alongside
23
29
  // porf_native_fetch_read_value (which every inline-C block below already
24
30
  // calls with no forward declaration of its own — that one gets one from
@@ -45,7 +51,8 @@ static int sb_io_all(int fd, unsigned char* buf, size_t len, int writing) {
45
51
  // connect() handshake or close() each time. -1 = not connected.
46
52
  static int sb_broker_fd = -1;
47
53
 
48
- static int sb_broker_connect(void) {
54
+ // A new connection to the broker: its fd, or a negative code.
55
+ static int sb_broker_open(void) {
49
56
  const char* port_s = getenv("SB_BROKER_PORT");
50
57
  if (!port_s) return -10;
51
58
  signal(SIGPIPE, SIG_IGN); // a dead broker must yield EPIPE, not kill the sprout
@@ -59,16 +66,23 @@ static int sb_broker_connect(void) {
59
66
  addr.sin_port = htons((unsigned short)atoi(port_s));
60
67
  addr.sin_addr.s_addr = htonl(0x7f000001u); // 127.0.0.1
61
68
  if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { close(fd); return -2; }
69
+ return fd;
70
+ }
71
+
72
+ static int sb_broker_connect(void) {
73
+ int fd = sb_broker_open();
74
+ if (fd < 0) return fd;
62
75
  sb_broker_fd = fd;
63
76
  return 0;
64
77
  }
65
78
 
66
- // Send one framed request, read one framed reply, on the persistent fd.
67
- // Send one payload verbatim and read one reply. The caller owns the payload's
68
- // shape: a v0 exchange prepends the token line, a v1 one carries the token in
69
- // its JSON and must reach the broker byte for byte — prefixing it would leave
70
- // the marker in the wrong place and the frame would read as v0.
71
- static int sb_broker_exchange_raw(const char* body, size_t body_len, char** resp_out, size_t* resp_len_out) {
79
+ // Send one payload verbatim on fd and read one reply. The caller owns the
80
+ // payload's shape: a v0 exchange prepends the token line, a v1 one carries the
81
+ // token in its JSON and must reach the broker byte for byte — prefixing it would
82
+ // leave the marker in the wrong place and the frame would read as v0.
83
+ // -3 and -5 mean the broker never had the whole request; -4 and -6 mean it did,
84
+ // and may have acted on it.
85
+ static int sb_broker_exchange_fd(int fd, const char* body, size_t body_len, char** resp_out, size_t* resp_len_out) {
72
86
  unsigned char* frame = (unsigned char*)malloc(4 + body_len);
73
87
  if (!frame) return -5;
74
88
  frame[0] = (unsigned char)(body_len & 0xff);
@@ -76,23 +90,28 @@ static int sb_broker_exchange_raw(const char* body, size_t body_len, char** resp
76
90
  frame[2] = (unsigned char)((body_len >> 16) & 0xff);
77
91
  frame[3] = (unsigned char)((body_len >> 24) & 0xff);
78
92
  if (body_len) memcpy(frame + 4, body, body_len);
79
- int wr = sb_io_all(sb_broker_fd, frame, 4 + body_len, 1);
93
+ int wr = sb_io_all(fd, frame, 4 + body_len, 1);
80
94
  free(frame);
81
95
  if (wr != 0) return -3;
82
96
 
83
97
  unsigned char rhdr[4];
84
- if (sb_io_all(sb_broker_fd, rhdr, 4, 0) != 0) return -4;
98
+ if (sb_io_all(fd, rhdr, 4, 0) != 0) return -4;
85
99
  size_t rlen = (size_t)rhdr[0] | ((size_t)rhdr[1] << 8) | ((size_t)rhdr[2] << 16) | ((size_t)rhdr[3] << 24);
86
100
 
87
101
  char* buf = (char*)malloc(rlen ? rlen : 1);
88
102
  if (!buf) return -5;
89
- if (rlen && sb_io_all(sb_broker_fd, (unsigned char*)buf, rlen, 0) != 0) { free(buf); return -6; }
103
+ if (rlen && sb_io_all(fd, (unsigned char*)buf, rlen, 0) != 0) { free(buf); return -6; }
90
104
 
91
105
  *resp_out = buf;
92
106
  *resp_len_out = rlen;
93
107
  return 0;
94
108
  }
95
109
 
110
+ // The same exchange on the persistent connection.
111
+ static int sb_broker_exchange_raw(const char* body, size_t body_len, char** resp_out, size_t* resp_len_out) {
112
+ return sb_broker_exchange_fd(sb_broker_fd, body, body_len, resp_out, resp_len_out);
113
+ }
114
+
96
115
  // #63 — the binary reply from the last v1 exchange, handed to JS on request.
97
116
  // Stashed rather than returned inline so an 8 MB object body is one allocation
98
117
  // in the Porffor heap, not a substring of a bigger one.
@@ -207,61 +226,488 @@ static int sb_req_is_fetch(const char* req, size_t n) {
207
226
  return 0;
208
227
  }
209
228
 
210
- // A v1 exchange: marker, json length, json, then the body bytes. The reply is
211
- // split the same way, its JSON returned and its bytes stashed for __sbTakeBin.
212
- static int sb_broker_roundtrip_v1(const char* json, size_t json_len, const char* body, size_t body_len,
213
- char** resp_out, size_t* resp_len_out) {
229
+ // A v1 request: marker, json length, json, then the body bytes. malloc'd.
230
+ static char* sb_v1_request(const char* json, size_t json_len, const char* body, size_t body_len, size_t* out_len) {
214
231
  size_t req_len = 1 + 4 + json_len + body_len;
215
232
  char* req = (char*)malloc(req_len);
216
- if (!req) return -4;
233
+ if (!req) return 0;
217
234
  req[0] = 1;
218
235
  unsigned int jl = (unsigned int)json_len;
219
236
  memcpy(req + 1, &jl, 4);
220
237
  memcpy(req + 5, json, json_len);
221
238
  if (body_len) memcpy(req + 5 + json_len, body, body_len);
239
+ *out_len = req_len;
240
+ return req;
241
+ }
222
242
 
223
- char* resp = 0; size_t resp_len = 0;
243
+ // Splits a v1 reply, which it takes ownership of, into its JSON (returned,
244
+ // NUL-terminated, malloc'd) and its bytes (*bin, malloc'd, or 0 when there are
245
+ // none). Marker 1 is a final frame, 2 one of a streamed response's earlier ones. A
246
+ // broker that answered v0 to a v1 request predates this: its reply is returned
247
+ // whole, so the error it wrote is what the handler sees. 0 on OOM.
248
+ static char* sb_v1_split(char* resp, size_t resp_len, size_t* json_len, char** bin, size_t* bin_len) {
249
+ *bin = 0;
250
+ *bin_len = 0;
251
+ if (resp_len >= 5 && ((unsigned char)resp[0] == 1 || (unsigned char)resp[0] == 2)) {
252
+ unsigned int rjl = 0;
253
+ memcpy(&rjl, resp + 1, 4);
254
+ if (5 + (size_t)rjl <= resp_len) {
255
+ size_t n = resp_len - 5 - rjl;
256
+ if (n) {
257
+ *bin = (char*)malloc(n);
258
+ if (*bin) { memcpy(*bin, resp + 5 + rjl, n); *bin_len = n; }
259
+ }
260
+ char* json = (char*)malloc(rjl + 1);
261
+ if (json) { memcpy(json, resp + 5, rjl); json[rjl] = 0; *json_len = rjl; }
262
+ free(resp);
263
+ return json;
264
+ }
265
+ }
266
+ *json_len = resp_len;
267
+ return resp;
268
+ }
269
+
270
+ // One v1 exchange on *fd (-1 = connect first), with up to four tries. A failure
271
+ // before the broker had the whole request is always retried. One after it is
272
+ // retried only on a connection that had served an earlier exchange, which may
273
+ // simply have gone stale (a broker restart); on a fresh one the broker may have
274
+ // acted on it, and fetch and service.fetch are not deduplicated by id.
275
+ static int sb_v1_send(int* fd, const char* req, size_t req_len, char** resp, size_t* resp_len) {
224
276
  int rc = -3;
225
277
  for (int attempt = 0; attempt < 4; attempt++) {
226
278
  sb_backoff(attempt);
227
- if (sb_broker_fd < 0) {
228
- int c = sb_broker_connect();
229
- if (c != 0) { rc = c; continue; }
279
+ int reused = *fd >= 0;
280
+ if (!reused) {
281
+ int c = sb_broker_open();
282
+ if (c < 0) { rc = c; continue; }
283
+ *fd = c;
230
284
  }
231
- rc = sb_broker_exchange_raw(req, req_len, &resp, &resp_len);
232
- if (rc == 0) break;
233
- close(sb_broker_fd);
234
- sb_broker_fd = -1;
285
+ rc = sb_broker_exchange_fd(*fd, req, req_len, resp, resp_len);
286
+ if (rc == 0) return 0;
287
+ close(*fd);
288
+ *fd = -1;
289
+ if (!reused && rc != -3 && rc != -5) return rc;
235
290
  }
291
+ return rc;
292
+ }
293
+
294
+ // A v1 exchange on the persistent connection. The reply's JSON is returned and
295
+ // its bytes stashed for __sbTakeBin.
296
+ static int sb_broker_roundtrip_v1(const char* json, size_t json_len, const char* body, size_t body_len,
297
+ char** resp_out, size_t* resp_len_out) {
298
+ size_t req_len = 0;
299
+ char* req = sb_v1_request(json, json_len, body, body_len, &req_len);
300
+ if (!req) return -4;
301
+ char* resp = 0; size_t resp_len = 0;
302
+ int rc = sb_v1_send(&sb_broker_fd, req, req_len, &resp, &resp_len);
236
303
  free(req);
237
304
  if (rc != 0) return rc;
238
-
239
305
  if (sb_bin_reply) { free(sb_bin_reply); sb_bin_reply = 0; sb_bin_reply_len = 0; }
240
- if (resp_len >= 5 && (unsigned char)resp[0] == 1) {
241
- unsigned int rjl = 0;
242
- memcpy(&rjl, resp + 1, 4);
243
- if (5 + (size_t)rjl <= resp_len) {
244
- size_t bin_len = resp_len - 5 - rjl;
245
- if (bin_len) {
246
- sb_bin_reply = (char*)malloc(bin_len);
247
- if (sb_bin_reply) { memcpy(sb_bin_reply, resp + 5 + rjl, bin_len); sb_bin_reply_len = bin_len; }
306
+ char* json_only = sb_v1_split(resp, resp_len, resp_len_out, &sb_bin_reply, &sb_bin_reply_len);
307
+ if (!json_only) return -4;
308
+ *resp_out = json_only;
309
+ return 0;
310
+ }
311
+
312
+ // Async exchanges, for a host with an event loop (the standalone server). The
313
+ // host installs a waker with sb_async_set_waker(); fetch() and service bindings
314
+ // then hand each exchange to one I/O thread and return at once, so the sprout
315
+ // keeps serving while calls are in flight — including the calls back into itself
316
+ // that recursion makes, however deep. The I/O thread multiplexes non-blocking
317
+ // broker connections with poll(), one per exchange in flight, and keeps a few
318
+ // idle ones for reuse. A finished exchange is queued and the waker called; the
319
+ // host then runs sb_async_drain() on its loop, which fires the JS callback
320
+ // registered under the request's timer id. A streamed response is delivered frame
321
+ // by frame (its head, then each chunk), each firing the callback, which the JS side
322
+ // registers as an interval for that. The thread touches only malloc'd jobs, never
323
+ // the JS heap. The same contract as the embedded transport's B18, which signals a
324
+ // pipe.
325
+ void porf_native_fetch_fire_timer(u32 id);
326
+ void porf_native_fetch_timer_clear(u32 id);
327
+ typedef struct sb_async_job {
328
+ struct sb_async_job* next;
329
+ u32 id;
330
+ // The framed request ([u32 LE length][v1 payload]) and how much of it is sent.
331
+ unsigned char* out; size_t out_len, out_off;
332
+ // The reply: its length prefix, then its payload.
333
+ unsigned char hdr[4]; size_t hdr_got;
334
+ char* in; size_t in_len, in_got;
335
+ int fd, reused, attempt, connecting;
336
+ // Asked for a streamed response; and has delivered part of one, so never retried.
337
+ int stream, delivered;
338
+ long long retry_at; // ms; 0 = not waiting to retry
339
+ char* json; size_t json_len;
340
+ char* bin; size_t bin_len;
341
+ } sb_async_job;
342
+ // One frame's worth of reply for the loop: a whole response, or part of a streamed one.
343
+ typedef struct sb_async_delivery {
344
+ struct sb_async_delivery* next;
345
+ u32 id;
346
+ int final;
347
+ char* json; size_t json_len;
348
+ char* bin; size_t bin_len;
349
+ } sb_async_delivery;
350
+ // Idle connections kept for the next exchanges.
351
+ #define SB_IO_IDLE 8
352
+ static pthread_mutex_t sb_async_lock = PTHREAD_MUTEX_INITIALIZER;
353
+ static sb_async_job* sb_async_queue = 0;
354
+ static sb_async_delivery *sb_async_ready = 0, *sb_async_current = 0;
355
+ static int sb_io_started = 0, sb_io_wake[2] = { -1, -1 };
356
+ // Ids of exchanges whose caller gave up (an aborted signal), for the I/O thread.
357
+ static u32* sb_async_cancels = 0;
358
+ static size_t sb_async_cancels_n = 0, sb_async_cancels_cap = 0;
359
+ static void (*sb_async_waker)(void) = 0;
360
+
361
+ void sb_async_set_waker(void (*wake)(void)) { sb_async_waker = wake; }
362
+ int sb_async_enabled(void) { return sb_async_waker != 0; }
363
+
364
+ static long long sb_now_ms(void) {
365
+ struct timespec ts;
366
+ clock_gettime(CLOCK_MONOTONIC, &ts);
367
+ return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
368
+ }
369
+
370
+ static void sb_async_error(sb_async_job* j, int rc) {
371
+ char e[48];
372
+ int n = snprintf(e, sizeof(e), "{\"ok\":false,\"error\":\"broker rc %d\"}", rc);
373
+ j->json = (char*)malloc((size_t)n + 1);
374
+ if (!j->json) abort();
375
+ memcpy(j->json, e, (size_t)n + 1);
376
+ j->json_len = (size_t)n;
377
+ }
378
+
379
+ // Queues a reply frame for the loop, which takes ownership of json and bin.
380
+ static void sb_async_deliver(u32 id, int final, char* json, size_t json_len, char* bin, size_t bin_len) {
381
+ sb_async_delivery* d = (sb_async_delivery*)calloc(1, sizeof(sb_async_delivery));
382
+ if (!d) abort();
383
+ d->id = id; d->final = final;
384
+ d->json = json; d->json_len = json_len;
385
+ d->bin = bin; d->bin_len = bin_len;
386
+ pthread_mutex_lock(&sb_async_lock);
387
+ d->next = sb_async_ready;
388
+ sb_async_ready = d;
389
+ pthread_mutex_unlock(&sb_async_lock);
390
+ sb_async_waker();
391
+ }
392
+
393
+ // Hands a finished job (json set) back to the loop, and frees it.
394
+ static void sb_async_finish(sb_async_job* j) {
395
+ sb_async_deliver(j->id, 1, j->json, j->json_len, j->bin, j->bin_len);
396
+ free(j->out);
397
+ free(j->in);
398
+ free(j);
399
+ }
400
+
401
+ // A reply frame's payload -> its ASCII JSON and bytes (see sb_json_ascii). 0 on OOM.
402
+ static char* sb_io_reply_json(char* payload, size_t len, size_t* json_len, char** bin, size_t* bin_len) {
403
+ char* json = sb_v1_split(payload, len, json_len, bin, bin_len);
404
+ size_t al = 0;
405
+ char* ascii = json ? sb_json_ascii(json, *json_len, &al) : 0;
406
+ if (ascii) { free(json); json = ascii; *json_len = al; }
407
+ return json;
408
+ }
409
+
410
+ static void sb_io_nonblock(int fd) { fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); fcntl(fd, F_SETFD, FD_CLOEXEC); }
411
+
412
+ // Starts (or restarts) a job: an idle connection if there is one, else a new one.
413
+ static void sb_io_begin(sb_async_job* j, int* idle, int* idle_n) {
414
+ j->out_off = 0; j->hdr_got = 0; j->in_got = 0; j->in_len = 0;
415
+ free(j->in); j->in = 0;
416
+ j->retry_at = 0; j->connecting = 0;
417
+ if (*idle_n > 0) { j->fd = idle[--*idle_n]; j->reused = 1; return; }
418
+ j->reused = 0;
419
+ const char* port_s = getenv("SB_BROKER_PORT");
420
+ int fd = port_s ? socket(AF_INET, SOCK_STREAM, 0) : -1;
421
+ if (fd < 0) { j->fd = -1; return; }
422
+ int one = 1;
423
+ setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
424
+ sb_io_nonblock(fd);
425
+ struct sockaddr_in addr;
426
+ memset(&addr, 0, sizeof(addr));
427
+ addr.sin_family = AF_INET;
428
+ addr.sin_port = htons((unsigned short)atoi(port_s));
429
+ addr.sin_addr.s_addr = htonl(0x7f000001u);
430
+ if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
431
+ if (errno != EINPROGRESS) { close(fd); j->fd = -1; return; }
432
+ j->connecting = 1;
433
+ }
434
+ j->fd = fd;
435
+ }
436
+
437
+ // A failed attempt. Retried (after a backoff) when the broker never had the whole
438
+ // request, or the connection was a reused one that may have gone stale; failed
439
+ // otherwise, since fetch and service.fetch are not deduplicated by id.
440
+ static int sb_io_failed(sb_async_job* j, int rc) {
441
+ if (j->fd >= 0) { close(j->fd); j->fd = -1; }
442
+ int unsent = j->out_off < j->out_len;
443
+ if (!j->delivered && (unsent || j->reused) && ++j->attempt < 4) {
444
+ static const long ms[4] = { 0, 5, 25, 100 };
445
+ j->retry_at = sb_now_ms() + ms[j->attempt];
446
+ return 0;
447
+ }
448
+ sb_async_error(j, rc);
449
+ return 1;
450
+ }
451
+
452
+ // Moves a job along as far as its socket allows. 1 = done (json set), 0 = not yet.
453
+ static int sb_io_step(sb_async_job* j, int* idle, int* idle_n) {
454
+ if (j->connecting) {
455
+ int err = 0; socklen_t len = sizeof(err);
456
+ if (getsockopt(j->fd, SOL_SOCKET, SO_ERROR, &err, &len) != 0 || err != 0) return sb_io_failed(j, -2);
457
+ j->connecting = 0;
458
+ }
459
+ while (j->out_off < j->out_len) {
460
+ long n = write(j->fd, j->out + j->out_off, j->out_len - j->out_off);
461
+ if (n > 0) { j->out_off += (size_t)n; continue; }
462
+ if (n < 0 && errno == EINTR) continue;
463
+ if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0;
464
+ return sb_io_failed(j, -3);
465
+ }
466
+ for (;;) {
467
+ while (j->hdr_got < 4) {
468
+ long n = read(j->fd, j->hdr + j->hdr_got, 4 - j->hdr_got);
469
+ if (n > 0) { j->hdr_got += (size_t)n; continue; }
470
+ if (n < 0 && errno == EINTR) continue;
471
+ if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0;
472
+ return sb_io_failed(j, -4);
473
+ }
474
+ if (!j->in) {
475
+ j->in_len = (size_t)j->hdr[0] | ((size_t)j->hdr[1] << 8) | ((size_t)j->hdr[2] << 16) | ((size_t)j->hdr[3] << 24);
476
+ j->in = (char*)malloc(j->in_len ? j->in_len : 1);
477
+ if (!j->in) abort();
478
+ }
479
+ while (j->in_got < j->in_len) {
480
+ long n = read(j->fd, j->in + j->in_got, j->in_len - j->in_got);
481
+ if (n > 0) { j->in_got += (size_t)n; continue; }
482
+ if (n < 0 && errno == EINTR) continue;
483
+ if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0;
484
+ return sb_io_failed(j, -6);
485
+ }
486
+ char* payload = j->in;
487
+ size_t payload_len = j->in_len;
488
+ j->in = 0; j->in_len = 0; j->in_got = 0; j->hdr_got = 0;
489
+ int more = j->stream && payload_len >= 1 && (unsigned char)payload[0] == 2;
490
+ if (more) {
491
+ // Part of a streamed response: to the loop now, then on to the next frame.
492
+ size_t json_len = 0, bin_len = 0;
493
+ char* bin = 0;
494
+ char* json = sb_io_reply_json(payload, payload_len, &json_len, &bin, &bin_len);
495
+ if (!json) abort();
496
+ j->delivered = 1;
497
+ sb_async_deliver(j->id, 0, json, json_len, bin, bin_len);
498
+ continue;
499
+ }
500
+ if (*idle_n < SB_IO_IDLE) idle[(*idle_n)++] = j->fd; else close(j->fd);
501
+ j->fd = -1;
502
+ j->json = sb_io_reply_json(payload, payload_len, &j->json_len, &j->bin, &j->bin_len);
503
+ if (!j->json) sb_async_error(j, -4);
504
+ return 1;
505
+ }
506
+ }
507
+
508
+ static void* sb_io_thread(void* unused) {
509
+ (void)unused;
510
+ sb_async_job* active = 0;
511
+ int idle[SB_IO_IDLE], idle_n = 0;
512
+ struct pollfd* fds = 0;
513
+ sb_async_job** owners = 0;
514
+ size_t cap = 0;
515
+ for (;;) {
516
+ // New jobs.
517
+ pthread_mutex_lock(&sb_async_lock);
518
+ sb_async_job* fresh = sb_async_queue;
519
+ sb_async_queue = 0;
520
+ pthread_mutex_unlock(&sb_async_lock);
521
+ while (fresh) {
522
+ sb_async_job* j = fresh;
523
+ fresh = j->next;
524
+ sb_io_begin(j, idle, &idle_n);
525
+ j->next = active;
526
+ active = j;
527
+ }
528
+ // Exchanges given up on: their connections close, which tells the broker to stop.
529
+ pthread_mutex_lock(&sb_async_lock);
530
+ u32* cancels = sb_async_cancels;
531
+ size_t cancels_n = sb_async_cancels_n;
532
+ sb_async_cancels = 0;
533
+ sb_async_cancels_n = sb_async_cancels_cap = 0;
534
+ pthread_mutex_unlock(&sb_async_lock);
535
+ for (size_t c = 0; c < cancels_n; c++) {
536
+ for (sb_async_job** link = &active; *link; link = &(*link)->next) {
537
+ sb_async_job* j = *link;
538
+ if (j->id != cancels[c]) continue;
539
+ *link = j->next;
540
+ if (j->fd >= 0) close(j->fd);
541
+ free(j->out); free(j->in); free(j->json); free(j->bin); free(j);
542
+ break;
248
543
  }
249
- char* json_only = (char*)malloc(rjl + 1);
250
- if (!json_only) { free(resp); return -4; }
251
- memcpy(json_only, resp + 5, rjl);
252
- json_only[rjl] = 0;
253
- free(resp);
254
- *resp_out = json_only;
255
- *resp_len_out = rjl;
256
- return 0;
544
+ }
545
+ free(cancels);
546
+ // Retries that are due restart; a job with no socket fails, or waits to retry.
547
+ long long now = sb_now_ms();
548
+ for (sb_async_job** link = &active; *link;) {
549
+ sb_async_job* j = *link;
550
+ if (j->retry_at && j->retry_at <= now) sb_io_begin(j, idle, &idle_n);
551
+ if (!j->retry_at && j->fd < 0 && sb_io_failed(j, -2)) {
552
+ *link = j->next;
553
+ sb_async_finish(j);
554
+ continue;
555
+ }
556
+ link = &j->next;
557
+ }
558
+ long long soonest = 0;
559
+ size_t count = 0;
560
+ for (sb_async_job* j = active; j; j = j->next) {
561
+ if (!j->retry_at) count++;
562
+ else if (!soonest || j->retry_at < soonest) soonest = j->retry_at;
563
+ }
564
+ if (count + 1 > cap) {
565
+ cap = (count + 1) * 2;
566
+ fds = (struct pollfd*)realloc(fds, cap * sizeof(struct pollfd));
567
+ owners = (sb_async_job**)realloc(owners, cap * sizeof(sb_async_job*));
568
+ if (!fds || !owners) abort();
569
+ }
570
+ fds[0].fd = sb_io_wake[0]; fds[0].events = POLLIN; fds[0].revents = 0;
571
+ size_t n = 1;
572
+ for (sb_async_job* j = active; j; j = j->next) {
573
+ if (j->retry_at) continue;
574
+ fds[n].fd = j->fd;
575
+ fds[n].events = j->connecting || j->out_off < j->out_len ? POLLOUT : POLLIN;
576
+ fds[n].revents = 0;
577
+ owners[n++] = j;
578
+ }
579
+ int timeout = soonest ? (int)(soonest > now ? soonest - now : 0) : -1;
580
+ if (poll(fds, (nfds_t)n, timeout) < 0 && errno != EINTR) continue;
581
+ if (fds[0].revents) { char buf[64]; while (read(sb_io_wake[0], buf, sizeof(buf)) > 0) {} }
582
+ for (size_t i = 1; i < n; i++) {
583
+ if (!fds[i].revents) continue;
584
+ sb_async_job* j = owners[i];
585
+ if (!sb_io_step(j, idle, &idle_n)) continue;
586
+ // Done: unlink and hand back.
587
+ sb_async_job** link = &active;
588
+ while (*link != j) link = &(*link)->next;
589
+ *link = j->next;
590
+ sb_async_finish(j);
257
591
  }
258
592
  }
259
- // A broker that answered v0 to a v1 request predates this: pass its reply
260
- // through so the error it wrote is what the handler sees.
261
- *resp_out = resp;
262
- *resp_len_out = resp_len;
263
593
  return 0;
264
594
  }
595
+
596
+ // The inline C of __sbCallAsync calls this by prototype: Porffor may emit a
597
+ // function's body before this block.
598
+ void sb_async_submit_v1(u32 id, int stream, const char* json, size_t json_len, const char* body, size_t body_len) {
599
+ sb_async_job* j = (sb_async_job*)calloc(1, sizeof(sb_async_job));
600
+ if (!j) abort();
601
+ j->id = id;
602
+ j->stream = stream;
603
+ j->fd = -1;
604
+ size_t req_len = 0;
605
+ char* req = sb_v1_request(json, json_len, body, body_len, &req_len);
606
+ if (!req) abort();
607
+ j->out = (unsigned char*)malloc(4 + req_len);
608
+ if (!j->out) abort();
609
+ for (int k = 0; k < 4; k++) j->out[k] = (unsigned char)((req_len >> (8 * k)) & 0xff);
610
+ memcpy(j->out + 4, req, req_len);
611
+ free(req);
612
+ j->out_len = 4 + req_len;
613
+ pthread_mutex_lock(&sb_async_lock);
614
+ int start = !sb_io_started;
615
+ if (start) {
616
+ signal(SIGPIPE, SIG_IGN); // a dead broker must yield EPIPE, not kill the sprout
617
+ if (pipe(sb_io_wake) != 0) abort();
618
+ sb_io_nonblock(sb_io_wake[0]);
619
+ sb_io_nonblock(sb_io_wake[1]);
620
+ sb_io_started = 1;
621
+ }
622
+ j->next = sb_async_queue;
623
+ sb_async_queue = j;
624
+ pthread_mutex_unlock(&sb_async_lock);
625
+ if (start) {
626
+ pthread_attr_t attr;
627
+ pthread_attr_init(&attr);
628
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
629
+ pthread_attr_setstacksize(&attr, 256 * 1024);
630
+ pthread_t thread;
631
+ int failed = pthread_create(&thread, &attr, sb_io_thread, 0);
632
+ pthread_attr_destroy(&attr);
633
+ // No thread (a pids limit): every async call fails; the sync path still works.
634
+ if (failed) {
635
+ pthread_mutex_lock(&sb_async_lock);
636
+ sb_async_job* orphans = sb_async_queue;
637
+ sb_async_queue = 0;
638
+ sb_io_started = 0;
639
+ close(sb_io_wake[0]); close(sb_io_wake[1]);
640
+ sb_io_wake[0] = sb_io_wake[1] = -1;
641
+ pthread_mutex_unlock(&sb_async_lock);
642
+ while (orphans) {
643
+ sb_async_job* q = orphans;
644
+ orphans = q->next;
645
+ sb_async_error(q, -7);
646
+ sb_async_finish(q);
647
+ }
648
+ return;
649
+ }
650
+ }
651
+ char one = 1;
652
+ while (write(sb_io_wake[1], &one, 1) < 0 && errno == EINTR) {}
653
+ }
654
+
655
+ // The caller gave up on exchange id: the I/O thread drops it and closes its
656
+ // connection. One that already finished is dropped by the drain instead, since its
657
+ // placeholder timer is gone.
658
+ void sb_async_cancel(u32 id) {
659
+ pthread_mutex_lock(&sb_async_lock);
660
+ int started = sb_io_started;
661
+ if (started) {
662
+ if (sb_async_cancels_n == sb_async_cancels_cap) {
663
+ sb_async_cancels_cap = sb_async_cancels_cap ? sb_async_cancels_cap * 2 : 8;
664
+ sb_async_cancels = (u32*)realloc(sb_async_cancels, sb_async_cancels_cap * sizeof(u32));
665
+ if (!sb_async_cancels) abort();
666
+ }
667
+ sb_async_cancels[sb_async_cancels_n++] = id;
668
+ }
669
+ pthread_mutex_unlock(&sb_async_lock);
670
+ if (!started) return;
671
+ char one = 1;
672
+ while (write(sb_io_wake[1], &one, 1) < 0 && errno == EINTR) {}
673
+ }
674
+
675
+ const char* sb_async_reply_json(u32 id, size_t* len) {
676
+ if (!sb_async_current || sb_async_current->id != id) return 0;
677
+ *len = sb_async_current->json_len;
678
+ return sb_async_current->json;
679
+ }
680
+
681
+ // Host, on its loop: fires the callback for each reply frame, oldest first. Its
682
+ // bytes go where __sbTakeBin reads them. A final frame also drops the placeholder
683
+ // timer; an earlier frame of a streamed response leaves it for the next one.
684
+ void sb_async_drain(void) {
685
+ pthread_mutex_lock(&sb_async_lock);
686
+ sb_async_delivery* ready = sb_async_ready;
687
+ sb_async_ready = 0;
688
+ pthread_mutex_unlock(&sb_async_lock);
689
+ sb_async_delivery* oldest = 0;
690
+ while (ready) {
691
+ sb_async_delivery* next = ready->next;
692
+ ready->next = oldest;
693
+ oldest = ready;
694
+ ready = next;
695
+ }
696
+ while (oldest) {
697
+ sb_async_delivery* d = oldest;
698
+ oldest = d->next;
699
+ if (d->final) porf_native_fetch_timer_clear(d->id);
700
+ if (sb_bin_reply) free(sb_bin_reply);
701
+ sb_bin_reply = d->bin;
702
+ sb_bin_reply_len = d->bin_len;
703
+ d->bin = 0;
704
+ sb_async_current = d; // read by __sbHttpAsyncResult in the callback
705
+ porf_native_fetch_fire_timer(d->id);
706
+ sb_async_current = 0;
707
+ free(d->json);
708
+ free(d);
709
+ }
710
+ }
265
711
  `;
266
712
 
267
713
  // One request string in, one reply string out. `reqJson` is a parameter, so the
@@ -400,6 +846,12 @@ globalThis.__sbR2MultipartPut = function (bucket, key, uploadId, partNumber, bod
400
846
  globalThis.__sbFetchUpstream = function (op, msg) {
401
847
  const req = { v: 1, id: ++__sbReqId, token: __sbEnv("SB_BROKER_TOKEN"), op };
402
848
  for (const k in msg) req[k] = msg[k];
849
+ // Queued (async host); the reply reaches the callback registered under asyncId, in
850
+ // several frames when msg.stream asks for a streamed response.
851
+ if (msg.asyncId > 0) {
852
+ __sbCallAsync(JSON.stringify(req), "", msg.asyncId, msg.stream === true ? "1" : "0");
853
+ return { ok: true, pending: true };
854
+ }
403
855
  const reply = JSON.parse(__sbCallBin(JSON.stringify(req), ""));
404
856
  if (reply.ok === false) throw new Error("sproutboat " + op + ": " + (reply.error || "failed"));
405
857
  const bytes = __sbTakeBin();
@@ -407,16 +859,74 @@ globalThis.__sbFetchUpstream = function (op, msg) {
407
859
  return reply;
408
860
  };
409
861
 
410
- // Async fetch (a host that drives requests off its event loop, like sproutd) exists
411
- // only in the embedded transport. Here fetch() is always synchronous.
862
+ // Queues one v1 exchange for the I/O thread, under timer id `asyncId`. `stream` is "1"
863
+ // when the reply may come as several frames, each firing the callback; a string, like
864
+ // the embedded transport's flags, so the C below reads it the same way whatever type
865
+ // Porffor infers.
866
+ // oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
867
+ function __sbCallAsync(reqJson, body, asyncId, stream) {
868
+ // oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
869
+ Porffor.c`
870
+ void sb_async_submit_v1(u32, int, const char*, size_t, const char*, size_t);
871
+ const char* __j; size_t __jl; char* __jo = 0;
872
+ porf_native_fetch_read_value(reqJson, &__j, &__jl, &__jo);
873
+ const char* __b; size_t __bl; char* __bo = 0;
874
+ if (porf_native_fetch_read_raw_bytes(body, &__b, &__bl) != 0) {
875
+ porf_native_fetch_read_value(body, &__b, &__bl, &__bo);
876
+ }
877
+ const char* __s; size_t __sl; char* __so = 0;
878
+ porf_native_fetch_read_value(stream, &__s, &__sl, &__so);
879
+ const int __stream = __sl > 0 && __s[0] == '1';
880
+ if (__so) free(__so);
881
+ if (asyncId.type == 1 && asyncId.val > 0) sb_async_submit_v1((u32)asyncId.val, __stream, __j, __jl, __b, __bl);
882
+ if (__jo) free(__jo);
883
+ if (__bo) free(__bo);
884
+ `;
885
+ }
886
+
887
+ // The caller gave up on the exchange under timer id `asyncId`: its connection
888
+ // closes, which the broker reads as "stop" and passes on.
889
+ // oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
890
+ function __sbCancelAsync(asyncId) {
891
+ // oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
892
+ Porffor.c`
893
+ void sb_async_cancel(u32);
894
+ if (asyncId.type == 1 && asyncId.val > 0) sb_async_cancel((u32)asyncId.val);
895
+ `;
896
+ }
897
+
898
+ // Whether the host runs exchanges off its loop (it installed a waker).
412
899
  function __sbHttpAsyncEnabled() {
413
- return false;
900
+ let res = false;
901
+ // oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
902
+ Porffor.c`
903
+ int sb_async_enabled(void);
904
+ if (sb_async_enabled()) res = porf_box(1, 2);
905
+ `;
906
+ return res;
414
907
  }
415
- function __sbHttpAsyncResult() {
416
- return "";
908
+
909
+ // The reply JSON of the async exchange now firing its callback, or "".
910
+ // oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
911
+ function __sbHttpAsyncResult(asyncId) {
912
+ let res = "";
913
+ // oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
914
+ Porffor.c`
915
+ const char* sb_async_reply_json(u32, size_t*);
916
+ size_t __n = 0;
917
+ const char* __r = asyncId.type == 1 ? sb_async_reply_json((u32)asyncId.val, &__n) : 0;
918
+ if (__r) res = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, __n), 195);
919
+ `;
920
+ return res;
417
921
  }
418
- function __sbFetchReply() {
419
- throw new Error("async fetch needs the embedded transport");
922
+
923
+ // An async exchange's reply JSON -> the fetch op's result, its bytes as the body.
924
+ function __sbFetchReply(raw) {
925
+ const reply = JSON.parse(raw);
926
+ if (reply.ok === false) throw new Error(reply.error || "failed");
927
+ const bytes = __sbTakeBin();
928
+ reply.body = reply.body != null ? __sbToBytes(reply.body) : bytes;
929
+ return reply;
420
930
  }
421
931
 
422
932
  /** Static asset metadata stays JSON; the response body uses the v1 byte tail. */