Skip to content

fix(chat): drain queue entries enqueued during lock release - #929

Open
salmundani wants to merge 1 commit into
vercel:mainfrom
salmundani:salmun/fix/enqueue-bug
Open

salmundani wants to merge 1 commit into
vercel:mainfrom
salmundani:salmun/fix/enqueue-bug

Conversation

@salmundani

Copy link
Copy Markdown

Summary

Fixes #928

Test plan

Six new tests added that fail in the old code and pass with the fix.

pnpm validate also passes

Checklist

  • All commits are signed and verified
  • All commits are signed off for the DCO (git commit -s)
  • pnpm validate passes
  • Changeset added (or N/A — see CONTRIBUTING.md)
  • Documentation updated (or N/A)

Signed-off-by: Daniel Salmun <salmundani@gmail.com>
@salmundani
salmundani requested a review from a team as a code owner September 14, 2026 21:33
@salmundani

Copy link
Copy Markdown
Author

By the way, exposing the class's internal synchronization primitives isn't usually the best idea (check "Java Concurrency in Practice" for practical examples). The bug I fixed is probably still latent in the failure paths of the code.

Redesigning the StateAdapter so it has the following methods would be much cleaner in the long term.

export interface StateAdapter {
  // Everything above stays. The drop strategy and the fallback still use it.

  /**
   * Atomically: take the lock if it is free, otherwise add the entry to the
   * queue. When `enqueueOnClaim` is true, the entry is also queued on claim,
   * which debounce and burst need.
   */
  claimOrEnqueue?(
    threadId: string,
    entry: QueueEntry,
    options: {
      ttlMs: number;
      maxSize: number;
      onQueueFull: "drop-oldest" | "drop-newest";
      enqueueOnClaim: boole
    }
  ): Promise<ClaimResult>;

  /**
   * Atomically and token-checked: pop all pending entries, or release the
   * lock if the queue is e token no longer holds
   * the lock, and then pop
   */
  takeOrRelease?(lock: Lock): Promise<TakeResult>;
}

export type ClaimResult =
  | { status: "claimed"; lock: Lock }
  | { status: "queued"; dep
  | { status: "dropped"; depth: number };

export type TakeResult =
  | { status: "taken"; entries: QueueEntry[] }
  | { status: "released" }
  | { status: "lost" };

Similar to the atomic putIfAbsent in Java's ConcurrentHashMap.

These new methods should be optional as to not break 3rd party adapters, or otherwise be part of a major release.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Queued message is stranded when it is enqueued between the holder's last dequeue and its lock release

1 participant