zynagent-first application framework

Cache#

action.cache, hook.cache, middleware.cache, and job.cache store disposable values in a PostgreSQL database separate from the application database. Guards cannot use the cache. Values are byte strings; the caller owns serialization, versioned keys, and invalidation. Cache entries are not HTTP responses or a source of durable application state.

try action.cache.put("catalog:v2:top", bytes, .{
    .ttl = .fromSeconds(60),
});
const cached = try action.cache.get("catalog:v2:top");
_ = try action.cache.delete("catalog:v2:top");

get returns null for missing or expired entries. put atomically replaces an entry, including its expiration and FIFO position. delete returns whether any row was removed, even an expired row. Values returned by requests and jobs use their scoped allocator and cannot be retained afterward. All three operations report database errors.

fetch takes a key, TTL options, a context, and a Zig compute function. It calls that function with context on a miss or read failure. It returns the computed bytes, even when the subsequent cache write fails. Errors returned by compute still propagate. Concurrent misses may compute independently. Invalid keys and TTLs fail without invoking the callback. If a computed value exceeds the configured per-entry limit, it is returned without caching it.

Keys must be nonempty UTF-8 text without NUL bytes and of at most 256 bytes; keys containing secrets are not supported. TTLs must be positive and fit in signed PostgreSQL microseconds. Values are not encrypted by default. Cache expiry uses PostgreSQL's clock. Expired rows never satisfy reads; bounded maintenance on writes removes expired rows, then evicts the oldest writes when PostgreSQL's allocated table and index size exceeds the soft size target. Vacuum reuses dead-row space, but this target is not a hard disk quota. Reads do not update eviction order.

Configuration#

The optional top-level .cache section in zyn.zon accepts:

.cache = .{
    .max_value_bytes = 2_097_152,
    .target_size_bytes = 1_073_741_824,
    .profiles = .{
        .production = .{ .database = "production_cache" },
    },
},

The displayed limits are also the defaults. max_value_bytes must be from 1 to 16 MiB; target_size_bytes must be positive and fit in a signed 64-bit integer. The size target uses PostgreSQL's allocated table and index size; it is not a hard disk quota. Profile entries may override database, host, port, username, and password_credential. Omitted connection fields inherit the selected application database connection. Without a database override, Zyn appends _cache to the application database name. Cache and application database names must be distinct and fit within 63 bytes. The selected cache profile is testing for zyn test and production for deployment.

Managed development creates the cache database on first use; managed deployment provisions it before starting a release. The cache schema is versioned independently and installed on application startup. External PostgreSQL users provision the database and grant access to the configured role. A separate password is referenced through a declared Zyn credential, never placed directly in zyn.zon. Development and managed production use the selected application PostgreSQL role unless configured otherwise.

zyn cache clear truncates the development cache without changing the application database. Managed tests use a separate disposable PostgreSQL fixture for each application handle; app.cache accesses that fixture directly. Clearing, dropping, or restoring the cache does not affect application tables. Route rate limits store their counters in a separate zyn.rate_limits table in the same cache database. zyn cache clear does not truncate that table.