Parent: #474 Related: #480, #493, #874, #885, #888
Problem
Promise rejection-tracker state currently changes before the corresponding realm queue publication:
- settlement sets
rejection_queued = true, unlocks the Promise, then performs a fallible unhandled-queue append; - first post-notification handler attachment sets
rejection_handled_notified = true, unlocks the Promise, then performs a fallible handled-queue append.
If either append returns OutOfMemory, the Promise can claim the notification is queued/already accounted for while no queue root exists. zig-js supports bounded-heap recovery, so aborting the current evaluation does not justify permanently losing or duplicating the host event.
A reserve-before-commit transaction is insufficient: native resolving functions consume their shared once-only bit before calling resolve/reject, so reservation OOM could leave a pending Promise whose exact settlement cannot be retried. #874 therefore owns the shared allocation-free FIFO representation; this issue owns the Promise-state/publication transaction across Promise.lock and realm_lock.
Required implementation
- Publish unhandled and later-handled tracker state through the allocation-free #874 FIFO, removing the allocator failure boundary entirely.
- Use one explicit canonical
realm_lock → Promise.locktransaction, with a Promise-lock probe followed by revalidation after acquiring the canonical order. - Commit the queue root, GC insertion barrier, and corresponding Promise state before releasing either lock.
- Preserve exact FIFO/reentrant ordering, one-time unhandled delivery, early-handled skipping, later
rejectionHandleddelivery, precise tracing/relocation, and shared-realm synchronization. - Keep the ordinary fulfilled and already-handled fast paths free of realm-lock acquisition.
No-workaround rules
- No swallowed OOM, best-effort notification loss, unbounded emergency reserve, global serialization, event cap, retry loop that can duplicate callbacks, or Context-destruction-only recovery.
- Do not clear a state bit after failure unless a deterministic later operation is guaranteed to republish the exact pending event.
- Do not leave a fallible allocator operation between consuming a once-only capability and publishing required rejection-tracker state.
Acceptance
- Deterministic failing-allocator witnesses prove both unhandled and later-handled publication perform zero allocations while flags and FIFO membership commit exactly.
- Repeated settlement, pre-notification handling, post-notification handling, empty queues, and reentrant callbacks deliver or suppress each event exactly once.
- Concurrent handler attachment cannot deadlock, lose, reorder, or duplicate notification under no-GIL execution.
- Moving/concurrent GC rewrites and roots the intrusive queue; Promise remains in its 256-byte allocation class; bounded-heap recovery and Context teardown remain exact.
- Private rejection dispatch, relevant threadfuzz, full units, and suppression-free TSan pass.
- Exact Promise Test262 coverage has zero regressions; documentation makes no unsupported throughput or OOM guarantee.
Security boundary
Rejection fanout and allocation pressure are script-controlled. The engine must fail closed without corrupting tracker state, retaining an unrooted managed pointer, or silently suppressing a host-visible event after recovery. The distinct Promise-to-microtask scheduling transaction remains tracked by #885.