Skip to main content

text_document/
backend.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 Cyril Jacquet
3
4//! A backend several documents can share.
5//!
6//! Every [`TextDocument`](crate::TextDocument) built by [`TextDocument::new`](crate::TextDocument::new)
7//! owns a whole application context: a store, an undo manager, an event hub, and
8//! an OS thread draining that hub. That is right for one document and wrong for
9//! a hundred. A host that opens a document per scene of a manuscript pays a
10//! hundred threads to display one book, and each thread reserves eight megabytes
11//! of address space for a loop that is idle almost all of the time.
12//!
13//! # What can be shared, and what cannot
14//!
15//! Not the store, and not the undo stack. Every repository's `snapshot` and
16//! `restore` take and put back the **whole** store (see
17//! `Transaction::snapshot_store`), so two documents sharing one would undo and
18//! roll each other back. Each document keeps its own.
19//!
20//! The event hub can be shared, and that is where the thread is. One hub means
21//! one drain, so a backend holds one [`EventHubClient`] and one thread however
22//! many documents are built in it.
23//!
24//! # Telling one document's events from another's
25//!
26//! With a shared hub, every document's long-operation subscription sees every
27//! document's long-operation events. Each document therefore records the ids of
28//! the operations it started and ignores an event carrying any other id. The
29//! filter lives in the document, inside the lock the callback already takes, so
30//! there is no second structure to keep in step and no second lock to order
31//! against the first.
32//!
33//! # Lifetime
34//!
35//! The pump stops when the backend drops, not when a document does: a document
36//! that shut the hub down on its own way out would stop delivery for every
37//! sibling still open. Hold the backend for as long as any document built in it.
38
39use std::sync::Arc;
40
41use frontend::AppContext;
42use frontend::event_hub_client::EventHubClient;
43
44/// A shared document backend: one event hub, one pump thread, one
45/// long-operation manager, for any number of documents.
46///
47/// Cheap to clone (an `Arc`), and every clone names the same backend. Build one
48/// per project, or per whatever scope wants its documents to share a thread, and
49/// create documents in it with
50/// [`TextDocument::new_in`](crate::TextDocument::new_in).
51#[derive(Clone)]
52pub struct DocumentBackend {
53    inner: Arc<BackendInner>,
54}
55
56struct BackendInner {
57    /// The context whose hub, shutdown channel and long-operation manager every
58    /// document in this backend shares. Its own store and undo stack are unused:
59    /// each document brings its own, because undo works on a whole store.
60    ctx: AppContext,
61    /// The one drain, and the one thread. Documents subscribe here.
62    client: EventHubClient,
63}
64
65impl DocumentBackend {
66    /// Build a backend, starting its single event pump.
67    pub fn new() -> Self {
68        let ctx = AppContext::new();
69        let client = EventHubClient::new(&ctx.event_hub);
70        client.start(ctx.shutdown_rx.clone());
71        Self {
72            inner: Arc::new(BackendInner { ctx, client }),
73        }
74    }
75
76    /// The context documents built here share.
77    pub(crate) fn shared_ctx(&self) -> &AppContext {
78        &self.inner.ctx
79    }
80
81    /// The client documents built here subscribe on.
82    pub(crate) fn client(&self) -> &EventHubClient {
83        &self.inner.client
84    }
85}
86
87impl Default for DocumentBackend {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl std::fmt::Debug for DocumentBackend {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("DocumentBackend")
96            .field(
97                "documents_sharing_it",
98                &(Arc::strong_count(&self.inner) - 1),
99            )
100            .finish()
101    }
102}
103
104impl Drop for BackendInner {
105    /// Stop the pump. This is the only place that may: a document doing it on
106    /// its own way out would stop delivery for every sibling still open.
107    fn drop(&mut self) {
108        self.ctx.shutdown();
109    }
110}