feat(v1.1.6): realtime channels + v1.1.5 follow-ups + version bumps
Server-side realtime SSE on per-app pub/sub topics, plus the three
v1.1.5 follow-ups and the version bumps.
Realtime:
- topics registry (0021) + admin endpoints + Capability::AppTopicManage
(-> app:admin; no new scope).
- GET /realtime/topics/{topic} SSE endpoint (orchestrator-core data
plane): Host -> app, RealtimeAuthority gate (404 missing/internal,
401 bad/absent token), broadcast::Receiver stream + heartbeat.
- RealtimeBroadcaster / RealtimeEvent / RealtimeAuthority traits
(picloud-shared); InProcessBroadcaster + GC (orchestrator-core);
DB-backed RealtimeAuthorityImpl (manager-core). Publish path fans out
to in-process subscribers after the durable outbox commit (best-effort,
panic-isolated).
- HMAC subscriber tokens (subscriber_token.rs) + app_secrets table (0022)
+ pubsub::subscriber_token SDK (schema 1.6 -> 1.7). TTL clamp + env
overrides.
- Dashboard Topics tab (register/list/edit/delete, prominent external
badge, flip confirmation).
v1.1.5 follow-ups:
- Empty blobs accepted (NewFile/FileUpdate::validate) + round-trip test.
- Orphan *.tmp.* sweeper (spawn_files_orphan_sweep).
- Dispatcher e2e tests, one per trigger kind (DATABASE_URL-gated).
Versions: workspace 1.1.6, SDK 1.7, dashboard 0.12.0. Schema-snapshot
golden re-blessed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,9 +40,85 @@ pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<Sdk
|
||||
},
|
||||
);
|
||||
}
|
||||
// `pubsub::subscriber_token(topics)` — uses the configured default
|
||||
// TTL.
|
||||
{
|
||||
let svc = svc.clone();
|
||||
let cx = cx.clone();
|
||||
module.set_native_fn(
|
||||
"subscriber_token",
|
||||
move |topics: Array| -> Result<String, Box<EvalAltResult>> {
|
||||
mint_token(&svc, &cx, topics, None)
|
||||
},
|
||||
);
|
||||
}
|
||||
// `pubsub::subscriber_token(topics, ttl)` — `ttl` is an integer
|
||||
// (seconds) or `()` for the default.
|
||||
{
|
||||
let svc = svc.clone();
|
||||
let cx = cx.clone();
|
||||
module.set_native_fn(
|
||||
"subscriber_token",
|
||||
move |topics: Array, ttl: Dynamic| -> Result<String, Box<EvalAltResult>> {
|
||||
let ttl = ttl_from_dynamic(&ttl)?;
|
||||
mint_token(&svc, &cx, topics, ttl)
|
||||
},
|
||||
);
|
||||
}
|
||||
engine.register_static_module("pubsub", module.into());
|
||||
}
|
||||
|
||||
/// Interpret the optional `ttl` argument: `()` → use the default,
|
||||
/// integer → that many seconds, anything else → throw.
|
||||
fn ttl_from_dynamic(ttl: &Dynamic) -> Result<Option<i64>, Box<EvalAltResult>> {
|
||||
if ttl.is_unit() {
|
||||
return Ok(None);
|
||||
}
|
||||
ttl.as_int().map(Some).map_err(|_| -> Box<EvalAltResult> {
|
||||
EvalAltResult::ErrorRuntime(
|
||||
"pubsub::subscriber_token: ttl must be an integer (seconds) or ()".into(),
|
||||
rhai::Position::NONE,
|
||||
)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn mint_token(
|
||||
svc: &Arc<dyn picloud_shared::PubsubService>,
|
||||
cx: &Arc<SdkCallCx>,
|
||||
topics: Array,
|
||||
ttl: Option<i64>,
|
||||
) -> Result<String, Box<EvalAltResult>> {
|
||||
// Every element must be a string; surface a clear error otherwise.
|
||||
let mut names = Vec::with_capacity(topics.len());
|
||||
for t in topics {
|
||||
if !t.is_string() {
|
||||
return Err(EvalAltResult::ErrorRuntime(
|
||||
"pubsub::subscriber_token: topics must be an array of strings".into(),
|
||||
rhai::Position::NONE,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
names.push(t.into_string().unwrap_or_default());
|
||||
}
|
||||
let svc = svc.clone();
|
||||
let cx = cx.clone();
|
||||
let handle = TokioHandle::try_current().map_err(|e| -> Box<EvalAltResult> {
|
||||
EvalAltResult::ErrorRuntime(
|
||||
format!("pubsub: no tokio runtime available: {e}").into(),
|
||||
rhai::Position::NONE,
|
||||
)
|
||||
.into()
|
||||
})?;
|
||||
// SubscriberToken errors already carry the full
|
||||
// "pubsub::subscriber_token: …" wording, so surface them verbatim.
|
||||
handle
|
||||
.block_on(async move { svc.mint_subscriber_token(&cx, names, ttl).await })
|
||||
.map_err(|err| -> Box<EvalAltResult> {
|
||||
EvalAltResult::ErrorRuntime(format!("{err}").into(), rhai::Position::NONE).into()
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert a Rhai `Dynamic` message into JSON, base64-encoding any
|
||||
/// `Blob` (at any nesting depth). Mirrors `bridge::dynamic_to_json` but
|
||||
/// adds the blob arm the pub/sub wire contract requires.
|
||||
|
||||
Reference in New Issue
Block a user