TopicSystem.vala 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using Inversion;
  4. using Astralis;
  5. namespace Statum {
  6. public enum ModifierOutcome {
  7. UPDATED,
  8. UNCHANGED,
  9. CLEARED,
  10. }
  11. // ------------------------------------------------------------------
  12. // HeldStates
  13. // ------------------------------------------------------------------
  14. public class HeldStates : Object {
  15. private Dictionary<string, string> slot_keys;
  16. private StateService state_service;
  17. internal HeldStates(Dictionary<string, string> slot_keys, StateService state_service) {
  18. this.slot_keys = slot_keys;
  19. this.state_service = state_service;
  20. }
  21. public T? get<T>(string type_name) {
  22. string slot_key;
  23. if (!slot_keys.try_get(type_name, out slot_key)) {
  24. return null;
  25. }
  26. var slot = state_service.get_slot(slot_key);
  27. if (slot == null || ((!)slot).current_state == null) {
  28. return null;
  29. }
  30. try {
  31. return GObjectMapping.from_properties_typed<T>(typeof(T), ((!)slot).current_state.public_data);
  32. } catch (GLib.Error e) {
  33. return null;
  34. }
  35. }
  36. public Properties? get_properties(string type_name) {
  37. string slot_key;
  38. if (!slot_keys.try_get(type_name, out slot_key)) {
  39. return null;
  40. }
  41. var slot = state_service.get_slot(slot_key);
  42. if (slot == null || ((!)slot).current_state == null) {
  43. return null;
  44. }
  45. return ((!)slot).current_state.public_data;
  46. }
  47. }
  48. // ------------------------------------------------------------------
  49. // TopicLoader (internal non-generic bridge)
  50. // ------------------------------------------------------------------
  51. public interface TopicLoader : Object {
  52. public abstract async GLib.Object? load_payload(string topic_key) throws GLib.Error;
  53. }
  54. // ------------------------------------------------------------------
  55. // Topic<TPayload>
  56. // ------------------------------------------------------------------
  57. public abstract class Topic<TPayload> : Object, TopicLoader {
  58. public abstract async TPayload load(string topic_key) throws GLib.Error;
  59. public async GLib.Object? load_payload(string topic_key) throws GLib.Error {
  60. return (GLib.Object) yield load(topic_key);
  61. }
  62. }
  63. // ------------------------------------------------------------------
  64. // TopicStateModifier (internal non-generic bridge)
  65. // ------------------------------------------------------------------
  66. public interface TopicStateModifier : Object {
  67. public abstract Type state_type { get; }
  68. public abstract async ModifierOutcome apply(GLib.Object payload, string slot_key, HeldStates held) throws GLib.Error;
  69. }
  70. // ------------------------------------------------------------------
  71. // StateModifier<TPayload, TState>
  72. // ------------------------------------------------------------------
  73. public abstract class StateModifier : Object, TopicStateModifier {
  74. protected StateService state_service = inject<StateService>();
  75. public abstract Type state_type { get; }
  76. public string state_type_name {
  77. get { return state_type.name(); }
  78. }
  79. /**
  80. * Derive new state from the payload + context.
  81. *
  82. * @param payload The trigger payload (cast to your expected type).
  83. * @param current The current state of the slot, or null if expired.
  84. * @param held Live lookup of the client's held states.
  85. * @return New Object → push; same ref as {@code current} → skip; null → clear.
  86. */
  87. public abstract GLib.Object? derive(GLib.Object payload, GLib.Object? current, HeldStates held);
  88. public async ModifierOutcome apply(GLib.Object payload, string slot_key, HeldStates held) throws GLib.Error {
  89. var slot = state_service.get_slot(slot_key);
  90. GLib.Object? current = null;
  91. if (slot != null && ((!)slot).current_state != null) {
  92. try {
  93. current = GObjectMapping.from_properties(state_type, ((!)slot).current_state.public_data);
  94. } catch (GLib.Error e) {
  95. current = null;
  96. }
  97. }
  98. var result = derive(payload, current, held);
  99. if (result == null) {
  100. yield state_service.clear_slot(slot_key);
  101. return ModifierOutcome.CLEARED;
  102. }
  103. if ((void*) result == (void*) current) {
  104. return ModifierOutcome.UNCHANGED;
  105. }
  106. var new_state = new State() {
  107. type_name = (slot != null && ((!)slot).current_state != null)
  108. ? ((!)slot).current_state.type_name
  109. : state_type_name,
  110. public_data = GObjectMapping.to_properties((!)result),
  111. private_data = (slot != null && ((!)slot).current_state != null)
  112. ? ((!)slot).current_state.private_data ?? new PropertyDictionary()
  113. : new PropertyDictionary()
  114. };
  115. yield state_service.update(slot_key, new_state);
  116. return ModifierOutcome.UPDATED;
  117. }
  118. }
  119. // ------------------------------------------------------------------
  120. // TopicRegistry
  121. // ------------------------------------------------------------------
  122. public class TopicRegistry : Object {
  123. private StateService state_service = inject<StateService>();
  124. private Container container = inject<Container>();
  125. private Dictionary<string, Series<Watcher>> topic_watchers = new Dictionary<string, Series<Watcher>>();
  126. private Dictionary<string, Type> topic_types = new Dictionary<string, Type>();
  127. private Dictionary<string, Dictionary<string, Type>> prefix_modifier_types = new Dictionary<string, Dictionary<string, Type>>();
  128. public void register_topic<TTopic>(string prefix) {
  129. container.register_transient<TTopic>();
  130. topic_types[prefix] = typeof(TTopic);
  131. }
  132. public void register_modifier<TModifier, TState>(string prefix) {
  133. container.register_transient<TModifier>();
  134. var stn = typeof(TState).name();
  135. Dictionary<string, Type> modifiers;
  136. if (!prefix_modifier_types.try_get(prefix, out modifiers)) {
  137. modifiers = new Dictionary<string, Type>();
  138. prefix_modifier_types[prefix] = modifiers;
  139. }
  140. modifiers[stn] = typeof(TModifier);
  141. }
  142. public void watch(string topic_key, string slot_key, string slot_type,
  143. ReadOnlyAssociative<string, HeldSlot> request_held) {
  144. var held_keys = new Dictionary<string, string>();
  145. foreach (var kvp in request_held) {
  146. held_keys[kvp.key] = kvp.value.key;
  147. }
  148. var watcher = new Watcher() {
  149. slot_key = slot_key,
  150. slot_type = slot_type,
  151. held_keys = held_keys
  152. };
  153. Series<Watcher> watchers;
  154. if (!topic_watchers.try_get(topic_key, out watchers)) {
  155. watchers = new Series<Watcher>();
  156. topic_watchers[topic_key] = watchers;
  157. }
  158. watchers.add(watcher);
  159. }
  160. public async void trigger(string topic_key) throws GLib.Error {
  161. // Early exit: no watchers → skip the load entirely.
  162. Series<Watcher> watchers;
  163. if (!topic_watchers.try_get(topic_key, out watchers) || watchers == null) {
  164. return;
  165. }
  166. if (((!)watchers).to_array().length == 0) {
  167. return;
  168. }
  169. var prefix = extract_prefix(topic_key);
  170. Type topic_type;
  171. if (!topic_types.try_get(prefix, out topic_type)) {
  172. return;
  173. }
  174. var scope = container.create_transient_scope();
  175. var topic_obj = scope.resolve_type(topic_type);
  176. if (!(topic_obj is TopicLoader)) {
  177. return;
  178. }
  179. var payload = yield ((TopicLoader)(!)topic_obj).load_payload(topic_key);
  180. Dictionary<string, Type> modifier_types;
  181. if (!prefix_modifier_types.try_get(prefix, out modifier_types) || modifier_types == null) {
  182. return;
  183. }
  184. var to_remove = new Series<Watcher>();
  185. foreach (var watcher in (!)watchers) {
  186. Type modifier_type;
  187. if (!((!)modifier_types).try_get(watcher.slot_type, out modifier_type)) {
  188. continue;
  189. }
  190. var held = new HeldStates(watcher.held_keys, state_service);
  191. try {
  192. var modifier = (TopicStateModifier) scope.resolve_type(modifier_type);
  193. var outcome = yield modifier.apply(payload, watcher.slot_key, held);
  194. if (outcome == ModifierOutcome.CLEARED) {
  195. to_remove.add(watcher);
  196. }
  197. } catch (GLib.Error e) {
  198. try {
  199. yield state_service.clear_slot(watcher.slot_key);
  200. } catch {}
  201. to_remove.add(watcher);
  202. }
  203. }
  204. foreach (var w in to_remove) {
  205. ((!)watchers).remove(w);
  206. }
  207. }
  208. public void sweep() {
  209. foreach (var entry in topic_watchers) {
  210. var watchers = entry.value;
  211. var to_remove = new Series<Watcher>();
  212. foreach (var watcher in watchers) {
  213. if (state_service.get_slot(watcher.slot_key) == null) {
  214. to_remove.add(watcher);
  215. }
  216. }
  217. foreach (var w in to_remove) {
  218. watchers.remove(w);
  219. }
  220. }
  221. }
  222. private static string extract_prefix(string topic_key) {
  223. var colon = topic_key.index_of(":");
  224. return colon >= 0 ? topic_key.substring(0, colon) : topic_key;
  225. }
  226. private class Watcher : Object {
  227. public string slot_key;
  228. public string slot_type;
  229. public Dictionary<string, string> held_keys;
  230. }
  231. }
  232. }