feat(wasm): suppress stale client completions

req: client_local/011\nreq: client_local/012\nreq: operations/003
This commit is contained in:
slhx agent
2026-07-13 13:17:58 +02:00
parent ac525fe572
commit dd5ea0827f
6 changed files with 146 additions and 7 deletions
+50 -1
View File
@@ -19,6 +19,7 @@
const atomStores = new WeakMap();
const dragKeys = new WeakMap();
const clientHandlers = new Map();
const clientRuns = new WeakMap();
function roots() {
const found = [];
@@ -241,6 +242,11 @@
async function runClient(el, event) {
const name = el.getAttribute("data-hemx-client");
const root = rootOf(el);
const policy = el.getAttribute("data-hemx-client-policy") || "latest";
const run = { root, generation: (clientRuns.get(root)?.generation || 0) + 1 };
if (policy === "drop" && clientRuns.has(root)) return;
clientRuns.set(root, run);
const active = () => clientRuns.get(root) === run && root.isConnected;
const handler = clientHandlers.get(name);
showError(el, null);
showPending(el, true);
@@ -257,14 +263,17 @@
root.getAttribute(STATE) || "",
);
if (!(wire instanceof Uint8Array)) throw new Error(`client-local hemx handler ${name} returned an invalid effect batch`);
if (!active()) return;
applyBatch(wire, root);
} catch (error) {
if (!active()) return;
const fallback = el.hasAttribute("data-hemx-client-fallback");
showError(el, error);
emit(root, "hemx:client-error", { handler: name, message: String(error), fallback });
if (fallback) await send(el, event.type, el);
} finally {
showPending(el, false);
if (clientRuns.get(root) === run) clientRuns.delete(root);
if (el.isConnected) showPending(el, false);
}
}
@@ -943,6 +952,35 @@
anchor.origin === location.origin && !anchor.download && anchor.target !== "_blank";
}
function descendantRoots(node) {
const roots = [];
for (const child of node.children || []) {
if (child.hasAttribute(ROOT)) roots.push(child);
roots.push(...descendantRoots(child));
}
return roots;
}
function stopDescendantPolling(node) {
for (const child of node.children || []) {
if (child.hasAttribute("data-hemx-every") || child.hasAttribute("data-hemx-interval")) {
stopPolling(child);
}
stopDescendantPolling(child);
}
}
function cleanupRemovedRoot(root) {
clientRuns.delete(root);
const source = sseSources.get(root);
if (source) source.close();
sseSources.delete(root);
const observer = revealObservers.get(root);
if (observer) observer.disconnect();
revealObservers.delete(root);
stopDescendantPolling(root);
}
function start() {
roots().forEach((root) => {
try {
@@ -961,6 +999,15 @@
emit(root, "hemx:sse-error", String(error));
}
});
new MutationObserver((records) => {
records.forEach((record) => {
record.removedNodes.forEach((node) => {
if (!(node instanceof Element)) return;
if (node.hasAttribute(ROOT)) cleanupRemovedRoot(node);
descendantRoots(node).forEach(cleanupRemovedRoot);
});
});
}).observe(document.documentElement, { childList: true, subtree: true });
try {
history.replaceState(history.state || { hemx: true }, "", location.href);
} catch (error) {
@@ -985,7 +1032,9 @@
decodeAtomState,
registerClientHandler(name, handler) {
if (!name || typeof handler !== "function") throw new Error("client handler registration requires a name and function");
const previous = clientHandlers.get(name);
clientHandlers.set(name, handler);
return previous;
},
});