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:
@@ -177,8 +177,11 @@ pub enum FilesError {
|
||||
|
||||
impl NewFile {
|
||||
/// Validate required fields + length caps at the SDK boundary.
|
||||
/// `data` must be non-empty (v1.1.5 treats an empty blob as a
|
||||
/// missing `data` field — see HANDBACK §7).
|
||||
///
|
||||
/// Empty `data` is **accepted** as a valid stored state (v1.1.6
|
||||
/// relaxed the v1.1.5 rejection — empty files are a legitimate use
|
||||
/// case: sentinels, placeholders, zero-byte uploads. See HANDBACK
|
||||
/// §7). `name` and `content_type` are still required.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -191,9 +194,6 @@ impl NewFile {
|
||||
if self.content_type.trim().is_empty() {
|
||||
return Err(FilesError::MissingField("content_type"));
|
||||
}
|
||||
if self.data.is_empty() {
|
||||
return Err(FilesError::MissingField("data"));
|
||||
}
|
||||
if self.name.len() > MAX_FILE_NAME_BYTES {
|
||||
return Err(FilesError::NameTooLong(self.name.len()));
|
||||
}
|
||||
@@ -218,9 +218,9 @@ impl FileUpdate {
|
||||
/// Returns the field-specific [`FilesError`] for the first failing
|
||||
/// check.
|
||||
pub fn validate(&self, max_size: usize) -> Result<(), FilesError> {
|
||||
if self.data.is_empty() {
|
||||
return Err(FilesError::MissingField("data"));
|
||||
}
|
||||
// Empty replacement bytes are accepted (v1.1.6 relaxation —
|
||||
// consistent with NewFile::validate; updating a file to zero
|
||||
// bytes is as legitimate as creating one).
|
||||
if let Some(name) = &self.name {
|
||||
if name.trim().is_empty() {
|
||||
return Err(FilesError::MissingField("name"));
|
||||
|
||||
Reference in New Issue
Block a user