Skip to main content

webfont_generator/memory/
updates.rs

1use std::collections::{HashMap, HashSet};
2#[cfg(feature = "async")]
3use std::io::Error;
4use std::io::Result;
5use std::sync::OnceLock;
6
7use super::options::{invalid, validate_name};
8use super::{GenerateWebfontsFromSourcesResult, SvgSource, build};
9use crate::incremental::sources::{
10    MembershipChange, SourceUpdate, change_membership, ordered_sources, same_sources,
11    updated_sources,
12};
13use crate::input::{LoadedSvgSource, validate_glyph_names};
14use crate::rendering::css::{TemplateDependencies, template_dependencies};
15
16/// A changed source only; unchanged SVG text stays retained by the result.
17/// Deserializes objects tagged with `type`: `added`, `changed`, or `removed`.
18#[derive(serde::Deserialize)]
19#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
20pub enum SourceChange {
21    /// Introduce a previously unknown ID with its name and SVG text.
22    Added(SvgSource),
23    /// Replace an existing source; an absent name preserves its old name.
24    Changed {
25        /// Existing opaque ID.
26        id: String,
27        /// Replacement SVG text.
28        svg: String,
29        /// Optional replacement glyph name.
30        name: Option<String>,
31    },
32    /// Delete an existing ID from all designs.
33    Removed {
34        /// Existing opaque ID.
35        id: String,
36    },
37}
38
39/// Complete ordered ID membership for a named variant.
40#[derive(serde::Deserialize)]
41#[serde(rename_all = "camelCase", deny_unknown_fields)]
42pub struct VariantSourceSet {
43    /// Existing variant name; variant configuration cannot change during updates.
44    pub name: String,
45    /// Complete ordered IDs in this design.
46    pub ids: Vec<String>,
47}
48
49/// Complete membership after applying a source change batch.
50/// Deserializes either `{ "sources": [...] }` or `{ "variants": [...] }`.
51#[derive(serde::Deserialize)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub enum SourceMembership {
54    /// Ordinary generation: ordered source IDs.
55    Sources(Vec<String>),
56    /// Variant generation: all designs, in original order.
57    Variants(Vec<VariantSourceSet>),
58}
59
60/// An async update failure with the previous usable result, except on worker panic/cancellation.
61#[cfg(feature = "async")]
62pub struct SourceRegenerateError {
63    error: Error,
64    result: Option<Box<GenerateWebfontsFromSourcesResult>>,
65}
66
67#[cfg(feature = "async")]
68impl SourceRegenerateError {
69    pub(super) fn worker(error: tokio::task::JoinError) -> Self {
70        Self {
71            error: Error::other(format!("Source update task failed: {error}")),
72            result: None,
73        }
74    }
75    /// Inspect the underlying validation, generation, or worker error.
76    pub fn error(&self) -> &Error {
77        &self.error
78    }
79    /// Recover the previous result. Worker join failures cannot recover moved state.
80    pub fn into_result(self) -> Option<GenerateWebfontsFromSourcesResult> {
81        self.result.map(|result| *result)
82    }
83    /// Recover both the error and the previous result.
84    pub fn into_parts(self) -> (Error, Option<GenerateWebfontsFromSourcesResult>) {
85        (self.error, self.result.map(|result| *result))
86    }
87}
88
89#[cfg(feature = "async")]
90impl std::fmt::Debug for SourceRegenerateError {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("SourceRegenerateError")
93            .field("error", &self.error)
94            .field("recoverable", &self.result.is_some())
95            .finish()
96    }
97}
98#[cfg(feature = "async")]
99impl std::fmt::Display for SourceRegenerateError {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        self.error.fmt(f)
102    }
103}
104#[cfg(feature = "async")]
105impl std::error::Error for SourceRegenerateError {
106    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
107        Some(&self.error)
108    }
109}
110
111impl GenerateWebfontsFromSourcesResult {
112    fn membership_for_change(
113        &self,
114        id: &str,
115        change: MembershipChange<'_>,
116    ) -> Result<SourceMembership> {
117        let names = self.options.variants.as_ref().map(|family| {
118            family
119                .variants
120                .iter()
121                .map(|variant| variant.name.clone())
122                .collect::<Vec<_>>()
123        });
124        let mut sets = self
125            .options
126            .sources
127            .iter()
128            .map(|sources| sources.iter().map(|source| source.id.clone()).collect())
129            .collect::<Vec<_>>();
130        change_membership(&mut sets, names.as_deref(), id, change)?;
131        Ok(match names {
132            Some(names) => SourceMembership::Variants(
133                names
134                    .into_iter()
135                    .zip(sets)
136                    .map(|(name, ids)| VariantSourceSet { name, ids })
137                    .collect(),
138            ),
139            None => SourceMembership::Sources(sets.remove(0)),
140        })
141    }
142
143    /// Append a new source and regenerate. Families require an existing target variant.
144    /// Requires incremental generation. Inherits [`Self::regenerate_sync`]'s rollback semantics.
145    pub fn add_sync(&mut self, source: SvgSource, variant: Option<&str>) -> Result<()> {
146        let membership = self.membership_for_change(&source.id, MembershipChange::Add(variant))?;
147        self.regenerate_sync(membership, vec![SourceChange::Added(source)])
148    }
149
150    /// Replace an existing source in every consuming design, preserving its position.
151    /// An absent name preserves the old name. Inherits [`Self::regenerate_sync`]'s rollback semantics.
152    pub fn update_sync(&mut self, id: String, svg: String, name: Option<String>) -> Result<()> {
153        let membership = self.membership_for_change(&id, MembershipChange::Update)?;
154        self.regenerate_sync(membership, vec![SourceChange::Changed { id, svg, name }])
155    }
156
157    /// Remove an existing ID from every design. Rejects empty designs.
158    /// Inherits [`Self::regenerate_sync`]'s rollback semantics.
159    pub fn remove_sync(&mut self, id: String) -> Result<()> {
160        let membership = self.membership_for_change(&id, MembershipChange::Remove)?;
161        self.regenerate_sync(membership, vec![SourceChange::Removed { id }])
162    }
163
164    /// Consuming async equivalent of [`Self::add_sync`], with [`Self::regenerate`]'s recovery semantics.
165    #[cfg(feature = "async")]
166    pub async fn add(
167        self,
168        source: SvgSource,
169        variant: Option<String>,
170    ) -> std::result::Result<Self, SourceRegenerateError> {
171        let membership = match self
172            .membership_for_change(&source.id, MembershipChange::Add(variant.as_deref()))
173        {
174            Ok(membership) => membership,
175            Err(error) => {
176                return Err(SourceRegenerateError {
177                    error,
178                    result: Some(Box::new(self)),
179                });
180            }
181        };
182        self.regenerate(membership, vec![SourceChange::Added(source)])
183            .await
184    }
185
186    /// Consuming async equivalent of [`Self::update_sync`], with [`Self::regenerate`]'s recovery semantics.
187    #[cfg(feature = "async")]
188    pub async fn update(
189        self,
190        id: String,
191        svg: String,
192        name: Option<String>,
193    ) -> std::result::Result<Self, SourceRegenerateError> {
194        let membership = match self.membership_for_change(&id, MembershipChange::Update) {
195            Ok(membership) => membership,
196            Err(error) => {
197                return Err(SourceRegenerateError {
198                    error,
199                    result: Some(Box::new(self)),
200                });
201            }
202        };
203        self.regenerate(membership, vec![SourceChange::Changed { id, svg, name }])
204            .await
205    }
206
207    /// Consuming async equivalent of [`Self::remove_sync`], with [`Self::regenerate`]'s recovery semantics.
208    #[cfg(feature = "async")]
209    pub async fn remove(self, id: String) -> std::result::Result<Self, SourceRegenerateError> {
210        let membership = match self.membership_for_change(&id, MembershipChange::Remove) {
211            Ok(membership) => membership,
212            Err(error) => {
213                return Err(SourceRegenerateError {
214                    error,
215                    result: Some(Box::new(self)),
216                });
217            }
218        };
219        self.regenerate(membership, vec![SourceChange::Removed { id }])
220            .await
221    }
222
223    /// Apply a changed-source batch and complete ordered membership synchronously.
224    /// Requires `incremental: Some(true)`. On error, buffers, sources and renders remain usable;
225    /// a failed build discards partially mutated geometry caches so a retry starts safely.
226    pub fn regenerate_sync(
227        &mut self,
228        membership: SourceMembership,
229        changes: Vec<SourceChange>,
230    ) -> Result<()> {
231        let threads = self.options.common.threads;
232        crate::threads::install(threads, || self.regenerate_in_pool(membership, changes))
233    }
234
235    fn regenerate_in_pool(
236        &mut self,
237        membership: SourceMembership,
238        changes: Vec<SourceChange>,
239    ) -> Result<()> {
240        if self.caches.is_none() {
241            return Err(invalid("Source updates require incremental generation"));
242        }
243        let memberships = match (&self.options.variants, membership) {
244            (None, SourceMembership::Sources(ids)) => vec![ids],
245            (Some(variants), SourceMembership::Variants(sets)) => {
246                if sets.len() != variants.variants.len()
247                    || sets
248                        .iter()
249                        .zip(&variants.variants)
250                        .any(|(set, variant)| set.name != variant.name)
251                {
252                    return Err(invalid("Supply every variant in its original order"));
253                }
254                sets.into_iter().map(|set| set.ids).collect()
255            }
256            _ => {
257                return Err(invalid(
258                    "Source membership does not match the generation mode",
259                ));
260            }
261        };
262        if changes.is_empty()
263            && memberships
264                .iter()
265                .zip(&self.options.sources)
266                .all(|(ids, sources)| {
267                    ids.iter()
268                        .map(String::as_str)
269                        .eq(sources.iter().map(|source| source.id.as_str()))
270                })
271        {
272            return Ok(());
273        }
274        let mut staged = self.options.clone();
275        if let Some(variants) = &mut staged.variants {
276            for (variant, ids) in variants.variants.iter_mut().zip(&memberships) {
277                variant.source_ids = ids.clone();
278            }
279        }
280        let previous = staged
281            .sources
282            .iter()
283            .flatten()
284            .map(|source| (source.id.clone(), source.clone()))
285            .collect::<HashMap<_, _>>();
286        let mut changed_ids = HashSet::new();
287        let mut supplied_ids = HashSet::new();
288        let mut updates = Vec::with_capacity(changes.len());
289        for change in changes {
290            let id = match &change {
291                SourceChange::Added(source) => &source.id,
292                SourceChange::Changed { id, .. } | SourceChange::Removed { id } => id,
293            };
294            if id.is_empty() || !changed_ids.insert(id.clone()) {
295                return Err(invalid("Change IDs must be nonempty and unique"));
296            }
297            match change {
298                SourceChange::Added(source) => {
299                    if previous.contains_key(&source.id) {
300                        return Err(invalid(format!("Source {:?} already exists", source.id)));
301                    }
302                    supplied_ids.insert(source.id.clone());
303                    updates.push(SourceUpdate::Set(LoadedSvgSource {
304                        id: source.id,
305                        glyph_name: source.name,
306                        contents: source.svg.into(),
307                    }));
308                }
309                SourceChange::Changed { id, svg, name } => {
310                    let source = previous
311                        .get(&id)
312                        .ok_or_else(|| invalid(format!("Unknown source {id:?}")))?;
313                    supplied_ids.insert(id.clone());
314                    updates.push(SourceUpdate::Set(LoadedSvgSource {
315                        id,
316                        contents: svg.into(),
317                        glyph_name: name.unwrap_or_else(|| source.glyph_name.clone()),
318                    }));
319                }
320                SourceChange::Removed { id } => {
321                    if !previous.contains_key(&id) {
322                        return Err(invalid(format!("Unknown source {id:?}")));
323                    }
324                    updates.push(SourceUpdate::Remove(id));
325                }
326            }
327        }
328        let previous = previous.into_values().collect::<Vec<_>>();
329        let definitions = updated_sources(&previous, &updates);
330        let mut used = HashSet::new();
331        staged.sources = memberships
332            .into_iter()
333            .map(|ids| {
334                if ids.is_empty() {
335                    return Err(invalid("Each design must contain at least one source"));
336                }
337                let mut seen = HashSet::new();
338                for id in &ids {
339                    if !seen.insert(id.clone()) {
340                        return Err(invalid(format!("Duplicate source ID {id:?}")));
341                    }
342                    used.insert(id.clone());
343                }
344                let sources = ordered_sources(&definitions, &ids)
345                    .map_err(|id| invalid(format!("Unknown membership ID {id:?}")))?;
346                for source in &sources {
347                    validate_name(&source.glyph_name)?;
348                }
349                validate_glyph_names(&sources)?;
350                Ok(sources)
351            })
352            .collect::<Result<Vec<_>>>()?;
353        if supplied_ids.iter().any(|id| !used.contains(id)) {
354            return Err(invalid(
355                "Added or changed sources must appear in final membership",
356            ));
357        }
358        let same = staged
359            .sources
360            .iter()
361            .zip(&self.options.sources)
362            .all(|(new, old)| same_sources(old, new));
363        if same {
364            return Ok(());
365        }
366
367        let built = build(&mut staged, self.caches.as_mut(), &updates);
368        if let Some(persistent) = self.persistent.as_mut() {
369            persistent.caches_reset = built.is_err();
370        }
371        let fonts = built?;
372        let names_unchanged = self
373            .options
374            .sources
375            .iter()
376            .flatten()
377            .map(|source| &source.glyph_name)
378            .eq(staged
379                .sources
380                .iter()
381                .flatten()
382                .map(|source| &source.glyph_name));
383        let codepoints_unchanged = self.options.common.codepoints == staged.common.codepoints;
384        let keep_css = matches!(self.css.get(), Some(Ok(_)))
385            && self
386                .options
387                .css_template
388                .as_deref()
389                .map(template_dependencies)
390                .unwrap_or_else(TemplateDependencies::css_default)
391                .can_reuse_css_no_urls(names_unchanged, codepoints_unchanged);
392        let html_dependencies = self
393            .options
394            .html_template
395            .as_deref()
396            .map(template_dependencies)
397            .unwrap_or_else(TemplateDependencies::html_default);
398        let keep_html =
399            html_dependencies.can_reuse_html(names_unchanged, codepoints_unchanged, keep_css)
400                && matches!(self.html.get(), Some(Ok(_)));
401        self.options = staged;
402        if !names_unchanged {
403            self.ts = OnceLock::new();
404        }
405        self.fonts = fonts;
406        self.shared = OnceLock::new();
407        if !keep_css {
408            self.css = OnceLock::new();
409        }
410        if !keep_html {
411            self.html = OnceLock::new();
412        }
413        Ok(())
414    }
415
416    /// Apply [`Self::regenerate_sync`] on Tokio's blocking pool, returning the updated result.
417    /// Ordinary failures retain the original usable result in [`SourceRegenerateError`].
418    #[cfg(feature = "async")]
419    pub async fn regenerate(
420        mut self,
421        membership: SourceMembership,
422        changes: Vec<SourceChange>,
423    ) -> std::result::Result<Self, SourceRegenerateError> {
424        let runtime = match tokio::runtime::Handle::try_current() {
425            Ok(runtime) => runtime,
426            Err(error) => {
427                return Err(SourceRegenerateError {
428                    error: Error::other(format!("Source update requires a Tokio runtime: {error}")),
429                    result: Some(Box::new(self)),
430                });
431            }
432        };
433        runtime
434            .spawn_blocking(move || match self.regenerate_sync(membership, changes) {
435                Ok(()) => Ok(self),
436                Err(error) => Err(SourceRegenerateError {
437                    error,
438                    result: Some(Box::new(self)),
439                }),
440            })
441            .await
442            .map_err(SourceRegenerateError::worker)?
443    }
444}