feat(shared-topics): GroupPubsubService + shared publish fan-out + SDK handle (D2)
The runtime for shared topics:
- pubsub_repo: fan_out_publish gains `AND t.shared = FALSE` (a shared
trigger never fires on a per-app publish); new fan_out_shared_publish
matches `shared = true` pubsub triggers on the resolved owning group,
each delivery stamped with the writer app_id (M2 model).
- GroupPubsubService trait (shared) + GroupPubsubServiceImpl: resolve the
owning group (kind='topic') from cx.app_id's chain, require editor+
(GroupPubsubPublish, fails closed for anon), size-cap, fan out.
- SDK: `pubsub::shared_topic("name")` -> GroupTopicHandle with
`.publish(subtopic, msg)` / `.publish(msg)`; wired through Services +
the picloud binary (reuses the one PubsubRepo).
- authz: Capability::GroupPubsubPublish(GroupId), editor+ / script:write.
The owning-group chain walk is the isolation boundary; a sibling-subtree
app never resolves the topic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,71 @@ pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<Sdk
|
||||
},
|
||||
);
|
||||
}
|
||||
// §11.6 D2: `pubsub::shared_topic("name")` → a handle whose `.publish(...)`
|
||||
// publishes to the group's shared topic namespace (fans out to `shared`
|
||||
// group pubsub triggers on the owning group). The owning group resolves from
|
||||
// `cx.app_id` inside the service — never a script arg.
|
||||
{
|
||||
let group_svc = services.group_pubsub.clone();
|
||||
let cx = cx.clone();
|
||||
module.set_native_fn(
|
||||
"shared_topic",
|
||||
move |name: &str| -> Result<GroupTopicHandle, Box<EvalAltResult>> {
|
||||
if name.is_empty() {
|
||||
return Err("pubsub::shared_topic name must not be empty".into());
|
||||
}
|
||||
Ok(GroupTopicHandle {
|
||||
namespace: name.to_string(),
|
||||
service: group_svc.clone(),
|
||||
cx: cx.clone(),
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
engine.register_static_module("pubsub", module.into());
|
||||
engine.register_type_with_name::<GroupTopicHandle>("GroupTopicHandle");
|
||||
register_shared_publish(engine);
|
||||
}
|
||||
|
||||
/// §11.6 D2 handle returned by `pubsub::shared_topic("name")`. `.publish(sub,
|
||||
/// msg)` publishes `name.sub` to the group shared topic; `.publish(msg)`
|
||||
/// publishes to `name` directly.
|
||||
#[derive(Clone)]
|
||||
pub struct GroupTopicHandle {
|
||||
namespace: String,
|
||||
service: Arc<dyn picloud_shared::GroupPubsubService>,
|
||||
cx: Arc<SdkCallCx>,
|
||||
}
|
||||
|
||||
fn shared_publish(
|
||||
h: &mut GroupTopicHandle,
|
||||
subtopic: &str,
|
||||
message: Dynamic,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
let json = message_to_json(&message);
|
||||
let service = h.service.clone();
|
||||
let cx = h.cx.clone();
|
||||
let namespace = h.namespace.clone();
|
||||
let subtopic = subtopic.to_string();
|
||||
block_on("pubsub", async move {
|
||||
service.publish(&cx, &namespace, &subtopic, json).await
|
||||
})
|
||||
// The fan-out count isn't surfaced to scripts (fire-and-forget, like the
|
||||
// per-app publish).
|
||||
.map(|_n: u32| ())
|
||||
}
|
||||
|
||||
fn register_shared_publish(engine: &mut RhaiEngine) {
|
||||
engine.register_fn(
|
||||
"publish",
|
||||
|h: &mut GroupTopicHandle, subtopic: &str, message: Dynamic| {
|
||||
shared_publish(h, subtopic, message)
|
||||
},
|
||||
);
|
||||
// `.publish(msg)` with no subtopic → publish to the namespace directly.
|
||||
engine.register_fn("publish", |h: &mut GroupTopicHandle, message: Dynamic| {
|
||||
shared_publish(h, "", message)
|
||||
});
|
||||
}
|
||||
|
||||
/// Interpret the optional `ttl` argument: `()` → use the default,
|
||||
|
||||
Reference in New Issue
Block a user