Skip to content

Commit 5b0e3ea

Browse files
committed
fix: lru cache to support range requests
1 parent 88fab52 commit 5b0e3ea

File tree

2 files changed

+54
-19
lines changed

2 files changed

+54
-19
lines changed

src/lru-store.ts

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,55 @@
11
import type { Readable } from '@zarrita/storage';
22
import QuickLRU from 'quick-lru';
33

4-
export class LRUCacheStore<S extends Readable> {
5-
cache: QuickLRU<string, Promise<Uint8Array | undefined>>;
6-
constructor(public store: S, maxSize: number = 100) {
7-
this.cache = new QuickLRU({ maxSize });
8-
}
9-
get(...args: Parameters<S['get']>) {
10-
const [key, opts] = args;
11-
if (this.cache.has(key)) {
12-
return this.cache.get(key)!;
4+
type RangeQuery =
5+
| {
6+
offset: number;
7+
length: number;
138
}
14-
const value = Promise.resolve(this.store.get(key, opts)).catch((err) => {
15-
this.cache.delete(key);
9+
| {
10+
suffixLength: number;
11+
};
12+
13+
function normalizeKey(key: string, range?: RangeQuery) {
14+
if (!range) return key;
15+
if ('suffixLength' in range) return `${key}:-${range.suffixLength}`;
16+
return `${key}:${range.offset}:${range.offset + range.length - 1}`;
17+
}
18+
19+
export function lru<S extends Readable>(store: S, maxSize: number = 100) {
20+
const cache = new QuickLRU<string, Promise<Uint8Array | undefined>>({ maxSize });
21+
let getRange = store.getRange ? store.getRange.bind(store) : undefined;
22+
function get(...args: Parameters<S['get']>) {
23+
const [key, opts] = args;
24+
const cacheKey = normalizeKey(key);
25+
const cached = cache.get(cacheKey);
26+
if (cached) return cached;
27+
const result = Promise.resolve(store.get(key, opts)).catch((err) => {
28+
cache.delete(cacheKey);
1629
throw err;
1730
});
18-
this.cache.set(key, value);
19-
return value;
31+
cache.set(cacheKey, result);
32+
return result;
33+
}
34+
if (getRange) {
35+
getRange = (...args: Parameters<NonNullable<S['getRange']>>) => {
36+
const [key, range, opts] = args;
37+
const cacheKey = normalizeKey(key, range);
38+
const cached = cache.get(cacheKey);
39+
if (cached) return cached;
40+
const result = Promise.resolve(getRange!(key, range, opts)).catch((err) => {
41+
cache.delete(cacheKey);
42+
throw err;
43+
});
44+
cache.set(cacheKey, result);
45+
return result;
46+
};
2047
}
48+
return new Proxy(store, {
49+
get(target, prop, receiver) {
50+
if (prop === 'get') return get;
51+
if (prop === 'getRange' && getRange) return getRange;
52+
return Reflect.get(target, prop, receiver);
53+
},
54+
});
2155
}

src/utils.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { ZarrPixelSource } from '@hms-dbmi/viv';
2-
import { Matrix4 } from 'math.gl';
3-
import { LRUCacheStore } from './lru-store';
4-
import type { ViewState } from './state';
5-
2+
import type { Slice } from '@zarrita/indexing';
63
import * as zarr from '@zarrita/core';
7-
import { slice, get, type Slice } from '@zarrita/indexing';
4+
import { slice, get } from '@zarrita/indexing';
85
import { FetchStore, Readable } from '@zarrita/storage';
6+
import { Matrix4 } from 'math.gl';
7+
8+
import { lru } from './lru-store';
9+
import type { ViewState } from './state';
910

1011
export const MAX_CHANNELS = 6;
1112

@@ -38,7 +39,7 @@ async function normalizeStore(source: string | Readable): Promise<zarr.Location<
3839
}
3940

4041
// Wrap remote stores in a cache
41-
return zarr.root(new LRUCacheStore(store));
42+
return zarr.root(lru(store));
4243
}
4344

4445
return zarr.root(source);

0 commit comments

Comments
 (0)