Server-Sent Events

PageloveSSE is the live mutation streaming client: it opens a Server-Sent Events connection to a document, parses incoming HTML Microdata mutation events, applies them to the live DOM, and re-dispatches them as cancelable custom events on document.

When to reach for it

Reach for it when a view should update in real time as the underlying document changes — collaborative editing, live dashboards, multi-tab synchronization — without polling. For the default case, no application code is required at all: importing the module is enough.

Loading the library

Add the module to the page:

<script type="module" src="https://pagelove.github.io/beta-js/pagelove/sse.mjs"></script>

Importing pagelove/sse.mjs auto-instantiates one PageloveSSE subscribed to the current page URL. The default instance is held internally and begins streaming as soon as the module evaluates. Application code is only needed when subscribing to a different URL or when intercepting events.

The PageloveSSE class

import { PageloveSSE } from 'https://pagelove.github.io/beta-js/pagelove/sse.mjs';

Constructors

Form Subscription target
new PageloveSSE() window.location.href — the current page
new PageloveSSE(url) The given document URL

The constructor opens the underlying EventSource immediately, with withCredentials: true so cookies and authorization travel with the request.

Methods

Method Description
close() Close the underlying EventSource and drop the reference. The instance cannot be reopened — construct a new one to resubscribe.

Properties

Property Type Description
url string The URL this client is subscribed to. Read-only.
source EventSource | null The underlying EventSource, or null after close(). Read-only.

Static methods

Method Description
PageloveSSE.parse(data) Parse an HTML Microdata mutation payload. Returns { method, selector, path, host, body, etag } or null.
PageloveSSE.parseReset(data) Parse a reset payload. Returns the reason string, or "unknown" if no reason is present.

DOM mutations

When a mutation event arrives, PageloveSSE locates the target with the carried CSS selector and applies the change:

Method Action
POST Appends the new content as a child of the matched element.
PUT Replaces the matched element with the new content.
DELETE Removes the matched element.

If no element matches the selector, the event is silently dropped. Mutations that echo a local write (matched against the pending PLMethodStarted queue) are recognized as echoes and the DOM step is skipped — the local change already produced the result.

This local echo check only reconciles the same tab's own optimistic write against the mutation event that write eventually produces — it does not affect which events the server delivers. PageloveSSE connects with a plain EventSource and does not currently read the server-assigned connection token or send it back on writes (see Echo suppression), so the server falls back to its session-based suppression: a write from one browser tab is not delivered live to another tab of the same session, since same-origin tabs share the session cookie. Each tab only ever sees its own writes reflected through this local echo-matching step, not through the SSE stream. If your application needs one tab to see another same-session tab's live writes, subscribe with your own EventSource, listen for the pagelove-connection event to capture the server-assigned token, and send it back as a Pagelove-Connection header on every mutating request from that tab, as described in that section, rather than relying on the default PageloveSSE connection.

Events on document

Three custom events are dispatched on document. All three bubble and are composed.

Event When it fires Cancelable event.detail contains
PLMutation Before a parsed mutation is applied to the DOM yes — preventDefault() skips the DOM change method, selector, path, host, body, etag, element
PLMutationApplied After the mutation has been applied no Same shape as PLMutation. element is the new or modified node, or null for DELETE.
PLStreamReset When the server sends a reset event yes — preventDefault() suppresses the default location.reload() reason

PLMutation detail fields:

Field Type Description
method string HTTP method — POST, PUT, or DELETE.
selector string CSS selector of the target element.
body string HTML fragment to apply. Empty for DELETE.
path string Document path the mutation belongs to.
host string Virtual host the mutation belongs to.
etag string ETag of the resulting element, when supplied.
element Element The DOM element matched by the selector.

Listening for events

document.addEventListener('PLMutation', (event) => {
  const { method, selector, element } = event.detail;
  console.log(`incoming ${method} ${selector}`);
  // event.preventDefault(); // would skip the DOM change
});

PLMutationApplied follows the same shape and fires after the change has landed. PLStreamReset carries event.detail.reason; calling preventDefault() cancels the automatic reload.

Focus protection

Focus protection — deferring incoming changes that target the currently focused element until focus leaves — is not implemented in pagelove/sse.mjs. It lives in the Pagelove class, which observes the schema, queues patches against focused inputs, and exposes flushPendingPatches() and hasPendingPatches for explicit control.

Examples

Default — include the script

<script type="module" src="https://pagelove.github.io/beta-js/pagelove/sse.mjs"></script>

The page is now live. Any mutation the server streams for this URL is applied to the DOM as it arrives.

Manual subscription to a different URL

<script type="module">
  import { PageloveSSE } from 'https://pagelove.github.io/beta-js/pagelove/sse.mjs';

  const sidebar = new PageloveSSE('/sidebar.html');

  document.addEventListener('PLMutationApplied', (event) => {
    if (event.detail.path === '/sidebar.html') {
      console.log('sidebar updated', event.detail.selector);
    }
  });

  // Later, when the sidebar is dismissed:
  // sidebar.close();
</script>

A second subscription is opened against /sidebar.html alongside the auto-instantiated default. Both feed events to the same document listeners; consumers distinguish them by event.detail.path.

See also