⚡️ (core): Registro de acciones inmutable

This commit is contained in:
Manuel Cillero 2026-09-26 00:56:30 +02:00
parent 5b2ddeb6cc
commit 58d1fef4f1
15 changed files with 624 additions and 244 deletions

View file

@ -5,14 +5,14 @@
//! aplicación.
mod definition;
pub use definition::{ActionBox, ActionDispatcher, ActionKey};
pub use definition::{ActionBox, ActionDispatcher, ActionReferer};
mod list;
use list::ActionsList;
mod all;
pub(crate) use all::add_action;
pub use all::{dispatch_actions, try_dispatch_actions};
pub(crate) use all::publish_actions;
pub use all::{dispatch_actions, dispatch_referer, try_dispatch_actions};
// **< actions! >***********************************************************************************

View file

@ -1,46 +1,160 @@
use crate::core::action::{ActionBox, ActionDispatcher, ActionKey, ActionsList};
use parking_lot::RwLock;
use crate::UniqueId;
use crate::core::action::{ActionBox, ActionDispatcher, ActionReferer, ActionsList};
use std::collections::HashMap;
use std::sync::LazyLock;
use std::hash::{BuildHasherDefault, Hasher};
use std::sync::OnceLock;
// **< ACCIONES >***********************************************************************************
static ACTIONS: LazyLock<RwLock<HashMap<ActionKey, ActionsList>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
// Registro de acciones. Se construye una sola vez durante el arranque, al registrar las extensiones
// y antes de inicializarlas (`publish_actions()`). A partir de ahí es inmutable, por lo que
// despachar una acción no toma ningún bloqueo ni escribe en memoria compartida, evitando cualquier
// contención por más hilos que atiendan peticiones.
static ACTIONS: OnceLock<Registry> = OnceLock::new();
// **< AÑADIR ACCIONES >****************************************************************************
// Una clave del registro usa `TypeId`, que ya es un *hash* bien distribuido. No requiere SipHash.
type FastMap<K, V> = HashMap<K, V, BuildHasherDefault<FastHasher>>;
/// Registra una nueva acción en el sistema.
///
/// Si ya existen acciones con la misma `ActionKey`, la acción se añade a la misma lista. Si no, se
/// crea una nueva lista.
///
/// Las extensiones llamarán a esta función durante su inicialización para instalar acciones
/// personalizadas que modifiquen el comportamiento del *core* o de otros componentes.
pub(crate) fn add_action(action: ActionBox) {
let key = ActionKey::new(
action.type_id(),
action.referer_type_id(),
action.referer_id(),
);
let mut actions = ACTIONS.write();
if let Some(list) = actions.get_mut(&key) {
list.add(action);
} else {
let mut list = ActionsList::new();
list.add(action);
actions.insert(key, list);
struct Registry(FastMap<Slot, ActionEntry>);
// Tipo de acción y, si lo hay, tipo del referente (p. ej. el componente). Es `Copy`, así que
// buscarlo no reserva memoria; el identificador del referente se busca aparte, en `ActionEntry`.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
struct Slot {
action_type_id: UniqueId,
referer_type_id: Option<UniqueId>,
}
// Acciones de un tipo para un tipo de referente: las generales y, aparte, las filtradas por
// identificador. Es de sólo lectura y se obtiene ya construida del registro, sin bloqueos.
#[derive(Default)]
pub(crate) struct ActionEntry {
general: ActionsList,
by_id: HashMap<String, ActionsList>,
}
impl ActionEntry {
// Acciones que se aplican a cualquier referente del tipo.
pub(crate) fn general(&self) -> &ActionsList {
&self.general
}
// Indica si hay alguna acción filtrada por identificador. Permite no calcular el identificador
// del referente (que puede reservar memoria) cuando no hay ninguna.
pub(crate) fn has_ids(&self) -> bool {
!self.by_id.is_empty()
}
// Acciones que sólo se aplican al referente con ese identificador.
pub(crate) fn with_id(&self, id: &str) -> Option<&ActionsList> {
self.by_id.get(id)
}
}
// **< DESPLEGAR ACCIONES >*************************************************************************
// Busca la entrada de un tipo de acción y de referente.
fn action_entry(
action_type_id: UniqueId,
referer_type_id: Option<UniqueId>,
) -> Option<&'static ActionEntry> {
ACTIONS.get()?.0.get(&Slot {
action_type_id,
referer_type_id,
})
}
/// Despacha y ejecuta las funciones asociadas a una [`ActionKey`].
// **< REGISTRAR ACCIONES >*************************************************************************
// Registra todas las acciones de las extensiones y congela el registro.
//
// Las extensiones instalan sus acciones durante el arranque, antes de atender ninguna petición, y
// sólo aquí. `collect` se ejecuta una única vez: si el registro ya está construido (p. ej. por otra
// llamada a `Application::prepare()` en el mismo proceso, como hacen las pruebas) no se vuelve a
// construir ni se duplican las acciones.
pub(crate) fn publish_actions(collect: impl FnOnce() -> Vec<ActionBox>) {
ACTIONS.get_or_init(|| {
let mut slots: FastMap<Slot, ActionEntry> = FastMap::default();
for action in collect() {
let referer = action.referer();
let slot = Slot {
action_type_id: action.type_id(),
referer_type_id: referer.map(ActionReferer::referer_type_id),
};
let entry = slots.entry(slot).or_default();
match referer.and_then(|r| r.id().map(str::to_owned)) {
None => entry.general.add(action),
Some(id) => entry.by_id.entry(id).or_default().add(action),
}
}
Registry(slots)
});
}
// Función de *hash* rápida para claves formadas por `TypeId` (multiplicación y rotación, como
// FxHash). No es apta para claves controladas por un atacante; aquí las claves son tipos.
#[derive(Default)]
struct FastHasher(u64);
impl FastHasher {
const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
#[inline]
fn add(&mut self, word: u64) {
self.0 = (self.0.rotate_left(5) ^ word).wrapping_mul(Self::SEED);
}
}
impl Hasher for FastHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
let mut chunks = bytes.chunks_exact(8);
for chunk in &mut chunks {
self.add(u64::from_le_bytes(chunk.try_into().unwrap()));
}
let rest = chunks.remainder();
if !rest.is_empty() {
let mut last = [0u8; 8];
last[..rest.len()].copy_from_slice(rest);
self.add(u64::from_le_bytes(last));
}
}
#[inline]
fn write_u8(&mut self, n: u8) {
self.add(n as u64);
}
#[inline]
fn write_u64(&mut self, n: u64) {
self.add(n);
}
#[inline]
fn write_u128(&mut self, n: u128) {
self.add(n as u64);
self.add((n >> 64) as u64);
}
#[inline]
fn write_isize(&mut self, n: isize) {
self.add(n as u64);
}
#[inline]
fn finish(&self) -> u64 {
self.0.rotate_left(26)
}
}
// **< DESPACHAR ACCIONES >*************************************************************************
/// Despacha y ejecuta las funciones de las acciones de tipo `A` que no tienen referente.
///
/// Permite recorrer de forma segura y ordenada (por peso) la lista de funciones asociadas a una
/// acción específica.
/// Recorre de forma segura y ordenada (por peso) la lista de funciones registradas para esa acción.
///
/// Sólo alcanza las acciones que no declaran un [`ActionReferer`](super::ActionReferer) en
/// [`ActionDispatcher::referer()`]. Las que actúan sobre un tipo de objeto (y, opcionalmente, una
/// instancia concreta) se despachan con [`dispatch_referer()`].
///
/// # Parámetros genéricos
///
@ -50,28 +164,22 @@ pub(crate) fn add_action(action: ActionBox) {
/// # Ejemplo
///
/// ```rust,ignore
/// pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
/// dispatch_actions(
/// &ActionKey::new(
/// UniqueId::of::<Self>(),
/// Some(UniqueId::of::<C>()),
/// None,
/// ),
/// |action: &Self| (action.f)(component, cx),
/// );
/// pub(crate) fn dispatch(page: &mut Page) {
/// dispatch_actions(|action: &Self| (action.f)(page));
/// }
/// ```
pub fn dispatch_actions<A, F>(key: &ActionKey, f: F)
pub fn dispatch_actions<A, F>(f: F)
where
A: ActionDispatcher,
F: FnMut(&A),
{
if let Some(list) = ACTIONS.read().get(key) {
list.for_each(f);
if let Some(entry) = action_entry(UniqueId::of::<A>(), None) {
entry.general().for_each(f);
}
}
/// Despacha las funciones asociadas a una [`ActionKey`] con posible salida anticipada.
/// Despacha las funciones de las acciones de tipo `A` que no tienen referente, con posible salida
/// anticipada.
///
/// Funciona igual que [`dispatch_actions`], pero el closure puede devolver
/// [`std::ops::ControlFlow::Continue`] para continuar ejecutando la siguiente acción; o
@ -82,26 +190,70 @@ where
/// ```rust,ignore
/// pub(crate) fn check(cx: &Context, key: &str) -> bool {
/// let mut granted = false;
/// try_dispatch_actions(
/// &ActionKey::new(UniqueId::of::<Self>(), None, None),
/// |action: &Self| {
/// (action.f)(cx, key, &mut granted);
/// if granted {
/// std::ops::ControlFlow::Break(())
/// } else {
/// std::ops::ControlFlow::Continue(())
/// }
/// },
/// );
/// try_dispatch_actions(|action: &Self| {
/// (action.f)(cx, key, &mut granted);
/// if granted {
/// std::ops::ControlFlow::Break(())
/// } else {
/// std::ops::ControlFlow::Continue(())
/// }
/// });
/// granted
/// }
/// ```
pub fn try_dispatch_actions<A, F>(key: &ActionKey, f: F)
pub fn try_dispatch_actions<A, F>(f: F)
where
A: ActionDispatcher,
F: FnMut(&A) -> std::ops::ControlFlow<()>,
{
if let Some(list) = ACTIONS.read().get(key) {
list.try_for_each(f);
if let Some(entry) = action_entry(UniqueId::of::<A>(), None) {
entry.general().try_for_each(f);
}
}
/// Despacha las funciones de las acciones de tipo `A` asociadas a un referente de tipo `R` (p. ej.
/// un componente).
///
/// Se aplican primero las acciones del tipo `R` y después las que sólo afectan al referente con el
/// identificador que devuelva `id`, cada lista ordenada por peso. Sólo se llama a `id` (que suele
/// reservar memoria) si hay alguna acción filtrada por identificador para `R`.
///
/// El referente se presta a `id` y a `f` por turnos, así que `f` puede modificarlo.
///
/// # Parámetros genéricos
///
/// - `A`: Tipo de acción que esperamos procesar. Debe implementar [`ActionDispatcher`].
/// - `R`: Tipo del referente, el mismo que declara [`ActionDispatcher::referer()`] en su
/// [`ActionReferer`].
///
/// # Ejemplo
///
/// ```rust,ignore
/// pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
/// dispatch_referer(
/// component,
/// |c| c.id(),
/// |action: &Self, c| (action.f)(c, cx),
/// );
/// }
/// ```
pub fn dispatch_referer<A, R>(
referer: &mut R,
id: impl FnOnce(&R) -> Option<String>,
mut f: impl FnMut(&A, &mut R),
) where
A: ActionDispatcher,
R: 'static,
{
// Sin ninguna acción registrada para este tipo de referente no hay nada que hacer.
let Some(entry) = action_entry(UniqueId::of::<A>(), Some(UniqueId::of::<R>())) else {
return;
};
entry.general().for_each(|action: &A| f(action, referer));
if entry.has_ids()
&& let Some(id) = id(referer)
&& let Some(list) = entry.with_id(&id)
{
list.for_each(|action: &A| f(action, referer));
}
}

View file

@ -1,57 +1,68 @@
use crate::core::AnyInfo;
use crate::{UniqueId, Weight};
use crate::{Getters, UniqueId, Weight, util};
/// Tipo dinámico para encapsular cualquier acción que implementa [`ActionDispatcher`].
pub type ActionBox = Box<dyn ActionDispatcher>;
/// Clave para registrar las acciones y seleccionar las funciones asociadas.
/// Referente de una acción: el tipo de objeto sobre el que actúa (p. ej. un tipo de componente) y,
/// opcionalmente, el identificador de una instancia concreta.
///
/// Las funciones seleccionadas se van a [despachar](crate::core::action::dispatch_actions) y
/// ejecutar en un punto concreto del flujo de ejecución.
#[derive(Eq, PartialEq, Hash)]
pub struct ActionKey {
action_type_id: UniqueId,
referer_type_id: Option<UniqueId>,
referer_id: Option<String>,
/// Las acciones con referente se despachan con [`dispatch_referer()`]. Sin identificador afectan a
/// cualquier objeto del tipo; con identificador, sólo al que lo tiene.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// let any_button = ActionReferer::of::<Button>();
/// assert_eq!(any_button.id(), None);
///
/// // El identificador se normaliza; uno en blanco equivale a no tenerlo.
/// let one = ActionReferer::of::<Button>().with_id(" My Id ");
/// assert_eq!(one.id(), Some("my_id"));
/// assert_eq!(one.referer_type_id(), any_button.referer_type_id());
/// assert_eq!(one.with_id(" "), any_button);
/// ```
///
/// [`dispatch_referer()`]: crate::core::action::dispatch_referer
#[derive(Clone, Debug, Eq, Getters, PartialEq)]
pub struct ActionReferer {
#[getters(copy)]
referer_type_id: UniqueId,
#[getters(skip)]
id: Option<String>,
}
impl ActionKey {
/// Crea una nueva clave para un tipo de acción.
///
/// Se crea con los siguientes campos:
///
/// - `action_type_id`: Tipo de la acción.
/// - `referer_type_id`: Opcional, identificador de tipo ([`UniqueId`]) del componente referido.
/// - `referer_id`: Opcional, identificador de la instancia (p. ej. para asociar la acción a un
/// componente concreto).
///
/// Esta clave permitirá seleccionar las funciones a ejecutar para ese tipo de acción, con
/// filtros opcionales por componente o por una instancia concreta según su identificador.
pub fn new(
action_type_id: UniqueId,
referer_type_id: Option<UniqueId>,
referer_id: Option<String>,
) -> Self {
ActionKey {
action_type_id,
referer_type_id,
referer_id,
impl ActionReferer {
/// Referente para cualquier objeto del tipo `R`.
pub fn of<R: 'static>() -> Self {
ActionReferer {
referer_type_id: UniqueId::of::<R>(),
id: None,
}
}
}
/// Implementa el filtro predeterminado para despachar las funciones de una acción dada.
///
/// Las acciones tienen que sobrescribir los métodos para el filtro que apliquen. Por defecto
/// implementa un filtro nulo.
pub trait ActionDispatcher: AnyInfo + Send + Sync {
/// Devuelve el identificador de tipo ([`UniqueId`]) del objeto referido.
fn referer_type_id(&self) -> Option<UniqueId> {
None
/// Identificador de la instancia a la que se restringe el referente, si lo hay.
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
/// Devuelve el identificador del objeto referido.
fn referer_id(&self) -> Option<String> {
/// Restringe el referente al objeto con el identificador indicado. Si el identificador queda
/// vacío tras normalizarlo, el referente vuelve a aplicarse a cualquier objeto del tipo.
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id = util::normalize_token(id);
self
}
}
/// Define el comportamiento de una acción con su referente y su peso de ejecución.
///
/// Las acciones sobrescriben [`referer()`](Self::referer) si sólo se aplican a un tipo de objeto
/// (y, opcionalmente, a una instancia concreta). Por defecto no tienen referente.
pub trait ActionDispatcher: AnyInfo + Send + Sync {
/// Devuelve el [`ActionReferer`] de la acción, o `None` si no actúa sobre un tipo de objeto
/// concreto.
fn referer(&self) -> Option<&ActionReferer> {
None
}

View file

@ -3,20 +3,19 @@ use crate::core::AnyCast;
use crate::core::action::{ActionBox, ActionDispatcher};
use crate::trace;
use parking_lot::RwLock;
// Lista de acciones, ordenada por peso.
//
// Se construye al registrar las acciones, durante el arranque. A partir de ahí es de sólo lectura y
// recorrerla no aplica ningún bloqueo (ver `ActionEntry`).
#[derive(AutoDefault)]
pub struct ActionsList(RwLock<Vec<ActionBox>>);
pub struct ActionsList(Vec<ActionBox>);
impl ActionsList {
pub fn new() -> Self {
Self::default()
}
// Añade la acción y mantiene la lista ordenada por peso. La ordenación es estable: a igual peso
// se conserva el orden de registro.
pub fn add(&mut self, action: ActionBox) {
let mut list = self.0.write();
list.push(action);
list.sort_by_key(|a| a.weight());
self.0.push(action);
self.0.sort_by_key(|a| a.weight());
}
pub fn for_each<A, F>(&self, mut f: F)
@ -24,8 +23,7 @@ impl ActionsList {
A: ActionDispatcher,
F: FnMut(&A),
{
let list = self.0.read();
for a in list.iter() {
for a in self.0.iter() {
if let Some(action) = (**a).downcast_ref::<A>() {
f(action);
} else {
@ -39,8 +37,7 @@ impl ActionsList {
A: ActionDispatcher,
F: FnMut(&A) -> std::ops::ControlFlow<()>,
{
let list = self.0.read();
for a in list.iter() {
for a in self.0.iter() {
if let Some(action) = (**a).downcast_ref::<A>() {
if f(action).is_break() {
break;

View file

@ -1,4 +1,4 @@
use crate::core::action::add_action;
use crate::core::action::publish_actions;
use crate::core::extension::ExtensionRef;
use crate::core::theme::ThemeRef;
use crate::core::theme::all::THEMES;
@ -91,11 +91,14 @@ fn check_theme_parent_chain(theme: ThemeRef) {
// **< REGISTRO DE LAS ACCIONES >*******************************************************************
pub fn register_actions() {
for extension in EXTENSIONS.get().into_iter().flatten() {
for a in extension.actions() {
add_action(a);
}
}
publish_actions(|| {
EXTENSIONS
.get()
.into_iter()
.flatten()
.flat_map(|extension| extension.actions())
.collect()
});
}
// **< INICIALIZA LAS EXTENSIONES >*****************************************************************

View file

@ -101,9 +101,17 @@ pub trait Extension: AnyInfo + Send + Sync {
/// Devuelve la lista de acciones que la extensión registra.
///
/// Estas [acciones](crate::core::action) se despachan por orden de registro o por
/// [peso](crate::Weight) (ver [`actions!`](crate::actions)), permitiendo
/// personalizar el comportamiento de la aplicación en puntos específicos.
/// Estas [acciones] se despachan por orden de registro o por [peso] (ver [`actions!`]),
/// permitiendo personalizar el comportamiento de la aplicación en puntos específicos.
///
/// PageTop lo invoca una sola vez por proceso, al registrar las extensiones y antes de
/// [`initialize()`]. La lista queda fija a partir de ahí y no es posible añadir acciones más
/// tarde.
///
/// [acciones]: crate::core::action
/// [peso]: crate::Weight
/// [`actions!`]: crate::actions
/// [`initialize()`]: Self::initialize
fn actions(&self) -> Vec<ActionBox> {
actions![]
}