@sproutboat/runtime 0.44.0 → 0.46.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 +44 -0
- package/package.json +1 -1
- package/src/asset-rules.test.js +51 -30
- package/src/native-fetch-prelude.js +1213 -67
- package/src/r2-lazy.test.js +101 -0
- package/src/transport-broker.js +13 -0
- package/src/transport-embedded.js +534 -47
- package/test/fixtures/asset-redirect-cases.ts +28 -0
- package/test/fixtures/do-evict-app.js +172 -0
- package/test/native-asset-redirects.ts +80 -0
- package/test/native-cc.ts +178 -0
- package/test/native-compile.ts +53 -22
- package/test/native-db-table.ts +244 -0
- package/test/native-do-evict.ts +345 -0
- package/test/native-lto.ts +79 -0
|
@@ -3043,6 +3043,12 @@ if (globalThis.setTimeout == null) globalThis.setTimeout = setTimeout;
|
|
|
3043
3043
|
if (globalThis.clearTimeout == null) globalThis.clearTimeout = clearTimeout;
|
|
3044
3044
|
if (globalThis.setInterval == null) globalThis.setInterval = setInterval;
|
|
3045
3045
|
if (globalThis.clearInterval == null) globalThis.clearInterval = clearInterval;
|
|
3046
|
+
// The runtime's timers, for the prelude's own use. Once a Durable Object is built at the eviction date, the four globals become
|
|
3047
|
+
// wrappers that pin the object that armed a timer (__sbDOWrapTimers); a parked timer or a poll of ours must never pin anything.
|
|
3048
|
+
const __sbSetTimeout = globalThis.setTimeout;
|
|
3049
|
+
const __sbClearTimeout = globalThis.clearTimeout;
|
|
3050
|
+
const __sbSetInterval = globalThis.setInterval;
|
|
3051
|
+
const __sbClearInterval = globalThis.clearInterval;
|
|
3046
3052
|
|
|
3047
3053
|
// Prepended to every handler by tools/compile.ts, before Porffor's native-fetch
|
|
3048
3054
|
// bundled handler. Porffor's runtime/fetch-globals.js (checked through alpha-10)
|
|
@@ -3895,7 +3901,7 @@ if (typeof globalThis.EventTarget !== "function") {
|
|
|
3895
3901
|
}
|
|
3896
3902
|
static timeout(ms) {
|
|
3897
3903
|
const signal = new AbortSignal();
|
|
3898
|
-
|
|
3904
|
+
__sbSetTimeout(() => signal.__sbAbort(namedError("TimeoutError", "The operation timed out")), ms);
|
|
3899
3905
|
return signal;
|
|
3900
3906
|
}
|
|
3901
3907
|
static any(signals) {
|
|
@@ -11024,28 +11030,63 @@ function __sbR2StrictObject(meta, bytes, range, include) {
|
|
|
11024
11030
|
};
|
|
11025
11031
|
if (bytes == null) return obj;
|
|
11026
11032
|
let used = false;
|
|
11033
|
+
// A lazy body (bytes.lazy: the object's file, read as asked) is never read whole unless text(), json(),
|
|
11034
|
+
// arrayBuffer() or blob() ask for all of it; its stream reads one chunk at a time.
|
|
11035
|
+
const lazy = bytes.lazy === true;
|
|
11027
11036
|
const take = () => {
|
|
11028
11037
|
if (used) throw new TypeError("Body has already been used. It can only be used once. Use tee() first if you need to read it twice.");
|
|
11029
11038
|
used = true;
|
|
11030
|
-
return bytes;
|
|
11039
|
+
return lazy ? __sbR2ReadAll(bytes, range) : bytes;
|
|
11031
11040
|
};
|
|
11032
11041
|
// The stream is made when first asked for: text() and friends read the bytes directly,
|
|
11033
11042
|
// so an object read as text never holds a second copy (sproutd #22).
|
|
11034
11043
|
let stream = null;
|
|
11035
11044
|
Object.defineProperty(obj, "body", {
|
|
11036
11045
|
get: () => {
|
|
11037
|
-
if (stream === null) stream = new Response(__sbBufFrom(bytes)).body;
|
|
11046
|
+
if (stream === null) stream = lazy ? __sbR2LazyStream(bytes, range) : new Response(__sbBufFrom(bytes)).body;
|
|
11038
11047
|
return stream;
|
|
11039
11048
|
},
|
|
11040
11049
|
enumerable: true,
|
|
11041
11050
|
});
|
|
11042
11051
|
Object.defineProperty(obj, "bodyUsed", { get: () => used, enumerable: true });
|
|
11043
|
-
|
|
11044
|
-
obj.
|
|
11045
|
-
obj.
|
|
11046
|
-
obj.
|
|
11052
|
+
// A lazy body reads into a Uint8Array (never a bytestring), so each of these takes bytes, not text.
|
|
11053
|
+
obj.text = async () => (lazy ? new TextDecoder().decode(take()) : __sbFromUtf8(take()));
|
|
11054
|
+
obj.json = async () => JSON.parse(lazy ? new TextDecoder().decode(take()) : __sbFromUtf8(take()));
|
|
11055
|
+
obj.arrayBuffer = async () => (lazy ? take().buffer : __sbBufFrom(take()));
|
|
11056
|
+
obj.blob = async () => new Blob([lazy ? take().buffer : __sbBufFrom(take())], { type: obj.httpMetadata.contentType || "" });
|
|
11047
11057
|
return obj;
|
|
11048
11058
|
}
|
|
11059
|
+
// The chunk a streamed R2 body reads at a time (a Uint8Array of this size, then collected): a chunk is
|
|
11060
|
+
// what the worker's heap holds per read, and what the response's flow control hands to the host.
|
|
11061
|
+
const __SB_R2_CHUNK = 256 * 1024;
|
|
11062
|
+
// The bytes of a lazy body's window, all of them: text(), json(), arrayBuffer() and blob() need the whole
|
|
11063
|
+
// object in memory whatever they are called on.
|
|
11064
|
+
function __sbR2ReadAll(src, range) {
|
|
11065
|
+
const out = new Uint8Array(range.length);
|
|
11066
|
+
if (range.length > 0 && __sbR2ReadRange(src.bucket, src.key, src.blobId, range.offset, out) !== range.length)
|
|
11067
|
+
throw new Error("sproutboat r2.get: the object could not be read");
|
|
11068
|
+
return out;
|
|
11069
|
+
}
|
|
11070
|
+
// A lazy body's window as a ReadableStream of __SB_R2_CHUNK-byte reads.
|
|
11071
|
+
function __sbR2LazyStream(src, range) {
|
|
11072
|
+
let at = 0;
|
|
11073
|
+
return new ReadableStream({
|
|
11074
|
+
pull(controller) {
|
|
11075
|
+
if (at >= range.length) {
|
|
11076
|
+
controller.close();
|
|
11077
|
+
return;
|
|
11078
|
+
}
|
|
11079
|
+
const n = Math.min(__SB_R2_CHUNK, range.length - at);
|
|
11080
|
+
const chunk = new Uint8Array(n);
|
|
11081
|
+
if (__sbR2ReadRange(src.bucket, src.key, src.blobId, range.offset + at, chunk) !== n) {
|
|
11082
|
+
controller.error(new Error("sproutboat r2.get: the object could not be read"));
|
|
11083
|
+
return;
|
|
11084
|
+
}
|
|
11085
|
+
at += n;
|
|
11086
|
+
controller.enqueue(chunk);
|
|
11087
|
+
},
|
|
11088
|
+
});
|
|
11089
|
+
}
|
|
11049
11090
|
// A hex string's bytes, one char per byte.
|
|
11050
11091
|
function __sbR2Bytes(hex) {
|
|
11051
11092
|
let out = "";
|
|
@@ -11092,7 +11133,19 @@ function __sbMakeR2(name) {
|
|
|
11092
11133
|
get(key, options) {
|
|
11093
11134
|
return attempt(() => {
|
|
11094
11135
|
const o = options || {};
|
|
11095
|
-
const
|
|
11136
|
+
const objectKey = __sbR2Key(key, "get");
|
|
11137
|
+
// Where the transport can read an object's file as asked (embedded: sproutd and standalone), get() takes its
|
|
11138
|
+
// metadata and no bytes: a range reads only its own window, a body streams. The broker answers null here
|
|
11139
|
+
// and returns the object whole, as before.
|
|
11140
|
+
const opened = __sbR2Open(name, objectKey);
|
|
11141
|
+
if (opened !== null) {
|
|
11142
|
+
if (!opened.found) return null;
|
|
11143
|
+
const meta = opened.object;
|
|
11144
|
+
if (!__sbR2OnlyIf(o.onlyIf, meta.md5 || meta.etag, meta.uploaded)) return __sbR2StrictObject(meta, null, { offset: 0, length: meta.size });
|
|
11145
|
+
const window = o.range == null ? { offset: 0, length: meta.size } : __sbR2Range(o.range, meta.size);
|
|
11146
|
+
return __sbR2StrictObject(meta, { lazy: true, bucket: name, key: objectKey, blobId: opened.blobId }, window);
|
|
11147
|
+
}
|
|
11148
|
+
const r = __sbR2Get(name, objectKey);
|
|
11096
11149
|
if (!r.found) return null;
|
|
11097
11150
|
const meta = r.object;
|
|
11098
11151
|
if (!__sbR2OnlyIf(o.onlyIf, meta.md5 || meta.etag, meta.uploaded)) return __sbR2StrictObject(meta, null, { offset: 0, length: meta.size });
|
|
@@ -11202,27 +11255,104 @@ function __sbMakeR2(name) {
|
|
|
11202
11255
|
};
|
|
11203
11256
|
}
|
|
11204
11257
|
|
|
11205
|
-
// Asset rules use the same parsed manifest in both transports.
|
|
11206
|
-
// @sproutboat/assets
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
|
|
11258
|
+
// Asset rules use the same parsed manifest in both transports. Redirect matching mirrors
|
|
11259
|
+
// @sproutboat/assets (Cloudflare's `_redirects` rules: the whole path must match, so a trailing
|
|
11260
|
+
// slash is significant; `:name` is one or more characters up to the next "/"; one `*` is
|
|
11261
|
+
// `:splat`; a static rule beats a dynamic one); parity tests cover the portable implementation.
|
|
11262
|
+
function __sbRuleAlpha(code) {
|
|
11263
|
+
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
|
|
11264
|
+
}
|
|
11265
|
+
function __sbRuleWord(code) {
|
|
11266
|
+
return __sbRuleAlpha(code) || (code >= 48 && code <= 57) || code === 95;
|
|
11267
|
+
}
|
|
11268
|
+
function __sbRuleIsStatic(from) {
|
|
11269
|
+
for (let i = 0; i < from.length; i++) {
|
|
11270
|
+
const code = from.charCodeAt(i);
|
|
11271
|
+
if (code === 42) return false;
|
|
11272
|
+
if (code === 58 && i + 1 < from.length && __sbRuleAlpha(from.charCodeAt(i + 1))) return false;
|
|
11213
11273
|
}
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
11274
|
+
return true;
|
|
11275
|
+
}
|
|
11276
|
+
// Tokens are [kind, text-or-name] pairs: 0 literal text, 1 :name, 2 splat. Null: the rule can never match.
|
|
11277
|
+
function __sbRuleTokens(from) {
|
|
11278
|
+
const tokens = [];
|
|
11279
|
+
const names = [];
|
|
11280
|
+
let text = "";
|
|
11218
11281
|
for (let i = 0; i < from.length; i++) {
|
|
11219
|
-
|
|
11220
|
-
|
|
11282
|
+
const code = from.charCodeAt(i);
|
|
11283
|
+
if (code === 42) {
|
|
11284
|
+
if (text !== "") tokens.push([0, text]);
|
|
11285
|
+
text = "";
|
|
11286
|
+
if (names.indexOf("splat") !== -1) return null;
|
|
11287
|
+
names.push("splat");
|
|
11288
|
+
tokens.push([2, "splat"]);
|
|
11289
|
+
} else if (code === 58 && i + 1 < from.length && __sbRuleAlpha(from.charCodeAt(i + 1))) {
|
|
11290
|
+
let end = i + 2;
|
|
11291
|
+
while (end < from.length && __sbRuleWord(from.charCodeAt(end))) end++;
|
|
11292
|
+
const name = from.slice(i + 1, end);
|
|
11293
|
+
if (names.indexOf(name) !== -1) return null;
|
|
11294
|
+
names.push(name);
|
|
11295
|
+
if (text !== "") tokens.push([0, text]);
|
|
11296
|
+
text = "";
|
|
11297
|
+
tokens.push([1, name]);
|
|
11298
|
+
i = end - 1;
|
|
11299
|
+
} else {
|
|
11300
|
+
text += from.charAt(i);
|
|
11301
|
+
}
|
|
11221
11302
|
}
|
|
11303
|
+
if (text !== "") tokens.push([0, text]);
|
|
11304
|
+
return tokens;
|
|
11305
|
+
}
|
|
11306
|
+
function __sbRuleMatch(tokens, t, path, p, names, values) {
|
|
11307
|
+
if (t === tokens.length) return p === path.length;
|
|
11308
|
+
const kind = tokens[t][0];
|
|
11309
|
+
const word = tokens[t][1];
|
|
11310
|
+
if (kind === 0) {
|
|
11311
|
+
if (path.slice(p, p + word.length) !== word) return false;
|
|
11312
|
+
return __sbRuleMatch(tokens, t + 1, path, p + word.length, names, values);
|
|
11313
|
+
}
|
|
11314
|
+
let limit = path.length;
|
|
11315
|
+
if (kind === 1) {
|
|
11316
|
+
for (let i = p; i < path.length; i++) {
|
|
11317
|
+
if (path.charCodeAt(i) === 47) {
|
|
11318
|
+
limit = i;
|
|
11319
|
+
break;
|
|
11320
|
+
}
|
|
11321
|
+
}
|
|
11322
|
+
}
|
|
11323
|
+
const least = kind === 1 ? p + 1 : p;
|
|
11324
|
+
for (let q = limit; q >= least; q--) {
|
|
11325
|
+
names.push(word);
|
|
11326
|
+
values.push(path.slice(p, q));
|
|
11327
|
+
if (__sbRuleMatch(tokens, t + 1, path, q, names, values)) return true;
|
|
11328
|
+
names.pop();
|
|
11329
|
+
values.pop();
|
|
11330
|
+
}
|
|
11331
|
+
return false;
|
|
11332
|
+
}
|
|
11333
|
+
function __sbAssetRedirect(rule, path) {
|
|
11334
|
+
if (__sbRuleIsStatic(rule.from)) return rule.from === path ? rule.to : null;
|
|
11335
|
+
const tokens = __sbRuleTokens(rule.from);
|
|
11336
|
+
if (tokens === null) return null;
|
|
11337
|
+
const names = [];
|
|
11338
|
+
const values = [];
|
|
11339
|
+
if (!__sbRuleMatch(tokens, 0, path, 0, names, values)) return null;
|
|
11222
11340
|
let to = rule.to;
|
|
11223
|
-
for (
|
|
11341
|
+
for (let i = 0; i < names.length; i++) to = to.split(":" + names[i]).join(values[i]);
|
|
11224
11342
|
return to;
|
|
11225
11343
|
}
|
|
11344
|
+
// The first static rule that matches, else the first dynamic one: { status, location } or null.
|
|
11345
|
+
function __sbFindAssetRedirect(rules, path) {
|
|
11346
|
+
for (let i = 0; i < rules.length; i++) {
|
|
11347
|
+
if (__sbRuleIsStatic(rules[i].from) && rules[i].from === path) return { status: rules[i].status, location: rules[i].to };
|
|
11348
|
+
}
|
|
11349
|
+
for (let i = 0; i < rules.length; i++) {
|
|
11350
|
+
if (__sbRuleIsStatic(rules[i].from)) continue;
|
|
11351
|
+
const location = __sbAssetRedirect(rules[i], path);
|
|
11352
|
+
if (location !== null) return { status: rules[i].status, location };
|
|
11353
|
+
}
|
|
11354
|
+
return null;
|
|
11355
|
+
}
|
|
11226
11356
|
|
|
11227
11357
|
function __sbAssetHeaderMatches(pattern, path) {
|
|
11228
11358
|
const from = pattern.split("/").filter(Boolean);
|
|
@@ -11237,11 +11367,8 @@ function __sbAssetHeaderMatches(pattern, path) {
|
|
|
11237
11367
|
function __sbAssetResponse(path, r) {
|
|
11238
11368
|
if (r.location != null) return new Response(null, { status: r.status, headers: { location: r.location } });
|
|
11239
11369
|
const rules = r.rules || {};
|
|
11240
|
-
const
|
|
11241
|
-
|
|
11242
|
-
const to = __sbAssetRedirect(redirects[i], path);
|
|
11243
|
-
if (to !== null) return new Response(null, { status: redirects[i].status, headers: { location: to } });
|
|
11244
|
-
}
|
|
11370
|
+
const redirect = __sbFindAssetRedirect(rules.redirects || [], path);
|
|
11371
|
+
if (redirect !== null) return new Response(null, { status: redirect.status, headers: { location: redirect.location } });
|
|
11245
11372
|
const headers = new Headers();
|
|
11246
11373
|
if (r.type) headers.set("content-type", r.type);
|
|
11247
11374
|
if (r.found) headers.set("etag", '"' + r.hash + '"');
|
|
@@ -12155,7 +12282,7 @@ async function __sbPumpRequestBody(id, source, ended) {
|
|
|
12155
12282
|
if (bytes.length === 0) continue;
|
|
12156
12283
|
let backlog = Number(globalThis.__sbAsyncBody(id, "c", __sbToBytes(bytes)));
|
|
12157
12284
|
while (backlog > 4 * 1024 * 1024 && !ended()) {
|
|
12158
|
-
await new Promise((resolve) =>
|
|
12285
|
+
await new Promise((resolve) => __sbSetTimeout(resolve, 2));
|
|
12159
12286
|
backlog = Number(globalThis.__sbAsyncBody(id, "q", ""));
|
|
12160
12287
|
}
|
|
12161
12288
|
}
|
|
@@ -12175,10 +12302,10 @@ function __sbUpstreamStreamed(op, msg, label, signal) {
|
|
|
12175
12302
|
let done = false;
|
|
12176
12303
|
const finish = () => {
|
|
12177
12304
|
done = true;
|
|
12178
|
-
|
|
12305
|
+
__sbClearInterval(id);
|
|
12179
12306
|
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
12180
12307
|
};
|
|
12181
|
-
const id =
|
|
12308
|
+
const id = __sbSetInterval(() => {
|
|
12182
12309
|
if (done) return;
|
|
12183
12310
|
const raw = __sbHttpAsyncResult(id);
|
|
12184
12311
|
if (controller === null) {
|
|
@@ -12314,7 +12441,7 @@ function __sbUpstream(op, msg, label, signal, always) {
|
|
|
12314
12441
|
const settled = () => {
|
|
12315
12442
|
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
12316
12443
|
};
|
|
12317
|
-
const id =
|
|
12444
|
+
const id = __sbSetTimeout(() => {
|
|
12318
12445
|
settled();
|
|
12319
12446
|
try {
|
|
12320
12447
|
resolve(__sbUpstreamResponse(__sbFetchReply(__sbHttpAsyncResult(id))));
|
|
@@ -12325,7 +12452,7 @@ function __sbUpstream(op, msg, label, signal, always) {
|
|
|
12325
12452
|
if (signal) {
|
|
12326
12453
|
onAbort = () => {
|
|
12327
12454
|
settled();
|
|
12328
|
-
|
|
12455
|
+
__sbClearTimeout(id);
|
|
12329
12456
|
__sbCancelAsync(id);
|
|
12330
12457
|
reject(signal.reason);
|
|
12331
12458
|
};
|
|
@@ -12336,7 +12463,7 @@ function __sbUpstream(op, msg, label, signal, always) {
|
|
|
12336
12463
|
globalThis.__sbFetchUpstream(op, msg);
|
|
12337
12464
|
} catch (e) {
|
|
12338
12465
|
settled();
|
|
12339
|
-
|
|
12466
|
+
__sbClearTimeout(id);
|
|
12340
12467
|
reject(e);
|
|
12341
12468
|
}
|
|
12342
12469
|
});
|
|
@@ -12349,7 +12476,7 @@ function __sbUpstream(op, msg, label, signal, always) {
|
|
|
12349
12476
|
globalThis.__sbInstallFetch = function () {
|
|
12350
12477
|
__sbIngressStreamingSetup();
|
|
12351
12478
|
__sbInstallWebSockets();
|
|
12352
|
-
|
|
12479
|
+
const plainFetch = function (input, init) {
|
|
12353
12480
|
let parts;
|
|
12354
12481
|
try {
|
|
12355
12482
|
parts = __sbFetchParts(input, init);
|
|
@@ -12371,6 +12498,29 @@ globalThis.__sbInstallFetch = function () {
|
|
|
12371
12498
|
if (stream instanceof ReadableStream) return __sbCollectBody(stream).then((bytes) => send(__sbRequestBody(bytes)));
|
|
12372
12499
|
return send(__sbPartsBody(parts));
|
|
12373
12500
|
};
|
|
12501
|
+
// Inside a Durable Object's event an unawaited fetch keeps its object in memory until it settles.
|
|
12502
|
+
globalThis.fetch = function (input, init) {
|
|
12503
|
+
const rec = __sbDOL !== null ? __sbDOFrameRec() : undefined;
|
|
12504
|
+
if (rec === undefined || rec.dead) return plainFetch(input, init);
|
|
12505
|
+
__sbDOPin(rec);
|
|
12506
|
+
let p;
|
|
12507
|
+
try {
|
|
12508
|
+
p = plainFetch(input, init);
|
|
12509
|
+
} catch (error) {
|
|
12510
|
+
__sbDOUnpin(rec);
|
|
12511
|
+
throw error;
|
|
12512
|
+
}
|
|
12513
|
+
return p.then(
|
|
12514
|
+
(r) => {
|
|
12515
|
+
__sbDOUnpin(rec);
|
|
12516
|
+
return r;
|
|
12517
|
+
},
|
|
12518
|
+
(error) => {
|
|
12519
|
+
__sbDOUnpin(rec);
|
|
12520
|
+
throw error;
|
|
12521
|
+
},
|
|
12522
|
+
);
|
|
12523
|
+
};
|
|
12374
12524
|
};
|
|
12375
12525
|
|
|
12376
12526
|
// ---------------------------------------------------------------------------
|
|
@@ -12618,6 +12768,7 @@ function __sbDOCallback(fn, args) {
|
|
|
12618
12768
|
|
|
12619
12769
|
// One RPC call: method `method` with `args`, or the getter `method` when args is null.
|
|
12620
12770
|
function __sbDOInvoke(cls, id, method, args) {
|
|
12771
|
+
if (__sbDOEvictOn()) return __sbDOInvokeLive(cls, id, method, args);
|
|
12621
12772
|
let copied = null;
|
|
12622
12773
|
let inst;
|
|
12623
12774
|
try {
|
|
@@ -12644,7 +12795,38 @@ function __sbDOInvoke(cls, id, method, args) {
|
|
|
12644
12795
|
);
|
|
12645
12796
|
}
|
|
12646
12797
|
|
|
12798
|
+
// RPC into an object that may be evicted (compatibility date 2026-11-21): the event enters through __sbDORun.
|
|
12799
|
+
function __sbDOInvokeLive(cls, id, method, args) {
|
|
12800
|
+
let copied = null;
|
|
12801
|
+
try {
|
|
12802
|
+
if (args !== null) copied = __sbDOCopy(args);
|
|
12803
|
+
} catch (error) {
|
|
12804
|
+
return Promise.reject(error);
|
|
12805
|
+
}
|
|
12806
|
+
return __sbDORun(
|
|
12807
|
+
cls,
|
|
12808
|
+
id.toString(),
|
|
12809
|
+
id,
|
|
12810
|
+
(inst) => {
|
|
12811
|
+
const d = __sbDOFind(__sbDOClasses[cls], method);
|
|
12812
|
+
if (args === null) return d.get.call(inst);
|
|
12813
|
+
if (!d || !__sbIsFn(d.value)) throw new TypeError('The RPC receiver does not implement the method "' + method + '".');
|
|
12814
|
+
return d.value.apply(inst, copied);
|
|
12815
|
+
},
|
|
12816
|
+
false,
|
|
12817
|
+
).then(__sbDOCopy);
|
|
12818
|
+
}
|
|
12819
|
+
|
|
12647
12820
|
function __sbDOFetchV2(className, id, input, init) {
|
|
12821
|
+
if (__sbDOEvictOn()) {
|
|
12822
|
+
let request;
|
|
12823
|
+
try {
|
|
12824
|
+
request = new Request(input, init);
|
|
12825
|
+
} catch (error) {
|
|
12826
|
+
return Promise.reject(error);
|
|
12827
|
+
}
|
|
12828
|
+
return __sbDORun(className, id.toString(), id, (inst) => __sbDOFetchEvent(inst, request), false);
|
|
12829
|
+
}
|
|
12648
12830
|
let req;
|
|
12649
12831
|
let inst;
|
|
12650
12832
|
try {
|
|
@@ -12769,6 +12951,691 @@ function __sbGetDOInstance(cls, idText, idObj) {
|
|
|
12769
12951
|
return inst;
|
|
12770
12952
|
}
|
|
12771
12953
|
|
|
12954
|
+
// ---------------------------------------------------------------------------
|
|
12955
|
+
// Durable Object lifecycle (design 07 stage 1c, plan 018), from compatibility date 2026-11-21: an idle object
|
|
12956
|
+
// leaves memory and is constructed again by its next event, so one app can hold one object per customer.
|
|
12957
|
+
//
|
|
12958
|
+
// A record per object in a worker (not the bare instance): { cls, id, key, inst, state, gate, holder, tasks, busy,
|
|
12959
|
+
// pins, last, dead, prev, next, ... }. `busy` counts the events that entered through __sbDORun and have not
|
|
12960
|
+
// finished, their waitUntil drain included; `pins` counts things that outlive an event and cannot be rebuilt (a pending
|
|
12961
|
+
// timer, an unawaited fetch, a streamed response); open plain sockets are checked when idleness is. The records are one
|
|
12962
|
+
// least-recently-idle list (`head` the stalest). An object is evicted only when __sbDOIsIdle says so, in one synchronous
|
|
12963
|
+
// turn (__sbDOEvict): nothing in it awaits. Before the date nothing here runs and every object stays resident.
|
|
12964
|
+
//
|
|
12965
|
+
// Settings (environment; sproutd sets them from `limits ID do-idle-ms|objects|do-wait-ms`):
|
|
12966
|
+
// SB_DO_IDLE_MS how long an object sits idle before it leaves, 100 to 600000 (default 10000; 0 turns idle eviction off)
|
|
12967
|
+
// SB_DO_MAX most objects in memory in this worker (default 16 per MiB of SB_MEMORY_LIMIT, 64 to 4096; 4096 with no limit)
|
|
12968
|
+
// SB_DO_WAIT_MS how long an object's first event waits for a slot at the cap before it fails alone (default 5000)
|
|
12969
|
+
// SB_DO_QUEUE_MAX most events queued behind one object's input gate (default 1024)
|
|
12970
|
+
const __SB_DO_EVICT_DATE = "2026-11-21";
|
|
12971
|
+
const __sbDOCtxKey = {};
|
|
12972
|
+
let __sbDOL = null;
|
|
12973
|
+
|
|
12974
|
+
function __sbDOEvictOn() {
|
|
12975
|
+
return __sbCompatDate() >= __SB_DO_EVICT_DATE && __sbDOGated() && __sbDOv3();
|
|
12976
|
+
}
|
|
12977
|
+
|
|
12978
|
+
function __sbDOEnvNum(name, fallback, min, max) {
|
|
12979
|
+
const text = __sbEnv(name);
|
|
12980
|
+
if (text === "") return fallback;
|
|
12981
|
+
const n = Number(text);
|
|
12982
|
+
return n >= min && n <= max ? Math.trunc(n) : fallback;
|
|
12983
|
+
}
|
|
12984
|
+
|
|
12985
|
+
function __sbDOLife() {
|
|
12986
|
+
if (__sbDOL !== null) return __sbDOL;
|
|
12987
|
+
let idle = __sbDOEnvNum("SB_DO_IDLE_MS", 10000, 100, 600000);
|
|
12988
|
+
if (__sbEnv("SB_DO_IDLE_MS") === "0") idle = 0;
|
|
12989
|
+
const limit = Number(__sbEnv("SB_MEMORY_LIMIT"));
|
|
12990
|
+
const byLimit = limit > 0 ? Math.max(64, Math.min(4096, Math.floor(limit / 1048576) * 16)) : 4096;
|
|
12991
|
+
__sbDOL = {
|
|
12992
|
+
idle,
|
|
12993
|
+
cap: __sbDOEnvNum("SB_DO_MAX", byLimit, 1, 100000),
|
|
12994
|
+
waitMs: __sbDOEnvNum("SB_DO_WAIT_MS", 5000, 10, 600000),
|
|
12995
|
+
queueMax: __sbDOEnvNum("SB_DO_QUEUE_MAX", 1024, 1, 1000000),
|
|
12996
|
+
recs: {},
|
|
12997
|
+
head: null,
|
|
12998
|
+
tail: null,
|
|
12999
|
+
count: 0,
|
|
13000
|
+
waiters: [],
|
|
13001
|
+
sweepArmed: false,
|
|
13002
|
+
hostU: -1,
|
|
13003
|
+
restoreParked: false,
|
|
13004
|
+
restoreTries: {},
|
|
13005
|
+
noteAt: 0,
|
|
13006
|
+
noteText: "",
|
|
13007
|
+
pinned: 0,
|
|
13008
|
+
evictedIdle: 0,
|
|
13009
|
+
evictedCap: 0,
|
|
13010
|
+
evictedAbort: 0,
|
|
13011
|
+
waits: 0,
|
|
13012
|
+
waitMsMax: 0,
|
|
13013
|
+
overloaded: 0,
|
|
13014
|
+
zombie: 0,
|
|
13015
|
+
wakeUsLast: 0,
|
|
13016
|
+
wakeUsMax: 0,
|
|
13017
|
+
};
|
|
13018
|
+
__sbDOWrapTimers();
|
|
13019
|
+
return __sbDOL;
|
|
13020
|
+
}
|
|
13021
|
+
|
|
13022
|
+
function __sbDOEvictedError() {
|
|
13023
|
+
return new Error("This Durable Object instance was evicted from memory.");
|
|
13024
|
+
}
|
|
13025
|
+
|
|
13026
|
+
function __sbDOOverloaded(why) {
|
|
13027
|
+
const error = new Error("Durable Object is overloaded: " + why);
|
|
13028
|
+
error.overloaded = true;
|
|
13029
|
+
return error;
|
|
13030
|
+
}
|
|
13031
|
+
|
|
13032
|
+
// The record of the object whose event is running now (the frame the event entered carries it), or undefined.
|
|
13033
|
+
function __sbDOFrameRec() {
|
|
13034
|
+
const frame = __sbCtxGet();
|
|
13035
|
+
if (!frame) return undefined;
|
|
13036
|
+
const at = __sbFrameIndex(frame, __sbDOCtxKey);
|
|
13037
|
+
return at === -1 ? undefined : frame.vals[at];
|
|
13038
|
+
}
|
|
13039
|
+
|
|
13040
|
+
// ---- the list: head is the stalest record, tail the freshest ----
|
|
13041
|
+
function __sbDOLink(L, rec) {
|
|
13042
|
+
rec.prev = L.tail;
|
|
13043
|
+
rec.next = null;
|
|
13044
|
+
if (L.tail !== null) L.tail.next = rec;
|
|
13045
|
+
else L.head = rec;
|
|
13046
|
+
L.tail = rec;
|
|
13047
|
+
L.count++;
|
|
13048
|
+
}
|
|
13049
|
+
function __sbDOUnlink(L, rec) {
|
|
13050
|
+
if (rec.prev !== null) rec.prev.next = rec.next;
|
|
13051
|
+
else L.head = rec.next;
|
|
13052
|
+
if (rec.next !== null) rec.next.prev = rec.prev;
|
|
13053
|
+
else L.tail = rec.prev;
|
|
13054
|
+
rec.prev = null;
|
|
13055
|
+
rec.next = null;
|
|
13056
|
+
L.count--;
|
|
13057
|
+
}
|
|
13058
|
+
// Becomes idle now: the freshest record.
|
|
13059
|
+
function __sbDOFresh(L, rec, now) {
|
|
13060
|
+
rec.last = now === undefined ? Date.now() : now;
|
|
13061
|
+
if (L.tail === rec) return;
|
|
13062
|
+
__sbDOUnlink(L, rec);
|
|
13063
|
+
__sbDOLink(L, rec);
|
|
13064
|
+
}
|
|
13065
|
+
|
|
13066
|
+
// ---- idleness ----
|
|
13067
|
+
function __sbDOSocketsFree(L, rec) {
|
|
13068
|
+
const plain = rec.plain;
|
|
13069
|
+
if (plain.length > 0) {
|
|
13070
|
+
for (let i = plain.length - 1; i >= 0; i--) if (plain[i]._state === __SB_WS_CLOSED) plain.splice(i, 1);
|
|
13071
|
+
if (plain.length > 0) return false;
|
|
13072
|
+
}
|
|
13073
|
+
const tcp = rec.tcp;
|
|
13074
|
+
if (tcp.length > 0) {
|
|
13075
|
+
for (let i = tcp.length - 1; i >= 0; i--) if (tcp[i].id === 0) tcp.splice(i, 1);
|
|
13076
|
+
if (tcp.length > 0) return false;
|
|
13077
|
+
}
|
|
13078
|
+
const sockets = rec.holder.sockets;
|
|
13079
|
+
for (let i = 0; i < sockets.length; i++) {
|
|
13080
|
+
const server = sockets[i];
|
|
13081
|
+
if (server._state === __SB_WS_CLOSED) continue;
|
|
13082
|
+
const client = server._peer;
|
|
13083
|
+
// A pair whose two ends are both in the worker, or no host that takes a socket back: nothing outside holds it.
|
|
13084
|
+
if (client === null || client._host === 0) return false;
|
|
13085
|
+
if (L.hostU < 0) L.hostU = __sbWsOp("U", 0, "", 0) === "1" ? 1 : 0;
|
|
13086
|
+
if (L.hostU !== 1) return false;
|
|
13087
|
+
// An event that left the bridge and exists only in this worker (the bridge keeps the rest of the queue).
|
|
13088
|
+
if (client._inbox.length > 0 || server._inbox.length > 0 || client._flushing || server._flushing) return false;
|
|
13089
|
+
}
|
|
13090
|
+
return true;
|
|
13091
|
+
}
|
|
13092
|
+
function __sbDOIsIdle(L, rec) {
|
|
13093
|
+
const gate = rec.gate;
|
|
13094
|
+
return (
|
|
13095
|
+
rec.busy === 0 &&
|
|
13096
|
+
rec.pins === 0 &&
|
|
13097
|
+
gate.locks === 0 &&
|
|
13098
|
+
gate.waiting.length === 0 &&
|
|
13099
|
+
!gate.txnBusy &&
|
|
13100
|
+
gate.txnQueue.length === 0 &&
|
|
13101
|
+
__sbDOSocketsFree(L, rec)
|
|
13102
|
+
);
|
|
13103
|
+
}
|
|
13104
|
+
|
|
13105
|
+
function __sbDOPin(rec) {
|
|
13106
|
+
rec.pins++;
|
|
13107
|
+
if (rec.pins === 1) {
|
|
13108
|
+
rec.pinSince = Date.now();
|
|
13109
|
+
__sbDOL.pinned++;
|
|
13110
|
+
}
|
|
13111
|
+
}
|
|
13112
|
+
function __sbDOUnpin(rec) {
|
|
13113
|
+
if (rec.pins === 0) return;
|
|
13114
|
+
rec.pins--;
|
|
13115
|
+
if (rec.pins !== 0) return;
|
|
13116
|
+
const L = __sbDOL;
|
|
13117
|
+
if (rec.dead) return;
|
|
13118
|
+
L.pinned--;
|
|
13119
|
+
rec.pinLogged = false;
|
|
13120
|
+
if (rec.busy === 0) {
|
|
13121
|
+
__sbDOFresh(L, rec);
|
|
13122
|
+
__sbDOServe(L);
|
|
13123
|
+
__sbDOArmSweep(L);
|
|
13124
|
+
}
|
|
13125
|
+
}
|
|
13126
|
+
|
|
13127
|
+
// ---- eviction: one synchronous turn, nothing awaits ----
|
|
13128
|
+
// The reason says why: "idle", "cap" (a slot was needed), "abort" (state.abort()), "bcw" (blockConcurrencyWhile failed).
|
|
13129
|
+
// `force` skips the idleness check and closes the object's sockets (abort and reset); otherwise sockets go back to the host.
|
|
13130
|
+
function __sbDOEvict(rec, reason, force) {
|
|
13131
|
+
if (rec.dead) return false;
|
|
13132
|
+
const L = __sbDOL;
|
|
13133
|
+
if (!force && !__sbDOIsIdle(L, rec)) return false;
|
|
13134
|
+
if (rec.registered) {
|
|
13135
|
+
delete L.recs[rec.key];
|
|
13136
|
+
__sbDOUnlink(L, rec);
|
|
13137
|
+
rec.registered = false;
|
|
13138
|
+
if (rec.pins > 0) L.pinned--;
|
|
13139
|
+
}
|
|
13140
|
+
// Revoke: every state method of this instance now throws the evicted error, so a leftover closure cannot write as a second instance.
|
|
13141
|
+
rec.dead = true;
|
|
13142
|
+
__sbDOReleaseSockets(L, rec, force);
|
|
13143
|
+
if (rec.usedSql) {
|
|
13144
|
+
try {
|
|
13145
|
+
__sbRpc("do.sql.close", { cls: rec.cls, file: rec.sqlFile });
|
|
13146
|
+
} catch (error) {
|
|
13147
|
+
__sbLogUncaught("do.sql.close", error);
|
|
13148
|
+
}
|
|
13149
|
+
}
|
|
13150
|
+
const holder = rec.holder;
|
|
13151
|
+
if (holder !== null) {
|
|
13152
|
+
holder.inst = null;
|
|
13153
|
+
holder.rec = null;
|
|
13154
|
+
holder.sockets = [];
|
|
13155
|
+
}
|
|
13156
|
+
rec.inst = null;
|
|
13157
|
+
rec.state = null;
|
|
13158
|
+
rec.plain = [];
|
|
13159
|
+
rec.tcp = [];
|
|
13160
|
+
if (reason === "idle") L.evictedIdle++;
|
|
13161
|
+
else if (reason === "cap") L.evictedCap++;
|
|
13162
|
+
else L.evictedAbort++;
|
|
13163
|
+
return true;
|
|
13164
|
+
}
|
|
13165
|
+
|
|
13166
|
+
// The object's hibernatable sockets go back to the host (op g: unclaimed, their queued events kept), or are closed for a reset.
|
|
13167
|
+
function __sbDOReleaseSockets(L, rec, force) {
|
|
13168
|
+
const holder = rec.holder;
|
|
13169
|
+
if (holder === null) return;
|
|
13170
|
+
const sockets = holder.sockets;
|
|
13171
|
+
for (let i = 0; i < sockets.length; i++) {
|
|
13172
|
+
const server = sockets[i];
|
|
13173
|
+
const client = server._peer;
|
|
13174
|
+
if (client !== null && client._pumpTimer !== undefined) {
|
|
13175
|
+
__sbClearTimeout(client._pumpTimer);
|
|
13176
|
+
client._pumpTimer = undefined;
|
|
13177
|
+
}
|
|
13178
|
+
if (server._state === __SB_WS_CLOSED) continue;
|
|
13179
|
+
if (force) {
|
|
13180
|
+
try {
|
|
13181
|
+
server.close(1011, "Durable Object reset");
|
|
13182
|
+
} catch {
|
|
13183
|
+
/* already closing */
|
|
13184
|
+
}
|
|
13185
|
+
continue;
|
|
13186
|
+
}
|
|
13187
|
+
if (client !== null && client._host > 0) {
|
|
13188
|
+
const hostId = client._host;
|
|
13189
|
+
client._host = 0;
|
|
13190
|
+
server._state = __SB_WS_CLOSED;
|
|
13191
|
+
if (!L.restoreParked) {
|
|
13192
|
+
L.restoreParked = true;
|
|
13193
|
+
__sbWsRestorePark();
|
|
13194
|
+
}
|
|
13195
|
+
__sbWsOp("g", hostId, "", 0);
|
|
13196
|
+
}
|
|
13197
|
+
}
|
|
13198
|
+
}
|
|
13199
|
+
|
|
13200
|
+
// state.abort() and a failed blockConcurrencyWhile(): the object is forgotten (storage kept) and this instance is revoked.
|
|
13201
|
+
function __sbDOReset(rec, reason, error) {
|
|
13202
|
+
if (rec.dead) return;
|
|
13203
|
+
const L = __sbDOL;
|
|
13204
|
+
rec.deadError = error === undefined ? null : error;
|
|
13205
|
+
__sbDOEvict(rec, reason, true);
|
|
13206
|
+
// Events waiting at its gate now fail (each finds the record dead).
|
|
13207
|
+
rec.gate.locks = 0;
|
|
13208
|
+
__sbDOPump(rec.gate);
|
|
13209
|
+
__sbDOServe(L);
|
|
13210
|
+
}
|
|
13211
|
+
|
|
13212
|
+
function __sbDOStalest(L) {
|
|
13213
|
+
for (let rec = L.head; rec !== null; rec = rec.next) if (__sbDOIsIdle(L, rec)) return rec;
|
|
13214
|
+
return null;
|
|
13215
|
+
}
|
|
13216
|
+
|
|
13217
|
+
// The sweep: idle objects older than the timeout leave, from the stale end of the list. Bounded twice: `limit` records are looked at and at
|
|
13218
|
+
// most `most` leave, because an eviction closes a SQLite handle (a checkpoint and its fsyncs, about a millisecond) and the whole sweep is one
|
|
13219
|
+
// synchronous turn: 500 objects going at once would stop every request for half a second or more (measured: p99 1.9 s in an alarm storm).
|
|
13220
|
+
// What is left goes on the next tick; the cap evicts one object at a time when a slot is needed in the meantime.
|
|
13221
|
+
function __sbDOSweep(L, limit, most) {
|
|
13222
|
+
if (L.idle === 0) return;
|
|
13223
|
+
const now = Date.now();
|
|
13224
|
+
let seen = 0;
|
|
13225
|
+
let left = most;
|
|
13226
|
+
for (let rec = L.head; rec !== null && seen < limit && left > 0; ) {
|
|
13227
|
+
const next = rec.next;
|
|
13228
|
+
seen++;
|
|
13229
|
+
if (rec.busy === 0) {
|
|
13230
|
+
if (now - rec.last < L.idle) break;
|
|
13231
|
+
if (rec.pins > 0) {
|
|
13232
|
+
if (!rec.pinLogged && now - rec.pinSince > 60000) {
|
|
13233
|
+
rec.pinLogged = true;
|
|
13234
|
+
console.error("Durable Object " + rec.cls + " has been held in memory for over 60 s by a timer, a connection or a streamed response; it cannot be evicted while that lives");
|
|
13235
|
+
}
|
|
13236
|
+
} else if (__sbDOEvict(rec, "idle", false)) left--;
|
|
13237
|
+
}
|
|
13238
|
+
rec = next;
|
|
13239
|
+
}
|
|
13240
|
+
}
|
|
13241
|
+
|
|
13242
|
+
// The sweep timer is parked at the host (op D): the host fires it when its deadline passes, and it is not background work, so
|
|
13243
|
+
// an app whose objects are all evicted is still reaped on time. A host without the op (an older sproutd) gets no timer: objects
|
|
13244
|
+
// then leave when the next event arrives. A standalone binary has no reaper, so a plain timer does it.
|
|
13245
|
+
function __sbDOArmSweep(L) {
|
|
13246
|
+
if (L.sweepArmed || L.idle === 0 || L.count === 0) return;
|
|
13247
|
+
let delay = L.idle;
|
|
13248
|
+
if (L.head !== null) delay = Math.max(50, Math.min(L.idle, L.head.last + L.idle - Date.now()));
|
|
13249
|
+
const timer = __sbSetTimeout(__sbDOSweepFire, 2147483647);
|
|
13250
|
+
const parked = __sbWsOp("D", Math.trunc(delay), "", timer) === "0";
|
|
13251
|
+
if (parked) {
|
|
13252
|
+
L.sweepArmed = true;
|
|
13253
|
+
return;
|
|
13254
|
+
}
|
|
13255
|
+
__sbClearTimeout(timer);
|
|
13256
|
+
if (!__sbHosted()) {
|
|
13257
|
+
L.sweepArmed = true;
|
|
13258
|
+
__sbSetTimeout(__sbDOSweepFire, delay);
|
|
13259
|
+
}
|
|
13260
|
+
}
|
|
13261
|
+
function __sbDOSweepFire() {
|
|
13262
|
+
const L = __sbDOL;
|
|
13263
|
+
if (L === null) return;
|
|
13264
|
+
L.sweepArmed = false;
|
|
13265
|
+
const before = L.evictedIdle;
|
|
13266
|
+
__sbDOSweep(L, 100000, 24);
|
|
13267
|
+
// Objects left: SQLite hands the pages of the databases still open back too (the quiet collection follows this timer).
|
|
13268
|
+
if (L.evictedIdle !== before) {
|
|
13269
|
+
try {
|
|
13270
|
+
__sbRpc("do.release", {});
|
|
13271
|
+
} catch (error) {
|
|
13272
|
+
__sbLogUncaught("do.release", error);
|
|
13273
|
+
}
|
|
13274
|
+
}
|
|
13275
|
+
__sbDOServe(L);
|
|
13276
|
+
__sbDOArmSweep(L);
|
|
13277
|
+
__sbDONote(L, true);
|
|
13278
|
+
}
|
|
13279
|
+
|
|
13280
|
+
// ---- acquire: the record of an object, constructing it when it has none ----
|
|
13281
|
+
function __sbDOResolveId(cls, idText, idObj) {
|
|
13282
|
+
const stateId = idObj || (idText.indexOf("name:") === 0 ? __sbDONamedId(cls, idText.slice(5)) : __sbDOMakeId(idText, undefined));
|
|
13283
|
+
return stateId;
|
|
13284
|
+
}
|
|
13285
|
+
|
|
13286
|
+
// Returns the record, or a promise of one when the cap is full and nothing is idle.
|
|
13287
|
+
function __sbDOAcquire(cls, idText, idObj) {
|
|
13288
|
+
const L = __sbDOLife();
|
|
13289
|
+
// The common case: the caller's id text is the object's canonical id, so the record is one lookup away.
|
|
13290
|
+
const quick = L.recs[cls + " " + idText];
|
|
13291
|
+
if (quick !== undefined) return quick;
|
|
13292
|
+
if (!__sbDOClasses[cls]) throw new Error("no such Durable Object class: " + cls);
|
|
13293
|
+
const stateId = __sbDOResolveId(cls, idText, idObj);
|
|
13294
|
+
const id = stateId.toString();
|
|
13295
|
+
const key = cls + " " + id;
|
|
13296
|
+
const rec = L.recs[key];
|
|
13297
|
+
if (rec !== undefined) return rec;
|
|
13298
|
+
__sbDOSweep(L, 8, 4);
|
|
13299
|
+
if (L.count >= L.cap) {
|
|
13300
|
+
const victim = __sbDOStalest(L);
|
|
13301
|
+
if (victim === null) return __sbDOWait(L, cls, id, stateId, key);
|
|
13302
|
+
__sbDOEvict(victim, "cap", false);
|
|
13303
|
+
}
|
|
13304
|
+
return __sbDOBuild(L, cls, id, stateId, key);
|
|
13305
|
+
}
|
|
13306
|
+
|
|
13307
|
+
function __sbDOWait(L, cls, id, stateId, key) {
|
|
13308
|
+
L.waits++;
|
|
13309
|
+
return new Promise((resolve, reject) => {
|
|
13310
|
+
const w = { cls, id, stateId, key, resolve, reject, timer: 0, done: false, t0: Date.now() };
|
|
13311
|
+
w.timer = __sbSetTimeout(() => {
|
|
13312
|
+
if (w.done) return;
|
|
13313
|
+
w.done = true;
|
|
13314
|
+
const at = L.waiters.indexOf(w);
|
|
13315
|
+
if (at >= 0) L.waiters.splice(at, 1);
|
|
13316
|
+
L.overloaded++;
|
|
13317
|
+
reject(__sbDOOverloaded("too many objects are awake in this app"));
|
|
13318
|
+
}, L.waitMs);
|
|
13319
|
+
L.waiters.push(w);
|
|
13320
|
+
});
|
|
13321
|
+
}
|
|
13322
|
+
|
|
13323
|
+
// A slot or an idle object freed: serve the waiting first events in order.
|
|
13324
|
+
function __sbDOServe(L) {
|
|
13325
|
+
while (L.waiters.length > 0) {
|
|
13326
|
+
const w = L.waiters[0];
|
|
13327
|
+
let rec = L.recs[w.key];
|
|
13328
|
+
if (rec === undefined) {
|
|
13329
|
+
if (L.count >= L.cap) {
|
|
13330
|
+
const victim = __sbDOStalest(L);
|
|
13331
|
+
if (victim === null) return;
|
|
13332
|
+
__sbDOEvict(victim, "cap", false);
|
|
13333
|
+
}
|
|
13334
|
+
}
|
|
13335
|
+
L.waiters.shift();
|
|
13336
|
+
w.done = true;
|
|
13337
|
+
__sbClearTimeout(w.timer);
|
|
13338
|
+
const waited = Date.now() - w.t0;
|
|
13339
|
+
if (waited > L.waitMsMax) L.waitMsMax = waited;
|
|
13340
|
+
try {
|
|
13341
|
+
if (rec === undefined) rec = __sbDOBuild(L, w.cls, w.id, w.stateId, w.key);
|
|
13342
|
+
w.resolve(rec);
|
|
13343
|
+
} catch (error) {
|
|
13344
|
+
w.reject(error);
|
|
13345
|
+
}
|
|
13346
|
+
}
|
|
13347
|
+
}
|
|
13348
|
+
|
|
13349
|
+
// A clock finer than Date.now() for the wake counter (microseconds); the performance object is built on its first read.
|
|
13350
|
+
function __sbDONow() {
|
|
13351
|
+
const p = globalThis.performance;
|
|
13352
|
+
return p && __sbIsFn(p.now) ? p.now() : Date.now();
|
|
13353
|
+
}
|
|
13354
|
+
|
|
13355
|
+
function __sbDOBuild(L, cls, id, stateId, key) {
|
|
13356
|
+
const Ctor = __sbDOClasses[cls];
|
|
13357
|
+
const t0 = __sbDONow();
|
|
13358
|
+
if (stateId.name !== undefined) __sbDOAdopt(cls, "name:" + stateId.name, id);
|
|
13359
|
+
const tasks = [];
|
|
13360
|
+
const gate = __sbDOMakeGate();
|
|
13361
|
+
const rec = {
|
|
13362
|
+
cls,
|
|
13363
|
+
id,
|
|
13364
|
+
key,
|
|
13365
|
+
inst: null,
|
|
13366
|
+
state: null,
|
|
13367
|
+
gate,
|
|
13368
|
+
holder: null,
|
|
13369
|
+
tasks,
|
|
13370
|
+
busy: 0,
|
|
13371
|
+
pins: 0,
|
|
13372
|
+
pinSince: 0,
|
|
13373
|
+
pinLogged: false,
|
|
13374
|
+
last: Date.now(),
|
|
13375
|
+
usedSql: false,
|
|
13376
|
+
sqlFile: "",
|
|
13377
|
+
dead: false,
|
|
13378
|
+
registered: false,
|
|
13379
|
+
prev: null,
|
|
13380
|
+
next: null,
|
|
13381
|
+
frame: null,
|
|
13382
|
+
plain: [],
|
|
13383
|
+
tcp: [],
|
|
13384
|
+
deadError: null,
|
|
13385
|
+
};
|
|
13386
|
+
rec.frame = __sbFrameWith(undefined, __sbDOCtxKey, rec);
|
|
13387
|
+
const state = {
|
|
13388
|
+
id: stateId,
|
|
13389
|
+
storage: __sbDOAsyncStorage(cls, id, gate, rec),
|
|
13390
|
+
blockConcurrencyWhile(fn) {
|
|
13391
|
+
if (!__sbIsFn(fn))
|
|
13392
|
+
throw new TypeError("Failed to execute 'blockConcurrencyWhile' on 'DurableObjectState': parameter 1 is not of type 'function'.");
|
|
13393
|
+
if (rec.dead) throw __sbDOEvictedError();
|
|
13394
|
+
// No event enters until fn settles; a throw or 30 s without settling resets the object, as in workerd.
|
|
13395
|
+
gate.locks++;
|
|
13396
|
+
let settled = false;
|
|
13397
|
+
const done = () => {
|
|
13398
|
+
settled = true;
|
|
13399
|
+
__sbClearTimeout(timer);
|
|
13400
|
+
if (!rec.dead) {
|
|
13401
|
+
gate.locks--;
|
|
13402
|
+
__sbDOPump(gate);
|
|
13403
|
+
}
|
|
13404
|
+
};
|
|
13405
|
+
const timer = __sbSetTimeout(() => {
|
|
13406
|
+
if (settled) return;
|
|
13407
|
+
settled = true;
|
|
13408
|
+
console.error("blockConcurrencyWhile() did not settle in 30 seconds: the Durable Object " + cls + " was reset");
|
|
13409
|
+
__sbDOReset(rec, "bcw", new Error("Durable Object reset because blockConcurrencyWhile() did not settle in 30 seconds."));
|
|
13410
|
+
}, 30000);
|
|
13411
|
+
let r;
|
|
13412
|
+
try {
|
|
13413
|
+
r = fn();
|
|
13414
|
+
} catch (error) {
|
|
13415
|
+
done();
|
|
13416
|
+
__sbDOReset(rec, "bcw", new Error("Durable Object reset because blockConcurrencyWhile() threw: " + String((error && error.message) || error)));
|
|
13417
|
+
return Promise.reject(error);
|
|
13418
|
+
}
|
|
13419
|
+
return Promise.resolve(r).then(
|
|
13420
|
+
(v) => {
|
|
13421
|
+
done();
|
|
13422
|
+
return v;
|
|
13423
|
+
},
|
|
13424
|
+
(error) => {
|
|
13425
|
+
done();
|
|
13426
|
+
__sbDOReset(rec, "bcw", new Error("Durable Object reset because blockConcurrencyWhile() failed: " + String((error && error.message) || error)));
|
|
13427
|
+
throw error;
|
|
13428
|
+
},
|
|
13429
|
+
);
|
|
13430
|
+
},
|
|
13431
|
+
waitUntil(p) {
|
|
13432
|
+
if (rec.dead) throw __sbDOEvictedError();
|
|
13433
|
+
if (p && __sbIsFn(p.then)) tasks.push(p);
|
|
13434
|
+
},
|
|
13435
|
+
};
|
|
13436
|
+
state.props = {};
|
|
13437
|
+
state.abort = (reason) => {
|
|
13438
|
+
__sbDOReset(rec, "abort");
|
|
13439
|
+
throw new Error(reason == null ? "Durable Object was aborted." : String(reason));
|
|
13440
|
+
};
|
|
13441
|
+
const holder = { inst: null, rec, auto: null, cls, id, restored: [], sockets: [] };
|
|
13442
|
+
rec.holder = holder;
|
|
13443
|
+
rec.state = state;
|
|
13444
|
+
if (__sbWsEnabled()) __sbDOWebSocketState(state, holder);
|
|
13445
|
+
let inst;
|
|
13446
|
+
try {
|
|
13447
|
+
inst = new Ctor(state, __sbEnvValue());
|
|
13448
|
+
} catch (error) {
|
|
13449
|
+
rec.dead = true;
|
|
13450
|
+
throw error;
|
|
13451
|
+
}
|
|
13452
|
+
// blockConcurrencyWhile() reset the object while its constructor was still running (a synchronous throw).
|
|
13453
|
+
if (rec.dead) throw new Error("The Durable Object " + cls + " was reset while it was starting.");
|
|
13454
|
+
holder.inst = inst;
|
|
13455
|
+
inst.__sbTasks = tasks;
|
|
13456
|
+
inst.__sbGate = gate;
|
|
13457
|
+
rec.inst = inst;
|
|
13458
|
+
inst.__sbRec = rec;
|
|
13459
|
+
L.recs[key] = rec;
|
|
13460
|
+
rec.registered = true;
|
|
13461
|
+
__sbDOLink(L, rec);
|
|
13462
|
+
const wake = Math.round((__sbDONow() - t0) * 1000);
|
|
13463
|
+
L.wakeUsLast = wake;
|
|
13464
|
+
if (wake > L.wakeUsMax) L.wakeUsMax = wake;
|
|
13465
|
+
// Sockets restored from hibernation start delivering now that the object exists.
|
|
13466
|
+
const restored = holder.restored;
|
|
13467
|
+
holder.restored = [];
|
|
13468
|
+
for (let i = 0; i < restored.length; i++) __sbWsPump(restored[i]);
|
|
13469
|
+
__sbDOArmSweep(L);
|
|
13470
|
+
return rec;
|
|
13471
|
+
}
|
|
13472
|
+
|
|
13473
|
+
// ---- run an event: the one entry for RPC, stub.fetch, alarms and socket events ----
|
|
13474
|
+
// `run(inst)` is the event; it enters the object's gate and runs in a frame that names the object. The returned promise settles with
|
|
13475
|
+
// the event's value. The object counts as busy until the event and the drain of its waitUntil tasks have settled (`wait`: the
|
|
13476
|
+
// returned promise waits for that drain too).
|
|
13477
|
+
function __sbDORun(cls, idText, idObj, run, wait) {
|
|
13478
|
+
let acquired;
|
|
13479
|
+
try {
|
|
13480
|
+
acquired = __sbDOAcquire(cls, idText, idObj);
|
|
13481
|
+
} catch (error) {
|
|
13482
|
+
return Promise.reject(error);
|
|
13483
|
+
}
|
|
13484
|
+
if (__sbIsFn(acquired.then)) return acquired.then((rec) => __sbDOStart(rec, run, wait));
|
|
13485
|
+
return __sbDOStart(acquired, run, wait);
|
|
13486
|
+
}
|
|
13487
|
+
|
|
13488
|
+
function __sbDOStart(rec, run, wait) {
|
|
13489
|
+
const L = __sbDOL;
|
|
13490
|
+
if (rec.dead) return Promise.reject(rec.deadError !== null ? rec.deadError : __sbDOEvictedError());
|
|
13491
|
+
const gate = rec.gate;
|
|
13492
|
+
if (gate.waiting.length >= L.queueMax) {
|
|
13493
|
+
L.overloaded++;
|
|
13494
|
+
return Promise.reject(__sbDOOverloaded("too many events are queued for this object"));
|
|
13495
|
+
}
|
|
13496
|
+
rec.busy++;
|
|
13497
|
+
// The input gate, as __sbDOEnter has it: an open gate runs the event now, a shut one queues it behind the others.
|
|
13498
|
+
if (gate.locks === 0 && gate.waiting.length === 0) return __sbDOEvent(L, rec, run, wait);
|
|
13499
|
+
return new Promise((resolve) => {
|
|
13500
|
+
gate.waiting.push(() => {
|
|
13501
|
+
resolve(__sbDOEvent(L, rec, run, wait));
|
|
13502
|
+
});
|
|
13503
|
+
});
|
|
13504
|
+
}
|
|
13505
|
+
|
|
13506
|
+
// One event, in the object's frame; busy until it and the drain of its waitUntil tasks have settled. One coroutine and nothing else
|
|
13507
|
+
// per event: this path is paid by every call into every object.
|
|
13508
|
+
async function __sbDOEvent(L, rec, run, wait) {
|
|
13509
|
+
try {
|
|
13510
|
+
if (rec.dead) throw rec.deadError !== null ? rec.deadError : __sbDOEvictedError();
|
|
13511
|
+
return await __sbRunInFrame(rec.frame, run, [rec.inst]);
|
|
13512
|
+
} finally {
|
|
13513
|
+
const drained = __sbDOSettle(L, rec, wait);
|
|
13514
|
+
if (drained !== undefined) await drained;
|
|
13515
|
+
}
|
|
13516
|
+
}
|
|
13517
|
+
|
|
13518
|
+
// The event is over: drain its waitUntil tasks (it stays busy until they settle), then it may become idle.
|
|
13519
|
+
function __sbDOSettle(L, rec, wait) {
|
|
13520
|
+
if (rec.tasks.length > 0) {
|
|
13521
|
+
const finish = () => __sbDOIdle(L, rec);
|
|
13522
|
+
const drained = __sbDrainWaitUntil(rec.tasks);
|
|
13523
|
+
drained.then(finish, finish);
|
|
13524
|
+
return wait ? drained.then(undefined, () => {}) : undefined;
|
|
13525
|
+
}
|
|
13526
|
+
__sbDOIdle(L, rec);
|
|
13527
|
+
return undefined;
|
|
13528
|
+
}
|
|
13529
|
+
function __sbDOIdle(L, rec) {
|
|
13530
|
+
rec.busy--;
|
|
13531
|
+
if (rec.busy !== 0 || rec.dead) return;
|
|
13532
|
+
const now = Date.now();
|
|
13533
|
+
if (rec.pins === 0) __sbDOFresh(L, rec, now);
|
|
13534
|
+
if (L.waiters.length > 0) __sbDOServe(L);
|
|
13535
|
+
if (!L.sweepArmed) __sbDOArmSweep(L);
|
|
13536
|
+
if (now - L.noteAt >= 1000) __sbDONote(L, true);
|
|
13537
|
+
}
|
|
13538
|
+
|
|
13539
|
+
// ---- pins: a timer, an unawaited fetch and a streamed response keep their object in memory ----
|
|
13540
|
+
// The timer globals become wrappers once the first object is built. The prelude's own timers use __sbSetTimeout and the rest,
|
|
13541
|
+
// which are the runtime's, so a parked timer of ours never pins anything.
|
|
13542
|
+
function __sbDOWrapTimers() {
|
|
13543
|
+
const armed = new Map();
|
|
13544
|
+
const arm = (original, repeat) =>
|
|
13545
|
+
function (callback, delay, ...args) {
|
|
13546
|
+
const rec = __sbDOFrameRec();
|
|
13547
|
+
if (rec === undefined || rec.dead || !__sbIsFn(callback)) return original(callback, delay, ...args);
|
|
13548
|
+
const slot = { id: 0, rec };
|
|
13549
|
+
__sbDOPin(rec);
|
|
13550
|
+
const fired = repeat
|
|
13551
|
+
? callback
|
|
13552
|
+
: function (...fireArgs) {
|
|
13553
|
+
armed.delete(slot.id);
|
|
13554
|
+
__sbDOUnpin(slot.rec);
|
|
13555
|
+
return callback.apply(undefined, fireArgs);
|
|
13556
|
+
};
|
|
13557
|
+
slot.id = Number(original(fired, delay, ...args));
|
|
13558
|
+
armed.set(slot.id, slot);
|
|
13559
|
+
return slot.id;
|
|
13560
|
+
};
|
|
13561
|
+
const clear = (original) =>
|
|
13562
|
+
function (id) {
|
|
13563
|
+
const key = Number(id);
|
|
13564
|
+
const slot = armed.get(key);
|
|
13565
|
+
if (slot !== undefined) {
|
|
13566
|
+
armed.delete(key);
|
|
13567
|
+
__sbDOUnpin(slot.rec);
|
|
13568
|
+
}
|
|
13569
|
+
return original(id);
|
|
13570
|
+
};
|
|
13571
|
+
globalThis.setTimeout = arm(__sbSetTimeout, false);
|
|
13572
|
+
globalThis.setInterval = arm(__sbSetInterval, true);
|
|
13573
|
+
globalThis.clearTimeout = clear(__sbClearTimeout);
|
|
13574
|
+
globalThis.clearInterval = clear(__sbClearInterval);
|
|
13575
|
+
}
|
|
13576
|
+
|
|
13577
|
+
// ---- counters: cheap, one host note a second at most, only when changed ----
|
|
13578
|
+
function __sbDOStats(L) {
|
|
13579
|
+
let sqlOpen = 0;
|
|
13580
|
+
try {
|
|
13581
|
+
sqlOpen = Number(__sbRpc("do.stats", {}).stats.doOpen) || 0;
|
|
13582
|
+
} catch {
|
|
13583
|
+
sqlOpen = 0;
|
|
13584
|
+
}
|
|
13585
|
+
return (
|
|
13586
|
+
"objects_awake=" + L.count +
|
|
13587
|
+
" objects_pinned=" + L.pinned +
|
|
13588
|
+
" do_sql_open=" + sqlOpen +
|
|
13589
|
+
" evicted_idle=" + L.evictedIdle +
|
|
13590
|
+
" evicted_cap=" + L.evictedCap +
|
|
13591
|
+
" evicted_abort=" + L.evictedAbort +
|
|
13592
|
+
" waits=" + L.waits +
|
|
13593
|
+
" wait_ms_max=" + L.waitMsMax +
|
|
13594
|
+
" overloaded=" + L.overloaded +
|
|
13595
|
+
" zombie_calls=" + L.zombie +
|
|
13596
|
+
" wake_us=" + L.wakeUsLast + "," + L.wakeUsMax
|
|
13597
|
+
);
|
|
13598
|
+
}
|
|
13599
|
+
function __sbDONote(L, force) {
|
|
13600
|
+
const now = Date.now();
|
|
13601
|
+
if (!force && now - L.noteAt < 1000) return;
|
|
13602
|
+
L.noteAt = now;
|
|
13603
|
+
const text = __sbDOStats(L);
|
|
13604
|
+
if (text === L.noteText) return;
|
|
13605
|
+
L.noteText = text;
|
|
13606
|
+
if (__sbHosted()) __sbHostNote("do " + text);
|
|
13607
|
+
if (__sbEnv("SB_DO_STATS") === "1") console.error("do " + text);
|
|
13608
|
+
}
|
|
13609
|
+
|
|
13610
|
+
// A revoked state method: the evicted (or reset) object's instance cannot touch storage any more.
|
|
13611
|
+
function __sbDOZombie(rec) {
|
|
13612
|
+
__sbDOL.zombie++;
|
|
13613
|
+
return rec.deadError !== null ? rec.deadError : __sbDOEvictedError();
|
|
13614
|
+
}
|
|
13615
|
+
function __sbDOFetchEvent(inst, req) {
|
|
13616
|
+
return __sbFetchWithDrain(inst.fetch(req), [], false, true);
|
|
13617
|
+
}
|
|
13618
|
+
function __sbWsRestorePark() {
|
|
13619
|
+
const timer = __sbSetTimeout(__sbWsRestoreLoop, 2147483647);
|
|
13620
|
+
__sbWsOp("R", 0, "", timer);
|
|
13621
|
+
}
|
|
13622
|
+
function __sbWsRestoreFailed(id, error) {
|
|
13623
|
+
__sbLogUncaught("websocket", error);
|
|
13624
|
+
if (!(error && error.overloaded === true)) return;
|
|
13625
|
+
const L = __sbDOL;
|
|
13626
|
+
const tries = (L.restoreTries[id] || 0) + 1;
|
|
13627
|
+
L.restoreTries[id] = tries;
|
|
13628
|
+
if (tries >= 3) {
|
|
13629
|
+
delete L.restoreTries[id];
|
|
13630
|
+
// Three waits for a slot and none came: try again later, said to the client.
|
|
13631
|
+
__sbWsOp("c", id, "", 1013);
|
|
13632
|
+
__sbWsOp("r", id, "", 0);
|
|
13633
|
+
return;
|
|
13634
|
+
}
|
|
13635
|
+
// Back to the host's restore list with its queued events: the next slot serves it.
|
|
13636
|
+
__sbWsOp("g", id, "", 0);
|
|
13637
|
+
}
|
|
13638
|
+
|
|
12772
13639
|
function __sbDOStorage(cls, id) {
|
|
12773
13640
|
return {
|
|
12774
13641
|
get(key) {
|
|
@@ -12866,7 +13733,7 @@ function __sbDOPump(gate) {
|
|
|
12866
13733
|
// Shut the gate until the next timer turn (see above).
|
|
12867
13734
|
function __sbDOHold(gate) {
|
|
12868
13735
|
gate.locks++;
|
|
12869
|
-
|
|
13736
|
+
__sbSetTimeout(function () {
|
|
12870
13737
|
gate.locks--;
|
|
12871
13738
|
__sbDOPump(gate);
|
|
12872
13739
|
}, 0);
|
|
@@ -12902,6 +13769,12 @@ function __sbDOEnter(gate, run) {
|
|
|
12902
13769
|
// (kept on the instance, as __sbAlarmAt and __sbAlarmFails)
|
|
12903
13770
|
function __sbDOAlarmCall(inst, cls, id, at, attempts) {
|
|
12904
13771
|
if (__sbCompatDate() < "2026-11-04") return inst.alarm();
|
|
13772
|
+
// From the eviction date the count is the delivery row's (or the host's), never the instance's: the instance can be gone
|
|
13773
|
+
// between two runs of the same alarm.
|
|
13774
|
+
if (__sbDOEvictOn()) {
|
|
13775
|
+
const retries = attempts != null && Number(attempts) > 0 ? Number(attempts) : 0;
|
|
13776
|
+
return __sbDOAlarmRun(inst, { retryCount: retries, isRetry: retries > 0 });
|
|
13777
|
+
}
|
|
12905
13778
|
const when = Number(at) || 0;
|
|
12906
13779
|
if (inst.__sbAlarmAt !== when) {
|
|
12907
13780
|
inst.__sbAlarmAt = when;
|
|
@@ -13212,7 +14085,8 @@ function __sbDOStrictStorage(cls, id) {
|
|
|
13212
14085
|
setAlarm(when) {
|
|
13213
14086
|
const at = when instanceof Date ? when.getTime() : when;
|
|
13214
14087
|
if (__sbDOv3()) {
|
|
13215
|
-
const
|
|
14088
|
+
const live = __sbDOL !== null ? __sbDOL.recs[cls + " " + id] : undefined;
|
|
14089
|
+
const inst = live !== undefined ? live.inst : __sbDOInstances[cls + " " + id];
|
|
13216
14090
|
if (inst && !__sbIsFn(inst.alarm))
|
|
13217
14091
|
throw new TypeError("Your Durable Object class must have an alarm() handler in order to call setAlarm()");
|
|
13218
14092
|
if (when instanceof Date || Object(when) instanceof Number) {
|
|
@@ -13254,9 +14128,11 @@ function __sbDOStrictStorage(cls, id) {
|
|
|
13254
14128
|
}
|
|
13255
14129
|
|
|
13256
14130
|
// The 2026-10-28 storage: every method returns a Promise; reads shut the input gate.
|
|
13257
|
-
function __sbDOAsyncStorage(cls, id, gate) {
|
|
14131
|
+
function __sbDOAsyncStorage(cls, id, gate, rec) {
|
|
13258
14132
|
const s = __sbDOStrictStorage(cls, id);
|
|
14133
|
+
// rec is the record of an object that may be evicted (2026-11-21): once it is dead every storage call fails with the evicted error.
|
|
13259
14134
|
const settle = (f) => {
|
|
14135
|
+
if (rec !== undefined && rec.dead) return Promise.reject(__sbDOZombie(rec));
|
|
13260
14136
|
try {
|
|
13261
14137
|
return Promise.resolve(f());
|
|
13262
14138
|
} catch (error) {
|
|
@@ -13264,9 +14140,13 @@ function __sbDOAsyncStorage(cls, id, gate) {
|
|
|
13264
14140
|
}
|
|
13265
14141
|
};
|
|
13266
14142
|
const read = (f) => {
|
|
14143
|
+
if (rec !== undefined && rec.dead) return Promise.reject(__sbDOZombie(rec));
|
|
13267
14144
|
__sbDOHold(gate);
|
|
13268
14145
|
return settle(f);
|
|
13269
14146
|
};
|
|
14147
|
+
const alive = () => {
|
|
14148
|
+
if (rec !== undefined && rec.dead) throw __sbDOZombie(rec);
|
|
14149
|
+
};
|
|
13270
14150
|
const storage = {
|
|
13271
14151
|
get: (key) => read(() => s.get(key)),
|
|
13272
14152
|
list: (options) => read(() => s.list(options)),
|
|
@@ -13277,10 +14157,13 @@ function __sbDOAsyncStorage(cls, id, gate) {
|
|
|
13277
14157
|
setAlarm: (when) => settle(() => s.setAlarm(when)),
|
|
13278
14158
|
deleteAlarm: () => settle(() => s.deleteAlarm()),
|
|
13279
14159
|
sync: () => Promise.resolve(),
|
|
13280
|
-
transaction: (fn) =>
|
|
14160
|
+
transaction: (fn) => {
|
|
14161
|
+
if (rec !== undefined && rec.dead) return Promise.reject(__sbDOZombie(rec));
|
|
14162
|
+
return __sbDOTransaction(cls, id, s, gate, fn);
|
|
14163
|
+
},
|
|
13281
14164
|
};
|
|
13282
14165
|
if (__sbDOv2()) {
|
|
13283
|
-
const sql = __sbDOSql(cls, id);
|
|
14166
|
+
const sql = __sbDOSql(cls, id, rec);
|
|
13284
14167
|
storage.sql = sql;
|
|
13285
14168
|
storage.transactionSync = (fn) => sql.__sbTransaction(fn);
|
|
13286
14169
|
if (__sbDOv3()) {
|
|
@@ -13292,12 +14175,22 @@ function __sbDOAsyncStorage(cls, id, gate) {
|
|
|
13292
14175
|
});
|
|
13293
14176
|
// The synchronous key-value API, over the same keys as get() and put().
|
|
13294
14177
|
storage.kv = {
|
|
13295
|
-
get: (key) =>
|
|
14178
|
+
get: (key) => {
|
|
14179
|
+
alive();
|
|
14180
|
+
return s.get(key);
|
|
14181
|
+
},
|
|
13296
14182
|
put: (key, value) => {
|
|
14183
|
+
alive();
|
|
13297
14184
|
s.put(key, value);
|
|
13298
14185
|
},
|
|
13299
|
-
delete: (key) =>
|
|
13300
|
-
|
|
14186
|
+
delete: (key) => {
|
|
14187
|
+
alive();
|
|
14188
|
+
return s.delete(key);
|
|
14189
|
+
},
|
|
14190
|
+
list: (options) => {
|
|
14191
|
+
alive();
|
|
14192
|
+
return s.list(options).entries();
|
|
14193
|
+
},
|
|
13301
14194
|
};
|
|
13302
14195
|
// Point-in-time recovery needs the storage engine's log, which this one does not keep.
|
|
13303
14196
|
const pitr = () => Promise.reject(new Error("Point-in-time recovery is not supported by this Durable Object storage."));
|
|
@@ -13472,9 +14365,14 @@ function __sbDOSqlPolicy(text) {
|
|
|
13472
14365
|
}
|
|
13473
14366
|
}
|
|
13474
14367
|
|
|
13475
|
-
function __sbDOSql(cls, id) {
|
|
14368
|
+
function __sbDOSql(cls, id, rec) {
|
|
13476
14369
|
const file = __sbDOIsHex64(id) ? id.toLowerCase() : __sbDOSha("sbdo-file:" + id);
|
|
14370
|
+
if (rec !== undefined) rec.sqlFile = file;
|
|
13477
14371
|
const run = (sql, params) => {
|
|
14372
|
+
if (rec !== undefined) {
|
|
14373
|
+
if (rec.dead) throw __sbDOZombie(rec);
|
|
14374
|
+
rec.usedSql = true;
|
|
14375
|
+
}
|
|
13478
14376
|
try {
|
|
13479
14377
|
return __sbRpc("do.sql", { cls, file, sql, params });
|
|
13480
14378
|
} catch (err) {
|
|
@@ -14027,10 +14925,10 @@ async function __sbDrainWaitUntil(tasks, logRec) {
|
|
|
14027
14925
|
// busy for the full 25 s (sproutd will not reap a worker with timers left, B22).
|
|
14028
14926
|
let timer;
|
|
14029
14927
|
const cap = new Promise((resolve) => {
|
|
14030
|
-
timer =
|
|
14928
|
+
timer = __sbSetTimeout(resolve, __SB_WAITUNTIL_TIMEOUT_MS);
|
|
14031
14929
|
});
|
|
14032
14930
|
await Promise.race([__sbDrainAll(pending, logRec), cap]);
|
|
14033
|
-
|
|
14931
|
+
__sbClearTimeout(timer);
|
|
14034
14932
|
}
|
|
14035
14933
|
|
|
14036
14934
|
async function __sbDrainAll(pending, logRec) {
|
|
@@ -14090,7 +14988,8 @@ function __sbHasStreamBody(res) {
|
|
|
14090
14988
|
// x-sb-stream header, the native writer sends the status and headers and holds the
|
|
14091
14989
|
// connection open, and each chunk is written as it arrives. Hosts that do not stream
|
|
14092
14990
|
// (sproutd's workers, for now) link the weak fallbacks, report unsupported and keep buffering.
|
|
14093
|
-
// op: "s" supported, "o" open, "w" write data, "
|
|
14991
|
+
// op: "s" supported, "o" open, "w" write data (a bytestring), "W" write the Uint8Array in data, "p" the bytes written
|
|
14992
|
+
// the client's connection has not taken yet, "k" park the timer in data until that falls, "e" end. Returns a short string.
|
|
14094
14993
|
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
|
|
14095
14994
|
function __sbStreamOp(op, id, data) {
|
|
14096
14995
|
let out = "";
|
|
@@ -14100,6 +14999,11 @@ function __sbStreamOp(op, id, data) {
|
|
|
14100
14999
|
extern unsigned int sb_stream_open(void);
|
|
14101
15000
|
extern int sb_stream_write(unsigned int, const char*, size_t);
|
|
14102
15001
|
extern void sb_stream_end(unsigned int);
|
|
15002
|
+
// Flow control (sproutd): the bytes written and not yet taken by the client's connection, and a timer the
|
|
15003
|
+
// host fires when that falls. A host without them (a standalone executable, whose server buffers by itself)
|
|
15004
|
+
// reports none, so a stream is never held back there.
|
|
15005
|
+
extern size_t sb_stream_pending(unsigned int) __attribute__((weak));
|
|
15006
|
+
extern void sb_stream_wait(unsigned int, unsigned int) __attribute__((weak));
|
|
14103
15007
|
const char* __o; size_t __ol; char* __oo = 0;
|
|
14104
15008
|
porf_native_fetch_read_value(op, &__o, &__ol, &__oo);
|
|
14105
15009
|
char __op = __ol ? __o[0] : 0;
|
|
@@ -14119,7 +15023,17 @@ function __sbStreamOp(op, id, data) {
|
|
|
14119
15023
|
int __w = -1;
|
|
14120
15024
|
if (porf_native_fetch_read_raw_bytes(data, &__d, &__dl) == 0) __w = sb_stream_write(__id, __d, __dl);
|
|
14121
15025
|
snprintf(__r, sizeof(__r), "%d", __w);
|
|
14122
|
-
} else if (__op == '
|
|
15026
|
+
} else if (__op == 'W' && data.type == 81) {
|
|
15027
|
+
// A Uint8Array written as it is, with no bytestring copy of it.
|
|
15028
|
+
const u32 __ta = (u32)data.val;
|
|
15029
|
+
const int32_t __len = *(int32_t*)(MEM + __ta);
|
|
15030
|
+
const u32 __at = *(u32*)(MEM + __ta + 4) + 4;
|
|
15031
|
+
int __w = -1;
|
|
15032
|
+
if (__len >= 0) __w = sb_stream_write(__id, (const char*)(MEM + __at), (size_t)__len);
|
|
15033
|
+
snprintf(__r, sizeof(__r), "%d", __w);
|
|
15034
|
+
} else if (__op == 'p') snprintf(__r, sizeof(__r), "%zu", sb_stream_pending ? sb_stream_pending(__id) : (size_t)0);
|
|
15035
|
+
else if (__op == 'k') { if (sb_stream_wait && data.type == 1 && data.val > 0) sb_stream_wait(__id, (unsigned int)data.val); }
|
|
15036
|
+
else if (__op == 'e') { sb_stream_end(__id); snprintf(__r, sizeof(__r), "0"); }
|
|
14123
15037
|
}
|
|
14124
15038
|
out = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, strlen(__r)), 195);
|
|
14125
15039
|
`;
|
|
@@ -14130,6 +15044,16 @@ function __sbStreamingSupported() {
|
|
|
14130
15044
|
if (__sbStreaming < 0) __sbStreaming = __sbStreamOp("s", "0", "") === "1" ? 1 : 0;
|
|
14131
15045
|
return __sbStreaming === 1;
|
|
14132
15046
|
}
|
|
15047
|
+
// What the client's connection may hold of a stream before the pump waits for it (the host credits bytes as
|
|
15048
|
+
// they leave): a client slower than the app is waited for, not buffered for or cut off.
|
|
15049
|
+
const __SB_STREAM_WINDOW = 1024 * 1024;
|
|
15050
|
+
// Resolves when the host says the stream's unsent bytes fell (or the client went away).
|
|
15051
|
+
function __sbStreamDrained(id) {
|
|
15052
|
+
return new Promise((resolve) => {
|
|
15053
|
+
const timer = __sbSetTimeout(resolve, 2147483647);
|
|
15054
|
+
__sbStreamOp("k", id, timer);
|
|
15055
|
+
});
|
|
15056
|
+
}
|
|
14133
15057
|
function __sbStreamResponse(response) {
|
|
14134
15058
|
const stream = __sbRawBody(response);
|
|
14135
15059
|
const id = __sbStreamOp("o", "0", "");
|
|
@@ -14137,6 +15061,14 @@ function __sbStreamResponse(response) {
|
|
|
14137
15061
|
response.headers._immutable = false;
|
|
14138
15062
|
response.headers.set("x-sb-stream", id);
|
|
14139
15063
|
response.body = "";
|
|
15064
|
+
// A streamed response of a Durable Object keeps its object busy until the stream ends (this runs in the object's frame).
|
|
15065
|
+
if (__sbDOL !== null) {
|
|
15066
|
+
const owner = __sbDOFrameRec();
|
|
15067
|
+
if (owner !== undefined && !owner.dead) {
|
|
15068
|
+
__sbDOPin(owner);
|
|
15069
|
+
response._sbDOPin = owner;
|
|
15070
|
+
}
|
|
15071
|
+
}
|
|
14140
15072
|
(async () => {
|
|
14141
15073
|
const reader = stream.getReader();
|
|
14142
15074
|
try {
|
|
@@ -14148,10 +15080,22 @@ function __sbStreamResponse(response) {
|
|
|
14148
15080
|
else if (value instanceof ArrayBuffer) bytes = new Uint8Array(value);
|
|
14149
15081
|
else if (ArrayBuffer.isView(value)) bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
14150
15082
|
else throw new TypeError("This ReadableStream did not return bytes.");
|
|
14151
|
-
if (bytes.length > 0
|
|
14152
|
-
//
|
|
14153
|
-
|
|
14154
|
-
|
|
15083
|
+
if (bytes.length > 0) {
|
|
15084
|
+
// A whole Uint8Array goes to the host without a copy; a view of part of a buffer is copied.
|
|
15085
|
+
const wrote =
|
|
15086
|
+
bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength
|
|
15087
|
+
? __sbStreamOp("W", id, bytes)
|
|
15088
|
+
: __sbStreamOp("w", id, __sbToBytes(bytes));
|
|
15089
|
+
if (wrote !== "0") {
|
|
15090
|
+
// The client went away: stop the source.
|
|
15091
|
+
reader.cancel();
|
|
15092
|
+
break;
|
|
15093
|
+
}
|
|
15094
|
+
// Flow control: while the connection holds more than the window, read no more of the source.
|
|
15095
|
+
while (Number(__sbStreamOp("p", id, "")) > __SB_STREAM_WINDOW) {
|
|
15096
|
+
await __sbStreamDrained(id);
|
|
15097
|
+
// Woken by the client going away: the next write says so (-1).
|
|
15098
|
+
}
|
|
14155
15099
|
}
|
|
14156
15100
|
}
|
|
14157
15101
|
} catch (error) {
|
|
@@ -14159,6 +15103,7 @@ function __sbStreamResponse(response) {
|
|
|
14159
15103
|
__sbLogUncaught("fetch", error);
|
|
14160
15104
|
}
|
|
14161
15105
|
__sbStreamOp("e", id, "");
|
|
15106
|
+
if (response._sbDOPin !== undefined) __sbDOUnpin(response._sbDOPin);
|
|
14162
15107
|
})();
|
|
14163
15108
|
return response;
|
|
14164
15109
|
}
|
|
@@ -14301,6 +15246,10 @@ class __SbWebSocket {
|
|
|
14301
15246
|
if (this._hib !== null) throw new TypeError("Can't accept() a WebSocket that was accepted with acceptWebSocket().");
|
|
14302
15247
|
if (this._accepted) return;
|
|
14303
15248
|
this._accepted = true;
|
|
15249
|
+
if (__sbDOL !== null) {
|
|
15250
|
+
const rec = __sbDOFrameRec();
|
|
15251
|
+
if (rec !== undefined && !rec.dead) rec.plain.push(this);
|
|
15252
|
+
}
|
|
14304
15253
|
__sbWsSchedule(this);
|
|
14305
15254
|
}
|
|
14306
15255
|
send(message) {
|
|
@@ -14472,6 +15421,16 @@ function __sbWsDispatch(ws, event) {
|
|
|
14472
15421
|
|
|
14473
15422
|
// A hibernatable socket's event runs the object's handler through its input gate.
|
|
14474
15423
|
function __sbWsHibernated(ws, name, args) {
|
|
15424
|
+
const rec = ws._hib.holder.rec;
|
|
15425
|
+
if (rec !== undefined && rec !== null) {
|
|
15426
|
+
if (rec.dead || rec.inst === null || !__sbIsFn(rec.inst[name])) return;
|
|
15427
|
+
__sbDOStart(
|
|
15428
|
+
rec,
|
|
15429
|
+
(inst) => (name === "webSocketClose" ? inst.webSocketClose(args[0], args[1], args[2], args[3]) : inst.webSocketMessage(args[0], args[1])),
|
|
15430
|
+
false,
|
|
15431
|
+
).then(undefined, (error) => __sbLogUncaught("websocket", error));
|
|
15432
|
+
return;
|
|
15433
|
+
}
|
|
14475
15434
|
const inst = ws._hib.holder.inst;
|
|
14476
15435
|
if (inst == null || !__sbIsFn(inst[name])) return;
|
|
14477
15436
|
const run = () => {
|
|
@@ -14492,12 +15451,14 @@ function __sbWsHibernated(ws, name, args) {
|
|
|
14492
15451
|
// state from 2026-10-30. `holder.inst` is the object once it exists.
|
|
14493
15452
|
function __sbDOWebSocketState(state, holder) {
|
|
14494
15453
|
const sockets = [];
|
|
15454
|
+
holder.sockets = sockets;
|
|
14495
15455
|
const live = () => {
|
|
14496
15456
|
for (let i = sockets.length - 1; i >= 0; i--) if (sockets[i]._state === __SB_WS_CLOSED) sockets.splice(i, 1);
|
|
14497
15457
|
return sockets;
|
|
14498
15458
|
};
|
|
14499
15459
|
__sbWsHibRestore(holder, sockets);
|
|
14500
15460
|
state.acceptWebSocket = function (ws, tags) {
|
|
15461
|
+
if (holder.rec !== undefined && holder.rec !== null && holder.rec.dead) throw __sbDOZombie(holder.rec);
|
|
14501
15462
|
if (!(ws instanceof __SbWebSocket)) throw new TypeError("acceptWebSocket needs a WebSocket");
|
|
14502
15463
|
if (ws._accepted || ws._hib !== null || ws._host !== 0) throw new TypeError("This WebSocket was already accepted.");
|
|
14503
15464
|
const list = [];
|
|
@@ -14572,7 +15533,8 @@ function __sbWsPump(ws) {
|
|
|
14572
15533
|
for (;;) {
|
|
14573
15534
|
const head = __sbWsOp("p", id, "", 0);
|
|
14574
15535
|
if (head === "") {
|
|
14575
|
-
const timer =
|
|
15536
|
+
const timer = __sbSetTimeout(() => __sbWsPump(ws), 2147483647);
|
|
15537
|
+
ws._pumpTimer = timer;
|
|
14576
15538
|
// From 2026-11-01 a parked socket is not background work (z): its app may be reaped.
|
|
14577
15539
|
__sbWsOp(__sbWsHibOn() ? "z" : "w", id, "", timer);
|
|
14578
15540
|
return;
|
|
@@ -14661,9 +15623,9 @@ function __sbTcpPump(st) {
|
|
|
14661
15623
|
if (head === "") {
|
|
14662
15624
|
if (st.id === 0) return;
|
|
14663
15625
|
if (__sbWsCanWake()) {
|
|
14664
|
-
st.timer =
|
|
15626
|
+
st.timer = __sbSetTimeout(() => __sbTcpPump(st), 2147483647);
|
|
14665
15627
|
__sbWsOp("w", st.id, "", st.timer);
|
|
14666
|
-
} else st.timer =
|
|
15628
|
+
} else st.timer = __sbSetTimeout(() => __sbTcpPump(st), 10);
|
|
14667
15629
|
return;
|
|
14668
15630
|
}
|
|
14669
15631
|
const parts = head.split(" ");
|
|
@@ -14728,7 +15690,7 @@ function __sbTcpRelease(st) {
|
|
|
14728
15690
|
const id = st.id;
|
|
14729
15691
|
if (id === 0) return;
|
|
14730
15692
|
st.id = 0;
|
|
14731
|
-
if (st.timer !== undefined)
|
|
15693
|
+
if (st.timer !== undefined) __sbClearTimeout(st.timer);
|
|
14732
15694
|
__sbWsOp("r", id, "", 0);
|
|
14733
15695
|
__sbTcpOpen--;
|
|
14734
15696
|
}
|
|
@@ -14876,7 +15838,7 @@ function __sbTcpFacade(st, mode, upgrade) {
|
|
|
14876
15838
|
// the old socket ends once the upgrade is under way (workerd resolves its `closed`)
|
|
14877
15839
|
const oldResolve = st.closedResolve;
|
|
14878
15840
|
const next = __sbTcpFacade(st, "on", upgrade);
|
|
14879
|
-
|
|
15841
|
+
__sbSetTimeout(() => oldResolve(undefined), 0);
|
|
14880
15842
|
return next;
|
|
14881
15843
|
},
|
|
14882
15844
|
};
|
|
@@ -14908,7 +15870,7 @@ function __sbConnect(address, options) {
|
|
|
14908
15870
|
if (target.port === 25) {
|
|
14909
15871
|
st.failed = new Error("proxy request failed, cannot connect to the specified address");
|
|
14910
15872
|
st.eof = true;
|
|
14911
|
-
|
|
15873
|
+
__sbSetTimeout(() => __sbTcpSettle(st), 0);
|
|
14912
15874
|
return socket;
|
|
14913
15875
|
}
|
|
14914
15876
|
const spec = target.host + "\n" + target.port + "\n" + (mode === "on" ? "1" : mode === "starttls" ? "2" : "0") + "\n" + target.host;
|
|
@@ -14916,11 +15878,15 @@ function __sbConnect(address, options) {
|
|
|
14916
15878
|
if (!(id > 0)) {
|
|
14917
15879
|
st.failed = new Error(__sbTcpOpen >= 16 ? "Too many open connections" : "TCP sockets are not available on this host");
|
|
14918
15880
|
st.eof = true;
|
|
14919
|
-
|
|
15881
|
+
__sbSetTimeout(() => __sbTcpSettle(st), 0);
|
|
14920
15882
|
return socket;
|
|
14921
15883
|
}
|
|
14922
15884
|
st.id = id;
|
|
14923
15885
|
__sbTcpOpen++;
|
|
15886
|
+
if (__sbDOL !== null) {
|
|
15887
|
+
const rec = __sbDOFrameRec();
|
|
15888
|
+
if (rec !== undefined && !rec.dead) rec.tcp.push(st);
|
|
15889
|
+
}
|
|
14924
15890
|
__sbTcpPump(st);
|
|
14925
15891
|
return socket;
|
|
14926
15892
|
}
|
|
@@ -14997,10 +15963,14 @@ function __sbWsConnectOutbound(ws, input, protocols) {
|
|
|
14997
15963
|
ws._accepted = true;
|
|
14998
15964
|
ws._state = 0;
|
|
14999
15965
|
ws.url = target.url;
|
|
15966
|
+
if (__sbDOL !== null) {
|
|
15967
|
+
const rec = __sbDOFrameRec();
|
|
15968
|
+
if (rec !== undefined && !rec.dead) rec.plain.push(ws);
|
|
15969
|
+
}
|
|
15000
15970
|
const id = __sbWsOpen(target, list, null);
|
|
15001
15971
|
if (!(id > 0)) {
|
|
15002
15972
|
ws._state = __SB_WS_CLOSED;
|
|
15003
|
-
|
|
15973
|
+
__sbSetTimeout(() => {
|
|
15004
15974
|
__sbWsFire(ws, { type: "error", target: ws });
|
|
15005
15975
|
__sbWsFire(ws, { type: "close", code: 1006, reason: "", wasClean: false, target: ws });
|
|
15006
15976
|
}, 0);
|
|
@@ -15019,9 +15989,9 @@ function __sbWsPumpOut(ws) {
|
|
|
15019
15989
|
const head = __sbWsOp("p", id, "", 0);
|
|
15020
15990
|
if (head === "") {
|
|
15021
15991
|
if (__sbWsCanWake()) {
|
|
15022
|
-
const timer =
|
|
15992
|
+
const timer = __sbSetTimeout(() => __sbWsPumpOut(ws), 2147483647);
|
|
15023
15993
|
__sbWsOp("w", id, "", timer);
|
|
15024
|
-
} else
|
|
15994
|
+
} else __sbSetTimeout(() => __sbWsPumpOut(ws), 15);
|
|
15025
15995
|
return;
|
|
15026
15996
|
}
|
|
15027
15997
|
const parts = head.split(" ");
|
|
@@ -15127,7 +16097,7 @@ function __sbWsOutClose(ws, code, text) {
|
|
|
15127
16097
|
const pending = ws._pending;
|
|
15128
16098
|
ws._pending = null;
|
|
15129
16099
|
if (pending) pending.reject(new TypeError("WebSocket connection was closed before it opened"));
|
|
15130
|
-
else
|
|
16100
|
+
else __sbSetTimeout(() => {
|
|
15131
16101
|
__sbWsFire(ws, { type: "error", target: ws });
|
|
15132
16102
|
__sbWsFire(ws, { type: "close", code: 1006, reason: "", wasClean: false, target: ws });
|
|
15133
16103
|
}, 0);
|
|
@@ -15136,7 +16106,7 @@ function __sbWsOutClose(ws, code, text) {
|
|
|
15136
16106
|
ws._state = __SB_WS_CLOSING;
|
|
15137
16107
|
__sbWsOp("c", id, text, code === undefined ? 1000 : code);
|
|
15138
16108
|
// A peer that never answers the close is dropped after a while.
|
|
15139
|
-
|
|
16109
|
+
__sbSetTimeout(() => {
|
|
15140
16110
|
if (ws._host !== id) return;
|
|
15141
16111
|
ws._host = 0;
|
|
15142
16112
|
__sbWsOp("r", id, "", 0);
|
|
@@ -15329,8 +16299,7 @@ function __sbWsRestoreLoop() {
|
|
|
15329
16299
|
__sbLogUncaught("websocket", error);
|
|
15330
16300
|
}
|
|
15331
16301
|
}
|
|
15332
|
-
|
|
15333
|
-
__sbWsOp("R", 0, "", timer);
|
|
16302
|
+
__sbWsRestorePark();
|
|
15334
16303
|
}
|
|
15335
16304
|
function __sbWsRestoreSocket(id) {
|
|
15336
16305
|
const r = __sbRpc("do.storage.get", { cls: "__sbws", id: "index", key: "s" + id });
|
|
@@ -15349,6 +16318,10 @@ function __sbWsRestoreSocket(id) {
|
|
|
15349
16318
|
return;
|
|
15350
16319
|
}
|
|
15351
16320
|
// Constructing the object restores its sockets (__sbWsHibRestore) and starts their pumps.
|
|
16321
|
+
if (__sbDOEvictOn()) {
|
|
16322
|
+
__sbDORun(target[0], target[1], undefined, () => undefined, false).then(undefined, (error) => __sbWsRestoreFailed(id, error));
|
|
16323
|
+
return;
|
|
16324
|
+
}
|
|
15352
16325
|
__sbGetDOInstance(target[0], target[1]);
|
|
15353
16326
|
}
|
|
15354
16327
|
|
|
@@ -15784,7 +16757,7 @@ function __sbIngressStream(request, id) {
|
|
|
15784
16757
|
const state = Number(__sbReqBodyOp("p", id, ""));
|
|
15785
16758
|
if (state === 0) {
|
|
15786
16759
|
// Nothing yet: the server fires this timer when bytes, the end or an error arrive.
|
|
15787
|
-
const timer =
|
|
16760
|
+
const timer = __sbSetTimeout(() => step(controller, resolve), 2147483647);
|
|
15788
16761
|
__sbReqBodyOp("w", id, timer);
|
|
15789
16762
|
return;
|
|
15790
16763
|
}
|
|
@@ -15891,7 +16864,164 @@ function __sbTrustedProxiesOnce() {
|
|
|
15891
16864
|
return __sbTrustedCache;
|
|
15892
16865
|
}
|
|
15893
16866
|
|
|
16867
|
+
// R2 transfer tickets (createUploadUrl, createDownloadUrl): /__sb/r2/transfer/BUCKET/TOKEN, scoped to one object and
|
|
16868
|
+
// good once. A standalone executable's native server answers them before JavaScript runs (patchR2Transfer, #195);
|
|
16869
|
+
// on a host that forwards every path to the worker (sproutd) the worker answers them here with the same statuses
|
|
16870
|
+
// and headers, before the app's fetch() sees the request. The body goes straight between the connection and the
|
|
16871
|
+
// object's file, one step at a time (__sbR2XferOp): at most a window of it is ever in the worker.
|
|
16872
|
+
const __SB_R2_TRANSFER = "/__sb/r2/transfer/";
|
|
16873
|
+
// Set by the embedded transport (it has the ticket contexts and their steps); the broker's own listener owns the path.
|
|
16874
|
+
// oxlint-disable-next-line no-unassigned-vars -- set by the embedded transport, above
|
|
16875
|
+
var __sbR2TransferOn; // no initializer: the embedded transport (above) sets it, and an initializer here would run after it
|
|
16876
|
+
// [bucket, token] when the request is for a ticket (the same shape the native server accepts), else null.
|
|
16877
|
+
function __sbR2TransferTarget(request) {
|
|
16878
|
+
const method = request.method;
|
|
16879
|
+
if (method !== "GET" && method !== "PUT" && method !== "HEAD" && method !== "POST" && method !== "DELETE") return null;
|
|
16880
|
+
const url = request.url;
|
|
16881
|
+
const at = url.indexOf(__SB_R2_TRANSFER);
|
|
16882
|
+
if (at < 0) return null;
|
|
16883
|
+
const hostEnd = url.indexOf("/", url.indexOf("//") + 2);
|
|
16884
|
+
if (at !== hostEnd) return null;
|
|
16885
|
+
let rest = url.slice(at + __SB_R2_TRANSFER.length);
|
|
16886
|
+
const cut = rest.search(/[?#]/);
|
|
16887
|
+
if (cut >= 0) rest = rest.slice(0, cut);
|
|
16888
|
+
const slash = rest.indexOf("/");
|
|
16889
|
+
if (slash <= 0 || slash > 255 || rest.indexOf("/", slash + 1) >= 0) return null;
|
|
16890
|
+
const bucket = rest.slice(0, slash);
|
|
16891
|
+
const token = rest.slice(slash + 1);
|
|
16892
|
+
if (token.length !== 24 || !/^[0-9a-f]+$/.test(token) || !/^[A-Za-z0-9_]+$/.test(bucket)) return null;
|
|
16893
|
+
return [bucket, token];
|
|
16894
|
+
}
|
|
16895
|
+
function __sbR2TransferError(status, text) {
|
|
16896
|
+
return new Response(text, { status, headers: { "content-type": "text/plain" } });
|
|
16897
|
+
}
|
|
16898
|
+
// A transfer status as a response (the native server's table: anything else is a 500).
|
|
16899
|
+
function __sbR2TransferFailed(status, text) {
|
|
16900
|
+
const known = status === 404 || status === 410 || status === 413 || status === 422 || status === 503 || status === 507;
|
|
16901
|
+
return __sbR2TransferError(known ? status : 500, text);
|
|
16902
|
+
}
|
|
16903
|
+
// Resolves when the streamed request body `body` has more (or ended or failed): the host fires the parked timer.
|
|
16904
|
+
function __sbR2BodyWait(body) {
|
|
16905
|
+
return new Promise((resolve) => {
|
|
16906
|
+
const timer = __sbSetTimeout(resolve, 2147483647);
|
|
16907
|
+
__sbReqBodyOp("w", body, timer);
|
|
16908
|
+
});
|
|
16909
|
+
}
|
|
16910
|
+
async function __sbR2TransferUpload(request, bucket, token, body) {
|
|
16911
|
+
let declared = 0;
|
|
16912
|
+
const length = request.headers.get("content-length");
|
|
16913
|
+
if (length !== null && length !== "") {
|
|
16914
|
+
if (!/^[0-9]+$/.test(length) || length.length > 15) return __sbR2TransferError(400, "invalid content-length");
|
|
16915
|
+
declared = Number(length);
|
|
16916
|
+
}
|
|
16917
|
+
const opened = __sbR2XferOp("u", bucket, token, declared).split(" ");
|
|
16918
|
+
if (opened[0] !== "0") return __sbR2TransferFailed(Number(opened[0]), "R2 transfer unavailable");
|
|
16919
|
+
const handle = opened[1];
|
|
16920
|
+
let status = 0;
|
|
16921
|
+
if (body === 0) {
|
|
16922
|
+
// The host delivered the body whole (or there is none): one write.
|
|
16923
|
+
const raw = __sbRawBody(request);
|
|
16924
|
+
if (raw != null && raw !== "") status = Number(__sbR2XferOp("b", handle, raw, 0));
|
|
16925
|
+
} else {
|
|
16926
|
+
for (;;) {
|
|
16927
|
+
const step = __sbR2XferOp("s", handle, "", body).split(" ");
|
|
16928
|
+
const kind = step[0];
|
|
16929
|
+
if (kind === "d") continue;
|
|
16930
|
+
if (kind === "w") {
|
|
16931
|
+
await __sbR2BodyWait(body);
|
|
16932
|
+
continue;
|
|
16933
|
+
}
|
|
16934
|
+
if (kind === "e") break;
|
|
16935
|
+
// 'f': the client went away; 's': a status refused the bytes (the ticket is released with it).
|
|
16936
|
+
status = kind === "s" ? Number(step[1]) : 500;
|
|
16937
|
+
if (kind === "f") __sbR2XferOp("x", "0", "", Number(handle));
|
|
16938
|
+
__sbReqBodyOp("c", body, "");
|
|
16939
|
+
return kind === "f" ? __sbR2TransferError(500, "R2 transfer failed") : __sbR2TransferFailed(status, "R2 transfer failed");
|
|
16940
|
+
}
|
|
16941
|
+
__sbReqBodyOp("c", body, "");
|
|
16942
|
+
}
|
|
16943
|
+
if (status !== 0) return __sbR2TransferFailed(status, "R2 transfer failed");
|
|
16944
|
+
const finished = Number(__sbR2XferOp("f", handle, "", 0));
|
|
16945
|
+
if (finished === 201) return new Response(null, { status: 201 });
|
|
16946
|
+
return __sbR2TransferFailed(finished, "R2 transfer failed");
|
|
16947
|
+
}
|
|
16948
|
+
function __sbR2TransferDownload(request, bucket, token) {
|
|
16949
|
+
const opened = __sbR2XferOp("d", bucket, token, 0).split(" ");
|
|
16950
|
+
if (opened[0] !== "0") return __sbR2TransferFailed(Number(opened[0]), "R2 transfer unavailable");
|
|
16951
|
+
const handle = Number(opened[1]);
|
|
16952
|
+
const size = Number(opened[2]);
|
|
16953
|
+
const etag = opened[3];
|
|
16954
|
+
const close = () => __sbR2XferOp("c", "0", "", handle);
|
|
16955
|
+
const match = request.headers.get("if-none-match");
|
|
16956
|
+
if (match === "*" || match === etag || match === '"' + etag + '"') {
|
|
16957
|
+
close();
|
|
16958
|
+
return new Response(null, { status: 304, headers: { etag } });
|
|
16959
|
+
}
|
|
16960
|
+
let first = 0;
|
|
16961
|
+
let last = size ? size - 1 : 0;
|
|
16962
|
+
let partial = false;
|
|
16963
|
+
const range = request.headers.get("range");
|
|
16964
|
+
if (range) {
|
|
16965
|
+
const refuse = (why) => {
|
|
16966
|
+
close();
|
|
16967
|
+
return __sbR2TransferError(416, why);
|
|
16968
|
+
};
|
|
16969
|
+
if (range.slice(0, 6) !== "bytes=") return refuse("invalid range");
|
|
16970
|
+
const spec = range.slice(6);
|
|
16971
|
+
const dash = spec.indexOf("-");
|
|
16972
|
+
if (dash <= 0) return refuse("invalid range");
|
|
16973
|
+
const from = spec.slice(0, dash);
|
|
16974
|
+
const to = spec.slice(dash + 1);
|
|
16975
|
+
if (!/^[0-9]+$/.test(from) || (to !== "" && !/^[0-9]+$/.test(to))) return refuse("invalid range");
|
|
16976
|
+
first = Number(from);
|
|
16977
|
+
if (to !== "") last = Number(to);
|
|
16978
|
+
if (first >= size || last < first) return refuse("range outside object");
|
|
16979
|
+
if (last >= size) last = size - 1;
|
|
16980
|
+
partial = true;
|
|
16981
|
+
}
|
|
16982
|
+
const total = size === 0 ? 0 : last - first + 1;
|
|
16983
|
+
const headers = { "content-type": "application/octet-stream", etag, "accept-ranges": "bytes", "content-length": String(total) };
|
|
16984
|
+
if (partial) headers["content-range"] = "bytes " + first + "-" + last + "/" + size;
|
|
16985
|
+
let sent = 0;
|
|
16986
|
+
const stream = new ReadableStream({
|
|
16987
|
+
pull(controller) {
|
|
16988
|
+
if (sent >= total) {
|
|
16989
|
+
close();
|
|
16990
|
+
controller.close();
|
|
16991
|
+
return;
|
|
16992
|
+
}
|
|
16993
|
+
const chunk = new Uint8Array(Math.min(__SB_R2_CHUNK, total - sent));
|
|
16994
|
+
const got = Number(__sbR2XferOp("r", String(handle), "", first + sent, chunk));
|
|
16995
|
+
if (got !== chunk.length) {
|
|
16996
|
+
close();
|
|
16997
|
+
controller.error(new Error("R2 transfer failed"));
|
|
16998
|
+
return;
|
|
16999
|
+
}
|
|
17000
|
+
sent += got;
|
|
17001
|
+
controller.enqueue(chunk);
|
|
17002
|
+
},
|
|
17003
|
+
cancel() {
|
|
17004
|
+
close();
|
|
17005
|
+
},
|
|
17006
|
+
});
|
|
17007
|
+
return new Response(stream, { status: partial ? 206 : 200, headers });
|
|
17008
|
+
}
|
|
17009
|
+
// The response for a ticket request, or a Promise of it.
|
|
17010
|
+
function __sbR2Transfer(request, target) {
|
|
17011
|
+
// The streamed body's id, taken now: a ticket request never reaches __sbDecodeIngress.
|
|
17012
|
+
const body = __sbReqBodyTake();
|
|
17013
|
+
const method = request.method;
|
|
17014
|
+
if (method === "GET") return __sbR2TransferDownload(request, target[0], target[1]);
|
|
17015
|
+
if (method !== "PUT") return __sbR2TransferError(405, "R2 transfer requires GET or PUT");
|
|
17016
|
+
return __sbR2TransferUpload(request, target[0], target[1], body);
|
|
17017
|
+
}
|
|
17018
|
+
|
|
15894
17019
|
globalThis.__sbEntry = function (handlers, request) {
|
|
17020
|
+
if (__sbR2TransferOn) {
|
|
17021
|
+
const ticket = __sbR2TransferTarget(request);
|
|
17022
|
+
// Through the same finish as the app's answer (promise, then a stream body is sent as it is read).
|
|
17023
|
+
if (ticket !== null) return __sbFetchWithDrain(__sbR2Transfer(request, ticket), [], false, false, true, null, null);
|
|
17024
|
+
}
|
|
15895
17025
|
__sbDecodeIngress(request);
|
|
15896
17026
|
const trigger = __sbHeader(request.headers, "x-sb-trigger");
|
|
15897
17027
|
if (!trigger) {
|
|
@@ -16025,6 +17155,22 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
16025
17155
|
const body = __sbReadJson(request);
|
|
16026
17156
|
const __acls = String(body.cls || "");
|
|
16027
17157
|
const __aid = String(body.id || "");
|
|
17158
|
+
if (__sbDOEvictOn()) {
|
|
17159
|
+
const none = {};
|
|
17160
|
+
return __sbDORun(
|
|
17161
|
+
__acls,
|
|
17162
|
+
__aid,
|
|
17163
|
+
undefined,
|
|
17164
|
+
(instance) => (__sbIsFn(instance.alarm) ? __sbDOAlarmCall(instance, __acls, __aid, body.scheduledTime, body.attempts) : none),
|
|
17165
|
+
true,
|
|
17166
|
+
).then(
|
|
17167
|
+
(value) => (value === none ? new Response("no alarm handler", { status: 404 }) : new Response(null, { status: 204 })),
|
|
17168
|
+
(error) => {
|
|
17169
|
+
__sbLogUncaught("alarm", error);
|
|
17170
|
+
return error && error.overloaded === true ? new Response("alarm deferred: too many objects are awake", { status: 503 }) : new Response("alarm failed", { status: 500 });
|
|
17171
|
+
},
|
|
17172
|
+
);
|
|
17173
|
+
}
|
|
16028
17174
|
const inst = __sbGetDOInstance(__acls, __aid);
|
|
16029
17175
|
if (!__sbIsFn(inst.alarm)) return new Response("no alarm handler", { status: 404 });
|
|
16030
17176
|
const __retries = __sbCompatDate() >= "2026-11-04";
|