demo · v138
Sequence trace
Side-by-side timing diagram of the buggy old code path that v138 deprecates, vs the supported pattern. Highlights exactly where the race window opens up — and why the spec dropped the async behaviour to close it.
Deprecation in Chrome 138.
SourceBuffer.remove(start, end) while another operation is in flight previously deferred until the operation finished. v138 throws InvalidStateError instead — matching the spec.
pre-v138 (deprecated) RACE
0ms
sourceBuffer.appendBuffer(chunkA)
1ms
updating = true
2ms
sourceBuffer.remove(0, 5) ← previously queued silently
3ms
remove() returns — but didn't run yet
50ms
updateend fires for appendBuffer
51ms
remove() now actually starts
52ms
updating = true (again)
100ms
updateend fires for remove
v138 (supported) SYNC
0ms
sourceBuffer.appendBuffer(chunkA)
1ms
updating = true
2ms
sourceBuffer.remove(0, 5)
3ms
InvalidStateError thrown
3ms
your code: await waitForUpdateEnd()
50ms
updateend fires for appendBuffer
51ms
sourceBuffer.remove(0, 5) ← retry safely
100ms
updateend fires for remove
The fix
// pre-v138 — works, but with hidden queueing
sourceBuffer.appendBuffer(chunk);
sourceBuffer.remove(0, 5); // silently queued
// (race: was the chunk part of the range to remove? maybe!)
// v138 — explicit
sourceBuffer.appendBuffer(chunk);
await new Promise(r => sourceBuffer.addEventListener('updateend', r, {once: true}));
sourceBuffer.remove(0, 5); // safe now
await new Promise(r => sourceBuffer.addEventListener('updateend', r, {once: true}));