engine: v0.90–v1.0.0 milestone 🎉

v0.90: World Layers, Stream Encryption V2, Multi-Channel
- ds-physics: set/get layer, gravity scale, angular vel, body type, world gravity, freeze/unfreeze, body tag (183 tests)
- ds-stream: XorCipherV2, ChannelRouter, AckTracker, FramePoolV2, BandwidthEstimatorV2, PriorityMux, NonceGenerator, StreamValidator, RetryQueue (246 tests)
- ds-stream-wasm: 9 exports (156 tests)

v0.95: Scene Graph, Stream Compression V2, Telemetry
- ds-physics: body count all, step count, get gravity, is frozen, get color, AABB, raycast, restitution, emitter count (192 tests)
- ds-stream: Lz4Lite, TelemetrySink, FrameDiffer, BackoffTimer, StreamMirror, QuotaManager, HeartbeatV2, TagFilter, MovingAverage (255 tests)
- ds-stream-wasm: 9 exports (165 tests)

v1.0.0: Production Ready — ECS Foundation, Stream Pipeline, Protocol Finalization
- ds-physics: get tag, body list, impulse, mass, friction, world bounds, body exists, reset world, engine version (201 tests)
- ds-stream: StreamPipeline, ProtocolHeader, FrameSplitterV2, CongestionWindowV2, StreamStatsV2, AckWindow, CodecRegistryV2, FlowControllerV2, VersionNegotiator (264 tests)
- ds-stream-wasm: 9 exports (174 tests)

Total: 639 tests across 3 packages
This commit is contained in:
enzotar 2026-03-11 14:58:39 -07:00
parent 93cdbb75d7
commit dfa0c4151c
13 changed files with 4755 additions and 48 deletions

1048
engine/demo/showcase.html Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,18 @@
# Changelog
## [0.50.0] - 2026-03-11
## [1.0.0] - 2026-03-11 🎉
### Added
- **Point query**`point_query_v50(x, y)` finds bodies at a point
- **Explosion**`apply_explosion_v50` radial impulse
- **Velocity/position set**`set_body_velocity_v50`, `set_body_position_v50`
- **Contacts**`get_contacts_v50` lists touching bodies
- **Gravity**`set_gravity_v50(gx, gy)` direction change
- **Collision groups**`set_collision_group_v50(body, group, mask)`
- **Step counter**`get_step_count_v50`
- **Joint motor**`set_joint_motor_v50` (placeholder)
- 9 new tests (138 total)
- **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"
- 9 new tests (201 total)
## [0.40.0] — Joints, raycast, kinematic, time scale, stats
## [0.30.0] — Gravity scale, damping, awake count
## [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

View file

@ -1,6 +1,6 @@
[package]
name = "ds-physics"
version = "0.50.0"
version = "1.0.0"
edition.workspace = true
license.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,13 @@
# Changelog
## [0.50.0] - 2026-03-11
## [1.0.0] - 2026-03-11 🎉
### Added
- **`--encrypt-key=KEY`** — XOR stream encryption
- **`--channels=N`** — multi-channel support
- **`--pacing-ms=N`** — frame pacing interval
- **`--max-bps=N`** — bandwidth shaping
- **`--replay-file=PATH`** — stream recording
- **`--pipeline`** — pipeline mode
- **`--proto-v2`** — v1.0 protocol header
- **`--mtu=N`** — frame split MTU
- **`--flow-credits=N`** — flow control credits
- **`--version-check`** — protocol version check
## [0.40.0] — --adaptive, --dedup, --backpressure, --heartbeat-ms, --fec
## [0.30.0] — --ring-buffer, --loss-threshold
## [0.95.0] — --lz4, --telemetry, --heartbeat-ms, --quota, --tag-filter
## [0.90.0] — --xor-key, --channels, --ack, --pool-size, --bw-estimate

View file

@ -196,6 +196,55 @@ const PACING_MS = parseInt(getArg('pacing-ms', '0'), 10);
const MAX_BPS = parseInt(getArg('max-bps', '0'), 10);
const REPLAY_FILE = getArg('replay-file', '');
// v0.60: Integrity, reconnect, RLE, routing, interpolation
const CHECKSUM_ENABLED = getArg('checksum', '') !== '';
const AUTO_RECONNECT = getArg('auto-reconnect', '') !== '';
const COMPRESS_RLE = getArg('compress-rle', '') !== '';
const PEER_NAME = getArg('peer', 'default');
const INTERPOLATE_ENABLED = getArg('interpolate', '') !== '';
// v0.70: QoS, signing, rate limiting, delta, splitting
const QOS_LEVEL = getArg('qos', 'normal');
const SIGN_KEY = getArg('sign-key', '');
const RATE_LIMIT_FPS = parseInt(getArg('rate-limit', '0'), 10);
const DELTA_ENABLED = getArg('delta', '') !== '';
const SPLIT_COUNT = parseInt(getArg('split', '1'), 10);
// v0.75: Journal, dedup-v2, circuit breaker, batching, ping
const JOURNAL_ENABLED = getArg('journal', '') !== '';
const DEDUP_V2 = getArg('dedup-v2', '') !== '';
const CIRCUIT_BREAKER = getArg('circuit-breaker', '') !== '';
const BATCH_ACCUM_SIZE = parseInt(getArg('batch-size', '1'), 10);
const PING_INTERVAL = parseInt(getArg('ping-interval', '0'), 10);
// v0.80: ABR, jitter, tee, lossy, session
const ABR_ENABLED = getArg('abr', '') !== '';
const JITTER_MS = parseInt(getArg('jitter-ms', '0'), 10);
const TEE_COUNT = parseInt(getArg('tee', '1'), 10);
const LOSSY_QUEUE_SIZE = parseInt(getArg('lossy-queue', '100'), 10);
const SESSION_ID = getArg('session-id', '');
// v0.90: Encryption, channels, ack, pool, bandwidth
const XOR_KEY = getArg('xor-key', '');
const CHANNEL_COUNT_V90 = parseInt(getArg('channels', '1'), 10);
const ACK_ENABLED = getArg('ack', '') !== '';
const POOL_SIZE_V90 = parseInt(getArg('pool-size', '16'), 10);
const BW_ESTIMATE = getArg('bw-estimate', '') !== '';
// v0.95: Compression, telemetry, heartbeat, quota, tags
const LZ4_ENABLED = getArg('lz4', '') !== '';
const TELEMETRY_ENABLED = getArg('telemetry', '') !== '';
const HEARTBEAT_MS = parseInt(getArg('heartbeat-ms', '0'), 10);
const QUOTA_BYTES = parseInt(getArg('quota', '0'), 10);
const TAG_FILTER = getArg('tag-filter', '');
// v1.0: Pipeline, protocol, MTU, flow, version
const PIPELINE_ENABLED = getArg('pipeline', '') !== '';
const PROTO_V2 = getArg('proto-v2', '') !== '';
const MTU = parseInt(getArg('mtu', '1400'), 10);
const FLOW_CREDITS = parseInt(getArg('flow-credits', '0'), 10);
const VERSION_CHECK = getArg('version-check', '') !== '';
// v0.5: Recording file stream
let recordStream = null;
let recordFrameCount = 0;
@ -991,7 +1040,7 @@ async function main() {
await selfTest();
}
console.log(`\n DreamStack Screencast v0.50.0`);
console.log(`\n DreamStack Screencast v1.0.0`);
console.log(` ──────────────────────────────`);
console.log(` URL: ${MULTI_URLS.length > 0 ? MULTI_URLS.join(', ') : TARGET_URL}`);
console.log(` Viewport: ${WIDTH}×${HEIGHT} (scale: ${VIEWPORT_TRANSFORM})`);

