@sproutboat/runtime 0.16.0 → 0.17.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.
- package/CHANGELOG.md +68 -0
- package/WEB_STREAMS.md +7 -3
- package/package.json +1 -1
- package/src/asset-rules.test.js +125 -0
- package/src/egress.test.ts +248 -5
- package/src/gen-streams.ts +4 -5
- package/src/native-fetch-prelude.js +776 -112
- package/src/streams-entry.js +17 -3
- package/src/transport-broker.js +12 -0
- package/src/transport-embedded.js +403 -71
- package/test/native-bodies.ts +136 -0
- package/test/native-errors.ts +101 -0
- package/test/native-events.ts +102 -0
- package/test/native-iterators.ts +6 -1
- package/test/native-json.ts +102 -0
- package/test/native-raw-body.ts +118 -0
- package/test/native-request.ts +101 -0
- package/test/native-response-defaults.ts +98 -0
- package/test/native-streaming.ts +117 -0
- package/test/native-url.ts +145 -0
- package/test/native-waituntil.ts +107 -0
- package/test/native-web-apis.ts +110 -0
|
@@ -25,6 +25,7 @@ Porffor.c`
|
|
|
25
25
|
#include <string.h>
|
|
26
26
|
#include <strings.h>
|
|
27
27
|
#include <sys/time.h>
|
|
28
|
+
#include <pthread.h>
|
|
28
29
|
#include <bearssl.h>
|
|
29
30
|
|
|
30
31
|
// sqlite3 is linked in via SB_EXTRA_LINK (see patch-porffor.ts). Declared here
|
|
@@ -1112,8 +1113,65 @@ static int sb_egress_refused(const char* host, const struct sockaddr* sa, char*
|
|
|
1112
1113
|
}
|
|
1113
1114
|
// EGRESS:END
|
|
1114
1115
|
|
|
1116
|
+
// EGRESS-FD:BEGIN
|
|
1117
|
+
// SB_EGRESS_FD=N: a sandboxed host (sproutd) opens outbound connections for the
|
|
1118
|
+
// sprout, which may not open sockets itself. Ask on unix socket N with
|
|
1119
|
+
// "HOST PORT\n"; the host resolves, applies its policy and answers "ok\n" with the
|
|
1120
|
+
// connected socket attached (SCM_RIGHTS), or "no MESSAGE\n", which fetch() reports.
|
|
1121
|
+
// One question at a time (a lock, for async requests on several threads); any
|
|
1122
|
+
// broken exchange closes N for good, so a late answer can never be taken for the
|
|
1123
|
+
// next one.
|
|
1124
|
+
static int sb_egress_fd_ask(int via, const char* host, int port, char* refused, size_t refused_len);
|
|
1125
|
+
static int sb_egress_fd_connect(int via, const char* host, int port, char* refused, size_t refused_len) {
|
|
1126
|
+
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
|
1127
|
+
pthread_mutex_lock(&lock);
|
|
1128
|
+
int fd = sb_egress_fd_ask(via, host, port, refused, refused_len);
|
|
1129
|
+
pthread_mutex_unlock(&lock);
|
|
1130
|
+
return fd;
|
|
1131
|
+
}
|
|
1132
|
+
static int sb_egress_fd_ask(int via, const char* host, int port, char* refused, size_t refused_len) {
|
|
1133
|
+
static int broken = 0;
|
|
1134
|
+
if (broken) return -1;
|
|
1135
|
+
char line[512];
|
|
1136
|
+
int n = snprintf(line, sizeof(line), "%s %d\n", host, port);
|
|
1137
|
+
if (n <= 0 || (size_t)n >= sizeof(line)) return -1;
|
|
1138
|
+
struct timeval tv; tv.tv_sec = 60; tv.tv_usec = 0;
|
|
1139
|
+
setsockopt(via, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
|
1140
|
+
if (send(via, line, (size_t)n, 0) != n) { broken = 1; close(via); return -1; }
|
|
1141
|
+
char answer[512];
|
|
1142
|
+
union { struct cmsghdr align; char space[CMSG_SPACE(sizeof(int))]; } control;
|
|
1143
|
+
struct iovec io = { answer, sizeof(answer) - 1 };
|
|
1144
|
+
struct msghdr msg;
|
|
1145
|
+
memset(&msg, 0, sizeof(msg));
|
|
1146
|
+
msg.msg_iov = &io; msg.msg_iovlen = 1;
|
|
1147
|
+
msg.msg_control = control.space; msg.msg_controllen = sizeof(control.space);
|
|
1148
|
+
ssize_t got;
|
|
1149
|
+
do { got = recvmsg(via, &msg, 0); } while (got < 0 && errno == EINTR);
|
|
1150
|
+
int fd = -1;
|
|
1151
|
+
struct cmsghdr* c = got > 0 ? CMSG_FIRSTHDR(&msg) : 0;
|
|
1152
|
+
if (c && c->cmsg_level == SOL_SOCKET && c->cmsg_type == SCM_RIGHTS && c->cmsg_len == CMSG_LEN(sizeof(int)))
|
|
1153
|
+
memcpy(&fd, CMSG_DATA(c), sizeof(fd));
|
|
1154
|
+
if (got <= 0 || answer[got - 1] != '\n' || (msg.msg_flags & (MSG_TRUNC | MSG_CTRUNC))) {
|
|
1155
|
+
if (fd >= 0) close(fd);
|
|
1156
|
+
broken = 1; close(via);
|
|
1157
|
+
return -1;
|
|
1158
|
+
}
|
|
1159
|
+
answer[got - 1] = 0;
|
|
1160
|
+
if (!strcmp(answer, "ok") && fd >= 0) {
|
|
1161
|
+
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
|
1162
|
+
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
|
1163
|
+
return fd;
|
|
1164
|
+
}
|
|
1165
|
+
if (fd >= 0) close(fd);
|
|
1166
|
+
if (!strncmp(answer, "no ", 3) && !refused[0]) snprintf(refused, refused_len, "%s", answer + 3);
|
|
1167
|
+
return -1;
|
|
1168
|
+
}
|
|
1169
|
+
// EGRESS-FD:END
|
|
1170
|
+
|
|
1115
1171
|
// refused gets the first refusal; a caller reports it when nothing connected.
|
|
1116
1172
|
static int sb_tcp_connect(const char* host, int port, char* refused, size_t refused_len) {
|
|
1173
|
+
const char* via = getenv("SB_EGRESS_FD");
|
|
1174
|
+
if (via && *via) return sb_egress_fd_connect(atoi(via), host, port, refused, refused_len);
|
|
1117
1175
|
struct addrinfo hints, *res = 0, *it;
|
|
1118
1176
|
memset(&hints, 0, sizeof(hints));
|
|
1119
1177
|
hints.ai_family = AF_UNSPEC;
|
|
@@ -1151,10 +1209,13 @@ static int sb_tcp_connect(const char* host, int port, char* refused, size_t refu
|
|
|
1151
1209
|
}
|
|
1152
1210
|
|
|
1153
1211
|
// body_len, not strlen(body): a binary body can contain NUL bytes (#232).
|
|
1154
|
-
|
|
1212
|
+
// Host carries the port unless it is the scheme's default (RFC 9110 7.2).
|
|
1213
|
+
static void sb_build_request(sb_buf* req, const char* host, int port, int default_port, const char* path, const char* method,
|
|
1155
1214
|
const char* headers, const char* body, size_t body_len) {
|
|
1156
1215
|
sb_puts(req, method); sb_puts(req, " "); sb_puts(req, path); sb_puts(req, " HTTP/1.1\r\n");
|
|
1157
|
-
sb_puts(req, "Host: "); sb_puts(req, host);
|
|
1216
|
+
sb_puts(req, "Host: "); sb_puts(req, host);
|
|
1217
|
+
if (port != default_port) { char p[16]; snprintf(p, sizeof(p), ":%d", port); sb_puts(req, p); }
|
|
1218
|
+
sb_puts(req, "\r\n");
|
|
1158
1219
|
sb_puts(req, "Connection: close\r\n");
|
|
1159
1220
|
sb_puts(req, "Accept-Encoding: identity\r\n");
|
|
1160
1221
|
if (headers && *headers) sb_puts(req, headers);
|
|
@@ -1268,7 +1329,7 @@ static char* sb_http_plain(const char* host, int port, const char* path, const c
|
|
|
1268
1329
|
if (fd < 0) return refused[0] ? sb_error_json(refused, 0) : sb_error_json("could not connect to ", host);
|
|
1269
1330
|
|
|
1270
1331
|
sb_buf req = { 0, 0, 0 };
|
|
1271
|
-
sb_build_request(&req, host, path, method, headers, body, body_len);
|
|
1332
|
+
sb_build_request(&req, host, port, 80, path, method, headers, body, body_len);
|
|
1272
1333
|
size_t sent = 0;
|
|
1273
1334
|
while (sent < req.len) {
|
|
1274
1335
|
long n = write(fd, req.p + sent, req.len - sent);
|
|
@@ -1473,7 +1534,7 @@ static char* sb_https(const char* host, int port, const char* path, const char*
|
|
|
1473
1534
|
br_sslio_init(&ioc, &sc->eng, sb_sock_read, &fd, sb_sock_write, &fd);
|
|
1474
1535
|
|
|
1475
1536
|
sb_buf req = { 0, 0, 0 };
|
|
1476
|
-
sb_build_request(&req, host, path, method, headers, body, body_len);
|
|
1537
|
+
sb_build_request(&req, host, port, 443, path, method, headers, body, body_len);
|
|
1477
1538
|
int wrote = br_sslio_write_all(&ioc, req.p, req.len);
|
|
1478
1539
|
free(req.p);
|
|
1479
1540
|
if (wrote != 0) {
|
|
@@ -1527,6 +1588,129 @@ static char* sb_http_request(const char* host, int port, const char* path, const
|
|
|
1527
1588
|
return tls ? sb_https(host, port, path, method, headers, body, body_len)
|
|
1528
1589
|
: sb_http_plain(host, port, path, method, headers, body, body_len);
|
|
1529
1590
|
}
|
|
1591
|
+
|
|
1592
|
+
// ASYNC:BEGIN
|
|
1593
|
+
// Async outbound HTTP for a host with an event loop (sproutd, B18). The host calls
|
|
1594
|
+
// sb_async_enable() and watches the fd it returns. fetch() then queues each request
|
|
1595
|
+
// for a small pool of threads that run the blocking client above, and returns at
|
|
1596
|
+
// once. A finished request is queued and signalled on the fd; the host calls
|
|
1597
|
+
// sb_async_drain(), which fires the JS callback registered under the request's
|
|
1598
|
+
// timer id. Threads touch only their own malloc'd job, never the JS heap. Without
|
|
1599
|
+
// sb_async_enable() (standalone sprouts) nothing here runs.
|
|
1600
|
+
void porf_native_fetch_fire_timer(u32 id);
|
|
1601
|
+
void porf_native_fetch_timer_clear(u32 id);
|
|
1602
|
+
typedef struct sb_async_job {
|
|
1603
|
+
struct sb_async_job* next;
|
|
1604
|
+
u32 id;
|
|
1605
|
+
int port, tls;
|
|
1606
|
+
char *host, *path, *method, *headers, *body, *out;
|
|
1607
|
+
size_t body_len;
|
|
1608
|
+
} sb_async_job;
|
|
1609
|
+
#define SB_ASYNC_THREADS 8
|
|
1610
|
+
static pthread_mutex_t sb_async_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
1611
|
+
static sb_async_job *sb_async_queue = 0, *sb_async_queue_tail = 0, *sb_async_done = 0, *sb_async_current = 0;
|
|
1612
|
+
static int sb_async_pipe[2] = { -1, -1 };
|
|
1613
|
+
static int sb_async_threads = 0;
|
|
1614
|
+
|
|
1615
|
+
int sb_async_enable(void) {
|
|
1616
|
+
if (sb_async_pipe[0] >= 0) return sb_async_pipe[0];
|
|
1617
|
+
if (pipe(sb_async_pipe) != 0) return -1;
|
|
1618
|
+
for (int i = 0; i < 2; i++) {
|
|
1619
|
+
fcntl(sb_async_pipe[i], F_SETFD, FD_CLOEXEC);
|
|
1620
|
+
fcntl(sb_async_pipe[i], F_SETFL, fcntl(sb_async_pipe[i], F_GETFL) | O_NONBLOCK);
|
|
1621
|
+
}
|
|
1622
|
+
return sb_async_pipe[0];
|
|
1623
|
+
}
|
|
1624
|
+
int sb_async_enabled(void) { return sb_async_pipe[0] >= 0; }
|
|
1625
|
+
static void sb_async_free(sb_async_job* j) {
|
|
1626
|
+
free(j->host); free(j->path); free(j->method); free(j->headers); free(j->body); free(j->out); free(j);
|
|
1627
|
+
}
|
|
1628
|
+
// Called with sb_async_lock held.
|
|
1629
|
+
static void sb_async_finish_locked(sb_async_job* j) {
|
|
1630
|
+
j->next = sb_async_done;
|
|
1631
|
+
sb_async_done = j;
|
|
1632
|
+
char one = 1;
|
|
1633
|
+
while (write(sb_async_pipe[1], &one, 1) < 0 && errno == EINTR) {}
|
|
1634
|
+
}
|
|
1635
|
+
static void* sb_async_thread(void* unused) {
|
|
1636
|
+
(void)unused;
|
|
1637
|
+
for (;;) {
|
|
1638
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
1639
|
+
sb_async_job* j = sb_async_queue;
|
|
1640
|
+
if (!j) { sb_async_threads--; pthread_mutex_unlock(&sb_async_lock); return 0; }
|
|
1641
|
+
sb_async_queue = j->next;
|
|
1642
|
+
if (!sb_async_queue) sb_async_queue_tail = 0;
|
|
1643
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
1644
|
+
j->out = sb_http_request(j->host, j->port, j->path, j->method, j->headers, j->body, j->body_len, j->tls);
|
|
1645
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
1646
|
+
sb_async_finish_locked(j);
|
|
1647
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
static void sb_async_start(sb_async_job* j) {
|
|
1651
|
+
sb_init_anchors(); // built once, before any thread reads them
|
|
1652
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
1653
|
+
j->next = 0;
|
|
1654
|
+
if (sb_async_queue_tail) sb_async_queue_tail->next = j; else sb_async_queue = j;
|
|
1655
|
+
sb_async_queue_tail = j;
|
|
1656
|
+
int spawn = sb_async_threads < SB_ASYNC_THREADS;
|
|
1657
|
+
if (spawn) sb_async_threads++;
|
|
1658
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
1659
|
+
if (!spawn) return;
|
|
1660
|
+
pthread_attr_t attr;
|
|
1661
|
+
pthread_attr_init(&attr);
|
|
1662
|
+
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
|
1663
|
+
pthread_attr_setstacksize(&attr, 256 * 1024);
|
|
1664
|
+
pthread_t thread;
|
|
1665
|
+
int failed = pthread_create(&thread, &attr, sb_async_thread, 0);
|
|
1666
|
+
pthread_attr_destroy(&attr);
|
|
1667
|
+
if (!failed) return;
|
|
1668
|
+
// No thread (a pids limit): a running thread takes the queue; with none, fail it.
|
|
1669
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
1670
|
+
if (--sb_async_threads == 0) {
|
|
1671
|
+
while (sb_async_queue) {
|
|
1672
|
+
sb_async_job* q = sb_async_queue;
|
|
1673
|
+
sb_async_queue = q->next;
|
|
1674
|
+
q->out = sb_error_json("could not start a request: ", strerror(failed));
|
|
1675
|
+
sb_async_finish_locked(q);
|
|
1676
|
+
}
|
|
1677
|
+
sb_async_queue_tail = 0;
|
|
1678
|
+
}
|
|
1679
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
1680
|
+
}
|
|
1681
|
+
// The inline C of __sbHttpRaw and __sbHttpAsyncResult calls these by prototype: Porffor
|
|
1682
|
+
// may emit a function's body before this block.
|
|
1683
|
+
void sb_async_submit(u32 id, int port, int tls, char* host, char* path, char* method, char* headers,
|
|
1684
|
+
char* body, size_t body_len) {
|
|
1685
|
+
sb_async_job* j = (sb_async_job*)calloc(1, sizeof(sb_async_job));
|
|
1686
|
+
if (!j) abort();
|
|
1687
|
+
j->id = id; j->port = port; j->tls = tls;
|
|
1688
|
+
j->host = host; j->path = path; j->method = method; j->headers = headers;
|
|
1689
|
+
j->body = body; j->body_len = body_len;
|
|
1690
|
+
sb_async_start(j);
|
|
1691
|
+
}
|
|
1692
|
+
const char* sb_async_reply(u32 id) {
|
|
1693
|
+
return sb_async_current && sb_async_current->id == id ? sb_async_current->out : 0;
|
|
1694
|
+
}
|
|
1695
|
+
// Host: the fd became readable. Fires each finished request's callback.
|
|
1696
|
+
void sb_async_drain(void) {
|
|
1697
|
+
char buf[64];
|
|
1698
|
+
while (read(sb_async_pipe[0], buf, sizeof(buf)) > 0) {}
|
|
1699
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
1700
|
+
sb_async_job* done = sb_async_done;
|
|
1701
|
+
sb_async_done = 0;
|
|
1702
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
1703
|
+
while (done) {
|
|
1704
|
+
sb_async_job* j = done;
|
|
1705
|
+
done = j->next;
|
|
1706
|
+
porf_native_fetch_timer_clear(j->id); // the host's placeholder timer
|
|
1707
|
+
sb_async_current = j; // read by __sbHttpAsyncResult in the callback
|
|
1708
|
+
porf_native_fetch_fire_timer(j->id);
|
|
1709
|
+
sb_async_current = 0;
|
|
1710
|
+
sb_async_free(j);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
// ASYNC:END
|
|
1530
1714
|
`;
|
|
1531
1715
|
|
|
1532
1716
|
// R2 put: the body is its own parameter, so it never gets escaped.
|
|
@@ -1810,8 +1994,10 @@ function __sbD1BackupRaw(src, dest) {
|
|
|
1810
1994
|
// Outbound HTTP. String params so C can read each directly; the JS side has
|
|
1811
1995
|
// already split the URL; the connect refuses private addresses. `tlsFlag` is "1" or "0" —
|
|
1812
1996
|
// a string like the rest, so the marshalling stays uniform.
|
|
1997
|
+
// asyncId > 0 (B18): queue the request under that timer id and return "" at once;
|
|
1998
|
+
// its callback reads the reply with __sbHttpAsyncResult.
|
|
1813
1999
|
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
1814
|
-
function __sbHttpRaw(host, portStr, path, method, headersText, body, tlsFlag) {
|
|
2000
|
+
function __sbHttpRaw(host, portStr, path, method, headersText, body, tlsFlag, asyncId) {
|
|
1815
2001
|
let res = "";
|
|
1816
2002
|
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
1817
2003
|
Porffor.c`
|
|
@@ -1867,16 +2053,59 @@ function __sbHttpRaw(host, portStr, path, method, headersText, body, tlsFlag) {
|
|
|
1867
2053
|
char* __body = (char*)malloc(__bl + 1); memcpy(__body, __b, __bl); __body[__bl] = 0;
|
|
1868
2054
|
if (__bo) free(__bo);
|
|
1869
2055
|
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2056
|
+
const u32 __async = asyncId.type == 1 && asyncId.val > 0 ? (u32)asyncId.val : 0;
|
|
2057
|
+
if (__async) {
|
|
2058
|
+
void sb_async_submit(u32, int, int, char*, char*, char*, char*, char*, size_t);
|
|
2059
|
+
sb_async_submit(__async, atoi(__portbuf), __tls, __host, __path, __method, __hdrs, __body, __bl);
|
|
2060
|
+
} else {
|
|
2061
|
+
char* __out = sb_http_request(__host, atoi(__portbuf), __path, __method, __hdrs, __body, __bl, __tls);
|
|
2062
|
+
free(__host); free(__path); free(__method); free(__hdrs); free(__body);
|
|
2063
|
+
if (__out) {
|
|
2064
|
+
res = porf_box((f64)porf_native_fetch_alloc_bytestring(__out, strlen(__out)), 195);
|
|
2065
|
+
free(__out);
|
|
2066
|
+
}
|
|
1875
2067
|
}
|
|
1876
2068
|
`;
|
|
1877
2069
|
return res;
|
|
1878
2070
|
}
|
|
1879
2071
|
|
|
2072
|
+
// The reply of the async request now firing its callback (B18), or "".
|
|
2073
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
2074
|
+
function __sbHttpAsyncResult(asyncId) {
|
|
2075
|
+
let res = "";
|
|
2076
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
2077
|
+
Porffor.c`
|
|
2078
|
+
const char* sb_async_reply(u32);
|
|
2079
|
+
const char* __r = asyncId.type == 1 ? sb_async_reply((u32)asyncId.val) : 0;
|
|
2080
|
+
if (__r) res = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, strlen(__r)), 195);
|
|
2081
|
+
`;
|
|
2082
|
+
return res;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// Whether the host runs async requests (sb_async_enable was called).
|
|
2086
|
+
function __sbHttpAsyncEnabled() {
|
|
2087
|
+
let res = false;
|
|
2088
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
2089
|
+
Porffor.c`
|
|
2090
|
+
int sb_async_enabled(void);
|
|
2091
|
+
if (sb_async_enabled()) res = porf_box(1, 2);
|
|
2092
|
+
`;
|
|
2093
|
+
return res;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// The outbound client's JSON reply -> the fetch op's result.
|
|
2097
|
+
function __sbFetchReply(raw) {
|
|
2098
|
+
const reply = JSON.parse(raw);
|
|
2099
|
+
if (reply.ok === false) throw new Error(reply.error);
|
|
2100
|
+
// A TLS peer that closed without close_notify is normal on Connection:
|
|
2101
|
+
// close, and also what a truncation attack looks like. Accept it only when
|
|
2102
|
+
// Content-Length says the body arrived whole.
|
|
2103
|
+
if (reply.unclean && !reply.complete) {
|
|
2104
|
+
throw new Error("connection closed before the response was complete");
|
|
2105
|
+
}
|
|
2106
|
+
return { ok: true, status: reply.status, headers: reply.headers, body: reply.body };
|
|
2107
|
+
}
|
|
2108
|
+
|
|
1880
2109
|
// One statement in, one JSON reply out. `path`, `sql` and `paramsJson` are
|
|
1881
2110
|
// parameters so the generated C names them directly.
|
|
1882
2111
|
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
@@ -1954,7 +2183,8 @@ function __sbEnsureSchema() {
|
|
|
1954
2183
|
);
|
|
1955
2184
|
const kvColumns = __sbSql(s, "PRAGMA table_info(kv)", []).rows;
|
|
1956
2185
|
let hasKvExpiresAt = false;
|
|
1957
|
-
for (let i = 0; i < kvColumns.length; i++)
|
|
2186
|
+
for (let i = 0; i < kvColumns.length; i++)
|
|
2187
|
+
if (kvColumns[i][1] === "expires_at") hasKvExpiresAt = true;
|
|
1958
2188
|
if (!hasKvExpiresAt) __sbSql(s, "ALTER TABLE kv ADD COLUMN expires_at INTEGER", []);
|
|
1959
2189
|
// #233 — 1 when the value was put as bytes, same column as the broker's.
|
|
1960
2190
|
let hasKvBinary = false;
|
|
@@ -2011,6 +2241,10 @@ function __sbEnsureSchema() {
|
|
|
2011
2241
|
);
|
|
2012
2242
|
// Additive: old standalone store.sqlite files gain the poll index on open.
|
|
2013
2243
|
__sbSql(s, "CREATE INDEX IF NOT EXISTS mq_due ON mq (queue, dead, visible_at)", []);
|
|
2244
|
+
const mqColumns = __sbSql(s, "PRAGMA table_info(mq)", []).rows;
|
|
2245
|
+
let mqClaim = false;
|
|
2246
|
+
for (let i = 0; i < mqColumns.length; i++) if (mqColumns[i][1] === "claim_token") mqClaim = true;
|
|
2247
|
+
if (!mqClaim) __sbSql(s, "ALTER TABLE mq ADD COLUMN claim_token TEXT NOT NULL DEFAULT ''", []);
|
|
2014
2248
|
__sbSql(
|
|
2015
2249
|
s,
|
|
2016
2250
|
"CREATE TABLE IF NOT EXISTS do_storage (cls TEXT NOT NULL, id TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (cls, id, key))",
|
|
@@ -2020,6 +2254,11 @@ function __sbEnsureSchema() {
|
|
|
2020
2254
|
"CREATE TABLE IF NOT EXISTS do_alarm (cls TEXT NOT NULL, id TEXT NOT NULL, at INTEGER NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (cls, id))",
|
|
2021
2255
|
);
|
|
2022
2256
|
__sbSql(s, "CREATE INDEX IF NOT EXISTS do_alarm_due ON do_alarm (at)", []);
|
|
2257
|
+
__sbSql(
|
|
2258
|
+
s,
|
|
2259
|
+
"CREATE TABLE IF NOT EXISTS do_alarm_delivery (cls TEXT NOT NULL, id TEXT NOT NULL, at INTEGER NOT NULL, attempts INTEGER NOT NULL, token TEXT NOT NULL, expires INTEGER NOT NULL, PRIMARY KEY(cls,id))",
|
|
2260
|
+
[],
|
|
2261
|
+
);
|
|
2023
2262
|
__sbSql(
|
|
2024
2263
|
s,
|
|
2025
2264
|
"CREATE TABLE IF NOT EXISTS ae (dataset TEXT NOT NULL, ts INTEGER NOT NULL, indexes_json TEXT NOT NULL, blobs_json TEXT NOT NULL, doubles_json TEXT NOT NULL)",
|
|
@@ -2053,7 +2292,10 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
2053
2292
|
if (op === "ping") return { ok: true, op: "pong", echo: msg.msg };
|
|
2054
2293
|
|
|
2055
2294
|
if (op === "kv.get") {
|
|
2056
|
-
const r = __sbSql(store, "SELECT value, expires_at, binary FROM kv WHERE ns = ? AND key = ?", [
|
|
2295
|
+
const r = __sbSql(store, "SELECT value, expires_at, binary FROM kv WHERE ns = ? AND key = ?", [
|
|
2296
|
+
msg.ns,
|
|
2297
|
+
msg.key,
|
|
2298
|
+
]);
|
|
2057
2299
|
if (!r.rows.length) return { ok: true, found: false, value: null };
|
|
2058
2300
|
const expiresAt = r.rows[0][1];
|
|
2059
2301
|
if (expiresAt != null && expiresAt <= Date.now()) {
|
|
@@ -2104,6 +2346,11 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
2104
2346
|
const meta = (bundle.manifest && bundle.manifest.files) || {};
|
|
2105
2347
|
let path = String(msg.path || "/");
|
|
2106
2348
|
if (path.charAt(0) !== "/") path = "/" + path;
|
|
2349
|
+
const redirects = (bundle.manifest && bundle.manifest.redirects) || [];
|
|
2350
|
+
for (let i = 0; i < redirects.length; i++) {
|
|
2351
|
+
const location = __sbAssetRedirect(redirects[i], path);
|
|
2352
|
+
if (location !== null) return { ok: true, found: false, status: redirects[i].status, location };
|
|
2353
|
+
}
|
|
2107
2354
|
|
|
2108
2355
|
let key = null;
|
|
2109
2356
|
if (path.charAt(path.length - 1) === "/") {
|
|
@@ -2161,25 +2408,20 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
2161
2408
|
headerText += pairs[i][0] + ": " + pairs[i][1] + "\r\n";
|
|
2162
2409
|
}
|
|
2163
2410
|
const port = url.port ? url.port : tls ? "443" : "80";
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2411
|
+
const asyncId = msg.asyncId > 0 ? msg.asyncId : 0;
|
|
2412
|
+
const raw = __sbHttpRaw(
|
|
2413
|
+
url.hostname,
|
|
2414
|
+
port,
|
|
2415
|
+
url.pathname + url.search,
|
|
2416
|
+
String(msg.method || "GET").toUpperCase(),
|
|
2417
|
+
headerText,
|
|
2418
|
+
msg.body == null ? "" : String(msg.body),
|
|
2419
|
+
(tls ? "1" : "0") + (msg.bodyBinary === true ? "b" : ""),
|
|
2420
|
+
asyncId,
|
|
2174
2421
|
);
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
// Content-Length says the body arrived whole.
|
|
2179
|
-
if (reply.unclean && !reply.complete) {
|
|
2180
|
-
throw new Error("connection closed before the response was complete");
|
|
2181
|
-
}
|
|
2182
|
-
return { ok: true, status: reply.status, headers: reply.headers, body: reply.body };
|
|
2422
|
+
// B18: queued; the reply reaches the callback registered under asyncId.
|
|
2423
|
+
if (asyncId) return { ok: true, pending: true };
|
|
2424
|
+
return __sbFetchReply(raw);
|
|
2183
2425
|
}
|
|
2184
2426
|
|
|
2185
2427
|
if (op === "secret.get") {
|
|
@@ -2531,20 +2773,26 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
2531
2773
|
return { ok: true, entries };
|
|
2532
2774
|
}
|
|
2533
2775
|
if (op === "do.alarm.set") {
|
|
2534
|
-
__sbSql(
|
|
2535
|
-
|
|
2536
|
-
"
|
|
2537
|
-
[msg.cls,
|
|
2538
|
-
|
|
2539
|
-
|
|
2776
|
+
__sbSql(store,"BEGIN IMMEDIATE",[]);
|
|
2777
|
+
try {
|
|
2778
|
+
__sbSql(store,"DELETE FROM do_alarm_delivery WHERE cls=? AND id=? AND expires<=?",[msg.cls,msg.id,Date.now()]);
|
|
2779
|
+
__sbSql(store,"INSERT INTO do_alarm (cls,id,at,attempts) VALUES (?1,?2,?3,0) ON CONFLICT (cls,id) DO UPDATE SET at=?3,attempts=0",[msg.cls,msg.id,Math.trunc(Number(msg.at)||0)]);
|
|
2780
|
+
__sbSql(store,"COMMIT",[]);
|
|
2781
|
+
} catch (error) {__sbSql(store,"ROLLBACK",[]);throw error;}
|
|
2782
|
+
return {ok:true};
|
|
2540
2783
|
}
|
|
2541
2784
|
if (op === "do.alarm.get") {
|
|
2542
|
-
const r = __sbSql(store, "SELECT at FROM do_alarm WHERE cls
|
|
2785
|
+
const r = __sbSql(store, "SELECT at FROM do_alarm WHERE cls=?1 AND id=?2 UNION ALL SELECT at FROM do_alarm_delivery WHERE cls=?1 AND id=?2 AND expires<=?3 AND attempts<5 LIMIT 1", [msg.cls,msg.id,Date.now()+5000]);
|
|
2543
2786
|
return { ok: true, at: r.rows.length ? Number(r.rows[0][0]) : null };
|
|
2544
2787
|
}
|
|
2545
2788
|
if (op === "do.alarm.delete") {
|
|
2546
|
-
|
|
2547
|
-
|
|
2789
|
+
__sbSql(store,"BEGIN IMMEDIATE",[]);
|
|
2790
|
+
try {
|
|
2791
|
+
const retry=__sbSql(store,"DELETE FROM do_alarm_delivery WHERE cls=? AND id=?",[msg.cls,msg.id]);
|
|
2792
|
+
const pending=__sbSql(store,"DELETE FROM do_alarm WHERE cls=? AND id=?",[msg.cls,msg.id]);
|
|
2793
|
+
__sbSql(store,"COMMIT",[]);
|
|
2794
|
+
return {ok:true,deleted:Number(retry.changes)+Number(pending.changes)>0};
|
|
2795
|
+
} catch (error) {__sbSql(store,"ROLLBACK",[]);throw error;}
|
|
2548
2796
|
}
|
|
2549
2797
|
|
|
2550
2798
|
if (op === "ae.write") {
|
|
@@ -2604,10 +2852,11 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
2604
2852
|
|
|
2605
2853
|
if (op === "cache.match") {
|
|
2606
2854
|
const name = msg.name || "default";
|
|
2607
|
-
const r = __sbSql(
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2855
|
+
const r = __sbSql(
|
|
2856
|
+
store,
|
|
2857
|
+
"SELECT status, headers_json, body, expires_at FROM cache WHERE name = ? AND cache_key = ?",
|
|
2858
|
+
[name, msg.key],
|
|
2859
|
+
);
|
|
2611
2860
|
if (!r.rows.length) return { ok: true, found: false };
|
|
2612
2861
|
const [status, headersJson, body, expiresAt] = r.rows[0];
|
|
2613
2862
|
if (expiresAt != null && expiresAt <= Date.now()) {
|
|
@@ -2620,10 +2869,16 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
2620
2869
|
const name = msg.name || "default";
|
|
2621
2870
|
const status = Number(msg.status) || 200;
|
|
2622
2871
|
const headers = msg.headers || {};
|
|
2623
|
-
const cacheControl = String(
|
|
2872
|
+
const cacheControl = String(
|
|
2873
|
+
headers["cache-control"] || headers["Cache-Control"] || "",
|
|
2874
|
+
).toLowerCase();
|
|
2624
2875
|
// Mirrors Workers' own caches.default.put(): a 206 or an explicit
|
|
2625
2876
|
// no-store/private is a silent no-op, not an error.
|
|
2626
|
-
if (
|
|
2877
|
+
if (
|
|
2878
|
+
status === 206 ||
|
|
2879
|
+
cacheControl.indexOf("no-store") !== -1 ||
|
|
2880
|
+
cacheControl.indexOf("private") !== -1
|
|
2881
|
+
) {
|
|
2627
2882
|
return { ok: true, stored: false };
|
|
2628
2883
|
}
|
|
2629
2884
|
const sMaxAge = /s-maxage=(\d+)/.exec(cacheControl);
|
|
@@ -2731,7 +2986,10 @@ globalThis.__sbFetchUpstream = function (op, msg) {
|
|
|
2731
2986
|
};
|
|
2732
2987
|
|
|
2733
2988
|
globalThis.__sbAssetsGet = function (path) {
|
|
2734
|
-
|
|
2989
|
+
const reply = __sbEmbeddedDispatch({ op: "assets.get", path });
|
|
2990
|
+
const manifest = globalThis.__sbAssets.manifest || {};
|
|
2991
|
+
reply.rules = { headers: manifest.headers || [], redirects: manifest.redirects || [] };
|
|
2992
|
+
return reply;
|
|
2735
2993
|
};
|
|
2736
2994
|
|
|
2737
2995
|
/** The transport contract: one request string in, one reply string out. */
|
|
@@ -2852,17 +3110,36 @@ globalThis.__sbStartLocalTriggers = function (handlers, bindings) {
|
|
|
2852
3110
|
const now = Date.now();
|
|
2853
3111
|
for (let q = 0; q < queues.length; q++) {
|
|
2854
3112
|
const name = queues[q];
|
|
2855
|
-
const
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
3113
|
+
const claim = String(now) + ":" + String(Math.random());
|
|
3114
|
+
__sbSql(store, "BEGIN IMMEDIATE", []);
|
|
3115
|
+
let due;
|
|
3116
|
+
try {
|
|
3117
|
+
__sbSql(store, "UPDATE mq SET dead=1 WHERE queue=? AND visible_at<=? AND attempts>=5", [
|
|
3118
|
+
name,
|
|
3119
|
+
now,
|
|
3120
|
+
]);
|
|
3121
|
+
due = __sbSql(
|
|
3122
|
+
store,
|
|
3123
|
+
"SELECT id,body,attempts FROM mq WHERE queue=? AND dead=0 AND visible_at<=? ORDER BY visible_at LIMIT 10",
|
|
3124
|
+
[name, now],
|
|
3125
|
+
);
|
|
3126
|
+
for (let i = 0; i < due.rows.length; i++)
|
|
3127
|
+
__sbSql(
|
|
3128
|
+
store,
|
|
3129
|
+
"UPDATE mq SET visible_at=?,claim_token=?,attempts=attempts+1 WHERE queue=? AND id=?",
|
|
3130
|
+
[now + 30000, claim, name, due.rows[i][0]],
|
|
3131
|
+
);
|
|
3132
|
+
__sbSql(store, "COMMIT", []);
|
|
3133
|
+
} catch (error) {
|
|
3134
|
+
__sbSql(store, "ROLLBACK", []);
|
|
3135
|
+
throw error;
|
|
3136
|
+
}
|
|
2860
3137
|
if (due.rows.length === 0) continue;
|
|
2861
3138
|
// Hide the batch first, so a slow handler cannot have it delivered twice.
|
|
2862
3139
|
const messages = [];
|
|
2863
3140
|
for (let i = 0; i < due.rows.length; i++) {
|
|
2864
3141
|
const row = due.rows[i];
|
|
2865
|
-
|
|
3142
|
+
|
|
2866
3143
|
messages.push({ id: row[0], body: row[1], timestamp: now, attempts: Number(row[2]) + 1 });
|
|
2867
3144
|
}
|
|
2868
3145
|
// #171 — same ctx.waitUntil as the broker-delivered path, and the same
|
|
@@ -2879,13 +3156,17 @@ globalThis.__sbStartLocalTriggers = function (handlers, bindings) {
|
|
|
2879
3156
|
if (result && __sbIsFn(result.then)) result = await result;
|
|
2880
3157
|
if (__sbTasks.length) await __sbDrainWaitUntil(__sbTasks);
|
|
2881
3158
|
for (let i = 0; i < result.ack.length; i++) {
|
|
2882
|
-
__sbSql(
|
|
3159
|
+
__sbSql(
|
|
3160
|
+
store,
|
|
3161
|
+
"DELETE FROM mq WHERE queue=? AND id=? AND claim_token=? AND visible_at>?",
|
|
3162
|
+
[name, result.ack[i], claim, Date.now()],
|
|
3163
|
+
);
|
|
2883
3164
|
}
|
|
2884
3165
|
for (let i = 0; i < result.retry.length; i++) {
|
|
2885
3166
|
__sbSql(
|
|
2886
3167
|
store,
|
|
2887
|
-
"UPDATE mq SET
|
|
2888
|
-
[Date.now() + 5000, result.retry[i]],
|
|
3168
|
+
"UPDATE mq SET visible_at=?,claim_token='',dead=CASE WHEN attempts>=5 THEN 1 ELSE 0 END WHERE queue=? AND id=? AND claim_token=? AND visible_at>?",
|
|
3169
|
+
[Date.now() + 5000, name, result.retry[i], claim, Date.now()],
|
|
2889
3170
|
);
|
|
2890
3171
|
}
|
|
2891
3172
|
}
|
|
@@ -2896,25 +3177,76 @@ globalThis.__sbStartLocalTriggers = function (handlers, bindings) {
|
|
|
2896
3177
|
setInterval(async function () {
|
|
2897
3178
|
__sbEnsureSchema();
|
|
2898
3179
|
const now = Date.now();
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
3180
|
+
__sbSql(store, "BEGIN IMMEDIATE", []);
|
|
3181
|
+
let due;
|
|
3182
|
+
const claim = String(now) + ":" + String(Math.random());
|
|
3183
|
+
try {
|
|
3184
|
+
__sbSql(store,"DELETE FROM do_alarm_delivery WHERE expires<=? AND attempts>=5 AND EXISTS (SELECT 1 FROM do_alarm a WHERE a.cls=do_alarm_delivery.cls AND a.id=do_alarm_delivery.id)",[now]);
|
|
3185
|
+
const pending = __sbSql(
|
|
3186
|
+
store,
|
|
3187
|
+
"SELECT cls,id,at,attempts FROM do_alarm WHERE at<=? AND NOT EXISTS (SELECT 1 FROM do_alarm_delivery d WHERE d.cls=do_alarm.cls AND d.id=do_alarm.id) ORDER BY at LIMIT 10",
|
|
3188
|
+
[now],
|
|
3189
|
+
);
|
|
3190
|
+
for (let i = 0; i < pending.rows.length; i++) {
|
|
3191
|
+
const r = pending.rows[i];
|
|
3192
|
+
__sbSql(store, "INSERT INTO do_alarm_delivery VALUES (?,?,?,?,?,?)", [
|
|
3193
|
+
r[0],
|
|
3194
|
+
r[1],
|
|
3195
|
+
r[2],
|
|
3196
|
+
r[3],
|
|
3197
|
+
"",
|
|
3198
|
+
0,
|
|
3199
|
+
]);
|
|
3200
|
+
__sbSql(store, "DELETE FROM do_alarm WHERE cls=? AND id=? AND at=?", [r[0], r[1], r[2]]);
|
|
3201
|
+
}
|
|
3202
|
+
due = __sbSql(
|
|
3203
|
+
store,
|
|
3204
|
+
"SELECT cls,id,at,attempts FROM do_alarm_delivery WHERE expires<=? AND attempts<5 LIMIT 10",
|
|
3205
|
+
[now],
|
|
3206
|
+
);
|
|
3207
|
+
for (let i = 0; i < due.rows.length; i++)
|
|
3208
|
+
__sbSql(
|
|
3209
|
+
store,
|
|
3210
|
+
"UPDATE do_alarm_delivery SET token=?,expires=?,attempts=attempts+1 WHERE cls=? AND id=?",
|
|
3211
|
+
[claim, now + 30000, due.rows[i][0], due.rows[i][1]],
|
|
3212
|
+
);
|
|
3213
|
+
__sbSql(store, "COMMIT", []);
|
|
3214
|
+
} catch (error) {
|
|
3215
|
+
__sbSql(store, "ROLLBACK", []);
|
|
3216
|
+
throw error;
|
|
3217
|
+
}
|
|
2904
3218
|
for (let i = 0; i < due.rows.length; i++) {
|
|
2905
3219
|
const cls = due.rows[i][0];
|
|
2906
3220
|
const id = due.rows[i][1];
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3221
|
+
try {
|
|
3222
|
+
const instance = __sbGetDOInstance(cls, id);
|
|
3223
|
+
if (__sbIsFn(instance.alarm)) {
|
|
3224
|
+
const res = instance.alarm();
|
|
3225
|
+
if (res && __sbIsFn(res.then)) await res;
|
|
3226
|
+
if (instance.__sbTasks.length) await __sbDrainWaitUntil(instance.__sbTasks);
|
|
3227
|
+
}
|
|
3228
|
+
__sbSql(
|
|
3229
|
+
store,
|
|
3230
|
+
"DELETE FROM do_alarm_delivery WHERE cls=? AND id=? AND token=? AND expires>?",
|
|
3231
|
+
[cls, id, claim, Date.now()],
|
|
3232
|
+
);
|
|
3233
|
+
} catch {
|
|
3234
|
+
const scheduled = __sbSql(store, "SELECT 1 FROM do_alarm WHERE cls=? AND id=?", [
|
|
3235
|
+
cls,
|
|
3236
|
+
id,
|
|
3237
|
+
]);
|
|
3238
|
+
if (scheduled.rows.length)
|
|
3239
|
+
__sbSql(
|
|
3240
|
+
store,
|
|
3241
|
+
"DELETE FROM do_alarm_delivery WHERE cls=? AND id=? AND token=? AND expires>?",
|
|
3242
|
+
[cls, id, claim, Date.now()],
|
|
3243
|
+
);
|
|
3244
|
+
else
|
|
3245
|
+
__sbSql(
|
|
3246
|
+
store,
|
|
3247
|
+
"UPDATE do_alarm_delivery SET expires=? WHERE cls=? AND id=? AND token=? AND expires>?",
|
|
3248
|
+
[Date.now() + 5000, cls, id, claim, Date.now()],
|
|
3249
|
+
);
|
|
2918
3250
|
}
|
|
2919
3251
|
}
|
|
2920
3252
|
}, 500);
|