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}