Open
Conversation
🦋 Changeset detectedLatest commit: a0b6bfe The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
bfb66b7 to
37b46f5
Compare
04f0ee6 to
9c1bf45
Compare
Stores in zarrita can now be wrapped with composable middleware using
`wrapStore`. Each middleware intercepts store methods via Proxy and can
add new methods, with automatic delegation for anything not overridden.
Middleware supports a dual API — direct `withX(store, opts)` and curried
`withX(opts)` for use with the new `createStore` pipeline.
For middleware whose options depend on the store's request options type
(e.g., `mergeOptions` in range batching), `wrapStore.generic` uses a
higher-kinded type encoding via the `GenericOptions` interface to flow
the store's `O` type through to the options at the call site.
`withConsolidated` and `tryWithConsolidated` are renamed to
`withConsolidation` and `withMaybeConsolidation` for consistency. The old
names are re-exported with `@deprecated` JSDoc. `BatchedRangeStore` class
is replaced by `withRangeBatching` built on `wrapStore.generic`.
```ts
let store = await createStore(
new FetchStore("https://..."),
withConsolidation({ format: "v3" }),
withRangeBatching(),
);
store.contents(); // from consolidation
store.stats; // from batching
```
Contributor
The dual-callable pattern (direct + curried) on store middleware was
causing TypeScript to lose generic store options in pipeline
composition. For example, `mergeOptions`'s `batch` parameter was
`unknown[]` instead of `ReadonlyArray<RequestInit | undefined>` because
the curried form captured options before the store type was known.
This removes currying entirely in favor of the pattern used by
fetch-extras: pipelines use arrow functions `(s) => withX(s, opts)`
which naturally give TypeScript the store type at each step. No-config
middleware like `withConsolidation` can be passed uncalled since the
direct form has optional opts.
Renames `wrapStore` to `defineStoreMiddleware` and `createStore` to
`storeFrom`. The factory constraint now uses `AsyncReadable` (not `any`)
for the store parameter and `Partial<AsyncReadable>` for the return
type, giving autocomplete on both `store.get` and the returned
`get`/`getRange` overrides inside the factory body. Also removes
`RequireOverrides`, `HasGet`, `_dual`, and `isReadable` which were all
artifacts of the curried approach.
```ts
import * as zarr from "zarrita";
let store = await zarr.storeFrom(
new zarr.FetchStore("https://..."),
zarr.withConsolidation,
(s) => zarr.withRangeBatching(s, {
mergeOptions: (batch) => batch[0],
// ^? ReadonlyArray<RequestInit | undefined>
}),
);
```
ba97c72 to
6827720
Compare
Owner
Author
|
Here is a bigger example: import * as zarr from "zarrita";
// Simple middleware — adds getRange fallback for stores that lack it
const withEnsureGetRange = zarr.defineStoreMiddleware((store) => ({
async getRange(key, range, opts) {
if (store.getRange) return store.getRange(key, range, opts);
let data = await store.get(key, opts);
if (!data) return undefined;
if ("suffixLength" in range) {
return data.slice(data.length - range.suffixLength);
}
return data.slice(range.offset, range.offset + range.length);
},
}));
// Generic middleware — opts are inferred from the store's options type
interface LoggingOptions<O = unknown> {
format?: (key: string, options: O | undefined) => string;
}
interface LoggingOptsFor extends zarr.GenericOptions {
readonly options: LoggingOptions<this["_O"]>;
}
const withLogging = zarr.defineStoreMiddleware.generic<LoggingOptsFor>()(
(store, opts: LoggingOptions = {}) => {
let fmt = opts.format ?? ((key) => `GET ${key}`);
return {
async get(key, options) {
console.log(fmt(key, options));
return store.get(key, options);
},
};
},
);
// Compose in a pipeline — arrow functions give full type inference
let store = await zarr.storeFrom(
new zarr.FetchStore("https://example.com/data.zarr"),
withEnsureGetRange,
(s) =>
withLogging(s, {
format: (key, opts) => `${key} [signal=${opts?.signal?.aborted}]`,
// ^? RequestInit | undefined
}),
zarr.withConsolidation,
(s) => zarr.withRangeBatching(s, { mergeOptions: (batch) => batch[0] }),
);
store.contents(); // from consolidation
store.stats; // from batching |
The middleware result type was an opaque intersection chain like
`FetchStore & { contents: ... } & { stats: ... }` which was hard to read
in tooltips and error messages. This collapses the store's
`get`/`getRange` back into `AsyncReadable<O>` and shows only the
middleware extensions separately, so the result reads as
`AsyncReadable<RequestInit> & { contents: ..., stats: ... }`.
If the store or any middleware provides a concrete `getRange`, the
collapsed type uses `Required<AsyncReadable<O>>` to preserve the
non-optional guarantee. A store that only has `get` shows the default
`AsyncReadable<O>` with `getRange?` optional.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Introduces a composable store middleware system for wrapping stores with additional behavior (caching, batching, consolidated metadata, etc.) via Proxy delegation.
Core APIs:
zarr.defineStoreMiddleware(factory)— define a middleware that intercepts store methods. The factory receives anAsyncReadablestore and options, returns overrides and extensions. Unoverridden methods are automatically delegated via Proxy.zarr.defineStoreMiddleware.generic<OptsLambda>()(factory)— variant for middleware whose options depend on the store's request options type (usesGenericOptionsfor higher-kinded type encoding).zarr.storeFrom(store, ...middleware)— compose middleware in a pipeline. Each step is an arrow function that receives the store with full type inference.Built-in middleware:
zarr.withConsolidation— loads consolidated metadata (v2/v3), interceptsgetfor cached metadata lookups, addscontents()for listingzarr.withRangeBatching— batches concurrentgetRange()calls within a microtask tick, coalesces adjacent ranges, LRU caching