Spark System

Azu's Spark system provides client-side JavaScript for real-time DOM updates and WebSocket communication. It enables seamless real-time interactions between server-side components and client-side JavaScript.

Overview

The Spark system provides:

  • Automatic WebSocket management for real-time connections

  • DOM manipulation with server-sent updates

  • Event handling for client-server communication

  • Component synchronization between server and client

  • Error handling and reconnection logic

Architecture

Basic Setup

Including Spark

<!-- Include Spark in your HTML template -->
<!DOCTYPE html>
<html>
  <head>
    <title>Azu Application</title>
  </head>
  <body>
    <!-- Your content here -->

    <!-- Include Spark JavaScript -->
    <script src="/js/azu-spark.js"></script>
    <script>
      // Initialize Spark
      Spark.init({
        host: window.location.hostname,
        port: 3000,
        secure: window.location.protocol === "https:",
      });
    </script>
  </body>
</html>

Component Integration

class CounterComponent < Azu::Component
  def initialize(@initial_count : Int32 = 0)
    @count = @initial_count
  end

  def content
    div class: "counter", id: "counter-#{object_id}", data_spark_id: object_id do
      h3 "Counter"
      span id: "count", class: "count" do
        text @count.to_s
      end

      div class: "controls" do
        button onclick: "Spark.emit('increment')", class: "btn btn-primary" do
          text "Increment"
        end
        button onclick: "Spark.emit('decrement')", class: "btn btn-secondary" do
          text "Decrement"
        end
        button onclick: "Spark.emit('reset')", class: "btn btn-danger" do
          text "Reset"
        end
      end
    end
  end

  def on_event("increment", data)
    @count += 1
    update_element "count", @count.to_s
  end

  def on_event("decrement", data)
    @count -= 1
    update_element "count", @count.to_s
  end

  def on_event("reset", data)
    @count = @initial_count
    update_element "count", @count.to_s
  end
end

Spark API

Core Methods

// Initialize Spark
Spark.init({
  host: "localhost",
  port: 3000,
  secure: false,
  reconnectInterval: 5000,
  maxReconnectAttempts: 10,
});

// Emit events to server
Spark.emit("event_name", { data: "value" });

// Listen for server events
Spark.on("update", function (data) {
  console.log("Received update:", data);
});

// Update DOM elements
Spark.updateElement("element-id", "new content");

// Append content to elements
Spark.appendElement("element-id", "<div>New item</div>");

// Remove elements
Spark.removeElement("element-id");

// Get component instance
const component = Spark.getComponent("component-id");

Event Handling

// Listen for specific events
Spark.on("counter_updated", function (data) {
  const { count } = data;
  document.getElementById("count").textContent = count;
});

Spark.on("user_joined", function (data) {
  const { user } = data;
  const userList = document.getElementById("user-list");
  userList.innerHTML += `<li>${user.name} joined</li>`;
});

Spark.on("error", function (error) {
  console.error("Spark error:", error);
  showNotification("Connection error", "error");
});

// Remove event listeners
Spark.off("counter_updated");

DOM Manipulation

// Update element content
Spark.updateElement("message", "Hello, World!");

// Update element with HTML
Spark.updateElement("content", "<h1>Title</h1><p>Content</p>");

// Append content
Spark.appendElement("list", "<li>New item</li>");

// Prepend content
Spark.prependElement("list", "<li>First item</li>");

// Remove element
Spark.removeElement("old-message");

// Add/remove CSS classes
Spark.addClass("button", "active");
Spark.removeClass("button", "disabled");

// Toggle CSS classes
Spark.toggleClass("element", "highlighted");

// Set attributes
Spark.setAttribute("input", "value", "new value");
Spark.setAttribute("img", "src", "/new-image.jpg");

WebSocket Management

Connection Handling

// Connection status
Spark.isConnected(); // Returns boolean

// Manual connection/disconnection
Spark.connect();
Spark.disconnect();

// Connection events
Spark.on("connect", function () {
  console.log("Connected to server");
  document.body.classList.add("connected");
});

