Skip to content

Commit 115c737

Browse files
authored
chore: use option uid (#431)
* chore: use option uid * chore: fmt * chore: docs
1 parent 7a9248e commit 115c737

File tree

17 files changed

+996
-214
lines changed

17 files changed

+996
-214
lines changed

collab/src/folder/folder.rs

Lines changed: 683 additions & 73 deletions
Large diffs are not rendered by default.

collab/src/folder/folder_observe.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub(crate) fn subscribe_view_change(
5252
txn,
5353
&view_relations,
5454
&section_map,
55-
uid,
55+
Some(uid),
5656
mappings,
5757
) {
5858
deletion_cache.insert(view.id, Arc::new(view.clone()));
@@ -78,7 +78,7 @@ pub(crate) fn subscribe_view_change(
7878
txn,
7979
&view_relations,
8080
&section_map,
81-
uid,
81+
Some(uid),
8282
mappings,
8383
) {
8484
// Update deletion cache with the updated view

collab/src/folder/section.rs

Lines changed: 152 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ impl SectionMap {
4545
&self,
4646
txn: &T,
4747
section: Section,
48-
uid: i64,
48+
uid: Option<i64>,
4949
) -> Option<SectionOperation> {
5050
let container = self.get_section(txn, section.as_ref())?;
5151
Some(SectionOperation {
52-
uid: UserId::from(uid),
52+
uid: uid.map(UserId::from),
5353
container,
5454
section,
5555
change_tx: self.change_tx.clone(),
@@ -65,6 +65,71 @@ impl SectionMap {
6565
}
6666
}
6767

68+
/// Represents different types of user-specific view collections in a folder.
69+
///
70+
/// Sections are **per-user** organizational categories that allow each user in a
71+
/// collaborative folder to maintain their own personal view collections. Each section
72+
/// type has a specific semantic purpose.
73+
///
74+
/// # Section Types
75+
///
76+
/// ## Predefined Sections
77+
///
78+
/// - **Favorite**: Views the user has marked as favorites for quick access
79+
/// - **Recent**: Recently accessed views, typically ordered by access time
80+
/// - **Trash**: Views the user has deleted (pending permanent removal)
81+
/// - **Private**: Views that are private to the user and hidden from others
82+
///
83+
/// ## Custom Sections
84+
///
85+
/// - **Custom(String)**: User-defined section types for extensibility
86+
///
87+
/// # Storage Architecture
88+
///
89+
/// Each section in the CRDT is stored as a nested map structure:
90+
///
91+
/// ```text
92+
/// SectionMap
93+
/// ├─ "favorite" (MapRef)
94+
/// │ ├─ "1" (uid) → Array[SectionItem, SectionItem, ...]
95+
/// │ ├─ "2" (uid) → Array[SectionItem, SectionItem, ...]
96+
/// │ └─ ...
97+
/// ├─ "recent" (MapRef)
98+
/// │ └─ ...
99+
/// ├─ "trash" (MapRef)
100+
/// │ └─ ...
101+
/// └─ "private" (MapRef)
102+
/// └─ ...
103+
/// ```
104+
///
105+
/// This allows multiple users to collaborate on the same folder while maintaining
106+
/// independent personal collections. For example:
107+
/// - User 1's favorites don't affect User 2's favorites
108+
/// - Each user has their own trash bin
109+
/// - Each user has their own private views
110+
///
111+
/// # String Representation
112+
///
113+
/// Each section variant has a unique string identifier used as the CRDT map key:
114+
/// - `Favorite` → `"favorite"`
115+
/// - `Recent` → `"recent"`
116+
/// - `Trash` → `"trash"`
117+
/// - `Private` → `"private"`
118+
/// - `Custom("my_section")` → `"my_section"`
119+
///
120+
/// # Examples
121+
///
122+
/// ```rust,ignore
123+
/// use collab::folder::Section;
124+
///
125+
/// // Predefined sections
126+
/// let fav = Section::Favorite;
127+
/// assert_eq!(fav.as_ref(), "favorite");
128+
///
129+
/// // Custom section
130+
/// let custom = Section::from("my_custom_section".to_string());
131+
/// assert_eq!(custom.as_ref(), "my_custom_section");
132+
/// ```
68133
#[derive(Clone, Debug, PartialEq, Eq)]
69134
pub enum Section {
70135
Favorite,
@@ -119,7 +184,7 @@ pub enum TrashSectionChange {
119184
pub type SectionsByUid = HashMap<UserId, Vec<SectionItem>>;
120185

121186
pub struct SectionOperation {
122-
uid: UserId,
187+
uid: Option<UserId>,
123188
container: MapRef,
124189
section: Section,
125190
change_tx: Option<SectionChangeSender>,
@@ -130,8 +195,8 @@ impl SectionOperation {
130195
&self.container
131196
}
132197

133-
fn uid(&self) -> &UserId {
134-
&self.uid
198+
fn uid(&self) -> Option<&UserId> {
199+
self.uid.as_ref()
135200
}
136201

137202
pub fn get_sections<T: ReadTxn>(&self, txn: &T) -> SectionsByUid {
@@ -154,9 +219,12 @@ impl SectionOperation {
154219
}
155220

156221
pub fn contains_with_txn<T: ReadTxn>(&self, txn: &T, view_id: &ViewId) -> bool {
222+
let Some(uid) = self.uid() else {
223+
return false;
224+
};
157225
match self
158226
.container()
159-
.get_with_txn::<_, ArrayRef>(txn, self.uid().as_ref())
227+
.get_with_txn::<_, ArrayRef>(txn, uid.as_ref())
160228
{
161229
None => false,
162230
Some(array) => {
@@ -173,9 +241,12 @@ impl SectionOperation {
173241
}
174242

175243
pub fn get_all_section_item<T: ReadTxn>(&self, txn: &T) -> Vec<SectionItem> {
244+
let Some(uid) = self.uid() else {
245+
return vec![];
246+
};
176247
match self
177248
.container()
178-
.get_with_txn::<_, ArrayRef>(txn, self.uid().as_ref())
249+
.get_with_txn::<_, ArrayRef>(txn, uid.as_ref())
179250
{
180251
None => vec![],
181252
Some(array) => {
@@ -201,6 +272,9 @@ impl SectionOperation {
201272
id: T,
202273
prev_id: Option<T>,
203274
) {
275+
let Some(uid) = self.uid() else {
276+
return;
277+
};
204278
let section_items = self.get_all_section_item(txn);
205279
let id = id.as_ref();
206280
let old_pos = section_items
@@ -217,7 +291,7 @@ impl SectionOperation {
217291
.unwrap_or(0);
218292
let section_array = self
219293
.container()
220-
.get_with_txn::<_, ArrayRef>(txn, self.uid().as_ref());
294+
.get_with_txn::<_, ArrayRef>(txn, uid.as_ref());
221295
// If the new position index is greater than the length of the section, yrs will panic
222296
if new_pos > section_items.len() as u32 {
223297
return;
@@ -233,9 +307,12 @@ impl SectionOperation {
233307
txn: &mut TransactionMut,
234308
ids: Vec<T>,
235309
) {
310+
let Some(uid) = self.uid() else {
311+
return;
312+
};
236313
if let Some(fav_array) = self
237314
.container()
238-
.get_with_txn::<_, ArrayRef>(txn, self.uid().as_ref())
315+
.get_with_txn::<_, ArrayRef>(txn, uid.as_ref())
239316
{
240317
for id in &ids {
241318
if let Some(pos) = self
@@ -267,8 +344,11 @@ impl SectionOperation {
267344
}
268345

269346
pub fn add_sections_item(&self, txn: &mut TransactionMut, items: Vec<SectionItem>) {
347+
let Some(uid) = self.uid() else {
348+
return;
349+
};
270350
let item_ids = items.iter().map(|item| item.id).collect::<Vec<_>>();
271-
self.add_sections_for_user_with_txn(txn, self.uid(), items);
351+
self.add_sections_for_user_with_txn(txn, uid, items);
272352
if let Some(change_tx) = self.change_tx.as_ref() {
273353
match self.section {
274354
Section::Favorite => {},
@@ -298,16 +378,77 @@ impl SectionOperation {
298378
}
299379

300380
pub fn clear(&self, txn: &mut TransactionMut) {
381+
let Some(uid) = self.uid() else {
382+
return;
383+
};
301384
if let Some(array) = self
302385
.container()
303-
.get_with_txn::<_, ArrayRef>(txn, self.uid().as_ref())
386+
.get_with_txn::<_, ArrayRef>(txn, uid.as_ref())
304387
{
305388
let len = array.iter(txn).count();
306389
array.remove_range(txn, 0, len as u32);
307390
}
308391
}
309392
}
310393

394+
/// An item in a user's section, representing a view and when it was added.
395+
///
396+
/// `SectionItem` is the fundamental unit stored in sections (Favorite, Recent, Trash, Private).
397+
/// Each item records both which view is in the section and when it was added, enabling
398+
/// time-based operations like sorting by recency or tracking deletion times.
399+
///
400+
/// # Fields
401+
///
402+
/// * `id` - The UUID of the view in this section
403+
/// * `timestamp` - Unix timestamp (milliseconds) when the view was added to this section
404+
///
405+
/// # Storage Format
406+
///
407+
/// SectionItems are serialized as Yrs `Any` values (essentially JSON-like maps) in the CRDT:
408+
///
409+
/// ```json
410+
/// {
411+
/// "id": "550e8400-e29b-41d4-a716-446655440000",
412+
/// "timestamp": 1704067200000
413+
/// }
414+
/// ```
415+
///
416+
/// Multiple items are stored in a Yrs array per user per section:
417+
///
418+
/// ```text
419+
/// section["favorite"]["123"] = [
420+
/// SectionItem { id: view_1, timestamp: 1704067200000 },
421+
/// SectionItem { id: view_2, timestamp: 1704068000000 },
422+
/// ...
423+
/// ]
424+
/// ```
425+
///
426+
/// # Usage Patterns
427+
///
428+
/// ## Favorite Sections
429+
/// ```rust,ignore
430+
/// let favorites = folder.get_my_favorite_sections(Some(uid));
431+
/// for item in favorites {
432+
/// println!("View {} favorited at {}", item.id, item.timestamp);
433+
/// }
434+
/// ```
435+
///
436+
/// ## Recent Sections (sorted by timestamp)
437+
/// ```rust,ignore
438+
/// let mut recent = folder.get_my_recent_sections(Some(uid));
439+
/// recent.sort_by_key(|item| std::cmp::Reverse(item.timestamp)); // newest first
440+
/// let most_recent = recent.first();
441+
/// ```
442+
///
443+
/// ## Trash Sections (check deletion time)
444+
/// ```rust,ignore
445+
/// let trash = folder.get_my_trash_sections(Some(uid));
446+
/// let thirty_days_ago = current_timestamp() - (30 * 24 * 60 * 60 * 1000);
447+
/// let permanently_delete = trash
448+
/// .iter()
449+
/// .filter(|item| item.timestamp < thirty_days_ago)
450+
/// .collect::<Vec<_>>();
451+
/// ```
311452
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
312453
pub struct SectionItem {
313454
pub id: ViewId,

0 commit comments

Comments
 (0)