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 | var parent = evt.target || evt.detail.elt;
|
42 |
|
43 | switch (name) {
|
44 |
|
45 |
|
46 | case "htmx:beforeCleanupElement":
|
47 |
|
48 | var internalData = api.getInternalData(parent)
|
49 |
|
50 | if (internalData.webSocket) {
|
51 | internalData.webSocket.close();
|
52 | }
|
53 | return;
|
54 |
|
55 |
|
56 | case "htmx:beforeProcessNode":
|
57 | forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
|
58 | ensureWebSocket(child)
|
59 | });
|
60 | forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
|
61 | ensureWebSocketSend(child)
|
62 | });
|
63 | }
|
64 | }
|
65 | });
|
66 |
|
67 | function splitOnWhitespace(trigger) {
|
68 | return trigger.trim().split(/\s+/);
|
69 | }
|
70 |
|
71 | function getLegacyWebsocketURL(elt) {
|
72 | var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
|
73 | if (legacySSEValue) {
|
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") {
|
78 | return value[1];
|
79 | }
|
80 | }
|
81 | }
|
82 | }
|
83 |
|
84 | |
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 | function ensureWebSocket(socketElt) {
|
91 |
|
92 |
|
93 |
|
94 | if (!api.bodyContains(socketElt)) {
|
95 | return;
|
96 | }
|
97 |
|
98 |
|
99 | var wssSource = api.getAttributeValue(socketElt, "ws-connect")
|
100 |
|
101 | if (wssSource == null || wssSource === "") {
|
102 | var legacySource = getLegacyWebsocketURL(socketElt);
|
103 | if (legacySource == null) {
|
104 | return;
|
105 | } else {
|
106 | wssSource = legacySource;
|
107 | }
|
108 | }
|
109 |
|
110 |
|
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;
|
117 | }
|
118 | }
|
119 |
|
120 | var socketWrapper = createWebsocketWrapper(socketElt, function () {
|
121 | return htmx.createWebSocket(wssSource)
|
122 | });
|
123 |
|
124 | socketWrapper.addEventListener('message', function (event) {
|
125 | if (maybeCloseWebSocketSource(socketElt)) {
|
126 | return;
|
127 | }
|
128 |
|
129 | var response = event.data;
|
130 | if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
|
131 | message: response,
|
132 | socketWrapper: socketWrapper.publicInterface
|
133 | })) {
|
134 | return;
|
135 | }
|
136 |
|
137 | api.withExtensions(socketElt, function (extension) {
|
138 | response = extension.transformResponse(response, null, socketElt);
|
139 | });
|
140 |
|
141 | var settleInfo = api.makeSettleInfo(socketElt);
|
142 | var fragment = api.makeFragment(response);
|
143 |
|
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);
|
148 | }
|
149 | }
|
150 |
|
151 | api.settleImmediately(settleInfo.tasks);
|
152 | api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
|
153 | });
|
154 |
|
155 |
|
156 | api.getInternalData(socketElt).webSocket = socketWrapper;
|
157 | }
|
158 |
|
159 | |
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 | |
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 | function createWebsocketWrapper(socketElt, socketFunc) {
|
178 | var wrapper = {
|
179 | socket: null,
|
180 | messageQueue: [],
|
181 | retryCount: 0,
|
182 |
|
183 |
|
184 | events: {},
|
185 |
|
186 | addEventListener: function (event, handler) {
|
187 | if (this.socket) {
|
188 | this.socket.addEventListener(event, handler);
|
189 | }
|
190 |
|
191 | if (!this.events[event]) {
|
192 | this.events[event] = [];
|
193 | }
|
194 |
|
195 | this.events[event].push(handler);
|
196 | },
|
197 |
|
198 | sendImmediately: function (message, sendElt) {
|
199 | if (!this.socket) {
|
200 | api.triggerErrorEvent()
|
201 | }
|
202 | if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
|
203 | message: message,
|
204 | socketWrapper: this.publicInterface
|
205 | })) {
|
206 | this.socket.send(message);
|
207 | sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
|
208 | message: message,
|
209 | socketWrapper: this.publicInterface
|
210 | })
|
211 | }
|
212 | },
|
213 |
|
214 | send: function (message, sendElt) {
|
215 | if (this.socket.readyState !== this.socket.OPEN) {
|
216 | this.messageQueue.push({ message: message, sendElt: sendElt });
|
217 | } else {
|
218 | this.sendImmediately(message, sendElt);
|
219 | }
|
220 | },
|
221 |
|
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();
|
228 | } else {
|
229 | break;
|
230 | }
|
231 | }
|
232 | },
|
233 |
|
234 | init: function () {
|
235 | if (this.socket && this.socket.readyState === this.socket.OPEN) {
|
236 |
|
237 | this.socket.close()
|
238 | }
|
239 |
|
240 |
|
241 |
|
242 | var socket = socketFunc();
|
243 |
|
244 |
|
245 |
|
246 |
|
247 | api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
|
248 |
|
249 | this.socket = socket;
|
250 |
|
251 | socket.onopen = function (e) {
|
252 | wrapper.retryCount = 0;
|
253 | api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
|
254 | wrapper.handleQueuedMessages();
|
255 | }
|
256 |
|
257 | socket.onclose = function (e) {
|
258 |
|
259 |
|
260 | if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
|
261 | var delay = getWebSocketReconnectDelay(wrapper.retryCount);
|
262 | setTimeout(function () {
|
263 | wrapper.retryCount += 1;
|
264 | wrapper.init();
|
265 | }, delay);
|
266 | }
|
267 |
|
268 |
|
269 |
|
270 | api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
|
271 | };
|
272 |
|
273 | socket.onerror = function (e) {
|
274 | api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
|
275 | maybeCloseWebSocketSource(socketElt);
|
276 | };
|
277 |
|
278 | var events = this.events;
|
279 | Object.keys(events).forEach(function (k) {
|
280 | events[k].forEach(function (e) {
|
281 | socket.addEventListener(k, e);
|
282 | })
|
283 | });
|
284 | },
|
285 |
|
286 | close: function () {
|
287 | this.socket.close()
|
288 | }
|
289 | }
|
290 |
|
291 | wrapper.init();
|
292 |
|
293 | wrapper.publicInterface = {
|
294 | send: wrapper.send.bind(wrapper),
|
295 | sendImmediately: wrapper.sendImmediately.bind(wrapper),
|
296 | queue: wrapper.messageQueue
|
297 | };
|
298 |
|
299 | return wrapper;
|
300 | }
|
301 |
|
302 | |
303 |
|
304 |
|
305 |
|
306 |
|
307 | function ensureWebSocketSend(elt) {
|
308 | var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
|
309 | if (legacyAttribute && legacyAttribute !== 'send') {
|
310 | return;
|
311 | }
|
312 |
|
313 | var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
|
314 | processWebSocketSend(webSocketParent, elt);
|
315 | }
|
316 |
|
317 | |
318 |
|
319 |
|
320 |
|
321 |
|
322 | function hasWebSocket(node) {
|
323 | return api.getInternalData(node).webSocket != null;
|
324 | }
|
325 |
|
326 | |
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
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)) {
|
338 | return;
|
339 | }
|
340 |
|
341 |
|
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);
|
350 |
|
351 | var sendConfig = {
|
352 | parameters: filteredParameters,
|
353 | unfilteredParameters: allParameters,
|
354 | headers: headers,
|
355 | errors: errors,
|
356 |
|
357 | triggeringEvent: evt,
|
358 | messageBody: undefined,
|
359 | socketWrapper: socketWrapper.publicInterface
|
360 | };
|
361 |
|
362 | if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
|
363 | return;
|
364 | }
|
365 |
|
366 | if (errors && errors.length > 0) {
|
367 | api.triggerEvent(elt, 'htmx:validation:halted', errors);
|
368 | return;
|
369 | }
|
370 |
|
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);
|
377 | }
|
378 |
|
379 | socketWrapper.send(body, elt);
|
380 |
|
381 | if (evt && api.shouldCancel(evt, elt)) {
|
382 | evt.preventDefault();
|
383 | }
|
384 | });
|
385 | });
|
386 | }
|
387 |
|
388 | |
389 |
|
390 |
|
391 |
|
392 |
|
393 | function getWebSocketReconnectDelay(retryCount) {
|
394 |
|
395 |
|
396 | var delay = htmx.config.wsReconnectDelay;
|
397 | if (typeof delay === 'function') {
|
398 | return delay(retryCount);
|
399 | }
|
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();
|
404 | }
|
405 |
|
406 | logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
|
407 | }
|
408 |
|
409 | |
410 |
|
411 |
|
412 |
|
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 | function maybeCloseWebSocketSource(elt) {
|
419 | if (!api.bodyContains(elt)) {
|
420 | api.getInternalData(elt).webSocket.close();
|
421 | return true;
|
422 | }
|
423 | return false;
|
424 | }
|
425 |
|
426 | |
427 |
|
428 |
|
429 |
|
430 |
|
431 |
|
432 |
|
433 | function createWebSocket(url) {
|
434 | var sock = new WebSocket(url, []);
|
435 | sock.binaryType = htmx.config.wsBinaryType;
|
436 | return sock;
|
437 | }
|
438 |
|
439 | |
440 |
|
441 |
|
442 |
|
443 |
|
444 |
|
445 | function queryAttributeOnThisOrChildren(elt, attributeName) {
|
446 |
|
447 | var result = []
|
448 |
|
449 |
|
450 | if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
|
451 | result.push(elt);
|
452 | }
|
453 |
|
454 |
|
455 | elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
|
456 | result.push(node)
|
457 | })
|
458 |
|
459 | return result
|
460 | }
|
461 |
|
462 | |
463 |
|
464 |
|
465 |
|
466 |
|
467 | function forEach(arr, func) {
|
468 | if (arr) {
|
469 | for (var i = 0; i < arr.length; i++) {
|
470 | func(arr[i]);
|
471 | }
|
472 | }
|
473 | }
|
474 |
|
475 | })();
|
476 |
|