Spark.on("disconnect", function () {
  console.log("Disconnected from server");
  document.body.classList.remove("connected");
});

Spark.on("reconnect", function (attempt) {
  console.log(`Reconnected on attempt ${attempt}`);
});

Spark.on("reconnect_failed", function () {
  console.log("Failed to reconnect");
  showNotification("Connection lost", "error");
});

Error Handling

// Handle connection errors
Spark.on("error", function (error) {
  console.error("WebSocket error:", error);

  switch (error.type) {
    case "connection_failed":
      showNotification("Failed to connect", "error");
      break;
    case "message_parse_error":
      console.error("Invalid message format");
      break;
    case "timeout":
      showNotification("Request timeout", "warning");
      break;
  }
});

// Handle specific error types
Spark.on("auth_error", function (error) {
  console.error("Authentication failed:", error.message);
  redirectToLogin();
});

Spark.on("permission_error", function (error) {
  console.error("Permission denied:", error.message);
  showNotification("Access denied", "error");
});

Component Communication

Server to Client

class ChatComponent < Azu::Component
  def on_event("send_message", data)
    message_text = data["text"]?.try(&.as_s)
    return unless message_text

    # Add message to server state
    @messages << Message.new(message_text)

    # Update DOM for sender
    append_element "messages" do
      div class: "message own" do
        text message_text
      end
    end

    # Broadcast to other clients
    broadcast_update({
      type: "new_message",
      message: message_text,
      timestamp: Time.utc.to_rfc3339
    })
  end
end
// Client-side handling
Spark.on("new_message", function (data) {
  const { message, timestamp } = data;

  const messagesContainer = document.getElementById("messages");
  const messageElement = document.createElement("div");
  messageElement.className = "message";
  messageElement.innerHTML = `
    <span class="text">${message}</span>
    <time>${new Date(timestamp).toLocaleTimeString()}</time>
  `;

  messagesContainer.appendChild(messageElement);
  messagesContainer.scrollTop = messagesContainer.scrollHeight;
});

Client to Server

// Send form data
document
  .getElementById("message-form")
  .addEventListener("submit", function (e) {
    e.preventDefault();

    const input = document.getElementById("message-input");
    const message = input.value.trim();

    if (message) {
      Spark.emit("send_message", { text: message });
      input.value = "";
    }
  });

// Send button clicks
document.getElementById("like-button").addEventListener("click", function () {
  Spark.emit("like_post", { post_id: this.dataset.postId });
});

// Send user interactions
document.getElementById("typing-input").addEventListener("input", function () {
  Spark.emit("typing", { is_typing: true });
});

document.getElementById("typing-input").addEventListener("blur", function () {
  Spark.emit("typing", { is_typing: false });
});

Advanced Features

Component State Management

// Get component state
const state = Spark.getComponentState("counter-123");

// Set component state
Spark.setComponentState("counter-123", { count: 5 });

// Listen for state changes
Spark.onComponentStateChange("counter-123", function (newState, oldState) {
  console.log("State changed:", oldState, "->", newState);
});

// Subscribe to component updates
Spark.subscribeToComponent("counter-123", function (updates) {
  updates.forEach((update) => {
    switch (update.type) {
      case "element_update":
        Spark.updateElement(update.elementId, update.content);
        break;
      case "element_append":
        Spark.appendElement(update.elementId, update.content);
        break;
      case "element_remove":
        Spark.removeElement(update.elementId);
        break;
    }
  });
});

Batch Updates

// Batch multiple DOM updates
Spark.batch(function () {
  Spark.updateElement("counter", "5");
  Spark.updateElement("status", "Active");
  Spark.addClass("button", "highlighted");
});

// Or use the batch method directly
Spark.batch([
  { type: "update", elementId: "counter", content: "5" },
  { type: "update", elementId: "status", content: "Active" },
  { type: "addClass", elementId: "button", className: "highlighted" },
]);

Custom Event Handlers

