1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | (function () {
|
8 |
|
9 |
|
10 | var api;
|
11 |
|
12 | htmx.defineExtension("ws", {
|
13 |
|
14 | |
15 |
|
16 |
|
17 |
|
18 | init: function (apiRef) {
|
19 |
|
20 |
|
21 | api = apiRef;
|
22 |
|
23 |
|
24 | if (!htmx.createWebSocket) {
|
25 | htmx.createWebSocket = createWebSocket;
|
26 | }
|
27 |
|
28 |
|
29 | if (!htmx.config.wsReconnectDelay) {
|
30 | htmx.config.wsReconnectDelay = "full-jitter";
|
31 | }
|
32 | },
|
33 |
|
34 | |
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | onEvent: function (name, evt) {
|
41 |
|
42 | switch (name) {
|
43 |
|
44 |
|
45 | case "htmx:beforeCleanupElement":
|
46 |
|
47 | var internalData = api.getInternalData(evt.target)
|
48 |
|
49 | if (internalData.webSocket) {
|
50 | internalData.webSocket.close();
|
51 | }
|
52 | return;
|
53 |
|
54 |
|
55 | case "htmx:afterProcessNode":
|
56 | var parent = evt.target;
|
57 |
|
58 | forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
|
59 | ensureWebSocket(child)
|
60 | });
|
61 | forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
|
62 | ensureWebSocketSend(child)
|
63 | });
|
64 | }
|
65 | }
|
66 | });
|
67 |
|
68 | function splitOnWhitespace(trigger) {
|
69 | return trigger.trim().split(/\s+/);
|
70 | }
|
71 |
|
72 | function getLegacyWebsocketURL(elt) {
|
73 | var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
|
74 | if (legacySSEValue) {
|
75 | var values = splitOnWhitespace(legacySSEValue);
|
76 | for (var i = 0; i < values.length; i++) {
|
77 | var value = values[i].split(/:(.+)/);
|
78 | if (value[0] === "connect") {
|
79 | return value[1];
|
80 | }
|
81 | }
|
82 | }
|
83 | }
|
84 |
|
85 | |
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 | function ensureWebSocket(socketElt) {
|
92 |
|
93 |
|
94 |
|
95 | if (!api.bodyContains(socketElt)) {
|
96 | return;
|
97 | }
|
98 |
|
99 |
|
100 | var wssSource = api.getAttributeValue(socketElt, "ws-connect")
|
101 |
|
102 | if (wssSource == null || wssSource === "") {
|
103 | var legacySource = getLegacyWebsocketURL(socketElt);
|
104 | if (legacySource == null) {
|
105 | return;
|
106 | } else {
|
107 | wssSource = legacySource;
|
108 | }
|
109 | }
|
110 |
|
111 |
|
112 | if (wssSource.indexOf("/") === 0) {
|
113 | var base_part = location.hostname + (location.port ? ':' + location.port : '');
|
114 | if (location.protocol === 'https:') {
|
115 | wssSource = "wss://" + base_part + wssSource;
|
116 | } else if (location.protocol === 'http:') {
|
117 | wssSource = "ws://" + base_part + wssSource;
|
118 | }
|
119 | }
|
120 |
|
121 | var socketWrapper = createWebsocketWrapper(socketElt, function () {
|
122 | return htmx.createWebSocket(wssSource)
|
123 | });
|
124 |
|
125 | socketWrapper.addEventListener('message', function (event) {
|
126 | if (maybeCloseWebSocketSource(socketElt)) {
|
127 | return;
|
128 | }
|
129 |
|
130 | var response = event.data;
|
131 | if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
|
132 | message: response,
|
133 | socketWrapper: socketWrapper.publicInterface
|
134 | })) {
|
135 | return;
|
136 | }
|
137 |
|
138 | api.withExtensions(socketElt, function (extension) {
|
139 | response = extension.transformResponse(response, null, socketElt);
|
140 | });
|
141 |
|
142 | var settleInfo = api.makeSettleInfo(socketElt);
|
143 | var fragment = api.makeFragment(response);
|
144 |
|
145 | if (fragment.children.length) {
|
146 | var children = Array.from(fragment.children);
|
147 | for (var i = 0; i < children.length; i++) {
|
148 | api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
|
149 | }
|
150 | }
|
151 |
|
152 | api.settleImmediately(settleInfo.tasks);
|
153 | api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
|
154 | });
|
155 |
|
156 |
|
157 | api.getInternalData(socketElt).webSocket = socketWrapper;
|
158 | }
|
159 |
|
160 | |
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
172 | |
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 | function createWebsocketWrapper(socketElt, socketFunc) {
|
179 | var wrapper = {
|
180 | publicInterface: {
|
181 | send: this.send,
|
182 | sendImmediately: this.sendImmediately,
|
183 | queue: this.queue
|
184 | },
|
185 | socket: null,
|
186 | messageQueue: [],
|
187 | retryCount: 0,
|
188 |
|
189 |
|
190 | events: {},
|
191 |
|
192 | addEventListener: function (event, handler) {
|
193 | if (this.socket) {
|
194 | this.socket.addEventListener(event, handler);
|
195 | }
|
196 |
|
197 | if (!this.events[event]) {
|
198 | this.events[event] = [];
|
199 | }
|
200 |
|
201 | this.events[event].push(handler);
|
202 | },
|
203 |
|
204 | sendImmediately: function (message, sendElt) {
|
205 | if (!this.socket) {
|
206 | api.triggerErrorEvent()
|
207 | }
|
208 | if (sendElt && api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
|
209 | message: message,
|
210 | socketWrapper: this.publicInterface
|
211 | })) {
|
212 | this.socket.send(message);
|
213 | sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
|
214 | message: message,
|
215 | socketWrapper: this.publicInterface
|
216 | })
|
217 | }
|
218 | },
|
219 |
|
220 | send: function (message, sendElt) {
|
221 | if (this.socket.readyState !== this.socket.OPEN) {
|
222 | this.messageQueue.push({ message: message, sendElt: sendElt });
|
223 | } else {
|
224 | this.sendImmediately(message, sendElt);
|
225 | }
|
226 | },
|
227 |
|
228 | handleQueuedMessages: function () {
|
229 | while (this.messageQueue.length > 0) {
|
230 | var queuedItem = this.messageQueue[0]
|
231 | if (this.socket.readyState === this.socket.OPEN) {
|
232 | this.sendImmediately(queuedItem.message, queuedItem.sendElt);
|
233 | this.messageQueue.shift();
|
234 | } else {
|
235 | break;
|
236 | }
|
237 | }
|
238 | },
|
239 |
|
240 | init: function () {
|
241 | if (this.socket && this.socket.readyState === this.socket.OPEN) {
|
242 |
|
243 | this.socket.close()
|
244 | }
|
245 |
|
246 |
|
247 |
|
248 | var socket = socketFunc();
|
249 |
|
250 | this.socket = socket;
|
251 |
|
252 | socket.onopen = function (e) {
|
253 | wrapper.retryCount = 0;
|
254 | api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
|
255 | wrapper.handleQueuedMessages();
|
256 | }
|
257 |
|
258 | socket.onclose = function (e) {
|
259 |
|
260 |
|
261 | if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
|
262 | var delay = getWebSocketReconnectDelay(wrapper.retryCount);
|
263 | setTimeout(function () {
|
264 | wrapper.retryCount += 1;
|
265 | wrapper.init();
|
266 | }, delay);
|
267 | }
|
268 |
|
269 |
|
270 |
|
271 | api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
|
272 | };
|
273 |
|
274 | socket.onerror = function (e) {
|
275 | api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
|
276 | maybeCloseWebSocketSource(socketElt);
|
277 | };
|
278 |
|
279 | var events = this.events;
|
280 | Object.keys(events).forEach(function (k) {
|
281 | events[k].forEach(function (e) {
|
282 | socket.addEventListener(k, e);
|
283 | })
|
284 | });
|
285 | },
|
286 |
|
287 | close: function () {
|
288 | this.socket.close()
|
289 | }
|
290 | }
|
291 |
|
292 | wrapper.init();
|
293 |
|
294 | return wrapper;
|
295 | }
|
296 |
|
297 | |
298 |
|
299 |
|
300 |
|
301 |
|
302 | function ensureWebSocketSend(elt) {
|
303 | var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
|
304 | if (legacyAttribute && legacyAttribute !== 'send') {
|
305 | return;
|
306 | }
|
307 |
|
308 | var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
|
309 | processWebSocketSend(webSocketParent, elt);
|
310 | }
|
311 |
|
312 | |
313 |
|
314 |
|
315 |
|
316 |
|
317 | function hasWebSocket(node) {
|
318 | return api.getInternalData(node).webSocket != null;
|
319 | }
|
320 |
|
321 | |
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 | function processWebSocketSend(socketElt, sendElt) {
|
328 | var nodeData = api.getInternalData(sendElt);
|
329 | var triggerSpecs = api.getTriggerSpecs(sendElt);
|
330 | triggerSpecs.forEach(function (ts) {
|
331 | api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
|
332 | if (maybeCloseWebSocketSource(socketElt)) {
|
333 | return;
|
334 | }
|
335 |
|
336 |
|
337 | var socketWrapper = api.getInternalData(socketElt).webSocket;
|
338 | var headers = api.getHeaders(sendElt, socketElt);
|
339 | var results = api.getInputValues(sendElt, 'post');
|
340 | var errors = results.errors;
|
341 | var rawParameters = results.values;
|
342 | var expressionVars = api.getExpressionVars(sendElt);
|
343 | var allParameters = api.mergeObjects(rawParameters, expressionVars);
|
344 | var filteredParameters = api.filterValues(allParameters, sendElt);
|
345 |
|
346 | var sendConfig = {
|
347 | parameters: filteredParameters,
|
348 | unfilteredParameters: allParameters,
|
349 | headers: headers,
|
350 | errors: errors,
|
351 |
|
352 | triggeringEvent: evt,
|
353 | messageBody: undefined,
|
354 | socketWrapper: socketWrapper.publicInterface
|
355 | };
|
356 |
|
357 | if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
|
358 | return;
|
359 | }
|
360 |
|
361 | if (errors && errors.length > 0) {
|
362 | api.triggerEvent(elt, 'htmx:validation:halted', errors);
|
363 | return;
|
364 | }
|
365 |
|
366 | var body = sendConfig.messageBody;
|
367 | if (body === undefined) {
|
368 | var toSend = Object.assign({}, sendConfig.parameters);
|
369 | if (sendConfig.headers)
|
370 | toSend['HEADERS'] = headers;
|
371 | body = JSON.stringify(toSend);
|
372 | }
|
373 |
|
374 | socketWrapper.send(body, elt);
|
375 |
|
376 | if (api.shouldCancel(evt, elt)) {
|
377 | evt.preventDefault();
|
378 | }
|
379 | });
|
380 | });
|
381 | }
|
382 |
|
383 | |
384 |
|
385 |
|
386 |
|
387 |
|
388 | function getWebSocketReconnectDelay(retryCount) {
|
389 |
|
390 |
|
391 | var delay = htmx.config.wsReconnectDelay;
|
392 | if (typeof delay === 'function') {
|
393 | return delay(retryCount);
|
394 | }
|
395 | if (delay === 'full-jitter') {
|
396 | var exp = Math.min(retryCount, 6);
|
397 | var maxDelay = 1000 * Math.pow(2, exp);
|
398 | return maxDelay * Math.random();
|
399 | }
|
400 |
|
401 | logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
|
402 | }
|
403 |
|
404 | |
405 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 |
|
411 |
|
412 |
|
413 | function maybeCloseWebSocketSource(elt) {
|
414 | if (!api.bodyContains(elt)) {
|
415 | api.getInternalData(elt).webSocket.close();
|
416 | return true;
|
417 | }
|
418 | return false;
|
419 | }
|
420 |
|
421 | |
422 |
|
423 |
|
424 |
|
425 |
|
426 |
|
427 |
|
428 | function createWebSocket(url) {
|
429 | var sock = new WebSocket(url, []);
|
430 | sock.binaryType = htmx.config.wsBinaryType;
|
431 | return sock;
|
432 | }
|
433 |
|
434 | |
435 |
|
436 |
|
437 |
|
438 |
|
439 |
|
440 | function queryAttributeOnThisOrChildren(elt, attributeName) {
|
441 |
|
442 | var result = []
|
443 |
|
444 |
|
445 | if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
|
446 | result.push(elt);
|
447 | }
|
448 |
|
449 |
|
450 | elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
|
451 | result.push(node)
|
452 | })
|
453 |
|
454 | return result
|
455 | }
|
456 |
|
457 | |
458 |
|
459 |
|
460 |
|
461 |
|
462 | function forEach(arr, func) {
|
463 | if (arr) {
|
464 | for (var i = 0; i < arr.length; i++) {
|
465 | func(arr[i]);
|
466 | }
|
467 | }
|
468 | }
|
469 |
|
470 | })();
|
471 |
|