feat(shared-triggers): emit shared events from group services (M2.3)

ServiceEventEmitter gains emit_shared(cx, owning_group, event) (default no-op);
OutboxEventEmitter implements it — matches shared triggers on the owning group
and writes outbox rows stamped with the WRITER app_id (dispatch runs under the
writer). GroupKv/Docs/FilesServiceImpl gain an events field (Noop default +
with_events builder) and emit on set/create/update/delete; the host wires the
outbox emitter via with_events. Closes the "group trigger has no app to watch"
gap. Authoring + validation land in M2.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-01 20:33:09 +02:00
parent cf296cd2fd
commit 0210ace406
6 changed files with 381 additions and 28 deletions

View File

@@ -20,7 +20,7 @@
use async_trait::async_trait;
use thiserror::Error;
use crate::SdkCallCx;
use crate::{GroupId, SdkCallCx};
/// Trait every stateful service depends on to emit events. The host
/// binary constructs one instance and clones the Arc into each service.
@@ -31,6 +31,21 @@ pub trait ServiceEventEmitter: Send + Sync {
/// will return `Ok(())` once the event is durably persisted, the
/// dispatcher reads it out-of-band.
async fn emit(&self, cx: &SdkCallCx, event: ServiceEvent) -> Result<(), EmitError>;
/// §11.6: publish an event for a write to a SHARED (group-owned)
/// collection. Unlike `emit` (which fans out over the writer app's own +
/// inherited triggers), this matches `shared = true` triggers on the
/// `owning_group` and runs the handler under the writer app (`cx.app_id`).
/// Default no-op so the noop emitter + any non-outbox impls drop it.
async fn emit_shared(
&self,
cx: &SdkCallCx,
owning_group: GroupId,
event: ServiceEvent,
) -> Result<(), EmitError> {
let _ = (cx, owning_group, event);
Ok(())
}
}
/// One service event. `source` and `op` are `&'static str` because they