Skip to content

Stream

WebSocket-backed IPC stream for high-frequency or ordered data delivery.

Config keystream
JS namespaceStream
CoreYes — disabling cascades to shell
PhasesWebviewInject
Hard deps
PlatformsmacOS · Linux · Windows

Stream uses a local WebSocket server for ordered, low-latency data delivery. Use it for sustained flows — price ticks, log lines, LLM tokens, sensor data — where firing a new evaluateJavaScript call per message would saturate the event loop. For discrete, low-frequency signals see Event.


Disabling

Stream is active by default. Disabling it automatically disables shell.

yaml
plugins:
  disabled:
    - stream

Crystal → JavaScript

Sending from Crystal

crystal
app.stream.send("trade", { "symbol" => "BTC", "price" => 45123.50 })
app.stream.send("log-line", "build finished in 4.2s")
app.stream.send("heartbeat")  # no payload

app.stream.send is safe to call from any fiber. If no WebSocket client is connected, the call is a silent no-op.

Listening in JavaScript

js
import { lune } from "../lunejs/runtime/runtime.js";

lune.Stream.on("trade", (tick) => {
  console.log(tick.symbol, tick.price);
});

lune.Stream.once("ready", () => showReadyState());

JavaScript → Crystal

Sending from JavaScript

js
lune.Stream.send("stream-start");
lune.Stream.send("order", { symbol: "BTC", qty: 1, side: "buy" });

lune.Stream.send is fire-and-forget — no await needed.

Listening in Crystal

crystal
app.stream.on("order") do |data|
  place_order(data["symbol"].as_s, data["qty"].as_i)
end

app.stream.off("order")  # remove all handlers for this name

Handlers run in the stream's background fiber pool — keep them short or hand off to app.async.


JavaScript API

MethodSignatureDescription
onon(name, cb)Subscribe to a named channel
onceonce(name, cb)One-shot subscription
offoff(name, cb?)Remove a specific listener, or all if cb omitted
sendsend(name, data?)Fire-and-forget message to Crystal

Common patterns

High-frequency ticker

crystal
streaming = Atomic(Int32).new(0)

app.stream.on("stream-start") { |_| streaming.set(1) }
app.stream.on("stream-stop")  { |_| streaming.set(0) }

app.async("ticker") do
  loop do
    if streaming.get == 1
      app.stream.send("tick", { "price" => current_price })
      sleep 50.milliseconds
    else
      sleep 100.milliseconds
    end
  end
end
js
lune.Stream.on("tick", ({ price }) => renderTicker(price));
startButton.addEventListener("click", () => lune.Stream.send("stream-start"));
stopButton.addEventListener("click", () => lune.Stream.send("stream-stop"));

LLM token streaming

crystal
app.async do
  client.stream_chat(prompt) do |token|
    app.stream.send("token", token)
  end
  app.stream.send("done", nil)
end
js
let output = "";
lune.Stream.on("token", (tok) => {
  output += tok;
  el.textContent = output;
});
lune.Stream.once("done", () => {
  el.dataset.streaming = "false";
});

Log tail

crystal
app.async do
  File.open("/var/log/app.log") do |f|
    f.seek(0, IO::Seek::End)
    loop do
      line = f.gets
      line ? app.stream.send("log", line) : sleep(200.milliseconds)
    end
  end
end

Event vs Stream

EventStream
TransportevaluateJavaScript per callWebSocket frames
JS → Crystalawait Event.emit(...)lune.Stream.send(...) (no await)
ThroughputLow–mediumHigh (batched WS frames)
OrderingBest-effortGuaranteed per-connection
Best forUI signals, one-off notificationsTickers, log tails, token streams

Platform notes

  • macOS — Verified.
  • Linux — Untested.
  • Windows — Verified. WebSocket IPC; bind + listen pinned to the same execution context for IOCP correctness.

Released under the MIT License.