@sproutboat/runtime 0.2.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.
- package/CHANGELOG.md +7 -0
- package/README.md +6 -0
- package/package.json +11 -0
- package/src/index.ts +10 -0
- package/src/native-fetch-prelude.js +950 -0
- package/src/source.ts +44 -0
- package/src/transport-broker.js +299 -0
- package/src/transport-embedded.js +1574 -0
- package/src/wrap.ts +267 -0
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
// Prepended to every handler by tools/compile.ts, before Porffor's native-fetch
|
|
2
|
+
// esbuild bundle. Porffor's runtime/fetch-globals.js (checked through alpha-4)
|
|
3
|
+
// gives URL (href/origin/pathname/search only) and Response without a static
|
|
4
|
+
// json(). This adds, additively, the rest of the WHATWG surface Worker code
|
|
5
|
+
// expects: URLSearchParams (read + write), URL.prototype.searchParams and the
|
|
6
|
+
// protocol/host/hostname/port/hash accessors, static Response.json,
|
|
7
|
+
// crypto.randomUUID / crypto.getRandomValues, and structuredClone. Each is
|
|
8
|
+
// feature-detected; delete a block once Porffor ships that global.
|
|
9
|
+
// Tracked upstream in patches/UPSTREAM.md.
|
|
10
|
+
//
|
|
11
|
+
// Declared before it is referenced: a getter body that names a later top-level
|
|
12
|
+
// class throws ReferenceError in Porffor (see patches/UPSTREAM.md draft B).
|
|
13
|
+
|
|
14
|
+
// Duck-typing helpers, `typeof`-free (the repo's anti-slop lint bans `typeof`;
|
|
15
|
+
// these express the same spec-mandated checks and are verified under Porffor
|
|
16
|
+
// alpha-4 by examples/kitchen-sink/harness.ts).
|
|
17
|
+
function __sbIsStr(v) {
|
|
18
|
+
return Object(v) !== v && v === String(v);
|
|
19
|
+
}
|
|
20
|
+
function __sbIsFn(v) {
|
|
21
|
+
return v instanceof Function;
|
|
22
|
+
}
|
|
23
|
+
function __sbIsObj(v) {
|
|
24
|
+
return v !== null && Object(v) === v;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// #41 — cold-start phase marker. Runs as the first thing in the bundle: writes
|
|
28
|
+
// the current wall-clock ms to $SB_STARTUP_FILE so the supervisor can split
|
|
29
|
+
// cold-start into "spawn -> JS starts" (process + runtime bootstrap) and
|
|
30
|
+
// "JS starts -> listening" (module eval + server bind). No-op when unset.
|
|
31
|
+
function __sbStartupMark() {
|
|
32
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
33
|
+
Porffor.c`
|
|
34
|
+
const char* __f = getenv("SB_STARTUP_FILE");
|
|
35
|
+
if (__f) {
|
|
36
|
+
struct timespec __ts;
|
|
37
|
+
clock_gettime(CLOCK_REALTIME, &__ts);
|
|
38
|
+
double __ms = (double)__ts.tv_sec * 1000.0 + (double)__ts.tv_nsec / 1000000.0;
|
|
39
|
+
char __buf[32];
|
|
40
|
+
int __n = snprintf(__buf, sizeof(__buf), "%.0f", __ms);
|
|
41
|
+
int __fd = open(__f, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
|
42
|
+
if (__fd >= 0) { write(__fd, __buf, (size_t)__n); close(__fd); }
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
__sbStartupMark();
|
|
47
|
+
|
|
48
|
+
// #28 — process CPU time in ms (CLOCK_PROCESS_CPUTIME_ID). Marshalled back as a
|
|
49
|
+
// string via the same primitives as __sbEnv / __sbRandomBytes (proven working),
|
|
50
|
+
// then parsed — Porffor's number boxing for a bare inline-C assignment is not
|
|
51
|
+
// relied on. `__sbEntry` samples it around the handler for per-invocation CPU.
|
|
52
|
+
function __sbCpuMs() {
|
|
53
|
+
let res = "";
|
|
54
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
55
|
+
Porffor.c`
|
|
56
|
+
struct timespec __ts;
|
|
57
|
+
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &__ts);
|
|
58
|
+
double __ms = (double)__ts.tv_sec * 1000.0 + (double)__ts.tv_nsec / 1000000.0;
|
|
59
|
+
char __b[32];
|
|
60
|
+
int __n = snprintf(__b, sizeof(__b), "%.3f", __ms);
|
|
61
|
+
if (__n > 0) res = porf_box((f64)porf_native_fetch_alloc_bytestring(__b, (size_t)__n), 195);
|
|
62
|
+
`;
|
|
63
|
+
return res === "" ? 0 : parseFloat(res);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// #28 — stamp `x-sb-cpu-ms` onto a handler Response. Porffor alpha-4's
|
|
67
|
+
// native-fetch serializer only reads headers from the plain object passed to
|
|
68
|
+
// `new Response(body, { headers })` — a later `.set()` or a `Headers` instance
|
|
69
|
+
// is ignored on the wire — so the metric is carried by rebuilding the Response
|
|
70
|
+
// with one extra header. Safe only for a string body (the norm under
|
|
71
|
+
// http-sync-v0) with no Set-Cookie to comma-fold; anything else is returned
|
|
72
|
+
// untouched and the edge simply omits cpuMs for that request.
|
|
73
|
+
function __sbTagCpu(res, t0) {
|
|
74
|
+
const cpu = __sbCpuMs() - t0;
|
|
75
|
+
try {
|
|
76
|
+
const body = res && res.body;
|
|
77
|
+
if (__sbIsStr(body) && !res.headers.has("set-cookie")) {
|
|
78
|
+
const headers = {};
|
|
79
|
+
res.headers.forEach(function (value, name) {
|
|
80
|
+
headers[name] = value;
|
|
81
|
+
});
|
|
82
|
+
headers["x-sb-cpu-ms"] = (cpu >= 0 ? cpu : 0).toFixed(3);
|
|
83
|
+
return new Response(body, { status: res.status, headers: headers });
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
/* fall through to the original response */
|
|
87
|
+
}
|
|
88
|
+
return res;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class __SproutboatURLSearchParams {
|
|
92
|
+
constructor(init) {
|
|
93
|
+
this._keys = [];
|
|
94
|
+
this._vals = [];
|
|
95
|
+
let raw = init == null ? "" : String(init);
|
|
96
|
+
if (raw.charCodeAt(0) === 63) raw = raw.slice(1); // strip a leading '?'
|
|
97
|
+
if (raw.length === 0) return;
|
|
98
|
+
const pairs = raw.split("&");
|
|
99
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
100
|
+
const pair = pairs[i];
|
|
101
|
+
if (pair.length === 0) continue;
|
|
102
|
+
const eq = pair.indexOf("=");
|
|
103
|
+
const k = eq === -1 ? pair : pair.slice(0, eq);
|
|
104
|
+
const v = eq === -1 ? "" : pair.slice(eq + 1);
|
|
105
|
+
this._keys.push(decodeURIComponent(k.split("+").join(" ")));
|
|
106
|
+
this._vals.push(decodeURIComponent(v.split("+").join(" ")));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
get(name) {
|
|
110
|
+
for (let i = 0; i < this._keys.length; i++) if (this._keys[i] === name) return this._vals[i];
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
getAll(name) {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (let i = 0; i < this._keys.length; i++) if (this._keys[i] === name) out.push(this._vals[i]);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
has(name) {
|
|
119
|
+
for (let i = 0; i < this._keys.length; i++) if (this._keys[i] === name) return true;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
forEach(cb) {
|
|
123
|
+
for (let i = 0; i < this._keys.length; i++) cb(this._vals[i], this._keys[i], this);
|
|
124
|
+
}
|
|
125
|
+
// Mutators: standalone `new URLSearchParams()` building works. They do NOT
|
|
126
|
+
// write back into a URL's `search` (Porffor's URL has no setter) — build the
|
|
127
|
+
// string with toString() and assign it yourself.
|
|
128
|
+
append(name, value) {
|
|
129
|
+
this._keys.push(String(name));
|
|
130
|
+
this._vals.push(String(value));
|
|
131
|
+
}
|
|
132
|
+
set(name, value) {
|
|
133
|
+
let found = false;
|
|
134
|
+
for (let i = 0; i < this._keys.length; i++) {
|
|
135
|
+
if (this._keys[i] !== name) continue;
|
|
136
|
+
if (found) {
|
|
137
|
+
this._keys.splice(i, 1);
|
|
138
|
+
this._vals.splice(i, 1);
|
|
139
|
+
i--;
|
|
140
|
+
} else {
|
|
141
|
+
this._vals[i] = String(value);
|
|
142
|
+
found = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!found) this.append(name, value);
|
|
146
|
+
}
|
|
147
|
+
delete(name) {
|
|
148
|
+
for (let i = 0; i < this._keys.length; i++)
|
|
149
|
+
if (this._keys[i] === name) {
|
|
150
|
+
this._keys.splice(i, 1);
|
|
151
|
+
this._vals.splice(i, 1);
|
|
152
|
+
i--;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
sort() {
|
|
156
|
+
const idx = this._keys
|
|
157
|
+
.map((_, i) => i)
|
|
158
|
+
.sort((a, b) => (this._keys[a] < this._keys[b] ? -1 : this._keys[a] > this._keys[b] ? 1 : 0));
|
|
159
|
+
this._keys = idx.map((i) => this._keys[i]);
|
|
160
|
+
this._vals = idx.map((i) => this._vals[i]);
|
|
161
|
+
}
|
|
162
|
+
keys() {
|
|
163
|
+
return this._keys.slice();
|
|
164
|
+
}
|
|
165
|
+
values() {
|
|
166
|
+
return this._vals.slice();
|
|
167
|
+
}
|
|
168
|
+
get size() {
|
|
169
|
+
return this._keys.length;
|
|
170
|
+
}
|
|
171
|
+
toString() {
|
|
172
|
+
let out = "";
|
|
173
|
+
for (let i = 0; i < this._keys.length; i++) {
|
|
174
|
+
if (i > 0) out += "&";
|
|
175
|
+
out += encodeURIComponent(this._keys[i]) + "=" + encodeURIComponent(this._vals[i]);
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// URL and Response are always defined by Porffor's fetch-globals.js banner.
|
|
182
|
+
if (globalThis.URLSearchParams == null) globalThis.URLSearchParams = __SproutboatURLSearchParams;
|
|
183
|
+
|
|
184
|
+
if (!("searchParams" in URL.prototype)) {
|
|
185
|
+
Object.defineProperty(URL.prototype, "searchParams", {
|
|
186
|
+
configurable: true,
|
|
187
|
+
get() {
|
|
188
|
+
if (this.__sbSearchParams == null) this.__sbSearchParams = new __SproutboatURLSearchParams(this.search);
|
|
189
|
+
return this.__sbSearchParams;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (Response.json == null) {
|
|
195
|
+
Response.json = function (data, init) {
|
|
196
|
+
const response = new Response(JSON.stringify(data), init);
|
|
197
|
+
if (!response.headers.has("content-type")) response.headers.set("content-type", "application/json;charset=utf-8");
|
|
198
|
+
return response;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Porffor's URL exposes href / origin / pathname / search only. Add the rest of
|
|
203
|
+
// the WHATWG read surface, derived from `origin` (scheme://host[:port]).
|
|
204
|
+
// `hash` is always '' server-side — browsers strip the fragment before the
|
|
205
|
+
// request, so there is nothing to recover. Tracked upstream (patches/UPSTREAM.md).
|
|
206
|
+
function __sbDefineURLAccessor(name, get) {
|
|
207
|
+
if (!(name in URL.prototype)) Object.defineProperty(URL.prototype, name, { configurable: true, get });
|
|
208
|
+
}
|
|
209
|
+
__sbDefineURLAccessor("protocol", function () {
|
|
210
|
+
const i = this.origin.indexOf("://");
|
|
211
|
+
return i === -1 ? "" : this.origin.slice(0, i + 1);
|
|
212
|
+
});
|
|
213
|
+
__sbDefineURLAccessor("host", function () {
|
|
214
|
+
const i = this.origin.indexOf("://");
|
|
215
|
+
return i === -1 ? "" : this.origin.slice(i + 3);
|
|
216
|
+
});
|
|
217
|
+
__sbDefineURLAccessor("hostname", function () {
|
|
218
|
+
const h = this.host;
|
|
219
|
+
const c = h.indexOf(":");
|
|
220
|
+
return c === -1 ? h : h.slice(0, c);
|
|
221
|
+
});
|
|
222
|
+
__sbDefineURLAccessor("port", function () {
|
|
223
|
+
const h = this.host;
|
|
224
|
+
const c = h.indexOf(":");
|
|
225
|
+
return c === -1 ? "" : h.slice(c + 1);
|
|
226
|
+
});
|
|
227
|
+
__sbDefineURLAccessor("hash", function () {
|
|
228
|
+
return "";
|
|
229
|
+
});
|
|
230
|
+
__sbDefineURLAccessor("username", function () {
|
|
231
|
+
return "";
|
|
232
|
+
});
|
|
233
|
+
__sbDefineURLAccessor("password", function () {
|
|
234
|
+
return "";
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// crypto.randomUUID / getRandomValues are absent in native-fetch. Provide them
|
|
238
|
+
// backed by the OS CSPRNG (`__sbRandomBytes` -> inline C -> /dev/urandom), so
|
|
239
|
+
// tokens, idempotency keys and UUIDs are unpredictable. Deliberately no insecure
|
|
240
|
+
// fallback — a silent downgrade to a weak source is worse than throwing.
|
|
241
|
+
if (globalThis.crypto == null) globalThis.crypto = {};
|
|
242
|
+
if (globalThis.crypto.getRandomValues == null) {
|
|
243
|
+
globalThis.crypto.getRandomValues = function (view) {
|
|
244
|
+
const n = view.length >>> 0;
|
|
245
|
+
// WebCrypto caps a single call at 65536 bytes.
|
|
246
|
+
if (n > 65536) throw new RangeError("crypto.getRandomValues: byte length exceeds 65536");
|
|
247
|
+
if (n === 0) return view;
|
|
248
|
+
// One CSPRNG byte per element. Correct for Uint8Array (and randomUUID); a
|
|
249
|
+
// wider view gets its low byte filled, matching the previous polyfill's shape.
|
|
250
|
+
const bytes = __sbRandomBytes(String(n));
|
|
251
|
+
if (bytes.length !== n) throw new Error("crypto.getRandomValues: OS entropy source unavailable");
|
|
252
|
+
for (let i = 0; i < n; i++) view[i] = bytes.charCodeAt(i) & 0xff;
|
|
253
|
+
return view;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// structuredClone: JSON round-trip. Lossy (no Map/Set/Date/typed arrays), but
|
|
257
|
+
// covers the common "deep-copy a plain object" case Worker code relies on.
|
|
258
|
+
if (globalThis.structuredClone == null) {
|
|
259
|
+
// The suggested fix (use structuredClone) is circular — this IS the polyfill,
|
|
260
|
+
// and Porffor exposes no other deep-clone primitive.
|
|
261
|
+
// react-doctor-disable-next-line react-doctor/no-json-parse-stringify-clone
|
|
262
|
+
globalThis.structuredClone = function (value) {
|
|
263
|
+
return JSON.parse(JSON.stringify(value));
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (globalThis.crypto.randomUUID == null) {
|
|
268
|
+
globalThis.crypto.randomUUID = function () {
|
|
269
|
+
const b = new Uint8Array(16);
|
|
270
|
+
globalThis.crypto.getRandomValues(b);
|
|
271
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
272
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
273
|
+
const h = [];
|
|
274
|
+
for (let i = 0; i < 16; i++) h.push((b[i] + 0x100).toString(16).slice(1));
|
|
275
|
+
return `${h[0]}${h[1]}${h[2]}${h[3]}-${h[4]}${h[5]}-${h[6]}${h[7]}-${h[8]}${h[9]}-${h[10]}${h[11]}${h[12]}${h[13]}${h[14]}${h[15]}`;
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Bindings: env.<KV>, env.<SECRET>, env.<D1>, env.<R2>, and globalThis.fetch,
|
|
281
|
+
// backed by a Bun broker on a loopback TCP port. The transport is inline C —
|
|
282
|
+
// blocking write/read per call over ONE long-lived connection (http-sync-v0: one
|
|
283
|
+
// sprout event-loop turn per request, so a blocking roundtrip is acceptable).
|
|
284
|
+
// Wire frame:
|
|
285
|
+
// [u32 LE len][ <token> "\n" <json> ] reply: [u32 LE len][ <json> ]
|
|
286
|
+
// Shared C preamble: the headers, the two Porffor marshalling helpers every
|
|
287
|
+
// inline-C block uses, and the CSPRNG. Lives here rather than in a transport so
|
|
288
|
+
// both transports — and __sbRandomBytes / __sbEnv below — compile against the
|
|
289
|
+
// same declarations.
|
|
290
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
291
|
+
Porffor.c`
|
|
292
|
+
#include <sys/socket.h>
|
|
293
|
+
#include <netinet/in.h>
|
|
294
|
+
#include <netinet/tcp.h>
|
|
295
|
+
#include <signal.h>
|
|
296
|
+
#include <unistd.h>
|
|
297
|
+
#include <string.h>
|
|
298
|
+
#include <stdlib.h>
|
|
299
|
+
#include <stdio.h>
|
|
300
|
+
#include <fcntl.h>
|
|
301
|
+
#include <errno.h>
|
|
302
|
+
#include <time.h>
|
|
303
|
+
|
|
304
|
+
u32 porf_native_fetch_alloc_bytestring(const char* input, size_t len);
|
|
305
|
+
int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned);
|
|
306
|
+
|
|
307
|
+
// Fill buf with n bytes from the OS CSPRNG. /dev/urandom is present on Linux and
|
|
308
|
+
// macOS and inside the bubblewrap sandbox; blocking is not a concern after the
|
|
309
|
+
// pool is seeded. Returns 0, or -1 if the source could not be read in full.
|
|
310
|
+
static int sb_os_random(unsigned char* buf, size_t n) {
|
|
311
|
+
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
|
|
312
|
+
if (fd < 0) return -1;
|
|
313
|
+
size_t off = 0;
|
|
314
|
+
while (off < n) {
|
|
315
|
+
long r = read(fd, buf + off, n - off);
|
|
316
|
+
if (r <= 0) {
|
|
317
|
+
if (r < 0 && errno == EINTR) continue;
|
|
318
|
+
close(fd);
|
|
319
|
+
return -1;
|
|
320
|
+
}
|
|
321
|
+
off += (size_t)r;
|
|
322
|
+
}
|
|
323
|
+
close(fd);
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
`;
|
|
328
|
+
|
|
329
|
+
// TRANSPORT: wrap.ts splices one of transport-broker.js / transport-embedded.js here.
|
|
330
|
+
|
|
331
|
+
// `nStr` is the decimal byte count as a string (same string-param pattern as
|
|
332
|
+
// __sbEnv). Returns a bytestring of that many CSPRNG bytes, or '' on failure.
|
|
333
|
+
// oxlint-disable-next-line no-unused-vars -- `nStr` is read inside the RawC block below, not by JS.
|
|
334
|
+
function __sbRandomBytes(nStr) {
|
|
335
|
+
let out = "";
|
|
336
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
337
|
+
Porffor.c`
|
|
338
|
+
const char* __ns; size_t __nsl; char* __nso = 0;
|
|
339
|
+
porf_native_fetch_read_value(nStr, &__ns, &__nsl, &__nso);
|
|
340
|
+
char __nb[16];
|
|
341
|
+
size_t __k = __nsl < 15 ? __nsl : 15;
|
|
342
|
+
memcpy(__nb, __ns, __k); __nb[__k] = 0;
|
|
343
|
+
if (__nso) free(__nso);
|
|
344
|
+
long __n = atol(__nb);
|
|
345
|
+
if (__n > 0 && __n <= 65536) {
|
|
346
|
+
unsigned char* __b = (unsigned char*)malloc((size_t)__n);
|
|
347
|
+
if (__b) {
|
|
348
|
+
if (sb_os_random(__b, (size_t)__n) == 0)
|
|
349
|
+
out = porf_box((f64)porf_native_fetch_alloc_bytestring((const char*)__b, (size_t)__n), 195);
|
|
350
|
+
free(__b);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
`;
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// #63 — every request carries the protocol version it was built against and an
|
|
358
|
+
// id unique to this process. The id is what makes a resend safe: the transport
|
|
359
|
+
// retries the *same bytes*, so a broker that already applied the request can
|
|
360
|
+
// recognise it and replay its answer instead of applying it twice.
|
|
361
|
+
var __sbReqId = 0;
|
|
362
|
+
|
|
363
|
+
function __sbRpc(op, extra) {
|
|
364
|
+
const req = { v: 1, id: ++__sbReqId, op };
|
|
365
|
+
if (extra) for (const k in extra) req[k] = extra[k];
|
|
366
|
+
const reply = JSON.parse(__sbCall(JSON.stringify(req)));
|
|
367
|
+
if (reply && reply.ok === false) throw new Error(`sproutboat ${op}: ${reply.error || "failed"}`);
|
|
368
|
+
return reply;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// D1: a Cloudflare-shaped `env.<DB>` (prepare / bind / all / run / raw / first,
|
|
372
|
+
// plus batch and exec). Every call is one broker roundtrip.
|
|
373
|
+
function __sbMakeD1(dbName) {
|
|
374
|
+
function stmt(sql, params) {
|
|
375
|
+
const s = {
|
|
376
|
+
__sql: sql,
|
|
377
|
+
__params: params,
|
|
378
|
+
bind() {
|
|
379
|
+
return stmt(sql, Array.prototype.slice.call(arguments));
|
|
380
|
+
},
|
|
381
|
+
all() {
|
|
382
|
+
const r = __sbRpc("d1.query", { db: dbName, sql, params });
|
|
383
|
+
return { results: r.results || [], success: true, meta: r.meta || {} };
|
|
384
|
+
},
|
|
385
|
+
run() {
|
|
386
|
+
return s.all();
|
|
387
|
+
},
|
|
388
|
+
raw() {
|
|
389
|
+
const rows = s.all().results;
|
|
390
|
+
const out = [];
|
|
391
|
+
for (let i = 0; i < rows.length; i++) {
|
|
392
|
+
const cols = [];
|
|
393
|
+
for (const k in rows[i]) cols.push(rows[i][k]);
|
|
394
|
+
out.push(cols);
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
},
|
|
398
|
+
first(column) {
|
|
399
|
+
const rows = s.all().results;
|
|
400
|
+
if (rows.length === 0) return null;
|
|
401
|
+
return column == null ? rows[0] : rows[0][column];
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
return s;
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
prepare(sql) {
|
|
408
|
+
return stmt(String(sql), []);
|
|
409
|
+
},
|
|
410
|
+
batch(statements) {
|
|
411
|
+
const list = [];
|
|
412
|
+
for (let i = 0; i < (statements || []).length; i++)
|
|
413
|
+
list.push({ sql: statements[i].__sql, params: statements[i].__params });
|
|
414
|
+
const r = __sbRpc("d1.batch", { db: dbName, statements: list });
|
|
415
|
+
const out = [];
|
|
416
|
+
for (let i = 0; i < (r.results || []).length; i++)
|
|
417
|
+
out.push({ results: r.results[i].results || [], success: true, meta: r.results[i].meta || {} });
|
|
418
|
+
return out;
|
|
419
|
+
},
|
|
420
|
+
exec(sql) {
|
|
421
|
+
__sbRpc("d1.exec", { db: dbName, sql: String(sql) });
|
|
422
|
+
return { count: (String(sql).match(/;/g) || []).length, duration: 0 };
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// R2: a Cloudflare-shaped object. When `body` is present the sync accessors
|
|
428
|
+
// mirror R2ObjectBody's async ones (a sprout may `await` them harmlessly).
|
|
429
|
+
function __sbR2Object(meta, body) {
|
|
430
|
+
const obj = {
|
|
431
|
+
key: meta.key,
|
|
432
|
+
size: meta.size,
|
|
433
|
+
etag: meta.etag,
|
|
434
|
+
httpEtag: '"' + meta.etag + '"',
|
|
435
|
+
uploaded: meta.uploaded,
|
|
436
|
+
httpMetadata: meta.httpMetadata || {},
|
|
437
|
+
customMetadata: meta.customMetadata || {},
|
|
438
|
+
};
|
|
439
|
+
if (body != null) {
|
|
440
|
+
obj.body = body;
|
|
441
|
+
obj.text = function () {
|
|
442
|
+
return body;
|
|
443
|
+
};
|
|
444
|
+
obj.json = function () {
|
|
445
|
+
return JSON.parse(body);
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
return obj;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Installed only when the project declares bindings. `env` is the module-scoped
|
|
452
|
+
// object from compile.ts (a `const`, but mutable); we add the binding accessors
|
|
453
|
+
// to it in place. compile.ts emits `__sbInstallBindings(env, {...})` right after
|
|
454
|
+
// the `const env = {...}` line.
|
|
455
|
+
globalThis.__sbInstallBindings = function (target, bindings) {
|
|
456
|
+
if (!target) return;
|
|
457
|
+
|
|
458
|
+
for (let i = 0; i < (bindings.kv || []).length; i++) {
|
|
459
|
+
const ns = bindings.kv[i];
|
|
460
|
+
target[ns] = {
|
|
461
|
+
get(key) {
|
|
462
|
+
const r = __sbRpc("kv.get", { ns, key: String(key) });
|
|
463
|
+
return r.found ? r.value : null;
|
|
464
|
+
},
|
|
465
|
+
put(key, value) {
|
|
466
|
+
__sbRpc("kv.put", { ns, key: String(key), value: String(value) });
|
|
467
|
+
},
|
|
468
|
+
delete(key) {
|
|
469
|
+
__sbRpc("kv.delete", { ns, key: String(key) });
|
|
470
|
+
},
|
|
471
|
+
list(prefix) {
|
|
472
|
+
return __sbRpc("kv.list", { ns, prefix: prefix == null ? "" : String(prefix) }).keys || [];
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
for (let i = 0; i < (bindings.secrets || []).length; i++) {
|
|
478
|
+
const name = bindings.secrets[i];
|
|
479
|
+
// Fetch lazily, then freeze as a data property: a secret is process-lifetime
|
|
480
|
+
// immutable (a new value means a redeploy = a new process), so one broker
|
|
481
|
+
// round-trip on first read, zero after. A getter that RPCs on every access
|
|
482
|
+
// turns `'Bearer ' + env.KEY` in a loop into a syscall storm.
|
|
483
|
+
Object.defineProperty(target, name, {
|
|
484
|
+
configurable: true,
|
|
485
|
+
get() {
|
|
486
|
+
const value = __sbRpc("secret.get", { name }).value;
|
|
487
|
+
Object.defineProperty(target, name, { value, configurable: true, enumerable: true });
|
|
488
|
+
return value;
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
for (let i = 0; i < (bindings.d1 || []).length; i++) {
|
|
494
|
+
const name = bindings.d1[i];
|
|
495
|
+
target[name] = __sbMakeD1(name);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
for (let i = 0; i < (bindings.r2 || []).length; i++) {
|
|
499
|
+
const name = bindings.r2[i];
|
|
500
|
+
target[name] = {
|
|
501
|
+
put(key, value, options) {
|
|
502
|
+
const o = options || {};
|
|
503
|
+
// #56 — the body goes out of band where the transport allows it.
|
|
504
|
+
return __sbR2Put(
|
|
505
|
+
name,
|
|
506
|
+
String(key),
|
|
507
|
+
value == null ? "" : String(value),
|
|
508
|
+
o.httpMetadata || {},
|
|
509
|
+
o.customMetadata || {},
|
|
510
|
+
).object;
|
|
511
|
+
},
|
|
512
|
+
get(key) {
|
|
513
|
+
// #56 — bytes come back out of band on a transport that supports it, so
|
|
514
|
+
// an object body is never JSON-escaped into a frame.
|
|
515
|
+
const r = __sbR2Get(name, String(key));
|
|
516
|
+
return r.found ? __sbR2Object(r.object, r.body == null ? "" : r.body) : null;
|
|
517
|
+
},
|
|
518
|
+
head(key) {
|
|
519
|
+
const r = __sbRpc("r2.head", { bucket: name, key: String(key) });
|
|
520
|
+
return r.found ? __sbR2Object(r.object, null) : null;
|
|
521
|
+
},
|
|
522
|
+
delete(key) {
|
|
523
|
+
__sbRpc("r2.delete", { bucket: name, key: String(key) });
|
|
524
|
+
},
|
|
525
|
+
list(options) {
|
|
526
|
+
const o = options || {};
|
|
527
|
+
const r = __sbRpc("r2.list", {
|
|
528
|
+
bucket: name,
|
|
529
|
+
prefix: o.prefix == null ? "" : String(o.prefix),
|
|
530
|
+
cursor: o.cursor == null ? "" : String(o.cursor),
|
|
531
|
+
limit: o.limit == null ? 1000 : o.limit,
|
|
532
|
+
});
|
|
533
|
+
const objects = [];
|
|
534
|
+
for (let j = 0; j < (r.objects || []).length; j++) objects.push(__sbR2Object(r.objects[j], null));
|
|
535
|
+
return { objects, truncated: !!r.truncated, cursor: r.cursor || undefined };
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
for (let i = 0; i < (bindings.queues || []).length; i++) {
|
|
541
|
+
const name = bindings.queues[i];
|
|
542
|
+
target[name] = {
|
|
543
|
+
send(body, options) {
|
|
544
|
+
const o = options || {};
|
|
545
|
+
__sbRpc("queue.send", {
|
|
546
|
+
queue: name,
|
|
547
|
+
body: __sbIsStr(body) ? body : JSON.stringify(body),
|
|
548
|
+
delaySeconds: o.delaySeconds || 0,
|
|
549
|
+
});
|
|
550
|
+
},
|
|
551
|
+
sendBatch(messages) {
|
|
552
|
+
const list = [];
|
|
553
|
+
for (let j = 0; j < (messages || []).length; j++) {
|
|
554
|
+
const m = messages[j];
|
|
555
|
+
list.push({ body: __sbIsStr(m.body) ? m.body : JSON.stringify(m.body), delaySeconds: m.delaySeconds || 0 });
|
|
556
|
+
}
|
|
557
|
+
__sbRpc("queue.send_batch", { queue: name, messages: list });
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
for (let i = 0; i < (bindings.analytics || []).length; i++) {
|
|
563
|
+
const name = bindings.analytics[i];
|
|
564
|
+
target[name] = {
|
|
565
|
+
writeDataPoint(event) {
|
|
566
|
+
const e = event || {};
|
|
567
|
+
__sbRpc("ae.write", {
|
|
568
|
+
dataset: name,
|
|
569
|
+
indexes: e.indexes || [],
|
|
570
|
+
blobs: e.blobs || [],
|
|
571
|
+
doubles: e.doubles || [],
|
|
572
|
+
});
|
|
573
|
+
},
|
|
574
|
+
// Sproutboat extension (Cloudflare AE is write-only from a Worker — you
|
|
575
|
+
// query it via the SQL API). Returns { count, rows }.
|
|
576
|
+
query(options) {
|
|
577
|
+
const o = options || {};
|
|
578
|
+
return __sbRpc("ae.query", { dataset: name, limit: o.limit || 20 });
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
for (let i = 0; i < (bindings.do || []).length; i++) {
|
|
584
|
+
const b = bindings.do[i];
|
|
585
|
+
target[b.binding] = __sbMakeDONamespace(b.binding, b.className);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Static assets: env.<ASSETS>.fetch(request) -> broker `assets.get`. The edge
|
|
589
|
+
// already serves matching files directly; the sprout only calls this for paths
|
|
590
|
+
// it wants to own (SPA fallback, auth-gated files). The transport keeps the
|
|
591
|
+
// body byte-preserving for binary assets.
|
|
592
|
+
if (bindings.assets) {
|
|
593
|
+
target[bindings.assets] = {
|
|
594
|
+
fetch(input) {
|
|
595
|
+
let path = __sbIsStr(input) ? input : String((input && input.url) || "/");
|
|
596
|
+
try {
|
|
597
|
+
path = new URL(path, "http://a").pathname;
|
|
598
|
+
} catch {
|
|
599
|
+
/* use as-is */
|
|
600
|
+
}
|
|
601
|
+
const r = globalThis.__sbAssetsGet(path);
|
|
602
|
+
const headers = {};
|
|
603
|
+
if (r.type) headers["content-type"] = r.type;
|
|
604
|
+
if (r.found) headers["etag"] = '"' + r.hash + '"';
|
|
605
|
+
return new Response(r.body == null ? "" : r.body, { status: r.status || (r.found ? 200 : 404), headers });
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// #48 — worker-to-worker. Same wire shape as outbound fetch, but the broker
|
|
611
|
+
// resolves the target itself and forwards it internally, so this is not
|
|
612
|
+
// egress and is not subject to the outbound allowlist.
|
|
613
|
+
for (let i = 0; i < (bindings.services || []).length; i++) {
|
|
614
|
+
const binding = bindings.services[i].binding;
|
|
615
|
+
target[binding] = {
|
|
616
|
+
fetch(input, init) {
|
|
617
|
+
const url = __sbIsStr(input) ? input : String((input && input.url) || "https://service/");
|
|
618
|
+
const opts = init || (!__sbIsStr(input) && input) || {};
|
|
619
|
+
const headers = [];
|
|
620
|
+
if (opts.headers) {
|
|
621
|
+
if (__sbIsFn(opts.headers.forEach)) opts.headers.forEach((v, k) => headers.push([k, v]));
|
|
622
|
+
else for (const k in opts.headers) headers.push([k, opts.headers[k]]);
|
|
623
|
+
}
|
|
624
|
+
const r = __sbRpc("service.fetch", {
|
|
625
|
+
binding,
|
|
626
|
+
url,
|
|
627
|
+
method: opts.method || "GET",
|
|
628
|
+
headers,
|
|
629
|
+
body: opts.body == null ? null : String(opts.body),
|
|
630
|
+
});
|
|
631
|
+
const respHeaders = new Headers();
|
|
632
|
+
for (let j = 0; j < (r.headers || []).length; j++) respHeaders.set(r.headers[j][0], r.headers[j][1]);
|
|
633
|
+
return new Response(r.body == null ? "" : r.body, { status: r.status || 502, headers: respHeaders });
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if ((bindings.outbound || []).length > 0) {
|
|
639
|
+
globalThis.fetch = function (input, init) {
|
|
640
|
+
const url = __sbIsStr(input) ? input : String(input.url);
|
|
641
|
+
const opts = init || {};
|
|
642
|
+
const headers = [];
|
|
643
|
+
if (opts.headers) {
|
|
644
|
+
if (__sbIsFn(opts.headers.forEach)) opts.headers.forEach((v, k) => headers.push([k, v]));
|
|
645
|
+
else for (const k in opts.headers) headers.push([k, opts.headers[k]]);
|
|
646
|
+
}
|
|
647
|
+
const r = __sbRpc("fetch", {
|
|
648
|
+
url,
|
|
649
|
+
method: opts.method || "GET",
|
|
650
|
+
headers,
|
|
651
|
+
body: opts.body == null ? null : String(opts.body),
|
|
652
|
+
});
|
|
653
|
+
const respHeaders = new Headers();
|
|
654
|
+
for (let j = 0; j < (r.headers || []).length; j++) respHeaders.set(r.headers[j][0], r.headers[j][1]);
|
|
655
|
+
return new Response(r.body == null ? "" : r.body, { status: r.status || 502, headers: respHeaders });
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
// ---------------------------------------------------------------------------
|
|
661
|
+
// Durable Objects. The class runs here in the sandboxed sprout. There is exactly
|
|
662
|
+
// one sprout process per deployment (the supervisor model) and the native-fetch
|
|
663
|
+
// runtime processes one turn at a time, so calls to a given object id are
|
|
664
|
+
// already serialized — `env.<NS>.get(id).fetch()` invokes the instance directly,
|
|
665
|
+
// no round-trip. Only `state.storage.*` goes to the broker (so object state
|
|
666
|
+
// outlives a sprout restart), scoped to (class, id).
|
|
667
|
+
// ponytail: serialization relies on the single sprout process; a multi-sprout
|
|
668
|
+
// deployment needs the broker to hold a per-id lock (cloud). Storage ops are one
|
|
669
|
+
// key at a time; blockConcurrencyWhile just runs the fn.
|
|
670
|
+
|
|
671
|
+
function __sbMakeDONamespace(binding, className) {
|
|
672
|
+
return {
|
|
673
|
+
idFromName(name) {
|
|
674
|
+
return {
|
|
675
|
+
toString() {
|
|
676
|
+
return "name:" + String(name);
|
|
677
|
+
},
|
|
678
|
+
name: String(name),
|
|
679
|
+
};
|
|
680
|
+
},
|
|
681
|
+
idFromString(hex) {
|
|
682
|
+
return {
|
|
683
|
+
toString() {
|
|
684
|
+
return String(hex);
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
},
|
|
688
|
+
newUniqueId() {
|
|
689
|
+
return {
|
|
690
|
+
toString() {
|
|
691
|
+
return "uid:" + crypto.randomUUID();
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
},
|
|
695
|
+
get(id) {
|
|
696
|
+
const idStr = __sbIsStr(id) ? id : id.toString();
|
|
697
|
+
return {
|
|
698
|
+
fetch(input, init) {
|
|
699
|
+
let req;
|
|
700
|
+
if (__sbIsObj(input) && __sbIsStr(input.url) && !init) {
|
|
701
|
+
req = input;
|
|
702
|
+
} else {
|
|
703
|
+
const url = __sbIsStr(input) ? input : String((input && input.url) || "https://do/");
|
|
704
|
+
const opts = init || {};
|
|
705
|
+
const headers = new Headers();
|
|
706
|
+
if (opts.headers) {
|
|
707
|
+
if (__sbIsFn(opts.headers.forEach)) opts.headers.forEach((v, k) => headers.set(k, v));
|
|
708
|
+
else for (const k in opts.headers) headers.set(k, opts.headers[k]);
|
|
709
|
+
}
|
|
710
|
+
req = new Request(url, { method: opts.method || "GET", headers });
|
|
711
|
+
if (opts.body != null) req.body = String(opts.body);
|
|
712
|
+
}
|
|
713
|
+
return __sbGetDOInstance(className, idStr).fetch(req);
|
|
714
|
+
},
|
|
715
|
+
};
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const __sbDOClasses = {};
|
|
721
|
+
const __sbDOInstances = {};
|
|
722
|
+
globalThis.__sbRegisterDO = function (map) {
|
|
723
|
+
for (const k in map) __sbDOClasses[k] = map[k];
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
function __sbGetDOInstance(cls, id) {
|
|
727
|
+
const Ctor = __sbDOClasses[cls];
|
|
728
|
+
if (!Ctor) throw new Error("no such Durable Object class: " + cls);
|
|
729
|
+
const cacheKey = cls + " " + id;
|
|
730
|
+
let inst = __sbDOInstances[cacheKey];
|
|
731
|
+
if (!inst) {
|
|
732
|
+
const state = {
|
|
733
|
+
id: {
|
|
734
|
+
toString() {
|
|
735
|
+
return id;
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
storage: __sbDOStorage(cls, id),
|
|
739
|
+
blockConcurrencyWhile(fn) {
|
|
740
|
+
return fn();
|
|
741
|
+
},
|
|
742
|
+
waitUntil() {},
|
|
743
|
+
};
|
|
744
|
+
inst = new Ctor(state, globalThis.env);
|
|
745
|
+
__sbDOInstances[cacheKey] = inst;
|
|
746
|
+
}
|
|
747
|
+
return inst;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function __sbDOStorage(cls, id) {
|
|
751
|
+
return {
|
|
752
|
+
get(key) {
|
|
753
|
+
if (Array.isArray(key)) {
|
|
754
|
+
const out = new Map();
|
|
755
|
+
for (let i = 0; i < key.length; i++) {
|
|
756
|
+
const r = __sbRpc("do.storage.get", { cls, id, key: String(key[i]) });
|
|
757
|
+
if (r.found) out.set(key[i], JSON.parse(r.value));
|
|
758
|
+
}
|
|
759
|
+
return out;
|
|
760
|
+
}
|
|
761
|
+
const r = __sbRpc("do.storage.get", { cls, id, key: String(key) });
|
|
762
|
+
return r.found ? JSON.parse(r.value) : undefined;
|
|
763
|
+
},
|
|
764
|
+
put(key, value) {
|
|
765
|
+
if (key != null && __sbIsObj(key)) {
|
|
766
|
+
for (const k in key) __sbRpc("do.storage.put", { cls, id, key: String(k), value: JSON.stringify(key[k]) });
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
__sbRpc("do.storage.put", { cls, id, key: String(key), value: JSON.stringify(value) });
|
|
770
|
+
},
|
|
771
|
+
delete(key) {
|
|
772
|
+
if (Array.isArray(key)) {
|
|
773
|
+
let n = 0;
|
|
774
|
+
for (let i = 0; i < key.length; i++)
|
|
775
|
+
n += __sbRpc("do.storage.delete", { cls, id, key: String(key[i]) }).deleted ? 1 : 0;
|
|
776
|
+
return n;
|
|
777
|
+
}
|
|
778
|
+
return !!__sbRpc("do.storage.delete", { cls, id, key: String(key) }).deleted;
|
|
779
|
+
},
|
|
780
|
+
deleteAll() {
|
|
781
|
+
__sbRpc("do.storage.delete_all", { cls, id });
|
|
782
|
+
},
|
|
783
|
+
// #125 — alarms. Cloudflare takes a Date or epoch ms; at most one is
|
|
784
|
+
// pending per object, so setting one replaces any earlier alarm.
|
|
785
|
+
setAlarm(when) {
|
|
786
|
+
const at = when instanceof Date ? when.getTime() : Number(when);
|
|
787
|
+
__sbRpc("do.alarm.set", { cls, id, at: at });
|
|
788
|
+
},
|
|
789
|
+
getAlarm() {
|
|
790
|
+
const r = __sbRpc("do.alarm.get", { cls, id });
|
|
791
|
+
return r.at == null ? null : r.at;
|
|
792
|
+
},
|
|
793
|
+
deleteAlarm() {
|
|
794
|
+
__sbRpc("do.alarm.delete", { cls, id });
|
|
795
|
+
},
|
|
796
|
+
list(options) {
|
|
797
|
+
const o = options || {};
|
|
798
|
+
const r = __sbRpc("do.storage.list", {
|
|
799
|
+
cls,
|
|
800
|
+
id,
|
|
801
|
+
prefix: o.prefix == null ? "" : String(o.prefix),
|
|
802
|
+
limit: o.limit == null ? 1000 : o.limit,
|
|
803
|
+
});
|
|
804
|
+
const out = new Map();
|
|
805
|
+
for (let i = 0; i < (r.entries || []).length; i++) out.set(r.entries[i][0], JSON.parse(r.entries[i][1]));
|
|
806
|
+
return out;
|
|
807
|
+
},
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// ---------------------------------------------------------------------------
|
|
812
|
+
// Trigger dispatch. The compiled server only ever calls `fetch(request)`; this
|
|
813
|
+
// routes the internal `x-sb-trigger` requests (sent by the broker, authenticated
|
|
814
|
+
// with SB_BROKER_TOKEN) to the right user handler, and everything else to
|
|
815
|
+
// `handlers.fetch`.
|
|
816
|
+
|
|
817
|
+
// oxlint-disable-next-line no-unused-vars -- `name` is read inside the RawC block below, not by JS.
|
|
818
|
+
function __sbEnv(name) {
|
|
819
|
+
let res = "";
|
|
820
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
821
|
+
Porffor.c`
|
|
822
|
+
const char* __n; size_t __nl; char* __no = 0;
|
|
823
|
+
porf_native_fetch_read_value(name, &__n, &__nl, &__no);
|
|
824
|
+
char __key[128];
|
|
825
|
+
size_t __kn = __nl < 127 ? __nl : 127;
|
|
826
|
+
memcpy(__key, __n, __kn); __key[__kn] = 0;
|
|
827
|
+
if (__no) free(__no);
|
|
828
|
+
const char* __v = getenv(__key);
|
|
829
|
+
if (__v) res = porf_box((f64)porf_native_fetch_alloc_bytestring(__v, strlen(__v)), 195);
|
|
830
|
+
`;
|
|
831
|
+
return res;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function __sbTriggerAuthed(request) {
|
|
835
|
+
const want = __sbEnv("SB_BROKER_TOKEN");
|
|
836
|
+
// No token configured means no caller can be trusted to send one, so refuse
|
|
837
|
+
// rather than wave the request through. Every path that legitimately delivers
|
|
838
|
+
// a trigger over HTTP sets SB_BROKER_TOKEN — the supervisor per deployment,
|
|
839
|
+
// `sproutboat dev`, the standalone launcher. The one build that has no token
|
|
840
|
+
// is the embedded binary (#15), which fires its own triggers in-process and
|
|
841
|
+
// is also the one most likely to be listening on a public interface: exactly
|
|
842
|
+
// where "anyone may invoke scheduled()" would be a hole.
|
|
843
|
+
if (!want) return false;
|
|
844
|
+
return request.headers.get("x-sb-token") === want;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
globalThis.__sbEntry = function (handlers, request) {
|
|
848
|
+
const trigger = request.headers.get("x-sb-trigger");
|
|
849
|
+
if (!trigger) {
|
|
850
|
+
// #28 — per-invocation CPU time. One fetch turn per process (serial), so the
|
|
851
|
+
// process CPU delta across the handler is this invocation's CPU.
|
|
852
|
+
// ponytail: serial-turn assumption; revisit if the profile ever allows
|
|
853
|
+
// concurrent in-process requests.
|
|
854
|
+
//
|
|
855
|
+
// Sync handlers only. An async handler's promise is handed straight back:
|
|
856
|
+
// Porffor alpha-4's native-fetch server resolves the promise the handler
|
|
857
|
+
// itself returned, but never one derived from `.then()`, so chaining the
|
|
858
|
+
// tag on hangs the request forever. cpuMs is documented as absent for
|
|
859
|
+
// async handlers (see LogEvent in services/edge) — that is this.
|
|
860
|
+
const __t0 = __sbCpuMs();
|
|
861
|
+
const __res = handlers.fetch(request);
|
|
862
|
+
if (__res && __sbIsFn(__res.then)) return __res;
|
|
863
|
+
return __sbTagCpu(__res, __t0);
|
|
864
|
+
}
|
|
865
|
+
if (!__sbTriggerAuthed(request)) return new Response("forbidden", { status: 403 });
|
|
866
|
+
|
|
867
|
+
if (trigger === "scheduled") {
|
|
868
|
+
if (!__sbIsFn(handlers.scheduled)) return new Response("no scheduled handler", { status: 404 });
|
|
869
|
+
const body = __sbReadJson(request);
|
|
870
|
+
handlers.scheduled({ cron: body.cron || "", scheduledTime: body.scheduledTime || Date.now(), noRetry() {} });
|
|
871
|
+
return new Response("", { status: 204 });
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (trigger === "queue") {
|
|
875
|
+
if (!__sbIsFn(handlers.queue)) return new Response("no queue handler", { status: 404 });
|
|
876
|
+
const result = __sbRunQueueBatch(handlers, __sbReadJson(request));
|
|
877
|
+
return new Response(JSON.stringify(result), { headers: { "content-type": "application/json" } });
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (trigger === "alarm") {
|
|
881
|
+
const body = __sbReadJson(request);
|
|
882
|
+
const inst = __sbGetDOInstance(String(body.cls || ""), String(body.id || ""));
|
|
883
|
+
if (!__sbIsFn(inst.alarm)) return new Response("no alarm handler", { status: 404 });
|
|
884
|
+
inst.alarm();
|
|
885
|
+
return new Response("", { status: 204 });
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return new Response("unknown trigger", { status: 400 });
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Run one queue batch through the handler and report what it acked.
|
|
893
|
+
*
|
|
894
|
+
* Shared so the two ways a batch can arrive agree: over HTTP from the broker
|
|
895
|
+
* (deployed, and the phase-0 standalone launcher), or straight from the local
|
|
896
|
+
* timer in an embedded binary that has no broker to be delivered from.
|
|
897
|
+
*/
|
|
898
|
+
function __sbRunQueueBatch(handlers, body) {
|
|
899
|
+
const acked = [];
|
|
900
|
+
const retried = [];
|
|
901
|
+
const raw = body.messages || [];
|
|
902
|
+
const messages = [];
|
|
903
|
+
for (let i = 0; i < raw.length; i++) {
|
|
904
|
+
const m = raw[i];
|
|
905
|
+
const msg = {
|
|
906
|
+
id: m.id,
|
|
907
|
+
timestamp: m.timestamp,
|
|
908
|
+
attempts: m.attempts || 1,
|
|
909
|
+
body: __sbTryParse(m.body),
|
|
910
|
+
ack() {
|
|
911
|
+
if (acked.indexOf(m.id) === -1) acked.push(m.id);
|
|
912
|
+
},
|
|
913
|
+
retry() {
|
|
914
|
+
if (retried.indexOf(m.id) === -1) retried.push(m.id);
|
|
915
|
+
},
|
|
916
|
+
};
|
|
917
|
+
messages.push(msg);
|
|
918
|
+
}
|
|
919
|
+
const batch = {
|
|
920
|
+
queue: body.queue || "",
|
|
921
|
+
messages,
|
|
922
|
+
ackAll() {
|
|
923
|
+
for (let i = 0; i < messages.length; i++) messages[i].ack();
|
|
924
|
+
},
|
|
925
|
+
retryAll() {
|
|
926
|
+
for (let i = 0; i < messages.length; i++) messages[i].retry();
|
|
927
|
+
},
|
|
928
|
+
};
|
|
929
|
+
handlers.queue(batch);
|
|
930
|
+
// default: any message neither acked nor retried is treated as acked
|
|
931
|
+
for (let i = 0; i < messages.length; i++) {
|
|
932
|
+
if (acked.indexOf(messages[i].id) === -1 && retried.indexOf(messages[i].id) === -1) acked.push(messages[i].id);
|
|
933
|
+
}
|
|
934
|
+
return { ack: acked, retry: retried };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function __sbReadJson(request) {
|
|
938
|
+
try {
|
|
939
|
+
return JSON.parse(request.body == null ? "{}" : String(request.body));
|
|
940
|
+
} catch {
|
|
941
|
+
return {};
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function __sbTryParse(s) {
|
|
945
|
+
try {
|
|
946
|
+
return JSON.parse(s);
|
|
947
|
+
} catch {
|
|
948
|
+
return s;
|
|
949
|
+
}
|
|
950
|
+
}
|