Skip to content

[Feature] Add request cancellation to prevent stale data race conditions in ModalManager #74

@numbers-official

Description

@numbers-official

Description

The ModalManager.updateModal() method in src/modal/modal-manager.ts (lines 33-62) fires three parallel API requests without any mechanism to cancel or discard stale responses when the user opens a different asset.

Current Behavior

// modal-manager.ts lines 47-61
if (nidChanged) {
  fetchAsset(options.nid).then((assetData) => {
    this.updateModalAsset(assetData, true);
  });
  hasNftProduct(options.nid).then((hasNftProduct) =>
    this.updateModalAsset({ hasNftProduct }, false)
  );
  fetchAssetMetadata(options.nid).then((metadata) => {
    if (metadata) {
      this.updateModalAsset({
        hasC2pa: metadata.hasC2pa,
        showcaseLink: metadata.showcaseLink,
      }, false);
    }
  });
}

Problem

When a user rapidly clicks between multiple capture-eye elements:

  1. User clicks Asset A → 3 fetches start for NID-A
  2. User clicks Asset B before NID-A responses arrive → modal clears, 3 new fetches start for NID-B
  3. NID-A responses arrive → updateModalAsset() writes NID-A data into the modal now showing NID-B
  4. NID-B responses arrive later → partial overwrite, resulting in mixed data

This creates a data integrity issue where the modal displays a mix of data from two different assets. In a provenance verification widget, showing wrong creator/ownership data is a significant trust concern.

Suggested Implementation

Use AbortController to cancel in-flight requests when a new asset is opened:

private currentAbortController: AbortController | null = null;

async updateModal(options: ModalOptions, delay = 150): Promise<void> {
  if (this.currentAbortController) {
    this.currentAbortController.abort();
  }
  this.currentAbortController = new AbortController();
  const signal = this.currentAbortController.signal;

  // ... existing modal setup ...

  if (nidChanged) {
    const currentNid = options.nid;
    fetchAsset(currentNid, { signal }).then((assetData) => {
      if (this.nid === currentNid) {
        this.updateModalAsset(assetData, true);
      }
    }).catch(err => {
      if (err.name !== 'AbortError') console.error('Fetch error:', err);
    });
    // same pattern for hasNftProduct and fetchAssetMetadata
  }
}

Expected Impact

Files to Modify

  1. src/modal/modal-manager.ts — Add AbortController lifecycle
  2. src/asset/asset-service.ts — Accept optional AbortSignal in fetch functions
  3. src/modal/interaction-tracker.ts — Accept optional AbortSignal for event tracking

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions