store.rs

  1use crate::db::{self, ChannelId, UserId};
  2use anyhow::{anyhow, Result};
  3use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet};
  4use rpc::{proto, ConnectionId, Receipt};
  5use serde::Serialize;
  6use std::{
  7    collections::hash_map,
  8    ffi::{OsStr, OsString},
  9    mem,
 10    path::{Path, PathBuf},
 11    str,
 12    time::{Duration, Instant},
 13};
 14use tracing::instrument;
 15
 16#[derive(Default, Serialize)]
 17pub struct Store {
 18    connections: HashMap<ConnectionId, ConnectionState>,
 19    connections_by_user_id: HashMap<UserId, HashSet<ConnectionId>>,
 20    projects: HashMap<u64, Project>,
 21    #[serde(skip)]
 22    channels: HashMap<ChannelId, Channel>,
 23    next_project_id: u64,
 24}
 25
 26#[derive(Serialize)]
 27struct ConnectionState {
 28    user_id: UserId,
 29    admin: bool,
 30    projects: HashSet<u64>,
 31    requested_projects: HashSet<u64>,
 32    channels: HashSet<ChannelId>,
 33}
 34
 35#[derive(Serialize)]
 36pub struct Project {
 37    pub host_connection_id: ConnectionId,
 38    pub host_user_id: UserId,
 39    pub guests: HashMap<ConnectionId, (ReplicaId, UserId)>,
 40    #[serde(skip)]
 41    pub join_requests: HashMap<UserId, Vec<Receipt<proto::JoinProject>>>,
 42    pub active_replica_ids: HashSet<ReplicaId>,
 43    pub worktrees: BTreeMap<u64, Worktree>,
 44    pub language_servers: Vec<proto::LanguageServer>,
 45    #[serde(skip)]
 46    last_activity: Option<Instant>,
 47}
 48
 49#[derive(Default, Serialize)]
 50pub struct Worktree {
 51    pub root_name: String,
 52    pub visible: bool,
 53    #[serde(skip)]
 54    pub entries: HashMap<u64, proto::Entry>,
 55    #[serde(skip)]
 56    pub extension_counts: HashMap<OsString, usize>,
 57    #[serde(skip)]
 58    pub diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
 59    pub scan_id: u64,
 60}
 61
 62#[derive(Default)]
 63pub struct Channel {
 64    pub connection_ids: HashSet<ConnectionId>,
 65}
 66
 67pub type ReplicaId = u16;
 68
 69#[derive(Default)]
 70pub struct RemovedConnectionState {
 71    pub user_id: UserId,
 72    pub hosted_projects: HashMap<u64, Project>,
 73    pub guest_project_ids: HashSet<u64>,
 74    pub contact_ids: HashSet<UserId>,
 75}
 76
 77pub struct LeftProject {
 78    pub host_user_id: UserId,
 79    pub host_connection_id: ConnectionId,
 80    pub connection_ids: Vec<ConnectionId>,
 81    pub remove_collaborator: bool,
 82    pub cancel_request: Option<UserId>,
 83    pub unshare: bool,
 84}
 85
 86#[derive(Copy, Clone)]
 87pub struct Metrics {
 88    pub connections: usize,
 89    pub registered_projects: usize,
 90    pub active_projects: usize,
 91    pub shared_projects: usize,
 92}
 93
 94impl Store {
 95    pub fn metrics(&self) -> Metrics {
 96        let connections = self.connections.values().filter(|c| !c.admin).count();
 97        let mut registered_projects = 0;
 98        let mut active_projects = 0;
 99        let mut shared_projects = 0;
100        for project in self.projects.values() {
101            if let Some(connection) = self.connections.get(&project.host_connection_id) {
102                if !connection.admin {
103                    registered_projects += 1;
104                    if project.is_active() {
105                        active_projects += 1;
106                        if !project.guests.is_empty() {
107                            shared_projects += 1;
108                        }
109                    }
110                }
111            }
112        }
113
114        Metrics {
115            connections,
116            registered_projects,
117            active_projects,
118            shared_projects,
119        }
120    }
121
122    #[instrument(skip(self))]
123    pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) {
124        self.connections.insert(
125            connection_id,
126            ConnectionState {
127                user_id,
128                admin,
129                projects: Default::default(),
130                requested_projects: Default::default(),
131                channels: Default::default(),
132            },
133        );
134        self.connections_by_user_id
135            .entry(user_id)
136            .or_default()
137            .insert(connection_id);
138    }
139
140    #[instrument(skip(self))]
141    pub fn remove_connection(
142        &mut self,
143        connection_id: ConnectionId,
144    ) -> Result<RemovedConnectionState> {
145        let connection = self
146            .connections
147            .get_mut(&connection_id)
148            .ok_or_else(|| anyhow!("no such connection"))?;
149
150        let user_id = connection.user_id;
151        let connection_projects = mem::take(&mut connection.projects);
152        let connection_channels = mem::take(&mut connection.channels);
153
154        let mut result = RemovedConnectionState::default();
155        result.user_id = user_id;
156
157        // Leave all channels.
158        for channel_id in connection_channels {
159            self.leave_channel(connection_id, channel_id);
160        }
161
162        // Unregister and leave all projects.
163        for project_id in connection_projects {
164            if let Ok(project) = self.unregister_project(project_id, connection_id) {
165                result.hosted_projects.insert(project_id, project);
166            } else if self.leave_project(connection_id, project_id).is_ok() {
167                result.guest_project_ids.insert(project_id);
168            }
169        }
170
171        let user_connections = self.connections_by_user_id.get_mut(&user_id).unwrap();
172        user_connections.remove(&connection_id);
173        if user_connections.is_empty() {
174            self.connections_by_user_id.remove(&user_id);
175        }
176
177        self.connections.remove(&connection_id).unwrap();
178
179        Ok(result)
180    }
181
182    #[cfg(test)]
183    pub fn channel(&self, id: ChannelId) -> Option<&Channel> {
184        self.channels.get(&id)
185    }
186
187    pub fn join_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) {
188        if let Some(connection) = self.connections.get_mut(&connection_id) {
189            connection.channels.insert(channel_id);
190            self.channels
191                .entry(channel_id)
192                .or_default()
193                .connection_ids
194                .insert(connection_id);
195        }
196    }
197
198    pub fn leave_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) {
199        if let Some(connection) = self.connections.get_mut(&connection_id) {
200            connection.channels.remove(&channel_id);
201            if let hash_map::Entry::Occupied(mut entry) = self.channels.entry(channel_id) {
202                entry.get_mut().connection_ids.remove(&connection_id);
203                if entry.get_mut().connection_ids.is_empty() {
204                    entry.remove();
205                }
206            }
207        }
208    }
209
210    pub fn user_id_for_connection(&self, connection_id: ConnectionId) -> Result<UserId> {
211        Ok(self
212            .connections
213            .get(&connection_id)
214            .ok_or_else(|| anyhow!("unknown connection"))?
215            .user_id)
216    }
217
218    pub fn connection_ids_for_user<'a>(
219        &'a self,
220        user_id: UserId,
221    ) -> impl 'a + Iterator<Item = ConnectionId> {
222        self.connections_by_user_id
223            .get(&user_id)
224            .into_iter()
225            .flatten()
226            .copied()
227    }
228
229    pub fn is_user_online(&self, user_id: UserId) -> bool {
230        !self
231            .connections_by_user_id
232            .get(&user_id)
233            .unwrap_or(&Default::default())
234            .is_empty()
235    }
236
237    pub fn build_initial_contacts_update(
238        &self,
239        contacts: Vec<db::Contact>,
240    ) -> proto::UpdateContacts {
241        let mut update = proto::UpdateContacts::default();
242
243        for contact in contacts {
244            match contact {
245                db::Contact::Accepted {
246                    user_id,
247                    should_notify,
248                } => {
249                    update
250                        .contacts
251                        .push(self.contact_for_user(user_id, should_notify));
252                }
253                db::Contact::Outgoing { user_id } => {
254                    update.outgoing_requests.push(user_id.to_proto())
255                }
256                db::Contact::Incoming {
257                    user_id,
258                    should_notify,
259                } => update
260                    .incoming_requests
261                    .push(proto::IncomingContactRequest {
262                        requester_id: user_id.to_proto(),
263                        should_notify,
264                    }),
265            }
266        }
267
268        update
269    }
270
271    pub fn contact_for_user(&self, user_id: UserId, should_notify: bool) -> proto::Contact {
272        proto::Contact {
273            user_id: user_id.to_proto(),
274            projects: self.project_metadata_for_user(user_id),
275            online: self.is_user_online(user_id),
276            should_notify,
277        }
278    }
279
280    pub fn project_metadata_for_user(&self, user_id: UserId) -> Vec<proto::ProjectMetadata> {
281        let connection_ids = self.connections_by_user_id.get(&user_id);
282        let project_ids = connection_ids.iter().flat_map(|connection_ids| {
283            connection_ids
284                .iter()
285                .filter_map(|connection_id| self.connections.get(connection_id))
286                .flat_map(|connection| connection.projects.iter().copied())
287        });
288
289        let mut metadata = Vec::new();
290        for project_id in project_ids {
291            if let Some(project) = self.projects.get(&project_id) {
292                if project.host_user_id == user_id {
293                    metadata.push(proto::ProjectMetadata {
294                        id: project_id,
295                        visible_worktree_root_names: project
296                            .worktrees
297                            .values()
298                            .filter(|worktree| worktree.visible)
299                            .map(|worktree| worktree.root_name.clone())
300                            .collect(),
301                        guests: project
302                            .guests
303                            .values()
304                            .map(|(_, user_id)| user_id.to_proto())
305                            .collect(),
306                    });
307                }
308            }
309        }
310
311        metadata
312    }
313
314    pub fn register_project(
315        &mut self,
316        host_connection_id: ConnectionId,
317        host_user_id: UserId,
318    ) -> u64 {
319        let project_id = self.next_project_id;
320        self.projects.insert(
321            project_id,
322            Project {
323                host_connection_id,
324                host_user_id,
325                guests: Default::default(),
326                join_requests: Default::default(),
327                active_replica_ids: Default::default(),
328                worktrees: Default::default(),
329                language_servers: Default::default(),
330                last_activity: None,
331            },
332        );
333        if let Some(connection) = self.connections.get_mut(&host_connection_id) {
334            connection.projects.insert(project_id);
335        }
336        self.next_project_id += 1;
337        project_id
338    }
339
340    pub fn update_project(
341        &mut self,
342        project_id: u64,
343        worktrees: &[proto::WorktreeMetadata],
344        connection_id: ConnectionId,
345    ) -> Result<()> {
346        let project = self
347            .projects
348            .get_mut(&project_id)
349            .ok_or_else(|| anyhow!("no such project"))?;
350        if project.host_connection_id == connection_id {
351            let mut old_worktrees = mem::take(&mut project.worktrees);
352            for worktree in worktrees {
353                if let Some(old_worktree) = old_worktrees.remove(&worktree.id) {
354                    project.worktrees.insert(worktree.id, old_worktree);
355                } else {
356                    project.worktrees.insert(
357                        worktree.id,
358                        Worktree {
359                            root_name: worktree.root_name.clone(),
360                            visible: worktree.visible,
361                            ..Default::default()
362                        },
363                    );
364                }
365            }
366            Ok(())
367        } else {
368            Err(anyhow!("no such project"))?
369        }
370    }
371
372    pub fn unregister_project(
373        &mut self,
374        project_id: u64,
375        connection_id: ConnectionId,
376    ) -> Result<Project> {
377        match self.projects.entry(project_id) {
378            hash_map::Entry::Occupied(e) => {
379                if e.get().host_connection_id == connection_id {
380                    let project = e.remove();
381
382                    if let Some(host_connection) = self.connections.get_mut(&connection_id) {
383                        host_connection.projects.remove(&project_id);
384                    }
385
386                    for guest_connection in project.guests.keys() {
387                        if let Some(connection) = self.connections.get_mut(&guest_connection) {
388                            connection.projects.remove(&project_id);
389                        }
390                    }
391
392                    for requester_user_id in project.join_requests.keys() {
393                        if let Some(requester_connection_ids) =
394                            self.connections_by_user_id.get_mut(&requester_user_id)
395                        {
396                            for requester_connection_id in requester_connection_ids.iter() {
397                                if let Some(requester_connection) =
398                                    self.connections.get_mut(requester_connection_id)
399                                {
400                                    requester_connection.requested_projects.remove(&project_id);
401                                }
402                            }
403                        }
404                    }
405
406                    Ok(project)
407                } else {
408                    Err(anyhow!("no such project"))?
409                }
410            }
411            hash_map::Entry::Vacant(_) => Err(anyhow!("no such project"))?,
412        }
413    }
414
415    pub fn update_diagnostic_summary(
416        &mut self,
417        project_id: u64,
418        worktree_id: u64,
419        connection_id: ConnectionId,
420        summary: proto::DiagnosticSummary,
421    ) -> Result<Vec<ConnectionId>> {
422        let project = self
423            .projects
424            .get_mut(&project_id)
425            .ok_or_else(|| anyhow!("no such project"))?;
426        if project.host_connection_id == connection_id {
427            let worktree = project
428                .worktrees
429                .get_mut(&worktree_id)
430                .ok_or_else(|| anyhow!("no such worktree"))?;
431            worktree
432                .diagnostic_summaries
433                .insert(summary.path.clone().into(), summary);
434            return Ok(project.connection_ids());
435        }
436
437        Err(anyhow!("no such worktree"))?
438    }
439
440    pub fn start_language_server(
441        &mut self,
442        project_id: u64,
443        connection_id: ConnectionId,
444        language_server: proto::LanguageServer,
445    ) -> Result<Vec<ConnectionId>> {
446        let project = self
447            .projects
448            .get_mut(&project_id)
449            .ok_or_else(|| anyhow!("no such project"))?;
450        if project.host_connection_id == connection_id {
451            project.language_servers.push(language_server);
452            return Ok(project.connection_ids());
453        }
454
455        Err(anyhow!("no such project"))?
456    }
457
458    pub fn request_join_project(
459        &mut self,
460        requester_id: UserId,
461        project_id: u64,
462        receipt: Receipt<proto::JoinProject>,
463    ) -> Result<()> {
464        let connection = self
465            .connections
466            .get_mut(&receipt.sender_id)
467            .ok_or_else(|| anyhow!("no such connection"))?;
468        let project = self
469            .projects
470            .get_mut(&project_id)
471            .ok_or_else(|| anyhow!("no such project"))?;
472        connection.requested_projects.insert(project_id);
473        project.last_activity = Some(Instant::now());
474        project
475            .join_requests
476            .entry(requester_id)
477            .or_default()
478            .push(receipt);
479        Ok(())
480    }
481
482    pub fn deny_join_project_request(
483        &mut self,
484        responder_connection_id: ConnectionId,
485        requester_id: UserId,
486        project_id: u64,
487    ) -> Option<Vec<Receipt<proto::JoinProject>>> {
488        let project = self.projects.get_mut(&project_id)?;
489        if responder_connection_id != project.host_connection_id {
490            return None;
491        }
492
493        let receipts = project.join_requests.remove(&requester_id)?;
494        for receipt in &receipts {
495            let requester_connection = self.connections.get_mut(&receipt.sender_id)?;
496            requester_connection.requested_projects.remove(&project_id);
497        }
498        project.last_activity = Some(Instant::now());
499
500        Some(receipts)
501    }
502
503    pub fn accept_join_project_request(
504        &mut self,
505        responder_connection_id: ConnectionId,
506        requester_id: UserId,
507        project_id: u64,
508    ) -> Option<(Vec<(Receipt<proto::JoinProject>, ReplicaId)>, &Project)> {
509        let project = self.projects.get_mut(&project_id)?;
510        if responder_connection_id != project.host_connection_id {
511            return None;
512        }
513
514        let receipts = project.join_requests.remove(&requester_id)?;
515        let mut receipts_with_replica_ids = Vec::new();
516        for receipt in receipts {
517            let requester_connection = self.connections.get_mut(&receipt.sender_id)?;
518            requester_connection.requested_projects.remove(&project_id);
519            requester_connection.projects.insert(project_id);
520            let mut replica_id = 1;
521            while project.active_replica_ids.contains(&replica_id) {
522                replica_id += 1;
523            }
524            project.active_replica_ids.insert(replica_id);
525            project
526                .guests
527                .insert(receipt.sender_id, (replica_id, requester_id));
528            receipts_with_replica_ids.push((receipt, replica_id));
529        }
530
531        project.last_activity = Some(Instant::now());
532        Some((receipts_with_replica_ids, project))
533    }
534
535    pub fn leave_project(
536        &mut self,
537        connection_id: ConnectionId,
538        project_id: u64,
539    ) -> Result<LeftProject> {
540        let user_id = self.user_id_for_connection(connection_id)?;
541        let project = self
542            .projects
543            .get_mut(&project_id)
544            .ok_or_else(|| anyhow!("no such project"))?;
545
546        // If the connection leaving the project is a collaborator, remove it.
547        let remove_collaborator =
548            if let Some((replica_id, _)) = project.guests.remove(&connection_id) {
549                project.active_replica_ids.remove(&replica_id);
550                true
551            } else {
552                false
553            };
554
555        // If the connection leaving the project has a pending request, remove it.
556        // If that user has no other pending requests on other connections, indicate that the request should be cancelled.
557        let mut cancel_request = None;
558        if let Entry::Occupied(mut entry) = project.join_requests.entry(user_id) {
559            entry
560                .get_mut()
561                .retain(|receipt| receipt.sender_id != connection_id);
562            if entry.get().is_empty() {
563                entry.remove();
564                cancel_request = Some(user_id);
565            }
566        }
567
568        if let Some(connection) = self.connections.get_mut(&connection_id) {
569            connection.projects.remove(&project_id);
570        }
571
572        let connection_ids = project.connection_ids();
573        let unshare = connection_ids.len() <= 1 && project.join_requests.is_empty();
574        if unshare {
575            project.language_servers.clear();
576            for worktree in project.worktrees.values_mut() {
577                worktree.diagnostic_summaries.clear();
578                worktree.entries.clear();
579            }
580        }
581
582        project.last_activity = Some(Instant::now());
583
584        Ok(LeftProject {
585            host_connection_id: project.host_connection_id,
586            host_user_id: project.host_user_id,
587            connection_ids,
588            cancel_request,
589            unshare,
590            remove_collaborator,
591        })
592    }
593
594    pub fn update_worktree(
595        &mut self,
596        connection_id: ConnectionId,
597        project_id: u64,
598        worktree_id: u64,
599        worktree_root_name: &str,
600        removed_entries: &[u64],
601        updated_entries: &[proto::Entry],
602        scan_id: u64,
603    ) -> Result<(Vec<ConnectionId>, bool, &HashMap<OsString, usize>)> {
604        let project = self.write_project(project_id, connection_id)?;
605        let connection_ids = project.connection_ids();
606        let mut worktree = project.worktrees.entry(worktree_id).or_default();
607        let metadata_changed = worktree_root_name != worktree.root_name;
608        worktree.root_name = worktree_root_name.to_string();
609
610        for entry_id in removed_entries {
611            if let Some(entry) = worktree.entries.remove(&entry_id) {
612                if !entry.is_ignored {
613                    if let Some(extension) = extension_for_entry(&entry) {
614                        if let Some(count) = worktree.extension_counts.get_mut(extension) {
615                            *count = count.saturating_sub(1);
616                        }
617                    }
618                }
619            }
620        }
621
622        for entry in updated_entries {
623            if let Some(old_entry) = worktree.entries.insert(entry.id, entry.clone()) {
624                if !old_entry.is_ignored {
625                    if let Some(extension) = extension_for_entry(&old_entry) {
626                        if let Some(count) = worktree.extension_counts.get_mut(extension) {
627                            *count = count.saturating_sub(1);
628                        }
629                    }
630                }
631            }
632
633            if !entry.is_ignored {
634                if let Some(extension) = extension_for_entry(&entry) {
635                    if let Some(count) = worktree.extension_counts.get_mut(extension) {
636                        *count += 1;
637                    } else {
638                        worktree.extension_counts.insert(extension.into(), 1);
639                    }
640                }
641            }
642        }
643
644        worktree.scan_id = scan_id;
645        Ok((connection_ids, metadata_changed, &worktree.extension_counts))
646    }
647
648    pub fn project_connection_ids(
649        &self,
650        project_id: u64,
651        acting_connection_id: ConnectionId,
652    ) -> Result<Vec<ConnectionId>> {
653        Ok(self
654            .read_project(project_id, acting_connection_id)?
655            .connection_ids())
656    }
657
658    pub fn channel_connection_ids(&self, channel_id: ChannelId) -> Result<Vec<ConnectionId>> {
659        Ok(self
660            .channels
661            .get(&channel_id)
662            .ok_or_else(|| anyhow!("no such channel"))?
663            .connection_ids())
664    }
665
666    pub fn project(&self, project_id: u64) -> Result<&Project> {
667        self.projects
668            .get(&project_id)
669            .ok_or_else(|| anyhow!("no such project"))
670    }
671
672    pub fn register_project_activity(
673        &mut self,
674        project_id: u64,
675        connection_id: ConnectionId,
676    ) -> Result<()> {
677        self.write_project(project_id, connection_id)?.last_activity = Some(Instant::now());
678        Ok(())
679    }
680
681    pub fn read_project(&self, project_id: u64, connection_id: ConnectionId) -> Result<&Project> {
682        let project = self
683            .projects
684            .get(&project_id)
685            .ok_or_else(|| anyhow!("no such project"))?;
686        if project.host_connection_id == connection_id
687            || project.guests.contains_key(&connection_id)
688        {
689            Ok(project)
690        } else {
691            Err(anyhow!("no such project"))?
692        }
693    }
694
695    fn write_project(
696        &mut self,
697        project_id: u64,
698        connection_id: ConnectionId,
699    ) -> Result<&mut Project> {
700        let project = self
701            .projects
702            .get_mut(&project_id)
703            .ok_or_else(|| anyhow!("no such project"))?;
704        if project.host_connection_id == connection_id
705            || project.guests.contains_key(&connection_id)
706        {
707            Ok(project)
708        } else {
709            Err(anyhow!("no such project"))?
710        }
711    }
712
713    #[cfg(test)]
714    pub fn check_invariants(&self) {
715        for (connection_id, connection) in &self.connections {
716            for project_id in &connection.projects {
717                let project = &self.projects.get(&project_id).unwrap();
718                if project.host_connection_id != *connection_id {
719                    assert!(project.guests.contains_key(connection_id));
720                }
721
722                for (worktree_id, worktree) in project.worktrees.iter() {
723                    let mut paths = HashMap::default();
724                    for entry in worktree.entries.values() {
725                        let prev_entry = paths.insert(&entry.path, entry);
726                        assert_eq!(
727                            prev_entry,
728                            None,
729                            "worktree {:?}, duplicate path for entries {:?} and {:?}",
730                            worktree_id,
731                            prev_entry.unwrap(),
732                            entry
733                        );
734                    }
735                }
736            }
737            for channel_id in &connection.channels {
738                let channel = self.channels.get(channel_id).unwrap();
739                assert!(channel.connection_ids.contains(connection_id));
740            }
741            assert!(self
742                .connections_by_user_id
743                .get(&connection.user_id)
744                .unwrap()
745                .contains(connection_id));
746        }
747
748        for (user_id, connection_ids) in &self.connections_by_user_id {
749            for connection_id in connection_ids {
750                assert_eq!(
751                    self.connections.get(connection_id).unwrap().user_id,
752                    *user_id
753                );
754            }
755        }
756
757        for (project_id, project) in &self.projects {
758            let host_connection = self.connections.get(&project.host_connection_id).unwrap();
759            assert!(host_connection.projects.contains(project_id));
760
761            for guest_connection_id in project.guests.keys() {
762                let guest_connection = self.connections.get(guest_connection_id).unwrap();
763                assert!(guest_connection.projects.contains(project_id));
764            }
765            assert_eq!(project.active_replica_ids.len(), project.guests.len(),);
766            assert_eq!(
767                project.active_replica_ids,
768                project
769                    .guests
770                    .values()
771                    .map(|(replica_id, _)| *replica_id)
772                    .collect::<HashSet<_>>(),
773            );
774        }
775
776        for (channel_id, channel) in &self.channels {
777            for connection_id in &channel.connection_ids {
778                let connection = self.connections.get(connection_id).unwrap();
779                assert!(connection.channels.contains(channel_id));
780            }
781        }
782    }
783}
784
785impl Project {
786    fn is_active(&self) -> bool {
787        const ACTIVE_PROJECT_TIMEOUT: Duration = Duration::from_secs(60);
788        self.last_activity.map_or(false, |last_activity| {
789            last_activity.elapsed() < ACTIVE_PROJECT_TIMEOUT
790        })
791    }
792
793    pub fn guest_connection_ids(&self) -> Vec<ConnectionId> {
794        self.guests.keys().copied().collect()
795    }
796
797    pub fn connection_ids(&self) -> Vec<ConnectionId> {
798        self.guests
799            .keys()
800            .copied()
801            .chain(Some(self.host_connection_id))
802            .collect()
803    }
804}
805
806impl Channel {
807    fn connection_ids(&self) -> Vec<ConnectionId> {
808        self.connection_ids.iter().copied().collect()
809    }
810}
811
812fn extension_for_entry(entry: &proto::Entry) -> Option<&OsStr> {
813    str::from_utf8(&entry.path)
814        .ok()
815        .map(Path::new)
816        .and_then(|p| p.extension())
817}