djhtmx 1.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,467 @@
1
+ /*
2
+ WebSockets Extension
3
+ ============================
4
+ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
5
+ */
6
+
7
+ (function() {
8
+ /** @type {import("../htmx").HtmxInternalApi} */
9
+ var api
10
+
11
+ htmx.defineExtension('ws', {
12
+
13
+ /**
14
+ * init is called once, when this extension is first registered.
15
+ * @param {import("../htmx").HtmxInternalApi} apiRef
16
+ */
17
+ init: function(apiRef) {
18
+ // Store reference to internal API
19
+ api = apiRef
20
+
21
+ // Default function for creating new EventSource objects
22
+ if (!htmx.createWebSocket) {
23
+ htmx.createWebSocket = createWebSocket
24
+ }
25
+
26
+ // Default setting for reconnect delay
27
+ if (!htmx.config.wsReconnectDelay) {
28
+ htmx.config.wsReconnectDelay = 'full-jitter'
29
+ }
30
+ },
31
+
32
+ /**
33
+ * onEvent handles all events passed to this extension.
34
+ *
35
+ * @param {string} name
36
+ * @param {Event} evt
37
+ */
38
+ onEvent: function(name, evt) {
39
+ var parent = evt.target || evt.detail.elt
40
+ switch (name) {
41
+ // Try to close the socket when elements are removed
42
+ case 'htmx:beforeCleanupElement':
43
+
44
+ var internalData = api.getInternalData(parent)
45
+
46
+ if (internalData.webSocket) {
47
+ internalData.webSocket.close()
48
+ }
49
+ return
50
+
51
+ // Try to create websockets when elements are processed
52
+ case 'htmx:beforeProcessNode':
53
+
54
+ forEach(queryAttributeOnThisOrChildren(parent, 'ws-connect'), function(child) {
55
+ ensureWebSocket(child)
56
+ })
57
+ forEach(queryAttributeOnThisOrChildren(parent, 'ws-send'), function(child) {
58
+ ensureWebSocketSend(child)
59
+ })
60
+ }
61
+ }
62
+ })
63
+
64
+ function splitOnWhitespace(trigger) {
65
+ return trigger.trim().split(/\s+/)
66
+ }
67
+
68
+ function getLegacyWebsocketURL(elt) {
69
+ var legacySSEValue = api.getAttributeValue(elt, 'hx-ws')
70
+ if (legacySSEValue) {
71
+ var values = splitOnWhitespace(legacySSEValue)
72
+ for (var i = 0; i < values.length; i++) {
73
+ var value = values[i].split(/:(.+)/)
74
+ if (value[0] === 'connect') {
75
+ return value[1]
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ /**
82
+ * ensureWebSocket creates a new WebSocket on the designated element, using
83
+ * the element's "ws-connect" attribute.
84
+ * @param {HTMLElement} socketElt
85
+ * @returns
86
+ */
87
+ function ensureWebSocket(socketElt) {
88
+ // If the element containing the WebSocket connection no longer exists, then
89
+ // do not connect/reconnect the WebSocket.
90
+ if (!api.bodyContains(socketElt)) {
91
+ return
92
+ }
93
+
94
+ // Get the source straight from the element's value
95
+ var wssSource = api.getAttributeValue(socketElt, 'ws-connect')
96
+
97
+ if (wssSource == null || wssSource === '') {
98
+ var legacySource = getLegacyWebsocketURL(socketElt)
99
+ if (legacySource == null) {
100
+ return
101
+ } else {
102
+ wssSource = legacySource
103
+ }
104
+ }
105
+
106
+ // Guarantee that the wssSource value is a fully qualified URL
107
+ if (wssSource.indexOf('/') === 0) {
108
+ var base_part = location.hostname + (location.port ? ':' + location.port : '')
109
+ if (location.protocol === 'https:') {
110
+ wssSource = 'wss://' + base_part + wssSource
111
+ } else if (location.protocol === 'http:') {
112
+ wssSource = 'ws://' + base_part + wssSource
113
+ }
114
+ }
115
+
116
+ var socketWrapper = createWebsocketWrapper(socketElt, function() {
117
+ return htmx.createWebSocket(wssSource)
118
+ })
119
+
120
+ socketWrapper.addEventListener('message', function(event) {
121
+ if (maybeCloseWebSocketSource(socketElt)) {
122
+ return
123
+ }
124
+
125
+ var response = event.data
126
+ if (!api.triggerEvent(socketElt, 'htmx:wsBeforeMessage', {
127
+ message: response,
128
+ socketWrapper: socketWrapper.publicInterface
129
+ })) {
130
+ return
131
+ }
132
+
133
+ api.withExtensions(socketElt, function(extension) {
134
+ response = extension.transformResponse(response, null, socketElt)
135
+ })
136
+
137
+ var settleInfo = api.makeSettleInfo(socketElt)
138
+ var fragment = api.makeFragment(response)
139
+
140
+ if (fragment.children.length) {
141
+ var children = Array.from(fragment.children)
142
+ for (var i = 0; i < children.length; i++) {
143
+ api.oobSwap(api.getAttributeValue(children[i], 'hx-swap-oob') || 'true', children[i], settleInfo)
144
+ }
145
+ }
146
+
147
+ api.settleImmediately(settleInfo.tasks)
148
+ api.triggerEvent(socketElt, 'htmx:wsAfterMessage', { message: response, socketWrapper: socketWrapper.publicInterface })
149
+ })
150
+
151
+ // Put the WebSocket into the HTML Element's custom data.
152
+ api.getInternalData(socketElt).webSocket = socketWrapper
153
+ }
154
+
155
+ /**
156
+ * @typedef {Object} WebSocketWrapper
157
+ * @property {WebSocket} socket
158
+ * @property {Array<{message: string, sendElt: Element}>} messageQueue
159
+ * @property {number} retryCount
160
+ * @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
161
+ * @property {(message: string, sendElt: Element) => void} send
162
+ * @property {(event: string, handler: Function) => void} addEventListener
163
+ * @property {() => void} handleQueuedMessages
164
+ * @property {() => void} init
165
+ * @property {() => void} close
166
+ */
167
+ /**
168
+ *
169
+ * @param socketElt
170
+ * @param socketFunc
171
+ * @returns {WebSocketWrapper}
172
+ */
173
+ function createWebsocketWrapper(socketElt, socketFunc) {
174
+ var wrapper = {
175
+ socket: null,
176
+ messageQueue: [],
177
+ retryCount: 0,
178
+
179
+ /** @type {Object<string, Function[]>} */
180
+ events: {},
181
+
182
+ addEventListener: function(event, handler) {
183
+ if (this.socket) {
184
+ this.socket.addEventListener(event, handler)
185
+ }
186
+
187
+ if (!this.events[event]) {
188
+ this.events[event] = []
189
+ }
190
+
191
+ this.events[event].push(handler)
192
+ },
193
+
194
+ sendImmediately: function(message, sendElt) {
195
+ if (!this.socket) {
196
+ api.triggerErrorEvent()
197
+ }
198
+ if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
199
+ message,
200
+ socketWrapper: this.publicInterface
201
+ })) {
202
+ this.socket.send(message)
203
+ sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
204
+ message,
205
+ socketWrapper: this.publicInterface
206
+ })
207
+ }
208
+ },
209
+
210
+ send: function(message, sendElt) {
211
+ if (this.socket.readyState !== this.socket.OPEN) {
212
+ this.messageQueue.push({ message, sendElt })
213
+ } else {
214
+ this.sendImmediately(message, sendElt)
215
+ }
216
+ },
217
+
218
+ handleQueuedMessages: function() {
219
+ while (this.messageQueue.length > 0) {
220
+ var queuedItem = this.messageQueue[0]
221
+ if (this.socket.readyState === this.socket.OPEN) {
222
+ this.sendImmediately(queuedItem.message, queuedItem.sendElt)
223
+ this.messageQueue.shift()
224
+ } else {
225
+ break
226
+ }
227
+ }
228
+ },
229
+
230
+ init: function() {
231
+ if (this.socket && this.socket.readyState === this.socket.OPEN) {
232
+ // Close discarded socket
233
+ this.socket.close()
234
+ }
235
+
236
+ // Create a new WebSocket and event handlers
237
+ /** @type {WebSocket} */
238
+ var socket = socketFunc()
239
+
240
+ // The event.type detail is added for interface conformance with the
241
+ // other two lifecycle events (open and close) so a single handler method
242
+ // can handle them polymorphically, if required.
243
+ api.triggerEvent(socketElt, 'htmx:wsConnecting', { event: { type: 'connecting' } })
244
+
245
+ this.socket = socket
246
+
247
+ socket.onopen = function(e) {
248
+ wrapper.retryCount = 0
249
+ api.triggerEvent(socketElt, 'htmx:wsOpen', { event: e, socketWrapper: wrapper.publicInterface })
250
+ wrapper.handleQueuedMessages()
251
+ }
252
+
253
+ socket.onclose = function(e) {
254
+ // If socket should not be connected, stop further attempts to establish connection
255
+ // If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
256
+ if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
257
+ var delay = getWebSocketReconnectDelay(wrapper.retryCount)
258
+ setTimeout(function() {
259
+ wrapper.retryCount += 1
260
+ wrapper.init()
261
+ }, delay)
262
+ }
263
+
264
+ // Notify client code that connection has been closed. Client code can inspect `event` field
265
+ // to determine whether closure has been valid or abnormal
266
+ api.triggerEvent(socketElt, 'htmx:wsClose', { event: e, socketWrapper: wrapper.publicInterface })
267
+ }
268
+
269
+ socket.onerror = function(e) {
270
+ api.triggerErrorEvent(socketElt, 'htmx:wsError', { error: e, socketWrapper: wrapper })
271
+ maybeCloseWebSocketSource(socketElt)
272
+ }
273
+
274
+ var events = this.events
275
+ Object.keys(events).forEach(function(k) {
276
+ events[k].forEach(function(e) {
277
+ socket.addEventListener(k, e)
278
+ })
279
+ })
280
+ },
281
+
282
+ close: function() {
283
+ this.socket.close()
284
+ }
285
+ }
286
+
287
+ wrapper.init()
288
+
289
+ wrapper.publicInterface = {
290
+ send: wrapper.send.bind(wrapper),
291
+ sendImmediately: wrapper.sendImmediately.bind(wrapper),
292
+ queue: wrapper.messageQueue
293
+ }
294
+
295
+ return wrapper
296
+ }
297
+
298
+ /**
299
+ * ensureWebSocketSend attaches trigger handles to elements with
300
+ * "ws-send" attribute
301
+ * @param {HTMLElement} elt
302
+ */
303
+ function ensureWebSocketSend(elt) {
304
+ var legacyAttribute = api.getAttributeValue(elt, 'hx-ws')
305
+ if (legacyAttribute && legacyAttribute !== 'send') {
306
+ return
307
+ }
308
+
309
+ var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
310
+ processWebSocketSend(webSocketParent, elt)
311
+ }
312
+
313
+ /**
314
+ * hasWebSocket function checks if a node has webSocket instance attached
315
+ * @param {HTMLElement} node
316
+ * @returns {boolean}
317
+ */
318
+ function hasWebSocket(node) {
319
+ return api.getInternalData(node).webSocket != null
320
+ }
321
+
322
+ /**
323
+ * processWebSocketSend adds event listeners to the <form> element so that
324
+ * messages can be sent to the WebSocket server when the form is submitted.
325
+ * @param {HTMLElement} socketElt
326
+ * @param {HTMLElement} sendElt
327
+ */
328
+ function processWebSocketSend(socketElt, sendElt) {
329
+ var nodeData = api.getInternalData(sendElt)
330
+ var triggerSpecs = api.getTriggerSpecs(sendElt)
331
+ triggerSpecs.forEach(function(ts) {
332
+ api.addTriggerHandler(sendElt, ts, nodeData, function(elt, evt) {
333
+ if (maybeCloseWebSocketSource(socketElt)) {
334
+ return
335
+ }
336
+
337
+ /** @type {WebSocketWrapper} */
338
+ var socketWrapper = api.getInternalData(socketElt).webSocket
339
+ var headers = api.getHeaders(sendElt, api.getTarget(sendElt))
340
+ var results = api.getInputValues(sendElt, 'post')
341
+ var errors = results.errors
342
+ var rawParameters = Object.assign({}, results.values)
343
+ var expressionVars = api.getExpressionVars(sendElt)
344
+ var allParameters = api.mergeObjects(rawParameters, expressionVars)
345
+ var filteredParameters = api.filterValues(allParameters, sendElt)
346
+
347
+ var sendConfig = {
348
+ parameters: filteredParameters,
349
+ unfilteredParameters: allParameters,
350
+ headers,
351
+ errors,
352
+
353
+ triggeringEvent: evt,
354
+ messageBody: undefined,
355
+ socketWrapper: socketWrapper.publicInterface
356
+ }
357
+
358
+ if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
359
+ return
360
+ }
361
+
362
+ if (errors && errors.length > 0) {
363
+ api.triggerEvent(elt, 'htmx:validation:halted', errors)
364
+ return
365
+ }
366
+
367
+ var body = sendConfig.messageBody
368
+ if (body === undefined) {
369
+ var toSend = Object.assign({}, sendConfig.parameters)
370
+ if (sendConfig.headers) { toSend.HEADERS = headers }
371
+ body = JSON.stringify(toSend)
372
+ }
373
+
374
+ socketWrapper.send(body, elt)
375
+
376
+ if (evt && api.shouldCancel(evt, elt)) {
377
+ evt.preventDefault()
378
+ }
379
+ })
380
+ })
381
+ }
382
+
383
+ /**
384
+ * getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
385
+ * @param {number} retryCount // The number of retries that have already taken place
386
+ * @returns {number}
387
+ */
388
+ function getWebSocketReconnectDelay(retryCount) {
389
+ /** @type {"full-jitter" | ((retryCount:number) => number)} */
390
+ var delay = htmx.config.wsReconnectDelay
391
+ if (typeof delay === 'function') {
392
+ return delay(retryCount)
393
+ }
394
+ if (delay === 'full-jitter') {
395
+ var exp = Math.min(retryCount, 6)
396
+ var maxDelay = 1000 * Math.pow(2, exp)
397
+ return maxDelay * Math.random()
398
+ }
399
+
400
+ logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')
401
+ }
402
+
403
+ /**
404
+ * maybeCloseWebSocketSource checks to the if the element that created the WebSocket
405
+ * still exists in the DOM. If NOT, then the WebSocket is closed and this function
406
+ * returns TRUE. If the element DOES EXIST, then no action is taken, and this function
407
+ * returns FALSE.
408
+ *
409
+ * @param {*} elt
410
+ * @returns
411
+ */
412
+ function maybeCloseWebSocketSource(elt) {
413
+ if (!api.bodyContains(elt)) {
414
+ api.getInternalData(elt).webSocket.close()
415
+ return true
416
+ }
417
+ return false
418
+ }
419
+
420
+ /**
421
+ * createWebSocket is the default method for creating new WebSocket objects.
422
+ * it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
423
+ *
424
+ * @param {string} url
425
+ * @returns WebSocket
426
+ */
427
+ function createWebSocket(url) {
428
+ var sock = new WebSocket(url, [])
429
+ sock.binaryType = htmx.config.wsBinaryType
430
+ return sock
431
+ }
432
+
433
+ /**
434
+ * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
435
+ *
436
+ * @param {HTMLElement} elt
437
+ * @param {string} attributeName
438
+ */
439
+ function queryAttributeOnThisOrChildren(elt, attributeName) {
440
+ var result = []
441
+
442
+ // If the parent element also contains the requested attribute, then add it to the results too.
443
+ if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, 'hx-ws')) {
444
+ result.push(elt)
445
+ }
446
+
447
+ // Search all child nodes that match the requested attribute
448
+ elt.querySelectorAll('[' + attributeName + '], [data-' + attributeName + '], [data-hx-ws], [hx-ws]').forEach(function(node) {
449
+ result.push(node)
450
+ })
451
+
452
+ return result
453
+ }
454
+
455
+ /**
456
+ * @template T
457
+ * @param {T[]} arr
458
+ * @param {(T) => void} func
459
+ */
460
+ function forEach(arr, func) {
461
+ if (arr) {
462
+ for (var i = 0; i < arr.length; i++) {
463
+ func(arr[i])
464
+ }
465
+ }
466
+ }
467
+ })()