Sessions and Caching
Junction provides built-in session management (cookie-based, in-memory store with configurable TTL) and response caching (LRU-eviction with TTL, Vary header support, ETag generation, and Cache-Control parsing). Both are optional middleware that integrate directly with the app object.
Session Management
The session module provides an in-memory store with configurable TTL, cookie integration, and secure defaults. Sessions are identified by a session_id cookie (default name) that is httpOnly, secure, and SameSite=Lax by default.
Creating a Session Store
// Default store: 3600s TTL
let store = session.new_store(3600);
// Custom configuration
let store = session.new_store_with_config({
"ttl_seconds": 7200,
"cookie_name": "myapp_session",
"cookie_path": "/app",
"cookie_domain": ".example.com",
"secure": true,
"http_only": true,
"same_site": "Strict"
});Session CRUD Operations
// Get or create session
let data = session.get_session(store, session_id);
// Returns the data object (creates a new session if none exists or expired)
// Set session data
session.set_session(store, session_id, {"user_id": 42, "role": "admin"});
// Get/set/delete individual values
session.set_session_value(store, session_id, "theme", "dark");
let theme = session.get_session_value(store, session_id, "theme");
session.delete_session_value(store, session_id, "theme");
// Destroy session
session.destroy_session(store, session_id);
// Regenerate session ID (prevents session fixation)
let new_id = session.regenerate_session_id(store, session_id);
// Clean expired sessions
session.clean_expired_sessions(store);
// Count active sessions
let count = session.session_count(store);Session User Flow
junction.route(app, "POST", "/login", fn(req: Request) -> Response {
let body = junction.parse_json(req.body);
let username = body["username"];
let password = body["password"];
// Validate credentials
if (username != "admin" || password != "secret") {
return junction.error("LOGIN_FAILED", "Invalid credentials", 401);
}
// Create session
let session_id = "sess_" + std.crypto.random_hex(16);
session.set_session(app.sessions, session_id, {
"user_id": 1,
"username": username,
"role": "admin"
});
let res = junction.json({"login": "ok"}, 200);
junction.set_cookie(res, "session_id", session_id, {
"path": "/",
"http_only": true,
"secure": true,
"same_site": "Lax",
"max_age": 3600
});
return res;
});Session Middleware
The session_middleware(app) automatically resolves the session from the incoming cookie and attaches it to req.auth_context["session_data"]. The save_session() function persists the modified data and refreshes the cookie on the response.
junction.use_middleware(app, junction.session_middleware(app));
junction.route(app, "GET", "/profile", fn(req: Request) -> Response {
let session_data = req.auth_context["session_data"];
if (session_data == null || session_data["user_id"] == null) {
return junction.error("NOT_LOGGED_IN", "Login required", 401);
}
let res = junction.json({"user_id": session_data["user_id"]}, 200);
return junction.save_session(app, req, res);
});Session Configuration Options
| Field | Default | Description |
|---|---|---|
ttl_seconds | 3600 | Session time-to-live in seconds |
cookie_name | session_id | Name of the session cookie |
cookie_path | / | Cookie path scope |
cookie_domain | "" | Cookie domain scope |
secure | true | Only send cookie over HTTPS |
http_only | true | Prevent JavaScript access to cookie |
same_site | Lax | SameSite policy (Strict, Lax, None) |
Response Caching
The cache module provides an in-memory LRU-eviction cache for HTTP responses. It supports TTL-based expiration, Vary header differentiation, ETag generation, and Cache-Control header parsing. The cache is automatically created as app.cache_store when the app is created.
Creating a Cache Store
// Default: 300s TTL, 1000 max entries
let cache = cache.new_cache();
// Custom configuration
let cache = cache.new_cache_with_opts(600, 5000);Caching Responses
junction.route(app, "GET", "/expensive-data", fn(req: Request) -> Response {
// Check cache first
let cached = cache.get_cached_response(app.cache_store, req.path, req.query);
if (cached != null) {
return cached;
}
// Generate response
let data = compute_expensive_result();
let res = junction.json(data, 200);
// Store in cache with 60s TTL
cache.cache_response(app.cache_store, req.path, req.query, res, 60);
return res;
});Cache Invalidation
// Invalidate a specific cache entry
cache.invalidate(app.cache_store, "/expensive-data");
// Invalidate all entries with a given path prefix
cache.invalidate_by_path(app.cache_store, "/api/v1");
// Clear entire cache
cache.invalidate_all(app.cache_store);Vary Header Support
When a cached response has a Vary header, the cache stores the vary header values and validates them on lookup. Use get_cached_with_vary() to check vary headers explicitly.
let cached = cache.get_cached_with_vary(
app.cache_store, req.path, req.query, req.headers
);ETag Generation
let etag = cache.generate_etag_for_response(response);
// MD5 hash of body + serialized headers + status codeCache Statistics
let stats = cache.cache_stats(app.cache_store);
print(stats["entries"]); // Current entry count
print(stats["hit_count"]); // Total cache hits
print(stats["miss_count"]); // Total cache misses
print(stats["max_entries"]); // Maximum entries before evictionCache-Control Header Parsing
let cc = cache.parse_cache_control("public, max-age=3600, must-revalidate");
if (cc["public"]) { /* cacheable by shared caches */ }
if (cc["max_age"] > 0) { /* freshness lifetime */ }
if (cc["no_cache"]) { /* must revalidate */ }
if (cc["no_store"]) { /* do not cache */ }Cache Middleware
The cache_middleware(app) automatically serves cached responses for GET and HEAD requests before they reach the router.
junction.use_middleware(app, junction.cache_middleware(app));
// Later, in a handler, store the response:
junction.cache_response(app, req, res, 120);Cache Eviction Policy
When the cache exceeds max_entries, the oldest entry (by created_at timestamp) is evicted. The eviction check runs on each new cache write. Both the global hit and miss counters are maintained for observability.
LRU Record Keeping
// Direct access to cache internals
let entry_count = std.collections.length(app.cache_store["entries"]);
let hit_ratio = app.cache_store["hit_count"] /
(app.cache_store["hit_count"] + app.cache_store["miss_count"]);