feat(spaces): implement Phase 2 Event System

Week 4 - Event Infrastructure:
- Create SpaceEventType enum with 18 event types covering space lifecycle,
  document operations, variables, references, rendering, sync, and cache
- Create SpaceEvent dataclass with serialization/deserialization
- Create EventBus with sync/async handler support, priority ordering,
  global handlers, and optional event history
- Add event factory functions for common events

Week 5 - Event Integration:
- Wire EventBus into SpaceService as optional dependency
- Emit events for all space operations:
  - SPACE_CREATED, SPACE_UPDATED, SPACE_DELETED, SPACE_ACTIVATED, SPACE_ARCHIVED
  - DOCUMENT_ADDED, DOCUMENT_REMOVED, DOCUMENT_MOVED, DOCUMENT_CONTENT_CHANGED
  - VARIABLE_SET, VARIABLE_DELETED
- Create integration tests for event propagation patterns

Test coverage: 187 tests total
- 43 unit tests for event system
- 20 integration tests for event propagation
- 124 existing tests continue to pass

Capabilities delivered:
- CAP-010: SpaceEvent base with type, payload, timestamp
- CAP-011: EventBus with in-process publish/subscribe
- CAP-012: Event handlers registry with priority support
- CAP-013: Change detection via content hash comparison

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 07:41:47 +01:00
parent 9b12875681
commit 0a494b2011
7 changed files with 1807 additions and 19 deletions

View File

