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:beforeProcessNode":
|
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 | socket: null,
|
181 | messageQueue: [],
|
182 | retryCount: 0,
|
183 |
|
184 |
|
185 | events: {},
|
186 |
|
187 | addEventListener: function (event, handler) {
|
188 | if (this.socket) {
|
189 | this.socket.addEventListener(event, handler);
|
190 | }
|
191 |
|
192 | if (!this.events[event]) {
|
193 | this.events[event] = [];
|
194 | }
|
195 |
|
196 | this.events[event].push(handler);
|
197 | },
|
198 |
|
199 | sendImmediately: function (message, sendElt) {
|
200 | if (!this.socket) {
|
201 | api.triggerErrorEvent()
|
202 | }
|
203 | if (sendElt && api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
|
204 | message: message,
|
205 | socketWrapper: this.publicInterface
|
206 | })) {
|
207 | this.socket.send(message);
|
208 | sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
|
209 | message: message,
|
210 | socketWrapper: this.publicInterface
|
211 | })
|
212 | }
|
213 | },
|
214 |
|
215 | send: function (message, sendElt) {
|
216 | if (this.socket.readyState !== this.socket.OPEN) {
|
217 | this.messageQueue.push({ message: message, sendElt: sendElt });
|
218 | } else {
|
219 | this.sendImmediately(message, sendElt);
|
220 | }
|
221 | },
|
222 |
|
223 | handleQueuedMessages: function () {
|
224 | while (this.messageQueue.length > 0) {
|
225 | var queuedItem = this.messageQueue[0]
|
226 | if (this.socket.readyState === this.socket.OPEN) {
|
227 | this.sendImmediately(queuedItem.message, queuedItem.sendElt);
|
228 | this.messageQueue.shift();
|
229 | } else {
|
230 | break;
|
231 | }
|
232 | }
|
233 | },
|
234 |
|
235 | init: function () {
|
236 | if (this.socket && this.socket.readyState === this.socket.OPEN) {
|
237 |
|
238 | this.socket.close()
|
239 | }
|
240 |
|
241 |
|
242 |
|
243 | var socket = socketFunc();
|
244 |
|
245 |
|
246 |
|
247 |
|
248 | api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
|
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 | wrapper.publicInterface = {
|
295 | send: wrapper.send.bind(wrapper),
|
296 | sendImmediately: wrapper.sendImmediately.bind(wrapper),
|
297 | queue: wrapper.messageQueue
|
298 | };
|
299 |
|
300 | return wrapper;
|
301 | }
|
302 |
|
303 | |
304 |
|
305 |
|
306 |
|
307 |
|
308 | function ensureWebSocketSend(elt) {
|
309 | var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
|
310 | if (legacyAttribute && legacyAttribute !== 'send') {
|
311 | return;
|
312 | }
|
313 |
|
314 | var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
|
315 | processWebSocketSend(webSocketParent, elt);
|
316 | }
|
317 |
|
318 | |
319 |
|
320 |
|
321 |
|
322 |
|
323 | function hasWebSocket(node) {
|
324 | return api.getInternalData(node).webSocket != null;
|
325 | }
|
326 |
|
327 | |
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 | function processWebSocketSend(socketElt, sendElt) {
|
334 | var nodeData = api.getInternalData(sendElt);
|
335 | var triggerSpecs = api.getTriggerSpecs(sendElt);
|
336 | triggerSpecs.forEach(function (ts) {
|
337 | api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
|
338 | if (maybeCloseWebSocketSource(socketElt)) {
|
339 | return;
|
340 | }
|
341 |
|
342 |
|
343 | var socketWrapper = api.getInternalData(socketElt).webSocket;
|
344 | var headers = api.getHeaders(sendElt, api.getTarget(sendElt));
|
345 | var results = api.getInputValues(sendElt, 'post');
|
346 | var errors = results.errors;
|
347 | var rawParameters = results.values;
|
348 | var expressionVars = api.getExpressionVars(sendElt);
|
349 | var allParameters = api.mergeObjects(rawParameters, expressionVars);
|
350 | var filteredParameters = api.filterValues(allParameters, sendElt);
|
351 |
|
352 | var sendConfig = {
|
353 | parameters: filteredParameters,
|
354 | unfilteredParameters: allParameters,
|
355 | headers: headers,
|
356 | errors: errors,
|
357 |
|
358 | triggeringEvent: evt,
|
359 | messageBody: undefined,
|
360 | socketWrapper: socketWrapper.publicInterface
|
361 | };
|
362 |
|
363 | if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
|
364 | return;
|
365 | }
|
366 |
|
367 | if (errors && errors.length > 0) {
|
368 | api.triggerEvent(elt, 'htmx:validation:halted', errors);
|
369 | return;
|
370 | }
|
371 |
|
372 | var body = sendConfig.messageBody;
|
373 | if (body === undefined) {
|
374 | var toSend = Object.assign({}, sendConfig.parameters);
|
375 | if (sendConfig.headers)
|
376 | toSend['HEADERS'] = headers;
|
377 | body = JSON.stringify(toSend);
|
378 | }
|
379 |
|
380 | socketWrapper.send(body, elt);
|
381 |
|
382 | if (evt && api.shouldCancel(evt, elt)) {
|
383 | evt.preventDefault();
|
384 | }
|
385 | });
|
386 | });
|
387 | }
|
388 |
|
389 | |
390 |
|
391 |
|
392 |
|
393 |
|
394 | function getWebSocketReconnectDelay(retryCount) {
|
395 |
|
396 |
|
397 | var delay = htmx.config.wsReconnectDelay;
|
398 | if (typeof delay === 'function') {
|
399 | return delay(retryCount);
|
400 | }
|
401 | if (delay === 'full-jitter') {
|
402 | var exp = Math.min(retryCount, 6);
|
403 | var maxDelay = 1000 * Math.pow(2, exp);
|
404 | return maxDelay * Math.random();
|
405 | }
|
406 |
|
407 | logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
|
408 | }
|
409 |
|
410 | |
411 |
|
412 |
|
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 |
|
419 | function maybeCloseWebSocketSource(elt) {
|
420 | if (!api.bodyContains(elt)) {
|
421 | api.getInternalData(elt).webSocket.close();
|
422 | return true;
|
423 | }
|
424 | return false;
|
425 | }
|
426 |
|
427 | |
428 |
|
429 |
|
430 |
|
431 |
|
432 |
|
433 |
|
434 | function createWebSocket(url) {
|
435 | var sock = new WebSocket(url, []);
|
436 | sock.binaryType = htmx.config.wsBinaryType;
|
437 | return sock;
|
438 | }
|
439 |
|
440 | |
441 |
|
442 |
|
443 |
|
444 |
|
445 |
|
446 | function queryAttributeOnThisOrChildren(elt, attributeName) {
|
447 |
|
448 | var result = []
|
449 |
|
450 |
|
451 | if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
|
452 | result.push(elt);
|
453 | }
|
454 |
|
455 |
|
456 | elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
|
457 | result.push(node)
|
458 | })
|
459 |
|
460 | return result
|
461 | }
|
462 |
|
463 | |
464 |
|
465 |
|
466 |
|
467 |
|
468 | function forEach(arr, func) {
|
469 | if (arr) {
|
470 | for (var i = 0; i < arr.length; i++) {
|
471 | func(arr[i]);
|
472 | }
|
473 | }
|
474 | }
|
475 |
|
476 | })();
|
477 |
|