use vize_carton::{CompactString, FxHashMap, FxHashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct ReactiveId(u32);
impl ReactiveId {
#[inline(always)]
pub const fn new(id: u32) -> Self {
Self(id)
}
#[inline(always)]
pub const fn as_u32(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ReactiveKind {
Ref = 0,
ShallowRef = 1,
Reactive = 2,
ShallowReactive = 3,
Computed = 4,
Readonly = 5,
ShallowReadonly = 6,
ToRef = 7,
ToRefs = 8,
}
impl ReactiveKind {
#[inline]
pub const fn needs_value_access(self) -> bool {
matches!(
self,
Self::Ref | Self::ShallowRef | Self::Computed | Self::ToRef
)
}
pub fn from_name(name: &str) -> Option<Self> {
match name {
"ref" => Some(Self::Ref),
"shallowRef" => Some(Self::ShallowRef),
"reactive" => Some(Self::Reactive),
"shallowReactive" => Some(Self::ShallowReactive),
"computed" => Some(Self::Computed),
"readonly" => Some(Self::Readonly),
"shallowReadonly" => Some(Self::ShallowReadonly),
"toRef" => Some(Self::ToRef),
"toRefs" => Some(Self::ToRefs),
_ => None,
}
}
#[inline]
pub const fn to_display(self) -> &'static str {
match self {
Self::Ref | Self::ShallowRef | Self::ToRef | Self::ToRefs => "st",
Self::Reactive | Self::ShallowReactive => "ist",
Self::Computed => "drv",
Self::Readonly | Self::ShallowReadonly => "ro",
}
}
}
#[derive(Debug, Clone)]
pub struct ReactiveSource {
pub id: ReactiveId,
pub name: CompactString,
pub kind: ReactiveKind,
pub declaration_offset: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReactivityLossKind {
ReactiveDestructure {
source_name: CompactString,
destructured_props: Vec<CompactString>,
},
RefValueDestructure {
source_name: CompactString,
destructured_props: Vec<CompactString>,
},
RefValueExtract {
source_name: CompactString,
target_name: CompactString,
},
ReactiveSpread { source_name: CompactString },
ReactiveReassign { source_name: CompactString },
}
#[derive(Debug, Clone)]
pub struct ReactivityLoss {
pub kind: ReactivityLossKind,
pub start: u32,
pub end: u32,
}
#[derive(Debug, Default)]
pub struct ReactivityTracker {
sources: Vec<ReactiveSource>,
by_name: FxHashMap<CompactString, ReactiveId>,
reactive_names: FxHashSet<CompactString>,
losses: Vec<ReactivityLoss>,
next_id: u32,
}
impl ReactivityTracker {
#[inline]
pub fn new() -> Self {
Self::default()
}
pub fn register(
&mut self,
name: CompactString,
kind: ReactiveKind,
declaration_offset: u32,
) -> ReactiveId {
let id = ReactiveId::new(self.next_id);
self.next_id += 1;
self.reactive_names.insert(name.clone());
self.by_name.insert(name.clone(), id);
self.sources.push(ReactiveSource {
id,
name,
kind,
declaration_offset,
});
id
}
#[inline]
pub fn lookup(&self, name: &str) -> Option<&ReactiveSource> {
self.by_name
.get(name)
.and_then(|id| self.sources.get(id.as_u32() as usize))
}
#[inline]
pub fn is_reactive(&self, name: &str) -> bool {
self.reactive_names.contains(name)
}
#[inline]
pub fn needs_value_access(&self, name: &str) -> bool {
self.lookup(name)
.is_some_and(|s| s.kind.needs_value_access())
}
#[inline]
pub fn sources(&self) -> &[ReactiveSource] {
&self.sources
}
#[inline]
pub fn count(&self) -> usize {
self.sources.len()
}
#[inline]
pub fn reactive_names(&self) -> &FxHashSet<CompactString> {
&self.reactive_names
}
#[inline]
pub fn add_loss(&mut self, loss: ReactivityLoss) {
self.losses.push(loss);
}
pub fn record_destructure(
&mut self,
source_name: CompactString,
destructured_props: Vec<CompactString>,
start: u32,
end: u32,
) {
if let Some(source) = self.lookup(source_name.as_str()) {
let kind = if source.kind.needs_value_access() {
ReactivityLossKind::RefValueDestructure {
source_name,
destructured_props,
}
} else {
ReactivityLossKind::ReactiveDestructure {
source_name,
destructured_props,
}
};
self.losses.push(ReactivityLoss { kind, start, end });
}
}
pub fn record_spread(&mut self, source_name: CompactString, start: u32, end: u32) {
if self.is_reactive(source_name.as_str()) {
self.losses.push(ReactivityLoss {
kind: ReactivityLossKind::ReactiveSpread { source_name },
start,
end,
});
}
}
pub fn record_ref_value_extract(
&mut self,
source_name: CompactString,
target_name: CompactString,
start: u32,
end: u32,
) {
if let Some(source) = self.lookup(source_name.as_str()) {
if source.kind.needs_value_access() {
self.losses.push(ReactivityLoss {
kind: ReactivityLossKind::RefValueExtract {
source_name,
target_name,
},
start,
end,
});
}
}
}
pub fn record_reassign(&mut self, source_name: CompactString, start: u32, end: u32) {
if self.is_reactive(source_name.as_str()) {
self.losses.push(ReactivityLoss {
kind: ReactivityLossKind::ReactiveReassign { source_name },
start,
end,
});
}
}
#[inline]
pub fn losses(&self) -> &[ReactivityLoss] {
&self.losses
}
#[inline]
pub fn has_losses(&self) -> bool {
!self.losses.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::{ReactiveKind, ReactivityLossKind, ReactivityTracker};
use vize_carton::CompactString;
#[test]
fn test_reactivity_tracker() {
let mut tracker = ReactivityTracker::new();
tracker.register(CompactString::new("count"), ReactiveKind::Ref, 0);
tracker.register(CompactString::new("state"), ReactiveKind::Reactive, 20);
assert!(tracker.is_reactive("count"));
assert!(tracker.needs_value_access("count"));
assert!(tracker.is_reactive("state"));
assert!(!tracker.needs_value_access("state"));
assert!(!tracker.is_reactive("unknown"));
}
#[test]
fn test_reactive_destructure_loss() {
let mut tracker = ReactivityTracker::new();
tracker.register(CompactString::new("state"), ReactiveKind::Reactive, 0);
tracker.record_destructure(
CompactString::new("state"),
vec![CompactString::new("count"), CompactString::new("name")],
10,
20,
);
assert!(tracker.has_losses());
assert_eq!(tracker.losses().len(), 1);
match &tracker.losses()[0].kind {
ReactivityLossKind::ReactiveDestructure {
source_name,
destructured_props,
} => {
assert_eq!(source_name.as_str(), "state");
assert_eq!(destructured_props.len(), 2);
}
_ => panic!("Expected ReactiveDestructure"),
}
}
#[test]
fn test_reactive_spread_loss() {
let mut tracker = ReactivityTracker::new();
tracker.register(CompactString::new("state"), ReactiveKind::Reactive, 0);
tracker.record_spread(CompactString::new("state"), 10, 20);
assert!(tracker.has_losses());
assert_eq!(tracker.losses().len(), 1);
match &tracker.losses()[0].kind {
ReactivityLossKind::ReactiveSpread { source_name } => {
assert_eq!(source_name.as_str(), "state");
}
_ => panic!("Expected ReactiveSpread"),
}
}
#[test]
fn test_ref_value_extract_loss() {
let mut tracker = ReactivityTracker::new();
tracker.register(CompactString::new("count"), ReactiveKind::Ref, 0);
tracker.record_ref_value_extract(
CompactString::new("count"),
CompactString::new("value"),
10,
20,
);
assert!(tracker.has_losses());
assert_eq!(tracker.losses().len(), 1);
match &tracker.losses()[0].kind {
ReactivityLossKind::RefValueExtract {
source_name,
target_name,
} => {
assert_eq!(source_name.as_str(), "count");
assert_eq!(target_name.as_str(), "value");
}
_ => panic!("Expected RefValueExtract"),
}
}
#[test]
fn test_non_reactive_no_loss() {
let mut tracker = ReactivityTracker::new();
tracker.register(CompactString::new("state"), ReactiveKind::Reactive, 0);
tracker.record_destructure(
CompactString::new("other"),
vec![CompactString::new("count")],
10,
20,
);
assert!(!tracker.has_losses());
}
}