1use oxigdal_core::error::OxiGdalError;
7use std::fmt;
8use wasm_bindgen::prelude::*;
9
10pub type WasmResult<T> = std::result::Result<T, WasmError>;
12
13#[derive(Debug, Clone)]
15pub enum WasmError {
16 Fetch(FetchError),
18
19 Canvas(CanvasError),
21
22 Worker(WorkerError),
24
25 TileCache(TileCacheError),
27
28 JsInterop(JsInteropError),
30
31 OxiGdal(String),
33
34 InvalidOperation {
36 operation: String,
38 reason: String,
40 },
41
42 NotFound {
44 resource: String,
46 identifier: String,
48 },
49
50 OutOfMemory {
52 requested: usize,
54 available: Option<usize>,
56 },
57
58 Timeout {
60 operation: String,
62 duration_ms: u64,
64 },
65
66 Format {
68 expected: String,
70 actual: String,
72 },
73}
74
75#[derive(Debug, Clone)]
77pub enum FetchError {
78 NetworkFailure {
80 url: String,
82 message: String,
84 },
85
86 HttpError {
88 status: u16,
90 status_text: String,
92 url: String,
94 },
95
96 CorsError {
98 url: String,
100 details: String,
102 },
103
104 RangeNotSupported {
106 url: String,
108 },
109
110 ParseError {
112 expected: String,
114 message: String,
116 },
117
118 Timeout {
120 url: String,
122 timeout_ms: u64,
124 },
125
126 RetryLimitExceeded {
128 url: String,
130 attempts: u32,
132 },
133
134 InvalidSize {
136 expected: u64,
138 actual: u64,
140 },
141}
142
143#[derive(Debug, Clone)]
145pub enum CanvasError {
146 ImageDataCreation {
148 width: u32,
150 height: u32,
152 message: String,
154 },
155
156 InvalidDimensions {
158 width: u32,
160 height: u32,
162 reason: String,
164 },
165
166 ColorSpaceConversion {
168 from: String,
170 to: String,
172 details: String,
174 },
175
176 BufferSizeMismatch {
178 expected: usize,
180 actual: usize,
182 },
183
184 ContextUnavailable {
186 context_type: String,
188 },
189
190 RenderingFailed {
192 operation: String,
194 message: String,
196 },
197
198 InvalidParameter(String),
200}
201
202#[derive(Debug, Clone)]
204pub enum WorkerError {
205 CreationFailed {
207 message: String,
209 },
210
211 Terminated {
213 worker_id: u32,
215 },
216
217 PostMessageFailed {
219 worker_id: u32,
221 message: String,
223 },
224
225 PoolExhausted {
227 pool_size: usize,
229 pending_jobs: usize,
231 },
232
233 ResponseTimeout {
235 worker_id: u32,
237 job_id: u64,
239 timeout_ms: u64,
241 },
242
243 InvalidResponse {
245 expected: String,
247 actual: String,
249 },
250}
251
252#[derive(Debug, Clone)]
254pub enum TileCacheError {
255 Miss {
257 key: String,
259 },
260
261 Full {
263 current_size: usize,
265 max_size: usize,
267 },
268
269 InvalidCoordinates {
271 level: u32,
273 x: u32,
275 y: u32,
277 reason: String,
279 },
280
281 SizeMismatch {
283 expected: usize,
285 actual: usize,
287 },
288
289 EvictionFailed {
291 message: String,
293 },
294}
295
296#[derive(Debug, Clone)]
298pub enum JsInteropError {
299 TypeConversion {
301 expected: String,
303 actual: String,
305 },
306
307 PropertyAccess {
309 property: String,
311 message: String,
313 },
314
315 FunctionCall {
317 function: String,
319 message: String,
321 },
322
323 PromiseRejection {
325 promise: String,
327 reason: String,
329 },
330
331 InvalidJsValue {
333 expected: String,
335 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
564impl From<WasmError> for JsValue {
566 fn from(err: WasmError) -> Self {
567 JsValue::from_str(&err.to_string())
568 }
569}
570
571impl From<OxiGdalError> for WasmError {
573 fn from(err: OxiGdalError) -> Self {
574 Self::OxiGdal(err.to_string())
575 }
576}
577
578impl From<FetchError> for WasmError {
580 fn from(err: FetchError) -> Self {
581 Self::Fetch(err)
582 }
583}
584
585impl From<CanvasError> for WasmError {
587 fn from(err: CanvasError) -> Self {
588 Self::Canvas(err)
589 }
590}
591
592impl From<WorkerError> for WasmError {
594 fn from(err: WorkerError) -> Self {
595 Self::Worker(err)
596 }
597}
598
599impl From<TileCacheError> for WasmError {
601 fn from(err: TileCacheError) -> Self {
602 Self::TileCache(err)
603 }
604}
605
606impl From<JsInteropError> for WasmError {
608 fn from(err: JsInteropError) -> Self {
609 Self::JsInterop(err)
610 }
611}
612
613#[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#[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#[allow(dead_code)]
636pub struct WasmErrorBuilder;
637
638#[allow(dead_code)]
639impl WasmErrorBuilder {
640 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 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 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 pub fn worker_creation(message: impl Into<String>) -> WasmError {
672 WasmError::Worker(WorkerError::CreationFailed {
673 message: message.into(),
674 })
675 }
676
677 pub fn cache_miss(key: impl Into<String>) -> WasmError {
679 WasmError::TileCache(TileCacheError::Miss { key: key.into() })
680 }
681
682 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 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 pub fn out_of_memory(requested: usize, available: Option<usize>) -> WasmError {
700 WasmError::OutOfMemory {
701 requested,
702 available,
703 }
704 }
705
706 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}