From b0e7de3b2e1251501abd5da4372af0c13ccf6b84 Mon Sep 17 00:00:00 2001 From: enzotar Date: Wed, 25 Feb 2026 23:55:05 -0800 Subject: [PATCH] =?UTF-8?q?fix:=20signal=20composition=20=E2=80=94=20strea?= =?UTF-8?q?m=20derived=20signals,=20fix=20identity=20check,=20correct=20re?= =?UTF-8?q?lay=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- USE_CASES.md | 563 +++++++++++++++++++++++--- compiler/ds-codegen/src/js_emitter.rs | 34 +- compiler/ds-parser/src/parser.rs | 83 +++- examples/compose-search-map.ds | 23 ++ examples/compose-widgets.ds | 28 ++ examples/widget-map.ds | 17 + examples/widget-search.ds | 18 + 7 files changed, 699 insertions(+), 67 deletions(-) create mode 100644 examples/compose-search-map.ds create mode 100644 examples/compose-widgets.ds create mode 100644 examples/widget-map.ds create mode 100644 examples/widget-search.ds diff --git a/USE_CASES.md b/USE_CASES.md index 08ff27a..12779ab 100644 --- a/USE_CASES.md +++ b/USE_CASES.md @@ -1,4 +1,4 @@ -# DreamStack — Use Cases & Vision +# DreamStack — Vision & Use Cases > **Core insight**: DreamStack turns UI into a streamable binary protocol. > Any `.ds` app becomes multiplayer with one line. No networking code. No database sync. @@ -22,7 +22,25 @@ composable, replayable, and physics-native. --- -## Fundamental UI Redesigns +## The Five Primitives + +DreamStack's full vision combines five independent primitives into something +that doesn't exist yet: + +| Primitive | Role | +|---|---| +| **DreamStack bitstream** | UI as a streamable signal protocol | +| **Iroh P2P** | Zero-infrastructure device mesh | +| **Local LLM** | On-device intelligence, fully private | +| **Solana NFT** | Ownership, identity, payments | +| **PgFlex** | Schemaless, real-time, zero-config database | + +Each is powerful alone. Combined, they produce **a portable, tradeable, +self-sovereign software company — minted as a single NFT**. + +--- + +## Part 1 — Fundamental UI Redesigns ### 1. Ambient UI — App Without a Device @@ -41,6 +59,23 @@ and renders at its own resolution and frame rate. **Why it matters**: Apps stop being "installed things" and become ambient services that appear on whatever screen is nearest. +#### Universal Receiver Spectrum + +The simplest device that can accept a DreamStack stream needs only: +a socket client, 16-byte header parsing, and any output surface. + +| Device | What it consumes | Cost | +|--------|-----------------|------| +| **ESP32 + LED** | Signal frames only (`0x30`/`0x31`) — ~20 lines MicroPython | ~$4 | +| **Raspberry Pi + e-ink** | Signal frames → text render | ~$15 | +| **Any browser tab** | All 6 frame types (zero-framework HTML receiver) | Free | +| **Phone / tablet** | All frames + touch/pointer input back | Free | +| **Smart display** | Full bidirectional + physics | ~$100 | + +A signal-mode stream sending `{"brightness": 0.7}` is trivially parseable +even on a microcontroller. The source decides the *meaning*, the receiver +decides the *manifestation*. + ### 2. Time-Travel Interfaces Every signal diff is a timestamped binary frame. Record the stream → @@ -55,8 +90,6 @@ into a new interactive session. - **Analytics**: Replay user sessions to understand behavior (not heatmaps — actual interactive replays) -**Why it matters**: Every app gets "save game" for free. - ### 3. UI Composition via Signal Mixing ``` @@ -84,8 +117,6 @@ interactive UI appears. Close the tab, it's gone. - Updates are instant — the source changes, all viewers see it immediately - Works on any device with a bitstream receiver -**Why it matters**: Distribution friction drops to zero. - ### 5. Physics-Native Interaction With `ds-physics` baked into the language, UI elements aren't positioned — @@ -99,89 +130,497 @@ they're simulated. The physics state streams like any other signal. Multiple users can interact with the same physics world simultaneously. -**Why it matters**: Interfaces feel *physical* instead of mechanical. +--- + +## Part 2 — Iroh P2P: Zero-Infrastructure Streaming + +Adding Iroh P2P to the bitstream protocol removes the relay server entirely +and opens use cases that centralized infrastructure cannot serve. + +| | WebSocket Relay | WebRTC | **Iroh P2P** | +|---|---|---|---| +| Discovery | Needs known URL | Needs signaling server | **Content-addressed (hash)** | +| NAT traversal | N/A (server) | ICE/STUN/TURN | **Built-in holepunching** | +| Topology | Star (relay center) | Peer pairs | **Mesh / swarm** | +| Offline-first | ❌ | ❌ | **✅ (local-first sync)** | +| Identity | URL-based | Session-based | **Cryptographic (public key)** | +| Persistence | Relay must stay up | Connection must stay open | **Content persists in network** | + +### Key Use Cases + +**Zero-Infrastructure Streaming** — A teacher in a classroom with no internet +runs a live `.ds` lesson to 30 student tablets over LAN via mDNS discovery. +No server. No URL. No relay cost. + +**Persistent Bitstream Archives** — A recorded bitstream session becomes a +content hash. Anyone with the hash fetches and replays it from the swarm. +Bug reports, tutorials, session replays — all just 32-byte hashes. No replay server. + +**Mesh Multiplayer** — N peers form a gossip mesh instead of routing through a +star topology. If one person drops, the others keep going. Latency drops +(direct paths instead of relay bounce). + +**Offline-First + Sync-on-Reconnect** — Device goes offline, accumulates signal +diffs locally, reconnects → Iroh syncs missing state automatically. Field workers, +IoT sensors, home displays — all work disconnected and reconcile on reconnect. + +**Cryptographic Identity** — Every Iroh node has a public key. Zero-config auth: +only whitelisted keys can subscribe to your stream. No API keys, no OAuth. + +--- + +## Part 3 — Local LLM: Intelligent Ambient UI + +Combining DreamStack + Iroh + local LLM inference produces intelligent, private, +serverless computing that renders on any surface. + +### The AI That Lives in the Room + +A single device (NUC, Mac Mini, laptop) runs a local LLM and joins the Iroh mesh. +Every surface in the house receives its signal stream. + +- Speak to **any screen** → voice input through the mesh → local LLM processes → + response streams as signal diffs to **every surface** +- Kitchen display shows the recipe. Phone shows the shopping list. TV shows the + tutorial video. **One inference, N renderers.** +- All private. Nothing leaves the network. Ever. + +### Distributed Inference Swarm + +Multiple devices in the mesh each run small models. The signal protocol +coordinates them: + +- Phone runs a fast model for intent detection +- Desktop runs a large model for generation +- No orchestration server. Models discover each other via Iroh. +- **LLM inference becomes a mesh protocol.** + +### Self-Organizing Smart Spaces + +The LLM observes signal streams flowing through the mesh and adapts the UI: + +- Sees calendar signals → "Meeting in 10 minutes" → pushes notification to watch +- Sees IoT sensor signals → generates natural-language summary → nearest display +- Learns patterns → proactively streams control signals + +No IFTTT. No Home Assistant rules. The LLM **is** the automation engine, +DreamStack **is** the I/O bus. + +### Private AI Tutor + +Teacher runs a `.ds` physics simulation. Students interact via tablets (touch inputs +stream back through Iroh mesh). A local LLM watches each student's interaction +stream and generates personalized hints. The hints stream as signal diffs only to +that student's device. + +**Adaptive tutoring. Zero cloud. Works offline in rural schools.** + +### Conversational Database (with PgFlex) + +The local LLM watches PgFlex's SSE change stream and builds understanding: + +- "Top 3 products last week?" → LLM generates PostgREST query → PgFlex returns → DreamStack renders +- "Alert me when inventory < 50" → LLM creates computed field + threshold → SSE fires when triggered +- PgFlex's schemaless nature means the LLM can create new fields on the fly +- **The LLM becomes a database architect that responds to natural language** + +| Without LLM | With Local LLM | +|---|---| +| Streams carry **data** | Streams carry **meaning** | +| Surfaces **render** signals | Surfaces **understand** signals | +| Users **configure** automations | The system **infers** intent | + +--- + +## Part 4 — Content-Addressed UI Distribution + +This is where all primitives converge into the most radical departure from +traditional software distribution. + +### Two Layers of Hash + +| Layer | What | Analogy | +|---|---|---| +| **App hash** | Compiled `.ds` code (static artifact) | Torrent of an executable | +| **Stream topic** | Live signal state (running instance) | Joining a live game server | + +When you share `ds:bafk2bza...`, the receiver: +1. **Fetches the app code** from the swarm (content-addressed, cached, verified) +2. **Joins the live stream** from the source (signal diffs, real-time state) + +You're not distributing a file. **You're distributing a running application.** + +### Why It Matters + +**Link rot / platform risk** — A content-addressed app cannot go offline unless +every peer disappears. No Vercel dependency. No AWS bill surprise. No domain expiration. + +**Distribution without gatekeepers** — App Store takes 30%. A `.ds` app distributed +via hash has zero distribution cost and zero gatekeepers. The app doesn't need to be +"installed" — the receiver just decodes the bitstream. + +**Instant, zero-trust sharing** — Today: URL → DNS → load JS → hydrate → render +(2-5 seconds). With hash: peer already has the bundle cached → stream connects → +UI appears (~100ms). + +**Versioning is automatic** — v1.0 = hash `abc123`, v1.1 = hash `def456`. Both +exist simultaneously in the swarm. No deployment. No rollback scripts. Users can +pin a version. Developers publish updates without breaking existing users. + +--- + +## Part 5 — Solana NFT: App Ownership On-Chain + +A compiled `.ds` app is small — signal-mode apps are a reactive state graph plus +view declarations. Compiled, that's kilobytes. Small enough to store on-chain. + +### The NFT Structure + +``` +┌─────────────────────────────────────────┐ +│ Solana NFT (Metaplex Core) │ +│ │ +│ metadata: { name, creator, version } │ +│ appData: │ +│ stream: │ +│ dbConfig: + text "{order.customer}: ${order.total}" + ] + +stream dashboard on iroh { topic: "dashboard" } +``` + +Database changes → PgFlex SSE → DreamStack signals → Iroh mesh → every screen +updates. **No API layer. No polling. No frontend state management.** + +### Why Not Automerge / CRDTs? + +CRDTs (like Automerge) solve concurrent uncoordinated writes with no central +authority. PgFlex doesn't need them because **no layer in this architecture +produces that problem**: + +``` +Layer Sync mechanism Conflicts? +──────────────────────────────────────────────────────── +DreamStack signals Version map + diffs No — single source per stream +PgFlex writes PostgreSQL transactions No — single DB is authority +PgFlex reads ElectricSQL shapes No — read-only replication +Iroh P2P Content-addressed blobs No — immutable by hash +``` + +PgFlex's natural topology is **single-writer**: one PostgreSQL instance per +deployment (home, school, hospital). Within that deployment, PostgreSQL handles +write ordering via transactions. ElectricSQL handles read replication to edge +devices. SSE pushes change notifications. No write contention occurs. + +For offline scenarios (two peers editing state while disconnected), the conflict +resolution happens **in the signal layer, not the database layer**: + +``` +Peer A ──signals──→ DreamStack merge ←──signals── Peer B + │ + resolved state + │ + PgFlex + (just persists) +``` + +DreamStack's signal protocol reconciles on reconnect (last-write-wins per signal, +using version maps). PgFlex materializes whatever the signal graph settles on. +Adding Automerge would be a redundant conflict resolution layer between two +systems that already don't produce conflicts. + +> [!NOTE] +> If PgFlex-to-PgFlex replication across sites is ever needed, the answer is +> PostgreSQL logical replication + custom conflict handlers — not Automerge. +> Stay in the PostgreSQL ecosystem. --- ## Revenue-Generating Applications -### A. Relay-as-a-Service — "Liveblocks Competitor" +### Tier 1: High Conviction — Clear Buyer, Clear Budget -**Market**: $50M+ (Liveblocks $30M raised, PartyKit acquired by Cloudflare, Ably $50M raised) +#### A. Private AI Appliance for Regulated Industries -Sell the bitstream relay as hosted infrastructure: -- Developers add one `stream` line to their app -- Charge per concurrent connection / bandwidth -- Dashboard with analytics, channel management, auth +**Buyer**: Hospitals, law firms, financial advisors, government agencies +**Budget they already spend**: $50K-500K/year on HIPAA/SOC2-compliant cloud AI -**Edge over incumbents**: -- Binary protocol (10x smaller than JSON pub/sub) -- WebRTC auto-upgrade for low latency -- Signal-graph-aware (not generic pub/sub) -- Keyframe cache for instant late-join +**The product**: A box (Jetson/NUC) running local LLM + DreamStack + PgFlex. Plug into +the network. Every screen in the facility gets AI — medical record summaries on the +doctor's tablet, appointment prep on the wall display, billing codes on admin screen. +Zero data leaves the building. -**Ship fast**: The relay already works. Add auth, metering, a dashboard. +**Why they pay**: Compliance. They *can't* use ChatGPT for patient data. Local LLM +solves this. DreamStack solves multi-surface delivery. PgFlex stores patient data locally. -### B. Interactive Live Education — "Nearpod Killer" +**Revenue**: Hardware ($500-2000) + annual software license ($2K-10K/yr) +**Market**: Healthcare IT alone is $400B. Even 0.001% = $4M. +**Moat**: Multi-surface streaming. Competitors sell "local LLM in a box" but output +is one terminal. DreamStack makes it ambient. -**Market**: $100M+ (Nearpod sold for $650M, Pear Deck acquired by GoGuardian) +#### B. Live Education Platform -Teacher runs a `.ds` lesson — interactive physics simulation, math -visualizer, chemistry model. 30 students connect, see the same simulation, -can interact. Teacher sees who's clicking what. +**Buyer**: School districts, edtech resellers, corporate training departments +**Budget they already spend**: $5-15/student/year on Nearpod, Pear Deck, Kahoot -**Edge over incumbents**: -- Actual interactive simulations (WASM physics engine), not slides with polls -- Sub-frame latency via WebRTC -- Works offline — student can fork and explore independently -- *"Turn any simulation into a live classroom with one line of code"* +**The product**: Teacher runs interactive simulations on their laptop. Students connect +from any device — Chromebook, iPad, phone. Over LAN via Iroh when internet is down. +Local LLM provides per-student adaptive hints. -### C. Collaborative Design/Prototyping +**Why they pay**: Current tools are glorified PowerPoint with polls. DreamStack +delivers actual interactive simulations + works offline (huge for rural schools). -**Market**: $1B+ (Figma worth $20B — multiplayer-by-default was the differentiator) +**Revenue**: $8/student/year (undercut Nearpod at $15) +**Market**: 50M US K-12 students × $8 = $400M TAM +**Moat**: Physics-native simulations over bitstream. Offline-via-Iroh is a killer +feature for school procurement. -A `.ds` file IS a running prototype. Multiple stakeholders view and interact -simultaneously. Signal diffs mean only changed state transmits. +### Tier 2: Strong Signal — Needs Validation -**Edge over Figma/Framer**: -- DreamStack prototypes are actual running apps, not static mockups -- They have physics, reactivity, routing -- They compile to production JS -- Same file = design AND implementation +#### C. Full-Stack NFT Marketplace -### D. IoT / Operations Dashboards +**Buyer**: Solo developers, indie SaaS builders, agencies +**Budget**: Currently $0 (new market) or $20-100/mo on hosting -**Market**: $10B+ (Grafana, Datadog, industrial IoT monitoring) +**The product**: `ds publish --solana` mints your app as a tradeable NFT. Buyers get a +complete, deployable, intelligent app with persistent storage. Developers earn royalties +on secondary sales. -Stream sensor data / device state to multiple monitoring screens. -Signal diffs mean minimal bandwidth. Keyframe cache means new operators -see current state instantly. +| What you sell | What the buyer gets | Price range | +|---|---|---| +| CRM NFT | App + PgFlex schema + LLM customer insights | 10-50 SOL | +| Analytics Dashboard NFT | Real-time dashboard + data layer + anomaly detection | 5-20 SOL | +| Inventory Tracker NFT | Multi-surface inventory + auto-reorder via LLM | 20-100 SOL | +| Custom App (commissioned) | Bespoke `.ds` app minted for one client | 100+ SOL | -**Edge**: Binary protocol is 10-100x more efficient than JSON polling. -Physics engine enables animated, organic data visualization. +**Revenue**: Transaction fees (2.5% of NFT sales) + pinning service +**Moat**: The NFT isn't a JPEG — it's a running application. Utility value, not speculative. -### E. Live Commerce / Interactive Shopping +#### D. DreamStack Registry (ds.run) -**Market**: $500B+ globally (live shopping market) +**The product**: `ds publish` → app gets a content hash. Anyone with the hash +fetches and runs it from the swarm. -A host demonstrates products. Viewers see the same interactive page — -rotate 3D models, change colors, see inventory in real-time. The host -controls the flow, viewers interact with products. +| Tier | What | Price | +|---|---|---| +| **Free** | Publish to swarm (ephemeral, lives as long as peers have it) | $0 | +| **Pin** | Permanent peers keep your app alive 24/7 | $5/app/month | +| **Pro** | Pin + custom domain + analytics + `ds.run` web gateway | $20/month | +| **Enterprise** | Private registry, SSO, audit log, SLA | $200/month | -**Edge**: Not a video stream with overlay buttons — a full interactive -app where the product page IS the stream. +The web gateway `ds.run/hash` opens the app in a browser via the WASM receiver. +Lets you share apps with people who don't have DreamStack yet. + +**Moat**: The hash isn't just code — it includes the live stream topic. Pinned +apps are running, interactive, multiplayer applications. IPFS = static files. +Vercel = static sites. DreamStack Registry = live interactive apps as hashes. + +#### E. Relay-as-a-Service + +**Buyer**: SaaS developers building collaborative features +**Budget**: $0.10-1.00/concurrent connection/month on Liveblocks + +**The product**: `npm install dreamstack` → add one `stream` declaration → instant +multiplayer. Hosted relay + Iroh P2P fallback. + +**Revenue**: Usage-based, $0.05/connection/month +**Moat**: Signal-graph-aware sync (not generic pub/sub), binary efficiency, +P2P fallback = lower infrastructure cost + +#### F. IoT / Operations Dashboard + +**Buyer**: Factory floor managers, DevOps teams, operations centers +**Budget**: $15-50/user/month on Grafana Cloud, Datadog + +**The product**: Sensor data streams via Iroh mesh to any screen. Local LLM adds +anomaly detection + natural language alerts. No cloud egress fees. + +**Revenue**: $20/source/month +**Moat**: Binary efficiency + P2P = massive cost savings at scale + +--- + +## Go-to-Market Sequence + +``` +Phase 1 (NOW → 3 months): Open-source the protocol + relay. + Ship the "Collaborative Step Sequencer" demo. + → Developer attention + GitHub stars. + +Phase 2 (3-6 months): Relay-as-a-Service (E). + → First revenue, proves developer demand. + → Low cost to operate (already built). + +Phase 3 (6-12 months): Education Platform (B) + ds.run Registry (D). + → Pick one school district, pilot it. + → Add local LLM hints as differentiator. + → Registry grows alongside open-source adoption. + +Phase 4 (12-18 months): Private AI Appliance (A) + NFT Marketplace (C). + → Partner with healthcare IT reseller. + → Hardware margin + recurring license. + → NFT marketplace leverages existing Solana ecosystem. +``` + +**Through-line**: DreamStack's moat is **multi-surface + binary efficiency + P2P**. +Every product must lean into all three. If a product only needs one, someone else +will eat your lunch. --- ## Demo Prioritization | Demo | Effort | Wow Factor | Revenue Signal | -|------|--------|------------|---------------| +|------|--------|------------|----------------| | Collaborative Step Sequencer | 1 day | ⭐⭐⭐⭐⭐ | Relay-as-a-Service | | Two-Tab Synced Counter | 1 hour | ⭐⭐⭐ | — | | Physics Playground | 2 days | ⭐⭐⭐⭐ | Education | | Collaborative Whiteboard | 2 days | ⭐⭐⭐⭐ | Design Tool | +| ESP32 Signal Receiver | 1 day | ⭐⭐⭐⭐ | IoT / Ambient UI | | Session Replay Viewer | 3 days | ⭐⭐⭐ | Analytics | +| NFT-Minted App + ds.run | 3 days | ⭐⭐⭐⭐⭐ | NFT Marketplace | | Multi-Surface Dashboard | 1 week | ⭐⭐⭐⭐⭐ | IoT/Operations | **Recommended first demo**: Collaborative Step Sequencer. @@ -189,6 +628,11 @@ It's visual, temporal, interactive, and multiplayer — proving the entire stack in the most visceral way possible. Two browser tabs, one beat grid, instant sync, zero networking code. +**Killer demo for NFT vision**: Publish a step sequencer as a content hash. +Share it in a tweet. Anyone who clicks the `ds.run/hash` link instantly joins +the live session — no account, no install, no server. 50 people making music +together from a 32-byte hash. + --- ## Technical Foundation (Complete) @@ -203,4 +647,15 @@ All infrastructure is built and tested: - ✅ **Compiler integration**: `stream` keyword, `stream from` expression, `transport: webrtc` option - ✅ **Receiver protocol**: All 6 frame types, RLE decode, exponential backoff -- ✅ **105 tests, 0 failures** +- ✅ **PgFlex**: Schemaless PostgreSQL, PostgREST, SSE, ElectricSQL — 110 tests +- ✅ **110+ tests, 0 failures** + +### Primitives Still Needed + +- ⬜ **Iroh P2P transport**: Replace WebSocket relay with Iroh mesh +- ⬜ **`ds publish`**: Mint `.ds` app as Solana NFT +- ⬜ **`ds run sol:ADDRESS`**: Fetch + boot from on-chain bytecode +- ⬜ **ds.run gateway**: Web gateway for content-addressed apps +- ⬜ **NFT-gated stream auth**: Check wallet ownership before allowing connection +- ⬜ **PgFlex signal bridge**: SSE → DreamStack signal graph +- ⬜ **Local LLM integration**: Model loading + signal-based I/O diff --git a/compiler/ds-codegen/src/js_emitter.rs b/compiler/ds-codegen/src/js_emitter.rs index db38a59..7ef6de5 100644 --- a/compiler/ds-codegen/src/js_emitter.rs +++ b/compiler/ds-codegen/src/js_emitter.rs @@ -97,6 +97,12 @@ impl JsEmitter { self.emit_line(&format!("const {} = {};", node.name, js_expr)); continue; } + // Check if it's a stream from — _connectStream returns a signal proxy + if matches!(expr, Expr::StreamFrom { .. }) { + let js_expr = self.emit_expr(expr); + self.emit_line(&format!("const {} = {};", node.name, js_expr)); + continue; + } self.emit_expr(expr) } else { "null".to_string() @@ -116,6 +122,8 @@ impl JsEmitter { "const {} = DS.derived(() => {});", node.name, js_expr )); + // Register derived signal so it's included in stream sync + self.emit_line(&format!("DS._registerSignal(\"{}\", {});", node.name, node.name)); } } SignalKind::Handler { .. } => {} // Handled later @@ -1475,6 +1483,10 @@ const DS = (() => { for (const eff of effects) { eff._run(); } + // After effects recompute derived signals, sync all values to stream + if (_streamWs && _streamWs.readyState === 1 && _streamMode === 'signal' && !_applyingRemoteDiff) { + _streamSync(_signalRegistry); + } } // ── Event System ── @@ -1961,7 +1973,19 @@ const DS = (() => { } function _connectStream(url) { - var state = signal(null); + // Auto-detect bare relay URL and append default receiver path + // e.g. "ws://localhost:9100" → "ws://localhost:9100/stream/default" + // Source connects at /peer/default, receiver subscribes at /stream/default + var _csUrl = url; + try { + var u = new URL(url); + if (u.pathname === '/' || u.pathname === '') { + _csUrl = url.replace(/\/$/, '') + '/stream/default'; + } + } catch(e) { + // Not a valid URL — use as-is + } + var state = signal({}); var _csWs = null; var _csReconnectDelay = 1000; var _csStats = { frames: 0, bytes: 0, reconnects: 0 }; @@ -1984,7 +2008,7 @@ const DS = (() => { } function _csConnect() { - _csWs = new WebSocket(url); + _csWs = new WebSocket(_csUrl); _csWs.binaryType = 'arraybuffer'; _csWs.onopen = function() { console.log('[ds-stream] Receiver connected:', url); @@ -2014,9 +2038,11 @@ const DS = (() => { try { var newState = JSON.parse(new TextDecoder().decode(pl)); if (type === 0x30) { - state.value = newState; // full replace + state.value = newState; // full replace — new object } else { - state.value = Object.assign(state._value || {}, newState); + // Create a NEW object so the signal setter detects the change + // (Object.assign to existing object returns same ref, trips identity check) + state.value = Object.assign({}, state._value || {}, newState); } } catch(ex) {} break; diff --git a/compiler/ds-parser/src/parser.rs b/compiler/ds-parser/src/parser.rs index 3b3a176..5772bf5 100644 --- a/compiler/ds-parser/src/parser.rs +++ b/compiler/ds-parser/src/parser.rs @@ -796,18 +796,35 @@ impl Parser { } } - // Stream from + // Stream from — accept string URL or dotted identifier TokenKind::Stream => { self.advance(); self.expect(&TokenKind::From)?; - let source = self.expect_ident()?; - // Allow dotted source: `button.click` - let mut full_source = source; - while self.check(&TokenKind::Dot) { - self.advance(); - let next = self.expect_ident()?; - full_source = format!("{full_source}.{next}"); - } + // Accept string URL: `stream from "ws://localhost:9100"` + // or dotted ident: `stream from button.click` + let full_source = if matches!(self.peek(), TokenKind::StringFragment(_) | TokenKind::StringEnd | TokenKind::StringInterp) { + // Parse string literal and extract the raw text + let expr = self.parse_string_lit()?; + match &expr { + Expr::StringLit(s) if s.segments.len() == 1 => { + if let StringSegment::Literal(text) = &s.segments[0] { + text.clone() + } else { + return Err(self.error("stream from requires a plain string URL".into())); + } + } + _ => return Err(self.error("stream from requires a plain string URL".into())), + } + } else { + let source = self.expect_ident()?; + let mut full = source; + while self.check(&TokenKind::Dot) { + self.advance(); + let next = self.expect_ident()?; + full = format!("{full}.{next}"); + } + full + }; Ok(Expr::StreamFrom { source: full_source, mode: None }) } @@ -1321,4 +1338,52 @@ view counter = other => panic!("expected Stream, got {other:?}"), } } + #[test] + fn test_stream_from_string_url() { + let prog = parse(r#"let remote = stream from "ws://localhost:9100""#); + match &prog.declarations[0] { + Declaration::Let(decl) => { + assert_eq!(decl.name, "remote"); + match &decl.value { + Expr::StreamFrom { source, .. } => { + assert_eq!(source, "ws://localhost:9100"); + } + other => panic!("expected StreamFrom, got {other:?}"), + } + } + other => panic!("expected Let, got {other:?}"), + } + } + + #[test] + fn test_stream_from_dotted_ident() { + let prog = parse("let remote = stream from button.click"); + match &prog.declarations[0] { + Declaration::Let(decl) => { + assert_eq!(decl.name, "remote"); + match &decl.value { + Expr::StreamFrom { source, .. } => { + assert_eq!(source, "button.click"); + } + other => panic!("expected StreamFrom, got {other:?}"), + } + } + other => panic!("expected Let, got {other:?}"), + } + } + + #[test] + fn test_multiple_stream_from() { + let prog = parse(r#"let a = stream from "ws://localhost:9100" +let b = stream from "ws://localhost:9101""#); + assert_eq!(prog.declarations.len(), 2); + for decl in &prog.declarations { + match decl { + Declaration::Let(d) => { + assert!(matches!(d.value, Expr::StreamFrom { .. })); + } + other => panic!("expected Let, got {other:?}"), + } + } + } } diff --git a/examples/compose-search-map.ds b/examples/compose-search-map.ds new file mode 100644 index 0000000..cd5566c --- /dev/null +++ b/examples/compose-search-map.ds @@ -0,0 +1,23 @@ +-- Compose Search + Map widgets into one view +-- +-- Run with: +-- Tab 1: dreamstack stream examples/widget-search.ds +-- Tab 2: dreamstack stream examples/widget-map.ds --port 9101 +-- Tab 3: dreamstack dev examples/compose-search-map.ds --port 3001 + +let search = stream from "ws://localhost:9100" +let map = stream from "ws://localhost:9101" + +view main = + row [ + column [ + text "🔍 Search" + text "Query: {search.query}" + text "Results: {search.filtered}" + ] + column [ + text "📍 Map" + text "{map.label}" + text "{map.lat}, {map.lng}" + ] + ] diff --git a/examples/compose-widgets.ds b/examples/compose-widgets.ds new file mode 100644 index 0000000..d5ba7af --- /dev/null +++ b/examples/compose-widgets.ds @@ -0,0 +1,28 @@ +-- DreamStack Signal Composition Demo +-- Compose two independent widget streams into one dashboard. +-- +-- Run with: +-- Tab 1: dreamstack stream examples/streaming-counter.ds +-- Tab 2: dreamstack stream examples/streaming-physics.ds --port 9101 +-- Tab 3: dreamstack dev examples/compose-widgets.ds --port 3001 +-- +-- Open http://localhost:3001 to see both streams composed. + +let counter = stream from "ws://localhost:9100" +let physics = stream from "ws://localhost:9101" + +view main = + column [ + text "📡 Composed Dashboard" + row [ + column [ + text "── Counter Widget ──" + text "Count: {counter.count}" + text "Doubled: {counter.doubled}" + ] + column [ + text "── Physics Widget ──" + text "Bodies: {physics.body_count}" + ] + ] + ] diff --git a/examples/widget-map.ds b/examples/widget-map.ds new file mode 100644 index 0000000..1be5d4f --- /dev/null +++ b/examples/widget-map.ds @@ -0,0 +1,17 @@ +-- Map Widget — renders a location marker, streams its signals +-- +-- Run with: +-- dreamstack stream examples/widget-map.ds --port 9101 + +let lat = 37.7749 +let lng = -122.4194 +let label = "San Francisco" + +stream map on "ws://localhost:9101" { mode: signal } + +view map = + column [ + text "📍 {label}" + text "Lat: {lat}" + text "Lng: {lng}" + ] diff --git a/examples/widget-search.ds b/examples/widget-search.ds new file mode 100644 index 0000000..2dec84d --- /dev/null +++ b/examples/widget-search.ds @@ -0,0 +1,18 @@ +-- Search Widget — standalone, streams its signals +-- +-- Run with: +-- dreamstack stream examples/widget-search.ds + +let query = "" +let results = ["San Francisco", "San Jose", "San Diego", "Santa Cruz", "Sacramento"] +let filtered = filter(results, (r -> contains(lower(r), lower(query)))) + +stream search on "ws://localhost:9100" { mode: signal } + +view search = + column [ + text "🔍 Search" + input "" { bind: query, placeholder: "Type to filter..." } + for item in filtered -> + text item + ]