Realtime Features

SSE connections, optimistic updates, and state binding for real-time applications

Realtime Features

Constela provides declarative primitives for building real-time applications: SSE connections, optimistic updates, and state binding.

SSE Connections

Establish Server-Sent Events connections for real-time data.

Connect

json
{
  "do": "sseConnect",
  "connection": "notifications",
  "url": { "expr": "lit", "value": "/api/events" },
  "eventTypes": ["message", "update", "delete"],
  "reconnect": {
    "enabled": true,
    "strategy": "exponential",
    "maxRetries": 5,
    "baseDelay": 1000,
    "maxDelay": 30000
  },
  "onOpen": [
    { "do": "set", "target": "connected", "value": { "expr": "lit", "value": true } }
  ],
  "onMessage": [
    { "do": "update", "target": "messages", "operation": "push", "value": { "expr": "var", "name": "payload" } }
  ],
  "onError": [
    { "do": "set", "target": "error", "value": { "expr": "var", "name": "payload" } }
  ]
}

SSEConnectStep Properties

PropertyTypeRequiredDescription
connectionstringYesConnection identifier
urlExpressionYesSSE endpoint URL
eventTypesstring[]NoEvent types to listen for
reconnectReconnectConfigNoReconnection settings
onOpenActionStep[]NoSteps on connection open
onMessageActionStep[]NoSteps on message received
onErrorActionStep[]NoSteps on error

Reconnection Strategies

StrategyDescription
exponentialExponential backoff (1s, 2s, 4s, 8s...)
linearLinear backoff (1s, 2s, 3s, 4s...)
noneNo automatic reconnection

Close Connection

json
{ "do": "sseClose", "connection": "notifications" }

Optimistic Updates

Apply UI changes immediately with automatic rollback on failure.

Apply Optimistic Update

json
{
  "do": "optimistic",
  "target": "posts",
  "path": { "expr": "var", "name": "index" },
  "value": { "expr": "lit", "value": { "liked": true } },
  "result": "updateId",
  "timeout": 5000
}
PropertyTypeRequiredDescription
targetstringYesState field to update
pathExpressionNoPath within state (for nested updates)
valueExpressionYesValue to apply
resultstringNoVariable name for update ID
timeoutnumberNoAuto-rollback timeout in ms

Confirm Update

Confirm the optimistic update was successful:

json
{ "do": "confirm", "id": { "expr": "var", "name": "updateId" } }

Reject Update

Reject the update and rollback to previous state:

json
{ "do": "reject", "id": { "expr": "var", "name": "updateId" } }

Complete Example

json
{
  "name": "likePost",
  "steps": [
    {
      "do": "optimistic",
      "target": "posts",
      "path": { "expr": "var", "name": "payload", "path": "index" },
      "value": { "expr": "lit", "value": { "liked": true, "likeCount": 1 } },
      "result": "updateId",
      "timeout": 5000
    },
    {
      "do": "fetch",
      "url": {
        "expr": "concat",
        "items": [
          { "expr": "lit", "value": "/api/posts/" },
          { "expr": "var", "name": "payload", "path": "id" },
          { "expr": "lit", "value": "/like" }
        ]
      },
      "method": "POST",
      "onSuccess": [
        { "do": "confirm", "id": { "expr": "var", "name": "updateId" } }
      ],
      "onError": [
        { "do": "reject", "id": { "expr": "var", "name": "updateId" } }
      ]
    }
  ]
}

State Binding

Automatically bind SSE messages to state.

Bind

json
{
  "do": "bind",
  "connection": "notifications",
  "eventType": "update",
  "target": "messages",
  "path": { "expr": "var", "name": "payload", "path": "id" },
  "transform": { "expr": "get", "base": { "expr": "var", "name": "payload" }, "path": "data" },
  "patch": false
}
PropertyTypeRequiredDescription
connectionstringYesConnection identifier
eventTypestringNoSpecific event type to bind
targetstringYesState field to update
pathExpressionNoPath for nested updates
transformExpressionNoTransform incoming data
patchbooleanNoUse JSON Patch mode

Unbind

Remove a state binding:

json
{ "do": "unbind", "connection": "notifications", "target": "messages" }

Complete Real-time Chat Example

json
{
  "version": "1.0",
  "state": {
    "connected": { "type": "boolean", "initial": false },
    "messages": { "type": "list", "initial": [] },
    "newMessage": { "type": "string", "initial": "" },
    "error": { "type": "object", "initial": null }
  },
  "lifecycle": {
    "onMount": "connect",
    "onUnmount": "disconnect"
  },
  "actions": [
    {
      "name": "connect",
      "steps": [
        {
          "do": "sseConnect",
          "connection": "chat",
          "url": { "expr": "lit", "value": "/api/chat/events" },
          "eventTypes": ["message", "user-joined", "user-left"],
          "reconnect": {
            "enabled": true,
            "strategy": "exponential",
            "maxRetries": 10,
            "baseDelay": 1000
          },
          "onOpen": [
            { "do": "set", "target": "connected", "value": { "expr": "lit", "value": true } },
            { "do": "set", "target": "error", "value": { "expr": "lit", "value": null } }
          ],
          "onMessage": [
            { "do": "update", "target": "messages", "operation": "push", "value": { "expr": "var", "name": "payload" } }
          ],
          "onError": [
            { "do": "set", "target": "connected", "value": { "expr": "lit", "value": false } },
            { "do": "set", "target": "error", "value": { "expr": "var", "name": "payload" } }
          ]
        }
      ]
    },
    {
      "name": "disconnect",
      "steps": [
        { "do": "sseClose", "connection": "chat" }
      ]
    },
    {
      "name": "sendMessage",
      "steps": [
        {
          "do": "optimistic",
          "target": "messages",
          "value": {
            "expr": "lit",
            "value": { "id": "temp", "text": "", "pending": true }
          },
          "result": "updateId"
        },
        {
          "do": "fetch",
          "url": { "expr": "lit", "value": "/api/chat/send" },
          "method": "POST",
          "body": { "expr": "state", "name": "newMessage" },
          "onSuccess": [
            { "do": "confirm", "id": { "expr": "var", "name": "updateId" } },
            { "do": "set", "target": "newMessage", "value": { "expr": "lit", "value": "" } }
          ],
          "onError": [
            { "do": "reject", "id": { "expr": "var", "name": "updateId" } }
          ]
        }
      ]
    }
  ],
  "view": {
    "kind": "element",
    "tag": "div",
    "children": [
      {
        "kind": "if",
        "condition": { "expr": "not", "operand": { "expr": "state", "name": "connected" } },
        "then": {
          "kind": "element",
          "tag": "div",
          "props": { "className": { "expr": "lit", "value": "status disconnected" } },
          "children": [{ "kind": "text", "value": { "expr": "lit", "value": "Disconnected. Reconnecting..." } }]
        }
      },
      {
        "kind": "each",
        "items": { "expr": "state", "name": "messages" },
        "as": "msg",
        "key": { "expr": "var", "name": "msg", "path": "id" },
        "body": {
          "kind": "element",
          "tag": "div",
          "props": { "className": { "expr": "lit", "value": "message" } },
          "children": [{ "kind": "text", "value": { "expr": "var", "name": "msg", "path": "text" } }]
        }
      },
      {
        "kind": "element",
        "tag": "input",
        "props": {
          "value": { "expr": "state", "name": "newMessage" },
          "onInput": { "event": "input", "action": "updateInput" },
          "onKeydown": { "event": "keydown", "action": "handleKeydown" }
        }
      }
    ]
  }
}