fraiseql_functions/types.rs
1//! Core types for function execution.
2
3use std::time::Duration;
4
5use serde::{Deserialize, Serialize};
6use sha2::Digest;
7
8/// Marker a function (or a host op) puts in a thrown error's message to signal the
9/// failure is **permanent** — do not retry, dead-letter immediately.
10///
11/// Both runtimes surface a guest failure as a string, so permanence travels as this
12/// sentinel substring: when the error message contains it, the runtime classifies
13/// the failure as a client error (4xx) — which the durable dispatcher dead-letters
14/// on the first attempt — rather than the default transient `Unsupported` (501).
15///
16/// A guest can also throw `Object.assign(new Error(msg), { fraiseqlPermanent: true })`
17/// (Deno); the wrapper folds that into this marker. Host ops that already know a
18/// failure is permanent (e.g. `send_email` on a denied identity or a rejected
19/// recipient) prepend it automatically.
20pub const PERMANENT_ERROR_MARKER: &str = "[fraiseql:permanent]";
21
22/// Supported runtime types for serverless functions.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[non_exhaustive]
25pub enum RuntimeType {
26 /// `WebAssembly` Component Model runtime.
27 Wasm,
28 /// Deno (`JavaScript`/`TypeScript` via V8) runtime.
29 Deno,
30}
31
32impl RuntimeType {
33 /// Get supported file extensions for this runtime.
34 #[must_use]
35 pub const fn supported_extensions(&self) -> &[&str] {
36 match self {
37 RuntimeType::Wasm => &[".wasm"],
38 RuntimeType::Deno => &[".js", ".ts", ".mjs", ".mts"],
39 }
40 }
41}
42
43/// A compiled function module ready for execution.
44#[derive(Debug, Clone)]
45pub struct FunctionModule {
46 /// Unique name for this function.
47 pub name: String,
48 /// Hash of the module source (for caching).
49 pub source_hash: String,
50 /// Compiled bytecode or source text.
51 pub bytecode: bytes::Bytes,
52 /// Which runtime executes this module.
53 pub runtime: RuntimeType,
54}
55
56impl FunctionModule {
57 /// Create a new WASM module from compiled bytecode.
58 pub fn from_bytecode(name: String, bytecode: bytes::Bytes) -> Self {
59 let source_hash = hex::encode(sha2::Sha256::digest(&bytecode));
60 Self {
61 name,
62 source_hash,
63 bytecode,
64 runtime: RuntimeType::Wasm,
65 }
66 }
67
68 /// Create a new source-based module (JavaScript/TypeScript).
69 #[must_use]
70 pub fn from_source(name: String, source: String, runtime: RuntimeType) -> Self {
71 let bytecode = bytes::Bytes::from(source);
72 let source_hash = hex::encode(sha2::Sha256::digest(&bytecode));
73 Self {
74 name,
75 source_hash,
76 bytecode,
77 runtime,
78 }
79 }
80}
81
82/// Trigger event payload for a function invocation.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct EventPayload {
85 /// Type of trigger: "mutation", "subscription", "cron", "webhook", etc.
86 pub trigger_type: String,
87 /// Entity name (e.g., "User", "Post").
88 pub entity: String,
89 /// Event kind (e.g., "created", "updated", "deleted").
90 pub event_kind: String,
91 /// Event data (JSON).
92 pub data: serde_json::Value,
93 /// Timestamp when the event occurred.
94 pub timestamp: chrono::DateTime<chrono::Utc>,
95}
96
97/// Definition of a serverless function for deployment and execution.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct FunctionDefinition {
100 /// Unique name for this function.
101 pub name: String,
102 /// Trigger type and configuration (e.g., "after:mutation:createUser", "cron:0 * * * *",
103 /// "<http:GET:/users/:id>").
104 pub trigger: String,
105 /// Which runtime executes this function.
106 pub runtime: RuntimeType,
107 /// Optional timeout in milliseconds (overrides defaults).
108 /// - For `before:mutation` triggers: defaults to 500ms
109 /// - For other triggers: defaults to 5s
110 pub timeout_ms: Option<u64>,
111
112 /// Fire-and-forget opt-out for durable dispatch.
113 ///
114 /// After-mutation function dispatch is durable by default: a transient
115 /// failure is retried with backoff and, once retries are exhausted, the
116 /// invocation is dead-lettered so money- and send-path work is never
117 /// silently lost. Set `re_runnable = true` for work that is safe to simply
118 /// re-run later (e.g. LLM scoring) — such dispatch stays fire-and-forget with
119 /// no retry or dead-letter overhead. See ADR 0015 for the rationale.
120 #[serde(default)]
121 pub re_runnable: bool,
122
123 /// Per-function retry policy for durable dispatch.
124 ///
125 /// `None` uses the server default (overridable via `FRAISEQL_FUNCTIONS_RETRY_*`
126 /// environment variables). Ignored when [`re_runnable`](Self::re_runnable) is
127 /// `true`. Reuses the observer subsystem's [`RetryConfig`] so retry semantics
128 /// are identical across both subsystems.
129 ///
130 /// [`RetryConfig`]: fraiseql_observers::RetryConfig
131 #[serde(default)]
132 pub retry: Option<fraiseql_observers::RetryConfig>,
133}
134
135impl FunctionDefinition {
136 /// Create a new function definition.
137 #[must_use]
138 pub fn new(name: &str, trigger: &str, runtime: RuntimeType) -> Self {
139 Self {
140 name: name.to_string(),
141 trigger: trigger.to_string(),
142 runtime,
143 timeout_ms: None,
144 re_runnable: false,
145 retry: None,
146 }
147 }
148
149 /// Mark this function as re-runnable (fire-and-forget) dispatch.
150 ///
151 /// See [`re_runnable`](Self::re_runnable) and ADR 0015.
152 #[must_use]
153 pub const fn re_runnable(mut self) -> Self {
154 self.re_runnable = true;
155 self
156 }
157
158 /// Set a custom timeout for this function.
159 #[must_use]
160 pub const fn with_timeout(mut self, timeout_ms: u64) -> Self {
161 self.timeout_ms = Some(timeout_ms);
162 self
163 }
164
165 /// Get the effective timeout for this function.
166 #[must_use]
167 pub fn effective_timeout(&self) -> Duration {
168 match self.timeout_ms {
169 Some(ms) => Duration::from_millis(ms),
170 None => {
171 // before:mutation defaults to 500ms; others default to 5s
172 if self.trigger.starts_with("before:mutation") {
173 Duration::from_millis(500)
174 } else {
175 Duration::from_secs(5)
176 }
177 },
178 }
179 }
180
181 /// Check if this function is a before:mutation trigger.
182 #[must_use]
183 pub fn is_before_mutation(&self) -> bool {
184 self.trigger.starts_with("before:mutation:")
185 }
186
187 /// Check if this function is an after:mutation trigger.
188 #[must_use]
189 pub fn is_after_mutation(&self) -> bool {
190 self.trigger.starts_with("after:mutation:")
191 }
192
193 /// Check if this function is an after:storage trigger.
194 #[must_use]
195 pub fn is_after_storage(&self) -> bool {
196 self.trigger.starts_with("after:storage:")
197 }
198
199 /// Check if this function is an after:ingest trigger.
200 #[must_use]
201 pub fn is_after_ingest(&self) -> bool {
202 self.trigger == "after:ingest" || self.trigger.starts_with("after:ingest:")
203 }
204
205 /// Check if this function is a cron trigger.
206 #[must_use]
207 pub fn is_cron(&self) -> bool {
208 self.trigger.starts_with("cron:")
209 }
210
211 /// Check if this function is an HTTP trigger.
212 #[must_use]
213 pub fn is_http(&self) -> bool {
214 self.trigger.starts_with("http:")
215 }
216}
217
218/// Log level for structured logging.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220#[non_exhaustive]
221pub enum LogLevel {
222 /// Debug level.
223 Debug,
224 /// Info level.
225 Info,
226 /// Warning level.
227 Warn,
228 /// Error level.
229 Error,
230}
231
232/// A single log entry from function execution.
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234pub struct LogEntry {
235 /// Log level.
236 pub level: LogLevel,
237 /// Log message.
238 pub message: String,
239 /// When the log was written.
240 pub timestamp: chrono::DateTime<chrono::Utc>,
241}
242
243/// Result of a function invocation.
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct FunctionResult {
246 /// Return value from the function (may be None if function returns void).
247 pub value: Option<serde_json::Value>,
248 /// All logs captured during execution.
249 pub logs: Vec<LogEntry>,
250 /// Total execution duration.
251 pub duration: Duration,
252 /// Peak memory usage in bytes.
253 pub memory_peak_bytes: u64,
254}
255
256/// Resource limits for function execution.
257#[derive(Debug, Clone)]
258pub struct ResourceLimits {
259 /// Maximum memory allocation in bytes.
260 pub max_memory_bytes: u64,
261 /// Maximum execution duration.
262 pub max_duration: Duration,
263 /// Maximum number of log entries to capture.
264 pub max_log_entries: usize,
265}
266
267impl Default for ResourceLimits {
268 fn default() -> Self {
269 Self {
270 max_memory_bytes: 128 * 1024 * 1024, // 128 MB
271 max_duration: Duration::from_secs(5), // 5 seconds
272 max_log_entries: 10_000,
273 }
274 }
275}
276
277#[cfg(test)]
278mod tests;