Skip to content

Commit 1606ea7

Browse files
authored
Standardize hx-sse events, config, and legacy support (#3929)
Improve SSE lifecycle and compatibility
1 parent 0dc0f44 commit 1606ea7

5 files changed

Lines changed: 219 additions & 168 deletions

File tree

src/editors/jetbrains/htmx.web-types.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -536,14 +536,14 @@
536536
"doc-url": "https://four.htmx.org/reference/events/htmx-after-viewTransition"
537537
},
538538
{
539-
"name": "before:sse:connection",
539+
"name": "sse:before:connection",
540540
"description": "Fires before an SSE connection attempt. `detail.connection` can be modified; set `cancelled` or cancel the event to stop connecting.",
541-
"doc-url": "https://four.htmx.org/extensions/hx-sse#htmxbeforesseconnection"
541+
"doc-url": "https://four.htmx.org/extensions/hx-sse#htmxssebeforeconnection"
542542
},
543543
{
544-
"name": "after:sse:connection",
544+
"name": "sse:after:connection",
545545
"description": "Fires after a successful SSE connection or reconnection. `detail.connection` describes the connection.",
546-
"doc-url": "https://four.htmx.org/extensions/hx-sse#htmxaftersseconnection"
546+
"doc-url": "https://four.htmx.org/extensions/hx-sse#htmxsseafterconnection"
547547
},
548548
{
549549
"name": "sse:close",
@@ -556,14 +556,14 @@
556556
"doc-url": "https://four.htmx.org/extensions/hx-sse#htmxsseerror"
557557
},
558558
{
559-
"name": "before:sse:message",
559+
"name": "sse:before:message",
560560
"description": "Fires before an SSE message is processed. `detail.message` contains `data`, `event`, `id`, and `cancelled`.",
561-
"doc-url": "https://four.htmx.org/extensions/hx-sse#htmxbeforessemessage"
561+
"doc-url": "https://four.htmx.org/extensions/hx-sse#htmxssebeforemessage"
562562
},
563563
{
564-
"name": "after:sse:message",
564+
"name": "sse:after:message",
565565
"description": "Fires after an SSE message is processed. `detail.message` contains `data`, `event`, and `id`.",
566-
"doc-url": "https://four.htmx.org/extensions/hx-sse#htmxafterssemessage"
566+
"doc-url": "https://four.htmx.org/extensions/hx-sse#htmxsseaftermessage"
567567
},
568568
{
569569
"name": "before:ws:connection",

src/ext/hx-sse.js

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
(() => {
22
let api;
3+
let warnedLegacyAttributes = new Set();
34

45
// ========================================
56
// HELPERS
67
// ========================================
78

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,
1215
reconnectDelay: 500,
1316
reconnectMaxDelay: 60000,
1417
reconnectMaxAttempts: Infinity,
1518
reconnectJitter: 0.3,
16-
pauseOnBackground: isConnect
19+
pauseOnBackground: hasHxSseConnect,
20+
...htmx.config.sse,
21+
...hxConfig
1722
};
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};
2323
}
2424

2525
function clearLastEventIdHeader(headers) {
@@ -116,7 +116,7 @@
116116
// with the saved request context (no full pipeline re-run).
117117
async function handleSSEResponse(ctx) {
118118
let element = ctx.sourceElement;
119-
let config = getConfig(ctx);
119+
let config = getConfig(element);
120120
let reconnectRequested = false;
121121

122122
let connection = {
@@ -158,13 +158,13 @@
158158
}
159159

160160
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) {
162162
cleanup(element, 'cancelled');
163163
return;
164164
}
165165

166166
connection.status = ctx.response.status;
167-
api.triggerHtmxEvent(element, 'htmx:after:sse:connection', {connection});
167+
api.triggerHtmxEvent(element, 'htmx:sse:after:connection', {connection});
168168

169169
let currentResponse = ctx.response.raw;
170170

@@ -197,7 +197,7 @@
197197
}
198198

199199
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;
201201

202202
await new Promise(r => {
203203
connection.delayCanceller = r;
@@ -236,7 +236,7 @@
236236
}
237237

238238
connection.status = currentResponse.status;
239-
api.triggerHtmxEvent(element, 'htmx:after:sse:connection', {connection});
239+
api.triggerHtmxEvent(element, 'htmx:sse:after:connection', {connection});
240240
connection.attempt = 0;
241241
}
242242

@@ -259,14 +259,14 @@
259259
let detail = {
260260
message: {data: msg.data, event: msg.event, id: msg.id, cancelled: false}
261261
};
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;
263263

264264
if (msg.retry != null) config.reconnectDelay = msg.retry;
265265

266266
if (detail.message.event) {
267267
htmx.trigger(element, detail.message.event, {data: detail.message.data, id: detail.message.id});
268268
delete detail.message.cancelled;
269-
api.triggerHtmxEvent(element, 'htmx:after:sse:message', detail);
269+
api.triggerHtmxEvent(element, 'htmx:sse:after:message', detail);
270270

271271
// hx-sse:close="eventname" — close connection on matching event
272272
let closeEvent = api.attributeValue(element, 'hx-sse:close');
@@ -282,7 +282,7 @@
282282
if (!ctx.swap.includes('swapEmpty')) ctx.swap += ' swapEmpty:false';
283283
await htmx.swap(ctx);
284284
delete detail.message.cancelled;
285-
api.triggerHtmxEvent(element, 'htmx:after:sse:message', detail);
285+
api.triggerHtmxEvent(element, 'htmx:sse:after:message', detail);
286286
}
287287
} catch (e) {
288288
if (!connection.abortController?.signal?.aborted) {
@@ -338,17 +338,15 @@
338338
// ========================================
339339

340340
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;
343343

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`);
348348
}
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);
352350
}
353351
}
354352

@@ -381,16 +379,24 @@
381379
},
382380

383381
htmx_after_process: (element) => {
384-
checkLegacyAttributes(element);
385-
processElement(element);
386382
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);
387396
let sseAttr = CSS.escape('hx-sse' + mc + 'connect');
388397
let sseSelector = `[${sseAttr}]`;
389398
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);
394400
},
395401

396402
htmx_before_cleanup: (element) => {

src/scripts/upgrade-check.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@
8282
}
8383

8484
SSE_EVENT_RENAMES = {
85-
"htmx:sseOpen": "htmx:after:sse:connection",
85+
"htmx:sseOpen": "htmx:sse:after:connection",
8686
"htmx:sseError": "htmx:sse:error",
87-
"htmx:sseBeforeMessage": "htmx:before:sse:message",
88-
"htmx:sseMessage": "htmx:after:sse:message",
87+
"htmx:sseBeforeMessage": "htmx:sse:before:message",
88+
"htmx:sseMessage": "htmx:sse:after:message",
8989
"htmx:sseClose": "htmx:sse:close",
9090
}
9191

@@ -102,6 +102,7 @@
102102
# Extension attribute renames
103103
EXT_ATTR_RENAMES = {
104104
"sse-connect": "rename to hx-sse:connect",
105+
"sse-close": "rename to hx-sse:close",
105106
"sse-swap": "removed — SSE now integrates with standard htmx request pipeline",
106107
"ws-connect": "rename to hx-ws:connect",
107108
"ws-send": "rename to hx-ws:send",

0 commit comments

Comments
 (0)