@sproutboat/runtime 0.9.1 → 0.9.3

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,18 @@
1
1
  # @sproutboat/runtime
2
2
 
3
+ ## 0.9.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 5ee065c: Stream standalone direct R2 byte ranges safely and support ETag conditionals.
8
+
9
+ ## 0.9.2
10
+
11
+ ### Patch Changes
12
+
13
+ - d8b7877: Add native standalone R2 direct-download tickets with bounded, backpressure-aware
14
+ file reads, including upgrades for existing patched Porffor caches.
15
+
3
16
  ## 0.9.1
4
17
 
5
18
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -581,7 +581,7 @@ static void sb_r2_delete_blob(const char* path, const char* bucket, const char*
581
581
  typedef struct sb_r2_transfer_ctx {
582
582
  FILE* file;
583
583
  sqlite3* db;
584
- char store[1024], bucket[256], key[512], token[32], expected[65], http[1024], custom[1024], temporary[1200];
584
+ char store[1024], bucket[256], key[512], token[32], expected[65], etag[65], http[1024], custom[1024], temporary[1200];
585
585
  size_t size, max_bytes;
586
586
  } sb_r2_transfer_ctx;
587
587
 
@@ -746,6 +746,66 @@ 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, etag 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
+ const char* etag = (const char*)sqlite3_column_text(st, 1);
780
+ if (!etag || strlen(etag) >= sizeof(ctx->etag)) { sqlite3_finalize(st); free(ctx); return 404; }
781
+ snprintf(ctx->etag, sizeof(ctx->etag), "%s", etag);
782
+ char blobpath[1024];
783
+ sb_r2_blob_path(blobpath, sizeof(blobpath), ctx->store, bucket, ctx->key, generation ? generation : "");
784
+ sqlite3_finalize(st);
785
+ ctx->file = fopen(blobpath, "rb");
786
+ if (!ctx->file) { free(ctx); return 404; }
787
+ fseek(ctx->file, 0, SEEK_END); long length = ftell(ctx->file); rewind(ctx->file);
788
+ if (length < 0) { fclose(ctx->file); free(ctx); return 500; }
789
+ ctx->size = (size_t)length;
790
+ st = 0;
791
+ 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; }
792
+ sqlite3_bind_text(st, 1, token, -1, SB_SQLITE_TRANSIENT);
793
+ int claimed = sqlite3_step(st) == SB_SQLITE_DONE && sqlite3_changes(ctx->db) == 1;
794
+ sqlite3_finalize(st);
795
+ if (!claimed) { fclose(ctx->file); free(ctx); return 410; }
796
+ *out = ctx;
797
+ return 0;
798
+ }
799
+
800
+ size_t sb_r2_transfer_download_size(sb_r2_transfer_ctx* ctx) { return ctx ? ctx->size : 0; }
801
+ const char* sb_r2_transfer_download_etag(sb_r2_transfer_ctx* ctx) { return ctx ? ctx->etag : ""; }
802
+ size_t sb_r2_transfer_download_read(sb_r2_transfer_ctx* ctx, size_t offset, char* out, size_t cap) {
803
+ if (!ctx || !ctx->file || offset >= ctx->size || fseek(ctx->file, (long)offset, SEEK_SET) != 0) return 0;
804
+ size_t wanted = ctx->size - offset < cap ? ctx->size - offset : cap;
805
+ return fread(out, 1, wanted, ctx->file);
806
+ }
807
+ void sb_r2_transfer_download_close(sb_r2_transfer_ctx* ctx) { if (ctx) { if (ctx->file) fclose(ctx->file); free(ctx); } }
808
+
749
809
  // Run a script: one or more statements separated by semicolons. sqlite3_exec
750
810
  // handles the whole string, which prepare/step does not — it compiles the first
751
811
  // statement and silently ignores the rest, so a schema built from one exec call
package/src/wrap.ts CHANGED
@@ -200,7 +200,7 @@ export function wrapNativeFetchHandler(
200
200
  const nativeTransferAbi =
201
201
  transport === "embedded"
202
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) {}`;\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";
204
204
  const wire = hasBindings(bindings)
205
205
  ? `__sbInstallBindings(env, ${JSON.stringify(bindings)});\n`
206
206
  : "";