|
1 | 1 | (() => { |
2 | 2 | let api; |
| 3 | + let warnedLegacyAttributes = new Set(); |
3 | 4 |
|
4 | 5 | // ======================================== |
5 | 6 | // HELPERS |
6 | 7 | // ======================================== |
7 | 8 |
|
8 | | - function getConfig(ctx) { |
9 | | - let isConnect = api.attributeValue(ctx.sourceElement, 'hx-sse:connect') != null; |
10 | | - let defaults = { |
11 | | - reconnect: isConnect, |
| 9 | + function getConfig(element) { |
| 10 | + let hasHxSseConnect = api.attributeValue(element, 'hx-sse:connect') != null; |
| 11 | + let hxConfig = api.HCON.parse(api.attributeValue(element, 'hx-config')).sse || {}; |
| 12 | + |
| 13 | + return { |
| 14 | + reconnect: hasHxSseConnect, |
12 | 15 | reconnectDelay: 500, |
13 | 16 | reconnectMaxDelay: 60000, |
14 | 17 | reconnectMaxAttempts: Infinity, |
15 | 18 | reconnectJitter: 0.3, |
16 | | - pauseOnBackground: isConnect |
| 19 | + pauseOnBackground: hasHxSseConnect, |
| 20 | + ...htmx.config.sse, |
| 21 | + ...hxConfig |
17 | 22 | }; |
18 | | - let global = htmx.config.sse || {}; |
19 | | - // hx-config="sse.reconnect:true sse.reconnectDelay:50ms" is parsed by |
20 | | - // core's __mergeConfig into ctx.request.sse during createRequestContext |
21 | | - let perElement = ctx.request.sse || {}; |
22 | | - return {...defaults, ...global, ...perElement}; |
23 | 23 | } |
24 | 24 |
|
25 | 25 | function clearLastEventIdHeader(headers) { |
|
116 | 116 | // with the saved request context (no full pipeline re-run). |
117 | 117 | async function handleSSEResponse(ctx) { |
118 | 118 | let element = ctx.sourceElement; |
119 | | - let config = getConfig(ctx); |
| 119 | + let config = getConfig(element); |
120 | 120 | let reconnectRequested = false; |
121 | 121 |
|
122 | 122 | let connection = { |
|
158 | 158 | } |
159 | 159 |
|
160 | 160 | connection.cancelled = false; |
161 | | - if (!api.triggerHtmxEvent(element, 'htmx:before:sse:connection', {connection}) || connection.cancelled) { |
| 161 | + if (!api.triggerHtmxEvent(element, 'htmx:sse:before:connection', {connection}) || connection.cancelled) { |
162 | 162 | cleanup(element, 'cancelled'); |
163 | 163 | return; |
164 | 164 | } |
165 | 165 |
|
166 | 166 | connection.status = ctx.response.status; |
167 | | - api.triggerHtmxEvent(element, 'htmx:after:sse:connection', {connection}); |
| 167 | + api.triggerHtmxEvent(element, 'htmx:sse:after:connection', {connection}); |
168 | 168 |
|
169 | 169 | let currentResponse = ctx.response.raw; |
170 | 170 |
|
|
197 | 197 | } |
198 | 198 |
|
199 | 199 | connection.cancelled = false; |
200 | | - if (!api.triggerHtmxEvent(element, 'htmx:before:sse:connection', {connection}) || connection.cancelled) break; |
| 200 | + if (!api.triggerHtmxEvent(element, 'htmx:sse:before:connection', {connection}) || connection.cancelled) break; |
201 | 201 |
|
202 | 202 | await new Promise(r => { |
203 | 203 | connection.delayCanceller = r; |
|
236 | 236 | } |
237 | 237 |
|
238 | 238 | connection.status = currentResponse.status; |
239 | | - api.triggerHtmxEvent(element, 'htmx:after:sse:connection', {connection}); |
| 239 | + api.triggerHtmxEvent(element, 'htmx:sse:after:connection', {connection}); |
240 | 240 | connection.attempt = 0; |
241 | 241 | } |
242 | 242 |
|
|
259 | 259 | let detail = { |
260 | 260 | message: {data: msg.data, event: msg.event, id: msg.id, cancelled: false} |
261 | 261 | }; |
262 | | - if (!api.triggerHtmxEvent(element, 'htmx:before:sse:message', detail) || detail.message.cancelled) continue; |
| 262 | + if (!api.triggerHtmxEvent(element, 'htmx:sse:before:message', detail) || detail.message.cancelled) continue; |
263 | 263 |
|
264 | 264 | if (msg.retry != null) config.reconnectDelay = msg.retry; |
265 | 265 |
|
266 | 266 | if (detail.message.event) { |
267 | 267 | htmx.trigger(element, detail.message.event, {data: detail.message.data, id: detail.message.id}); |
268 | 268 | delete detail.message.cancelled; |
269 | | - api.triggerHtmxEvent(element, 'htmx:after:sse:message', detail); |
| 269 | + api.triggerHtmxEvent(element, 'htmx:sse:after:message', detail); |
270 | 270 |
|
271 | 271 | // hx-sse:close="eventname" — close connection on matching event |
272 | 272 | let closeEvent = api.attributeValue(element, 'hx-sse:close'); |
|
282 | 282 | if (!ctx.swap.includes('swapEmpty')) ctx.swap += ' swapEmpty:false'; |
283 | 283 | await htmx.swap(ctx); |
284 | 284 | delete detail.message.cancelled; |
285 | | - api.triggerHtmxEvent(element, 'htmx:after:sse:message', detail); |
| 285 | + api.triggerHtmxEvent(element, 'htmx:sse:after:message', detail); |
286 | 286 | } |
287 | 287 | } catch (e) { |
288 | 288 | if (!connection.abortController?.signal?.aborted) { |
|
338 | 338 | // ======================================== |
339 | 339 |
|
340 | 340 | function checkLegacyAttributes(element) { |
341 | | - if (element.hasAttribute('sse-connect')) { |
342 | | - console.warn('htmx: [hx-sse] legacy attribute sse-connect is deprecated; use hx-sse:connect instead'); |
| 341 | + for (let attribute of ['sse-connect', 'sse-close', 'sse-swap']) { |
| 342 | + if (!element.hasAttribute(attribute) || warnedLegacyAttributes.has(attribute)) continue; |
343 | 343 |
|
344 | | - let url = element.getAttribute('sse-connect'); |
345 | | - let attr = (htmx.config.prefix || 'hx-') + 'sse' + (htmx.config.metaCharacter || ':') + 'connect'; |
346 | | - if (!element.hasAttribute(attr)) { |
347 | | - element.setAttribute(attr, url); |
| 344 | + if (attribute === 'sse-swap') { |
| 345 | + console.warn('htmx: [hx-sse] sse-swap is removed in htmx 4. Unnamed SSE messages are swapped automatically. Named events are dispatched as DOM events.'); |
| 346 | + } else { |
| 347 | + console.warn(`htmx: [hx-sse] legacy attribute ${attribute} is deprecated; use hx-sse:${attribute.slice(4)} instead`); |
348 | 348 | } |
349 | | - } |
350 | | - if (element.hasAttribute('sse-swap')) { |
351 | | - console.warn('htmx: [hx-sse] sse-swap is removed in htmx 4. Unnamed SSE messages are swapped automatically. Named events are dispatched as DOM events.'); |
| 349 | + warnedLegacyAttributes.add(attribute); |
352 | 350 | } |
353 | 351 | } |
354 | 352 |
|
|
381 | 379 | }, |
382 | 380 |
|
383 | 381 | htmx_after_process: (element) => { |
384 | | - checkLegacyAttributes(element); |
385 | | - processElement(element); |
386 | 382 | let mc = htmx.config.metaCharacter || ':'; |
| 383 | + let processSSEElement = (element) => { |
| 384 | + checkLegacyAttributes(element); |
| 385 | + for (let name of ['connect', 'close']) { |
| 386 | + let legacyAttr = `sse-${name}`; |
| 387 | + if (!element.hasAttribute(legacyAttr)) continue; |
| 388 | + |
| 389 | + let attr = (htmx.config.prefix || 'hx-') + 'sse' + mc + name; |
| 390 | + if (!element.hasAttribute(attr)) element.setAttribute(attr, element.getAttribute(legacyAttr)); |
| 391 | + } |
| 392 | + processElement(element); |
| 393 | + }; |
| 394 | + |
| 395 | + processSSEElement(element); |
387 | 396 | let sseAttr = CSS.escape('hx-sse' + mc + 'connect'); |
388 | 397 | let sseSelector = `[${sseAttr}]`; |
389 | 398 | if (htmx.config.prefix) sseSelector += `,[${CSS.escape(htmx.config.prefix + 'sse' + mc + 'connect')}]`; |
390 | | - element.querySelectorAll(`${sseSelector},[sse-connect]`).forEach((el) => { |
391 | | - checkLegacyAttributes(el); |
392 | | - processElement(el); |
393 | | - }); |
| 399 | + element.querySelectorAll(`${sseSelector},[sse-connect],[sse-close],[sse-swap]`).forEach(processSSEElement); |
394 | 400 | }, |
395 | 401 |
|
396 | 402 | htmx_before_cleanup: (element) => { |
|
0 commit comments