myko/core/item/traits.rs
1//! Item trait definitions and registration.
2
3use std::{
4 any::Any,
5 fmt::Debug,
6 sync::{Arc, OnceLock},
7};
8
9use serde::{Serialize, de::DeserializeOwned};
10use serde_json::{Value, value::RawValue};
11
12// AHash on the lookup index — every wire-emit JSON serialize hits this.
13type AMap<K, V> = std::collections::HashMap<K, V, ahash::RandomState>;
14
15use crate::common::with_id::WithId;
16
17// ─────────────────────────────────────────────────────────────────────────────
18// AnyItem - Type-erased item trait
19// ─────────────────────────────────────────────────────────────────────────────
20
21pub trait AnyItem: WithId + erased_serde::Serialize + Any + Debug + Send + Sync + 'static {
22 /// Returns self as &dyn Any for downcasting.
23 fn as_any(&self) -> &dyn Any;
24
25 /// Returns the entity type name (e.g., "Target", "Scene").
26 fn entity_type(&self) -> &'static str;
27
28 /// Typed equality across erased items.
29 fn equals(&self, other: &dyn AnyItem) -> bool;
30
31 /// Returns the current `#[server_owned]` field value, if this item has one.
32 fn server_owner(&self) -> Option<&str> {
33 None
34 }
35
36 /// Returns a clone of this item with the `#[server_owned]` field set to `server_id`.
37 /// Returns `None` if this item has no `#[server_owned]` field.
38 fn bake_server_owner(&self, server_id: &str) -> Option<Arc<dyn AnyItem>> {
39 let _ = server_id;
40 None
41 }
42}
43
44// Generate `impl serde::Serialize for dyn AnyItem` via erased_serde.
45// This enables direct serialization of type-erased items without going
46// through serde_json::Value first.
47erased_serde::serialize_trait_object!(AnyItem);
48
49impl PartialEq for dyn AnyItem {
50 fn eq(&self, other: &Self) -> bool {
51 self.equals(other)
52 }
53}
54
55impl Eq for dyn AnyItem {}
56
57// ─────────────────────────────────────────────────────────────────────────────
58// Item Registration - inventory-based registration
59// ─────────────────────────────────────────────────────────────────────────────
60
61inventory::collect!(ItemRegistration);
62
63/// Type alias for item parse function.
64pub type ItemParseFn = fn(Value) -> Result<Arc<dyn AnyItem>, anyhow::Error>;
65
66/// Type alias for direct-from-bytes parse. Skips the `serde_json::Value`
67/// round-trip when raw bytes are available — bench: ~2.27× faster than
68/// the bytes → Value → from_value path.
69pub type ItemParseBytesFn = fn(&[u8]) -> Result<Arc<dyn AnyItem>, anyhow::Error>;
70
71/// Type alias for typed JSON serialize function. Each registered entity emits
72/// a monomorphized shim that downcasts to the concrete type and calls the
73/// typed `serde_json` serializer, producing a `RawValue` that the outer
74/// serializer embeds without going through `erased_serde`.
75pub type ItemSerializeJsonFn = fn(&dyn AnyItem) -> Result<Box<RawValue>, serde_json::Error>;
76
77/// Registration entry for an item type.
78/// Collected via inventory for automatic discovery.
79pub struct ItemRegistration {
80 pub entity_type: &'static str,
81 /// Crate where this entity is defined (for type_gen filtering)
82 pub crate_name: &'static str,
83 /// Parse function that deserializes JSON into the typed item
84 pub parse: ItemParseFn,
85 /// Direct-from-bytes parser. Available when the wire layer can hand off
86 /// raw bytes; skips the `Value` allocation. Currently registered but only
87 /// used opportunistically — the existing `parse(Value)` path stays as the
88 /// universal entry point until callers can plumb bytes end-to-end.
89 pub parse_bytes: ItemParseBytesFn,
90 /// Typed JSON serialize shim. The macro emits `|any| {
91 /// let typed = any.as_any().downcast_ref::<T>()?; serde_json::value::to_raw_value(typed) }`.
92 /// Wire emit (`ErasedWrappedItem::serialize`) dispatches through this for
93 /// human-readable serializers, sidestepping erased_serde's vtable overhead.
94 pub serialize_json: ItemSerializeJsonFn,
95}
96
97/// O(1) entity_type → registration index, lazily built from the inventory.
98fn item_registry_index() -> &'static AMap<&'static str, &'static ItemRegistration> {
99 static INDEX: OnceLock<AMap<&'static str, &'static ItemRegistration>> = OnceLock::new();
100 INDEX.get_or_init(|| {
101 inventory::iter::<ItemRegistration>
102 .into_iter()
103 .map(|reg| (reg.entity_type, reg))
104 .collect()
105 })
106}
107
108/// Look up the typed JSON serialize shim for an entity_type.
109/// Returns `None` for unregistered types (e.g. the client-side `ValueItem`
110/// wrapper), in which case callers should fall back to `erased_serde`.
111pub fn lookup_item_registration(entity_type: &str) -> Option<&'static ItemRegistration> {
112 item_registry_index().get(entity_type).copied()
113}
114
115/// Opt-in ingest buffering policy for high-volume entity streams.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
117pub enum IngestBufferPolicy {
118 #[default]
119 None,
120 TimeWindow {
121 window_ms: u64,
122 },
123}
124
125/// Registration entry for an entity type's ingest buffering policy.
126pub struct IngestBufferRegistration {
127 pub entity_type: &'static str,
128 pub policy: IngestBufferPolicy,
129}
130
131inventory::collect!(IngestBufferRegistration);
132
133// ─────────────────────────────────────────────────────────────────────────────
134// Eventable - Trait for items that can be sent as events
135// ─────────────────────────────────────────────────────────────────────────────
136
137pub trait Eventable:
138 AnyItem + Serialize + DeserializeOwned + Clone + PartialEq + Sized + Any
139{
140 /// Static entity type name (use `entity_type()` from `AnyItem` for instance access).
141 const ENTITY_NAME_STATIC: &'static str;
142
143 /// Back-compat helper for generic call sites that still expect a function.
144 fn entity_name_static() -> &'static str {
145 Self::ENTITY_NAME_STATIC
146 }
147
148 /// Opt-in ingest buffering policy for this entity type.
149 fn ingest_buffer_policy() -> IngestBufferPolicy {
150 IngestBufferPolicy::None
151 }
152
153 /// Parse JSON into this item type.
154 fn parse(value: Value) -> Result<Arc<dyn AnyItem>, anyhow::Error> {
155 let item = serde_json::from_value::<Self>(value)?;
156 Ok(Arc::new(item))
157 }
158
159 /// Parse JSON bytes into this item type. Skips the `Value` round-trip
160 /// when callers can hand off raw bytes (~2.27× faster than the
161 /// `Value → from_value` path on a typical entity payload).
162 fn parse_bytes(bytes: &[u8]) -> Result<Arc<dyn AnyItem>, anyhow::Error> {
163 let item = serde_json::from_slice::<Self>(bytes)?;
164 Ok(Arc::new(item))
165 }
166}