12
htmx.defineExtension("ws", {
18
init: function (apiRef) {
24
if (!htmx.createWebSocket) {
25
htmx.createWebSocket = createWebSocket;
29
if (!htmx.config.wsReconnectDelay) {
30
htmx.config.wsReconnectDelay = "full-jitter";
40
onEvent: function (name, evt) {
41
var parent = evt.target || evt.detail.elt;
46
case "htmx:beforeCleanupElement":
48
var internalData = api.getInternalData(parent)
50
if (internalData.webSocket) {
51
internalData.webSocket.close();
56
case "htmx:beforeProcessNode":
57
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
58
ensureWebSocket(child)
60
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
61
ensureWebSocketSend(child)
67
function splitOnWhitespace(trigger) {
68
return trigger.trim().split(/\s+/);
71
function getLegacyWebsocketURL(elt) {
72
var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
74
var values = splitOnWhitespace(legacySSEValue);
75
for (var i = 0; i < values.length; i++) {
76
var value = values[i].split(/:(.+)/);
77
if (value[0] === "connect") {
90
function ensureWebSocket(socketElt) {
94
if (!api.bodyContains(socketElt)) {
99
var wssSource = api.getAttributeValue(socketElt, "ws-connect")
101
if (wssSource == null || wssSource === "") {
102
var legacySource = getLegacyWebsocketURL(socketElt);
103
if (legacySource == null) {
106
wssSource = legacySource;
111
if (wssSource.indexOf("/") === 0) {
112
var base_part = location.hostname + (location.port ? ':' + location.port : '');
113
if (location.protocol === 'https:') {
114
wssSource = "wss://" + base_part + wssSource;
115
} else if (location.protocol === 'http:') {
116
wssSource = "ws://" + base_part + wssSource;
120
var socketWrapper = createWebsocketWrapper(socketElt, function () {
121
return htmx.createWebSocket(wssSource)
124
socketWrapper.addEventListener('message', function (event) {
125
if (maybeCloseWebSocketSource(socketElt)) {
129
var response = event.data;
130
if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
132
socketWrapper: socketWrapper.publicInterface
137
api.withExtensions(socketElt, function (extension) {
138
response = extension.transformResponse(response, null, socketElt);
141
var settleInfo = api.makeSettleInfo(socketElt);
142
var fragment = api.makeFragment(response);
144
if (fragment.children.length) {
145
var children = Array.from(fragment.children);
146
for (var i = 0; i < children.length; i++) {
147
api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
151
api.settleImmediately(settleInfo.tasks);
152
api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
156
api.getInternalData(socketElt).webSocket = socketWrapper;
177
function createWebsocketWrapper(socketElt, socketFunc) {
186
addEventListener: function (event, handler) {
188
this.socket.addEventListener(event, handler);
191
if (!this.events[event]) {
192
this.events[event] = [];
195
this.events[event].push(handler);
198
sendImmediately: function (message, sendElt) {
200
api.triggerErrorEvent()
202
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
204
socketWrapper: this.publicInterface
206
this.socket.send(message);
207
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
209
socketWrapper: this.publicInterface
214
send: function (message, sendElt) {
215
if (this.socket.readyState !== this.socket.OPEN) {
216
this.messageQueue.push({ message: message, sendElt: sendElt });
218
this.sendImmediately(message, sendElt);
222
handleQueuedMessages: function () {
223
while (this.messageQueue.length > 0) {
224
var queuedItem = this.messageQueue[0]
225
if (this.socket.readyState === this.socket.OPEN) {
226
this.sendImmediately(queuedItem.message, queuedItem.sendElt);
227
this.messageQueue.shift();
235
if (this.socket && this.socket.readyState === this.socket.OPEN) {
242
var socket = socketFunc();
247
api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
249
this.socket = socket;
251
socket.onopen = function (e) {
252
wrapper.retryCount = 0;
253
api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
254
wrapper.handleQueuedMessages();
257
socket.onclose = function (e) {
260
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
261
var delay = getWebSocketReconnectDelay(wrapper.retryCount);
262
setTimeout(function () {
263
wrapper.retryCount += 1;
270
api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
273
socket.onerror = function (e) {
274
api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
275
maybeCloseWebSocketSource(socketElt);
278
var events = this.events;
279
Object.keys(events).forEach(function (k) {
280
events[k].forEach(function (e) {
281
socket.addEventListener(k, e);
293
wrapper.publicInterface = {
294
send: wrapper.send.bind(wrapper),
295
sendImmediately: wrapper.sendImmediately.bind(wrapper),
296
queue: wrapper.messageQueue
307
function ensureWebSocketSend(elt) {
308
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
309
if (legacyAttribute && legacyAttribute !== 'send') {
313
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
314
processWebSocketSend(webSocketParent, elt);
322
function hasWebSocket(node) {
323
return api.getInternalData(node).webSocket != null;
332
function processWebSocketSend(socketElt, sendElt) {
333
var nodeData = api.getInternalData(sendElt);
334
var triggerSpecs = api.getTriggerSpecs(sendElt);
335
triggerSpecs.forEach(function (ts) {
336
api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
337
if (maybeCloseWebSocketSource(socketElt)) {
342
var socketWrapper = api.getInternalData(socketElt).webSocket;
343
var headers = api.getHeaders(sendElt, api.getTarget(sendElt));
344
var results = api.getInputValues(sendElt, 'post');
345
var errors = results.errors;
346
var rawParameters = results.values;
347
var expressionVars = api.getExpressionVars(sendElt);
348
var allParameters = api.mergeObjects(rawParameters, expressionVars);
349
var filteredParameters = api.filterValues(allParameters, sendElt);
352
parameters: filteredParameters,
353
unfilteredParameters: allParameters,
357
triggeringEvent: evt,
358
messageBody: undefined,
359
socketWrapper: socketWrapper.publicInterface
362
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
366
if (errors && errors.length > 0) {
367
api.triggerEvent(elt, 'htmx:validation:halted', errors);
371
var body = sendConfig.messageBody;
372
if (body === undefined) {
373
var toSend = Object.assign({}, sendConfig.parameters);
374
if (sendConfig.headers)
375
toSend['HEADERS'] = headers;
376
body = JSON.stringify(toSend);
379
socketWrapper.send(body, elt);
381
if (evt && api.shouldCancel(evt, elt)) {
382
evt.preventDefault();
393
function getWebSocketReconnectDelay(retryCount) {
396
var delay = htmx.config.wsReconnectDelay;
397
if (typeof delay === 'function') {
398
return delay(retryCount);
400
if (delay === 'full-jitter') {
401
var exp = Math.min(retryCount, 6);
402
var maxDelay = 1000 * Math.pow(2, exp);
403
return maxDelay * Math.random();
406
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
418
function maybeCloseWebSocketSource(elt) {
419
if (!api.bodyContains(elt)) {
420
api.getInternalData(elt).webSocket.close();
433
function createWebSocket(url) {
434
var sock = new WebSocket(url, []);
435
sock.binaryType = htmx.config.wsBinaryType;
445
function queryAttributeOnThisOrChildren(elt, attributeName) {
450
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
455
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
467
function forEach(arr, func) {
469
for (var i = 0; i < arr.length; i++) {