Rerun the script in response to updated widget state. This uses DAG-based dependency tracking (from AST instrumentation in Phase 3) to determine which atoms are affected and should be recomputed. Fallback to full rerun is triggered if affected atoms cannot be deter
(self, new_widget_states: dict[str, Any] | None = None)
| 114 | raise |
| 115 | |
| 116 | async def rerun(self, new_widget_states: dict[str, Any] | None = None): |
| 117 | """ |
| 118 | Rerun the script in response to updated widget state. |
| 119 | |
| 120 | This uses DAG-based dependency tracking (from AST instrumentation in Phase 3) |
| 121 | to determine which atoms are affected and should be recomputed. |
| 122 | |
| 123 | Fallback to full rerun is triggered if affected atoms cannot be determined. |
| 124 | |
| 125 | Args: |
| 126 | new_widget_states (dict[str, Any] | None): Updated component states (by ID). |
| 127 | """ |
| 128 | |
| 129 | # Basic debouncing - skip if last run was too recent |
| 130 | current_time = time.time() |
| 131 | if current_time - self._last_run_time < 0.1: |
| 132 | logger.info("[ScriptRunner] Skipping rerun due to debounce") |
| 133 | return |
| 134 | |
| 135 | if not new_widget_states: |
| 136 | logger.info("[ScriptRunner] No new states for rerun") |
| 137 | return |
| 138 | |
| 139 | if not self._service.is_reactivity_enabled: |
| 140 | logger.info("[ScriptRunner] Reactivity disabled — rerunning entire script with updated widget state") |
| 141 | return await self.run_script() |
| 142 | |
| 143 | try: |
| 144 | with self._lock: |
| 145 | for component_id, value in new_widget_states.items(): |
| 146 | old_value = self.widget_states.get(component_id) |
| 147 | self.widget_states[component_id] = value |
| 148 | if logger.isEnabledFor(logging.DEBUG): |
| 149 | logger.debug(f"[ScriptRunner] Updated state: {component_id=} -> {value=} (was {old_value=})") |
| 150 | self._run_count += 1 |
| 151 | self._last_run_time = current_time |
| 152 | |
| 153 | # determine affected components and force recomputation |
| 154 | changed_component_ids = set(new_widget_states.keys()) |
| 155 | workflow = self._service.get_workflow() |
| 156 | |
| 157 | changed_atoms = set() |
| 158 | for cid in changed_component_ids: |
| 159 | atom = workflow.get_component_producer(cid) |
| 160 | if atom: |
| 161 | changed_atoms.add(atom) |
| 162 | else: |
| 163 | logger.warning(f"[ScriptRunner] No producer found for component_id: {cid}") |
| 164 | |
| 165 | affected_atoms = workflow._get_affected_atoms(changed_atoms) |
| 166 | |
| 167 | # Inject updated widget states into workflow context before executing |
| 168 | for component_id, new_value in self.widget_states.items(): |
| 169 | producer_atom = workflow.get_component_producer(component_id) |
| 170 | if producer_atom: |
| 171 | workflow.context.set_variable(producer_atom, new_value) |
| 172 | |
| 173 | if not changed_atoms and not affected_atoms: |
no test coverage detected