Skip to content

Commit 0f37caa

Browse files
authored
feat(storage): Configurable RequestInit from FetchStore constructor (#77)
1 parent 0b201b0 commit 0f37caa

File tree

7 files changed

+221
-19
lines changed

7 files changed

+221
-19
lines changed

.changeset/shy-pigs-serve.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@zarrita/storage": patch
3+
---
4+
5+
feat: allow `RequestInit` options to be configured in `FetchStore` constructor

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"license": "MIT",
33
"scripts": {
44
"build": "tsc --build",
5-
"test": "vitest",
5+
"test": "vitest --api",
66
"fmt": "deno fmt --use-tabs packages docs",
77
"lint": "deno fmt --use-tabs packages docs --check"
88
},
+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
3+
import FetchStore from "../src/fetch.js";
4+
5+
// `vitest --api` exposes the port 51204
6+
// ref: https://vitest.dev/config/#api
7+
let href = "http://localhost:51204/fixtures/v3/data.zarr";
8+
9+
describe("FetchStore", () => {
10+
afterEach(() => {
11+
vi.restoreAllMocks();
12+
});
13+
14+
it("reads a file from string url", async () => {
15+
let store = new FetchStore(href);
16+
let bytes = await store.get("/zarr.json");
17+
expect(bytes).toBeInstanceOf(Uint8Array);
18+
expect(JSON.parse(new TextDecoder().decode(bytes))).toMatchInlineSnapshot(`
19+
{
20+
"attributes": {},
21+
"node_type": "group",
22+
"zarr_format": 3,
23+
}
24+
`);
25+
});
26+
27+
it("reads a file from URL", async () => {
28+
let store = new FetchStore(new URL(href));
29+
let bytes = await store.get("/zarr.json");
30+
expect(bytes).toBeInstanceOf(Uint8Array);
31+
expect(JSON.parse(new TextDecoder().decode(bytes))).toMatchInlineSnapshot(`
32+
{
33+
"attributes": {},
34+
"node_type": "group",
35+
"zarr_format": 3,
36+
}
37+
`);
38+
});
39+
40+
it("reads multi-part path", async () => {
41+
let store = new FetchStore(href);
42+
let bytes = await store.get("/1d.chunked.i2/zarr.json");
43+
expect(bytes).toBeInstanceOf(Uint8Array);
44+
expect(JSON.parse(new TextDecoder().decode(bytes))).toMatchInlineSnapshot(`
45+
{
46+
"attributes": {},
47+
"chunk_grid": {
48+
"configuration": {
49+
"chunk_shape": [
50+
2,
51+
],
52+
},
53+
"name": "regular",
54+
},
55+
"chunk_key_encoding": {
56+
"configuration": {
57+
"separator": "/",
58+
},
59+
"name": "default",
60+
},
61+
"codecs": [
62+
{
63+
"configuration": {
64+
"endian": "little",
65+
},
66+
"name": "endian",
67+
},
68+
{
69+
"configuration": {
70+
"blocksize": 0,
71+
"clevel": 5,
72+
"cname": "zstd",
73+
"shuffle": "noshuffle",
74+
"typesize": 4,
75+
},
76+
"name": "blosc",
77+
},
78+
],
79+
"data_type": "int16",
80+
"dimension_names": null,
81+
"fill_value": 0,
82+
"node_type": "array",
83+
"shape": [
84+
4,
85+
],
86+
"zarr_format": 3,
87+
}
88+
`);
89+
});
90+
91+
it("returns undefined for missing file", async () => {
92+
let store = new FetchStore(href);
93+
expect(await store.get("/missing.json")).toBeUndefined();
94+
});
95+
96+
it("forwards request options to fetch", async () => {
97+
let headers = { "x-test": "test" };
98+
let store = new FetchStore(href);
99+
let spy = vi.spyOn(globalThis, "fetch");
100+
await store.get("/zarr.json", { headers });
101+
expect(spy).toHaveBeenCalledWith(href + "/zarr.json", { headers });
102+
});
103+
104+
it("forwards request options to fetch when configured globally", async () => {
105+
let headers = { "x-test": "test" };
106+
let store = new FetchStore(href, { headers });
107+
let spy = vi.spyOn(globalThis, "fetch");
108+
await store.get("/zarr.json");
109+
expect(spy).toHaveBeenCalledWith(href + "/zarr.json", { headers });
110+
});
111+
112+
it("overrides request options", async () => {
113+
let opts: RequestInit = {
114+
headers: { "x-test": "root", "x-test2": "root" },
115+
cache: "no-cache",
116+
};
117+
let store = new FetchStore(href, opts);
118+
let spy = vi.spyOn(globalThis, "fetch");
119+
await store.get("/zarr.json", { headers: { "x-test": "override" } });
120+
expect(spy).toHaveBeenCalledWith(href + "/zarr.json", {
121+
headers: { "x-test": "override" },
122+
cache: "no-cache",
123+
});
124+
});
125+
126+
it("checks if key exists", async () => {
127+
let store = new FetchStore(href);
128+
expect(await store.has("/zarr.json")).toBe(true);
129+
expect(await store.has("/missing.json")).toBe(false);
130+
});
131+
});

packages/storage/__tests__/fs.test.ts

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
2+
3+
import * as fs from "node:fs/promises";
4+
import * as path from "node:path";
5+
import * as url from "node:url";
6+
7+
import FileSystemStore from "../src/fs.js";
8+
9+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
10+
const store_path = path.join(__dirname, "teststore");
11+
12+
beforeAll(async () => {
13+
await fs.mkdir(store_path);
14+
});
15+
16+
afterAll(async () => {
17+
await fs.rm(store_path, { recursive: true });
18+
});
19+
20+
describe("FileSystemStore", () => {
21+
it("writes a file", async () => {
22+
const store = new FileSystemStore(store_path);
23+
await store.set("/foo-write", new TextEncoder().encode("bar"));
24+
expect(
25+
await fs.readFile(path.join(store_path, "foo-write"), "utf-8"),
26+
).toBe("bar");
27+
});
28+
it("reads a file", async () => {
29+
const store = new FileSystemStore(store_path);
30+
await fs.writeFile(path.join(store_path, "foo-read"), "bar");
31+
expect(
32+
await store.get("/foo-read").then((buf) => new TextDecoder().decode(buf)),
33+
).toBe("bar");
34+
});
35+
it("returns undefined for a non-existent file", async () => {
36+
const store = new FileSystemStore(store_path);
37+
expect(await store.get("/foo-does-not-exist")).toBe(undefined);
38+
});
39+
it("deletes a file", async () => {
40+
const store = new FileSystemStore(store_path);
41+
await fs.writeFile(path.join(store_path, "foo-delete"), "bar");
42+
await store.delete("/foo-delete");
43+
expect(
44+
await fs
45+
.readFile(path.join(store_path, "foo-delete"), "utf-8")
46+
.catch((err) => err.code),
47+
).toBe("ENOENT");
48+
});
49+
it("checks if a file exists", async () => {
50+
const store = new FileSystemStore(store_path);
51+
await fs.writeFile(path.join(store_path, "foo-exists"), "bar");
52+
expect(await store.has("/foo-exists")).toBe(true);
53+
expect(await store.has("/foo-does-not-exist")).toBe(false);
54+
});
55+
});

packages/storage/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
},
1010
"exports": {
1111
".": {
12-
"types": "./dist/index.d.ts",
13-
"import": "./dist/index.js"
12+
"types": "./dist/src/index.d.ts",
13+
"import": "./dist/src/index.js"
1414
},
1515
"./*": {
16-
"types": "./dist/*.d.ts",
17-
"import": "./dist/*.js"
16+
"types": "./dist/src/*.d.ts",
17+
"import": "./dist/src/*.js"
1818
}
1919
},
2020
"dependencies": {

packages/storage/src/fetch.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,23 @@ function resolve(root: string | URL, path: AbsolutePath): URL {
1717
* Must polyfill `fetch` for use in Node.js.
1818
*
1919
* ```typescript
20-
* import * as zarr from "zarrita/v2";
20+
* import * as zarr from "@zarrita/core";
2121
* const store = new FetchStore("http://localhost:8080/data.zarr");
22-
* const arr = await zarr.get_array(store);
22+
* const arr = await zarr.get(store, { kind: "array" });
2323
* ```
2424
*/
2525
class FetchStore implements Async<Readable<RequestInit>> {
26-
constructor(public url: string | URL) {}
26+
constructor(
27+
public url: string | URL,
28+
public options: RequestInit = {},
29+
) {}
2730

2831
async get(
2932
key: AbsolutePath,
3033
opts: RequestInit = {},
3134
): Promise<Uint8Array | undefined> {
3235
const { href } = resolve(this.url, key);
33-
const res = await fetch(href, opts);
36+
const res = await fetch(href, { ...this.options, ...opts });
3437
if (res.status === 404 || res.status === 403) {
3538
return undefined;
3639
}
@@ -39,7 +42,7 @@ class FetchStore implements Async<Readable<RequestInit>> {
3942
}
4043

4144
has(key: AbsolutePath): Promise<boolean> {
42-
// TODO: make parameter, use HEAD request if possible.
45+
// TODO: make parameter, use HEAD request if possible?
4346
return this.get(key).then((res) => res !== undefined);
4447
}
4548
}

packages/storage/src/ref.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,31 @@ import { parse } from "reference-spec-reader";
22
import { fetch_range, strip_prefix, uri2href } from "./util.js";
33
import type { AbsolutePath, Async, Readable } from "./types.js";
44

5+
type ReferenceEntry = string | [url: string | null] | [
6+
url: string | null,
7+
offset: number,
8+
length: number,
9+
];
10+
511
interface ReferenceStoreOptions {
612
target?: string | URL;
713
}
814

915
/** @experimental */
1016
class ReferenceStore implements Async<Readable<RequestInit>> {
11-
private target?: string | URL;
17+
#refs: Map<string, ReferenceEntry>;
18+
#opts: ReferenceStoreOptions;
1219

1320
constructor(
14-
private refs: ReturnType<typeof parse>,
21+
refs: Map<string, ReferenceEntry>,
1522
opts: ReferenceStoreOptions = {},
1623
) {
17-
this.target = opts.target;
24+
this.#refs = refs;
25+
this.#opts = opts;
1826
}
1927

2028
async get(key: AbsolutePath, opts: RequestInit = {}) {
21-
let ref = this.refs.get(strip_prefix(key));
29+
let ref = this.#refs.get(strip_prefix(key));
2230

2331
if (!ref) return;
2432

@@ -30,7 +38,7 @@ class ReferenceStore implements Async<Readable<RequestInit>> {
3038
}
3139

3240
let [urlOrNull, offset, size] = ref;
33-
let url = urlOrNull ?? this.target;
41+
let url = urlOrNull ?? this.#opts.target;
3442
if (!url) {
3543
throw Error(`No url for key ${key}, and no target url provided.`);
3644
}
@@ -47,16 +55,16 @@ class ReferenceStore implements Async<Readable<RequestInit>> {
4755
}
4856

4957
async has(key: AbsolutePath) {
50-
return this.refs.has(strip_prefix(key));
58+
return this.#refs.has(strip_prefix(key));
5159
}
5260

53-
static fromSpec(spec: Record<string, any>, opts: ReferenceStoreOptions = {}) {
61+
static fromSpec(spec: Record<string, any>, opts?: ReferenceStoreOptions) {
5462
let refs = parse(spec);
5563
return new ReferenceStore(refs, opts);
5664
}
5765

58-
static async fromUrl(refUrl: string | URL, opts: ReferenceStoreOptions = {}) {
59-
let spec = await fetch(refUrl as string).then((res) => res.json());
66+
static async fromUrl(url: string | URL, opts?: ReferenceStoreOptions) {
67+
let spec = await fetch(url).then((res) => res.json());
6068
return ReferenceStore.fromSpec(spec, opts);
6169
}
6270
}

0 commit comments

Comments
 (0)