Skip to content

Commit ba97c72

Browse files
committed
Simplify middleware API to direct-only form
The original middleware system supported a "dual" API where each middleware could be called directly (withX(store, opts)) or curried (withX(opts)(store)) for use in createStore pipelines. The curried form couldn't properly infer generic store options (e.g., RequestInit from FetchStore), leading to unknown types in callbacks like mergeOptions. This replaces the curried approach with a simpler pattern inspired by fetch-extras: pipelines use arrow functions (s => withX(s, opts)) which naturally give TypeScript the store type for full inference. The curried overloads, _dual helper, and related disambiguation types are removed. Also renames wrapStore → defineStoreMiddleware and createStore → storeFrom for clarity, constrains factories to AsyncReadable (all middleware is async), and adds Partial<AsyncReadable> return constraint for autocomplete in the factory body.
1 parent 065c721 commit ba97c72

File tree

7 files changed

+290
-283
lines changed

7 files changed

+290
-283
lines changed

.changeset/middleware-store-wrappers.md

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
"zarrita": minor
33
---
44

5-
Introduce composable store middleware via `wrapStore`
5+
Introduce composable store middleware via `defineStoreMiddleware`
66

77
**New APIs:**
88

9-
- `wrapStore(factory)` — define a store middleware with dual API (direct + curried) and automatic Proxy delegation. Supports sync and async factories.
10-
- `wrapStore.generic<OptsLambda>()(factory)` — for middleware whose options depend on the store's request options type (e.g., `mergeOptions`). Uses `GenericOptions` interface for higher-kinded type encoding.
11-
- `createStore(store, ...middleware)` — compose middleware in a pipeline. Returns `Promise` to support async middleware like `withConsolidation`.
9+
- `zarr.defineStoreMiddleware(factory)` — define a store middleware with automatic Proxy delegation. The factory receives an `AsyncReadable` store and options, returning overrides and extensions. Supports sync and async factories.
10+
- `zarr.defineStoreMiddleware.generic<OptsLambda>()(factory)` — for middleware whose options depend on the store's request options type (e.g., `mergeOptions`). Uses `GenericOptions` interface for higher-kinded type encoding.
11+
- `zarr.storeFrom(store, ...middleware)` — compose middleware in a pipeline. Each middleware is `(store) => newStore`. Returns `Promise` to support async middleware like `withConsolidation`.
1212

1313
**Renamed:**
1414

@@ -23,26 +23,22 @@ Introduce composable store middleware via `wrapStore`
2323
The previous exports are still available but deprecated. Update your imports:
2424

2525
```ts
26-
// Before
27-
import { withConsolidated, tryWithConsolidated, withRangeBatching } from "zarrita";
26+
import * as zarr from "zarrita";
2827

29-
// After
30-
import { withConsolidation, withMaybeConsolidation, withRangeBatching } from "zarrita";
31-
32-
// Pipeline composition
33-
let store = await createStore(
34-
new FetchStore("https://..."),
35-
withConsolidation({ format: "v3" }),
36-
withRangeBatching(),
28+
// Pipeline composition (use arrow functions for full type inference)
29+
let store = await zarr.storeFrom(
30+
new zarr.FetchStore("https://..."),
31+
zarr.withConsolidation,
32+
(s) => zarr.withRangeBatching(s, { mergeOptions: (batch) => batch[0] }),
3733
);
3834
```
3935

4036
**Defining custom middleware:**
4137

4238
```ts
43-
import { wrapStore } from "zarrita";
39+
import * as zarr from "zarrita";
4440

45-
const withCaching = wrapStore(
41+
const withCaching = zarr.defineStoreMiddleware(
4642
(store, opts: { maxSize?: number }) => {
4743
let cache = new Map();
4844
return {
@@ -59,4 +55,4 @@ const withCaching = wrapStore(
5955
);
6056
```
6157

62-
`BatchedRangeStore` class has been removed in favor of `withRangeBatching` built on `wrapStore`.
58+
`BatchedRangeStore` class has been removed in favor of `withRangeBatching` built on `zarr.defineStoreMiddleware`.

packages/zarrita/__tests__/middleware-types.test.ts

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,53 @@ import type { AbsolutePath, AsyncReadable } from "@zarrita/storage";
22
import { expectType } from "tintype";
33
import { describe, test } from "vitest";
44
import * as zarr from "../src/index.js";
5-
import { wrapStore } from "../src/middleware/define.js";
5+
import { defineStoreMiddleware } from "../src/middleware/define.js";
66

