Skip to content

A Cloudflare Worker implementation of Server-Sent Events (SSE), Datastar and Mustache with a minimal router.

Notifications You must be signed in to change notification settings

code-poel/cloudflare-sse-datastar

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cloudflare Worker: SSE, Datastar and Mustache.

This repository an implementation of Datastar driven by a Cloudflare static asset worker sending Server Sent Events. It leverages a bare-bones router and Mustache for template rendering (if necessary).

Note: This project is currently experimental and serves as a Proof of Concept (POC). It may not be suitable for production use and is subject to change. Use it at your own risk.

Demo Frontend

The project includes a demo frontend that showcases some features of the SSE implementation. The demo provides interactive examples.

You can run the demo locally by following the development instructions below.

Demo Frontend Screenshot

Dependencies

Core Libraries

  • @tsndr/cloudflare-worker-router: Lightweight router for Cloudflare Workers with TypeScript support
  • @janl/mustache.js: Logic-less templating engine for rendering HTML templates
  • @cloudflare/workers-types: TypeScript definitions for Cloudflare Workers

Custom Utilities

  • mergeFragments: Merges one or more fragments into the DOM.
  • mergeSignals: Updates the signals with new values.
  • removeFragments: Removes one or more HTML fragments that match the provided selector from the DOM.
  • removeSignals: Removes signals that match one or more provided paths.
  • executeScript: Executes JavaScript in the browser.

Wokrer / SSE Specific

  • repeatingEvent: Helper for creating repeating SSE events
  • createSSEResponse: Utility for creating Server-Sent Events responses

Datastar Event Types

The worker supports all (as of this writing) Datastar server sent event types. These include:

Merge Fragments

event: datastar-merge-fragments
data: fragments <minified-html>
data: selector <css-selector>
data: mergeMode <mode>
data: useViewTransition <boolean>
retry: <milliseconds>

Merge Signals

event: datastar-merge-signals
data: signals <json-string>
data: onlyIfMissing <boolean>
retry: <milliseconds>

Remove Fragments

event: datastar-remove-fragments
data: selector <css-selector>
retry: <milliseconds>

Remove Signals

event: datastar-remove-signals
data: paths <path1>
data: paths <path2>
retry: <milliseconds>

Execute Script

event: datastar-execute-script
data: autoRemove <boolean>
data: attributes <name> <value>
data: script <javascript-code>
retry: <milliseconds>

Implementation Details

HTML Minification

All HTML fragments pushed through the mergeFragments event type are automatically minified before being sent to the client. The minification process attempts to:

  • Remove unnecessary whitespace
  • Preserve essential whitespace in text content
  • Maintain HTML structure and functionality

Connection Retry Behavior

All event types support an optional retry field that specifies the number of milliseconds to wait before attempting to reconnect if the connection is lost. This follows the Server-Sent Events specification for reconnection handling.

// Example with retry configuration
const event = mergeFragments({
  fragment: '<div>Test</div>',
  selector: '#target',
  retry: 5000  // Will retry connection after 5 seconds if disconnected
});

The retry field is optional and can be:

  • A number: Specifies milliseconds to wait before retrying
  • null/undefined: No retry will be attempted
  • Not specified: No retry will be attempted

Custom Event Types

While we provide factory functions for common event types, you can create custom events by implementing the Event interface:

interface Event {
  type: string;
  format(): string;
  _repeating?: {
    frequency: number;
    originalEvent: Event | null;
  };
}

The /heartbeat endpoint demonstrates this with a custom event that:

  • Uses type: null for a comment event
  • Implements a custom format() method
  • Includes repeating functionality

Available Datastar Example Endpoints

GET /merge-fragments

Updates HTML content with static content.

const event = mergeFragments({
  fragment: '<div>Static content</div>',
  selector: '#listing',
  retry: 5000  // Optional: retry after 5 seconds if disconnected
});

Fragment Types

The fragment property in mergeFragments can be either:

  • A string: For static content that doesn't change (evaluated once when the event is created)
  • A function: For dynamic content that needs to be re-evaluated (called each time the event is sent)
// Static content - evaluated once
fragment: '<div>Static content</div>'

// Dynamic content - re-evaluated each time
fragment: () => `<div>${new Date().toLocaleTimeString()}</div>`

Important notes about dynamic fragments:

  1. The function is called each time the event is sent
  2. Use for content that needs to be fresh (clocks, counters, real-time data)
  3. Can be combined with repeating events for periodic updates
  4. The function should be pure and fast, as it may be called frequently

GET /merge-fragments-repeating

Updates HTML content with dynamic content that changes every second.

const event = mergeFragments({
  fragment: () => `<div id="clock">${new Date().toLocaleTimeString()}</div>`,
  selector: '#clock',
  retry: 5000  // Optional: retry after 5 seconds if disconnected
});
const repeatEvent = repeatingEvent(event, 1000);

GET /merge-signals

Updates client-side state.

const event = mergeSignals({
  signals: {
    foo: 'merged'
  },
  retry: 5000  // Optional: retry after 5 seconds if disconnected
});

GET /remove-fragments

Removes HTML elements from the DOM.

const removeEvent = removeFragments({
  selector: '#listing',
  retry: 5000  // Optional: retry after 5 seconds if disconnected
});

GET /remove-signals

Removes signals from client-side state.

const removeEvent = removeSignals({
  paths: ['foo', 'nested.baz'],
  retry: 5000  // Optional: retry after 5 seconds if disconnected
});

GET /execute-script

Executes JavaScript code on the client side.

const executeEvent = executeScript({
  autoRemove: true,
  attributes: [
    { name: 'type', value: 'module' },
    { name: 'defer', value: true }
  ],
  scripts: [
    'console.log("Hello, world!")',
    'console.log("Here is a second console line to output!")'
  ],
  retry: 5000  // Optional: retry after 5 seconds if disconnected
});

GET /heartbeat

Sends a timestamp every second to keep the connection alive. This is an example of an ad-hoc Event type that doesn't use our standard event factories. It's particularly useful in the Cloudflare Workers environment where connections might be closed due to:

  • CPU usage limits
  • Connection timeouts
  • Worker instance recycling

The heartbeat ensures the connection stays alive and provides a way to detect connection issues on the client side.

const timestampEvent = {
  type: null,
  format() {
    return `: ${new Date().toLocaleTimeString()}\n\n`;
  },
  _repeating: {
    frequency: 1000,
    originalEvent: null
  }
};

Development

# Install dependencies
npm install

# Run locally
npm run dev

# Visit Demo Frontend @ http://localhost:8787/

# Run tests
npm test

# Run tests in watch mode
npm run test:watch

# Run tests with coverage
npm run test:coverage

# Deploy to Cloudflare
npm run deploy

About

A Cloudflare Worker implementation of Server-Sent Events (SSE), Datastar and Mustache with a minimal router.

Topics

Resources

Stars

Watchers

Forks