Skip to content

feat: make connect() cancellable with AbortSignal to fix cross-handler race #64

@adnaan

Description

@adnaan

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:

  1. A calls `handleNavigationResponse`, which calls `connect(A_selector)`
  2. Inside A's connect: `querySelector(A_selector)` → found (wrapper has A_ID)
  3. Inside A's connect: `webSocketManager.disconnect()` + `await webSocketManager.connect()`
  4. Before A's Promise resolves, B calls `handleNavigationResponse`
  5. B synchronously sets wrapper's data-lvt-id = B_ID, calls `connect(B_selector)`
  6. B's connect kills A's in-flight transport, opens a new one
  7. Eventually A's Promise resolves and continues its post-await setup
  8. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions