Skip to content

Commit

Permalink
feat(core): Add tryWithConsolidated helper (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
manzt authored Jan 14, 2024
1 parent 4d177d8 commit 191d95c
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 18 deletions.
23 changes: 23 additions & 0 deletions .changeset/green-pandas-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@zarrita/core": minor
---

feat: Add `tryWithConsolidated` store helper

Provides a convenient way to open a store that may or may not have consolidated
metadata. Ideal for usage senarios with known access paths, since store with
consolidated metadata do not incur additional network requests when accessing
underlying groups and arrays.

```js
import * as zarr from "zarrita";

let store = await zarr.tryWithConsolidated(
new zarr.FetchStore("https://localhost:8080/data.zarr");
);

// The following do not read from the store
// (make network requests) if it is consolidated.
let grp = await zarr.open(store, { kind: "group" });
let foo = await zarr.open(grp.resolve("foo"), { kind: "array" });
```
32 changes: 31 additions & 1 deletion packages/core/__tests__/consolidated.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import * as path from "node:path";
import * as url from "node:url";

import { FileSystemStore } from "@zarrita/storage";
import { withConsolidated } from "../src/consolidated.js";
import { tryWithConsolidated, withConsolidated } from "../src/consolidated.js";
import { open } from "../src/open.js";
import { Array as ZarrArray } from "../src/hierarchy.js";
import { NodeNotFoundError } from "../src/errors.js";

let __dirname = path.dirname(url.fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -94,4 +95,33 @@ describe("withConsolidated", () => {
let arr = await open(grp.resolve("1d.chunked.i2"), { kind: "array" });
expect(arr.kind).toBe("array");
});

it("throws if consolidated metadata is missing", async () => {
let root = path.join(
__dirname,
"../../../fixtures/v2/data.zarr/3d.contiguous.i2",
);
let try_open = () => withConsolidated(new FileSystemStore(root));
await expect(try_open).rejects.toThrowError(NodeNotFoundError);
await expect(try_open).rejects.toThrowErrorMatchingInlineSnapshot(
'"Node not found: v2 consolidated metadata"',
);
});
});

describe("tryWithConsolidated", () => {
it("creates Listable from consolidated store", async () => {
let root = path.join(__dirname, "../../../fixtures/v2/data.zarr");
let store = await tryWithConsolidated(new FileSystemStore(root));
expect(store).toHaveProperty("contents");
});

it("falls back to original store if missing consolidated metadata", async () => {
let root = path.join(
__dirname,
"../../../fixtures/v2/data.zarr/3d.contiguous.i2",
);
let store = await tryWithConsolidated(new FileSystemStore(root));
expect(store).toBeInstanceOf(FileSystemStore);
});
});
18 changes: 12 additions & 6 deletions packages/core/__tests__/open.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,9 +463,12 @@ describe("v2", () => {
});

it("throws when group is not found", async () => {
await expect(open.v2(store.resolve("/not/a/group"), { kind: "group" }))
.rejects
.toThrow(NodeNotFoundError);
let try_open = () =>
open.v2(store.resolve("/not/a/group"), { kind: "group" });
await expect(try_open).rejects.toThrowError(NodeNotFoundError);
await expect(try_open).rejects.toThrowErrorMatchingInlineSnapshot(
'"Node not found: v2 group"',
);
});

describe("opens array from group", async () => {
Expand Down Expand Up @@ -717,9 +720,12 @@ describe("v3", () => {
});

it("throws when group not found", async () => {
await expect(open.v3(store.resolve("/not/a/group"), { kind: "group" }))
.rejects
.toThrow(NodeNotFoundError);
const try_open = () =>
open.v3(store.resolve("/not/a/group"), { kind: "group" });
await expect(try_open).rejects.toThrowError(NodeNotFoundError);
await expect(try_open).rejects.toThrowErrorMatchingInlineSnapshot(
'"Node not found: v3 array or group"',
);
});

describe("opens array from group", async () => {
Expand Down
58 changes: 47 additions & 11 deletions packages/core/src/consolidated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,33 @@ function is_v3(meta: Metadata): meta is ArrayMetadata | GroupMetadata {
return "zarr_format" in meta && meta.zarr_format === 3;
}

/**
* Open a consolidated store.
*
* This will open a store with Zarr v2 consolidated metadata (`.zmetadata`).
* @see {@link https://zarr.readthedocs.io/en/stable/spec/v2.html#consolidated-metadata}
*
* @param store The store to open.
* @returns A listable store.
*
* @example
* ```js
* let store = await withConsolidated(
* new zarr.FetchStore("https://my-bucket.s3.amazonaws.com");
* );
* store.contents(); // [{ path: "/", kind: "group" }, { path: "/foo", kind: "array" }, ...]
* let grp = zarr.open(store); // Open the root group.
* let foo = zarr.open(grp.resolve(contents[1].path)); // Open the foo array
* ```
*/
export async function withConsolidated<Store extends Readable>(
store: Store,
): Promise<Listable<Store>> {
let known_meta: Record<AbsolutePath, Metadata> =
await get_consolidated_metadata(store)
.then((meta) => {
let new_meta: Record<AbsolutePath, Metadata> = {};
for (let [key, value] of Object.entries(meta.metadata)) {
new_meta[`/${key}`] = value;
}
return new_meta;
})
.catch(() => ({}));

let v2_meta = await get_consolidated_metadata(store);
let known_meta: Record<AbsolutePath, Metadata> = {};
for (let [key, value] of Object.entries(v2_meta.metadata)) {
known_meta[`/${key}`] = value;
}
return {
async get(
...args: Parameters<Store["get"]>
Expand Down Expand Up @@ -100,3 +113,26 @@ export async function withConsolidated<Store extends Readable>(
},
};
}

/**
* Try to open a consolidated store, but fall back to the original store if the
* consolidated metadata is missing.
*
* Provides a convenient way to open a store that may or may not have consolidated,
* returning a consistent interface for both cases. Ideal for usage senarios with
* known access paths, since store with consolidated metadata do not incur
* additional network requests when accessing underlying groups and arrays.
*
* @param store The store to open.
* @returns A listable store.
*/
export async function tryWithConsolidated<Store extends Readable>(
store: Store,
): Promise<Listable<Store> | Store> {
return withConsolidated(store).catch((e: unknown) => {
if (e instanceof NodeNotFoundError) {
return store;
}
throw e;
});
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export {
export { open } from "./open.js";
export { create } from "./create.js";
export { registry } from "./codecs.js";
export { tryWithConsolidated, withConsolidated } from "./consolidated.js";
export type * from "./metadata.js";
2 changes: 2 additions & 0 deletions packages/zarrita/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ it("exports all the things", () => {
"root": [Function],
"set": [Function],
"slice": [Function],
"tryWithConsolidated": [Function],
"withConsolidated": [Function],
}
`);
});

0 comments on commit 191d95c

Please sign in to comment.