@sproutboat/toolchain 0.4.2 → 0.4.4
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 +15 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +4 -0
- package/src/patch.test.ts +12 -0
- package/src/patch.ts +134 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.4.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d8b7877: Add native standalone R2 direct-download tickets with bounded, backpressure-aware
|
|
8
|
+
file reads, including upgrades for existing patched Porffor caches.
|
|
9
|
+
|
|
10
|
+
## 0.4.3
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 0b0a666: Add a native standalone R2 direct-upload bridge that streams transfer-ticket
|
|
15
|
+
request chunks to a temporary blob file, validates the digest, and atomically
|
|
16
|
+
publishes the object without buffering the upload in the Porffor request body.
|
|
17
|
+
|
|
3
18
|
## 0.4.2
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -40,6 +40,10 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
40
40
|
" i32 slot = 0;\n for (auto [key, value] : *req) {\n slot++;\n }\n" +
|
|
41
41
|
" *((i32*)(porf_mem + headers_ptr)) = slot;\n\n return headers_ptr;\n}\n" +
|
|
42
42
|
"static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {\n" +
|
|
43
|
+
" const std::string_view method = req->getCaseSensitiveMethod();\n" +
|
|
44
|
+
" const i32 method_ptr = get_method_ptr(method);\n" +
|
|
45
|
+
" if (method_ptr == 0) return;\n" +
|
|
46
|
+
" __porffor_js_enter();\n const i32 url_ptr = alloc_request_url(req);\n" +
|
|
43
47
|
" const i32 headers_ptr = collect_headers(req);\n}\n" +
|
|
44
48
|
"static bool is_forbidden_response_header(std::string_view key) {\n" +
|
|
45
49
|
' return key == "connection" ||\n' +
|
package/src/patch.test.ts
CHANGED
|
@@ -76,6 +76,11 @@ static i32 collect_headers(uWS::HttpRequest* req) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {
|
|
79
|
+
const std::string_view method = req->getCaseSensitiveMethod();
|
|
80
|
+
const i32 method_ptr = get_method_ptr(method);
|
|
81
|
+
if (method_ptr == 0) return;
|
|
82
|
+
__porffor_js_enter();
|
|
83
|
+
const i32 url_ptr = alloc_request_url(req);
|
|
79
84
|
const i32 headers_ptr = collect_headers(req);
|
|
80
85
|
}
|
|
81
86
|
|
|
@@ -159,6 +164,13 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
|
|
|
159
164
|
expect(once.match(/case 302: return "302 Found";/g)).toHaveLength(1);
|
|
160
165
|
// Body-limit edit still rides along.
|
|
161
166
|
expect(once).toContain("sb_request_body_max");
|
|
167
|
+
// #195: a valid transfer ticket is intercepted before JS request roots and
|
|
168
|
+
// the ordinary PendingRequest body allocation path.
|
|
169
|
+
expect(once).toContain("sb_r2_transfer_open");
|
|
170
|
+
expect(once).toContain("sb_r2_transfer_write");
|
|
171
|
+
expect(once).toContain("try_handle_r2_transfer(res, req, method)");
|
|
172
|
+
expect(once).toContain("sb_r2_transfer_download_open");
|
|
173
|
+
expect(once).toContain("sb_r2_transfer_download_v1");
|
|
162
174
|
|
|
163
175
|
// #163: collect_headers takes res, drops a client-sent x-sb-remote-addr, and
|
|
164
176
|
// appends the real peer; the call site passes res through.
|
package/src/patch.ts
CHANGED
|
@@ -413,6 +413,128 @@ const WRITE_RESPONSE_INJECT =
|
|
|
413
413
|
"}";
|
|
414
414
|
const WRITE_RESPONSE_MARKER = "porf_native_fetch_read_raw_bytes(body_value";
|
|
415
415
|
|
|
416
|
+
// #195: reserve a direct-transfer ticket before native-fetch allocates the
|
|
417
|
+
// ordinary in-memory PendingRequest. uWS then gives each wire chunk directly
|
|
418
|
+
// to the runtime C ABI, which owns the temporary object file.
|
|
419
|
+
const R2_TRANSFER_ANCHOR = "static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {\n";
|
|
420
|
+
const R2_TRANSFER_INJECT = `extern "C" {
|
|
421
|
+
struct sb_r2_transfer_ctx;
|
|
422
|
+
int sb_r2_transfer_open(const char*, const char*, size_t, sb_r2_transfer_ctx**);
|
|
423
|
+
int sb_r2_transfer_write(sb_r2_transfer_ctx*, const char*, size_t);
|
|
424
|
+
int sb_r2_transfer_finish(sb_r2_transfer_ctx*);
|
|
425
|
+
void sb_r2_transfer_abort(sb_r2_transfer_ctx*);
|
|
426
|
+
int sb_r2_transfer_download_open(const char*, const char*, sb_r2_transfer_ctx**);
|
|
427
|
+
size_t sb_r2_transfer_download_size(sb_r2_transfer_ctx*);
|
|
428
|
+
size_t sb_r2_transfer_download_read(sb_r2_transfer_ctx*, size_t, char*, size_t);
|
|
429
|
+
void sb_r2_transfer_download_close(sb_r2_transfer_ctx*);
|
|
430
|
+
}
|
|
431
|
+
static bool sb_r2_transfer_path(std::string_view path, std::string* bucket, std::string* token) {
|
|
432
|
+
static constexpr std::string_view prefix = "/__sb/r2/transfer/";
|
|
433
|
+
if (path.size() < prefix.size() || path.substr(0, prefix.size()) != prefix) return false;
|
|
434
|
+
const std::string_view rest = path.substr(prefix.size());
|
|
435
|
+
const size_t slash = rest.find('/');
|
|
436
|
+
if (slash == std::string_view::npos || slash == 0 || slash > 255 || rest.find('/', slash + 1) != std::string_view::npos) return false;
|
|
437
|
+
const std::string_view b = rest.substr(0, slash), t = rest.substr(slash + 1);
|
|
438
|
+
if (t.size() != 24) return false;
|
|
439
|
+
for (char c : b) if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')) return false;
|
|
440
|
+
for (char c : t) if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return false;
|
|
441
|
+
*bucket = std::string(b); *token = std::string(t); return true;
|
|
442
|
+
}
|
|
443
|
+
static std::string sb_r2_transfer_status(int status) {
|
|
444
|
+
switch (status) {
|
|
445
|
+
case 201: return "201 Created"; case 404: return "404 Not Found"; case 410: return "410 Gone";
|
|
446
|
+
case 413: return "413 Payload Too Large"; case 422: return "422 Unprocessable Content";
|
|
447
|
+
case 503: return "503 Service Unavailable"; case 507: return "507 Insufficient Storage";
|
|
448
|
+
default: return "500 Internal Server Error";
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
static bool try_handle_r2_transfer(uWS::HttpResponse<false>* res, uWS::HttpRequest* req, std::string_view method) {
|
|
452
|
+
std::string bucket, token;
|
|
453
|
+
if (!sb_r2_transfer_path(req->getUrl(), &bucket, &token)) return false;
|
|
454
|
+
// sb_r2_transfer_download_v1
|
|
455
|
+
if (method == "GET") {
|
|
456
|
+
sb_r2_transfer_ctx* download = nullptr;
|
|
457
|
+
const int open_status = sb_r2_transfer_download_open(bucket.c_str(), token.c_str(), &download);
|
|
458
|
+
if (open_status != 0 || !download) { respond_with_error(res, sb_r2_transfer_status(open_status), "R2 transfer unavailable", true); return true; }
|
|
459
|
+
struct Download { sb_r2_transfer_ctx* ctx; uintmax_t size; bool closed = false; };
|
|
460
|
+
auto state = std::make_shared<Download>(Download{download, sb_r2_transfer_download_size(download)});
|
|
461
|
+
auto pump = std::make_shared<std::function<bool()>>();
|
|
462
|
+
*pump = [res, state, pump]() {
|
|
463
|
+
if (state->closed) return false;
|
|
464
|
+
const uintmax_t offset = res->getWriteOffset();
|
|
465
|
+
char chunk[65536];
|
|
466
|
+
const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)offset, chunk, sizeof(chunk));
|
|
467
|
+
const auto result = res->tryEnd(std::string_view(chunk, bytes), state->size);
|
|
468
|
+
if (result.second) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
|
|
469
|
+
if (!result.first) res->onWritable([pump](uintmax_t) { return (*pump)(); });
|
|
470
|
+
return false;
|
|
471
|
+
};
|
|
472
|
+
res->writeHeader("Content-Type", "application/octet-stream");
|
|
473
|
+
res->onAborted([state]() { if (!state->closed) { sb_r2_transfer_download_close(state->ctx); state->closed = true; } });
|
|
474
|
+
(*pump)();
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
if (method != "PUT") { respond_with_error(res, "405 Method Not Allowed", "R2 transfer requires GET or PUT", true); return true; }
|
|
478
|
+
size_t declared = 0;
|
|
479
|
+
const std::string_view content_length = req->getHeader("content-length");
|
|
480
|
+
if (!content_length.empty() && !parse_content_length(content_length, &declared)) { respond_with_error(res, "400 Bad Request", "invalid content-length", true); return true; }
|
|
481
|
+
sb_r2_transfer_ctx* opened = nullptr;
|
|
482
|
+
const int open_status = sb_r2_transfer_open(bucket.c_str(), token.c_str(), declared, &opened);
|
|
483
|
+
if (open_status != 0 || !opened) { respond_with_error(res, sb_r2_transfer_status(open_status), "R2 transfer unavailable", true); return true; }
|
|
484
|
+
auto ctx = std::make_shared<sb_r2_transfer_ctx*>(opened);
|
|
485
|
+
res->onAborted([ctx]() { if (*ctx) { sb_r2_transfer_abort(*ctx); *ctx = nullptr; } });
|
|
486
|
+
res->onData([res, ctx](std::string_view chunk, bool is_last) {
|
|
487
|
+
if (!*ctx) return;
|
|
488
|
+
const int write_status = sb_r2_transfer_write(*ctx, chunk.data(), chunk.size());
|
|
489
|
+
if (write_status != 0) { sb_r2_transfer_abort(*ctx); *ctx = nullptr; respond_with_error(res, sb_r2_transfer_status(write_status), "R2 transfer failed", true); return; }
|
|
490
|
+
if (is_last) {
|
|
491
|
+
const int finish_status = sb_r2_transfer_finish(*ctx); *ctx = nullptr;
|
|
492
|
+
if (finish_status == 201) { res->writeStatus("201 Created"); res->end(); }
|
|
493
|
+
else respond_with_error(res, sb_r2_transfer_status(finish_status), "R2 transfer failed", true);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
static void on_request(uWS::HttpResponse<false>* res, uWS::HttpRequest* req) {
|
|
499
|
+
`;
|
|
500
|
+
const R2_TRANSFER_MARKER = "sb_r2_transfer_path(std::string_view path";
|
|
501
|
+
const R2_TRANSFER_CALL_ANCHOR = " __porffor_js_enter();\n const i32 url_ptr = alloc_request_url(req);";
|
|
502
|
+
const R2_TRANSFER_CALL_INJECT =
|
|
503
|
+
" if (try_handle_r2_transfer(res, req, method)) return;\n" + R2_TRANSFER_CALL_ANCHOR;
|
|
504
|
+
const R2_TRANSFER_CALL_MARKER = "try_handle_r2_transfer(res, req, method)";
|
|
505
|
+
const R2_DOWNLOAD_CACHE_MARKER = "sb_r2_transfer_download_v1";
|
|
506
|
+
const R2_DOWNLOAD_DECL_ANCHOR = " void sb_r2_transfer_abort(sb_r2_transfer_ctx*);\n}";
|
|
507
|
+
const R2_DOWNLOAD_DECL_INJECT =
|
|
508
|
+
" void sb_r2_transfer_abort(sb_r2_transfer_ctx*);\n" +
|
|
509
|
+
" int sb_r2_transfer_download_open(const char*, const char*, sb_r2_transfer_ctx**);\n" +
|
|
510
|
+
" size_t sb_r2_transfer_download_size(sb_r2_transfer_ctx*);\n" +
|
|
511
|
+
" size_t sb_r2_transfer_download_read(sb_r2_transfer_ctx*, size_t, char*, size_t);\n" +
|
|
512
|
+
" void sb_r2_transfer_download_close(sb_r2_transfer_ctx*);\n}";
|
|
513
|
+
const R2_DOWNLOAD_BRANCH_ANCHOR = ' if (method != "PUT") { respond_with_error(res, "405 Method Not Allowed", "R2 transfer requires PUT", true); return true; }';
|
|
514
|
+
const R2_DOWNLOAD_BRANCH_INJECT = ` // sb_r2_transfer_download_v1
|
|
515
|
+
if (method == "GET") {
|
|
516
|
+
sb_r2_transfer_ctx* download = nullptr;
|
|
517
|
+
const int status = sb_r2_transfer_download_open(bucket.c_str(), token.c_str(), &download);
|
|
518
|
+
if (status != 0 || !download) { respond_with_error(res, sb_r2_transfer_status(status), "R2 transfer unavailable", true); return true; }
|
|
519
|
+
struct Download { sb_r2_transfer_ctx* ctx; uintmax_t size; bool closed = false; };
|
|
520
|
+
auto state = std::make_shared<Download>(Download{download, sb_r2_transfer_download_size(download)});
|
|
521
|
+
auto pump = std::make_shared<std::function<bool()>>();
|
|
522
|
+
*pump = [res, state, pump]() {
|
|
523
|
+
if (state->closed) return false;
|
|
524
|
+
const uintmax_t offset = res->getWriteOffset(); char chunk[65536];
|
|
525
|
+
const size_t bytes = sb_r2_transfer_download_read(state->ctx, (size_t)offset, chunk, sizeof(chunk));
|
|
526
|
+
const auto result = res->tryEnd(std::string_view(chunk, bytes), state->size);
|
|
527
|
+
if (result.second) { sb_r2_transfer_download_close(state->ctx); state->closed = true; return false; }
|
|
528
|
+
if (!result.first) res->onWritable([pump](uintmax_t) { return (*pump)(); });
|
|
529
|
+
return false;
|
|
530
|
+
};
|
|
531
|
+
res->writeHeader("Content-Type", "application/octet-stream");
|
|
532
|
+
res->onAborted([state]() { if (!state->closed) { sb_r2_transfer_download_close(state->ctx); state->closed = true; } });
|
|
533
|
+
(*pump)();
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
if (method != "PUT") { respond_with_error(res, "405 Method Not Allowed", "R2 transfer requires GET or PUT", true); return true; }`;
|
|
537
|
+
|
|
416
538
|
const done = new Set<string>();
|
|
417
539
|
|
|
418
540
|
/** Edits to Porffor's uWebSockets shim: `[marker, anchor, inject, what]`. */
|
|
@@ -444,6 +566,8 @@ const UWS_EDITS = [
|
|
|
444
566
|
WRITE_RESPONSE_INJECT,
|
|
445
567
|
"detect x-sb-raw-body, steer the body read (#176)",
|
|
446
568
|
],
|
|
569
|
+
[R2_TRANSFER_MARKER, R2_TRANSFER_ANCHOR, R2_TRANSFER_INJECT, "native direct R2 transfer ingress (#195)"],
|
|
570
|
+
[R2_TRANSFER_CALL_MARKER, R2_TRANSFER_CALL_ANCHOR, R2_TRANSFER_CALL_INJECT, "direct R2 transfer dispatch (#195)"],
|
|
447
571
|
] as const;
|
|
448
572
|
|
|
449
573
|
/** The uWebSockets shim source: body limit + the #156 status-line fix. Exported for tests. */
|
|
@@ -451,6 +575,16 @@ export async function patchUwebsockets(root: string): Promise<void> {
|
|
|
451
575
|
const file = resolve(root, "compiler/uwebsockets.js");
|
|
452
576
|
let src = await readFile(file, "utf8");
|
|
453
577
|
let changed = false;
|
|
578
|
+
// Upgrade already-patched caches from the initial upload-only bridge. New
|
|
579
|
+
// caches receive the full bridge below; old ones need declarations plus the
|
|
580
|
+
// GET dispatch grafted onto their existing transfer handler.
|
|
581
|
+
if (src.includes(R2_TRANSFER_MARKER) && !src.includes(R2_DOWNLOAD_CACHE_MARKER)) {
|
|
582
|
+
if (!src.includes(R2_DOWNLOAD_DECL_ANCHOR) || !src.includes(R2_DOWNLOAD_BRANCH_ANCHOR))
|
|
583
|
+
throw new Error(`could not upgrade Porffor's direct R2 transfer bridge: anchor not found in ${file}.`);
|
|
584
|
+
src = src.replace(R2_DOWNLOAD_DECL_ANCHOR, R2_DOWNLOAD_DECL_INJECT);
|
|
585
|
+
src = src.replace(R2_DOWNLOAD_BRANCH_ANCHOR, R2_DOWNLOAD_BRANCH_INJECT);
|
|
586
|
+
changed = true;
|
|
587
|
+
}
|
|
454
588
|
for (const [marker, anchor, inject, what] of UWS_EDITS) {
|
|
455
589
|
if (src.includes(marker)) continue;
|
|
456
590
|
if (!src.includes(anchor)) {
|