Skip to content

Commit b3b2d0c

Browse files
authored
fix: avoid aborting active source buffer removals (#29)
## Summary - distinguish active SourceBuffer append and remove operations inside the serialized MSE queue - abort only active append operations when playback is cleared or destroyed - let asynchronous remove operations finish instead of triggering Chromium's disallowed abort exception - preserve queued-operation rejection and subsequent queue draining after the removal completes - publish the corrected player contract as 0.1.31 ## Verification - `bun install --frozen-lockfile` - `bun audit --audit-level=high` - `bun run check` - `bun run check:versions` - `bun run check:docs` - `bun run check:jsr` - `bun run test:coverage` (76 pass) - `bun run build` - `git diff --check` - CI run 29373870733
1 parent 1d63103 commit b3b2d0c

4 files changed

Lines changed: 54 additions & 7 deletions

File tree

‎jsr.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://jsr.io/schema/config-file.v1.json",
33
"name": "@typetype/mse",
4-
"version": "0.1.30",
4+
"version": "0.1.31",
55
"exports": "./src/index.ts",
66
"publish": {
77
"include": ["LICENSE", "README.md", "src/**/*.ts"]

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@typetype/mse",
3-
"version": "0.1.30",
3+
"version": "0.1.31",
44
"description": "MSE playback engine for TypeType",
55
"license": "MIT",
66
"type": "module",

‎src/append-queue.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
type QueueItem = {
2+
kind: "append" | "remove";
23
action: () => void;
34
resolve: () => void;
45
reject: (error: Error) => void;
@@ -17,7 +18,12 @@ export class AppendQueue {
1718
append(data: ArrayBuffer): Promise<void> {
1819
if (this.disposed) return Promise.reject(new Error("Append queue is disposed"));
1920
return new Promise((resolve, reject) => {
20-
this.queue.push({ action: () => this.sourceBuffer.appendBuffer(data), resolve, reject });
21+
this.queue.push({
22+
kind: "append",
23+
action: () => this.sourceBuffer.appendBuffer(data),
24+
resolve,
25+
reject,
26+
});
2127
this.drain();
2228
});
2329
}
@@ -27,6 +33,7 @@ export class AppendQueue {
2733
if (endSeconds <= startSeconds) return Promise.resolve();
2834
return new Promise((resolve, reject) => {
2935
this.queue.push({
36+
kind: "remove",
3037
action: () => this.sourceBuffer.remove(startSeconds, endSeconds),
3138
resolve,
3239
reject,
@@ -51,19 +58,21 @@ export class AppendQueue {
5158

5259
clear(): void {
5360
const error = new DOMException("Operation aborted", "AbortError");
54-
this.current?.reject(error);
61+
const current = this.current;
62+
current?.reject(error);
5563
for (const item of this.queue.splice(0)) item.reject(error);
5664
this.current = null;
57-
if (this.sourceBuffer.updating) this.sourceBuffer.abort();
65+
if (this.sourceBuffer.updating && current?.kind === "append") this.sourceBuffer.abort();
5866
}
5967

6068
destroy(): void {
6169
this.disposed = true;
6270
const error = new DOMException("Operation aborted", "AbortError");
63-
this.current?.reject(error);
71+
const current = this.current;
72+
current?.reject(error);
6473
for (const item of this.queue.splice(0)) item.reject(error);
6574
this.current = null;
66-
if (this.sourceBuffer.updating) this.sourceBuffer.abort();
75+
if (this.sourceBuffer.updating && current?.kind === "append") this.sourceBuffer.abort();
6776
this.sourceBuffer.removeEventListener("updateend", this.handleUpdateEnd);
6877
this.sourceBuffer.removeEventListener("error", this.handleError);
6978
}

‎tests/append-queue.test.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { AppendQueue } from "../src/append-queue";
33

44
class FakeSourceBuffer extends EventTarget {
55
updating = false;
6+
operation: "append" | "remove" | null = null;
7+
abortCalls = 0;
68
readonly removed: Array<[number, number]> = [];
79

810
constructor(private readonly ranges: Array<[number, number]> = []) {
@@ -18,15 +20,25 @@ class FakeSourceBuffer extends EventTarget {
1820
}
1921

2022
appendBuffer(): void {
23+
this.operation = "append";
2124
this.updating = true;
2225
}
2326

2427
abort(): void {
28+
if (this.operation === "remove") {
29+
throw new DOMException(
30+
"Aborting asynchronous remove() operation is disallowed",
31+
"InvalidStateError",
32+
);
33+
}
34+
this.abortCalls += 1;
35+
this.operation = null;
2536
this.updating = false;
2637
}
2738

2839
remove(start: number, end: number): void {
2940
this.removed.push([start, end]);
41+
this.operation = "remove";
3042
this.updating = true;
3143
}
3244
}
@@ -41,9 +53,34 @@ test("clear rejects active and queued appends", async () => {
4153

4254
await expect(active).rejects.toMatchObject({ name: "AbortError" });
4355
await expect(pending).rejects.toMatchObject({ name: "AbortError" });
56+
expect(source.abortCalls).toBe(1);
4457
expect(source.updating).toBe(false);
4558
});
4659

60+
test("clear does not abort an active remove", async () => {
61+
const source = new FakeSourceBuffer([[0, 10]]);
62+
const queue = new AppendQueue(source as unknown as SourceBuffer);
63+
const active = queue.remove(0, 5);
64+
65+
queue.clear();
66+
67+
await expect(active).rejects.toMatchObject({ name: "AbortError" });
68+
expect(source.abortCalls).toBe(0);
69+
expect(source.updating).toBe(true);
70+
});
71+
72+
test("destroy does not abort an active remove", async () => {
73+
const source = new FakeSourceBuffer([[0, 10]]);
74+
const queue = new AppendQueue(source as unknown as SourceBuffer);
75+
const active = queue.remove(0, 5);
76+
77+
queue.destroy();
78+
79+
await expect(active).rejects.toMatchObject({ name: "AbortError" });
80+
expect(source.abortCalls).toBe(0);
81+
expect(source.updating).toBe(true);
82+
});
83+
4784
test("reset removes every buffered range without replacing the source buffer", async () => {
4885
const source = new FakeSourceBuffer([
4986
[4, 8],
@@ -53,6 +90,7 @@ test("reset removes every buffered range without replacing the source buffer", a
5390

5491
const reset = queue.reset();
5592
expect(source.removed).toEqual([[4, 20]]);
93+
source.operation = null;
5694
source.updating = false;
5795
source.dispatchEvent(new Event("updateend"));
5896

0 commit comments

Comments
 (0)