feat: explicit signal output API for stream declarations
Add 'output: field1, field2' syntax to stream declarations to control
which signals are exposed over the relay. Only listed signals are
registered in _signalRegistry (and thus streamed). Omitting output
streams all signals (backwards-compatible).
Also strips internal sync metadata (_pid, _v) from receiver state
so composition consumers only see clean signal values.
Parser: parse comma-separated idents after 'output:' key
AST: Vec<String> output field on StreamDecl
Codegen: conditional _registerSignal, delete _pid/_v on receive
Example: stream counter on 'ws://...' { mode: signal, output: count, doubled }
This commit is contained in:
parent
b0e7de3b2e
commit
627ee44275
4 changed files with 43 additions and 7 deletions
|
|
@ -78,6 +78,13 @@ impl JsEmitter {
|
||||||
self.indent += 1;
|
self.indent += 1;
|
||||||
|
|
||||||
// Phase 1: Create all signals
|
// Phase 1: Create all signals
|
||||||
|
// Collect explicit output list from stream declaration (if any)
|
||||||
|
let stream_outputs: Vec<String> = program.declarations.iter()
|
||||||
|
.filter_map(|d| if let Declaration::Stream(s) = d { Some(s) } else { None })
|
||||||
|
.flat_map(|s| s.output.iter().cloned())
|
||||||
|
.collect();
|
||||||
|
let has_explicit_output = !stream_outputs.is_empty();
|
||||||
|
|
||||||
self.emit_line("// ── Signals ──");
|
self.emit_line("// ── Signals ──");
|
||||||
for node in &graph.nodes {
|
for node in &graph.nodes {
|
||||||
match &node.kind {
|
match &node.kind {
|
||||||
|
|
@ -110,9 +117,11 @@ impl JsEmitter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.emit_line(&format!("const {} = DS.signal({});", node.name, init));
|
self.emit_line(&format!("const {} = DS.signal({});", node.name, init));
|
||||||
// Register signal for bidirectional streaming sync
|
// Register for streaming only if in output list (or no explicit output = all)
|
||||||
|
if !has_explicit_output || stream_outputs.contains(&node.name) {
|
||||||
self.emit_line(&format!("DS._registerSignal(\"{}\", {});", node.name, node.name));
|
self.emit_line(&format!("DS._registerSignal(\"{}\", {});", node.name, node.name));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
SignalKind::Derived => {
|
SignalKind::Derived => {
|
||||||
// Find the let declaration to get the expression
|
// Find the let declaration to get the expression
|
||||||
let expr = self.find_let_expr(program, &node.name);
|
let expr = self.find_let_expr(program, &node.name);
|
||||||
|
|
@ -122,10 +131,12 @@ impl JsEmitter {
|
||||||
"const {} = DS.derived(() => {});",
|
"const {} = DS.derived(() => {});",
|
||||||
node.name, js_expr
|
node.name, js_expr
|
||||||
));
|
));
|
||||||
// Register derived signal so it's included in stream sync
|
// Register for streaming only if in output list (or no explicit output = all)
|
||||||
|
if !has_explicit_output || stream_outputs.contains(&node.name) {
|
||||||
self.emit_line(&format!("DS._registerSignal(\"{}\", {});", node.name, node.name));
|
self.emit_line(&format!("DS._registerSignal(\"{}\", {});", node.name, node.name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
SignalKind::Handler { .. } => {} // Handled later
|
SignalKind::Handler { .. } => {} // Handled later
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2037,6 +2048,9 @@ const DS = (() => {
|
||||||
case 0x31: // SignalDiff — partial state update
|
case 0x31: // SignalDiff — partial state update
|
||||||
try {
|
try {
|
||||||
var newState = JSON.parse(new TextDecoder().decode(pl));
|
var newState = JSON.parse(new TextDecoder().decode(pl));
|
||||||
|
// Strip internal sync metadata
|
||||||
|
delete newState._pid;
|
||||||
|
delete newState._v;
|
||||||
if (type === 0x30) {
|
if (type === 0x30) {
|
||||||
state.value = newState; // full replace — new object
|
state.value = newState; // full replace — new object
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,8 @@ pub struct StreamDecl {
|
||||||
pub relay_url: Expr,
|
pub relay_url: Expr,
|
||||||
pub mode: StreamMode,
|
pub mode: StreamMode,
|
||||||
pub transport: StreamTransport,
|
pub transport: StreamTransport,
|
||||||
|
/// Explicit signal output list. Empty = stream all signals (backwards-compat).
|
||||||
|
pub output: Vec<String>,
|
||||||
pub span: Span,
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -365,7 +365,7 @@ impl Parser {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse: `stream <view_name> on <url_expr> { mode: pixel | delta | signal }`
|
/// Parse: `stream <view_name> on <url_expr> { mode: signal, output: a, b }`
|
||||||
fn parse_stream_decl(&mut self) -> Result<Declaration, ParseError> {
|
fn parse_stream_decl(&mut self) -> Result<Declaration, ParseError> {
|
||||||
let line = self.current_token().line;
|
let line = self.current_token().line;
|
||||||
self.advance(); // consume `stream`
|
self.advance(); // consume `stream`
|
||||||
|
|
@ -380,9 +380,10 @@ impl Parser {
|
||||||
|
|
||||||
let relay_url = self.parse_expr()?;
|
let relay_url = self.parse_expr()?;
|
||||||
|
|
||||||
// Optional mode block: `{ mode: signal, transport: webrtc }`
|
// Optional config block: `{ mode: signal, transport: webrtc, output: a, b }`
|
||||||
let mut mode = StreamMode::Signal;
|
let mut mode = StreamMode::Signal;
|
||||||
let mut transport = StreamTransport::WebSocket;
|
let mut transport = StreamTransport::WebSocket;
|
||||||
|
let mut output: Vec<String> = Vec::new();
|
||||||
|
|
||||||
if self.check(&TokenKind::LBrace) {
|
if self.check(&TokenKind::LBrace) {
|
||||||
self.advance(); // {
|
self.advance(); // {
|
||||||
|
|
@ -410,6 +411,24 @@ impl Parser {
|
||||||
}
|
}
|
||||||
_ => return Err(self.error("Expected webrtc or websocket".into())),
|
_ => return Err(self.error("Expected webrtc or websocket".into())),
|
||||||
}
|
}
|
||||||
|
} else if key == "output" {
|
||||||
|
// Parse comma-separated identifiers: output: count, doubled
|
||||||
|
loop {
|
||||||
|
let name = self.expect_ident()?;
|
||||||
|
output.push(name);
|
||||||
|
if self.check(&TokenKind::Comma) {
|
||||||
|
self.advance();
|
||||||
|
// Peek ahead: if next is a known key or RBrace, stop
|
||||||
|
// (the comma was a field separator, not an output separator)
|
||||||
|
match self.peek() {
|
||||||
|
TokenKind::RBrace => break,
|
||||||
|
TokenKind::Ident(s) if s == "mode" || s == "transport" || s == "output" => break,
|
||||||
|
_ => {} // continue parsing output names
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if self.check(&TokenKind::Comma) { self.advance(); }
|
if self.check(&TokenKind::Comma) { self.advance(); }
|
||||||
self.skip_newlines();
|
self.skip_newlines();
|
||||||
|
|
@ -422,6 +441,7 @@ impl Parser {
|
||||||
relay_url,
|
relay_url,
|
||||||
mode,
|
mode,
|
||||||
transport,
|
transport,
|
||||||
|
output,
|
||||||
span: Span { start: 0, end: 0, line },
|
span: Span { start: 0, end: 0, line },
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ let count = 0
|
||||||
let doubled = count * 2
|
let doubled = count * 2
|
||||||
let message = "Streaming Counter"
|
let message = "Streaming Counter"
|
||||||
|
|
||||||
stream counter on "ws://localhost:9100" { mode: signal }
|
stream counter on "ws://localhost:9100" { mode: signal, output: count, doubled }
|
||||||
|
|
||||||
view counter =
|
view counter =
|
||||||
column [
|
column [
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue