diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d18a23a..621982d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,9 @@ jobs: with: node-version: '20' + - name: Run release-mode WASM tests + run: wasm-pack test --headless --chrome --release + - name: Verify WASM build run: | ls -la pkg/ diff --git a/Cargo.lock b/Cargo.lock index 75f86d7..6f3abce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "absurder-sql" -version = "0.1.27" +version = "0.1.28" dependencies = [ "console_error_panic_hook", "console_log", diff --git a/Cargo.toml b/Cargo.toml index 757bc89..167ce8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "absurder-sql" -version = "0.1.27" +version = "0.1.28" authors = ["Nicholas G. Piesco"] description = "High-performance SQLite for browsers (IndexedDB or Hybrid OPFS), native apps, and mobile, with export/import and multi-tab coordination" license = "AGPL-3.0" diff --git a/absurder-sql-mobile/Cargo.lock b/absurder-sql-mobile/Cargo.lock index cb8f357..aa1aa80 100644 --- a/absurder-sql-mobile/Cargo.lock +++ b/absurder-sql-mobile/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "absurder-sql" -version = "0.1.27" +version = "0.1.28" dependencies = [ "crc32fast", "futures", diff --git a/package.json b/package.json index 182720e..79c8c00 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@npiesco/absurder-sql", - "version": "0.1.27", + "version": "0.1.28", "description": "High-performance SQLite for browsers with IndexedDB or Hybrid OPFS persistence, export/import, multi-tab coordination, and a production offline PWA.", "type": "module", "main": "./pkg/absurder_sql.js", diff --git a/pkg/package.json b/pkg/package.json index 869a0c0..aaa728b 100644 --- a/pkg/package.json +++ b/pkg/package.json @@ -5,7 +5,7 @@ "Nicholas G. Piesco" ], "description": "High-performance SQLite for browsers (IndexedDB or Hybrid OPFS), native apps, and mobile, with export/import and multi-tab coordination", - "version": "0.1.27", + "version": "0.1.28", "license": "AGPL-3.0", "repository": { "type": "git", diff --git a/src/storage/wasm_indexeddb.rs b/src/storage/wasm_indexeddb.rs index 03e743a..83456e8 100644 --- a/src/storage/wasm_indexeddb.rs +++ b/src/storage/wasm_indexeddb.rs @@ -1104,8 +1104,13 @@ pub async fn persist_to_indexeddb_event_based( let blocks = blocks_clone.clone(); let metadata = metadata_clone.clone(); async move { - persist_to_indexeddb_event_based_internal(&db_name, blocks, metadata, commit_marker) - .await + persist_to_indexeddb_event_based_internal( + &db_name, + blocks, + metadata, + Some(commit_marker), + ) + .await } }) .await; @@ -1139,7 +1144,7 @@ async fn persist_to_indexeddb_event_based_internal( db_name: &str, blocks: Vec<(u64, Vec)>, metadata: Vec<(u64, BlockMetadataPersist)>, - commit_marker: u64, + commit_marker: Option, ) -> Result<(), DatabaseError> { use wasm_bindgen::JsCast; use wasm_bindgen::closure::Closure; @@ -1343,7 +1348,9 @@ async fn persist_to_indexeddb_event_based_internal( // Store metadata with truly idempotent keys: (db_name, block_id) for (block_id, mut metadata) in metadata { let key = format!("{}:{}", db_name, block_id); - metadata.version = u32::try_from(commit_marker).unwrap_or(u32::MAX); + if let Some(commit_marker) = commit_marker { + metadata.version = u32::try_from(commit_marker).unwrap_or(u32::MAX); + } if metadata.last_modified_ms == 0 { metadata.last_modified_ms = js_sys::Date::now() as u64; } @@ -1367,10 +1374,13 @@ async fn persist_to_indexeddb_event_based_internal( let _ = metadata_store.put_with_key(&value, &key.into()); } - // Store commit marker - let commit_key = format!("{}:commit_marker", db_name); - let commit_value = js_sys::Number::from(commit_marker as f64); - let _ = metadata_store.put_with_key(&commit_value, &commit_key.into()); + // Store commit marker only for complete commits. Crash simulation persists the + // caller-supplied metadata version while intentionally leaving the marker behind. + if let Some(commit_marker) = commit_marker { + let commit_key = format!("{}:commit_marker", db_name); + let commit_value = js_sys::Number::from(commit_marker as f64); + let _ = metadata_store.put_with_key(&commit_value, &commit_key.into()); + } // Wait for transaction to complete let (tx_tx, tx_rx) = oneshot::channel(); @@ -1429,6 +1439,7 @@ async fn persist_to_indexeddb_event_based_internal( #[cfg(target_arch = "wasm32")] pub async fn sync_async(storage: &BlockStorage) -> Result<(), DatabaseError> { log::debug!("Using ASYNC sync_async method"); + let sync_started_ms = js_sys::Date::now(); // Get current commit marker let current_commit = vfs_sync::with_global_commit_marker(|cm| { let cm = cm; @@ -1453,6 +1464,11 @@ pub async fn sync_async(storage: &BlockStorage) -> Result<(), DatabaseError> { .iter() .map(|(k, v)| (*k, v.clone())) .collect(); + let blocks_synced = to_persist.len(); + storage.observability.record_sync_start( + blocks_synced, + blocks_synced * super::block_storage::BLOCK_SIZE, + ); let metadata_to_persist: Vec<(u64, BlockMetadataPersist)> = to_persist .iter() @@ -1529,7 +1545,11 @@ pub async fn sync_async(storage: &BlockStorage) -> Result<(), DatabaseError> { #[cfg(feature = "telemetry")] None, ) - .await?; + .await + .map_err(|error| { + storage.observability.record_sync_failure(&error); + error + })?; } crate::storage::StorageBackend::Opfs | crate::storage::StorageBackend::Hybrid => { super::hybrid_store::hybrid_persist( @@ -1542,7 +1562,11 @@ pub async fn sync_async(storage: &BlockStorage) -> Result<(), DatabaseError> { #[cfg(feature = "telemetry")] None, ) - .await?; + .await + .map_err(|error| { + storage.observability.record_sync_failure(&error); + error + })?; } } } @@ -1553,6 +1577,11 @@ pub async fn sync_async(storage: &BlockStorage) -> Result<(), DatabaseError> { dirty.clear(); } + let duration_ms = ((js_sys::Date::now() - sync_started_ms).ceil() as u64).max(1); + storage + .observability + .record_sync_success(duration_ms, blocks_synced); + Ok(()) } @@ -1573,24 +1602,24 @@ pub async fn persist_to_indexeddb( blocks_vec.len() ); - log::debug!("About to call persist_to_indexeddb_event_based"); - - // For crash simulation, we'll use the existing event-based persistence - // but without advancing the commit marker (that's handled by the caller) - let result = persist_to_indexeddb_event_based( - db_name, - blocks_vec, - metadata, - 0, - #[cfg(feature = "telemetry")] - None, - #[cfg(feature = "telemetry")] - None, - ) - .await; + log::debug!("About to persist crash simulation data without a commit marker"); + + let db_name = db_name.to_string(); + let blocks_clone = blocks_vec.clone(); + let metadata_clone = metadata.clone(); + let result = + super::retry_logic::with_retry("persist_to_indexeddb_without_commit_marker", || { + let db_name = db_name.clone(); + let blocks = blocks_clone.clone(); + let metadata = metadata_clone.clone(); + async move { + persist_to_indexeddb_event_based_internal(&db_name, blocks, metadata, None).await + } + }) + .await; log::debug!( - "persist_to_indexeddb_event_based completed with result: {:?}", + "Crash simulation persistence completed with result: {:?}", result.is_ok() ); diff --git a/tests/indexeddb_error_handling_tests.rs b/tests/indexeddb_error_handling_tests.rs index b101da3..bb95dff 100644 --- a/tests/indexeddb_error_handling_tests.rs +++ b/tests/indexeddb_error_handling_tests.rs @@ -8,10 +8,21 @@ use absurder_sql::storage::wasm_indexeddb::{ persist_to_indexeddb_event_based, restore_from_indexeddb, }; +use absurder_sql::storage::{BlockMetadataPersist, ChecksumAlgorithm, ChecksumManager}; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); +fn metadata_for(data: &[u8], version: u32) -> BlockMetadataPersist { + let algo = ChecksumAlgorithm::FastHash; + BlockMetadataPersist { + checksum: ChecksumManager::compute_checksum_with(data, algo), + last_modified_ms: 0, + version, + algo, + } +} + /// Test that restore_from_indexeddb returns a Result instead of bool #[wasm_bindgen_test] async fn test_restore_returns_result_type() { @@ -32,8 +43,9 @@ async fn test_restore_returns_result_type() { #[wasm_bindgen_test] async fn test_persist_handles_transaction_errors() { let db_name = "test_persist_error_db"; - let blocks = vec![(1u64, vec![1u8, 2, 3, 4])]; - let metadata = vec![(1u64, 100u64)]; + let block_data = vec![1u8, 2, 3, 4]; + let metadata = vec![(1u64, metadata_for(&block_data, 100))]; + let blocks = vec![(1u64, block_data)]; // This should not panic even if there are issues let result = persist_to_indexeddb_event_based( @@ -100,8 +112,8 @@ async fn test_persist_large_blocks() { let db_name = "test_persist_large_db"; // Create a large block (1MB) let large_data = vec![0u8; 1024 * 1024]; + let metadata = vec![(1u64, metadata_for(&large_data, 1000))]; let blocks = vec![(1u64, large_data)]; - let metadata = vec![(1u64, 1000u64)]; // Should handle large data without panicking let result = persist_to_indexeddb_event_based( @@ -195,10 +207,11 @@ async fn test_concurrent_access_errors() { // Try to perform operations that might have concurrent access issues // This should not panic but return meaningful errors + let block_data = vec![1, 2, 3]; let result = persist_to_indexeddb_event_based( db_name, - vec![(1, vec![1, 2, 3])], - vec![(1, 1)], + vec![(1, block_data.clone())], + vec![(1, metadata_for(&block_data, 1))], 1, #[cfg(feature = "telemetry")] None, diff --git a/tests/indexeddb_retry_tests.rs b/tests/indexeddb_retry_tests.rs index db8dae1..66f56f6 100644 --- a/tests/indexeddb_retry_tests.rs +++ b/tests/indexeddb_retry_tests.rs @@ -9,12 +9,23 @@ #![cfg(target_arch = "wasm32")] use absurder_sql::storage::wasm_indexeddb::persist_to_indexeddb_event_based; +use absurder_sql::storage::{BlockMetadataPersist, ChecksumAlgorithm, ChecksumManager}; use absurder_sql::types::DatabaseError; use absurder_sql::{Database, DatabaseConfig}; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); +fn metadata_for(data: &[u8], version: u32) -> BlockMetadataPersist { + let algo = ChecksumAlgorithm::FastHash; + BlockMetadataPersist { + checksum: ChecksumManager::compute_checksum_with(data, algo), + last_modified_ms: 0, + version, + algo, + } +} + /// Test that quota exceeded errors are detected #[wasm_bindgen_test] async fn test_quota_exceeded_error_detection() { @@ -235,8 +246,9 @@ async fn test_persist_with_retry_on_failure() { use js_sys::Date; let db_name = "test_persist_with_retry"; - let blocks = vec![(1u64, vec![1u8, 2, 3, 4])]; - let metadata = vec![(1u64, 100u64)]; + let block_data = vec![1u8, 2, 3, 4]; + let metadata = vec![(1u64, metadata_for(&block_data, 100))]; + let blocks = vec![(1u64, block_data)]; let start = Date::now();