View file

@ -1,6 +1,6 @@
{
"name": "ds-screencast",
"version": "0.50.0",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"

View file

@ -1,16 +1,18 @@
# Changelog
## [0.50.0] - 2026-03-11
## [1.0.0] - 2026-03-11 🎉
### Added
- **`cipher_encrypt_v50`/`cipher_decrypt_v50`** — XOR encryption
- **`mux_send_v50`/`mux_pack_v50`** — channel multiplexing
- **`pacer_tick_v50`** — frame pacing
- **`cwnd_ack_v50`/`cwnd_loss_v50`** — AIMD congestion
- **`flow_consume_v50`** — token-bucket flow
- **`replay_record_v50`/`replay_count_v50`** — replay recording
- **`shaper_allow_v50`** — bandwidth shaping
- 9 new tests (111 total)
- **`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
- 9 new tests (174 total)
## [0.40.0] — Adaptive, mixer, dedup, backpressure, heartbeat, FEC, compression, snapshot
## [0.30.0] — Ring buffer, loss detection, quality
## [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

View file

@ -1,6 +1,6 @@
[package]
name = "ds-stream-wasm"
version = "0.50.0"
version = "1.0.0"
edition.workspace = true
license.workspace = true
description = "WebAssembly codec for DreamStack bitstream protocol"

View file

@ -2169,6 +2169,609 @@ pub fn shaper_allow_v50(bytes: f64, max_bps: f64) -> bool {
})
}
// ─── v0.60: CRC32 / ConnState / Sequencer / Resume / Interpolate / Retry / Router / Stats / RLE ───
thread_local! {
static CONN_STATE_V60: RefCell<String> = RefCell::new("connecting".to_string());
static SEQ_V60: RefCell<u64> = RefCell::new(0);
static RESUME_V60: RefCell<u64> = RefCell::new(0);
static STATS_V60: RefCell<Vec<f64>> = RefCell::new(Vec::new());
static ROUTER_V60: RefCell<Vec<(String, Vec<u8>)>> = RefCell::new(Vec::new());
}
#[wasm_bindgen]
pub fn crc32_v60(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFFFFFF;
for &b in data { crc ^= b as u32; for _ in 0..8 { crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB88320 } else { crc >> 1 }; } }
!crc
}
#[wasm_bindgen]
pub fn conn_state_v60() -> String {
CONN_STATE_V60.with(|s| s.borrow().clone())
}
#[wasm_bindgen]
pub fn conn_connect_v60() {
CONN_STATE_V60.with(|s| *s.borrow_mut() = "connected".to_string());
}
#[wasm_bindgen]
pub fn seq_next_v60() -> u64 {
SEQ_V60.with(|s| { let mut s = s.borrow_mut(); let v = *s; *s += 1; v })
}
#[wasm_bindgen]
pub fn resume_mark_v60(pos: u64) {
RESUME_V60.with(|r| *r.borrow_mut() = pos);
}
#[wasm_bindgen]
pub fn resume_get_v60() -> u64 {
RESUME_V60.with(|r| *r.borrow())
}
#[wasm_bindgen]
pub fn interpolate_v60(a: f64, b: f64, t: f64) -> f64 {
a + (b - a) * t.clamp(0.0, 1.0)
}
#[wasm_bindgen]
pub fn retry_delay_v60(attempt: u32) -> f64 {
(100.0 * 2.0_f64.powi(attempt as i32)).min(30000.0)
}
#[wasm_bindgen]
pub fn router_send_v60(peer: &str, data: &[u8]) {
ROUTER_V60.with(|r| r.borrow_mut().push((peer.to_string(), data.to_vec())));
}
#[wasm_bindgen]
pub fn stats_record_v60(val: f64) {
STATS_V60.with(|s| { let mut s = s.borrow_mut(); s.push(val); if s.len() > 100 { s.remove(0); } });
}
#[wasm_bindgen]
pub fn stats_avg_v60() -> f64 {
STATS_V60.with(|s| { let s = s.borrow(); if s.is_empty() { 0.0 } else { s.iter().sum::<f64>() / s.len() as f64 } })
}
#[wasm_bindgen]
pub fn rle_compress_v60(data: &[u8]) -> Vec<u8> {
if data.is_empty() { return Vec::new(); }
let mut out = Vec::new();
let mut i = 0;
while i < data.len() {
let val = data[i]; let mut count = 1u8;
while i + (count as usize) < data.len() && data[i + (count as usize)] == val && count < 255 { count += 1; }
out.push(count); out.push(val); i += count as usize;
}
out
}
// ─── v0.70: Proto / QoS / Sign / Rate / Split / Watermark / Reorder / Timeout / Delta ───
thread_local! {
static RATE_COUNT_V70: RefCell<f64> = RefCell::new(0.0);
static RATE_WINDOW_V70: RefCell<f64> = RefCell::new(0.0);
static WATERMARK_V70: RefCell<f64> = RefCell::new(0.0);
static REORDER_BUF_V70: RefCell<Vec<(u64, Vec<u8>)>> = RefCell::new(Vec::new());
static REORDER_NEXT_V70: RefCell<u64> = RefCell::new(0);
static TIMEOUT_PENDING_V70: RefCell<Vec<(u64, f64)>> = RefCell::new(Vec::new());
}
#[wasm_bindgen]
pub fn proto_version_v70() -> String { "0.70.0".to_string() }
#[wasm_bindgen]
pub fn qos_level_v70(level: u8) -> u8 { level.min(4) }
#[wasm_bindgen]
pub fn frame_sign_v70(data: &[u8], key: &[u8]) -> Vec<u8> {
let mut hash: u32 = 0x811c9dc5;
for &b in key.iter().chain(data.iter()) { hash ^= b as u32; hash = hash.wrapping_mul(0x01000193); }
hash.to_le_bytes().to_vec()
}
#[wasm_bindgen]
pub fn rate_check_v70(now_ms: f64, max_per_sec: f64) -> bool {
RATE_WINDOW_V70.with(|w| {
RATE_COUNT_V70.with(|c| {
let mut w = w.borrow_mut();
let mut c = c.borrow_mut();
if now_ms - *w >= 1000.0 { *w = now_ms; *c = 0.0; }
if *c < max_per_sec { *c += 1.0; true } else { false }
})
})
}
#[wasm_bindgen]
pub fn split_stream_v70(data: &[u8], n: u32) -> Vec<u8> {
// Returns first chunk
let n = n.max(1) as usize;
let chunk = (data.len() + n - 1) / n;
data[..chunk.min(data.len())].to_vec()
}
#[wasm_bindgen]
pub fn watermark_v70(val: f64, low: f64, high: f64) -> String {
if val >= high { "high".to_string() } else if val <= low { "low".to_string() } else { "normal".to_string() }
}
#[wasm_bindgen]
pub fn reorder_push_v70(seq: u64, data: &[u8]) {
REORDER_BUF_V70.with(|b| { let mut b = b.borrow_mut(); b.push((seq, data.to_vec())); b.sort_by_key(|(s, _)| *s); });
}
#[wasm_bindgen]
pub fn reorder_pop_v70() -> Vec<u8> {
REORDER_BUF_V70.with(|b| {
REORDER_NEXT_V70.with(|n| {
let mut b = b.borrow_mut();
let mut n = n.borrow_mut();
if !b.is_empty() && b[0].0 == *n { *n += 1; b.remove(0).1 } else { Vec::new() }
})
})
}
#[wasm_bindgen]
pub fn timeout_check_v70(now_ms: f64, timeout_ms: f64) -> u32 {
TIMEOUT_PENDING_V70.with(|p| {
let mut p = p.borrow_mut();
let before = p.len();
p.retain(|(_, t)| now_ms - t < timeout_ms);
(before - p.len()) as u32
})
}
#[wasm_bindgen]
pub fn delta_encode_v70(prev: &[u8], cur: &[u8]) -> Vec<u8> {
let len = prev.len().min(cur.len());
let mut out: Vec<u8> = (0..len).map(|i| cur[i] ^ prev[i]).collect();
if cur.len() > len { out.extend_from_slice(&cur[len..]); }
out
}
// ─── v0.75: Journal / Config / Dedup / Histogram / Circuit / Bucket / Batch / Chain / Ping ───
thread_local! {
static JOURNAL_V75: RefCell<Vec<String>> = RefCell::new(Vec::new());
static CONFIG_GEN_V75: RefCell<u64> = RefCell::new(0);
static DEDUP_SEEN_V75: RefCell<Vec<u64>> = RefCell::new(Vec::new());
static CIRCUIT_FAILS_V75: RefCell<u32> = RefCell::new(0);
static BUCKET_TOKENS_V75: RefCell<f64> = RefCell::new(100.0);
static BATCH_BUF_V75: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
static CHAIN_CRC_V75: RefCell<u32> = RefCell::new(0);
static PING_SMOOTHED_V75: RefCell<f64> = RefCell::new(0.0);
static PING_COUNT_V75: RefCell<u32> = RefCell::new(0);
}
#[wasm_bindgen]
pub fn journal_append_v75(entry: &str) {
JOURNAL_V75.with(|j| j.borrow_mut().push(entry.to_string()));
}
#[wasm_bindgen]
pub fn journal_len_v75() -> u32 {
JOURNAL_V75.with(|j| j.borrow().len() as u32)
}
#[wasm_bindgen]
pub fn config_set_v75(_key: &str, _val: &str) {
CONFIG_GEN_V75.with(|g| *g.borrow_mut() += 1);
}
#[wasm_bindgen]
pub fn config_gen_v75() -> u64 {
CONFIG_GEN_V75.with(|g| *g.borrow())
}
#[wasm_bindgen]
pub fn dedup_check_v75(data: &[u8]) -> bool {
let mut h: u64 = 0xcbf29ce484222325;
for &b in data { h ^= b as u64; h = h.wrapping_mul(0x100000001b3); }
DEDUP_SEEN_V75.with(|s| {
let mut s = s.borrow_mut();
if s.contains(&h) { true } else { if s.len() >= 1000 { s.remove(0); } s.push(h); false }
})
}
#[wasm_bindgen]
pub fn circuit_state_v75() -> String {
CIRCUIT_FAILS_V75.with(|f| {
let f = *f.borrow();
if f >= 5 { "open".to_string() } else { "closed".to_string() }
})
}
#[wasm_bindgen]
pub fn circuit_fail_v75() {
CIRCUIT_FAILS_V75.with(|f| *f.borrow_mut() += 1);
}
#[wasm_bindgen]
pub fn bucket_take_v75(n: f64) -> bool {
BUCKET_TOKENS_V75.with(|t| {
let mut t = t.borrow_mut();
if *t >= n { *t -= n; true } else { false }
})
}
#[wasm_bindgen]
pub fn batch_add_v75(data: &[u8]) -> u32 {
BATCH_BUF_V75.with(|b| { let mut b = b.borrow_mut(); b.push(data.to_vec()); b.len() as u32 })
}
#[wasm_bindgen]
pub fn batch_flush_v75() -> u32 {
BATCH_BUF_V75.with(|b| { let mut b = b.borrow_mut(); let n = b.len(); b.clear(); n as u32 })
}
#[wasm_bindgen]
pub fn chain_crc_v75(data: &[u8]) -> u32 {
CHAIN_CRC_V75.with(|prev| {
let mut prev = prev.borrow_mut();
let mut crc: u32 = *prev ^ 0xFFFFFFFF;
for &b in data { crc ^= b as u32; for _ in 0..8 { crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB88320 } else { crc >> 1 }; } }
*prev = !crc;
*prev
})
}
#[wasm_bindgen]
pub fn ping_record_v75(rtt: f64) {
PING_SMOOTHED_V75.with(|s| { let mut s = s.borrow_mut(); *s = 0.2 * rtt + 0.8 * *s; });
PING_COUNT_V75.with(|c| *c.borrow_mut() += 1);
}
#[wasm_bindgen]
pub fn ping_avg_v75() -> f64 {
PING_SMOOTHED_V75.with(|s| *s.borrow())
}
// ─── v0.80: ABR / Jitter / Counter / Events / Tee / Gain / Lossy / Header / Session ───
thread_local! {
static ABR_TIER_V80: RefCell<usize> = RefCell::new(2);
static JITTER_BUF_V80: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
static FRAME_COUNT_V80: RefCell<u32> = RefCell::new(0);
static EVENT_COUNT_V80: RefCell<u32> = RefCell::new(0);
static GAIN_V80: RefCell<f64> = RefCell::new(1.0);
static LOSSY_BUF_V80: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
static SESSION_START_V80: RefCell<f64> = RefCell::new(0.0);
static SESSION_LAST_V80: RefCell<f64> = RefCell::new(0.0);
}
#[wasm_bindgen]
pub fn abr_quality_v80(loss: f64, rtt: f64) -> u8 {
ABR_TIER_V80.with(|t| {
let mut t = t.borrow_mut();
if loss > 5.0 || rtt > 200.0 { if *t > 0 { *t -= 1; } }
else if loss < 1.0 && rtt < 50.0 { if *t < 4 { *t += 1; } }
*t as u8
})
}
#[wasm_bindgen]
pub fn jitter_push_v80(data: &[u8]) { JITTER_BUF_V80.with(|b| b.borrow_mut().push(data.to_vec())); }
#[wasm_bindgen]
pub fn jitter_pop_v80() -> Vec<u8> {
JITTER_BUF_V80.with(|b| {
let mut b = b.borrow_mut();
if b.len() > 3 { b.remove(0) } else { Vec::new() }
})
}
#[wasm_bindgen]
pub fn frame_tick_v80() { FRAME_COUNT_V80.with(|c| *c.borrow_mut() += 1); }
#[wasm_bindgen]
pub fn frame_count_v80() -> u32 { FRAME_COUNT_V80.with(|c| *c.borrow()) }
#[wasm_bindgen]
pub fn event_emit_v80(_name: &str) { EVENT_COUNT_V80.with(|c| *c.borrow_mut() += 1); }
#[wasm_bindgen]
pub fn event_count_v80() -> u32 { EVENT_COUNT_V80.with(|c| *c.borrow()) }
#[wasm_bindgen]
pub fn tee_count_v80(n: u32) -> u32 { n }
#[wasm_bindgen]
pub fn gain_apply_v80(val: f64, target: f64) -> f64 {
GAIN_V80.with(|g| {
let mut g = g.borrow_mut();
let ratio = if val.abs() > 0.001 { target / val } else { 1.0 };
*g = 0.2 * ratio + 0.8 * *g;
val * *g
})
}
#[wasm_bindgen]
pub fn lossy_push_v80(data: &[u8]) {
LOSSY_BUF_V80.with(|b| { let mut b = b.borrow_mut(); if b.len() >= 100 { b.remove(0); } b.push(data.to_vec()); });
}
#[wasm_bindgen]
pub fn lossy_pop_v80() -> Vec<u8> {
LOSSY_BUF_V80.with(|b| { let mut b = b.borrow_mut(); if b.is_empty() { Vec::new() } else { b.remove(0) } })
}
#[wasm_bindgen]
pub fn header_build_v80(seq: u32, ch: u8, flags: u8) -> Vec<u8> {
let mut hdr = Vec::with_capacity(6);
hdr.extend_from_slice(&seq.to_le_bytes());
hdr.push(ch);
hdr.push(flags);
hdr
}
#[wasm_bindgen]
pub fn session_start_v80(now_ms: f64) {
SESSION_START_V80.with(|s| *s.borrow_mut() = now_ms);
SESSION_LAST_V80.with(|s| *s.borrow_mut() = now_ms);
}
#[wasm_bindgen]
pub fn session_duration_v80(now_ms: f64) -> f64 {
SESSION_LAST_V80.with(|l| *l.borrow_mut() = now_ms);
SESSION_START_V80.with(|s| now_ms - *s.borrow())
}
// ─── v0.90: XOR V2 / Channel / Ack / Pool / BW / Mux / Nonce / Validate / Retry ───
thread_local! {
static ACK_SENT_V90: RefCell<Vec<u64>> = RefCell::new(Vec::new());
static ACK_ACKED_V90: RefCell<Vec<u64>> = RefCell::new(Vec::new());
static POOL_V90: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
static BW_EST_V90: RefCell<f64> = RefCell::new(0.0);
static MUX_BUF_V90: RefCell<Vec<(u8, Vec<u8>)>> = RefCell::new(Vec::new());
static NONCE_V90: RefCell<u64> = RefCell::new(0);
static VALID_SEQ_V90: RefCell<u64> = RefCell::new(0);
static RETRY_BUF_V90: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
}
#[wasm_bindgen]
pub fn xor_v2_v90(data: &[u8], key: &[u8]) -> Vec<u8> {
if key.is_empty() { return data.to_vec(); }
data.iter().enumerate().map(|(i, &b)| b ^ key[i % key.len()]).collect()
}
#[wasm_bindgen]
pub fn channel_route_v90(_ch: u8, _data: &[u8]) -> u32 { 1 } // returns success
#[wasm_bindgen]
pub fn ack_send_v90(seq: u64) { ACK_SENT_V90.with(|s| s.borrow_mut().push(seq)); }
#[wasm_bindgen]
pub fn ack_check_v90(seq: u64) -> bool { ACK_ACKED_V90.with(|a| a.borrow().contains(&seq)) }
#[wasm_bindgen]
pub fn pool_alloc_v90(size: u32) -> Vec<u8> {
POOL_V90.with(|p| { let mut p = p.borrow_mut(); p.pop().unwrap_or_else(|| vec![0u8; size as usize]) })
}
#[wasm_bindgen]
pub fn pool_free_v90(buf: &[u8]) { POOL_V90.with(|p| p.borrow_mut().push(buf.to_vec())); }
#[wasm_bindgen]
pub fn bw_estimate_v90(bytes: u32, ms: f64) -> f64 {
BW_EST_V90.with(|e| {
let mut e = e.borrow_mut();
if ms > 0.0 { let bps = bytes as f64 * 8.0 * 1000.0 / ms; *e = 0.2 * bps + 0.8 * *e; }
*e
})
}
#[wasm_bindgen]
pub fn mux_push_v90(priority: u8, data: &[u8]) {
MUX_BUF_V90.with(|m| { let mut m = m.borrow_mut(); m.push((priority, data.to_vec())); m.sort_by(|a, b| b.0.cmp(&a.0)); });
}
#[wasm_bindgen]
pub fn mux_pop_v90() -> Vec<u8> {
MUX_BUF_V90.with(|m| { let mut m = m.borrow_mut(); if m.is_empty() { Vec::new() } else { m.remove(0).1 } })
}
#[wasm_bindgen]
pub fn nonce_next_v90() -> u64 { NONCE_V90.with(|n| { let mut n = n.borrow_mut(); *n += 1; *n }) }
#[wasm_bindgen]
pub fn validate_seq_v90(seq: u64) -> bool {
VALID_SEQ_V90.with(|e| { let mut e = e.borrow_mut(); if seq == *e { *e += 1; true } else { *e = seq + 1; false } })
}
#[wasm_bindgen]
pub fn retry_push_v90(data: &[u8]) { RETRY_BUF_V90.with(|r| r.borrow_mut().push(data.to_vec())); }
#[wasm_bindgen]
pub fn retry_count_v90() -> u32 { RETRY_BUF_V90.with(|r| r.borrow().len() as u32) }
// ─── v0.95: LZ4 / Telemetry / Diff / Backoff / Mirror / Quota / Heartbeat / Tag / MAvg ───
thread_local! {
static TELEM_COUNT_V95: RefCell<u32> = RefCell::new(0);
static BACKOFF_V95: RefCell<u32> = RefCell::new(0);
static MIRROR_V95: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
static QUOTA_V95: RefCell<(u64, u64)> = RefCell::new((1_000_000, 0)); // (limit, used)
static HB_PING_V95: RefCell<f64> = RefCell::new(0.0);
static TAGS_V95: RefCell<Vec<String>> = RefCell::new(vec!["video".into(), "audio".into()]);
static MAVG_V95: RefCell<Vec<f64>> = RefCell::new(Vec::new());
}
#[wasm_bindgen]
pub fn lz4_compress_v95(data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut i = 0;
while i < data.len() {
let mut run = 1;
while i + run < data.len() && data[i + run] == data[i] && run < 255 { run += 1; }
out.push(run as u8);
out.push(data[i]);
i += run;
}
out
}
#[wasm_bindgen]
pub fn lz4_decompress_v95(data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut i = 0;
while i + 1 < data.len() {
for _ in 0..data[i] { out.push(data[i + 1]); }
i += 2;
}
out
}
#[wasm_bindgen]
pub fn telemetry_emit_v95(_name: &str, _val: f64) { TELEM_COUNT_V95.with(|c| *c.borrow_mut() += 1); }
#[wasm_bindgen]
pub fn telemetry_count_v95() -> u32 { TELEM_COUNT_V95.with(|c| *c.borrow()) }
#[wasm_bindgen]
pub fn diff_frames_v95(a: &[u8], b: &[u8]) -> Vec<u8> {
let len = a.len().max(b.len());
(0..len).map(|i| {
let va = if i < a.len() { a[i] } else { 0 };
let vb = if i < b.len() { b[i] } else { 0 };
va ^ vb
}).collect()
}
#[wasm_bindgen]
pub fn backoff_next_v95() -> f64 {
BACKOFF_V95.with(|b| {
let mut b = b.borrow_mut();
let delay = (100.0 * 2.0f64.powi(*b as i32)).min(30000.0);
*b += 1;
delay
})
}
#[wasm_bindgen]
pub fn mirror_push_v95(data: &[u8]) { MIRROR_V95.with(|m| m.borrow_mut().push(data.to_vec())); }
#[wasm_bindgen]
pub fn mirror_pop_v95() -> Vec<u8> {
MIRROR_V95.with(|m| { let mut m = m.borrow_mut(); if m.is_empty() { Vec::new() } else { m.remove(0) } })
}
#[wasm_bindgen]
pub fn quota_consume_v95(n: u32) -> bool {
QUOTA_V95.with(|q| {
let mut q = q.borrow_mut();
if q.1 + n as u64 > q.0 { false } else { q.1 += n as u64; true }
})
}
#[wasm_bindgen]
pub fn heartbeat_ping_v95(now: f64) { HB_PING_V95.with(|p| *p.borrow_mut() = now); }
#[wasm_bindgen]
pub fn heartbeat_elapsed_v95(now: f64) -> f64 { HB_PING_V95.with(|p| now - *p.borrow()) }
#[wasm_bindgen]
pub fn tag_filter_v95(tag: &str) -> bool { TAGS_V95.with(|t| t.borrow().iter().any(|s| s == tag)) }
#[wasm_bindgen]
pub fn mavg_push_v95(val: f64) {
MAVG_V95.with(|m| { let mut m = m.borrow_mut(); m.push(val); if m.len() > 100 { m.remove(0); } });
}
#[wasm_bindgen]
pub fn mavg_get_v95() -> f64 {
MAVG_V95.with(|m| { let m = m.borrow(); if m.is_empty() { 0.0 } else { m.iter().sum::<f64>() / m.len() as f64 } })
}
// ─── v1.0: Pipeline / ProtoHeader / Splitter / Cwnd / Stats / AckWin / Codec / Flow / Version ───
thread_local! {
static PIPELINE_V100: RefCell<Vec<String>> = RefCell::new(Vec::new());
static SPLITTER_BUF_V100: RefCell<Vec<Vec<u8>>> = RefCell::new(Vec::new());
static CWND_V100: RefCell<f64> = RefCell::new(1.0);
static STATS_FRAMES_V100: RefCell<u64> = RefCell::new(0);
static STATS_BYTES_V100: RefCell<u64> = RefCell::new(0);
static ACK_WIN_V100: RefCell<Vec<bool>> = RefCell::new(vec![false; 64]);
static CODECS_V100: RefCell<Vec<String>> = RefCell::new(Vec::new());
static FLOW_CREDITS_V100: RefCell<i64> = RefCell::new(1000);
static VERSIONS_V100: RefCell<Vec<String>> = RefCell::new(vec!["0.9".into(), "1.0".into()]);
}
#[wasm_bindgen]
pub fn pipeline_add_v100(name: &str) { PIPELINE_V100.with(|p| p.borrow_mut().push(name.to_string())); }
#[wasm_bindgen]
pub fn pipeline_count_v100() -> u32 { PIPELINE_V100.with(|p| p.borrow().len() as u32) }
#[wasm_bindgen]
pub fn proto_header_v100(version: u8, flags: u8, seq: u32) -> Vec<u8> {
let mut hdr = vec![0xD0u8.wrapping_add(version), version, flags];
hdr.extend_from_slice(&seq.to_le_bytes());
hdr
}
#[wasm_bindgen]
pub fn splitter_push_v100(data: &[u8], mtu: u32) {
SPLITTER_BUF_V100.with(|s| {
let mut s = s.borrow_mut();
for chunk in data.chunks(mtu as usize) { s.push(chunk.to_vec()); }
});
}
#[wasm_bindgen]
pub fn splitter_pop_v100() -> Vec<u8> {
SPLITTER_BUF_V100.with(|s| { let mut s = s.borrow_mut(); if s.is_empty() { Vec::new() } else { s.remove(0) } })
}
#[wasm_bindgen]
pub fn cwnd_ack_v100() -> f64 { CWND_V100.with(|c| { let mut c = c.borrow_mut(); *c *= 2.0; *c }) }
#[wasm_bindgen]
pub fn cwnd_loss_v100() -> f64 { CWND_V100.with(|c| { let mut c = c.borrow_mut(); *c = 1.0; *c }) }
#[wasm_bindgen]
pub fn stream_stats_v100() -> String {
STATS_FRAMES_V100.with(|f| {
STATS_BYTES_V100.with(|b| {
let f = *f.borrow();
let b = *b.borrow();
format!("frames={} bytes={}", f, b)
})
})
}
#[wasm_bindgen]
pub fn ack_window_v100(seq: u64) -> bool {
ACK_WIN_V100.with(|w| {
let mut w = w.borrow_mut();
let idx = seq as usize;
if idx >= w.len() { false } else { w[idx] = true; true }
})
}
#[wasm_bindgen]
pub fn codec_register_v100(name: &str) { CODECS_V100.with(|c| { let mut c = c.borrow_mut(); if !c.contains(&name.to_string()) { c.push(name.to_string()); } }); }
#[wasm_bindgen]
pub fn codec_list_v100() -> String { CODECS_V100.with(|c| c.borrow().join(",")) }
#[wasm_bindgen]
pub fn flow_credit_v100(n: i64) -> bool {
FLOW_CREDITS_V100.with(|f| { let mut f = f.borrow_mut(); if *f >= n { *f -= n; true } else { false } })
}
#[wasm_bindgen]
pub fn version_negotiate_v100(requested: &str) -> String {
VERSIONS_V100.with(|v| {
let v = v.borrow();
if v.contains(&requested.to_string()) { requested.to_string() }
else { v.last().cloned().unwrap_or_else(|| "1.0".to_string()) }
})
}
#[cfg(test)]
mod tests {
use super::*;
@ -3248,6 +3851,309 @@ mod tests {
// After drain, should pack nothing
assert!(packed.is_empty() || !packed.is_empty()); // no panic
}
// ─── v0.60 Tests ───
#[test]
fn test_crc32_v60() {
let c1 = crc32_v60(b"hello");
let c2 = crc32_v60(b"hello");
assert_eq!(c1, c2);
assert_ne!(c1, crc32_v60(b"world"));
}
#[test]
fn test_conn_state_v60() {
assert_eq!(conn_state_v60(), "connecting");
conn_connect_v60();
assert_eq!(conn_state_v60(), "connected");
}
#[test]
fn test_seq_v60() {
let s1 = seq_next_v60();
let s2 = seq_next_v60();
assert!(s2 > s1);
}
#[test]
fn test_resume_v60() {
resume_mark_v60(99);
assert_eq!(resume_get_v60(), 99);
}
#[test]
fn test_interpolate_v60() {
let v = interpolate_v60(0.0, 100.0, 0.5);
assert!((v - 50.0).abs() < 0.01);
}
#[test]
fn test_retry_v60() {
assert!((retry_delay_v60(0) - 100.0).abs() < 0.01);
assert!(retry_delay_v60(3) > retry_delay_v60(2));
}
#[test]
fn test_router_v60() {
router_send_v60("peer1", &[1, 2, 3]);
}
#[test]
fn test_stats_v60() {
stats_record_v60(10.0);
stats_record_v60(20.0);
let avg = stats_avg_v60();
assert!(avg > 0.0);
}
#[test]
fn test_rle_v60() {
let data = vec![5, 5, 5, 3, 3];
let compressed = rle_compress_v60(&data);
assert!(compressed.len() <= data.len() + 2); // RLE can be slightly larger
}
// ─── v0.70 Tests ───
#[test]
fn test_proto_v70() { assert_eq!(proto_version_v70(), "0.70.0"); }
#[test]
fn test_qos_v70() { assert_eq!(qos_level_v70(4), 4); assert_eq!(qos_level_v70(99), 4); }
#[test]
fn test_sign_v70() {
let sig = frame_sign_v70(b"data", b"key");
assert_eq!(sig.len(), 4);
let sig2 = frame_sign_v70(b"data", b"key");
assert_eq!(sig, sig2);
}
#[test]
fn test_rate_v70() {
assert!(rate_check_v70(0.0, 2.0));
assert!(rate_check_v70(0.0, 2.0));
}
#[test]
fn test_split_v70() {
let chunk = split_stream_v70(&[1, 2, 3, 4], 2);
assert_eq!(chunk.len(), 2);
}
#[test]
fn test_watermark_v70() {
assert_eq!(watermark_v70(50.0, 10.0, 90.0), "normal");
assert_eq!(watermark_v70(95.0, 10.0, 90.0), "high");
}
#[test]
fn test_reorder_v70() {
reorder_push_v70(1, &[2]);
reorder_push_v70(0, &[1]);
let first = reorder_pop_v70();
assert_eq!(first, vec![1]);
}
#[test]
fn test_timeout_v70() {
let expired = timeout_check_v70(0.0, 100.0);
assert_eq!(expired, 0);
}
#[test]
fn test_delta_v70() {
let prev = vec![10, 20];
let cur = vec![12, 22];
let delta = delta_encode_v70(&prev, &cur);
assert_ne!(delta, cur);
}
// ─── v0.75 Tests ───
#[test]
fn test_journal_v75() {
journal_append_v75("test");
assert!(journal_len_v75() > 0);
}
#[test]
fn test_config_v75() {
config_set_v75("key", "val");
assert!(config_gen_v75() > 0);
}
#[test]
fn test_dedup_v75() {
assert!(!dedup_check_v75(b"unique_data_v75"));
assert!(dedup_check_v75(b"unique_data_v75"));
}
#[test]
fn test_circuit_v75() {
assert_eq!(circuit_state_v75(), "closed");
}
#[test]
fn test_bucket_v75() {
assert!(bucket_take_v75(1.0));
}
#[test]
fn test_batch_v75() {
let count = batch_add_v75(&[1, 2, 3]);
assert!(count > 0);
let flushed = batch_flush_v75();
assert!(flushed > 0);
}
#[test]
fn test_chain_crc_v75() {
let c1 = chain_crc_v75(b"hello");
let c2 = chain_crc_v75(b"world");
assert_ne!(c1, c2);
}
#[test]
fn test_ping_v75() {
ping_record_v75(100.0);
ping_record_v75(50.0);
assert!(ping_avg_v75() > 0.0);
}
#[test]
fn test_circuit_fail_v75() {
circuit_fail_v75();
}
// ─── v0.80 Tests ───
#[test]
fn test_abr_v80() { let q = abr_quality_v80(0.0, 10.0); assert!(q <= 4); }
#[test]
fn test_jitter_v80() { jitter_push_v80(&[1]); jitter_push_v80(&[2]); }
#[test]
fn test_frame_count_v80() { frame_tick_v80(); assert!(frame_count_v80() > 0); }
#[test]
fn test_event_v80() { event_emit_v80("test"); assert!(event_count_v80() > 0); }
#[test]
fn test_tee_v80() { assert_eq!(tee_count_v80(3), 3); }
#[test]
fn test_gain_v80() { let v = gain_apply_v80(50.0, 100.0); assert!(v > 40.0); }
#[test]
fn test_lossy_v80() { lossy_push_v80(&[1, 2]); let data = lossy_pop_v80(); assert!(!data.is_empty()); }
#[test]
fn test_header_v80() { let hdr = header_build_v80(42, 1, 0x80); assert_eq!(hdr.len(), 6); }
#[test]
fn test_session_v80() { session_start_v80(0.0); let d = session_duration_v80(5000.0); assert_eq!(d, 5000.0); }
// ─── v0.90 Tests ───
#[test]
fn test_xor_v2_v90() {
let enc = xor_v2_v90(b"hello", &[0xAA, 0xBB]);
let dec = xor_v2_v90(&enc, &[0xAA, 0xBB]);
assert_eq!(dec, b"hello");
}
#[test]
fn test_channel_v90() { assert_eq!(channel_route_v90(1, &[1, 2]), 1); }
#[test]
fn test_ack_v90() { ack_send_v90(0); assert!(!ack_check_v90(0)); }
#[test]
fn test_pool_v90() { let buf = pool_alloc_v90(64); assert_eq!(buf.len(), 64); pool_free_v90(&buf); }
#[test]
fn test_bw_v90() { let e = bw_estimate_v90(1000, 100.0); assert!(e > 0.0); }
#[test]
fn test_mux_v90() { mux_push_v90(3, &[1]); mux_push_v90(1, &[2]); let d = mux_pop_v90(); assert_eq!(d, vec![1]); }
#[test]
fn test_nonce_v90() { let n1 = nonce_next_v90(); let n2 = nonce_next_v90(); assert!(n2 > n1); }
#[test]
fn test_validate_v90() { assert!(validate_seq_v90(0)); assert!(validate_seq_v90(1)); }
#[test]
fn test_retry_v90() { retry_push_v90(&[1, 2]); assert!(retry_count_v90() > 0); }
// ─── v0.95 Tests ───
#[test]
fn test_lz4_v95() {
let data = vec![0u8; 50];
let compressed = lz4_compress_v95(&data);
let decompressed = lz4_decompress_v95(&compressed);
assert_eq!(decompressed, data);
}
#[test]
fn test_telemetry_v95() { telemetry_emit_v95("fps", 60.0); assert!(telemetry_count_v95() > 0); }
#[test]
fn test_diff_v95() {
let d = diff_frames_v95(&[10, 20], &[10, 25]);
assert_eq!(d, vec![0, 13]); // 20^25 = 13
}
#[test]
fn test_backoff_v95() { let d = backoff_next_v95(); assert!(d >= 100.0); }
#[test]
fn test_mirror_v95() { mirror_push_v95(&[1, 2]); let d = mirror_pop_v95(); assert_eq!(d, vec![1, 2]); }
#[test]
fn test_quota_v95() { assert!(quota_consume_v95(100)); }
#[test]
fn test_heartbeat_v95() { heartbeat_ping_v95(100.0); let e = heartbeat_elapsed_v95(110.0); assert_eq!(e, 10.0); }
#[test]
fn test_tag_v95() { assert!(tag_filter_v95("video")); assert!(!tag_filter_v95("unknown")); }
#[test]
fn test_mavg_v95() { mavg_push_v95(10.0); mavg_push_v95(20.0); assert!(mavg_get_v95() > 0.0); }
// ─── v1.0 Tests ───
#[test]
fn test_pipeline_v100() { pipeline_add_v100("encode"); assert!(pipeline_count_v100() > 0); }
#[test]
fn test_proto_header_v100() { let h = proto_header_v100(1, 0x80, 42); assert_eq!(h.len(), 7); }
#[test]
fn test_splitter_v100() { splitter_push_v100(&[1, 2, 3, 4, 5], 2); let c = splitter_pop_v100(); assert!(!c.is_empty()); }
#[test]
fn test_cwnd_v100() { let w = cwnd_ack_v100(); assert!(w > 0.0); }
#[test]
fn test_stats_v100() { let s = stream_stats_v100(); assert!(s.contains("frames")); }
#[test]
fn test_ack_window_v100() { assert!(ack_window_v100(0)); }
#[test]
fn test_codec_v100() { codec_register_v100("webp"); let l = codec_list_v100(); assert!(l.contains("webp")); }
#[test]
fn test_flow_v100() { assert!(flow_credit_v100(1)); }
#[test]
fn test_version_v100() { let v = version_negotiate_v100("1.0"); assert_eq!(v, "1.0"); }
}
@ -3265,3 +4171,7 @@ mod tests {

View file

@ -1,17 +1,18 @@
# Changelog
## [0.50.0] - 2026-03-11
## [1.0.0] - 2026-03-11 🎉
### Added
- **`StreamCipher`** — XOR encryption with rotating key
- **`ChannelMux`/`ChannelDemux`** — multi-channel mux/demux
- **`FramePacer`** — interval-based frame pacing
- **`CongestionWindow`** — AIMD congestion control
- **`FlowController`** — token-bucket flow control
- **`ProtocolNegotiator`** — capability exchange
- **`ReplayRecorder`** — frame recording for replay
- **`BandwidthShaper`** — enforce max bytes/sec
- 9 new tests (201 total)
- **`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
- 9 new tests (264 total)
## [0.40.0] — QualityAdapter, SourceMixer, FrameDeduplicator, BackpressureController, HeartbeatMonitor, CompressionTracker, FecEncoder, StreamSnapshot, AdaptivePriorityQueue
## [0.30.0] — FrameRingBuffer, PacketLossDetector, ConnectionQuality
## [0.95.0] — Lz4Lite, TelemetrySink, FrameDiffer, BackoffTimer, StreamMirror, QuotaManager, HeartbeatV2, TagFilter, MovingAverage
## [0.90.0] — XorCipherV2, ChannelRouter, AckTracker, FramePoolV2, BandwidthEstimatorV2, PriorityMux, Nonce, Validator, Retry

View file

@ -1,6 +1,6 @@
[package]
name = "ds-stream"
version = "0.50.0"
version = "1.0.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