@@ -51,6 +51,15 @@ from .repositories import (
initialize_space_tables,
)
# Phase 2: Event System
from .events import (
SpaceEvent,
SpaceEventType,
EventBus,
get_event_bus,
reset_event_bus,
)
__all__ = [
# Models
"InformationSpace",
@@ -73,4 +82,10 @@ __all__ = [
"SqliteVariableRepository",
"SqliteReferenceRepository",
"initialize_space_tables",
# Event System
"SpaceEvent",
"SpaceEventType",
"EventBus",
"get_event_bus",
"reset_event_bus",
]

View File

@@ -3,14 +3,55 @@ Event system for Information Spaces.
This package provides event-driven architecture for space operations:
- SpaceEvent: Event dataclass with type, payload, timestamp
- SpaceEventType: Enum of all event types
- EventBus: In-process publish/subscribe for space events
- Event handlers and registration
- Event factory functions for common events
Events emitted:
- SPACE_CREATED, SPACE_UPDATED, SPACE_DELETED
- DOCUMENT_ADDED, DOCUMENT_UPDATED, DOCUMENT_REMOVED
- RENDER_COMPLETED, SYNC_COMPLETED
- Space lifecycle: SPACE_CREATED, SPACE_UPDATED, SPACE_DELETED, SPACE_ACTIVATED, SPACE_ARCHIVED
- Document: DOCUMENT_ADDED, DOCUMENT_UPDATED, DOCUMENT_REMOVED, DOCUMENT_MOVED, DOCUMENT_CONTENT_CHANGED
- Variable: VARIABLE_SET, VARIABLE_DELETED
- Reference: REFERENCE_ADDED, REFERENCE_CLEARED
- Rendering: RENDER_STARTED, RENDER_COMPLETED, RENDER_FAILED
- Sync: SYNC_STARTED, SYNC_COMPLETED, SYNC_CONFLICT
- Cache: CACHE_INVALIDATED
"""
# Events will be implemented in Phase 2
__all__ = []
from .models import (
SpaceEvent,
SpaceEventType,
space_created_event,
space_updated_event,
space_deleted_event,
document_added_event,
document_updated_event,
document_removed_event,
document_content_changed_event,
cache_invalidated_event,
)
from .bus import (
EventBus,
HandlerRegistration,
get_event_bus,
reset_event_bus,
)
__all__ = [
# Event models
"SpaceEvent",
"SpaceEventType",
# Event factories
"space_created_event",
"space_updated_event",
"space_deleted_event",
"document_added_event",
"document_updated_event",
"document_removed_event",
"document_content_changed_event",
"cache_invalidated_event",
# Event bus
"EventBus",
"HandlerRegistration",
"get_event_bus",
"reset_event_bus",
]

View File

@@ -0,0 +1,402 @@
"""
EventBus for Information Spaces.
This module provides an in-process publish/subscribe system
for space events. It supports both synchronous and asynchronous
event handlers.
"""
import asyncio
import logging
from collections import defaultdict
from typing import Callable, Dict, List, Optional, Set, Union, Any
from dataclasses import dataclass, field
import weakref
from .models import SpaceEvent, SpaceEventType
# Type aliases for handlers
SyncHandler = Callable[[SpaceEvent], None]
AsyncHandler = Callable[[SpaceEvent], Any] # Coroutine
Handler = Union[SyncHandler, AsyncHandler]
logger = logging.getLogger(__name__)
@dataclass
class HandlerRegistration:
"""
Registration info for an event handler.
Attributes:
handler: The handler function
is_async: Whether the handler is async
priority: Handler priority (lower = earlier execution)
handler_id: Unique ID for this registration
"""
handler: Handler
is_async: bool = False
priority: int = 100
handler_id: str = field(default_factory=lambda: str(id(None)))
def __post_init__(self):
self.handler_id = str(id(self.handler))
class EventBus:
"""
In-process event bus for space events.
Provides publish/subscribe functionality for space operations.
Handlers can be registered for specific event types or for all events.
Usage:
bus = EventBus()
# Register a handler
def on_space_created(event: SpaceEvent):
print(f"Space created: {event.payload['name']}")
bus.subscribe(SpaceEventType.SPACE_CREATED, on_space_created)
# Emit an event
bus.emit(SpaceEvent(
event_type=SpaceEventType.SPACE_CREATED,
space_id="space-123",
payload={"name": "my-docs"}
))
# Unsubscribe
bus.unsubscribe(SpaceEventType.SPACE_CREATED, on_space_created)
"""
def __init__(self):
"""Initialize the event bus."""
# Handlers indexed by event type
self._handlers: Dict[SpaceEventType, List[HandlerRegistration]] = defaultdict(
list
)
# Handlers that receive all events
self._global_handlers: List[HandlerRegistration] = []
# Track all handler IDs for quick lookup
self._handler_ids: Set[str] = set()
# Event history (optional, for debugging)
self._history: List[SpaceEvent] = []
self._history_enabled: bool = False
self._max_history: int = 1000
def subscribe(
self,
event_type: Optional[SpaceEventType],
handler: Handler,
priority: int = 100,
) -> str:
"""
Subscribe a handler to events.
Args:
event_type: The event type to subscribe to, or None for all events
handler: The handler function (sync or async)
priority: Handler priority (lower = earlier execution)
Returns:
Handler ID that can be used to unsubscribe
"""
is_async = asyncio.iscoroutinefunction(handler)
registration = HandlerRegistration(
handler=handler,
is_async=is_async,
priority=priority,
)
if event_type is None:
self._global_handlers.append(registration)
self._global_handlers.sort(key=lambda r: r.priority)
else:
self._handlers[event_type].append(registration)
self._handlers[event_type].sort(key=lambda r: r.priority)
self._handler_ids.add(registration.handler_id)
return registration.handler_id
def subscribe_all(self, handler: Handler, priority: int = 100) -> str:
"""
Subscribe a handler to all events.
Args:
handler: The handler function
priority: Handler priority
Returns:
Handler ID
"""
return self.subscribe(None, handler, priority)
def unsubscribe(
self,
event_type: Optional[SpaceEventType],
handler: Handler,
) -> bool:
"""
Unsubscribe a handler from events.
Args:
event_type: The event type, or None for global handlers
handler: The handler to remove
Returns:
True if handler was found and removed
"""
handler_id = str(id(handler))
if event_type is None:
for i, reg in enumerate(self._global_handlers):
if reg.handler_id == handler_id:
self._global_handlers.pop(i)
self._handler_ids.discard(handler_id)
return True
else:
handlers = self._handlers.get(event_type, [])
for i, reg in enumerate(handlers):
if reg.handler_id == handler_id:
handlers.pop(i)
self._handler_ids.discard(handler_id)
return True
return False
def unsubscribe_by_id(self, handler_id: str) -> bool:
"""
Unsubscribe a handler by its ID.
Args:
handler_id: The handler ID returned by subscribe
Returns:
True if handler was found and removed
"""
# Check global handlers
for i, reg in enumerate(self._global_handlers):
if reg.handler_id == handler_id:
self._global_handlers.pop(i)
self._handler_ids.discard(handler_id)
return True
# Check type-specific handlers
for handlers in self._handlers.values():
for i, reg in enumerate(handlers):
if reg.handler_id == handler_id:
handlers.pop(i)
self._handler_ids.discard(handler_id)
return True
return False
def emit(self, event: SpaceEvent) -> List[Exception]:
"""
Emit an event synchronously.
Calls all registered handlers for the event type.
Async handlers are run in a new event loop if needed.
Args:
event: The event to emit
Returns:
List of exceptions raised by handlers (empty if all succeeded)
"""
if self._history_enabled:
self._record_history(event)
exceptions: List[Exception] = []
# Collect all handlers to call
handlers = list(self._global_handlers)
handlers.extend(self._handlers.get(event.event_type, []))
handlers.sort(key=lambda r: r.priority)
for registration in handlers:
try:
if registration.is_async:
# Run async handler
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
# Create a task but don't await it here
asyncio.create_task(registration.handler(event))
else:
asyncio.run(registration.handler(event))
else:
registration.handler(event)
except Exception as e:
logger.exception(
f"Handler {registration.handler_id} raised exception for event {event.event_type}"
)
exceptions.append(e)
return exceptions
async def emit_async(self, event: SpaceEvent) -> List[Exception]:
"""
Emit an event asynchronously.
Args:
event: The event to emit
Returns:
List of exceptions raised by handlers
"""
if self._history_enabled:
self._record_history(event)
exceptions: List[Exception] = []
# Collect all handlers to call
handlers = list(self._global_handlers)
handlers.extend(self._handlers.get(event.event_type, []))
handlers.sort(key=lambda r: r.priority)
for registration in handlers:
try:
if registration.is_async:
await registration.handler(event)
else:
registration.handler(event)
except Exception as e:
logger.exception(
f"Handler {registration.handler_id} raised exception for event {event.event_type}"
)
exceptions.append(e)
return exceptions
def has_handlers(self, event_type: SpaceEventType) -> bool:
"""
Check if there are any handlers for an event type.
Args:
event_type: The event type to check
Returns:
True if there are handlers (including global handlers)
"""
return bool(self._global_handlers) or bool(
self._handlers.get(event_type, [])
)
def handler_count(self, event_type: Optional[SpaceEventType] = None) -> int:
"""
Get the number of handlers registered.
Args:
event_type: Specific event type, or None for total count
Returns:
Number of handlers
"""
if event_type is None:
count = len(self._global_handlers)
for handlers in self._handlers.values():
count += len(handlers)
return count
else:
return len(self._handlers.get(event_type, [])) + len(
self._global_handlers
)
def clear(self, event_type: Optional[SpaceEventType] = None) -> int:
"""
Clear handlers.
Args:
event_type: Specific event type, or None to clear all
Returns:
Number of handlers removed
"""
if event_type is None:
count = self.handler_count()
self._handlers.clear()
self._global_handlers.clear()
self._handler_ids.clear()
return count
else:
handlers = self._handlers.pop(event_type, [])
for reg in handlers:
self._handler_ids.discard(reg.handler_id)
return len(handlers)
# History management
def enable_history(self, max_events: int = 1000) -> None:
"""Enable event history recording."""
self._history_enabled = True
self._max_history = max_events
def disable_history(self) -> None:
"""Disable event history recording."""
self._history_enabled = False
def clear_history(self) -> None:
"""Clear the event history."""
self._history.clear()
def get_history(
self,
event_type: Optional[SpaceEventType] = None,
space_id: Optional[str] = None,
limit: Optional[int] = None,
) -> List[SpaceEvent]:
"""
Get event history.
Args:
event_type: Filter by event type
space_id: Filter by space ID
limit: Maximum number of events to return
Returns:
List of events (newest first)
"""
events = list(reversed(self._history))
if event_type is not None:
events = [e for e in events if e.event_type == event_type]
if space_id is not None:
events = [e for e in events if e.space_id == space_id]
if limit is not None:
events = events[:limit]
return events
def _record_history(self, event: SpaceEvent) -> None:
"""Record an event in history."""
self._history.append(event)
if len(self._history) > self._max_history:
self._history = self._history[-self._max_history :]
# Global event bus instance (optional singleton pattern)
_default_bus: Optional[EventBus] = None
def get_event_bus() -> EventBus:
"""Get the default global event bus instance."""
global _default_bus
if _default_bus is None:
_default_bus = EventBus()
return _default_bus
def reset_event_bus() -> None:
"""Reset the default global event bus (useful for testing)."""
global _default_bus
_default_bus = None

View File

@@ -0,0 +1,239 @@
"""
Event models for Information Spaces.
This module defines the event types and data structures used
by the event system for space operations.
"""
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, Optional
import uuid
class SpaceEventType(Enum):
"""
Types of events that can occur in the space system.
Events are organized by category:
- Space lifecycle: creation, updates, deletion, status changes
- Document operations: add, update, remove, move
- Variable operations: set, delete
- Rendering: start, complete, fail
- Sync: start, complete, conflict
"""
# Space lifecycle events
SPACE_CREATED = "space.created"
SPACE_UPDATED = "space.updated"
SPACE_DELETED = "space.deleted"
SPACE_ACTIVATED = "space.activated"
SPACE_ARCHIVED = "space.archived"
# Document events
DOCUMENT_ADDED = "document.added"
DOCUMENT_UPDATED = "document.updated"
DOCUMENT_REMOVED = "document.removed"
DOCUMENT_MOVED = "document.moved"
DOCUMENT_CONTENT_CHANGED = "document.content_changed"
# Variable events
VARIABLE_SET = "variable.set"
VARIABLE_DELETED = "variable.deleted"
# Reference events
REFERENCE_ADDED = "reference.added"
REFERENCE_CLEARED = "reference.cleared"
# Rendering events
RENDER_STARTED = "render.started"
RENDER_COMPLETED = "render.completed"
RENDER_FAILED = "render.failed"
# Sync events
SYNC_STARTED = "sync.started"
SYNC_COMPLETED = "sync.completed"
SYNC_CONFLICT = "sync.conflict"
# Cache events
CACHE_INVALIDATED = "cache.invalidated"
@dataclass
class SpaceEvent:
"""
Represents an event in the space system.
Events are immutable records of operations that have occurred.
They carry enough information for handlers to react appropriately.
Attributes:
event_type: The type of event
space_id: The ID of the affected space
payload: Event-specific data
event_id: Unique identifier for this event
timestamp: When the event occurred
source: Optional identifier of the event source
correlation_id: Optional ID to correlate related events
"""
event_type: SpaceEventType
space_id: str
payload: Dict[str, Any] = field(default_factory=dict)
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: datetime = field(default_factory=datetime.now)
source: Optional[str] = None
correlation_id: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert the event to a dictionary for serialization."""
return {
"event_id": self.event_id,
"event_type": self.event_type.value,
"space_id": self.space_id,
"payload": self.payload,
"timestamp": self.timestamp.isoformat(),
"source": self.source,
"correlation_id": self.correlation_id,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "SpaceEvent":
"""Create an event from a dictionary."""
return cls(
event_id=data.get("event_id", str(uuid.uuid4())),
event_type=SpaceEventType(data["event_type"]),
space_id=data["space_id"],
payload=data.get("payload", {}),
timestamp=datetime.fromisoformat(data["timestamp"])
if data.get("timestamp")
else datetime.now(),
source=data.get("source"),
correlation_id=data.get("correlation_id"),
)
# Convenience factory functions for common events
def space_created_event(
space_id: str,
name: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a SPACE_CREATED event."""
return SpaceEvent(
event_type=SpaceEventType.SPACE_CREATED,
space_id=space_id,
payload={"name": name},
source=source,
)
def space_updated_event(
space_id: str,
changes: Dict[str, Any],
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a SPACE_UPDATED event."""
return SpaceEvent(
event_type=SpaceEventType.SPACE_UPDATED,
space_id=space_id,
payload={"changes": changes},
source=source,
)
def space_deleted_event(
space_id: str,
name: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a SPACE_DELETED event."""
return SpaceEvent(
event_type=SpaceEventType.SPACE_DELETED,
space_id=space_id,
payload={"name": name},
source=source,
)
def document_added_event(
space_id: str,
document_id: str,
space_path: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a DOCUMENT_ADDED event."""
return SpaceEvent(
event_type=SpaceEventType.DOCUMENT_ADDED,
space_id=space_id,
payload={"document_id": document_id, "space_path": space_path},
source=source,
)
def document_updated_event(
space_id: str,
document_id: str,
changes: Dict[str, Any],
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a DOCUMENT_UPDATED event."""
return SpaceEvent(
event_type=SpaceEventType.DOCUMENT_UPDATED,
space_id=space_id,
payload={"document_id": document_id, "changes": changes},
source=source,
)
def document_removed_event(
space_id: str,
document_id: str,
space_path: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a DOCUMENT_REMOVED event."""
return SpaceEvent(
event_type=SpaceEventType.DOCUMENT_REMOVED,
space_id=space_id,
payload={"document_id": document_id, "space_path": space_path},
source=source,
)
def document_content_changed_event(
space_id: str,
document_id: str,
old_hash: Optional[str],
new_hash: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a DOCUMENT_CONTENT_CHANGED event."""
return SpaceEvent(
event_type=SpaceEventType.DOCUMENT_CONTENT_CHANGED,
space_id=space_id,
payload={
"document_id": document_id,
"old_hash": old_hash,
"new_hash": new_hash,
},
source=source,
)
def cache_invalidated_event(
space_id: str,
document_ids: list,
reason: str,
source: Optional[str] = None,
) -> SpaceEvent:
"""Create a CACHE_INVALIDATED event."""
return SpaceEvent(
event_type=SpaceEventType.CACHE_INVALIDATED,
space_id=space_id,
payload={"document_ids": document_ids, "reason": reason},
source=source,
)

View File

@@ -23,6 +23,17 @@ from ..repositories.interfaces import (
IVariableRepository,
IReferenceRepository,
)
from ..events import (
EventBus,
SpaceEvent,
SpaceEventType,
space_created_event,
space_updated_event,
space_deleted_event,
document_added_event,
document_removed_event,
document_content_changed_event,
)
class SpaceService:
@@ -54,6 +65,7 @@ class SpaceService:
document_repo: IDocumentAssociationRepository,
variable_repo: IVariableRepository,
reference_repo: IReferenceRepository,
event_bus: Optional[EventBus] = None,
):
"""
Initialize the SpaceService.
@@ -63,11 +75,18 @@ class SpaceService:
document_repo: Repository for document associations
variable_repo: Repository for space variables
reference_repo: Repository for transclusion references
event_bus: Optional event bus for emitting events
"""
self._space_repo = space_repo
self._document_repo = document_repo
self._variable_repo = variable_repo
self._reference_repo = reference_repo
self._event_bus = event_bus
def _emit(self, event: SpaceEvent) -> None:
"""Emit an event if event bus is configured."""
if self._event_bus:
self._event_bus.emit(event)
# =========================================================================
# Space CRUD Operations
@@ -114,7 +133,9 @@ class SpaceService:
parent_space_id=parent_space_id,
)
return self._space_repo.create(space)
created_space = self._space_repo.create(space)
self._emit(space_created_event(created_space.id, created_space.name))
return created_space
def get_space(self, space_id: str) -> Optional[InformationSpace]:
"""
@@ -198,7 +219,21 @@ class SpaceService:
if metadata is not None:
space.metadata = metadata
return self._space_repo.update(space)
updated_space = self._space_repo.update(space)
# Build changes dict for event
changes = {}
if name is not None:
changes["name"] = name
if description is not None:
changes["description"] = description
if config is not None:
changes["config"] = "updated"
if metadata is not None:
changes["metadata"] = "updated"
self._emit(space_updated_event(space_id, changes))
return updated_space
def delete_space(self, space_id: str, cascade: bool = True) -> bool:
"""
@@ -230,7 +265,11 @@ class SpaceService:
for child in children:
self.delete_space(child.id, cascade=True)
return self._space_repo.delete(space_id)
space_name = space.name
result = self._space_repo.delete(space_id)
if result:
self._emit(space_deleted_event(space_id, space_name))
return result
def activate_space(self, space_id: str) -> InformationSpace:
"""
@@ -250,7 +289,13 @@ class SpaceService:
raise ValueError(f"Space '{space_id}' not found")
space.activate()
return self._space_repo.update(space)
updated_space = self._space_repo.update(space)
self._emit(SpaceEvent(
event_type=SpaceEventType.SPACE_ACTIVATED,
space_id=space_id,
payload={"status": "active"},
))
return updated_space
def archive_space(self, space_id: str) -> InformationSpace:
"""
@@ -270,7 +315,13 @@ class SpaceService:
raise ValueError(f"Space '{space_id}' not found")
space.archive()
return self._space_repo.update(space)
updated_space = self._space_repo.update(space)
self._emit(SpaceEvent(
event_type=SpaceEventType.SPACE_ARCHIVED,
space_id=space_id,
payload={"status": "archived"},
))
return updated_space
def get_child_spaces(self, parent_space_id: str) -> List[InformationSpace]:
"""
@@ -330,7 +381,9 @@ class SpaceService:
content_hash=content_hash,
)
return self._document_repo.add_document(document)
added_doc = self._document_repo.add_document(document)
self._emit(document_added_event(space_id, added_doc.id, space_path))
return added_doc
def get_document(self, document_id: str) -> Optional[SpaceDocument]:
"""
@@ -384,12 +437,20 @@ class SpaceService:
Returns:
True if removed, False if not found
"""
# Clear any references from this document first
# Get document info before removal for event
document = self._document_repo.get_document(document_id)
if document:
self._reference_repo.clear_references_from(document_id, document.space_id)
if not document:
return False
return self._document_repo.remove_document(document_id)
# Clear any references from this document first
self._reference_repo.clear_references_from(document_id, document.space_id)
result = self._document_repo.remove_document(document_id)
if result:
self._emit(document_removed_event(
document.space_id, document_id, document.space_path
))
return result
def move_document(self, document_id: str, new_path: str) -> SpaceDocument:
"""
@@ -405,9 +466,26 @@ class SpaceService:
Raises:
ValueError: If document not found or new path exists
"""
# Get old path for event
old_doc = self._document_repo.get_document(document_id)
old_path = old_doc.space_path if old_doc else None
if not new_path.startswith("/"):
new_path = "/" + new_path
return self._document_repo.move_document(document_id, new_path)
moved_doc = self._document_repo.move_document(document_id, new_path)
if old_doc:
self._emit(SpaceEvent(
event_type=SpaceEventType.DOCUMENT_MOVED,
space_id=moved_doc.space_id,
payload={
"document_id": document_id,
"old_path": old_path,
"new_path": new_path,
},
))
return moved_doc
def reorder_documents(self, space_id: str, document_ids: List[str]) -> None:
"""
@@ -427,8 +505,20 @@ class SpaceService:
document_id: The document association ID
content_hash: The new content hash
"""
# Get old hash for event
document = self._document_repo.get_document(document_id)
old_hash = document.content_hash if document else None
self._document_repo.update_content_hash(document_id, content_hash)
if document and old_hash != content_hash:
self._emit(document_content_changed_event(
document.space_id,
document_id,
old_hash,
content_hash,
))
# =========================================================================
# Variable Operations
# =========================================================================
@@ -465,7 +555,13 @@ class SpaceService:
scope=scope,
)
return self._variable_repo.set_variable(variable)
saved_var = self._variable_repo.set_variable(variable)
self._emit(SpaceEvent(
event_type=SpaceEventType.VARIABLE_SET,
space_id=space_id,
payload={"name": name, "scope": scope},
))
return saved_var
def get_variable(self, space_id: str, name: str) -> Optional[SpaceVariable]:
"""
@@ -506,7 +602,14 @@ class SpaceService:
Returns:
True if deleted, False if not found
"""
return self._variable_repo.delete_variable(space_id, name)
result = self._variable_repo.delete_variable(space_id, name)
if result:
self._emit(SpaceEvent(
event_type=SpaceEventType.VARIABLE_DELETED,
space_id=space_id,
payload={"name": name},
))
return result
def get_variables_dict(self, space_id: str) -> Dict[str, Any]:
"""