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}