@sproutboat/runtime 0.9.0 → 0.9.2

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 CHANGED
@@ -1,5 +1,20 @@
1
1
  # @sproutboat/runtime
2
2
 
3
+ ## 0.9.2
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.9.1
11
+
12
+ ### Patch Changes
13
+
14
+ - e0944a4: Provide the native direct-transfer ABI in broker builds so the shared
15
+ uWebSockets patch does not introduce unresolved symbols outside standalone
16
+ mode.
17
+
3
18
  ## 0.9.0
4
19
 
5
20
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -746,6 +746,62 @@ void sb_r2_transfer_abort(sb_r2_transfer_ctx* ctx) {
746
746
  free(ctx);
747
747
  }
748
748
 
749
+ // Claims a download ticket and opens its immutable blob generation. The C++
750
+ // socket shim reads this handle at its current write offset, so no object-sized
751
+ // buffer is ever allocated in either the JS or C++ heap.
752
+ int sb_r2_transfer_download_open(const char* bucket, const char* token, sb_r2_transfer_ctx** out) {
753
+ *out = 0;
754
+ if (!bucket || !token || strlen(bucket) >= 256 || strlen(token) != 24) return 404;
755
+ sb_r2_transfer_ctx* ctx = (sb_r2_transfer_ctx*)calloc(1, sizeof(*ctx));
756
+ if (!ctx) return 503;
757
+ sb_r2_transfer_store_path(ctx->store, sizeof(ctx->store));
758
+ int index = sb_db_for(ctx->store);
759
+ if (index < 0) { free(ctx); return 503; }
760
+ ctx->db = sb_dbs[index];
761
+ sqlite3_stmt* st = 0;
762
+ const char* ticket_sql = "SELECT key FROM r2_transfer WHERE token=?1 AND bucket=?2 AND method='download' AND used=0 AND expires>=?3";
763
+ if (sqlite3_prepare_v2(ctx->db, ticket_sql, -1, &st, 0) != 0 || !st) { free(ctx); return 503; }
764
+ sqlite3_bind_text(st, 1, token, -1, SB_SQLITE_TRANSIENT);
765
+ sqlite3_bind_text(st, 2, bucket, -1, SB_SQLITE_TRANSIENT);
766
+ sqlite3_bind_int64(st, 3, (int64_t)time(0) * 1000);
767
+ if (sqlite3_step(st) != SB_SQLITE_ROW) { sqlite3_finalize(st); free(ctx); return 410; }
768
+ const char* key = (const char*)sqlite3_column_text(st, 0);
769
+ if (!key || strlen(key) >= sizeof(ctx->key)) { sqlite3_finalize(st); free(ctx); return 404; }
770
+ snprintf(ctx->bucket, sizeof(ctx->bucket), "%s", bucket);
771
+ snprintf(ctx->key, sizeof(ctx->key), "%s", key);
772
+ sqlite3_finalize(st);
773
+ st = 0;
774
+ if (sqlite3_prepare_v2(ctx->db, "SELECT blob_id FROM r2 WHERE bucket=?1 AND key=?2", -1, &st, 0) != 0 || !st) { free(ctx); return 503; }
775
+ sqlite3_bind_text(st, 1, bucket, -1, SB_SQLITE_TRANSIENT);
776
+ sqlite3_bind_text(st, 2, ctx->key, -1, SB_SQLITE_TRANSIENT);
777
+ if (sqlite3_step(st) != SB_SQLITE_ROW) { sqlite3_finalize(st); free(ctx); return 404; }
778
+ const char* generation = (const char*)sqlite3_column_text(st, 0);
779
+ char blobpath[1024];
780
+ sb_r2_blob_path(blobpath, sizeof(blobpath), ctx->store, bucket, ctx->key, generation ? generation : "");
781
+ sqlite3_finalize(st);
782
+ ctx->file = fopen(blobpath, "rb");
783
+ if (!ctx->file) { free(ctx); return 404; }
784
+ fseek(ctx->file, 0, SEEK_END); long length = ftell(ctx->file); rewind(ctx->file);
785
+ if (length < 0) { fclose(ctx->file); free(ctx); return 500; }
786
+ ctx->size = (size_t)length;
787
+ st = 0;
788
+ if (sqlite3_prepare_v2(ctx->db, "UPDATE r2_transfer SET used=1 WHERE token=?1 AND used=0", -1, &st, 0) != 0 || !st) { fclose(ctx->file); free(ctx); return 503; }
789
+ sqlite3_bind_text(st, 1, token, -1, SB_SQLITE_TRANSIENT);
790
+ int claimed = sqlite3_step(st) == SB_SQLITE_DONE && sqlite3_changes(ctx->db) == 1;
791
+ sqlite3_finalize(st);
792
+ if (!claimed) { fclose(ctx->file); free(ctx); return 410; }
793
+ *out = ctx;
794
+ return 0;
795
+ }
796
+
797
+ size_t sb_r2_transfer_download_size(sb_r2_transfer_ctx* ctx) { return ctx ? ctx->size : 0; }
798
+ size_t sb_r2_transfer_download_read(sb_r2_transfer_ctx* ctx, size_t offset, char* out, size_t cap) {
799
+ if (!ctx || !ctx->file || offset >= ctx->size || fseek(ctx->file, (long)offset, SEEK_SET) != 0) return 0;
800
+ size_t wanted = ctx->size - offset < cap ? ctx->size - offset : cap;
801
+ return fread(out, 1, wanted, ctx->file);
802
+ }
803
+ void sb_r2_transfer_download_close(sb_r2_transfer_ctx* ctx) { if (ctx) { if (ctx->file) fclose(ctx->file); free(ctx); } }
804
+
749
805
  // Run a script: one or more statements separated by semicolons. sqlite3_exec
