fix(state): surface an initialization failure instead of hanging silently - #264
Merged
Merged
Conversation
…ntly A declaration or source error on the first mount used to leave connectedCallbackPromise pending forever with no diagnostic (#257). The one unguarded `await this._initialize()` now lands failures: one console.error, initialize and loading promises resolved, the connectedCallback promise marked handled and rejected with the original error object. Every throw site reachable from initialization was derived from the code: the seven `$` declaration validators, the four source-loading paths, the SSR data merge, and the "one <wcs-state> per root" raise. Consequences that land with it: - getBindingsReady on a failed root rejects instead of reporting ready. - Volumes waiting on a failed root settle with one report each instead of waiting forever. - setInitialState on a failed element raises a replace-this-element diagnostic instead of doing nothing. The known-defect pins for 欠陥9 now assert the contract instead of the hang. The re-set path is untouched and still throws synchronously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 2 of #257, from three adversarial reviews of the first landing. - Re-registering the SAME element on a root is idempotent, so a DOM move during load no longer rejects a healthy element. - The failed-root marker lives only while the failed element is still attached, so a corrected root adopts volumes that connected in the recovery window. - The pre-initialize awaits (bind-component, DCC) land the same way; fail-fast paths that already resolve their own promises pass through untouched. - The rootNode-getter raise is enumerated and pinned. - The duplicate-root message now tells the author the second element stays unregistered and must be removed. Docs: the volume wording narrowed to what the code does, the stray blank line under the 2.3.0 heading removed, and an SSR design-doc line citation refreshed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 3 of #257. Round 2 classified an element detached mid-load as an initialization failure, which put a diagnostic, a permanent rejection and element poisoning on pages with no author error at all. A pooled element (remove then append) stayed rejected forever even after it initialized and rendered, and @wcstack/server and @wcstack/testing await that promise. - `_initialize` reports completion instead of dereferencing a root node that is gone; the connection ends quietly and the next connect settles the promises, matching main. - The `$connectedCallback` call carries the generation guard that startWatch and startStreams already had, so a DOM move during load no longer runs the author's callback twice. - Volume slot reservations are released when a volume is orphaned or when a settled, never-grafted volume leaves the DOM. - The README and CHANGELOG now split the failure sites by what they do to connectedCallbackPromise rather than by feature name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 4 of #257. Round 3 added a slot release so a corrected root could adopt a volume again, and it produced two measured defects: releasing by the current mount attribute freed another live element's reservation, and a volume detached mid-load kept its slot with no path left to free it. The reservation has had no release path since before this work, so it is an ownership bug of its own rather than part of this fix. - The release and its call sites are removed; reserveVolumeSlot and the ledger are back to their previous behaviour. - Round 2's orphan landing stays: volumes waiting on a root that fails settle with one report each. - The docs no longer claim a slot is released, and say instead that an orphaned volume does not re-graft when a corrected root appears. - The failure-site enumerations now include the silent mount fail-fast class and the fifth raw bind-component raise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…guarantees Rounds 5 and 6 of #257. Each round of prose about which failure site rejects, resolves loudly, or resolves silently introduced a new false claim: a slot release that does not happen, a volume source failure called a rejection, a silence that is not silent, a companion report that usually is not there. The classification is now written at the grain that is robustly true. A root element's initialization failure rejects and reports once. A volume never rejects; some volume failures report nothing of their own, and the error leaves as the discarded connectedCallback promise. A detach during load rejects nothing. Readers who need the exact behaviour of one site are pointed at the test that pins every case. The SSR design note now names the function it actually quotes instead of a line number that keeps drifting. Documentation only: no source behaviour, no test assertion and no test title changed, and the suite stays at 3269 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #257.
The defect
connectedCallbackhad one unguardedawait this._initialize(). A throw from anywhere inside initialization skipped every promise settlement, soconnectedCallbackPromiseandinitializePromisestayed pending forever with no diagnostic. The author saw a page that never rendered, andrenderToString()/mount()never returned. The re-set path threw synchronously, so the two paths disagreed about the same mistake.The fix
Initialization failures now land: one
console.error, the initialize and loading promises resolved, the connectedCallback promise marked handled and rejected with the original error object rather than a wrapper.initializePromisestill resolves, so one element's mistake does not drag the rest of the page's bindings down throughwaitForStateInitialize'sPromise.all.The covered throw sites were derived from the code, not from a shape matrix: the seven
$declaration validators, the four source-loading paths, the SSR data merge, the DCC andbind-componentsetup raises, and the "one<wcs-state>per root" rule.Three consequences land with it:
getBindingsReady(root)rejects for a root whose state element failed, instead of reporting "ready" for a page with no bindings.setInitialState()on an element that already failed raises a replace-this-element diagnostic instead of doing nothing.Two things this PR deliberately does not do
A detach during load is not a failure. An intermediate round classified it as one, which put a diagnostic, a permanent rejection and element poisoning on pages with no author error at all. A pooled element (remove then append) stayed rejected forever even after it initialized and rendered, and
@wcstack/serverand@wcstack/testingawait that promise. That classification was reversed: the connection ends quietly, as onmain, and the next connect settles the promises.The volume slot lifecycle is out of scope. A round added a slot release so a corrected root could adopt a volume again, and it produced two measured defects: releasing by the current mount attribute freed another live element's reservation, and a volume detached mid-load kept its slot with no path left to free it. The reservation has had no release path since before this work, so it is an ownership bug of its own. It is removed here and filed separately; the docs now state what is true, which is that an orphaned volume does not re-graft and the page has to be reloaded.
Behaviour changes worth knowing
<wcs-state>used to kill the whole page, the first element's page now renders and only the duplicate is refused. That second element stays unregistered but keeps the state it loaded, so remove it. Moving a healthy element in the DOM is not a duplicate and is never refused.$connectedCallbackand$streamseach start exactly once for the live connection.mainstarted$streamszero times in that shape.Verification
Five rounds. Each round was reviewed by three adversarial agents with separate lenses, and the first four rounds each turned up a measured regression on pages with no author error — which is why the detach classification was reversed and the slot work was cut. The lens that found them was running the same probe against this branch and against
mainand comparing the numbers.npm run test:coveragenpm run lintnpx tsc --noEmitThe known-defect pins for 欠陥9 now assert the contract instead of the hang. The per-site behaviour is pinned in
packages/state/__tests__/integration.initFailureDiagnostics.test.ts, which the README and CHANGELOG point at rather than restating.🤖 Generated with Claude Code