7-
describe("createStore", () => {
7+
describe("storeFrom", () => {
88
test("no middleware returns store as-is", () => {
9-
let store = zarr.createStore(new zarr.FetchStore(""));
9+
let store = zarr.storeFrom(new zarr.FetchStore(""));
1010
expectType(store).toMatchInlineSnapshot(`Promise<zarr.FetchStore>`);
1111
});
12+
13+
test("direct form in pipeline infers store options", () => {
14+
let store = zarr.storeFrom(new zarr.FetchStore(""), (s) =>
15+
zarr.withRangeBatching(s, {
16+
mergeOptions: (batch) => {
17+
expectType(batch).toMatchInlineSnapshot(
18+
`ReadonlyArray<RequestInit | undefined>`,
19+
);
20+
return batch[0];
21+
},
22+
}),
23+
);
24+
expectType(store).toMatchInlineSnapshot(`
25+
Promise<
26+
zarr.FetchStore & { stats: Readonly<zarr.RangeBatchingStats> }
27+
>
28+
`);
29+
});
30+
31+
test("no-config middleware can be passed uncalled", () => {
32+
function check() {
33+
return zarr.storeFrom(
34+
new zarr.FetchStore(""),
35+
zarr.withConsolidation,
36+
(s) => zarr.withRangeBatching(s, { mergeOptions: (batch) => batch[0] }),
37+
);
38+
}
39+
expectType(check).toMatchInlineSnapshot(`
40+
() => Promise<
41+
zarr.FetchStore & {
42+
contents: () => { path: AbsolutePath; kind: "array" | "group" }[];
43+
} & { stats: Readonly<zarr.RangeBatchingStats> }
44+
>
45+
`);
46+
});
1247
});
1348

14-
describe("wrapStore", () => {
49+
describe("defineStoreMiddleware", () => {
1550
test("simple: extensions appear, store methods preserved", () => {
16-
let withCustom = wrapStore(
51+
let withCustom = defineStoreMiddleware(
1752
(store: AsyncReadable, _opts: { flag: boolean }) => {
1853
return {
1954
async get(key: AbsolutePath) {
@@ -37,7 +72,7 @@ describe("wrapStore", () => {
3772
retries?: number;
3873
}
3974

40-
let withThing = wrapStore(
75+
let withThing = defineStoreMiddleware(
4176
<O>(store: AsyncReadable<O>, opts: ThingOptions<O>) => {
4277
return {
4378
async get(key: AbsolutePath, options?: O) {
@@ -58,26 +93,30 @@ describe("wrapStore", () => {
5893
});
5994

6095
test("chaining preserves Options through wrappers", () => {
61-
let withA = wrapStore((store: AsyncReadable, _opts: { a: number }) => {
62-
return {
63-
async get(key: AbsolutePath) {
64-
return store.get(key);
65-
},
66-
methodA(): number {
67-
return 1;
68-
},
69-
};
70-
});
71-
let withB = wrapStore((store: AsyncReadable, _opts: { b: string }) => {
72-
return {
73-
async get(key: AbsolutePath) {
74-
return store.get(key);
75-
},
76-
methodB(): string {
77-
return "hello";
78-
},
79-
};
80-
});
96+
let withA = defineStoreMiddleware(
97+
(store: AsyncReadable, _opts: { a: number }) => {
98+
return {
99+
async get(key: AbsolutePath) {
100+
return store.get(key);
101+
},
102+
methodA(): number {
103+
return 1;
104+
},
105+
};
106+
},
107+
);
108+
let withB = defineStoreMiddleware(
109+
(store: AsyncReadable, _opts: { b: string }) => {
110+
return {
111+
async get(key: AbsolutePath) {
112+
return store.get(key);
113+
},
114+
methodB(): string {
115+
return "hello";
116+
},
117+
};
118+
},
119+
);
81120
let store = withB(withA(new zarr.FetchStore(""), { a: 1 }), { b: "x" });
82121
expectType(store).toMatchInlineSnapshot(`
83122
zarr.FetchStore & { methodA: () => number } & {

packages/zarrita/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ export {
4141
withMaybeConsolidation,
4242
withMaybeConsolidation as tryWithConsolidated,
4343
} from "./middleware/consolidation.js";
44-
export { createStore } from "./middleware/create-store.js";
44+
export { createStore as storeFrom } from "./middleware/create-store.js";
4545
export type { GenericOptions } from "./middleware/define.js";
46-
export { wrapStore } from "./middleware/define.js";
46+
export { defineStoreMiddleware } from "./middleware/define.js";
4747
export type {
4848
RangeBatchingOptions,
4949
RangeBatchingStats,

packages/zarrita/src/middleware/consolidation.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AbsolutePath, Readable } from "@zarrita/storage";
1+
import type { AbsolutePath, AsyncReadable, Readable } from "@zarrita/storage";
22
import { JsonDecodeError, KeyError, NodeNotFoundError } from "../errors.js";
33
import type {
44
ArrayMetadata,
@@ -15,7 +15,7 @@ import {
1515
jsonEncodeObject,
1616
rethrowUnless,
1717
} from "../util.js";
18-
import { wrapStore } from "./define.js";
18+
import { defineStoreMiddleware } from "./define.js";
1919

2020
type ConsolidatedMetadataV2 = {
2121
metadata: Record<string, ArrayMetadataV2 | GroupMetadataV2>;
@@ -198,16 +198,16 @@ export type Listable<Store extends Readable> = Store & {
198198
* let store = await zarr.withConsolidation(rawStore, { format: "v3" });
199199
*
200200
* // In a pipeline
201-
* let store = await zarr.createStore(
201+
* let store = await zarr.storeFrom(
202202
* new zarr.FetchStore("https://..."),
203-
* zarr.withConsolidation({ format: "v3" }),
203+
* (s) => zarr.withConsolidation(s, { format: "v3" }),
204204
* );
205205
*
206206
* store.contents(); // [{ path: "/", kind: "group" }, ...]
207207
* ```
208208
*/
209-
export const withConsolidation = wrapStore(
210-
async (store: Readable, opts: ConsolidationOptions = {}) => {
209+
export const withConsolidation = defineStoreMiddleware(
210+
async (store, opts: ConsolidationOptions = {}) => {
211211
let formats = resolveFormats(store, opts.format);
212212
let lastError: unknown;
213213
for (let format of formats) {
@@ -263,7 +263,7 @@ export const withConsolidation = wrapStore(
263263
* Like {@linkcode withConsolidation}, but falls back to the original store if
264264
* no consolidated metadata is found (instead of throwing).
265265
*/
266-
export async function withMaybeConsolidation<Store extends Readable>(
266+
export async function withMaybeConsolidation<Store extends AsyncReadable>(
267267
store: Store,
268268
opts: ConsolidationOptions = {},
269269
): Promise<Listable<Store> | Store> {

packages/zarrita/src/middleware/create-store.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
import type { Readable } from "@zarrita/storage";
1+
import type { AsyncReadable } from "@zarrita/storage";
22

3-
export function createStore<S extends Readable>(store: S): Promise<S>;
4-
export function createStore<S extends Readable, A>(
3+
export function createStore<S extends AsyncReadable>(store: S): Promise<S>;
4+
export function createStore<S extends AsyncReadable, A>(
55
store: S,
66
m1: (store: S) => A,
77
): Promise<Awaited<A>>;
8-
export function createStore<S extends Readable, A, B>(
8+
export function createStore<S extends AsyncReadable, A, B>(
99
store: S,
1010
m1: (store: S) => A,
1111
m2: (store: Awaited<A>) => B,
1212
): Promise<Awaited<B>>;
13-
export function createStore<S extends Readable, A, B, C>(
13+
export function createStore<S extends AsyncReadable, A, B, C>(
1414
store: S,
1515
m1: (store: S) => A,
1616
m2: (store: Awaited<A>) => B,
1717
m3: (store: Awaited<B>) => C,
1818
): Promise<Awaited<C>>;
19-
export function createStore<S extends Readable, A, B, C, D>(
19+
export function createStore<S extends AsyncReadable, A, B, C, D>(
2020
store: S,
2121
m1: (store: S) => A,
2222
m2: (store: Awaited<A>) => B,
2323
m3: (store: Awaited<B>) => C,
2424
m4: (store: Awaited<C>) => D,
2525
): Promise<Awaited<D>>;
26-
export function createStore<S extends Readable, A, B, C, D, E>(
26+
export function createStore<S extends AsyncReadable, A, B, C, D, E>(
2727
store: S,
2828
m1: (store: S) => A,
2929
m2: (store: Awaited<A>) => B,
@@ -32,7 +32,7 @@ export function createStore<S extends Readable, A, B, C, D, E>(
3232
m5: (store: Awaited<D>) => E,
3333
): Promise<Awaited<E>>;
3434
export async function createStore(
35-
store: Readable,
35+
store: AsyncReadable,
3636
...middlewares: ((store: unknown) => unknown)[]
3737
): Promise<unknown> {
3838
let result: unknown = store;

0 commit comments

Comments
 (0)