use std::cmp::Ordering;
use std::collections::BTreeMap;
use hyper_util::rt::TokioIo;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use tonic::Request;
use tonic::codegen::async_trait;
use tonic::metadata::MetadataValue;
use tonic::service::Interceptor;
use tonic::service::interceptor::InterceptedService;
use tonic::transport::{Channel, ClientTlsConfig, Endpoint, Uri};
use tower::service_fn;
use crate::env::{
ENV_HOST_SERVICE_SOCKET, ENV_HOST_SERVICE_TOKEN, HOST_SERVICE_BINDING_HEADER,
host_service_configured,
};
use crate::generated::v1::{self as pb, indexed_db_client::IndexedDbClient};
type IndexedDbTransport = InterceptedService<Channel, RelayTokenInterceptor>;
const INDEXEDDB_RELAY_TOKEN_HEADER: &str = "x-gestalt-host-service-relay-token";
const CURSOR_CHANNEL_BUFFER: usize = 1;
const TRANSACTION_CHANNEL_BUFFER: usize = 1;
#[derive(Debug, thiserror::Error)]
pub enum IndexedDBError {
#[error("not found")]
NotFound,
#[error("already exists")]
AlreadyExists,
#[error("cursor is keys-only; value not available")]
KeysOnly,
#[error("{0}")]
InvalidArgument(String),
#[error("{0}")]
Transaction(String),
#[error("{0}")]
Transport(#[from] tonic::transport::Error),
#[error("{0}")]
Status(#[from] tonic::Status),
#[error("{0}")]
Env(String),
}
pub type Record = BTreeMap<String, serde_json::Value>;
#[derive(Debug, Clone, PartialEq)]
pub enum Key {
Int(i64),
Float(f64),
Str(String),
Date(std::time::SystemTime),
Bytes(Vec<u8>),
Array(Vec<Key>),
}
impl From<&str> for Key {
fn from(value: &str) -> Self {
Self::Str(value.to_string())
}
}
impl From<String> for Key {
fn from(value: String) -> Self {
Self::Str(value)
}
}
impl From<i64> for Key {
fn from(value: i64) -> Self {
Self::Int(value)
}
}
impl From<i32> for Key {
fn from(value: i32) -> Self {
Self::Int(i64::from(value))
}
}
impl From<u64> for Key {
fn from(value: u64) -> Self {
Self::Int(value as i64)
}
}
impl From<f64> for Key {
fn from(value: f64) -> Self {
Self::Float(value)
}
}
impl From<bool> for Key {
fn from(value: bool) -> Self {
Self::Int(i64::from(value))
}
}
impl From<std::time::SystemTime> for Key {
fn from(value: std::time::SystemTime) -> Self {
Self::Date(value)
}
}
impl From<Vec<u8>> for Key {
fn from(value: Vec<u8>) -> Self {
Self::Bytes(value)
}
}
impl From<&[u8]> for Key {
fn from(value: &[u8]) -> Self {
Self::Bytes(value.to_vec())
}
}
impl<T: Into<Key>> From<Vec<T>> for Key {
fn from(values: Vec<T>) -> Self {
if values.len() == 1 {
values.into_iter().next().expect("one value").into()
} else {
Self::Array(values.into_iter().map(Into::into).collect())
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct KeyRange {
pub lower: Option<Key>,
pub upper: Option<Key>,
pub lower_open: bool,
pub upper_open: bool,
}
impl KeyRange {
pub fn only(key: impl Into<Key>) -> Self {
let key = key.into();
Self {
lower: Some(key.clone()),
upper: Some(key),
lower_open: false,
upper_open: false,
}
}
pub fn bound(
lower: impl Into<Key>,
upper: impl Into<Key>,
lower_open: bool,
upper_open: bool,
) -> Self {
Self {
lower: Some(lower.into()),
upper: Some(upper.into()),
lower_open,
upper_open,
}
}
pub fn lower_bound(key: impl Into<Key>, open: bool) -> Self {
Self {
lower: Some(key.into()),
upper: None,
lower_open: open,
upper_open: false,
}
}
pub fn upper_bound(key: impl Into<Key>, open: bool) -> Self {
Self {
lower: None,
upper: Some(key.into()),
lower_open: false,
upper_open: open,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Query {
All,
Key(Key),
Range(KeyRange),
}
impl Query {
pub fn all() -> Self {
Self::All
}
fn to_proto(&self) -> Option<pb::IndexedDbQuery> {
use crate::indexeddb_query_codec::{QueryKind, query_to_proto};
query_to_proto(match self {
Self::All => QueryKind::All,
Self::Key(key) => QueryKind::Key(key),
Self::Range(range) => QueryKind::Range {
lower: range.lower.as_ref(),
upper: range.upper.as_ref(),
lower_open: range.lower_open,
upper_open: range.upper_open,
},
})
}
}
impl From<KeyRange> for Query {
fn from(range: KeyRange) -> Self {
Self::Range(range)
}
}
impl<T: Into<Key>> From<T> for Query {
fn from(value: T) -> Self {
Self::Key(value.into())
}
}
impl From<Option<KeyRange>> for Query {
fn from(range: Option<KeyRange>) -> Self {
match range {
None => Self::All,
Some(range) => Self::Range(range),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IndexSchema {
pub name: String,
pub key_path: Vec<String>,
pub unique: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObjectStoreSchema {
pub indexes: Vec<IndexSchema>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorDirection {
Next,
NextUnique,
Prev,
PrevUnique,
}
impl CursorDirection {
fn to_proto(self) -> i32 {
match self {
Self::Next => pb::CursorDirection::CursorNext as i32,
Self::NextUnique => pb::CursorDirection::CursorNextUnique as i32,
Self::Prev => pb::CursorDirection::CursorPrev as i32,
Self::PrevUnique => pb::CursorDirection::CursorPrevUnique as i32,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransactionMode {
Readonly,
Readwrite,
}
impl TransactionMode {
fn to_proto(self) -> i32 {
match self {
Self::Readonly => pb::TransactionMode::TransactionReadonly as i32,
Self::Readwrite => pb::TransactionMode::TransactionReadwrite as i32,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TransactionDurabilityHint {
#[default]
Default,
Strict,
Relaxed,
}
impl TransactionDurabilityHint {
fn to_proto(self) -> i32 {
match self {
Self::Default => pb::TransactionDurabilityHint::TransactionDurabilityDefault as i32,
Self::Strict => pb::TransactionDurabilityHint::TransactionDurabilityStrict as i32,
Self::Relaxed => pb::TransactionDurabilityHint::TransactionDurabilityRelaxed as i32,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TransactionOptions {
pub durability_hint: TransactionDurabilityHint,
}
#[async_trait]
pub trait IndexedDBApi: Send {
type ObjectStore: ObjectStoreApi;
type Transaction: TransactionApi;
async fn create_object_store(
&mut self,
name: &str,
schema: ObjectStoreSchema,
) -> Result<Self::ObjectStore, IndexedDBError>;
async fn delete_object_store(&mut self, name: &str) -> Result<(), IndexedDBError>;
fn object_store(&self, name: &str) -> Self::ObjectStore;
async fn transaction(
&self,
stores: &[&str],
mode: TransactionMode,
options: TransactionOptions,
) -> Result<Self::Transaction, IndexedDBError>;
}
#[async_trait]
pub trait ObjectStoreApi: Send {
type Index: IndexApi;
type Cursor: CursorApi;
async fn get(&mut self, id: &str) -> Result<Record, IndexedDBError>;
async fn get_key(&mut self, id: &str) -> Result<String, IndexedDBError>;
async fn add(&mut self, record: Record) -> Result<(), IndexedDBError>;
async fn put(&mut self, record: Record) -> Result<(), IndexedDBError>;
async fn delete(&mut self, id: &str) -> Result<(), IndexedDBError>;
async fn clear(&mut self) -> Result<(), IndexedDBError>;
async fn get_all(&mut self, query: Query) -> Result<Vec<Record>, IndexedDBError>;
async fn get_all_keys(&mut self, query: Query) -> Result<Vec<String>, IndexedDBError>;
async fn count(&mut self, query: Query) -> Result<i64, IndexedDBError>;
async fn delete_range(&mut self, query: Query) -> Result<i64, IndexedDBError>;
fn index(&self, name: &str) -> Self::Index;
async fn open_cursor(
&mut self,
query: Query,
direction: CursorDirection,
) -> Result<Self::Cursor, IndexedDBError>;
async fn open_key_cursor(
&mut self,
query: Query,
direction: CursorDirection,
) -> Result<Self::Cursor, IndexedDBError>;
}
#[async_trait]
pub trait IndexApi: Send {
type Cursor: CursorApi;
async fn get(&mut self, query: Query) -> Result<Record, IndexedDBError>;
async fn get_key(&mut self, query: Query) -> Result<String, IndexedDBError>;
async fn get_all(&mut self, query: Query) -> Result<Vec<Record>, IndexedDBError>;
async fn get_all_keys(&mut self, query: Query) -> Result<Vec<String>, IndexedDBError>;
async fn count(&mut self, query: Query) -> Result<i64, IndexedDBError>;
async fn delete(&mut self, query: Query) -> Result<i64, IndexedDBError>;
async fn open_cursor(
&mut self,
query: Query,
direction: CursorDirection,
) -> Result<Self::Cursor, IndexedDBError>;
async fn open_key_cursor(
&mut self,
query: Query,
direction: CursorDirection,
) -> Result<Self::Cursor, IndexedDBError>;
}
#[async_trait]
pub trait TransactionApi: Send {
type ObjectStore<'a>: TransactionObjectStoreApi + 'a
where
Self: 'a;
fn object_store<'a>(&'a mut self, name: &str) -> Self::ObjectStore<'a>;
async fn commit(&mut self) -> Result<(), IndexedDBError>;
async fn abort(&mut self, reason: &str) -> Result<(), IndexedDBError>;
}
#[async_trait]
pub trait TransactionObjectStoreApi: Send {
type Index<'a>: TransactionIndexApi + 'a
where
Self: 'a;
async fn get(&mut self, id: &str) -> Result<Record, IndexedDBError>;
async fn get_key(&mut self, id: &str) -> Result<String, IndexedDBError>;
async fn add(&mut self, record: Record) -> Result<(), IndexedDBError>;
async fn put(&mut self, record: Record) -> Result<(), IndexedDBError>;
async fn delete(&mut self, id: &str) -> Result<(), IndexedDBError>;
async fn clear(&mut self) -> Result<(), IndexedDBError>;
async fn get_all(&mut self, query: Query) -> Result<Vec<Record>, IndexedDBError>;
async fn get_all_keys(&mut self, query: Query) -> Result<Vec<String>, IndexedDBError>;
async fn count(&mut self, query: Query) -> Result<i64, IndexedDBError>;
async fn delete_range(&mut self, query: Query) -> Result<i64, IndexedDBError>;
fn index<'a>(&'a mut self, name: &str) -> Self::Index<'a>;
}
#[async_trait]
pub trait TransactionIndexApi: Send {
async fn get(&mut self, query: Query) -> Result<Record, IndexedDBError>;
async fn get_key(&mut self, query: Query) -> Result<String, IndexedDBError>;
async fn get_all(&mut self, query: Query) -> Result<Vec<Record>, IndexedDBError>;
async fn get_all_keys(&mut self, query: Query) -> Result<Vec<String>, IndexedDBError>;
async fn count(&mut self, query: Query) -> Result<i64, IndexedDBError>;
async fn delete(&mut self, query: Query) -> Result<i64, IndexedDBError>;
}
#[async_trait]
pub trait CursorApi: Send {
fn key(&self) -> Option<Key>;
fn primary_key(&self) -> &str;
fn value(&self) -> Result<Record, IndexedDBError>;
async fn continue_next(&mut self) -> Result<bool, IndexedDBError>;
async fn continue_to_key(&mut self, key: impl Into<Key> + Send)
-> Result<bool, IndexedDBError>;
async fn advance(&mut self, count: i32) -> Result<bool, IndexedDBError>;
async fn delete(&mut self) -> Result<(), IndexedDBError>;
async fn update(&mut self, value: Record) -> Result<(), IndexedDBError>;
async fn close(self) -> Result<(), IndexedDBError>
where
Self: Sized;
}
#[derive(Debug, Clone)]
pub struct IndexedDBOpenCursorRequest {
pub store: String,
pub range: Option<KeyRange>,
pub direction: CursorDirection,
pub keys_only: bool,
pub index: String,
pub values: Vec<serde_json::Value>,
}
impl Default for IndexedDBOpenCursorRequest {
fn default() -> Self {
Self {
store: String::new(),
range: None,
direction: CursorDirection::Next,
keys_only: false,
index: String::new(),
values: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IndexedDBCursorSnapshotEntry {
pub key: Key,
pub primary_key: String,
pub primary_key_value: Key,
pub record: Record,
}
#[derive(Debug, Clone)]
pub struct IndexedDBCursorSnapshot {
pub index_cursor: bool,
pub keys_only: bool,
pub reverse: bool,
pub unique: bool,
pub entries: Vec<IndexedDBCursorSnapshotEntry>,
pub pos: isize,
}
impl IndexedDBCursorSnapshot {
pub fn new(req: &IndexedDBOpenCursorRequest) -> Self {
Self {
index_cursor: !req.index.is_empty(),
keys_only: req.keys_only,
reverse: matches!(
req.direction,
CursorDirection::Prev | CursorDirection::PrevUnique
),
unique: matches!(
req.direction,
CursorDirection::NextUnique | CursorDirection::PrevUnique
),
entries: Vec::new(),
pos: -1,
}
}
pub fn load(
&mut self,
mut entries: Vec<IndexedDBCursorSnapshotEntry>,
range: Option<&KeyRange>,
) -> Result<(), IndexedDBError> {
entries.sort_by(|left, right| {
let mut cmp = compare_indexeddb_values(&left.key, &right.key);
if cmp == Ordering::Equal {
cmp = compare_indexeddb_values(&left.primary_key_value, &right.primary_key_value);
}
if self.reverse { cmp.reverse() } else { cmp }
});
self.entries = self.apply_range(entries, range)?;
self.pos = -1;
Ok(())
}
pub fn apply_range(
&self,
entries: Vec<IndexedDBCursorSnapshotEntry>,
range: Option<&KeyRange>,
) -> Result<Vec<IndexedDBCursorSnapshotEntry>, IndexedDBError> {
let Some(range) = range else {
return Ok(entries);
};
let (lower, upper) = indexeddb_range_bounds(Some(range), self.index_cursor);
let mut filtered = Vec::with_capacity(entries.len());
for entry in entries {
let key = normalize_indexeddb_bound(&entry.key, self.index_cursor);
if let Some(lower) = &lower {
let cmp = compare_indexeddb_values(&key, lower);
if range.lower_open && cmp != Ordering::Greater {
continue;
}
if !range.lower_open && cmp == Ordering::Less {
continue;
}
}
if let Some(upper) = &upper {
let cmp = compare_indexeddb_values(&key, upper);
if range.upper_open && cmp != Ordering::Less {
continue;
}
if !range.upper_open && cmp == Ordering::Greater {
continue;
}
}
filtered.push(entry);
}
Ok(filtered)
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<Option<&IndexedDBCursorSnapshotEntry>, IndexedDBError> {
if self.unique
&& self.index_cursor
&& self.pos >= 0
&& (self.pos as usize) < self.entries.len()
{
let previous = self.entries[self.pos as usize].key.clone();
self.pos += 1;
while (self.pos as usize) < self.entries.len() {
if compare_indexeddb_values(&self.entries[self.pos as usize].key, &previous)
!= Ordering::Equal
{
return Ok(Some(self.current()?));
}
self.pos += 1;
}
return Ok(None);
}
self.pos += 1;
if (self.pos as usize) >= self.entries.len() {
return Ok(None);
}
Ok(Some(self.current()?))
}
pub fn continue_to_key(
&mut self,
target: &Key,
) -> Result<Option<&IndexedDBCursorSnapshotEntry>, IndexedDBError> {
let previous = if self.unique
&& self.index_cursor
&& self.pos >= 0
&& (self.pos as usize) < self.entries.len()
{
Some(self.entries[self.pos as usize].key.clone())
} else {
None
};
self.pos += 1;
while (self.pos as usize) < self.entries.len() {
let current = &self.entries[self.pos as usize].key;
if let Some(previous) = &previous {
if self.unique
&& self.index_cursor
&& compare_indexeddb_values(current, previous) == Ordering::Equal
{
self.pos += 1;
continue;
}
}
let cmp = compare_indexeddb_values(current, target);
if self.reverse {
if cmp != Ordering::Greater {
return Ok(Some(self.current()?));
}
} else if cmp != Ordering::Less {
return Ok(Some(self.current()?));
}
self.pos += 1;
}
Ok(None)
}
pub fn advance(
&mut self,
count: i32,
) -> Result<Option<&IndexedDBCursorSnapshotEntry>, IndexedDBError> {
if count <= 0 {
return Err(IndexedDBError::InvalidArgument(
"advance count must be positive".to_string(),
));
}
for i in 0..count {
if self.next()?.is_none() {
return Ok(None);
}
if i == count - 1 {
return Ok(Some(self.current()?));
}
}
Ok(None)
}
pub fn current(&self) -> Result<&IndexedDBCursorSnapshotEntry, IndexedDBError> {
if self.pos < 0 || (self.pos as usize) >= self.entries.len() {
return Err(IndexedDBError::NotFound);
}
Ok(&self.entries[self.pos as usize])
}
}
pub fn new_indexeddb_cursor_snapshot(req: &IndexedDBOpenCursorRequest) -> IndexedDBCursorSnapshot {
IndexedDBCursorSnapshot::new(req)
}
pub fn indexeddb_range_bounds(
range: Option<&KeyRange>,
index_cursor: bool,
) -> (Option<Key>, Option<Key>) {
let Some(range) = range else {
return (None, None);
};
let lower = range
.lower
.as_ref()
.map(|value| normalize_indexeddb_bound(value, index_cursor));
let upper = range
.upper
.as_ref()
.map(|value| normalize_indexeddb_bound(value, index_cursor));
(lower, upper)
}
pub fn compare_indexeddb_values(left: &Key, right: &Key) -> Ordering {
compare_keys(left, right)
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
enum KeyKind {
Number,
Date,
String,
Binary,
Array,
}
fn key_kind(key: &Key) -> KeyKind {
match key {
Key::Int(_) | Key::Float(_) => KeyKind::Number,
Key::Date(_) => KeyKind::Date,
Key::Str(_) => KeyKind::String,
Key::Bytes(_) => KeyKind::Binary,
Key::Array(_) => KeyKind::Array,
}
}
pub fn compare_keys(left: &Key, right: &Key) -> Ordering {
let left_kind = key_kind(left);
let right_kind = key_kind(right);
if left_kind != right_kind {
return left_kind.cmp(&right_kind);
}
match (left, right) {
(Key::Int(left), Key::Int(right)) => left.cmp(right),
(Key::Float(left), Key::Float(right)) => compare_float_keys(*left, *right),
(Key::Int(left), Key::Float(right)) => compare_int_float_keys(*left, *right),
(Key::Float(left), Key::Int(right)) => compare_int_float_keys(*right, *left).reverse(),
(Key::Date(left), Key::Date(right)) => left.cmp(right),
(Key::Str(left), Key::Str(right)) => compare_utf16_strings(left, right),
(Key::Bytes(left), Key::Bytes(right)) => left.cmp(right),
(Key::Array(left), Key::Array(right)) => {
for (left_value, right_value) in left.iter().zip(right.iter()) {
let cmp = compare_keys(left_value, right_value);
if cmp != Ordering::Equal {
return cmp;
}
}
left.len().cmp(&right.len())
}
_ => Ordering::Equal,
}
}
pub fn key_in_range(key: &Key, range: &KeyRange) -> bool {
if let Some(lower) = &range.lower {
let cmp = compare_keys(key, lower);
if range.lower_open {
if cmp != Ordering::Greater {
return false;
}
} else if cmp == Ordering::Less {
return false;
}
}
if let Some(upper) = &range.upper {
let cmp = compare_keys(key, upper);
if range.upper_open {
if cmp != Ordering::Less {
return false;
}
} else if cmp == Ordering::Greater {
return false;
}
}
true
}
pub fn match_query(key: &Key, query: &Query) -> bool {
match query {
Query::All => true,
Query::Key(target) => compare_keys(key, target) == Ordering::Equal,
Query::Range(range) => key_in_range(key, range),
}
}
fn normalize_indexeddb_bound(value: &Key, index_cursor: bool) -> Key {
if !index_cursor {
return value.clone();
}
if matches!(value, Key::Array(_)) {
return value.clone();
}
Key::Array(vec![value.clone()])
}
fn compare_utf16_strings(left: &str, right: &str) -> Ordering {
let left: Vec<u16> = left.encode_utf16().collect();
let right: Vec<u16> = right.encode_utf16().collect();
for (left_unit, right_unit) in left.iter().zip(right.iter()) {
match left_unit.cmp(right_unit) {
Ordering::Equal => {}
other => return other,
}
}
left.len().cmp(&right.len())
}
fn compare_int_float_keys(left: i64, right: f64) -> Ordering {
if right.is_nan() {
return Ordering::Equal;
}
match (left as f64).partial_cmp(&right) {
Some(ordering) => ordering,
None => left.cmp(&(right as i64)),
}
}
fn compare_float_keys(left: f64, right: f64) -> Ordering {
left.partial_cmp(&right).unwrap_or(Ordering::Equal)
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, UNIX_EPOCH};
fn entry(key: Key, primary_key: &str, primary_key_value: Key) -> IndexedDBCursorSnapshotEntry {
IndexedDBCursorSnapshotEntry {
key,
primary_key: primary_key.to_string(),
primary_key_value,
record: Record::new(),
}
}
#[test]
fn cursor_snapshot_sorts_ranges_and_skips_duplicate_unique_index_keys() {
let mut snapshot = new_indexeddb_cursor_snapshot(&IndexedDBOpenCursorRequest {
direction: CursorDirection::NextUnique,
index: "by_status".to_string(),
..Default::default()
});
snapshot
.load(
vec![
entry(
Key::Array(vec![Key::from("todo")]),
"issue-2",
Key::from("issue-2"),
),
entry(
Key::Array(vec![Key::from("done")]),
"issue-3",
Key::from("issue-3"),
),
entry(
Key::Array(vec![Key::from("todo")]),
"issue-1",
Key::from("issue-1"),
),
],
Some(&KeyRange {
lower: Some(Key::Array(vec![Key::from("done")])),
upper: Some(Key::Array(vec![Key::from("todo")])),
lower_open: false,
upper_open: false,
}),
)
.expect("load");
assert_eq!(
snapshot.next().expect("first").unwrap().primary_key,
"issue-3"
);
assert_eq!(
snapshot.next().expect("second").unwrap().primary_key,
"issue-1"
);
assert!(snapshot.next().expect("exhausted").is_none());
}
#[test]
fn cursor_snapshot_advance_moves_exactly_count_entries_from_current_position() {
let mut snapshot = new_indexeddb_cursor_snapshot(&IndexedDBOpenCursorRequest::default());
snapshot
.load(
vec![
entry(Key::from("a"), "a", Key::from("a")),
entry(Key::from("b"), "b", Key::from("b")),
entry(Key::from("c"), "c", Key::from("c")),
],
None,
)
.expect("load");
assert_eq!(snapshot.next().expect("first").unwrap().primary_key, "a");
assert_eq!(
snapshot.advance(1).expect("second").unwrap().primary_key,
"b"
);
assert_eq!(
snapshot.advance(1).expect("third").unwrap().primary_key,
"c"
);
}
#[test]
fn cursor_snapshot_index_range_accepts_scalar_entry_keys() {
let mut snapshot = new_indexeddb_cursor_snapshot(&IndexedDBOpenCursorRequest {
index: "by_status".to_string(),
..Default::default()
});
snapshot
.load(
vec![
entry(Key::from("done"), "issue-2", Key::from("issue-2")),
entry(Key::from("active"), "issue-1", Key::from("issue-1")),
],
Some(&KeyRange {
lower: Some(Key::from("active")),
upper: Some(Key::from("active")),
lower_open: false,
upper_open: false,
}),
)
.expect("load");
let first = snapshot.next().expect("first").unwrap();
assert_eq!(first.primary_key, "issue-1");
assert_eq!(first.key, Key::from("active"));
assert!(snapshot.next().expect("exhausted").is_none());
}
#[test]
fn range_bounds_normalize_scalar_index_bounds() {
let (lower, upper) = indexeddb_range_bounds(
Some(&KeyRange {
lower: Some(Key::from("active")),
upper: Some(Key::Array(vec![Key::from("done")])),
lower_open: false,
upper_open: false,
}),
true,
);
assert_eq!(lower, Some(Key::Array(vec![Key::from("active")])));
assert_eq!(upper, Some(Key::Array(vec![Key::from("done")])));
}
#[test]
fn compare_values_orders_composite_keys() {
assert_eq!(
compare_indexeddb_values(
&Key::Array(vec![Key::from("active"), Key::Int(1)]),
&Key::Array(vec![Key::from("active"), Key::Int(2)])
),
Ordering::Less
);
assert_eq!(
compare_indexeddb_values(
&Key::Array(vec![Key::from("active"), Key::Int(2)]),
&Key::Array(vec![Key::from("active"), Key::Int(2)])
),
Ordering::Equal
);
assert_eq!(
compare_indexeddb_values(
&Key::Array(vec![Key::from("active"), Key::Int(3)]),
&Key::Array(vec![Key::from("active"), Key::Int(2)])
),
Ordering::Greater
);
}
#[test]
fn compare_values_orders_large_integer_keys_exactly() {
assert_eq!(
compare_indexeddb_values(
&Key::Int(9_007_199_254_740_993),
&Key::Int(9_007_199_254_740_992)
),
Ordering::Greater
);
assert_eq!(
compare_indexeddb_values(&Key::Int(i64::MAX), &Key::Float(u64::MAX as f64)),
Ordering::Less
);
}
#[test]
fn compare_keys_orders_date_before_string_and_bytes_after_string() {
let date = Key::Date(UNIX_EPOCH + Duration::from_secs(1_700_000_000));
assert_eq!(compare_keys(&Key::Int(1), &date), Ordering::Less);
assert_eq!(compare_keys(&date, &Key::from("a")), Ordering::Less);
assert_eq!(
compare_keys(&Key::from("a"), &Key::Bytes(vec![0x00])),
Ordering::Less
);
}
#[test]
fn date_and_bytes_keys_match_exact_queries() {
let date = Key::Date(UNIX_EPOCH + Duration::from_secs(1_700_000_000));
let bytes = Key::Bytes(vec![0x01, 0x02]);
assert!(match_query(&date, &Query::Key(date.clone())));
assert!(match_query(&bytes, &Query::Key(bytes.clone())));
assert!(key_in_range(
&date,
&KeyRange {
lower: Some(date.clone()),
upper: Some(date.clone()),
lower_open: false,
upper_open: false,
}
));
}
}
pub struct Cursor {
tx: mpsc::Sender<pb::CursorClientMessage>,
stream: tonic::Streaming<pb::CursorResponse>,
keys_only: bool,
entry: Option<pb::CursorEntry>,
done: bool,
}
impl Cursor {
pub fn key(&self) -> Option<Key> {
let entry = self.entry.as_ref()?;
entry
.key
.as_ref()
.map(crate::indexeddb_query_codec::key_from_wire_key_value)
}
pub fn primary_key(&self) -> &str {
self.entry
.as_ref()
.map(|e| e.primary_key.as_str())
.unwrap_or("")
}
pub fn value(&self) -> Result<Record, IndexedDBError> {
if self.keys_only {
return Err(IndexedDBError::KeysOnly);
}
let entry = self.entry.as_ref().ok_or(IndexedDBError::NotFound)?;
Ok(entry
.record
.as_ref()
.map(pb_record_to_record)
.unwrap_or_default())
}
pub async fn continue_next(&mut self) -> Result<bool, IndexedDBError> {
let cmd = pb::cursor_command::Command::Next(true);
self.send_and_recv(cmd).await
}
pub async fn continue_to_key(&mut self, key: impl Into<Key>) -> Result<bool, IndexedDBError> {
let key = key.into();
let cmd = pb::cursor_command::Command::ContinueToKey(pb::CursorKeyTarget {
key: Some(crate::indexeddb_query_codec::cursor_key_to_proto(&key)),
});
self.send_and_recv(cmd).await
}
pub async fn advance(&mut self, count: i32) -> Result<bool, IndexedDBError> {
let cmd = pb::cursor_command::Command::Advance(count);
self.send_and_recv(cmd).await
}
pub async fn delete(&mut self) -> Result<(), IndexedDBError> {
if self.done {
return Err(IndexedDBError::NotFound);
}
let cmd = pb::cursor_command::Command::Delete(true);
self.send_mutation(cmd).await
}
pub async fn update(&mut self, value: Record) -> Result<(), IndexedDBError> {
if self.done {
return Err(IndexedDBError::NotFound);
}
let cmd = pb::cursor_command::Command::Update(record_to_pb_record(value));
self.send_mutation(cmd).await
}
pub async fn close(self) -> Result<(), IndexedDBError> {
let msg = pb::CursorClientMessage {
msg: Some(pb::cursor_client_message::Msg::Command(pb::CursorCommand {
command: Some(pb::cursor_command::Command::Close(true)),
})),
};
self.tx
.send(msg)
.await
.map_err(|e| IndexedDBError::Status(tonic::Status::internal(e.to_string())))?;
Ok(())
}
async fn send_mutation(
&mut self,
cmd: pb::cursor_command::Command,
) -> Result<(), IndexedDBError> {
let msg = pb::CursorClientMessage {
msg: Some(pb::cursor_client_message::Msg::Command(pb::CursorCommand {
command: Some(cmd),
})),
};
self.tx
.send(msg)
.await
.map_err(|e| IndexedDBError::Status(tonic::Status::internal(e.to_string())))?;
let resp = self
.stream
.message()
.await
.map_err(map_status)?
.ok_or_else(|| {
IndexedDBError::Status(tonic::Status::internal(
"cursor stream ended during mutation",
))
})?;
match resp.result {
Some(pb::cursor_response::Result::Entry(entry)) => {
self.entry = Some(entry);
}
Some(pb::cursor_response::Result::Done(_)) => {}
None => {
return Err(IndexedDBError::Status(tonic::Status::internal(
"unexpected cursor mutation ack",
)));
}
}
Ok(())
}
async fn send_and_recv(
&mut self,
cmd: pb::cursor_command::Command,
) -> Result<bool, IndexedDBError> {
if self.done {
return Ok(false);
}
let msg = pb::CursorClientMessage {
msg: Some(pb::cursor_client_message::Msg::Command(pb::CursorCommand {
command: Some(cmd),
})),
};
self.tx
.send(msg)
.await
.map_err(|e| IndexedDBError::Status(tonic::Status::internal(e.to_string())))?;
let resp = self
.stream
.message()
.await
.map_err(map_status)?
.ok_or_else(|| {
IndexedDBError::Status(tonic::Status::internal("cursor stream ended"))
})?;
match resp.result {
Some(pb::cursor_response::Result::Entry(entry)) => {
self.entry = Some(entry);
self.done = false;
Ok(true)
}
Some(pb::cursor_response::Result::Done(exhausted)) => {
if exhausted {
self.done = true;
}
self.entry = None;
Ok(false)
}
None => {
self.entry = None;
self.done = true;
Ok(false)
}
}
}
}
async fn open_cursor_inner(
client: &mut IndexedDbClient<IndexedDbTransport>,
req: pb::OpenCursorRequest,
) -> Result<Cursor, IndexedDBError> {
let keys_only = req.keys_only;
let (tx, rx) = mpsc::channel::<pb::CursorClientMessage>(CURSOR_CHANNEL_BUFFER);
let open_msg = pb::CursorClientMessage {
msg: Some(pb::cursor_client_message::Msg::Open(req)),
};
tx.send(open_msg)
.await
.map_err(|e| IndexedDBError::Status(tonic::Status::internal(e.to_string())))?;
let receiver_stream = ReceiverStream::new(rx);
let mut stream = client
.open_cursor(receiver_stream)
.await
.map_err(map_status)?
.into_inner();
let ack = stream.message().await.map_err(map_status)?.ok_or_else(|| {
IndexedDBError::Status(tonic::Status::internal("cursor stream ended during open"))
})?;
match ack.result {
Some(pb::cursor_response::Result::Done(false)) => {}
Some(pb::cursor_response::Result::Done(true)) => {
return Err(IndexedDBError::Status(tonic::Status::internal(
"unexpected exhausted cursor open ack",
)));
}
_ => {
return Err(IndexedDBError::Status(tonic::Status::internal(
"unexpected cursor open ack",
)));
}
}
Ok(Cursor {
tx,
stream,
keys_only,
entry: None,
done: false,
})
}
pub struct IndexedDB {
client: IndexedDbClient<IndexedDbTransport>,
}
impl IndexedDB {
pub async fn connect() -> Result<Self, IndexedDBError> {
Self::connect_named("").await
}
pub async fn connect_named(name: &str) -> Result<Self, IndexedDBError> {
host_service_configured("indexeddb").map_err(IndexedDBError::Env)?;
let target = std::env::var(ENV_HOST_SERVICE_SOCKET)
.map_err(|_| IndexedDBError::Env(format!("{ENV_HOST_SERVICE_SOCKET} is not set")))?;
let token = std::env::var(ENV_HOST_SERVICE_TOKEN).unwrap_or_default();
let channel = match parse_indexeddb_target(&target)? {
IndexedDBTarget::Unix(path) => {
Endpoint::try_from("http://[::]:50051")?
.connect_with_connector(service_fn(move |_: Uri| {
let path = path.clone();
async move {
tokio::net::UnixStream::connect(path)
.await
.map(TokioIo::new)
}
}))
.await?
}
IndexedDBTarget::Tcp(address) => {
Endpoint::from_shared(format!("http://{address}"))?
.connect()
.await?
}
IndexedDBTarget::Tls(address) => {
Endpoint::from_shared(format!("https://{address}"))?
.tls_config(ClientTlsConfig::new().with_native_roots())?
.connect()
.await?
}
};
let client = IndexedDbClient::with_interceptor(
channel,
relay_token_interceptor(token.trim(), name)?,
);
Ok(Self { client })
}
pub async fn create_object_store(
&mut self,
name: &str,
schema: ObjectStoreSchema,
) -> Result<ObjectStore, IndexedDBError> {
let indexes = schema
.indexes
.into_iter()
.map(|idx| pb::IndexSchema {
name: idx.name,
key_path: idx.key_path,
unique: idx.unique,
})
.collect();
self.client
.create_object_store(pb::CreateObjectStoreRequest {
name: name.to_string(),
schema: Some(pb::ObjectStoreSchema {
indexes,
columns: vec![],
}),
})
.await
.map_err(map_status)?;
Ok(self.object_store(name))
}
pub async fn delete_object_store(&mut self, name: &str) -> Result<(), IndexedDBError> {
self.client
.delete_object_store(pb::DeleteObjectStoreRequest {
name: name.to_string(),
})
.await
.map_err(map_status)?;
Ok(())
}
pub fn object_store(&self, name: &str) -> ObjectStore {
ObjectStore {
client: self.client.clone(),
store: name.to_string(),
}
}
pub async fn transaction(
&self,
stores: &[&str],
mode: TransactionMode,
options: TransactionOptions,
) -> Result<Transaction, IndexedDBError> {
let (tx, rx) = mpsc::channel::<pb::TransactionClientMessage>(TRANSACTION_CHANNEL_BUFFER);
tx.send(pb::TransactionClientMessage {
msg: Some(pb::transaction_client_message::Msg::Begin(
pb::BeginTransactionRequest {
stores: stores.iter().map(|store| store.to_string()).collect(),
mode: mode.to_proto(),
durability_hint: options.durability_hint.to_proto(),
},
)),
})
.await
.map_err(|e| IndexedDBError::Status(tonic::Status::internal(e.to_string())))?;
let receiver_stream = ReceiverStream::new(rx);
let mut client = self.client.clone();
let mut stream = client
.transaction(receiver_stream)
.await
.map_err(map_status)?
.into_inner();
let ack = stream.message().await.map_err(map_status)?.ok_or_else(|| {
IndexedDBError::Transaction("transaction stream ended during begin".to_string())
})?;
match ack.msg {
Some(pb::transaction_server_message::Msg::Begin(_)) => {}
_ => {
return Err(IndexedDBError::Transaction(
"expected transaction begin response".to_string(),
));
}
}
Ok(Transaction {
tx: Some(tx),
stream,
request_id: 0,
closed: false,
})
}
}
pub struct Transaction {
tx: Option<mpsc::Sender<pb::TransactionClientMessage>>,
stream: tonic::Streaming<pb::TransactionServerMessage>,
request_id: u64,
closed: bool,
}
impl Transaction {
pub fn object_store<'a>(&'a mut self, name: &str) -> TransactionObjectStore<'a> {
TransactionObjectStore {
tx: self,
store: name.to_string(),
}
}
pub async fn commit(&mut self) -> Result<(), IndexedDBError> {
self.ensure_open()?;
let tx = self.tx.as_ref().ok_or_else(|| {
IndexedDBError::Transaction("transaction is already finished".to_string())
})?;
tx.send(pb::TransactionClientMessage {
msg: Some(pb::transaction_client_message::Msg::Commit(
pb::TransactionCommitRequest {},
)),
})
.await
.map_err(|e| IndexedDBError::Status(tonic::Status::internal(e.to_string())))?;
self.closed = true;
self.tx.take();
let resp = self
.stream
.message()
.await
.map_err(map_status)?
.ok_or_else(|| {
IndexedDBError::Transaction("transaction stream ended during commit".to_string())
})?;
match resp.msg {
Some(pb::transaction_server_message::Msg::Commit(commit)) => {
map_rpc_status(commit.error)
}
_ => Err(IndexedDBError::Transaction(
"expected transaction commit response".to_string(),
)),
}
}
pub async fn abort(&mut self, reason: &str) -> Result<(), IndexedDBError> {
if self.closed {
return Ok(());
}
let tx = self.tx.as_ref().ok_or_else(|| {
IndexedDBError::Transaction("transaction is already finished".to_string())
})?;
tx.send(pb::TransactionClientMessage {
msg: Some(pb::transaction_client_message::Msg::Abort(
pb::TransactionAbortRequest {
reason: reason.to_string(),
},
)),
})
.await
.map_err(|e| IndexedDBError::Status(tonic::Status::internal(e.to_string())))?;
self.closed = true;
self.tx.take();
let resp = self
.stream
.message()
.await
.map_err(map_status)?
.ok_or_else(|| {
IndexedDBError::Transaction("transaction stream ended during abort".to_string())
})?;
match resp.msg {
Some(pb::transaction_server_message::Msg::Abort(abort)) => map_rpc_status(abort.error),
_ => Err(IndexedDBError::Transaction(
"expected transaction abort response".to_string(),
)),
}
}
async fn send_operation(
&mut self,
operation: pb::transaction_operation::Operation,
) -> Result<pb::TransactionOperationResponse, IndexedDBError> {
self.ensure_open()?;
self.request_id += 1;
let request_id = self.request_id;
let tx = self.tx.as_ref().ok_or_else(|| {
IndexedDBError::Transaction("transaction is already finished".to_string())
})?;
tx.send(pb::TransactionClientMessage {
msg: Some(pb::transaction_client_message::Msg::Operation(
pb::TransactionOperation {
request_id,
operation: Some(operation),
},
)),
})
.await
.map_err(|e| IndexedDBError::Status(tonic::Status::internal(e.to_string())))?;
let resp = self
.stream
.message()
.await
.map_err(map_status)?
.ok_or_else(|| {
IndexedDBError::Transaction("transaction stream ended during operation".to_string())
})?;
let op = match resp.msg {
Some(pb::transaction_server_message::Msg::Operation(op)) => op,
_ => {
self.close_locally();
return Err(IndexedDBError::Transaction(
"expected transaction operation response".to_string(),
));
}
};
if op.request_id != request_id {
self.close_locally();
return Err(IndexedDBError::Transaction(
"transaction response request id mismatch".to_string(),
));
}
if let Err(err) = map_rpc_status(op.error.clone()) {
self.close_locally();
return Err(err);
}
Ok(op)
}
fn ensure_open(&self) -> Result<(), IndexedDBError> {
if self.closed {
return Err(IndexedDBError::Transaction(
"transaction is already finished".to_string(),
));
}
Ok(())
}
fn close_locally(&mut self) {
self.closed = true;
self.tx.take();
}
}
pub struct TransactionObjectStore<'a> {
tx: &'a mut Transaction,
store: String,
}
impl TransactionObjectStore<'_> {
pub async fn get(&mut self, id: &str) -> Result<Record, IndexedDBError> {
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::Get(
pb::ObjectStoreRequest {
store: self.store.clone(),
id: id.to_string(),
},
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Record(record)) => Ok(record
.record
.as_ref()
.map(pb_record_to_record)
.unwrap_or_default()),
_ => Err(unexpected_transaction_result()),
}
}
pub async fn get_key(&mut self, id: &str) -> Result<String, IndexedDBError> {
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::GetKey(
pb::ObjectStoreRequest {
store: self.store.clone(),
id: id.to_string(),
},
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Key(key)) => Ok(key.key),
_ => Err(unexpected_transaction_result()),
}
}
pub async fn add(&mut self, record: Record) -> Result<(), IndexedDBError> {
self.tx
.send_operation(pb::transaction_operation::Operation::Add(
pb::RecordRequest {
store: self.store.clone(),
record: Some(record_to_pb_record(record)),
},
))
.await?;
Ok(())
}
pub async fn put(&mut self, record: Record) -> Result<(), IndexedDBError> {
self.tx
.send_operation(pb::transaction_operation::Operation::Put(
pb::RecordRequest {
store: self.store.clone(),
record: Some(record_to_pb_record(record)),
},
))
.await?;
Ok(())
}
pub async fn delete(&mut self, id: &str) -> Result<(), IndexedDBError> {
self.tx
.send_operation(pb::transaction_operation::Operation::Delete(
pb::ObjectStoreRequest {
store: self.store.clone(),
id: id.to_string(),
},
))
.await?;
Ok(())
}
pub async fn clear(&mut self) -> Result<(), IndexedDBError> {
self.tx
.send_operation(pb::transaction_operation::Operation::Clear(
pb::ObjectStoreNameRequest {
store: self.store.clone(),
},
))
.await?;
Ok(())
}
pub async fn get_all(
&mut self,
query: impl Into<Query>,
count: Option<u32>,
) -> Result<Vec<Record>, IndexedDBError> {
let query = query.into();
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::GetAll(
pb::ObjectStoreRangeRequest {
store: self.store.clone(),
query: query.to_proto(),
count,
},
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Records(records)) => {
Ok(records.records.iter().map(pb_record_to_record).collect())
}
_ => Err(unexpected_transaction_result()),
}
}
pub async fn get_all_keys(
&mut self,
query: impl Into<Query>,
count: Option<u32>,
) -> Result<Vec<String>, IndexedDBError> {
let query = query.into();
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::GetAllKeys(
pb::ObjectStoreRangeRequest {
store: self.store.clone(),
query: query.to_proto(),
count,
},
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Keys(keys)) => Ok(keys.keys),
_ => Err(unexpected_transaction_result()),
}
}
pub async fn count(&mut self, query: impl Into<Query>) -> Result<i64, IndexedDBError> {
let query = query.into();
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::Count(
pb::ObjectStoreRangeRequest {
store: self.store.clone(),
query: query.to_proto(),
count: None,
},
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Count(count)) => Ok(count.count),
_ => Err(unexpected_transaction_result()),
}
}
pub async fn delete_range(&mut self, query: impl Into<Query>) -> Result<i64, IndexedDBError> {
let query = query.into();
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::DeleteRange(
pb::ObjectStoreRangeRequest {
store: self.store.clone(),
query: query.to_proto(),
count: None,
},
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Delete(deleted)) => {
Ok(deleted.deleted)
}
_ => Err(unexpected_transaction_result()),
}
}
pub fn index<'a>(&'a mut self, name: &str) -> TransactionIndex<'a> {
TransactionIndex {
tx: &mut *self.tx,
store: self.store.clone(),
index: name.to_string(),
}
}
}
pub struct TransactionIndex<'a> {
tx: &'a mut Transaction,
store: String,
index: String,
}
impl TransactionIndex<'_> {
pub async fn get(&mut self, query: impl Into<Query>) -> Result<Record, IndexedDBError> {
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::IndexGet(
self.index_request(query.into(), None),
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Record(record)) => Ok(record
.record
.as_ref()
.map(pb_record_to_record)
.unwrap_or_default()),
_ => Err(unexpected_transaction_result()),
}
}
pub async fn get_key(&mut self, query: impl Into<Query>) -> Result<String, IndexedDBError> {
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::IndexGetKey(
self.index_request(query.into(), None),
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Key(key)) => Ok(key.key),
_ => Err(unexpected_transaction_result()),
}
}
pub async fn get_all(
&mut self,
query: impl Into<Query>,
count: Option<u32>,
) -> Result<Vec<Record>, IndexedDBError> {
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::IndexGetAll(
self.index_request(query.into(), count),
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Records(records)) => {
Ok(records.records.iter().map(pb_record_to_record).collect())
}
_ => Err(unexpected_transaction_result()),
}
}
pub async fn get_all_keys(
&mut self,
query: impl Into<Query>,
count: Option<u32>,
) -> Result<Vec<String>, IndexedDBError> {
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::IndexGetAllKeys(
self.index_request(query.into(), count),
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Keys(keys)) => Ok(keys.keys),
_ => Err(unexpected_transaction_result()),
}
}
pub async fn count(&mut self, query: impl Into<Query>) -> Result<i64, IndexedDBError> {
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::IndexCount(
self.index_request(query.into(), None),
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Count(count)) => Ok(count.count),
_ => Err(unexpected_transaction_result()),
}
}
pub async fn delete(&mut self, query: impl Into<Query>) -> Result<i64, IndexedDBError> {
let resp = self
.tx
.send_operation(pb::transaction_operation::Operation::IndexDelete(
self.index_request(query.into(), None),
))
.await?;
match resp.result {
Some(pb::transaction_operation_response::Result::Delete(deleted)) => {
Ok(deleted.deleted)
}
_ => Err(unexpected_transaction_result()),
}
}
fn index_request(&self, query: Query, count: Option<u32>) -> pb::IndexQueryRequest {
pb::IndexQueryRequest {
store: self.store.clone(),
index: self.index.clone(),
query: query.to_proto(),
count,
}
}
}
enum IndexedDBTarget {
Unix(String),
Tcp(String),
Tls(String),
}
fn parse_indexeddb_target(raw_target: &str) -> Result<IndexedDBTarget, IndexedDBError> {
let target = raw_target.trim();
if target.is_empty() {
return Err(IndexedDBError::Env(
"IndexedDB transport target is required".to_string(),
));
}
if let Some(address) = target.strip_prefix("tcp://") {
let address = address.trim();
if address.is_empty() {
return Err(IndexedDBError::Env(format!(
"IndexedDB tcp target {raw_target:?} is missing host:port"
)));
}
return Ok(IndexedDBTarget::Tcp(address.to_string()));
}
if let Some(address) = target.strip_prefix("tls://") {
let address = address.trim();
if address.is_empty() {
return Err(IndexedDBError::Env(format!(
"IndexedDB tls target {raw_target:?} is missing host:port"
)));
}
return Ok(IndexedDBTarget::Tls(address.to_string()));
}
if let Some(path) = target.strip_prefix("unix://") {
let path = path.trim();
if path.is_empty() {
return Err(IndexedDBError::Env(format!(
"IndexedDB unix target {raw_target:?} is missing a socket path"
)));
}
return Ok(IndexedDBTarget::Unix(path.to_string()));
}
if target.contains("://") {
let scheme = target.split("://").next().unwrap_or_default();
return Err(IndexedDBError::Env(format!(
"unsupported IndexedDB target scheme {scheme:?}"
)));
}
Ok(IndexedDBTarget::Unix(target.to_string()))
}
pub struct ObjectStore {
client: IndexedDbClient<IndexedDbTransport>,
store: String,
}
impl ObjectStore {
pub async fn get(&mut self, id: &str) -> Result<Record, IndexedDBError> {
let resp = self
.client
.get(pb::ObjectStoreRequest {
store: self.store.clone(),
id: id.to_string(),
})
.await
.map_err(map_status)?;
Ok(resp
.into_inner()
.record
.as_ref()
.map(pb_record_to_record)
.unwrap_or_default())
}
pub async fn get_key(&mut self, id: &str) -> Result<String, IndexedDBError> {
let resp = self
.client
.get_key(pb::ObjectStoreRequest {
store: self.store.clone(),
id: id.to_string(),
})
.await
.map_err(map_status)?;
Ok(resp.into_inner().key)
}
pub async fn add(&mut self, record: Record) -> Result<(), IndexedDBError> {
self.client
.add(pb::RecordRequest {
store: self.store.clone(),
record: Some(record_to_pb_record(record)),
})
.await
.map_err(map_status)?;
Ok(())
}
pub async fn put(&mut self, record: Record) -> Result<(), IndexedDBError> {
self.client
.put(pb::RecordRequest {
store: self.store.clone(),
record: Some(record_to_pb_record(record)),
})
.await
.map_err(map_status)?;
Ok(())
}
pub async fn delete(&mut self, id: &str) -> Result<(), IndexedDBError> {
self.client
.delete(pb::ObjectStoreRequest {
store: self.store.clone(),
id: id.to_string(),
})
.await
.map_err(map_status)?;
Ok(())
}
pub async fn clear(&mut self) -> Result<(), IndexedDBError> {
self.client
.clear(pb::ObjectStoreNameRequest {
store: self.store.clone(),
})
.await
.map_err(map_status)?;
Ok(())
}
pub async fn get_all(
&mut self,
query: impl Into<Query>,
count: Option<u32>,
) -> Result<Vec<Record>, IndexedDBError> {
let query = query.into();
let resp = self
.client
.get_all(pb::ObjectStoreRangeRequest {
store: self.store.clone(),
query: query.to_proto(),
count,
})
.await
.map_err(map_status)?;
Ok(resp
.into_inner()
.records
.iter()
.map(pb_record_to_record)
.collect())
}
pub async fn get_all_keys(
&mut self,
query: impl Into<Query>,
count: Option<u32>,
) -> Result<Vec<String>, IndexedDBError> {
let query = query.into();
let resp = self
.client
.get_all_keys(pb::ObjectStoreRangeRequest {
store: self.store.clone(),
query: query.to_proto(),
count,
})
.await
.map_err(map_status)?;
Ok(resp.into_inner().keys)
}
pub async fn count(&mut self, query: impl Into<Query>) -> Result<i64, IndexedDBError> {
let query = query.into();
let resp = self
.client
.count(pb::ObjectStoreRangeRequest {
store: self.store.clone(),
query: query.to_proto(),
count: None,
})
.await
.map_err(map_status)?;
Ok(resp.into_inner().count)
}
pub async fn delete_range(&mut self, query: impl Into<Query>) -> Result<i64, IndexedDBError> {
let query = query.into();
let resp = self
.client
.delete_range(pb::ObjectStoreRangeRequest {
store: self.store.clone(),
query: query.to_proto(),
count: None,
})
.await
.map_err(map_status)?;
Ok(resp.into_inner().deleted)
}
pub fn index(&self, name: &str) -> Index {
Index {
client: self.client.clone(),
store: self.store.clone(),
index: name.to_string(),
}
}
pub async fn open_cursor(
&mut self,
query: impl Into<Query>,
direction: CursorDirection,
) -> Result<Cursor, IndexedDBError> {
let query = query.into();
let req = pb::OpenCursorRequest {
store: self.store.clone(),
index: String::new(),
query: query.to_proto(),
direction: direction.to_proto(),
keys_only: false,
};
open_cursor_inner(&mut self.client, req).await
}
pub async fn open_key_cursor(
&mut self,
query: impl Into<Query>,
direction: CursorDirection,
) -> Result<Cursor, IndexedDBError> {
let query = query.into();
let req = pb::OpenCursorRequest {
store: self.store.clone(),
index: String::new(),
query: query.to_proto(),
direction: direction.to_proto(),
keys_only: true,
};
open_cursor_inner(&mut self.client, req).await
}
}
pub struct Index {
client: IndexedDbClient<IndexedDbTransport>,
store: String,
index: String,
}
impl Index {
fn index_request(&self, query: Query, count: Option<u32>) -> pb::IndexQueryRequest {
pb::IndexQueryRequest {
store: self.store.clone(),
index: self.index.clone(),
query: query.to_proto(),
count,
}
}
pub async fn get(&mut self, query: impl Into<Query>) -> Result<Record, IndexedDBError> {
let resp = self
.client
.index_get(self.index_request(query.into(), None))
.await
.map_err(map_status)?;
Ok(resp
.into_inner()
.record
.as_ref()
.map(pb_record_to_record)
.unwrap_or_default())
}
pub async fn get_key(&mut self, query: impl Into<Query>) -> Result<String, IndexedDBError> {
let resp = self
.client
.index_get_key(self.index_request(query.into(), None))
.await
.map_err(map_status)?;
Ok(resp.into_inner().key)
}
pub async fn get_all(
&mut self,
query: impl Into<Query>,
count: Option<u32>,
) -> Result<Vec<Record>, IndexedDBError> {
let resp = self
.client
.index_get_all(self.index_request(query.into(), count))
.await
.map_err(map_status)?;
Ok(resp
.into_inner()
.records
.iter()
.map(pb_record_to_record)
.collect())
}
pub async fn get_all_keys(
&mut self,
query: impl Into<Query>,
count: Option<u32>,
) -> Result<Vec<String>, IndexedDBError> {
let resp = self
.client
.index_get_all_keys(self.index_request(query.into(), count))
.await
.map_err(map_status)?;
Ok(resp.into_inner().keys)
}
pub async fn count(&mut self, query: impl Into<Query>) -> Result<i64, IndexedDBError> {
let resp = self
.client
.index_count(self.index_request(query.into(), None))
.await
.map_err(map_status)?;
Ok(resp.into_inner().count)
}
pub async fn delete(&mut self, query: impl Into<Query>) -> Result<i64, IndexedDBError> {
let resp = self
.client
.index_delete(self.index_request(query.into(), None))
.await
.map_err(map_status)?;
Ok(resp.into_inner().deleted)
}
pub async fn open_cursor(
&mut self,
query: impl Into<Query>,
direction: CursorDirection,
) -> Result<Cursor, IndexedDBError> {
let query = query.into();
let req = pb::OpenCursorRequest {
store: self.store.clone(),
index: self.index.clone(),
query: query.to_proto(),
direction: direction.to_proto(),
keys_only: false,
};
open_cursor_inner(&mut self.client, req).await
}
pub async fn open_key_cursor(
&mut self,
query: impl Into<Query>,
direction: CursorDirection,
) -> Result<Cursor, IndexedDBError> {
let query = query.into();
let req = pb::OpenCursorRequest {
store: self.store.clone(),
index: self.index.clone(),
query: query.to_proto(),
direction: direction.to_proto(),
keys_only: true,
};
open_cursor_inner(&mut self.client, req).await
}
}
#[async_trait]
impl IndexedDBApi for IndexedDB {
type ObjectStore = ObjectStore;
type Transaction = Transaction;
async fn create_object_store(
&mut self,
name: &str,
schema: ObjectStoreSchema,
) -> Result<ObjectStore, IndexedDBError> {
IndexedDB::create_object_store(self, name, schema).await
}
async fn delete_object_store(&mut self, name: &str) -> Result<(), IndexedDBError> {
IndexedDB::delete_object_store(self, name).await
}
fn object_store(&self, name: &str) -> ObjectStore {
IndexedDB::object_store(self, name)
}
async fn transaction(
&self,
stores: &[&str],
mode: TransactionMode,
options: TransactionOptions,
) -> Result<Transaction, IndexedDBError> {
IndexedDB::transaction(self, stores, mode, options).await
}
}
#[async_trait]
impl ObjectStoreApi for ObjectStore {
type Index = Index;
type Cursor = Cursor;
async fn get(&mut self, id: &str) -> Result<Record, IndexedDBError> {
ObjectStore::get(self, id).await
}
async fn get_key(&mut self, id: &str) -> Result<String, IndexedDBError> {
ObjectStore::get_key(self, id).await
}
async fn add(&mut self, record: Record) -> Result<(), IndexedDBError> {
ObjectStore::add(self, record).await
}
async fn put(&mut self, record: Record) -> Result<(), IndexedDBError> {
ObjectStore::put(self, record).await
}
async fn delete(&mut self, id: &str) -> Result<(), IndexedDBError> {
ObjectStore::delete(self, id).await
}
async fn clear(&mut self) -> Result<(), IndexedDBError> {
ObjectStore::clear(self).await
}
async fn get_all(&mut self, query: Query) -> Result<Vec<Record>, IndexedDBError> {
ObjectStore::get_all(self, query, None).await
}
async fn get_all_keys(&mut self, query: Query) -> Result<Vec<String>, IndexedDBError> {
ObjectStore::get_all_keys(self, query, None).await
}
async fn count(&mut self, query: Query) -> Result<i64, IndexedDBError> {
ObjectStore::count(self, query).await
}
async fn delete_range(&mut self, query: Query) -> Result<i64, IndexedDBError> {
ObjectStore::delete_range(self, query).await
}
fn index(&self, name: &str) -> Index {
ObjectStore::index(self, name)
}
async fn open_cursor(
&mut self,
query: Query,
direction: CursorDirection,
) -> Result<Cursor, IndexedDBError> {
ObjectStore::open_cursor(self, query, direction).await
}
async fn open_key_cursor(
&mut self,
query: Query,
direction: CursorDirection,
) -> Result<Cursor, IndexedDBError> {
ObjectStore::open_key_cursor(self, query, direction).await
}
}
#[async_trait]
impl IndexApi for Index {
type Cursor = Cursor;
async fn get(&mut self, query: Query) -> Result<Record, IndexedDBError> {
Index::get(self, query).await
}
async fn get_key(&mut self, query: Query) -> Result<String, IndexedDBError> {
Index::get_key(self, query).await
}
async fn get_all(&mut self, query: Query) -> Result<Vec<Record>, IndexedDBError> {
Index::get_all(self, query, None).await
}
async fn get_all_keys(&mut self, query: Query) -> Result<Vec<String>, IndexedDBError> {
Index::get_all_keys(self, query, None).await
}
async fn count(&mut self, query: Query) -> Result<i64, IndexedDBError> {
Index::count(self, query).await
}
async fn delete(&mut self, query: Query) -> Result<i64, IndexedDBError> {
Index::delete(self, query).await
}
async fn open_cursor(
&mut self,
query: Query,
direction: CursorDirection,
) -> Result<Cursor, IndexedDBError> {
Index::open_cursor(self, query, direction).await
}
async fn open_key_cursor(
&mut self,
query: Query,
direction: CursorDirection,
) -> Result<Cursor, IndexedDBError> {
Index::open_key_cursor(self, query, direction).await
}
}
#[async_trait]
impl TransactionApi for Transaction {
type ObjectStore<'a> = TransactionObjectStore<'a>;
fn object_store<'a>(&'a mut self, name: &str) -> TransactionObjectStore<'a> {
Transaction::object_store(self, name)
}
async fn commit(&mut self) -> Result<(), IndexedDBError> {
Transaction::commit(self).await
}
async fn abort(&mut self, reason: &str) -> Result<(), IndexedDBError> {
Transaction::abort(self, reason).await
}
}
#[async_trait]
impl<'tx> TransactionObjectStoreApi for TransactionObjectStore<'tx> {
type Index<'a>
= TransactionIndex<'a>
where
Self: 'a;
async fn get(&mut self, id: &str) -> Result<Record, IndexedDBError> {
TransactionObjectStore::get(self, id).await
}
async fn get_key(&mut self, id: &str) -> Result<String, IndexedDBError> {
TransactionObjectStore::get_key(self, id).await
}
async fn add(&mut self, record: Record) -> Result<(), IndexedDBError> {
TransactionObjectStore::add(self, record).await
}
async fn put(&mut self, record: Record) -> Result<(), IndexedDBError> {
TransactionObjectStore::put(self, record).await
}
async fn delete(&mut self, id: &str) -> Result<(), IndexedDBError> {
TransactionObjectStore::delete(self, id).await
}
async fn clear(&mut self) -> Result<(), IndexedDBError> {
TransactionObjectStore::clear(self).await
}
async fn get_all(&mut self, query: Query) -> Result<Vec<Record>, IndexedDBError> {
TransactionObjectStore::get_all(self, query, None).await
}
async fn get_all_keys(&mut self, query: Query) -> Result<Vec<String>, IndexedDBError> {
TransactionObjectStore::get_all_keys(self, query, None).await
}
async fn count(&mut self, query: Query) -> Result<i64, IndexedDBError> {
TransactionObjectStore::count(self, query).await
}
async fn delete_range(&mut self, query: Query) -> Result<i64, IndexedDBError> {
TransactionObjectStore::delete_range(self, query).await
}
fn index<'a>(&'a mut self, name: &str) -> TransactionIndex<'a> {
TransactionObjectStore::index(self, name)
}
}
#[async_trait]
impl TransactionIndexApi for TransactionIndex<'_> {
async fn get(&mut self, query: Query) -> Result<Record, IndexedDBError> {
TransactionIndex::get(self, query).await
}
async fn get_key(&mut self, query: Query) -> Result<String, IndexedDBError> {
TransactionIndex::get_key(self, query).await
}
async fn get_all(&mut self, query: Query) -> Result<Vec<Record>, IndexedDBError> {
TransactionIndex::get_all(self, query, None).await
}
async fn get_all_keys(&mut self, query: Query) -> Result<Vec<String>, IndexedDBError> {
TransactionIndex::get_all_keys(self, query, None).await
}
async fn count(&mut self, query: Query) -> Result<i64, IndexedDBError> {
TransactionIndex::count(self, query).await
}
async fn delete(&mut self, query: Query) -> Result<i64, IndexedDBError> {
TransactionIndex::delete(self, query).await
}
}
#[async_trait]
impl CursorApi for Cursor {
fn key(&self) -> Option<Key> {
Cursor::key(self)
}
fn primary_key(&self) -> &str {
Cursor::primary_key(self)
}
fn value(&self) -> Result<Record, IndexedDBError> {
Cursor::value(self)
}
async fn continue_next(&mut self) -> Result<bool, IndexedDBError> {
Cursor::continue_next(self).await
}
async fn continue_to_key(
&mut self,
key: impl Into<Key> + Send,
) -> Result<bool, IndexedDBError> {
Cursor::continue_to_key(self, key).await
}
async fn advance(&mut self, count: i32) -> Result<bool, IndexedDBError> {
Cursor::advance(self, count).await
}
async fn delete(&mut self) -> Result<(), IndexedDBError> {
Cursor::delete(self).await
}
async fn update(&mut self, value: Record) -> Result<(), IndexedDBError> {
Cursor::update(self, value).await
}
async fn close(self) -> Result<(), IndexedDBError> {
Cursor::close(self).await
}
}
fn map_status(err: tonic::Status) -> IndexedDBError {
match err.code() {
tonic::Code::NotFound => IndexedDBError::NotFound,
tonic::Code::AlreadyExists => IndexedDBError::AlreadyExists,
tonic::Code::InvalidArgument => IndexedDBError::InvalidArgument(err.message().to_string()),
tonic::Code::FailedPrecondition => IndexedDBError::Transaction(err.message().to_string()),
_ => IndexedDBError::Status(err),
}
}
fn map_rpc_status(
status: Option<crate::generated::google::rpc::Status>,
) -> Result<(), IndexedDBError> {
let Some(status) = status else {
return Ok(());
};
match status.code {
0 => Ok(()),
5 => Err(IndexedDBError::NotFound),
6 => Err(IndexedDBError::AlreadyExists),
3 => Err(IndexedDBError::InvalidArgument(status.message)),
9 => Err(IndexedDBError::Transaction(status.message)),
_ => Err(IndexedDBError::Transaction(status.message)),
}
}
fn unexpected_transaction_result() -> IndexedDBError {
IndexedDBError::Transaction("unexpected transaction operation result".to_string())
}
fn record_to_pb_record(record: Record) -> pb::Record {
pb::Record {
fields: record
.into_iter()
.map(|(k, v)| (k, json_to_typed_value(&v)))
.collect(),
}
}
fn pb_record_to_record(r: &pb::Record) -> Record {
r.fields
.iter()
.map(|(k, v)| (k.clone(), typed_value_to_json(v)))
.collect()
}
fn json_to_typed_value(v: &serde_json::Value) -> pb::TypedValue {
use pb::typed_value::Kind;
let kind = match v {
serde_json::Value::Null => Kind::NullValue(0),
serde_json::Value::Bool(b) => Kind::BoolValue(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Kind::IntValue(i)
} else {
Kind::FloatValue(n.as_f64().unwrap_or(0.0))
}
}
serde_json::Value::String(s) => Kind::StringValue(s.clone()),
serde_json::Value::Array(arr) => {
let values = arr.iter().map(json_to_prost_value).collect();
Kind::JsonValue(prost_types::Value {
kind: Some(prost_types::value::Kind::ListValue(
prost_types::ListValue { values },
)),
})
}
serde_json::Value::Object(obj) => {
let fields = obj
.iter()
.map(|(k, v)| (k.clone(), json_to_prost_value(v)))
.collect();
Kind::JsonValue(prost_types::Value {
kind: Some(prost_types::value::Kind::StructValue(prost_types::Struct {
fields,
})),
})
}
};
pb::TypedValue { kind: Some(kind) }
}
fn prost_value_to_json(v: &prost_types::Value) -> serde_json::Value {
use prost_types::value::Kind;
match &v.kind {
Some(Kind::NullValue(_)) => serde_json::Value::Null,
Some(Kind::BoolValue(b)) => serde_json::Value::Bool(*b),
Some(Kind::NumberValue(n)) => serde_json::json!(*n),
Some(Kind::StringValue(s)) => serde_json::Value::String(s.clone()),
Some(Kind::ListValue(list)) => {
serde_json::Value::Array(list.values.iter().map(prost_value_to_json).collect())
}
Some(Kind::StructValue(st)) => {
let obj: serde_json::Map<String, serde_json::Value> = st
.fields
.iter()
.map(|(k, v)| (k.clone(), prost_value_to_json(v)))
.collect();
serde_json::Value::Object(obj)
}
None => serde_json::Value::Null,
}
}
fn json_to_prost_value(v: &serde_json::Value) -> prost_types::Value {
use prost_types::value::Kind;
let kind = match v {
serde_json::Value::Null => Kind::NullValue(0),
serde_json::Value::Bool(b) => Kind::BoolValue(*b),
serde_json::Value::Number(n) => Kind::NumberValue(n.as_f64().unwrap_or(0.0)),
serde_json::Value::String(s) => Kind::StringValue(s.clone()),
serde_json::Value::Array(arr) => {
let values = arr.iter().map(json_to_prost_value).collect();
Kind::ListValue(prost_types::ListValue { values })
}
serde_json::Value::Object(obj) => {
let fields = obj
.iter()
.map(|(k, v)| (k.clone(), json_to_prost_value(v)))
.collect();
Kind::StructValue(prost_types::Struct { fields })
}
};
prost_types::Value { kind: Some(kind) }
}
fn typed_value_to_json(v: &pb::TypedValue) -> serde_json::Value {
use pb::typed_value::Kind;
match &v.kind {
Some(Kind::NullValue(_)) => serde_json::Value::Null,
Some(Kind::BoolValue(b)) => serde_json::Value::Bool(*b),
Some(Kind::IntValue(i)) => serde_json::json!(*i),
Some(Kind::FloatValue(f)) => serde_json::json!(*f),
Some(Kind::StringValue(s)) => serde_json::Value::String(s.clone()),
Some(Kind::BytesValue(b)) => serde_json::json!(b),
Some(Kind::JsonValue(pv)) => prost_value_to_json(pv),
Some(Kind::TimeValue(ts)) => {
serde_json::Value::String(format!("{}.{}", ts.seconds, ts.nanos))
}
None => serde_json::Value::Null,
}
}
fn relay_token_interceptor(
token: &str,
binding: &str,
) -> Result<RelayTokenInterceptor, IndexedDBError> {
let relay_token = if token.trim().is_empty() {
None
} else {
Some(MetadataValue::try_from(token.to_string()).map_err(|err| {
IndexedDBError::Env(format!("invalid IndexedDB relay token metadata: {err}"))
})?)
};
let binding = if binding.trim().is_empty() {
None
} else {
Some(
MetadataValue::try_from(binding.trim().to_string()).map_err(|err| {
IndexedDBError::Env(format!("invalid IndexedDB binding metadata: {err}"))
})?,
)
};
Ok(RelayTokenInterceptor {
relay_token,
binding,
})
}
#[derive(Clone)]
struct RelayTokenInterceptor {
relay_token: Option<MetadataValue<tonic::metadata::Ascii>>,
binding: Option<MetadataValue<tonic::metadata::Ascii>>,
}
impl Interceptor for RelayTokenInterceptor {
fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, tonic::Status> {
if let Some(header) = self.relay_token.clone() {
request
.metadata_mut()
.insert(INDEXEDDB_RELAY_TOKEN_HEADER, header);
}
if let Some(header) = self.binding.clone() {
request
.metadata_mut()
.insert(HOST_SERVICE_BINDING_HEADER, header);
}
Ok(request)
}
}