diff options
Diffstat (limited to 'htmx/websocket.js')
| -rw-r--r-- | htmx/websocket.js | 420 |
1 files changed, 233 insertions, 187 deletions
diff --git a/htmx/websocket.js b/htmx/websocket.js index be1f08d..d57d055 100644 --- a/htmx/websocket.js +++ b/htmx/websocket.js @@ -4,28 +4,27 @@ WebSockets Extension This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions. */ -(function() { +(function () { /** @type {import("../htmx").HtmxInternalApi} */ - var api - - htmx.defineExtension('ws', { + var api; + htmx.defineExtension("ws", { /** * init is called once, when this extension is first registered. * @param {import("../htmx").HtmxInternalApi} apiRef */ - init: function(apiRef) { + init: function (apiRef) { // Store reference to internal API - api = apiRef + api = apiRef; // Default function for creating new EventSource objects if (!htmx.createWebSocket) { - htmx.createWebSocket = createWebSocket + htmx.createWebSocket = createWebSocket; } // Default setting for reconnect delay if (!htmx.config.wsReconnectDelay) { - htmx.config.wsReconnectDelay = 'full-jitter' + htmx.config.wsReconnectDelay = "full-jitter"; } }, @@ -35,44 +34,48 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f * @param {string} name * @param {Event} evt */ - onEvent: function(name, evt) { - var parent = evt.target || evt.detail.elt + onEvent: function (name, evt) { + var parent = evt.target || evt.detail.elt; switch (name) { // Try to close the socket when elements are removed - case 'htmx:beforeCleanupElement': - - var internalData = api.getInternalData(parent) + case "htmx:beforeCleanupElement": + var internalData = api.getInternalData(parent); if (internalData.webSocket) { - internalData.webSocket.close() + internalData.webSocket.close(); } - return - - // Try to create websockets when elements are processed - case 'htmx:beforeProcessNode': - - forEach(queryAttributeOnThisOrChildren(parent, 'ws-connect'), function(child) { - ensureWebSocket(child) - }) - forEach(queryAttributeOnThisOrChildren(parent, 'ws-send'), function(child) { - ensureWebSocketSend(child) - }) + return; + + // Try to create websockets when elements are processed + case "htmx:beforeProcessNode": + forEach( + queryAttributeOnThisOrChildren(parent, "ws-connect"), + function (child) { + ensureWebSocket(child); + }, + ); + forEach( + queryAttributeOnThisOrChildren(parent, "ws-send"), + function (child) { + ensureWebSocketSend(child); + }, + ); } - } - }) + }, + }); function splitOnWhitespace(trigger) { - return trigger.trim().split(/\s+/) + return trigger.trim().split(/\s+/); } function getLegacyWebsocketURL(elt) { - var legacySSEValue = api.getAttributeValue(elt, 'hx-ws') + var legacySSEValue = api.getAttributeValue(elt, "hx-ws"); if (legacySSEValue) { - var values = splitOnWhitespace(legacySSEValue) + var values = splitOnWhitespace(legacySSEValue); for (var i = 0; i < values.length; i++) { - var value = values[i].split(/:(.+)/) - if (value[0] === 'connect') { - return value[1] + var value = values[i].split(/:(.+)/); + if (value[0] === "connect") { + return value[1]; } } } @@ -88,68 +91,78 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f // If the element containing the WebSocket connection no longer exists, then // do not connect/reconnect the WebSocket. if (!api.bodyContains(socketElt)) { - return + return; } // Get the source straight from the element's value - var wssSource = api.getAttributeValue(socketElt, 'ws-connect') + var wssSource = api.getAttributeValue(socketElt, "ws-connect"); - if (wssSource == null || wssSource === '') { - var legacySource = getLegacyWebsocketURL(socketElt) + if (wssSource == null || wssSource === "") { + var legacySource = getLegacyWebsocketURL(socketElt); if (legacySource == null) { - return + return; } else { - wssSource = legacySource + wssSource = legacySource; } } // Guarantee that the wssSource value is a fully qualified URL - if (wssSource.indexOf('/') === 0) { - var base_part = location.hostname + (location.port ? ':' + location.port : '') - if (location.protocol === 'https:') { - wssSource = 'wss://' + base_part + wssSource - } else if (location.protocol === 'http:') { - wssSource = 'ws://' + base_part + wssSource + if (wssSource.indexOf("/") === 0) { + var base_part = + location.hostname + (location.port ? ":" + location.port : ""); + if (location.protocol === "https:") { + wssSource = "wss://" + base_part + wssSource; + } else if (location.protocol === "http:") { + wssSource = "ws://" + base_part + wssSource; } } - var socketWrapper = createWebsocketWrapper(socketElt, function() { - return htmx.createWebSocket(wssSource) - }) + var socketWrapper = createWebsocketWrapper(socketElt, function () { + return htmx.createWebSocket(wssSource); + }); - socketWrapper.addEventListener('message', function(event) { + socketWrapper.addEventListener("message", function (event) { if (maybeCloseWebSocketSource(socketElt)) { - return + return; } - var response = event.data - if (!api.triggerEvent(socketElt, 'htmx:wsBeforeMessage', { - message: response, - socketWrapper: socketWrapper.publicInterface - })) { - return + var response = event.data; + if ( + !api.triggerEvent(socketElt, "htmx:wsBeforeMessage", { + message: response, + socketWrapper: socketWrapper.publicInterface, + }) + ) { + return; } - api.withExtensions(socketElt, function(extension) { - response = extension.transformResponse(response, null, socketElt) - }) + api.withExtensions(socketElt, function (extension) { + response = extension.transformResponse(response, null, socketElt); + }); - var settleInfo = api.makeSettleInfo(socketElt) - var fragment = api.makeFragment(response) + var settleInfo = api.makeSettleInfo(socketElt); + var fragment = api.makeFragment(response); if (fragment.children.length) { - var children = Array.from(fragment.children) + var children = Array.from(fragment.children); for (var i = 0; i < children.length; i++) { - api.oobSwap(api.getAttributeValue(children[i], 'hx-swap-oob') || 'true', children[i], settleInfo) + api.oobSwap( + api.getAttributeValue(children[i], "hx-swap-oob") || "true", + children[i], + settleInfo, + ); } } - api.settleImmediately(settleInfo.tasks) - api.triggerEvent(socketElt, 'htmx:wsAfterMessage', { message: response, socketWrapper: socketWrapper.publicInterface }) - }) + api.settleImmediately(settleInfo.tasks); + api.triggerEvent(socketElt, "htmx:wsAfterMessage", { + message: response, + socketWrapper: socketWrapper.publicInterface, + }); + }); // Put the WebSocket into the HTML Element's custom data. - api.getInternalData(socketElt).webSocket = socketWrapper + api.getInternalData(socketElt).webSocket = socketWrapper; } /** @@ -179,120 +192,138 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f /** @type {Object<string, Function[]>} */ events: {}, - addEventListener: function(event, handler) { + addEventListener: function (event, handler) { if (this.socket) { - this.socket.addEventListener(event, handler) + this.socket.addEventListener(event, handler); } if (!this.events[event]) { - this.events[event] = [] + this.events[event] = []; } - this.events[event].push(handler) + this.events[event].push(handler); }, - sendImmediately: function(message, sendElt) { + sendImmediately: function (message, sendElt) { if (!this.socket) { - api.triggerErrorEvent() + api.triggerErrorEvent(); } - if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', { - message, - socketWrapper: this.publicInterface - })) { - this.socket.send(message) - sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', { + if ( + !sendElt || + api.triggerEvent(sendElt, "htmx:wsBeforeSend", { message, - socketWrapper: this.publicInterface + socketWrapper: this.publicInterface, }) + ) { + this.socket.send(message); + sendElt && + api.triggerEvent(sendElt, "htmx:wsAfterSend", { + message, + socketWrapper: this.publicInterface, + }); } }, - send: function(message, sendElt) { + send: function (message, sendElt) { if (this.socket.readyState !== this.socket.OPEN) { - this.messageQueue.push({ message, sendElt }) + this.messageQueue.push({ message, sendElt }); } else { - this.sendImmediately(message, sendElt) + this.sendImmediately(message, sendElt); } }, - handleQueuedMessages: function() { + handleQueuedMessages: function () { while (this.messageQueue.length > 0) { - var queuedItem = this.messageQueue[0] + var queuedItem = this.messageQueue[0]; if (this.socket.readyState === this.socket.OPEN) { - this.sendImmediately(queuedItem.message, queuedItem.sendElt) - this.messageQueue.shift() + this.sendImmediately(queuedItem.message, queuedItem.sendElt); + this.messageQueue.shift(); } else { - break + break; } } }, - init: function() { + init: function () { if (this.socket && this.socket.readyState === this.socket.OPEN) { // Close discarded socket - this.socket.close() + this.socket.close(); } // Create a new WebSocket and event handlers /** @type {WebSocket} */ - var socket = socketFunc() + var socket = socketFunc(); // The event.type detail is added for interface conformance with the // other two lifecycle events (open and close) so a single handler method // can handle them polymorphically, if required. - api.triggerEvent(socketElt, 'htmx:wsConnecting', { event: { type: 'connecting' } }) - - this.socket = socket - - socket.onopen = function(e) { - wrapper.retryCount = 0 - api.triggerEvent(socketElt, 'htmx:wsOpen', { event: e, socketWrapper: wrapper.publicInterface }) - wrapper.handleQueuedMessages() - } - - socket.onclose = function(e) { + api.triggerEvent(socketElt, "htmx:wsConnecting", { + event: { type: "connecting" }, + }); + + this.socket = socket; + + socket.onopen = function (e) { + wrapper.retryCount = 0; + api.triggerEvent(socketElt, "htmx:wsOpen", { + event: e, + socketWrapper: wrapper.publicInterface, + }); + wrapper.handleQueuedMessages(); + }; + + socket.onclose = function (e) { // If socket should not be connected, stop further attempts to establish connection // If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause. - if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) { - var delay = getWebSocketReconnectDelay(wrapper.retryCount) - setTimeout(function() { - wrapper.retryCount += 1 - wrapper.init() - }, delay) + if ( + !maybeCloseWebSocketSource(socketElt) && + [1006, 1012, 1013].indexOf(e.code) >= 0 + ) { + var delay = getWebSocketReconnectDelay(wrapper.retryCount); + setTimeout(function () { + wrapper.retryCount += 1; + wrapper.init(); + }, delay); } // Notify client code that connection has been closed. Client code can inspect `event` field // to determine whether closure has been valid or abnormal - api.triggerEvent(socketElt, 'htmx:wsClose', { event: e, socketWrapper: wrapper.publicInterface }) - } - - socket.onerror = function(e) { - api.triggerErrorEvent(socketElt, 'htmx:wsError', { error: e, socketWrapper: wrapper }) - maybeCloseWebSocketSource(socketElt) - } - - var events = this.events - Object.keys(events).forEach(function(k) { - events[k].forEach(function(e) { - socket.addEventListener(k, e) - }) - }) + api.triggerEvent(socketElt, "htmx:wsClose", { + event: e, + socketWrapper: wrapper.publicInterface, + }); + }; + + socket.onerror = function (e) { + api.triggerErrorEvent(socketElt, "htmx:wsError", { + error: e, + socketWrapper: wrapper, + }); + maybeCloseWebSocketSource(socketElt); + }; + + var events = this.events; + Object.keys(events).forEach(function (k) { + events[k].forEach(function (e) { + socket.addEventListener(k, e); + }); + }); }, - close: function() { - this.socket.close() - } - } + close: function () { + this.socket.close(); + }, + }; - wrapper.init() + wrapper.init(); wrapper.publicInterface = { send: wrapper.send.bind(wrapper), sendImmediately: wrapper.sendImmediately.bind(wrapper), - queue: wrapper.messageQueue - } + queue: wrapper.messageQueue, + }; - return wrapper + return wrapper; } /** @@ -301,13 +332,13 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f * @param {HTMLElement} elt */ function ensureWebSocketSend(elt) { - var legacyAttribute = api.getAttributeValue(elt, 'hx-ws') - if (legacyAttribute && legacyAttribute !== 'send') { - return + var legacyAttribute = api.getAttributeValue(elt, "hx-ws"); + if (legacyAttribute && legacyAttribute !== "send") { + return; } - var webSocketParent = api.getClosestMatch(elt, hasWebSocket) - processWebSocketSend(webSocketParent, elt) + var webSocketParent = api.getClosestMatch(elt, hasWebSocket); + processWebSocketSend(webSocketParent, elt); } /** @@ -316,7 +347,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f * @returns {boolean} */ function hasWebSocket(node) { - return api.getInternalData(node).webSocket != null + return api.getInternalData(node).webSocket != null; } /** @@ -326,23 +357,23 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f * @param {HTMLElement} sendElt */ function processWebSocketSend(socketElt, sendElt) { - var nodeData = api.getInternalData(sendElt) - var triggerSpecs = api.getTriggerSpecs(sendElt) - triggerSpecs.forEach(function(ts) { - api.addTriggerHandler(sendElt, ts, nodeData, function(elt, evt) { + var nodeData = api.getInternalData(sendElt); + var triggerSpecs = api.getTriggerSpecs(sendElt); + triggerSpecs.forEach(function (ts) { + api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) { if (maybeCloseWebSocketSource(socketElt)) { - return + return; } /** @type {WebSocketWrapper} */ - var socketWrapper = api.getInternalData(socketElt).webSocket - var headers = api.getHeaders(sendElt, api.getTarget(sendElt)) - var results = api.getInputValues(sendElt, 'post') - var errors = results.errors - var rawParameters = Object.assign({}, results.values) - var expressionVars = api.getExpressionVars(sendElt) - var allParameters = api.mergeObjects(rawParameters, expressionVars) - var filteredParameters = api.filterValues(allParameters, sendElt) + var socketWrapper = api.getInternalData(socketElt).webSocket; + var headers = api.getHeaders(sendElt, api.getTarget(sendElt)); + var results = api.getInputValues(sendElt, "post"); + var errors = results.errors; + var rawParameters = Object.assign({}, results.values); + var expressionVars = api.getExpressionVars(sendElt); + var allParameters = api.mergeObjects(rawParameters, expressionVars); + var filteredParameters = api.filterValues(allParameters, sendElt); var sendConfig = { parameters: filteredParameters, @@ -352,32 +383,34 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f triggeringEvent: evt, messageBody: undefined, - socketWrapper: socketWrapper.publicInterface - } + socketWrapper: socketWrapper.publicInterface, + }; - if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) { - return + if (!api.triggerEvent(elt, "htmx:wsConfigSend", sendConfig)) { + return; } if (errors && errors.length > 0) { - api.triggerEvent(elt, 'htmx:validation:halted', errors) - return + api.triggerEvent(elt, "htmx:validation:halted", errors); + return; } - var body = sendConfig.messageBody + var body = sendConfig.messageBody; if (body === undefined) { - var toSend = Object.assign({}, sendConfig.parameters) - if (sendConfig.headers) { toSend.HEADERS = headers } - body = JSON.stringify(toSend) + var toSend = Object.assign({}, sendConfig.parameters); + if (sendConfig.headers) { + toSend.HEADERS = headers; + } + body = JSON.stringify(toSend); } - socketWrapper.send(body, elt) + socketWrapper.send(body, elt); if (evt && api.shouldCancel(evt, elt)) { - evt.preventDefault() + evt.preventDefault(); } - }) - }) + }); + }); } /** @@ -387,17 +420,19 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f */ function getWebSocketReconnectDelay(retryCount) { /** @type {"full-jitter" | ((retryCount:number) => number)} */ - var delay = htmx.config.wsReconnectDelay - if (typeof delay === 'function') { - return delay(retryCount) + var delay = htmx.config.wsReconnectDelay; + if (typeof delay === "function") { + return delay(retryCount); } - if (delay === 'full-jitter') { - var exp = Math.min(retryCount, 6) - var maxDelay = 1000 * Math.pow(2, exp) - return maxDelay * Math.random() + if (delay === "full-jitter") { + var exp = Math.min(retryCount, 6); + var maxDelay = 1000 * Math.pow(2, exp); + return maxDelay * Math.random(); } - logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"') + logError( + 'htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"', + ); } /** @@ -411,10 +446,10 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f */ function maybeCloseWebSocketSource(elt) { if (!api.bodyContains(elt)) { - api.getInternalData(elt).webSocket.close() - return true + api.getInternalData(elt).webSocket.close(); + return true; } - return false + return false; } /** @@ -425,9 +460,9 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f * @returns WebSocket */ function createWebSocket(url) { - var sock = new WebSocket(url, []) - sock.binaryType = htmx.config.wsBinaryType - return sock + var sock = new WebSocket(url, []); + sock.binaryType = htmx.config.wsBinaryType; + return sock; } /** @@ -437,19 +472,30 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f * @param {string} attributeName */ function queryAttributeOnThisOrChildren(elt, attributeName) { - var result = [] + var result = []; // If the parent element also contains the requested attribute, then add it to the results too. - if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, 'hx-ws')) { - result.push(elt) + if ( + api.hasAttribute(elt, attributeName) || + api.hasAttribute(elt, "hx-ws") + ) { + result.push(elt); } // Search all child nodes that match the requested attribute - elt.querySelectorAll('[' + attributeName + '], [data-' + attributeName + '], [data-hx-ws], [hx-ws]').forEach(function(node) { - result.push(node) - }) - - return result + elt + .querySelectorAll( + "[" + + attributeName + + "], [data-" + + attributeName + + "], [data-hx-ws], [hx-ws]", + ) + .forEach(function (node) { + result.push(node); + }); + + return result; } /** @@ -460,8 +506,8 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f function forEach(arr, func) { if (arr) { for (var i = 0; i < arr.length; i++) { - func(arr[i]) + func(arr[i]); } } } -})() +})(); |