// Register custom event handlers
Spark.registerHandler("custom_event", function (data) {
  // Handle custom event
  console.log("Custom event:", data);
});

// Register DOM update handlers
Spark.registerUpdateHandler("special-element", function (content) {
  // Custom update logic
  const element = document.getElementById("special-element");
  element.innerHTML = content;
  element.classList.add("updated");

  // Trigger animation
  element.style.animation = "fadeIn 0.3s ease-in";
});

Performance Optimization

Debounced Events

// Debounce frequent events
const debouncedTyping = Spark.debounce(function (isTyping) {
  Spark.emit("typing", { is_typing: isTyping });
}, 300);

document.getElementById("message-input").addEventListener("input", function () {
  debouncedTyping(true);
});

document.getElementById("message-input").addEventListener("blur", function () {
  debouncedTyping(false);
});

Throttled Updates

// Throttle DOM updates
const throttledUpdate = Spark.throttle(function (elementId, content) {
  Spark.updateElement(elementId, content);
}, 100);

// Use throttled update for frequent changes
setInterval(function () {
  throttledUpdate("clock", new Date().toLocaleTimeString());
}, 50);

Connection Pooling

// Configure connection pooling
Spark.init({
  host: "localhost",
  port: 3000,
  poolSize: 5,
  maxConnections: 20,
  connectionTimeout: 5000,
});

Error Recovery

Automatic Reconnection

// Configure reconnection
Spark.init({
  host: "localhost",
  port: 3000,
  reconnectInterval: 5000,
  maxReconnectAttempts: 10,
  backoffMultiplier: 1.5,
});

// Handle reconnection events
Spark.on("reconnect_attempt", function (attempt) {
  console.log(`Reconnection attempt ${attempt}`);
  showNotification(`Reconnecting... (${attempt})`, "info");
});

Spark.on("reconnect_success", function () {
  console.log("Successfully reconnected");
  showNotification("Reconnected successfully", "success");
});

Spark.on("reconnect_failed", function () {
  console.log("Failed to reconnect");
  showNotification("Connection lost. Please refresh the page.", "error");
});

Message Queuing

// Enable message queuing during disconnection
Spark.init({
  host: "localhost",
  port: 3000,
  queueMessages: true,
  maxQueueSize: 100,
});

// Messages sent while disconnected are queued and sent when reconnected
Spark.on("disconnect", function () {
  console.log("Disconnected - messages will be queued");
});

Spark.on("reconnect", function () {
  console.log("Reconnected - sending queued messages");
});

Debugging

Debug Mode

// Enable debug mode
Spark.init({
  host: "localhost",
  port: 3000,
  debug: true,
});

// Debug events
Spark.on("debug", function (message) {
  console.log("Spark Debug:", message);
});

// Log all events
Spark.on("*", function (event, data) {
  console.log(`Spark Event [${event}]:`, data);
});

Performance Monitoring

// Monitor performance
Spark.on("performance", function (metrics) {
  console.log("Performance metrics:", metrics);
  // metrics includes: latency, messageCount, errorRate, etc.
});

// Monitor connection quality
Spark.on("connection_quality", function (quality) {
  console.log("Connection quality:", quality);
  // quality includes: rtt, packetLoss, bandwidth, etc.
});

Best Practices

1. Event Management

  • Use descriptive event names

  • Validate event data on both client and server

  • Clean up event listeners when components are destroyed

  • Use namespaced events for complex applications

2. DOM Updates

  • Batch multiple DOM updates when possible

  • Use specific element IDs for updates

  • Avoid updating large DOM trees frequently

  • Use CSS classes for state changes

3. Error Handling

  • Always handle connection errors

  • Implement proper reconnection logic

  • Provide user feedback for connection issues

  • Log errors for debugging

4. Performance

  • Debounce frequent events

  • Throttle DOM updates

  • Use connection pooling for multiple components

  • Monitor performance metrics

Next Steps


Ready to add real-time interactivity? Start with the basic Spark setup above, then explore Live Components for complete real-time solutions.

Last updated

Was this helpful?