feat: publish av-tools 0.1.0

This commit is contained in:
2026-07-24 14:58:03 +02:00
commit fcc537b024
254 changed files with 63270 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
import { describe, expect, it, vi } from 'vitest';
import {
aggregatePcmPeaks,
createWaveformRuntime,
WaveformCancellationError,
WaveformRuntimeBusyError,
WaveformRuntimeTerminatedError,
type WaveformWorkerLike,
} from '../../src/waveform';
import type { WaveformWorkerAggregateRequest } from '../../src/waveform/waveform.types';
describe('waveform worker runtime', () => {
it('offloads aggregation, reports progress, and preserves caller bytes', async () => {
const worker = new FakeWorker();
const input = new Int16Array([-32_768, 0, 16_384, 32_767]);
const originalBytes = [...new Uint8Array(input.buffer)];
const progress = vi.fn();
const runtime = createWaveformRuntime({ workerFactory: () => worker });
const resultPromise = runtime.aggregate(
{
container: 'raw',
data: input,
encoding: 's16le',
sampleRate: 8_000,
},
{ bucketCount: 2, onProgress: progress }
);
const request = worker.lastRequest;
expect(request).toBeDefined();
expect(request?.input.data).not.toBe(input.buffer);
expect([...new Uint8Array(input.buffer)]).toEqual(originalBytes);
worker.emitMessage({
type: 'progress',
requestId: request?.requestId,
progress: 0.5,
});
const peaks = aggregatePcmPeaks(
{
container: 'raw',
data: request?.input.data ?? new ArrayBuffer(0),
encoding: 's16le',
sampleRate: 8_000,
},
{ bucketCount: 2 }
);
worker.emitMessage({
type: 'result',
requestId: request?.requestId,
peaks,
});
await expect(resultPromise).resolves.toMatchObject({ bucketCount: 2 });
expect(progress).toHaveBeenCalledWith(0.5);
expect(progress).toHaveBeenLastCalledWith(1);
expect(worker.terminated).toBe(true);
expect(runtime.isActive).toBe(false);
});
it('terminates the worker on AbortSignal cancellation and remains reusable', async () => {
const workers: FakeWorker[] = [];
const runtime = createWaveformRuntime({
workerFactory: () => {
const worker = new FakeWorker();
workers.push(worker);
return worker;
},
});
const controller = new AbortController();
const first = runtime.aggregate(rawInput(), {
bucketCount: 2,
signal: controller.signal,
});
controller.abort();
await expect(first).rejects.toBeInstanceOf(WaveformCancellationError);
expect(workers[0]?.terminated).toBe(true);
expect(runtime.isActive).toBe(false);
const second = runtime.aggregate(rawInput(), { bucketCount: 2 });
const request = workers[1]?.lastRequest;
const peaks = aggregatePcmPeaks(rawInput(), { bucketCount: 2 });
workers[1]?.emitMessage({
type: 'result',
requestId: request?.requestId,
peaks,
});
await expect(second).resolves.toMatchObject({ bucketCount: 2 });
});
it('supports explicit cancellation and rejects overlapping work', async () => {
const worker = new FakeWorker();
const runtime = createWaveformRuntime({ workerFactory: () => worker });
const active = runtime.aggregate(rawInput(), { bucketCount: 2 });
await expect(
runtime.aggregate(rawInput(), { bucketCount: 2 })
).rejects.toBeInstanceOf(WaveformRuntimeBusyError);
expect(runtime.cancelActive()).toBe(true);
await expect(active).rejects.toBeInstanceOf(WaveformCancellationError);
expect(runtime.cancelActive()).toBe(false);
});
it('falls back synchronously when workers are unavailable or blocked', async () => {
const runtime = createWaveformRuntime({ workerFactory: null });
await expect(
runtime.aggregate(rawInput(), { bucketCount: 2 })
).resolves.toMatchObject({ bucketCount: 2 });
const blockedRuntime = createWaveformRuntime({
workerFactory: () => {
throw new Error('CSP blocked worker');
},
});
await expect(
blockedRuntime.aggregate(rawInput(), { bucketCount: 2 })
).resolves.toMatchObject({ bucketCount: 2 });
expect(blockedRuntime.mode).toBe('synchronous');
});
it('tracks and can cancel synchronous fallback before it starts', async () => {
const runtime = createWaveformRuntime({ workerFactory: null });
const active = runtime.aggregate(rawInput(), { bucketCount: 2 });
expect(runtime.isActive).toBe(true);
const overlapping = runtime.aggregate(rawInput(), { bucketCount: 2 });
expect(runtime.cancelActive()).toBe(true);
await expect(overlapping).rejects.toBeInstanceOf(WaveformRuntimeBusyError);
await expect(active).rejects.toBeInstanceOf(WaveformCancellationError);
expect(runtime.isActive).toBe(false);
});
it('makes terminate permanent and cancels active work', async () => {
const worker = new FakeWorker();
const runtime = createWaveformRuntime({ workerFactory: () => worker });
const active = runtime.aggregate(rawInput(), { bucketCount: 2 });
runtime.terminate();
await expect(active).rejects.toBeInstanceOf(WaveformCancellationError);
await expect(
runtime.aggregate(rawInput(), { bucketCount: 2 })
).rejects.toBeInstanceOf(WaveformRuntimeTerminatedError);
});
});
class FakeWorker implements WaveformWorkerLike {
readonly #messageListeners = new Set<
(event: MessageEvent<unknown>) => void
>();
readonly #errorListeners = new Set<(event: Event) => void>();
readonly #messageErrorListeners = new Set<(event: Event) => void>();
lastRequest: WaveformWorkerAggregateRequest | undefined;
terminated = false;
postMessage(message: unknown): void {
this.lastRequest = message as WaveformWorkerAggregateRequest;
}
addEventListener(
type: 'message' | 'error' | 'messageerror',
listener:
((event: MessageEvent<unknown>) => void) | ((event: Event) => void)
): void {
if (type === 'message') {
this.#messageListeners.add(
listener as (event: MessageEvent<unknown>) => void
);
} else if (type === 'error') {
this.#errorListeners.add(listener as (event: Event) => void);
} else {
this.#messageErrorListeners.add(listener as (event: Event) => void);
}
}
removeEventListener(
type: 'message' | 'error' | 'messageerror',
listener:
((event: MessageEvent<unknown>) => void) | ((event: Event) => void)
): void {
if (type === 'message') {
this.#messageListeners.delete(
listener as (event: MessageEvent<unknown>) => void
);
} else if (type === 'error') {
this.#errorListeners.delete(listener as (event: Event) => void);
} else {
this.#messageErrorListeners.delete(listener as (event: Event) => void);
}
}
terminate(): void {
this.terminated = true;
}
emitMessage(data: unknown): void {
const event = { data } as MessageEvent<unknown>;
for (const listener of this.#messageListeners) {
listener(event);
}
}
}
function rawInput() {
return {
container: 'raw' as const,
data: new Int16Array([-100, 100, -200, 200]),
encoding: 's16le' as const,
sampleRate: 8_000,
};
}