@@ -23,6 +23,7 @@ use std::collections::HashMap;
2323
2424use serde:: { Deserialize , Serialize } ;
2525use serde_json:: Value ;
26+ use uuid:: Uuid ;
2627
2728// ---------------------------------------------------------------------------
2829// Plugin Context
@@ -107,13 +108,84 @@ impl Default for PluginContext {
107108// Plugin Context Table
108109// ---------------------------------------------------------------------------
109110
110- /// Lookup table of `PluginContext` instances indexed by plugin ID.
111+ /// Threaded execution state carried from one hook invocation to the next
112+ /// within a single request lifecycle (e.g., `pre_invoke` → `post_invoke`).
111113///
112- /// Threaded across hook invocations so that a plugin's `local_state`
113- /// persists from one hook to the next within the same request lifecycle
114- /// (e.g., `pre_invoke` → `post_invoke`).
114+ /// The table holds the canonical pipeline state in two parts:
115115///
116- /// The caller receives the table back in `PipelineResult` and passes
117- /// it into the next hook invocation. On the first hook call, pass
118- /// `None` — the executor creates fresh contexts for each plugin.
119- pub type PluginContextTable = HashMap < String , PluginContext > ;
116+ /// - `global_state` — a single shared map across all plugins. The executor
117+ /// clones this into each plugin's `PluginContext.global_state` at the
118+ /// start of a run, then commits the plugin's possibly-modified copy back
119+ /// when the run completes (last-writer-wins for serial phases).
120+ /// - `local_states` — per-plugin private state, indexed by plugin ID.
121+ /// Persists across hook invocations so a plugin's `pre_invoke` can stash
122+ /// data its `post_invoke` will read.
123+ ///
124+ /// Storing `global_state` once (rather than copying it inside every per-plugin
125+ /// `PluginContext`) makes the canonical state explicit and removes the
126+ /// non-deterministic "pick an arbitrary plugin's snapshot" pattern that was
127+ /// previously needed to recover it.
128+ ///
129+ /// Returned by the executor in `PipelineResult` and passed back into the
130+ /// next hook call. On the first hook call pass `None` — the executor
131+ /// creates a fresh table.
132+ #[ derive( Debug , Default , Clone , Serialize , Deserialize ) ]
133+ pub struct PluginContextTable {
134+ /// Authoritative shared state across all plugins in the pipeline.
135+ #[ serde( default ) ]
136+ pub global_state : HashMap < String , Value > ,
137+
138+ /// Per-plugin local state, indexed by plugin ID (`Uuid`).
139+ #[ serde( default ) ]
140+ pub local_states : HashMap < Uuid , HashMap < String , Value > > ,
141+ }
142+
143+ impl PluginContextTable {
144+ /// Create an empty context table.
145+ pub fn new ( ) -> Self {
146+ Self :: default ( )
147+ }
148+
149+ /// Build a `PluginContext` for the given plugin, *removing* its stored
150+ /// local_state from the table and seeding it with a fresh clone of the
151+ /// canonical global_state. Use in serial phases where the plugin will
152+ /// commit its local_state changes back via [`store_context`].
153+ ///
154+ /// If the plugin has no stored local_state yet, its context starts
155+ /// empty (first invocation in the request lifecycle).
156+ pub fn take_context ( & mut self , plugin_id : Uuid ) -> PluginContext {
157+ PluginContext {
158+ local_state : self . local_states . remove ( & plugin_id) . unwrap_or_default ( ) ,
159+ global_state : self . global_state . clone ( ) ,
160+ }
161+ }
162+
163+ /// Build a `PluginContext` for the given plugin without mutating the
164+ /// table — the local_state is *cloned* and the global_state is cloned.
165+ /// Use in read-only phases (audit, concurrent, fire-and-forget) where
166+ /// per-plugin mutations should not influence subsequent plugins.
167+ pub fn snapshot_context ( & self , plugin_id : Uuid ) -> PluginContext {
168+ PluginContext {
169+ local_state : self . local_states . get ( & plugin_id) . cloned ( ) . unwrap_or_default ( ) ,
170+ global_state : self . global_state . clone ( ) ,
171+ }
172+ }
173+
174+ /// Commit a plugin's context back into the table after it ran. Replaces
175+ /// the canonical global_state with the plugin's possibly-modified copy
176+ /// (move, no clone) and stores the plugin's local_state for next time.
177+ pub fn store_context ( & mut self , plugin_id : Uuid , ctx : PluginContext ) {
178+ self . global_state = ctx. global_state ;
179+ self . local_states . insert ( plugin_id, ctx. local_state ) ;
180+ }
181+
182+ /// Number of plugins with stored local_state in the table.
183+ pub fn len ( & self ) -> usize {
184+ self . local_states . len ( )
185+ }
186+
187+ /// Whether the table holds no per-plugin local_state.
188+ pub fn is_empty ( & self ) -> bool {
189+ self . local_states . is_empty ( )
190+ }
191+ }
0 commit comments