Relay HTTP /meta endpoint:
- GET /meta → JSON with all channel stats
- GET /meta/{name} → JSON with specific channel stats, schema, current state
- Uses TCP peek to intercept raw HTTP before WS handshake
- CORS headers for browser access
Signal deduplication:
- _lastSentValues tracks last-sent value per signal
- JSON.stringify comparison skips unchanged values
- Prevents redundant WS frames from derived signals
Periodic auto-sync:
- Every 50 diff batches, source sends full SignalSync (0x30)
- Relay can compact its cache instead of accumulating infinite diffs
- Resets pending_signal_diffs in relay cache
All 57 relay tests pass. All 9 examples pass.
- New streaming-dashboard.ds: first example combining components + streaming
- 4 Card grid with Badge, Button, match, stream receivers
- Receives live data from counter, clock, stats streams
- Button callbacks (Refresh/Reset) work within stream context
- All 8 examples pass regression (47,799 bytes)
- Duplicate prop keys now merge into Block expressions
click: a then click: b → Block([a, b])
- All actions fire in one click (was silently overwriting)
- streaming-mood.ds upgraded to semicolons
- streaming-stats.ds upgraded to semicolons
- Browser-verified: Happy button fires 3 actions (mood+color+energy)
- All 7 examples pass regression
- Added can_be_pattern() with look-ahead disambiguation
- Ident only treated as pattern if followed by -> or (
- Keywords (row/column/when/each) correctly terminate match
- Match arms now support: "active" -> row [ Badge + text ]
- Siblings after match (text, button, row) no longer consumed
- Project Manager updated with rich row/Badge match arms
- All 6 existing examples pass regression
- Dashboard with metrics, match status, progress bars
- Task Manager with dynamic add/remove via push/remove
- Team directory with member cards and status badges
- Settings with toggle and about section
- 64,622 bytes, zero console errors, all 11 components used
- Home/Counter/Todos/About with hash navigation
- State persists across route changes
- Uses existing router infrastructure (no compiler changes)
- navigate keyword, matchRoute, _route signal
- Imports Card, Badge, Button from components/
- Counter with callback props, Greeting with when/else
- Mood match expression with Badge, Todo dynamic lists
- Full init → build flow verified (51,401 bytes, 0 errors)
- When/else inside slots: anchor parentNode is null during initial effect
Fixed with named effect function + requestAnimationFrame retry
- Match parser now terminates on ] } else tokens (works inside containers)
- Updated Progress/Badge components
- Added examples/showcase.ds: 5-section demo exercising all features
- Recursive file watching: catches imported file changes from registry/
- Auto-detect project root: walks up to find registry/ or examples/ dir
- Auto-open browser on startup (xdg-open/open/cmd)
- Verified HMR live reload: 0ms recompile on file save
- AST: When(cond, body) -> When(cond, body, Option<else_body>)
- Parser: optional 'else -> expr' after when body
- Codegen: reactive DOM swap with anchor comments
- Signal graph + type checker updated for 3-arg When
- Component prop signal wrapping: .value compatible accessors
- Added examples/when-else-demo.ds
Codegen: BinOp::Div now emits Math.trunc(l / r)
- Clock displays 0:1:30 instead of 0.016:1.5:30
- Affects both emit_expr and predicate_to_js
Lexer: removed duplicate 'in' keyword mapping
- InKw at line 312 is canonical, removed old In at line 338
Examples: added project-tracker.ds (each loops + cards)
Add compose-metrics.ds (Layer 1): receives counter+clock+stats,
derives uptime/events/status, re-streams on /peer/metrics. This app
is BOTH a receiver and a source.
Add compose-master.ds (Layer 2): receives chained metrics from
Layer 1 + mood direct from Layer 0. Demonstrates multi-layer
signal composition with independent stream mixing.
Verified: Uptime: 51s, Total Events: 9 flowing through the full
three-layer chain to the master dashboard.
The 'default' relay channel accumulated stale Source connections from
previous sessions, causing frame delivery issues to Receivers on
/stream/default. Moving counter to an explicit /peer/counter channel
(matching clock, stats, mood pattern) fixes the composition dashboard.
All 4 streams now show live data: Count: 3, Doubled: 6.
Add 3 new streaming apps with explicit output declarations:
- streaming-clock.ds: output hours, minutes, seconds (tick private)
- streaming-stats.ds: output total, average, max (sum private)
- streaming-mood.ds: output mood, energy, color (clicks private)
Add compose-dashboard.ds that receives all 4 streams via unique
relay channels (/stream/default, /stream/clock, /stream/stats,
/stream/mood) into a single dashboard view.
Each app demonstrates selective signal registration — only declared
outputs are streamed, internal state remains private.
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 }
- Register derived signals in _signalRegistry so _streamSync includes them
- Auto-sync all signals (source + derived) after flush() recomputes effects
- Fix Object.assign identity check: create new object so signal setter detects changes
- Change _connectStream receiver path from /signal/main to /stream/default
- Initialize stream state with {} instead of null to prevent crashes
- Emit StreamFrom bindings directly without double-wrapping in signal()
Verified: static build shows Count: 9, Doubled: 18 on composition page.
HMR interference with WebSocket connections is a separate issue.
- _signalVersions: monotonic counter per signal, incremented on each local mutation
- Diffs include _v: {name: version} for version comparison
- _applyRemoteDiff only applies if remote version >= local version
- Prevents stale overwrites when both devices edit simultaneously
- onopen snapshot includes version map for late-joiner consistency
- bind: input handler now emits _streamDiff so typed text syncs
- Peers broadcast full state snapshot on WS open (late joiners sync)
- Removed dead _handleRemoteInput 0x31 case (old source/receiver model)
- Preserved scroll (0x50), pointer, and key handlers
- Added /peer/{name} route to relay: all clients are equal peers
- handle_peer: binary broadcast to all other peers, catchup for late joiners
- Simplified runtime: single /peer/ WS replaces dual source+receiver
- _peerId: random 8-char ID prevents self-echo from broadcast
- _pid in each diff JSON, filtered in _applyRemoteDiff
- Fix URL routing: /source/default and /stream/default paths
- _streamDiff sends via both source WS and receiver WS (INPUT flag)
- Source rebroadcasts 0x31 diffs from receivers to all other receivers
- Echo loop guard prevents infinite rebroadcast
Array access: Expr::Index in AST, [expr] postfix parsing, codegen for
reads (grid.value[i.value]) and writes (event handler assignments with
root signal extraction for stream diff broadcasting).
Timer: 'every N -> expr' declaration. Every keyword in lexer. EveryDecl
in AST. parse_every_decl in parser. setInterval codegen with DS.flush.
String interpolation: already committed separately.
Type checker: handles Expr::Index (infers array element type).
110 tests, 0 failures.
- streaming-counter.ds: reactive counter with 'stream counter on ws://...'
- streaming-receiver.ds: remote receiver with 'stream from ws://...'
- E2E verified: compiles to streaming JS, connects to relay, sends signal diffs