From af01610bd0b933a1442bb5b05209e52d07332d59 Mon Sep 17 00:00:00 2001 From: Jordan Matelsky Date: Tue, 21 Jul 2026 12:58:01 -0400 Subject: [PATCH] Add synchronous parse API --- README.md | 7 +++++-- docs/index.md | 7 +++++-- src/index.ts | 12 ++++++++++-- test/load.test.ts | 47 +++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0236c4f..fac139a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Supports **Node ≥18**, modern browsers, and Deno/Bun. ```ts // Modern named export (recommended) -import { load } from "npyjs"; +import { load, parse } from "npyjs"; // Back-compatibility class (matches legacy docs/tests) import npyjs from "npyjs"; @@ -41,11 +41,14 @@ import npyjs from "npyjs"; ### 1. Functional API (preferred) ```ts -import { load } from "npyjs"; +import { load, parse } from "npyjs"; const arr = await load("my-array.npy"); // arr has { data, shape, dtype, fortranOrder } console.log(arr.shape); // e.g., [100, 784] + +// Parse bytes synchronously when fetching or reading is handled separately +const parsed = parse(arrayBuffer); ``` ### 2. Legacy Class API (still supported) diff --git a/docs/index.md b/docs/index.md index b1d78b4..4d54163 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,7 +22,7 @@ Supports **Node ≥18**, modern browsers, and Deno/Bun. ```ts // Modern named export (recommended) -import { load } from "npyjs"; +import { load, parse } from "npyjs"; // Back-compatibility class (matches legacy docs/tests) import npyjs from "npyjs"; @@ -35,11 +35,14 @@ import npyjs from "npyjs"; ### 1. Functional API (preferred) ```ts -import { load } from "npyjs"; +import { load, parse } from "npyjs"; const arr = await load("my-array.npy"); // arr has { data, shape, dtype, fortranOrder } console.log(arr.shape); // e.g., [100, 784] + +// Parse bytes synchronously when fetching or reading is handled separately +const parsed = parse(arrayBuffer); ``` ### 2. Legacy Class API (still supported) diff --git a/src/index.ts b/src/index.ts index fb079c3..5088b03 100644 --- a/src/index.ts +++ b/src/index.ts @@ -143,7 +143,7 @@ function f16toF32(u16: number): number { } export async function load(source: string | ArrayBuffer | ArrayBufferView | Blob, opts: Options = {}): Promise { - let buf: ArrayBufferLike; + let buf: ArrayBuffer; if (typeof source === "string") { const res = await fetch(source); buf = await res.arrayBuffer(); @@ -152,9 +152,13 @@ export async function load(source: string | ArrayBuffer | ArrayBufferView | Blob } else if (source instanceof Blob) { buf = await source.arrayBuffer(); } else { - buf = source.buffer; + buf = source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength) as ArrayBuffer; } + return parse(buf, opts); +} + +export function parse(buf: ArrayBuffer, opts: Options = {}): NpyArray { const { headerOffset, headerLen } = readHeader(buf); const headerBytes = new Uint8Array(buf, headerOffset, headerLen); const header = textDecoder.decode(headerBytes).trim(); @@ -384,6 +388,10 @@ export default class N { async load(source: string | ArrayBuffer | ArrayBufferView) { return load(source, this.opts); } + + parse(buf: ArrayBuffer) { + return parse(buf, this.opts); + } static float16ToFloat32(u16: number) { return f16toF32(u16); diff --git a/test/load.test.ts b/test/load.test.ts index 42c3446..ad5ecd2 100644 --- a/test/load.test.ts +++ b/test/load.test.ts @@ -1,10 +1,10 @@ // tests/load.test.ts import { describe, it, expect, beforeAll, afterAll } from "vitest"; -import { promises as fs } from "fs"; +import { promises as fs, readFileSync } from "fs"; import path from "path"; import http from "http"; -import N from "../index.js"; +import N, { load, parse } from "../src/index.js"; // --- small HTTP file server for fetch() based loader --- function startServer(root = process.cwd()): Promise<{ server: http.Server; baseUrl: string }> { @@ -74,10 +74,45 @@ afterAll(async () => { }); describe("npyjs parser", () => { + it("parses an ArrayBuffer synchronously", () => { + const file = readFileSync("test/data/10-float32.npy"); + const buffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength); + + const data = parse(buffer); + + expect(data.shape).toEqual([10]); + expect(data.dtype).toBe("f4"); + expect(data.data).toBeInstanceOf(Float32Array); + }); + + it("returns the same result from load and parse", async () => { + const file = readFileSync("test/data/10-float32.npy"); + const buffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength); + + expect(await load(buffer)).toEqual(parse(buffer)); + }); + + it("loads only the bytes in an ArrayBufferView", async () => { + const file = readFileSync("test/data/10-float32.npy"); + const padded = Buffer.concat([Buffer.from([0]), file, Buffer.from([0])]); + const view = padded.subarray(1, padded.length - 1); + + expect(await load(view)).toEqual(parse(file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength))); + }); + + it("exposes synchronous parsing on the class API", () => { + const file = readFileSync("test/data/10-float16.npy"); + const buffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength); + + const data = new N({ convertFloat16: false }).parse(buffer); + + expect(data.data).toBeInstanceOf(Uint16Array); + }); + it("parses npy files and matches tail values", async () => { // records.json should map filename (no .npy) -> array of the last 5 expected values const records = JSON.parse(await fs.readFile("test/records.json", "utf8")); - const n = new (N as any)(); + const n = new N(); for (const fname of Object.keys(records)) { const fpath = path.join("test", `${fname}.npy`); @@ -93,7 +128,7 @@ describe("npyjs parser", () => { it("loads from Blob", async () => { const records = JSON.parse(await fs.readFile("test/records.json", "utf8")); - const n = new (N as any)(); + const n = new N(); const firstFile = Object.keys(records)[0]; const fpath = path.join("test", `${firstFile}.npy`); @@ -142,11 +177,11 @@ describe("npyjs parser", () => { it("respects convertFloat16 flag (Float32Array vs Uint16Array)", async () => { const url = `${baseUrl}/test/data/10-float16.npy`; - const nDefault = new (N as any)(); // conversion enabled by default + const nDefault = new N(); // conversion enabled by default const converted = await nDefault.load(url); expect(converted.data instanceof Float32Array).toBe(true); - const nRaw = new (N as any)({ convertFloat16: false }); + const nRaw = new N({ convertFloat16: false }); const raw = await nRaw.load(url); expect(raw.data instanceof Uint16Array).toBe(true); });