750
806
  // handles the whole string, which prepare/step does not — it compiles the first
751
807
  // statement and silently ignores the rest, so a schema built from one exec call
package/src/wrap.ts CHANGED
@@ -175,6 +175,8 @@ export function wrapNativeFetchHandler(
175
175
  appName: string = "app",
176
176
  /** #15 — assets baked into the module for a binary that has no files beside it. */
177
177
  assets?: { manifest: unknown; files: Record<string, string> },
178
+ /** The selected transport determines whether the direct-transfer C ABI is real or stubbed. */
179
+ transport: Transport = "broker",
178
180
  ): string {
179
181
  const neutralised = neutraliseExports(source);
180
182
  if (neutralised === null || !/\bfetch\s*\(/.test(source)) {
@@ -191,9 +193,14 @@ export function wrapNativeFetchHandler(
191
193
  // #15 — and enforces the outbound allowlist itself, with no broker to do it.
192
194
  `globalThis.__sbOutbound = ${JSON.stringify(bindings.outbound)};\n` +
193
195
  (assets ? `globalThis.__sbAssets = ${JSON.stringify(assets)};\n` : "");
194
- // The native transfer hook runs before JavaScript receives its Request, so
195
- // it needs the same default data-directory name as the embedded transport.
196
- const nativeAppName = `Porffor.c\`const char* sb_standalone_app_name(void) { return ${JSON.stringify(appName)}; }\`;\n`;
196
+ // The native transfer hook runs before JavaScript receives its Request. It
197
+ // has a real file-backed implementation only in embedded builds. The broker
198
+ // transport supplies ABI-compatible stubs because its separate transfer
199
+ // listener owns these URLs, while the shared uWebSockets patch is global.
200
+ const nativeTransferAbi =
201
+ transport === "embedded"
202
+ ? `Porffor.c\`const char* sb_standalone_app_name(void) { return ${JSON.stringify(appName)}; }\`;\n`
203
+ : "Porffor.c`typedef struct sb_r2_transfer_ctx { int unused; } sb_r2_transfer_ctx; int sb_r2_transfer_open(const char* a, const char* b, size_t c, sb_r2_transfer_ctx** d) { if (d) *d = 0; return 404; } int sb_r2_transfer_write(sb_r2_transfer_ctx* a, const char* b, size_t c) { return 404; } int sb_r2_transfer_finish(sb_r2_transfer_ctx* a) { return 404; } void sb_r2_transfer_abort(sb_r2_transfer_ctx* a) {} int sb_r2_transfer_download_open(const char* a, const char* b, sb_r2_transfer_ctx** c) { if (c) *c = 0; return 404; } size_t sb_r2_transfer_download_size(sb_r2_transfer_ctx* a) { return 0; } size_t sb_r2_transfer_download_read(sb_r2_transfer_ctx* a, size_t b, char* c, size_t d) { return 0; } void sb_r2_transfer_download_close(sb_r2_transfer_ctx* a) {}`;\n";
197
204
  const wire = hasBindings(bindings)
198
205
  ? `__sbInstallBindings(env, ${JSON.stringify(bindings)});\n`
199
206
  : "";
@@ -208,7 +215,7 @@ export function wrapNativeFetchHandler(
208
215
  : "";
209
216
 
210
217
  return (
211
- `${prelude}\n${nativeAppName}${compat}${env}${wire}` +
218
+ `${prelude}\n${nativeTransferAbi}${compat}${env}${wire}` +
212
219
  `${neutralised}\n` +
213
220
  `${registerDO}${triggers}` +
214
221
  `export default {\n port: ${port},\n fetch(request) { return __sbEntry(__sbHandlers, request); }\n};\n`