@sproutboat/runtime 0.9.5 → 0.10.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 +17 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +5 -2
- package/src/transport-embedded.js +30 -13
- package/src/wrap.ts +21 -8
- package/src/wrap.test.ts +0 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7287d9b: KV: native key expiration (baronunread/sproutboat#58). `env.KV.put(key, value, { expirationTtl })` / `{ expiration }` now match the Cloudflare Workers contract: `expirationTtl` is seconds from now (rejected below 60, matching CF), `expiration` is an absolute unix-seconds timestamp. An expired key reads as absent from `get()` and is omitted from `list()`; the row is hard-deleted lazily the next time it's touched by a get. No new transport op — both `expirationTtl` and `expiration` are optional fields on the existing `kv.put` frame, so an older broker/prelude pair that ignores them keeps working exactly as before.
|
|
8
|
+
- 7287d9b: Version metadata binding (baronunread/sproutboat#126). `sproutboat.jsonc` gains a `version_metadata` key naming a binding (like wrangler's `[version_metadata]`), and `wrapNativeFetchHandler` bakes `env.<binding> = { id, tag, timestamp }` directly into the module at build time — no broker round trip. `id`/`tag`/`timestamp` are supplied by the caller (the CLI derives them from the artifact digest, the project name, and the build time); this package only wires the config field through and does the baking.
|
|
9
|
+
|
|
10
|
+
## 0.9.6
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- f070d70: Revert native R2 transfer size-gating (#202). It saved ~11KB of `__text`
|
|
15
|
+
(~1.3% of a typical binary) and on macOS didn't even change the shipped
|
|
16
|
+
file size, since `__TEXT` is page-aligned. Not worth the `#ifdef`
|
|
17
|
+
guard-placement bug class it introduced. R2 transfer support is native code
|
|
18
|
+
again, always compiled in with 404 stubs where unused, same as before #202.
|
|
19
|
+
|
|
3
20
|
## 0.9.5
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1170,8 +1170,11 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
1170
1170
|
const r = __sbRpc("kv.get", { ns, key: String(key) });
|
|
1171
1171
|
return r.found ? r.value : null;
|
|
1172
1172
|
},
|
|
1173
|
-
put(key, value) {
|
|
1174
|
-
|
|
1173
|
+
put(key, value, options) {
|
|
1174
|
+
const msg = { ns, key: String(key), value: String(value) };
|
|
1175
|
+
if (options && options.expirationTtl != null) msg.expirationTtl = Number(options.expirationTtl);
|
|
1176
|
+
else if (options && options.expiration != null) msg.expiration = Number(options.expiration);
|
|
1177
|
+
__sbRpc("kv.put", msg);
|
|
1175
1178
|
},
|
|
1176
1179
|
delete(key) {
|
|
1177
1180
|
__sbRpc("kv.delete", { ns, key: String(key) });
|
|
@@ -578,7 +578,6 @@ static void sb_r2_delete_blob(const char* path, const char* bucket, const char*
|
|
|
578
578
|
// path before it allocates a PendingRequest body. The context owns only a file
|
|
579
579
|
// descriptor and small metadata buffers; object bytes stay in the kernel page
|
|
580
580
|
// cache until the final fixed-size digest pass.
|
|
581
|
-
#ifdef SB_R2_TRANSFER
|
|
582
581
|
typedef struct sb_r2_transfer_ctx {
|
|
583
582
|
FILE* file;
|
|
584
583
|
sqlite3* db;
|
|
@@ -806,7 +805,6 @@ size_t sb_r2_transfer_download_read(sb_r2_transfer_ctx* ctx, size_t offset, char
|
|
|
806
805
|
return fread(out, 1, wanted, ctx->file);
|
|
807
806
|
}
|
|
808
807
|
void sb_r2_transfer_download_close(sb_r2_transfer_ctx* ctx) { if (ctx) { if (ctx->file) fclose(ctx->file); free(ctx); } }
|
|
809
|
-
#endif
|
|
810
808
|
|
|
811
809
|
// Run a script: one or more statements separated by semicolons. sqlite3_exec
|
|
812
810
|
// handles the whole string, which prepare/step does not — it compiles the first
|
|
@@ -1781,8 +1779,13 @@ function __sbEnsureSchema() {
|
|
|
1781
1779
|
const s = __sbStore();
|
|
1782
1780
|
__sbSql(
|
|
1783
1781
|
s,
|
|
1784
|
-
"CREATE TABLE IF NOT EXISTS kv (ns TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL,
|
|
1782
|
+
"CREATE TABLE IF NOT EXISTS kv (ns TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, " +
|
|
1783
|
+
"expires_at INTEGER, PRIMARY KEY (ns, key))",
|
|
1785
1784
|
);
|
|
1785
|
+
const kvColumns = __sbSql(s, "PRAGMA table_info(kv)", []).rows;
|
|
1786
|
+
let hasKvExpiresAt = false;
|
|
1787
|
+
for (let i = 0; i < kvColumns.length; i++) if (kvColumns[i][1] === "expires_at") hasKvExpiresAt = true;
|
|
1788
|
+
if (!hasKvExpiresAt) __sbSql(s, "ALTER TABLE kv ADD COLUMN expires_at INTEGER", []);
|
|
1786
1789
|
// #56 — no `body` column: an object's bytes live in their own file under
|
|
1787
1790
|
// "<data-dir>/r2-blobs/" (sb_r2_put_c/sb_r2_get_c), never in the sqlite row.
|
|
1788
1791
|
__sbSql(
|
|
@@ -1871,16 +1874,29 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1871
1874
|
if (op === "ping") return { ok: true, op: "pong", echo: msg.msg };
|
|
1872
1875
|
|
|
1873
1876
|
if (op === "kv.get") {
|
|
1874
|
-
const r = __sbSql(store, "SELECT value FROM kv WHERE ns = ? AND key = ?", [msg.ns, msg.key]);
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1877
|
+
const r = __sbSql(store, "SELECT value, expires_at FROM kv WHERE ns = ? AND key = ?", [msg.ns, msg.key]);
|
|
1878
|
+
if (!r.rows.length) return { ok: true, found: false, value: null };
|
|
1879
|
+
const expiresAt = r.rows[0][1];
|
|
1880
|
+
if (expiresAt != null && expiresAt <= Date.now()) {
|
|
1881
|
+
__sbSql(store, "DELETE FROM kv WHERE ns = ? AND key = ?", [msg.ns, msg.key]);
|
|
1882
|
+
return { ok: true, found: false, value: null };
|
|
1883
|
+
}
|
|
1884
|
+
return { ok: true, found: true, value: r.rows[0][0] };
|
|
1878
1885
|
}
|
|
1879
1886
|
if (op === "kv.put") {
|
|
1887
|
+
// #58 — CF rejects expirationTtl under 60 seconds; matched here.
|
|
1888
|
+
let expiresAt = null;
|
|
1889
|
+
if (msg.expirationTtl != null) {
|
|
1890
|
+
const ttl = Number(msg.expirationTtl);
|
|
1891
|
+
if (!(ttl >= 60)) throw new Error("expirationTtl must be at least 60 seconds");
|
|
1892
|
+
expiresAt = Date.now() + ttl * 1000;
|
|
1893
|
+
} else if (msg.expiration != null) {
|
|
1894
|
+
expiresAt = Number(msg.expiration) * 1000;
|
|
1895
|
+
}
|
|
1880
1896
|
__sbSql(
|
|
1881
1897
|
store,
|
|
1882
|
-
"INSERT INTO kv (ns, key, value) VALUES (?1,?2,?3) ON CONFLICT (ns, key) DO UPDATE SET value = ?3",
|
|
1883
|
-
[msg.ns, msg.key, msg.value],
|
|
1898
|
+
"INSERT INTO kv (ns, key, value, expires_at) VALUES (?1,?2,?3,?4) ON CONFLICT (ns, key) DO UPDATE SET value = ?3, expires_at = ?4",
|
|
1899
|
+
[msg.ns, msg.key, msg.value, expiresAt],
|
|
1884
1900
|
);
|
|
1885
1901
|
return { ok: true };
|
|
1886
1902
|
}
|
|
@@ -1889,10 +1905,11 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1889
1905
|
return { ok: true };
|
|
1890
1906
|
}
|
|
1891
1907
|
if (op === "kv.list") {
|
|
1892
|
-
const r = __sbSql(
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1908
|
+
const r = __sbSql(
|
|
1909
|
+
store,
|
|
1910
|
+
"SELECT key FROM kv WHERE ns = ? AND key LIKE ? || '%' AND (expires_at IS NULL OR expires_at > ?) ORDER BY key",
|
|
1911
|
+
[msg.ns, msg.prefix || "", Date.now()],
|
|
1912
|
+
);
|
|
1896
1913
|
const keys = [];
|
|
1897
1914
|
for (let i = 0; i < r.rows.length; i++) keys.push(r.rows[i][0]);
|
|
1898
1915
|
return { ok: true, keys };
|
package/src/wrap.ts
CHANGED
|
@@ -177,6 +177,13 @@ export function wrapNativeFetchHandler(
|
|
|
177
177
|
assets?: { manifest: unknown; files: Record<string, string> },
|
|
178
178
|
/** The selected transport determines whether the direct-transfer C ABI is real or stubbed. */
|
|
179
179
|
transport: Transport = "broker",
|
|
180
|
+
/**
|
|
181
|
+
* #126 — `env.<binding> = { id, tag, timestamp }`. Baked at build time from the
|
|
182
|
+
* artifact digest, like `vars`: the control plane's own deployment id is a
|
|
183
|
+
* separate, later-assigned thing (minted at upload), so this is the artifact's
|
|
184
|
+
* own self-describing identity rather than a broker round trip.
|
|
185
|
+
*/
|
|
186
|
+
versionMetadata?: { binding: string; id: string; tag: string; timestamp: string },
|
|
180
187
|
): string {
|
|
181
188
|
const neutralised = neutraliseExports(source);
|
|
182
189
|
if (neutralised === null || !/\bfetch\s*\(/.test(source)) {
|
|
@@ -184,6 +191,13 @@ export function wrapNativeFetchHandler(
|
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
const env = `const env = ${JSON.stringify(vars)};\nglobalThis.env = env;\n`;
|
|
194
|
+
const versionMetadataLine = versionMetadata
|
|
195
|
+
? `env[${JSON.stringify(versionMetadata.binding)}] = ${JSON.stringify({
|
|
196
|
+
id: versionMetadata.id,
|
|
197
|
+
tag: versionMetadata.tag,
|
|
198
|
+
timestamp: versionMetadata.timestamp,
|
|
199
|
+
})};\n`
|
|
200
|
+
: "";
|
|
187
201
|
// Baked, not a binding: the date belongs to the artifact, and a handler must
|
|
188
202
|
// not be able to change the semantics it was compiled against at runtime.
|
|
189
203
|
const compat =
|
|
@@ -194,14 +208,13 @@ export function wrapNativeFetchHandler(
|
|
|
194
208
|
`globalThis.__sbOutbound = ${JSON.stringify(bindings.outbound)};\n` +
|
|
195
209
|
(assets ? `globalThis.__sbAssets = ${JSON.stringify(assets)};\n` : "");
|
|
196
210
|
// The native transfer hook runs before JavaScript receives its Request. It
|
|
197
|
-
//
|
|
198
|
-
//
|
|
211
|
+
// has a real file-backed implementation only in embedded builds. The broker
|
|
212
|
+
// transport supplies ABI-compatible stubs because its separate transfer
|
|
213
|
+
// listener owns these URLs, while the shared uWebSockets patch is global.
|
|
199
214
|
const nativeTransferAbi =
|
|
200
|
-
|
|
201
|
-
?
|
|
202
|
-
:
|
|
203
|
-
? `Porffor.c\`const char* sb_standalone_app_name(void) { return ${JSON.stringify(appName)}; }\`;\n`
|
|
204
|
-
: '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; } const char* sb_r2_transfer_download_etag(sb_r2_transfer_ctx* a) { return ""; } 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';
|
|
215
|
+
transport === "embedded"
|
|
216
|
+
? `Porffor.c\`const char* sb_standalone_app_name(void) { return ${JSON.stringify(appName)}; }\`;\n`
|
|
217
|
+
: "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; } const char* sb_r2_transfer_download_etag(sb_r2_transfer_ctx* a) { return \"\"; } 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";
|
|
205
218
|
const wire = hasBindings(bindings)
|
|
206
219
|
? `__sbInstallBindings(env, ${JSON.stringify(bindings)});\n`
|
|
207
220
|
: "";
|
|
@@ -216,7 +229,7 @@ export function wrapNativeFetchHandler(
|
|
|
216
229
|
: "";
|
|
217
230
|
|
|
218
231
|
return (
|
|
219
|
-
`${prelude}\n${nativeTransferAbi}${compat}${env}${wire}` +
|
|
232
|
+
`${prelude}\n${nativeTransferAbi}${compat}${env}${versionMetadataLine}${wire}` +
|
|
220
233
|
`${neutralised}\n` +
|
|
221
234
|
`${registerDO}${triggers}` +
|
|
222
235
|
`export default {\n port: ${port},\n fetch(request) { return __sbEntry(__sbHandlers, request); }\n};\n`
|
package/src/wrap.test.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
import { EMPTY_BINDINGS, wrapNativeFetchHandler } from "./wrap";
|
|
3
|
-
|
|
4
|
-
const handler = "export default { fetch() { return new Response('ok'); } };";
|
|
5
|
-
|
|
6
|
-
test("direct R2 transfer ABI is emitted only for an R2 binding", () => {
|
|
7
|
-
const empty = wrapNativeFetchHandler(handler, "", {}, EMPTY_BINDINGS);
|
|
8
|
-
expect(empty).not.toContain("sb_r2_transfer_open");
|
|
9
|
-
|
|
10
|
-
const r2 = wrapNativeFetchHandler(handler, "", {}, { ...EMPTY_BINDINGS, r2: ["OBJECTS"] });
|
|
11
|
-
expect(r2).toContain("sb_r2_transfer_open");
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("scrypt remains available without a capability declaration", () => {
|
|
15
|
-
const wrapped = wrapNativeFetchHandler(handler, "sb_scrypt", {}, EMPTY_BINDINGS);
|
|
16
|
-
expect(wrapped).toContain("sb_scrypt");
|
|
17
|
-
});
|