@sproutboat/runtime 0.9.6 → 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 +7 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +5 -2
- package/src/transport-embedded.js +30 -11
- package/src/wrap.ts +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.9.6
|
|
4
11
|
|
|
5
12
|
### 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) });
|
|
@@ -1779,8 +1779,13 @@ function __sbEnsureSchema() {
|
|
|
1779
1779
|
const s = __sbStore();
|
|
1780
1780
|
__sbSql(
|
|
1781
1781
|
s,
|
|
1782
|
-
"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))",
|
|
1783
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", []);
|
|
1784
1789
|
// #56 — no `body` column: an object's bytes live in their own file under
|
|
1785
1790
|
// "<data-dir>/r2-blobs/" (sb_r2_put_c/sb_r2_get_c), never in the sqlite row.
|
|
1786
1791
|
__sbSql(
|
|
@@ -1869,16 +1874,29 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1869
1874
|
if (op === "ping") return { ok: true, op: "pong", echo: msg.msg };
|
|
1870
1875
|
|
|
1871
1876
|
if (op === "kv.get") {
|
|
1872
|
-
const r = __sbSql(store, "SELECT value FROM kv WHERE ns = ? AND key = ?", [msg.ns, msg.key]);
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
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] };
|
|
1876
1885
|
}
|
|
1877
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
|
+
}
|
|
1878
1896
|
__sbSql(
|
|
1879
1897
|
store,
|
|
1880
|
-
"INSERT INTO kv (ns, key, value) VALUES (?1,?2,?3) ON CONFLICT (ns, key) DO UPDATE SET value = ?3",
|
|
1881
|
-
[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],
|
|
1882
1900
|
);
|
|
1883
1901
|
return { ok: true };
|
|
1884
1902
|
}
|
|
@@ -1887,10 +1905,11 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1887
1905
|
return { ok: true };
|
|
1888
1906
|
}
|
|
1889
1907
|
if (op === "kv.list") {
|
|
1890
|
-
const r = __sbSql(
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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
|
+
);
|
|
1894
1913
|
const keys = [];
|
|
1895
1914
|
for (let i = 0; i < r.rows.length; i++) keys.push(r.rows[i][0]);
|
|
1896
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 =
|
|
@@ -215,7 +229,7 @@ export function wrapNativeFetchHandler(
|
|
|
215
229
|
: "";
|
|
216
230
|
|
|
217
231
|
return (
|
|
218
|
-
`${prelude}\n${nativeTransferAbi}${compat}${env}${wire}` +
|
|
232
|
+
`${prelude}\n${nativeTransferAbi}${compat}${env}${versionMetadataLine}${wire}` +
|
|
219
233
|
`${neutralised}\n` +
|
|
220
234
|
`${registerDO}${triggers}` +
|
|
221
235
|
`export default {\n port: ${port},\n fetch(request) { return __sbEntry(__sbHandlers, request); }\n};\n`
|