diff --git a/src/utils/net/blob.ts b/src/utils/net/blob.ts new file mode 100644 index 0000000..6fd3343 --- /dev/null +++ b/src/utils/net/blob.ts @@ -0,0 +1,46 @@ +import { Blob as BlobPolyfill, blobFrom, blobFromSync } from "node-fetch"; +import { ConstructorReturnType } from "@/utils/types"; + +/** + * A `Blob` encapsulates immutable, raw data that can be safely shared across multiple worker threads. + */ +export const Blob = BlobPolyfill; + +/** + * A `Blob` encapsulates immutable, raw data that can be safely shared across multiple worker threads. + */ +export type Blob = ConstructorReturnType; + +/** + * Checks if an object is a `Blob`. + * + * @param blob - The object to check. + * + * @returns `true` if the object is a `Blob`; otherwise, `false`. + */ +export function isBlob(blob: unknown): blob is Blob { + const name = blob?.[Symbol.toStringTag]; + return name === "Blob" || name === "File"; +} + +/** + * Reads a file from the given path and returns its content as a `Blob`. + * + * @param path - The file path to read the content from. + * + * @returns A `Promise` that resolves to a `Blob` containing the file content. + */ +export function readBlob(path: string): Promise { + return blobFrom(path); +} + +/** + * Synchronously reads a file from the given path and returns its content as a `Blob`. + * + * @param path - The file path to read the content from. + * + * @returns A `Blob` containing the file content. + */ +export function readBlobSync(path: string): Blob { + return blobFromSync(path); +} diff --git a/tests/unit/utils/net/blob.spec.ts b/tests/unit/utils/net/blob.spec.ts new file mode 100644 index 0000000..eef278a --- /dev/null +++ b/tests/unit/utils/net/blob.spec.ts @@ -0,0 +1,53 @@ +import mockFs from "mock-fs"; +import { Blob, isBlob, readBlob, readBlobSync } from "@/utils/net/blob"; + +beforeEach(() => { + mockFs({ + "test.txt": "test", + }); +}); + +afterEach(() => { + mockFs.restore(); +}); + +describe("isBlob", () => { + test("returns true for Blob instances", () => { + expect(isBlob(new Blob([]))).toBe(true); + }); + + test("returns false for non-Blob objects", () => { + expect(isBlob({})).toBe(false); + }); + + test("returns false for null and undefined", () => { + expect(isBlob(null)).toBe(false); + expect(isBlob(undefined)).toBe(false); + }); +}); + +describe("readBlob", () => { + test("reads a file and returns its content as a Blob", async () => { + const blob = await readBlob("test.txt"); + + expect(isBlob(blob)).toBe(true); + expect(await blob.text()).toBe("test"); + }); + + test("throws an error for non-existent file", async () => { + await expect(readBlob("non-existent.txt")).rejects.toThrow(); + }); +}); + +describe("readBlobSync", () => { + test("reads a file synchronously and returns its content as a Blob", async () => { + const blob = readBlobSync("test.txt"); + + expect(isBlob(blob)).toBe(true); + expect(await blob.text()).toBe("test"); + }); + + test("throws an error for non-existent file", () => { + expect(() => readBlobSync("non-existent.txt")).toThrow(); + }); +});