feat(ds-stream): v2.0-2.3 composable codec pipeline
v2.0 — Pipeline Architecture - Frame, CodecResult, Codec trait, Pipeline builder - 6 adapters: Passthrough, Dedup, Compress, Pacer, Slicer, Stats v2.1 — Multi-frame & new codecs - CodecOutput::Many fan-out, EncryptCodec, FilterCodec - Codec::reset(), encode_all/decode_all, real SlicerCodec chunking v2.2 — Observability & reassembly - PipelineResult (frames+errors+consumed), StageMetric - ReassemblyCodec, ConditionalCodec, Pipeline presets & metrics v2.3 — Integrity & rate control - ChecksumCodec (CRC32), RateLimitCodec (token bucket), TagCodec - Pipeline::chain(), Pipeline::describe() 13 codec adapters, 474 tests (all green, 0 regressions)
This commit is contained in:
parent
fbbdeb0bc4
commit
35b39a1cf1
15 changed files with 9830 additions and 47 deletions
129
CHANGELOG.md
129
CHANGELOG.md
|
|
@ -4,6 +4,135 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### 🏗️ Engine v2.3.0 — ds-stream Pipeline v4 — 2026-03-11
|
||||
|
||||
- **`ChecksumCodec`** — CRC32 integrity: append on encode, verify+strip on decode, Error on mismatch
|
||||
- **`RateLimitCodec`** — Token bucket algorithm (burst-tolerant, refills over time)
|
||||
- **`TagCodec`** — Attach channel ID for mux routing, drop wrong-channel frames on decode
|
||||
- **`Pipeline::chain()`** — Compose two pipelines into one
|
||||
- **`Pipeline::describe()`** — Human-readable dump (`dedup → compress → encrypt`)
|
||||
- **Error propagation tested** — Corrupt checksum → `PipelineResult.errors` collects the error
|
||||
- 474 total ds-stream tests (+13 new)
|
||||
|
||||
### 🏗️ Engine v2.2.0 — ds-stream Pipeline v3 — 2026-03-11
|
||||
|
||||
- **`PipelineResult`** — returns frames + collected errors + consumed count
|
||||
- **`StageMetric`** — per-stage frames_in/frames_out/consumed/errors observability
|
||||
- **`ReassemblyCodec`** — reassemble chunked frames (counterpart to SlicerCodec)
|
||||
- **`ConditionalCodec`** — wrap any codec with runtime enable/disable toggle
|
||||
- **`Pipeline::signal(key)`** — preset: dedup→compress→encrypt
|
||||
- **`Pipeline::media(mtu)`** — preset: compress→slicer
|
||||
- **`Pipeline::metrics()`** — per-stage counter snapshot
|
||||
- **Slicer↔Reassembly roundtrip** — verified end-to-end chunk→reassemble
|
||||
- 461 total ds-stream tests (+12 new)
|
||||
|
||||
### 🏗️ Engine v2.1.0 — ds-stream Pipeline v2 — 2026-03-11
|
||||
|
||||
- **`CodecOutput::Many`** — codecs can fan-out (1 frame → N frames)
|
||||
- **`EncryptCodec`** — XOR cipher encrypt/decrypt adapter
|
||||
- **`FilterCodec`** — drop frames by type (`FilterCodec::drop_control()`)
|
||||
- **`Codec::reset()`** — clear internal state on reconnect
|
||||
- **`Pipeline::encode_all/decode_all`** — batch frame processing
|
||||
- **`SlicerCodec`** rewritten — real MTU chunking via `Many` (no more truncation)
|
||||
- **Full roundtrip test** — Compress→Encrypt→encode→decode→verify original
|
||||
- 449 total ds-stream tests (+11 new)
|
||||
|
||||
### 🏗️ Engine v2.0.0 — ds-stream Pipeline Architecture — 2026-03-11
|
||||
|
||||
- **[NEW] `pipeline.rs`** — `Frame`, `CodecResult`, `Codec` trait, `Pipeline` builder
|
||||
- **6 codec adapters** — `PassthroughCodec`, `DedupCodec`, `CompressCodec`, `PacerCodec`, `SlicerCodec`, `StatsCodec`
|
||||
- **15 pipeline tests** — frame roundtrip, pipeline composition, per-codec verification (438 total ds-stream tests)
|
||||
|
||||
### 🚀 Engine v1.15.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `StreamStats1150`, `PacketCoalescer`, `ErrorBudget` (423 tests)
|
||||
- **ds-stream-wasm** — `stats_v1150`, `coal_v1150`, `ebudget_v1150` WASM bindings (281 tests)
|
||||
- **ds-physics** — `find_fastest_body_v1150`, `get_total_mass_v1150`, `apply_magnet_v1150`, `get_body_angle_v1150` (350 tests)
|
||||
|
||||
### 🚀 Engine v1.14.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `PriorityQueue1140`, `LatencyTracker1140`, `FrameWindow` (413 tests)
|
||||
- **ds-stream-wasm** — `pq_v1140`, `lat_v1140`, `fw_v1140` WASM bindings (274 tests)
|
||||
- **ds-physics** — `get_body_aabb_v1140`, `get_max_speed_v1140`, `apply_buoyancy_v1140`, `is_body_colliding_v1140` (340 tests)
|
||||
|
||||
### 🎯 Engine v1.13.0 — 2026-03-11 — **1,000 TESTS MILESTONE**
|
||||
|
||||
- **ds-stream** — `ChannelMux1130`, `FrameSlicer`, `BandwidthProbe` (403 tests)
|
||||
- **ds-stream-wasm** — `mux_v1130`, `slicer_v1130`, `probe_v1130` WASM bindings (267 tests)
|
||||
- **ds-physics** — `get_body_distance_v1130`, `get_world_centroid_v1130`, `apply_wind_v1130`, `count_bodies_in_radius_v1130` (330 tests)
|
||||
|
||||
### 🚀 Engine v1.12.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `FrameFingerprint`, `AdaptiveBitrate1120`, `JitterBuffer1120` (393 tests)
|
||||
- **ds-stream-wasm** — `fingerprint_v1120`, `abr_v1120`, `jitter_v1120` WASM bindings (260 tests)
|
||||
- **ds-physics** — `get_contact_count_v1120`, `get_world_momentum_v1120`, `apply_vortex_v1120`, `get_body_inertia_v1120` (320 tests)
|
||||
|
||||
### 🚀 Engine v1.11.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `FrameDropPolicy`, `StreamTimeline`, `PacketPacer` (383 tests)
|
||||
- **ds-stream-wasm** — `drop_policy_v1110`, `timeline_v1110`, `pacer_v1110` WASM bindings (253 tests)
|
||||
- **ds-physics** — `get_world_aabb_v1110`, `get_total_angular_ke_v1110`, `apply_drag_field_v1110`, `get_body_speed_v1110` (310 tests)
|
||||
|
||||
### 🚀 Engine v1.10.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `FrameRateLimiter`, `DeltaAccumulator`, `ConnectionGrade` (373 tests)
|
||||
- **ds-stream-wasm** — `rate_limit_v1100`, `delta_accum_v1100`, `conn_grade_v1100` WASM bindings (246 tests)
|
||||
- **ds-physics** — `get_body_rotation_v1100`, `get_energy_ratio_v1100`, `apply_explosion_v1100`, `get_nearest_body_v1100` (300 tests)
|
||||
|
||||
### 🚀 Engine v1.9.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `SequenceValidator`, `FrameTagMap`, `BurstDetector` (363 tests)
|
||||
- **ds-stream-wasm** — `seq_v190`, `burst_v190`, `tag_v190` WASM bindings (239 tests)
|
||||
- **ds-physics** — `get_gravity_v190`, `get_body_mass_v190`, `apply_central_impulse_v190`, `get_total_potential_energy_v190` (290 tests)
|
||||
|
||||
### 🚀 Engine v1.8.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `RingMetric180`, `FrameCompactor`, `RetransmitQueue` (353 tests)
|
||||
- **ds-stream-wasm** — `ring_v180`, `compactor_v180`, `retransmit_v180` WASM bindings (232 tests)
|
||||
- **ds-physics** — `set/get_angular_damping_v180`, `set_restitution_v180`, `sleep/wake_body_v180`, `count_sleeping_v180` (280 tests)
|
||||
|
||||
### 🚀 Engine v1.7.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `DelayBuffer170`, `PacketInterleaver`, `HeartbeatWatchdog` (343 tests)
|
||||
- **ds-stream-wasm** — `loss_v170`, `watchdog_v170`, `interleave_v170` WASM bindings (225 tests)
|
||||
- **ds-physics** — `set/get_linear_damping_v170`, `is_out_of_bounds_v170`, `clamp_velocities_v170`, `count_dynamic_bodies_v170` (270 tests)
|
||||
|
||||
### 🚀 Engine v1.6.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `AesFrameCipher`, `LossInjector`, `StreamCheckpoint` (333 tests)
|
||||
- **ds-stream-wasm** — `throttle_v160`, `cipher_v160`, `checkpoint_v160` WASM bindings (218 tests)
|
||||
- **ds-physics** — `get_body_type_v160`, `get_world_center_of_mass_v160`, `apply_gravity_well_v160`, `get_total_kinetic_energy_v160` (260 tests)
|
||||
|
||||
### 🚀 Engine v1.5.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `ContentDedup`, `StreamHealthTracker`, `FrameThrottler` (323 tests)
|
||||
- **ds-stream-wasm** — `bw_limiter_v150`, `dedup_v150`, `metrics_v150` WASM bindings (211 tests)
|
||||
- **ds-physics** — `apply_force_field_v150`, `freeze/unfreeze_body_v150`, `get_distance_v150`, `get_angular_momentum_v150` (250 tests)
|
||||
|
||||
### 🚀 Engine v1.4.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `SessionRecorder`, `PriorityScheduler`, `BandwidthLimiter` (313 tests)
|
||||
- **ds-stream-wasm** — `compositor_v140`, `recorder_v140`, `priority_v140` WASM bindings (204 tests)
|
||||
- **ds-physics** — `raycast_v140`, `get_collision_events_v140`, `time_scale_v140`, `get_body_momentum_v140` (240 tests)
|
||||
|
||||
### 🚀 Engine v1.3.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `QualityPolicy`, `FrameLayerCompositor`, `ReorderBuffer`, `QualityDecision`+`FrameMode` (303 tests)
|
||||
- **ds-stream-wasm** — `quality_decide_v130`, `reorder_push/drain_v130`, `channel_auth_check_v130` (197 tests)
|
||||
- **ds-physics** — `snapshot_scene_v130`, `restore_scene_v130`, `get_body_aabb_v130`, `apply_torque_v130` (230 tests)
|
||||
|
||||
### 🚀 Engine v1.2.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `HapticPayload`, `haptic_frame()`, `FrameBatch`, `StreamDigest`, `ChannelAuth` (291 tests)
|
||||
- **ds-stream-wasm** — `haptic_message_v120`, `batch_frames_v120`, `digest_v120` (190 tests)
|
||||
- **ds-physics** — `create_spring_joint_v120`, `get_joint_info_v120`, `set_body_rotation_v120`, `get_body_energy_v120` (220 tests)
|
||||
|
||||
### 🚀 Engine v1.1.0 — 2026-03-11
|
||||
|
||||
- **ds-stream** — `FrameType::Error`, `ErrorPayload`, `ProtocolInfo`, `encrypt_frame/decrypt_frame`, `FrameRouter`, `error_frame()` builder (277 tests)
|
||||
- **ds-stream-wasm** — `error_frame_v110`, `decode_error_payload`, `protocol_info_v110`, `encrypt_frame_v110/decrypt_frame_v110` (183 tests)
|
||||
- **ds-physics** — `get_body_velocity_v110`, `set_body_position_v110`, `get_contact_pairs_v110`, `engine_version_v110` (210 tests)
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- V2 phase 1 — array access, timer, string interpolation
|
||||
|
|
|
|||
|
|
@ -1,18 +1,50 @@
|
|||
# Changelog
|
||||
|
||||
## [1.6.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- **`get_body_type_v160(body)`** — returns 0=dynamic, 1=kinematic, 2=fixed, -1=invalid
|
||||
- **`get_world_center_of_mass_v160()`** — mass-weighted center of all dynamic bodies
|
||||
- **`apply_gravity_well_v160(cx, cy, radius, strength)`** — attractive radial force (pulls toward center)
|
||||
- **`get_total_kinetic_energy_v160()`** — sum of 0.5·m·v² for all dynamic bodies
|
||||
- **`engine_version_v160()`** — version string
|
||||
- 10 new tests (260 total)
|
||||
|
||||
## [1.5.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- `apply_force_field_v150` — repulsive radial force
|
||||
- `freeze/unfreeze_body_v150` — kinematic toggle
|
||||
- `get_distance_v150` — center-to-center distance
|
||||
- `get_angular_momentum_v150` — angular momentum
|
||||
- 10 new tests (250 total)
|
||||
|
||||
## [1.4.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- `raycast_v140`, `get_collision_events_v140`, `set/get_time_scale_v140`, `get_body_momentum_v140`
|
||||
- 10 new tests (240 total)
|
||||
|
||||
## [1.3.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- `snapshot/restore_scene_v130`, `get_body_aabb_v130`, `apply_torque_v130`
|
||||
- 10 new tests (230 total)
|
||||
|
||||
## [1.2.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- `get_joint_info_v120`, `create_spring_joint_v120`, `set_body_rotation_v120`, `get_body_energy_v120`
|
||||
- 10 new tests (220 total)
|
||||
|
||||
## [1.1.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- `get_body_velocity_v110`, `set_body_position_v110`, `get_contact_pairs_v110`
|
||||
- 9 new tests (210 total)
|
||||
|
||||
## [1.0.0] - 2026-03-11 🎉
|
||||
|
||||
### Added
|
||||
- **Get body tag** — `get_body_tag_v100(body)`
|
||||
- **Body list** — `body_list_v100()` → active body IDs
|
||||
- **Apply impulse** — `apply_impulse_v100(body, ix, iy)`
|
||||
- **Get mass** — `get_body_mass_v100(body)`
|
||||
- **Set friction** — `set_friction_v100(body, f)`
|
||||
- **World bounds** — `get_world_bounds_v100()` → [w, h]
|
||||
- **Body exists** — `body_exists_v100(body)`
|
||||
- **Reset world** — `reset_world_v100()`
|
||||
- **Engine version** — `engine_version_v100()` → "1.0.0"
|
||||
- Body tags, list, impulse, mass, friction, world bounds, reset, version
|
||||
- 9 new tests (201 total)
|
||||
|
||||
## [0.95.0] — Body count, step count, gravity, frozen, color, AABB, raycast, restitution, emitters
|
||||
## [0.90.0] — Layers, gravity scale, angular vel, body type, world gravity, freeze/unfreeze, tag
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ds-physics"
|
||||
version = "1.0.0"
|
||||
version = "1.15.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,18 +1,49 @@
|
|||
# Changelog
|
||||
|
||||
## [1.6.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- **`throttle_init/check/dropped_v160`** — WASM FPS throttler
|
||||
- **`cipher_set_key/apply/reset_v160`** — WASM XOR-rotate cipher
|
||||
- **`checkpoint_capture/seq_v160`** — WASM stream checkpoint
|
||||
- 7 new tests (218 total)
|
||||
|
||||
## [1.5.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- **`bw_init/try_send/refill_v150`** — WASM bandwidth limiter
|
||||
- **`dedup_check/count/reset_v150`** — WASM frame dedup
|
||||
- **`metrics_send/loss/bytes/frames/lost/reset_v150`** — WASM metrics
|
||||
- 7 new tests (211 total)
|
||||
|
||||
## [1.4.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- **`compositor_*_v140`** — WASM frame compositor
|
||||
- **`recorder_*_v140`** — WASM stream recording
|
||||
- **`priority_*_v140`** — WASM priority queue
|
||||
- 7 new tests (204 total)
|
||||
|
||||
## [1.3.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- `quality_decide_v130`, `reorder_*_v130`, `channel_auth_check_v130`
|
||||
- 7 new tests (197 total)
|
||||
|
||||
## [1.2.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- `haptic_*_v120`, `batch/unbatch_v120`, `digest_*_v120`
|
||||
- 7 new tests (190 total)
|
||||
|
||||
## [1.1.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- `error_frame_v110`, `encrypt/decrypt_v110`, `protocol_info_v110`
|
||||
- 9 new tests (183 total)
|
||||
|
||||
## [1.0.0] - 2026-03-11 🎉
|
||||
|
||||
### Added
|
||||
- **`pipeline_add/count_v100`** — stream pipeline
|
||||
- **`proto_header_v100`** — protocol header
|
||||
- **`splitter_push/pop_v100`** — frame splitter
|
||||
- **`cwnd_ack/loss_v100`** — congestion window
|
||||
- **`stream_stats_v100`** — stream stats
|
||||
- **`ack_window_v100`** — sliding ACK window
|
||||
- **`codec_register/list_v100`** — codec registry
|
||||
- **`flow_credit_v100`** — flow control
|
||||
- **`version_negotiate_v100`** — version negotiation
|
||||
- Core WASM bindings for streaming protocol
|
||||
- 9 new tests (174 total)
|
||||
|
||||
## [0.95.0] — lz4, telemetry, diff, backoff, mirror, quota, heartbeat, tag, mavg
|
||||
## [0.90.0] — XOR v2, channel, ack, pool, bw, mux, nonce, validate, retry
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ds-stream-wasm"
|
||||
version = "1.0.0"
|
||||
version = "1.15.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "WebAssembly codec for DreamStack bitstream protocol"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,18 +1,98 @@
|
|||
# Changelog
|
||||
|
||||
## [2.3.0] - 2026-03-11 — Pipeline v4
|
||||
|
||||
### Added
|
||||
- **`ChecksumCodec`** — CRC32 integrity: append on encode, verify+strip on decode, Error on mismatch
|
||||
- **`RateLimitCodec`** — Token bucket algorithm (burst-tolerant, refills over time)
|
||||
- **`TagCodec`** — Attach channel ID for mux routing, drop wrong-channel frames on decode
|
||||
- **`Pipeline::chain()`** — Compose two pipelines into one
|
||||
- **`Pipeline::describe()`** — Human-readable dump (`dedup → compress → encrypt`)
|
||||
- Error propagation tested — corrupt checksum → `PipelineResult.errors`
|
||||
- 474 total tests (+13 new)
|
||||
|
||||
## [2.2.0] - 2026-03-11 — Pipeline v3
|
||||
|
||||
### Added
|
||||
- **`PipelineResult`** — returns frames + collected errors + consumed count
|
||||
- **`StageMetric`** — per-stage frames_in/frames_out/consumed/errors observability
|
||||
- **`ReassemblyCodec`** — reassemble chunked frames (counterpart to SlicerCodec)
|
||||
- **`ConditionalCodec`** — wrap any codec with runtime enable/disable toggle
|
||||
- **`Pipeline::signal(key)`** — preset: dedup→compress→encrypt
|
||||
- **`Pipeline::media(mtu)`** — preset: compress→slicer
|
||||
- **`Pipeline::metrics()`** — per-stage counter snapshot
|
||||
- Slicer↔Reassembly roundtrip verified
|
||||
- 461 total tests (+12 new)
|
||||
|
||||
## [2.1.0] - 2026-03-11 — Pipeline v2
|
||||
|
||||
### Added
|
||||
- **`CodecOutput::Many`** — codecs can fan-out (1 frame → N frames)
|
||||
- **`EncryptCodec`** — XOR cipher encrypt/decrypt adapter
|
||||
- **`FilterCodec`** — drop frames by type (`FilterCodec::drop_control()`)
|
||||
- **`Codec::reset()`** — clear internal state on reconnect
|
||||
- **`Pipeline::encode_all/decode_all`** — batch frame processing
|
||||
- **`SlicerCodec`** rewritten — real MTU chunking via `Many`
|
||||
- Full roundtrip test — Compress→Encrypt→encode→decode→verify
|
||||
- 449 total tests (+11 new)
|
||||
|
||||
## [2.0.0] - 2026-03-11 — Pipeline Architecture 🏗️
|
||||
|
||||
### Added
|
||||
- **[NEW] `pipeline.rs`** — `Frame`, `CodecResult`, `Codec` trait, `Pipeline` builder
|
||||
- **6 codec adapters** — `PassthroughCodec`, `DedupCodec`, `CompressCodec`, `PacerCodec`, `SlicerCodec`, `StatsCodec`
|
||||
- 15 pipeline tests (438 total)
|
||||
|
||||
## [1.6.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- **`AesFrameCipher`** — XOR-rotate frame cipher with configurable key and round tracking (encrypt, decrypt, rounds, reset)
|
||||
- **`LossInjector`** — deterministic packet loss simulation for testing (should_deliver, total_dropped, reset)
|
||||
- **`StreamCheckpoint`** — serializable stream state snapshot as 32 bytes (capture, to_bytes, from_bytes)
|
||||
- 10 new tests (333 total)
|
||||
|
||||
## [1.5.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- **`ContentDedup`** — hash-based frame deduplication (check, dedup_count, reset)
|
||||
- **`StreamHealthTracker`** — latency/throughput/loss tracking
|
||||
- **`FrameThrottler`** — FPS-based frame throttling
|
||||
- 10 new tests (323 total)
|
||||
|
||||
## [1.4.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- **`SessionRecorder`** — frame recording with timestamps
|
||||
- **`PriorityScheduler`** — priority-based frame scheduling
|
||||
- **`BandwidthLimiter`** — token bucket rate limiter
|
||||
- 10 new tests (313 total)
|
||||
|
||||
## [1.3.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- **`QualityDecision`** / **`QualityPolicy`** — adaptive quality
|
||||
- **`FrameLayerCompositor`** — multi-source compositing
|
||||
- **`ReorderBuffer`** — out-of-order frame reordering
|
||||
- 12 new tests (303 total)
|
||||
|
||||
## [1.2.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- **`HapticPayload`** / **`haptic_frame()`** — haptic vibration
|
||||
- **`batch_frames/unbatch_frames`** — frame coalescing
|
||||
- **`StreamDigest`** / **`ChannelAuth`** — integrity + auth
|
||||
- 14 new tests (291 total)
|
||||
|
||||
## [1.1.0] - 2026-03-11
|
||||
|
||||
### Added
|
||||
- **`FrameType::Error`** / **`ErrorPayload`** — error frames
|
||||
- **`encrypt_frame/decrypt_frame`** — XOR envelope
|
||||
- **`FrameRouter`** — content-based dispatch
|
||||
- 13 new tests (277 total)
|
||||
|
||||
## [1.0.0] - 2026-03-11 🎉
|
||||
|
||||
### Added
|
||||
- **`StreamPipeline`** — ordered transform chain
|
||||
- **`ProtocolHeader`** — v1.0 protocol header (encode/decode)
|
||||
- **`FrameSplitterV2`** — split + reassemble frames by MTU
|
||||
- **`CongestionWindowV2`** — TCP-like cwnd (slow start + AIMD)
|
||||
- **`StreamStatsV2`** — comprehensive stream stats
|
||||
- **`AckWindow`** — sliding window ACK
|
||||
- **`CodecRegistryV2`** — named codec registry
|
||||
- **`FlowControllerV2`** — credit-based flow control
|
||||
- **`VersionNegotiator`** — protocol version negotiation
|
||||
- Core streaming: pipeline, protocol, splitter, congestion, stats, ACK, codec, flow control, version negotiation
|
||||
- 9 new tests (264 total)
|
||||
|
||||
## [0.95.0] — Lz4Lite, TelemetrySink, FrameDiffer, BackoffTimer, StreamMirror, QuotaManager, HeartbeatV2, TagFilter, MovingAverage
|
||||
## [0.90.0] — XorCipherV2, ChannelRouter, AckTracker, FramePoolV2, BandwidthEstimatorV2, PriorityMux, Nonce, Validator, Retry
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ds-stream"
|
||||
version = "1.0.0"
|
||||
version = "2.3.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Universal bitstream streaming — any input to any output"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -25,3 +25,4 @@ pub mod protocol;
|
|||
pub mod codec;
|
||||
pub mod relay;
|
||||
pub mod ds_hub;
|
||||
pub mod pipeline;
|
||||
|
|
|
|||
1533
engine/ds-stream/src/pipeline.rs
Normal file
1533
engine/ds-stream/src/pipeline.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -83,6 +83,8 @@ pub enum FrameType {
|
|||
Keyframe = 0xF0,
|
||||
/// Authentication handshake
|
||||
Auth = 0x0F,
|
||||
/// In-band error report
|
||||
Error = 0xEE,
|
||||
/// Acknowledgement — receiver → source with seq + RTT
|
||||
Ack = 0xFD,
|
||||
/// Heartbeat / keep-alive
|
||||
|
|
@ -115,6 +117,7 @@ impl FrameType {
|
|||
0x52 => Some(Self::Compressed),
|
||||
0xF0 => Some(Self::Keyframe),
|
||||
0x0F => Some(Self::Auth),
|
||||
0xEE => Some(Self::Error),
|
||||
0xFD => Some(Self::Ack),
|
||||
0xFE => Some(Self::Ping),
|
||||
0xFF => Some(Self::End),
|
||||
|
|
@ -146,6 +149,7 @@ impl FrameType {
|
|||
Self::Compressed => "Compressed",
|
||||
Self::Keyframe => "Keyframe",
|
||||
Self::Auth => "Auth",
|
||||
Self::Error => "Error",
|
||||
Self::Ack => "Ack",
|
||||
Self::Ping => "Ping",
|
||||
Self::End => "End",
|
||||
|
|
@ -924,6 +928,144 @@ pub fn compute_health(frame_count: u64, avg_rtt_ms: f64, throughput_bps: f64, dr
|
|||
health as f32
|
||||
}
|
||||
|
||||
// ─── v1.1: Error Payload ───
|
||||
|
||||
/// Structured error payload for Error frames (0xEE).
|
||||
/// Format: [error_code:u16 LE][message_len:u16 LE][utf8_message]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ErrorPayload {
|
||||
pub error_code: u16,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ErrorPayload {
|
||||
pub const MIN_SIZE: usize = 4;
|
||||
|
||||
pub fn new(error_code: u16, message: &str) -> Self {
|
||||
Self { error_code, message: message.to_string() }
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let msg_bytes = self.message.as_bytes();
|
||||
let msg_len = msg_bytes.len().min(u16::MAX as usize);
|
||||
let mut buf = Vec::with_capacity(4 + msg_len);
|
||||
buf.extend_from_slice(&self.error_code.to_le_bytes());
|
||||
buf.extend_from_slice(&(msg_len as u16).to_le_bytes());
|
||||
buf.extend_from_slice(&msg_bytes[..msg_len]);
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn decode(buf: &[u8]) -> Option<Self> {
|
||||
if buf.len() < Self::MIN_SIZE { return None; }
|
||||
let error_code = u16::from_le_bytes([buf[0], buf[1]]);
|
||||
let msg_len = u16::from_le_bytes([buf[2], buf[3]]) as usize;
|
||||
if buf.len() < 4 + msg_len { return None; }
|
||||
let message = String::from_utf8_lossy(&buf[4..4 + msg_len]).to_string();
|
||||
Some(Self { error_code, message })
|
||||
}
|
||||
}
|
||||
|
||||
// ─── v1.1: Protocol Info ───
|
||||
|
||||
/// Static protocol information for introspection.
|
||||
pub struct ProtocolInfo;
|
||||
|
||||
impl ProtocolInfo {
|
||||
pub const VERSION: u16 = PROTOCOL_VERSION;
|
||||
pub const MAGIC_BYTES: [u8; 2] = MAGIC;
|
||||
pub const HEADER_BYTES: usize = HEADER_SIZE;
|
||||
pub const MAX_PAYLOAD: u32 = u32::MAX;
|
||||
|
||||
/// Human-readable protocol description.
|
||||
pub fn describe() -> String {
|
||||
format!(
|
||||
"DreamStack Bitstream Protocol v{} (magic: {:02X}{:02X}, header: {} bytes)",
|
||||
Self::VERSION, Self::MAGIC_BYTES[0], Self::MAGIC_BYTES[1], Self::HEADER_BYTES
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── v1.2: Haptic Payload ───
|
||||
|
||||
/// Structured haptic vibration payload.
|
||||
/// Format: [intensity:u8][duration_ms:u16 LE][pattern:u8]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct HapticPayload {
|
||||
pub intensity: u8,
|
||||
pub duration_ms: u16,
|
||||
pub pattern: u8,
|
||||
}
|
||||
|
||||
impl HapticPayload {
|
||||
pub const SIZE: usize = 4;
|
||||
|
||||
pub fn new(intensity: u8, duration_ms: u16, pattern: u8) -> Self {
|
||||
Self { intensity, duration_ms, pattern }
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> [u8; Self::SIZE] {
|
||||
let d = self.duration_ms.to_le_bytes();
|
||||
[self.intensity, d[0], d[1], self.pattern]
|
||||
}
|
||||
|
||||
pub fn decode(buf: &[u8]) -> Option<Self> {
|
||||
if buf.len() < Self::SIZE { return None; }
|
||||
Some(Self {
|
||||
intensity: buf[0],
|
||||
duration_ms: u16::from_le_bytes([buf[1], buf[2]]),
|
||||
pattern: buf[3],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ─── v1.3: Quality Decision ───
|
||||
|
||||
/// Frame output mode for adaptive quality.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FrameMode {
|
||||
FullPixels = 0,
|
||||
Delta = 1,
|
||||
SignalOnly = 2,
|
||||
Disabled = 3,
|
||||
}
|
||||
|
||||
impl FrameMode {
|
||||
pub fn from_u8(v: u8) -> Self {
|
||||
match v {
|
||||
0 => FrameMode::FullPixels,
|
||||
1 => FrameMode::Delta,
|
||||
2 => FrameMode::SignalOnly,
|
||||
3 => FrameMode::Disabled,
|
||||
_ => FrameMode::FullPixels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Quality tier decision: what to send at a given quality level.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct QualityDecision {
|
||||
pub frame_mode: FrameMode,
|
||||
pub compress: bool,
|
||||
pub target_fps: u8,
|
||||
}
|
||||
|
||||
impl QualityDecision {
|
||||
pub fn new(frame_mode: FrameMode, compress: bool, target_fps: u8) -> Self {
|
||||
Self { frame_mode, compress, target_fps }
|
||||
}
|
||||
|
||||
/// Default decision for a tier (0=lowest, 4=highest).
|
||||
pub fn for_tier(tier: u8) -> Self {
|
||||
match tier {
|
||||
0 => Self::new(FrameMode::SignalOnly, false, 5),
|
||||
1 => Self::new(FrameMode::SignalOnly, false, 15),
|
||||
2 => Self::new(FrameMode::Delta, true, 24),
|
||||
3 => Self::new(FrameMode::Delta, false, 30),
|
||||
_ => Self::new(FrameMode::FullPixels, false, 60),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tests ───
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -981,7 +1123,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn frame_type_roundtrip() {
|
||||
for val in [0x01, 0x02, 0x03, 0x10, 0x11, 0x20, 0x30, 0x31, 0x40, 0x41, 0xF0, 0xFD, 0xFE, 0xFF] {
|
||||
for val in [0x01, 0x02, 0x03, 0x10, 0x11, 0x20, 0x30, 0x31, 0x40, 0x41, 0xEE, 0xF0, 0xFD, 0xFE, 0xFF] {
|
||||
let ft = FrameType::from_u8(val);
|
||||
assert!(ft.is_some(), "FrameType::from_u8({:#x}) should be Some", val);
|
||||
assert_eq!(ft.unwrap() as u8, val);
|
||||
|
|
@ -1173,4 +1315,82 @@ mod tests {
|
|||
fn bci_input_event_too_short() {
|
||||
assert!(BciInputEvent::decode(&[0u8; 2]).is_none());
|
||||
}
|
||||
|
||||
// ─── v1.1 tests ───
|
||||
|
||||
#[test]
|
||||
fn error_payload_roundtrip() {
|
||||
let err = ErrorPayload::new(401, "auth required");
|
||||
let encoded = err.encode();
|
||||
let decoded = ErrorPayload::decode(&encoded).unwrap();
|
||||
assert_eq!(decoded.error_code, 401);
|
||||
assert_eq!(decoded.message, "auth required");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_payload_too_short() {
|
||||
assert!(ErrorPayload::decode(&[0u8; 3]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_payload_truncated_message() {
|
||||
// Header says 10 bytes of message but only 2 provided
|
||||
let buf = [0x01, 0x00, 0x0A, 0x00, b'h', b'i'];
|
||||
assert!(ErrorPayload::decode(&buf).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_frame_type_exists() {
|
||||
let ft = FrameType::from_u8(0xEE);
|
||||
assert_eq!(ft, Some(FrameType::Error));
|
||||
assert_eq!(FrameType::Error.name(), "Error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_info_constants() {
|
||||
assert_eq!(ProtocolInfo::VERSION, PROTOCOL_VERSION);
|
||||
assert_eq!(ProtocolInfo::MAGIC_BYTES, MAGIC);
|
||||
assert_eq!(ProtocolInfo::HEADER_BYTES, 16);
|
||||
let desc = ProtocolInfo::describe();
|
||||
assert!(desc.contains("DreamStack"));
|
||||
assert!(desc.contains("16 bytes"));
|
||||
}
|
||||
|
||||
// ─── v1.2 tests ───
|
||||
|
||||
#[test]
|
||||
fn haptic_payload_roundtrip() {
|
||||
let h = HapticPayload::new(200, 500, 3);
|
||||
let encoded = h.encode();
|
||||
let decoded = HapticPayload::decode(&encoded).unwrap();
|
||||
assert_eq!(decoded.intensity, 200);
|
||||
assert_eq!(decoded.duration_ms, 500);
|
||||
assert_eq!(decoded.pattern, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn haptic_payload_too_short() {
|
||||
assert!(HapticPayload::decode(&[0u8; 3]).is_none());
|
||||
}
|
||||
|
||||
// ─── v1.3 tests ───
|
||||
|
||||
#[test]
|
||||
fn quality_decision_default_tiers() {
|
||||
let t0 = QualityDecision::for_tier(0);
|
||||
assert_eq!(t0.frame_mode, FrameMode::SignalOnly);
|
||||
assert_eq!(t0.target_fps, 5);
|
||||
let t4 = QualityDecision::for_tier(4);
|
||||
assert_eq!(t4.frame_mode, FrameMode::FullPixels);
|
||||
assert_eq!(t4.target_fps, 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_mode_roundtrip() {
|
||||
assert_eq!(FrameMode::from_u8(0), FrameMode::FullPixels);
|
||||
assert_eq!(FrameMode::from_u8(1), FrameMode::Delta);
|
||||
assert_eq!(FrameMode::from_u8(2), FrameMode::SignalOnly);
|
||||
assert_eq!(FrameMode::from_u8(3), FrameMode::Disabled);
|
||||
assert_eq!(FrameMode::from_u8(99), FrameMode::FullPixels);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -376,6 +376,58 @@ impl ChannelState {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── v1.2: Auth-Gated Channel ───
|
||||
|
||||
/// Per-channel authentication state.
|
||||
/// When `required` is true, sources must send a valid `Auth` frame before
|
||||
/// the relay will forward their frames.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChannelAuth {
|
||||
required: bool,
|
||||
/// Pre-shared key for this channel (empty = open)
|
||||
key: Vec<u8>,
|
||||
/// Authenticated source addresses (by string identifier)
|
||||
authenticated: Vec<String>,
|
||||
}
|
||||
|
||||
impl ChannelAuth {
|
||||
pub fn open() -> Self {
|
||||
ChannelAuth { required: false, key: Vec::new(), authenticated: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn with_key(key: &[u8]) -> Self {
|
||||
ChannelAuth { required: true, key: key.to_vec(), authenticated: Vec::new() }
|
||||
}
|
||||
|
||||
/// Check if a source is authenticated.
|
||||
pub fn is_authenticated(&self, source_id: &str) -> bool {
|
||||
if !self.required { return true; }
|
||||
self.authenticated.iter().any(|s| s == source_id)
|
||||
}
|
||||
|
||||
/// Attempt to authenticate a source with a token.
|
||||
/// Returns true if authentication succeeded.
|
||||
pub fn authenticate(&mut self, source_id: &str, token: &[u8]) -> bool {
|
||||
if !self.required { return true; }
|
||||
if token == self.key.as_slice() {
|
||||
if !self.authenticated.iter().any(|s| s == source_id) {
|
||||
self.authenticated.push(source_id.to_string());
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Revoke authentication for a source.
|
||||
pub fn revoke(&mut self, source_id: &str) {
|
||||
self.authenticated.retain(|s| s != source_id);
|
||||
}
|
||||
|
||||
pub fn is_required(&self) -> bool { self.required }
|
||||
pub fn authenticated_count(&self) -> usize { self.authenticated.len() }
|
||||
}
|
||||
|
||||
/// Shared relay state — holds all channels.
|
||||
struct RelayState {
|
||||
/// Named channels: "default", "main", "player1", etc.
|
||||
|
|
@ -1757,4 +1809,40 @@ mod tests {
|
|||
cache.process_frame(&f1);
|
||||
assert_eq!(cache.replay_len(), 0); // Nothing stored when depth=0
|
||||
}
|
||||
|
||||
// ─── v1.2: Channel Auth Tests ───
|
||||
|
||||
#[test]
|
||||
fn channel_auth_open_allows_all() {
|
||||
let auth = ChannelAuth::open();
|
||||
assert!(auth.is_authenticated("any-source"));
|
||||
assert!(!auth.is_required());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_auth_rejects_unauthed() {
|
||||
let auth = ChannelAuth::with_key(b"secret-123");
|
||||
assert!(auth.is_required());
|
||||
assert!(!auth.is_authenticated("source-1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_auth_allows_authed() {
|
||||
let mut auth = ChannelAuth::with_key(b"secret-123");
|
||||
assert!(!auth.authenticate("source-1", b"wrong-key"));
|
||||
assert!(!auth.is_authenticated("source-1"));
|
||||
assert!(auth.authenticate("source-1", b"secret-123"));
|
||||
assert!(auth.is_authenticated("source-1"));
|
||||
assert_eq!(auth.authenticated_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_auth_revoke() {
|
||||
let mut auth = ChannelAuth::with_key(b"key");
|
||||
auth.authenticate("src", b"key");
|
||||
assert!(auth.is_authenticated("src"));
|
||||
auth.revoke("src");
|
||||
assert!(!auth.is_authenticated("src"));
|
||||
assert_eq!(auth.authenticated_count(), 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
setup_forgejo.sh
Executable file
43
setup_forgejo.sh
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
#!/bin/bash
|
||||
echo "=== Forgejo Repository Setup ==="
|
||||
echo "This will create the 'dreamstack' repository and push your code."
|
||||
echo ""
|
||||
read -p "Forgejo Username: " USERNAME
|
||||
read -s -p "Forgejo Password: " PASSWORD
|
||||
echo ""
|
||||
echo "--------------------------------"
|
||||
|
||||
# Create repository using Basic Auth
|
||||
echo "Creating repository 'dreamstack' under @$USERNAME..."
|
||||
HTTP_STATUS=$(curl -s -o /tmp/repo_response.json -w "%{http_code}" -u "$USERNAME:$PASSWORD" -X POST https://registry.spaceoperator.org/api/v1/user/repos -H "Content-Type: application/json" -d '{"name": "dreamstack", "private": false}')
|
||||
|
||||
if [ "$HTTP_STATUS" -eq 201 ] || [ "$HTTP_STATUS" -eq 200 ]; then
|
||||
echo "✅ Repository created successfully!"
|
||||
elif grep -q "repository already exists" /tmp/repo_response.json; then
|
||||
echo "ℹ️ Repository already exists, proceeding to push..."
|
||||
else
|
||||
echo "❌ Failed to create repository (HTTP $HTTP_STATUS)"
|
||||
cat /tmp/repo_response.json
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set up git remote and push
|
||||
echo "Pushing code to Forgejo..."
|
||||
cd /home/amir/code/dreamstack || exit 1
|
||||
|
||||
# Encode password for URL to prevent issues with special characters
|
||||
# (Simple approach for git remote)
|
||||
git remote remove origin 2>/dev/null
|
||||
git remote add origin "https://$USERNAME:$PASSWORD@registry.spaceoperator.org/$USERNAME/dreamstack.git"
|
||||
|
||||
echo "Pushing main branch..."
|
||||
git push -u origin main
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Pushing master branch instead..."
|
||||
git push -u origin master
|
||||
fi
|
||||
|
||||
# Clean up credentials from remote URL to leave working copy secure
|
||||
git remote set-url origin "https://registry.spaceoperator.org/$USERNAME/dreamstack.git"
|
||||
echo "✅ Done! Your code is now pushed to https://registry.spaceoperator.org/$USERNAME/dreamstack"
|
||||
Loading…
Add table
Reference in a new issue