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 == undefined) {
|
25 | htmx.createWebSocket = createWebSocket;
|
26 | }
|
27 |
|
28 |
|
29 | if (htmx.config.wsReconnectDelay == undefined) {
|
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 != undefined) {
|
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 |
|
92 | function ensureWebSocket(elt, retryCount) {
|
93 |
|
94 |
|
95 |
|
96 | if (!api.bodyContains(elt)) {
|
97 | return;
|
98 | }
|
99 |
|
100 |
|
101 | var wssSource = api.getAttributeValue(elt, "ws-connect")
|
102 |
|
103 | if (wssSource == null || wssSource === "") {
|
104 | var legacySource = getLegacyWebsocketURL(elt);
|
105 | if (legacySource == null) {
|
106 | return;
|
107 | } else {
|
108 | wssSource = legacySource;
|
109 | }
|
110 | }
|
111 |
|
112 |
|
113 | if (retryCount == undefined) {
|
114 | retryCount = 0;
|
115 | }
|
116 |
|
117 |
|
118 | if (wssSource.indexOf("/") == 0) {
|
119 | var base_part = location.hostname + (location.port ? ':'+location.port: '');
|
120 | if (location.protocol == 'https:') {
|
121 | wssSource = "wss://" + base_part + wssSource;
|
122 | } else if (location.protocol == 'http:') {
|
123 | wssSource = "ws://" + base_part + wssSource;
|
124 | }
|
125 | }
|
126 |
|
127 |
|
128 |
|
129 | var socket = htmx.createWebSocket(wssSource);
|
130 |
|
131 | var messageQueue = [];
|
132 |
|
133 | socket.onopen = function (e) {
|
134 | retryCount = 0;
|
135 | handleQueuedMessages(messageQueue, socket);
|
136 | }
|
137 |
|
138 | socket.onclose = function (e) {
|
139 |
|
140 | if ([1006, 1012, 1013].indexOf(e.code) >= 0) {
|
141 | var delay = getWebSocketReconnectDelay(retryCount);
|
142 | setTimeout(function() {
|
143 | ensureWebSocket(elt, retryCount+1);
|
144 | }, delay);
|
145 | }
|
146 | };
|
147 |
|
148 | socket.onerror = function (e) {
|
149 | api.triggerErrorEvent(elt, "htmx:wsError", {error:e, socket:socket});
|
150 | maybeCloseWebSocketSource(elt);
|
151 | };
|
152 |
|
153 | socket.addEventListener('message', function (event) {
|
154 | if (maybeCloseWebSocketSource(elt)) {
|
155 | return;
|
156 | }
|
157 |
|
158 | var response = event.data;
|
159 | api.withExtensions(elt, function(extension){
|
160 | response = extension.transformResponse(response, null, elt);
|
161 | });
|
162 |
|
163 | var settleInfo = api.makeSettleInfo(elt);
|
164 | var fragment = api.makeFragment(response);
|
165 |
|
166 | if (fragment.children.length) {
|
167 | var children = Array.from(fragment.children);
|
168 | for (var i = 0; i < children.length; i++) {
|
169 | api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
|
170 | }
|
171 | }
|
172 |
|
173 | api.settleImmediately(settleInfo.tasks);
|
174 | });
|
175 |
|
176 |
|
177 | api.getInternalData(elt).webSocket = socket;
|
178 | api.getInternalData(elt).webSocketMessageQueue = messageQueue;
|
179 | }
|
180 |
|
181 | |
182 |
|
183 |
|
184 |
|
185 |
|
186 | function ensureWebSocketSend(elt) {
|
187 | var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
|
188 | if (legacyAttribute && legacyAttribute !== 'send') {
|
189 | return;
|
190 | }
|
191 |
|
192 | var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
|
193 | processWebSocketSend(webSocketParent, elt);
|
194 | }
|
195 |
|
196 | |
197 |
|
198 |
|
199 |
|
200 |
|
201 | function hasWebSocket(node) {
|
202 | return api.getInternalData(node).webSocket != null;
|
203 | }
|
204 |
|
205 | |
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 | function processWebSocketSend(parent, child) {
|
212 | var nodeData = api.getInternalData(child);
|
213 | let triggerSpecs = api.getTriggerSpecs(child);
|
214 | triggerSpecs.forEach(function(ts) {
|
215 | api.addTriggerHandler(child, ts, nodeData, function (evt) {
|
216 | var webSocket = api.getInternalData(parent).webSocket;
|
217 | var messageQueue = api.getInternalData(parent).webSocketMessageQueue;
|
218 | var headers = api.getHeaders(child, parent);
|
219 | var results = api.getInputValues(child, 'post');
|
220 | var errors = results.errors;
|
221 | var rawParameters = results.values;
|
222 | var expressionVars = api.getExpressionVars(child);
|
223 | var allParameters = api.mergeObjects(rawParameters, expressionVars);
|
224 | var filteredParameters = api.filterValues(allParameters, child);
|
225 | filteredParameters['HEADERS'] = headers;
|
226 | if (errors && errors.length > 0) {
|
227 | api.triggerEvent(child, 'htmx:validation:halted', errors);
|
228 | return;
|
229 | }
|
230 | webSocketSend(webSocket, JSON.stringify(filteredParameters), messageQueue);
|
231 | if(api.shouldCancel(evt, child)){
|
232 | evt.preventDefault();
|
233 | }
|
234 | });
|
235 | });
|
236 | }
|
237 |
|
238 | |
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 | function webSocketSend(socket, message, messageQueue) {
|
247 | if (socket.readyState != socket.OPEN) {
|
248 | messageQueue.push(message);
|
249 | } else {
|
250 | socket.send(message);
|
251 | }
|
252 | }
|
253 |
|
254 | |
255 |
|
256 |
|
257 | function handleQueuedMessages(messageQueue, socket) {
|
258 | while (messageQueue.length > 0) {
|
259 | var message = messageQueue[0]
|
260 | if (socket.readyState == socket.OPEN) {
|
261 | socket.send(message);
|
262 | messageQueue.shift()
|
263 | } else {
|
264 | break;
|
265 | }
|
266 | }
|
267 | }
|
268 |
|
269 | |
270 |
|
271 |
|
272 |
|
273 |
|
274 | function getWebSocketReconnectDelay(retryCount) {
|
275 |
|
276 |
|
277 | var delay = htmx.config.wsReconnectDelay;
|
278 | if (typeof delay === 'function') {
|
279 | return delay(retryCount);
|
280 | }
|
281 | if (delay === 'full-jitter') {
|
282 | var exp = Math.min(retryCount, 6);
|
283 | var maxDelay = 1000 * Math.pow(2, exp);
|
284 | return maxDelay * Math.random();
|
285 | }
|
286 |
|
287 | logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
|
288 | }
|
289 |
|
290 | |
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 | function maybeCloseWebSocketSource(elt) {
|
300 | if (!api.bodyContains(elt)) {
|
301 | api.getInternalData(elt).webSocket.close();
|
302 | return true;
|
303 | }
|
304 | return false;
|
305 | }
|
306 |
|
307 | |
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 | function createWebSocket(url){
|
315 | return new WebSocket(url, []);
|
316 | }
|
317 |
|
318 | |
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 | function queryAttributeOnThisOrChildren(elt, attributeName) {
|
325 |
|
326 | var result = []
|
327 |
|
328 |
|
329 | if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
|
330 | result.push(elt);
|
331 | }
|
332 |
|
333 |
|
334 | elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function(node) {
|
335 | result.push(node)
|
336 | })
|
337 |
|
338 | return result
|
339 | }
|
340 |
|
341 | |
342 |
|
343 |
|
344 |
|
345 |
|
346 | function forEach(arr, func) {
|
347 | if (arr) {
|
348 | for (var i = 0; i < arr.length; i++) {
|
349 | func(arr[i]);
|
350 | }
|
351 | }
|
352 | }
|
353 |
|
354 | })();
|
355 |
|