Skip to main content

oxigdal_wasm/
error.rs

1//! WASM-specific error types and handling
2//!
3//! This module provides comprehensive error handling for WebAssembly operations,
4//! including fetch errors, canvas errors, worker errors, and tile cache errors.
5
6use oxigdal_core::error::OxiGdalError;
7use std::fmt;
8use wasm_bindgen::prelude::*;
9
10/// Result type for WASM operations
11pub type WasmResult<T> = std::result::Result<T, WasmError>;
12
13/// Comprehensive WASM error types
14#[derive(Debug, Clone)]
15pub enum WasmError {
16    /// Fetch API errors
17    Fetch(FetchError),
18
19    /// Canvas rendering errors
20    Canvas(CanvasError),
21
22    /// Web Worker errors
23    Worker(WorkerError),
24
25    /// Tile cache errors
26    TileCache(TileCacheError),
27
28    /// JavaScript interop errors
29    JsInterop(JsInteropError),
30
31    /// OxiGDAL core errors
32    OxiGdal(String),
33
34    /// Invalid operation
35    InvalidOperation {
36        /// Operation description
37        operation: String,
38        /// Reason for invalidity
39        reason: String,
40    },
41
42    /// Resource not found
43    NotFound {
44        /// Resource type
45        resource: String,
46        /// Resource identifier
47        identifier: String,
48    },
49
50    /// Out of memory
51    OutOfMemory {
52        /// Requested size in bytes
53        requested: usize,
54        /// Available size in bytes
55        available: Option<usize>,
56    },
57
58    /// Timeout error
59    Timeout {
60        /// Operation that timed out
61        operation: String,
62        /// Duration in milliseconds
63        duration_ms: u64,
64    },
65
66    /// Format error
67    Format {
68        /// Expected format
69        expected: String,
70        /// Actual format
71        actual: String,
72    },
73}
74
75/// Fetch-related errors
76#[derive(Debug, Clone)]
77pub enum FetchError {
78    /// Network request failed
79    NetworkFailure {
80        /// URL that failed
81        url: String,
82        /// Error message
83        message: String,
84    },
85
86    /// HTTP error response
87    HttpError {
88        /// HTTP status code
89        status: u16,
90        /// Status text
91        status_text: String,
92        /// URL
93        url: String,
94    },
95
96    /// CORS error
97    CorsError {
98        /// URL
99        url: String,
100        /// Details
101        details: String,
102    },
103
104    /// Range request not supported
105    RangeNotSupported {
106        /// URL
107        url: String,
108    },
109
110    /// Response parsing failed
111    ParseError {
112        /// Expected type
113        expected: String,
114        /// Error details
115        message: String,
116    },
117
118    /// Request timeout
119    Timeout {
120        /// URL
121        url: String,
122        /// Timeout duration in milliseconds
123        timeout_ms: u64,
124    },
125
126    /// Retry limit exceeded
127    RetryLimitExceeded {
128        /// URL
129        url: String,
130        /// Number of attempts
131        attempts: u32,
132    },
133
134    /// Invalid response size
135    InvalidSize {
136        /// Expected size
137        expected: u64,
138        /// Actual size
139        actual: u64,
140    },
141}
142
143/// Canvas rendering errors
144#[derive(Debug, Clone)]
145pub enum CanvasError {
146    /// Failed to create ImageData
147    ImageDataCreation {
148        /// Width
149        width: u32,
150        /// Height
151        height: u32,
152        /// Error message
153        message: String,
154    },
155
156    /// Invalid dimensions
157    InvalidDimensions {
158        /// Width
159        width: u32,
160        /// Height
161        height: u32,
162        /// Reason
163        reason: String,
164    },
165
166    /// Color space conversion failed
167    ColorSpaceConversion {
168        /// Source color space
169        from: String,
170        /// Target color space
171        to: String,
172        /// Error details
173        details: String,
174    },
175
176    /// Buffer size mismatch
177    BufferSizeMismatch {
178        /// Expected size
179        expected: usize,
180        /// Actual size
181        actual: usize,
182    },
183
184    /// Canvas context unavailable
185    ContextUnavailable {
186        /// Context type
187        context_type: String,
188    },
189
190    /// Rendering operation failed
191    RenderingFailed {
192        /// Operation
193        operation: String,
194        /// Error message
195        message: String,
196    },
197
198    /// Invalid parameter provided
199    InvalidParameter(String),
200}
201
202/// Web Worker errors
203#[derive(Debug, Clone)]
204pub enum WorkerError {
205    /// Worker creation failed
206    CreationFailed {
207        /// Error message
208        message: String,
209    },
210
211    /// Worker terminated unexpectedly
212    Terminated {
213        /// Worker ID
214        worker_id: u32,
215    },
216
217    /// Message posting failed
218    PostMessageFailed {
219        /// Worker ID
220        worker_id: u32,
221        /// Error details
222        message: String,
223    },
224
225    /// Worker pool exhausted
226    PoolExhausted {
227        /// Pool size
228        pool_size: usize,
229        /// Pending jobs
230        pending_jobs: usize,
231    },
232
233    /// Worker response timeout
234    ResponseTimeout {
235        /// Worker ID
236        worker_id: u32,
237        /// Job ID
238        job_id: u64,
239        /// Timeout duration in milliseconds
240        timeout_ms: u64,
241    },
242
243    /// Invalid worker response
244    InvalidResponse {
245        /// Expected response type
246        expected: String,
247        /// Actual response
248        actual: String,
249    },
250}
251
252/// Tile cache errors
253#[derive(Debug, Clone)]
254pub enum TileCacheError {
255    /// Cache miss
256    Miss {
257        /// Tile key
258        key: String,
259    },
260
261    /// Cache full
262    Full {
263        /// Current size in bytes
264        current_size: usize,
265        /// Maximum size in bytes
266        max_size: usize,
267    },
268
269    /// Invalid tile coordinates
270    InvalidCoordinates {
271        /// Level
272        level: u32,
273        /// X coordinate
274        x: u32,
275        /// Y coordinate
276        y: u32,
277        /// Reason
278        reason: String,
279    },
280
281    /// Tile size mismatch
282    SizeMismatch {
283        /// Expected size
284        expected: usize,
285        /// Actual size
286        actual: usize,
287    },
288
289    /// Eviction failed
290    EvictionFailed {
291        /// Error details
292        message: String,
293    },
294}
295
296/// JavaScript interop errors
297#[derive(Debug, Clone)]
298pub enum JsInteropError {
299    /// Type conversion failed
300    TypeConversion {
301        /// Expected type
302        expected: String,
303        /// Actual type
304        actual: String,
305    },
306
307    /// Property access failed
308    PropertyAccess {
309        /// Property name
310        property: String,
311        /// Error message
312        message: String,
313    },
314
315    /// Function call failed
316    FunctionCall {
317        /// Function name
318        function: String,
319        /// Error message
320        message: String,
321    },
322
323    /// Promise rejection
324    PromiseRejection {
325        /// Promise description
326        promise: String,
327        /// Rejection reason
328        reason: String,
329    },
330
331    /// Invalid JsValue
332    InvalidJsValue {
333        /// Expected type
334        expected: String,
335        /// Error details
336        details: String,
337    },
338}
339
340impl fmt::Display for WasmError {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        match self {
343            Self::Fetch(e) => write!(f, "Fetch error: {e}"),
344            Self::Canvas(e) => write!(f, "Canvas error: {e}"),
345            Self::Worker(e) => write!(f, "Worker error: {e}"),
346            Self::TileCache(e) => write!(f, "Tile cache error: {e}"),
347            Self::JsInterop(e) => write!(f, "JS interop error: {e}"),
348            Self::OxiGdal(msg) => write!(f, "OxiGDAL error: {msg}"),
349            Self::InvalidOperation { operation, reason } => {
350                write!(f, "Invalid operation '{operation}': {reason}")
351            }
352            Self::NotFound {
353                resource,
354                identifier,
355            } => {
356                write!(f, "{resource} not found: {identifier}")
357            }
358            Self::OutOfMemory {
359                requested,
360                available,
361            } => {
362                if let Some(avail) = available {
363                    write!(
364                        f,
365                        "Out of memory: requested {requested} bytes, {avail} available"
366                    )
367                } else {
368                    write!(f, "Out of memory: requested {requested} bytes")
369                }
370            }
371            Self::Timeout {
372                operation,
373                duration_ms,
374            } => {
375                write!(f, "Operation '{operation}' timed out after {duration_ms}ms")
376            }
377            Self::Format { expected, actual } => {
378                write!(f, "Format error: expected {expected}, got {actual}")
379            }
380        }
381    }
382}
383
384impl fmt::Display for FetchError {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        match self {
387            Self::NetworkFailure { url, message } => {
388                write!(f, "Network failure for {url}: {message}")
389            }
390            Self::HttpError {
391                status,
392                status_text,
393                url,
394            } => {
395                write!(f, "HTTP {status} {status_text} for {url}")
396            }
397            Self::CorsError { url, details } => {
398                write!(f, "CORS error for {url}: {details}")
399            }
400            Self::RangeNotSupported { url } => {
401                write!(f, "Range requests not supported for {url}")
402            }
403            Self::ParseError { expected, message } => {
404                write!(f, "Parse error: expected {expected}, {message}")
405            }
406            Self::Timeout { url, timeout_ms } => {
407                write!(f, "Request to {url} timed out after {timeout_ms}ms")
408            }
409            Self::RetryLimitExceeded { url, attempts } => {
410                write!(
411                    f,
412                    "Retry limit exceeded for {url} after {attempts} attempts"
413                )
414            }
415            Self::InvalidSize { expected, actual } => {
416                write!(
417                    f,
418                    "Invalid response size: expected {expected}, got {actual}"
419                )
420            }
421        }
422    }
423}
424
425impl fmt::Display for CanvasError {
426    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427        match self {
428            Self::ImageDataCreation {
429                width,
430                height,
431                message,
432            } => {
433                write!(f, "Failed to create ImageData {width}x{height}: {message}")
434            }
435            Self::InvalidDimensions {
436                width,
437                height,
438                reason,
439            } => {
440                write!(f, "Invalid dimensions {width}x{height}: {reason}")
441            }
442            Self::ColorSpaceConversion { from, to, details } => {
443                write!(f, "Color space conversion {from} -> {to} failed: {details}")
444            }
445            Self::BufferSizeMismatch { expected, actual } => {
446                write!(f, "Buffer size mismatch: expected {expected}, got {actual}")
447            }
448            Self::ContextUnavailable { context_type } => {
449                write!(f, "Canvas context '{context_type}' unavailable")
450            }
451            Self::RenderingFailed { operation, message } => {
452                write!(f, "Rendering operation '{operation}' failed: {message}")
453            }
454            Self::InvalidParameter(msg) => {
455                write!(f, "Invalid parameter: {msg}")
456            }
457        }
458    }
459}
460
461impl fmt::Display for WorkerError {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        match self {
464            Self::CreationFailed { message } => {
465                write!(f, "Worker creation failed: {message}")
466            }
467            Self::Terminated { worker_id } => {
468                write!(f, "Worker {worker_id} terminated unexpectedly")
469            }
470            Self::PostMessageFailed { worker_id, message } => {
471                write!(f, "Failed to post message to worker {worker_id}: {message}")
472            }
473            Self::PoolExhausted {
474                pool_size,
475                pending_jobs,
476            } => {
477                write!(
478                    f,
479                    "Worker pool exhausted: {pool_size} workers, {pending_jobs} pending jobs"
480                )
481            }
482            Self::ResponseTimeout {
483                worker_id,
484                job_id,
485                timeout_ms,
486            } => {
487                write!(
488                    f,
489                    "Worker {worker_id} job {job_id} timed out after {timeout_ms}ms"
490                )
491            }
492            Self::InvalidResponse { expected, actual } => {
493                write!(
494                    f,
495                    "Invalid worker response: expected {expected}, got {actual}"
496                )
497            }
498        }
499    }
500}
501
502impl fmt::Display for TileCacheError {
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        match self {
505            Self::Miss { key } => {
506                write!(f, "Cache miss for tile {key}")
507            }
508            Self::Full {
509                current_size,
510                max_size,
511            } => {
512                write!(f, "Cache full: {current_size}/{max_size} bytes")
513            }
514            Self::InvalidCoordinates {
515                level,
516                x,
517                y,
518                reason,
519            } => {
520                write!(f, "Invalid tile coordinates ({level}, {x}, {y}): {reason}")
521            }
522            Self::SizeMismatch { expected, actual } => {
523                write!(f, "Tile size mismatch: expected {expected}, got {actual}")
524            }
525            Self::EvictionFailed { message } => {
526                write!(f, "Cache eviction failed: {message}")
527            }
528        }
529    }
530}
531
532impl fmt::Display for JsInteropError {
533    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
534        match self {
535            Self::TypeConversion { expected, actual } => {
536                write!(
537                    f,
538                    "Type conversion failed: expected {expected}, got {actual}"
539                )
540            }
541            Self::PropertyAccess { property, message } => {
542                write!(f, "Property access failed for '{property}': {message}")
543            }
544            Self::FunctionCall { function, message } => {
545                write!(f, "Function call failed for '{function}': {message}")
546            }
547            Self::PromiseRejection { promise, reason } => {
548                write!(f, "Promise rejected for '{promise}': {reason}")
549            }
550            Self::InvalidJsValue { expected, details } => {
551                write!(f, "Invalid JsValue: expected {expected}, {details}")
552            }
553        }
554    }
555}
556
557impl std::error::Error for WasmError {}
558impl std::error::Error for FetchError {}
559impl std::error::Error for CanvasError {}
560impl std::error::Error for WorkerError {}
561impl std::error::Error for TileCacheError {}
562impl std::error::Error for JsInteropError {}
563
564/// Convert `WasmError` to `JsValue` for WASM bindings
565impl From<WasmError> for JsValue {
566    fn from(err: WasmError) -> Self {
567        JsValue::from_str(&err.to_string())
568    }
569}
570
571/// Convert `OxiGdalError` to `WasmError`
572impl From<OxiGdalError> for WasmError {
573    fn from(err: OxiGdalError) -> Self {
574        Self::OxiGdal(err.to_string())
575    }
576}
577
578/// Convert `FetchError` to `WasmError`
579impl From<FetchError> for WasmError {
580    fn from(err: FetchError) -> Self {
581        Self::Fetch(err)
582    }
583}
584
585/// Convert `CanvasError` to `WasmError`
586impl From<CanvasError> for WasmError {
587    fn from(err: CanvasError) -> Self {
588        Self::Canvas(err)
589    }
590}
591
592/// Convert `WorkerError` to `WasmError`
593impl From<WorkerError> for WasmError {
594    fn from(err: WorkerError) -> Self {
595        Self::Worker(err)
596    }
597}
598
599/// Convert `TileCacheError` to `WasmError`
600impl From<TileCacheError> for WasmError {
601    fn from(err: TileCacheError) -> Self {
602        Self::TileCache(err)
603    }
604}
605
606/// Convert `JsInteropError` to `WasmError`
607impl From<JsInteropError> for WasmError {
608    fn from(err: JsInteropError) -> Self {
609        Self::JsInterop(err)
610    }
611}
612
613/// Helper to convert `JsValue` to `WasmError`
614#[allow(dead_code)]
615pub fn js_to_wasm_error(js_val: JsValue, context: &str) -> WasmError {
616    let message = if let Some(s) = js_val.as_string() {
617        s
618    } else {
619        format!("{js_val:?}")
620    };
621
622    WasmError::JsInterop(JsInteropError::FunctionCall {
623        function: context.to_string(),
624        message,
625    })
626}
627
628/// Helper to convert errors to `JsValue`
629#[allow(dead_code)]
630pub fn to_js_value<E: std::fmt::Display>(err: E) -> JsValue {
631    JsValue::from_str(&err.to_string())
632}
633
634/// Error builder for common patterns
635#[allow(dead_code)]
636pub struct WasmErrorBuilder;
637
638#[allow(dead_code)]
639impl WasmErrorBuilder {
640    /// Create a fetch network failure error
641    pub fn fetch_network(url: impl Into<String>, message: impl Into<String>) -> WasmError {
642        WasmError::Fetch(FetchError::NetworkFailure {
643            url: url.into(),
644            message: message.into(),
645        })
646    }
647
648    /// Create a fetch HTTP error
649    pub fn fetch_http(
650        status: u16,
651        status_text: impl Into<String>,
652        url: impl Into<String>,
653    ) -> WasmError {
654        WasmError::Fetch(FetchError::HttpError {
655            status,
656            status_text: status_text.into(),
657            url: url.into(),
658        })
659    }
660
661    /// Create a canvas ImageData creation error
662    pub fn canvas_image_data(width: u32, height: u32, message: impl Into<String>) -> WasmError {
663        WasmError::Canvas(CanvasError::ImageDataCreation {
664            width,
665            height,
666            message: message.into(),
667        })
668    }
669
670    /// Create a worker creation error
671    pub fn worker_creation(message: impl Into<String>) -> WasmError {
672        WasmError::Worker(WorkerError::CreationFailed {
673            message: message.into(),
674        })
675    }
676
677    /// Create a tile cache miss error
678    pub fn cache_miss(key: impl Into<String>) -> WasmError {
679        WasmError::TileCache(TileCacheError::Miss { key: key.into() })
680    }
681
682    /// Create an invalid operation error
683    pub fn invalid_op(operation: impl Into<String>, reason: impl Into<String>) -> WasmError {
684        WasmError::InvalidOperation {
685            operation: operation.into(),
686            reason: reason.into(),
687        }
688    }
689
690    /// Create a not found error
691    pub fn not_found(resource: impl Into<String>, identifier: impl Into<String>) -> WasmError {
692        WasmError::NotFound {
693            resource: resource.into(),
694            identifier: identifier.into(),
695        }
696    }
697
698    /// Create an out of memory error
699    pub fn out_of_memory(requested: usize, available: Option<usize>) -> WasmError {
700        WasmError::OutOfMemory {
701            requested,
702            available,
703        }
704    }
705
706    /// Create a timeout error
707    pub fn timeout(operation: impl Into<String>, duration_ms: u64) -> WasmError {
708        WasmError::Timeout {
709            operation: operation.into(),
710            duration_ms,
711        }
712    }
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718
719    #[test]
720    fn test_error_display() {
721        let err = WasmErrorBuilder::fetch_network("https://example.com", "Connection refused");
722        assert!(err.to_string().contains("Network failure"));
723        assert!(err.to_string().contains("example.com"));
724    }
725
726    #[test]
727    fn test_error_conversion() {
728        let fetch_err = FetchError::HttpError {
729            status: 404,
730            status_text: "Not Found".to_string(),
731            url: "https://example.com".to_string(),
732        };
733        let wasm_err: WasmError = fetch_err.into();
734        assert!(matches!(wasm_err, WasmError::Fetch(_)));
735    }
736
737    #[test]
738    #[cfg(target_arch = "wasm32")]
739    fn test_js_value_conversion() {
740        let err = WasmErrorBuilder::invalid_op("test", "invalid");
741        let js_val: JsValue = err.into();
742        assert!(js_val.is_string());
743    }
744
745    #[test]
746    fn test_error_builder() {
747        let err = WasmErrorBuilder::out_of_memory(1024, Some(512));
748        assert!(matches!(err, WasmError::OutOfMemory { .. }));
749        assert!(err.to_string().contains("1024"));
750    }
751}