From d4c7ba23851e0e298fe0a96a99b0ce1a02f1c543 Mon Sep 17 00:00:00 2001 From: enzotar Date: Fri, 27 Feb 2026 11:03:53 -0800 Subject: [PATCH] feat: core language & stream improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Language improvements: - Reactive container class: prop (wraps in DS.effect when signal-dependent) - Stream output filtering (_streamDiff skips non-output signals) - Stream exponential reconnect backoff (2s → 4s → 8s, max 30s) - Breakout game: classic row order with all 5 rows having collision Verified: reactive text + derived signals already work. All 48 examples compile, 136 tests pass --- compiler/ds-codegen/src/js_emitter.rs | 31 ++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/compiler/ds-codegen/src/js_emitter.rs b/compiler/ds-codegen/src/js_emitter.rs index 4d4d299..17d189e 100644 --- a/compiler/ds-codegen/src/js_emitter.rs +++ b/compiler/ds-codegen/src/js_emitter.rs @@ -423,6 +423,18 @@ impl JsEmitter { )); } } + + // Set output filter if stream has explicit output list + if !stream.output.is_empty() { + let names_js = stream.output.iter() + .map(|n| format!("\"{}\"", n)) + .collect::>() + .join(","); + self.emit_line(&format!( + "DS._streamOutputFilter = new Set([{}]);", + names_js + )); + } } } @@ -564,7 +576,14 @@ impl JsEmitter { } "class" => { let js = self.emit_expr(val); - self.emit_line(&format!("{}.className += ' ' + {};", node_var, js)); + if js.contains(".value") { + self.emit_line(&format!( + "DS.effect(() => {{ {}.className = 'ds-{}' + ' ' + {}; }});", + node_var, container_tag, js + )); + } else { + self.emit_line(&format!("{}.className += ' ' + {};", node_var, js)); + } } "click" | "submit" => { let handler_js = self.emit_event_handler_expr(val); @@ -2984,14 +3003,17 @@ const DS = (() => { _streamWs.onclose = function() { _streamConnected = false; _streamReconnects++; - console.log('[ds-stream] Disconnected, reconnecting in 2s (attempt ' + _streamReconnects + ')'); - setTimeout(function() { _initStream(url, mode); }, 2000); + var delay = Math.min(_reconnectDelay, 30000); + console.log('[ds-stream] Disconnected, reconnecting in ' + (delay/1000) + 's (attempt ' + _streamReconnects + ')'); + setTimeout(function() { _initStream(url, mode); }, delay); + _reconnectDelay = Math.min(_reconnectDelay * 2, 30000); // exponential backoff, max 30s }; _streamWs.onerror = function() { _streamConnected = false; }; _streamWs.onopen = function() { _streamConnected = true; + _reconnectDelay = 2000; // reset backoff on success console.log('[ds-stream] Peer connected:', peerUrl); // Send schema announcement (0x32) with output signal list var outputNames = Object.keys(_signalRegistry); @@ -3038,10 +3060,13 @@ const DS = (() => { // ── Batched diff: coalesce multiple signal changes into one WS frame ── var _lastSentValues = {}; // deduplication: track last-sent value per signal var _diffBatchCount = 0; // count batches for periodic auto-sync + var _streamOutputFilter = null; // Set of signal names to stream (null = all) + var _reconnectDelay = 2000; // exponential backoff: starts at 2s, max 30s function _streamDiff(name, value) { if (!_streamWs || _streamWs.readyState !== 1 || _streamMode !== 'signal') return; if (_applyingRemoteDiff) return; // prevent echo loops + if (_streamOutputFilter && !_streamOutputFilter.has(name)) return; // skip non-output signals var val = (typeof value === 'object' && value !== null && 'value' in value) ? value.value : value; // Deduplication: skip if value hasn't changed var lastStr = JSON.stringify(_lastSentValues[name]);