Skip to main content

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;