atri.dad/public/js/htmx.sse.js

294 lines
8.8 KiB
JavaScript
Raw Permalink Normal View History

2023-05-18 20:04:55 -06:00
/*
Server Sent Events Extension
============================
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
*/
2025-01-13 23:54:01 -06:00
(function () {
2024-10-30 23:50:34 -06:00
/** @type {import("../htmx").HtmxInternalApi} */
2025-01-13 23:54:01 -06:00
var api;
2024-10-30 23:50:34 -06:00
2025-01-13 23:54:01 -06:00
htmx.defineExtension("sse", {
2024-10-30 23:50:34 -06:00
/**
* Init saves the provided reference to the internal HTMX API.
*
* @param {import("../htmx").HtmxInternalApi} api
* @returns void
*/
2025-01-13 23:54:01 -06:00
init: function (apiRef) {
2024-10-30 23:50:34 -06:00
// store a reference to the internal API.
2025-01-13 23:54:01 -06:00
api = apiRef;
2024-10-30 23:50:34 -06:00
// set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) {
2025-01-13 23:54:01 -06:00
htmx.createEventSource = createEventSource;
2024-10-30 23:50:34 -06:00
}
},
2025-01-13 23:54:01 -06:00
getSelectors: function () {
return [
"[sse-connect]",
"[data-sse-connect]",
"[sse-swap]",
"[data-sse-swap]",
];
2024-10-30 23:50:34 -06:00
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
* @returns void
*/
2025-01-13 23:54:01 -06:00
onEvent: function (name, evt) {
var parent = evt.target || evt.detail.elt;
2024-10-30 23:50:34 -06:00
switch (name) {
2025-01-13 23:54:01 -06:00
case "htmx:beforeCleanupElement":
var internalData = api.getInternalData(parent);
2024-10-30 23:50:34 -06:00
// Try to remove remove an EventSource when elements are removed
2025-01-13 23:54:01 -06:00
var source = internalData.sseEventSource;
2024-10-30 23:50:34 -06:00
if (source) {
2025-01-13 23:54:01 -06:00
api.triggerEvent(parent, "htmx:sseClose", {
2024-10-30 23:50:34 -06:00
source,
2025-01-13 23:54:01 -06:00
type: "nodeReplaced",
});
internalData.sseEventSource.close();
2024-10-30 23:50:34 -06:00
}
2025-01-13 23:54:01 -06:00
return;
2024-10-30 23:50:34 -06:00
// Try to create EventSources when elements are processed
2025-01-13 23:54:01 -06:00
case "htmx:afterProcessNode":
ensureEventSourceOnElement(parent);
2024-10-30 23:50:34 -06:00
}
2025-01-13 23:54:01 -06:00
},
});
2024-10-30 23:50:34 -06:00
/// ////////////////////////////////////////////
// HELPER FUNCTIONS
/// ////////////////////////////////////////////
/**
* createEventSource is the default method for creating new EventSource objects.
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
*
* @param {string} url
* @returns EventSource
*/
function createEventSource(url) {
2025-01-13 23:54:01 -06:00
return new EventSource(url, { withCredentials: true });
2024-10-30 23:50:34 -06:00
}
/**
* registerSSE looks for attributes that can contain sse events, right
* now hx-trigger and sse-swap and adds listeners based on these attributes too
* the closest event source
*
* @param {HTMLElement} elt
*/
function registerSSE(elt) {
// Add message handlers for every `sse-swap` attribute
2025-01-13 23:54:01 -06:00
if (api.getAttributeValue(elt, "sse-swap")) {
2024-10-30 23:50:34 -06:00
// Find closest existing event source
2025-01-13 23:54:01 -06:00
var sourceElement = api.getClosestMatch(elt, hasEventSource);
2024-10-30 23:50:34 -06:00
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
2025-01-13 23:54:01 -06:00
return null; // no eventsource in parentage, orphaned element
2024-10-30 23:50:34 -06:00
}
// Set internalData and source
2025-01-13 23:54:01 -06:00
var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource;
2024-10-30 23:50:34 -06:00
2025-01-13 23:54:01 -06:00
var sseSwapAttr = api.getAttributeValue(elt, "sse-swap");
var sseEventNames = sseSwapAttr.split(",");
2024-10-30 23:50:34 -06:00
for (var i = 0; i < sseEventNames.length; i++) {
2025-01-13 23:54:01 -06:00
const sseEventName = sseEventNames[i].trim();
const listener = function (event) {
2024-10-30 23:50:34 -06:00
// If the source is missing then close SSE
if (maybeCloseSSESource(sourceElement)) {
2025-01-13 23:54:01 -06:00
return;
2024-10-30 23:50:34 -06:00
}
// If the body no longer contains the element, remove the listener
if (!api.bodyContains(elt)) {
2025-01-13 23:54:01 -06:00
source.removeEventListener(sseEventName, listener);
return;
2024-10-30 23:50:34 -06:00
}
// swap the response into the DOM and trigger a notification
2025-01-13 23:54:01 -06:00
if (!api.triggerEvent(elt, "htmx:sseBeforeMessage", event)) {
return;
2024-10-30 23:50:34 -06:00
}
2025-01-13 23:54:01 -06:00
swap(elt, event.data);
api.triggerEvent(elt, "htmx:sseMessage", event);
};
2024-10-30 23:50:34 -06:00
// Register the new listener
2025-01-13 23:54:01 -06:00
api.getInternalData(elt).sseEventListener = listener;
source.addEventListener(sseEventName, listener);
2024-10-30 23:50:34 -06:00
}
}
// Add message handlers for every `hx-trigger="sse:*"` attribute
2025-01-13 23:54:01 -06:00
if (api.getAttributeValue(elt, "hx-trigger")) {
2024-10-30 23:50:34 -06:00
// Find closest existing event source
2025-01-13 23:54:01 -06:00
var sourceElement = api.getClosestMatch(elt, hasEventSource);
2024-10-30 23:50:34 -06:00
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
2025-01-13 23:54:01 -06:00
return null; // no eventsource in parentage, orphaned element
2024-10-30 23:50:34 -06:00
}
// Set internalData and source
2025-01-13 23:54:01 -06:00
var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource;
2024-10-30 23:50:34 -06:00
2025-01-13 23:54:01 -06:00
var triggerSpecs = api.getTriggerSpecs(elt);
triggerSpecs.forEach(function (ts) {
if (ts.trigger.slice(0, 4) !== "sse:") {
return;
2024-10-30 23:50:34 -06:00
}
var listener = function (event) {
if (maybeCloseSSESource(sourceElement)) {
2025-01-13 23:54:01 -06:00
return;
2024-10-30 23:50:34 -06:00
}
if (!api.bodyContains(elt)) {
2025-01-13 23:54:01 -06:00
source.removeEventListener(ts.trigger.slice(4), listener);
2024-10-30 23:50:34 -06:00
}
// Trigger events to be handled by the rest of htmx
2025-01-13 23:54:01 -06:00
htmx.trigger(elt, ts.trigger, event);
htmx.trigger(elt, "htmx:sseMessage", event);
};
2024-10-30 23:50:34 -06:00
// Register the new listener
2025-01-13 23:54:01 -06:00
api.getInternalData(elt).sseEventListener = listener;
source.addEventListener(ts.trigger.slice(4), listener);
});
2024-10-30 23:50:34 -06:00
}
}
/**
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
* is created and stored in the element's internalData.
* @param {HTMLElement} elt
* @param {number} retryCount
* @returns {EventSource | null}
*/
function ensureEventSourceOnElement(elt, retryCount) {
if (elt == null) {
2025-01-13 23:54:01 -06:00
return null;
2024-10-30 23:50:34 -06:00
}
// handle extension source creation attribute
2025-01-13 23:54:01 -06:00
if (api.getAttributeValue(elt, "sse-connect")) {
var sseURL = api.getAttributeValue(elt, "sse-connect");
2024-10-30 23:50:34 -06:00
if (sseURL == null) {
2025-01-13 23:54:01 -06:00
return;
2024-10-30 23:50:34 -06:00
}
2025-01-13 23:54:01 -06:00
ensureEventSource(elt, sseURL, retryCount);
2024-10-30 23:50:34 -06:00
}
2025-01-13 23:54:01 -06:00
registerSSE(elt);
2024-10-30 23:50:34 -06:00
}
function ensureEventSource(elt, url, retryCount) {
2025-01-13 23:54:01 -06:00
var source = htmx.createEventSource(url);
2024-10-30 23:50:34 -06:00
2025-01-13 23:54:01 -06:00
source.onerror = function (err) {
2024-10-30 23:50:34 -06:00
// Log an error event
2025-01-13 23:54:01 -06:00
api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source });
2024-10-30 23:50:34 -06:00
// If parent no longer exists in the document, then clean up this EventSource
if (maybeCloseSSESource(elt)) {
2025-01-13 23:54:01 -06:00
return;
2024-10-30 23:50:34 -06:00
}
// Otherwise, try to reconnect the EventSource
if (source.readyState === EventSource.CLOSED) {
2025-01-13 23:54:01 -06:00
retryCount = retryCount || 0;
retryCount = Math.max(Math.min(retryCount * 2, 128), 1);
var timeout = retryCount * 500;
window.setTimeout(function () {
ensureEventSourceOnElement(elt, retryCount);
}, timeout);
2024-10-30 23:50:34 -06:00
}
2025-01-13 23:54:01 -06:00
};
2024-10-30 23:50:34 -06:00
2025-01-13 23:54:01 -06:00
source.onopen = function (evt) {
api.triggerEvent(elt, "htmx:sseOpen", { source });
2024-10-30 23:50:34 -06:00
if (retryCount && retryCount > 0) {
2025-01-13 23:54:01 -06:00
const childrenToFix = elt.querySelectorAll(
"[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]",
);
2024-10-30 23:50:34 -06:00
for (let i = 0; i < childrenToFix.length; i++) {
2025-01-13 23:54:01 -06:00
registerSSE(childrenToFix[i]);
2024-10-30 23:50:34 -06:00
}
// We want to increase the reconnection delay for consecutive failed attempts only
2025-01-13 23:54:01 -06:00
retryCount = 0;
2024-10-30 23:50:34 -06:00
}
2025-01-13 23:54:01 -06:00
};
2024-10-30 23:50:34 -06:00
2025-01-13 23:54:01 -06:00
api.getInternalData(elt).sseEventSource = source;
2024-10-30 23:50:34 -06:00
var closeAttribute = api.getAttributeValue(elt, "sse-close");
if (closeAttribute) {
// close eventsource when this message is received
2025-01-13 23:54:01 -06:00
source.addEventListener(closeAttribute, function () {
api.triggerEvent(elt, "htmx:sseClose", {
2024-10-30 23:50:34 -06:00
source,
2025-01-13 23:54:01 -06:00
type: "message",
});
source.close();
2024-10-30 23:50:34 -06:00
});
}
}
/**
* maybeCloseSSESource confirms that the parent element still exists.
* If not, then any associated SSE source is closed and the function returns true.
*
* @param {HTMLElement} elt
* @returns boolean
*/
function maybeCloseSSESource(elt) {
if (!api.bodyContains(elt)) {
2025-01-13 23:54:01 -06:00
var source = api.getInternalData(elt).sseEventSource;
2024-10-30 23:50:34 -06:00
if (source != undefined) {
2025-01-13 23:54:01 -06:00
api.triggerEvent(elt, "htmx:sseClose", {
2024-10-30 23:50:34 -06:00
source,
2025-01-13 23:54:01 -06:00
type: "nodeMissing",
});
source.close();
2024-10-30 23:50:34 -06:00
// source = null
2025-01-13 23:54:01 -06:00
return true;
2024-10-30 23:50:34 -06:00
}
}
2025-01-13 23:54:01 -06:00
return false;
2024-10-30 23:50:34 -06:00
}
/**
* @param {HTMLElement} elt
* @param {string} content
*/
function swap(elt, content) {
2025-01-13 23:54:01 -06:00
api.withExtensions(elt, function (extension) {
content = extension.transformResponse(content, null, elt);
});
2024-10-30 23:50:34 -06:00
2025-01-13 23:54:01 -06:00
var swapSpec = api.getSwapSpecification(elt);
var target = api.getTarget(elt);
api.swap(target, content, swapSpec);
2024-10-30 23:50:34 -06:00
}
function hasEventSource(node) {
2025-01-13 23:54:01 -06:00
return api.getInternalData(node).sseEventSource != null;
2024-10-30 23:50:34 -06:00
}
2025-01-13 23:54:01 -06:00
})();