@sproutboat/runtime 0.28.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +488 -25
- package/src/transport-embedded.js +23 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.29.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 65f03ae: Cold standalone builds are fast again. Runtime 0.29.0's Durable Object storage codec named
|
|
8
|
+
eight typed-array constructors, which linked each builtin into every app and added about
|
|
9
|
+
7 s (45%) to an uncached build (16 s to 23 s for a one-line app on an M-series Mac). The
|
|
10
|
+
CLI's native-restore test passed its 600 s CI timeout.
|
|
11
|
+
|
|
12
|
+
From compatibility date `2026-10-28`, Durable Object storage round-trips `Uint8Array`,
|
|
13
|
+
`DataView` and `ArrayBuffer`. Any other typed array now throws a DataCloneError in
|
|
14
|
+
`put()` instead of being stored: keep its bytes in a `Uint8Array`. Before this change
|
|
15
|
+
they came back with the wrong type. Earlier dates are unchanged.
|
|
16
|
+
|
|
17
|
+
## 0.29.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 8df17ef: Durable Objects like workerd from compatibility date `2026-10-28`:
|
|
22
|
+
|
|
23
|
+
- **Input gate.** While an object's storage read is in flight, no other event
|
|
24
|
+
(`stub.fetch()`, an alarm) enters it, so `const v = await storage.get("n"); await
|
|
25
|
+
storage.put("n", v + 1)` is safe under concurrent calls. Ten concurrent increments used
|
|
26
|
+
to end at 1. An event that awaits anything else (a fetch, a timer) lets the next one
|
|
27
|
+
in, as in workerd. `blockConcurrencyWhile()` and `transaction()` hold the gate until
|
|
28
|
+
they settle, and `stub.fetch()` always returns a Promise.
|
|
29
|
+
- **Storage returns Promises** (`get`, `put`, `delete`, `deleteAll`, `list`, the alarm
|
|
30
|
+
methods), and adds `sync()` and `transaction()` (with `txn.rollback()`; a throw rolls
|
|
31
|
+
back; a transaction reads its own writes).
|
|
32
|
+
- **Structured values.** Date, Map, Set, RegExp, ArrayBuffer and typed arrays, BigInt,
|
|
33
|
+
`undefined` inside containers, NaN, the infinities and -0 round-trip.
|
|
34
|
+
`put(k, undefined)` throws a TypeError and a function throws a DataCloneError, before
|
|
35
|
+
anything is written.
|
|
36
|
+
- `list()` takes `start`, `startAfter`, `end` and `reverse`, and `limit: 0` throws.
|
|
37
|
+
`setAlarm()` refuses a value that is not a Date or a number. `deleteAll()` also drops
|
|
38
|
+
the alarm.
|
|
39
|
+
|
|
40
|
+
Earlier dates keep synchronous JSON storage and no gate. Values written from
|
|
41
|
+
`2026-10-28` start with `#sb1:`, and every date reads both formats.
|
|
42
|
+
|
|
43
|
+
Both transports' `do.storage.list` take `start`, `startAfter`, `end` and `reverse`, and
|
|
44
|
+
match the prefix exactly (SQL `LIKE` also matched `_` and `%` as wildcards, ignoring
|
|
45
|
+
case, so a prefix such as `user_` listed other keys).
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- 8df17ef: Queues fixes at every compatibility date (data loss, so not gated):
|
|
50
|
+
|
|
51
|
+
- On the embedded transport (standalone sprouts and sproutd), `sendBatch()` stored every
|
|
52
|
+
body as the text `"[object Object]"`. It now stores the body it was given, and both
|
|
53
|
+
`send()` and `sendBatch()` honour `delaySeconds` there, as the broker already did.
|
|
54
|
+
- A consumer whose `queue()` throws now retries every message it had not explicitly
|
|
55
|
+
acked or retried, as on Workers. The embedded transport used to ack the whole batch,
|
|
56
|
+
so a failing consumer lost its messages. Explicit `ack()` / `ackAll()` calls made
|
|
57
|
+
before the throw still count (Cloudflare's documented behaviour; miniflare retries
|
|
58
|
+
those too).
|
|
59
|
+
|
|
60
|
+
From compatibility date `2026-10-28`, `sendBatch(messages, { delaySeconds })` delays
|
|
61
|
+
every message whose own `delaySeconds` is unset or 0. Earlier dates ignore the batch
|
|
62
|
+
option.
|
|
63
|
+
|
|
3
64
|
## 0.28.0
|
|
4
65
|
|
|
5
66
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -5905,11 +5905,18 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
5905
5905
|
delaySeconds: o.delaySeconds || 0,
|
|
5906
5906
|
});
|
|
5907
5907
|
},
|
|
5908
|
-
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;
|
|
5909
5912
|
const list = [];
|
|
5910
5913
|
for (let j = 0; j < (messages || []).length; j++) {
|
|
5911
5914
|
const m = messages[j];
|
|
5912
|
-
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
|
+
});
|
|
5913
5920
|
}
|
|
5914
5921
|
__sbRpc("queue.send_batch", { queue: name, messages: list });
|
|
5915
5922
|
},
|
|
@@ -6514,14 +6521,14 @@ globalThis.__sbInstallFetch = function () {
|
|
|
6514
6521
|
|
|
6515
6522
|
// ---------------------------------------------------------------------------
|
|
6516
6523
|
// Durable Objects. The class runs here in the sandboxed sprout. There is exactly
|
|
6517
|
-
// one sprout process per deployment (the supervisor model)
|
|
6518
|
-
//
|
|
6519
|
-
//
|
|
6520
|
-
//
|
|
6521
|
-
//
|
|
6522
|
-
//
|
|
6523
|
-
//
|
|
6524
|
-
//
|
|
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).
|
|
6525
6532
|
|
|
6526
6533
|
function __sbMakeDONamespace(binding, className) {
|
|
6527
6534
|
return {
|
|
@@ -6570,9 +6577,14 @@ function __sbMakeDONamespace(binding, className) {
|
|
|
6570
6577
|
if (opts.body != null) req.body = String(opts.body);
|
|
6571
6578
|
}
|
|
6572
6579
|
const inst = __sbGetDOInstance(className, idStr);
|
|
6573
|
-
const
|
|
6574
|
-
|
|
6575
|
-
|
|
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();
|
|
6576
6588
|
},
|
|
6577
6589
|
};
|
|
6578
6590
|
},
|
|
@@ -6596,15 +6608,39 @@ function __sbGetDOInstance(cls, id) {
|
|
|
6596
6608
|
// `__sbDOInstances`), so the queue lives on `state` too and is drained by
|
|
6597
6609
|
// whichever call site invoked `fetch`/`alarm` this turn.
|
|
6598
6610
|
const __sbTasks = [];
|
|
6611
|
+
const gate = __sbDOMakeGate();
|
|
6599
6612
|
const state = {
|
|
6600
6613
|
id: {
|
|
6601
6614
|
toString() {
|
|
6602
6615
|
return id;
|
|
6603
6616
|
},
|
|
6604
6617
|
},
|
|
6605
|
-
storage: __sbDOStorage(cls, id),
|
|
6618
|
+
storage: __sbDOGated() ? __sbDOAsyncStorage(cls, id, gate) : __sbDOStorage(cls, id),
|
|
6606
6619
|
blockConcurrencyWhile(fn) {
|
|
6607
|
-
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
|
+
);
|
|
6608
6644
|
},
|
|
6609
6645
|
waitUntil(p) {
|
|
6610
6646
|
if (p && __sbIsFn(p.then)) __sbTasks.push(p);
|
|
@@ -6612,6 +6648,7 @@ function __sbGetDOInstance(cls, id) {
|
|
|
6612
6648
|
};
|
|
6613
6649
|
inst = new Ctor(state, globalThis.env);
|
|
6614
6650
|
inst.__sbTasks = __sbTasks;
|
|
6651
|
+
inst.__sbGate = gate;
|
|
6615
6652
|
__sbDOInstances[cacheKey] = inst;
|
|
6616
6653
|
}
|
|
6617
6654
|
return inst;
|
|
@@ -6624,12 +6661,12 @@ function __sbDOStorage(cls, id) {
|
|
|
6624
6661
|
const out = new Map();
|
|
6625
6662
|
for (let i = 0; i < key.length; i++) {
|
|
6626
6663
|
const r = __sbRpc("do.storage.get", { cls, id, key: String(key[i]) });
|
|
6627
|
-
if (r.found) out.set(key[i],
|
|
6664
|
+
if (r.found) out.set(key[i], __sbDODecode(r.value));
|
|
6628
6665
|
}
|
|
6629
6666
|
return out;
|
|
6630
6667
|
}
|
|
6631
6668
|
const r = __sbRpc("do.storage.get", { cls, id, key: String(key) });
|
|
6632
|
-
return r.found ?
|
|
6669
|
+
return r.found ? __sbDODecode(r.value) : undefined;
|
|
6633
6670
|
},
|
|
6634
6671
|
put(key, value) {
|
|
6635
6672
|
if (key != null && __sbIsObj(key)) {
|
|
@@ -6672,12 +6709,434 @@ function __sbDOStorage(cls, id) {
|
|
|
6672
6709
|
limit: o.limit == null ? 1000 : o.limit,
|
|
6673
6710
|
});
|
|
6674
6711
|
const out = new Map();
|
|
6675
|
-
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]));
|
|
6676
6713
|
return out;
|
|
6677
6714
|
},
|
|
6678
6715
|
};
|
|
6679
6716
|
}
|
|
6680
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
|
+
// Uint8Array and DataView only. Each other typed-array constructor named here links that
|
|
6809
|
+
// builtin into every app: the eight of them added about 7 s (45%) to a cold standalone
|
|
6810
|
+
// build. __sbDOPack refuses the other views instead of changing their type.
|
|
6811
|
+
function __sbDOView(name, bytes) {
|
|
6812
|
+
return name === "DataView" ? new DataView(bytes.buffer) : bytes;
|
|
6813
|
+
}
|
|
6814
|
+
|
|
6815
|
+
// A value as JSON: null, booleans, strings and finite numbers are themselves; everything
|
|
6816
|
+
// else is an array whose first element names its type, so no plain value is ambiguous.
|
|
6817
|
+
/* oxlint-disable anti-slop/no-runtime-typeof -- structured clone branches on the value's type, as the HTML spec's StructuredSerialize does. */
|
|
6818
|
+
function __sbDOPack(v, seen) {
|
|
6819
|
+
if (v === null || v === true || v === false || __sbIsStr(v)) return v;
|
|
6820
|
+
const t = typeof v;
|
|
6821
|
+
if (t === "number") {
|
|
6822
|
+
if (v === 0 && 1 / v < 0) return ["N", "-0"];
|
|
6823
|
+
return Number.isFinite(v) ? v : ["N", String(v)];
|
|
6824
|
+
}
|
|
6825
|
+
if (t === "bigint") return ["B", String(v)];
|
|
6826
|
+
if (t === "undefined") return ["U"];
|
|
6827
|
+
if (t !== "object") throw __sbDOCloneError(v);
|
|
6828
|
+
if (seen.indexOf(v) !== -1) throw __sbDOCloneError("a circular value");
|
|
6829
|
+
seen.push(v);
|
|
6830
|
+
let out;
|
|
6831
|
+
if (v instanceof Date) out = ["D", v.getTime()];
|
|
6832
|
+
else if (v instanceof Map) {
|
|
6833
|
+
out = ["M"];
|
|
6834
|
+
v.forEach((x, k) => {
|
|
6835
|
+
out.push(__sbDOPack(k, seen), __sbDOPack(x, seen));
|
|
6836
|
+
});
|
|
6837
|
+
} else if (v instanceof Set) {
|
|
6838
|
+
out = ["S"];
|
|
6839
|
+
v.forEach((x) => {
|
|
6840
|
+
out.push(__sbDOPack(x, seen));
|
|
6841
|
+
});
|
|
6842
|
+
} else if (v instanceof ArrayBuffer) out = ["AB", __sbDOHex(new Uint8Array(v))];
|
|
6843
|
+
else if (ArrayBuffer.isView(v)) {
|
|
6844
|
+
const name = Object.prototype.toString.call(v).slice(8, -1);
|
|
6845
|
+
if (name !== "Uint8Array" && name !== "DataView") {
|
|
6846
|
+
const error = new Error(name + " is not supported in Durable Object storage here yet; store a Uint8Array of its bytes.");
|
|
6847
|
+
error.name = "DataCloneError";
|
|
6848
|
+
throw error;
|
|
6849
|
+
}
|
|
6850
|
+
out = ["V", name, __sbDOHex(new Uint8Array(v.buffer, v.byteOffset, v.byteLength))];
|
|
6851
|
+
}
|
|
6852
|
+
else if (v instanceof RegExp) out = ["R", v.source, v.flags];
|
|
6853
|
+
else if (Array.isArray(v)) {
|
|
6854
|
+
out = ["A"];
|
|
6855
|
+
for (let i = 0; i < v.length; i++) out.push(__sbDOPack(v[i], seen));
|
|
6856
|
+
} else if (v instanceof Error) out = ["E", String(v.name), String(v.message)];
|
|
6857
|
+
else {
|
|
6858
|
+
out = ["O"];
|
|
6859
|
+
const keys = Object.keys(v);
|
|
6860
|
+
for (let i = 0; i < keys.length; i++) out.push(keys[i], __sbDOPack(v[keys[i]], seen));
|
|
6861
|
+
}
|
|
6862
|
+
seen.pop();
|
|
6863
|
+
return out;
|
|
6864
|
+
}
|
|
6865
|
+
/* oxlint-enable anti-slop/no-runtime-typeof */
|
|
6866
|
+
|
|
6867
|
+
function __sbDOUnpack(p) {
|
|
6868
|
+
if (!Array.isArray(p)) return p;
|
|
6869
|
+
const tag = p[0];
|
|
6870
|
+
if (tag === "U") return undefined;
|
|
6871
|
+
if (tag === "N") return p[1] === "-0" ? -0 : Number(p[1]);
|
|
6872
|
+
if (tag === "B") return BigInt(p[1]);
|
|
6873
|
+
if (tag === "D") return new Date(p[1]);
|
|
6874
|
+
if (tag === "AB") return __sbDOUnhex(p[1]).buffer;
|
|
6875
|
+
if (tag === "V") return __sbDOView(p[1], __sbDOUnhex(p[2]));
|
|
6876
|
+
if (tag === "R") return new RegExp(p[1], p[2]);
|
|
6877
|
+
if (tag === "E") {
|
|
6878
|
+
const e = new Error(p[2]);
|
|
6879
|
+
e.name = p[1];
|
|
6880
|
+
return e;
|
|
6881
|
+
}
|
|
6882
|
+
if (tag === "M") {
|
|
6883
|
+
const m = new Map();
|
|
6884
|
+
for (let i = 1; i + 1 < p.length; i += 2) m.set(__sbDOUnpack(p[i]), __sbDOUnpack(p[i + 1]));
|
|
6885
|
+
return m;
|
|
6886
|
+
}
|
|
6887
|
+
if (tag === "S") {
|
|
6888
|
+
const s = new Set();
|
|
6889
|
+
for (let i = 1; i < p.length; i++) s.add(__sbDOUnpack(p[i]));
|
|
6890
|
+
return s;
|
|
6891
|
+
}
|
|
6892
|
+
if (tag === "A") {
|
|
6893
|
+
const a = [];
|
|
6894
|
+
for (let i = 1; i < p.length; i++) a.push(__sbDOUnpack(p[i]));
|
|
6895
|
+
return a;
|
|
6896
|
+
}
|
|
6897
|
+
const o = {};
|
|
6898
|
+
for (let i = 1; i + 1 < p.length; i += 2) o[p[i]] = __sbDOUnpack(p[i + 1]);
|
|
6899
|
+
return o;
|
|
6900
|
+
}
|
|
6901
|
+
|
|
6902
|
+
function __sbDOEncode(value) {
|
|
6903
|
+
if (value === undefined) throw new TypeError("put() called with undefined value.");
|
|
6904
|
+
return __sbDOTag + JSON.stringify(__sbDOPack(value, []));
|
|
6905
|
+
}
|
|
6906
|
+
|
|
6907
|
+
// Reads both formats at every date: tagged (2026-10-28 writes) and plain JSON (earlier ones).
|
|
6908
|
+
function __sbDODecode(text) {
|
|
6909
|
+
if (__sbIsStr(text) && text.indexOf(__sbDOTag) === 0) return __sbDOUnpack(JSON.parse(text.slice(__sbDOTag.length)));
|
|
6910
|
+
return JSON.parse(text);
|
|
6911
|
+
}
|
|
6912
|
+
|
|
6913
|
+
function __sbDOListLimit(options) {
|
|
6914
|
+
const o = options || {};
|
|
6915
|
+
if (o.limit != null && !(Number(o.limit) > 0)) throw new TypeError("List limit must be positive.");
|
|
6916
|
+
return o;
|
|
6917
|
+
}
|
|
6918
|
+
|
|
6919
|
+
function __sbDOInRange(key, o) {
|
|
6920
|
+
if (o.prefix != null && key.indexOf(String(o.prefix)) !== 0) return false;
|
|
6921
|
+
if (o.start != null && key < String(o.start)) return false;
|
|
6922
|
+
if (o.startAfter != null && key <= String(o.startAfter)) return false;
|
|
6923
|
+
if (o.end != null && key >= String(o.end)) return false;
|
|
6924
|
+
return true;
|
|
6925
|
+
}
|
|
6926
|
+
|
|
6927
|
+
// The synchronous core of the 2026-10-28 storage: structured values, list() ranges.
|
|
6928
|
+
function __sbDOStrictStorage(cls, id) {
|
|
6929
|
+
const get = (key) => {
|
|
6930
|
+
const r = __sbRpc("do.storage.get", { cls, id, key: String(key) });
|
|
6931
|
+
return r.found ? __sbDODecode(r.value) : undefined;
|
|
6932
|
+
};
|
|
6933
|
+
const put = (key, value) => {
|
|
6934
|
+
__sbRpc("do.storage.put", { cls, id, key: String(key), value: __sbDOEncode(value) });
|
|
6935
|
+
};
|
|
6936
|
+
const del = (key) => !!__sbRpc("do.storage.delete", { cls, id, key: String(key) }).deleted;
|
|
6937
|
+
return {
|
|
6938
|
+
get(key) {
|
|
6939
|
+
if (!Array.isArray(key)) return get(key);
|
|
6940
|
+
const out = new Map();
|
|
6941
|
+
for (let i = 0; i < key.length; i++) {
|
|
6942
|
+
const v = get(key[i]);
|
|
6943
|
+
if (v !== undefined) out.set(key[i], v);
|
|
6944
|
+
}
|
|
6945
|
+
return out;
|
|
6946
|
+
},
|
|
6947
|
+
put(key, value) {
|
|
6948
|
+
if (key != null && __sbIsObj(key)) {
|
|
6949
|
+
// Encode every value before writing any, so one bad value writes nothing.
|
|
6950
|
+
const keys = Object.keys(key);
|
|
6951
|
+
const encoded = [];
|
|
6952
|
+
for (let i = 0; i < keys.length; i++) encoded.push(__sbDOEncode(key[keys[i]]));
|
|
6953
|
+
for (let i = 0; i < keys.length; i++) __sbRpc("do.storage.put", { cls, id, key: keys[i], value: encoded[i] });
|
|
6954
|
+
return;
|
|
6955
|
+
}
|
|
6956
|
+
put(key, value);
|
|
6957
|
+
},
|
|
6958
|
+
delete(key) {
|
|
6959
|
+
if (!Array.isArray(key)) return del(key);
|
|
6960
|
+
let n = 0;
|
|
6961
|
+
for (let i = 0; i < key.length; i++) n += del(key[i]) ? 1 : 0;
|
|
6962
|
+
return n;
|
|
6963
|
+
},
|
|
6964
|
+
deleteAll() {
|
|
6965
|
+
// workerd's deleteAll() also drops the alarm.
|
|
6966
|
+
__sbRpc("do.storage.delete_all", { cls, id });
|
|
6967
|
+
__sbRpc("do.alarm.delete", { cls, id });
|
|
6968
|
+
},
|
|
6969
|
+
setAlarm(when) {
|
|
6970
|
+
const at = when instanceof Date ? when.getTime() : when;
|
|
6971
|
+
if (!Number.isFinite(at))
|
|
6972
|
+
throw new TypeError("Failed to execute 'setAlarm' on 'DurableObjectStorage': parameter 1 is not of type 'date'.");
|
|
6973
|
+
__sbRpc("do.alarm.set", { cls, id, at: Number(at) });
|
|
6974
|
+
},
|
|
6975
|
+
getAlarm() {
|
|
6976
|
+
const r = __sbRpc("do.alarm.get", { cls, id });
|
|
6977
|
+
return r.at == null ? null : r.at;
|
|
6978
|
+
},
|
|
6979
|
+
deleteAlarm() {
|
|
6980
|
+
__sbRpc("do.alarm.delete", { cls, id });
|
|
6981
|
+
},
|
|
6982
|
+
list(options) {
|
|
6983
|
+
const o = __sbDOListLimit(options);
|
|
6984
|
+
const r = __sbRpc("do.storage.list", {
|
|
6985
|
+
cls,
|
|
6986
|
+
id,
|
|
6987
|
+
prefix: o.prefix == null ? "" : String(o.prefix),
|
|
6988
|
+
start: o.start == null ? null : String(o.start),
|
|
6989
|
+
startAfter: o.startAfter == null ? null : String(o.startAfter),
|
|
6990
|
+
end: o.end == null ? null : String(o.end),
|
|
6991
|
+
reverse: !!o.reverse,
|
|
6992
|
+
limit: o.limit == null ? 10000 : Number(o.limit),
|
|
6993
|
+
});
|
|
6994
|
+
const out = new Map();
|
|
6995
|
+
const entries = r.entries || [];
|
|
6996
|
+
// A transport that predates the range fields returns a superset: filter it here.
|
|
6997
|
+
for (let i = 0; i < entries.length; i++)
|
|
6998
|
+
if (__sbDOInRange(entries[i][0], o)) out.set(entries[i][0], __sbDODecode(entries[i][1]));
|
|
6999
|
+
return out;
|
|
7000
|
+
},
|
|
7001
|
+
};
|
|
7002
|
+
}
|
|
7003
|
+
|
|
7004
|
+
// The 2026-10-28 storage: every method returns a Promise; reads shut the input gate.
|
|
7005
|
+
function __sbDOAsyncStorage(cls, id, gate) {
|
|
7006
|
+
const s = __sbDOStrictStorage(cls, id);
|
|
7007
|
+
const settle = (f) => {
|
|
7008
|
+
try {
|
|
7009
|
+
return Promise.resolve(f());
|
|
7010
|
+
} catch (error) {
|
|
7011
|
+
return Promise.reject(error);
|
|
7012
|
+
}
|
|
7013
|
+
};
|
|
7014
|
+
const read = (f) => {
|
|
7015
|
+
__sbDOHold(gate);
|
|
7016
|
+
return settle(f);
|
|
7017
|
+
};
|
|
7018
|
+
return {
|
|
7019
|
+
get: (key) => read(() => s.get(key)),
|
|
7020
|
+
list: (options) => read(() => s.list(options)),
|
|
7021
|
+
getAlarm: () => read(() => s.getAlarm()),
|
|
7022
|
+
put: (key, value) => settle(() => s.put(key, value)),
|
|
7023
|
+
delete: (key) => settle(() => s.delete(key)),
|
|
7024
|
+
deleteAll: () => settle(() => s.deleteAll()),
|
|
7025
|
+
setAlarm: (when) => settle(() => s.setAlarm(when)),
|
|
7026
|
+
deleteAlarm: () => settle(() => s.deleteAlarm()),
|
|
7027
|
+
sync: () => Promise.resolve(),
|
|
7028
|
+
transaction: (fn) => __sbDOTransaction(cls, id, s, gate, fn),
|
|
7029
|
+
};
|
|
7030
|
+
}
|
|
7031
|
+
|
|
7032
|
+
// storage.transaction(fn): fn gets a txn with the storage methods; its writes stay in
|
|
7033
|
+
// memory (read back by its own get and list) and are written when fn resolves, unless it
|
|
7034
|
+
// threw or called txn.rollback(). The input gate stays shut meanwhile, so no other event
|
|
7035
|
+
// sees a half-written transaction.
|
|
7036
|
+
function __sbDOTransaction(cls, id, s, gate, fn) {
|
|
7037
|
+
const writes = new Map(); // key -> encoded value, or null for a delete
|
|
7038
|
+
let rolledBack = false;
|
|
7039
|
+
const getOne = (key) => {
|
|
7040
|
+
const k = String(key);
|
|
7041
|
+
if (!writes.has(k)) return s.get(k);
|
|
7042
|
+
const v = writes.get(k);
|
|
7043
|
+
return v === null ? undefined : __sbDODecode(v);
|
|
7044
|
+
};
|
|
7045
|
+
const putOne = (key, value) => {
|
|
7046
|
+
writes.set(String(key), __sbDOEncode(value));
|
|
7047
|
+
};
|
|
7048
|
+
const delOne = (key) => {
|
|
7049
|
+
const existed = getOne(key) !== undefined;
|
|
7050
|
+
writes.set(String(key), null);
|
|
7051
|
+
return existed;
|
|
7052
|
+
};
|
|
7053
|
+
const txn = {
|
|
7054
|
+
get(key) {
|
|
7055
|
+
if (!Array.isArray(key)) return Promise.resolve(getOne(key));
|
|
7056
|
+
const out = new Map();
|
|
7057
|
+
for (let i = 0; i < key.length; i++) {
|
|
7058
|
+
const v = getOne(key[i]);
|
|
7059
|
+
if (v !== undefined) out.set(key[i], v);
|
|
7060
|
+
}
|
|
7061
|
+
return Promise.resolve(out);
|
|
7062
|
+
},
|
|
7063
|
+
put(key, value) {
|
|
7064
|
+
try {
|
|
7065
|
+
if (key != null && __sbIsObj(key)) {
|
|
7066
|
+
const keys = Object.keys(key);
|
|
7067
|
+
for (let i = 0; i < keys.length; i++) putOne(keys[i], key[keys[i]]);
|
|
7068
|
+
} else putOne(key, value);
|
|
7069
|
+
return Promise.resolve();
|
|
7070
|
+
} catch (error) {
|
|
7071
|
+
return Promise.reject(error);
|
|
7072
|
+
}
|
|
7073
|
+
},
|
|
7074
|
+
delete(key) {
|
|
7075
|
+
if (!Array.isArray(key)) return Promise.resolve(delOne(key));
|
|
7076
|
+
let n = 0;
|
|
7077
|
+
for (let i = 0; i < key.length; i++) n += delOne(key[i]) ? 1 : 0;
|
|
7078
|
+
return Promise.resolve(n);
|
|
7079
|
+
},
|
|
7080
|
+
list(options) {
|
|
7081
|
+
try {
|
|
7082
|
+
const o = __sbDOListLimit(options);
|
|
7083
|
+
const merged = s.list({ prefix: o.prefix, start: o.start, startAfter: o.startAfter, end: o.end });
|
|
7084
|
+
writes.forEach((v, k) => {
|
|
7085
|
+
if (!__sbDOInRange(k, o)) return;
|
|
7086
|
+
if (v === null) merged.delete(k);
|
|
7087
|
+
else merged.set(k, __sbDODecode(v));
|
|
7088
|
+
});
|
|
7089
|
+
const keys = [];
|
|
7090
|
+
merged.forEach((v, k) => keys.push(k));
|
|
7091
|
+
keys.sort();
|
|
7092
|
+
if (o.reverse) keys.reverse();
|
|
7093
|
+
const out = new Map();
|
|
7094
|
+
const limit = o.limit == null ? keys.length : Number(o.limit);
|
|
7095
|
+
for (let i = 0; i < keys.length && i < limit; i++) out.set(keys[i], merged.get(keys[i]));
|
|
7096
|
+
return Promise.resolve(out);
|
|
7097
|
+
} catch (error) {
|
|
7098
|
+
return Promise.reject(error);
|
|
7099
|
+
}
|
|
7100
|
+
},
|
|
7101
|
+
rollback() {
|
|
7102
|
+
rolledBack = true;
|
|
7103
|
+
},
|
|
7104
|
+
getAlarm: () => Promise.resolve(s.getAlarm()),
|
|
7105
|
+
setAlarm: (when) => Promise.resolve(s.setAlarm(when)),
|
|
7106
|
+
deleteAlarm: () => Promise.resolve(s.deleteAlarm()),
|
|
7107
|
+
};
|
|
7108
|
+
gate.locks++;
|
|
7109
|
+
const done = () => {
|
|
7110
|
+
gate.locks--;
|
|
7111
|
+
__sbDOPump(gate);
|
|
7112
|
+
};
|
|
7113
|
+
let r;
|
|
7114
|
+
try {
|
|
7115
|
+
r = fn(txn);
|
|
7116
|
+
} catch (error) {
|
|
7117
|
+
done();
|
|
7118
|
+
return Promise.reject(error);
|
|
7119
|
+
}
|
|
7120
|
+
return Promise.resolve(r).then(
|
|
7121
|
+
(v) => {
|
|
7122
|
+
try {
|
|
7123
|
+
if (!rolledBack)
|
|
7124
|
+
writes.forEach((value, key) => {
|
|
7125
|
+
if (value === null) __sbRpc("do.storage.delete", { cls, id, key });
|
|
7126
|
+
else __sbRpc("do.storage.put", { cls, id, key, value });
|
|
7127
|
+
});
|
|
7128
|
+
} finally {
|
|
7129
|
+
done();
|
|
7130
|
+
}
|
|
7131
|
+
return v;
|
|
7132
|
+
},
|
|
7133
|
+
(error) => {
|
|
7134
|
+
done();
|
|
7135
|
+
throw error;
|
|
7136
|
+
},
|
|
7137
|
+
);
|
|
7138
|
+
}
|
|
7139
|
+
|
|
6681
7140
|
// ---------------------------------------------------------------------------
|
|
6682
7141
|
// Trigger dispatch. The compiled server only ever calls `fetch(request)`; this
|
|
6683
7142
|
// routes the internal `x-sb-trigger` requests (sent by the broker, authenticated
|
|
@@ -7347,7 +7806,7 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
7347
7806
|
if (!__sbIsFn(inst.alarm)) return new Response("no alarm handler", { status: 404 });
|
|
7348
7807
|
let __ares;
|
|
7349
7808
|
try {
|
|
7350
|
-
__ares = inst.alarm();
|
|
7809
|
+
__ares = __sbDOGated() ? __sbDOEnter(inst.__sbGate, () => inst.alarm()) : inst.alarm();
|
|
7351
7810
|
} catch (error) {
|
|
7352
7811
|
// #179 — same rule as fetch: don't let a throw here crash the process.
|
|
7353
7812
|
__sbLogUncaught("alarm", error);
|
|
@@ -7372,6 +7831,7 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
7372
7831
|
async function __sbRunQueueBatch(handlers, body, ctx) {
|
|
7373
7832
|
const acked = [];
|
|
7374
7833
|
const retried = [];
|
|
7834
|
+
let threw = false;
|
|
7375
7835
|
const raw = body.messages || [];
|
|
7376
7836
|
const messages = [];
|
|
7377
7837
|
for (let i = 0; i < raw.length; i++) {
|
|
@@ -7408,15 +7868,18 @@ async function __sbRunQueueBatch(handlers, body, ctx) {
|
|
|
7408
7868
|
// the response go out) before its own `await`s did, so any ack()/retry()
|
|
7409
7869
|
// past the first one never counted.
|
|
7410
7870
|
if (__qres && __sbIsFn(__qres.then)) await __qres;
|
|
7411
|
-
} catch {
|
|
7871
|
+
} catch (error) {
|
|
7412
7872
|
// #179 — a throw here must not crash the process. Whatever ack()/retry()
|
|
7413
|
-
// calls the handler made before throwing still count
|
|
7414
|
-
|
|
7415
|
-
|
|
7873
|
+
// calls the handler made before throwing still count.
|
|
7874
|
+
threw = true;
|
|
7875
|
+
__sbLogUncaught("queue", error);
|
|
7416
7876
|
}
|
|
7417
|
-
//
|
|
7877
|
+
// A message neither acked nor retried is acked when the handler returns, and retried
|
|
7878
|
+
// when it throws, as on Workers. A throw used to ack the whole batch on the embedded
|
|
7879
|
+
// transport (standalone, sproutd), so a failing consumer lost its messages.
|
|
7418
7880
|
for (let i = 0; i < messages.length; i++) {
|
|
7419
|
-
if (acked.indexOf(messages[i].id) === -1 && retried.indexOf(messages[i].id) === -1)
|
|
7881
|
+
if (acked.indexOf(messages[i].id) === -1 && retried.indexOf(messages[i].id) === -1)
|
|
7882
|
+
(threw ? retried : acked).push(messages[i].id);
|
|
7420
7883
|
}
|
|
7421
7884
|
return { ack: acked, retry: retried };
|
|
7422
7885
|
}
|
|
@@ -4194,12 +4194,17 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
4194
4194
|
}
|
|
4195
4195
|
|
|
4196
4196
|
if (op === "queue.send" || op === "queue.send_batch") {
|
|
4197
|
-
|
|
4197
|
+
// send_batch's items are { body, delaySeconds } objects, like the broker's. They were
|
|
4198
|
+
// stored with String(item), so every sendBatch() body arrived as "[object Object]"
|
|
4199
|
+
// (data loss), and delaySeconds was ignored on both calls.
|
|
4200
|
+
const items = op === "queue.send" ? [{ body: msg.body, delaySeconds: msg.delaySeconds }] : msg.messages || [];
|
|
4198
4201
|
for (let i = 0; i < items.length; i++) {
|
|
4202
|
+
const item = items[i] || {};
|
|
4203
|
+
const delay = Math.max(0, Number(item.delaySeconds) || 0) * 1000;
|
|
4199
4204
|
__sbSql(
|
|
4200
4205
|
store,
|
|
4201
4206
|
"INSERT INTO mq (queue, id, body, visible_at, attempts, dead) VALUES (?1,?2,?3,?4,0,0)",
|
|
4202
|
-
[msg.queue, __sbHex(12), String(
|
|
4207
|
+
[msg.queue, __sbHex(12), String(item.body), Date.now() + delay],
|
|
4203
4208
|
);
|
|
4204
4209
|
}
|
|
4205
4210
|
return { ok: true };
|
|
@@ -4240,8 +4245,21 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
4240
4245
|
store,
|
|
4241
4246
|
// Same clamp as the broker: an unbounded list builds the whole result in
|
|
4242
4247
|
// memory before the caller sees any of it.
|
|
4243
|
-
|
|
4244
|
-
|
|
4248
|
+
// start, startAfter, end and reverse come from 2026-10-28 storage (null when unset).
|
|
4249
|
+
// The prefix match is exact: LIKE also matched _ and % as wildcards, case-insensitively.
|
|
4250
|
+
"SELECT key, value FROM do_storage WHERE cls = ?1 AND id = ?2 AND substr(key, 1, length(?3)) = ?3" +
|
|
4251
|
+
" AND (?5 IS NULL OR key >= ?5) AND (?6 IS NULL OR key > ?6) AND (?7 IS NULL OR key < ?7)" +
|
|
4252
|
+
(msg.reverse ? " ORDER BY key DESC" : " ORDER BY key") +
|
|
4253
|
+
" LIMIT ?4",
|
|
4254
|
+
[
|
|
4255
|
+
msg.cls,
|
|
4256
|
+
msg.id,
|
|
4257
|
+
msg.prefix || "",
|
|
4258
|
+
Math.min(Math.max(Number(msg.limit) || 1000, 1), 10000),
|
|
4259
|
+
msg.start == null ? null : String(msg.start),
|
|
4260
|
+
msg.startAfter == null ? null : String(msg.startAfter),
|
|
4261
|
+
msg.end == null ? null : String(msg.end),
|
|
4262
|
+
],
|
|
4245
4263
|
);
|
|
4246
4264
|
const entries = [];
|
|
4247
4265
|
for (let i = 0; i < r.rows.length; i++) entries.push([r.rows[i][0], r.rows[i][1]]);
|
|
@@ -4799,7 +4817,7 @@ globalThis.__sbStartLocalTriggers = function (handlers, bindings) {
|
|
|
4799
4817
|
try {
|
|
4800
4818
|
const instance = __sbGetDOInstance(cls, id);
|
|
4801
4819
|
if (__sbIsFn(instance.alarm)) {
|
|
4802
|
-
const res = instance.alarm();
|
|
4820
|
+
const res = __sbDOGated() ? __sbDOEnter(instance.__sbGate, () => instance.alarm()) : instance.alarm();
|
|
4803
4821
|
if (res && __sbIsFn(res.then)) await res;
|
|
4804
4822
|
if (instance.__sbTasks.length) await __sbDrainWaitUntil(instance.__sbTasks);
|
|
4805
4823
|
}
|