Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion absurder-sql-mobile/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion pkg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
81 changes: 55 additions & 26 deletions src/storage/wasm_indexeddb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1139,7 +1144,7 @@ async fn persist_to_indexeddb_event_based_internal(
db_name: &str,
blocks: Vec<(u64, Vec<u8>)>,
metadata: Vec<(u64, BlockMetadataPersist)>,
commit_marker: u64,
commit_marker: Option<u64>,
) -> Result<(), DatabaseError> {
use wasm_bindgen::JsCast;
use wasm_bindgen::closure::Closure;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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()
Expand Down Expand Up @@ -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(
Expand All @@ -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
})?;
}
}
}
Expand All @@ -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(())
}

Expand All @@ -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()
);

Expand Down
23 changes: 18 additions & 5 deletions tests/indexeddb_error_handling_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 14 additions & 2 deletions tests/indexeddb_retry_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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();

Expand Down
Loading