diff --git a/jsr.json b/jsr.json index 7524b35..69cfbcd 100644 --- a/jsr.json +++ b/jsr.json @@ -1,7 +1,7 @@ { "$schema": "https://jsr.io/schema/config-file.v1.json", "name": "@typetype/mse", - "version": "0.1.30", + "version": "0.1.31", "exports": "./src/index.ts", "publish": { "include": ["LICENSE", "README.md", "src/**/*.ts"] diff --git a/package.json b/package.json index 07d2e8e..3ccd3e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@typetype/mse", - "version": "0.1.30", + "version": "0.1.31", "description": "MSE playback engine for TypeType", "license": "MIT", "type": "module", diff --git a/src/append-queue.ts b/src/append-queue.ts index f62746d..d3dd4c9 100644 --- a/src/append-queue.ts +++ b/src/append-queue.ts @@ -1,4 +1,5 @@ type QueueItem = { + kind: "append" | "remove"; action: () => void; resolve: () => void; reject: (error: Error) => void; @@ -17,7 +18,12 @@ export class AppendQueue { append(data: ArrayBuffer): Promise { if (this.disposed) return Promise.reject(new Error("Append queue is disposed")); return new Promise((resolve, reject) => { - this.queue.push({ action: () => this.sourceBuffer.appendBuffer(data), resolve, reject }); + this.queue.push({ + kind: "append", + action: () => this.sourceBuffer.appendBuffer(data), + resolve, + reject, + }); this.drain(); }); } @@ -27,6 +33,7 @@ export class AppendQueue { if (endSeconds <= startSeconds) return Promise.resolve(); return new Promise((resolve, reject) => { this.queue.push({ + kind: "remove", action: () => this.sourceBuffer.remove(startSeconds, endSeconds), resolve, reject, @@ -51,19 +58,21 @@ export class AppendQueue { clear(): void { const error = new DOMException("Operation aborted", "AbortError"); - this.current?.reject(error); + const current = this.current; + current?.reject(error); for (const item of this.queue.splice(0)) item.reject(error); this.current = null; - if (this.sourceBuffer.updating) this.sourceBuffer.abort(); + if (this.sourceBuffer.updating && current?.kind === "append") this.sourceBuffer.abort(); } destroy(): void { this.disposed = true; const error = new DOMException("Operation aborted", "AbortError"); - this.current?.reject(error); + const current = this.current; + current?.reject(error); for (const item of this.queue.splice(0)) item.reject(error); this.current = null; - if (this.sourceBuffer.updating) this.sourceBuffer.abort(); + if (this.sourceBuffer.updating && current?.kind === "append") this.sourceBuffer.abort(); this.sourceBuffer.removeEventListener("updateend", this.handleUpdateEnd); this.sourceBuffer.removeEventListener("error", this.handleError); } diff --git a/tests/append-queue.test.ts b/tests/append-queue.test.ts index 8f6e09d..57d370c 100644 --- a/tests/append-queue.test.ts +++ b/tests/append-queue.test.ts @@ -3,6 +3,8 @@ import { AppendQueue } from "../src/append-queue"; class FakeSourceBuffer extends EventTarget { updating = false; + operation: "append" | "remove" | null = null; + abortCalls = 0; readonly removed: Array<[number, number]> = []; constructor(private readonly ranges: Array<[number, number]> = []) { @@ -18,15 +20,25 @@ class FakeSourceBuffer extends EventTarget { } appendBuffer(): void { + this.operation = "append"; this.updating = true; } abort(): void { + if (this.operation === "remove") { + throw new DOMException( + "Aborting asynchronous remove() operation is disallowed", + "InvalidStateError", + ); + } + this.abortCalls += 1; + this.operation = null; this.updating = false; } remove(start: number, end: number): void { this.removed.push([start, end]); + this.operation = "remove"; this.updating = true; } } @@ -41,9 +53,34 @@ test("clear rejects active and queued appends", async () => { await expect(active).rejects.toMatchObject({ name: "AbortError" }); await expect(pending).rejects.toMatchObject({ name: "AbortError" }); + expect(source.abortCalls).toBe(1); expect(source.updating).toBe(false); }); +test("clear does not abort an active remove", async () => { + const source = new FakeSourceBuffer([[0, 10]]); + const queue = new AppendQueue(source as unknown as SourceBuffer); + const active = queue.remove(0, 5); + + queue.clear(); + + await expect(active).rejects.toMatchObject({ name: "AbortError" }); + expect(source.abortCalls).toBe(0); + expect(source.updating).toBe(true); +}); + +test("destroy does not abort an active remove", async () => { + const source = new FakeSourceBuffer([[0, 10]]); + const queue = new AppendQueue(source as unknown as SourceBuffer); + const active = queue.remove(0, 5); + + queue.destroy(); + + await expect(active).rejects.toMatchObject({ name: "AbortError" }); + expect(source.abortCalls).toBe(0); + expect(source.updating).toBe(true); +}); + test("reset removes every buffered range without replacing the source buffer", async () => { const source = new FakeSourceBuffer([ [4, 8], @@ -53,6 +90,7 @@ test("reset removes every buffered range without replacing the source buffer", a const reset = queue.reset(); expect(source.removed).toEqual([[4, 20]]); + source.operation = null; source.updating = false; source.dispatchEvent(new Event("updateend"));