Architecture

How LOSOS works — from data to screen and back.

The layers

┌─────────────────────────────────┐
│  Apps (panes)                   │  todo, chat, pokedex...
├─────────────────────────────────┤
│  Shell                          │  shell.js — tabs, pane routing
├─────────────────────────────────┤
│  Extensions                     │  xlogin (auth), store-crdt (sync)
├─────────────────────────────────┤
│  Kernel                         │  store.js + html.js + lion
│                                 │  reactivity, rendering, data
├─────────────────────────────────┤
│  Data layer                     │  JSON-LD at URLs
│                                 │  HTTP PUT/GET, WebSocket, Solid pods
└─────────────────────────────────┘

The kernel is what you can't remove — store, html, lion. Everything else is pluggable. The shell is one possible UI. xlogin is one possible auth driver.

The reactive flow

store.set()  →  dirty tracking  →  debounced PUT  →  WebSocket pub
                     ↓                                      ↓
               onChange fires  ←  other clients reload  ←  sub
                     ↓
               renderApp()  →  html`` patches DOM
  1. You call store.set(node, 'status', 'COMPLETED')
  2. The store marks the data dirty and starts a debounce timer
  3. The store calls all onChange listeners — your renderApp() runs
  4. render() compares the new template values to the previous ones and patches only what changed
  5. After the debounce, the store PUTs the JSON-LD to the server
  6. The server sends a WebSocket pub message to other clients
  7. Other clients reload() and re-render

Data model

LOSOS uses JSON-LD — JSON with a @context that maps short keys to standard URIs. It looks like plain JSON but is linked data.

{
  "@context": {
    "dct": "http://purl.org/dc/terms/",
    "title": "dct:title"
  },
  "@id": "#this",
  "@type": "Tracker",
  "title": "My Tasks"
}

Key principles:

The URI is the router

There is no router library. Every JSON-LD node has a URI. Navigate to a URI, the shell loads the data, finds matching panes, renders. @id is the route. @type picks the view.

Pane discovery

The shell scans for <script data-pane> tags and loads each module. For each pane, it calls canHandle(subject, store). Panes that return true get a tab. The first match renders immediately.

This means data chooses its view. Add a new @type, add a pane that handles it. No configuration, no routing table.

Persistence

The store auto-saves via HTTP PUT. It discovers WebSocket support via the Updates-Via response header on the first save. If present, it connects and subscribes for live updates.

createStore(data, {
  url: 'https://pod.example/data.jsonld',
  authFetch: window.xlogin.authFetch,
  debounce: 800
})

// store.set() → waits 800ms → PUT to url
// PUT response has Updates-Via header → WebSocket connects
// Other clients get pub → reload() → re-render

Why not a virtual DOM?

LOSOS uses tagged template literals. The template strings are the same object reference on every render (JavaScript interns template literals). So render() can check: same strings? Patch only the ${} holes. Different strings? Full rebuild.

This is faster than a virtual DOM for the size of apps LOSOS targets. No diffing, no reconciliation, no fiber scheduler. Just a for loop comparing values by reference.

For lists, keyed() matches items by key and skips items whose template values haven't changed. In a 50-item list where 1 item changes, 49 skip entirely.

What LOSOS is not

Future

  1. Data declares its view — a ui:defaultPane triple on @type. The pane URL lives in the data itself.
  2. User preferences — choose your pane per type. Alice uses kanban, Bob uses list. Same data.
  3. Pane registry — community-shared panes. One line to add a calendar view, a kanban board, a chart.
  4. JSON Schema forms — auto-generate edit forms from $schema. Validate on save.
  5. Offline — service worker queues PUTs when disconnected, replays on reconnect.
  6. Conflict resolution — ETags and If-Match for safe concurrent editing.