using Invercargill; using Invercargill.DataStructures; using Inversion; using Astralis; namespace Statum { public enum ModifierOutcome { UPDATED, UNCHANGED, CLEARED, } // ------------------------------------------------------------------ // HeldStates // ------------------------------------------------------------------ public class HeldStates : Object { private Dictionary slot_keys; private StateService state_service; internal HeldStates(Dictionary slot_keys, StateService state_service) { this.slot_keys = slot_keys; this.state_service = state_service; } public T? get(string type_name) { string slot_key; if (!slot_keys.try_get(type_name, out slot_key)) { return null; } var slot = state_service.get_slot(slot_key); if (slot == null || ((!)slot).current_state == null) { return null; } try { return GObjectMapping.from_properties_typed(typeof(T), ((!)slot).current_state.public_data); } catch (GLib.Error e) { return null; } } public Properties? get_properties(string type_name) { string slot_key; if (!slot_keys.try_get(type_name, out slot_key)) { return null; } var slot = state_service.get_slot(slot_key); if (slot == null || ((!)slot).current_state == null) { return null; } return ((!)slot).current_state.public_data; } } // ------------------------------------------------------------------ // TopicLoader (internal non-generic bridge) // ------------------------------------------------------------------ public interface TopicLoader : Object { public abstract async GLib.Object? load_payload(string topic_key) throws GLib.Error; } // ------------------------------------------------------------------ // Topic // ------------------------------------------------------------------ public abstract class Topic : Object, TopicLoader { public abstract async TPayload load(string topic_key) throws GLib.Error; public async GLib.Object? load_payload(string topic_key) throws GLib.Error { return (GLib.Object) yield load(topic_key); } } // ------------------------------------------------------------------ // TopicStateModifier (internal non-generic bridge) // ------------------------------------------------------------------ public interface TopicStateModifier : Object { public abstract Type state_type { get; } public abstract async ModifierOutcome apply(GLib.Object payload, string slot_key, HeldStates held) throws GLib.Error; } // ------------------------------------------------------------------ // StateModifier // ------------------------------------------------------------------ public abstract class StateModifier : Object, TopicStateModifier { protected StateService state_service = inject(); public abstract Type state_type { get; } public string state_type_name { get { return state_type.name(); } } /** * Derive new state from the payload + context. * * @param payload The trigger payload (cast to your expected type). * @param current The current state of the slot, or null if expired. * @param held Live lookup of the client's held states. * @return New Object → push; same ref as {@code current} → skip; null → clear. */ public abstract GLib.Object? derive(GLib.Object payload, GLib.Object? current, HeldStates held); public async ModifierOutcome apply(GLib.Object payload, string slot_key, HeldStates held) throws GLib.Error { var slot = state_service.get_slot(slot_key); GLib.Object? current = null; if (slot != null && ((!)slot).current_state != null) { try { current = GObjectMapping.from_properties(state_type, ((!)slot).current_state.public_data); } catch (GLib.Error e) { current = null; } } var result = derive(payload, current, held); if (result == null) { yield state_service.clear_slot(slot_key); return ModifierOutcome.CLEARED; } if ((void*) result == (void*) current) { return ModifierOutcome.UNCHANGED; } var new_state = new State() { type_name = (slot != null && ((!)slot).current_state != null) ? ((!)slot).current_state.type_name : state_type_name, public_data = GObjectMapping.to_properties((!)result), private_data = (slot != null && ((!)slot).current_state != null) ? ((!)slot).current_state.private_data ?? new PropertyDictionary() : new PropertyDictionary() }; yield state_service.update(slot_key, new_state); return ModifierOutcome.UPDATED; } } // ------------------------------------------------------------------ // TopicRegistry // ------------------------------------------------------------------ public class TopicRegistry : Object { private StateService state_service = inject(); private Container container = inject(); private Dictionary> topic_watchers = new Dictionary>(); private Dictionary topic_types = new Dictionary(); private Dictionary> prefix_modifier_types = new Dictionary>(); public void register_topic(string prefix) { container.register_transient(); topic_types[prefix] = typeof(TTopic); } public void register_modifier(string prefix) { container.register_transient(); var stn = typeof(TState).name(); Dictionary modifiers; if (!prefix_modifier_types.try_get(prefix, out modifiers)) { modifiers = new Dictionary(); prefix_modifier_types[prefix] = modifiers; } modifiers[stn] = typeof(TModifier); } public void watch(string topic_key, string slot_key, string slot_type, ReadOnlyAssociative request_held) { var held_keys = new Dictionary(); foreach (var kvp in request_held) { held_keys[kvp.key] = kvp.value.key; } var watcher = new Watcher() { slot_key = slot_key, slot_type = slot_type, held_keys = held_keys }; Series watchers; if (!topic_watchers.try_get(topic_key, out watchers)) { watchers = new Series(); topic_watchers[topic_key] = watchers; } watchers.add(watcher); } public async void trigger(string topic_key) throws GLib.Error { // Early exit: no watchers → skip the load entirely. Series watchers; if (!topic_watchers.try_get(topic_key, out watchers) || watchers == null) { return; } if (((!)watchers).to_array().length == 0) { return; } var prefix = extract_prefix(topic_key); Type topic_type; if (!topic_types.try_get(prefix, out topic_type)) { return; } var scope = container.create_transient_scope(); var topic_obj = scope.resolve_type(topic_type); if (!(topic_obj is TopicLoader)) { return; } var payload = yield ((TopicLoader)(!)topic_obj).load_payload(topic_key); Dictionary modifier_types; if (!prefix_modifier_types.try_get(prefix, out modifier_types) || modifier_types == null) { return; } var to_remove = new Series(); foreach (var watcher in (!)watchers) { Type modifier_type; if (!((!)modifier_types).try_get(watcher.slot_type, out modifier_type)) { continue; } var held = new HeldStates(watcher.held_keys, state_service); try { var modifier = (TopicStateModifier) scope.resolve_type(modifier_type); var outcome = yield modifier.apply(payload, watcher.slot_key, held); if (outcome == ModifierOutcome.CLEARED) { to_remove.add(watcher); } } catch (GLib.Error e) { try { yield state_service.clear_slot(watcher.slot_key); } catch {} to_remove.add(watcher); } } foreach (var w in to_remove) { ((!)watchers).remove(w); } } public void sweep() { foreach (var entry in topic_watchers) { var watchers = entry.value; var to_remove = new Series(); foreach (var watcher in watchers) { if (state_service.get_slot(watcher.slot_key) == null) { to_remove.add(watcher); } } foreach (var w in to_remove) { watchers.remove(w); } } } private static string extract_prefix(string topic_key) { var colon = topic_key.index_of(":"); return colon >= 0 ? topic_key.substring(0, colon) : topic_key; } private class Watcher : Object { public string slot_key; public string slot_type; public Dictionary held_keys; } } }