Context
Discovered during PR #58 (cross-handler SPA navigation fixes).
Problem
If two cross-handler SPA navigations happen in rapid succession (A then B within the same event loop tick), navigation A's `connect()` can still be executing its post-await setup (useHTTP assignment, initial state rendering, event delegation) when navigation B starts.
Because there's only one `WebSocketManager` transport at a time:
- A calls `handleNavigationResponse`, which calls `connect(A_selector)`
- Inside A's connect: `querySelector(A_selector)` → found (wrapper has A_ID)
- Inside A's connect: `webSocketManager.disconnect()` + `await webSocketManager.connect()`
- Before A's Promise resolves, B calls `handleNavigationResponse`
- B synchronously sets wrapper's data-lvt-id = B_ID, calls `connect(B_selector)`
- B's connect kills A's in-flight transport, opens a new one
- Eventually A's Promise resolves and continues its post-await setup
- A's `setupEventDelegation()` runs on the wrapper (which now has B_ID)
The idempotent setup methods minimize fallout (they dedupe by wrapper ID), but the intermediate state is inconsistent.
Proposed Fix
Make `connect()` cancellable via an `AbortSignal`:
```ts
interface ConnectOptions {
signal?: AbortSignal;
}
async connect(selector: string, options?: ConnectOptions): Promise {
// ... existing setup ...
const connectionResult = await this.webSocketManager.connect();
if (options?.signal?.aborted) return;
// ... post-await setup ...
}
```
In `handleNavigationResponse`, each cross-handler navigation creates an `AbortController` that supersedes the previous one:
```ts
this.connectAbortController?.abort();
this.connectAbortController = new AbortController();
this.connect(selector, { signal: this.connectAbortController.signal });
```
Workaround
PR #58 accepts the race as a known limitation and documents it in `handleNavigationResponse`. Two successive cross-handler navigations within a single event loop tick are rare in practice.
Context
Discovered during PR #58 (cross-handler SPA navigation fixes).
Problem
If two cross-handler SPA navigations happen in rapid succession (A then B within the same event loop tick), navigation A's `connect()` can still be executing its post-await setup (useHTTP assignment, initial state rendering, event delegation) when navigation B starts.
Because there's only one `WebSocketManager` transport at a time:
The idempotent setup methods minimize fallout (they dedupe by wrapper ID), but the intermediate state is inconsistent.
Proposed Fix
Make `connect()` cancellable via an `AbortSignal`:
```ts
interface ConnectOptions {
signal?: AbortSignal;
}
async connect(selector: string, options?: ConnectOptions): Promise {
// ... existing setup ...
const connectionResult = await this.webSocketManager.connect();
if (options?.signal?.aborted) return;
// ... post-await setup ...
}
```
In `handleNavigationResponse`, each cross-handler navigation creates an `AbortController` that supersedes the previous one:
```ts
this.connectAbortController?.abort();
this.connectAbortController = new AbortController();
this.connect(selector, { signal: this.connectAbortController.signal });
```
Workaround
PR #58 accepts the race as a known limitation and documents it in `handleNavigationResponse`. Two successive cross-handler navigations within a single event loop tick are rare in practice.