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:
@@ -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]:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user