@sproutboat/runtime 0.31.0 → 0.33.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 +52 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +696 -29
- package/src/queues.test.js +159 -0
- package/src/transport-embedded.js +106 -25
- package/src/wrap.ts +32 -1
- package/test/native-websockets.ts +178 -0
|
@@ -4295,6 +4295,9 @@ int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_
|
|
|
4295
4295
|
int porf_native_fetch_read_raw_bytes(jsval value, const char** out_buf, size_t* out_len);
|
|
4296
4296
|
// B22: set only by sproutd's worker host (native-host-bridge.c, at init); null everywhere else.
|
|
4297
4297
|
void (*sb_host_note_hook)(const char* text, size_t length) = 0;
|
|
4298
|
+
// WebSockets (from 2026-10-30): set only by a host that carries client sockets (sproutd's
|
|
4299
|
+
// native-host-bridge.c, at init); null everywhere else. See __sbWsOp for the ops.
|
|
4300
|
+
long long (*sb_ws_hook)(int op, unsigned int id, const char* data, size_t length, char* out, size_t cap) = 0;
|
|
4298
4301
|
|
|
4299
4302
|
// --- #133: SHA-2 + HMAC ---------------------------------------------------
|
|
4300
4303
|
// Standalone links BearSSL, but a deployed sprout does not, and this inline C
|
|
@@ -5751,9 +5754,182 @@ function __sbAssetResponse(path, r) {
|
|
|
5751
5754
|
// object from compile.ts (a `const`, but mutable); we add the binding accessors
|
|
5752
5755
|
// to it in place. compile.ts emits `__sbInstallBindings(env, {...})` right after
|
|
5753
5756
|
// the `const env = {...}` line.
|
|
5757
|
+
// Queues from compatibility date 2026-10-31, as Cloudflare documents them:
|
|
5758
|
+
// - send() and sendBatch() return Promises of { metadata: { metrics } } (the queue's backlog
|
|
5759
|
+
// after the send) and check their arguments with workerd's errors: a body over 128 KiB, an
|
|
5760
|
+
// empty batch, more than 100 messages or 256 KiB, a batch that isn't iterable;
|
|
5761
|
+
// - bodies keep their content type: "json" (the default), "text", "bytes" or "v8" (structured
|
|
5762
|
+
// clone, with the Durable Object storage codec), so a string that looks like JSON arrives as
|
|
5763
|
+
// the same string;
|
|
5764
|
+
// - a message is { id, timestamp (a Date: when it was sent), body, attempts } with ack() and
|
|
5765
|
+
// retry({ delaySeconds }) on its prototype; the first of ack() and retry() wins, as a
|
|
5766
|
+
// message's own call wins over ackAll() and retryAll(); a batch has queue (the queue's name),
|
|
5767
|
+
// messages and metadata;
|
|
5768
|
+
// - by default a message is retried at once, 3 times, then dropped (or moved to the dead
|
|
5769
|
+
// letter queue); earlier dates wait 5 s and try 5 times.
|
|
5770
|
+
// The queue name and consumer settings (bindings.queueConfig, from `queues` in
|
|
5771
|
+
// sproutboat.jsonc) apply at every date: they are opt-in.
|
|
5772
|
+
function __sbQueuesStrict() {
|
|
5773
|
+
return globalThis.__sbCompat >= "2026-10-31";
|
|
5774
|
+
}
|
|
5775
|
+
|
|
5776
|
+
// binding -> { binding, queue, maxBatchSize?, maxBatchTimeout?, maxRetries?, retryDelay?, deadLetterQueue? }
|
|
5777
|
+
var __sbQueueConf = {};
|
|
5778
|
+
// queue name -> the binding that stores it
|
|
5779
|
+
var __sbQueueByName = {};
|
|
5780
|
+
function __sbQueueSetup(bindings) {
|
|
5781
|
+
const queues = bindings.queues || [];
|
|
5782
|
+
const config = bindings.queueConfig || [];
|
|
5783
|
+
for (let i = 0; i < queues.length; i++) __sbQueueConf[queues[i]] = { binding: queues[i], queue: queues[i] };
|
|
5784
|
+
for (let i = 0; i < config.length; i++)
|
|
5785
|
+
if (config[i] && __sbQueueConf[config[i].binding]) __sbQueueConf[config[i].binding] = config[i];
|
|
5786
|
+
for (let i = 0; i < queues.length; i++) __sbQueueByName[__sbQueueConf[queues[i]].queue] = queues[i];
|
|
5787
|
+
}
|
|
5788
|
+
function __sbQueueSettings(binding) {
|
|
5789
|
+
return __sbQueueConf[binding] || { binding, queue: binding };
|
|
5790
|
+
}
|
|
5791
|
+
// Where the queue named `name` is stored: its binding, or "#name" for a dead letter queue
|
|
5792
|
+
// this worker doesn't bind (its messages wait there; "#" can't begin a binding name).
|
|
5793
|
+
function __sbQueuePart(name) {
|
|
5794
|
+
return __sbQueueByName[name] || "#" + name;
|
|
5795
|
+
}
|
|
5796
|
+
|
|
5797
|
+
const __sbQueueTag = "#sbq1:";
|
|
5798
|
+
const __sbQueueMaxMessage = 128 * 1024;
|
|
5799
|
+
const __sbQueueMaxBatch = 256 * 1024;
|
|
5800
|
+
|
|
5801
|
+
function __sbQueueBytesOf(value) {
|
|
5802
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
5803
|
+
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
5804
|
+
return null;
|
|
5805
|
+
}
|
|
5806
|
+
function __sbQueueTypeName(value) {
|
|
5807
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- workerd's error names the received type
|
|
5808
|
+
return value === null ? "null" : typeof value;
|
|
5809
|
+
}
|
|
5810
|
+
|
|
5811
|
+
// A body as the stored text "#sbq1:TYPE:PAYLOAD", and its size as Queues counts it.
|
|
5812
|
+
function __sbQueueEncode(body, contentType) {
|
|
5813
|
+
const type = contentType === undefined ? "json" : contentType;
|
|
5814
|
+
let payload;
|
|
5815
|
+
let size;
|
|
5816
|
+
if (type === "json") {
|
|
5817
|
+
if (body === undefined) throw new TypeError("Message body cannot be undefined");
|
|
5818
|
+
try {
|
|
5819
|
+
payload = JSON.stringify(body);
|
|
5820
|
+
} catch (error) {
|
|
5821
|
+
if (error instanceof TypeError && /BigInt/i.test(String(error.message)))
|
|
5822
|
+
throw new TypeError("Do not know how to serialize a BigInt");
|
|
5823
|
+
throw error;
|
|
5824
|
+
}
|
|
5825
|
+
if (payload === undefined) throw new TypeError("Message body cannot be undefined");
|
|
5826
|
+
size = new TextEncoder().encode(payload).length;
|
|
5827
|
+
} else if (type === "text") {
|
|
5828
|
+
if (!__sbIsStr(body))
|
|
5829
|
+
throw new TypeError('Content Type "text" requires a value of type string, but received: ' + __sbQueueTypeName(body));
|
|
5830
|
+
payload = body;
|
|
5831
|
+
size = new TextEncoder().encode(body).length;
|
|
5832
|
+
} else if (type === "bytes") {
|
|
5833
|
+
const bytes = __sbQueueBytesOf(body);
|
|
5834
|
+
if (bytes === null)
|
|
5835
|
+
throw new TypeError('Content Type "bytes" requires a value of type ArrayBuffer, but received: ' + __sbQueueTypeName(body));
|
|
5836
|
+
payload = __sbDOHex(bytes);
|
|
5837
|
+
size = bytes.length;
|
|
5838
|
+
} else if (type === "v8") {
|
|
5839
|
+
payload = JSON.stringify(__sbDOPack(body, []));
|
|
5840
|
+
size = new TextEncoder().encode(payload).length;
|
|
5841
|
+
} else throw new TypeError("Unsupported queue message content type: " + type);
|
|
5842
|
+
if (size > __sbQueueMaxMessage)
|
|
5843
|
+
throw new Error("Queue send failed: message is " + size + " bytes, over the limit of " + __sbQueueMaxMessage);
|
|
5844
|
+
return { text: __sbQueueTag + type + ":" + payload, size };
|
|
5845
|
+
}
|
|
5846
|
+
|
|
5847
|
+
// Reads both formats at every date: tagged (2026-10-31 sends) and the earlier text.
|
|
5848
|
+
function __sbQueueDecode(raw) {
|
|
5849
|
+
if (__sbIsStr(raw) && raw.indexOf(__sbQueueTag) === 0) {
|
|
5850
|
+
const rest = raw.slice(__sbQueueTag.length);
|
|
5851
|
+
const colon = rest.indexOf(":");
|
|
5852
|
+
const type = rest.slice(0, colon);
|
|
5853
|
+
const payload = rest.slice(colon + 1);
|
|
5854
|
+
if (type === "text") return payload;
|
|
5855
|
+
if (type === "json") return JSON.parse(payload);
|
|
5856
|
+
if (type === "bytes") return __sbDOUnhex(payload).buffer;
|
|
5857
|
+
if (type === "v8") return __sbDOUnpack(JSON.parse(payload));
|
|
5858
|
+
}
|
|
5859
|
+
return __sbTryParse(raw);
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5862
|
+
function __sbQueueMetrics(metrics) {
|
|
5863
|
+
const m = metrics || {};
|
|
5864
|
+
const oldest = Number(m.oldestMessageTimestamp) || 0;
|
|
5865
|
+
return {
|
|
5866
|
+
backlogCount: Number(m.backlogCount) || 0,
|
|
5867
|
+
backlogBytes: Number(m.backlogBytes) || 0,
|
|
5868
|
+
// Milliseconds since the epoch, as Cloudflare documents it; none for an empty queue.
|
|
5869
|
+
oldestMessageTimestamp: oldest > 0 ? oldest : undefined,
|
|
5870
|
+
};
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5873
|
+
function __sbQueueDelay(value) {
|
|
5874
|
+
return value == null ? 0 : Math.max(0, Number(value) || 0);
|
|
5875
|
+
}
|
|
5876
|
+
|
|
5877
|
+
function __sbMakeQueue(binding) {
|
|
5878
|
+
return {
|
|
5879
|
+
send(body, options) {
|
|
5880
|
+
try {
|
|
5881
|
+
const o = options || {};
|
|
5882
|
+
const encoded = __sbQueueEncode(body, o.contentType);
|
|
5883
|
+
const reply = __sbRpc("queue.send", {
|
|
5884
|
+
queue: binding,
|
|
5885
|
+
body: encoded.text,
|
|
5886
|
+
delaySeconds: __sbQueueDelay(o.delaySeconds),
|
|
5887
|
+
metrics: true,
|
|
5888
|
+
});
|
|
5889
|
+
return Promise.resolve({ metadata: { metrics: __sbQueueMetrics(reply.metrics) } });
|
|
5890
|
+
} catch (error) {
|
|
5891
|
+
return Promise.reject(error);
|
|
5892
|
+
}
|
|
5893
|
+
},
|
|
5894
|
+
sendBatch(messages, options) {
|
|
5895
|
+
try {
|
|
5896
|
+
if (messages == null || !__sbIsFn(messages[Symbol.iterator]))
|
|
5897
|
+
throw new TypeError("Failed to execute 'sendBatch' on 'WorkerQueue': parameter 1 is not of type 'Sequence'.");
|
|
5898
|
+
const batchDelay = __sbQueueDelay(options && options.delaySeconds);
|
|
5899
|
+
const list = [];
|
|
5900
|
+
let total = 0;
|
|
5901
|
+
for (const m of messages) {
|
|
5902
|
+
const item = m || {};
|
|
5903
|
+
const encoded = __sbQueueEncode(item.body, item.contentType);
|
|
5904
|
+
total += encoded.size;
|
|
5905
|
+
// A message delay of 0 (or none) takes the batch delay, as miniflare does.
|
|
5906
|
+
list.push({ body: encoded.text, delaySeconds: __sbQueueDelay(item.delaySeconds) || batchDelay });
|
|
5907
|
+
}
|
|
5908
|
+
if (list.length === 0) throw new TypeError("sendBatch() requires at least one message");
|
|
5909
|
+
if (list.length > 100) throw new Error("Queue sendBatch failed: " + list.length + " messages, over the limit of 100");
|
|
5910
|
+
if (total > __sbQueueMaxBatch)
|
|
5911
|
+
throw new Error("Queue sendBatch failed: " + total + " bytes, over the limit of " + __sbQueueMaxBatch);
|
|
5912
|
+
const reply = __sbRpc("queue.send_batch", { queue: binding, messages: list, metrics: true });
|
|
5913
|
+
return Promise.resolve({ metadata: { metrics: __sbQueueMetrics(reply.metrics) } });
|
|
5914
|
+
} catch (error) {
|
|
5915
|
+
return Promise.reject(error);
|
|
5916
|
+
}
|
|
5917
|
+
},
|
|
5918
|
+
metrics() {
|
|
5919
|
+
try {
|
|
5920
|
+
return Promise.resolve(__sbQueueMetrics(__sbRpc("queue.metrics", { queue: binding }).metrics));
|
|
5921
|
+
} catch (error) {
|
|
5922
|
+
return Promise.reject(error);
|
|
5923
|
+
}
|
|
5924
|
+
},
|
|
5925
|
+
};
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5754
5928
|
globalThis.__sbInstallBindings = function (target, bindings) {
|
|
5755
5929
|
if (!target) return;
|
|
5756
5930
|
|
|
5931
|
+
__sbQueueSetup(bindings);
|
|
5932
|
+
|
|
5757
5933
|
for (let i = 0; i < (bindings.kv || []).length; i++) {
|
|
5758
5934
|
const ns = bindings.kv[i];
|
|
5759
5935
|
target[ns] = __sbKvStrict() ? __sbMakeKv(ns) : {
|
|
@@ -5898,7 +6074,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
5898
6074
|
|
|
5899
6075
|
for (let i = 0; i < (bindings.queues || []).length; i++) {
|
|
5900
6076
|
const name = bindings.queues[i];
|
|
5901
|
-
target[name] = {
|
|
6077
|
+
target[name] = __sbQueuesStrict() ? __sbMakeQueue(name) : {
|
|
5902
6078
|
send(body, options) {
|
|
5903
6079
|
const o = options || {};
|
|
5904
6080
|
__sbRpc("queue.send", {
|
|
@@ -6498,6 +6674,7 @@ function __sbUpstream(op, msg, label, signal, always) {
|
|
|
6498
6674
|
// prelude must not replace fetch just by being evaluated.
|
|
6499
6675
|
globalThis.__sbInstallFetch = function () {
|
|
6500
6676
|
__sbIngressStreamingSetup();
|
|
6677
|
+
__sbInstallWebSockets();
|
|
6501
6678
|
globalThis.fetch = function (input, init) {
|
|
6502
6679
|
let parts;
|
|
6503
6680
|
try {
|
|
@@ -6859,7 +7036,11 @@ function __sbGetDOInstance(cls, idText, idObj) {
|
|
|
6859
7036
|
if (p && __sbIsFn(p.then)) __sbTasks.push(p);
|
|
6860
7037
|
},
|
|
6861
7038
|
};
|
|
7039
|
+
// The hibernation API, from 2026-10-30 (where WebSocketPair exists).
|
|
7040
|
+
const holder = { inst: null, auto: null };
|
|
7041
|
+
if (__sbWsEnabled()) __sbDOWebSocketState(state, holder);
|
|
6862
7042
|
inst = new Ctor(state, globalThis.env);
|
|
7043
|
+
holder.inst = inst;
|
|
6863
7044
|
inst.__sbTasks = __sbTasks;
|
|
6864
7045
|
inst.__sbGate = gate;
|
|
6865
7046
|
__sbDOInstances[cacheKey] = inst;
|
|
@@ -7841,10 +8022,414 @@ function __sbStreamResponse(response) {
|
|
|
7841
8022
|
return response;
|
|
7842
8023
|
}
|
|
7843
8024
|
|
|
7844
|
-
|
|
8025
|
+
// ---------------------------------------------------------------------------
|
|
8026
|
+
// WebSockets, from compatibility date 2026-10-30, as in workerd: WebSocketPair, accept(),
|
|
8027
|
+
// send(), close(), message and close events, new Response(null, { status: 101, webSocket })
|
|
8028
|
+
// and, for Durable Objects, the hibernation API (__sbDOWebSocketState).
|
|
8029
|
+
//
|
|
8030
|
+
// A pair whose ends both stay in this worker (a Worker and its Durable Object) is plain
|
|
8031
|
+
// JavaScript, so it works on every host. The client end of a 101 response becomes a real
|
|
8032
|
+
// socket only on a host that carries it (sproutd: its bridge sets sb_ws_hook); the host
|
|
8033
|
+
// then stands in for that end: what the app sends to it goes out, and what the remote
|
|
8034
|
+
// client sends arrives as if that end had sent it.
|
|
8035
|
+
//
|
|
8036
|
+
// __sbWsOp(op, id, data, n), on the host socket `id`:
|
|
8037
|
+
// "o" open one (returns its id, "0" without a host), "t" send text data, "b" send the
|
|
8038
|
+
// bytes in data, "c" close with code n and reason data, "p" poll the next event ("" for
|
|
8039
|
+
// none, else "KIND LENGTH CODE": 0 text, 1 binary, 2 close, 3 connection lost), "i" take
|
|
8040
|
+
// that event's payload into the Uint8Array data, "k" acknowledge n bytes dispatched,
|
|
8041
|
+
// "w" fire timer n when an event arrives, "r" release. Returns a short string.
|
|
8042
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
|
|
8043
|
+
function __sbWsOp(op, id, data, n) {
|
|
8044
|
+
let out = "";
|
|
8045
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
8046
|
+
Porffor.c`
|
|
8047
|
+
const char* __o; size_t __ol; char* __oo = 0;
|
|
8048
|
+
porf_native_fetch_read_value(op, &__o, &__ol, &__oo);
|
|
8049
|
+
char __op = __ol ? __o[0] : 0;
|
|
8050
|
+
if (__oo) free(__oo);
|
|
8051
|
+
const unsigned int __id = id.type == 1 && id.val > 0 ? (unsigned int)id.val : 0;
|
|
8052
|
+
const size_t __n = n.type == 1 && n.val > 0 ? (size_t)n.val : 0;
|
|
8053
|
+
char __r[64]; __r[0] = 0;
|
|
8054
|
+
if (sb_ws_hook) {
|
|
8055
|
+
long long __w = -1;
|
|
8056
|
+
if (__op == 'p') {
|
|
8057
|
+
sb_ws_hook('p', __id, 0, 0, __r, sizeof(__r));
|
|
8058
|
+
} else if (__op == 'i') {
|
|
8059
|
+
char* __at = 0; size_t __cap = 0;
|
|
8060
|
+
if (data.type == 81) {
|
|
8061
|
+
const u32 __ta = (u32)data.val;
|
|
8062
|
+
const int32_t __len = *(int32_t*)(MEM + __ta);
|
|
8063
|
+
if (__len > 0) { __at = (char*)(MEM + *(u32*)(MEM + __ta + 4) + 4); __cap = (size_t)__len; }
|
|
8064
|
+
}
|
|
8065
|
+
__w = sb_ws_hook('i', __id, 0, 0, __at, __cap);
|
|
8066
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8067
|
+
} else if (__op == 'b') {
|
|
8068
|
+
const char* __d = 0; size_t __dl = 0;
|
|
8069
|
+
if (porf_native_fetch_read_raw_bytes(data, &__d, &__dl) == 0) __w = sb_ws_hook('b', __id, __d, __dl, 0, 0);
|
|
8070
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8071
|
+
} else if (__op == 't' || __op == 'c') {
|
|
8072
|
+
const char* __d; size_t __dl; char* __do = 0;
|
|
8073
|
+
porf_native_fetch_read_value(data, &__d, &__dl, &__do);
|
|
8074
|
+
__w = sb_ws_hook(__op, __id, __d, __dl, 0, __n);
|
|
8075
|
+
if (__do) free(__do);
|
|
8076
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8077
|
+
} else {
|
|
8078
|
+
__w = sb_ws_hook(__op, __id, 0, 0, 0, __n);
|
|
8079
|
+
snprintf(__r, sizeof(__r), "%lld", __w);
|
|
8080
|
+
}
|
|
8081
|
+
} else if (__op == 'o') snprintf(__r, sizeof(__r), "0");
|
|
8082
|
+
out = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, strlen(__r)), 195);
|
|
8083
|
+
`;
|
|
8084
|
+
return out;
|
|
8085
|
+
}
|
|
8086
|
+
|
|
8087
|
+
const __SB_WS_OPEN = 1;
|
|
8088
|
+
const __SB_WS_CLOSING = 2;
|
|
8089
|
+
const __SB_WS_CLOSED = 3;
|
|
8090
|
+
const __sbWsKey = {};
|
|
8091
|
+
|
|
8092
|
+
// One end of a WebSocketPair. Events are plain objects with the fields apps read (type,
|
|
8093
|
+
// data; code, reason, wasClean), dispatched to on<type> (when assigned) and then to the
|
|
8094
|
+
// addEventListener listeners, as workerd does. Binary messages arrive as a Blob unless
|
|
8095
|
+
// binaryType is "arraybuffer" (workerd's standard binary type).
|
|
8096
|
+
class __SbWebSocket {
|
|
8097
|
+
constructor(key) {
|
|
8098
|
+
// Outbound WebSockets (new WebSocket(url)) are not supported; the CLI refuses them too.
|
|
8099
|
+
if (key !== __sbWsKey) throw new TypeError("WebSocket: only WebSocketPair can make a WebSocket here");
|
|
8100
|
+
this._peer = null;
|
|
8101
|
+
this._accepted = false;
|
|
8102
|
+
this._inbox = [];
|
|
8103
|
+
this._flushing = false;
|
|
8104
|
+
this._listeners = {};
|
|
8105
|
+
this._state = __SB_WS_OPEN;
|
|
8106
|
+
this._closeSent = false;
|
|
8107
|
+
this._closeSeen = false;
|
|
8108
|
+
// The host socket this end stands for (the client end of a 101 response), or 0.
|
|
8109
|
+
this._host = 0;
|
|
8110
|
+
// { holder, tags } once a Durable Object took it with ctx.acceptWebSocket().
|
|
8111
|
+
this._hib = null;
|
|
8112
|
+
this._attachment = undefined;
|
|
8113
|
+
this.binaryType = "blob";
|
|
8114
|
+
this.url = null;
|
|
8115
|
+
this.protocol = "";
|
|
8116
|
+
this.extensions = "";
|
|
8117
|
+
}
|
|
8118
|
+
get readyState() {
|
|
8119
|
+
return this._state;
|
|
8120
|
+
}
|
|
8121
|
+
get bufferedAmount() {
|
|
8122
|
+
return 0;
|
|
8123
|
+
}
|
|
8124
|
+
accept() {
|
|
8125
|
+
if (this._host !== 0) throw new TypeError("Can't accept() a WebSocket that was already used in a response.");
|
|
8126
|
+
if (this._hib !== null) throw new TypeError("Can't accept() a WebSocket that was accepted with acceptWebSocket().");
|
|
8127
|
+
if (this._accepted) return;
|
|
8128
|
+
this._accepted = true;
|
|
8129
|
+
__sbWsSchedule(this);
|
|
8130
|
+
}
|
|
8131
|
+
send(message) {
|
|
8132
|
+
if (!this._accepted && this._hib === null)
|
|
8133
|
+
throw new TypeError("You must call one of accept() or state.acceptWebSocket() on this WebSocket before sending messages.");
|
|
8134
|
+
if (this._closeSent) throw new TypeError("Can't call WebSocket send() after close().");
|
|
8135
|
+
if (this._state === __SB_WS_CLOSED || this._peer === null) return;
|
|
8136
|
+
let event;
|
|
8137
|
+
if (__sbIsStr(message)) event = { kind: 0, data: message };
|
|
8138
|
+
else if (message instanceof ArrayBuffer) event = { kind: 1, data: message.slice(0) };
|
|
8139
|
+
else if (ArrayBuffer.isView(message))
|
|
8140
|
+
event = { kind: 1, data: new Uint8Array(message.buffer, message.byteOffset, message.byteLength).slice().buffer };
|
|
8141
|
+
else event = { kind: 0, data: String(message) };
|
|
8142
|
+
__sbWsDeliver(this._peer, event);
|
|
8143
|
+
}
|
|
8144
|
+
close(code, reason) {
|
|
8145
|
+
if (code !== undefined) {
|
|
8146
|
+
code = Number(code);
|
|
8147
|
+
if (!(code === 1000 || (code >= 3000 && code <= 4999))) throw __sbCryptoError("InvalidAccessError", "Invalid WebSocket close code: " + code + ".");
|
|
8148
|
+
}
|
|
8149
|
+
const text = reason === undefined ? "" : String(reason);
|
|
8150
|
+
if (__sbToBytes(text).length > 123)
|
|
8151
|
+
throw __sbCryptoError("SyntaxError", "WebSocket close reason must not be longer than 123 bytes when UTF-8 encoded.");
|
|
8152
|
+
if (this._closeSent) return;
|
|
8153
|
+
this._closeSent = true;
|
|
8154
|
+
this._state = this._closeSeen ? __SB_WS_CLOSED : __SB_WS_CLOSING;
|
|
8155
|
+
if (this._peer !== null) __sbWsDeliver(this._peer, { kind: 2, code: code === undefined ? 1005 : code, reason: text, clean: true });
|
|
8156
|
+
}
|
|
8157
|
+
addEventListener(type, listener) {
|
|
8158
|
+
if (listener == null) return;
|
|
8159
|
+
const list = this._listeners[type] || (this._listeners[type] = []);
|
|
8160
|
+
for (let i = 0; i < list.length; i++) if (list[i] === listener) return;
|
|
8161
|
+
list.push(listener);
|
|
8162
|
+
}
|
|
8163
|
+
removeEventListener(type, listener) {
|
|
8164
|
+
const list = this._listeners[type];
|
|
8165
|
+
if (!list) return;
|
|
8166
|
+
for (let i = 0; i < list.length; i++) {
|
|
8167
|
+
if (list[i] === listener) {
|
|
8168
|
+
list.splice(i, 1);
|
|
8169
|
+
return;
|
|
8170
|
+
}
|
|
8171
|
+
}
|
|
8172
|
+
}
|
|
8173
|
+
dispatchEvent(event) {
|
|
8174
|
+
__sbWsFire(this, event);
|
|
8175
|
+
return true;
|
|
8176
|
+
}
|
|
8177
|
+
serializeAttachment(value) {
|
|
8178
|
+
this._attachment = value === undefined ? undefined : structuredClone(value);
|
|
8179
|
+
}
|
|
8180
|
+
deserializeAttachment() {
|
|
8181
|
+
return this._attachment === undefined ? null : structuredClone(this._attachment);
|
|
8182
|
+
}
|
|
8183
|
+
}
|
|
8184
|
+
__SbWebSocket.CONNECTING = 0;
|
|
8185
|
+
__SbWebSocket.OPEN = __SB_WS_OPEN;
|
|
8186
|
+
__SbWebSocket.CLOSING = __SB_WS_CLOSING;
|
|
8187
|
+
__SbWebSocket.CLOSED = __SB_WS_CLOSED;
|
|
8188
|
+
__SbWebSocket.READY_STATE_CONNECTING = 0;
|
|
8189
|
+
__SbWebSocket.READY_STATE_OPEN = __SB_WS_OPEN;
|
|
8190
|
+
__SbWebSocket.READY_STATE_CLOSING = __SB_WS_CLOSING;
|
|
8191
|
+
__SbWebSocket.READY_STATE_CLOSED = __SB_WS_CLOSED;
|
|
8192
|
+
|
|
8193
|
+
class __SbWebSocketPair {
|
|
8194
|
+
constructor() {
|
|
8195
|
+
const client = new __SbWebSocket(__sbWsKey);
|
|
8196
|
+
const server = new __SbWebSocket(__sbWsKey);
|
|
8197
|
+
client._peer = server;
|
|
8198
|
+
server._peer = client;
|
|
8199
|
+
this[0] = client;
|
|
8200
|
+
this[1] = server;
|
|
8201
|
+
}
|
|
8202
|
+
}
|
|
8203
|
+
|
|
8204
|
+
class __SbWebSocketRequestResponsePair {
|
|
8205
|
+
constructor(request, response) {
|
|
8206
|
+
if (!__sbIsStr(request) || !__sbIsStr(response)) throw new TypeError("WebSocketRequestResponsePair needs two strings");
|
|
8207
|
+
this.request = request;
|
|
8208
|
+
this.response = response;
|
|
8209
|
+
}
|
|
8210
|
+
}
|
|
8211
|
+
|
|
8212
|
+
function __sbWsFire(ws, event) {
|
|
8213
|
+
const handler = ws["on" + event.type];
|
|
8214
|
+
if (__sbIsFn(handler)) {
|
|
8215
|
+
try {
|
|
8216
|
+
handler.call(ws, event);
|
|
8217
|
+
} catch (error) {
|
|
8218
|
+
__sbLogUncaught("websocket", error);
|
|
8219
|
+
}
|
|
8220
|
+
}
|
|
8221
|
+
const list = ws._listeners[event.type];
|
|
8222
|
+
if (!list) return;
|
|
8223
|
+
const copy = list.slice();
|
|
8224
|
+
for (let i = 0; i < copy.length; i++) {
|
|
8225
|
+
const fn = copy[i];
|
|
8226
|
+
try {
|
|
8227
|
+
if (__sbIsFn(fn)) fn.call(ws, event);
|
|
8228
|
+
else if (fn && __sbIsFn(fn.handleEvent)) fn.handleEvent(event);
|
|
8229
|
+
} catch (error) {
|
|
8230
|
+
__sbLogUncaught("websocket", error);
|
|
8231
|
+
}
|
|
8232
|
+
}
|
|
8233
|
+
}
|
|
8234
|
+
|
|
8235
|
+
// An event arrives at `ws`, sent by its peer: to the host when `ws` is a host socket,
|
|
8236
|
+
// else into ws's inbox, dispatched once it is accepted.
|
|
8237
|
+
function __sbWsDeliver(ws, event) {
|
|
8238
|
+
if (ws._host !== 0) {
|
|
8239
|
+
const id = ws._host;
|
|
8240
|
+
if (event.kind === 0) __sbWsOp("t", id, event.data, 0);
|
|
8241
|
+
else if (event.kind === 1) __sbWsOp("b", id, __sbToBytes(new Uint8Array(event.data)), 0);
|
|
8242
|
+
else if (!ws._closeSeen) {
|
|
8243
|
+
ws._closeSeen = true;
|
|
8244
|
+
__sbWsOp("c", id, event.reason, event.code);
|
|
8245
|
+
}
|
|
8246
|
+
return;
|
|
8247
|
+
}
|
|
8248
|
+
ws._inbox.push(event);
|
|
8249
|
+
__sbWsSchedule(ws);
|
|
8250
|
+
}
|
|
8251
|
+
|
|
8252
|
+
// One event per turn, as workerd's receive loop does: a reply the handler sends reaches the
|
|
8253
|
+
// other end before this end's next message.
|
|
8254
|
+
function __sbWsSchedule(ws) {
|
|
8255
|
+
if (ws._flushing || !ws._inbox.length || (!ws._accepted && ws._hib === null)) return;
|
|
8256
|
+
ws._flushing = true;
|
|
8257
|
+
Promise.resolve().then(() => {
|
|
8258
|
+
ws._flushing = false;
|
|
8259
|
+
if (ws._inbox.length && (ws._accepted || ws._hib !== null)) __sbWsDispatch(ws, ws._inbox.shift());
|
|
8260
|
+
__sbWsSchedule(ws);
|
|
8261
|
+
});
|
|
8262
|
+
}
|
|
8263
|
+
|
|
8264
|
+
function __sbWsFlush(ws) {
|
|
8265
|
+
while (ws._inbox.length && (ws._accepted || ws._hib !== null)) __sbWsDispatch(ws, ws._inbox.shift());
|
|
8266
|
+
}
|
|
8267
|
+
|
|
8268
|
+
// Dispatches one event that arrived at ws: to its listeners, or to its Durable Object's
|
|
8269
|
+
// webSocketMessage / webSocketClose. A close is answered with a close (workerd replies to
|
|
8270
|
+
// a close automatically), and ends the socket.
|
|
8271
|
+
function __sbWsDispatch(ws, event) {
|
|
8272
|
+
if (event.kind === 2) {
|
|
8273
|
+
ws._closeSeen = true;
|
|
8274
|
+
if (!ws._closeSent) {
|
|
8275
|
+
ws._closeSent = true;
|
|
8276
|
+
if (ws._peer !== null) __sbWsDeliver(ws._peer, { kind: 2, code: event.code, reason: event.reason, clean: event.clean });
|
|
8277
|
+
}
|
|
8278
|
+
ws._state = __SB_WS_CLOSED;
|
|
8279
|
+
if (ws._hib !== null) return __sbWsHibernated(ws, "webSocketClose", [ws, event.code, event.reason, event.clean]);
|
|
8280
|
+
return __sbWsFire(ws, { type: "close", code: event.code, reason: event.reason, wasClean: event.clean, target: ws });
|
|
8281
|
+
}
|
|
8282
|
+
if (ws._hib !== null) {
|
|
8283
|
+
const auto = ws._hib.holder.auto;
|
|
8284
|
+
if (auto !== null && event.kind === 0 && event.data === auto.request) {
|
|
8285
|
+
ws.send(auto.response);
|
|
8286
|
+
return;
|
|
8287
|
+
}
|
|
8288
|
+
return __sbWsHibernated(ws, "webSocketMessage", [ws, event.data]);
|
|
8289
|
+
}
|
|
8290
|
+
const data = event.kind === 1 && ws.binaryType !== "arraybuffer" ? new Blob([event.data]) : event.data;
|
|
8291
|
+
__sbWsFire(ws, { type: "message", data, target: ws });
|
|
8292
|
+
}
|
|
8293
|
+
|
|
8294
|
+
// A hibernatable socket's event runs the object's handler through its input gate.
|
|
8295
|
+
function __sbWsHibernated(ws, name, args) {
|
|
8296
|
+
const inst = ws._hib.holder.inst;
|
|
8297
|
+
if (inst == null || !__sbIsFn(inst[name])) return;
|
|
8298
|
+
const run = () => {
|
|
8299
|
+
if (name === "webSocketClose") return inst.webSocketClose(args[0], args[1], args[2], args[3]);
|
|
8300
|
+
return inst.webSocketMessage(args[0], args[1]);
|
|
8301
|
+
};
|
|
8302
|
+
const done = (p) => {
|
|
8303
|
+
if (p && __sbIsFn(p.then)) p.then(undefined, (error) => __sbLogUncaught("websocket", error));
|
|
8304
|
+
};
|
|
8305
|
+
try {
|
|
8306
|
+
done(__sbDOGated() ? __sbDOEnter(inst.__sbGate, run) : run());
|
|
8307
|
+
} catch (error) {
|
|
8308
|
+
__sbLogUncaught("websocket", error);
|
|
8309
|
+
}
|
|
8310
|
+
}
|
|
8311
|
+
|
|
8312
|
+
// ctx.acceptWebSocket() and the rest of the hibernation API, added to a Durable Object's
|
|
8313
|
+
// state from 2026-10-30. `holder.inst` is the object once it exists.
|
|
8314
|
+
function __sbDOWebSocketState(state, holder) {
|
|
8315
|
+
const sockets = [];
|
|
8316
|
+
const live = () => {
|
|
8317
|
+
for (let i = sockets.length - 1; i >= 0; i--) if (sockets[i]._state === __SB_WS_CLOSED) sockets.splice(i, 1);
|
|
8318
|
+
return sockets;
|
|
8319
|
+
};
|
|
8320
|
+
state.acceptWebSocket = function (ws, tags) {
|
|
8321
|
+
if (!(ws instanceof __SbWebSocket)) throw new TypeError("acceptWebSocket needs a WebSocket");
|
|
8322
|
+
if (ws._accepted || ws._hib !== null || ws._host !== 0) throw new TypeError("This WebSocket was already accepted.");
|
|
8323
|
+
const list = [];
|
|
8324
|
+
if (tags != null) for (const t of tags) list.push(String(t));
|
|
8325
|
+
if (list.length > 10) throw new TypeError("A WebSocket can have at most 10 tags.");
|
|
8326
|
+
ws._hib = { holder, tags: list };
|
|
8327
|
+
live().push(ws);
|
|
8328
|
+
__sbWsSchedule(ws);
|
|
8329
|
+
};
|
|
8330
|
+
state.getWebSockets = function (tag) {
|
|
8331
|
+
const out = [];
|
|
8332
|
+
const all = live();
|
|
8333
|
+
for (let i = 0; i < all.length; i++) if (tag === undefined || all[i]._hib.tags.indexOf(String(tag)) >= 0) out.push(all[i]);
|
|
8334
|
+
return out;
|
|
8335
|
+
};
|
|
8336
|
+
state.getTags = function (ws) {
|
|
8337
|
+
if (!(ws instanceof __SbWebSocket) || ws._hib === null) throw new TypeError("getTags: not a WebSocket accepted by this object");
|
|
8338
|
+
return ws._hib.tags.slice();
|
|
8339
|
+
};
|
|
8340
|
+
state.setWebSocketAutoResponse = function (pair) {
|
|
8341
|
+
if (pair != null && !(pair instanceof __SbWebSocketRequestResponsePair))
|
|
8342
|
+
throw new TypeError("setWebSocketAutoResponse needs a WebSocketRequestResponsePair");
|
|
8343
|
+
holder.auto = pair == null ? null : pair;
|
|
8344
|
+
};
|
|
8345
|
+
state.getWebSocketAutoResponse = function () {
|
|
8346
|
+
return holder.auto;
|
|
8347
|
+
};
|
|
8348
|
+
state.getWebSocketAutoResponseTimestamp = function () {
|
|
8349
|
+
return null;
|
|
8350
|
+
};
|
|
8351
|
+
state.setHibernatableWebSocketEventTimeout = function () {};
|
|
8352
|
+
state.getHibernatableWebSocketEventTimeout = function () {
|
|
8353
|
+
return null;
|
|
8354
|
+
};
|
|
8355
|
+
}
|
|
8356
|
+
|
|
8357
|
+
function __sbWsEnabled() {
|
|
8358
|
+
return globalThis.WebSocketPair === __SbWebSocketPair;
|
|
8359
|
+
}
|
|
8360
|
+
function __sbHasWebSocket(r) {
|
|
8361
|
+
return r != null && r._webSocket != null && __sbWsEnabled();
|
|
8362
|
+
}
|
|
8363
|
+
|
|
8364
|
+
// The client end of a 101 response becomes a host socket: marked x-sb-websocket for the
|
|
8365
|
+
// host (which upgrades the connection), with what the app already sent going out after it.
|
|
8366
|
+
function __sbWsHostResponse(r) {
|
|
8367
|
+
const ws = r._webSocket;
|
|
8368
|
+
if (!(ws instanceof __SbWebSocket) || ws._host !== 0 || ws._accepted || ws._hib !== null) {
|
|
8369
|
+
return __sbErrorResponse(new TypeError("A 101 response needs the unaccepted client end of a WebSocketPair."), false);
|
|
8370
|
+
}
|
|
8371
|
+
const id = Number(__sbWsOp("o", 0, "", 0));
|
|
8372
|
+
if (!(id > 0)) return new Response("WebSockets are not supported by this host yet", { status: 501 });
|
|
8373
|
+
ws._host = id;
|
|
8374
|
+
r.headers._immutable = false;
|
|
8375
|
+
r.headers.set("x-sb-websocket", String(id));
|
|
8376
|
+
const held = ws._inbox;
|
|
8377
|
+
ws._inbox = [];
|
|
8378
|
+
for (let i = 0; i < held.length; i++) __sbWsDeliver(ws, held[i]);
|
|
8379
|
+
__sbWsPump(ws);
|
|
8380
|
+
return r;
|
|
8381
|
+
}
|
|
8382
|
+
|
|
8383
|
+
// Takes what the host received on ws's socket and dispatches it to the app's end, then
|
|
8384
|
+
// waits (a parked timer the host fires) for more.
|
|
8385
|
+
function __sbWsPump(ws) {
|
|
8386
|
+
const id = ws._host;
|
|
8387
|
+
for (;;) {
|
|
8388
|
+
const head = __sbWsOp("p", id, "", 0);
|
|
8389
|
+
if (head === "") {
|
|
8390
|
+
const timer = setTimeout(() => __sbWsPump(ws), 2147483647);
|
|
8391
|
+
__sbWsOp("w", id, "", timer);
|
|
8392
|
+
return;
|
|
8393
|
+
}
|
|
8394
|
+
const parts = head.split(" ");
|
|
8395
|
+
const kind = Number(parts[0]);
|
|
8396
|
+
const length = Number(parts[1]);
|
|
8397
|
+
const bytes = new Uint8Array(length);
|
|
8398
|
+
__sbWsOp("i", id, bytes, 0);
|
|
8399
|
+
let event;
|
|
8400
|
+
if (kind === 1) event = { kind: 1, data: bytes.buffer };
|
|
8401
|
+
else if (kind === 0 || kind === 2) {
|
|
8402
|
+
let text = length > 0 ? __sbFromUtf8Native(bytes) : "";
|
|
8403
|
+
if (text === "" && length > 0) text = new TextDecoder().decode(bytes);
|
|
8404
|
+
event = kind === 0 ? { kind: 0, data: text } : { kind: 2, code: Number(parts[2]), reason: text, clean: true };
|
|
8405
|
+
} else event = { kind: 2, code: 1006, reason: "WebSocket disconnected without sending Close frame.", clean: false };
|
|
8406
|
+
if (event.kind === 2) ws._closeSeen = true;
|
|
8407
|
+
const peer = ws._peer;
|
|
8408
|
+
if (peer !== null) {
|
|
8409
|
+
peer._inbox.push(event);
|
|
8410
|
+
// Dispatched now, so the acknowledgement below means the handler has run.
|
|
8411
|
+
if (!peer._flushing) __sbWsFlush(peer);
|
|
8412
|
+
}
|
|
8413
|
+
__sbWsOp("k", id, "", length);
|
|
8414
|
+
if (event.kind === 2) {
|
|
8415
|
+
__sbWsOp("r", id, "", 0);
|
|
8416
|
+
return;
|
|
8417
|
+
}
|
|
8418
|
+
}
|
|
8419
|
+
}
|
|
8420
|
+
|
|
8421
|
+
function __sbInstallWebSockets() {
|
|
8422
|
+
if (globalThis.__sbCompat < "2026-10-30") return;
|
|
8423
|
+
globalThis.WebSocketPair = __SbWebSocketPair;
|
|
8424
|
+
globalThis.WebSocket = __SbWebSocket;
|
|
8425
|
+
globalThis.WebSocketRequestResponsePair = __SbWebSocketRequestResponsePair;
|
|
8426
|
+
}
|
|
8427
|
+
|
|
8428
|
+
async function __sbFetchWithDrain(res, tasks, bindingCall, rethrow, top) {
|
|
7845
8429
|
let r;
|
|
7846
8430
|
try {
|
|
7847
8431
|
r = res && __sbIsFn(res.then) ? await res : res;
|
|
8432
|
+
if (top && __sbHasWebSocket(r)) r = __sbWsHostResponse(r);
|
|
7848
8433
|
if (__sbHasStreamBody(r)) {
|
|
7849
8434
|
if (__sbStreamingSupported()) __sbStreamResponse(r);
|
|
7850
8435
|
else r.body = await __sbCollectBody(__sbRawBody(r));
|
|
@@ -8141,7 +8726,9 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
8141
8726
|
}
|
|
8142
8727
|
// Same promise-identity rule as above applies to `__sbFetchWithDrain`'s
|
|
8143
8728
|
// own return, which is why it's tail-called rather than chained on here.
|
|
8144
|
-
if (__sbTasks.length || (__res && __sbIsFn(__res.then)) || __sbHasStreamBody(__res)) return __sbFetchWithDrain(__res, __sbTasks, bindingCall);
|
|
8729
|
+
if (__sbTasks.length || (__res && __sbIsFn(__res.then)) || __sbHasStreamBody(__res)) return __sbFetchWithDrain(__res, __sbTasks, bindingCall, false, true);
|
|
8730
|
+
// A WebSocket handshake answer (from 2026-10-30): its client end becomes the host socket.
|
|
8731
|
+
if (__sbHasWebSocket(__res)) return __sbWsHostResponse(__res);
|
|
8145
8732
|
return __sbTagCpu(__res, __t0);
|
|
8146
8733
|
}
|
|
8147
8734
|
if (!__sbTriggerAuthed(request)) return new Response("forbidden", { status: 403 });
|
|
@@ -8226,37 +8813,64 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
8226
8813
|
* timer in an embedded binary that has no broker to be delivered from.
|
|
8227
8814
|
*/
|
|
8228
8815
|
async function __sbRunQueueBatch(handlers, body, ctx) {
|
|
8816
|
+
const strict = __sbQueuesStrict();
|
|
8817
|
+
const binding = String(body.queue || "");
|
|
8818
|
+
const settings = __sbQueueSettings(binding);
|
|
8229
8819
|
const acked = [];
|
|
8230
8820
|
const retried = [];
|
|
8821
|
+
// id -> seconds, from retry({ delaySeconds }) (2026-10-31)
|
|
8822
|
+
const delays = {};
|
|
8231
8823
|
let threw = false;
|
|
8232
8824
|
const raw = body.messages || [];
|
|
8233
8825
|
const messages = [];
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8826
|
+
let batch;
|
|
8827
|
+
if (strict) {
|
|
8828
|
+
// The first of ack() and retry() on a message wins; ackAll() and retryAll() decide the
|
|
8829
|
+
// messages nothing has decided yet.
|
|
8830
|
+
const decided = {};
|
|
8831
|
+
const ack = (id) => {
|
|
8832
|
+
if (decided[id] !== true) {
|
|
8833
|
+
decided[id] = true;
|
|
8834
|
+
acked.push(id);
|
|
8835
|
+
}
|
|
8836
|
+
};
|
|
8837
|
+
const retry = (id, options) => {
|
|
8838
|
+
if (decided[id] !== true) {
|
|
8839
|
+
decided[id] = true;
|
|
8840
|
+
retried.push(id);
|
|
8841
|
+
if (options && options.delaySeconds != null) delays[id] = __sbQueueDelay(options.delaySeconds);
|
|
8842
|
+
}
|
|
8843
|
+
};
|
|
8844
|
+
for (let i = 0; i < raw.length; i++) messages.push(new __SbQueueMessage(raw[i], ack, retry));
|
|
8845
|
+
batch = new __SbQueueBatch(settings.queue, messages, body.metadata, ack, retry);
|
|
8846
|
+
} else {
|
|
8847
|
+
for (let i = 0; i < raw.length; i++) {
|
|
8848
|
+
const m = raw[i];
|
|
8849
|
+
const msg = {
|
|
8850
|
+
id: m.id,
|
|
8851
|
+
timestamp: m.timestamp,
|
|
8852
|
+
attempts: m.attempts || 1,
|
|
8853
|
+
body: __sbQueueDecode(m.body),
|
|
8854
|
+
ack() {
|
|
8855
|
+
if (acked.indexOf(m.id) === -1) acked.push(m.id);
|
|
8856
|
+
},
|
|
8857
|
+
retry() {
|
|
8858
|
+
if (retried.indexOf(m.id) === -1) retried.push(m.id);
|
|
8859
|
+
},
|
|
8860
|
+
};
|
|
8861
|
+
messages.push(msg);
|
|
8862
|
+
}
|
|
8863
|
+
batch = {
|
|
8864
|
+
queue: settings.queue,
|
|
8865
|
+
messages,
|
|
8866
|
+
ackAll() {
|
|
8867
|
+
for (let i = 0; i < messages.length; i++) messages[i].ack();
|
|
8243
8868
|
},
|
|
8244
|
-
|
|
8245
|
-
|
|
8869
|
+
retryAll() {
|
|
8870
|
+
for (let i = 0; i < messages.length; i++) messages[i].retry();
|
|
8246
8871
|
},
|
|
8247
8872
|
};
|
|
8248
|
-
messages.push(msg);
|
|
8249
8873
|
}
|
|
8250
|
-
const batch = {
|
|
8251
|
-
queue: body.queue || "",
|
|
8252
|
-
messages,
|
|
8253
|
-
ackAll() {
|
|
8254
|
-
for (let i = 0; i < messages.length; i++) messages[i].ack();
|
|
8255
|
-
},
|
|
8256
|
-
retryAll() {
|
|
8257
|
-
for (let i = 0; i < messages.length; i++) messages[i].retry();
|
|
8258
|
-
},
|
|
8259
|
-
};
|
|
8260
8874
|
try {
|
|
8261
8875
|
const __qres = globalThis.__sbCompat >= "2026-09-28"
|
|
8262
8876
|
? handlers.queue(batch, globalThis.env, ctx)
|
|
@@ -8274,12 +8888,65 @@ async function __sbRunQueueBatch(handlers, body, ctx) {
|
|
|
8274
8888
|
// A message neither acked nor retried is acked when the handler returns, and retried
|
|
8275
8889
|
// when it throws, as on Workers. A throw used to ack the whole batch on the embedded
|
|
8276
8890
|
// transport (standalone, sproutd), so a failing consumer lost its messages.
|
|
8277
|
-
for (let i = 0; i <
|
|
8278
|
-
if (acked.indexOf(
|
|
8279
|
-
(threw ? retried : acked).push(
|
|
8891
|
+
for (let i = 0; i < raw.length; i++) {
|
|
8892
|
+
if (acked.indexOf(raw[i].id) === -1 && retried.indexOf(raw[i].id) === -1)
|
|
8893
|
+
(threw ? retried : acked).push(raw[i].id);
|
|
8894
|
+
}
|
|
8895
|
+
const out = { ack: acked, retry: retried };
|
|
8896
|
+
// Retry delays and what happens past the last retry. Sent only where they differ from
|
|
8897
|
+
// what a store does by itself (5 s, 5 attempts, then marked dead), so earlier dates
|
|
8898
|
+
// without settings answer as before. A store that predates these fields (an older
|
|
8899
|
+
// broker) ignores them and retries the messages listed in `retry`.
|
|
8900
|
+
if (strict || settings.maxRetries !== undefined || settings.retryDelay !== undefined || settings.deadLetterQueue !== undefined) {
|
|
8901
|
+
const maxRetries = settings.maxRetries !== undefined ? settings.maxRetries : strict ? 3 : 4;
|
|
8902
|
+
const retryDelay = settings.retryDelay !== undefined ? settings.retryDelay : strict ? 0 : 5;
|
|
8903
|
+
const attempts = {};
|
|
8904
|
+
for (let i = 0; i < raw.length; i++) attempts[raw[i].id] = Number(raw[i].attempts) || 1;
|
|
8905
|
+
out.delays = {};
|
|
8906
|
+
out.dead = [];
|
|
8907
|
+
for (let i = 0; i < retried.length; i++) {
|
|
8908
|
+
const id = retried[i];
|
|
8909
|
+
if (attempts[id] > maxRetries) out.dead.push(id);
|
|
8910
|
+
else out.delays[id] = delays[id] !== undefined ? delays[id] : retryDelay;
|
|
8911
|
+
}
|
|
8912
|
+
if (out.dead.length && settings.deadLetterQueue !== undefined) out.deadLetter = __sbQueuePart(settings.deadLetterQueue);
|
|
8280
8913
|
}
|
|
8281
|
-
return
|
|
8914
|
+
return out;
|
|
8915
|
+
}
|
|
8916
|
+
|
|
8917
|
+
// A delivered message and batch from 2026-10-31: their data are own properties, as in
|
|
8918
|
+
// workerd, and ack/retry live on the prototype, out of Object.keys().
|
|
8919
|
+
function __SbQueueMessage(m, ack, retry) {
|
|
8920
|
+
this.id = String(m.id);
|
|
8921
|
+
this.timestamp = new Date(Number(m.timestamp) || Date.now());
|
|
8922
|
+
let decoded;
|
|
8923
|
+
try {
|
|
8924
|
+
decoded = __sbQueueDecode(m.body);
|
|
8925
|
+
} catch {
|
|
8926
|
+
decoded = m.body;
|
|
8927
|
+
}
|
|
8928
|
+
this.body = decoded;
|
|
8929
|
+
this.attempts = Number(m.attempts) || 1;
|
|
8930
|
+
Object.defineProperty(this, "__sbDecide", { value: { ack, retry }, enumerable: false });
|
|
8282
8931
|
}
|
|
8932
|
+
__SbQueueMessage.prototype.ack = function () {
|
|
8933
|
+
this.__sbDecide.ack(this.id);
|
|
8934
|
+
};
|
|
8935
|
+
__SbQueueMessage.prototype.retry = function (options) {
|
|
8936
|
+
this.__sbDecide.retry(this.id, options);
|
|
8937
|
+
};
|
|
8938
|
+
function __SbQueueBatch(queue, messages, metadata, ack, retry) {
|
|
8939
|
+
this.queue = queue;
|
|
8940
|
+
this.messages = messages;
|
|
8941
|
+
this.metadata = { metrics: __sbQueueMetrics(metadata && metadata.metrics) };
|
|
8942
|
+
Object.defineProperty(this, "__sbDecide", { value: { ack, retry }, enumerable: false });
|
|
8943
|
+
}
|
|
8944
|
+
__SbQueueBatch.prototype.ackAll = function () {
|
|
8945
|
+
for (let i = 0; i < this.messages.length; i++) this.__sbDecide.ack(this.messages[i].id);
|
|
8946
|
+
};
|
|
8947
|
+
__SbQueueBatch.prototype.retryAll = function (options) {
|
|
8948
|
+
for (let i = 0; i < this.messages.length; i++) this.__sbDecide.retry(this.messages[i].id, options);
|
|
8949
|
+
};
|
|
8283
8950
|
|
|
8284
8951
|
function __sbReadJson(request) {
|
|
8285
8952
|
try {
|