From 4615ca2e9edac25573ffb25ccead84e9f5ce94e8 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Mon, 14 Sep 2026 15:20:10 +0300 Subject: [PATCH 1/5] feat: add ProxyWasm cache module and CDN example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the cache-sync WIT interface for ProxyWasm/CDN apps, mirroring the existing key_value module. Free functions rather than a handle-based Store, since the cache has no named stores — every operation is scoped to the calling application and addressed by key alone. - src/proxywasm/cache.rs: get, set, delete, exists, incr, expire, purge, purge_prefix, with an Error enum matching cache-types.error - src/proxywasm/mod.rs: eight proxy_cache_* FFI declarations - examples/cdn/cache: CDN example exercising all operations via query parameters, plus debugger fixtures - docs: HOST_SDK_CONTRACT FFI table, examples README, plugin manifest entries, CHANGELOG decision log The proxy_cache_* symbol names and signatures follow the proxy_kv_store_* convention and still need confirming against the host implementation. The example uses a path dependency until proxywasm::cache is published. --- context/CHANGELOG.md | 26 + context/architecture/HOST_SDK_CONTRACT.md | 16 + context/reference/ERROR_CODES.md | 21 +- examples/README.md | 1 + examples/cdn/cache/Cargo.lock | 549 ++++++++++++++++++ examples/cdn/cache/Cargo.toml | 15 + examples/cdn/cache/README.md | 44 ++ .../cdn/cache/fixtures/cache-get.live.json | 8 + .../cdn/cache/fixtures/cache-get.test.json | 26 + .../cdn/cache/fixtures/cache-incr.live.json | 8 + .../cdn/cache/fixtures/cache-incr.test.json | 26 + .../cdn/cache/fixtures/cache-set.live.json | 8 + .../cdn/cache/fixtures/cache-set.test.json | 26 + .../cache/fixtures/invalid-action.live.json | 8 + .../cache/fixtures/invalid-action.test.json | 26 + .../cache/fixtures/no-query-params.live.json | 8 + .../cache/fixtures/no-query-params.test.json | 23 + examples/cdn/cache/src/lib.rs | 251 ++++++++ fastedge-plugin-source/manifest.json | 28 + src/proxywasm/cache.rs | 240 ++++++++ src/proxywasm/mod.rs | 49 +- 21 files changed, 1403 insertions(+), 4 deletions(-) create mode 100644 examples/cdn/cache/Cargo.lock create mode 100644 examples/cdn/cache/Cargo.toml create mode 100644 examples/cdn/cache/README.md create mode 100644 examples/cdn/cache/fixtures/cache-get.live.json create mode 100644 examples/cdn/cache/fixtures/cache-get.test.json create mode 100644 examples/cdn/cache/fixtures/cache-incr.live.json create mode 100644 examples/cdn/cache/fixtures/cache-incr.test.json create mode 100644 examples/cdn/cache/fixtures/cache-set.live.json create mode 100644 examples/cdn/cache/fixtures/cache-set.test.json create mode 100644 examples/cdn/cache/fixtures/invalid-action.live.json create mode 100644 examples/cdn/cache/fixtures/invalid-action.test.json create mode 100644 examples/cdn/cache/fixtures/no-query-params.live.json create mode 100644 examples/cdn/cache/fixtures/no-query-params.test.json create mode 100644 examples/cdn/cache/src/lib.rs create mode 100644 src/proxywasm/cache.rs diff --git a/context/CHANGELOG.md b/context/CHANGELOG.md index e0b643a..a094ac1 100644 --- a/context/CHANGELOG.md +++ b/context/CHANGELOG.md @@ -4,6 +4,32 @@ This file tracks agent decisions, architectural changes, and context for future --- +## [2026-09-14] — ProxyWasm cache module + +### Overview +Added `fastedge::proxywasm::cache` — the ProxyWasm/CDN counterpart of the `cache-sync` WIT interface already exposed to HTTP apps as `fastedge::cache`. + +### Decisions +- Free functions rather than a handle-based `Store` (as in `key_value`): the `cache-sync` WIT interface has no named stores or handles, every operation is scoped to the calling app and addressed by key alone. +- `Error` mirrors the WIT `cache-types.error` variant (`AccessDenied`, `InternalError`, `Other(String)`) instead of reusing `key_value::Error` (which carries `NoSuchStore`). +- `option` TTL is carried over FFI as a plain `u64` with `0` meaning "no expiry", avoiding an extra flag/pointer parameter for a value that has no meaningful zero case. +- Host status codes follow the real ProxyWasm status enum (`0` ok, `1` not found, `2` bad argument — which also carries access denial, as in `key_value` — `10` internal failure); "not found" is folded into `Ok(None)` / `Ok(false)` / no-op per the WIT contract. Note `reference/ERROR_CODES.md` previously listed `3`/`6` for these, which does not match the host enum; corrected in the same change. + +### Changes +- `src/proxywasm/cache.rs` — new module: `get`, `set`, `delete`, `exists`, `incr`, `expire`, `purge`, `purge_prefix` +- `src/proxywasm/mod.rs` — `pub mod cache`, eight `proxy_cache_*` FFI declarations, module docs +- `context/architecture/HOST_SDK_CONTRACT.md` — documented the `proxy_cache_*` FFI functions +- `examples/cdn/cache/` — new CDN example exercising all eight operations via query parameters, modelled on `examples/cdn/key_value/` (no `store` param — the cache has no named stores) +- `examples/README.md` — listed the new CDN example +- `fastedge-plugin-source/manifest.json` — `cdn-cache-blueprint` / `cdn-cache-pattern` source + target entries + +### Follow-up +- The `proxy_cache_*` symbol names and signatures were derived from the existing `proxy_kv_store_*` convention — they must be confirmed against the host implementation before release. +- `examples/cdn/cache/Cargo.toml` uses a path dependency (`fastedge = { path = "../../.." }`) because `proxywasm::cache` is not in the published 0.4.2 crate. Every other example uses the registry dep — switch this one to `{ version = "0.4", features = ["proxywasm"] }` once the module is published. +- Per `context/PLUGIN_CONTRACT.md` steps 3-4, the `fastedge-plugin` repo still needs intent files (`cdn/cache-rust.md`, `cdn/examples-cache-rust.md`) and placeholder reference files at the mapped target paths, or the next sync will fail for the two new manifest entries. + +--- + ## [2026-04-07] — Migrated Rust examples from FastEdge-examples ### Overview diff --git a/context/architecture/HOST_SDK_CONTRACT.md b/context/architecture/HOST_SDK_CONTRACT.md index de1c061..2bebd4a 100644 --- a/context/architecture/HOST_SDK_CONTRACT.md +++ b/context/architecture/HOST_SDK_CONTRACT.md @@ -52,6 +52,21 @@ These are the `extern "C"` functions the host makes available to WASM modules. T | `proxy_kv_store_zscan(handle, key, len, pattern, plen, ret, ret_len)` | `Store::zscan(key, pattern)` | Sorted set pattern scan | | `proxy_kv_store_bf_exists(handle, key, len, item, ilen, ret)` | `Store::bf_exists(key, item)` | Bloom filter membership check | +### Cache + +| FFI Function | SDK Wrapper | Purpose | +|-------------|-------------|---------| +| `proxy_cache_get(key, len, ret, ret_len)` | `cache::get(key)` | Retrieve cached value by key | +| `proxy_cache_set(key, len, value, vlen, ttl_ms)` | `cache::set(key, value, ttl)` | Store value; `ttl_ms = 0` means no expiry | +| `proxy_cache_delete(key, len)` | `cache::delete(key)` | Delete a cached key (no-op if absent) | +| `proxy_cache_exists(key, len, ret)` | `cache::exists(key)` | Key membership check | +| `proxy_cache_incr(key, len, delta, ret)` | `cache::incr(key, delta)` | Atomic integer increment/decrement | +| `proxy_cache_expire(key, len, ttl_ms, ret)` | `cache::expire(key, ttl)` | Set/update key expiry | +| `proxy_cache_purge(ret)` | `cache::purge()` | Delete all of the app's cached keys | +| `proxy_cache_purge_prefix(prefix, len, ret)` | `cache::purge_prefix(prefix)` | Delete the app's keys matching a prefix | + +The cache has no handle — every operation is scoped to the calling application and addressed by key alone. Mirrors the `cache-sync` WIT interface used by HTTP apps. + ### Secrets | FFI Function | SDK Wrapper | Purpose | @@ -86,6 +101,7 @@ For the WIT-based Component Model path, the same capabilities are exposed as typ The WIT world (`gcore:fastedge/reactor`) imports: - `http` + `http-client` — request/response types and outbound HTTP - `key-value` — persistent storage (same operations as FFI above) +- `cache-sync` — ephemeral cache (same operations as FFI above) - `secret` — encrypted secrets (same operations as FFI above) - `dictionary` — read-only config (same as FFI above) - `utils` — diagnostics (same as FFI above) diff --git a/context/reference/ERROR_CODES.md b/context/reference/ERROR_CODES.md index 290bcc2..355647d 100644 --- a/context/reference/ERROR_CODES.md +++ b/context/reference/ERROR_CODES.md @@ -71,6 +71,14 @@ These typically surface when: | `AccessDenied` | App doesn't have permission to access this store | | `InternalError` | Platform-side storage error | +### Cache (`cache::Error`) + +| Variant | Meaning | +|---------|---------| +| `AccessDenied` | App doesn't have permission to use the cache — check that the app's cache mode is enabled | +| `InternalError` | Platform-side cache error | +| `Other(String)` | Unrecognized host status code | + ### Secrets (`secret::Error`) | Variant | Meaning | @@ -89,9 +97,16 @@ The `proxy_*` FFI functions return `u32` status codes: |-------|---------| | `0` | Success | | `1` | Not found (key doesn't exist) | -| `2` | Bad argument | -| `3` | Not allowed | -| `6` | Internal failure | +| `2` | Bad argument — also how the host reports access denial | +| `3` | Serialization failure | +| `4` | Parse failure | +| `6` | Invalid memory access | +| `7` | Empty | +| `8` | CAS mismatch | +| `10` | Internal failure | +| `12` | Unimplemented | + +These are the values of the host's `ProxyStatus` enum (`fastedge_proxywasm::v2::ProxyStatus`) — the authoritative list lives there, not here. The SDK's ProxyWasm wrappers in `src/proxywasm/` translate these into Rust `Result` types — application code doesn't see raw status codes. diff --git a/examples/README.md b/examples/README.md index 4660737..2de79b6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -76,6 +76,7 @@ Examples are organized into three categories: | [custom](./cdn/custom/) | Return HTTP status codes based on request path with optional delay | | [http_call](./cdn/http_call/) | Make asynchronous HTTP calls to external services | | [key_value](./cdn/key_value/) | KV store operations via query parameters | +| [cache](./cdn/cache/) | Cache operations via query parameters — get, set, incr, expire, purge | | [geo_redirect](./cdn/geo_redirect/) | Route requests to country-specific origins based on geoIP | | [large_env_variable](./cdn/large_env_variable/) | Read large (> 64KB) environment variables using the dictionary API | | [jwt](./cdn/jwt/) | Validate JWT tokens on incoming requests (signature and expiration) | diff --git a/examples/cdn/cache/Cargo.lock b/examples/cdn/cache/Cargo.lock new file mode 100644 index 0000000..2129976 --- /dev/null +++ b/examples/cdn/cache/Cargo.lock @@ -0,0 +1,549 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cache" +version = "0.1.0" +dependencies = [ + "fastedge", + "proxy-wasm", + "querystring", + "serde_json", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastedge" +version = "0.4.2" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen", +] + +[[package]] +name = "fastedge-derive" +version = "0.4.2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proxy-wasm" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de8f6564bd52c2f4ff79fa5d1bd3bc10d8f822162af8d527e121e46703496aa0" +dependencies = [ + "hashbrown 0.16.1", + "log", +] + +[[package]] +name = "querystring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9318ead08c799aad12a55a3e78b82e0b6167271ffd1f627b758891282f739187" + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.119", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/cdn/cache/Cargo.toml b/examples/cdn/cache/Cargo.toml new file mode 100644 index 0000000..880c44e --- /dev/null +++ b/examples/cdn/cache/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] + +[package] +name = "cache" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +fastedge = { path = "../../..", features = ["proxywasm"] } +querystring = "1.1" +serde_json = "1" diff --git a/examples/cdn/cache/README.md b/examples/cdn/cache/README.md new file mode 100644 index 0000000..d480cec --- /dev/null +++ b/examples/cdn/cache/README.md @@ -0,0 +1,44 @@ +[← Back to examples](../../README.md) + +# Cache (CDN) + +Implements cache operations via query parameters — get, set, delete, exists, incr, expire, purge, and purgePrefix — using the proxy-wasm ABI. + +Unlike the [key_value](../key_value/) example there is no `store` parameter: the cache has no named stores or handles, so every operation is scoped to the calling application and addressed by key alone. + +## Usage + +| Query | Operation | +|---|---| +| `?action=get&key=` | Read a value. `response` is `null` when the key is absent. | +| `?action=set&key=&value=&ttl=` | Store a value. Omit `ttl` for no expiry. | +| `?action=delete&key=` | Delete a key. No-op when the key is absent. | +| `?action=exists&key=` | Key membership check. | +| `?action=incr&key=&delta=` | Atomic increment. `delta` may be negative; a missing key starts at `0`. | +| `?action=expire&key=&ttl=` | Set or update a key's expiry. `response` is `false` when the key is absent. | +| `?action=purge` | Delete every key owned by this app; returns the number deleted. | +| `?action=purgePrefix&prefix=` | Delete this app's keys starting with `prefix`; returns the number deleted. | + +Defaults to `action=get` when `action` is omitted. All responses are JSON; errors return status 500 with `{"error": "..."}`. + +```sh +curl 'https:///?action=set&key=hits&value=0&ttl=60000' +curl 'https:///?action=incr&key=hits&delta=1' +curl 'https:///?action=get&key=hits' +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/cache.wasm +``` + +## APIs used + +- `fastedge::proxywasm::cache::get(key)` — retrieve cached bytes by key; returns `Ok(Option>)` +- `fastedge::proxywasm::cache::set(key, bytes, ttl_ms)` — store bytes with optional TTL in milliseconds; `None` means no expiry +- `fastedge::proxywasm::cache::delete(key)` / `exists(key)` — remove a key, or test for its presence +- `fastedge::proxywasm::cache::incr(key, delta)` — atomic counter update, returns the new value +- `fastedge::proxywasm::cache::expire(key, ttl_ms)` — re-arm a key's expiry, returns whether the key existed +- `fastedge::proxywasm::cache::purge()` / `purge_prefix(prefix)` — bulk delete, returns the number of keys removed diff --git a/examples/cdn/cache/fixtures/cache-get.live.json b/examples/cdn/cache/fixtures/cache-get.live.json new file mode 100644 index 0000000..1451036 --- /dev/null +++ b/examples/cdn/cache/fixtures/cache-get.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "bodyContains": "mykey", + "headers": { + "content-type": "application/json" + } + } +} diff --git a/examples/cdn/cache/fixtures/cache-get.test.json b/examples/cdn/cache/fixtures/cache-get.test.json new file mode 100644 index 0000000..483b057 --- /dev/null +++ b/examples/cdn/cache/fixtures/cache-get.test.json @@ -0,0 +1,26 @@ +{ + "appType": "proxy-wasm", + "description": "Cache get operation — calls proxy_cache_get host function (runner gap expected)", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com" + }, + "body": "" + }, + "properties": { + "request.query": "action=get&key=mykey" + }, + "response": { + "headers": { + "content-type": "text/plain" + }, + "body": "upstream response" + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/cache/fixtures/cache-incr.live.json b/examples/cdn/cache/fixtures/cache-incr.live.json new file mode 100644 index 0000000..0a2755e --- /dev/null +++ b/examples/cdn/cache/fixtures/cache-incr.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "bodyContains": "hits", + "headers": { + "content-type": "application/json" + } + } +} diff --git a/examples/cdn/cache/fixtures/cache-incr.test.json b/examples/cdn/cache/fixtures/cache-incr.test.json new file mode 100644 index 0000000..2b5e83e --- /dev/null +++ b/examples/cdn/cache/fixtures/cache-incr.test.json @@ -0,0 +1,26 @@ +{ + "appType": "proxy-wasm", + "description": "Cache incr operation — calls proxy_cache_incr host function (runner gap expected)", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com" + }, + "body": "" + }, + "properties": { + "request.query": "action=incr&key=hits&delta=1" + }, + "response": { + "headers": { + "content-type": "text/plain" + }, + "body": "upstream response" + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/cache/fixtures/cache-set.live.json b/examples/cdn/cache/fixtures/cache-set.live.json new file mode 100644 index 0000000..7509885 --- /dev/null +++ b/examples/cdn/cache/fixtures/cache-set.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "bodyContains": "myvalue", + "headers": { + "content-type": "application/json" + } + } +} diff --git a/examples/cdn/cache/fixtures/cache-set.test.json b/examples/cdn/cache/fixtures/cache-set.test.json new file mode 100644 index 0000000..d796fed --- /dev/null +++ b/examples/cdn/cache/fixtures/cache-set.test.json @@ -0,0 +1,26 @@ +{ + "appType": "proxy-wasm", + "description": "Cache set operation with TTL — calls proxy_cache_set host function (runner gap expected)", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com" + }, + "body": "" + }, + "properties": { + "request.query": "action=set&key=mykey&value=myvalue&ttl=60000" + }, + "response": { + "headers": { + "content-type": "text/plain" + }, + "body": "upstream response" + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/cache/fixtures/invalid-action.live.json b/examples/cdn/cache/fixtures/invalid-action.live.json new file mode 100644 index 0000000..1c2fd00 --- /dev/null +++ b/examples/cdn/cache/fixtures/invalid-action.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "bodyContains": "Invalid action 'bogus'", + "headers": { + "content-type": "application/json" + } + } +} diff --git a/examples/cdn/cache/fixtures/invalid-action.test.json b/examples/cdn/cache/fixtures/invalid-action.test.json new file mode 100644 index 0000000..8ee7d24 --- /dev/null +++ b/examples/cdn/cache/fixtures/invalid-action.test.json @@ -0,0 +1,26 @@ +{ + "appType": "proxy-wasm", + "description": "Unsupported action — returns error listing supported actions", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com" + }, + "body": "" + }, + "properties": { + "request.query": "action=bogus&key=mykey" + }, + "response": { + "headers": { + "content-type": "text/plain" + }, + "body": "upstream response" + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/cache/fixtures/no-query-params.live.json b/examples/cdn/cache/fixtures/no-query-params.live.json new file mode 100644 index 0000000..b47bbb8 --- /dev/null +++ b/examples/cdn/cache/fixtures/no-query-params.live.json @@ -0,0 +1,8 @@ +{ + "expected": { + "bodyContains": "App must be called with query parameters", + "headers": { + "content-type": "application/json" + } + } +} diff --git a/examples/cdn/cache/fixtures/no-query-params.test.json b/examples/cdn/cache/fixtures/no-query-params.test.json new file mode 100644 index 0000000..0741519 --- /dev/null +++ b/examples/cdn/cache/fixtures/no-query-params.test.json @@ -0,0 +1,23 @@ +{ + "appType": "proxy-wasm", + "description": "No query parameters — returns error about missing query params", + "request": { + "method": "GET", + "url": "http://fastedge-builtin.debug", + "headers": { + "host": "example.com" + }, + "body": "" + }, + "response": { + "headers": { + "content-type": "text/plain" + }, + "body": "upstream response" + }, + "logLevel": 2, + "wasm": { + "path": "/.fastedge-debug/app.wasm", + "description": "Default debugger WASM binary" + } +} diff --git a/examples/cdn/cache/src/lib.rs b/examples/cdn/cache/src/lib.rs new file mode 100644 index 0000000..95a7c8d --- /dev/null +++ b/examples/cdn/cache/src/lib.rs @@ -0,0 +1,251 @@ +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating cache operations via the proxy-wasm interface. + +Unlike the KV store, the cache has no named stores or handles — every operation is +scoped to the calling application and addressed by key alone. + +Supports all cache operations via query parameters: + ?action=get&key= + ?action=set&key=&value=[&ttl=] + ?action=delete&key= + ?action=exists&key= + ?action=incr&key=&delta= + ?action=expire&key=&ttl= + ?action=purge + ?action=purgePrefix&prefix= + +Defaults to action=get if not specified. Omitting `ttl` on `set` stores the value +with no expiry. +*/ + +use fastedge::proxywasm::cache; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use serde_json::json; +use std::collections::HashMap; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { Box::new(CacheRoot) }); +}} + +struct CacheRoot; + +impl Context for CacheRoot {} + +impl RootContext for CacheRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(CacheContext)) + } +} + +struct CacheContext; + +impl Context for CacheContext {} + +impl HttpContext for CacheContext { + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + // Remove content-length since we replace the body + self.set_http_response_header("content-length", None); + self.set_http_response_header("content-type", Some("application/json")); + self.set_http_response_header("transfer-encoding", Some("chunked")); + Action::Continue + } + + fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + if !end_of_stream { + return Action::Pause; + } + + let query = self + .get_property(vec!["request", "query"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_default(); + + if query.is_empty() { + self.send_error("App must be called with query parameters", body_size); + return Action::Continue; + } + + let params: HashMap<&str, &str> = querystring::querify(&query).into_iter().collect(); + + let action = params.get("action").copied().unwrap_or("get"); + + let result = match action { + "get" => self.handle_get(¶ms), + "set" => self.handle_set(¶ms), + "delete" => self.handle_delete(¶ms), + "exists" => self.handle_exists(¶ms), + "incr" => self.handle_incr(¶ms), + "expire" => self.handle_expire(¶ms), + "purge" => self.handle_purge(), + "purgePrefix" => self.handle_purge_prefix(¶ms), + _ => Err(format!( + "Invalid action '{}'. Supported: get, set, delete, exists, incr, expire, purge, purgePrefix", + action + )), + }; + + let body = match result { + Ok(json) => json, + Err(msg) => { + self.send_error(&msg, body_size); + return Action::Continue; + } + }; + + self.set_http_response_body(0, body_size, body.as_bytes()); + + Action::Continue + } +} + +impl CacheContext { + fn handle_get(&self, params: &HashMap<&str, &str>) -> Result { + let key = *params.get("key").ok_or("Missing required param 'key' for 'get' action")?; + match cache::get(key) { + Ok(Some(value)) => { + let value_str = String::from_utf8_lossy(&value); + Ok(json!({ + "action": "get", + "key": key, + "response": value_str.as_ref() + }).to_string()) + } + Ok(None) => Ok(json!({ + "action": "get", + "key": key, + "response": null + }).to_string()), + Err(e) => Err(format!("Cache get error: {}", e)), + } + } + + fn handle_set(&self, params: &HashMap<&str, &str>) -> Result { + let key = *params.get("key").ok_or("Missing required param 'key' for 'set' action")?; + let value = *params.get("value").ok_or("Missing required param 'value' for 'set' action")?; + // no 'ttl' param means no expiry + let ttl_ms = match params.get("ttl") { + Some(ttl) => Some( + ttl.parse::() + .map_err(|_| "Invalid 'ttl' value: must be a positive number of milliseconds".to_string())?, + ), + None => None, + }; + + match cache::set(key, value.as_bytes(), ttl_ms) { + Ok(()) => Ok(json!({ + "action": "set", + "key": key, + "value": value, + "ttlMs": ttl_ms, + "response": true + }).to_string()), + Err(e) => Err(format!("Cache set error: {}", e)), + } + } + + fn handle_delete(&self, params: &HashMap<&str, &str>) -> Result { + let key = *params.get("key").ok_or("Missing required param 'key' for 'delete' action")?; + match cache::delete(key) { + Ok(()) => Ok(json!({ + "action": "delete", + "key": key, + "response": true + }).to_string()), + Err(e) => Err(format!("Cache delete error: {}", e)), + } + } + + fn handle_exists(&self, params: &HashMap<&str, &str>) -> Result { + let key = *params.get("key").ok_or("Missing required param 'key' for 'exists' action")?; + match cache::exists(key) { + Ok(exists) => Ok(json!({ + "action": "exists", + "key": key, + "response": exists + }).to_string()), + Err(e) => Err(format!("Cache exists error: {}", e)), + } + } + + fn handle_incr(&self, params: &HashMap<&str, &str>) -> Result { + let key = *params.get("key").ok_or("Missing required param 'key' for 'incr' action")?; + let delta: i64 = params + .get("delta") + .ok_or("Missing required param 'delta' for 'incr' action")? + .parse() + .map_err(|_| "Invalid 'delta' value: must be an integer".to_string())?; + + match cache::incr(key, delta) { + Ok(value) => Ok(json!({ + "action": "incr", + "key": key, + "delta": delta, + "response": value + }).to_string()), + Err(e) => Err(format!("Cache incr error: {}", e)), + } + } + + fn handle_expire(&self, params: &HashMap<&str, &str>) -> Result { + let key = *params.get("key").ok_or("Missing required param 'key' for 'expire' action")?; + let ttl_ms: u64 = params + .get("ttl") + .ok_or("Missing required param 'ttl' for 'expire' action")? + .parse() + .map_err(|_| "Invalid 'ttl' value: must be a positive number of milliseconds".to_string())?; + + match cache::expire(key, ttl_ms) { + Ok(updated) => Ok(json!({ + "action": "expire", + "key": key, + "ttlMs": ttl_ms, + "response": updated + }).to_string()), + Err(e) => Err(format!("Cache expire error: {}", e)), + } + } + + fn handle_purge(&self) -> Result { + match cache::purge() { + Ok(deleted) => Ok(json!({ + "action": "purge", + "response": deleted + }).to_string()), + Err(e) => Err(format!("Cache purge error: {}", e)), + } + } + + fn handle_purge_prefix(&self, params: &HashMap<&str, &str>) -> Result { + let prefix = *params + .get("prefix") + .ok_or("Missing required param 'prefix' for 'purgePrefix' action")?; + + match cache::purge_prefix(prefix) { + Ok(deleted) => Ok(json!({ + "action": "purgePrefix", + "prefix": prefix, + "response": deleted + }).to_string()), + Err(e) => Err(format!("Cache purgePrefix error: {}", e)), + } + } + + fn send_error(&self, msg: &str, body_size: usize) { + println!("{}", msg); + self.set_property( + vec!["response", "status"], + Some(b"500"), + ); + let error_body = json!({"error": msg}).to_string(); + self.set_http_response_body(0, body_size, error_body.as_bytes()); + } +} diff --git a/fastedge-plugin-source/manifest.json b/fastedge-plugin-source/manifest.json index 3cbfb34..2285096 100644 --- a/fastedge-plugin-source/manifest.json +++ b/fastedge-plugin-source/manifest.json @@ -216,6 +216,25 @@ "description": "CDN KV Store example — reference pattern (docs)" }, + "cdn-cache-blueprint": { + "files": [ + "examples/cdn/cache/src/lib.rs", + "examples/cdn/cache/Cargo.toml", + "examples/cdn/cache/README.md" + ], + "required": true, + "description": "CDN Cache example — scaffold blueprint" + }, + "cdn-cache-pattern": { + "files": [ + "examples/cdn/cache/src/lib.rs", + "examples/cdn/cache/Cargo.toml", + "examples/cdn/cache/README.md" + ], + "required": true, + "description": "CDN Cache example — reference pattern (docs)" + }, + "cdn-headers-blueprint": { "files": [ "examples/cdn/headers/src/lib.rs", @@ -909,6 +928,15 @@ "section": null }, + "cdn-cache-blueprint": { + "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/cdn/cache-rust.md", + "section": null + }, + "cdn-cache-pattern": { + "reference_file": "plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-rust.md", + "section": null + }, + "cdn-headers-blueprint": { "reference_file": "plugins/gcore-fastedge/skills/scaffold/reference/cdn/headers-rust.md", "section": null diff --git a/src/proxywasm/cache.rs b/src/proxywasm/cache.rs new file mode 100644 index 0000000..f191542 --- /dev/null +++ b/src/proxywasm/cache.rs @@ -0,0 +1,240 @@ +//! FastEdge cache storage (ProxyWasm API). +//! +//! This module provides an interface for the ephemeral cache, which is implemented by the host. +//! It is the ProxyWasm counterpart of the Component Model [`fastedge::cache`](crate::cache) +//! module and mirrors the `cache-sync` WIT interface. +//! +//! An example of using the FastEdge cache looks like: +//! +//! ``` +//! use fastedge::proxywasm::cache; +//! use proxy_wasm::traits::*; +//! use proxy_wasm::types::*; +//! +//! proxy_wasm::main! {{ +//! proxy_wasm::set_log_level(LogLevel::Trace); +//! proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpBodyRoot) }); +//! }} +//! +//! struct HttpBodyRoot; +//! +//! impl Context for HttpBodyRoot {} +//! +//! impl RootContext for HttpBodyRoot { +//! fn get_type(&self) -> Option { +//! Some(ContextType::HttpContext) +//! } +//! +//! fn create_http_context(&self, _: u32) -> Option> { +//! Some(Box::new(HttpBody)) +//! } +//! } +//! +//! struct HttpBody; +//! +//! impl Context for HttpBody {} +//! +//! impl HttpContext for HttpBody { +//! fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { +//! +//! let Ok(cached) = cache::get("key-3338664") else { +//! return Action::Pause; +//! }; +//! +//! if cached.is_none() { +//! // store the value for 5 minutes +//! let _ = cache::set("key-3338664", b"value", Some(300_000)); +//! } +//! +//! Action::Continue +//! } +//! } +//! ``` +//! + +use std::fmt::Display; +use std::ptr::null_mut; + +/// The set of errors which may be raised by functions in this interface +#[derive(Debug, Clone)] +pub enum Error { + /// The requesting component does not have access to the specified cache + /// (which may or may not exist). + AccessDenied, + /// An unexpected internal error occurred. + InternalError, + /// Some implementation-specific error has occurred (e.g. I/O) + Other(String), +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::AccessDenied => write!(f, "access denied"), + Error::InternalError => write!(f, "internal error"), + Error::Other(msg) => write!(f, "other error: {}", msg), + } + } +} + +/// Maps a non-success host status code onto an [`Error`]. +/// +/// Codes follow the ProxyWasm status enum the host reports: `2` (bad argument) +/// carries access denial, as it does for `key_value`, and `10` is an internal +/// failure. +fn error_from_status(status: u32) -> Error { + match status { + 2 => Error::AccessDenied, + 10 => Error::InternalError, + status => Error::Other(format!("unexpected status: {}", status)), + } +} + +/// Get the value associated with `key`. +/// +/// Returns `Ok(None)` if the key does not exist. +pub fn get(key: &str) -> Result>, Error> { + let mut return_data: *mut u8 = null_mut(); + let mut return_size: usize = 0; + + unsafe { + match super::proxy_cache_get( + key.as_ptr(), + key.len(), + &mut return_data, + &mut return_size, + ) { + 0 => { + if !return_data.is_null() { + Ok(Some(Vec::from_raw_parts( + return_data, + return_size, + return_size, + ))) + } else { + Ok(None) + } + } + 1 => Ok(None), + status => Err(error_from_status(status)), + } + } +} + +/// Set the value for `key` with an optional expiry. +/// +/// If the key already exists, its current value is overwritten. +/// If the key does not exist, a new key-value pair is created. +/// +/// `ttl_ms` is the time-to-live in milliseconds. Pass `None` for no expiry. +pub fn set(key: &str, value: &[u8], ttl_ms: Option) -> Result<(), Error> { + unsafe { + // the host treats a zero time-to-live as "no expiry" + match super::proxy_cache_set( + key.as_ptr(), + key.len(), + value.as_ptr(), + value.len(), + ttl_ms.unwrap_or(0), + ) { + 0 => Ok(()), + status => Err(error_from_status(status)), + } + } +} + +/// Delete the key-value pair associated with `key`. +/// +/// If the key does not exist, this operation is a no-op. +pub fn delete(key: &str) -> Result<(), Error> { + unsafe { + match super::proxy_cache_delete(key.as_ptr(), key.len()) { + 0 | 1 => Ok(()), + status => Err(error_from_status(status)), + } + } +} + +/// Check whether `key` exists in the cache. +/// +/// Returns `Ok(true)` if the key exists, `Ok(false)` otherwise. +pub fn exists(key: &str) -> Result { + let mut return_exists: u32 = 0; + + unsafe { + match super::proxy_cache_exists(key.as_ptr(), key.len(), &mut return_exists) { + 0 => Ok(return_exists != 0), + 1 => Ok(false), + status => Err(error_from_status(status)), + } + } +} + +/// Increment the integer value stored at `key` by `delta`. +/// +/// If the key does not exist, it is initialised to `0` before incrementing. +/// The operation is atomic. `delta` may be negative to decrement. +/// +/// Returns the new value after the increment, or an error if the operation fails +/// (for example, if the stored value is not an integer). +pub fn incr(key: &str, delta: i64) -> Result { + let mut return_value: i64 = 0; + + unsafe { + match super::proxy_cache_incr(key.as_ptr(), key.len(), delta, &mut return_value) { + 0 => Ok(return_value), + status => Err(error_from_status(status)), + } + } +} + +/// Set or update the expiry of `key` to `ttl_ms` milliseconds from now. +/// +/// Returns `Ok(false)` if the key does not exist, `Ok(true)` if the expiry was +/// updated successfully. +pub fn expire(key: &str, ttl_ms: u64) -> Result { + let mut return_updated: u32 = 0; + + unsafe { + match super::proxy_cache_expire(key.as_ptr(), key.len(), ttl_ms, &mut return_updated) { + 0 => Ok(return_updated != 0), + 1 => Ok(false), + status => Err(error_from_status(status)), + } + } +} + +/// Purge all cache entries owned by the calling application. +/// +/// The host scans the application's key index, deletes every cached key, +/// and then removes the index itself. +/// +/// Returns the number of keys that were deleted. +pub fn purge() -> Result { + let mut return_count: u64 = 0; + + unsafe { + match super::proxy_cache_purge(&mut return_count) { + 0 => Ok(return_count), + status => Err(error_from_status(status)), + } + } +} + +/// Purge all cache entries whose keys begin with `prefix`. +/// +/// The host scans the application's key index for keys that begin with the +/// given prefix, deletes every matched key, and removes the matched entries +/// from the index (the index itself is kept for any remaining keys). +/// +/// Returns the number of keys that were deleted. +pub fn purge_prefix(prefix: &str) -> Result { + let mut return_count: u64 = 0; + + unsafe { + match super::proxy_cache_purge_prefix(prefix.as_ptr(), prefix.len(), &mut return_count) { + 0 => Ok(return_count), + status => Err(error_from_status(status)), + } + } +} diff --git a/src/proxywasm/mod.rs b/src/proxywasm/mod.rs index af87a6f..1fd5483 100644 --- a/src/proxywasm/mod.rs +++ b/src/proxywasm/mod.rs @@ -2,7 +2,7 @@ //! //! This module provides a ProxyWasm-compatible subset of the FastEdge Component Model APIs //! for applications that need to run in ProxyWasm environment. It -//! currently exposes key-value, secret, dictionary, and related utility operations via +//! currently exposes key-value, cache, secret, dictionary, and related utility operations via //! FFI (Foreign Function Interface) calls. //! //! # Usage @@ -17,6 +17,7 @@ //! # Modules //! //! - [`key_value`]: Key-value storage operations +//! - [`cache`]: Ephemeral cache operations //! - [`secret`]: Secret management //! - [`dictionary`]: Dictionary lookups //! - [`utils`]: Utility functions @@ -28,6 +29,7 @@ //! may lead to undefined behavior. pub mod key_value; +pub mod cache; pub mod secret; pub mod dictionary; pub mod utils; @@ -103,6 +105,51 @@ extern "C" { return_handle: *mut u32, ) -> u32; + fn proxy_cache_get( + key_data: *const u8, + key_size: usize, + return_value_data: *mut *mut u8, + return_value_size: *mut usize, + ) -> u32; + + fn proxy_cache_set( + key_data: *const u8, + key_size: usize, + value_data: *const u8, + value_size: usize, + ttl_ms: u64, + ) -> u32; + + fn proxy_cache_delete(key_data: *const u8, key_size: usize) -> u32; + + fn proxy_cache_exists( + key_data: *const u8, + key_size: usize, + return_exists: *mut u32, + ) -> u32; + + fn proxy_cache_incr( + key_data: *const u8, + key_size: usize, + delta: i64, + return_value: *mut i64, + ) -> u32; + + fn proxy_cache_expire( + key_data: *const u8, + key_size: usize, + ttl_ms: u64, + return_updated: *mut u32, + ) -> u32; + + fn proxy_cache_purge(return_count: *mut u64) -> u32; + + fn proxy_cache_purge_prefix( + prefix_data: *const u8, + prefix_size: usize, + return_count: *mut u64, + ) -> u32; + fn stats_set_user_diag( value_data: *const u8, value_size: usize, From 570e8a271b1cb069e14ef9a8b31da14545217771 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Tue, 15 Sep 2026 15:29:13 +0300 Subject: [PATCH 2/5] fix: handle unrecognized host statuses gracefully in secret module - Replace `panic!` with `Err(status)` for unexpected host statuses in `get` and `set` methods. - Add comments to clarify behavior for unrecognized statuses. --- src/proxywasm/cache.rs | 2 +- src/proxywasm/secret.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/proxywasm/cache.rs b/src/proxywasm/cache.rs index f191542..ce69911 100644 --- a/src/proxywasm/cache.rs +++ b/src/proxywasm/cache.rs @@ -38,7 +38,7 @@ //! fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { //! //! let Ok(cached) = cache::get("key-3338664") else { -//! return Action::Pause; +//! return Action::Pa§use; //! }; //! //! if cached.is_none() { diff --git a/src/proxywasm/secret.rs b/src/proxywasm/secret.rs index d7f2846..9b8c62b 100644 --- a/src/proxywasm/secret.rs +++ b/src/proxywasm/secret.rs @@ -76,7 +76,10 @@ pub fn get(key: &str) -> Result>, u32> { } } 1 => Ok(None), - status => panic!("unexpected status: {}", status), + // Any other status (e.g. 6, invalid memory access) is reported to + // the caller — an unrecognised host status is not a reason to + // abort the guest. + status => Err(status), } } } @@ -136,7 +139,8 @@ pub fn get_effective_at(key: &str, at: u32) -> Result>, u32> { } } 1 => Ok(None), - status => panic!("unexpected status: {}", status), + // See `get`: an unrecognised host status is returned, not panicked on. + status => Err(status), } } } From 24862e4ea8baeacb78515c54751f50b2f8bbe6a2 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Wed, 16 Sep 2026 11:46:47 +0300 Subject: [PATCH 3/5] docs: expand Key Value example with usage, query examples, and response format - Updated README to provide detailed instructions for using the Key Value CDN example. - Added query examples for all supported actions (`get`, `scan`, `zrange`, `zscan`, `bfExists`). - Documented request requirements, response format, and error handling. - Updated `get_property` method for compatibility (`["request.query"]`). --- examples/cdn/cache/src/lib.rs | 2 +- examples/cdn/key_value/README.md | 89 ++++++++++++++++++++++++++++++- examples/cdn/key_value/src/lib.rs | 2 +- 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/examples/cdn/cache/src/lib.rs b/examples/cdn/cache/src/lib.rs index 95a7c8d..7e53f82 100644 --- a/examples/cdn/cache/src/lib.rs +++ b/examples/cdn/cache/src/lib.rs @@ -65,7 +65,7 @@ impl HttpContext for CacheContext { } let query = self - .get_property(vec!["request", "query"]) + .get_property(vec!["request.query"]) .and_then(|bytes| String::from_utf8(bytes).ok()) .unwrap_or_default(); diff --git a/examples/cdn/key_value/README.md b/examples/cdn/key_value/README.md index 365970f..9c34166 100644 --- a/examples/cdn/key_value/README.md +++ b/examples/cdn/key_value/README.md @@ -2,4 +2,91 @@ # Key Value (CDN) -Implements KV store operations via query parameters — get, scan, zrange, zscan, and bfExists — using the proxy-wasm ABI. +This example shows how to read and write data from a FastEdge KV store from a CDN app. +It intercepts the HTTP response, reads the request query string, and executes a KV operation against a named store. + +## What it does + +The app supports the following actions: + +- `get` — fetch one key +- `scan` — list keys matching a pattern +- `zrange` — read sorted-set entries by score range +- `zscan` — list sorted-set entries matching a pattern +- `bfExists` — check whether a Bloom filter contains an item + +The request must include at least: + +- `store=` — KV store name +- `action=` — optional, defaults to `get` + +For each action, the app validates required parameters and returns a JSON response body. + +## Supported query examples + +### Get a key + +```text +?store=my_store&action=get&key=user:42 +``` + +### List keys by pattern + +```text +?store=my_store&action=scan&match=user:* +``` + +### Read a sorted set by score range + +```text +?store=my_store&action=zrange&key=leaderboard&min=0&max=100 +``` + +### Search sorted-set members by pattern + +```text +?store=my_store&action=zscan&key=leaderboard&match=user:* +``` + +### Check a Bloom filter item + +```text +?store=my_store&action=bfExists&key=visitors&item=alice@example.com +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/key_value.wasm +``` + +This example is a CDN app, so it targets `wasm32-wasip1`. + +## Response format + +The app replaces the response body with JSON and sets `content-type: application/json`. +A successful response looks like this: + +```json +{ + "store": "my_store", + "action": "get", + "key": "user:42", + "response": "Alice" +} +``` + +If a required parameter is missing or the KV operation fails, the app responds with an error payload: + +```json +{ + "error": "Missing required param 'key' for 'get' action" +} +``` + +## Notes + +- The app requires query parameters to run. +- If `action` is omitted, it defaults to `get`. +- The code uses `fastedge::proxywasm::key_value::Store` and `proxy_wasm` lifecycle hooks to operate on the KV store. diff --git a/examples/cdn/key_value/src/lib.rs b/examples/cdn/key_value/src/lib.rs index 520b8d6..d15bb2f 100644 --- a/examples/cdn/key_value/src/lib.rs +++ b/examples/cdn/key_value/src/lib.rs @@ -58,7 +58,7 @@ impl HttpContext for KvStoreContext { } let query = self - .get_property(vec!["request", "query"]) + .get_property(vec!["request.query"]) .and_then(|bytes| String::from_utf8(bytes).ok()) .unwrap_or_default(); From ebeb106e5dfddcbc04fd9cb768eef84cb4fb3697 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Wed, 16 Sep 2026 12:23:11 +0300 Subject: [PATCH 4/5] docs: update CDN_APPS documentation with Cache module details - Added documentation for the new `fastedge::proxywasm::cache` module, including function definitions, error handling, and usage examples. - Updated `generate-docs.sh` to include cache module source files in the CDN_APPS docs generation process. - Refined tier descriptions to include cache usage alongside KV, secrets, and dictionary. - Fixed formatting inconsistencies in tables for better readability. --- docs/CDN_APPS.md | 152 +++++++++++++++++++----- fastedge-plugin-source/generate-docs.sh | 2 +- 2 files changed, 120 insertions(+), 34 deletions(-) diff --git a/docs/CDN_APPS.md b/docs/CDN_APPS.md index cb4edb0..3f5169c 100644 --- a/docs/CDN_APPS.md +++ b/docs/CDN_APPS.md @@ -41,7 +41,7 @@ proxy-wasm = "0.2" log = "0.4" ``` -**Tier 2 — CDN app with FastEdge host services** (KV, secrets, dictionary): +**Tier 2 — CDN app with FastEdge host services** (KV, cache, secrets, dictionary): ```toml [package] @@ -201,7 +201,7 @@ Both `Context` and `HttpContext` must be implemented. The `Context` impl can be ### Lifecycle Callbacks | Callback | Phase | Description | -| ---------------------------------------------------------------- | ---------------- | --------------------------------------------------- | +| ----------------------------------------------------------------- | ---------------- | ---------------------------------------------------- | | `on_http_request_headers(num_headers, end_of_stream) -> Action` | Request headers | Inspect or modify request headers before forwarding | | `on_http_request_body(body_size, end_of_stream) -> Action` | Request body | Inspect or modify request body before forwarding | | `on_http_response_headers(num_headers, end_of_stream) -> Action` | Response headers | Inspect or modify response headers from origin | @@ -214,7 +214,7 @@ All callbacks have default no-op implementations. Override only the phases your Every lifecycle callback returns an `Action` that controls what happens next. | Action | Meaning | -| -------------------------------- | -------------------------------------------------------------------------- | +| --------------------------------- | ---------------------------------------------------------------------------- | | `Action::Continue` | Pass the request or response through to the next stage | | `Action::Pause` | Stop processing; used after `send_http_response` to short-circuit origin | | `Action::StopIterationAndBuffer` | Buffer the current body chunk; continue accumulating until `end_of_stream` | @@ -362,23 +362,23 @@ CDN apps access request metadata through `self.get_property(vec![...])`. The ret **Path format:** Always pass the property identifier as a single dotted string in a one-element vec — e.g., `vec!["request.path"]`, `vec!["response.status"]`, `vec!["request.geo.long"]`. Do **not** split on dots (e.g., `vec!["response", "status"]` is incorrect). -| Property | Encoding | Description | -| ---------------------- | --------------------- | -------------------------------------------------------------------------------- | -| `request.path` | UTF-8 string | URL path | -| `request.query` | UTF-8 string | Query string | -| `request.url` | UTF-8 string | Full request URL | -| `request.host` | UTF-8 string | Domain (may have `shield_` prefix on edge shield nodes) | -| `request.scheme` | UTF-8 string | HTTP scheme (from X-Forwarded-Proto) | -| `request.extension` | UTF-8 string | File extension | -| `request.x_real_ip` | UTF-8 string | Client IP address | -| `request.country` | UTF-8 string | 2-letter ISO country code (geo-IP) | -| `request.country.name` | UTF-8 string | Full country name | -| `request.city` | UTF-8 string | City name | -| `request.region` | UTF-8 string | Region/state | -| `request.continent` | UTF-8 string | Continent | -| `request.asn` | UTF-8 string | Autonomous System Number | -| `request.geo.lat` | UTF-8 string | Latitude | -| `request.geo.long` | UTF-8 string | Longitude | +| Property | Encoding | Description | +| ----------------------- | ---------------------- | ------------------------------------------------------------------------------------ | +| `request.path` | UTF-8 string | URL path | +| `request.query` | UTF-8 string | Query string | +| `request.url` | UTF-8 string | Full request URL | +| `request.host` | UTF-8 string | Domain (may have `shield_` prefix on edge shield nodes) | +| `request.scheme` | UTF-8 string | HTTP scheme (from X-Forwarded-Proto) | +| `request.extension` | UTF-8 string | File extension | +| `request.x_real_ip` | UTF-8 string | Client IP address | +| `request.country` | UTF-8 string | 2-letter ISO country code (geo-IP) | +| `request.country.name` | UTF-8 string | Full country name | +| `request.city` | UTF-8 string | City name | +| `request.region` | UTF-8 string | Region/state | +| `request.continent` | UTF-8 string | Continent | +| `request.asn` | UTF-8 string | Autonomous System Number | +| `request.geo.lat` | UTF-8 string | Latitude | +| `request.geo.long` | UTF-8 string | Longitude | | `response.status` | 2-byte big-endian u16 | Response status code (**binary, NOT a string** — decode with `u16::from_be_bytes`) | Most properties are UTF-8 strings decoded with `std::str::from_utf8()`. The `response.status` property is binary-encoded and must be decoded as a big-endian `u16`. Do not use `String::from_utf8` for this property. @@ -440,7 +440,7 @@ pub struct Store { /* ... */ } ``` | Method | Return Type | Description | -| ------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------ | +| --------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------- | | `Store::new()` | `Result` | Open the default store | | `Store::open(name: &str)` | `Result` | Open a named store | | `Store::get(key: &str)` | `Result>, Error>` | Get the value for a key; `None` if key does not exist | @@ -460,7 +460,7 @@ pub enum Error { ``` | Variant | Description | -| --------------- | ----------------------------------------------------------- | +| ----------------- | -------------------------------------------------------------- | | `NoSuchStore` | The store label is not recognized by the host | | `AccessDenied` | The application does not have access to the specified store | | `Other(String)` | An implementation-specific error (e.g., I/O failure) | @@ -513,6 +513,91 @@ impl HttpContext for RateLimitFilter { } ``` +### Cache (`fastedge::proxywasm::cache`) + +Provides ephemeral cache storage, implemented by the host. Unlike `key_value::Store`, cache operations are not scoped to a named store or handle — every function is a free function keyed directly, and every entry is scoped to the calling application. + +```rust,ignore +pub fn get(key: &str) -> Result>, Error> +pub fn set(key: &str, value: &[u8], ttl_ms: Option) -> Result<(), Error> +pub fn delete(key: &str) -> Result<(), Error> +pub fn exists(key: &str) -> Result +pub fn incr(key: &str, delta: i64) -> Result +pub fn expire(key: &str, ttl_ms: u64) -> Result +pub fn purge() -> Result +pub fn purge_prefix(prefix: &str) -> Result +``` + +| Function | Return Type | Description | +| ------------------------------------------------------ | ------------------------------------ | --------------------------------------------------------------------------------- | +| `get(key: &str)` | `Result>, Error>` | Get the value for a key; `None` if the key does not exist | +| `set(key: &str, value: &[u8], ttl_ms: Option)` | `Result<(), Error>` | Set a value with an optional expiry; `None` means no expiry | +| `delete(key: &str)` | `Result<(), Error>` | Delete a key; a no-op if the key does not exist | +| `exists(key: &str)` | `Result` | Test whether a key exists | +| `incr(key: &str, delta: i64)` | `Result` | Atomically increment (or decrement) an integer value; returns the new value | +| `expire(key: &str, ttl_ms: u64)` | `Result` | Set or update a key's expiry; `false` if the key does not exist | +| `purge()` | `Result` | Delete all cache entries owned by the calling application | +| `purge_prefix(prefix: &str)` | `Result` | Delete all cache entries whose key begins with `prefix` | + +`purge()` and `purge_prefix()` both return the number of keys that were deleted. + +#### `Error` + +```rust,ignore +pub enum Error { + AccessDenied, + InternalError, + Other(String), +} +``` + +| Variant | Description | +| ----------------- | -------------------------------------------------------------- | +| `AccessDenied` | The application does not have access to the specified cache | +| `InternalError` | An unexpected internal error occurred | +| `Other(String)` | An implementation-specific error (e.g., I/O failure) | + +#### Example — cache a computed value in the response headers phase + +```rust,no_run +use fastedge::proxywasm::cache; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new(CacheRoot) }); +}} + +struct CacheRoot; +impl Context for CacheRoot {} +impl RootContext for CacheRoot { + fn get_type(&self) -> Option { Some(ContextType::HttpContext) } + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(CacheFilter)) + } +} + +struct CacheFilter; +impl Context for CacheFilter {} + +impl HttpContext for CacheFilter { + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + match cache::get("key-3338664") { + Ok(Some(_cached)) => { + // reuse the cached value + } + Ok(None) => { + // store the value for 5 minutes + let _ = cache::set("key-3338664", b"value", Some(300_000)); + } + Err(_) => {} + } + Action::Continue + } +} +``` + ### Secret Management (`fastedge::proxywasm::secret`) Provides access to encrypted secrets stored in the FastEdge platform. @@ -523,7 +608,7 @@ pub fn get_effective_at(key: &str, at: u32) -> Result>, u32> ``` | Function | Return Type | Description | -| -------------------------------------- | ------------------------------ | -------------------------------------------------- | +| ----------------------------------------- | --------------------------------- | ------------------------------------------------------ | | `get(key: &str)` | `Result>, u32>` | Get the current value of a secret | | `get_effective_at(key: &str, at: u32)` | `Result>, u32>` | Get the secret value effective at a Unix timestamp | @@ -740,16 +825,17 @@ The `log` crate macros (`info!`, `warn!`, `error!`, etc.) work when `proxy_wasm: ## API Comparison: HTTP vs CDN -| Service | HTTP Apps (Component Model) | CDN Apps (ProxyWasm) | -| ------------- | ------------------------------------------------------------------- | -------------------------------------------------------- | -| Key-Value | `fastedge::key_value::Store` | `fastedge::proxywasm::key_value::Store` | -| Secrets | `fastedge::secret::get` | `fastedge::proxywasm::secret::get` | -| Dictionary | `fastedge::dictionary::get` | `fastedge::proxywasm::dictionary::get` | -| Diagnostics | `fastedge::utils::set_user_diag` | `fastedge::proxywasm::utils::set_user_diag` | -| Error types | Typed `Error` enums | `u32` status codes (secret) or typed `Error` (key_value) | -| Cargo feature | None required | `features = ["proxywasm"]` | -| Build target | `wasm32-wasip1` (basic) / `wasm32-wasip2` (wstd) | `wasm32-wasip1` | -| Handler | `#[wstd::http_server]` (recommended) / `#[fastedge::http]` (basic) | `proxy_wasm::main!` + traits | +| Service | HTTP Apps (Component Model) | CDN Apps (ProxyWasm) | +| --------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| Key-Value | `fastedge::key_value::Store` | `fastedge::proxywasm::key_value::Store` | +| Cache | `fastedge::cache` | `fastedge::proxywasm::cache` | +| Secrets | `fastedge::secret::get` | `fastedge::proxywasm::secret::get` | +| Dictionary | `fastedge::dictionary::get` | `fastedge::proxywasm::dictionary::get` | +| Diagnostics | `fastedge::utils::set_user_diag` | `fastedge::proxywasm::utils::set_user_diag` | +| Error types | Typed `Error` enums | `u32` status codes (secret) or typed `Error` (key_value, cache) | +| Cargo feature | None required | `features = ["proxywasm"]` | +| Build target | `wasm32-wasip1` (basic) / `wasm32-wasip2` (wstd) | `wasm32-wasip1` | +| Handler | `#[wstd::http_server]` (recommended) / `#[fastedge::http]` (basic) | `proxy_wasm::main!` + traits | ## See Also diff --git a/fastedge-plugin-source/generate-docs.sh b/fastedge-plugin-source/generate-docs.sh index dc8519b..2a97688 100755 --- a/fastedge-plugin-source/generate-docs.sh +++ b/fastedge-plugin-source/generate-docs.sh @@ -89,7 +89,7 @@ ALL_FILES=("${TIER1_FILES[@]}" "${TIER2_FILES[@]}" "${TIER3_FILES[@]}") declare -A SOURCE_FILES SOURCE_FILES[SDK_API.md]="src/lib.rs src/http_client.rs derive/src/lib.rs Cargo.toml" SOURCE_FILES[HOST_SERVICES.md]="src/lib.rs src/proxywasm/key_value.rs src/proxywasm/secret.rs src/proxywasm/dictionary.rs src/proxywasm/utils.rs" -SOURCE_FILES[CDN_APPS.md]="src/proxywasm/mod.rs src/proxywasm/key_value.rs src/proxywasm/secret.rs src/proxywasm/dictionary.rs src/proxywasm/utils.rs examples/cdn/hello_world/src/lib.rs examples/cdn/hello_world/Cargo.toml examples/cdn/custom/src/lib.rs examples/cdn/custom/Cargo.toml examples/cdn/jwt/src/lib.rs examples/cdn/jwt/Cargo.toml examples/cdn/key_value/src/lib.rs examples/cdn/key_value/Cargo.toml examples/cdn/geoblock/src/lib.rs examples/cdn/geoblock/Cargo.toml examples/cdn/variables_and_secrets/src/lib.rs examples/cdn/variables_and_secrets/Cargo.toml" +SOURCE_FILES[CDN_APPS.md]="src/proxywasm/mod.rs src/proxywasm/cache.rs src/proxywasm/key_value.rs src/proxywasm/secret.rs src/proxywasm/dictionary.rs src/proxywasm/utils.rs examples/cdn/hello_world/src/lib.rs examples/cdn/hello_world/Cargo.toml examples/cdn/custom/src/lib.rs examples/cdn/custom/Cargo.toml examples/cdn/jwt/src/lib.rs examples/cdn/jwt/Cargo.toml examples/cdn/key_value/src/lib.rs examples/cdn/key_value/Cargo.toml examples/cdn/cache/src/lib.rs examples/cdn/cache/Cargo.toml examples/cdn/geoblock/src/lib.rs examples/cdn/geoblock/Cargo.toml examples/cdn/variables_and_secrets/src/lib.rs examples/cdn/variables_and_secrets/Cargo.toml" SOURCE_FILES[quickstart.md]="Cargo.toml src/lib.rs examples/http/wasi/hello_world/Cargo.toml examples/http/wasi/hello_world/src/lib.rs examples/http/basic/hello_world/src/lib.rs" SOURCE_FILES[INDEX.md]="Cargo.toml" From 40ae4ce425eb908aa4eabb6f7665646f901d3ed7 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Wed, 16 Sep 2026 14:14:38 +0300 Subject: [PATCH 5/5] fix: correct typo in cache Action and update response status property path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed a typo in `cache.rs` (`Pa§use` to `Pause`) in error handling logic. - Standardized property path format in `send_error` function for CDN examples (`response.status` instead of `response`, `status`). --- examples/cdn/cache/src/lib.rs | 2 +- examples/cdn/key_value/src/lib.rs | 2 +- src/proxywasm/cache.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/cdn/cache/src/lib.rs b/examples/cdn/cache/src/lib.rs index 7e53f82..c7f5ac4 100644 --- a/examples/cdn/cache/src/lib.rs +++ b/examples/cdn/cache/src/lib.rs @@ -242,7 +242,7 @@ impl CacheContext { fn send_error(&self, msg: &str, body_size: usize) { println!("{}", msg); self.set_property( - vec!["response", "status"], + vec!["response.status"], Some(b"500"), ); let error_body = json!({"error": msg}).to_string(); diff --git a/examples/cdn/key_value/src/lib.rs b/examples/cdn/key_value/src/lib.rs index d15bb2f..f42b82f 100644 --- a/examples/cdn/key_value/src/lib.rs +++ b/examples/cdn/key_value/src/lib.rs @@ -225,7 +225,7 @@ impl KvStoreContext { fn send_error(&self, msg: &str, body_size: usize) { println!("{}", msg); self.set_property( - vec!["response", "status"], + vec!["response.status"], Some(b"500"), ); let error_body = json!({"error": msg}).to_string(); diff --git a/src/proxywasm/cache.rs b/src/proxywasm/cache.rs index ce69911..f191542 100644 --- a/src/proxywasm/cache.rs +++ b/src/proxywasm/cache.rs @@ -38,7 +38,7 @@ //! fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { //! //! let Ok(cached) = cache::get("key-3338664") else { -//! return Action::Pa§use; +//! return Action::Pause; //! }; //! //! if cached.is_none() {