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

@@ -161,21 +161,26 @@ pub async fn build_app(
));
// §11.6 shared group collections (KV): a separate store keyed by the owning
// group, resolved from cx.app_id's chain. Reuses the KV value-size cap.
let group_kv: Arc<dyn picloud_shared::GroupKvService> =
Arc::new(GroupKvServiceImpl::with_max_value_bytes(
let group_kv: Arc<dyn picloud_shared::GroupKvService> = Arc::new(
GroupKvServiceImpl::with_max_value_bytes(
Arc::new(PostgresGroupKvRepo::new(pool.clone())),
Arc::new(PostgresGroupCollectionResolver::new(pool.clone())),
authz.clone(),
picloud_manager_core::kv_service::kv_max_value_bytes_from_env(),
));
)
// §11.6 shared-collection triggers: fire `shared = true` triggers.
.with_events(events.clone()),
);
// §11.6 shared group collections (docs): group-keyed group_docs store.
let group_docs: Arc<dyn picloud_shared::GroupDocsService> =
Arc::new(GroupDocsServiceImpl::with_max_value_bytes(
let group_docs: Arc<dyn picloud_shared::GroupDocsService> = Arc::new(
GroupDocsServiceImpl::with_max_value_bytes(
Arc::new(PostgresGroupDocsRepo::new(pool.clone())),
Arc::new(PostgresGroupCollectionResolver::new(pool.clone())),
authz.clone(),
picloud_manager_core::docs_service::docs_max_value_bytes_from_env(),
));
)
.with_events(events.clone()),
);
let docs: Arc<dyn DocsService> = Arc::new(DocsServiceImpl::with_max_value_bytes(
docs_repo,
authz.clone(),
@@ -216,14 +221,17 @@ pub async fn build_app(
));
// §11.6 shared group collections (files): group-keyed blobs under
// `<root>/files/groups/<group_id>/...`, resolved from cx.app_id's chain.
// Shares the per-file size cap; no events (no app to attribute a trigger to).
let group_files: Arc<dyn picloud_shared::GroupFilesService> =
Arc::new(GroupFilesServiceImpl::new(
// Shares the per-file size cap; fires `shared = true` triggers under the
// writer app (§11.6 shared-collection triggers).
let group_files: Arc<dyn picloud_shared::GroupFilesService> = Arc::new(
GroupFilesServiceImpl::new(
Arc::new(FsGroupFilesRepo::new(pool.clone(), files_root.clone())),
Arc::new(PostgresGroupCollectionResolver::new(pool.clone())),
authz.clone(),
files_max_size,
));
)
.with_events(events.clone()),
);
// v1.1.6 realtime: the in-process broadcaster is shared between the
// publish path (PubsubServiceImpl fans out to SSE subscribers after
// the durable outbox fan-out) and the SSE endpoint (subscribe side).