@sproutboat/runtime 0.27.1 → 0.29.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 +59 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +677 -35
- package/src/transport-broker.js +159 -8
- package/src/transport-embedded.js +23 -5
- package/test/native-request-streaming.ts +221 -0
|
@@ -4745,7 +4745,7 @@ static size_t sb_utf8_encode_units(const uint16_t* s, size_t len, unsigned char*
|
|
|
4745
4745
|
|
|
4746
4746
|
`;
|
|
4747
4747
|
|
|
4748
|
-
// sproutd #22: __sbFromUtf8 for a string of bytes (one byte per unit), in C.
|
|
4748
|
+
// sproutd #22: __sbFromUtf8 for a string of bytes (one byte per unit) or a Uint8Array, in C.
|
|
4749
4749
|
// Returns "" when it cannot (not a string, or out of memory), so the caller
|
|
4750
4750
|
// falls back to the JS loop. All-ASCII byte strings are already their text.
|
|
4751
4751
|
// oxlint-disable-next-line no-unused-vars -- `bytes` is read inside the RawC block below, not by JS.
|
|
@@ -4753,9 +4753,10 @@ function __sbFromUtf8Native(bytes) {
|
|
|
4753
4753
|
let out = "";
|
|
4754
4754
|
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
4755
4755
|
Porffor.c`
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
const
|
|
4756
|
+
const int __view = bytes.type == 81 && *(int32_t*)(MEM + (u32)bytes.val) >= 0;
|
|
4757
|
+
if (bytes.type == 195 || bytes.type == 67 || __view) {
|
|
4758
|
+
const u32 __p = __view ? *(u32*)(MEM + (u32)bytes.val + 4) : (u32)bytes.val;
|
|
4759
|
+
const size_t __n = __view ? (size_t)*(int32_t*)(MEM + (u32)bytes.val) : (size_t)*(u32*)(MEM + __p);
|
|
4759
4760
|
int __ascii = bytes.type == 195;
|
|
4760
4761
|
if (__ascii) {
|
|
4761
4762
|
const unsigned char* __s = (const unsigned char*)(MEM + __p + 4);
|
|
@@ -4766,7 +4767,7 @@ function __sbFromUtf8Native(bytes) {
|
|
|
4766
4767
|
unsigned char* __b = (unsigned char*)malloc(__n + 1);
|
|
4767
4768
|
uint16_t* __u = (uint16_t*)malloc(__n * 2 + 2);
|
|
4768
4769
|
if (__b && __u) {
|
|
4769
|
-
if (bytes.type
|
|
4770
|
+
if (bytes.type != 67) memcpy(__b, MEM + __p + 4, __n);
|
|
4770
4771
|
else { const uint16_t* __w = (const uint16_t*)(MEM + __p + 4); for (size_t __i = 0; __i < __n; __i++) __b[__i] = (unsigned char)(__w[__i] & 0xff); }
|
|
4771
4772
|
int __wide = 0;
|
|
4772
4773
|
const size_t __k = sb_utf8_decode_units(__b, __n, __u, &__wide);
|
|
@@ -5904,11 +5905,18 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
5904
5905
|
delaySeconds: o.delaySeconds || 0,
|
|
5905
5906
|
});
|
|
5906
5907
|
},
|
|
5907
|
-
sendBatch(messages) {
|
|
5908
|
+
sendBatch(messages, options) {
|
|
5909
|
+
// From 2026-10-28 sendBatch(messages, { delaySeconds }) delays every message that
|
|
5910
|
+
// sets none of its own, as on Workers; before, the batch option was ignored.
|
|
5911
|
+
const batchDelay = globalThis.__sbCompat >= "2026-10-28" ? (options && options.delaySeconds) || 0 : 0;
|
|
5908
5912
|
const list = [];
|
|
5909
5913
|
for (let j = 0; j < (messages || []).length; j++) {
|
|
5910
5914
|
const m = messages[j];
|
|
5911
|
-
list.push({
|
|
5915
|
+
list.push({
|
|
5916
|
+
body: __sbIsStr(m.body) ? m.body : JSON.stringify(m.body),
|
|
5917
|
+
// A message delay of 0 (or none) takes the batch delay, as miniflare does.
|
|
5918
|
+
delaySeconds: m.delaySeconds || batchDelay,
|
|
5919
|
+
});
|
|
5912
5920
|
}
|
|
5913
5921
|
__sbRpc("queue.send_batch", { queue: name, messages: list });
|
|
5914
5922
|
},
|
|
@@ -6010,9 +6018,14 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
6010
6018
|
async function __sbServiceFetch(binding, input, init) {
|
|
6011
6019
|
const parts = __sbFetchParts(input, init);
|
|
6012
6020
|
if (parts.signal) parts.signal.throwIfAborted();
|
|
6013
|
-
// A stream body
|
|
6021
|
+
// A stream body goes out as it is read where the transport can (__sbStreamsBody), else it
|
|
6022
|
+
// is read to the end first.
|
|
6014
6023
|
const body = __sbUnwrapBody(parts.body);
|
|
6015
|
-
const wire =
|
|
6024
|
+
const wire = __sbStreamsBody(body)
|
|
6025
|
+
? { bodyStream: true, __sbBodySource: body }
|
|
6026
|
+
: body instanceof ReadableStream
|
|
6027
|
+
? __sbRequestBody(await __sbCollectBody(body))
|
|
6028
|
+
: __sbPartsBody({ ...parts, body });
|
|
6016
6029
|
const msg = { binding, url: parts.url, method: parts.method, headers: parts.headers, ...wire };
|
|
6017
6030
|
const response = await __sbFetchFollowing("service.fetch", msg, "sproutboat service " + binding, parts.redirect, parts.signal);
|
|
6018
6031
|
// The callee threw: as in workerd, its error is thrown here.
|
|
@@ -6270,7 +6283,52 @@ function __sbUpstreamResponse(r) {
|
|
|
6270
6283
|
// whole (a broker from before streaming, or sproutd's embedded transport) is handled
|
|
6271
6284
|
// as before. Cancelling the body, or the signal, drops the exchange, which stops the
|
|
6272
6285
|
// upstream request.
|
|
6286
|
+
// Streamed request bodies, from compatibility date 2026-10-27, on a transport that sends
|
|
6287
|
+
// them in frames (the broker's, on an async host, against a broker that takes them):
|
|
6288
|
+
// fetch() and service bindings send a ReadableStream body as it is read, instead of
|
|
6289
|
+
// reading it to the end first. As in workerd, a streamed body can't follow a 307 or 308.
|
|
6290
|
+
function __sbStreamsBody(body) {
|
|
6291
|
+
return (
|
|
6292
|
+
body instanceof ReadableStream &&
|
|
6293
|
+
globalThis.__sbCompat >= "2026-10-27" &&
|
|
6294
|
+
globalThis.__sbBodyStreamable != null &&
|
|
6295
|
+
globalThis.__sbBodyStreamable()
|
|
6296
|
+
);
|
|
6297
|
+
}
|
|
6298
|
+
// Reads source into the exchange under timer id `id`, a frame per chunk, while ended()
|
|
6299
|
+
// is false. Above 4 MiB not yet written it waits for the socket to catch up.
|
|
6300
|
+
async function __sbPumpRequestBody(id, source, ended) {
|
|
6301
|
+
const reader = source.getReader();
|
|
6302
|
+
try {
|
|
6303
|
+
for (;;) {
|
|
6304
|
+
const { value, done } = await reader.read();
|
|
6305
|
+
if (done) break;
|
|
6306
|
+
if (ended()) {
|
|
6307
|
+
reader.cancel();
|
|
6308
|
+
return;
|
|
6309
|
+
}
|
|
6310
|
+
let bytes = null;
|
|
6311
|
+
if (value instanceof Uint8Array) bytes = value;
|
|
6312
|
+
else if (value instanceof ArrayBuffer) bytes = new Uint8Array(value);
|
|
6313
|
+
else if (ArrayBuffer.isView(value)) bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
6314
|
+
else throw new TypeError("This ReadableStream did not return bytes.");
|
|
6315
|
+
if (bytes.length === 0) continue;
|
|
6316
|
+
let backlog = Number(globalThis.__sbAsyncBody(id, "c", __sbToBytes(bytes)));
|
|
6317
|
+
while (backlog > 4 * 1024 * 1024 && !ended()) {
|
|
6318
|
+
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
6319
|
+
backlog = Number(globalThis.__sbAsyncBody(id, "q", ""));
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
globalThis.__sbAsyncBody(id, "e", "");
|
|
6323
|
+
} catch (error) {
|
|
6324
|
+
globalThis.__sbAsyncBody(id, "x", "");
|
|
6325
|
+
__sbLogUncaught("fetch", error);
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
|
|
6273
6329
|
function __sbUpstreamStreamed(op, msg, label, signal) {
|
|
6330
|
+
const source = msg.__sbBodySource;
|
|
6331
|
+
if (source !== undefined) delete msg.__sbBodySource;
|
|
6274
6332
|
return new Promise((resolve, reject) => {
|
|
6275
6333
|
let controller = null;
|
|
6276
6334
|
let onAbort = null;
|
|
@@ -6338,6 +6396,7 @@ function __sbUpstreamStreamed(op, msg, label, signal) {
|
|
|
6338
6396
|
msg.asyncId = id;
|
|
6339
6397
|
msg.stream = true;
|
|
6340
6398
|
globalThis.__sbFetchUpstream(op, msg);
|
|
6399
|
+
if (source !== undefined) __sbPumpRequestBody(id, source, () => done);
|
|
6341
6400
|
} catch (e) {
|
|
6342
6401
|
finish();
|
|
6343
6402
|
reject(e);
|
|
@@ -6375,7 +6434,8 @@ function __sbFollowHop(op, msg, label, mode, signal, hops) {
|
|
|
6375
6434
|
next.method = "GET";
|
|
6376
6435
|
next.body = null;
|
|
6377
6436
|
delete next.bodyBinary;
|
|
6378
|
-
|
|
6437
|
+
delete next.bodyStream;
|
|
6438
|
+
} else if (msg.bodyStream === true) throw new TypeError(label + ": a streamed request body can't follow a " + res.status + " redirect");
|
|
6379
6439
|
return __sbFollowHop(op, next, label, mode, signal, hops + 1);
|
|
6380
6440
|
};
|
|
6381
6441
|
const out = __sbUpstream(op, msg, label, signal);
|
|
@@ -6435,6 +6495,7 @@ function __sbUpstream(op, msg, label, signal, always) {
|
|
|
6435
6495
|
// Worker's fetch. Installed by the generated module (wrap.ts), not here: the
|
|
6436
6496
|
// prelude must not replace fetch just by being evaluated.
|
|
6437
6497
|
globalThis.__sbInstallFetch = function () {
|
|
6498
|
+
__sbIngressStreamingSetup();
|
|
6438
6499
|
globalThis.fetch = function (input, init) {
|
|
6439
6500
|
let parts;
|
|
6440
6501
|
try {
|
|
@@ -6448,9 +6509,11 @@ globalThis.__sbInstallFetch = function () {
|
|
|
6448
6509
|
const msg = { url: parts.url, method: parts.method, headers: parts.headers, ...body };
|
|
6449
6510
|
return __sbFetchFollowing("fetch", msg, "sproutboat fetch", parts.redirect, parts.signal);
|
|
6450
6511
|
};
|
|
6451
|
-
// A stream body
|
|
6452
|
-
//
|
|
6512
|
+
// A stream body goes out as it is read, from 2026-10-27 where the transport can
|
|
6513
|
+
// (__sbStreamsBody); else it is read to the end first. It was sent as the text
|
|
6514
|
+
// "[object ReadableStream]".
|
|
6453
6515
|
const stream = __sbUnwrapBody(parts.body);
|
|
6516
|
+
if (__sbStreamsBody(stream)) return send({ bodyStream: true, __sbBodySource: stream });
|
|
6454
6517
|
if (stream instanceof ReadableStream) return __sbCollectBody(stream).then((bytes) => send(__sbRequestBody(bytes)));
|
|
6455
6518
|
return send(__sbPartsBody(parts));
|
|
6456
6519
|
};
|
|
@@ -6458,14 +6521,14 @@ globalThis.__sbInstallFetch = function () {
|
|
|
6458
6521
|
|
|
6459
6522
|
// ---------------------------------------------------------------------------
|
|
6460
6523
|
// Durable Objects. The class runs here in the sandboxed sprout. There is exactly
|
|
6461
|
-
// one sprout process per deployment (the supervisor model)
|
|
6462
|
-
//
|
|
6463
|
-
//
|
|
6464
|
-
//
|
|
6465
|
-
//
|
|
6466
|
-
//
|
|
6467
|
-
//
|
|
6468
|
-
//
|
|
6524
|
+
// one sprout process per deployment (the supervisor model), so one instance per
|
|
6525
|
+
// id: `env.<NS>.get(id).fetch()` invokes the instance directly, no round-trip.
|
|
6526
|
+
// Events still interleave at every await; from 2026-10-28 an input gate (below,
|
|
6527
|
+
// __sbDOEnter) keeps them out while a storage read is in flight, as in workerd.
|
|
6528
|
+
// Only `state.storage.*` goes to the broker (so object state outlives a sprout
|
|
6529
|
+
// restart), scoped to (class, id).
|
|
6530
|
+
// ponytail: one instance per id relies on the single sprout process; a
|
|
6531
|
+
// multi-sprout deployment needs the broker to hold a per-id lock (cloud).
|
|
6469
6532
|
|
|
6470
6533
|
function __sbMakeDONamespace(binding, className) {
|
|
6471
6534
|
return {
|
|
@@ -6514,9 +6577,14 @@ function __sbMakeDONamespace(binding, className) {
|
|
|
6514
6577
|
if (opts.body != null) req.body = String(opts.body);
|
|
6515
6578
|
}
|
|
6516
6579
|
const inst = __sbGetDOInstance(className, idStr);
|
|
6517
|
-
const
|
|
6518
|
-
|
|
6519
|
-
|
|
6580
|
+
const deliver = () => {
|
|
6581
|
+
const res = inst.fetch(req);
|
|
6582
|
+
if (inst.__sbTasks.length || (res && __sbIsFn(res.then)) || __sbHasStreamBody(res)) return __sbFetchWithDrain(res, inst.__sbTasks);
|
|
6583
|
+
return res;
|
|
6584
|
+
};
|
|
6585
|
+
// From 2026-10-28 the call waits at the object's input gate, as in workerd.
|
|
6586
|
+
if (__sbDOGated()) return __sbDOEnter(inst.__sbGate, deliver);
|
|
6587
|
+
return deliver();
|
|
6520
6588
|
},
|
|
6521
6589
|
};
|
|
6522
6590
|
},
|
|
@@ -6540,15 +6608,39 @@ function __sbGetDOInstance(cls, id) {
|
|
|
6540
6608
|
// `__sbDOInstances`), so the queue lives on `state` too and is drained by
|
|
6541
6609
|
// whichever call site invoked `fetch`/`alarm` this turn.
|
|
6542
6610
|
const __sbTasks = [];
|
|
6611
|
+
const gate = __sbDOMakeGate();
|
|
6543
6612
|
const state = {
|
|
6544
6613
|
id: {
|
|
6545
6614
|
toString() {
|
|
6546
6615
|
return id;
|
|
6547
6616
|
},
|
|
6548
6617
|
},
|
|
6549
|
-
storage: __sbDOStorage(cls, id),
|
|
6618
|
+
storage: __sbDOGated() ? __sbDOAsyncStorage(cls, id, gate) : __sbDOStorage(cls, id),
|
|
6550
6619
|
blockConcurrencyWhile(fn) {
|
|
6551
|
-
return fn();
|
|
6620
|
+
if (!__sbDOGated()) return fn();
|
|
6621
|
+
// No event enters until fn settles (2026-10-28).
|
|
6622
|
+
gate.locks++;
|
|
6623
|
+
const done = () => {
|
|
6624
|
+
gate.locks--;
|
|
6625
|
+
__sbDOPump(gate);
|
|
6626
|
+
};
|
|
6627
|
+
let r;
|
|
6628
|
+
try {
|
|
6629
|
+
r = fn();
|
|
6630
|
+
} catch (error) {
|
|
6631
|
+
done();
|
|
6632
|
+
return Promise.reject(error);
|
|
6633
|
+
}
|
|
6634
|
+
return Promise.resolve(r).then(
|
|
6635
|
+
(v) => {
|
|
6636
|
+
done();
|
|
6637
|
+
return v;
|
|
6638
|
+
},
|
|
6639
|
+
(error) => {
|
|
6640
|
+
done();
|
|
6641
|
+
throw error;
|
|
6642
|
+
},
|
|
6643
|
+
);
|
|
6552
6644
|
},
|
|
6553
6645
|
waitUntil(p) {
|
|
6554
6646
|
if (p && __sbIsFn(p.then)) __sbTasks.push(p);
|
|
@@ -6556,6 +6648,7 @@ function __sbGetDOInstance(cls, id) {
|
|
|
6556
6648
|
};
|
|
6557
6649
|
inst = new Ctor(state, globalThis.env);
|
|
6558
6650
|
inst.__sbTasks = __sbTasks;
|
|
6651
|
+
inst.__sbGate = gate;
|
|
6559
6652
|
__sbDOInstances[cacheKey] = inst;
|
|
6560
6653
|
}
|
|
6561
6654
|
return inst;
|
|
@@ -6568,12 +6661,12 @@ function __sbDOStorage(cls, id) {
|
|
|
6568
6661
|
const out = new Map();
|
|
6569
6662
|
for (let i = 0; i < key.length; i++) {
|
|
6570
6663
|
const r = __sbRpc("do.storage.get", { cls, id, key: String(key[i]) });
|
|
6571
|
-
if (r.found) out.set(key[i],
|
|
6664
|
+
if (r.found) out.set(key[i], __sbDODecode(r.value));
|
|
6572
6665
|
}
|
|
6573
6666
|
return out;
|
|
6574
6667
|
}
|
|
6575
6668
|
const r = __sbRpc("do.storage.get", { cls, id, key: String(key) });
|
|
6576
|
-
return r.found ?
|
|
6669
|
+
return r.found ? __sbDODecode(r.value) : undefined;
|
|
6577
6670
|
},
|
|
6578
6671
|
put(key, value) {
|
|
6579
6672
|
if (key != null && __sbIsObj(key)) {
|
|
@@ -6616,12 +6709,435 @@ function __sbDOStorage(cls, id) {
|
|
|
6616
6709
|
limit: o.limit == null ? 1000 : o.limit,
|
|
6617
6710
|
});
|
|
6618
6711
|
const out = new Map();
|
|
6619
|
-
for (let i = 0; i < (r.entries || []).length; i++) out.set(r.entries[i][0],
|
|
6712
|
+
for (let i = 0; i < (r.entries || []).length; i++) out.set(r.entries[i][0], __sbDODecode(r.entries[i][1]));
|
|
6713
|
+
return out;
|
|
6714
|
+
},
|
|
6715
|
+
};
|
|
6716
|
+
}
|
|
6717
|
+
|
|
6718
|
+
|
|
6719
|
+
// From compatibility date 2026-10-28 Durable Objects behave like workerd's in three ways:
|
|
6720
|
+
//
|
|
6721
|
+
// - Input gate. While a storage read is in flight, no other event (stub.fetch(), an alarm)
|
|
6722
|
+
// enters the object; the reader's continuation runs first. Reads here are synchronous, so
|
|
6723
|
+
// the gate stays shut until the next timer turn, by which point every microtask the read
|
|
6724
|
+
// woke (the reader's code up to its next real await) has run. So
|
|
6725
|
+
// `const v = await storage.get("n"); await storage.put("n", v + 1)` is safe under
|
|
6726
|
+
// concurrent calls; it used to lose updates (10 concurrent increments ended at 1). An
|
|
6727
|
+
// event that awaits anything else (fetch, a timer) lets the next one in, as in workerd.
|
|
6728
|
+
// blockConcurrencyWhile() and transaction() shut it until they settle.
|
|
6729
|
+
// - Storage methods return Promises.
|
|
6730
|
+
// - Values are structured-cloned (Date, Map, Set, RegExp, ArrayBuffer and views, BigInt,
|
|
6731
|
+
// undefined inside containers, NaN and the infinities), not JSON: put(k, undefined) and
|
|
6732
|
+
// functions throw, and list() takes start, startAfter, end and reverse.
|
|
6733
|
+
//
|
|
6734
|
+
// Before that date storage is synchronous JSON and events interleave at every await.
|
|
6735
|
+
// Values written in the new format start with __sbDOTag (no JSON text starts with "#"),
|
|
6736
|
+
// so either side reads both.
|
|
6737
|
+
function __sbDOGated() {
|
|
6738
|
+
return globalThis.__sbCompat >= "2026-10-28";
|
|
6739
|
+
}
|
|
6740
|
+
|
|
6741
|
+
function __sbDOMakeGate() {
|
|
6742
|
+
return { locks: 0, waiting: [] };
|
|
6743
|
+
}
|
|
6744
|
+
|
|
6745
|
+
// Deliver queued events while the gate is open. A delivered event that reads storage shuts
|
|
6746
|
+
// the gate again, which stops the loop.
|
|
6747
|
+
function __sbDOPump(gate) {
|
|
6748
|
+
while (gate.locks === 0 && gate.waiting.length > 0) gate.waiting.shift()();
|
|
6749
|
+
}
|
|
6750
|
+
|
|
6751
|
+
// Shut the gate until the next timer turn (see above).
|
|
6752
|
+
function __sbDOHold(gate) {
|
|
6753
|
+
gate.locks++;
|
|
6754
|
+
setTimeout(function () {
|
|
6755
|
+
gate.locks--;
|
|
6756
|
+
__sbDOPump(gate);
|
|
6757
|
+
}, 0);
|
|
6758
|
+
}
|
|
6759
|
+
|
|
6760
|
+
// Run an event now if the gate is open and nothing is queued ahead of it, else queue it.
|
|
6761
|
+
// Always a Promise, like workerd's stub.fetch().
|
|
6762
|
+
function __sbDOEnter(gate, run) {
|
|
6763
|
+
if (gate.locks === 0 && gate.waiting.length === 0) {
|
|
6764
|
+
try {
|
|
6765
|
+
return Promise.resolve(run());
|
|
6766
|
+
} catch (error) {
|
|
6767
|
+
return Promise.reject(error);
|
|
6768
|
+
}
|
|
6769
|
+
}
|
|
6770
|
+
return new Promise(function (resolve, reject) {
|
|
6771
|
+
gate.waiting.push(function () {
|
|
6772
|
+
try {
|
|
6773
|
+
resolve(run());
|
|
6774
|
+
} catch (error) {
|
|
6775
|
+
reject(error);
|
|
6776
|
+
}
|
|
6777
|
+
});
|
|
6778
|
+
});
|
|
6779
|
+
}
|
|
6780
|
+
|
|
6781
|
+
const __sbDOTag = "#sb1:";
|
|
6782
|
+
|
|
6783
|
+
function __sbDOCloneError(value) {
|
|
6784
|
+
let text = "value";
|
|
6785
|
+
try {
|
|
6786
|
+
text = String(value);
|
|
6787
|
+
} catch {
|
|
6788
|
+
/* a value whose String() throws still gets the error below */
|
|
6789
|
+
}
|
|
6790
|
+
const error = new Error(text + " could not be cloned.");
|
|
6791
|
+
error.name = "DataCloneError";
|
|
6792
|
+
return error;
|
|
6793
|
+
}
|
|
6794
|
+
|
|
6795
|
+
function __sbDOHex(bytes) {
|
|
6796
|
+
const digits = "0123456789abcdef";
|
|
6797
|
+
let out = "";
|
|
6798
|
+
for (let i = 0; i < bytes.length; i++) out += digits[bytes[i] >> 4] + digits[bytes[i] & 15];
|
|
6799
|
+
return out;
|
|
6800
|
+
}
|
|
6801
|
+
|
|
6802
|
+
function __sbDOUnhex(text) {
|
|
6803
|
+
const out = new Uint8Array(text.length / 2);
|
|
6804
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(text.slice(i * 2, i * 2 + 2), 16);
|
|
6805
|
+
return out;
|
|
6806
|
+
}
|
|
6807
|
+
|
|
6808
|
+
function __sbDOView(name, bytes) {
|
|
6809
|
+
const buf = bytes.buffer;
|
|
6810
|
+
if (name === "Uint8Array") return bytes;
|
|
6811
|
+
if (name === "Int8Array") return new Int8Array(buf);
|
|
6812
|
+
if (name === "Uint8ClampedArray") return new Uint8ClampedArray(buf);
|
|
6813
|
+
if (name === "Uint16Array") return new Uint16Array(buf);
|
|
6814
|
+
if (name === "Int16Array") return new Int16Array(buf);
|
|
6815
|
+
if (name === "Uint32Array") return new Uint32Array(buf);
|
|
6816
|
+
if (name === "Int32Array") return new Int32Array(buf);
|
|
6817
|
+
if (name === "Float32Array") return new Float32Array(buf);
|
|
6818
|
+
if (name === "Float64Array") return new Float64Array(buf);
|
|
6819
|
+
if (name === "DataView") return new DataView(buf);
|
|
6820
|
+
return bytes;
|
|
6821
|
+
}
|
|
6822
|
+
|
|
6823
|
+
// A value as JSON: null, booleans, strings and finite numbers are themselves; everything
|
|
6824
|
+
// else is an array whose first element names its type, so no plain value is ambiguous.
|
|
6825
|
+
/* oxlint-disable anti-slop/no-runtime-typeof -- structured clone branches on the value's type, as the HTML spec's StructuredSerialize does. */
|
|
6826
|
+
function __sbDOPack(v, seen) {
|
|
6827
|
+
if (v === null || v === true || v === false || __sbIsStr(v)) return v;
|
|
6828
|
+
const t = typeof v;
|
|
6829
|
+
if (t === "number") {
|
|
6830
|
+
if (v === 0 && 1 / v < 0) return ["N", "-0"];
|
|
6831
|
+
return Number.isFinite(v) ? v : ["N", String(v)];
|
|
6832
|
+
}
|
|
6833
|
+
if (t === "bigint") return ["B", String(v)];
|
|
6834
|
+
if (t === "undefined") return ["U"];
|
|
6835
|
+
if (t !== "object") throw __sbDOCloneError(v);
|
|
6836
|
+
if (seen.indexOf(v) !== -1) throw __sbDOCloneError("a circular value");
|
|
6837
|
+
seen.push(v);
|
|
6838
|
+
let out;
|
|
6839
|
+
if (v instanceof Date) out = ["D", v.getTime()];
|
|
6840
|
+
else if (v instanceof Map) {
|
|
6841
|
+
out = ["M"];
|
|
6842
|
+
v.forEach((x, k) => {
|
|
6843
|
+
out.push(__sbDOPack(k, seen), __sbDOPack(x, seen));
|
|
6844
|
+
});
|
|
6845
|
+
} else if (v instanceof Set) {
|
|
6846
|
+
out = ["S"];
|
|
6847
|
+
v.forEach((x) => {
|
|
6848
|
+
out.push(__sbDOPack(x, seen));
|
|
6849
|
+
});
|
|
6850
|
+
} else if (v instanceof ArrayBuffer) out = ["AB", __sbDOHex(new Uint8Array(v))];
|
|
6851
|
+
else if (ArrayBuffer.isView(v))
|
|
6852
|
+
out = ["V", Object.prototype.toString.call(v).slice(8, -1), __sbDOHex(new Uint8Array(v.buffer, v.byteOffset, v.byteLength))];
|
|
6853
|
+
else if (v instanceof RegExp) out = ["R", v.source, v.flags];
|
|
6854
|
+
else if (Array.isArray(v)) {
|
|
6855
|
+
out = ["A"];
|
|
6856
|
+
for (let i = 0; i < v.length; i++) out.push(__sbDOPack(v[i], seen));
|
|
6857
|
+
} else if (v instanceof Error) out = ["E", String(v.name), String(v.message)];
|
|
6858
|
+
else {
|
|
6859
|
+
out = ["O"];
|
|
6860
|
+
const keys = Object.keys(v);
|
|
6861
|
+
for (let i = 0; i < keys.length; i++) out.push(keys[i], __sbDOPack(v[keys[i]], seen));
|
|
6862
|
+
}
|
|
6863
|
+
seen.pop();
|
|
6864
|
+
return out;
|
|
6865
|
+
}
|
|
6866
|
+
/* oxlint-enable anti-slop/no-runtime-typeof */
|
|
6867
|
+
|
|
6868
|
+
function __sbDOUnpack(p) {
|
|
6869
|
+
if (!Array.isArray(p)) return p;
|
|
6870
|
+
const tag = p[0];
|
|
6871
|
+
if (tag === "U") return undefined;
|
|
6872
|
+
if (tag === "N") return p[1] === "-0" ? -0 : Number(p[1]);
|
|
6873
|
+
if (tag === "B") return BigInt(p[1]);
|
|
6874
|
+
if (tag === "D") return new Date(p[1]);
|
|
6875
|
+
if (tag === "AB") return __sbDOUnhex(p[1]).buffer;
|
|
6876
|
+
if (tag === "V") return __sbDOView(p[1], __sbDOUnhex(p[2]));
|
|
6877
|
+
if (tag === "R") return new RegExp(p[1], p[2]);
|
|
6878
|
+
if (tag === "E") {
|
|
6879
|
+
const e = new Error(p[2]);
|
|
6880
|
+
e.name = p[1];
|
|
6881
|
+
return e;
|
|
6882
|
+
}
|
|
6883
|
+
if (tag === "M") {
|
|
6884
|
+
const m = new Map();
|
|
6885
|
+
for (let i = 1; i + 1 < p.length; i += 2) m.set(__sbDOUnpack(p[i]), __sbDOUnpack(p[i + 1]));
|
|
6886
|
+
return m;
|
|
6887
|
+
}
|
|
6888
|
+
if (tag === "S") {
|
|
6889
|
+
const s = new Set();
|
|
6890
|
+
for (let i = 1; i < p.length; i++) s.add(__sbDOUnpack(p[i]));
|
|
6891
|
+
return s;
|
|
6892
|
+
}
|
|
6893
|
+
if (tag === "A") {
|
|
6894
|
+
const a = [];
|
|
6895
|
+
for (let i = 1; i < p.length; i++) a.push(__sbDOUnpack(p[i]));
|
|
6896
|
+
return a;
|
|
6897
|
+
}
|
|
6898
|
+
const o = {};
|
|
6899
|
+
for (let i = 1; i + 1 < p.length; i += 2) o[p[i]] = __sbDOUnpack(p[i + 1]);
|
|
6900
|
+
return o;
|
|
6901
|
+
}
|
|
6902
|
+
|
|
6903
|
+
function __sbDOEncode(value) {
|
|
6904
|
+
if (value === undefined) throw new TypeError("put() called with undefined value.");
|
|
6905
|
+
return __sbDOTag + JSON.stringify(__sbDOPack(value, []));
|
|
6906
|
+
}
|
|
6907
|
+
|
|
6908
|
+
// Reads both formats at every date: tagged (2026-10-28 writes) and plain JSON (earlier ones).
|
|
6909
|
+
function __sbDODecode(text) {
|
|
6910
|
+
if (__sbIsStr(text) && text.indexOf(__sbDOTag) === 0) return __sbDOUnpack(JSON.parse(text.slice(__sbDOTag.length)));
|
|
6911
|
+
return JSON.parse(text);
|
|
6912
|
+
}
|
|
6913
|
+
|
|
6914
|
+
function __sbDOListLimit(options) {
|
|
6915
|
+
const o = options || {};
|
|
6916
|
+
if (o.limit != null && !(Number(o.limit) > 0)) throw new TypeError("List limit must be positive.");
|
|
6917
|
+
return o;
|
|
6918
|
+
}
|
|
6919
|
+
|
|
6920
|
+
function __sbDOInRange(key, o) {
|
|
6921
|
+
if (o.prefix != null && key.indexOf(String(o.prefix)) !== 0) return false;
|
|
6922
|
+
if (o.start != null && key < String(o.start)) return false;
|
|
6923
|
+
if (o.startAfter != null && key <= String(o.startAfter)) return false;
|
|
6924
|
+
if (o.end != null && key >= String(o.end)) return false;
|
|
6925
|
+
return true;
|
|
6926
|
+
}
|
|
6927
|
+
|
|
6928
|
+
// The synchronous core of the 2026-10-28 storage: structured values, list() ranges.
|
|
6929
|
+
function __sbDOStrictStorage(cls, id) {
|
|
6930
|
+
const get = (key) => {
|
|
6931
|
+
const r = __sbRpc("do.storage.get", { cls, id, key: String(key) });
|
|
6932
|
+
return r.found ? __sbDODecode(r.value) : undefined;
|
|
6933
|
+
};
|
|
6934
|
+
const put = (key, value) => {
|
|
6935
|
+
__sbRpc("do.storage.put", { cls, id, key: String(key), value: __sbDOEncode(value) });
|
|
6936
|
+
};
|
|
6937
|
+
const del = (key) => !!__sbRpc("do.storage.delete", { cls, id, key: String(key) }).deleted;
|
|
6938
|
+
return {
|
|
6939
|
+
get(key) {
|
|
6940
|
+
if (!Array.isArray(key)) return get(key);
|
|
6941
|
+
const out = new Map();
|
|
6942
|
+
for (let i = 0; i < key.length; i++) {
|
|
6943
|
+
const v = get(key[i]);
|
|
6944
|
+
if (v !== undefined) out.set(key[i], v);
|
|
6945
|
+
}
|
|
6946
|
+
return out;
|
|
6947
|
+
},
|
|
6948
|
+
put(key, value) {
|
|
6949
|
+
if (key != null && __sbIsObj(key)) {
|
|
6950
|
+
// Encode every value before writing any, so one bad value writes nothing.
|
|
6951
|
+
const keys = Object.keys(key);
|
|
6952
|
+
const encoded = [];
|
|
6953
|
+
for (let i = 0; i < keys.length; i++) encoded.push(__sbDOEncode(key[keys[i]]));
|
|
6954
|
+
for (let i = 0; i < keys.length; i++) __sbRpc("do.storage.put", { cls, id, key: keys[i], value: encoded[i] });
|
|
6955
|
+
return;
|
|
6956
|
+
}
|
|
6957
|
+
put(key, value);
|
|
6958
|
+
},
|
|
6959
|
+
delete(key) {
|
|
6960
|
+
if (!Array.isArray(key)) return del(key);
|
|
6961
|
+
let n = 0;
|
|
6962
|
+
for (let i = 0; i < key.length; i++) n += del(key[i]) ? 1 : 0;
|
|
6963
|
+
return n;
|
|
6964
|
+
},
|
|
6965
|
+
deleteAll() {
|
|
6966
|
+
// workerd's deleteAll() also drops the alarm.
|
|
6967
|
+
__sbRpc("do.storage.delete_all", { cls, id });
|
|
6968
|
+
__sbRpc("do.alarm.delete", { cls, id });
|
|
6969
|
+
},
|
|
6970
|
+
setAlarm(when) {
|
|
6971
|
+
const at = when instanceof Date ? when.getTime() : when;
|
|
6972
|
+
if (!Number.isFinite(at))
|
|
6973
|
+
throw new TypeError("Failed to execute 'setAlarm' on 'DurableObjectStorage': parameter 1 is not of type 'date'.");
|
|
6974
|
+
__sbRpc("do.alarm.set", { cls, id, at: Number(at) });
|
|
6975
|
+
},
|
|
6976
|
+
getAlarm() {
|
|
6977
|
+
const r = __sbRpc("do.alarm.get", { cls, id });
|
|
6978
|
+
return r.at == null ? null : r.at;
|
|
6979
|
+
},
|
|
6980
|
+
deleteAlarm() {
|
|
6981
|
+
__sbRpc("do.alarm.delete", { cls, id });
|
|
6982
|
+
},
|
|
6983
|
+
list(options) {
|
|
6984
|
+
const o = __sbDOListLimit(options);
|
|
6985
|
+
const r = __sbRpc("do.storage.list", {
|
|
6986
|
+
cls,
|
|
6987
|
+
id,
|
|
6988
|
+
prefix: o.prefix == null ? "" : String(o.prefix),
|
|
6989
|
+
start: o.start == null ? null : String(o.start),
|
|
6990
|
+
startAfter: o.startAfter == null ? null : String(o.startAfter),
|
|
6991
|
+
end: o.end == null ? null : String(o.end),
|
|
6992
|
+
reverse: !!o.reverse,
|
|
6993
|
+
limit: o.limit == null ? 10000 : Number(o.limit),
|
|
6994
|
+
});
|
|
6995
|
+
const out = new Map();
|
|
6996
|
+
const entries = r.entries || [];
|
|
6997
|
+
// A transport that predates the range fields returns a superset: filter it here.
|
|
6998
|
+
for (let i = 0; i < entries.length; i++)
|
|
6999
|
+
if (__sbDOInRange(entries[i][0], o)) out.set(entries[i][0], __sbDODecode(entries[i][1]));
|
|
6620
7000
|
return out;
|
|
6621
7001
|
},
|
|
6622
7002
|
};
|
|
6623
7003
|
}
|
|
6624
7004
|
|
|
7005
|
+
// The 2026-10-28 storage: every method returns a Promise; reads shut the input gate.
|
|
7006
|
+
function __sbDOAsyncStorage(cls, id, gate) {
|
|
7007
|
+
const s = __sbDOStrictStorage(cls, id);
|
|
7008
|
+
const settle = (f) => {
|
|
7009
|
+
try {
|
|
7010
|
+
return Promise.resolve(f());
|
|
7011
|
+
} catch (error) {
|
|
7012
|
+
return Promise.reject(error);
|
|
7013
|
+
}
|
|
7014
|
+
};
|
|
7015
|
+
const read = (f) => {
|
|
7016
|
+
__sbDOHold(gate);
|
|
7017
|
+
return settle(f);
|
|
7018
|
+
};
|
|
7019
|
+
return {
|
|
7020
|
+
get: (key) => read(() => s.get(key)),
|
|
7021
|
+
list: (options) => read(() => s.list(options)),
|
|
7022
|
+
getAlarm: () => read(() => s.getAlarm()),
|
|
7023
|
+
put: (key, value) => settle(() => s.put(key, value)),
|
|
7024
|
+
delete: (key) => settle(() => s.delete(key)),
|
|
7025
|
+
deleteAll: () => settle(() => s.deleteAll()),
|
|
7026
|
+
setAlarm: (when) => settle(() => s.setAlarm(when)),
|
|
7027
|
+
deleteAlarm: () => settle(() => s.deleteAlarm()),
|
|
7028
|
+
sync: () => Promise.resolve(),
|
|
7029
|
+
transaction: (fn) => __sbDOTransaction(cls, id, s, gate, fn),
|
|
7030
|
+
};
|
|
7031
|
+
}
|
|
7032
|
+
|
|
7033
|
+
// storage.transaction(fn): fn gets a txn with the storage methods; its writes stay in
|
|
7034
|
+
// memory (read back by its own get and list) and are written when fn resolves, unless it
|
|
7035
|
+
// threw or called txn.rollback(). The input gate stays shut meanwhile, so no other event
|
|
7036
|
+
// sees a half-written transaction.
|
|
7037
|
+
function __sbDOTransaction(cls, id, s, gate, fn) {
|
|
7038
|
+
const writes = new Map(); // key -> encoded value, or null for a delete
|
|
7039
|
+
let rolledBack = false;
|
|
7040
|
+
const getOne = (key) => {
|
|
7041
|
+
const k = String(key);
|
|
7042
|
+
if (!writes.has(k)) return s.get(k);
|
|
7043
|
+
const v = writes.get(k);
|
|
7044
|
+
return v === null ? undefined : __sbDODecode(v);
|
|
7045
|
+
};
|
|
7046
|
+
const putOne = (key, value) => {
|
|
7047
|
+
writes.set(String(key), __sbDOEncode(value));
|
|
7048
|
+
};
|
|
7049
|
+
const delOne = (key) => {
|
|
7050
|
+
const existed = getOne(key) !== undefined;
|
|
7051
|
+
writes.set(String(key), null);
|
|
7052
|
+
return existed;
|
|
7053
|
+
};
|
|
7054
|
+
const txn = {
|
|
7055
|
+
get(key) {
|
|
7056
|
+
if (!Array.isArray(key)) return Promise.resolve(getOne(key));
|
|
7057
|
+
const out = new Map();
|
|
7058
|
+
for (let i = 0; i < key.length; i++) {
|
|
7059
|
+
const v = getOne(key[i]);
|
|
7060
|
+
if (v !== undefined) out.set(key[i], v);
|
|
7061
|
+
}
|
|
7062
|
+
return Promise.resolve(out);
|
|
7063
|
+
},
|
|
7064
|
+
put(key, value) {
|
|
7065
|
+
try {
|
|
7066
|
+
if (key != null && __sbIsObj(key)) {
|
|
7067
|
+
const keys = Object.keys(key);
|
|
7068
|
+
for (let i = 0; i < keys.length; i++) putOne(keys[i], key[keys[i]]);
|
|
7069
|
+
} else putOne(key, value);
|
|
7070
|
+
return Promise.resolve();
|
|
7071
|
+
} catch (error) {
|
|
7072
|
+
return Promise.reject(error);
|
|
7073
|
+
}
|
|
7074
|
+
},
|
|
7075
|
+
delete(key) {
|
|
7076
|
+
if (!Array.isArray(key)) return Promise.resolve(delOne(key));
|
|
7077
|
+
let n = 0;
|
|
7078
|
+
for (let i = 0; i < key.length; i++) n += delOne(key[i]) ? 1 : 0;
|
|
7079
|
+
return Promise.resolve(n);
|
|
7080
|
+
},
|
|
7081
|
+
list(options) {
|
|
7082
|
+
try {
|
|
7083
|
+
const o = __sbDOListLimit(options);
|
|
7084
|
+
const merged = s.list({ prefix: o.prefix, start: o.start, startAfter: o.startAfter, end: o.end });
|
|
7085
|
+
writes.forEach((v, k) => {
|
|
7086
|
+
if (!__sbDOInRange(k, o)) return;
|
|
7087
|
+
if (v === null) merged.delete(k);
|
|
7088
|
+
else merged.set(k, __sbDODecode(v));
|
|
7089
|
+
});
|
|
7090
|
+
const keys = [];
|
|
7091
|
+
merged.forEach((v, k) => keys.push(k));
|
|
7092
|
+
keys.sort();
|
|
7093
|
+
if (o.reverse) keys.reverse();
|
|
7094
|
+
const out = new Map();
|
|
7095
|
+
const limit = o.limit == null ? keys.length : Number(o.limit);
|
|
7096
|
+
for (let i = 0; i < keys.length && i < limit; i++) out.set(keys[i], merged.get(keys[i]));
|
|
7097
|
+
return Promise.resolve(out);
|
|
7098
|
+
} catch (error) {
|
|
7099
|
+
return Promise.reject(error);
|
|
7100
|
+
}
|
|
7101
|
+
},
|
|
7102
|
+
rollback() {
|
|
7103
|
+
rolledBack = true;
|
|
7104
|
+
},
|
|
7105
|
+
getAlarm: () => Promise.resolve(s.getAlarm()),
|
|
7106
|
+
setAlarm: (when) => Promise.resolve(s.setAlarm(when)),
|
|
7107
|
+
deleteAlarm: () => Promise.resolve(s.deleteAlarm()),
|
|
7108
|
+
};
|
|
7109
|
+
gate.locks++;
|
|
7110
|
+
const done = () => {
|
|
7111
|
+
gate.locks--;
|
|
7112
|
+
__sbDOPump(gate);
|
|
7113
|
+
};
|
|
7114
|
+
let r;
|
|
7115
|
+
try {
|
|
7116
|
+
r = fn(txn);
|
|
7117
|
+
} catch (error) {
|
|
7118
|
+
done();
|
|
7119
|
+
return Promise.reject(error);
|
|
7120
|
+
}
|
|
7121
|
+
return Promise.resolve(r).then(
|
|
7122
|
+
(v) => {
|
|
7123
|
+
try {
|
|
7124
|
+
if (!rolledBack)
|
|
7125
|
+
writes.forEach((value, key) => {
|
|
7126
|
+
if (value === null) __sbRpc("do.storage.delete", { cls, id, key });
|
|
7127
|
+
else __sbRpc("do.storage.put", { cls, id, key, value });
|
|
7128
|
+
});
|
|
7129
|
+
} finally {
|
|
7130
|
+
done();
|
|
7131
|
+
}
|
|
7132
|
+
return v;
|
|
7133
|
+
},
|
|
7134
|
+
(error) => {
|
|
7135
|
+
done();
|
|
7136
|
+
throw error;
|
|
7137
|
+
},
|
|
7138
|
+
);
|
|
7139
|
+
}
|
|
7140
|
+
|
|
6625
7141
|
// ---------------------------------------------------------------------------
|
|
6626
7142
|
// Trigger dispatch. The compiled server only ever calls `fetch(request)`; this
|
|
6627
7143
|
// routes the internal `x-sb-trigger` requests (sent by the broker, authenticated
|
|
@@ -7031,11 +7547,133 @@ async function __sbQueueWithDrain(result, tasks) {
|
|
|
7031
7547
|
return new Response(JSON.stringify(r), { headers: { "content-type": "application/json" } });
|
|
7032
7548
|
}
|
|
7033
7549
|
|
|
7550
|
+
// Streamed request bodies, from compatibility date 2026-10-27, on a native server that
|
|
7551
|
+
// streams them (a standalone or platform sprout; request-streaming.ts in the toolchain).
|
|
7552
|
+
// The handler runs once the headers are in, and request.body hands out the bytes as they
|
|
7553
|
+
// arrive: the server pauses the socket while more than 1 MiB waits unread. Before that
|
|
7554
|
+
// date the server buffers the whole body and the handler gets it at once, as before.
|
|
7555
|
+
// op: "e" enable (at startup), "t" take the current request's body id, "p" poll (a byte
|
|
7556
|
+
// count, "0" nothing yet, "-1" ended, "-2" failed), "i" copy into the Uint8Array in data,
|
|
7557
|
+
// "w" wait on the timer id in data, "c" close. Returns a short string.
|
|
7558
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
|
|
7559
|
+
function __sbReqBodyOp(op, id, data) {
|
|
7560
|
+
let out = "";
|
|
7561
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
7562
|
+
Porffor.c`
|
|
7563
|
+
extern int sb_reqbody_enable(void);
|
|
7564
|
+
extern unsigned int sb_reqbody_take_current(void);
|
|
7565
|
+
extern long long sb_reqbody_poll(unsigned int);
|
|
7566
|
+
extern size_t sb_reqbody_take(unsigned int, char*, size_t);
|
|
7567
|
+
extern void sb_reqbody_wait(unsigned int, unsigned int);
|
|
7568
|
+
extern void sb_reqbody_close(unsigned int);
|
|
7569
|
+
const char* __o; size_t __ol; char* __oo = 0;
|
|
7570
|
+
porf_native_fetch_read_value(op, &__o, &__ol, &__oo);
|
|
7571
|
+
char __op = __ol ? __o[0] : 0;
|
|
7572
|
+
if (__oo) free(__oo);
|
|
7573
|
+
const unsigned int __id = id.type == 1 && id.val > 0 ? (unsigned int)id.val : 0;
|
|
7574
|
+
char __r[32]; __r[0] = 0;
|
|
7575
|
+
if (__op == 'e') snprintf(__r, sizeof(__r), "%d", sb_reqbody_enable());
|
|
7576
|
+
else if (__op == 't') snprintf(__r, sizeof(__r), "%u", sb_reqbody_take_current());
|
|
7577
|
+
else if (__op == 'p') snprintf(__r, sizeof(__r), "%lld", sb_reqbody_poll(__id));
|
|
7578
|
+
else if (__op == 'i' && data.type == 81) {
|
|
7579
|
+
const u32 __ta = (u32)data.val;
|
|
7580
|
+
const int32_t __len = *(int32_t*)(MEM + __ta);
|
|
7581
|
+
if (__len > 0) {
|
|
7582
|
+
const u32 __at = *(u32*)(MEM + __ta + 4) + 4;
|
|
7583
|
+
snprintf(__r, sizeof(__r), "%zu", sb_reqbody_take(__id, (char*)(MEM + __at), (size_t)__len));
|
|
7584
|
+
}
|
|
7585
|
+
} else if (__op == 'w' && data.type == 1 && data.val > 0) sb_reqbody_wait(__id, (unsigned int)data.val);
|
|
7586
|
+
else if (__op == 'c') sb_reqbody_close(__id);
|
|
7587
|
+
out = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, strlen(__r)), 195);
|
|
7588
|
+
`;
|
|
7589
|
+
return out;
|
|
7590
|
+
}
|
|
7591
|
+
|
|
7592
|
+
// Turns streaming on in the native server, from 2026-10-27. Called once at startup, after
|
|
7593
|
+
// the compatibility date is set (__sbInstallFetch).
|
|
7594
|
+
function __sbIngressStreamingSetup() {
|
|
7595
|
+
if (globalThis.__sbCompat >= "2026-10-27") __sbReqBodyOp("e", 0, "");
|
|
7596
|
+
}
|
|
7597
|
+
|
|
7598
|
+
// request.body for a streamed request: chunks as the server receives them.
|
|
7599
|
+
function __sbIngressStream(request, id) {
|
|
7600
|
+
let closed = false;
|
|
7601
|
+
const close = () => {
|
|
7602
|
+
if (!closed) {
|
|
7603
|
+
closed = true;
|
|
7604
|
+
__sbReqBodyOp("c", id, "");
|
|
7605
|
+
}
|
|
7606
|
+
};
|
|
7607
|
+
const step = (controller, resolve) => {
|
|
7608
|
+
const state = Number(__sbReqBodyOp("p", id, ""));
|
|
7609
|
+
if (state === 0) {
|
|
7610
|
+
// Nothing yet: the server fires this timer when bytes, the end or an error arrive.
|
|
7611
|
+
const timer = setTimeout(() => step(controller, resolve), 2147483647);
|
|
7612
|
+
__sbReqBodyOp("w", id, timer);
|
|
7613
|
+
return;
|
|
7614
|
+
}
|
|
7615
|
+
if (state > 0) {
|
|
7616
|
+
const chunk = new Uint8Array(state);
|
|
7617
|
+
__sbReqBodyOp("i", id, chunk);
|
|
7618
|
+
controller.enqueue(chunk);
|
|
7619
|
+
} else {
|
|
7620
|
+
close();
|
|
7621
|
+
if (state === -1) controller.close();
|
|
7622
|
+
else controller.error(new TypeError("Network connection lost."));
|
|
7623
|
+
}
|
|
7624
|
+
resolve();
|
|
7625
|
+
};
|
|
7626
|
+
const stream = new ReadableStream({
|
|
7627
|
+
pull(controller) {
|
|
7628
|
+
return new Promise((resolve) => step(controller, resolve));
|
|
7629
|
+
},
|
|
7630
|
+
cancel() {
|
|
7631
|
+
close();
|
|
7632
|
+
},
|
|
7633
|
+
});
|
|
7634
|
+
request.body = stream;
|
|
7635
|
+
// The bytes of the whole body, read through the stream (so bodyUsed and locking hold).
|
|
7636
|
+
const all = async () => {
|
|
7637
|
+
const body = request.body;
|
|
7638
|
+
if (request.bodyUsed) throw new TypeError("Body has already been used. It can only be used once. Use tee() first if you need to read it twice.");
|
|
7639
|
+
request._bodyUsed = true;
|
|
7640
|
+
const reader = body.getReader();
|
|
7641
|
+
const chunks = [];
|
|
7642
|
+
let size = 0;
|
|
7643
|
+
for (;;) {
|
|
7644
|
+
const { value, done } = await reader.read();
|
|
7645
|
+
if (done) break;
|
|
7646
|
+
chunks.push(value);
|
|
7647
|
+
size += value.length;
|
|
7648
|
+
}
|
|
7649
|
+
if (chunks.length === 1) return chunks[0];
|
|
7650
|
+
const out = new Uint8Array(size);
|
|
7651
|
+
let at = 0;
|
|
7652
|
+
for (const chunk of chunks) {
|
|
7653
|
+
out.set(chunk, at);
|
|
7654
|
+
at += chunk.length;
|
|
7655
|
+
}
|
|
7656
|
+
return out;
|
|
7657
|
+
};
|
|
7658
|
+
request.text = async () => __sbFromUtf8(await all());
|
|
7659
|
+
request.json = async () => JSON.parse(__sbFromUtf8(await all()));
|
|
7660
|
+
request.arrayBuffer = async () => {
|
|
7661
|
+
const bytes = await all();
|
|
7662
|
+
return bytes.byteLength === bytes.buffer.byteLength ? bytes.buffer : bytes.slice().buffer;
|
|
7663
|
+
};
|
|
7664
|
+
request.bytes = async () => await all();
|
|
7665
|
+
}
|
|
7666
|
+
|
|
7034
7667
|
// Native HTTP ingress stores the body as one character per raw byte. Keep
|
|
7035
7668
|
// that representation for binary consumers, but decode text lazily before
|
|
7036
7669
|
// handing the request to user code. Request.text() in Porffor otherwise treats
|
|
7037
7670
|
// the raw bytes as already-decoded characters and corrupts Unicode/HMAC input.
|
|
7038
7671
|
function __sbDecodeIngress(request) {
|
|
7672
|
+
const streamed = globalThis.__sbCompat >= "2026-10-27" ? Number(__sbReqBodyOp("t", 0, "")) : 0;
|
|
7673
|
+
if (streamed > 0) {
|
|
7674
|
+
__sbIngressStream(request, streamed);
|
|
7675
|
+
return;
|
|
7676
|
+
}
|
|
7039
7677
|
const raw = __sbRawBody(request);
|
|
7040
7678
|
if (raw == null) return;
|
|
7041
7679
|
// A GET or HEAD has no body, as in workerd (from 2026-10-23): request.body is null, and
|
|
@@ -7169,7 +7807,7 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
7169
7807
|
if (!__sbIsFn(inst.alarm)) return new Response("no alarm handler", { status: 404 });
|
|
7170
7808
|
let __ares;
|
|
7171
7809
|
try {
|
|
7172
|
-
__ares = inst.alarm();
|
|
7810
|
+
__ares = __sbDOGated() ? __sbDOEnter(inst.__sbGate, () => inst.alarm()) : inst.alarm();
|
|
7173
7811
|
} catch (error) {
|
|
7174
7812
|
// #179 — same rule as fetch: don't let a throw here crash the process.
|
|
7175
7813
|
__sbLogUncaught("alarm", error);
|
|
@@ -7194,6 +7832,7 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
7194
7832
|
async function __sbRunQueueBatch(handlers, body, ctx) {
|
|
7195
7833
|
const acked = [];
|
|
7196
7834
|
const retried = [];
|
|
7835
|
+
let threw = false;
|
|
7197
7836
|
const raw = body.messages || [];
|
|
7198
7837
|
const messages = [];
|
|
7199
7838
|
for (let i = 0; i < raw.length; i++) {
|
|
@@ -7230,15 +7869,18 @@ async function __sbRunQueueBatch(handlers, body, ctx) {
|
|
|
7230
7869
|
// the response go out) before its own `await`s did, so any ack()/retry()
|
|
7231
7870
|
// past the first one never counted.
|
|
7232
7871
|
if (__qres && __sbIsFn(__qres.then)) await __qres;
|
|
7233
|
-
} catch {
|
|
7872
|
+
} catch (error) {
|
|
7234
7873
|
// #179 — a throw here must not crash the process. Whatever ack()/retry()
|
|
7235
|
-
// calls the handler made before throwing still count
|
|
7236
|
-
|
|
7237
|
-
|
|
7874
|
+
// calls the handler made before throwing still count.
|
|
7875
|
+
threw = true;
|
|
7876
|
+
__sbLogUncaught("queue", error);
|
|
7238
7877
|
}
|
|
7239
|
-
//
|
|
7878
|
+
// A message neither acked nor retried is acked when the handler returns, and retried
|
|
7879
|
+
// when it throws, as on Workers. A throw used to ack the whole batch on the embedded
|
|
7880
|
+
// transport (standalone, sproutd), so a failing consumer lost its messages.
|
|
7240
7881
|
for (let i = 0; i < messages.length; i++) {
|
|
7241
|
-
if (acked.indexOf(messages[i].id) === -1 && retried.indexOf(messages[i].id) === -1)
|
|
7882
|
+
if (acked.indexOf(messages[i].id) === -1 && retried.indexOf(messages[i].id) === -1)
|
|
7883
|
+
(threw ? retried : acked).push(messages[i].id);
|
|
7242
7884
|
}
|
|
7243
7885
|
return { ack: acked, retry: retried };
|
|
7244
7886
|
}
|