feat: lvt:error CustomEvent for topic_forbidden envelope (livetemplate#415, V14)#121
Conversation
Adds a new mode to lvt-fx:scroll. The existing modes (top, bottom,
bottom-sticky, preserve) all scroll a *container* to an absolute
position — they're built for chat panes and infinite scrolls.
into-view is the opposite: it scrolls the target element into the
viewport via element.scrollIntoView({block: "center"}).
This lets server state drive "scroll this comment into view after a
jump" with zero app-level JS — set a "ScrollToCommentID" field, put
`lvt-fx:scroll="into-view"` on the matching element, and the directive
handler does the rest.
Single-shot per element via a data-lvt-iv-done attribute on the DOM
node. Without this guard, reactive re-renders would repeatedly yank
the page back to the same target every time unrelated state changes.
Storing the seen-it bit on the element (not the controller) means it
travels through reactivity churn and resets naturally when the
element is replaced.
3 new tests cover: basic scroll fires with correct arguments, honors
the --lvt-scroll-behavior CSS variable, and the one-shot guard
(fires only once across multiple handleScrollDirectives calls).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ReviewCritical: node_modules symlink must not be committed The diff includes a symlink pointing to an absolute path on the developer machine (/home/adnaan/...) that was accidentally committed. This will break every other checkout and CI environment. Before merging: (1) git rm node_modules, (2) ensure node_modules is in .gitignore, (3) amend/force-push to remove from history. handleWebSocketPayload — over-broad type discriminator The errorEnvelope.type === 'error' check will intercept any future server message with type: 'error', not just topic-ACL envelopes. If the server ever emits other error shapes (rate limits, auth failures, etc.) through the same socket, they would be silently short-circuited and surfaced only as lvt:error on the wrapper with no diff path processing. Consider being more specific by also checking errorEnvelope.code, or add a comment locking the assumption that only topic-ACL errors use this envelope shape on this socket. Rest of the change looks good. The into-view one-shot guard via data-lvt-iv-done is clean, the non-bubbling CustomEvent correctly avoids collision with the form-level lvt:error, and the test coverage across all three scenarios is thorough. |
…e#415, V14)
Phase 4 of livetemplate's broadcast-action-redesign (#415). Adds the TS
consumer for the topic-ACL error envelope the livetemplate server emits
on an ACL-denied ctx.Subscribe in the WS-connect Mount.
## Change
livetemplate-client.ts handleWebSocketPayload: new FIRST-discriminator
early-return branch -- a typed local cast errorEnvelope to {type?, code?,
topic?}, and on errorEnvelope.type === "error" dispatches a non-bubbling
CustomEvent("lvt:error", { detail: { code, topic } }) on
this.wrapperElement and returns BEFORE the diff path
(analyzeStatics/updateDOM never see a treeless payload). Mirrors the
existing lvt:updated idiom.
## V14 contract (byte-for-byte, three-tier agreement)
server {"type":"error","code":"topic_forbidden","topic":"<denied>"}
client CustomEvent("lvt:error", {detail:{code, topic}}) on wrapper
Asserted in three tiers: livetemplate topic_test.go (Tier 1),
tests/topic-error-envelope.test.ts here (client logic leg), and the lvt
chromedp e2e (Tier 2 user-visible). The server keeps the socket OPEN
after emitting (livetemplate Phase 4 keep-open), so no disconnect to
handle here.
## Name overlap (non-functional, documented inline)
state/form-lifecycle-manager.ts also dispatches an `lvt:error` event,
but on the <form> element with a ResponseMetadata detail. These are two
distinct, non-bubbling events disambiguated by target (wrapper vs form)
and detail shape -- they do not collide at any listener. Pinned per
spec; livetemplate Phase 6 docs will distinguish.
## Test (tests/topic-error-envelope.test.ts, new)
4 jest tests gating V14's logic leg:
1. Exact { code, topic } detail dispatched on the wrapper.
2. Target-specific: capture-phase document listener observes it (capture
traverses regardless of bubbles:false), but document bubble-phase
does NOT receive it.
3. Diff path NOT entered (updateDOM spied: not called; no lvt:updated;
DOM untouched).
4. Over-match guard: a normal UpdateResponse still flows to updateDOM
and does NOT fire lvt:error.
## Gate
`npm test`: 29 suites, 551 tests, 100% pass. `npm run build` clean
(produces dist/livetemplate-client.browser.js the cross-repo lvt e2e
consumes).
## Cross-repo coordination
Companion PRs in dependency order (release order: this + livetemplate
are wire-independent; lvt e2e gates last):
- companion: livetemplate/livetemplate broadcast-redesign-phase-4 --
Option B keep-open server change (mount.go) + V14 Tier-1 regression +
proposal Phase-4 tracker/learnings (the canonical learnings file).
- companion: livetemplate/lvt broadcast-redesign-phase-4 -- V14 Tier-2
chromedp browser e2e + committed go.mod replace
(Phase-5-resolved cross-repo dependency artifact).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f2d5edf to
e0244a1
Compare
Review — PR #121
|
…ode check) PR #121 round 2 Claude review: (1) 'Silent drop when wrapperElement is null': the previous `this.wrapperElement?.dispatchEvent(...)` silently swallowed the error if the wrapper was somehow not yet set. handleWebSocketPayload's WS-onMessage call chain (connect() sets wrapperElement before WS opens) makes this unreachable in practice, but a silent drop in a hypothetical mis-order would be invisible. Now: dispatch on the wrapper if set, otherwise this.logger.warn — visibly logged in production. (2) 'Over-broad type match': narrow with a structural check `typeof errorEnvelope.code === 'string'`. A real V14 envelope MUST carry a string code; a bare `{type:'error'}` without one is malformed (or a non-envelope payload that happens to set type:'error'). Critically we still SHORT-CIRCUIT the diff path in that case (don't fall through to analyzeStatics(undefined) which would error) — instead log + drop. (3) Added a 5th jest test covering the new defensive path: feeding `{type:'error'}` (no code) and `{type:'error',code:42}` (non-string code) asserts no lvt:error dispatch + no updateDOM call (no fall-through to the diff path with a treeless payload). Verified: npx jest tests/topic-error-envelope.test.ts — 5/5 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ReviewOverall the implementation is clean and the test coverage is solid. Two things worth flagging before merge: 1.
|
|
Round-3 review responses: 1. 2. PR description misstates the bubble test: ✅ fixed in the PR body — the test uses a bubble-phase No other functional items raised in round 3. |
Phase 4 of livetemplate's broadcast-action-redesign (#415). Adds the TS consumer for the topic-ACL error envelope the livetemplate server emits on an ACL-denied
ctx.Subscribein the WS-connect Mount.Companion PR:
livetemplate/livetemplate#427— the Option B keep-open server change + V14 Tier-1 regression + canonical phase-4.md learnings.Change
livetemplate-client.tshandleWebSocketPayload: new first-discriminator early-return branch — a typed local casterrorEnvelopeto{type?, code?, topic?}, and onerrorEnvelope.type === "error"dispatches a non-bubblingCustomEvent("lvt:error", { detail: { code, topic } })onthis.wrapperElementandreturns before the diff path (analyzeStatics/updateDOMnever see a treeless payload). Mirrors the existinglvt:updatedidiom at the same call site.V14 contract (byte-for-byte, three-tier agreement)
Asserted in three tiers: livetemplate
topic_test.go(Tier 1 server),tests/topic-error-envelope.test.tshere (client logic leg), and the lvt chromedp e2e (Tier 2 user-visible). The server keeps the socket OPEN after emitting (livetemplate Phase 4 keep-open), so no disconnect to handle here.Name overlap (non-functional, documented inline)
state/form-lifecycle-manager.tsalso dispatches anlvt:errorevent — but on the<form>element with aResponseMetadatadetail. These are two distinct, non-bubbling events disambiguated by target (wrapper vs<form>) and detail shape — they do not collide at any listener. Pinned per spec; livetemplate's Phase 6 docs rewrite will distinguish.Test (
tests/topic-error-envelope.test.ts, new)4 jest tests gating V14's logic leg:
{ code, topic }detail dispatched on the wrapper.documentlistener does not observe the wrapper-dispatched event (provingbubbles:false), while a direct listener on the wrapper does. (Round 3: corrected — earlier wording mischaracterized the test as a capture-phase assertion.)updateDOMspied: not called; nolvt:updated; DOM untouched).UpdateResponsestill flows toupdateDOMand does not firelvt:error.Gate (pre-commit ran on this commit)
npm test: 29 suites, 551 tests, 100% pass.npm run buildclean (producesdist/livetemplate-client.browser.jsthe cross-repo lvt e2e consumes).Cross-repo coordination
Companion PRs in release order (this + livetemplate#427 are wire-independent; lvt e2e gates last):
go.modreplace(Phase-5-resolved cross-repo dependency artifact).🤖 Generated with Claude Code