1use crate::types::{Attachment, EditEntry, ImageMetadata, Reaction, SiteMetadata};
14use crate::simd::hex::{bytes_to_hex_32, bytes_to_hex_string, hex_to_bytes_32};
15
16fn hex_to_bytes_16(hex: &str) -> [u8; 16] {
19 let mut out = [0u8; 16];
20 let h = hex.as_bytes();
21 let mut padded = [b'0'; 32];
23 let copy_len = h.len().min(32);
24 padded[..copy_len].copy_from_slice(&h[..copy_len]);
25 for i in 0..16 {
26 let hi = hex_nibble(padded[i * 2]);
27 let lo = hex_nibble(padded[i * 2 + 1]);
28 out[i] = (hi << 4) | lo;
29 }
30 out
31}
32
33#[inline]
34fn hex_nibble(b: u8) -> u8 {
35 match b {
36 b'0'..=b'9' => b - b'0',
37 b'a'..=b'f' => b - b'a' + 10,
38 b'A'..=b'F' => b - b'A' + 10,
39 _ => 0,
40 }
41}
42
43const PENDING_ID_MARKER: u8 = 0x01;
49
50const PENDING_ID_SENTINEL: [u8; 15] = [0xFE; 15];
56
57#[inline]
62pub fn encode_message_id(id: &str) -> [u8; 32] {
63 if let Some(timestamp_str) = id.strip_prefix("pending-") {
64 let mut bytes = [0u8; 32];
66 bytes[0] = PENDING_ID_MARKER;
67 if let Ok(timestamp) = timestamp_str.parse::<u128>() {
68 bytes[1..17].copy_from_slice(×tamp.to_le_bytes());
69 }
70 bytes[17..32].copy_from_slice(&PENDING_ID_SENTINEL);
71 bytes
72 } else {
73 hex_to_bytes_32(id)
74 }
75}
76
77#[inline]
79pub fn decode_message_id(bytes: &[u8; 32]) -> String {
80 if bytes[0] == PENDING_ID_MARKER && bytes[17..32] == PENDING_ID_SENTINEL {
81 let mut timestamp_bytes = [0u8; 16];
83 timestamp_bytes.copy_from_slice(&bytes[1..17]);
84 let timestamp = u128::from_le_bytes(timestamp_bytes);
85 format!("pending-{}", timestamp)
86 } else {
87 bytes_to_hex_32(bytes)
88 }
89}
90
91#[inline]
98pub fn timestamp_to_compact(ms: u64) -> u64 {
99 ms
100}
101
102#[inline]
104pub fn timestamp_from_compact(compact: u64) -> u64 {
105 compact
106}
107
108const EPOCH_2020_SECS: u64 = 1577836800;
110
111#[inline]
114pub fn secs_to_compact(secs: u64) -> u32 {
115 if secs == 0 { return 0; }
116 secs.saturating_sub(EPOCH_2020_SECS) as u32
117}
118
119#[inline]
122pub fn secs_from_compact(compact: u32) -> u64 {
123 if compact == 0 { return 0; }
124 EPOCH_2020_SECS + compact as u64
125}
126
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
138pub struct MessageFlags(u8);
139
140impl MessageFlags {
141 pub const NONE: Self = Self(0);
142 pub const MINE: Self = Self(0b00001);
143 pub const PENDING: Self = Self(0b00010);
144 pub const FAILED: Self = Self(0b00100);
145 const REPLY_ATTACH_MASK: u8 = 0b11000;
148 const REPLY_ATTACH_SHIFT: u8 = 3;
149
150 #[inline]
151 pub fn is_mine(self) -> bool {
152 self.0 & Self::MINE.0 != 0
153 }
154
155 #[inline]
156 pub fn is_pending(self) -> bool {
157 self.0 & Self::PENDING.0 != 0
158 }
159
160 #[inline]
161 pub fn is_failed(self) -> bool {
162 self.0 & Self::FAILED.0 != 0
163 }
164
165 #[inline]
168 pub fn replied_to_has_attachment(self) -> Option<bool> {
169 match (self.0 & Self::REPLY_ATTACH_MASK) >> Self::REPLY_ATTACH_SHIFT {
170 0b00 => None, 0b01 => Some(false), 0b10 => Some(true), _ => None, }
175 }
176
177 #[inline]
178 pub fn set_mine(&mut self, value: bool) {
179 if value {
180 self.0 |= Self::MINE.0;
181 } else {
182 self.0 &= !Self::MINE.0;
183 }
184 }
185
186 #[inline]
187 pub fn set_pending(&mut self, value: bool) {
188 if value {
189 self.0 |= Self::PENDING.0;
190 } else {
191 self.0 &= !Self::PENDING.0;
192 }
193 }
194
195 #[inline]
196 pub fn set_failed(&mut self, value: bool) {
197 if value {
198 self.0 |= Self::FAILED.0;
199 } else {
200 self.0 &= !Self::FAILED.0;
201 }
202 }
203
204 #[inline]
206 pub fn set_replied_to_has_attachment(&mut self, value: Option<bool>) {
207 self.0 &= !Self::REPLY_ATTACH_MASK;
209 let bits = match value {
211 None => 0b00,
212 Some(false) => 0b01,
213 Some(true) => 0b10,
214 };
215 self.0 |= bits << Self::REPLY_ATTACH_SHIFT;
216 }
217
218 #[inline]
220 pub fn from_bools(mine: bool, pending: bool, failed: bool) -> Self {
221 let mut flags = Self::NONE;
222 flags.set_mine(mine);
223 flags.set_pending(pending);
224 flags.set_failed(failed);
225 flags
226 }
227
228 #[inline]
230 pub fn from_all(mine: bool, pending: bool, failed: bool, replied_to_has_attachment: Option<bool>) -> Self {
231 let mut flags = Self::from_bools(mine, pending, failed);
232 flags.set_replied_to_has_attachment(replied_to_has_attachment);
233 flags
234 }
235}
236
237use std::alloc::{alloc, dealloc, Layout};
242use std::marker::PhantomData;
243use std::ptr::NonNull;
244
245pub struct TinyVec<T> {
261 ptr: Option<NonNull<u8>>,
263 _marker: PhantomData<T>,
264}
265
266impl<T> TinyVec<T> {
267 #[inline]
269 pub const fn new() -> Self {
270 Self {
271 ptr: None,
272 _marker: PhantomData,
273 }
274 }
275
276 pub fn from_vec(vec: Vec<T>) -> Self {
278 if vec.is_empty() {
279 return Self::new();
280 }
281
282 let len = vec.len().min(255) as u8;
283
284 let (layout, items_offset) = Self::layout_for(len as usize);
286
287 unsafe {
288 let ptr = alloc(layout);
290 if ptr.is_null() {
291 std::alloc::handle_alloc_error(layout);
292 }
293
294 *ptr = len;
296
297 let items_ptr = ptr.add(items_offset) as *mut T;
299 for (i, item) in vec.into_iter().take(len as usize).enumerate() {
300 std::ptr::write(items_ptr.add(i), item);
301 }
302
303 Self {
304 ptr: NonNull::new(ptr),
305 _marker: PhantomData,
306 }
307 }
308 }
309
310 fn layout_for(len: usize) -> (Layout, usize) {
312 let header_layout = Layout::new::<u8>();
313 let items_layout = Layout::array::<T>(len).unwrap();
314 header_layout.extend(items_layout).unwrap()
315 }
316
317 #[inline]
319 pub fn len(&self) -> usize {
320 match self.ptr {
321 None => 0,
322 Some(ptr) => unsafe { *ptr.as_ptr() as usize },
323 }
324 }
325
326 #[inline]
327 pub fn is_empty(&self) -> bool {
328 self.ptr.is_none()
329 }
330
331 #[inline]
333 fn items_offset() -> usize {
334 let header_layout = Layout::new::<u8>();
335 let items_layout = Layout::new::<T>();
336 header_layout.extend(items_layout).map(|(_, offset)| offset).unwrap_or(1)
337 }
338
339 #[inline]
341 pub fn as_slice(&self) -> &[T] {
342 match self.ptr {
343 None => &[],
344 Some(ptr) => unsafe {
345 let base = ptr.as_ptr();
346 let len = *base as usize;
347 let items_ptr = base.add(Self::items_offset()) as *const T;
348 std::slice::from_raw_parts(items_ptr, len)
349 },
350 }
351 }
352
353 #[inline]
355 pub fn as_mut_slice(&mut self) -> &mut [T] {
356 match self.ptr {
357 None => &mut [],
358 Some(ptr) => unsafe {
359 let base = ptr.as_ptr();
360 let len = *base as usize;
361 let items_ptr = base.add(Self::items_offset()) as *mut T;
362 std::slice::from_raw_parts_mut(items_ptr, len)
363 },
364 }
365 }
366
367 #[inline]
369 pub fn iter(&self) -> std::slice::Iter<'_, T> {
370 self.as_slice().iter()
371 }
372
373 #[inline]
375 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> {
376 self.as_mut_slice().iter_mut()
377 }
378
379 pub fn to_vec(&self) -> Vec<T>
381 where
382 T: Clone,
383 {
384 self.as_slice().to_vec()
385 }
386
387 #[inline]
389 pub fn first(&self) -> Option<&T> {
390 self.as_slice().first()
391 }
392
393 #[inline]
395 pub fn last(&self) -> Option<&T> {
396 self.as_slice().last()
397 }
398
399 #[inline]
401 pub fn last_mut(&mut self) -> Option<&mut T> {
402 self.as_mut_slice().last_mut()
403 }
404
405 #[inline]
407 pub fn get(&self, index: usize) -> Option<&T> {
408 self.as_slice().get(index)
409 }
410
411 #[inline]
413 pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
414 self.as_mut_slice().get_mut(index)
415 }
416
417 pub fn push(&mut self, item: T)
419 where
420 T: Clone,
421 {
422 let mut vec = self.to_vec();
423 vec.push(item);
424 *self = Self::from_vec(vec);
425 }
426
427 pub fn retain<F>(&mut self, f: F)
429 where
430 T: Clone,
431 F: FnMut(&T) -> bool,
432 {
433 let mut vec = self.to_vec();
434 vec.retain(f);
435 *self = Self::from_vec(vec);
436 }
437
438 pub fn any<F>(&self, f: F) -> bool
440 where
441 F: FnMut(&T) -> bool,
442 {
443 self.as_slice().iter().any(f)
444 }
445}
446
447impl<T> std::ops::Index<usize> for TinyVec<T> {
449 type Output = T;
450
451 fn index(&self, index: usize) -> &Self::Output {
452 &self.as_slice()[index]
453 }
454}
455
456impl<T> std::ops::IndexMut<usize> for TinyVec<T> {
457 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
458 &mut self.as_mut_slice()[index]
459 }
460}
461
462impl<'a, T> IntoIterator for &'a TinyVec<T> {
464 type Item = &'a T;
465 type IntoIter = std::slice::Iter<'a, T>;
466
467 fn into_iter(self) -> Self::IntoIter {
468 self.as_slice().iter()
469 }
470}
471
472impl<'a, T> IntoIterator for &'a mut TinyVec<T> {
474 type Item = &'a mut T;
475 type IntoIter = std::slice::IterMut<'a, T>;
476
477 fn into_iter(self) -> Self::IntoIter {
478 self.as_mut_slice().iter_mut()
479 }
480}
481
482impl<T> Default for TinyVec<T> {
483 fn default() -> Self {
484 Self::new()
485 }
486}
487
488impl<T: Clone> Clone for TinyVec<T> {
489 fn clone(&self) -> Self {
490 Self::from_vec(self.to_vec())
491 }
492}
493
494impl<T: std::fmt::Debug> std::fmt::Debug for TinyVec<T> {
495 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496 f.debug_list().entries(self.as_slice()).finish()
497 }
498}
499
500impl<T> Drop for TinyVec<T> {
501 fn drop(&mut self) {
502 if let Some(ptr) = self.ptr {
503 unsafe {
504 let base = ptr.as_ptr();
505 let len = *base as usize;
506 let items_ptr = base.add(Self::items_offset()) as *mut T;
507
508 for i in 0..len {
510 std::ptr::drop_in_place(items_ptr.add(i));
511 }
512
513 let (layout, _) = Self::layout_for(len);
515 dealloc(base, layout);
516 }
517 }
518 }
519}
520
521unsafe impl<T: Send> Send for TinyVec<T> {}
523unsafe impl<T: Sync> Sync for TinyVec<T> {}
524
525#[derive(Clone, Debug)]
537pub struct CompactReaction {
538 pub id: [u8; 32],
540 pub reference_id: [u8; 32],
542 pub author_idx: u16,
544 pub emoji: Box<str>,
546 pub emoji_url: Option<Box<str>>,
549}
550
551impl CompactReaction {
552 #[inline]
554 pub fn id_hex(&self) -> String {
555 bytes_to_hex_32(&self.id)
556 }
557
558 #[inline]
560 pub fn reference_id_hex(&self) -> String {
561 bytes_to_hex_32(&self.reference_id)
562 }
563
564 pub fn from_reaction(reaction: &Reaction, interner: &mut NpubInterner) -> Self {
566 Self {
567 id: hex_to_bytes_32(&reaction.id),
568 reference_id: hex_to_bytes_32(&reaction.reference_id),
569 author_idx: interner.intern(&reaction.author_id),
570 emoji: reaction.emoji.clone().into_boxed_str(),
571 emoji_url: reaction.emoji_url.as_deref().map(|s| s.into()),
572 }
573 }
574
575 pub fn from_reaction_owned(reaction: Reaction, interner: &mut NpubInterner) -> Self {
577 Self {
578 id: hex_to_bytes_32(&reaction.id),
579 reference_id: hex_to_bytes_32(&reaction.reference_id),
580 author_idx: interner.intern(&reaction.author_id),
581 emoji: reaction.emoji.into_boxed_str(),
582 emoji_url: reaction.emoji_url.map(|s| s.into_boxed_str()),
583 }
584 }
585
586 pub fn to_reaction(&self, interner: &NpubInterner) -> Reaction {
588 Reaction {
589 id: self.id_hex(),
590 reference_id: self.reference_id_hex(),
591 author_id: interner.resolve(self.author_idx)
592 .map(|s| s.to_string())
593 .unwrap_or_default(),
594 emoji: self.emoji.to_string(),
595 emoji_url: self.emoji_url.as_deref().map(|s| s.to_string()),
596 }
597 }
598}
599
600#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
606pub struct AttachmentFlags(u8);
607
608impl AttachmentFlags {
609 pub const NONE: Self = Self(0);
610 const DOWNLOADING: u8 = 0b0001;
611 const DOWNLOADED: u8 = 0b0010;
612 const SHORT_NONCE: u8 = 0b0100; #[inline]
615 pub fn is_downloading(self) -> bool { self.0 & Self::DOWNLOADING != 0 }
616 #[inline]
617 pub fn is_downloaded(self) -> bool { self.0 & Self::DOWNLOADED != 0 }
618 #[inline]
619 pub fn is_short_nonce(self) -> bool { self.0 & Self::SHORT_NONCE != 0 }
620
621 #[inline]
622 pub fn set_downloading(&mut self, value: bool) {
623 if value { self.0 |= Self::DOWNLOADING; } else { self.0 &= !Self::DOWNLOADING; }
624 }
625 #[inline]
626 pub fn set_downloaded(&mut self, value: bool) {
627 if value { self.0 |= Self::DOWNLOADED; } else { self.0 &= !Self::DOWNLOADED; }
628 }
629 #[inline]
630 pub fn set_short_nonce(&mut self, value: bool) {
631 if value { self.0 |= Self::SHORT_NONCE; } else { self.0 &= !Self::SHORT_NONCE; }
632 }
633
634 pub fn from_bools(downloading: bool, downloaded: bool) -> Self {
635 let mut flags = Self::NONE;
636 flags.set_downloading(downloading);
637 flags.set_downloaded(downloaded);
638 flags
639 }
640}
641
642#[derive(Clone, Debug)]
653pub struct CompactAttachment {
654 pub id: [u8; 32],
657 pub key: [u8; 32],
659 pub nonce: [u8; 16],
661 pub size: u64,
663 pub flags: AttachmentFlags,
665
666 pub extension: Box<str>,
669 pub url: Box<str>,
671 pub path: Box<str>,
673
674 pub img_meta: Option<Box<ImageMetadata>>,
677 pub group_id: Option<Box<[u8; 32]>>,
679 pub original_hash: Option<Box<[u8; 32]>>,
681 pub webxdc_topic: Option<Box<str>>,
683 pub mls_filename: Option<Box<str>>,
685 pub scheme_version: Option<Box<str>>,
687 pub name: String,
689}
690
691impl CompactAttachment {
692 #[inline]
694 pub fn downloaded(&self) -> bool { self.flags.is_downloaded() }
695 #[inline]
696 pub fn downloading(&self) -> bool { self.flags.is_downloading() }
697 #[inline]
698 pub fn set_downloaded(&mut self, value: bool) { self.flags.set_downloaded(value); }
699 #[inline]
700 pub fn set_downloading(&mut self, value: bool) { self.flags.set_downloading(value); }
701
702 #[inline]
704 pub fn id_eq(&self, hex_id: &str) -> bool {
705 self.id == hex_to_bytes_32(hex_id)
706 }
707
708 #[inline]
710 pub fn id_hex(&self) -> String {
711 bytes_to_hex_32(&self.id)
712 }
713
714 pub fn key_hex(&self) -> String {
716 if self.key == [0u8; 32] {
717 String::new()
718 } else {
719 bytes_to_hex_32(&self.key)
720 }
721 }
722
723 pub fn nonce_hex(&self) -> String {
725 if self.nonce == [0u8; 16] {
726 String::new()
727 } else if self.flags.is_short_nonce() {
728 bytes_to_hex_string(&self.nonce[..12])
730 } else {
731 bytes_to_hex_string(&self.nonce)
733 }
734 }
735
736 pub fn from_attachment(att: &Attachment) -> Self {
738 let is_short_nonce = att.nonce.len() == 24;
740 let mut flags = AttachmentFlags::from_bools(att.downloading, att.downloaded);
741 flags.set_short_nonce(is_short_nonce);
742
743 Self {
744 id: hex_to_bytes_32(&att.id),
745 key: if att.key.is_empty() { [0u8; 32] } else { hex_to_bytes_32(&att.key) },
746 nonce: if att.nonce.is_empty() { [0u8; 16] } else { parse_nonce(&att.nonce) },
747 size: att.size,
748 flags,
749 extension: att.extension.clone().into_boxed_str(),
750 url: att.url.clone().into_boxed_str(),
751 path: att.path.clone().into_boxed_str(),
752 img_meta: att.img_meta.clone().map(Box::new),
753 group_id: att.group_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
754 original_hash: att.original_hash.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
755 webxdc_topic: att.webxdc_topic.clone().map(|s| s.into_boxed_str()),
756 mls_filename: att.mls_filename.clone().map(|s| s.into_boxed_str()),
757 scheme_version: att.scheme_version.clone().map(|s| s.into_boxed_str()),
758 name: att.name.clone(),
759 }
760 }
761
762 pub fn from_attachment_owned(att: Attachment) -> Self {
764 let is_short_nonce = att.nonce.len() == 24;
766 let mut flags = AttachmentFlags::from_bools(att.downloading, att.downloaded);
767 flags.set_short_nonce(is_short_nonce);
768
769 Self {
770 id: hex_to_bytes_32(&att.id),
771 key: if att.key.is_empty() { [0u8; 32] } else { hex_to_bytes_32(&att.key) },
772 nonce: if att.nonce.is_empty() { [0u8; 16] } else { parse_nonce(&att.nonce) },
773 size: att.size,
774 flags,
775 extension: att.extension.into_boxed_str(),
776 url: att.url.into_boxed_str(),
777 path: att.path.into_boxed_str(),
778 img_meta: att.img_meta.map(Box::new),
779 group_id: att.group_id.map(|s| Box::new(hex_to_bytes_32(&s))),
780 original_hash: att.original_hash.map(|s| Box::new(hex_to_bytes_32(&s))),
781 webxdc_topic: att.webxdc_topic.map(|s| s.into_boxed_str()),
782 mls_filename: att.mls_filename.map(|s| s.into_boxed_str()),
783 scheme_version: att.scheme_version.map(|s| s.into_boxed_str()),
784 name: att.name,
785 }
786 }
787
788 pub fn to_attachment(&self) -> Attachment {
790 Attachment {
791 id: self.id_hex(),
792 key: self.key_hex(),
793 nonce: self.nonce_hex(),
794 extension: self.extension.to_string(),
795 name: self.name.clone(),
796 url: self.url.to_string(),
797 path: self.path.to_string(),
798 size: self.size,
799 img_meta: self.img_meta.as_ref().map(|b| (**b).clone()),
800 downloading: self.flags.is_downloading(),
801 downloaded: self.flags.is_downloaded(),
802 webxdc_topic: self.webxdc_topic.as_ref().map(|s| s.to_string()),
803 group_id: self.group_id.as_ref().map(|b| bytes_to_hex_32(b)),
804 original_hash: self.original_hash.as_ref().map(|b| bytes_to_hex_32(b)),
805 scheme_version: self.scheme_version.as_ref().map(|s| s.to_string()),
806 mls_filename: self.mls_filename.as_ref().map(|s| s.to_string()),
807 }
808 }
809}
810
811#[inline]
815fn parse_nonce(hex: &str) -> [u8; 16] {
816 hex_to_bytes_16(hex)
817}
818
819#[derive(Clone, Debug, Default)]
831pub struct NpubInterner {
832 npubs: Vec<String>,
834 sorted: Vec<u16>,
836}
837
838pub const NO_NPUB: u16 = u16::MAX;
840
841impl NpubInterner {
842 pub fn new() -> Self {
843 Self {
844 npubs: Vec::new(),
845 sorted: Vec::new(),
846 }
847 }
848
849 pub fn with_capacity(capacity: usize) -> Self {
851 Self {
852 npubs: Vec::with_capacity(capacity),
853 sorted: Vec::with_capacity(capacity),
854 }
855 }
856
857 pub fn intern(&mut self, npub: &str) -> u16 {
862 let result = self.sorted.binary_search_by(|&idx| {
864 self.npubs[idx as usize].as_str().cmp(npub)
865 });
866
867 match result {
868 Ok(pos) => self.sorted[pos], Err(insert_pos) => {
870 if self.npubs.len() >= NO_NPUB as usize {
873 return NO_NPUB;
874 }
875 let new_idx = self.npubs.len() as u16;
877 self.npubs.push(npub.to_string());
878 self.sorted.insert(insert_pos, new_idx);
879 new_idx
880 }
881 }
882 }
883
884 #[inline]
886 pub fn intern_opt(&mut self, npub: Option<&str>) -> u16 {
887 match npub {
888 Some(s) if !s.is_empty() => self.intern(s),
889 _ => NO_NPUB,
890 }
891 }
892
893 pub fn lookup(&self, npub: &str) -> Option<u16> {
895 self.sorted.binary_search_by(|&idx| {
896 self.npubs[idx as usize].as_str().cmp(npub)
897 }).ok().map(|pos| self.sorted[pos])
898 }
899
900 #[inline]
904 pub fn resolve(&self, idx: u16) -> Option<&str> {
905 if idx == NO_NPUB {
906 return None;
907 }
908 self.npubs.get(idx as usize).map(|s| s.as_str())
909 }
910
911 #[inline]
913 pub fn len(&self) -> usize {
914 self.npubs.len()
915 }
916
917 #[inline]
918 pub fn is_empty(&self) -> bool {
919 self.npubs.is_empty()
920 }
921
922 pub fn memory_usage(&self) -> usize {
924 std::mem::size_of::<Self>()
925 + self.npubs.capacity() * std::mem::size_of::<String>()
926 + self.npubs.iter().map(|s| s.capacity()).sum::<usize>()
927 + self.sorted.capacity() * std::mem::size_of::<u16>()
928 }
929}
930
931#[derive(Clone, Debug)]
947pub struct CompactMessage {
948 pub id: [u8; 32],
950 pub at: u64,
952 pub flags: MessageFlags,
954 pub npub_idx: u16,
956 pub replied_to: Option<Box<[u8; 32]>>,
958 pub replied_to_npub_idx: u16,
960 pub wrapper_id: Option<Box<[u8; 32]>>,
962
963 pub content: Box<str>,
966 pub replied_to_content: Option<Box<str>>,
968 pub attachments: TinyVec<CompactAttachment>,
970 pub reactions: TinyVec<CompactReaction>,
972 #[allow(clippy::box_collection)]
974 pub edit_history: Option<Box<Vec<EditEntry>>>,
975 pub preview_metadata: Option<Box<SiteMetadata>>,
977 #[allow(clippy::box_collection)]
980 pub emoji_tags: Option<Box<Vec<crate::types::EmojiTag>>>,
981}
982
983impl CompactMessage {
984 #[inline]
986 pub fn has_reply(&self) -> bool {
987 self.replied_to.is_some()
988 }
989
990 #[inline]
992 pub fn is_edited(&self) -> bool {
993 self.edit_history.is_some()
994 }
995
996 #[inline]
998 pub fn id_hex(&self) -> String {
999 decode_message_id(&self.id)
1000 }
1001
1002 #[inline]
1004 pub fn replied_to_hex(&self) -> String {
1005 match &self.replied_to {
1006 Some(id) => bytes_to_hex_32(id),
1007 None => String::new(),
1008 }
1009 }
1010
1011 #[inline]
1013 pub fn wrapper_id_hex(&self) -> Option<String> {
1014 self.wrapper_id.as_ref().map(|id| bytes_to_hex_32(id))
1015 }
1016
1017 #[inline]
1019 pub fn timestamp_ms(&self) -> u64 {
1020 timestamp_from_compact(self.at)
1021 }
1022
1023 pub fn apply_edit(&mut self, new_content: String, edited_at: u64, emoji_tags: Vec<crate::types::EmojiTag>) {
1029 if self.edit_history.is_none() {
1031 self.edit_history = Some(Box::new(vec![EditEntry {
1032 content: self.content.to_string(),
1033 edited_at: self.timestamp_ms(), }]));
1035 }
1036
1037 let mut is_latest = true;
1038 if let Some(ref mut history) = self.edit_history {
1039 if history.iter().any(|e| e.edited_at == edited_at) {
1041 return;
1042 }
1043
1044 history.push(EditEntry {
1046 content: new_content.clone(),
1047 edited_at,
1048 });
1049
1050 history.sort_by_key(|e| e.edited_at);
1052 is_latest = history.last().map(|e| e.edited_at == edited_at).unwrap_or(true);
1053 }
1054
1055 if is_latest {
1057 self.content = new_content.into_boxed_str();
1058 self.emoji_tags = if emoji_tags.is_empty() { None } else { Some(Box::new(emoji_tags)) };
1059 }
1060 }
1061
1062 #[inline]
1064 pub fn replied_to_has_attachment(&self) -> Option<bool> {
1065 self.flags.replied_to_has_attachment()
1066 }
1067
1068 pub fn add_reaction(&mut self, reaction: Reaction, interner: &mut NpubInterner) -> bool {
1071 let reaction_id = hex_to_bytes_32(&reaction.id);
1073
1074 if self.reactions.iter().any(|r| r.id == reaction_id) {
1076 return false;
1077 }
1078
1079 let compact = CompactReaction::from_reaction_owned(reaction, interner);
1081 let mut reactions = self.reactions.to_vec();
1082 reactions.push(compact);
1083 self.reactions = TinyVec::from_vec(reactions);
1084 true
1085 }
1086
1087 pub fn remove_reaction(&mut self, reaction_id: &str) -> bool {
1089 let target = hex_to_bytes_32(reaction_id);
1090 if !self.reactions.iter().any(|r| r.id == target) {
1091 return false;
1092 }
1093 let mut reactions = self.reactions.to_vec();
1094 reactions.retain(|r| r.id != target);
1095 self.reactions = TinyVec::from_vec(reactions);
1096 true
1097 }
1098
1099 #[inline]
1101 pub fn is_mine(&self) -> bool { self.flags.is_mine() }
1102 #[inline]
1103 pub fn is_pending(&self) -> bool { self.flags.is_pending() }
1104 #[inline]
1105 pub fn is_failed(&self) -> bool { self.flags.is_failed() }
1106
1107 #[inline]
1109 pub fn set_pending(&mut self, value: bool) { self.flags.set_pending(value); }
1110 #[inline]
1111 pub fn set_failed(&mut self, value: bool) { self.flags.set_failed(value); }
1112 #[inline]
1113 pub fn set_mine(&mut self, value: bool) { self.flags.set_mine(value); }
1114}
1115
1116#[derive(Clone, Debug, Default)]
1125pub struct CompactMessageVec {
1126 messages: Vec<CompactMessage>,
1128 id_index: Vec<([u8; 32], u32)>,
1130}
1131
1132impl CompactMessageVec {
1133 pub fn new() -> Self {
1134 Self {
1135 messages: Vec::new(),
1136 id_index: Vec::new(),
1137 }
1138 }
1139
1140 pub fn with_capacity(capacity: usize) -> Self {
1141 Self {
1142 messages: Vec::with_capacity(capacity),
1143 id_index: Vec::with_capacity(capacity),
1144 }
1145 }
1146
1147 #[inline]
1149 pub fn len(&self) -> usize {
1150 self.messages.len()
1151 }
1152
1153 #[inline]
1154 pub fn is_empty(&self) -> bool {
1155 self.messages.is_empty()
1156 }
1157
1158 #[inline]
1160 pub fn messages(&self) -> &[CompactMessage] {
1161 &self.messages
1162 }
1163
1164 #[inline]
1166 pub fn messages_mut(&mut self) -> &mut Vec<CompactMessage> {
1167 &mut self.messages
1168 }
1169
1170 #[inline]
1172 pub fn iter(&self) -> std::slice::Iter<'_, CompactMessage> {
1173 self.messages.iter()
1174 }
1175
1176 #[inline]
1178 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, CompactMessage> {
1179 self.messages.iter_mut()
1180 }
1181
1182 #[inline]
1184 pub fn last(&self) -> Option<&CompactMessage> {
1185 self.messages.last()
1186 }
1187
1188 #[inline]
1190 pub fn last_timestamp(&self) -> Option<u64> {
1191 self.messages.last().map(|m| timestamp_from_compact(m.at))
1192 }
1193
1194 #[inline]
1196 pub fn first(&self) -> Option<&CompactMessage> {
1197 self.messages.first()
1198 }
1199
1200 pub fn find_by_id(&self, id: &[u8; 32]) -> Option<&CompactMessage> {
1202 let pos = self.id_index
1203 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1204 .ok()?;
1205 let msg_pos = self.id_index[pos].1 as usize;
1206 self.messages.get(msg_pos)
1207 }
1208
1209 pub fn find_by_id_mut(&mut self, id: &[u8; 32]) -> Option<&mut CompactMessage> {
1211 let pos = self.id_index
1212 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1213 .ok()?;
1214 let msg_pos = self.id_index[pos].1 as usize;
1215 self.messages.get_mut(msg_pos)
1216 }
1217
1218 pub fn find_by_hex_id(&self, id_str: &str) -> Option<&CompactMessage> {
1220 if id_str.is_empty() {
1221 return None;
1222 }
1223 let id = encode_message_id(id_str);
1224 self.find_by_id(&id)
1225 }
1226
1227 pub fn find_by_hex_id_mut(&mut self, id_str: &str) -> Option<&mut CompactMessage> {
1229 if id_str.is_empty() {
1230 return None;
1231 }
1232 let id = encode_message_id(id_str);
1233 self.find_by_id_mut(&id)
1234 }
1235
1236 pub fn contains_id(&self, id: &[u8; 32]) -> bool {
1238 self.id_index
1239 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1240 .is_ok()
1241 }
1242
1243 pub fn remove_by_hex_id(&mut self, id_str: &str) -> bool {
1245 if id_str.is_empty() {
1246 return false;
1247 }
1248 let id = encode_message_id(id_str);
1249 let idx_pos = match self.id_index.binary_search_by(|(idx_id, _)| idx_id.cmp(&id)) {
1251 Ok(pos) => pos,
1252 Err(_) => return false,
1253 };
1254 let msg_pos = self.id_index[idx_pos].1 as usize;
1255 self.messages.remove(msg_pos);
1257 self.rebuild_index();
1259 true
1260 }
1261
1262 pub fn contains_hex_id(&self, id_str: &str) -> bool {
1264 if id_str.is_empty() {
1265 return false;
1266 }
1267 let id = encode_message_id(id_str);
1268 self.contains_id(&id)
1269 }
1270
1271 pub fn insert(&mut self, msg: CompactMessage) -> bool {
1277 if self.contains_id(&msg.id) {
1279 return false;
1280 }
1281
1282 let msg_id = msg.id;
1283
1284 if self.messages.last().is_none_or(|last| msg.at >= last.at) {
1287 let msg_pos = self.messages.len() as u32;
1288 self.messages.push(msg);
1289
1290 let idx_pos = self.id_index
1293 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1294 .unwrap_err();
1295 self.id_index.insert(idx_pos, (msg_id, msg_pos));
1296
1297 return true;
1298 }
1299
1300 let msg_pos = match self.messages.binary_search_by(|m| m.at.cmp(&msg.at)) {
1303 Ok(pos) => pos,
1304 Err(pos) => pos,
1305 };
1306
1307 for (_, pos) in &mut self.id_index {
1309 if *pos >= msg_pos as u32 {
1310 *pos += 1;
1311 }
1312 }
1313
1314 self.messages.insert(msg_pos, msg);
1316
1317 let idx_pos = self.id_index
1319 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1320 .unwrap_err();
1321 self.id_index.insert(idx_pos, (msg_id, msg_pos as u32));
1322
1323 true
1324 }
1325
1326 pub fn rebuild_index(&mut self) {
1328 self.id_index.clear();
1329 self.id_index.reserve(self.messages.len());
1330 for (pos, msg) in self.messages.iter().enumerate() {
1331 self.id_index.push((msg.id, pos as u32));
1332 }
1333 self.id_index.sort_by(|(a, _), (b, _)| a.cmp(b));
1334 }
1335
1336 pub fn insert_batch(&mut self, messages: impl IntoIterator<Item = CompactMessage>) -> usize {
1345 let messages: Vec<_> = messages.into_iter().collect();
1346 if messages.is_empty() {
1347 return 0;
1348 }
1349
1350 let mut to_add: Vec<CompactMessage> = Vec::with_capacity(messages.len());
1352 for msg in messages {
1353 if !self.contains_id(&msg.id) {
1354 to_add.push(msg);
1355 }
1356 }
1357
1358 if to_add.is_empty() {
1359 return 0;
1360 }
1361
1362 let added = to_add.len();
1363
1364 let our_first = self.messages.first().map(|m| m.at);
1366 let our_last = self.messages.last().map(|m| m.at);
1367 let their_min = to_add.iter().map(|m| m.at).min().unwrap();
1368 let their_max = to_add.iter().map(|m| m.at).max().unwrap();
1369
1370 if self.messages.is_empty() {
1371 self.messages = to_add;
1373 self.messages.sort_by_key(|m| m.at);
1374 self.rebuild_index();
1375 } else if their_min >= our_last.unwrap() {
1376 to_add.sort_by_key(|m| m.at);
1378 let base_pos = self.messages.len() as u32;
1379 for (i, msg) in to_add.into_iter().enumerate() {
1380 let msg_id = msg.id;
1381 self.messages.push(msg);
1382 let idx_pos = self.id_index
1384 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1385 .unwrap_err();
1386 self.id_index.insert(idx_pos, (msg_id, base_pos + i as u32));
1387 }
1388 } else if their_max <= our_first.unwrap() {
1389 to_add.sort_by_key(|m| m.at);
1391 let prepend_count = to_add.len();
1392
1393 for (_, pos) in &mut self.id_index {
1395 *pos += prepend_count as u32;
1396 }
1397
1398 let mut new_index_entries: Vec<_> = to_add.iter()
1400 .enumerate()
1401 .map(|(i, msg)| (msg.id, i as u32))
1402 .collect();
1403 new_index_entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1404
1405 let old_index = std::mem::take(&mut self.id_index);
1407 self.id_index.reserve(old_index.len() + new_index_entries.len());
1408
1409 let mut old_iter = old_index.into_iter().peekable();
1410 let mut new_iter = new_index_entries.into_iter().peekable();
1411
1412 while old_iter.peek().is_some() || new_iter.peek().is_some() {
1413 match (old_iter.peek(), new_iter.peek()) {
1414 (Some((old_id, _)), Some((new_id, _))) => {
1415 if old_id < new_id {
1416 self.id_index.push(old_iter.next().unwrap());
1417 } else {
1418 self.id_index.push(new_iter.next().unwrap());
1419 }
1420 }
1421 (Some(_), None) => self.id_index.push(old_iter.next().unwrap()),
1422 (None, Some(_)) => self.id_index.push(new_iter.next().unwrap()),
1423 (None, None) => break,
1424 }
1425 }
1426
1427 let mut new_messages = to_add;
1429 new_messages.append(&mut self.messages);
1430 self.messages = new_messages;
1431 } else {
1432 self.messages.extend(to_add);
1434 self.messages.sort_by_key(|m| m.at);
1435 self.rebuild_index();
1436 }
1437
1438 added
1439 }
1440
1441 pub fn memory_usage(&self) -> usize {
1443 std::mem::size_of::<Self>()
1444 + self.messages.capacity() * std::mem::size_of::<CompactMessage>()
1445 + self.id_index.capacity() * std::mem::size_of::<([u8; 32], u32)>()
1446 }
1448
1449 pub fn drain(&mut self, range: std::ops::Range<usize>) -> std::vec::Drain<'_, CompactMessage> {
1451 let drain = self.messages.drain(range);
1452 drain
1454 }
1455
1456 pub fn sort_by_key<K, F>(&mut self, f: F)
1458 where
1459 F: FnMut(&CompactMessage) -> K,
1460 K: Ord,
1461 {
1462 self.messages.sort_by_key(f);
1463 self.rebuild_index();
1464 }
1465
1466 pub fn clear(&mut self) {
1468 self.messages.clear();
1469 self.id_index.clear();
1470 }
1471}
1472
1473use crate::types::Message;
1478
1479impl CompactMessage {
1480 pub fn from_message(msg: &Message, interner: &mut NpubInterner) -> Self {
1482 Self {
1483 id: encode_message_id(&msg.id),
1484 at: timestamp_to_compact(msg.at),
1485 flags: MessageFlags::from_all(msg.mine, msg.pending, msg.failed, msg.replied_to_has_attachment),
1486 npub_idx: interner.intern_opt(msg.npub.as_deref()),
1487 replied_to: if msg.replied_to.is_empty() {
1489 None
1490 } else {
1491 Some(Box::new(hex_to_bytes_32(&msg.replied_to)))
1492 },
1493 replied_to_npub_idx: interner.intern_opt(msg.replied_to_npub.as_deref()),
1494 wrapper_id: msg.wrapper_event_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
1496 content: msg.content.clone().into_boxed_str(),
1498 replied_to_content: msg.replied_to_content.as_ref().map(|s| s.clone().into_boxed_str()),
1499 attachments: TinyVec::from_vec(
1501 msg.attachments.iter()
1502 .map(CompactAttachment::from_attachment)
1503 .collect()
1504 ),
1505 reactions: TinyVec::from_vec(
1507 msg.reactions.iter()
1508 .map(|r| CompactReaction::from_reaction(r, interner))
1509 .collect()
1510 ),
1511 edit_history: msg.edit_history.clone().map(Box::new),
1513 preview_metadata: msg.preview_metadata.clone().map(Box::new),
1514 emoji_tags: if msg.emoji_tags.is_empty() {
1515 None
1516 } else {
1517 Some(Box::new(msg.emoji_tags.clone()))
1518 },
1519 }
1520 }
1521
1522 pub fn from_message_owned(msg: Message, interner: &mut NpubInterner) -> Self {
1527 Self {
1528 id: encode_message_id(&msg.id),
1529 at: timestamp_to_compact(msg.at),
1530 flags: MessageFlags::from_all(msg.mine, msg.pending, msg.failed, msg.replied_to_has_attachment),
1531 npub_idx: interner.intern_opt(msg.npub.as_deref()),
1532 replied_to: if msg.replied_to.is_empty() {
1534 None
1535 } else {
1536 Some(Box::new(hex_to_bytes_32(&msg.replied_to)))
1537 },
1538 replied_to_npub_idx: interner.intern_opt(msg.replied_to_npub.as_deref()),
1539 wrapper_id: msg.wrapper_event_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
1541 content: msg.content.into_boxed_str(),
1543 replied_to_content: msg.replied_to_content.map(|s| s.into_boxed_str()),
1544 attachments: TinyVec::from_vec(
1546 msg.attachments.into_iter()
1547 .map(CompactAttachment::from_attachment_owned)
1548 .collect()
1549 ),
1550 reactions: TinyVec::from_vec(
1552 msg.reactions.into_iter()
1553 .map(|r| CompactReaction::from_reaction_owned(r, interner))
1554 .collect()
1555 ),
1556 edit_history: msg.edit_history.map(Box::new),
1558 preview_metadata: msg.preview_metadata.map(Box::new),
1559 emoji_tags: if msg.emoji_tags.is_empty() {
1560 None
1561 } else {
1562 Some(Box::new(msg.emoji_tags))
1563 },
1564 }
1565 }
1566
1567 pub fn to_message(&self, interner: &NpubInterner) -> Message {
1569 Message {
1570 id: self.id_hex(),
1571 at: self.timestamp_ms(), mine: self.flags.is_mine(),
1573 pending: self.flags.is_pending(),
1574 failed: self.flags.is_failed(),
1575 edited: self.is_edited(),
1576 npub: interner.resolve(self.npub_idx).map(|s| s.to_string()),
1577 replied_to: self.replied_to_hex(),
1578 replied_to_content: self.replied_to_content.as_ref().map(|s| s.to_string()),
1579 replied_to_npub: interner.resolve(self.replied_to_npub_idx).map(|s| s.to_string()),
1580 replied_to_has_attachment: self.flags.replied_to_has_attachment(),
1581 replied_to_attachment_extension: None,
1584 wrapper_event_id: self.wrapper_id_hex(),
1585 content: self.content.to_string(),
1586 attachments: self.attachments.iter()
1588 .map(|a| a.to_attachment())
1589 .collect(),
1590 reactions: self.reactions.iter()
1592 .map(|r| r.to_reaction(interner))
1593 .collect(),
1594 edit_history: self.edit_history.as_ref().map(|b| (**b).clone()),
1596 preview_metadata: self.preview_metadata.as_ref().map(|b| (**b).clone()),
1597 emoji_tags: self.emoji_tags.as_ref().map(|b| (**b).clone()).unwrap_or_default(),
1598 }
1599 }
1600}
1601
1602#[cfg(test)]
1603mod tests {
1604 use super::*;
1605
1606 #[test]
1610 fn real_id_starting_with_marker_byte_roundtrips() {
1611 let id = "01b95c05179c6d6abbc60b9a35198fdee6e0ce5b80a0b7ac8d465d81d038a73d";
1612 let encoded = encode_message_id(id);
1613 assert_eq!(decode_message_id(&encoded), id);
1614 }
1615
1616 #[test]
1617 fn pending_id_roundtrips() {
1618 let id = "pending-306878031314";
1619 let encoded = encode_message_id(id);
1620 assert_eq!(encoded[0], PENDING_ID_MARKER);
1621 assert_eq!(decode_message_id(&encoded), id);
1622 }
1623
1624 #[test]
1625 fn marker_byte_without_sentinel_decodes_as_hex() {
1626 let mut bytes = [0xABu8; 32];
1628 bytes[0] = PENDING_ID_MARKER;
1629 let decoded = decode_message_id(&bytes);
1630 assert!(!decoded.starts_with("pending-"));
1631 assert_eq!(decoded.len(), 64);
1632 }
1633
1634 #[test]
1635 fn test_message_flags() {
1636 let mut flags = MessageFlags::NONE;
1637 assert!(!flags.is_mine());
1638 assert!(!flags.is_pending());
1639 assert!(!flags.is_failed());
1640
1641 flags.set_mine(true);
1642 assert!(flags.is_mine());
1643
1644 flags.set_pending(true);
1645 assert!(flags.is_pending());
1646 assert!(flags.is_mine()); flags.set_mine(false);
1649 assert!(!flags.is_mine());
1650 assert!(flags.is_pending()); }
1652
1653 #[test]
1654 fn test_npub_interner() {
1655 let mut interner = NpubInterner::new();
1656
1657 let idx1 = interner.intern("npub1alice");
1658 let idx2 = interner.intern("npub1bob");
1659 let idx3 = interner.intern("npub1alice"); assert_eq!(idx1, idx3); assert_ne!(idx1, idx2);
1663
1664 assert_eq!(interner.resolve(idx1), Some("npub1alice"));
1665 assert_eq!(interner.resolve(idx2), Some("npub1bob"));
1666 assert_eq!(interner.resolve(NO_NPUB), None);
1667 }
1668
1669 #[test]
1670 fn test_compact_message_vec_insert_and_find() {
1671 let mut vec = CompactMessageVec::new();
1672 let mut interner = NpubInterner::new();
1673
1674 let msg1 = CompactMessage {
1675 id: hex_to_bytes_32("0000000000000000000000000000000000000000000000000000000000000001"),
1676 at: 1000,
1677 flags: MessageFlags::NONE,
1678 npub_idx: interner.intern("npub1test"),
1679 replied_to: None,
1680 replied_to_npub_idx: NO_NPUB,
1681 wrapper_id: None,
1682 content: "First message".to_string().into_boxed_str(),
1683 replied_to_content: None,
1684 attachments: TinyVec::new(),
1685 reactions: TinyVec::new(),
1686 edit_history: None,
1687 preview_metadata: None, emoji_tags: None,
1689 };
1690
1691 let msg2 = CompactMessage {
1692 id: hex_to_bytes_32("0000000000000000000000000000000000000000000000000000000000000002"),
1693 at: 2000,
1694 flags: MessageFlags::MINE,
1695 npub_idx: interner.intern("npub1me"),
1696 replied_to: None,
1697 replied_to_npub_idx: NO_NPUB,
1698 wrapper_id: None,
1699 content: "Second message".to_string().into_boxed_str(),
1700 replied_to_content: None,
1701 attachments: TinyVec::new(),
1702 reactions: TinyVec::new(),
1703 edit_history: None,
1704 preview_metadata: None, emoji_tags: None,
1706 };
1707
1708 assert!(vec.insert(msg1));
1709 assert!(vec.insert(msg2));
1710 assert_eq!(vec.len(), 2);
1711
1712 let found = vec.find_by_hex_id("0000000000000000000000000000000000000000000000000000000000000001");
1714 assert!(found.is_some());
1715 assert_eq!(&*found.unwrap().content, "First message");
1716
1717 let not_found = vec.find_by_hex_id("0000000000000000000000000000000000000000000000000000000000000099");
1719 assert!(not_found.is_none());
1720 }
1721
1722 #[test]
1723 fn test_duplicate_insert_rejected() {
1724 let mut vec = CompactMessageVec::new();
1725
1726 let msg = CompactMessage {
1727 id: hex_to_bytes_32("abcd000000000000000000000000000000000000000000000000000000000000"),
1728 at: 1000,
1729 flags: MessageFlags::NONE,
1730 npub_idx: NO_NPUB,
1731 replied_to: None,
1732 replied_to_npub_idx: NO_NPUB,
1733 wrapper_id: None,
1734 content: "Test".to_string().into_boxed_str(),
1735 replied_to_content: None,
1736 attachments: TinyVec::new(),
1737 reactions: TinyVec::new(),
1738 edit_history: None,
1739 preview_metadata: None, emoji_tags: None,
1741 };
1742
1743 assert!(vec.insert(msg.clone()));
1744 assert!(!vec.insert(msg)); assert_eq!(vec.len(), 1);
1746 }
1747
1748 #[test]
1750 fn benchmark_compact_vs_message() {
1751 use std::time::Instant;
1752
1753 const NUM_MESSAGES: usize = 10_000;
1754 const NUM_UNIQUE_USERS: usize = 50; println!("\n========================================");
1757 println!(" COMPACT MESSAGE BENCHMARK");
1758 println!(" {} messages, {} unique users", NUM_MESSAGES, NUM_UNIQUE_USERS);
1759 println!("========================================\n");
1760
1761 let users: Vec<String> = (0..NUM_UNIQUE_USERS)
1763 .map(|i| format!("npub1{:0>62}", i))
1764 .collect();
1765
1766 let messages: Vec<Message> = (0..NUM_MESSAGES)
1768 .map(|i| {
1769 let user_idx = i % NUM_UNIQUE_USERS;
1770 Message {
1771 id: format!("{:0>64x}", i),
1772 at: 1700000000000 + (i as u64 * 1000),
1773 mine: user_idx == 0,
1774 pending: false,
1775 failed: false,
1776 edited: false,
1777 npub: Some(users[user_idx].clone()),
1778 replied_to: if i > 0 && i % 5 == 0 {
1779 format!("{:0>64x}", i - 1)
1780 } else {
1781 String::new()
1782 },
1783 replied_to_content: if i > 0 && i % 5 == 0 {
1784 Some("Previous message content".to_string())
1785 } else {
1786 None
1787 },
1788 replied_to_npub: if i > 0 && i % 5 == 0 {
1789 Some(users[(i - 1) % NUM_UNIQUE_USERS].clone())
1790 } else {
1791 None
1792 },
1793 replied_to_has_attachment: None,
1794 replied_to_attachment_extension: None,
1795 wrapper_event_id: Some(format!("{:0>64x}", i + 1000000)),
1796 content: format!("This is message number {} with some typical content length.", i),
1797 attachments: vec![],
1798 reactions: vec![],
1799 edit_history: None,
1800 preview_metadata: None,
1801 emoji_tags: Vec::new(),
1802 }
1803 })
1804 .collect();
1805
1806 println!("--- STRUCT SIZES ---");
1808 println!(" Message struct: {} bytes", std::mem::size_of::<Message>());
1809 println!(" CompactMessage struct: {} bytes", std::mem::size_of::<CompactMessage>());
1810 println!(" Savings per struct: {} bytes ({:.1}%)",
1811 std::mem::size_of::<Message>().saturating_sub(std::mem::size_of::<CompactMessage>()),
1812 (1.0 - std::mem::size_of::<CompactMessage>() as f64 / std::mem::size_of::<Message>() as f64) * 100.0
1813 );
1814 println!();
1815
1816 let msg_heap_estimate: usize = messages.iter().map(|m| {
1818 m.id.capacity()
1819 + m.npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
1820 + m.replied_to.capacity()
1821 + m.replied_to_content.as_ref().map(|s| s.capacity()).unwrap_or(0)
1822 + m.replied_to_npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
1823 + m.wrapper_event_id.as_ref().map(|s| s.capacity()).unwrap_or(0)
1824 + m.content.capacity()
1825 }).sum();
1826 let msg_total = messages.len() * std::mem::size_of::<Message>() + msg_heap_estimate;
1827
1828 println!("--- INSERT BENCHMARK ---");
1830
1831 let mut interner = NpubInterner::with_capacity(NUM_UNIQUE_USERS);
1833 let mut compact_vec = CompactMessageVec::with_capacity(NUM_MESSAGES);
1834
1835 let insert_start = Instant::now();
1836 for msg in &messages {
1837 let compact = CompactMessage::from_message(msg, &mut interner);
1838 compact_vec.insert(compact);
1839 }
1840 let insert_elapsed = insert_start.elapsed();
1841
1842 println!(" Sequential insert (optimized append path):");
1843 println!(" {} messages in {:?}", NUM_MESSAGES, insert_elapsed);
1844 println!(" Rate: {:.0} msgs/sec", NUM_MESSAGES as f64 / insert_elapsed.as_secs_f64());
1845 println!(" Per message: {:.3} us ({} ns)",
1846 insert_elapsed.as_micros() as f64 / NUM_MESSAGES as f64,
1847 insert_elapsed.as_nanos() / NUM_MESSAGES as u128);
1848 println!();
1849
1850 let mut interner2 = NpubInterner::with_capacity(NUM_UNIQUE_USERS);
1852 let mut compact_vec2 = CompactMessageVec::with_capacity(NUM_MESSAGES);
1853
1854 let batch_start = Instant::now();
1855 let compact_messages: Vec<_> = messages.iter()
1856 .map(|msg| CompactMessage::from_message(msg, &mut interner2))
1857 .collect();
1858 let batch_added = compact_vec2.insert_batch(compact_messages);
1859 let batch_elapsed = batch_start.elapsed();
1860
1861 println!(" Batch insert (pagination/history load):");
1862 println!(" {} messages in {:?}", batch_added, batch_elapsed);
1863 println!(" Rate: {:.0} msgs/sec", NUM_MESSAGES as f64 / batch_elapsed.as_secs_f64());
1864 println!(" Per message: {:.3} us ({} ns)",
1865 batch_elapsed.as_micros() as f64 / NUM_MESSAGES as f64,
1866 batch_elapsed.as_nanos() / NUM_MESSAGES as u128);
1867 println!();
1868
1869 println!("--- MEMORY COMPARISON ---");
1871 let compact_heap_estimate: usize = compact_vec.iter().map(|m| {
1872 m.content.len() + m.replied_to_content.as_ref().map(|s| s.len()).unwrap_or(0)
1874 + m.attachments.len() * std::mem::size_of::<Attachment>() + if m.attachments.is_empty() { 0 } else { 1 }
1875 + m.reactions.len() * std::mem::size_of::<Reaction>() + if m.reactions.is_empty() { 0 } else { 1 }
1876 }).sum();
1877 let compact_struct_mem = compact_vec.len() * std::mem::size_of::<CompactMessage>();
1878 let compact_index_mem = compact_vec.len() * std::mem::size_of::<([u8; 32], u32)>();
1879 let interner_mem = interner.memory_usage();
1880 let compact_total = compact_struct_mem + compact_heap_estimate + compact_index_mem + interner_mem;
1881
1882 println!(" Regular Message storage:");
1883 println!(" Struct memory: {:>10} bytes", messages.len() * std::mem::size_of::<Message>());
1884 println!(" Heap (strings): {:>10} bytes", msg_heap_estimate);
1885 println!(" TOTAL: {:>10} bytes ({:.2} MB)", msg_total, msg_total as f64 / 1_000_000.0);
1886 println!();
1887 println!(" CompactMessage storage:");
1888 println!(" Struct memory: {:>10} bytes", compact_struct_mem);
1889 println!(" Heap (strings): {:>10} bytes", compact_heap_estimate);
1890 println!(" ID index: {:>10} bytes", compact_index_mem);
1891 println!(" Interner: {:>10} bytes ({} unique npubs)", interner_mem, interner.len());
1892 println!(" TOTAL: {:>10} bytes ({:.2} MB)", compact_total, compact_total as f64 / 1_000_000.0);
1893 println!();
1894 println!(" SAVINGS: {} bytes ({:.1}%)",
1895 msg_total.saturating_sub(compact_total),
1896 (1.0 - compact_total as f64 / msg_total as f64) * 100.0
1897 );
1898 println!(" Per message: {} -> {} bytes (avg)",
1899 msg_total / NUM_MESSAGES,
1900 compact_total / NUM_MESSAGES
1901 );
1902 println!();
1903
1904 println!("--- LOOKUP BENCHMARK ---");
1906
1907 let lookup_ids: Vec<String> = (0..1000)
1909 .map(|i| format!("{:0>64x}", i * 10)) .collect();
1911
1912 let lookup_start = Instant::now();
1914 let mut found_count = 0;
1915 for _ in 0..100 { for id in &lookup_ids {
1917 if compact_vec.find_by_hex_id(id).is_some() {
1918 found_count += 1;
1919 }
1920 }
1921 }
1922 let lookup_elapsed = lookup_start.elapsed();
1923 let total_lookups = 100 * lookup_ids.len();
1924
1925 println!(" Binary search (CompactMessageVec):");
1926 println!(" {} lookups in {:?}", total_lookups, lookup_elapsed);
1927 println!(" Rate: {:.0} lookups/sec", total_lookups as f64 / lookup_elapsed.as_secs_f64());
1928 println!(" Per lookup: {:.2} us", lookup_elapsed.as_micros() as f64 / total_lookups as f64);
1929 println!(" Found: {} / {}", found_count, total_lookups);
1930 println!();
1931
1932 let linear_start = Instant::now();
1934 let mut linear_found = 0;
1935 for _ in 0..100 {
1936 for id in &lookup_ids {
1937 if messages.iter().find(|m| &m.id == id).is_some() {
1938 linear_found += 1;
1939 }
1940 }
1941 }
1942 let linear_elapsed = linear_start.elapsed();
1943
1944 println!(" Linear search (Vec<Message>):");
1945 println!(" {} lookups in {:?}", total_lookups, linear_elapsed);
1946 println!(" Rate: {:.0} lookups/sec", total_lookups as f64 / linear_elapsed.as_secs_f64());
1947 println!(" Per lookup: {:.2} us", linear_elapsed.as_micros() as f64 / total_lookups as f64);
1948 println!();
1949
1950 let speedup = linear_elapsed.as_nanos() as f64 / lookup_elapsed.as_nanos() as f64;
1951 println!(" SPEEDUP: {:.1}x faster with binary search!", speedup);
1952 println!();
1953
1954 println!("--- INTERNER EFFICIENCY ---");
1956 let npub_string_size = 63 + 1; let naive_npub_mem = NUM_MESSAGES * npub_string_size * 2; let actual_npub_mem = interner_mem;
1959 println!(" Naive (every msg stores npubs): {} bytes", naive_npub_mem);
1960 println!(" Interned ({} unique): {} bytes", interner.len(), actual_npub_mem);
1961 println!(" SAVINGS: {} bytes ({:.1}%)",
1962 naive_npub_mem.saturating_sub(actual_npub_mem),
1963 (1.0 - actual_npub_mem as f64 / naive_npub_mem as f64) * 100.0
1964 );
1965 println!();
1966
1967 println!("========================================");
1968 println!(" BENCHMARK COMPLETE");
1969 println!("========================================\n");
1970
1971 assert_eq!(compact_vec.len(), NUM_MESSAGES);
1973 assert_eq!(interner.len(), NUM_UNIQUE_USERS);
1974 assert_eq!(found_count, linear_found);
1975 }
1976
1977 #[test]
1984 fn benchmark_profile_lookup() {
1985 use std::time::Instant;
1986 use std::hint::black_box;
1987
1988 const NUM_PROFILES: usize = 60;
1989 const NUM_LOOKUPS: usize = 100_000;
1990
1991 println!("\n========================================");
1992 println!(" PROFILE LOOKUP BENCHMARK");
1993 println!(" {} profiles, {} lookups each method", NUM_PROFILES, NUM_LOOKUPS);
1994 println!("========================================\n");
1995
1996 let npubs: Vec<String> = (0..NUM_PROFILES)
1998 .map(|i| format!("npub1{:0>58}", format!("{:x}", i * 7919 + 1000))) .collect();
2000
2001 let old_ids: Vec<String> = npubs.iter().rev().cloned().collect(); let mut sorted_ids: Vec<String> = npubs.clone();
2006 sorted_ids.sort();
2007
2008 let mut interner = NpubInterner::new();
2010 let mut profiles: Vec<crate::profile::Profile> = npubs.iter().map(|npub| {
2011 let mut p = crate::profile::Profile::new();
2012 p.id = interner.intern(npub);
2013 p
2014 }).collect();
2015 profiles.sort_by(|a, b| a.id.cmp(&b.id));
2016
2017 let lookup_targets: Vec<&str> = (0..NUM_LOOKUPS)
2019 .map(|i| npubs[i % NUM_PROFILES].as_str())
2020 .collect();
2021
2022 let handle_targets: Vec<u16> = lookup_targets.iter()
2024 .map(|&npub| interner.lookup(npub).unwrap())
2025 .collect();
2026
2027 let start = Instant::now();
2029 let mut found = 0u64;
2030 for &target in &lookup_targets {
2031 if old_ids.iter().any(|id| id == target) {
2032 found += 1;
2033 }
2034 }
2035 let linear_elapsed = start.elapsed();
2036 assert_eq!(found, NUM_LOOKUPS as u64);
2037
2038 let start = Instant::now();
2040 found = 0;
2041 for &target in &lookup_targets {
2042 if sorted_ids.binary_search_by(|id| id.as_str().cmp(target)).is_ok() {
2043 found += 1;
2044 }
2045 }
2046 let string_bs_elapsed = start.elapsed();
2047 assert_eq!(found, NUM_LOOKUPS as u64);
2048
2049 let start = Instant::now();
2051 found = 0;
2052 for &handle in &handle_targets {
2053 if profiles.binary_search_by(|p| p.id.cmp(black_box(&handle))).is_ok() {
2054 found += 1;
2055 }
2056 }
2057 let direct_elapsed = start.elapsed();
2058 assert_eq!(found, NUM_LOOKUPS as u64);
2059
2060 println!("--- LOOKUP METHODS ---");
2062 println!(" 1. Linear scan (old id: String):");
2063 println!(" {:?} total, {:.0} ns/lookup",
2064 linear_elapsed,
2065 linear_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2066 println!();
2067 println!(" 2. String binary search (intermediate):");
2068 println!(" {:?} total, {:.0} ns/lookup",
2069 string_bs_elapsed,
2070 string_bs_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2071 println!(" vs linear: {:.1}x faster",
2072 linear_elapsed.as_nanos() as f64 / string_bs_elapsed.as_nanos() as f64);
2073 println!();
2074 println!(" 3. Direct u16 handle lookup (current id: u16):");
2075 println!(" {:?} total, {:.0} ns/lookup",
2076 direct_elapsed,
2077 direct_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2078 println!(" vs linear: {:.1}x faster",
2079 linear_elapsed.as_nanos() as f64 / direct_elapsed.as_nanos() as f64);
2080 println!(" vs string BS: {:.1}x faster",
2081 string_bs_elapsed.as_nanos() as f64 / direct_elapsed.as_nanos() as f64);
2082 println!();
2083
2084 println!("--- MEMORY PER PROFILE ---");
2086 println!(" Old (id: String): ~87 bytes (24 String header + ~63 heap)");
2087 println!(" Current (id: u16): 2 bytes (inline)");
2088 println!(" Savings: ~85 bytes/profile, ~{} bytes for {} profiles",
2089 85 * NUM_PROFILES, NUM_PROFILES);
2090 println!(" Interner (shared): {} bytes (shared with message system)",
2091 interner.memory_usage());
2092 println!();
2093
2094 println!("========================================");
2095 println!(" BENCHMARK COMPLETE");
2096 println!("========================================\n");
2097
2098 for npub in &npubs {
2100 assert!(old_ids.iter().any(|id| id == npub));
2101 assert!(sorted_ids.binary_search_by(|id| id.as_str().cmp(npub.as_str())).is_ok());
2102 let h = interner.lookup(npub).unwrap();
2103 assert!(profiles.binary_search_by(|p| p.id.cmp(&h)).is_ok());
2104 }
2105 }
2106
2107 #[test]
2112 fn pending_id_roundtrip_regular_hex() {
2113 let hex = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
2114 let encoded = encode_message_id(hex);
2115 let decoded = decode_message_id(&encoded);
2116 assert_eq!(decoded, hex, "regular hex ID should roundtrip exactly");
2117 }
2118
2119 #[test]
2120 fn pending_id_roundtrip_pending() {
2121 let id = "pending-1234567890123456789";
2122 let encoded = encode_message_id(id);
2123 let decoded = decode_message_id(&encoded);
2124 assert_eq!(decoded, id, "pending ID should roundtrip exactly");
2125 }
2126
2127 #[test]
2128 fn pending_id_zero_timestamp() {
2129 let id = "pending-0";
2130 let encoded = encode_message_id(id);
2131 assert_eq!(encoded[0], PENDING_ID_MARKER, "first byte should be marker");
2132 let decoded = decode_message_id(&encoded);
2133 assert_eq!(decoded, id, "pending-0 should roundtrip");
2134 }
2135
2136 #[test]
2137 fn pending_id_max_u128_timestamp() {
2138 let max = u128::MAX;
2139 let id = format!("pending-{}", max);
2140 let encoded = encode_message_id(&id);
2141 let decoded = decode_message_id(&encoded);
2142 assert_eq!(decoded, id, "pending with max u128 should roundtrip");
2143 }
2144
2145 #[test]
2146 fn pending_id_all_zero_hex() {
2147 let hex = "0000000000000000000000000000000000000000000000000000000000000000";
2148 let encoded = encode_message_id(hex);
2149 let decoded = decode_message_id(&encoded);
2150 assert_eq!(decoded, hex, "all-zero hex ID should roundtrip");
2151 assert_ne!(encoded[0], PENDING_ID_MARKER);
2153 }
2154
2155 #[test]
2156 fn pending_id_all_ff_hex() {
2157 let hex = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
2158 let encoded = encode_message_id(hex);
2159 let decoded = decode_message_id(&encoded);
2160 assert_eq!(decoded, hex, "all-ff hex ID should roundtrip");
2161 assert_eq!(encoded, [0xff; 32], "all-ff should decode to all 0xff bytes");
2162 }
2163
2164 #[test]
2165 fn pending_id_mixed_case_hex() {
2166 let lower = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
2167 let mixed = "ABCDEF0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789";
2168 let encoded_lower = encode_message_id(lower);
2169 let encoded_mixed = encode_message_id(mixed);
2170 assert_eq!(encoded_lower, encoded_mixed, "mixed case should produce same bytes as lowercase");
2171 }
2172
2173 #[test]
2174 fn pending_id_short_hex_partial_decode() {
2175 let short = "abcdef";
2177 let encoded = encode_message_id(short);
2178 assert_eq!(encoded[0..14], [0u8; 14], "leading bytes should be zero for short input");
2180 }
2181
2182 #[test]
2183 fn pending_id_marker_distinguishes_from_real_id() {
2184 let hex = "0100000000000000000000000000000000000000000000000000000000000000";
2189 let encoded = encode_message_id(hex);
2190 let decoded = decode_message_id(&encoded);
2191 assert_eq!(decoded, hex, "marker-leading real id must roundtrip exactly");
2192 }
2193
2194 #[test]
2195 fn pending_id_large_timestamp() {
2196 let id = "pending-99999999999999999";
2197 let encoded = encode_message_id(&id);
2198 let decoded = decode_message_id(&encoded);
2199 assert_eq!(decoded, id, "large but valid timestamp should roundtrip");
2200 }
2201
2202 #[test]
2203 fn pending_id_invalid_timestamp_becomes_zero() {
2204 let id = "pending-notanumber";
2206 let encoded = encode_message_id(id);
2207 assert_eq!(encoded[0], PENDING_ID_MARKER);
2208 assert_eq!(&encoded[1..17], &[0u8; 16]);
2210 let decoded = decode_message_id(&encoded);
2211 assert_eq!(decoded, "pending-0", "invalid timestamp parses as pending-0");
2212 }
2213
2214 #[test]
2219 fn timestamp_to_compact_and_back_roundtrip() {
2220 let ms: u64 = 1705320000000;
2222 let compact = timestamp_to_compact(ms);
2223 let restored = timestamp_from_compact(compact);
2224 assert_eq!(restored / 1000, ms / 1000, "roundtrip should preserve seconds");
2226 }
2227
2228 #[test]
2229 fn secs_to_compact_and_back_roundtrip() {
2230 let secs: u64 = 1705320000; let compact = secs_to_compact(secs);
2232 let restored = secs_from_compact(compact);
2233 assert_eq!(restored, secs, "secs should roundtrip exactly");
2234 }
2235
2236 #[test]
2237 fn timestamp_zero_preservation() {
2238 assert_eq!(secs_to_compact(0), 0, "zero secs should produce zero compact");
2240 assert_eq!(secs_from_compact(0), 0, "zero compact should produce zero secs");
2241 }
2242
2243 #[test]
2244 fn timestamp_epoch_boundary() {
2245 let epoch_secs: u64 = 1577836800;
2247 let compact = secs_to_compact(epoch_secs);
2248 assert_eq!(compact, 0, "epoch boundary should map to compact 0");
2249 let restored = secs_from_compact(compact);
2250 assert_eq!(restored, 0, "compact 0 returns sentinel 0");
2252 }
2253
2254 #[test]
2255 fn timestamp_epoch_boundary_ms() {
2256 let epoch_ms: u64 = 1577836800000;
2257 let compact = timestamp_to_compact(epoch_ms);
2258 assert_eq!(compact, epoch_ms, "full u64 — identity function");
2259 let restored = timestamp_from_compact(compact);
2260 assert_eq!(restored, epoch_ms, "roundtrip preserves value");
2261 }
2262
2263 #[test]
2264 fn timestamp_current_time_roundtrip() {
2265 let secs: u64 = 1774800000;
2267 let compact = secs_to_compact(secs);
2268 let restored = secs_from_compact(compact);
2269 assert_eq!(restored, secs, "current-era timestamp should roundtrip");
2270 assert!(compact > 0, "current time should be past epoch");
2271 }
2272
2273 #[test]
2274 fn timestamp_far_future_year_2100() {
2275 let secs: u64 = 4102444800;
2277 let compact = secs_to_compact(secs);
2278 let restored = secs_from_compact(compact);
2279 assert_eq!(restored, secs, "year 2100 should roundtrip");
2280 assert!(compact <= u32::MAX, "year 2100 should fit in u32");
2282 }
2283
2284 #[test]
2285 fn timestamp_pre_epoch_saturates_to_zero() {
2286 let secs: u64 = 1500000000; let compact = secs_to_compact(secs);
2289 assert_eq!(compact, 0, "pre-epoch timestamp should saturate to 0");
2291 }
2292
2293 #[test]
2294 fn timestamp_ms_sub_second_precision_preserved() {
2295 let ms: u64 = 1705320000999;
2296 let compact = timestamp_to_compact(ms);
2297 let restored = timestamp_from_compact(compact);
2298 assert_eq!(restored, ms, "sub-second ms must be preserved");
2299 }
2300
2301 #[test]
2302 fn timestamp_one_second_after_epoch() {
2303 let secs: u64 = 1577836801; let compact = secs_to_compact(secs);
2305 assert_eq!(compact, 1, "one second after epoch should be compact 1");
2306 let restored = secs_from_compact(compact);
2307 assert_eq!(restored, secs, "should restore to original");
2308 }
2309
2310 #[test]
2315 fn message_flags_mine_independent() {
2316 let mut flags = MessageFlags::NONE;
2317 flags.set_mine(true);
2318 assert!(flags.is_mine(), "mine should be set");
2319 assert!(!flags.is_pending(), "pending should not be set");
2320 assert!(!flags.is_failed(), "failed should not be set");
2321 }
2322
2323 #[test]
2324 fn message_flags_pending_independent() {
2325 let mut flags = MessageFlags::NONE;
2326 flags.set_pending(true);
2327 assert!(!flags.is_mine(), "mine should not be set");
2328 assert!(flags.is_pending(), "pending should be set");
2329 assert!(!flags.is_failed(), "failed should not be set");
2330 }
2331
2332 #[test]
2333 fn message_flags_failed_independent() {
2334 let mut flags = MessageFlags::NONE;
2335 flags.set_failed(true);
2336 assert!(!flags.is_mine(), "mine should not be set");
2337 assert!(!flags.is_pending(), "pending should not be set");
2338 assert!(flags.is_failed(), "failed should be set");
2339 }
2340
2341 #[test]
2342 fn message_flags_from_bools_all_false() {
2343 let flags = MessageFlags::from_bools(false, false, false);
2344 assert!(!flags.is_mine());
2345 assert!(!flags.is_pending());
2346 assert!(!flags.is_failed());
2347 assert_eq!(flags, MessageFlags::NONE, "all false should equal NONE");
2348 }
2349
2350 #[test]
2351 fn message_flags_from_bools_all_true() {
2352 let flags = MessageFlags::from_bools(true, true, true);
2353 assert!(flags.is_mine(), "mine should be set");
2354 assert!(flags.is_pending(), "pending should be set");
2355 assert!(flags.is_failed(), "failed should be set");
2356 }
2357
2358 #[test]
2359 fn message_flags_from_bools_various_combos() {
2360 let flags = MessageFlags::from_bools(true, false, true);
2361 assert!(flags.is_mine());
2362 assert!(!flags.is_pending());
2363 assert!(flags.is_failed());
2364
2365 let flags = MessageFlags::from_bools(false, true, false);
2366 assert!(!flags.is_mine());
2367 assert!(flags.is_pending());
2368 assert!(!flags.is_failed());
2369 }
2370
2371 #[test]
2372 fn message_flags_from_all_replied_to_none() {
2373 let flags = MessageFlags::from_all(false, false, false, None);
2374 assert_eq!(flags.replied_to_has_attachment(), None, "None should roundtrip");
2375 }
2376
2377 #[test]
2378 fn message_flags_from_all_replied_to_some_false() {
2379 let flags = MessageFlags::from_all(false, false, false, Some(false));
2380 assert_eq!(flags.replied_to_has_attachment(), Some(false), "Some(false) should roundtrip");
2381 }
2382
2383 #[test]
2384 fn message_flags_from_all_replied_to_some_true() {
2385 let flags = MessageFlags::from_all(false, false, false, Some(true));
2386 assert_eq!(flags.replied_to_has_attachment(), Some(true), "Some(true) should roundtrip");
2387 }
2388
2389 #[test]
2390 fn message_flags_multiple_set_simultaneously() {
2391 let flags = MessageFlags::from_all(true, true, false, Some(true));
2392 assert!(flags.is_mine());
2393 assert!(flags.is_pending());
2394 assert!(!flags.is_failed());
2395 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2396 }
2397
2398 #[test]
2399 fn message_flags_default_is_all_false() {
2400 let flags = MessageFlags::default();
2401 assert!(!flags.is_mine());
2402 assert!(!flags.is_pending());
2403 assert!(!flags.is_failed());
2404 assert_eq!(flags.replied_to_has_attachment(), None);
2405 assert_eq!(flags, MessageFlags::NONE);
2406 }
2407
2408 #[test]
2409 fn message_flags_bit_patterns_correct() {
2410 assert_eq!(MessageFlags::MINE.0, 0b00001, "MINE bit pattern");
2411 assert_eq!(MessageFlags::PENDING.0, 0b00010, "PENDING bit pattern");
2412 assert_eq!(MessageFlags::FAILED.0, 0b00100, "FAILED bit pattern");
2413 }
2414
2415 #[test]
2416 fn message_flags_set_then_clear() {
2417 let mut flags = MessageFlags::from_bools(true, true, true);
2418 flags.set_mine(false);
2419 assert!(!flags.is_mine(), "mine should be cleared");
2420 assert!(flags.is_pending(), "pending should remain set");
2421 assert!(flags.is_failed(), "failed should remain set");
2422 }
2423
2424 #[test]
2425 fn message_flags_replied_to_overwrite() {
2426 let mut flags = MessageFlags::from_all(false, false, false, Some(true));
2427 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2428 flags.set_replied_to_has_attachment(Some(false));
2429 assert_eq!(flags.replied_to_has_attachment(), Some(false), "overwrite should work");
2430 flags.set_replied_to_has_attachment(None);
2431 assert_eq!(flags.replied_to_has_attachment(), None, "clearing to None should work");
2432 }
2433
2434 #[test]
2435 fn message_flags_replied_to_does_not_interfere_with_other_bits() {
2436 let mut flags = MessageFlags::from_bools(true, true, true);
2437 flags.set_replied_to_has_attachment(Some(true));
2438 assert!(flags.is_mine(), "mine should still be set");
2439 assert!(flags.is_pending(), "pending should still be set");
2440 assert!(flags.is_failed(), "failed should still be set");
2441 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2442 }
2443
2444 #[test]
2449 fn attachment_flags_downloading_independent() {
2450 let mut flags = AttachmentFlags::NONE;
2451 flags.set_downloading(true);
2452 assert!(flags.is_downloading());
2453 assert!(!flags.is_downloaded());
2454 assert!(!flags.is_short_nonce());
2455 }
2456
2457 #[test]
2458 fn attachment_flags_downloaded_independent() {
2459 let mut flags = AttachmentFlags::NONE;
2460 flags.set_downloaded(true);
2461 assert!(!flags.is_downloading());
2462 assert!(flags.is_downloaded());
2463 assert!(!flags.is_short_nonce());
2464 }
2465
2466 #[test]
2467 fn attachment_flags_short_nonce_independent() {
2468 let mut flags = AttachmentFlags::NONE;
2469 flags.set_short_nonce(true);
2470 assert!(!flags.is_downloading());
2471 assert!(!flags.is_downloaded());
2472 assert!(flags.is_short_nonce());
2473 }
2474
2475 #[test]
2476 fn attachment_flags_from_bools() {
2477 let flags = AttachmentFlags::from_bools(true, false);
2478 assert!(flags.is_downloading());
2479 assert!(!flags.is_downloaded());
2480
2481 let flags = AttachmentFlags::from_bools(false, true);
2482 assert!(!flags.is_downloading());
2483 assert!(flags.is_downloaded());
2484 }
2485
2486 #[test]
2487 fn attachment_flags_all_set() {
2488 let mut flags = AttachmentFlags::NONE;
2489 flags.set_downloading(true);
2490 flags.set_downloaded(true);
2491 flags.set_short_nonce(true);
2492 assert!(flags.is_downloading());
2493 assert!(flags.is_downloaded());
2494 assert!(flags.is_short_nonce());
2495 }
2496
2497 #[test]
2498 fn attachment_flags_set_then_clear() {
2499 let mut flags = AttachmentFlags::NONE;
2500 flags.set_downloading(true);
2501 flags.set_downloaded(true);
2502 flags.set_downloading(false);
2503 assert!(!flags.is_downloading(), "downloading should be cleared");
2504 assert!(flags.is_downloaded(), "downloaded should remain set");
2505 }
2506
2507 #[test]
2508 fn attachment_flags_default_none() {
2509 let flags = AttachmentFlags::NONE;
2510 assert!(!flags.is_downloading());
2511 assert!(!flags.is_downloaded());
2512 assert!(!flags.is_short_nonce());
2513 assert_eq!(flags, AttachmentFlags::default());
2514 }
2515
2516 #[test]
2517 fn attachment_flags_bit_values() {
2518 let mut flags = AttachmentFlags::NONE;
2520 flags.set_downloading(true);
2521 assert_eq!(flags.0, 0b0001);
2522
2523 let mut flags = AttachmentFlags::NONE;
2524 flags.set_downloaded(true);
2525 assert_eq!(flags.0, 0b0010);
2526
2527 let mut flags = AttachmentFlags::NONE;
2528 flags.set_short_nonce(true);
2529 assert_eq!(flags.0, 0b0100);
2530 }
2531
2532 #[test]
2537 fn interner_returns_incrementing_handles() {
2538 let mut interner = NpubInterner::new();
2539 let h0 = interner.intern("npub1aaa");
2540 let h1 = interner.intern("npub1bbb");
2541 let h2 = interner.intern("npub1ccc");
2542 assert_eq!(h0, 0, "first intern should be handle 0");
2543 assert_eq!(h1, 1, "second intern should be handle 1");
2544 assert_eq!(h2, 2, "third intern should be handle 2");
2545 }
2546
2547 #[test]
2548 fn interner_lookup_finds_interned() {
2549 let mut interner = NpubInterner::new();
2550 let h = interner.intern("npub1alice");
2551 let found = interner.lookup("npub1alice");
2552 assert_eq!(found, Some(h), "lookup should find interned string");
2553 }
2554
2555 #[test]
2556 fn interner_lookup_returns_none_for_unknown() {
2557 let interner = NpubInterner::new();
2558 assert_eq!(interner.lookup("npub1unknown"), None, "lookup on empty interner should be None");
2559 }
2560
2561 #[test]
2562 fn interner_lookup_returns_none_for_not_interned() {
2563 let mut interner = NpubInterner::new();
2564 interner.intern("npub1alice");
2565 assert_eq!(interner.lookup("npub1bob"), None, "lookup for non-interned should be None");
2566 }
2567
2568 #[test]
2569 fn interner_resolve_returns_string() {
2570 let mut interner = NpubInterner::new();
2571 let h = interner.intern("npub1test123");
2572 assert_eq!(interner.resolve(h), Some("npub1test123"), "resolve should return the original string");
2573 }
2574
2575 #[test]
2576 fn interner_resolve_returns_none_for_no_npub() {
2577 let interner = NpubInterner::new();
2578 assert_eq!(interner.resolve(NO_NPUB), None, "resolve(NO_NPUB) should be None");
2579 }
2580
2581 #[test]
2582 fn interner_resolve_returns_none_for_out_of_bounds() {
2583 let mut interner = NpubInterner::new();
2584 interner.intern("npub1only");
2585 assert_eq!(interner.resolve(999), None, "out-of-bounds handle should resolve to None");
2586 }
2587
2588 #[test]
2589 fn interner_duplicate_returns_same_handle() {
2590 let mut interner = NpubInterner::new();
2591 let h1 = interner.intern("npub1dup");
2592 let h2 = interner.intern("npub1dup");
2593 let h3 = interner.intern("npub1dup");
2594 assert_eq!(h1, h2, "duplicate intern should return same handle");
2595 assert_eq!(h2, h3, "duplicate intern should return same handle");
2596 assert_eq!(interner.len(), 1, "duplicates should not increase length");
2597 }
2598
2599 #[test]
2600 fn interner_100_unique_npubs_stress() {
2601 let mut interner = NpubInterner::new();
2602 let mut handles = Vec::new();
2603
2604 for i in 0..100 {
2605 let npub = format!("npub1stress{:04}", i);
2606 let h = interner.intern(&npub);
2607 handles.push((h, npub));
2608 }
2609
2610 assert_eq!(interner.len(), 100, "should have 100 unique npubs");
2611
2612 for (h, npub) in &handles {
2614 assert_eq!(interner.resolve(*h), Some(npub.as_str()),
2615 "handle {} should resolve to {}", h, npub);
2616 }
2617
2618 for (h, npub) in &handles {
2620 assert_eq!(interner.lookup(npub), Some(*h),
2621 "lookup for {} should return handle {}", npub, h);
2622 }
2623
2624 for (h, npub) in &handles {
2626 assert_eq!(interner.intern(npub), *h,
2627 "re-interning {} should return same handle {}", npub, h);
2628 }
2629 assert_eq!(interner.len(), 100, "re-interning should not grow interner");
2630 }
2631
2632 #[test]
2633 fn interner_memory_usage_reasonable() {
2634 let mut interner = NpubInterner::new();
2635 for i in 0..50 {
2636 interner.intern(&format!("npub1{:0>62}", i));
2637 }
2638 let mem = interner.memory_usage();
2639 assert!(mem > 0, "memory usage should be positive");
2641 assert!(mem < 100_000, "memory usage for 50 npubs should be under 100KB, was {}", mem);
2642 }
2643
2644 #[test]
2645 fn interner_empty() {
2646 let interner = NpubInterner::new();
2647 assert_eq!(interner.len(), 0);
2648 assert!(interner.is_empty());
2649 assert_eq!(interner.resolve(0), None);
2650 assert_eq!(interner.lookup("anything"), None);
2651 assert!(interner.memory_usage() > 0, "even empty interner has struct overhead");
2652 }
2653
2654 #[test]
2655 fn interner_intern_opt_none() {
2656 let mut interner = NpubInterner::new();
2657 let h = interner.intern_opt(None);
2658 assert_eq!(h, NO_NPUB, "intern_opt(None) should return NO_NPUB");
2659 assert_eq!(interner.len(), 0, "None should not add to interner");
2660 }
2661
2662 #[test]
2663 fn interner_intern_opt_empty_string() {
2664 let mut interner = NpubInterner::new();
2665 let h = interner.intern_opt(Some(""));
2666 assert_eq!(h, NO_NPUB, "intern_opt(Some('')) should return NO_NPUB");
2667 }
2668
2669 #[test]
2670 fn interner_intern_opt_some_value() {
2671 let mut interner = NpubInterner::new();
2672 let h = interner.intern_opt(Some("npub1real"));
2673 assert_ne!(h, NO_NPUB, "intern_opt(Some(value)) should not return NO_NPUB");
2674 assert_eq!(interner.resolve(h), Some("npub1real"));
2675 }
2676
2677 fn make_compact_msg(hex_id: &str, timestamp: u64) -> CompactMessage {
2683 CompactMessage {
2684 id: encode_message_id(hex_id),
2685 at: timestamp,
2686 flags: MessageFlags::NONE,
2687 npub_idx: NO_NPUB,
2688 replied_to: None,
2689 replied_to_npub_idx: NO_NPUB,
2690 wrapper_id: None,
2691 content: "test".into(),
2692 replied_to_content: None,
2693 attachments: TinyVec::new(),
2694 reactions: TinyVec::new(),
2695 edit_history: None,
2696 preview_metadata: None,
2697 emoji_tags: None,
2698 }
2699 }
2700
2701 #[test]
2702 fn compact_vec_insert_single_message() {
2703 let mut vec = CompactMessageVec::new();
2704 let msg = make_compact_msg(
2705 "1111111111111111111111111111111111111111111111111111111111111111",
2706 100,
2707 );
2708 assert!(vec.insert(msg), "insert should succeed");
2709 assert_eq!(vec.len(), 1);
2710 assert!(!vec.is_empty());
2711 }
2712
2713 #[test]
2714 fn compact_vec_insert_duplicate_rejected() {
2715 let mut vec = CompactMessageVec::new();
2716 let id = "2222222222222222222222222222222222222222222222222222222222222222";
2717 let msg1 = make_compact_msg(id, 100);
2718 let msg2 = make_compact_msg(id, 200); assert!(vec.insert(msg1), "first insert should succeed");
2720 assert!(!vec.insert(msg2), "duplicate ID should be rejected");
2721 assert_eq!(vec.len(), 1);
2722 }
2723
2724 #[test]
2725 fn compact_vec_insert_batch_multiple() {
2726 let mut vec = CompactMessageVec::new();
2727 let msgs = vec![
2728 make_compact_msg("aa00000000000000000000000000000000000000000000000000000000000000", 100),
2729 make_compact_msg("bb00000000000000000000000000000000000000000000000000000000000000", 200),
2730 make_compact_msg("cc00000000000000000000000000000000000000000000000000000000000000", 300),
2731 ];
2732 let added = vec.insert_batch(msgs);
2733 assert_eq!(added, 3, "all 3 should be added");
2734 assert_eq!(vec.len(), 3);
2735 }
2736
2737 #[test]
2738 fn compact_vec_insert_batch_dedup() {
2739 let mut vec = CompactMessageVec::new();
2740 let id = "dd00000000000000000000000000000000000000000000000000000000000000";
2741 vec.insert(make_compact_msg(id, 100));
2742
2743 let msgs = vec![
2744 make_compact_msg(id, 200), make_compact_msg("ee00000000000000000000000000000000000000000000000000000000000000", 300),
2746 ];
2747 let added = vec.insert_batch(msgs);
2748 assert_eq!(added, 1, "only non-duplicate should be added");
2749 assert_eq!(vec.len(), 2);
2750 }
2751
2752 #[test]
2753 fn compact_vec_find_by_hex_id() {
2754 let mut vec = CompactMessageVec::new();
2755 let id = "ff00000000000000000000000000000000000000000000000000000000000001";
2756 let mut msg = make_compact_msg(id, 500);
2757 msg.content = "found me".into();
2758 vec.insert(msg);
2759
2760 let found = vec.find_by_hex_id(id);
2761 assert!(found.is_some(), "should find by hex id");
2762 assert_eq!(&*found.unwrap().content, "found me");
2763 }
2764
2765 #[test]
2766 fn compact_vec_find_by_hex_id_not_found() {
2767 let mut vec = CompactMessageVec::new();
2768 vec.insert(make_compact_msg(
2769 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2770 ));
2771 let found = vec.find_by_hex_id(
2772 "bb00000000000000000000000000000000000000000000000000000000000000",
2773 );
2774 assert!(found.is_none(), "should not find non-existent ID");
2775 }
2776
2777 #[test]
2778 fn compact_vec_find_by_hex_id_empty_string() {
2779 let mut vec = CompactMessageVec::new();
2780 vec.insert(make_compact_msg(
2781 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2782 ));
2783 assert!(vec.find_by_hex_id("").is_none(), "empty string should return None");
2784 }
2785
2786 #[test]
2787 fn compact_vec_find_by_hex_id_mut() {
2788 let mut vec = CompactMessageVec::new();
2789 let id = "ff00000000000000000000000000000000000000000000000000000000000002";
2790 vec.insert(make_compact_msg(id, 500));
2791
2792 let found = vec.find_by_hex_id_mut(id);
2793 assert!(found.is_some(), "should find mutable ref by hex id");
2794 found.unwrap().content = "modified".into();
2795
2796 let found = vec.find_by_hex_id(id);
2798 assert_eq!(&*found.unwrap().content, "modified");
2799 }
2800
2801 #[test]
2802 fn compact_vec_contains_hex_id() {
2803 let mut vec = CompactMessageVec::new();
2804 let id = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
2805 vec.insert(make_compact_msg(id, 100));
2806
2807 assert!(vec.contains_hex_id(id), "should contain inserted ID");
2808 assert!(!vec.contains_hex_id(
2809 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
2810 "should not contain non-inserted ID");
2811 assert!(!vec.contains_hex_id(""), "empty string should not match");
2812 }
2813
2814 #[test]
2815 fn compact_vec_remove_by_hex_id() {
2816 let mut vec = CompactMessageVec::new();
2817 let id1 = "1100000000000000000000000000000000000000000000000000000000000000";
2818 let id2 = "2200000000000000000000000000000000000000000000000000000000000000";
2819 vec.insert(make_compact_msg(id1, 100));
2820 vec.insert(make_compact_msg(id2, 200));
2821 assert_eq!(vec.len(), 2);
2822
2823 assert!(vec.remove_by_hex_id(id1), "remove should succeed");
2824 assert_eq!(vec.len(), 1);
2825 assert!(!vec.contains_hex_id(id1), "removed ID should not be found");
2826 assert!(vec.contains_hex_id(id2), "remaining ID should still be found");
2827 }
2828
2829 #[test]
2830 fn compact_vec_remove_nonexistent() {
2831 let mut vec = CompactMessageVec::new();
2832 vec.insert(make_compact_msg(
2833 "1100000000000000000000000000000000000000000000000000000000000000", 100,
2834 ));
2835 assert!(!vec.remove_by_hex_id(
2836 "9900000000000000000000000000000000000000000000000000000000000000"),
2837 "removing non-existent should return false");
2838 assert!(!vec.remove_by_hex_id(""), "removing empty should return false");
2839 assert_eq!(vec.len(), 1);
2840 }
2841
2842 #[test]
2843 fn compact_vec_last_timestamp() {
2844 let mut vec = CompactMessageVec::new();
2845 assert_eq!(vec.last_timestamp(), None, "empty vec should have no last timestamp");
2846
2847 vec.insert(make_compact_msg(
2848 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2849 ));
2850 vec.insert(make_compact_msg(
2851 "bb00000000000000000000000000000000000000000000000000000000000000", 300,
2852 ));
2853 vec.insert(make_compact_msg(
2854 "cc00000000000000000000000000000000000000000000000000000000000000", 200,
2855 ));
2856
2857 let last_ts = vec.last_timestamp().unwrap();
2858 let expected = timestamp_from_compact(300);
2860 assert_eq!(last_ts, expected, "last timestamp should be the largest");
2861 }
2862
2863 #[test]
2864 fn compact_vec_empty_operations() {
2865 let vec = CompactMessageVec::new();
2866 assert!(vec.is_empty());
2867 assert_eq!(vec.len(), 0);
2868 assert!(vec.last().is_none());
2869 assert!(vec.first().is_none());
2870 assert!(vec.last_timestamp().is_none());
2871 assert!(!vec.contains_hex_id("anything"));
2872 assert!(vec.find_by_hex_id("anything").is_none());
2873 }
2874
2875 #[test]
2876 fn compact_vec_1000_message_stress() {
2877 let mut vec = CompactMessageVec::new();
2878
2879 for i in 0..1000u64 {
2881 let id = format!("{:0>64x}", i);
2882 let msg = make_compact_msg(&id, i * 10);
2883 assert!(vec.insert(msg), "insert {} should succeed", i);
2884 }
2885 assert_eq!(vec.len(), 1000);
2886
2887 for i in 0..1000u64 {
2889 let id = format!("{:0>64x}", i);
2890 assert!(vec.contains_hex_id(&id), "should find message {}", i);
2891 let found = vec.find_by_hex_id(&id).unwrap();
2892 assert_eq!(found.at, i * 10, "timestamp should match for message {}", i);
2893 }
2894
2895 for i in 1000..1010u64 {
2897 let id = format!("{:0>64x}", i);
2898 assert!(!vec.contains_hex_id(&id), "should not find non-existent {}", i);
2899 }
2900
2901 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
2903 for w in timestamps.windows(2) {
2904 assert!(w[0] <= w[1], "messages should be sorted by timestamp: {} <= {}", w[0], w[1]);
2905 }
2906 }
2907
2908 #[test]
2909 fn compact_vec_rebuild_index_after_id_change() {
2910 let mut vec = CompactMessageVec::new();
2911 let old_id = "aa00000000000000000000000000000000000000000000000000000000000000";
2912 let new_id = "ff00000000000000000000000000000000000000000000000000000000000000";
2913 vec.insert(make_compact_msg(old_id, 100));
2914
2915 vec.messages_mut()[0].id = encode_message_id(new_id);
2917 assert!(!vec.contains_hex_id(new_id), "stale index should not find new ID");
2919
2920 vec.rebuild_index();
2922 assert!(vec.contains_hex_id(new_id), "after rebuild, new ID should be found");
2923 assert!(!vec.contains_hex_id(old_id), "after rebuild, old ID should not be found");
2924 }
2925
2926 #[test]
2927 fn compact_vec_pending_id_lookup() {
2928 let mut vec = CompactMessageVec::new();
2929 let pending = "pending-9876543210";
2930 vec.insert(make_compact_msg(pending, 500));
2931
2932 assert!(vec.contains_hex_id(pending), "should find pending ID");
2933 let found = vec.find_by_hex_id(pending);
2934 assert!(found.is_some(), "should find pending message");
2935 assert_eq!(found.unwrap().id_hex(), pending, "id_hex should match pending string");
2936 }
2937
2938 #[test]
2939 fn compact_vec_out_of_order_insert() {
2940 let mut vec = CompactMessageVec::new();
2941 vec.insert(make_compact_msg(
2943 "bb00000000000000000000000000000000000000000000000000000000000000", 300,
2944 ));
2945 vec.insert(make_compact_msg(
2946 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2947 ));
2948 vec.insert(make_compact_msg(
2949 "cc00000000000000000000000000000000000000000000000000000000000000", 200,
2950 ));
2951
2952 assert_eq!(vec.len(), 3);
2953 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
2955 assert_eq!(timestamps, vec![100, 200, 300], "should be sorted by timestamp");
2956
2957 assert!(vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
2959 assert!(vec.contains_hex_id("bb00000000000000000000000000000000000000000000000000000000000000"));
2960 assert!(vec.contains_hex_id("cc00000000000000000000000000000000000000000000000000000000000000"));
2961 }
2962
2963 #[test]
2964 fn compact_vec_batch_prepend() {
2965 let mut vec = CompactMessageVec::new();
2966 vec.insert(make_compact_msg(
2968 "cc00000000000000000000000000000000000000000000000000000000000000", 300,
2969 ));
2970 vec.insert(make_compact_msg(
2971 "dd00000000000000000000000000000000000000000000000000000000000000", 400,
2972 ));
2973
2974 let older = vec![
2976 make_compact_msg("aa00000000000000000000000000000000000000000000000000000000000000", 100),
2977 make_compact_msg("bb00000000000000000000000000000000000000000000000000000000000000", 200),
2978 ];
2979 let added = vec.insert_batch(older);
2980 assert_eq!(added, 2);
2981 assert_eq!(vec.len(), 4);
2982
2983 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
2985 assert_eq!(timestamps, vec![100, 200, 300, 400]);
2986
2987 assert!(vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
2989 assert!(vec.contains_hex_id("dd00000000000000000000000000000000000000000000000000000000000000"));
2990 }
2991
2992 #[test]
2993 fn compact_vec_clear() {
2994 let mut vec = CompactMessageVec::new();
2995 vec.insert(make_compact_msg(
2996 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2997 ));
2998 vec.insert(make_compact_msg(
2999 "bb00000000000000000000000000000000000000000000000000000000000000", 200,
3000 ));
3001 assert_eq!(vec.len(), 2);
3002
3003 vec.clear();
3004 assert!(vec.is_empty());
3005 assert_eq!(vec.len(), 0);
3006 assert!(!vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
3007 }
3008
3009 fn make_full_message() -> Message {
3015 Message {
3016 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3017 content: "Hello, world!".into(),
3018 replied_to: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3019 replied_to_content: Some("Original message".into()),
3020 replied_to_npub: Some("npub1replier".into()),
3021 replied_to_has_attachment: Some(true),
3022 replied_to_attachment_extension: None,
3023 preview_metadata: Some(SiteMetadata {
3024 domain: "example.com".into(),
3025 og_title: Some("Test Page".into()),
3026 og_description: Some("A test description".into()),
3027 og_image: Some("https://example.com/img.png".into()),
3028 og_url: Some("https://example.com".into()),
3029 og_type: Some("website".into()),
3030 title: Some("Test".into()),
3031 description: Some("Desc".into()),
3032 favicon: Some("https://example.com/favicon.ico".into()),
3033 }),
3034 attachments: vec![Attachment {
3035 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3036 key: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3037 nonce: "cccccccccccccccccccccccccccccccc".into(), extension: "png".into(),
3039 name: "photo.png".into(),
3040 url: "https://blossom.example.com".into(),
3041 path: "/tmp/photo.png".into(),
3042 size: 12345,
3043 img_meta: Some(ImageMetadata {
3044 thumbhash: "abc123".into(),
3045 width: 800,
3046 height: 600,
3047 }),
3048 downloading: false,
3049 downloaded: true,
3050 webxdc_topic: None,
3051 group_id: None,
3052 original_hash: None,
3053 scheme_version: None,
3054 mls_filename: None,
3055 }],
3056 reactions: vec![Reaction {
3057 id: "dddd000000000000000000000000000000000000000000000000000000000000".into(),
3058 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3059 author_id: "npub1reactor".into(),
3060 emoji: "\u{1f44d}".into(), emoji_url: None,
3062 }],
3063 at: 1705320000000, pending: false,
3065 failed: false,
3066 mine: true,
3067 npub: Some("npub1sender".into()),
3068 wrapper_event_id: Some("eeee000000000000000000000000000000000000000000000000000000000000".into()),
3069 edited: true,
3070 edit_history: Some(vec![
3071 EditEntry { content: "Original".into(), edited_at: 1705320000000 },
3072 EditEntry { content: "Edited".into(), edited_at: 1705320060000 },
3073 ]),
3074 emoji_tags: Vec::new(),
3075 }
3076 }
3077
3078 #[test]
3079 fn compact_message_from_message_roundtrip_all_fields() {
3080 let msg = make_full_message();
3081 let mut interner = NpubInterner::new();
3082 let compact = CompactMessage::from_message(&msg, &mut interner);
3083 let restored = compact.to_message(&interner);
3084
3085 assert_eq!(restored.id, msg.id, "id mismatch");
3086 assert_eq!(restored.content, msg.content, "content mismatch");
3087 assert_eq!(restored.mine, msg.mine, "mine mismatch");
3088 assert_eq!(restored.pending, msg.pending, "pending mismatch");
3089 assert_eq!(restored.failed, msg.failed, "failed mismatch");
3090 assert_eq!(restored.npub, msg.npub, "npub mismatch");
3091 assert_eq!(restored.replied_to, msg.replied_to, "replied_to mismatch");
3092 assert_eq!(restored.replied_to_content, msg.replied_to_content, "replied_to_content mismatch");
3093 assert_eq!(restored.replied_to_npub, msg.replied_to_npub, "replied_to_npub mismatch");
3094 assert_eq!(restored.replied_to_has_attachment, msg.replied_to_has_attachment, "replied_to_has_attachment mismatch");
3095 assert_eq!(restored.wrapper_event_id, msg.wrapper_event_id, "wrapper_event_id mismatch");
3096 assert_eq!(restored.edited, msg.edited, "edited mismatch");
3097 assert_eq!(restored.edit_history, msg.edit_history, "edit_history mismatch");
3098 assert_eq!(restored.preview_metadata, msg.preview_metadata, "preview_metadata mismatch");
3099 assert_eq!(restored.at / 1000, msg.at / 1000, "timestamp seconds mismatch");
3101 assert_eq!(restored.attachments.len(), 1, "should have 1 attachment");
3103 assert_eq!(restored.attachments[0].id, msg.attachments[0].id);
3104 assert_eq!(restored.attachments[0].name, msg.attachments[0].name);
3105 assert_eq!(restored.attachments[0].size, msg.attachments[0].size);
3106 assert_eq!(restored.reactions.len(), 1, "should have 1 reaction");
3108 assert_eq!(restored.reactions[0].emoji, msg.reactions[0].emoji);
3109 }
3110
3111 #[test]
3112 fn compact_message_from_message_owned_roundtrip() {
3113 let msg = make_full_message();
3114 let msg_clone = msg.clone();
3115 let mut interner = NpubInterner::new();
3116 let compact = CompactMessage::from_message_owned(msg, &mut interner);
3117 let restored = compact.to_message(&interner);
3118
3119 assert_eq!(restored.id, msg_clone.id, "id mismatch");
3120 assert_eq!(restored.content, msg_clone.content, "content mismatch");
3121 assert_eq!(restored.mine, msg_clone.mine, "mine mismatch");
3122 assert_eq!(restored.npub, msg_clone.npub, "npub mismatch");
3123 assert_eq!(restored.edit_history, msg_clone.edit_history, "edit_history mismatch");
3124 }
3125
3126 #[test]
3127 fn compact_message_pending_flag() {
3128 let msg = Message {
3129 id: "pending-1234567890".into(),
3130 pending: true,
3131 ..Message::default()
3132 };
3133 let mut interner = NpubInterner::new();
3134 let compact = CompactMessage::from_message(&msg, &mut interner);
3135 assert!(compact.is_pending(), "pending flag should be set");
3136 let restored = compact.to_message(&interner);
3137 assert!(restored.pending, "pending should roundtrip");
3138 assert_eq!(restored.id, "pending-1234567890", "pending ID should roundtrip");
3139 }
3140
3141 #[test]
3142 fn compact_message_failed_flag() {
3143 let msg = Message {
3144 id: "pending-999".into(),
3145 failed: true,
3146 pending: true,
3147 ..Message::default()
3148 };
3149 let mut interner = NpubInterner::new();
3150 let compact = CompactMessage::from_message(&msg, &mut interner);
3151 assert!(compact.is_failed(), "failed flag should be set");
3152 assert!(compact.is_pending(), "pending flag should also be set");
3153 let restored = compact.to_message(&interner);
3154 assert!(restored.failed);
3155 assert!(restored.pending);
3156 }
3157
3158 #[test]
3159 fn compact_message_with_attachments_roundtrip() {
3160 let msg = Message {
3161 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3162 attachments: vec![
3163 Attachment {
3164 id: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3165 extension: "jpg".into(),
3166 name: "sunset.jpg".into(),
3167 size: 5000,
3168 downloaded: true,
3169 ..Attachment::default()
3170 },
3171 Attachment {
3172 id: "2222222222222222222222222222222222222222222222222222222222222222".into(),
3173 extension: "mp4".into(),
3174 name: "video.mp4".into(),
3175 size: 50000,
3176 downloaded: false,
3177 downloading: true,
3178 ..Attachment::default()
3179 },
3180 ],
3181 ..Message::default()
3182 };
3183 let mut interner = NpubInterner::new();
3184 let compact = CompactMessage::from_message(&msg, &mut interner);
3185 assert_eq!(compact.attachments.len(), 2);
3186
3187 let restored = compact.to_message(&interner);
3188 assert_eq!(restored.attachments.len(), 2);
3189 assert_eq!(restored.attachments[0].name, "sunset.jpg");
3190 assert_eq!(restored.attachments[0].extension, "jpg");
3191 assert!(restored.attachments[0].downloaded);
3192 assert_eq!(restored.attachments[1].name, "video.mp4");
3193 assert!(restored.attachments[1].downloading);
3194 assert!(!restored.attachments[1].downloaded);
3195 }
3196
3197 #[test]
3198 fn compact_message_with_reactions_roundtrip() {
3199 let msg = Message {
3200 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3201 reactions: vec![
3202 Reaction {
3203 id: "aaa0000000000000000000000000000000000000000000000000000000000000".into(),
3204 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3205 author_id: "npub1alice".into(),
3206 emoji: "\u{2764}".into(), emoji_url: None,
3208 },
3209 Reaction {
3210 id: "bbb0000000000000000000000000000000000000000000000000000000000000".into(),
3211 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3212 author_id: "npub1bob".into(),
3213 emoji: "\u{1f525}".into(), emoji_url: None,
3215 },
3216 ],
3217 ..Message::default()
3218 };
3219 let mut interner = NpubInterner::new();
3220 let compact = CompactMessage::from_message(&msg, &mut interner);
3221 let restored = compact.to_message(&interner);
3222
3223 assert_eq!(restored.reactions.len(), 2);
3224 assert_eq!(restored.reactions[0].emoji, "\u{2764}");
3225 assert_eq!(restored.reactions[0].author_id, "npub1alice");
3226 assert_eq!(restored.reactions[1].emoji, "\u{1f525}");
3227 assert_eq!(restored.reactions[1].author_id, "npub1bob");
3228 }
3229
3230 #[test]
3231 fn compact_message_with_edit_history_roundtrip() {
3232 let msg = Message {
3233 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3234 content: "Final version".into(),
3235 edited: true,
3236 edit_history: Some(vec![
3237 EditEntry { content: "First draft".into(), edited_at: 1000 },
3238 EditEntry { content: "Second draft".into(), edited_at: 2000 },
3239 EditEntry { content: "Final version".into(), edited_at: 3000 },
3240 ]),
3241 ..Message::default()
3242 };
3243 let mut interner = NpubInterner::new();
3244 let compact = CompactMessage::from_message(&msg, &mut interner);
3245 assert!(compact.is_edited());
3246
3247 let restored = compact.to_message(&interner);
3248 assert!(restored.edited);
3249 let history = restored.edit_history.unwrap();
3250 assert_eq!(history.len(), 3);
3251 assert_eq!(history[0].content, "First draft");
3252 assert_eq!(history[2].content, "Final version");
3253 }
3254
3255 #[test]
3256 fn compact_message_with_preview_metadata_roundtrip() {
3257 let meta = SiteMetadata {
3258 domain: "example.com".into(),
3259 og_title: Some("Title".into()),
3260 og_description: Some("Desc".into()),
3261 og_image: Some("https://example.com/img.png".into()),
3262 og_url: None,
3263 og_type: None,
3264 title: None,
3265 description: None,
3266 favicon: None,
3267 };
3268 let msg = Message {
3269 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3270 preview_metadata: Some(meta.clone()),
3271 ..Message::default()
3272 };
3273 let mut interner = NpubInterner::new();
3274 let compact = CompactMessage::from_message(&msg, &mut interner);
3275 let restored = compact.to_message(&interner);
3276
3277 let restored_meta = restored.preview_metadata.unwrap();
3278 assert_eq!(restored_meta.domain, "example.com");
3279 assert_eq!(restored_meta.og_title, Some("Title".into()));
3280 assert_eq!(restored_meta.og_image, Some("https://example.com/img.png".into()));
3281 assert_eq!(restored_meta.og_url, None);
3282 }
3283
3284 #[test]
3285 fn compact_message_with_replied_to_roundtrip() {
3286 let msg = Message {
3287 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3288 replied_to: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3289 replied_to_content: Some("Original text".into()),
3290 replied_to_npub: Some("npub1original".into()),
3291 replied_to_has_attachment: Some(false),
3292 ..Message::default()
3293 };
3294 let mut interner = NpubInterner::new();
3295 let compact = CompactMessage::from_message(&msg, &mut interner);
3296 assert!(compact.has_reply());
3297
3298 let restored = compact.to_message(&interner);
3299 assert_eq!(restored.replied_to, "1111111111111111111111111111111111111111111111111111111111111111");
3300 assert_eq!(restored.replied_to_content, Some("Original text".into()));
3301 assert_eq!(restored.replied_to_npub, Some("npub1original".into()));
3302 assert_eq!(restored.replied_to_has_attachment, Some(false));
3303 }
3304
3305 #[test]
3306 fn compact_message_empty_roundtrip() {
3307 let msg = Message::default();
3308 let mut interner = NpubInterner::new();
3309 let compact = CompactMessage::from_message(&msg, &mut interner);
3310 let restored = compact.to_message(&interner);
3311
3312 assert_eq!(restored.id, "0000000000000000000000000000000000000000000000000000000000000000",
3313 "empty ID should decode as all zeros hex");
3314 assert_eq!(restored.content, "");
3315 assert!(!restored.mine);
3316 assert!(!restored.pending);
3317 assert!(!restored.failed);
3318 assert!(!restored.edited);
3319 assert_eq!(restored.npub, None);
3320 assert!(restored.replied_to.is_empty() || restored.replied_to == "0000000000000000000000000000000000000000000000000000000000000000");
3321 assert_eq!(restored.replied_to_content, None);
3322 assert_eq!(restored.replied_to_npub, None);
3323 assert_eq!(restored.replied_to_has_attachment, None);
3324 assert_eq!(restored.wrapper_event_id, None);
3325 assert!(restored.attachments.is_empty());
3326 assert!(restored.reactions.is_empty());
3327 assert_eq!(restored.edit_history, None);
3328 assert_eq!(restored.preview_metadata, None);
3329 }
3330
3331 #[test]
3336 fn compact_attachment_from_attachment_roundtrip() {
3337 let att = Attachment {
3338 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3339 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3340 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(), extension: "zip".into(),
3342 name: "archive.zip".into(),
3343 url: "https://blossom.test.com".into(),
3344 path: "/downloads/archive.zip".into(),
3345 size: 99999,
3346 img_meta: None,
3347 downloading: false,
3348 downloaded: true,
3349 webxdc_topic: None,
3350 group_id: None,
3351 original_hash: None,
3352 scheme_version: None,
3353 mls_filename: None,
3354 };
3355
3356 let compact = CompactAttachment::from_attachment(&att);
3357 let restored = compact.to_attachment();
3358
3359 assert_eq!(restored.id, att.id, "id mismatch");
3360 assert_eq!(restored.key, att.key, "key mismatch");
3361 assert_eq!(restored.nonce, att.nonce, "nonce mismatch");
3362 assert_eq!(restored.extension, att.extension, "extension mismatch");
3363 assert_eq!(restored.name, att.name, "name mismatch");
3364 assert_eq!(restored.url, att.url, "url mismatch");
3365 assert_eq!(restored.path, att.path, "path mismatch");
3366 assert_eq!(restored.size, att.size, "size mismatch");
3367 assert_eq!(restored.downloading, att.downloading, "downloading mismatch");
3368 assert_eq!(restored.downloaded, att.downloaded, "downloaded mismatch");
3369 }
3370
3371 #[test]
3372 fn compact_attachment_from_attachment_owned_roundtrip() {
3373 let att = Attachment {
3374 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3375 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3376 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(),
3377 extension: "pdf".into(),
3378 name: "document.pdf".into(),
3379 url: "https://server.com".into(),
3380 path: "".into(),
3381 size: 1024,
3382 img_meta: None,
3383 downloading: false,
3384 downloaded: false,
3385 webxdc_topic: None,
3386 group_id: None,
3387 original_hash: None,
3388 scheme_version: None,
3389 mls_filename: None,
3390 };
3391 let att_clone = att.clone();
3392
3393 let compact = CompactAttachment::from_attachment_owned(att);
3394 let restored = compact.to_attachment();
3395
3396 assert_eq!(restored.id, att_clone.id);
3397 assert_eq!(restored.name, att_clone.name);
3398 assert_eq!(restored.size, att_clone.size);
3399 }
3400
3401 #[test]
3402 fn compact_attachment_key_nonce_zeros() {
3403 let att = Attachment {
3404 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3405 key: "".into(), nonce: "".into(), ..Attachment::default()
3408 };
3409 let compact = CompactAttachment::from_attachment(&att);
3410 assert_eq!(compact.key, [0u8; 32], "empty key should be all zeros");
3411 assert_eq!(compact.nonce, [0u8; 16], "empty nonce should be all zeros");
3412
3413 let restored = compact.to_attachment();
3414 assert_eq!(restored.key, "", "zero key should restore as empty string");
3415 assert_eq!(restored.nonce, "", "zero nonce should restore as empty string");
3416 }
3417
3418 #[test]
3419 fn compact_attachment_short_nonce_mls_12byte() {
3420 let att = Attachment {
3422 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3423 nonce: "aabbccddaabbccddaabbccdd".into(), ..Attachment::default()
3425 };
3426 let compact = CompactAttachment::from_attachment(&att);
3427 assert!(compact.flags.is_short_nonce(), "12-byte nonce should set short_nonce flag");
3428
3429 let restored = compact.to_attachment();
3430 assert_eq!(restored.nonce, "aabbccddaabbccddaabbccdd", "short nonce should roundtrip");
3431 }
3432
3433 #[test]
3434 fn compact_attachment_long_nonce_dm_16byte() {
3435 let att = Attachment {
3437 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3438 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(), ..Attachment::default()
3440 };
3441 let compact = CompactAttachment::from_attachment(&att);
3442 assert!(!compact.flags.is_short_nonce(), "16-byte nonce should NOT set short_nonce flag");
3443
3444 let restored = compact.to_attachment();
3445 assert_eq!(restored.nonce, "aabbccddaabbccddaabbccddaabbccdd", "long nonce should roundtrip");
3446 }
3447
3448 #[test]
3449 fn compact_attachment_id_eq_comparison() {
3450 let att = Attachment {
3451 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3452 ..Attachment::default()
3453 };
3454 let compact = CompactAttachment::from_attachment(&att);
3455
3456 assert!(compact.id_eq("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"),
3457 "id_eq should match same hex");
3458 assert!(!compact.id_eq("1111111111111111111111111111111111111111111111111111111111111111"),
3459 "id_eq should not match different hex");
3460 }
3461
3462 #[test]
3463 fn compact_attachment_all_optional_fields_none() {
3464 let att = Attachment::default();
3465 let compact = CompactAttachment::from_attachment(&att);
3466
3467 assert!(compact.img_meta.is_none());
3468 assert!(compact.group_id.is_none());
3469 assert!(compact.original_hash.is_none());
3470 assert!(compact.webxdc_topic.is_none());
3471 assert!(compact.mls_filename.is_none());
3472 assert!(compact.scheme_version.is_none());
3473
3474 let restored = compact.to_attachment();
3475 assert!(restored.img_meta.is_none());
3476 assert!(restored.group_id.is_none());
3477 assert!(restored.original_hash.is_none());
3478 assert!(restored.webxdc_topic.is_none());
3479 assert!(restored.mls_filename.is_none());
3480 assert!(restored.scheme_version.is_none());
3481 }
3482
3483 #[test]
3484 fn compact_attachment_all_optional_fields_some() {
3485 let att = Attachment {
3486 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3487 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3488 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(),
3489 extension: "xdc".into(),
3490 name: "app.xdc".into(),
3491 url: "https://server.com/file".into(),
3492 path: "/local/app.xdc".into(),
3493 size: 50000,
3494 img_meta: Some(ImageMetadata {
3495 thumbhash: "hash123".into(),
3496 width: 1920,
3497 height: 1080,
3498 }),
3499 downloading: false,
3500 downloaded: true,
3501 webxdc_topic: Some("game-state".into()),
3502 group_id: Some("cccc000000000000000000000000000000000000000000000000000000000000".into()),
3503 original_hash: Some("dddd000000000000000000000000000000000000000000000000000000000000".into()),
3504 scheme_version: Some("mip04-v1".into()),
3505 mls_filename: Some("encrypted.bin".into()),
3506 };
3507
3508 let compact = CompactAttachment::from_attachment(&att);
3509
3510 assert!(compact.img_meta.is_some());
3511 assert!(compact.group_id.is_some());
3512 assert!(compact.original_hash.is_some());
3513 assert!(compact.webxdc_topic.is_some());
3514 assert!(compact.mls_filename.is_some());
3515 assert!(compact.scheme_version.is_some());
3516
3517 let restored = compact.to_attachment();
3518 let meta = restored.img_meta.unwrap();
3519 assert_eq!(meta.thumbhash, "hash123");
3520 assert_eq!(meta.width, 1920);
3521 assert_eq!(meta.height, 1080);
3522 assert_eq!(restored.webxdc_topic, Some("game-state".into()));
3523 assert_eq!(restored.group_id.unwrap(), att.group_id.unwrap());
3524 assert_eq!(restored.original_hash.unwrap(), att.original_hash.unwrap());
3525 assert_eq!(restored.scheme_version, Some("mip04-v1".into()));
3526 assert_eq!(restored.mls_filename, Some("encrypted.bin".into()));
3527 }
3528
3529 #[test]
3534 fn compact_reaction_from_reaction_roundtrip() {
3535 let reaction = Reaction {
3536 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3537 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3538 author_id: "npub1alice".into(),
3539 emoji: "+".into(),
3540 emoji_url: None,
3541 };
3542 let mut interner = NpubInterner::new();
3543 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3544 let restored = compact.to_reaction(&interner);
3545
3546 assert_eq!(restored.id, reaction.id, "id mismatch");
3547 assert_eq!(restored.reference_id, reaction.reference_id, "reference_id mismatch");
3548 assert_eq!(restored.author_id, reaction.author_id, "author_id mismatch");
3549 assert_eq!(restored.emoji, reaction.emoji, "emoji mismatch");
3550 }
3551
3552 #[test]
3553 fn compact_reaction_author_resolved_via_interner() {
3554 let mut interner = NpubInterner::new();
3555 let alice_handle = interner.intern("npub1alice");
3556
3557 let reaction = Reaction {
3558 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3559 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3560 author_id: "npub1alice".into(),
3561 emoji: "+".into(),
3562 emoji_url: None,
3563 };
3564 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3565 assert_eq!(compact.author_idx, alice_handle, "should reuse existing interner handle");
3566
3567 let resolved = interner.resolve(compact.author_idx).unwrap();
3568 assert_eq!(resolved, "npub1alice");
3569 }
3570
3571 #[test]
3572 fn compact_reaction_unicode_emoji() {
3573 let reaction = Reaction {
3574 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3575 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3576 author_id: "npub1test".into(),
3577 emoji: "\u{1f431}\u{200d}\u{1f4bb}".into(), emoji_url: None,
3579 };
3580 let mut interner = NpubInterner::new();
3581 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3582 let restored = compact.to_reaction(&interner);
3583 assert_eq!(restored.emoji, "\u{1f431}\u{200d}\u{1f4bb}", "complex unicode emoji should roundtrip");
3584 }
3585
3586 #[test]
3587 fn compact_reaction_custom_emoji() {
3588 let reaction = Reaction {
3589 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3590 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3591 author_id: "npub1test".into(),
3592 emoji: ":cat_heart_eyes:".into(),
3593 emoji_url: None,
3594 };
3595 let mut interner = NpubInterner::new();
3596 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3597 let restored = compact.to_reaction(&interner);
3598 assert_eq!(restored.emoji, ":cat_heart_eyes:", "custom emoji shortcode should roundtrip");
3599 }
3600
3601 #[test]
3602 fn compact_reaction_owned_conversion() {
3603 let reaction = Reaction {
3604 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3605 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3606 author_id: "npub1bob".into(),
3607 emoji: "\u{1f44d}".into(), emoji_url: None,
3609 };
3610 let reaction_clone = reaction.clone();
3611 let mut interner = NpubInterner::new();
3612 let compact = CompactReaction::from_reaction_owned(reaction, &mut interner);
3613 let restored = compact.to_reaction(&interner);
3614
3615 assert_eq!(restored.id, reaction_clone.id);
3616 assert_eq!(restored.author_id, reaction_clone.author_id);
3617 assert_eq!(restored.emoji, reaction_clone.emoji);
3618 }
3619
3620 #[test]
3625 fn tinyvec_empty() {
3626 let tv: TinyVec<u32> = TinyVec::new();
3627 assert!(tv.is_empty());
3628 assert_eq!(tv.len(), 0);
3629 assert_eq!(tv.as_slice(), &[] as &[u32]);
3630 assert_eq!(tv.first(), None);
3631 assert_eq!(tv.last(), None);
3632 }
3633
3634 #[test]
3635 fn tinyvec_from_vec_and_back() {
3636 let original = vec![1u32, 2, 3, 4, 5];
3637 let tv = TinyVec::from_vec(original.clone());
3638 assert_eq!(tv.len(), 5);
3639 assert_eq!(tv.to_vec(), original);
3640 }
3641
3642 #[test]
3643 fn tinyvec_indexing() {
3644 let tv = TinyVec::from_vec(vec![10u32, 20, 30]);
3645 assert_eq!(tv[0], 10);
3646 assert_eq!(tv[1], 20);
3647 assert_eq!(tv[2], 30);
3648 assert_eq!(tv.get(0), Some(&10));
3649 assert_eq!(tv.get(3), None);
3650 }
3651
3652 #[test]
3653 fn tinyvec_push() {
3654 let mut tv = TinyVec::from_vec(vec![1u32, 2]);
3655 tv.push(3);
3656 assert_eq!(tv.len(), 3);
3657 assert_eq!(tv.to_vec(), vec![1, 2, 3]);
3658 }
3659
3660 #[test]
3661 fn tinyvec_clone() {
3662 let tv = TinyVec::from_vec(vec!["hello".to_string(), "world".to_string()]);
3663 let cloned = tv.clone();
3664 assert_eq!(cloned.len(), 2);
3665 assert_eq!(cloned[0], "hello");
3666 assert_eq!(cloned[1], "world");
3667 }
3668
3669 #[test]
3670 fn tinyvec_retain() {
3671 let mut tv = TinyVec::from_vec(vec![1u32, 2, 3, 4, 5]);
3672 tv.retain(|&x| x % 2 == 0);
3673 assert_eq!(tv.to_vec(), vec![2, 4]);
3674 }
3675
3676 #[test]
3677 fn tinyvec_empty_from_empty_vec() {
3678 let tv = TinyVec::<u32>::from_vec(vec![]);
3679 assert!(tv.is_empty());
3680 assert_eq!(tv.len(), 0);
3681 }
3682
3683 #[test]
3684 fn tinyvec_iter() {
3685 let tv = TinyVec::from_vec(vec![10u32, 20, 30]);
3686 let sum: u32 = tv.iter().sum();
3687 assert_eq!(sum, 60);
3688 }
3689
3690 #[test]
3691 fn tinyvec_any() {
3692 let tv = TinyVec::from_vec(vec![1u32, 2, 3]);
3693 assert!(tv.any(|&x| x == 2));
3694 assert!(!tv.any(|&x| x == 99));
3695 }
3696
3697 #[test]
3702 fn hex_to_bytes_16_full_32_chars() {
3703 let hex = "aabbccddaabbccddaabbccddaabbccdd";
3704 let bytes = hex_to_bytes_16(hex);
3705 assert_eq!(bytes[0], 0xaa);
3706 assert_eq!(bytes[1], 0xbb);
3707 assert_eq!(bytes[15], 0xdd);
3708 }
3709
3710 #[test]
3711 fn hex_to_bytes_16_short_input_padded() {
3712 let hex = "aabb";
3714 let bytes = hex_to_bytes_16(hex);
3715 assert_eq!(bytes[0], 0xaa);
3716 assert_eq!(bytes[1], 0xbb);
3717 for i in 2..16 {
3719 assert_eq!(bytes[i], 0, "byte {} should be 0 from padding", i);
3720 }
3721 }
3722
3723 #[test]
3724 fn bytes_to_hex_string_roundtrip() {
3725 let bytes: Vec<u8> = vec![0xaa, 0xbb, 0xcc, 0xdd];
3726 let hex = bytes_to_hex_string(&bytes);
3727 assert_eq!(hex, "aabbccdd");
3728 }
3729}