From b461fb58a2b218bc3ad21bee83fb0a86218a5377 Mon Sep 17 00:00:00 2001 From: Borewit Date: Sat, 4 Jan 2025 19:58:10 +0100 Subject: [PATCH] Properly close web-stream-tokenizer Add unit tests for web-stream --- lib/core.ts | 9 ++++++-- test/metadata-parsers.ts | 22 +++++++++++++++--- test/test-file-amr.ts | 6 +++-- test/util.ts | 50 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/lib/core.ts b/lib/core.ts index 91e787530..b2441108e 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -39,8 +39,13 @@ export async function parseBlob(blob: Blob, options: IOptions = {}): Promise { - return parseFromTokenizer(fromWebStream(webStream, {fileInfo: typeof fileInfo === 'string' ? {mimeType: fileInfo} : fileInfo}), options); +export async function parseWebStream(webStream: AnyWebByteStream, fileInfo?: IFileInfo | string, options: IOptions = {}): Promise { + const tokenizer = fromWebStream(webStream, {fileInfo: typeof fileInfo === 'string' ? {mimeType: fileInfo} : fileInfo}); + try { + return await parseFromTokenizer(tokenizer, options); + } finally { + await tokenizer.close(); + } } /** diff --git a/test/metadata-parsers.ts b/test/metadata-parsers.ts index da7c1a5f9..cc09889e3 100644 --- a/test/metadata-parsers.ts +++ b/test/metadata-parsers.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import * as mm from '../lib/index.js'; import type { IAudioMetadata, IOptions } from '../lib/index.js'; +import { makeReadableByteFileStream } from './util.js'; type ParseFileMethod = (skipTest: () => void, filePath: string, mimeType?: string, options?: IOptions) => Promise<{metadata: IAudioMetadata, randomRead: boolean}>; @@ -27,14 +28,29 @@ export const Parsers: IParser[] = [ }, { description: 'parseStream (Node.js)', initParser: async (skipTest, filePath: string, mimeType?: string, options?: IOptions) => { - const stream = fs.createReadStream(filePath); + const nodeStream = fs.createReadStream(filePath); try { return { - metadata: await mm.parseStream(stream, {mimeType: mimeType}, options), + metadata: await mm.parseStream(nodeStream, {mimeType: mimeType}, options), randomRead: false }; } finally { - stream.close(); + nodeStream.close(); + } + } + }, { + description: 'parseWebStream', + initParser: async (skipTest, filePath: string, mimeType?: string, options?: IOptions) => { + const webStream = await makeReadableByteFileStream(filePath); + try { + return { + // ToDo: add unit tests, where the fileSize is not provided (passed) + metadata: await mm.parseWebStream(webStream.stream, {mimeType: mimeType, size: webStream.fileSize}, options), + randomRead: false + }; + } finally { + await webStream.stream.cancel() + await webStream.closeFile(); } } }, { diff --git a/test/test-file-amr.ts b/test/test-file-amr.ts index 07b70ba32..f85c741a1 100644 --- a/test/test-file-amr.ts +++ b/test/test-file-amr.ts @@ -7,9 +7,11 @@ const amrPath = path.join(samplePath, 'amr'); describe('Adaptive Multi-Rate (AMR) audio file', () => { - Parsers.forEach(parser => { + Parsers + .filter(parser => parser.description !== 'parseWebStream') // Parsing if AMR to slow via web-stream + .forEach(parser => { - describe(parser.description, () => { + describe(parser.description,() => { it('parse: sample.amr', async function () { const {metadata} = await parser.initParser(() => this.skip(), path.join(amrPath, 'sample.amr'), 'audio/amr', {duration: true}); diff --git a/test/util.ts b/test/util.ts index c4c9b5b1f..9d16c3248 100644 --- a/test/util.ts +++ b/test/util.ts @@ -3,6 +3,8 @@ import { Readable } from 'node:stream'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import fs from 'node:fs/promises'; +import { ReadableStream } from 'node:stream/web'; const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); @@ -22,4 +24,52 @@ export class SourceStream extends Readable { } } +export async function makeReadableByteFileStream(filename: string, delay = 0): Promise<{ fileSize: number, stream: ReadableStream, closeFile: () => Promise }> { + + let position = 0; + const fileInfo = await fs.stat(filename); + const fileHandle = await fs.open(filename, 'r'); + + return { + fileSize: fileInfo.size, + stream: new ReadableStream({ + type: 'bytes', + + async pull(controller) { + + // @ts-ignore + const view = controller.byobRequest.view; + + setTimeout(async () => { + try { + const {bytesRead} = await fileHandle.read(view, 0, view.byteLength, position); + if (bytesRead === 0) { + await fileHandle.close(); + controller.close(); + // @ts-ignore + controller.byobRequest.respond(0); + } else { + position += bytesRead; + // @ts-ignore + controller.byobRequest.respond(bytesRead); + } + } catch (err) { + controller.error(err); + await fileHandle.close(); + } + }, delay); + }, + + cancel() { + return fileHandle.close(); + }, + + autoAllocateChunkSize: 1024 + }), + closeFile: () => { + return fileHandle.close(); + } + }; +} + export const samplePath = path.join(dirname, 'samples');