use std::collections::HashMap;
use bao_cdp::servo_bridge::DEFAULT_BROWSER_CONTEXT_ID;
use servo::ContextPartitionId;
#[derive(Debug)]
struct BrowserContextEntry {
id: String,
partition: ContextPartitionId,
}
#[derive(Default)]
pub struct BrowserContextRegistry {
contexts: HashMap<String, BrowserContextEntry>,
page_contexts: HashMap<usize, String>,
next_id: u64,
}
impl BrowserContextRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn create_context(&mut self, partition: ContextPartitionId) -> String {
self.next_id += 1;
let id = format!("bao-context-{:016x}", self.next_id);
self.contexts.insert(
id.clone(),
BrowserContextEntry {
id: id.clone(),
partition,
},
);
id
}
pub fn partition_of_context(&self, id: &str) -> Option<ContextPartitionId> {
self.contexts.get(id).map(|entry| entry.partition)
}
pub fn dispose_context(&mut self, id: &str) -> Result<Vec<usize>, String> {
if self.contexts.remove(id).is_none() {
return Err(format!("Failed to find browser context: {id}"));
}
let mut orphaned: Vec<usize> = self
.page_contexts
.iter()
.filter(|(_, ctx)| ctx.as_str() == id)
.map(|(page, _)| *page)
.collect();
orphaned.sort_unstable();
for page in &orphaned {
self.page_contexts.remove(page);
}
Ok(orphaned)
}
pub fn list_contexts(&self) -> Vec<String> {
let mut ids: Vec<String> = self.contexts.keys().cloned().collect();
ids.sort();
ids
}
pub fn context_exists(&self, id: &str) -> bool {
self.contexts.contains_key(id)
}
pub fn attribute_page(&mut self, page_id: usize, context_id: Option<&str>) {
match context_id {
Some(ctx) if ctx != DEFAULT_BROWSER_CONTEXT_ID && self.context_exists(ctx) => {
self.page_contexts.insert(page_id, ctx.to_string());
}
_ => {
self.page_contexts.remove(&page_id);
}
}
}
pub fn context_of_page(&self, page_id: usize) -> &str {
self.page_contexts
.get(&page_id)
.map(|s| s.as_str())
.unwrap_or(DEFAULT_BROWSER_CONTEXT_ID)
}
pub fn remove_page(&mut self, page_id: usize) {
self.page_contexts.remove(&page_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_context_mints_unique_non_default_ids() {
let mut reg = BrowserContextRegistry::new();
let a = reg.create_context(ContextPartitionId::new());
let b = reg.create_context(ContextPartitionId::new());
assert_ne!(a, b, "each create mints a fresh id");
assert_ne!(a, DEFAULT_BROWSER_CONTEXT_ID);
assert_ne!(b, DEFAULT_BROWSER_CONTEXT_ID);
assert!(a.starts_with("bao-context-"));
assert!(reg.context_exists(&a));
assert!(reg.context_exists(&b));
}
#[test]
fn list_contexts_returns_explicit_only_sorted() {
let mut reg = BrowserContextRegistry::new();
assert!(
reg.list_contexts().is_empty(),
"default is implicit, never listed"
);
let a = reg.create_context(ContextPartitionId::new());
let b = reg.create_context(ContextPartitionId::new());
assert_eq!(reg.list_contexts(), vec![a, b]); }
#[test]
fn dispose_context_returns_member_pages_and_forgets_them() {
let mut reg = BrowserContextRegistry::new();
let ctx = reg.create_context(ContextPartitionId::new());
reg.attribute_page(3, Some(&ctx));
reg.attribute_page(4, Some(&ctx));
reg.attribute_page(5, None);
let orphaned = reg.dispose_context(&ctx).unwrap();
assert_eq!(orphaned, vec![3, 4], "member pages handed to the closer");
assert!(!reg.context_exists(&ctx));
assert_eq!(reg.context_of_page(3), DEFAULT_BROWSER_CONTEXT_ID);
assert_eq!(reg.context_of_page(4), DEFAULT_BROWSER_CONTEXT_ID);
assert_eq!(reg.context_of_page(5), DEFAULT_BROWSER_CONTEXT_ID);
}
#[test]
fn dispose_unknown_or_default_context_fails_closed() {
let mut reg = BrowserContextRegistry::new();
let err = reg.dispose_context("bao-context-99").unwrap_err();
assert_eq!(err, "Failed to find browser context: bao-context-99");
let err = reg.dispose_context(DEFAULT_BROWSER_CONTEXT_ID).unwrap_err();
assert!(err.contains("Failed to find browser context"));
}
#[test]
fn context_of_page_defaults_when_unattributed() {
let reg = BrowserContextRegistry::new();
assert_eq!(reg.context_of_page(1), DEFAULT_BROWSER_CONTEXT_ID);
assert_eq!(reg.context_of_page(42), DEFAULT_BROWSER_CONTEXT_ID);
}
#[test]
fn attribute_page_records_only_known_explicit_contexts() {
let mut reg = BrowserContextRegistry::new();
let ctx = reg.create_context(ContextPartitionId::new());
reg.attribute_page(7, Some(&ctx));
assert_eq!(reg.context_of_page(7), ctx);
reg.attribute_page(8, Some("bao-context-404"));
assert_eq!(reg.context_of_page(8), DEFAULT_BROWSER_CONTEXT_ID);
reg.attribute_page(9, Some(DEFAULT_BROWSER_CONTEXT_ID));
assert_eq!(reg.context_of_page(9), DEFAULT_BROWSER_CONTEXT_ID);
reg.attribute_page(10, None);
assert_eq!(reg.context_of_page(10), DEFAULT_BROWSER_CONTEXT_ID);
}
#[test]
fn remove_page_drops_attribution_idempotently() {
let mut reg = BrowserContextRegistry::new();
let ctx = reg.create_context(ContextPartitionId::new());
reg.attribute_page(2, Some(&ctx));
reg.remove_page(2);
reg.remove_page(2);
assert_eq!(reg.context_of_page(2), DEFAULT_BROWSER_CONTEXT_ID);
}
}