From 20cc10f69d97159ead2f7e96bf4cfcf9584bd15f Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 2 Oct 2025 09:51:22 -0700 Subject: [PATCH 01/91] PriorityCache updates --- packages/core/src/index.ts | 1 + .../shared-priority-cache/priority-cache.ts | 144 +++++++++++------- .../src/shared-priority-cache/shared-cache.ts | 21 +-- .../core/src/shared-priority-cache/utils.ts | 4 +- packages/omezarr/package.json | 3 +- pnpm-lock.yaml | 9 ++ site/src/examples/omezarr/render-utils.ts | 6 +- 7 files changed, 118 insertions(+), 70 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5a9711bc..558188d7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -20,4 +20,5 @@ export type { export { RenderServer } from './abstract/render-server'; export { Logger, logger } from './logger'; +export { PriorityCache, AsyncPriorityCache, type Cacheable } from './shared-priority-cache/priority-cache'; export { SharedPriorityCache } from './shared-priority-cache/shared-cache'; diff --git a/packages/core/src/shared-priority-cache/priority-cache.ts b/packages/core/src/shared-priority-cache/priority-cache.ts index 782c301a..b2983e98 100644 --- a/packages/core/src/shared-priority-cache/priority-cache.ts +++ b/packages/core/src/shared-priority-cache/priority-cache.ts @@ -2,11 +2,11 @@ import { MinHeap } from './min-heap'; import { KeyedMinHeap } from './keyed-heap'; type CacheKey = string; -export interface Resource { +export interface Cacheable { destroy?: () => void; sizeInBytes: () => number; } -export interface Store { +export interface Store { set(k: K, v: V): void; get(k: K): V | undefined; has(k: K): boolean; @@ -14,45 +14,37 @@ export interface Store { keys(): Iterable; values(): Iterable; } -type PendingResource = { +type PendingResource = { key: CacheKey; - fetch: (sig: AbortSignal) => Promise; + fetch: (sig: AbortSignal) => Promise; }; function negate(fn: (k: CacheKey) => number) { return (k: CacheKey) => -fn(k); } export type FetchResult = { status: 'success' } | { status: 'failure'; reason: unknown }; -export class PriorityCache { - private store: Store; + +export class PriorityCache { + private store: Store; private evictPriority: MinHeap; - private fetchPriority: KeyedMinHeap; - private pendingFetches: Map; private limit: number; private used: number; - private MAX_INFLIGHT_FETCHES: number; - private notify: undefined | ((k: CacheKey, result: FetchResult) => void); + // items with lower scores will be evicted before items with high scores constructor( - store: Store, + store: Store, score: (k: CacheKey) => number, limitInBytes: number, - maxFetches: number, - onDataArrived?: (key: CacheKey, result: FetchResult) => void, ) { this.store = store; this.evictPriority = new MinHeap(5000, score); - this.fetchPriority = new KeyedMinHeap(5000, negate(score), (pr) => pr.key); this.limit = limitInBytes; - this.pendingFetches = new Map(); this.used = 0; - this.MAX_INFLIGHT_FETCHES = maxFetches; - this.notify = onDataArrived; } // add {key:item} to the cache - return false (and fail) if the key is already present // may evict items to make room // return true on success - put(key: CacheKey, item: Resource): boolean { + put(key: CacheKey, item: T): boolean { if (this.store.has(key)) { return false; } @@ -65,12 +57,84 @@ export class PriorityCache { this.used += size; return true; } - private sanitizedSize(item: Resource) { + private sanitizedSize(item: T) { const givenSize = item.sizeInBytes?.() ?? 0; const size = Number.isFinite(givenSize) ? Math.max(0, givenSize) : 0; return size; } - enqueue(key: CacheKey, fetcher: (abort: AbortSignal) => Promise) { + + // it is expected that the score function is not "pure" - + // it has a closure over data that changes over time, representing changing priorities + // thus - the owner of this cache has a responsibility to notify the cache when significant + // changes in priority occur! + reprioritize(score: (k: CacheKey) => number) { + this.evictPriority.rebuild(score); + } + + get(key: CacheKey): T | undefined { + return this.store.get(key); + } + + has(key: CacheKey): boolean { + return this.store.has(key); + } + + cached(key: CacheKey): boolean { + return this.store.has(key); + } + + isFull(): boolean { + return this.used >= this.limit; + } + + private evictLowestPriority() { + const evictMe = this.evictPriority.popMinItem(); + if (evictMe === null) return false; + + const data = this.store.get(evictMe); + if (data) { + data.destroy?.(); + this.store.delete(evictMe); + const size = this.sanitizedSize(data); + this.used -= size; + } + return true; + } + + private evictUntil(targetUsedBytes: number) { + while (this.used > targetUsedBytes) { + if (!this.evictLowestPriority()) { + // note: evictLowestPriority mutates this.used + return; // all items evicted... + } + } + } +} + + +export class AsyncPriorityCache extends PriorityCache { + private fetchPriority: KeyedMinHeap, CacheKey>; + private pendingFetches: Map; + private MAX_INFLIGHT_FETCHES: number; + private notify: undefined | ((k: CacheKey, result: FetchResult) => void); + + // items with lower scores will be evicted before items with high scores + constructor( + store: Store, + score: (k: CacheKey) => number, + limitInBytes: number, + maxFetches: number, + onDataArrived?: (key: CacheKey, result: FetchResult) => void, + ) { + super(store, score, limitInBytes); + + this.fetchPriority = new KeyedMinHeap, CacheKey>(5000, negate(score), (pr) => pr.key); + this.pendingFetches = new Map(); + this.MAX_INFLIGHT_FETCHES = maxFetches; + this.notify = onDataArrived; + } + + enqueue(key: CacheKey, fetcher: (abort: AbortSignal) => Promise) { // enqueue the item, if we dont already have it, or are not already asking if (!this.has(key) && !this.pendingFetches.has(key) && !this.fetchPriority.hasItemWithKey(key)) { this.fetchPriority.addItem({ key, fetch: fetcher }); @@ -79,7 +143,8 @@ export class PriorityCache { } return false; } - private beginFetch({ key, fetch }: PendingResource) { + + private beginFetch({ key, fetch }: PendingResource) { const abort = new AbortController(); this.pendingFetches.set(key, abort); return fetch(abort.signal) @@ -95,6 +160,7 @@ export class PriorityCache { this.fetchToLimit(); }); } + private fetchToLimit() { let toFetch = Math.max(0, this.MAX_INFLIGHT_FETCHES - this.pendingFetches.size); for (let i = 0; i < toFetch; i++) { @@ -116,8 +182,8 @@ export class PriorityCache { // it has a closure over data that changes over time, representing changing priorities // thus - the owner of this cache has a responsibility to notify the cache when significant // changes in priority occur! - reprioritize(score: (k: CacheKey) => number) { - this.evictPriority.rebuild(score); + override reprioritize(score: (k: CacheKey) => number) { + super.reprioritize(score); this.fetchPriority.rebuild(negate(score)); for (const [key, abort] of this.pendingFetches) { if (score(key) === 0) { @@ -126,38 +192,8 @@ export class PriorityCache { } } } - get(key: CacheKey): Resource | undefined { - return this.store.get(key); - } - has(key: CacheKey): boolean { - return this.store.has(key); - } cachedOrPending(key: CacheKey): boolean { - return this.store.has(key) || this.fetchPriority.hasItemWithKey(key) || this.pendingFetches.has(key); - } - isFull(): boolean { - return this.used >= this.limit; - } - private evictLowestPriority() { - const evictMe = this.evictPriority.popMinItem(); - if (evictMe === null) return false; - - const data = this.store.get(evictMe); - if (data) { - data.destroy?.(); - this.store.delete(evictMe); - const size = this.sanitizedSize(data); - this.used -= size; - } - return true; - } - private evictUntil(targetUsedBytes: number) { - while (this.used > targetUsedBytes) { - if (!this.evictLowestPriority()) { - // note: evictLowestPriority mutates this.used - return; // all items evicted... - } - } + return this.cached(key) || this.fetchPriority.hasItemWithKey(key) || this.pendingFetches.has(key); } } diff --git a/packages/core/src/shared-priority-cache/shared-cache.ts b/packages/core/src/shared-priority-cache/shared-cache.ts index 085a23ac..1b431f3b 100644 --- a/packages/core/src/shared-priority-cache/shared-cache.ts +++ b/packages/core/src/shared-priority-cache/shared-cache.ts @@ -1,6 +1,7 @@ -import { PriorityCache, type Store, type Resource, type FetchResult } from './priority-cache'; +import { type Store, type Cacheable, type FetchResult, AsyncPriorityCache } from './priority-cache'; import { mergeAndAdd, prioritizeCacheKeys, priorityDelta } from './utils'; import uniqueId from 'lodash/uniqueId'; + // goal: we want clients of the cache to experience a type-safe interface - // they expect that the things coming out of the cache are the type they expect (what they put in it) // this is not strictly true, as the cache is shared, and other clients may use different types @@ -10,18 +11,18 @@ import uniqueId from 'lodash/uniqueId'; // metadata {url, bounds} for a tile in a larger dataset. // ItemContent = the actual heavy data that Item is a placeholder for - for example one or more arrays of // raw data used by the client of the cache - the value we are caching. -type CacheInterface> = { +type CacheInterface> = { get: (k: Item) => ItemContent | undefined; has: (k: Item) => boolean; unsubscribeFromCache: () => void; setPriorities: (low: Iterable, high: Iterable) => void; }; -export type ClientSpec> = { - isValue: (v: Record) => v is ItemContent; +export type ClientSpec> = { + isValue: (v: Record) => v is ItemContent; cacheKeys: (item: Item) => { [k in keyof ItemContent]: string }; onDataArrived?: (cacheKey: string, result: FetchResult) => void; - fetch: (item: Item) => { [k in keyof ItemContent]: (abort: AbortSignal) => Promise }; + fetch: (item: Item) => { [k in keyof ItemContent]: (abort: AbortSignal) => Promise }; }; type KV> = readonly [keyof T, T[keyof T]]; @@ -43,13 +44,13 @@ type Client = { | ((cacheKey: string, result: { status: 'success' } | { status: 'failure'; reason: unknown }) => void); }; export class SharedPriorityCache { - private cache: PriorityCache; + private cache: AsyncPriorityCache; private clients: Record; private importance: Record; - constructor(store: Store, limitInBytes: number, max_concurrent_fetches = 10) { + constructor(store: Store, limitInBytes: number, max_concurrent_fetches = 10) { this.importance = {}; this.clients = {}; - this.cache = new PriorityCache( + this.cache = new AsyncPriorityCache( store, (ck) => this.importance[ck] ?? 0, limitInBytes, @@ -57,7 +58,7 @@ export class SharedPriorityCache { (ck, result) => this.onCacheEntryArrived(ck, result), ); } - registerClient>( + registerClient>( spec: ClientSpec, ): CacheInterface { const id = uniqueId('client'); @@ -101,7 +102,7 @@ export class SharedPriorityCache { return { get: (k: Item) => { const keys = spec.cacheKeys(k); - const v = mapFields, Resource | undefined>(keys, (k) => this.cache.get(k)); + const v = mapFields, Cacheable | undefined>(keys, (k) => this.cache.get(k)); return spec.isValue(v) ? v : undefined; }, has: (k: Item) => { diff --git a/packages/core/src/shared-priority-cache/utils.ts b/packages/core/src/shared-priority-cache/utils.ts index 7f3af449..1ef2775a 100644 --- a/packages/core/src/shared-priority-cache/utils.ts +++ b/packages/core/src/shared-priority-cache/utils.ts @@ -1,9 +1,9 @@ -import type { Resource } from './priority-cache'; +import type { Cacheable } from './priority-cache'; import type { ClientSpec } from './shared-cache'; // some utils for tracking changing priorities in our priority cache -export function prioritizeCacheKeys>( +export function prioritizeCacheKeys>( spec: ClientSpec, items: Iterable, priority: 1 | 2, diff --git a/packages/omezarr/package.json b/packages/omezarr/package.json index e53a9358..c9f216c1 100644 --- a/packages/omezarr/package.json +++ b/packages/omezarr/package.json @@ -48,9 +48,10 @@ "registry": "https://npm.pkg.github.com/AllenInstitute" }, "dependencies": { - "@alleninstitute/vis-geometry": "workspace:*", "@alleninstitute/vis-core": "workspace:*", + "@alleninstitute/vis-geometry": "workspace:*", "regl": "2.1.0", + "uuid": "13.0.0", "zarrita": "0.5.3", "zod": "4.1.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 790ff78d..ebfb467d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: regl: specifier: 2.1.0 version: 2.1.0 + uuid: + specifier: 13.0.0 + version: 13.0.0 zarrita: specifier: 0.5.3 version: 0.5.3 @@ -4118,6 +4121,10 @@ packages: resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} engines: {node: '>= 4'} + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + uzip-module@1.0.3: resolution: {integrity: sha512-AMqwWZaknLM77G+VPYNZLEruMGWGzyigPK3/Whg99B3S6vGHuqsyl5ZrOv1UUF3paGK1U6PM0cnayioaryg/fA==} @@ -9240,6 +9247,8 @@ snapshots: utility-types@3.11.0: {} + uuid@13.0.0: {} + uzip-module@1.0.3: {} vfile-location@5.0.3: diff --git a/site/src/examples/omezarr/render-utils.ts b/site/src/examples/omezarr/render-utils.ts index f64ec787..ec87b719 100644 --- a/site/src/examples/omezarr/render-utils.ts +++ b/site/src/examples/omezarr/render-utils.ts @@ -1,4 +1,4 @@ -import type { SharedPriorityCache, CachedTexture, Resource } from '@alleninstitute/vis-core'; +import type { SharedPriorityCache, CachedTexture, Cacheable } from '@alleninstitute/vis-core'; import type { vec2 } from '@alleninstitute/vis-geometry'; import { buildOmeZarrSliceRenderer, @@ -9,7 +9,7 @@ import { } from '@alleninstitute/vis-omezarr'; import type REGL from 'regl'; -class Tex implements Resource { +class Tex implements Cacheable { texture: CachedTexture; constructor(tx: CachedTexture) { this.texture = tx; @@ -65,7 +65,7 @@ export function buildConnectedRenderer( }, isValue: (v): v is Record => renderer.isPrepared( - mapValues(v, (tx: Resource | undefined) => (tx && tx instanceof Tex ? tx.texture : undefined)), + mapValues(v, (tx: Cacheable | undefined) => (tx && tx instanceof Tex ? tx.texture : undefined)), ), onDataArrived: onData, }); From 64a4607852a8b8d6fc8c3731c263e8d29fae6297 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 2 Oct 2025 09:52:19 -0700 Subject: [PATCH 02/91] WIP --- .../omezarr/src/sliceview/slice-renderer.ts | 2 +- .../src/zarr/cached-store/cached-store.ts | 550 ++++++++++++++++++ .../cached-store/fetch-slice.interface.ts | 88 +++ .../zarr/cached-store/fetch-slice.worker.ts | 72 +++ .../src/zarr/cached-store/worker-pool.ts | 128 ++++ .../omezarr/omezarr-via-priority-cache.tsx | 12 +- 6 files changed, 850 insertions(+), 2 deletions(-) create mode 100644 packages/omezarr/src/zarr/cached-store/cached-store.ts create mode 100644 packages/omezarr/src/zarr/cached-store/fetch-slice.interface.ts create mode 100644 packages/omezarr/src/zarr/cached-store/fetch-slice.worker.ts create mode 100644 packages/omezarr/src/zarr/cached-store/worker-pool.ts diff --git a/packages/omezarr/src/sliceview/slice-renderer.ts b/packages/omezarr/src/sliceview/slice-renderer.ts index c835b966..0fbcf4b8 100644 --- a/packages/omezarr/src/sliceview/slice-renderer.ts +++ b/packages/omezarr/src/sliceview/slice-renderer.ts @@ -98,7 +98,7 @@ function isPrepared(cacheData: Record): cach return keys.every((key) => cacheData[key]?.type === 'texture'); } -type Decoder = ( +export type Decoder = ( dataset: OmeZarrMetadata, req: ZarrRequest, level: OmeZarrShapedDataset, diff --git a/packages/omezarr/src/zarr/cached-store/cached-store.ts b/packages/omezarr/src/zarr/cached-store/cached-store.ts new file mode 100644 index 00000000..1b1994bf --- /dev/null +++ b/packages/omezarr/src/zarr/cached-store/cached-store.ts @@ -0,0 +1,550 @@ +import { logger, type WebResource, PriorityCache, Cacheable } from '@alleninstitute/vis-core'; +import { + OmeZarrAttrsSchema, + OmeZarrMetadata, + type OmeZarrAttrs, + type OmeZarrAxis, + type OmeZarrShapedDataset, +} from '../types'; +import * as zarr from 'zarrita'; +import { ZodError } from 'zod'; + +import { Decoder, VoxelTileImage } from '../../sliceview/slice-renderer'; +import { ZarrRequest } from '../loading'; +import { WorkerPool } from './worker-pool'; + +const MAX_ATTRS_CACHE_BYTES = 16 * 2 ** 10; // 16 MB -- aribtrarily chosen at this point +const MAX_DATA_CACHE_BYTES = 256 * 2 ** 10; // 256 MB -- aribtrarily chosen at this point + +// @TODO implement a much more context-aware cache size limiting mechanism +const getAttrsCacheSizeLimit = () => { + return MAX_ATTRS_CACHE_BYTES; +}; + +// @TODO implement a much more context-aware cache size limiting mechanism +const getDataCacheSizeLimit = () => { + return MAX_ATTRS_CACHE_BYTES; +}; + +const asCacheKey = ( + key: zarr.AbsolutePath, + range?: zarr.RangeQuery | undefined +): string => { + const keyStr = JSON.stringify(key); + const rangeStr = range ? JSON.stringify(range) : "no-range"; + return `${keyStr} ${rangeStr}`; +}; + +type FetchStoreOptions = { + overrides?: RequestInit; + useSuffixRequest?: boolean; +}; + +type CopyableCachedFetchStoreOptions = FetchStoreOptions & { + initCache?: ReadonlyMap | undefined; +}; + +class CacheableByteArray implements Cacheable { + #arr: Uint8Array; + + constructor(arr: Uint8Array) { + this.#arr = arr; + } + + destroy() { } + + sizeInBytes(): number { + return this.#arr.byteLength; + } + + buffer(): ArrayBufferLike { + return this.#arr.buffer; + } +} + +type CacheKey = string; + +type TransferrableRequestInit = Omit & { + body?: string; + headers?: [string, string][] | Record +}; + +/** + * The parameters required to create a matching CachedFetchStore within a + * + */ +export type CachedFetchStoreInit = { + url: string | URL, + maxBytes: number, + initCache: Map, + options?: { + overrides?: TransferrableRequestInit, + useSuffixRequest?: boolean + } +}; + +export class CachedMultithreadedFetchStore extends zarr.FetchStore { + #workerPool: WorkerPool; + #dataCache: PriorityCache; + + /** + * Maps cache keys to numeric times; the higher the time, the higher the priority. + * + * This effectively means that more frequently-requested items will be kept longer. + */ + #priorityMap: Map; + + constructor( + url: string | URL, + maxBytes: number, + options?: FetchStoreOptions + ) { + super(url, options); + this.#dataCache = new PriorityCache(new Map(), (h: CacheKey) => this.score(h), maxBytes); + this.#priorityMap = new Map; + } + + protected score(key: CacheKey): number { + return this.#priorityMap.get(key) ?? 0; + } + + #retrieve(cacheKey: CacheKey,) + + async get( + key: zarr.AbsolutePath, + options?: RequestInit + ): Promise { + const cacheKey = asCacheKey(key); + return this.#retrieve(cacheKey, key); + } + + async getRange( + key: zarr.AbsolutePath, + range: zarr.RangeQuery, + options?: RequestInit + ): Promise { + const cacheKey = asCacheKey(key, range); + return this.#retrieveRange(cacheKey, () => super.getRange(key, range, options)); + } +} + + +export class CopyableCachedFetchStore extends zarr.FetchStore { + /** + * Caches in-progress request promises so that only one request + * is ever made for the same data. + */ + #promiseCache: Map>; + + /** + * Caches the raw response results from previous requests. + */ + #dataCache: PriorityCache; + + /** + * The set of unique cache keys stored across either the promise + * cache or the data cache. + */ + #cacheKeys: Set; + + /** + * Stores the configuration options that were used to initialize + * the underlying FetchStore. + */ + #options: FetchStoreOptions | undefined; + + constructor( + url: string | URL, + maxBytes: number, + options?: CopyableCachedFetchStoreOptions | undefined + ) { + super(url, options); + const { initCache, ...remainingOptions } = options || {}; + this.#promiseCache = new Map(); + this.#dataCache = new PriorityCache(new Map(), (h: CacheKey) => this.#score(h), maxBytes); + this.#options = remainingOptions; + // this.#maxCacheBytes = maxBytes; + // this.#currCacheBytes = 0; + this.#cacheKeys = new Set(); + + if (initCache && initCache.size > 0) { + initCache.forEach((value, key) => this.#dataCache.put(key, new CacheableByteArray(value.slice()))); + } + } + + /** + * Attempts to add a new data block into the data cache, mapped to a unique cache key. + * If the data is undefined or there isn't enough space in the cache, nothing happens. + * + * @param cacheKey The unique key associated with the data block to cache + * @param data The data block to cache + */ + #updateDataCache(cacheKey: string, data: Uint8Array | undefined) { + if (data === undefined) { + this.#cacheKeys.delete(cacheKey); + return; // Zarrita's FetchStore returns an undefined when a 404 is encountered; do nothing + } + this.#dataCache.put(cacheKey, new CacheableByteArray(data.slice())); + } + + #uncachePromise(cacheKey: string): boolean { + return this.#promiseCache.delete(cacheKey); + } + + /** + * Wraps a data request promise within another promise that ensures that the results of + * the data request are correctly managed within the cache before being returned to the + * original caller. + * + * @param cacheKey The key representing the signature of a specific data request + * @param promise The data request represented as a Promise + * @returns the wrapping Promise + */ + #wrapPromise( + cacheKey: string, + promise: Promise + ): Promise { + return new Promise((resolve, reject) => { + promise + .then((data) => { + this.#updateDataCache(cacheKey, data); + resolve(data); + }) + .catch((e) => { + logger.error(`request for ${cacheKey}] failed to fetch; error:`, e); + reject(e); + }) + .finally(() => { + this.#uncachePromise(cacheKey); + }); + }); + } + + /** + * Performs a cache lookup of the specified cache key to see if there is either + * a) already-loaded data for that cache key, or + * b) a data-request promise underway for that cache key. + * + * The cache key is intended to represent the distinctive signature of a request for + * a particular set of data. Typically, this will be either a URL, or a URL and a + * byte range within the resource indicated by the URL. + * + * If neither data nor promise is found, a new request is sent, using either a direct + * URL request (using `FetchStore.get`) or a range request (using `FetchStore.getRange`). + * @param cacheKey The key representing the signature of a specific data request + * @param fetchFn Either FetchStore.get or FetchStore.getRange, wrapped in a closure + * @returns A cache-managed promise of the results from the data request + */ + #retrieve(cacheKey: string, fetchFn: () => Promise) { + const cachedData = this.#dataCache.get(cacheKey); + + if (cachedData === undefined) { + logger.debug( + `request for ${cacheKey}] not found in data cache, checking promise cache` + ); + const cachedPromise = this.#promiseCache.get(cacheKey); + if (cachedPromise === undefined) { + logger.debug( + `request for ${cacheKey}] not found in promise cache; fetching...` + ); + const promise = this.#wrapPromise(cacheKey, fetchFn()); + this.#promiseCache.set(cacheKey, promise); + this.#cacheKeys.add(cacheKey); + return promise; + } + } + return new Promise((resolve) => { + resolve(cachedData); + }); + } + + get( + key: zarr.AbsolutePath, + options?: RequestInit + ): Promise { + const cacheKey = asCacheKey(key); + return this.#retrieve(cacheKey, () => super.get(key, options)); + } + + getRange( + key: zarr.AbsolutePath, + range: zarr.RangeQuery, + options?: RequestInit + ): Promise { + const cacheKey = asCacheKey(key, range); + return this.#retrieve(cacheKey, () => super.getRange(key, range, options)); + } + + /** + * Copies this store into a new instance, with the same URL/location, configuration, + * and current cached data. + * + * NOTE: This function copies only the current cache data and does not copy over any + * active/unfulfilled requests for additional data. If needed, use `await .pending()` + * to wait for all active promises to be fulfilled. + * @returns A new CopyableCachedFetchStore copied from this one + */ + copy(): CopyableCachedFetchStore { + return new CopyableCachedFetchStore(this.url, this.#maxCacheBytes, { + ...this.#options, + initCache: this.#dataCache, + }); + } + + get cacheKeys(): SetIterator { + return this.#cacheKeys.values(); + } + + /** + * Synchronously delete all cached data from this store. + */ + clearData() { + this.#dataCache.clear(); + this.#currCacheBytes = 0; + } + + /** + * Waits until all active requests for data are completed. + * + * NOTE: This does not await any promises that are added AFTER this + * function is called. + */ + async pending() { + await Promise.allSettled(this.#promiseCache.values()); + } + + /** + * Waits until all active requests for data are completed, + * then delets all cached data from this store. + */ + async clear() { + await this.pending(); + this.clearData(); + } +} + + +export class CachedOmeZarrLoader { + #groupAttrsCache: Map; + #arrayAttrsCache: Map; + #arrayDataCache: PriorityCache; + + #maxDataBytes: number; + + #decoder: Decoder; + + constructor(maxDataBytes: number, maxWorkers: number) { + this.#maxDataBytes = Math.max(0, Math.min(maxDataBytes, getDataCacheSizeLimit())); + this.#groupAttrsCache = new Map(); + this.#arrayAttrsCache = new Map(); + this.#arrayDataCache = new PriorityCache(new Map(), (h: CacheKey) => this.#score(h), this.#maxDataBytes); l + this.#decoder = ( + dataset: OmeZarrMetadata, + req: ZarrRequest, + level: OmeZarrShapedDataset, + signal?: AbortSignal + ): Promise => this.#decode(dataset, req, level, signal); + } + + + + + #getStore(url: string): CachedFetchStore { + const store = this.#stores.get(url); // @TODO may not be a safe assumption that this is always the root URL; may not be an issue, though? + if (store === undefined) { + const newStore = new CachedFetchStore(url, getCacheSizeLimit()); + this.#stores.set(url, newStore); + return newStore; + } + return store; + } + + #decode( + dataset: OmeZarrMetadata, + req: ZarrRequest, + level: OmeZarrShapedDataset, + signal?: AbortSignal + ): Promise { + // check if `dataset` is in cache, if not, load it + // load group attrs + // load array attrs + // check if level matches one in the loaded version of `dataset`, if not, error + // retrieve `shape` and `path` from `level` + // retrieve `zarrVersion` from `dataset` + // retrieve `axes` from multiscale via `level`, `dataset` + this.#loadSlice(path, shape, axes, zarrVersion); + } + + get decoder(): Decoder { + return this.#decoder; + } + + #loadSlice(path: string, query: zarr.Slice) { + // check cache + // cache hit: return slice data + // cache miss: prepare worker and launch + } + + loadAttrs(res: WebResource): Promise { + const cached = this.#attrsCache.get(res.url); + if (cached !== undefined) { + return cached; + } + + const store = this.#getStore(res.url); + + const promise = new Promise(async (resolve, reject) => { + try { + const group = await zarr.open(store, { kind: "group" }); + const attrs = OmeZarrAttrsSchema.parse(group.attrs); + resolve(attrs); + } catch (e) { + if (e instanceof ZodError) { + logger.error("could not load Zarr file: parsing failed"); + } else { + logger.error("could not load Zarr file: ", e); + } + reject(e); + } + }); + + this.#attrsCache.set(res.url, promise); + return promise; + } + + async loadArrayMetadata() { } + + async loadMetadata(res: WebResource): Promise { + const cached = this.#metadataCache.get(res.url); + if (cached !== undefined) { + return cached; + } + + const store = this.#getStore(res.url); + + const promise = new Promise(async (resolve, reject) => { + const attrs: OmeZarrAttrs = await loadZarrAttrsFileFromStore(store); + const version = attrs.zarrVersion; + const arrays = await Promise.all( + attrs.multiscales + .map((multiscale) => { + return ( + multiscale.datasets?.map(async (dataset) => { + return ( + await loadZarrArrayFileFromStore( + store, + dataset.path, + version, + loadV2ArrayAttrs + ) + ).metadata; + }) ?? [] + ); + }) + .reduce((prev, curr) => prev.concat(curr)) + .filter((v) => v !== undefined) + ); + resolve(new OmeZarrMetadata(res.url, attrs, arrays, version)); + }); + this.#metadataCache.set(res.url, promise); + return promise; + } +} + +class CachedFetchStore extends zarr.FetchStore { + #cache: Map>; + #maxBytes: number; + #currBytes: number; + + constructor( + url: string | URL, + maxBytes: number, + options?: { + overrides?: RequestInit; + useSuffixRequest?: boolean; + } + ) { + super(url, options); + this.#cache = new Map(); + } + + get( + key: zarr.AbsolutePath, + options?: RequestInit + ): Promise { + const cacheKey = asCacheKey(key); + if (this.#cache.has(cacheKey)) { + logger.debug(`get: ${cacheKey} [CACHED]`); + } else { + logger.debug(`get: ${cacheKey}`); + } + const cached = this.#cache.get(cacheKey); + if (cached === undefined) { + const promise = super.get(key, options); + this.#cache.set(cacheKey, promise); + return promise; + } + return cached; + } + + getRange( + key: zarr.AbsolutePath, + range: zarr.RangeQuery, + options?: RequestInit + ): Promise { + const cacheKey = asCacheKey(key, range); + if (this.#cache.has(cacheKey)) { + console.log(`getRange: ${cacheKey} [CACHED]`); + } else { + console.log(`getRange: ${cacheKey}`); + } + const cached = this.#cache.get(cacheKey); + if (cached === undefined) { + const promise = super.getRange(key, range, options); + this.#cache.set(cacheKey, promise); + return promise; + } + return cached; + } +} + + +export async function loadSlice2( + url: string, + axes: OmeZarrAxis[], + r: ZarrRequest, + path: string, + zarrVersion: number, + shape: ReadonlyArray, + signal?: AbortSignal +) { + // put the request in native order + const store = new CachedFetchStore(url, 8 * 2 ** 10); + // if (!level) { + // const message = 'invalid Zarr data: no datasets found'; + // logger.error(message); + // throw new VisZarrDataError(message); + // } + // const arr = metadata.arrays.find((a) => a.path === level.path); + // if (!arr) { + // const message = `cannot load slice: no array found for path [${level.path}]`; + // logger.error(message); + // throw new VisZarrDataError(message); + // } + const { raw } = await loadZarrArrayFileFromStore( + store, + path, + zarrVersion, + false + ); + const result = await zarr.get(raw, buildQuery(r, axes, shape), { + opts: { signal: signal ?? null }, + }); + if (typeof result === "number") { + throw new Error("oh noes, slice came back all weird"); + } + return { + shape: result.shape, + buffer: result, + }; +} diff --git a/packages/omezarr/src/zarr/cached-store/fetch-slice.interface.ts b/packages/omezarr/src/zarr/cached-store/fetch-slice.interface.ts new file mode 100644 index 00000000..203d4b2f --- /dev/null +++ b/packages/omezarr/src/zarr/cached-store/fetch-slice.interface.ts @@ -0,0 +1,88 @@ +import type { AbsolutePath, RangeQuery } from 'zarrita'; +import z from 'zod'; + +export type TransferrableRequestInit = Omit & { + body?: string; + headers?: [string, string][] | Record; +}; + +export type FetchSliceMessagePayload = { + rootUrl: string; + path: AbsolutePath; + range: RangeQuery; + options?: TransferrableRequestInit | undefined; +}; + +export const FETCH_SLICE_MESSAGE_TYPE = 'fetch-slice' as const; +export const FETCH_SLICE_RESPOSNE_MESSAGE_TYPE = 'fetch-slice-response' as const; +export const CANCEL_MESSAGE_TYPE = 'cancel' as const; + +export type FetchSliceMessage = { + type: typeof FETCH_SLICE_MESSAGE_TYPE; + id: string; + payload: FetchSliceMessagePayload; +}; + +export type FetchSliceResponseMessage = { + type: typeof FETCH_SLICE_RESPOSNE_MESSAGE_TYPE; + id: string; + payload: ArrayBufferLike | undefined; +}; + +export type CancelMessage = { + type: typeof CANCEL_MESSAGE_TYPE; + id: string; +}; + +const FetchSliceMessagePayloadSchema = z.object({ + rootUrl: z.string().nonempty(), + path: z.string().nonempty().startsWith('/'), + range: z.union([ + z.object({ + offset: z.number(), + length: z.number(), + }), + z.object({ + suffixLength: z.number(), + }), + ]), + options: z.unknown().optional(), // being "lazy" for now; doing a full schema for this could be complex and fragile +}); + +const FetchSliceMessageSchema = z.object({ + type: z.literal(FETCH_SLICE_MESSAGE_TYPE), + id: z.string().nonempty(), + payload: FetchSliceMessagePayloadSchema, +}); + +const FetchSliceResponseMessageSchema = z.object({ + type: z.literal(FETCH_SLICE_RESPOSNE_MESSAGE_TYPE), + id: z.string().nonempty(), + payload: z.unknown().optional(), // unclear if it's feasible/wise to define a schema for this one +}); + +const CancelMessageSchema = z.object({ + type: z.literal(CANCEL_MESSAGE_TYPE), + id: z.string().nonempty(), +}); + +export function isFetchSliceMessage(val: unknown): val is FetchSliceMessage { + return FetchSliceMessageSchema.safeParse(val).success; +} + +export function isFetchSliceResponseMessage(val: unknown): val is FetchSliceResponseMessage { + return FetchSliceResponseMessageSchema.safeParse(val).success; +} + +export function isCancelMessage(val: unknown): val is CancelMessage { + return CancelMessageSchema.safeParse(val).success; +} + +export function isCancellationError(err: unknown): boolean { + return ( + err === 'cancelled' || + (typeof err === 'object' && + err !== null && + (('name' in err && err.name === 'AbortError') || ('code' in err && err.code === 20))) + ); +} diff --git a/packages/omezarr/src/zarr/cached-store/fetch-slice.worker.ts b/packages/omezarr/src/zarr/cached-store/fetch-slice.worker.ts new file mode 100644 index 00000000..42b8f443 --- /dev/null +++ b/packages/omezarr/src/zarr/cached-store/fetch-slice.worker.ts @@ -0,0 +1,72 @@ +// a web-worker which fetches slices of data, decodes them, and returns the result as a flat float32 array, using transferables + +import { type AbsolutePath, type RangeQuery, FetchStore } from 'zarrita'; +import { logger } from '@alleninstitute/vis-core'; +import type { CancelMessage, FetchSliceMessage, TransferrableRequestInit } from './fetch-slice.interface'; +import { isCancellationError, isCancelMessage, isFetchSliceMessage } from './fetch-slice.interface'; + +async function fetchSlice( + rootUrl: string, + path: AbsolutePath, + range: RangeQuery, + options?: TransferrableRequestInit | undefined, + abortController?: AbortController | undefined, +): Promise { + const store = new FetchStore(rootUrl); + return store.getRange(path, range, { ...(options || {}), signal: abortController?.signal }); +} + +const abortControllers: Record = {}; + +const handleFetchSlice = (message: FetchSliceMessage) => { + const { id, payload } = message; + const { rootUrl, path, range, options } = payload; + + if (id in abortControllers) { + logger.error('cannot send message: request ID already in use'); + return; + } + + const abort = new AbortController(); + abortControllers[id] = abort; + + fetchSlice(rootUrl, path, range, options, abort) + .then((result: Uint8Array | undefined) => { + const buffer = result?.buffer; + const options = buffer !== undefined ? { transfer: [buffer] } : {}; + self.postMessage( + { + type: 'fetch-slice-response', + id, + payload: result?.buffer, + }, + { ...options }, + ); + }) + .catch((e) => { + if (!isCancellationError(e)) { + logger.error('error in slice fetch worker: ', e); + } + // can ignore if it is a cancellation error + }); +}; + +const handleCancel = (message: CancelMessage) => { + const { id } = message; + const abortController = abortControllers[id]; + if (!abortController) { + logger.warn('attempted to cancel a non-existent request'); + } else { + abortController.abort('cancelled'); + } +}; + +self.onmessage = async (e: MessageEvent) => { + const { data: message } = e; + + if (isFetchSliceMessage(message)) { + handleFetchSlice(message); + } else if (isCancelMessage(message)) { + handleCancel(message); + } +}; diff --git a/packages/omezarr/src/zarr/cached-store/worker-pool.ts b/packages/omezarr/src/zarr/cached-store/worker-pool.ts new file mode 100644 index 00000000..d68b42ad --- /dev/null +++ b/packages/omezarr/src/zarr/cached-store/worker-pool.ts @@ -0,0 +1,128 @@ +import type { Decoder, OmeZarrMetadata, OmeZarrShapedDataset, ZarrRequest } from '@alleninstitute/vis-omezarr'; +import type { ZarrSliceRequest } from '../types'; +import { v4 as uuidv4 } from 'uuid'; + +type PromiseResolve = (t: T) => void; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type PromiseReject = (reason: any) => void; + + +type MessagePromise = { + requestCacheKey: string; + resolve: PromiseResolve; + reject: PromiseReject; + promise: Promise; +}; + +type ExpectedResultSlice = { + type: 'slice'; + id: string; +} & Slice; +type Slice = { + data: Float32Array; + shape: number[]; +}; + +function isExpectedResult(obj: unknown): obj is ExpectedResultSlice { + return typeof obj === 'object' && obj !== null && 'type' in obj && obj.type === 'slice'; +} + +export class WorkerPool { + #workers: Worker[]; + #promises: Record; + #which: number; + + constructor(size: number, workerModule: URL) { + this.#workers = new Array(size); + for (let i = 0; i < size; i++) { + this.#workers[i] = new Worker(workerModule, { type: 'module' }); + this.#workers[i].onmessage = (msg) => this.#handleResponse(msg); + } + this.#promises = {}; + this.#which = 0; + } + + #handleResponse(msg: MessageEvent) { + const { data: payload } = msg; + if (isExpectedResult(payload)) { + const prom = this.#promises[payload.id]; + if (prom) { + const { data, shape } = payload; + prom.resolve({ data, shape }); + delete this.#promises[payload.id]; + } + } + } + + #roundRobin() { + this.#which = (this.#which + 1) % this.#workers.length; + } + + submitRequest() { + + + this.#roundRobin(); + } + + #sendMessageToWorker(workerIndex: number, message: T) { + const worker = this.#workers[workerIndex]; + if (worker === undefined) { + throw new Error('cannot send message to worker: index invalid'); + } + worker.postMessage(message); + } + + #createMessagePromise(cacheKey: string): MessagePromise { + const { promise, resolve, reject } = Promise.withResolvers(); + + return { + requestCacheKey: cacheKey, + resolve, + reject, + promise, + }; + } + + requestSlice(metadata: OmeZarrMetadata, req: ZarrRequest, level: OmeZarrShapedDataset, signal?: AbortSignal) { + const reqId = `rq${uuidv4()}`; + const cacheKey = JSON.stringify({ url: metadata.url, req, level }); + const selectedWorker = this.#which; + + const message: ZarrSliceRequest = { + id: reqId, + type: 'ZarrSliceRequest', + metadata: metadata.dehydrate(), + req, + level, + }; + + const messagePromise = this.#createMessagePromise(cacheKey); + this.#promises[reqId] = messagePromise; + + if (signal) { + signal.onabort = () => { + this.#sendMessageToWorker(selectedWorker, { type: 'cancel', id: reqId }); + messagePromise.reject('cancelled'); + }; + } + + this.#sendMessageToWorker(selectedWorker, message); + this.#roundRobin(); + + return messagePromise.promise; + } +} + +// a singleton... +let slicePool: SliceWorkerPool; +export function getSlicePool() { + if (!slicePool) { + slicePool = new SliceWorkerPool(6); + } + return slicePool; +} + +export const multithreadedDecoder: Decoder = (metadata, req, level: OmeZarrShapedDataset, signal?: AbortSignal) => { + return getSlicePool().requestSlice(metadata, req, level, signal); +}; diff --git a/site/src/examples/omezarr/omezarr-via-priority-cache.tsx b/site/src/examples/omezarr/omezarr-via-priority-cache.tsx index 7649b965..c99f413c 100644 --- a/site/src/examples/omezarr/omezarr-via-priority-cache.tsx +++ b/site/src/examples/omezarr/omezarr-via-priority-cache.tsx @@ -41,12 +41,22 @@ const demoOptions: DemoOption[] = [ }, ]; +const overrideOption: DemoOption = { + value: 'opt5', + label: 'Example V3 OME-Zarr', + res: { + type: 's3', + region: 'us-west-2', + url: 's3://public-development-802451596237-us-west-2/tissuecyte/478097069/ome_zarr_conversion/478097069.zarr' + } +} + const screenSize: vec2 = [800, 800]; export function OmezarrDemo() { return ( - + ); } From 1ec2ad083ca67e38256c177c691d7b9d60f056c2 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 2 Oct 2025 12:03:53 -0700 Subject: [PATCH 03/91] Fixed some linting issues --- .../core/src/shared-priority-cache/priority-cache.test.ts | 4 ++-- packages/core/src/shared-priority-cache/shared-cache.ts | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/core/src/shared-priority-cache/priority-cache.test.ts b/packages/core/src/shared-priority-cache/priority-cache.test.ts index 40c54201..91c90ea2 100644 --- a/packages/core/src/shared-priority-cache/priority-cache.test.ts +++ b/packages/core/src/shared-priority-cache/priority-cache.test.ts @@ -1,7 +1,7 @@ /** biome-ignore-all lint/suspicious/noConsole: */ import { beforeEach, describe, expect, test } from 'vitest'; -import { AsyncPriorityCache, Cacheable } from './priority-cache'; -import { FakeStore, Payload, PayloadFactory, PromiseFarm } from './test-utils'; +import { AsyncPriorityCache, type Cacheable } from './priority-cache'; +import { FakeStore, type Payload, PayloadFactory, PromiseFarm } from './test-utils'; let factory = new PayloadFactory(); diff --git a/packages/core/src/shared-priority-cache/shared-cache.ts b/packages/core/src/shared-priority-cache/shared-cache.ts index 1b431f3b..0df0f32f 100644 --- a/packages/core/src/shared-priority-cache/shared-cache.ts +++ b/packages/core/src/shared-priority-cache/shared-cache.ts @@ -34,7 +34,13 @@ function mapFields, Result>( r: R, fn: (v: R[keyof R]) => Result, ): { [k in keyof R]: Result } { - return entries(r).reduce((acc, [k, v]) => ({ ...acc, [k]: fn(v) }), {} as { [k in keyof R]: Result }); + return entries(r).reduce( + (acc, [k, v]) => { + acc[k] = fn(v); + return acc; + }, + {} as { [k in keyof R]: Result }, + ); } type Client = { From 118d46df2e5d0f6c8dbe2a45bcbad481b9ced1d6 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 2 Oct 2025 12:19:57 -0700 Subject: [PATCH 04/91] Fixed more linting warnings --- site/src/examples/omezarr/render-utils.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/site/src/examples/omezarr/render-utils.ts b/site/src/examples/omezarr/render-utils.ts index ec87b719..d4a61ac3 100644 --- a/site/src/examples/omezarr/render-utils.ts +++ b/site/src/examples/omezarr/render-utils.ts @@ -28,9 +28,10 @@ type Thing = { settings: RenderSettings; }; function mapValues, V, R>(obj: T, fn: (v: V) => R): { [k in keyof T]: R } { - return Object.keys(obj).reduce( + return (Object.keys(obj) as (keyof T)[]).reduce( // typecast is annoyingly necessary in this case to avoid a linting warning (acc, k) => { - return { ...acc, [k]: fn(obj[k]) }; + acc[k] = fn(obj[k]); + return acc; }, {} as { [k in keyof T]: R }, ); @@ -48,8 +49,9 @@ export function buildConnectedRenderer( const client = cache.registerClient>({ cacheKeys: (item) => { const channelKeys = Object.keys(item.settings.channels); - return channelKeys.reduce((chans, key) => { - return { ...chans, [key]: renderer.cacheKey(item.tile, key, item.dataset, item.settings) }; + return channelKeys.reduce>((chans, key) => { + chans[key] = renderer.cacheKey(item.tile, key, item.dataset, item.settings); + return chans; }, {}); }, fetch: (item) => { From 4cdf283ea7935d3923efc4c447778ef36f8a3644 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 3 Oct 2025 08:41:50 -0700 Subject: [PATCH 05/91] WIP 2 --- .../fetch-slice.interface.ts | 24 ++++ .../fetch-slice.worker.ts | 0 .../store.ts} | 90 ++++++++++-- .../src/zarr/cached-loading/worker-pool.ts | 107 +++++++++++++++ .../src/zarr/cached-store/worker-pool.ts | 128 ------------------ 5 files changed, 207 insertions(+), 142 deletions(-) rename packages/omezarr/src/zarr/{cached-store => cached-loading}/fetch-slice.interface.ts (82%) rename packages/omezarr/src/zarr/{cached-store => cached-loading}/fetch-slice.worker.ts (100%) rename packages/omezarr/src/zarr/{cached-store/cached-store.ts => cached-loading/store.ts} (85%) create mode 100644 packages/omezarr/src/zarr/cached-loading/worker-pool.ts delete mode 100644 packages/omezarr/src/zarr/cached-store/worker-pool.ts diff --git a/packages/omezarr/src/zarr/cached-store/fetch-slice.interface.ts b/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts similarity index 82% rename from packages/omezarr/src/zarr/cached-store/fetch-slice.interface.ts rename to packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts index 203d4b2f..c5eb5cde 100644 --- a/packages/omezarr/src/zarr/cached-store/fetch-slice.interface.ts +++ b/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts @@ -13,6 +13,30 @@ export type FetchSliceMessagePayload = { options?: TransferrableRequestInit | undefined; }; +export type WorkerMessage = { + type: string; +}; + +export type WorkerMessageWithId = WorkerMessage & { + id: string; +}; + +const WorkerMessageSchema = z.object({ + type: z.string(), +}); + +const WorkerMessageWithIdSchema = WorkerMessageSchema.extend({ + id: z.string().nonempty(), +}); + +export function isWorkerMessage(val: unknown): val is WorkerMessage { + return WorkerMessageSchema.safeParse(val).success; +} + +export function isWorkerMessageWithId(val: unknown): val is WorkerMessageWithId { + return WorkerMessageWithIdSchema.safeParse(val).success; +} + export const FETCH_SLICE_MESSAGE_TYPE = 'fetch-slice' as const; export const FETCH_SLICE_RESPOSNE_MESSAGE_TYPE = 'fetch-slice-response' as const; export const CANCEL_MESSAGE_TYPE = 'cancel' as const; diff --git a/packages/omezarr/src/zarr/cached-store/fetch-slice.worker.ts b/packages/omezarr/src/zarr/cached-loading/fetch-slice.worker.ts similarity index 100% rename from packages/omezarr/src/zarr/cached-store/fetch-slice.worker.ts rename to packages/omezarr/src/zarr/cached-loading/fetch-slice.worker.ts diff --git a/packages/omezarr/src/zarr/cached-store/cached-store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts similarity index 85% rename from packages/omezarr/src/zarr/cached-store/cached-store.ts rename to packages/omezarr/src/zarr/cached-loading/store.ts index 1b1994bf..b4b80b8e 100644 --- a/packages/omezarr/src/zarr/cached-store/cached-store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -1,4 +1,4 @@ -import { logger, type WebResource, PriorityCache, Cacheable } from '@alleninstitute/vis-core'; +import { logger, type WebResource, PriorityCache, type Cacheable } from '@alleninstitute/vis-core'; import { OmeZarrAttrsSchema, OmeZarrMetadata, @@ -9,21 +9,23 @@ import { import * as zarr from 'zarrita'; import { ZodError } from 'zod'; -import { Decoder, VoxelTileImage } from '../../sliceview/slice-renderer'; -import { ZarrRequest } from '../loading'; +import type { Decoder, VoxelTileImage } from '../../sliceview/slice-renderer'; +import type { ZarrRequest } from '../loading'; import { WorkerPool } from './worker-pool'; +import { FETCH_SLICE_MESSAGE_TYPE, isFetchSliceResponseMessage } from './fetch-slice.interface'; -const MAX_ATTRS_CACHE_BYTES = 16 * 2 ** 10; // 16 MB -- aribtrarily chosen at this point -const MAX_DATA_CACHE_BYTES = 256 * 2 ** 10; // 256 MB -- aribtrarily chosen at this point +const DEFAULT_MAX_ATTRS_CACHE_BYTES = 16 * 2 ** 10; // 16 MB -- aribtrarily chosen at this point +const DEFAULT_MAX_DATA_CACHE_BYTES = 256 * 2 ** 10; // 256 MB -- aribtrarily chosen at this point +const DEFAULT_NUM_WORKERS = 6; // @TODO implement a much more context-aware cache size limiting mechanism const getAttrsCacheSizeLimit = () => { - return MAX_ATTRS_CACHE_BYTES; + return DEFAULT_MAX_ATTRS_CACHE_BYTES; }; // @TODO implement a much more context-aware cache size limiting mechanism const getDataCacheSizeLimit = () => { - return MAX_ATTRS_CACHE_BYTES; + return DEFAULT_MAX_ATTRS_CACHE_BYTES; }; const asCacheKey = ( @@ -83,8 +85,23 @@ export type CachedFetchStoreInit = { } }; -export class CachedMultithreadedFetchStore extends zarr.FetchStore { +export type CachingMultithreadedFetchStoreOptions = { + maxBytes?: number | undefined; + numWorkers?: number | undefined; + fetchStoreOptions?: FetchStoreOptions | undefined; +} + +export class CachingMultithreadedFetchStore extends zarr.FetchStore { + /** + * Maintains a pool of available worker threads. + */ #workerPool: WorkerPool; + + /** + * Stores the current set of cached data that has been successfully + * fetched. This data is stored in raw byte array form so that it + * integrates properly with the Zarrita framework. + */ #dataCache: PriorityCache; /** @@ -94,28 +111,69 @@ export class CachedMultithreadedFetchStore extends zarr.FetchStore { */ #priorityMap: Map; + /** + * A callback form of the `score` function. + */ + #scoreFn: (h: CacheKey) => number; + constructor( url: string | URL, maxBytes: number, - options?: FetchStoreOptions + options?: CachingMultithreadedFetchStoreOptions ) { - super(url, options); - this.#dataCache = new PriorityCache(new Map(), (h: CacheKey) => this.score(h), maxBytes); + super(url, options?.fetchStoreOptions); + this.#scoreFn = (h: CacheKey) => this.score(h); + this.#dataCache = new PriorityCache(new Map(), this.#scoreFn, maxBytes); this.#priorityMap = new Map; + this.#workerPool = new WorkerPool(options?.numWorkers ?? DEFAULT_NUM_WORKERS, new URL('./fetch-slice.worker.ts', import.meta.url)); } protected score(key: CacheKey): number { return this.#priorityMap.get(key) ?? 0; } - #retrieve(cacheKey: CacheKey,) + #fromCache(cacheKey: CacheKey): Uint8Array | undefined { + const cached = this.#dataCache.get(cacheKey); + if (cached === undefined) { + return undefined; + } + this.#priorityMap.set(cacheKey, Date.now()); + return new Uint8Array(cached.buffer()); + } + + async #doFetch( + key: zarr.AbsolutePath, + range?: zarr.RangeQuery | undefined, + options?: RequestInit + ): Promise { + const response = await this.#workerPool.submitRequest({ + type: FETCH_SLICE_MESSAGE_TYPE, + rootUrl: this.url, + path: key, + range, + options + }, isFetchSliceResponseMessage, []); + if (response.payload === undefined) { + return undefined; + } + const arr = new Uint8Array(response.payload); + const cacheKey = asCacheKey(key, range); + + this.#priorityMap.set(cacheKey, Date.now()); + this.#dataCache.put(cacheKey, new CacheableByteArray(arr)); + return arr; + } async get( key: zarr.AbsolutePath, options?: RequestInit ): Promise { const cacheKey = asCacheKey(key); - return this.#retrieve(cacheKey, key); + const cached = this.#fromCache(cacheKey); + if (cached !== undefined) { + return cached; + } + return this.#doFetch(key, undefined, options); } async getRange( @@ -124,7 +182,11 @@ export class CachedMultithreadedFetchStore extends zarr.FetchStore { options?: RequestInit ): Promise { const cacheKey = asCacheKey(key, range); - return this.#retrieveRange(cacheKey, () => super.getRange(key, range, options)); + const cached = this.#fromCache(cacheKey); + if (cached !== undefined) { + return cached; + } + return this.#doFetch(key, range, options); } } diff --git a/packages/omezarr/src/zarr/cached-loading/worker-pool.ts b/packages/omezarr/src/zarr/cached-loading/worker-pool.ts new file mode 100644 index 00000000..cf5ba36c --- /dev/null +++ b/packages/omezarr/src/zarr/cached-loading/worker-pool.ts @@ -0,0 +1,107 @@ +import { + isWorkerMessageWithId, + type WorkerMessage, + type WorkerMessageWithId, +} from './fetch-slice.interface'; +import { v4 as uuidv4 } from 'uuid'; +import { logger } from '@alleninstitute/vis-core'; + +type PromiseResolve = (t: T) => void; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +// biome-ignore lint/suspicious/noExplicitAny: This is aligned with the standard Promise API +type PromiseReject = (reason: any) => void; + +type MessageValidator = TypeGuardFunction; + +type TypeGuardFunction = (value: T) => value is S; + +type MessagePromise = { + validator: MessageValidator; + resolve: PromiseResolve; + reject: PromiseReject; + promise: Promise; +}; + +export class WorkerPool { + #workers: Worker[]; + #promises: Map>; + #which: number; + + constructor(size: number, workerModule: URL) { + this.#workers = new Array(size); + for (let i = 0; i < size; i++) { + this.#workers[i] = new Worker(workerModule, { type: 'module' }); + this.#workers[i].onmessage = (msg) => this.#handleResponse(msg); + } + this.#promises = new Map(); + this.#which = 0; + } + + #handleResponse(msg: MessageEvent) { + const { data } = msg; + if (isWorkerMessageWithId(data)) { + const { id } = data; + const messagePromise = this.#promises.get(id); + this.#promises.delete(id); + if (messagePromise === undefined) { + logger.warn('unexpected message from worker'); + return; + } + if (!messagePromise.validator(data)) { + logger.error('invalid response from worker: message type did not match expected type'); + return; + } + messagePromise.resolve(data); + } + } + + #roundRobin() { + this.#which = (this.#which + 1) % this.#workers.length; + } + + submitRequest( + message: RequestType, + responseValidator: MessageValidator, + transfers: Transferable[], + signal?: AbortSignal | undefined, + ): Promise { + const reqId = `rq${uuidv4()}`; + const workerIndex = this.#which; + const messageWithId = { ...message, id: reqId }; + const messagePromise = this.#createMessagePromise(responseValidator); + + // TODO this cast is very annoying; would be nice to remove it + this.#promises.set(reqId, messagePromise as unknown as MessagePromise); + + if (signal) { + signal.onabort = () => { + this.#sendMessageToWorker(workerIndex, { type: 'cancel', id: reqId }, []); + messagePromise.reject('cancelled'); + }; + } + + this.#sendMessageToWorker(workerIndex, messageWithId, transfers); + this.#roundRobin(); + return messagePromise.promise; + } + + #sendMessageToWorker(workerIndex: number, message: WorkerMessageWithId, transfers: Transferable[]) { + const worker = this.#workers[workerIndex]; + if (worker === undefined) { + throw new Error('cannot send message to worker: index invalid'); + } + worker.postMessage(message, transfers); + } + + #createMessagePromise(responseValidator: MessageValidator): MessagePromise { + const { promise, resolve, reject } = Promise.withResolvers(); + + return { + validator: responseValidator, + resolve, + reject, + promise, + }; + } +} diff --git a/packages/omezarr/src/zarr/cached-store/worker-pool.ts b/packages/omezarr/src/zarr/cached-store/worker-pool.ts deleted file mode 100644 index d68b42ad..00000000 --- a/packages/omezarr/src/zarr/cached-store/worker-pool.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { Decoder, OmeZarrMetadata, OmeZarrShapedDataset, ZarrRequest } from '@alleninstitute/vis-omezarr'; -import type { ZarrSliceRequest } from '../types'; -import { v4 as uuidv4 } from 'uuid'; - -type PromiseResolve = (t: T) => void; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type PromiseReject = (reason: any) => void; - - -type MessagePromise = { - requestCacheKey: string; - resolve: PromiseResolve; - reject: PromiseReject; - promise: Promise; -}; - -type ExpectedResultSlice = { - type: 'slice'; - id: string; -} & Slice; -type Slice = { - data: Float32Array; - shape: number[]; -}; - -function isExpectedResult(obj: unknown): obj is ExpectedResultSlice { - return typeof obj === 'object' && obj !== null && 'type' in obj && obj.type === 'slice'; -} - -export class WorkerPool { - #workers: Worker[]; - #promises: Record; - #which: number; - - constructor(size: number, workerModule: URL) { - this.#workers = new Array(size); - for (let i = 0; i < size; i++) { - this.#workers[i] = new Worker(workerModule, { type: 'module' }); - this.#workers[i].onmessage = (msg) => this.#handleResponse(msg); - } - this.#promises = {}; - this.#which = 0; - } - - #handleResponse(msg: MessageEvent) { - const { data: payload } = msg; - if (isExpectedResult(payload)) { - const prom = this.#promises[payload.id]; - if (prom) { - const { data, shape } = payload; - prom.resolve({ data, shape }); - delete this.#promises[payload.id]; - } - } - } - - #roundRobin() { - this.#which = (this.#which + 1) % this.#workers.length; - } - - submitRequest() { - - - this.#roundRobin(); - } - - #sendMessageToWorker(workerIndex: number, message: T) { - const worker = this.#workers[workerIndex]; - if (worker === undefined) { - throw new Error('cannot send message to worker: index invalid'); - } - worker.postMessage(message); - } - - #createMessagePromise(cacheKey: string): MessagePromise { - const { promise, resolve, reject } = Promise.withResolvers(); - - return { - requestCacheKey: cacheKey, - resolve, - reject, - promise, - }; - } - - requestSlice(metadata: OmeZarrMetadata, req: ZarrRequest, level: OmeZarrShapedDataset, signal?: AbortSignal) { - const reqId = `rq${uuidv4()}`; - const cacheKey = JSON.stringify({ url: metadata.url, req, level }); - const selectedWorker = this.#which; - - const message: ZarrSliceRequest = { - id: reqId, - type: 'ZarrSliceRequest', - metadata: metadata.dehydrate(), - req, - level, - }; - - const messagePromise = this.#createMessagePromise(cacheKey); - this.#promises[reqId] = messagePromise; - - if (signal) { - signal.onabort = () => { - this.#sendMessageToWorker(selectedWorker, { type: 'cancel', id: reqId }); - messagePromise.reject('cancelled'); - }; - } - - this.#sendMessageToWorker(selectedWorker, message); - this.#roundRobin(); - - return messagePromise.promise; - } -} - -// a singleton... -let slicePool: SliceWorkerPool; -export function getSlicePool() { - if (!slicePool) { - slicePool = new SliceWorkerPool(6); - } - return slicePool; -} - -export const multithreadedDecoder: Decoder = (metadata, req, level: OmeZarrShapedDataset, signal?: AbortSignal) => { - return getSlicePool().requestSlice(metadata, req, level, signal); -}; From 9d8e7a0491c0b3cfdc6afbf764bf712716c28970 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 3 Oct 2025 09:13:51 -0700 Subject: [PATCH 06/91] Fmt updates --- .../omezarr/src/zarr/cached-loading/store.ts | 558 ++---------------- .../src/zarr/cached-loading/worker-pool.ts | 10 +- .../src/zarr/cached-loading/zarr-loader.ts | 436 ++++++++++++++ site/src/examples/omezarr/render-utils.ts | 3 +- 4 files changed, 505 insertions(+), 502 deletions(-) create mode 100644 packages/omezarr/src/zarr/cached-loading/zarr-loader.ts diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index b4b80b8e..be6255b6 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -1,39 +1,13 @@ -import { logger, type WebResource, PriorityCache, type Cacheable } from '@alleninstitute/vis-core'; -import { - OmeZarrAttrsSchema, - OmeZarrMetadata, - type OmeZarrAttrs, - type OmeZarrAxis, - type OmeZarrShapedDataset, -} from '../types'; +import { PriorityCache, type Cacheable } from '@alleninstitute/vis-core'; import * as zarr from 'zarrita'; -import { ZodError } from 'zod'; - -import type { Decoder, VoxelTileImage } from '../../sliceview/slice-renderer'; -import type { ZarrRequest } from '../loading'; import { WorkerPool } from './worker-pool'; import { FETCH_SLICE_MESSAGE_TYPE, isFetchSliceResponseMessage } from './fetch-slice.interface'; -const DEFAULT_MAX_ATTRS_CACHE_BYTES = 16 * 2 ** 10; // 16 MB -- aribtrarily chosen at this point -const DEFAULT_MAX_DATA_CACHE_BYTES = 256 * 2 ** 10; // 256 MB -- aribtrarily chosen at this point const DEFAULT_NUM_WORKERS = 6; -// @TODO implement a much more context-aware cache size limiting mechanism -const getAttrsCacheSizeLimit = () => { - return DEFAULT_MAX_ATTRS_CACHE_BYTES; -}; - -// @TODO implement a much more context-aware cache size limiting mechanism -const getDataCacheSizeLimit = () => { - return DEFAULT_MAX_ATTRS_CACHE_BYTES; -}; - -const asCacheKey = ( - key: zarr.AbsolutePath, - range?: zarr.RangeQuery | undefined -): string => { +const asCacheKey = (key: zarr.AbsolutePath, range?: zarr.RangeQuery | undefined): string => { const keyStr = JSON.stringify(key); - const rangeStr = range ? JSON.stringify(range) : "no-range"; + const rangeStr = range ? JSON.stringify(range) : 'no-range'; return `${keyStr} ${rangeStr}`; }; @@ -42,10 +16,6 @@ type FetchStoreOptions = { useSuffixRequest?: boolean; }; -type CopyableCachedFetchStoreOptions = FetchStoreOptions & { - initCache?: ReadonlyMap | undefined; -}; - class CacheableByteArray implements Cacheable { #arr: Uint8Array; @@ -53,7 +23,7 @@ class CacheableByteArray implements Cacheable { this.#arr = arr; } - destroy() { } + destroy() {} sizeInBytes(): number { return this.#arr.byteLength; @@ -66,30 +36,45 @@ class CacheableByteArray implements Cacheable { type CacheKey = string; -type TransferrableRequestInit = Omit & { +type TransferableRequestInit = Omit & { body?: string; - headers?: [string, string][] | Record + headers?: Record; }; -/** - * The parameters required to create a matching CachedFetchStore within a - * - */ -export type CachedFetchStoreInit = { - url: string | URL, - maxBytes: number, - initCache: Map, - options?: { - overrides?: TransferrableRequestInit, - useSuffixRequest?: boolean +const copyHeaders = (headers: RequestInit['headers']): Record | undefined => { + if (Array.isArray(headers)) { + const result: Record = {}; + headers.forEach(([key, val]) => { + // TODO is key, val the correct order here? + result[key] = val; + }); + return result; + } + if (headers instanceof Headers) { + const result: Record = {}; + headers.forEach((val, key) => { + result[key] = val; + }); + return result; } + return headers; +}; + +const copyToTransferableRequestInit = (req: RequestInit | undefined): TransferableRequestInit => { + if (req === undefined) { + return {}; + } + const updReq = { ...req }; + delete updReq.signal; + delete updReq.window; + return { ...updReq, body: req.body?.toString(), headers: copyHeaders(req.headers) }; }; export type CachingMultithreadedFetchStoreOptions = { maxBytes?: number | undefined; numWorkers?: number | undefined; fetchStoreOptions?: FetchStoreOptions | undefined; -} +}; export class CachingMultithreadedFetchStore extends zarr.FetchStore { /** @@ -106,7 +91,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { /** * Maps cache keys to numeric times; the higher the time, the higher the priority. - * + * * This effectively means that more frequently-requested items will be kept longer. */ #priorityMap: Map; @@ -116,16 +101,19 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { */ #scoreFn: (h: CacheKey) => number; - constructor( - url: string | URL, - maxBytes: number, - options?: CachingMultithreadedFetchStoreOptions - ) { + constructor(url: string | URL, maxBytes: number, options?: CachingMultithreadedFetchStoreOptions) { super(url, options?.fetchStoreOptions); this.#scoreFn = (h: CacheKey) => this.score(h); - this.#dataCache = new PriorityCache(new Map(), this.#scoreFn, maxBytes); - this.#priorityMap = new Map; - this.#workerPool = new WorkerPool(options?.numWorkers ?? DEFAULT_NUM_WORKERS, new URL('./fetch-slice.worker.ts', import.meta.url)); + this.#dataCache = new PriorityCache( + new Map(), + this.#scoreFn, + maxBytes, + ); + this.#priorityMap = new Map(); + this.#workerPool = new WorkerPool( + options?.numWorkers ?? DEFAULT_NUM_WORKERS, + new URL('./fetch-slice.worker.ts', import.meta.url), + ); } protected score(key: CacheKey): number { @@ -143,16 +131,20 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { async #doFetch( key: zarr.AbsolutePath, - range?: zarr.RangeQuery | undefined, - options?: RequestInit + range: zarr.RangeQuery | undefined, + options: TransferableRequestInit, ): Promise { - const response = await this.#workerPool.submitRequest({ - type: FETCH_SLICE_MESSAGE_TYPE, - rootUrl: this.url, - path: key, - range, - options - }, isFetchSliceResponseMessage, []); + const response = await this.#workerPool.submitRequest( + { + type: FETCH_SLICE_MESSAGE_TYPE, + rootUrl: this.url, + path: key, + range, + options, + }, + isFetchSliceResponseMessage, + [], + ); if (response.payload === undefined) { return undefined; } @@ -164,449 +156,27 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { return arr; } - async get( - key: zarr.AbsolutePath, - options?: RequestInit - ): Promise { + async get(key: zarr.AbsolutePath, options?: RequestInit): Promise { const cacheKey = asCacheKey(key); const cached = this.#fromCache(cacheKey); if (cached !== undefined) { return cached; } - return this.#doFetch(key, undefined, options); + const workerOptions = copyToTransferableRequestInit(options); + return this.#doFetch(key, undefined, workerOptions); } async getRange( key: zarr.AbsolutePath, range: zarr.RangeQuery, - options?: RequestInit + options?: RequestInit, ): Promise { const cacheKey = asCacheKey(key, range); const cached = this.#fromCache(cacheKey); if (cached !== undefined) { return cached; } - return this.#doFetch(key, range, options); - } -} - - -export class CopyableCachedFetchStore extends zarr.FetchStore { - /** - * Caches in-progress request promises so that only one request - * is ever made for the same data. - */ - #promiseCache: Map>; - - /** - * Caches the raw response results from previous requests. - */ - #dataCache: PriorityCache; - - /** - * The set of unique cache keys stored across either the promise - * cache or the data cache. - */ - #cacheKeys: Set; - - /** - * Stores the configuration options that were used to initialize - * the underlying FetchStore. - */ - #options: FetchStoreOptions | undefined; - - constructor( - url: string | URL, - maxBytes: number, - options?: CopyableCachedFetchStoreOptions | undefined - ) { - super(url, options); - const { initCache, ...remainingOptions } = options || {}; - this.#promiseCache = new Map(); - this.#dataCache = new PriorityCache(new Map(), (h: CacheKey) => this.#score(h), maxBytes); - this.#options = remainingOptions; - // this.#maxCacheBytes = maxBytes; - // this.#currCacheBytes = 0; - this.#cacheKeys = new Set(); - - if (initCache && initCache.size > 0) { - initCache.forEach((value, key) => this.#dataCache.put(key, new CacheableByteArray(value.slice()))); - } - } - - /** - * Attempts to add a new data block into the data cache, mapped to a unique cache key. - * If the data is undefined or there isn't enough space in the cache, nothing happens. - * - * @param cacheKey The unique key associated with the data block to cache - * @param data The data block to cache - */ - #updateDataCache(cacheKey: string, data: Uint8Array | undefined) { - if (data === undefined) { - this.#cacheKeys.delete(cacheKey); - return; // Zarrita's FetchStore returns an undefined when a 404 is encountered; do nothing - } - this.#dataCache.put(cacheKey, new CacheableByteArray(data.slice())); - } - - #uncachePromise(cacheKey: string): boolean { - return this.#promiseCache.delete(cacheKey); - } - - /** - * Wraps a data request promise within another promise that ensures that the results of - * the data request are correctly managed within the cache before being returned to the - * original caller. - * - * @param cacheKey The key representing the signature of a specific data request - * @param promise The data request represented as a Promise - * @returns the wrapping Promise - */ - #wrapPromise( - cacheKey: string, - promise: Promise - ): Promise { - return new Promise((resolve, reject) => { - promise - .then((data) => { - this.#updateDataCache(cacheKey, data); - resolve(data); - }) - .catch((e) => { - logger.error(`request for ${cacheKey}] failed to fetch; error:`, e); - reject(e); - }) - .finally(() => { - this.#uncachePromise(cacheKey); - }); - }); - } - - /** - * Performs a cache lookup of the specified cache key to see if there is either - * a) already-loaded data for that cache key, or - * b) a data-request promise underway for that cache key. - * - * The cache key is intended to represent the distinctive signature of a request for - * a particular set of data. Typically, this will be either a URL, or a URL and a - * byte range within the resource indicated by the URL. - * - * If neither data nor promise is found, a new request is sent, using either a direct - * URL request (using `FetchStore.get`) or a range request (using `FetchStore.getRange`). - * @param cacheKey The key representing the signature of a specific data request - * @param fetchFn Either FetchStore.get or FetchStore.getRange, wrapped in a closure - * @returns A cache-managed promise of the results from the data request - */ - #retrieve(cacheKey: string, fetchFn: () => Promise) { - const cachedData = this.#dataCache.get(cacheKey); - - if (cachedData === undefined) { - logger.debug( - `request for ${cacheKey}] not found in data cache, checking promise cache` - ); - const cachedPromise = this.#promiseCache.get(cacheKey); - if (cachedPromise === undefined) { - logger.debug( - `request for ${cacheKey}] not found in promise cache; fetching...` - ); - const promise = this.#wrapPromise(cacheKey, fetchFn()); - this.#promiseCache.set(cacheKey, promise); - this.#cacheKeys.add(cacheKey); - return promise; - } - } - return new Promise((resolve) => { - resolve(cachedData); - }); - } - - get( - key: zarr.AbsolutePath, - options?: RequestInit - ): Promise { - const cacheKey = asCacheKey(key); - return this.#retrieve(cacheKey, () => super.get(key, options)); - } - - getRange( - key: zarr.AbsolutePath, - range: zarr.RangeQuery, - options?: RequestInit - ): Promise { - const cacheKey = asCacheKey(key, range); - return this.#retrieve(cacheKey, () => super.getRange(key, range, options)); - } - - /** - * Copies this store into a new instance, with the same URL/location, configuration, - * and current cached data. - * - * NOTE: This function copies only the current cache data and does not copy over any - * active/unfulfilled requests for additional data. If needed, use `await .pending()` - * to wait for all active promises to be fulfilled. - * @returns A new CopyableCachedFetchStore copied from this one - */ - copy(): CopyableCachedFetchStore { - return new CopyableCachedFetchStore(this.url, this.#maxCacheBytes, { - ...this.#options, - initCache: this.#dataCache, - }); - } - - get cacheKeys(): SetIterator { - return this.#cacheKeys.values(); - } - - /** - * Synchronously delete all cached data from this store. - */ - clearData() { - this.#dataCache.clear(); - this.#currCacheBytes = 0; - } - - /** - * Waits until all active requests for data are completed. - * - * NOTE: This does not await any promises that are added AFTER this - * function is called. - */ - async pending() { - await Promise.allSettled(this.#promiseCache.values()); - } - - /** - * Waits until all active requests for data are completed, - * then delets all cached data from this store. - */ - async clear() { - await this.pending(); - this.clearData(); - } -} - - -export class CachedOmeZarrLoader { - #groupAttrsCache: Map; - #arrayAttrsCache: Map; - #arrayDataCache: PriorityCache; - - #maxDataBytes: number; - - #decoder: Decoder; - - constructor(maxDataBytes: number, maxWorkers: number) { - this.#maxDataBytes = Math.max(0, Math.min(maxDataBytes, getDataCacheSizeLimit())); - this.#groupAttrsCache = new Map(); - this.#arrayAttrsCache = new Map(); - this.#arrayDataCache = new PriorityCache(new Map(), (h: CacheKey) => this.#score(h), this.#maxDataBytes); l - this.#decoder = ( - dataset: OmeZarrMetadata, - req: ZarrRequest, - level: OmeZarrShapedDataset, - signal?: AbortSignal - ): Promise => this.#decode(dataset, req, level, signal); - } - - - - - #getStore(url: string): CachedFetchStore { - const store = this.#stores.get(url); // @TODO may not be a safe assumption that this is always the root URL; may not be an issue, though? - if (store === undefined) { - const newStore = new CachedFetchStore(url, getCacheSizeLimit()); - this.#stores.set(url, newStore); - return newStore; - } - return store; - } - - #decode( - dataset: OmeZarrMetadata, - req: ZarrRequest, - level: OmeZarrShapedDataset, - signal?: AbortSignal - ): Promise { - // check if `dataset` is in cache, if not, load it - // load group attrs - // load array attrs - // check if level matches one in the loaded version of `dataset`, if not, error - // retrieve `shape` and `path` from `level` - // retrieve `zarrVersion` from `dataset` - // retrieve `axes` from multiscale via `level`, `dataset` - this.#loadSlice(path, shape, axes, zarrVersion); - } - - get decoder(): Decoder { - return this.#decoder; - } - - #loadSlice(path: string, query: zarr.Slice) { - // check cache - // cache hit: return slice data - // cache miss: prepare worker and launch - } - - loadAttrs(res: WebResource): Promise { - const cached = this.#attrsCache.get(res.url); - if (cached !== undefined) { - return cached; - } - - const store = this.#getStore(res.url); - - const promise = new Promise(async (resolve, reject) => { - try { - const group = await zarr.open(store, { kind: "group" }); - const attrs = OmeZarrAttrsSchema.parse(group.attrs); - resolve(attrs); - } catch (e) { - if (e instanceof ZodError) { - logger.error("could not load Zarr file: parsing failed"); - } else { - logger.error("could not load Zarr file: ", e); - } - reject(e); - } - }); - - this.#attrsCache.set(res.url, promise); - return promise; - } - - async loadArrayMetadata() { } - - async loadMetadata(res: WebResource): Promise { - const cached = this.#metadataCache.get(res.url); - if (cached !== undefined) { - return cached; - } - - const store = this.#getStore(res.url); - - const promise = new Promise(async (resolve, reject) => { - const attrs: OmeZarrAttrs = await loadZarrAttrsFileFromStore(store); - const version = attrs.zarrVersion; - const arrays = await Promise.all( - attrs.multiscales - .map((multiscale) => { - return ( - multiscale.datasets?.map(async (dataset) => { - return ( - await loadZarrArrayFileFromStore( - store, - dataset.path, - version, - loadV2ArrayAttrs - ) - ).metadata; - }) ?? [] - ); - }) - .reduce((prev, curr) => prev.concat(curr)) - .filter((v) => v !== undefined) - ); - resolve(new OmeZarrMetadata(res.url, attrs, arrays, version)); - }); - this.#metadataCache.set(res.url, promise); - return promise; - } -} - -class CachedFetchStore extends zarr.FetchStore { - #cache: Map>; - #maxBytes: number; - #currBytes: number; - - constructor( - url: string | URL, - maxBytes: number, - options?: { - overrides?: RequestInit; - useSuffixRequest?: boolean; - } - ) { - super(url, options); - this.#cache = new Map(); - } - - get( - key: zarr.AbsolutePath, - options?: RequestInit - ): Promise { - const cacheKey = asCacheKey(key); - if (this.#cache.has(cacheKey)) { - logger.debug(`get: ${cacheKey} [CACHED]`); - } else { - logger.debug(`get: ${cacheKey}`); - } - const cached = this.#cache.get(cacheKey); - if (cached === undefined) { - const promise = super.get(key, options); - this.#cache.set(cacheKey, promise); - return promise; - } - return cached; - } - - getRange( - key: zarr.AbsolutePath, - range: zarr.RangeQuery, - options?: RequestInit - ): Promise { - const cacheKey = asCacheKey(key, range); - if (this.#cache.has(cacheKey)) { - console.log(`getRange: ${cacheKey} [CACHED]`); - } else { - console.log(`getRange: ${cacheKey}`); - } - const cached = this.#cache.get(cacheKey); - if (cached === undefined) { - const promise = super.getRange(key, range, options); - this.#cache.set(cacheKey, promise); - return promise; - } - return cached; - } -} - - -export async function loadSlice2( - url: string, - axes: OmeZarrAxis[], - r: ZarrRequest, - path: string, - zarrVersion: number, - shape: ReadonlyArray, - signal?: AbortSignal -) { - // put the request in native order - const store = new CachedFetchStore(url, 8 * 2 ** 10); - // if (!level) { - // const message = 'invalid Zarr data: no datasets found'; - // logger.error(message); - // throw new VisZarrDataError(message); - // } - // const arr = metadata.arrays.find((a) => a.path === level.path); - // if (!arr) { - // const message = `cannot load slice: no array found for path [${level.path}]`; - // logger.error(message); - // throw new VisZarrDataError(message); - // } - const { raw } = await loadZarrArrayFileFromStore( - store, - path, - zarrVersion, - false - ); - const result = await zarr.get(raw, buildQuery(r, axes, shape), { - opts: { signal: signal ?? null }, - }); - if (typeof result === "number") { - throw new Error("oh noes, slice came back all weird"); + const workerOptions = copyToTransferableRequestInit(options); + return this.#doFetch(key, range, workerOptions); } - return { - shape: result.shape, - buffer: result, - }; } diff --git a/packages/omezarr/src/zarr/cached-loading/worker-pool.ts b/packages/omezarr/src/zarr/cached-loading/worker-pool.ts index cf5ba36c..a7e96268 100644 --- a/packages/omezarr/src/zarr/cached-loading/worker-pool.ts +++ b/packages/omezarr/src/zarr/cached-loading/worker-pool.ts @@ -1,14 +1,10 @@ -import { - isWorkerMessageWithId, - type WorkerMessage, - type WorkerMessageWithId, -} from './fetch-slice.interface'; +import { isWorkerMessageWithId, type WorkerMessage, type WorkerMessageWithId } from './fetch-slice.interface'; import { v4 as uuidv4 } from 'uuid'; import { logger } from '@alleninstitute/vis-core'; type PromiseResolve = (t: T) => void; -// eslint-disable-next-line @typescript-eslint/no-explicit-any +// eslint-disable-next-line @typescript-eslint/no-explicit-any // biome-ignore lint/suspicious/noExplicitAny: This is aligned with the standard Promise API type PromiseReject = (reason: any) => void; @@ -72,7 +68,7 @@ export class WorkerPool { const messagePromise = this.#createMessagePromise(responseValidator); // TODO this cast is very annoying; would be nice to remove it - this.#promises.set(reqId, messagePromise as unknown as MessagePromise); + this.#promises.set(reqId, messagePromise as unknown as MessagePromise); if (signal) { signal.onabort = () => { diff --git a/packages/omezarr/src/zarr/cached-loading/zarr-loader.ts b/packages/omezarr/src/zarr/cached-loading/zarr-loader.ts new file mode 100644 index 00000000..8f840af1 --- /dev/null +++ b/packages/omezarr/src/zarr/cached-loading/zarr-loader.ts @@ -0,0 +1,436 @@ + +const DEFAULT_MAX_ATTRS_CACHE_BYTES = 16 * 2 ** 10; // 16 MB -- aribtrarily chosen at this point +const DEFAULT_MAX_DATA_CACHE_BYTES = 256 * 2 ** 10; // 256 MB -- aribtrarily chosen at this point + +// @TODO implement a much more context-aware cache size limiting mechanism +const getAttrsCacheSizeLimit = () => { + return DEFAULT_MAX_ATTRS_CACHE_BYTES; +}; + +// @TODO implement a much more context-aware cache size limiting mechanism +const getDataCacheSizeLimit = () => { + return DEFAULT_MAX_ATTRS_CACHE_BYTES; +}; + + + +export class CopyableCachedFetchStore extends zarr.FetchStore { + /** + * Caches in-progress request promises so that only one request + * is ever made for the same data. + */ + #promiseCache: Map>; + + /** + * Caches the raw response results from previous requests. + */ + #dataCache: PriorityCache; + + /** + * The set of unique cache keys stored across either the promise + * cache or the data cache. + */ + #cacheKeys: Set; + + /** + * Stores the configuration options that were used to initialize + * the underlying FetchStore. + */ + #options: FetchStoreOptions | undefined; + + constructor( + url: string | URL, + maxBytes: number, + options?: CopyableCachedFetchStoreOptions | undefined + ) { + super(url, options); + const { initCache, ...remainingOptions } = options || {}; + this.#promiseCache = new Map(); + this.#dataCache = new PriorityCache(new Map(), (h: CacheKey) => this.#score(h), maxBytes); + this.#options = remainingOptions; + // this.#maxCacheBytes = maxBytes; + // this.#currCacheBytes = 0; + this.#cacheKeys = new Set(); + + if (initCache && initCache.size > 0) { + initCache.forEach((value, key) => this.#dataCache.put(key, new CacheableByteArray(value.slice()))); + } + } + + /** + * Attempts to add a new data block into the data cache, mapped to a unique cache key. + * If the data is undefined or there isn't enough space in the cache, nothing happens. + * + * @param cacheKey The unique key associated with the data block to cache + * @param data The data block to cache + */ + #updateDataCache(cacheKey: string, data: Uint8Array | undefined) { + if (data === undefined) { + this.#cacheKeys.delete(cacheKey); + return; // Zarrita's FetchStore returns an undefined when a 404 is encountered; do nothing + } + this.#dataCache.put(cacheKey, new CacheableByteArray(data.slice())); + } + + #uncachePromise(cacheKey: string): boolean { + return this.#promiseCache.delete(cacheKey); + } + + /** + * Wraps a data request promise within another promise that ensures that the results of + * the data request are correctly managed within the cache before being returned to the + * original caller. + * + * @param cacheKey The key representing the signature of a specific data request + * @param promise The data request represented as a Promise + * @returns the wrapping Promise + */ + #wrapPromise( + cacheKey: string, + promise: Promise + ): Promise { + return new Promise((resolve, reject) => { + promise + .then((data) => { + this.#updateDataCache(cacheKey, data); + resolve(data); + }) + .catch((e) => { + logger.error(`request for ${cacheKey}] failed to fetch; error:`, e); + reject(e); + }) + .finally(() => { + this.#uncachePromise(cacheKey); + }); + }); + } + + /** + * Performs a cache lookup of the specified cache key to see if there is either + * a) already-loaded data for that cache key, or + * b) a data-request promise underway for that cache key. + * + * The cache key is intended to represent the distinctive signature of a request for + * a particular set of data. Typically, this will be either a URL, or a URL and a + * byte range within the resource indicated by the URL. + * + * If neither data nor promise is found, a new request is sent, using either a direct + * URL request (using `FetchStore.get`) or a range request (using `FetchStore.getRange`). + * @param cacheKey The key representing the signature of a specific data request + * @param fetchFn Either FetchStore.get or FetchStore.getRange, wrapped in a closure + * @returns A cache-managed promise of the results from the data request + */ + #retrieve(cacheKey: string, fetchFn: () => Promise) { + const cachedData = this.#dataCache.get(cacheKey); + + if (cachedData === undefined) { + logger.debug( + `request for ${cacheKey}] not found in data cache, checking promise cache` + ); + const cachedPromise = this.#promiseCache.get(cacheKey); + if (cachedPromise === undefined) { + logger.debug( + `request for ${cacheKey}] not found in promise cache; fetching...` + ); + const promise = this.#wrapPromise(cacheKey, fetchFn()); + this.#promiseCache.set(cacheKey, promise); + this.#cacheKeys.add(cacheKey); + return promise; + } + } + return new Promise((resolve) => { + resolve(cachedData); + }); + } + + get( + key: zarr.AbsolutePath, + options?: RequestInit + ): Promise { + const cacheKey = asCacheKey(key); + return this.#retrieve(cacheKey, () => super.get(key, options)); + } + + getRange( + key: zarr.AbsolutePath, + range: zarr.RangeQuery, + options?: RequestInit + ): Promise { + const cacheKey = asCacheKey(key, range); + return this.#retrieve(cacheKey, () => super.getRange(key, range, options)); + } + + /** + * Copies this store into a new instance, with the same URL/location, configuration, + * and current cached data. + * + * NOTE: This function copies only the current cache data and does not copy over any + * active/unfulfilled requests for additional data. If needed, use `await .pending()` + * to wait for all active promises to be fulfilled. + * @returns A new CopyableCachedFetchStore copied from this one + */ + copy(): CopyableCachedFetchStore { + return new CopyableCachedFetchStore(this.url, this.#maxCacheBytes, { + ...this.#options, + initCache: this.#dataCache, + }); + } + + get cacheKeys(): SetIterator { + return this.#cacheKeys.values(); + } + + /** + * Synchronously delete all cached data from this store. + */ + clearData() { + this.#dataCache.clear(); + this.#currCacheBytes = 0; + } + + /** + * Waits until all active requests for data are completed. + * + * NOTE: This does not await any promises that are added AFTER this + * function is called. + */ + async pending() { + await Promise.allSettled(this.#promiseCache.values()); + } + + /** + * Waits until all active requests for data are completed, + * then delets all cached data from this store. + */ + async clear() { + await this.pending(); + this.clearData(); + } +} + + + +export class CachedOmeZarrLoader { + #groupAttrsCache: Map; + #arrayAttrsCache: Map; + #arrayDataCache: PriorityCache; + + #maxDataBytes: number; + + #decoder: Decoder; + + constructor(maxDataBytes: number, maxWorkers: number) { + this.#maxDataBytes = Math.max(0, Math.min(maxDataBytes, getDataCacheSizeLimit())); + this.#groupAttrsCache = new Map(); + this.#arrayAttrsCache = new Map(); + this.#arrayDataCache = new PriorityCache(new Map(), (h: CacheKey) => this.#score(h), this.#maxDataBytes); l + this.#decoder = ( + dataset: OmeZarrMetadata, + req: ZarrRequest, + level: OmeZarrShapedDataset, + signal?: AbortSignal + ): Promise => this.#decode(dataset, req, level, signal); + } + + + + + #getStore(url: string): CachedFetchStore { + const store = this.#stores.get(url); // @TODO may not be a safe assumption that this is always the root URL; may not be an issue, though? + if (store === undefined) { + const newStore = new CachedFetchStore(url, getCacheSizeLimit()); + this.#stores.set(url, newStore); + return newStore; + } + return store; + } + + #decode( + dataset: OmeZarrMetadata, + req: ZarrRequest, + level: OmeZarrShapedDataset, + signal?: AbortSignal + ): Promise { + // check if `dataset` is in cache, if not, load it + // load group attrs + // load array attrs + // check if level matches one in the loaded version of `dataset`, if not, error + // retrieve `shape` and `path` from `level` + // retrieve `zarrVersion` from `dataset` + // retrieve `axes` from multiscale via `level`, `dataset` + this.#loadSlice(path, shape, axes, zarrVersion); + } + + get decoder(): Decoder { + return this.#decoder; + } + + #loadSlice(path: string, query: zarr.Slice) { + // check cache + // cache hit: return slice data + // cache miss: prepare worker and launch + } + + loadAttrs(res: WebResource): Promise { + const cached = this.#attrsCache.get(res.url); + if (cached !== undefined) { + return cached; + } + + const store = this.#getStore(res.url); + + const promise = new Promise(async (resolve, reject) => { + try { + const group = await zarr.open(store, { kind: "group" }); + const attrs = OmeZarrAttrsSchema.parse(group.attrs); + resolve(attrs); + } catch (e) { + if (e instanceof ZodError) { + logger.error("could not load Zarr file: parsing failed"); + } else { + logger.error("could not load Zarr file: ", e); + } + reject(e); + } + }); + + this.#attrsCache.set(res.url, promise); + return promise; + } + + async loadArrayMetadata() { } + + async loadMetadata(res: WebResource): Promise { + const cached = this.#metadataCache.get(res.url); + if (cached !== undefined) { + return cached; + } + + const store = this.#getStore(res.url); + + const promise = new Promise(async (resolve, reject) => { + const attrs: OmeZarrAttrs = await loadZarrAttrsFileFromStore(store); + const version = attrs.zarrVersion; + const arrays = await Promise.all( + attrs.multiscales + .map((multiscale) => { + return ( + multiscale.datasets?.map(async (dataset) => { + return ( + await loadZarrArrayFileFromStore( + store, + dataset.path, + version, + loadV2ArrayAttrs + ) + ).metadata; + }) ?? [] + ); + }) + .reduce((prev, curr) => prev.concat(curr)) + .filter((v) => v !== undefined) + ); + resolve(new OmeZarrMetadata(res.url, attrs, arrays, version)); + }); + this.#metadataCache.set(res.url, promise); + return promise; + } +} + +class CachedFetchStore extends zarr.FetchStore { + #cache: Map>; + #maxBytes: number; + #currBytes: number; + + constructor( + url: string | URL, + maxBytes: number, + options?: { + overrides?: RequestInit; + useSuffixRequest?: boolean; + } + ) { + super(url, options); + this.#cache = new Map(); + } + + get( + key: zarr.AbsolutePath, + options?: RequestInit + ): Promise { + const cacheKey = asCacheKey(key); + if (this.#cache.has(cacheKey)) { + logger.debug(`get: ${cacheKey} [CACHED]`); + } else { + logger.debug(`get: ${cacheKey}`); + } + const cached = this.#cache.get(cacheKey); + if (cached === undefined) { + const promise = super.get(key, options); + this.#cache.set(cacheKey, promise); + return promise; + } + return cached; + } + + getRange( + key: zarr.AbsolutePath, + range: zarr.RangeQuery, + options?: RequestInit + ): Promise { + const cacheKey = asCacheKey(key, range); + if (this.#cache.has(cacheKey)) { + console.log(`getRange: ${cacheKey} [CACHED]`); + } else { + console.log(`getRange: ${cacheKey}`); + } + const cached = this.#cache.get(cacheKey); + if (cached === undefined) { + const promise = super.getRange(key, range, options); + this.#cache.set(cacheKey, promise); + return promise; + } + return cached; + } +} + + +export async function loadSlice2( + url: string, + axes: OmeZarrAxis[], + r: ZarrRequest, + path: string, + zarrVersion: number, + shape: ReadonlyArray, + signal?: AbortSignal +) { + // put the request in native order + const store = new CachedFetchStore(url, 8 * 2 ** 10); + // if (!level) { + // const message = 'invalid Zarr data: no datasets found'; + // logger.error(message); + // throw new VisZarrDataError(message); + // } + // const arr = metadata.arrays.find((a) => a.path === level.path); + // if (!arr) { + // const message = `cannot load slice: no array found for path [${level.path}]`; + // logger.error(message); + // throw new VisZarrDataError(message); + // } + const { raw } = await loadZarrArrayFileFromStore( + store, + path, + zarrVersion, + false + ); + const result = await zarr.get(raw, buildQuery(r, axes, shape), { + opts: { signal: signal ?? null }, + }); + if (typeof result === "number") { + throw new Error("oh noes, slice came back all weird"); + } + return { + shape: result.shape, + buffer: result, + }; +} diff --git a/site/src/examples/omezarr/render-utils.ts b/site/src/examples/omezarr/render-utils.ts index d4a61ac3..384b651f 100644 --- a/site/src/examples/omezarr/render-utils.ts +++ b/site/src/examples/omezarr/render-utils.ts @@ -28,7 +28,8 @@ type Thing = { settings: RenderSettings; }; function mapValues, V, R>(obj: T, fn: (v: V) => R): { [k in keyof T]: R } { - return (Object.keys(obj) as (keyof T)[]).reduce( // typecast is annoyingly necessary in this case to avoid a linting warning + return (Object.keys(obj) as (keyof T)[]).reduce( + // typecast is annoyingly necessary in this case to avoid a linting warning (acc, k) => { acc[k] = fn(obj[k]); return acc; From 0f671f2efebe57dbcf07e2076f4d5b29c602e11d Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 3 Oct 2025 09:27:28 -0700 Subject: [PATCH 07/91] Cleanup --- .../omezarr/src/zarr/cached-loading/store.ts | 11 +- .../src/zarr/cached-loading/zarr-loader.ts | 436 ------------------ 2 files changed, 8 insertions(+), 439 deletions(-) delete mode 100644 packages/omezarr/src/zarr/cached-loading/zarr-loader.ts diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index be6255b6..a295bd7b 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -17,19 +17,24 @@ type FetchStoreOptions = { }; class CacheableByteArray implements Cacheable { - #arr: Uint8Array; + #arr: Uint8Array | null; constructor(arr: Uint8Array) { this.#arr = arr; } - destroy() {} + destroy() { + this.#arr = null; + } sizeInBytes(): number { - return this.#arr.byteLength; + return this.#arr?.byteLength ?? 0; } buffer(): ArrayBufferLike { + if (this.#arr === null) { + throw new Error('cannot retrieve data buffer: array is null'); + } return this.#arr.buffer; } } diff --git a/packages/omezarr/src/zarr/cached-loading/zarr-loader.ts b/packages/omezarr/src/zarr/cached-loading/zarr-loader.ts deleted file mode 100644 index 8f840af1..00000000 --- a/packages/omezarr/src/zarr/cached-loading/zarr-loader.ts +++ /dev/null @@ -1,436 +0,0 @@ - -const DEFAULT_MAX_ATTRS_CACHE_BYTES = 16 * 2 ** 10; // 16 MB -- aribtrarily chosen at this point -const DEFAULT_MAX_DATA_CACHE_BYTES = 256 * 2 ** 10; // 256 MB -- aribtrarily chosen at this point - -// @TODO implement a much more context-aware cache size limiting mechanism -const getAttrsCacheSizeLimit = () => { - return DEFAULT_MAX_ATTRS_CACHE_BYTES; -}; - -// @TODO implement a much more context-aware cache size limiting mechanism -const getDataCacheSizeLimit = () => { - return DEFAULT_MAX_ATTRS_CACHE_BYTES; -}; - - - -export class CopyableCachedFetchStore extends zarr.FetchStore { - /** - * Caches in-progress request promises so that only one request - * is ever made for the same data. - */ - #promiseCache: Map>; - - /** - * Caches the raw response results from previous requests. - */ - #dataCache: PriorityCache; - - /** - * The set of unique cache keys stored across either the promise - * cache or the data cache. - */ - #cacheKeys: Set; - - /** - * Stores the configuration options that were used to initialize - * the underlying FetchStore. - */ - #options: FetchStoreOptions | undefined; - - constructor( - url: string | URL, - maxBytes: number, - options?: CopyableCachedFetchStoreOptions | undefined - ) { - super(url, options); - const { initCache, ...remainingOptions } = options || {}; - this.#promiseCache = new Map(); - this.#dataCache = new PriorityCache(new Map(), (h: CacheKey) => this.#score(h), maxBytes); - this.#options = remainingOptions; - // this.#maxCacheBytes = maxBytes; - // this.#currCacheBytes = 0; - this.#cacheKeys = new Set(); - - if (initCache && initCache.size > 0) { - initCache.forEach((value, key) => this.#dataCache.put(key, new CacheableByteArray(value.slice()))); - } - } - - /** - * Attempts to add a new data block into the data cache, mapped to a unique cache key. - * If the data is undefined or there isn't enough space in the cache, nothing happens. - * - * @param cacheKey The unique key associated with the data block to cache - * @param data The data block to cache - */ - #updateDataCache(cacheKey: string, data: Uint8Array | undefined) { - if (data === undefined) { - this.#cacheKeys.delete(cacheKey); - return; // Zarrita's FetchStore returns an undefined when a 404 is encountered; do nothing - } - this.#dataCache.put(cacheKey, new CacheableByteArray(data.slice())); - } - - #uncachePromise(cacheKey: string): boolean { - return this.#promiseCache.delete(cacheKey); - } - - /** - * Wraps a data request promise within another promise that ensures that the results of - * the data request are correctly managed within the cache before being returned to the - * original caller. - * - * @param cacheKey The key representing the signature of a specific data request - * @param promise The data request represented as a Promise - * @returns the wrapping Promise - */ - #wrapPromise( - cacheKey: string, - promise: Promise - ): Promise { - return new Promise((resolve, reject) => { - promise - .then((data) => { - this.#updateDataCache(cacheKey, data); - resolve(data); - }) - .catch((e) => { - logger.error(`request for ${cacheKey}] failed to fetch; error:`, e); - reject(e); - }) - .finally(() => { - this.#uncachePromise(cacheKey); - }); - }); - } - - /** - * Performs a cache lookup of the specified cache key to see if there is either - * a) already-loaded data for that cache key, or - * b) a data-request promise underway for that cache key. - * - * The cache key is intended to represent the distinctive signature of a request for - * a particular set of data. Typically, this will be either a URL, or a URL and a - * byte range within the resource indicated by the URL. - * - * If neither data nor promise is found, a new request is sent, using either a direct - * URL request (using `FetchStore.get`) or a range request (using `FetchStore.getRange`). - * @param cacheKey The key representing the signature of a specific data request - * @param fetchFn Either FetchStore.get or FetchStore.getRange, wrapped in a closure - * @returns A cache-managed promise of the results from the data request - */ - #retrieve(cacheKey: string, fetchFn: () => Promise) { - const cachedData = this.#dataCache.get(cacheKey); - - if (cachedData === undefined) { - logger.debug( - `request for ${cacheKey}] not found in data cache, checking promise cache` - ); - const cachedPromise = this.#promiseCache.get(cacheKey); - if (cachedPromise === undefined) { - logger.debug( - `request for ${cacheKey}] not found in promise cache; fetching...` - ); - const promise = this.#wrapPromise(cacheKey, fetchFn()); - this.#promiseCache.set(cacheKey, promise); - this.#cacheKeys.add(cacheKey); - return promise; - } - } - return new Promise((resolve) => { - resolve(cachedData); - }); - } - - get( - key: zarr.AbsolutePath, - options?: RequestInit - ): Promise { - const cacheKey = asCacheKey(key); - return this.#retrieve(cacheKey, () => super.get(key, options)); - } - - getRange( - key: zarr.AbsolutePath, - range: zarr.RangeQuery, - options?: RequestInit - ): Promise { - const cacheKey = asCacheKey(key, range); - return this.#retrieve(cacheKey, () => super.getRange(key, range, options)); - } - - /** - * Copies this store into a new instance, with the same URL/location, configuration, - * and current cached data. - * - * NOTE: This function copies only the current cache data and does not copy over any - * active/unfulfilled requests for additional data. If needed, use `await .pending()` - * to wait for all active promises to be fulfilled. - * @returns A new CopyableCachedFetchStore copied from this one - */ - copy(): CopyableCachedFetchStore { - return new CopyableCachedFetchStore(this.url, this.#maxCacheBytes, { - ...this.#options, - initCache: this.#dataCache, - }); - } - - get cacheKeys(): SetIterator { - return this.#cacheKeys.values(); - } - - /** - * Synchronously delete all cached data from this store. - */ - clearData() { - this.#dataCache.clear(); - this.#currCacheBytes = 0; - } - - /** - * Waits until all active requests for data are completed. - * - * NOTE: This does not await any promises that are added AFTER this - * function is called. - */ - async pending() { - await Promise.allSettled(this.#promiseCache.values()); - } - - /** - * Waits until all active requests for data are completed, - * then delets all cached data from this store. - */ - async clear() { - await this.pending(); - this.clearData(); - } -} - - - -export class CachedOmeZarrLoader { - #groupAttrsCache: Map; - #arrayAttrsCache: Map; - #arrayDataCache: PriorityCache; - - #maxDataBytes: number; - - #decoder: Decoder; - - constructor(maxDataBytes: number, maxWorkers: number) { - this.#maxDataBytes = Math.max(0, Math.min(maxDataBytes, getDataCacheSizeLimit())); - this.#groupAttrsCache = new Map(); - this.#arrayAttrsCache = new Map(); - this.#arrayDataCache = new PriorityCache(new Map(), (h: CacheKey) => this.#score(h), this.#maxDataBytes); l - this.#decoder = ( - dataset: OmeZarrMetadata, - req: ZarrRequest, - level: OmeZarrShapedDataset, - signal?: AbortSignal - ): Promise => this.#decode(dataset, req, level, signal); - } - - - - - #getStore(url: string): CachedFetchStore { - const store = this.#stores.get(url); // @TODO may not be a safe assumption that this is always the root URL; may not be an issue, though? - if (store === undefined) { - const newStore = new CachedFetchStore(url, getCacheSizeLimit()); - this.#stores.set(url, newStore); - return newStore; - } - return store; - } - - #decode( - dataset: OmeZarrMetadata, - req: ZarrRequest, - level: OmeZarrShapedDataset, - signal?: AbortSignal - ): Promise { - // check if `dataset` is in cache, if not, load it - // load group attrs - // load array attrs - // check if level matches one in the loaded version of `dataset`, if not, error - // retrieve `shape` and `path` from `level` - // retrieve `zarrVersion` from `dataset` - // retrieve `axes` from multiscale via `level`, `dataset` - this.#loadSlice(path, shape, axes, zarrVersion); - } - - get decoder(): Decoder { - return this.#decoder; - } - - #loadSlice(path: string, query: zarr.Slice) { - // check cache - // cache hit: return slice data - // cache miss: prepare worker and launch - } - - loadAttrs(res: WebResource): Promise { - const cached = this.#attrsCache.get(res.url); - if (cached !== undefined) { - return cached; - } - - const store = this.#getStore(res.url); - - const promise = new Promise(async (resolve, reject) => { - try { - const group = await zarr.open(store, { kind: "group" }); - const attrs = OmeZarrAttrsSchema.parse(group.attrs); - resolve(attrs); - } catch (e) { - if (e instanceof ZodError) { - logger.error("could not load Zarr file: parsing failed"); - } else { - logger.error("could not load Zarr file: ", e); - } - reject(e); - } - }); - - this.#attrsCache.set(res.url, promise); - return promise; - } - - async loadArrayMetadata() { } - - async loadMetadata(res: WebResource): Promise { - const cached = this.#metadataCache.get(res.url); - if (cached !== undefined) { - return cached; - } - - const store = this.#getStore(res.url); - - const promise = new Promise(async (resolve, reject) => { - const attrs: OmeZarrAttrs = await loadZarrAttrsFileFromStore(store); - const version = attrs.zarrVersion; - const arrays = await Promise.all( - attrs.multiscales - .map((multiscale) => { - return ( - multiscale.datasets?.map(async (dataset) => { - return ( - await loadZarrArrayFileFromStore( - store, - dataset.path, - version, - loadV2ArrayAttrs - ) - ).metadata; - }) ?? [] - ); - }) - .reduce((prev, curr) => prev.concat(curr)) - .filter((v) => v !== undefined) - ); - resolve(new OmeZarrMetadata(res.url, attrs, arrays, version)); - }); - this.#metadataCache.set(res.url, promise); - return promise; - } -} - -class CachedFetchStore extends zarr.FetchStore { - #cache: Map>; - #maxBytes: number; - #currBytes: number; - - constructor( - url: string | URL, - maxBytes: number, - options?: { - overrides?: RequestInit; - useSuffixRequest?: boolean; - } - ) { - super(url, options); - this.#cache = new Map(); - } - - get( - key: zarr.AbsolutePath, - options?: RequestInit - ): Promise { - const cacheKey = asCacheKey(key); - if (this.#cache.has(cacheKey)) { - logger.debug(`get: ${cacheKey} [CACHED]`); - } else { - logger.debug(`get: ${cacheKey}`); - } - const cached = this.#cache.get(cacheKey); - if (cached === undefined) { - const promise = super.get(key, options); - this.#cache.set(cacheKey, promise); - return promise; - } - return cached; - } - - getRange( - key: zarr.AbsolutePath, - range: zarr.RangeQuery, - options?: RequestInit - ): Promise { - const cacheKey = asCacheKey(key, range); - if (this.#cache.has(cacheKey)) { - console.log(`getRange: ${cacheKey} [CACHED]`); - } else { - console.log(`getRange: ${cacheKey}`); - } - const cached = this.#cache.get(cacheKey); - if (cached === undefined) { - const promise = super.getRange(key, range, options); - this.#cache.set(cacheKey, promise); - return promise; - } - return cached; - } -} - - -export async function loadSlice2( - url: string, - axes: OmeZarrAxis[], - r: ZarrRequest, - path: string, - zarrVersion: number, - shape: ReadonlyArray, - signal?: AbortSignal -) { - // put the request in native order - const store = new CachedFetchStore(url, 8 * 2 ** 10); - // if (!level) { - // const message = 'invalid Zarr data: no datasets found'; - // logger.error(message); - // throw new VisZarrDataError(message); - // } - // const arr = metadata.arrays.find((a) => a.path === level.path); - // if (!arr) { - // const message = `cannot load slice: no array found for path [${level.path}]`; - // logger.error(message); - // throw new VisZarrDataError(message); - // } - const { raw } = await loadZarrArrayFileFromStore( - store, - path, - zarrVersion, - false - ); - const result = await zarr.get(raw, buildQuery(r, axes, shape), { - opts: { signal: signal ?? null }, - }); - if (typeof result === "number") { - throw new Error("oh noes, slice came back all weird"); - } - return { - shape: result.shape, - buffer: result, - }; -} From 5130891ef43cf64aa9b385206ead249a3058c495 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 3 Oct 2025 09:37:49 -0700 Subject: [PATCH 08/91] Un-export decoder (that's for later) --- packages/omezarr/src/sliceview/slice-renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/omezarr/src/sliceview/slice-renderer.ts b/packages/omezarr/src/sliceview/slice-renderer.ts index 92db7530..c9e84822 100644 --- a/packages/omezarr/src/sliceview/slice-renderer.ts +++ b/packages/omezarr/src/sliceview/slice-renderer.ts @@ -97,7 +97,7 @@ function isPrepared(cacheData: Record): cach return keys.every((key) => cacheData[key]?.type === 'texture'); } -export type Decoder = ( +type Decoder = ( dataset: OmeZarrMetadata, req: ZarrRequest, level: OmeZarrShapedDataset, From 7e0c526ff47c08099844f9dddfc0490fe330b846 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 3 Oct 2025 10:24:54 -0700 Subject: [PATCH 09/91] WIP --- packages/omezarr/src/zarr/omezarr-loader.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 packages/omezarr/src/zarr/omezarr-loader.ts diff --git a/packages/omezarr/src/zarr/omezarr-loader.ts b/packages/omezarr/src/zarr/omezarr-loader.ts new file mode 100644 index 00000000..f01d4626 --- /dev/null +++ b/packages/omezarr/src/zarr/omezarr-loader.ts @@ -0,0 +1,14 @@ +import { OmeZarrMetadata } +import { CachingMultithreadedFetchStore } from "./cached-loading/store"; + +export class OmeZarrLoader { + #store: CachingMultithreadedFetchStore; + + constructor(url: string | URL) { + this.#store = new CachingMultithreadedFetchStore(url); + } + + loadMetadata(): Promise { + + } +} \ No newline at end of file From 49540bf7df3d99e97cd45503d88ed6a322c44a2a Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 3 Oct 2025 10:25:18 -0700 Subject: [PATCH 10/91] Fixed how maxBytes is specified --- packages/omezarr/src/zarr/cached-loading/store.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index a295bd7b..dae68eb3 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -4,6 +4,12 @@ import { WorkerPool } from './worker-pool'; import { FETCH_SLICE_MESSAGE_TYPE, isFetchSliceResponseMessage } from './fetch-slice.interface'; const DEFAULT_NUM_WORKERS = 6; +const DEFAULT_MAX_DATA_CACHE_BYTES = 256 * 2 ** 10; // 256 MB -- aribtrarily chosen at this point + +// @TODO implement a much more context-aware cache size limiting mechanism +const getDataCacheSizeLimit = () => { + return DEFAULT_MAX_DATA_CACHE_BYTES; +}; const asCacheKey = (key: zarr.AbsolutePath, range?: zarr.RangeQuery | undefined): string => { const keyStr = JSON.stringify(key); @@ -106,13 +112,13 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { */ #scoreFn: (h: CacheKey) => number; - constructor(url: string | URL, maxBytes: number, options?: CachingMultithreadedFetchStoreOptions) { + constructor(url: string | URL, options?: CachingMultithreadedFetchStoreOptions) { super(url, options?.fetchStoreOptions); this.#scoreFn = (h: CacheKey) => this.score(h); this.#dataCache = new PriorityCache( new Map(), this.#scoreFn, - maxBytes, + options?.maxBytes ?? getDataCacheSizeLimit(), ); this.#priorityMap = new Map(); this.#workerPool = new WorkerPool( From 42e00985c8ec63f00b1afa9cba3ef0a21b1f372d Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 3 Oct 2025 12:27:07 -0700 Subject: [PATCH 11/91] Switched to using AsyncPriorityCache --- .../shared-priority-cache/priority-cache.ts | 131 ++++++++++-------- .../omezarr/src/zarr/cached-loading/store.ts | 114 ++++++++++++--- 2 files changed, 165 insertions(+), 80 deletions(-) diff --git a/packages/core/src/shared-priority-cache/priority-cache.ts b/packages/core/src/shared-priority-cache/priority-cache.ts index 08819280..c46a1c1a 100644 --- a/packages/core/src/shared-priority-cache/priority-cache.ts +++ b/packages/core/src/shared-priority-cache/priority-cache.ts @@ -6,6 +6,7 @@ export interface Cacheable { destroy?: () => void; sizeInBytes: () => number; } + export interface Store { set(k: K, v: V): void; get(k: K): V | undefined; @@ -14,43 +15,54 @@ export interface Store { keys(): Iterable; values(): Iterable; } -type PendingResource = { + +export type ScoreFn = (k: CacheKey) => number; + +type PendingCacheable = { key: CacheKey; fetch: (sig: AbortSignal) => Promise; }; -function negate(fn: (k: CacheKey) => number) { +function negate(fn: ScoreFn) { return (k: CacheKey) => -fn(k); } + export type FetchResult = { status: 'success' } | { status: 'failure'; reason: unknown }; export class PriorityCache { - private store: Store; - private evictPriority: MinHeap; - private limit: number; - private used: number; + #store: Store; + #evictPriority: MinHeap; + #limit: number; + #used: number; + #score: ScoreFn; // items with lower scores will be evicted before items with high scores - constructor(store: Store, score: (k: CacheKey) => number, limitInBytes: number) { - this.store = store; - this.evictPriority = new MinHeap(5000, score); - this.limit = limitInBytes; - this.used = 0; + constructor(store: Store, score: ScoreFn, limitInBytes: number) { + this.#store = store; + this.#score = score; + this.#evictPriority = new MinHeap(5000, score); + this.#limit = limitInBytes; + this.#used = 0; } + + protected get score(): ScoreFn { + return this.#score; + } + // add {key:item} to the cache - return false (and fail) if the key is already present // may evict items to make room // return true on success put(key: CacheKey, item: T): boolean { - if (this.store.has(key)) { + if (this.#store.has(key)) { return false; } const size = this.sanitizedSize(item); - if (this.used + size > this.limit) { - this.evictUntil(Math.max(0, this.limit - size)); + if (this.#used + size > this.#limit) { + this.evictUntil(Math.max(0, this.#limit - size)); } - this.evictPriority.addItem(key); - this.store.set(key, item); - this.used += size; + this.#evictPriority.addItem(key); + this.#store.set(key, item); + this.#used += size; return true; } private sanitizedSize(item: T) { @@ -63,42 +75,42 @@ export class PriorityCache { // it has a closure over data that changes over time, representing changing priorities // thus - the owner of this cache has a responsibility to notify the cache when significant // changes in priority occur! - reprioritize(score: (k: CacheKey) => number) { - this.evictPriority.rebuild(score); + reprioritize(score?: ScoreFn | undefined) { + this.#evictPriority.rebuild(score); } get(key: CacheKey): T | undefined { - return this.store.get(key); + return this.#store.get(key); } has(key: CacheKey): boolean { - return this.store.has(key); + return this.#store.has(key); } cached(key: CacheKey): boolean { - return this.store.has(key); + return this.#store.has(key); } isFull(): boolean { - return this.used >= this.limit; + return this.#used >= this.#limit; } private evictLowestPriority() { - const evictMe = this.evictPriority.popMinItem(); + const evictMe = this.#evictPriority.popMinItem(); if (evictMe === null) return false; - const data = this.store.get(evictMe); + const data = this.#store.get(evictMe); if (data) { data.destroy?.(); - this.store.delete(evictMe); + this.#store.delete(evictMe); const size = this.sanitizedSize(data); - this.used -= size; + this.#used -= size; } return true; } private evictUntil(targetUsedBytes: number) { - while (this.used > targetUsedBytes) { + while (this.#used > targetUsedBytes) { if (!this.evictLowestPriority()) { // note: evictLowestPriority mutates this.used return; // all items evicted... @@ -108,10 +120,10 @@ export class PriorityCache { } export class AsyncPriorityCache extends PriorityCache { - private fetchPriority: KeyedMinHeap, CacheKey>; - private pendingFetches: Map; - private MAX_INFLIGHT_FETCHES: number; - private notify: undefined | ((k: CacheKey, result: FetchResult) => void); + #fetchPriority: KeyedMinHeap, CacheKey>; + #pendingFetches: Map; + #MAX_INFLIGHT_FETCHES: number; + #notify: undefined | ((k: CacheKey, result: FetchResult) => void); // items with lower scores will be evicted before items with high scores constructor( @@ -123,46 +135,46 @@ export class AsyncPriorityCache extends PriorityCache { ) { super(store, score, limitInBytes); - this.fetchPriority = new KeyedMinHeap, CacheKey>(5000, negate(score), (pr) => pr.key); - this.pendingFetches = new Map(); - this.MAX_INFLIGHT_FETCHES = maxFetches; - this.notify = onDataArrived; + this.#fetchPriority = new KeyedMinHeap, CacheKey>(5000, negate(score), (pr) => pr.key); + this.#pendingFetches = new Map(); + this.#MAX_INFLIGHT_FETCHES = maxFetches; + this.#notify = onDataArrived; } - enqueue(key: CacheKey, fetcher: (abort: AbortSignal) => Promise) { + enqueue(key: CacheKey, fetcher: (abort: AbortSignal) => Promise): boolean { // enqueue the item, if we dont already have it, or are not already asking - if (!this.has(key) && !this.pendingFetches.has(key) && !this.fetchPriority.hasItemWithKey(key)) { - this.fetchPriority.addItem({ key, fetch: fetcher }); - this.fetchToLimit(); + if (!this.has(key) && !this.#pendingFetches.has(key) && !this.#fetchPriority.hasItemWithKey(key)) { + this.#fetchPriority.addItem({ key, fetch: fetcher }); + this.#fetchToLimit(); return true; } return false; } - private beginFetch({ key, fetch }: PendingResource) { + async #beginFetch({ key, fetch }: PendingCacheable) { const abort = new AbortController(); - this.pendingFetches.set(key, abort); + this.#pendingFetches.set(key, abort); return fetch(abort.signal) .then((resource) => { this.put(key, resource); - this.notify?.(key, { status: 'success' }); + this.#notify?.(key, { status: 'success' }); }) .catch((reason) => { - this.notify?.(key, { status: 'failure', reason }); + this.#notify?.(key, { status: 'failure', reason }); }) .finally(() => { - this.pendingFetches.delete(key); - this.fetchToLimit(); + this.#pendingFetches.delete(key); + this.#fetchToLimit(); }); } - private fetchToLimit() { - let toFetch = Math.max(0, this.MAX_INFLIGHT_FETCHES - this.pendingFetches.size); + #fetchToLimit() { + let toFetch = Math.max(0, this.#MAX_INFLIGHT_FETCHES - this.#pendingFetches.size); for (let i = 0; i < toFetch; i++) { - const fetchMe = this.fetchPriority.popMinItemWithScore(); + const fetchMe = this.#fetchPriority.popMinItemWithScore(); if (fetchMe !== null) { if (fetchMe.score !== 0) { - this.beginFetch(fetchMe.item); + this.#beginFetch(fetchMe.item); } else { toFetch += 1; // increasing the loop limit inside the loop... a bit sketchy } @@ -177,18 +189,23 @@ export class AsyncPriorityCache extends PriorityCache { // it has a closure over data that changes over time, representing changing priorities // thus - the owner of this cache has a responsibility to notify the cache when significant // changes in priority occur! - override reprioritize(score: (k: CacheKey) => number) { + override reprioritize(score?: ScoreFn | undefined) { super.reprioritize(score); - this.fetchPriority.rebuild(negate(score)); - for (const [key, abort] of this.pendingFetches) { - if (score(key) === 0) { - abort.abort(); - this.pendingFetches.delete(key); + const sc = score ?? this.score; + this.#fetchPriority.rebuild(negate(sc)); + for (const [key, abort] of this.#pendingFetches) { + if (sc(key) === 0) { + abort.abort('deprioritized'); + this.#pendingFetches.delete(key); } } } + pending(key: CacheKey): boolean { + return this.#fetchPriority.hasItemWithKey(key) || this.#pendingFetches.has(key); + } + cachedOrPending(key: CacheKey): boolean { - return this.cached(key) || this.fetchPriority.hasItemWithKey(key) || this.pendingFetches.has(key); + return this.cached(key) || this.pending(key); } } diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index dae68eb3..280a1c3b 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -1,16 +1,22 @@ -import { PriorityCache, type Cacheable } from '@alleninstitute/vis-core'; +import { AsyncPriorityCache, type FetchResult, logger, type Cacheable } from '@alleninstitute/vis-core'; import * as zarr from 'zarrita'; import { WorkerPool } from './worker-pool'; import { FETCH_SLICE_MESSAGE_TYPE, isFetchSliceResponseMessage } from './fetch-slice.interface'; const DEFAULT_NUM_WORKERS = 6; const DEFAULT_MAX_DATA_CACHE_BYTES = 256 * 2 ** 10; // 256 MB -- aribtrarily chosen at this point +const DEFAULT_NUM_CONCURRENT_FETCHES = DEFAULT_NUM_WORKERS; // @TODO implement a much more context-aware cache size limiting mechanism const getDataCacheSizeLimit = () => { return DEFAULT_MAX_DATA_CACHE_BYTES; }; +// @TODO implement a much more context-aware cache size limiting mechanism +const getMaxConcurrentFetches = () => { + return DEFAULT_NUM_CONCURRENT_FETCHES; +}; + const asCacheKey = (key: zarr.AbsolutePath, range?: zarr.RangeQuery | undefined): string => { const keyStr = JSON.stringify(key); const rangeStr = range ? JSON.stringify(range) : 'no-range'; @@ -84,9 +90,22 @@ const copyToTransferableRequestInit = (req: RequestInit | undefined): Transferab export type CachingMultithreadedFetchStoreOptions = { maxBytes?: number | undefined; numWorkers?: number | undefined; + maxFetches?: number | undefined; fetchStoreOptions?: FetchStoreOptions | undefined; }; +type PromiseResolve = (t: T) => void; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +// biome-ignore lint/suspicious/noExplicitAny: This is aligned with the standard Promise API +type PromiseReject = (reason: any) => void; + +type PendingRequest = { + resolve: PromiseResolve; + reject: PromiseReject; + promise: Promise; +}; + export class CachingMultithreadedFetchStore extends zarr.FetchStore { /** * Maintains a pool of available worker threads. @@ -98,7 +117,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { * fetched. This data is stored in raw byte array form so that it * integrates properly with the Zarrita framework. */ - #dataCache: PriorityCache; + #dataCache: AsyncPriorityCache; /** * Maps cache keys to numeric times; the higher the time, the higher the priority. @@ -107,6 +126,8 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { */ #priorityMap: Map; + #pendingRequests: Map>; + /** * A callback form of the `score` function. */ @@ -115,16 +136,19 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { constructor(url: string | URL, options?: CachingMultithreadedFetchStoreOptions) { super(url, options?.fetchStoreOptions); this.#scoreFn = (h: CacheKey) => this.score(h); - this.#dataCache = new PriorityCache( + this.#dataCache = new AsyncPriorityCache( new Map(), this.#scoreFn, options?.maxBytes ?? getDataCacheSizeLimit(), + options?.maxFetches ?? getMaxConcurrentFetches(), + (key: CacheKey, result: FetchResult) => this.#dataReceived(key, result), ); this.#priorityMap = new Map(); this.#workerPool = new WorkerPool( options?.numWorkers ?? DEFAULT_NUM_WORKERS, new URL('./fetch-slice.worker.ts', import.meta.url), ); + this.#pendingRequests = new Map(); } protected score(key: CacheKey): number { @@ -140,31 +164,73 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { return new Uint8Array(cached.buffer()); } + #dataReceived(key: CacheKey, result: FetchResult) { + const pending = this.#pendingRequests.get(key); + if (pending === undefined) { + logger.warn('received data for unrecognized request'); + return; + } + if (result.status === 'failure') { + const reason = new Error('data retrieval failed:', { cause: result.reason }); + pending.reject(reason); + this.#pendingRequests.delete(key); + throw reason; + } + const cacheable = this.#dataCache.get(key); + if (cacheable === undefined) { + pending.resolve(undefined); + return; + } + pending.resolve(new Uint8Array(cacheable.buffer())); + } + async #doFetch( key: zarr.AbsolutePath, range: zarr.RangeQuery | undefined, options: TransferableRequestInit, + abort: AbortSignal | undefined, ): Promise { - const response = await this.#workerPool.submitRequest( - { - type: FETCH_SLICE_MESSAGE_TYPE, - rootUrl: this.url, - path: key, - range, - options, - }, - isFetchSliceResponseMessage, - [], - ); - if (response.payload === undefined) { - return undefined; - } - const arr = new Uint8Array(response.payload); - const cacheKey = asCacheKey(key, range); + const fetcher = async (signal: AbortSignal) => { + const response = await this.#workerPool.submitRequest( + { + type: FETCH_SLICE_MESSAGE_TYPE, + rootUrl: this.url, + path: key, + range, + options, + signal, + }, + isFetchSliceResponseMessage, + [], + ); + if (response.payload === undefined) { + throw new Error('data retrieval failed: resoonse payload was empty'); + } + const arr = new Uint8Array(response.payload); + return new CacheableByteArray(arr); + }; + const cacheKey = asCacheKey(key, range); this.#priorityMap.set(cacheKey, Date.now()); - this.#dataCache.put(cacheKey, new CacheableByteArray(arr)); - return arr; + + if (abort) { + abort.onabort = () => { + this.#priorityMap.set(cacheKey, 0); + this.#dataCache.reprioritize(); + }; + } + + const queued = this.#dataCache.enqueue(cacheKey, fetcher); + if (!queued) { + const pending = this.#pendingRequests.get(cacheKey); + if (pending === undefined) { + throw new Error('data cache did not queue request, but request was not found to be pending'); + } + return await pending.promise; + } + const { promise, resolve, reject } = Promise.withResolvers(); + this.#pendingRequests.set(cacheKey, { promise, resolve, reject }); + return await promise; } async get(key: zarr.AbsolutePath, options?: RequestInit): Promise { @@ -174,7 +240,8 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { return cached; } const workerOptions = copyToTransferableRequestInit(options); - return this.#doFetch(key, undefined, workerOptions); + const abort = options?.signal ?? undefined; + return this.#doFetch(key, undefined, workerOptions, abort); } async getRange( @@ -188,6 +255,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { return cached; } const workerOptions = copyToTransferableRequestInit(options); - return this.#doFetch(key, range, workerOptions); + const abort = options?.signal ?? undefined; + return this.#doFetch(key, range, workerOptions, abort); } } From 5ba1bede1751bf50f76486039dac3e625c0d959b Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 3 Oct 2025 12:31:29 -0700 Subject: [PATCH 12/91] Removed destroy() from CacheableByteArray --- packages/omezarr/src/zarr/cached-loading/store.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 280a1c3b..199b7432 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -29,24 +29,19 @@ type FetchStoreOptions = { }; class CacheableByteArray implements Cacheable { - #arr: Uint8Array | null; + #arr: Uint8Array; constructor(arr: Uint8Array) { this.#arr = arr; } - destroy() { - this.#arr = null; - } + destroy() {} sizeInBytes(): number { return this.#arr?.byteLength ?? 0; } buffer(): ArrayBufferLike { - if (this.#arr === null) { - throw new Error('cannot retrieve data buffer: array is null'); - } return this.#arr.buffer; } } From 481f699e78cd530c88917273e879d5f8c51b52d4 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 3 Oct 2025 14:47:32 -0700 Subject: [PATCH 13/91] More WIP --- packages/omezarr/src/zarr/omezarr-fileset.ts | 33 ++++++++++++++++++++ packages/omezarr/src/zarr/omezarr-loader.ts | 14 --------- packages/omezarr/src/zarr/types.ts | 1 + 3 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 packages/omezarr/src/zarr/omezarr-fileset.ts delete mode 100644 packages/omezarr/src/zarr/omezarr-loader.ts diff --git a/packages/omezarr/src/zarr/omezarr-fileset.ts b/packages/omezarr/src/zarr/omezarr-fileset.ts new file mode 100644 index 00000000..2478569b --- /dev/null +++ b/packages/omezarr/src/zarr/omezarr-fileset.ts @@ -0,0 +1,33 @@ +import * as zarr from 'zarrita'; +import { z } from 'zod'; +import { OmeZarrArrayMetadata, OmeZarrAttrs, OmeZarrAttrsSchema, OmeZarrMetadata } from './types'; +import { CachingMultithreadedFetchStore } from "./cached-loading/store"; +import { logger } from '@alleninstitute/vis-core'; + +export class OmeZarrFileset { + #store: CachingMultithreadedFetchStore; + #root: zarr.Location; + #rootAttrs: OmeZarrAttrs | undefined; + #arrayAttrs: Map + + constructor(url: string | URL) { + this.#store = new CachingMultithreadedFetchStore(url); + this.#root = zarr.root(this.#store); + } + + async #loadRootAttrs(store: zarr.FetchStore): Promise { + const group = await zarr.open(store, { kind: 'group' }); + try { + return OmeZarrAttrsSchema.parse(group.attrs); + } catch (e) { + if (e instanceof z.ZodError) { + logger.error('could not load Zarr file: parsing failed'); + } + throw e; + } + } + + loadMetadata(): Promise { + + } +} \ No newline at end of file diff --git a/packages/omezarr/src/zarr/omezarr-loader.ts b/packages/omezarr/src/zarr/omezarr-loader.ts deleted file mode 100644 index f01d4626..00000000 --- a/packages/omezarr/src/zarr/omezarr-loader.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { OmeZarrMetadata } -import { CachingMultithreadedFetchStore } from "./cached-loading/store"; - -export class OmeZarrLoader { - #store: CachingMultithreadedFetchStore; - - constructor(url: string | URL) { - this.#store = new CachingMultithreadedFetchStore(url); - } - - loadMetadata(): Promise { - - } -} \ No newline at end of file diff --git a/packages/omezarr/src/zarr/types.ts b/packages/omezarr/src/zarr/types.ts index 9fe58875..58d26c02 100644 --- a/packages/omezarr/src/zarr/types.ts +++ b/packages/omezarr/src/zarr/types.ts @@ -4,6 +4,7 @@ import { logger, makeRGBAColorVector } from '@alleninstitute/vis-core'; import { z } from 'zod'; export type ZarrDimension = 't' | 'c' | 'z' | 'y' | 'x'; +export type OmeZarrDimension = ZarrDimension; // these dimension indices are given for a 4-element shape array const SHAPE_Z_DIM_INDEX = 1; From 46098bf0a80b58b6964cd037395ee91dc54182f0 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 3 Oct 2025 16:00:14 -0700 Subject: [PATCH 14/91] Returned to PriorityCache over AsyncPriorityCache --- .../omezarr/src/zarr/cached-loading/store.ts | 125 ++++++++---------- 1 file changed, 56 insertions(+), 69 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 199b7432..95bf9112 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -1,23 +1,17 @@ -import { AsyncPriorityCache, type FetchResult, logger, type Cacheable } from '@alleninstitute/vis-core'; +import { type Cacheable, PriorityCache } from '@alleninstitute/vis-core'; import * as zarr from 'zarrita'; import { WorkerPool } from './worker-pool'; -import { FETCH_SLICE_MESSAGE_TYPE, isFetchSliceResponseMessage } from './fetch-slice.interface'; +import { FETCH_SLICE_MESSAGE_TYPE, type FetchSliceResponseMessage, isFetchSliceResponseMessage } from './fetch-slice.interface'; const DEFAULT_NUM_WORKERS = 6; const DEFAULT_MAX_DATA_CACHE_BYTES = 256 * 2 ** 10; // 256 MB -- aribtrarily chosen at this point -const DEFAULT_NUM_CONCURRENT_FETCHES = DEFAULT_NUM_WORKERS; // @TODO implement a much more context-aware cache size limiting mechanism const getDataCacheSizeLimit = () => { return DEFAULT_MAX_DATA_CACHE_BYTES; }; -// @TODO implement a much more context-aware cache size limiting mechanism -const getMaxConcurrentFetches = () => { - return DEFAULT_NUM_CONCURRENT_FETCHES; -}; - -const asCacheKey = (key: zarr.AbsolutePath, range?: zarr.RangeQuery | undefined): string => { +export const asCacheKey = (key: zarr.AbsolutePath, range?: zarr.RangeQuery | undefined): string => { const keyStr = JSON.stringify(key); const rangeStr = range ? JSON.stringify(range) : 'no-range'; return `${keyStr} ${rangeStr}`; @@ -112,15 +106,18 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { * fetched. This data is stored in raw byte array form so that it * integrates properly with the Zarrita framework. */ - #dataCache: AsyncPriorityCache; + #dataCache: PriorityCache; /** * Maps cache keys to numeric times; the higher the time, the higher the priority. * * This effectively means that more frequently-requested items will be kept longer. */ - #priorityMap: Map; + #priorityByTimestamp: Map; + /** + * Stores in-progress requests that have not yet resolved. + */ #pendingRequests: Map>; /** @@ -131,14 +128,12 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { constructor(url: string | URL, options?: CachingMultithreadedFetchStoreOptions) { super(url, options?.fetchStoreOptions); this.#scoreFn = (h: CacheKey) => this.score(h); - this.#dataCache = new AsyncPriorityCache( + this.#dataCache = new PriorityCache( new Map(), this.#scoreFn, options?.maxBytes ?? getDataCacheSizeLimit(), - options?.maxFetches ?? getMaxConcurrentFetches(), - (key: CacheKey, result: FetchResult) => this.#dataReceived(key, result), ); - this.#priorityMap = new Map(); + this.#priorityByTimestamp = new Map(); this.#workerPool = new WorkerPool( options?.numWorkers ?? DEFAULT_NUM_WORKERS, new URL('./fetch-slice.worker.ts', import.meta.url), @@ -147,7 +142,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { } protected score(key: CacheKey): number { - return this.#priorityMap.get(key) ?? 0; + return this.#priorityByTimestamp.get(key) ?? 0; } #fromCache(cacheKey: CacheKey): Uint8Array | undefined { @@ -155,77 +150,67 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { if (cached === undefined) { return undefined; } - this.#priorityMap.set(cacheKey, Date.now()); + this.#priorityByTimestamp.set(cacheKey, Date.now()); return new Uint8Array(cached.buffer()); } - #dataReceived(key: CacheKey, result: FetchResult) { - const pending = this.#pendingRequests.get(key); - if (pending === undefined) { - logger.warn('received data for unrecognized request'); - return; - } - if (result.status === 'failure') { - const reason = new Error('data retrieval failed:', { cause: result.reason }); - pending.reject(reason); - this.#pendingRequests.delete(key); - throw reason; - } - const cacheable = this.#dataCache.get(key); - if (cacheable === undefined) { - pending.resolve(undefined); - return; - } - pending.resolve(new Uint8Array(cacheable.buffer())); - } - async #doFetch( key: zarr.AbsolutePath, range: zarr.RangeQuery | undefined, options: TransferableRequestInit, abort: AbortSignal | undefined, ): Promise { - const fetcher = async (signal: AbortSignal) => { - const response = await this.#workerPool.submitRequest( - { - type: FETCH_SLICE_MESSAGE_TYPE, - rootUrl: this.url, - path: key, - range, - options, - signal, - }, - isFetchSliceResponseMessage, - [], - ); - if (response.payload === undefined) { - throw new Error('data retrieval failed: resoonse payload was empty'); - } - const arr = new Uint8Array(response.payload); - return new CacheableByteArray(arr); - }; - + const cacheKey = asCacheKey(key, range); - this.#priorityMap.set(cacheKey, Date.now()); + this.#priorityByTimestamp.set(cacheKey, Date.now()); + this.#dataCache.reprioritize(); + + const pending = this.#pendingRequests.get(cacheKey); + if (pending !== undefined) { + return pending.promise; + } + + const { promise, resolve, reject } = Promise.withResolvers(); + + this.#pendingRequests.set(cacheKey, { promise, resolve, reject }); + if (abort) { abort.onabort = () => { - this.#priorityMap.set(cacheKey, 0); + this.#priorityByTimestamp.set(cacheKey, 0); this.#dataCache.reprioritize(); }; } - const queued = this.#dataCache.enqueue(cacheKey, fetcher); - if (!queued) { - const pending = this.#pendingRequests.get(cacheKey); - if (pending === undefined) { - throw new Error('data cache did not queue request, but request was not found to be pending'); + const request = this.#workerPool.submitRequest( + { + type: FETCH_SLICE_MESSAGE_TYPE, + rootUrl: this.url, + path: key, + range, + options, + abort, + }, + isFetchSliceResponseMessage, + [], + ); + + request.then((response: FetchSliceResponseMessage) => { + const payload = response.payload; + if (payload === undefined) { + resolve(undefined); + return; } - return await pending.promise; - } - const { promise, resolve, reject } = Promise.withResolvers(); - this.#pendingRequests.set(cacheKey, { promise, resolve, reject }); - return await promise; + const arr = new Uint8Array(payload); + this.#dataCache.put(cacheKey, new CacheableByteArray(arr)); + resolve(arr); + }).catch((e) => { + reject(e); + }).finally(() => { + this.#pendingRequests.delete(cacheKey); + }); + + return promise; } async get(key: zarr.AbsolutePath, options?: RequestInit): Promise { @@ -234,6 +219,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { if (cached !== undefined) { return cached; } + const workerOptions = copyToTransferableRequestInit(options); const abort = options?.signal ?? undefined; return this.#doFetch(key, undefined, workerOptions, abort); @@ -249,6 +235,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { if (cached !== undefined) { return cached; } + const workerOptions = copyToTransferableRequestInit(options); const abort = options?.signal ?? undefined; return this.#doFetch(key, range, workerOptions, abort); From c56789a82b8890516daf0ca9d8c1fe6a194ad92f Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 3 Oct 2025 16:00:45 -0700 Subject: [PATCH 15/91] Fmt fixes --- .../omezarr/src/zarr/cached-loading/store.ts | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 95bf9112..7c88d20d 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -1,7 +1,11 @@ import { type Cacheable, PriorityCache } from '@alleninstitute/vis-core'; import * as zarr from 'zarrita'; import { WorkerPool } from './worker-pool'; -import { FETCH_SLICE_MESSAGE_TYPE, type FetchSliceResponseMessage, isFetchSliceResponseMessage } from './fetch-slice.interface'; +import { + FETCH_SLICE_MESSAGE_TYPE, + type FetchSliceResponseMessage, + isFetchSliceResponseMessage, +} from './fetch-slice.interface'; const DEFAULT_NUM_WORKERS = 6; const DEFAULT_MAX_DATA_CACHE_BYTES = 256 * 2 ** 10; // 256 MB -- aribtrarily chosen at this point @@ -160,7 +164,6 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { options: TransferableRequestInit, abort: AbortSignal | undefined, ): Promise { - const cacheKey = asCacheKey(key, range); this.#priorityByTimestamp.set(cacheKey, Date.now()); @@ -174,7 +177,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { const { promise, resolve, reject } = Promise.withResolvers(); this.#pendingRequests.set(cacheKey, { promise, resolve, reject }); - + if (abort) { abort.onabort = () => { this.#priorityByTimestamp.set(cacheKey, 0); @@ -195,20 +198,23 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { [], ); - request.then((response: FetchSliceResponseMessage) => { - const payload = response.payload; - if (payload === undefined) { - resolve(undefined); - return; - } - const arr = new Uint8Array(payload); - this.#dataCache.put(cacheKey, new CacheableByteArray(arr)); - resolve(arr); - }).catch((e) => { - reject(e); - }).finally(() => { - this.#pendingRequests.delete(cacheKey); - }); + request + .then((response: FetchSliceResponseMessage) => { + const payload = response.payload; + if (payload === undefined) { + resolve(undefined); + return; + } + const arr = new Uint8Array(payload); + this.#dataCache.put(cacheKey, new CacheableByteArray(arr)); + resolve(arr); + }) + .catch((e) => { + reject(e); + }) + .finally(() => { + this.#pendingRequests.delete(cacheKey); + }); return promise; } @@ -235,7 +241,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { if (cached !== undefined) { return cached; } - + const workerOptions = copyToTransferableRequestInit(options); const abort = options?.signal ?? undefined; return this.#doFetch(key, range, workerOptions, abort); From a829891bc80aa9790b08e3bcd11d6f8d91bc69fb Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Mon, 6 Oct 2025 08:48:08 -0700 Subject: [PATCH 16/91] Added request tracking so aborting won't work if the same chunk is requested multiple times --- .../omezarr/src/zarr/cached-loading/store.ts | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 7c88d20d..5c139f2b 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -1,4 +1,4 @@ -import { type Cacheable, PriorityCache } from '@alleninstitute/vis-core'; +import { type Cacheable, logger, PriorityCache } from '@alleninstitute/vis-core'; import * as zarr from 'zarrita'; import { WorkerPool } from './worker-pool'; import { @@ -124,6 +124,16 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { */ #pendingRequests: Map>; + /** + * Stores one instance of a cache key for each time that cache key was requested, + * removing them all once that particular request is fulfilled. This allows us to + * keep track of whether or not it is safe to abort a pending request: as long as + * there are at least 2 instances of the same cache key in this array, then that + * means multiple requestors are waiting on a particular piece of data, and it is + * not safe to abort that request. + */ + #pendingRequestKeyCounts: Map; + /** * A callback form of the `score` function. */ @@ -143,6 +153,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { new URL('./fetch-slice.worker.ts', import.meta.url), ); this.#pendingRequests = new Map(); + this.#pendingRequestKeyCounts = new Map(); } protected score(key: CacheKey): number { @@ -158,6 +169,28 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { return new Uint8Array(cached.buffer()); } + #incrementKeyCount(cacheKey: CacheKey): number { + const count = this.#pendingRequestKeyCounts.get(cacheKey); + const newCount = count !== undefined ? count + 1 : 1; + this.#pendingRequestKeyCounts.set(cacheKey, newCount); + return newCount; + } + + #decrementKeyCount(cacheKey: CacheKey): number { + const count = this.#pendingRequestKeyCounts.get(cacheKey); + if (count === undefined) { + logger.warn('attempted to decrement a non-existent request key'); + return 0; + } + if (count <= 1) { + this.#pendingRequestKeyCounts.delete(cacheKey); + return 0; + } + const newCount = count - 1; + this.#pendingRequestKeyCounts.set(cacheKey, newCount); + return newCount; + } + async #doFetch( key: zarr.AbsolutePath, range: zarr.RangeQuery | undefined, @@ -169,6 +202,8 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { this.#priorityByTimestamp.set(cacheKey, Date.now()); this.#dataCache.reprioritize(); + this.#incrementKeyCount(cacheKey); + const pending = this.#pendingRequests.get(cacheKey); if (pending !== undefined) { return pending.promise; @@ -180,8 +215,11 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { if (abort) { abort.onabort = () => { - this.#priorityByTimestamp.set(cacheKey, 0); - this.#dataCache.reprioritize(); + const count = this.#decrementKeyCount(cacheKey); + if (count === 0) { + this.#priorityByTimestamp.set(cacheKey, 0); + this.#dataCache.reprioritize(); + } }; } @@ -214,6 +252,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { }) .finally(() => { this.#pendingRequests.delete(cacheKey); + this.#pendingRequestKeyCounts.delete(cacheKey); }); return promise; From 352cb5f735776ea4f9ccd2a7d223d89632fdaf20 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Mon, 6 Oct 2025 08:49:09 -0700 Subject: [PATCH 17/91] Fmt fixes --- packages/omezarr/src/zarr/cached-loading/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 5c139f2b..4775e5ab 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -176,7 +176,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { return newCount; } - #decrementKeyCount(cacheKey: CacheKey): number { + #decrementKeyCount(cacheKey: CacheKey): number { const count = this.#pendingRequestKeyCounts.get(cacheKey); if (count === undefined) { logger.warn('attempted to decrement a non-existent request key'); From 8d1053a75aad8a2ba801f956ddfcde2f884e1dbf Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Mon, 6 Oct 2025 11:44:34 -0700 Subject: [PATCH 18/91] Moved the WorkerMessage types to core --- packages/core/package.json | 3 ++- packages/core/src/index.ts | 2 ++ packages/core/src/workers/messages.ts | 25 +++++++++++++++++++ .../cached-loading/fetch-slice.interface.ts | 24 ------------------ .../src/zarr/cached-loading/worker-pool.ts | 2 +- pnpm-lock.yaml | 3 +++ 6 files changed, 33 insertions(+), 26 deletions(-) create mode 100644 packages/core/src/workers/messages.ts diff --git a/packages/core/package.json b/packages/core/package.json index d1350717..86a4ad58 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -56,7 +56,8 @@ "dependencies": { "@alleninstitute/vis-geometry": "workspace:*", "lodash": "4.17.21", - "regl": "2.1.0" + "regl": "2.1.0", + "zod": "4.1.11" }, "packageManager": "pnpm@9.14.2" } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 558188d7..f0310928 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -22,3 +22,5 @@ export { RenderServer } from './abstract/render-server'; export { Logger, logger } from './logger'; export { PriorityCache, AsyncPriorityCache, type Cacheable } from './shared-priority-cache/priority-cache'; export { SharedPriorityCache } from './shared-priority-cache/shared-cache'; + +export { type WorkerMessage, type WorkerMessageWithId, isWorkerMessage, isWorkerMessageWithId } from './workers/messages'; diff --git a/packages/core/src/workers/messages.ts b/packages/core/src/workers/messages.ts new file mode 100644 index 00000000..92575097 --- /dev/null +++ b/packages/core/src/workers/messages.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +export type WorkerMessage = { + type: string; +}; + +export type WorkerMessageWithId = WorkerMessage & { + id: string; +}; + +const WorkerMessageSchema = z.object({ + type: z.string(), +}); + +const WorkerMessageWithIdSchema = WorkerMessageSchema.extend({ + id: z.string().nonempty(), +}); + +export function isWorkerMessage(val: unknown): val is WorkerMessage { + return WorkerMessageSchema.safeParse(val).success; +} + +export function isWorkerMessageWithId(val: unknown): val is WorkerMessageWithId { + return WorkerMessageWithIdSchema.safeParse(val).success; +} diff --git a/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts b/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts index c5eb5cde..203d4b2f 100644 --- a/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts +++ b/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts @@ -13,30 +13,6 @@ export type FetchSliceMessagePayload = { options?: TransferrableRequestInit | undefined; }; -export type WorkerMessage = { - type: string; -}; - -export type WorkerMessageWithId = WorkerMessage & { - id: string; -}; - -const WorkerMessageSchema = z.object({ - type: z.string(), -}); - -const WorkerMessageWithIdSchema = WorkerMessageSchema.extend({ - id: z.string().nonempty(), -}); - -export function isWorkerMessage(val: unknown): val is WorkerMessage { - return WorkerMessageSchema.safeParse(val).success; -} - -export function isWorkerMessageWithId(val: unknown): val is WorkerMessageWithId { - return WorkerMessageWithIdSchema.safeParse(val).success; -} - export const FETCH_SLICE_MESSAGE_TYPE = 'fetch-slice' as const; export const FETCH_SLICE_RESPOSNE_MESSAGE_TYPE = 'fetch-slice-response' as const; export const CANCEL_MESSAGE_TYPE = 'cancel' as const; diff --git a/packages/omezarr/src/zarr/cached-loading/worker-pool.ts b/packages/omezarr/src/zarr/cached-loading/worker-pool.ts index a7e96268..ab4d3b61 100644 --- a/packages/omezarr/src/zarr/cached-loading/worker-pool.ts +++ b/packages/omezarr/src/zarr/cached-loading/worker-pool.ts @@ -1,4 +1,4 @@ -import { isWorkerMessageWithId, type WorkerMessage, type WorkerMessageWithId } from './fetch-slice.interface'; +import { isWorkerMessageWithId, type WorkerMessage, type WorkerMessageWithId } from '@alleninstitute/vis-core'; import { v4 as uuidv4 } from 'uuid'; import { logger } from '@alleninstitute/vis-core'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32212ad0..c7a0952c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: regl: specifier: 2.1.0 version: 2.1.0 + zod: + specifier: 4.1.11 + version: 4.1.11 devDependencies: '@types/lodash': specifier: 4.17.20 From 18e4939bb76fb532bd7b98910dcd71165411283e Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Mon, 6 Oct 2025 11:45:10 -0700 Subject: [PATCH 19/91] Fmt fixes --- packages/core/src/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f0310928..f7a6a726 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -23,4 +23,9 @@ export { Logger, logger } from './logger'; export { PriorityCache, AsyncPriorityCache, type Cacheable } from './shared-priority-cache/priority-cache'; export { SharedPriorityCache } from './shared-priority-cache/shared-cache'; -export { type WorkerMessage, type WorkerMessageWithId, isWorkerMessage, isWorkerMessageWithId } from './workers/messages'; +export { + type WorkerMessage, + type WorkerMessageWithId, + isWorkerMessage, + isWorkerMessageWithId, +} from './workers/messages'; From dbf2d9ea81a6a72a7b036cab41276049d1e60c1a Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Mon, 6 Oct 2025 12:03:27 -0700 Subject: [PATCH 20/91] Removed unnecessary waste in cached object handling --- packages/omezarr/src/zarr/cached-loading/store.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 4775e5ab..ace9bd4b 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -36,11 +36,11 @@ class CacheableByteArray implements Cacheable { destroy() {} sizeInBytes(): number { - return this.#arr?.byteLength ?? 0; + return this.#arr.byteLength; } - buffer(): ArrayBufferLike { - return this.#arr.buffer; + get array(): Uint8Array { + return this.#arr; } } @@ -166,7 +166,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { return undefined; } this.#priorityByTimestamp.set(cacheKey, Date.now()); - return new Uint8Array(cached.buffer()); + return cached.array; } #incrementKeyCount(cacheKey: CacheKey): number { From 57dcb6dad427ae365d3a730b4702d220ff0593dc Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Mon, 6 Oct 2025 14:09:40 -0700 Subject: [PATCH 21/91] Added some robustness to error management --- packages/core/src/workers/messages.ts | 13 +++++++++++-- .../omezarr/src/zarr/cached-loading/worker-pool.ts | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/core/src/workers/messages.ts b/packages/core/src/workers/messages.ts index 92575097..6cb6eb20 100644 --- a/packages/core/src/workers/messages.ts +++ b/packages/core/src/workers/messages.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { logger } from '../logger'; export type WorkerMessage = { type: string; @@ -17,9 +18,17 @@ const WorkerMessageWithIdSchema = WorkerMessageSchema.extend({ }); export function isWorkerMessage(val: unknown): val is WorkerMessage { - return WorkerMessageSchema.safeParse(val).success; + const { success, error } = WorkerMessageSchema.safeParse(val); + if (error) { + logger.error('parsing WorkerMessage failed', error); + } + return success; } export function isWorkerMessageWithId(val: unknown): val is WorkerMessageWithId { - return WorkerMessageWithIdSchema.safeParse(val).success; + const { success, error } = WorkerMessageWithIdSchema.safeParse(val); + if (error) { + logger.error('parsing WorkerMessageWithId failed', error); + } + return success; } diff --git a/packages/omezarr/src/zarr/cached-loading/worker-pool.ts b/packages/omezarr/src/zarr/cached-loading/worker-pool.ts index ab4d3b61..7ac1f2ea 100644 --- a/packages/omezarr/src/zarr/cached-loading/worker-pool.ts +++ b/packages/omezarr/src/zarr/cached-loading/worker-pool.ts @@ -45,10 +45,14 @@ export class WorkerPool { return; } if (!messagePromise.validator(data)) { - logger.error('invalid response from worker: message type did not match expected type'); + const reason = 'invalid response from worker: message type did not match expected type' + logger.error(reason); + messagePromise.reject(new Error(reason)); return; } messagePromise.resolve(data); + } else { + logger.error('encountered an invalid message; skipping'); } } From abd86602f43fd71e590391af418b71c3db412a61 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Mon, 6 Oct 2025 14:36:46 -0700 Subject: [PATCH 22/91] Made the WorkerPool a little more robust --- .../src/zarr/cached-loading/worker-pool.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/worker-pool.ts b/packages/omezarr/src/zarr/cached-loading/worker-pool.ts index 7ac1f2ea..3b540d35 100644 --- a/packages/omezarr/src/zarr/cached-loading/worker-pool.ts +++ b/packages/omezarr/src/zarr/cached-loading/worker-pool.ts @@ -1,6 +1,5 @@ -import { isWorkerMessageWithId, type WorkerMessage, type WorkerMessageWithId } from '@alleninstitute/vis-core'; import { v4 as uuidv4 } from 'uuid'; -import { logger } from '@alleninstitute/vis-core'; +import { logger, isWorkerMessageWithId, type WorkerMessage, type WorkerMessageWithId } from '@alleninstitute/vis-core'; type PromiseResolve = (t: T) => void; @@ -21,38 +20,39 @@ type MessagePromise = { export class WorkerPool { #workers: Worker[]; - #promises: Map>; + #promises: Map>; #which: number; constructor(size: number, workerModule: URL) { this.#workers = new Array(size); for (let i = 0; i < size; i++) { this.#workers[i] = new Worker(workerModule, { type: 'module' }); - this.#workers[i].onmessage = (msg) => this.#handleResponse(msg); + this.#workers[i].onmessage = (msg) => this.#handleResponse(i, msg); } this.#promises = new Map(); this.#which = 0; } - #handleResponse(msg: MessageEvent) { + #handleResponse(workerIndex: number, msg: MessageEvent) { const { data } = msg; + const messagePromise = this.#promises.get(workerIndex); + if (messagePromise === undefined) { + logger.warn('unexpected message from worker'); + return; + } if (isWorkerMessageWithId(data)) { - const { id } = data; - const messagePromise = this.#promises.get(id); - this.#promises.delete(id); - if (messagePromise === undefined) { - logger.warn('unexpected message from worker'); - return; - } + this.#promises.delete(workerIndex); if (!messagePromise.validator(data)) { - const reason = 'invalid response from worker: message type did not match expected type' + const reason = 'invalid response from worker: message type did not match expected type'; logger.error(reason); messagePromise.reject(new Error(reason)); return; } messagePromise.resolve(data); } else { - logger.error('encountered an invalid message; skipping'); + const reason = 'encountered an invalid message; skipping'; + logger.error(reason); + messagePromise.reject(new Error(reason)); } } @@ -72,7 +72,7 @@ export class WorkerPool { const messagePromise = this.#createMessagePromise(responseValidator); // TODO this cast is very annoying; would be nice to remove it - this.#promises.set(reqId, messagePromise as unknown as MessagePromise); + this.#promises.set(workerIndex, messagePromise as unknown as MessagePromise); if (signal) { signal.onabort = () => { From 1512cdca684e41884d3580f0bd7411041b2965a7 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 08:26:27 -0700 Subject: [PATCH 23/91] Moved WorkerPool to Core --- packages/core/package.json | 1 + .../zarr/cached-loading => core/src/workers}/worker-pool.ts | 3 ++- packages/omezarr/package.json | 1 - pnpm-lock.yaml | 6 +++--- 4 files changed, 6 insertions(+), 5 deletions(-) rename packages/{omezarr/src/zarr/cached-loading => core/src/workers}/worker-pool.ts (96%) diff --git a/packages/core/package.json b/packages/core/package.json index 86a4ad58..b19176f3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -57,6 +57,7 @@ "@alleninstitute/vis-geometry": "workspace:*", "lodash": "4.17.21", "regl": "2.1.0", + "uuid": "13.0.0", "zod": "4.1.11" }, "packageManager": "pnpm@9.14.2" diff --git a/packages/omezarr/src/zarr/cached-loading/worker-pool.ts b/packages/core/src/workers/worker-pool.ts similarity index 96% rename from packages/omezarr/src/zarr/cached-loading/worker-pool.ts rename to packages/core/src/workers/worker-pool.ts index 3b540d35..2d926076 100644 --- a/packages/omezarr/src/zarr/cached-loading/worker-pool.ts +++ b/packages/core/src/workers/worker-pool.ts @@ -1,5 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; -import { logger, isWorkerMessageWithId, type WorkerMessage, type WorkerMessageWithId } from '@alleninstitute/vis-core'; +import { logger } from '../logger'; +import { isWorkerMessageWithId, type WorkerMessage, type WorkerMessageWithId } from './messages'; type PromiseResolve = (t: T) => void; diff --git a/packages/omezarr/package.json b/packages/omezarr/package.json index 4996116e..0d9fe235 100644 --- a/packages/omezarr/package.json +++ b/packages/omezarr/package.json @@ -51,7 +51,6 @@ "@alleninstitute/vis-core": "workspace:*", "@alleninstitute/vis-geometry": "workspace:*", "regl": "2.1.0", - "uuid": "13.0.0", "zarrita": "0.5.3", "zod": "4.1.11" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7a0952c..508cd83d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: regl: specifier: 2.1.0 version: 2.1.0 + uuid: + specifier: 13.0.0 + version: 13.0.0 zod: specifier: 4.1.11 version: 4.1.11 @@ -83,9 +86,6 @@ importers: regl: specifier: 2.1.0 version: 2.1.0 - uuid: - specifier: 13.0.0 - version: 13.0.0 zarrita: specifier: 0.5.3 version: 0.5.3 From 58b9ce1d7e8cad07eb4bdf9b2fae4aa8c5c06826 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 08:39:36 -0700 Subject: [PATCH 24/91] Finished moving WorkerPool to Core --- packages/core/src/index.ts | 2 ++ packages/core/tsconfig.json | 6 +++--- packages/omezarr/src/zarr/cached-loading/store.ts | 7 +++---- packages/omezarr/tsconfig.json | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f7a6a726..507a5ba4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,3 +29,5 @@ export { isWorkerMessage, isWorkerMessageWithId, } from './workers/messages'; + +export { WorkerPool } from './workers/worker-pool'; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index baaee9ee..d8a6412f 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -5,9 +5,9 @@ "~/*": ["./*"] }, "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["es2022", "DOM"] + "module": "es6", + "target": "es2024", + "lib": ["es2024", "DOM"] }, "include": ["./src/index.ts"] } diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index ace9bd4b..ca71c782 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -1,6 +1,5 @@ -import { type Cacheable, logger, PriorityCache } from '@alleninstitute/vis-core'; +import { type Cacheable, logger, PriorityCache, WorkerPool } from '@alleninstitute/vis-core'; import * as zarr from 'zarrita'; -import { WorkerPool } from './worker-pool'; import { FETCH_SLICE_MESSAGE_TYPE, type FetchSliceResponseMessage, @@ -51,7 +50,7 @@ type TransferableRequestInit = Omit headers?: Record; }; -const copyHeaders = (headers: RequestInit['headers']): Record | undefined => { +const copyToTransferableHeaders = (headers: RequestInit['headers']): Record | undefined => { if (Array.isArray(headers)) { const result: Record = {}; headers.forEach(([key, val]) => { @@ -77,7 +76,7 @@ const copyToTransferableRequestInit = (req: RequestInit | undefined): Transferab const updReq = { ...req }; delete updReq.signal; delete updReq.window; - return { ...updReq, body: req.body?.toString(), headers: copyHeaders(req.headers) }; + return { ...updReq, body: req.body?.toString(), headers: copyToTransferableHeaders(req.headers) }; }; export type CachingMultithreadedFetchStoreOptions = { diff --git a/packages/omezarr/tsconfig.json b/packages/omezarr/tsconfig.json index baaee9ee..d8a6412f 100644 --- a/packages/omezarr/tsconfig.json +++ b/packages/omezarr/tsconfig.json @@ -5,9 +5,9 @@ "~/*": ["./*"] }, "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["es2022", "DOM"] + "module": "es6", + "target": "es2024", + "lib": ["es2024", "DOM"] }, "include": ["./src/index.ts"] } From 5495aa0718a1ccc752779178c4f70b375803be3f Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 10:52:31 -0700 Subject: [PATCH 25/91] Added worker status tracking. Added demo page. --- packages/core/src/index.ts | 1 + packages/core/src/workers/messages.ts | 26 +++++- packages/core/src/workers/worker-pool.ts | 47 ++++++++++- packages/dzi/tsconfig.json | 6 +- packages/geometry/tsconfig.json | 6 +- .../zarr/cached-loading/fetch-slice.worker.ts | 8 +- .../content/docs/examples/multithreading.mdx | 8 ++ .../multithreading/multithreading-demo.tsx | 80 +++++++++++++++++++ .../multithreading/multithreading.worker.ts | 9 +++ 9 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 site/src/content/docs/examples/multithreading.mdx create mode 100644 site/src/examples/multithreading/multithreading-demo.tsx create mode 100644 site/src/examples/multithreading/multithreading.worker.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 507a5ba4..bcb7da12 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -28,6 +28,7 @@ export { type WorkerMessageWithId, isWorkerMessage, isWorkerMessageWithId, + HEARTBEAT_RATE_MS, } from './workers/messages'; export { WorkerPool } from './workers/worker-pool'; diff --git a/packages/core/src/workers/messages.ts b/packages/core/src/workers/messages.ts index 6cb6eb20..bb9b96a7 100644 --- a/packages/core/src/workers/messages.ts +++ b/packages/core/src/workers/messages.ts @@ -9,18 +9,18 @@ export type WorkerMessageWithId = WorkerMessage & { id: string; }; -const WorkerMessageSchema = z.object({ +export const WorkerMessageSchema = z.object({ type: z.string(), }); -const WorkerMessageWithIdSchema = WorkerMessageSchema.extend({ +export const WorkerMessageWithIdSchema = WorkerMessageSchema.extend({ id: z.string().nonempty(), }); export function isWorkerMessage(val: unknown): val is WorkerMessage { const { success, error } = WorkerMessageSchema.safeParse(val); if (error) { - logger.error('parsing WorkerMessage failed', error); + logger.warn('parsing WorkerMessage failed', error); } return success; } @@ -28,7 +28,25 @@ export function isWorkerMessage(val: unknown): val is WorkerMessage { export function isWorkerMessageWithId(val: unknown): val is WorkerMessageWithId { const { success, error } = WorkerMessageWithIdSchema.safeParse(val); if (error) { - logger.error('parsing WorkerMessageWithId failed', error); + logger.warn('parsing WorkerMessageWithId failed', error); } return success; } + +export type HeartbeatMessage = { + type: 'heartbeat'; +}; + +export const HeartbeatMessageSchema = z.object({ + type: z.literal('heartbeat'), +}); + +export function isHeartbeatMessage(val: unknown): val is HeartbeatMessage { + const { success, error } = HeartbeatMessageSchema.safeParse(val); + if (error) { + logger.warn('parsing WorkerMessageWithId failed', error); + } + return success; +} + +export const HEARTBEAT_RATE_MS = 500; diff --git a/packages/core/src/workers/worker-pool.ts b/packages/core/src/workers/worker-pool.ts index 2d926076..660131d0 100644 --- a/packages/core/src/workers/worker-pool.ts +++ b/packages/core/src/workers/worker-pool.ts @@ -1,6 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; import { logger } from '../logger'; -import { isWorkerMessageWithId, type WorkerMessage, type WorkerMessageWithId } from './messages'; +import { isHeartbeatMessage, isWorkerMessageWithId, type WorkerMessage, type WorkerMessageWithId } from './messages'; type PromiseResolve = (t: T) => void; @@ -19,24 +19,51 @@ type MessagePromise = { promise: Promise; }; +export enum WorkerStatus { + Available = 'Available', + Working = 'Working', + Unresponsive = 'Unresponsive', +} + export class WorkerPool { #workers: Worker[]; #promises: Map>; + #statuses: Map; + #timeOfPreviousHeartbeat: Map; #which: number; constructor(size: number, workerModule: URL) { this.#workers = new Array(size); + this.#timeOfPreviousHeartbeat = new Map(); + this.#statuses = new Map(); for (let i = 0; i < size; i++) { this.#workers[i] = new Worker(workerModule, { type: 'module' }); - this.#workers[i].onmessage = (msg) => this.#handleResponse(i, msg); + this.#workers[i].onmessage = (msg) => this.#handleMessage(i, msg); + this.#timeOfPreviousHeartbeat.set(i, Date.now()); + this.#statuses.set(i, WorkerStatus.Available); + setInterval(() => { + const delta = Date.now() - (this.#timeOfPreviousHeartbeat.get(i) ?? 0); + if (delta && delta > 1500) { + this.#statuses.set(i, WorkerStatus.Unresponsive); + } + }, 2000); } this.#promises = new Map(); this.#which = 0; } - #handleResponse(workerIndex: number, msg: MessageEvent) { + #handleMessage(workerIndex: number, msg: MessageEvent) { const { data } = msg; const messagePromise = this.#promises.get(workerIndex); + if (isHeartbeatMessage(data)) { + this.#timeOfPreviousHeartbeat.set(workerIndex, Date.now()); + if (messagePromise === undefined) { + this.#statuses.set(workerIndex, WorkerStatus.Available); + } else { + this.#statuses.set(workerIndex, WorkerStatus.Working); + } + return; + } if (messagePromise === undefined) { logger.warn('unexpected message from worker'); return; @@ -72,7 +99,7 @@ export class WorkerPool { const messageWithId = { ...message, id: reqId }; const messagePromise = this.#createMessagePromise(responseValidator); - // TODO this cast is very annoying; would be nice to remove it + // TODO this cast vexes me; would be nice to remove it this.#promises.set(workerIndex, messagePromise as unknown as MessagePromise); if (signal) { @@ -105,4 +132,16 @@ export class WorkerPool { promise, }; } + + getStatus(workerIndex: number): WorkerStatus { + const status = this.#statuses.get(workerIndex); + if (status === undefined) { + throw new Error(`invalid worker index: ${workerIndex}`); + } + return status; + } + + getStatuses(): ReadonlyMap { + return new Map(this.#statuses.entries()); + } } diff --git a/packages/dzi/tsconfig.json b/packages/dzi/tsconfig.json index baaee9ee..d8a6412f 100644 --- a/packages/dzi/tsconfig.json +++ b/packages/dzi/tsconfig.json @@ -5,9 +5,9 @@ "~/*": ["./*"] }, "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["es2022", "DOM"] + "module": "es6", + "target": "es2024", + "lib": ["es2024", "DOM"] }, "include": ["./src/index.ts"] } diff --git a/packages/geometry/tsconfig.json b/packages/geometry/tsconfig.json index e0b8abd0..5a25cb32 100644 --- a/packages/geometry/tsconfig.json +++ b/packages/geometry/tsconfig.json @@ -4,9 +4,9 @@ "paths": { "~/*": ["./*"] }, - "module": "ES2022", - "target": "ES2022", - "lib": ["es2022"] + "module": "es6", + "target": "es2024", + "lib": ["es2024"] }, "include": ["./src/index.ts"], "exclude": ["tests/", "**/*.test.ts"] diff --git a/packages/omezarr/src/zarr/cached-loading/fetch-slice.worker.ts b/packages/omezarr/src/zarr/cached-loading/fetch-slice.worker.ts index 42b8f443..bd845a25 100644 --- a/packages/omezarr/src/zarr/cached-loading/fetch-slice.worker.ts +++ b/packages/omezarr/src/zarr/cached-loading/fetch-slice.worker.ts @@ -1,7 +1,7 @@ // a web-worker which fetches slices of data, decodes them, and returns the result as a flat float32 array, using transferables -import { type AbsolutePath, type RangeQuery, FetchStore } from 'zarrita'; -import { logger } from '@alleninstitute/vis-core'; +import { HEARTBEAT_RATE_MS, logger } from '@alleninstitute/vis-core'; +import { type AbsolutePath, FetchStore, type RangeQuery } from 'zarrita'; import type { CancelMessage, FetchSliceMessage, TransferrableRequestInit } from './fetch-slice.interface'; import { isCancellationError, isCancelMessage, isFetchSliceMessage } from './fetch-slice.interface'; @@ -61,6 +61,10 @@ const handleCancel = (message: CancelMessage) => { } }; +self.setInterval(() => { + self.postMessage({ type: 'heartbeat' }); +}, HEARTBEAT_RATE_MS); + self.onmessage = async (e: MessageEvent) => { const { data: message } = e; diff --git a/site/src/content/docs/examples/multithreading.mdx b/site/src/content/docs/examples/multithreading.mdx new file mode 100644 index 00000000..bb2b5ca0 --- /dev/null +++ b/site/src/content/docs/examples/multithreading.mdx @@ -0,0 +1,8 @@ +--- +title: Multithreading +tableOfContents: false +--- + +import { MultithreadingDemo } from '../../../examples/multithreading/multithreading-demo.tsx'; + + diff --git a/site/src/examples/multithreading/multithreading-demo.tsx b/site/src/examples/multithreading/multithreading-demo.tsx new file mode 100644 index 00000000..218a2c79 --- /dev/null +++ b/site/src/examples/multithreading/multithreading-demo.tsx @@ -0,0 +1,80 @@ +import { WorkerPool, type WorkerStatus } from '@alleninstitute/vis-core'; +import { useEffect, useState } from 'react'; + +export function MultithreadingDemo() { + const [numWorkers, setNumWorkers] = useState(0); + const [workerPool, setWorkerPool] = useState(null); + const [editingWorkers, setEditingWorkers] = useState(true); + const [currentStatuses, setCurrentStatuses] = useState | null>(null); + const [currentStatusKeys, setCurrentStatusKeys] = useState([]); + + /* + PLAN: + - [DONE] allow users to choose how many workers are in the pool (only while no requests are pending) + - [DONE] display the health and status of each worker in the pool + - specify some parameters for each request: + - duration of request + - type of message + - message contents (except ID) + = workers receiving such a message will wait for the specified length of time, then respond with the message contents + - experiment with creating a heartbeat signal -- will they respond while they're awaiting a timeout?= + */ + + useEffect(() => { + const interval = setInterval(() => { + const statuses = workerPool?.getStatuses() ?? null; + setCurrentStatuses(statuses); + const statusKeys = Array.from(statuses?.keys() ?? []); + setCurrentStatusKeys(statusKeys); + }, 100); + + return () => { + clearInterval(interval); + }; + }, [workerPool]); + + const workerModule = new URL('./multithreading.worker.ts', import.meta.url); + const updateWorkers = () => { + setEditingWorkers(false); + setWorkerPool(new WorkerPool(numWorkers, workerModule)); + }; + + return ( +
+
+ + Number of Workers: + + {editingWorkers ? ( + + setNumWorkers(Number.parseInt(e.target.value, 10))} + /> + + + ) : ( + + {numWorkers} + + + )} +
+
+ {(currentStatusKeys ?? []).map((key) => ( +
+ {currentStatuses?.get(key) ?? 'Undefined'} +
+ ))} +
+
+ ); +} diff --git a/site/src/examples/multithreading/multithreading.worker.ts b/site/src/examples/multithreading/multithreading.worker.ts new file mode 100644 index 00000000..c283c587 --- /dev/null +++ b/site/src/examples/multithreading/multithreading.worker.ts @@ -0,0 +1,9 @@ +import { HEARTBEAT_RATE_MS } from '@alleninstitute/vis-core'; + +self.setInterval(() => { + self.postMessage({ type: 'heartbeat' }); +}, HEARTBEAT_RATE_MS); + +self.onmessage = (e: MessageEvent) => { + self.postMessage(e); +}; From 2fcfcb38fb0b10baf4009afe354b6750ba7e1af7 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 11:34:31 -0700 Subject: [PATCH 26/91] Added type: module to all packages --- packages/core/package.json | 1 + packages/dzi/package.json | 1 + packages/geometry/package.json | 1 + packages/omezarr/package.json | 1 + 4 files changed, 4 insertions(+) diff --git a/packages/core/package.json b/packages/core/package.json index d1350717..48bdd1b8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,6 +32,7 @@ "main": "dist/main.js", "module": "dist/module.js", "types": "dist/types.d.ts", + "type": "module", "files": [ "dist" ], diff --git a/packages/dzi/package.json b/packages/dzi/package.json index 8b09d75f..deca0ab9 100644 --- a/packages/dzi/package.json +++ b/packages/dzi/package.json @@ -28,6 +28,7 @@ "main": "dist/main.js", "module": "dist/module.js", "types": "dist/types.d.ts", + "type": "module", "files": [ "dist" ], diff --git a/packages/geometry/package.json b/packages/geometry/package.json index 69db4421..b5cdaeb2 100644 --- a/packages/geometry/package.json +++ b/packages/geometry/package.json @@ -32,6 +32,7 @@ "main": "dist/main.js", "module": "dist/module.js", "types": "dist/types.d.ts", + "type": "module", "files": [ "dist" ], diff --git a/packages/omezarr/package.json b/packages/omezarr/package.json index 4996116e..0e8c5241 100644 --- a/packages/omezarr/package.json +++ b/packages/omezarr/package.json @@ -28,6 +28,7 @@ "main": "dist/main.js", "module": "dist/module.js", "types": "dist/types.d.ts", + "type": "module", "files": [ "dist" ], From 45441785c00d19dd8e74e29d868454e3d789855f Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 11:44:25 -0700 Subject: [PATCH 27/91] Adding pnpm-lock --- pnpm-lock.yaml | 2895 +++++++++++++++++++----------------------------- 1 file changed, 1160 insertions(+), 1735 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32212ad0..62499353 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 2.15.4(@parcel/core@2.15.4(@swc/helpers@0.5.17))(typescript@5.9.2) '@vitest/coverage-istanbul': specifier: 3.2.4 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(lightningcss@1.30.1)(yaml@2.7.1)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(lightningcss@1.30.2)(yaml@2.8.1)) buffer: specifier: 6.0.0 version: 6.0.0 @@ -37,7 +37,7 @@ importers: version: 5.9.2 vitest: specifier: 3.2.4 - version: 3.2.4(@types/debug@4.1.12)(lightningcss@1.30.1)(yaml@2.7.1) + version: 3.2.4(@types/debug@4.1.12)(lightningcss@1.30.2)(yaml@2.8.1) packages/core: dependencies: @@ -109,49 +109,49 @@ importers: version: 0.9.4(typescript@5.8.3) '@astrojs/mdx': specifier: 4.3.0 - version: 4.3.0(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1)) + version: 4.3.0(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1)) '@astrojs/react': specifier: 4.3.0 - version: 4.3.0(@types/node@22.1.0)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yaml@2.7.1) + version: 4.3.0(@types/node@22.1.0)(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(lightningcss@1.30.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yaml@2.8.1) '@astrojs/starlight': specifier: 0.34.3 - version: 0.34.3(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1)) + version: 0.34.3(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1)) '@czi-sds/components': specifier: 20.0.1 - version: 20.0.1(yhvdhk7sonewaw25ryh4hrzn6m) + version: 20.0.1(3d6g66iwpfly7u54dlbfcchst4) '@emotion/css': specifier: 11.11.2 version: 11.11.2 '@emotion/react': specifier: 11.11.4 - version: 11.11.4(@types/react@19.1.3)(react@19.1.0) + version: 11.11.4(@types/react@19.2.2)(react@19.1.0) '@emotion/styled': specifier: 11.11.5 - version: 11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0) + version: 11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0) '@mui/base': specifier: 5.0.0-beta.40 - version: 5.0.0-beta.40(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 5.0.0-beta.40(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/icons-material': specifier: 5.15.15 - version: 5.15.15(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.3)(react@19.1.0) + version: 5.15.15(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.2.2)(react@19.1.0) '@mui/lab': specifier: 5.0.0-alpha.175 - version: 5.0.0-alpha.175(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 5.0.0-alpha.175(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/material': specifier: 5.15.15 - version: 5.15.15(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 5.15.15(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/lodash': specifier: 4.17.16 version: 4.17.16 '@types/react': specifier: ^19.1.3 - version: 19.1.3 + version: 19.2.2 '@types/react-dom': specifier: ^19.1.3 - version: 19.1.3(@types/react@19.1.3) + version: 19.2.1(@types/react@19.2.2) astro: specifier: 5.8.0 - version: 5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1) + version: 5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1) file-saver: specifier: 2.0.5 version: 2.0.5 @@ -192,22 +192,21 @@ importers: packages: - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@astrojs/check@0.9.4': resolution: {integrity: sha512-IOheHwCtpUfvogHHsvu0AbeRZEnjJg3MopdLddkJE70mULItS/Vh37BHcI00mcOJcH1vhD3odbpvWokpxam7xA==} hasBin: true peerDependencies: typescript: ^5.0.0 - '@astrojs/compiler@2.12.0': - resolution: {integrity: sha512-7bCjW6tVDpUurQLeKBUN9tZ5kSv5qYrGmcn0sG0IwacL7isR2ZbyyA3AdZ4uxsuUFOS2SlgReTH7wkxO6zpqWA==} + '@astrojs/compiler@2.13.0': + resolution: {integrity: sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==} '@astrojs/internal-helpers@0.6.1': resolution: {integrity: sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A==} + '@astrojs/internal-helpers@0.7.3': + resolution: {integrity: sha512-6Pl0bQEIChuW5wqN7jdKrzWfCscW2rG/Cz+fzt4PhSQX2ivBpnhXgFUCs0M3DCYvjYHnPVG2W36X5rmFjZ62sw==} + '@astrojs/language-server@2.15.4': resolution: {integrity: sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A==} hasBin: true @@ -220,22 +219,18 @@ packages: prettier-plugin-astro: optional: true - '@astrojs/markdown-remark@6.3.1': - resolution: {integrity: sha512-c5F5gGrkczUaTVgmMW9g1YMJGzOtRvjjhw6IfGuxarM6ct09MpwysP10US729dy07gg8y+ofVifezvP3BNsWZg==} - '@astrojs/markdown-remark@6.3.2': resolution: {integrity: sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q==} + '@astrojs/markdown-remark@6.3.7': + resolution: {integrity: sha512-KXGdq6/BC18doBCYXp08alHlWChH0hdD2B1qv9wIyOHbvwI5K6I7FhSta8dq1hBQNdun8YkKPR013D/Hm8xd0g==} + '@astrojs/mdx@4.3.0': resolution: {integrity: sha512-OGX2KvPeBzjSSKhkCqrUoDMyzFcjKt5nTE5SFw3RdoLf0nrhyCXBQcCyclzWy1+P+XpOamn+p+hm1EhpCRyPxw==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} peerDependencies: astro: ^5.0.0 - '@astrojs/prism@3.2.0': - resolution: {integrity: sha512-GilTHKGCW6HMq7y3BUv9Ac7GMe/MO9gi9GW62GzKtth0SwukCu/qp2wLiGpEujhY+VVhaG9v7kv/5vFzvf4NYw==} - engines: {node: ^18.17.1 || ^20.3.0 || >=22.0.0} - '@astrojs/prism@3.3.0': resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} @@ -249,8 +244,8 @@ packages: react: ^17.0.2 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.2 || ^18.0.0 || ^19.0.0 - '@astrojs/sitemap@3.3.1': - resolution: {integrity: sha512-GRnDUCTviBSNfXJ0Jmur+1/C+z3g36jy79VyYggfe1uNyEYSTcmAfTTCmbytrRvJRNyJJnSfB/77Gnm9PiXRRg==} + '@astrojs/sitemap@3.6.0': + resolution: {integrity: sha512-4aHkvcOZBWJigRmMIAJwRQXBS+ayoP5z40OklTXYXhUDhwusz+DyDl+nSshY6y9DvkVEavwNcFO8FD81iGhXjg==} '@astrojs/starlight@0.34.3': resolution: {integrity: sha512-MAuD3NF+E+QXJJuVKofoR6xcPTP4BJmYWeOBd03udVdubNGVnPnSWVZAi+ZtnTofES4+mJdp8BNGf+ubUxkiiA==} @@ -268,36 +263,32 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.27.3': - resolution: {integrity: sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==} + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} engines: {node: '>=6.9.0'} - '@babel/core@7.27.1': - resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==} + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} engines: {node: '>=6.9.0'} - '@babel/core@7.27.7': - resolution: {integrity: sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.27.3': - resolution: {integrity: sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.27.5': - resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.27.1': resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.27.3': - resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -318,26 +309,12 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.4': - resolution: {integrity: sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.27.6': - resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - '@babel/parser@7.27.1': - resolution: {integrity: sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.27.4': - resolution: {integrity: sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.27.7': - resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} engines: {node: '>=6.0.0'} hasBin: true @@ -353,32 +330,20 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.27.0': - resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.27.4': - resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.27.7': - resolution: {integrity: sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.27.1': - resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.27.3': - resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==} + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.27.7': - resolution: {integrity: sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==} + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} '@biomejs/biome@2.2.2': @@ -437,8 +402,8 @@ packages: '@capsizecss/unpack@2.4.0': resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==} - '@ctrl/tinycolor@4.1.0': - resolution: {integrity: sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==} + '@ctrl/tinycolor@4.2.0': + resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} engines: {node: '>=14'} '@czi-sds/components@20.0.1': @@ -476,11 +441,11 @@ packages: '@emmetio/stream-reader@2.2.0': resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} - '@emnapi/runtime@1.4.3': - resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} - '@emotion/babel-plugin@11.11.0': - resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} '@emotion/cache@11.14.0': resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} @@ -491,14 +456,11 @@ packages: '@emotion/css@11.11.2': resolution: {integrity: sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew==} - '@emotion/hash@0.9.1': - resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} - '@emotion/is-prop-valid@1.2.2': - resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} - - '@emotion/memoize@0.8.1': - resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} '@emotion/memoize@0.9.0': resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} @@ -512,8 +474,8 @@ packages: '@types/react': optional: true - '@emotion/serialize@1.1.4': - resolution: {integrity: sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==} + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} '@emotion/sheet@1.4.0': resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} @@ -528,11 +490,11 @@ packages: '@types/react': optional: true - '@emotion/unitless@0.8.1': - resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} - '@emotion/use-insertion-effect-with-fallbacks@1.0.1': - resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} peerDependencies: react: '>=16.8.0' @@ -545,302 +507,158 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} - '@esbuild/aix-ppc64@0.25.4': - resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.5': - resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.4': - resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.5': - resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.4': - resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.25.5': - resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.4': - resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.5': - resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.4': - resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.5': - resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.4': - resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.5': - resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.4': - resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.5': - resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.4': - resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.5': - resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.4': - resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.5': - resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.4': - resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.25.5': - resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.4': - resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.5': - resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.4': - resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.5': - resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.4': - resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.5': - resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.4': - resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.5': - resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.4': - resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.5': - resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.4': - resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.25.5': - resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.4': - resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.5': - resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.4': - resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.25.5': - resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.4': - resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.5': - resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.4': - resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.25.5': - resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.4': - resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.5': - resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.25.4': - resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] + cpu: [arm64] + os: [openharmony] - '@esbuild/sunos-x64@0.25.5': - resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.4': - resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.25.5': - resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.4': - resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.5': - resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.4': - resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.25.5': - resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -857,20 +675,20 @@ packages: '@expressive-code/plugin-text-markers@0.41.3': resolution: {integrity: sha512-SN8tkIzDpA0HLAscEYD2IVrfLiid6qEdE9QLlGVSxO1KEw7qYvjpbNBQjUjMr5/jvTJ7ys6zysU2vLPHE0sb2g==} - '@floating-ui/core@1.6.1': - resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} - '@floating-ui/dom@1.6.4': - resolution: {integrity: sha512-0G8R+zOvQsAG1pg2Q99P21jiqxqGBW1iRe/iXHsBRBxnpXKFI8QwbB4x5KmYLggNO5m34IQgOIu9SCRfR/WWiQ==} + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} - '@floating-ui/react-dom@2.0.9': - resolution: {integrity: sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==} + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.2': - resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==} + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} '@img/sharp-darwin-arm64@0.33.5': resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} @@ -1095,29 +913,21 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - '@jridgewell/gen-mapping@0.3.11': - resolution: {integrity: sha512-C512c1ytBTio4MrpWKlJpyFHT6+qfFL8SZ58zBzJ1OOzUEjHeF1BtjY2fH7n4x/g2OV/KiiMLAivOp1DXmiMMw==} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/sourcemap-codec@1.5.3': - resolution: {integrity: sha512-AiR5uKpFxP3PjO4R19kQGIMwxyRyPuXmKEEy301V1C0+1rVjS94EZQXf1QKZYN8Q0YM+estSPhmx5JwNftv6nw==} - - '@jridgewell/trace-mapping@0.3.28': - resolution: {integrity: sha512-KNNHHwW3EIp4EDYOvYFGyIFfx36R2dNJYH4knnZlF8T5jdbD5Wx8xmSaQ2gP9URkJ04LGEtlcCtwArKcmFcwKw==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@lezer/common@1.2.3': resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} @@ -1155,8 +965,8 @@ packages: cpu: [x64] os: [win32] - '@mdx-js/mdx@3.1.0': - resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} + '@mdx-js/mdx@3.1.1': + resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} '@mischnic/json-sourcemap@0.1.1': resolution: {integrity: sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==} @@ -1216,8 +1026,8 @@ packages: '@types/react': optional: true - '@mui/core-downloads-tracker@5.15.15': - resolution: {integrity: sha512-aXnw29OWQ6I5A47iuWEI6qSSUfH6G/aCsW9KmW3LiFqr7uXZBK4Ks+z8G+qeIub8k0T5CMqlT2q0L+ZJTMrqpg==} + '@mui/core-downloads-tracker@5.18.0': + resolution: {integrity: sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA==} '@mui/icons-material@5.15.15': resolution: {integrity: sha512-kkeU/pe+hABcYDH6Uqy8RmIsr2S/y5bP2rp+Gat4CcRjCcVne6KudS1NrZQhUCRysrTDCAhcbcf9gt+/+pGO2g==} @@ -1275,8 +1085,8 @@ packages: '@types/react': optional: true - '@mui/styled-engine@5.16.14': - resolution: {integrity: sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw==} + '@mui/styled-engine@5.18.0': + resolution: {integrity: sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -1288,8 +1098,8 @@ packages: '@emotion/styled': optional: true - '@mui/system@5.17.1': - resolution: {integrity: sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg==} + '@mui/system@5.18.0': + resolution: {integrity: sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1312,8 +1122,8 @@ packages: '@types/react': optional: true - '@mui/types@7.4.1': - resolution: {integrity: sha512-gUL8IIAI52CRXP/MixT1tJKt3SI6tVv4U/9soFsTtAsHzaJQptZ42ffdHZV3niX1ei0aUgMvOxBBN0KYqdG39g==} + '@mui/types@7.4.7': + resolution: {integrity: sha512-8vVje9rdEr1rY8oIkYgP+Su5Kwl6ik7O3jQ0wl78JGSmiZhRHV+vkjooGdKD8pbtZbutXFVTWQYshu2b3sG9zw==} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: @@ -1345,31 +1155,36 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} - '@pagefind/darwin-arm64@1.3.0': - resolution: {integrity: sha512-365BEGl6ChOsauRjyVpBjXybflXAOvoMROw3TucAROHIcdBvXk9/2AmEvGFU0r75+vdQI4LJdJdpH4Y6Yqaj4A==} + '@pagefind/darwin-arm64@1.4.0': + resolution: {integrity: sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ==} cpu: [arm64] os: [darwin] - '@pagefind/darwin-x64@1.3.0': - resolution: {integrity: sha512-zlGHA23uuXmS8z3XxEGmbHpWDxXfPZ47QS06tGUq0HDcZjXjXHeLG+cboOy828QIV5FXsm9MjfkP5e4ZNbOkow==} + '@pagefind/darwin-x64@1.4.0': + resolution: {integrity: sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A==} cpu: [x64] os: [darwin] - '@pagefind/default-ui@1.3.0': - resolution: {integrity: sha512-CGKT9ccd3+oRK6STXGgfH+m0DbOKayX6QGlq38TfE1ZfUcPc5+ulTuzDbZUnMo+bubsEOIypm4Pl2iEyzZ1cNg==} + '@pagefind/default-ui@1.4.0': + resolution: {integrity: sha512-wie82VWn3cnGEdIjh4YwNESyS1G6vRHwL6cNjy9CFgNnWW/PGRjsLq300xjVH5sfPFK3iK36UxvIBymtQIEiSQ==} + + '@pagefind/freebsd-x64@1.4.0': + resolution: {integrity: sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q==} + cpu: [x64] + os: [freebsd] - '@pagefind/linux-arm64@1.3.0': - resolution: {integrity: sha512-8lsxNAiBRUk72JvetSBXs4WRpYrQrVJXjlRRnOL6UCdBN9Nlsz0t7hWstRk36+JqHpGWOKYiuHLzGYqYAqoOnQ==} + '@pagefind/linux-arm64@1.4.0': + resolution: {integrity: sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw==} cpu: [arm64] os: [linux] - '@pagefind/linux-x64@1.3.0': - resolution: {integrity: sha512-hAvqdPJv7A20Ucb6FQGE6jhjqy+vZ6pf+s2tFMNtMBG+fzcdc91uTw7aP/1Vo5plD0dAOHwdxfkyw0ugal4kcQ==} + '@pagefind/linux-x64@1.4.0': + resolution: {integrity: sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg==} cpu: [x64] os: [linux] - '@pagefind/windows-x64@1.3.0': - resolution: {integrity: sha512-BR1bIRWOMqkf8IoU576YDhij1Wd/Zf2kX/kCI0b2qzCKC8wcc2GQJaaRMCpzvCCrmliO4vtJ6RITp/AnoYUUmQ==} + '@pagefind/windows-x64@1.4.0': + resolution: {integrity: sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g==} cpu: [x64] os: [win32] @@ -1768,8 +1583,11 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@rollup/pluginutils@5.1.4': - resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -1777,226 +1595,136 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.40.2': - resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==} + '@rollup/rollup-android-arm-eabi@4.52.4': + resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm-eabi@4.44.1': - resolution: {integrity: sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.40.2': - resolution: {integrity: sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-android-arm64@4.44.1': - resolution: {integrity: sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==} + '@rollup/rollup-android-arm64@4.52.4': + resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.40.2': - resolution: {integrity: sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-arm64@4.44.1': - resolution: {integrity: sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==} + '@rollup/rollup-darwin-arm64@4.52.4': + resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.40.2': - resolution: {integrity: sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.44.1': - resolution: {integrity: sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==} + '@rollup/rollup-darwin-x64@4.52.4': + resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.40.2': - resolution: {integrity: sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-arm64@4.44.1': - resolution: {integrity: sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==} + '@rollup/rollup-freebsd-arm64@4.52.4': + resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.40.2': - resolution: {integrity: sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==} + '@rollup/rollup-freebsd-x64@4.52.4': + resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.44.1': - resolution: {integrity: sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.40.2': - resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-gnueabihf@4.44.1': - resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': + resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.40.2': - resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==} + '@rollup/rollup-linux-arm-musleabihf@4.52.4': + resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.44.1': - resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.40.2': - resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==} + '@rollup/rollup-linux-arm64-gnu@4.52.4': + resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.44.1': - resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==} + '@rollup/rollup-linux-arm64-musl@4.52.4': + resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.40.2': - resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.44.1': - resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loongarch64-gnu@4.40.2': - resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-loongarch64-gnu@4.44.1': - resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==} + '@rollup/rollup-linux-loong64-gnu@4.52.4': + resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': - resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': - resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==} + '@rollup/rollup-linux-ppc64-gnu@4.52.4': + resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.40.2': - resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==} + '@rollup/rollup-linux-riscv64-gnu@4.52.4': + resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.44.1': - resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==} + '@rollup/rollup-linux-riscv64-musl@4.52.4': + resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.40.2': - resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.44.1': - resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.40.2': - resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.44.1': - resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==} + '@rollup/rollup-linux-s390x-gnu@4.52.4': + resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.40.2': - resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==} + '@rollup/rollup-linux-x64-gnu@4.52.4': + resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.44.1': - resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==} + '@rollup/rollup-linux-x64-musl@4.52.4': + resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.40.2': - resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.44.1': - resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.40.2': - resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==} + '@rollup/rollup-openharmony-arm64@4.52.4': + resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} cpu: [arm64] - os: [win32] + os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.44.1': - resolution: {integrity: sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==} + '@rollup/rollup-win32-arm64-msvc@4.52.4': + resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.40.2': - resolution: {integrity: sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==} + '@rollup/rollup-win32-ia32-msvc@4.52.4': + resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.44.1': - resolution: {integrity: sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.40.2': - resolution: {integrity: sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==} + '@rollup/rollup-win32-x64-gnu@4.52.4': + resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.44.1': - resolution: {integrity: sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==} + '@rollup/rollup-win32-x64-msvc@4.52.4': + resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==} cpu: [x64] os: [win32] '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@shikijs/core@3.4.0': - resolution: {integrity: sha512-0YOzTSRDn/IAfQWtK791gs1u8v87HNGToU6IwcA3K7nPoVOrS2Dh6X6A6YfXgPTSkTwR5y6myk0MnI0htjnwrA==} + '@shikijs/core@3.13.0': + resolution: {integrity: sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA==} - '@shikijs/engine-javascript@3.4.0': - resolution: {integrity: sha512-1ywDoe+z/TPQKj9Jw0eU61B003J9DqUFRfH+DVSzdwPUFhR7yOmfyLzUrFz0yw8JxFg/NgzXoQyyykXgO21n5Q==} + '@shikijs/engine-javascript@3.13.0': + resolution: {integrity: sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg==} - '@shikijs/engine-oniguruma@3.4.0': - resolution: {integrity: sha512-zwcWlZ4OQuJ/+1t32ClTtyTU1AiDkK1lhtviRWoq/hFqPjCNyLj22bIg9rB7BfoZKOEOfrsGz7No33BPCf+WlQ==} + '@shikijs/engine-oniguruma@3.13.0': + resolution: {integrity: sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==} - '@shikijs/langs@3.4.0': - resolution: {integrity: sha512-bQkR+8LllaM2duU9BBRQU0GqFTx7TuF5kKlw/7uiGKoK140n1xlLAwCgXwSxAjJ7Htk9tXTFwnnsJTCU5nDPXQ==} + '@shikijs/langs@3.13.0': + resolution: {integrity: sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==} - '@shikijs/themes@3.4.0': - resolution: {integrity: sha512-YPP4PKNFcFGLxItpbU0ZW1Osyuk8AyZ24YEFaq04CFsuCbcqydMvMUTi40V2dkc0qs1U2uZFrnU6s5zI6IH+uA==} + '@shikijs/themes@3.13.0': + resolution: {integrity: sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==} - '@shikijs/types@3.4.0': - resolution: {integrity: sha512-EUT/0lGiE//7j5N/yTMNMT3eCWNcHJLrRKxT0NDXWIfdfSmFJKfPX7nMmRBrQnWboAzIsUziCThrYMMhjbMS1A==} + '@shikijs/types@3.13.0': + resolution: {integrity: sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -2005,68 +1733,68 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@swc/core-darwin-arm64@1.12.9': - resolution: {integrity: sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==} + '@swc/core-darwin-arm64@1.13.5': + resolution: {integrity: sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.12.9': - resolution: {integrity: sha512-hv2kls7Ilkm2EpeJz+I9MCil7pGS3z55ZAgZfxklEuYsxpICycxeH+RNRv4EraggN44ms+FWCjtZFu0LGg2V3g==} + '@swc/core-darwin-x64@1.13.5': + resolution: {integrity: sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.12.9': - resolution: {integrity: sha512-od9tDPiG+wMU9wKtd6y3nYJdNqgDOyLdgRRcrj1/hrbHoUPOM8wZQZdwQYGarw63iLXGgsw7t5HAF9Yc51ilFA==} + '@swc/core-linux-arm-gnueabihf@1.13.5': + resolution: {integrity: sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.12.9': - resolution: {integrity: sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==} + '@swc/core-linux-arm64-gnu@1.13.5': + resolution: {integrity: sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.12.9': - resolution: {integrity: sha512-yghFZWKPVVGbUdqiD7ft23G0JX6YFGDJPz9YbLLAwGuKZ9th3/jlWoQDAw1Naci31LQhVC+oIji6ozihSuwB2A==} + '@swc/core-linux-arm64-musl@1.13.5': + resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.12.9': - resolution: {integrity: sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==} + '@swc/core-linux-x64-gnu@1.13.5': + resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.12.9': - resolution: {integrity: sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==} + '@swc/core-linux-x64-musl@1.13.5': + resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.12.9': - resolution: {integrity: sha512-zHOusMVbOH9ik5RtRrMiGzLpKwxrPXgXkBm3SbUCa65HAdjV33NZ0/R9Rv1uPESALtEl2tzMYLUxYA5ECFDFhA==} + '@swc/core-win32-arm64-msvc@1.13.5': + resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.12.9': - resolution: {integrity: sha512-aWZf0PqE0ot7tCuhAjRkDFf41AzzSQO0x2xRfTbnhpROp57BRJ/N5eee1VULO/UA2PIJRG7GKQky5bSGBYlFug==} + '@swc/core-win32-ia32-msvc@1.13.5': + resolution: {integrity: sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.12.9': - resolution: {integrity: sha512-C25fYftXOras3P3anSUeXXIpxmEkdAcsIL9yrr0j1xepTZ/yKwpnQ6g3coj8UXdeJy4GTVlR6+Ow/QiBgZQNOg==} + '@swc/core-win32-x64-msvc@1.13.5': + resolution: {integrity: sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.12.9': - resolution: {integrity: sha512-O+LfT2JlVMsIMWG9x+rdxg8GzpzeGtCZQfXV7cKc1PjIKUkLFf1QJ7okuseA4f/9vncu37dQ2ZcRrPKy0Ndd5g==} + '@swc/core@1.13.5': + resolution: {integrity: sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -2080,8 +1808,8 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@swc/types@0.1.23': - resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2092,8 +1820,8 @@ packages: '@types/babel__template@7.4.4': resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - '@types/babel__traverse@7.20.7': - resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -2107,9 +1835,6 @@ packages: '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2152,19 +1877,21 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - '@types/prop-types@15.7.12': - resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/react-dom@19.1.3': - resolution: {integrity: sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==} + '@types/react-dom@19.2.1': + resolution: {integrity: sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==} peerDependencies: - '@types/react': ^19.0.0 + '@types/react': ^19.2.0 - '@types/react-transition-group@4.4.10': - resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' - '@types/react@19.1.3': - resolution: {integrity: sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==} + '@types/react@19.2.2': + resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} @@ -2178,11 +1905,11 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitejs/plugin-react@4.4.1': - resolution: {integrity: sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==} + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 '@vitest/coverage-istanbul@3.2.4': resolution: {integrity: sha512-IDlpuFJiWU9rhcKLkpzj8mFu/lpe64gVgnV15ZOrYx1iFzxxrxCzbExiUEKtwwXRvEiEMUS6iZeYgnMxgbqbxQ==} @@ -2218,25 +1945,25 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@volar/kit@2.4.13': - resolution: {integrity: sha512-x5b2JwVT+0YQcIR4uX0NaW9Ocf3ShQRvoA18OK9ZYoFyqIYnK/niuLc8uI7hcVaey2S3EPBe3QvLGD69DJ/t6Q==} + '@volar/kit@2.4.23': + resolution: {integrity: sha512-YuUIzo9zwC2IkN7FStIcVl1YS9w5vkSFEZfPvnu0IbIMaR9WHhc9ZxvlT+91vrcSoRY469H2jwbrGqpG7m1KaQ==} peerDependencies: typescript: '*' - '@volar/language-core@2.4.13': - resolution: {integrity: sha512-MnQJ7eKchJx5Oz+YdbqyFUk8BN6jasdJv31n/7r6/WwlOOv7qzvot6B66887l2ST3bUW4Mewml54euzpJWA6bg==} + '@volar/language-core@2.4.23': + resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} - '@volar/language-server@2.4.13': - resolution: {integrity: sha512-g8ucG5+FJgQT2r+Te1Pk+ppoPHCwzJ54gq/oN1utjtA3+iE9hp5E+M60Ks+hhGrexUPC/E3EABDQlCagmEal+Q==} + '@volar/language-server@2.4.23': + resolution: {integrity: sha512-k0iO+tybMGMMyrNdWOxgFkP0XJTdbH0w+WZlM54RzJU3WZSjHEupwL30klpM7ep4FO6qyQa03h+VcGHD4Q8gEg==} - '@volar/language-service@2.4.13': - resolution: {integrity: sha512-yngNLIxt1w3S60YLTRAa7MSE1IRmXcxGA9ttLjndY0Jc3owCFjeAWSPeXBILZBJOtdT8rP07JY1ozwUls/gvRg==} + '@volar/language-service@2.4.23': + resolution: {integrity: sha512-h5mU9DZ/6u3LCB9xomJtorNG6awBNnk9VuCioGsp6UtFiM8amvS5FcsaC3dabdL9zO0z+Gq9vIEMb/5u9K6jGQ==} - '@volar/source-map@2.4.13': - resolution: {integrity: sha512-l/EBcc2FkvHgz2ZxV+OZK3kMSroMr7nN3sZLF2/f6kWW66q8+tEL4giiYyFjt0BcubqJhBt6soYIrAPhg/Yr+Q==} + '@volar/source-map@2.4.23': + resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} - '@volar/typescript@2.4.13': - resolution: {integrity: sha512-Ukz4xv84swJPupZeoFsQoeJEOm7U9pqsEnaGGgt5ni3SCTa22m8oJP5Nng3Wed7Uw5RBELdLxxORX8YhJPyOgQ==} + '@volar/typescript@2.4.23': + resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} '@vscode/emmet-helper@2.11.0': resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} @@ -2244,9 +1971,6 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} - '@zarrita/storage@0.1.1': - resolution: {integrity: sha512-6/NUCvpzsIxfxeMv59jRTl/bOZg3GZfMP6iR8EIqrTaaE0S2jLL/ceX1OxcFBKnuA8/Z2YmgX4SFBHwFGrCcsw==} - '@zarrita/storage@0.1.3': resolution: {integrity: sha512-ZyCMYN3LuCNtKxro9876r/KyHyXV+ie2Bhk1qYsJR4Jp+sAjoVRRNNSJPsJxk64ZgFFezayO5S2hCu88/1Odwg==} @@ -2255,8 +1979,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true @@ -2270,16 +1994,16 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} anymatch@3.1.3: @@ -2307,8 +2031,8 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true - astro-expressive-code@0.41.2: - resolution: {integrity: sha512-HN0jWTnhr7mIV/2e6uu4PPRNNo/k4UEgTLZqbp3MrHU+caCARveG2yZxaZVBmxyiVdYqW5Pd3u3n2zjnshixbw==} + astro-expressive-code@0.41.3: + resolution: {integrity: sha512-u+zHMqo/QNLE2eqYRCrK3+XMlKakv33Bzuz+56V1gs8H0y6TZ0hIi3VNbIxeTn51NLn+mJfUV/A0kMNfE4rANw==} peerDependencies: astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 @@ -2340,6 +2064,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.8.13: + resolution: {integrity: sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==} + hasBin: true + bcp-47-match@2.0.3: resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} @@ -2366,8 +2094,8 @@ packages: brotli@1.3.3: resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} - browserslist@4.25.1: - resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + browserslist@4.26.3: + resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2386,22 +2114,22 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} - caniuse-lite@1.0.30001726: - resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} + caniuse-lite@1.0.30001748: + resolution: {integrity: sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chai@5.2.0: - resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} - engines: {node: '>=12'} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.4.1: - resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} character-entities-html4@2.1.0: @@ -2428,8 +2156,8 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} - ci-info@4.2.0: - resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==} + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} cli-boxes@3.0.0: @@ -2502,8 +2230,8 @@ packages: crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} - css-selector-parser@3.1.2: - resolution: {integrity: sha512-WfUcL99xWDs7b3eZPoRszWVfbNo8ErCF15PTvVROjkShGlAfjIkG6hlfj/sl6/rfo5Q9x9ryJ3VqVnAZDA+gcw==} + css-selector-parser@3.1.3: + resolution: {integrity: sha512-gJMigczVZqYAk0hPVzx/M4Hm1D9QOtqkdQk9005TNzDIUGzo5cnHEDiKUT7jGPximL/oYb+LIitcHFQ4aKupxg==} css-tree@3.1.0: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} @@ -2517,17 +2245,8 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -2535,8 +2254,8 @@ packages: supports-color: optional: true - decode-named-character-reference@1.1.0: - resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} @@ -2557,16 +2276,16 @@ packages: engines: {node: '>=0.10'} hasBin: true - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} deterministic-object-hash@2.0.2: resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} engines: {node: '>=18'} - devalue@5.1.1: - resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + devalue@5.3.2: + resolution: {integrity: sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -2603,14 +2322,14 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.178: - resolution: {integrity: sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==} + electron-to-chromium@1.5.232: + resolution: {integrity: sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==} emmet@2.4.11: resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} - emoji-regex@10.4.0: - resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + emoji-regex@10.5.0: + resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2618,12 +2337,12 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - entities@6.0.0: - resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -2634,13 +2353,8 @@ packages: esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} - esbuild@0.25.4: - resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.25.5: - resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} engines: {node: '>=18'} hasBin: true @@ -2704,14 +2418,15 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-uri@3.0.6: - resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -2739,8 +2454,8 @@ packages: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} engines: {node: '>=8'} - fontace@0.3.0: - resolution: {integrity: sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg==} + fontace@0.3.1: + resolution: {integrity: sha512-9f5g4feWT1jWT8+SbL85aLIRLIXUaDygaM2xPXRmzPYxrOMNok79Lr3FGJoKVNKibE0WCunNiEVG2mwuE+2qEg==} fontkit@2.0.4: resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} @@ -2765,8 +2480,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.3.0: - resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} get-port@4.2.0: @@ -2823,16 +2538,12 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} - h3@1.15.3: - resolution: {integrity: sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==} + h3@1.15.4: + resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -2917,8 +2628,8 @@ packages: html-whitespace-sensitive-tag-names@3.0.1: resolution: {integrity: sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==} - http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} human-signals@8.0.1: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} @@ -2930,12 +2641,12 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-meta-resolve@4.1.0: - resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} @@ -2952,8 +2663,8 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} @@ -3026,8 +2737,8 @@ packages: resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} engines: {node: '>=10'} - istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} jackspeak@3.4.3: @@ -3081,68 +2792,74 @@ packages: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} - lightningcss-darwin-arm64@1.30.1: - resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.30.1: - resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.30.1: - resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.30.1: - resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.30.1: - resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.30.1: - resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.30.1: - resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.30.1: - resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.30.1: - resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.30.1: - resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.30.1: - resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} lines-and-columns@1.2.4: @@ -3162,8 +2879,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.4: - resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3171,8 +2888,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} @@ -3380,8 +3097,8 @@ packages: resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} hasBin: true - msgpackr@1.11.4: - resolution: {integrity: sha512-uaff7RG9VIC4jacFW9xzL3jc0iM32DNHe4jYVycBcjUePT/Klnfj7pqtWJt9khvDFizmjN2TlYniYmSS2LIaZg==} + msgpackr@1.11.5: + resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -3404,8 +3121,8 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-fetch-native@1.6.6: - resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} @@ -3424,11 +3141,11 @@ packages: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true - node-mock-http@1.0.0: - resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==} + node-mock-http@1.0.3: + resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.23: + resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -3463,15 +3180,15 @@ packages: oniguruma-to-es@4.3.3: resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} - ordered-binary@1.5.3: - resolution: {integrity: sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==} + ordered-binary@1.6.0: + resolution: {integrity: sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==} p-limit@6.2.0: resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} engines: {node: '>=18'} - p-queue@8.1.0: - resolution: {integrity: sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==} + p-queue@8.1.1: + resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==} engines: {node: '>=18'} p-timeout@6.1.4: @@ -3481,11 +3198,11 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - package-manager-detector@1.3.0: - resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + package-manager-detector@1.4.0: + resolution: {integrity: sha512-rRZ+pR1Usc+ND9M2NkmCvE/LYJS+8ORVV9X0KuNSY/gFsp7RBHJM/ADh9LYq4Vvfq6QkKrW6/weuh8SMEtN5gw==} - pagefind@1.3.0: - resolution: {integrity: sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw==} + pagefind@1.4.0: + resolution: {integrity: sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g==} hasBin: true pako@0.2.9: @@ -3553,8 +3270,8 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} postcss-nested@6.2.0: @@ -3570,10 +3287,6 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -3622,11 +3335,11 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-is@19.1.0: - resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==} + react-is@19.2.0: + resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==} react-refresh@0.16.0: resolution: {integrity: sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==} @@ -3653,8 +3366,10 @@ packages: recma-build-jsx@1.0.0: resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} - recma-jsx@1.0.0: - resolution: {integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==} + recma-jsx@1.0.1: + resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 recma-parse@1.0.0: resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} @@ -3683,8 +3398,8 @@ packages: regl@2.1.1: resolution: {integrity: sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==} - rehype-expressive-code@0.41.2: - resolution: {integrity: sha512-vHYfWO9WxAw6kHHctddOt+P4266BtyT1mrOIuxJD+1ELuvuJAa5uBIhYt0OVMyOhlvf57hzWOXJkHnMhpaHyxw==} + rehype-expressive-code@0.41.3: + resolution: {integrity: sha512-8d9Py4c/V6I/Od2VIXFAdpiO2kc0SV2qTJsRAaqSIcM9aruW4ASLNe2kOEo1inXAAkIhpFzAHTc358HKbvpNUg==} rehype-format@5.0.1: resolution: {integrity: sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==} @@ -3710,8 +3425,8 @@ packages: remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} - remark-mdx@3.1.0: - resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} + remark-mdx@3.1.1: + resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} @@ -3768,13 +3483,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.40.2: - resolution: {integrity: sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - rollup@4.44.1: - resolution: {integrity: sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==} + rollup@4.52.4: + resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3794,13 +3504,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true @@ -3820,8 +3525,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@3.4.0: - resolution: {integrity: sha512-Ni80XHcqhOEXv5mmDAvf5p6PAJqbUc/RzFeaOqk+zP5DLvTPS3j0ckvA+MI87qoxTQ5RGJDVTbdl/ENLSyyAnQ==} + shiki@3.13.0: + resolution: {integrity: sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g==} siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3830,8 +3535,8 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -3841,8 +3546,8 @@ packages: engines: {node: '>=14.0.0', npm: '>=6.0.0'} hasBin: true - smol-toml@1.3.4: - resolution: {integrity: sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==} + smol-toml@1.4.2: + resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==} engines: {node: '>= 18'} source-map-js@1.2.1: @@ -3853,9 +3558,9 @@ packages: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} - source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -3888,16 +3593,16 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} strip-final-newline@4.0.0: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} - strip-literal@3.0.0: - resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} style-to-js@1.1.17: resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} @@ -3933,12 +3638,8 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.13: - resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} - engines: {node: '>=12.0.0'} - - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} tinypool@1.1.1: @@ -3949,8 +3650,8 @@ packages: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} - tinyspy@4.0.3: - resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} to-regex-range@5.0.1: @@ -3966,8 +3667,8 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - tsconfck@3.1.5: - resolution: {integrity: sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==} + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} hasBin: true peerDependencies: @@ -3976,9 +3677,6 @@ packages: typescript: optional: true - tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -3993,8 +3691,8 @@ packages: typesafe-path@0.2.2: resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} - typescript-auto-import-cache@0.3.5: - resolution: {integrity: sha512-fAIveQKsoYj55CozUiBoj4b/7WpN0i4o74wiGY5JVUEoD0XiqDk1tJqTEjgzL2/AizKQrXxyRosSebyDzBZKjw==} + typescript-auto-import-cache@0.3.6: + resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==} typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} @@ -4031,8 +3729,8 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unifont@0.5.0: - resolution: {integrity: sha512-4DueXMP5Hy4n607sh+vJ+rajoLu778aU3GzqeTCqsD/EaUcvqZT9wPC8kgK6Vjh22ZskrxyRCR71FwNOaYn6jA==} + unifont@0.5.2: + resolution: {integrity: sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg==} unist-util-find-after@5.0.0: resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} @@ -4064,8 +3762,8 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - unstorage@1.16.0: - resolution: {integrity: sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA==} + unstorage@1.17.1: + resolution: {integrity: sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ==} peerDependencies: '@azure/app-configuration': ^1.8.0 '@azure/cosmos': ^4.2.0 @@ -4075,10 +3773,11 @@ packages: '@azure/storage-blob': ^12.26.0 '@capacitor/preferences': ^6.0.3 || ^7.0.0 '@deno/kv': '>=0.9.0' - '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 '@planetscale/database': ^1.19.0 '@upstash/redis': ^1.34.3 '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 '@vercel/kv': ^1.0.1 aws4fetch: ^1.0.20 db0: '>=0.2.1' @@ -4110,6 +3809,8 @@ packages: optional: true '@vercel/blob': optional: true + '@vercel/functions': + optional: true '@vercel/kv': optional: true aws4fetch: @@ -4150,8 +3851,8 @@ packages: vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} - vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} @@ -4161,8 +3862,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@6.3.5: - resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} + vite@6.3.6: + resolution: {integrity: sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -4201,8 +3902,8 @@ packages: yaml: optional: true - vite@7.0.0: - resolution: {integrity: sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==} + vite@7.1.9: + resolution: {integrity: sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4241,10 +3942,10 @@ packages: yaml: optional: true - vitefu@1.0.6: - resolution: {integrity: sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==} + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 peerDependenciesMeta: vite: optional: true @@ -4336,11 +4037,11 @@ packages: '@volar/language-service': optional: true - vscode-css-languageservice@6.3.5: - resolution: {integrity: sha512-ehEIMXYPYEz/5Svi2raL9OKLpBt5dSAdoCFoLpo0TVFKrVpDemyuQwS3c3D552z/qQCg3pMp8oOLMObY6M3ajQ==} + vscode-css-languageservice@6.3.8: + resolution: {integrity: sha512-dBk/9ullEjIMbfSYAohGpDOisOVU1x2MQHOeU12ohGJQI7+r0PCimBwaa/pWpxl/vH4f7ibrBfxIZY3anGmHKQ==} - vscode-html-languageservice@5.4.0: - resolution: {integrity: sha512-9/cbc90BSYCghmHI7/VbWettHZdC7WYpz2g5gBK6UDUI1MkZbM773Q12uAYJx9jzAiNHPpyo6KzcwmcnugncAQ==} + vscode-html-languageservice@5.5.2: + resolution: {integrity: sha512-QpaUhCjvb7U/qThOzo4V6grwsRE62Jk/vf8BRJZoABlMw3oplLB5uovrvcrLO9vYhkeMiSjyqLnCxbfHzzZqmw==} vscode-json-languageservice@4.1.8: resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==} @@ -4421,8 +4122,8 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - wrap-ansi@9.0.0: - resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} xxhash-wasm@1.1.0: @@ -4447,9 +4148,9 @@ packages: resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==} engines: {node: '>= 14'} - yaml@2.7.1: - resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} - engines: {node: '>= 14'} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} hasBin: true yargs-parser@21.1.1: @@ -4464,12 +4165,12 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - yocto-spinner@0.2.2: - resolution: {integrity: sha512-21rPcM3e4vCpOXThiFRByX8amU5By1R0wNS8Oex+DP3YgC8xdU0vEJ/K8cbPLiIJVosSSysgcFof6s6MSD5/Vw==} + yocto-spinner@0.2.3: + resolution: {integrity: sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==} engines: {node: '>=18.19'} - yoctocolors@2.1.1: - resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} zarrita@0.5.1: @@ -4478,8 +4179,8 @@ packages: zarrita@0.5.3: resolution: {integrity: sha512-to2JRq9nHwYdhWEwJwytHYdMYC6I0f2vKUAQfYkgrwwy8OjShhwmv4sH9drzyV3sLzdy7XS9N1ipdMA/Qjspow==} - zod-to-json-schema@3.24.5: - resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} peerDependencies: zod: ^3.24.1 @@ -4500,11 +4201,6 @@ packages: snapshots: - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.28 - '@astrojs/check@0.9.4(typescript@5.8.3)': dependencies: '@astrojs/language-server': 2.15.4(typescript@5.8.3) @@ -4516,41 +4212,43 @@ snapshots: - prettier - prettier-plugin-astro - '@astrojs/compiler@2.12.0': {} + '@astrojs/compiler@2.13.0': {} '@astrojs/internal-helpers@0.6.1': {} + '@astrojs/internal-helpers@0.7.3': {} + '@astrojs/language-server@2.15.4(typescript@5.8.3)': dependencies: - '@astrojs/compiler': 2.12.0 + '@astrojs/compiler': 2.13.0 '@astrojs/yaml2ts': 0.2.2 - '@jridgewell/sourcemap-codec': 1.5.0 - '@volar/kit': 2.4.13(typescript@5.8.3) - '@volar/language-core': 2.4.13 - '@volar/language-server': 2.4.13 - '@volar/language-service': 2.4.13 + '@jridgewell/sourcemap-codec': 1.5.5 + '@volar/kit': 2.4.23(typescript@5.8.3) + '@volar/language-core': 2.4.23 + '@volar/language-server': 2.4.23 + '@volar/language-service': 2.4.23 fast-glob: 3.3.3 muggle-string: 0.4.1 - volar-service-css: 0.0.62(@volar/language-service@2.4.13) - volar-service-emmet: 0.0.62(@volar/language-service@2.4.13) - volar-service-html: 0.0.62(@volar/language-service@2.4.13) - volar-service-prettier: 0.0.62(@volar/language-service@2.4.13) - volar-service-typescript: 0.0.62(@volar/language-service@2.4.13) - volar-service-typescript-twoslash-queries: 0.0.62(@volar/language-service@2.4.13) - volar-service-yaml: 0.0.62(@volar/language-service@2.4.13) - vscode-html-languageservice: 5.4.0 + volar-service-css: 0.0.62(@volar/language-service@2.4.23) + volar-service-emmet: 0.0.62(@volar/language-service@2.4.23) + volar-service-html: 0.0.62(@volar/language-service@2.4.23) + volar-service-prettier: 0.0.62(@volar/language-service@2.4.23) + volar-service-typescript: 0.0.62(@volar/language-service@2.4.23) + volar-service-typescript-twoslash-queries: 0.0.62(@volar/language-service@2.4.23) + volar-service-yaml: 0.0.62(@volar/language-service@2.4.23) + vscode-html-languageservice: 5.5.2 vscode-uri: 3.1.0 transitivePeerDependencies: - typescript - '@astrojs/markdown-remark@6.3.1': + '@astrojs/markdown-remark@6.3.2': dependencies: '@astrojs/internal-helpers': 0.6.1 - '@astrojs/prism': 3.2.0 + '@astrojs/prism': 3.3.0 github-slugger: 2.0.0 hast-util-from-html: 2.0.3 hast-util-to-text: 4.0.2 - import-meta-resolve: 4.1.0 + import-meta-resolve: 4.2.0 js-yaml: 4.1.0 mdast-util-definitions: 6.0.0 rehype-raw: 7.0.0 @@ -4559,8 +4257,8 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.2 remark-smartypants: 3.0.2 - shiki: 3.4.0 - smol-toml: 1.3.4 + shiki: 3.13.0 + smol-toml: 1.4.2 unified: 11.0.5 unist-util-remove-position: 5.0.0 unist-util-visit: 5.0.0 @@ -4569,14 +4267,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/markdown-remark@6.3.2': + '@astrojs/markdown-remark@6.3.7': dependencies: - '@astrojs/internal-helpers': 0.6.1 + '@astrojs/internal-helpers': 0.7.3 '@astrojs/prism': 3.3.0 github-slugger: 2.0.0 hast-util-from-html: 2.0.3 hast-util-to-text: 4.0.2 - import-meta-resolve: 4.1.0 + import-meta-resolve: 4.2.0 js-yaml: 4.1.0 mdast-util-definitions: 6.0.0 rehype-raw: 7.0.0 @@ -4585,8 +4283,8 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.2 remark-smartypants: 3.0.2 - shiki: 3.4.0 - smol-toml: 1.3.4 + shiki: 3.13.0 + smol-toml: 1.4.2 unified: 11.0.5 unist-util-remove-position: 5.0.0 unist-util-visit: 5.0.0 @@ -4595,12 +4293,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@4.3.0(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1))': + '@astrojs/mdx@4.3.0(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1))': dependencies: '@astrojs/markdown-remark': 6.3.2 - '@mdx-js/mdx': 3.1.0(acorn@8.14.1) - acorn: 8.14.1 - astro: 5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1) + '@mdx-js/mdx': 3.1.1 + acorn: 8.15.0 + astro: 5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1) es-module-lexer: 1.7.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -4608,29 +4306,25 @@ snapshots: rehype-raw: 7.0.0 remark-gfm: 4.0.1 remark-smartypants: 3.0.2 - source-map: 0.7.4 + source-map: 0.7.6 unist-util-visit: 5.0.0 vfile: 6.0.3 transitivePeerDependencies: - supports-color - '@astrojs/prism@3.2.0': - dependencies: - prismjs: 1.30.0 - '@astrojs/prism@3.3.0': dependencies: prismjs: 1.30.0 - '@astrojs/react@4.3.0(@types/node@22.1.0)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yaml@2.7.1)': + '@astrojs/react@4.3.0(@types/node@22.1.0)(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(lightningcss@1.30.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yaml@2.8.1)': dependencies: - '@types/react': 19.1.3 - '@types/react-dom': 19.1.3(@types/react@19.1.3) - '@vitejs/plugin-react': 4.4.1(vite@6.3.5(@types/node@22.1.0)(lightningcss@1.30.1)(yaml@2.7.1)) + '@types/react': 19.2.2 + '@types/react-dom': 19.2.1(@types/react@19.2.2) + '@vitejs/plugin-react': 4.7.0(vite@6.3.6(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1)) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) ultrahtml: 1.6.0 - vite: 6.3.5(@types/node@22.1.0)(lightningcss@1.30.1)(yaml@2.7.1) + vite: 6.3.6(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -4645,23 +4339,23 @@ snapshots: - tsx - yaml - '@astrojs/sitemap@3.3.1': + '@astrojs/sitemap@3.6.0': dependencies: sitemap: 8.0.0 stream-replace-string: 2.0.0 zod: 3.25.76 - '@astrojs/starlight@0.34.3(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1))': + '@astrojs/starlight@0.34.3(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1))': dependencies: - '@astrojs/markdown-remark': 6.3.1 - '@astrojs/mdx': 4.3.0(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1)) - '@astrojs/sitemap': 3.3.1 - '@pagefind/default-ui': 1.3.0 + '@astrojs/markdown-remark': 6.3.7 + '@astrojs/mdx': 4.3.0(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1)) + '@astrojs/sitemap': 3.6.0 + '@pagefind/default-ui': 1.4.0 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1) - astro-expressive-code: 0.41.2(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1)) + astro: 5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1) + astro-expressive-code: 0.41.3(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 @@ -4673,7 +4367,7 @@ snapshots: mdast-util-directive: 3.1.0 mdast-util-to-markdown: 2.1.2 mdast-util-to-string: 4.0.0 - pagefind: 1.3.0 + pagefind: 1.4.0 rehype: 13.0.2 rehype-format: 5.0.1 remark-directive: 3.0.1 @@ -4686,8 +4380,8 @@ snapshots: '@astrojs/telemetry@3.3.0': dependencies: - ci-info: 4.2.0 - debug: 4.4.0 + ci-info: 4.3.1 + debug: 4.4.3 dlv: 1.1.3 dset: 3.1.4 is-docker: 3.0.0 @@ -4698,7 +4392,7 @@ snapshots: '@astrojs/yaml2ts@0.2.2': dependencies: - yaml: 2.7.1 + yaml: 2.8.1 '@babel/code-frame@7.27.1': dependencies: @@ -4706,94 +4400,59 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.27.3': {} - - '@babel/core@7.27.1': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.1) - '@babel/helpers': 7.27.4 - '@babel/parser': 7.27.4 - '@babel/template': 7.27.2 - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.3 - convert-source-map: 2.0.0 - debug: 4.4.1 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color + '@babel/compat-data@7.28.4': {} - '@babel/core@7.27.7': + '@babel/core@7.28.4': dependencies: - '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 + '@babel/generator': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.7) - '@babel/helpers': 7.27.6 - '@babel/parser': 7.27.7 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 '@babel/template': 7.27.2 - '@babel/traverse': 7.27.7 - '@babel/types': 7.27.7 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/generator@7.27.3': + '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.27.7 - '@babel/types': 7.27.3 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.28 - jsesc: 3.1.0 - - '@babel/generator@7.27.5': - dependencies: - '@babel/parser': 7.27.7 - '@babel/types': 7.27.7 - '@jridgewell/gen-mapping': 0.3.11 - '@jridgewell/trace-mapping': 0.3.28 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.27.3 + '@babel/compat-data': 7.28.4 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.1 + browserslist: 4.26.3 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-module-imports@7.27.1': - dependencies: - '@babel/traverse': 7.27.4 - '@babel/types': 7.27.3 - transitivePeerDependencies: - - supports-color + '@babel/helper-globals@7.28.0': {} - '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.1)': + '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/core': 7.27.1 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.4 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.7)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.27.7 + '@babel/core': 7.28.4 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.27.4 + '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -4805,83 +4464,46 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.27.4': + '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.27.3 + '@babel/types': 7.28.4 - '@babel/helpers@7.27.6': + '@babel/parser@7.28.4': dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.27.7 + '@babel/types': 7.28.4 - '@babel/parser@7.27.1': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/types': 7.27.1 - - '@babel/parser@7.27.4': - dependencies: - '@babel/types': 7.27.3 - - '@babel/parser@7.27.7': - dependencies: - '@babel/types': 7.27.7 - - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)': - dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.1)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/runtime@7.27.0': - dependencies: - regenerator-runtime: 0.14.1 + '@babel/runtime@7.28.4': {} '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.27.7 - '@babel/types': 7.27.3 - - '@babel/traverse@7.27.4': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.3 - '@babel/parser': 7.27.7 - '@babel/template': 7.27.2 - '@babel/types': 7.27.3 - debug: 4.4.1 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 - '@babel/traverse@7.27.7': + '@babel/traverse@7.28.4': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.27.5 - '@babel/parser': 7.27.7 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 '@babel/template': 7.27.2 - '@babel/types': 7.27.7 - debug: 4.4.1 - globals: 11.12.0 + '@babel/types': 7.28.4 + debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.27.1': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - - '@babel/types@7.27.3': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - - '@babel/types@7.27.7': + '@babel/types@7.28.4': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -4929,18 +4551,18 @@ snapshots: transitivePeerDependencies: - encoding - '@ctrl/tinycolor@4.1.0': {} + '@ctrl/tinycolor@4.2.0': {} - '@czi-sds/components@20.0.1(yhvdhk7sonewaw25ryh4hrzn6m)': + '@czi-sds/components@20.0.1(3d6g66iwpfly7u54dlbfcchst4)': dependencies: '@emotion/core': 11.0.0 '@emotion/css': 11.11.2 - '@emotion/react': 11.11.4(@types/react@19.1.3)(react@19.1.0) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0) - '@mui/base': 5.0.0-beta.40(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@mui/icons-material': 5.15.15(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.3)(react@19.1.0) - '@mui/lab': 5.0.0-alpha.175(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@mui/material': 5.15.15(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@emotion/react': 11.11.4(@types/react@19.2.2)(react@19.1.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0) + '@mui/base': 5.0.0-beta.40(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/icons-material': 5.15.15(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.2.2)(react@19.1.0) + '@mui/lab': 5.0.0-alpha.175(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/material': 5.15.15(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -4967,18 +4589,18 @@ snapshots: '@emmetio/stream-reader@2.2.0': {} - '@emnapi/runtime@1.4.3': + '@emnapi/runtime@1.5.0': dependencies: - tslib: 2.6.2 + tslib: 2.8.1 optional: true - '@emotion/babel-plugin@11.11.0': + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.27.1 - '@babel/runtime': 7.27.0 - '@emotion/hash': 0.9.1 - '@emotion/memoize': 0.8.1 - '@emotion/serialize': 1.1.4 + '@babel/runtime': 7.28.4 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 babel-plugin-macros: 3.1.0 convert-source-map: 1.9.0 escape-string-regexp: 4.0.0 @@ -5000,68 +4622,66 @@ snapshots: '@emotion/css@11.11.2': dependencies: - '@emotion/babel-plugin': 11.11.0 + '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 - '@emotion/serialize': 1.1.4 + '@emotion/serialize': 1.3.3 '@emotion/sheet': 1.4.0 '@emotion/utils': 1.4.2 transitivePeerDependencies: - supports-color - '@emotion/hash@0.9.1': {} + '@emotion/hash@0.9.2': {} - '@emotion/is-prop-valid@1.2.2': + '@emotion/is-prop-valid@1.4.0': dependencies: - '@emotion/memoize': 0.8.1 - - '@emotion/memoize@0.8.1': {} + '@emotion/memoize': 0.9.0 '@emotion/memoize@0.9.0': {} - '@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0)': + '@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.0 - '@emotion/babel-plugin': 11.11.0 + '@babel/runtime': 7.28.4 + '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 - '@emotion/serialize': 1.1.4 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@19.1.0) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) '@emotion/utils': 1.4.2 '@emotion/weak-memoize': 0.3.1 hoist-non-react-statics: 3.3.2 react: 19.1.0 optionalDependencies: - '@types/react': 19.1.3 + '@types/react': 19.2.2 transitivePeerDependencies: - supports-color - '@emotion/serialize@1.1.4': + '@emotion/serialize@1.3.3': dependencies: - '@emotion/hash': 0.9.1 - '@emotion/memoize': 0.8.1 - '@emotion/unitless': 0.8.1 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 '@emotion/utils': 1.4.2 csstype: 3.1.3 '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0)': + '@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.0 - '@emotion/babel-plugin': 11.11.0 - '@emotion/is-prop-valid': 1.2.2 - '@emotion/react': 11.11.4(@types/react@19.1.3)(react@19.1.0) - '@emotion/serialize': 1.1.4 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@19.1.0) + '@babel/runtime': 7.28.4 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.4.0 + '@emotion/react': 11.11.4(@types/react@19.2.2)(react@19.1.0) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) '@emotion/utils': 1.4.2 react: 19.1.0 optionalDependencies: - '@types/react': 19.1.3 + '@types/react': 19.2.2 transitivePeerDependencies: - supports-color - '@emotion/unitless@0.8.1': {} + '@emotion/unitless@0.10.0': {} - '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@19.1.0)': + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.0)': dependencies: react: 19.1.0 @@ -5071,159 +4691,87 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} - '@esbuild/aix-ppc64@0.25.4': - optional: true - - '@esbuild/aix-ppc64@0.25.5': - optional: true - - '@esbuild/android-arm64@0.25.4': - optional: true - - '@esbuild/android-arm64@0.25.5': - optional: true - - '@esbuild/android-arm@0.25.4': - optional: true - - '@esbuild/android-arm@0.25.5': - optional: true - - '@esbuild/android-x64@0.25.4': - optional: true - - '@esbuild/android-x64@0.25.5': - optional: true - - '@esbuild/darwin-arm64@0.25.4': - optional: true - - '@esbuild/darwin-arm64@0.25.5': - optional: true - - '@esbuild/darwin-x64@0.25.4': + '@esbuild/aix-ppc64@0.25.10': optional: true - '@esbuild/darwin-x64@0.25.5': + '@esbuild/android-arm64@0.25.10': optional: true - '@esbuild/freebsd-arm64@0.25.4': + '@esbuild/android-arm@0.25.10': optional: true - '@esbuild/freebsd-arm64@0.25.5': + '@esbuild/android-x64@0.25.10': optional: true - '@esbuild/freebsd-x64@0.25.4': + '@esbuild/darwin-arm64@0.25.10': optional: true - '@esbuild/freebsd-x64@0.25.5': + '@esbuild/darwin-x64@0.25.10': optional: true - '@esbuild/linux-arm64@0.25.4': + '@esbuild/freebsd-arm64@0.25.10': optional: true - '@esbuild/linux-arm64@0.25.5': + '@esbuild/freebsd-x64@0.25.10': optional: true - '@esbuild/linux-arm@0.25.4': + '@esbuild/linux-arm64@0.25.10': optional: true - '@esbuild/linux-arm@0.25.5': + '@esbuild/linux-arm@0.25.10': optional: true - '@esbuild/linux-ia32@0.25.4': + '@esbuild/linux-ia32@0.25.10': optional: true - '@esbuild/linux-ia32@0.25.5': + '@esbuild/linux-loong64@0.25.10': optional: true - '@esbuild/linux-loong64@0.25.4': + '@esbuild/linux-mips64el@0.25.10': optional: true - '@esbuild/linux-loong64@0.25.5': + '@esbuild/linux-ppc64@0.25.10': optional: true - '@esbuild/linux-mips64el@0.25.4': + '@esbuild/linux-riscv64@0.25.10': optional: true - '@esbuild/linux-mips64el@0.25.5': + '@esbuild/linux-s390x@0.25.10': optional: true - '@esbuild/linux-ppc64@0.25.4': + '@esbuild/linux-x64@0.25.10': optional: true - '@esbuild/linux-ppc64@0.25.5': + '@esbuild/netbsd-arm64@0.25.10': optional: true - '@esbuild/linux-riscv64@0.25.4': + '@esbuild/netbsd-x64@0.25.10': optional: true - '@esbuild/linux-riscv64@0.25.5': + '@esbuild/openbsd-arm64@0.25.10': optional: true - '@esbuild/linux-s390x@0.25.4': + '@esbuild/openbsd-x64@0.25.10': optional: true - '@esbuild/linux-s390x@0.25.5': + '@esbuild/openharmony-arm64@0.25.10': optional: true - '@esbuild/linux-x64@0.25.4': + '@esbuild/sunos-x64@0.25.10': optional: true - '@esbuild/linux-x64@0.25.5': + '@esbuild/win32-arm64@0.25.10': optional: true - '@esbuild/netbsd-arm64@0.25.4': + '@esbuild/win32-ia32@0.25.10': optional: true - '@esbuild/netbsd-arm64@0.25.5': - optional: true - - '@esbuild/netbsd-x64@0.25.4': - optional: true - - '@esbuild/netbsd-x64@0.25.5': - optional: true - - '@esbuild/openbsd-arm64@0.25.4': - optional: true - - '@esbuild/openbsd-arm64@0.25.5': - optional: true - - '@esbuild/openbsd-x64@0.25.4': - optional: true - - '@esbuild/openbsd-x64@0.25.5': - optional: true - - '@esbuild/sunos-x64@0.25.4': - optional: true - - '@esbuild/sunos-x64@0.25.5': - optional: true - - '@esbuild/win32-arm64@0.25.4': - optional: true - - '@esbuild/win32-arm64@0.25.5': - optional: true - - '@esbuild/win32-ia32@0.25.4': - optional: true - - '@esbuild/win32-ia32@0.25.5': - optional: true - - '@esbuild/win32-x64@0.25.4': - optional: true - - '@esbuild/win32-x64@0.25.5': + '@esbuild/win32-x64@0.25.10': optional: true '@expressive-code/core@0.41.3': dependencies: - '@ctrl/tinycolor': 4.1.0 + '@ctrl/tinycolor': 4.2.0 hast-util-select: 6.0.4 hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 @@ -5240,28 +4788,28 @@ snapshots: '@expressive-code/plugin-shiki@0.41.3': dependencies: '@expressive-code/core': 0.41.3 - shiki: 3.4.0 + shiki: 3.13.0 '@expressive-code/plugin-text-markers@0.41.3': dependencies: '@expressive-code/core': 0.41.3 - '@floating-ui/core@1.6.1': + '@floating-ui/core@1.7.3': dependencies: - '@floating-ui/utils': 0.2.2 + '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.6.4': + '@floating-ui/dom@1.7.4': dependencies: - '@floating-ui/core': 1.6.1 - '@floating-ui/utils': 0.2.2 + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.0.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@floating-ui/react-dom@2.1.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@floating-ui/dom': 1.6.4 + '@floating-ui/dom': 1.7.4 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@floating-ui/utils@0.2.2': {} + '@floating-ui/utils@0.2.10': {} '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: @@ -5396,12 +4944,12 @@ snapshots: '@img/sharp-wasm32@0.33.5': dependencies: - '@emnapi/runtime': 1.4.3 + '@emnapi/runtime': 1.5.0 optional: true '@img/sharp-wasm32@0.34.1': dependencies: - '@emnapi/runtime': 1.4.3 + '@emnapi/runtime': 1.5.0 optional: true '@img/sharp-win32-ia32@0.33.5': @@ -5420,36 +4968,31 @@ snapshots: dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 '@istanbuljs/schema@0.1.3': {} - '@jridgewell/gen-mapping@0.3.11': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.3 - '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/gen-mapping@0.3.8': + '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.3 - '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/sourcemap-codec@1.5.3': {} + '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.28': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.3 + '@jridgewell/sourcemap-codec': 1.5.5 '@lezer/common@1.2.3': {} @@ -5475,12 +5018,13 @@ snapshots: '@lmdb/lmdb-win32-x64@2.8.5': optional: true - '@mdx-js/mdx@3.1.0(acorn@8.14.1)': + '@mdx-js/mdx@3.1.1': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdx': 2.0.13 + acorn: 8.15.0 collapse-white-space: 2.1.0 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 @@ -5489,20 +5033,19 @@ snapshots: hast-util-to-jsx-runtime: 2.3.6 markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 - recma-jsx: 1.0.0(acorn@8.14.1) + recma-jsx: 1.0.1(acorn@8.15.0) recma-stringify: 1.0.0 rehype-recma: 1.0.0 - remark-mdx: 3.1.0 + remark-mdx: 3.1.1 remark-parse: 11.0.0 remark-rehype: 11.1.2 - source-map: 0.7.4 + source-map: 0.7.6 unified: 11.0.5 unist-util-position-from-estree: 2.0.0 unist-util-stringify-position: 4.0.0 unist-util-visit: 5.0.0 vfile: 6.0.3 transitivePeerDependencies: - - acorn - supports-color '@mischnic/json-sourcemap@0.1.1': @@ -5529,139 +5072,140 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true - '@mui/base@5.0.0-beta.40(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/base@5.0.0-beta.40(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.0 - '@floating-ui/react-dom': 2.0.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@mui/types': 7.4.1(@types/react@19.1.3) - '@mui/utils': 5.17.1(@types/react@19.1.3)(react@19.1.0) + '@babel/runtime': 7.28.4 + '@floating-ui/react-dom': 2.1.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/types': 7.4.7(@types/react@19.2.2) + '@mui/utils': 5.17.1(@types/react@19.2.2)(react@19.1.0) '@popperjs/core': 2.11.8 clsx: 2.1.1 prop-types: 15.8.1 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: - '@types/react': 19.1.3 + '@types/react': 19.2.2 - '@mui/base@5.0.0-beta.40-0(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/base@5.0.0-beta.40-0(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.0 - '@floating-ui/react-dom': 2.0.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@mui/types': 7.4.1(@types/react@19.1.3) - '@mui/utils': 5.17.1(@types/react@19.1.3)(react@19.1.0) + '@babel/runtime': 7.28.4 + '@floating-ui/react-dom': 2.1.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/types': 7.4.7(@types/react@19.2.2) + '@mui/utils': 5.17.1(@types/react@19.2.2)(react@19.1.0) '@popperjs/core': 2.11.8 clsx: 2.1.1 prop-types: 15.8.1 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: - '@types/react': 19.1.3 + '@types/react': 19.2.2 - '@mui/core-downloads-tracker@5.15.15': {} + '@mui/core-downloads-tracker@5.18.0': {} - '@mui/icons-material@5.15.15(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.3)(react@19.1.0)': + '@mui/icons-material@5.15.15(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.2.2)(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.0 - '@mui/material': 5.15.15(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@babel/runtime': 7.28.4 + '@mui/material': 5.15.15(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 optionalDependencies: - '@types/react': 19.1.3 + '@types/react': 19.2.2 - '@mui/lab@5.0.0-alpha.175(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/lab@5.0.0-alpha.175(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.0 - '@mui/base': 5.0.0-beta.40-0(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@mui/material': 5.15.15(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@mui/system': 5.17.1(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0) - '@mui/types': 7.4.1(@types/react@19.1.3) - '@mui/utils': 5.17.1(@types/react@19.1.3)(react@19.1.0) + '@babel/runtime': 7.28.4 + '@mui/base': 5.0.0-beta.40-0(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/material': 5.15.15(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/system': 5.18.0(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0) + '@mui/types': 7.4.7(@types/react@19.2.2) + '@mui/utils': 5.17.1(@types/react@19.2.2)(react@19.1.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: - '@emotion/react': 11.11.4(@types/react@19.1.3)(react@19.1.0) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0) - '@types/react': 19.1.3 - - '@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@babel/runtime': 7.27.0 - '@mui/base': 5.0.0-beta.40(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@mui/core-downloads-tracker': 5.15.15 - '@mui/system': 5.17.1(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0) - '@mui/types': 7.4.1(@types/react@19.1.3) - '@mui/utils': 5.17.1(@types/react@19.1.3)(react@19.1.0) - '@types/react-transition-group': 4.4.10 + '@emotion/react': 11.11.4(@types/react@19.2.2)(react@19.1.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0) + '@types/react': 19.2.2 + + '@mui/material@5.15.15(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@mui/base': 5.0.0-beta.40(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/core-downloads-tracker': 5.18.0 + '@mui/system': 5.18.0(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0) + '@mui/types': 7.4.7(@types/react@19.2.2) + '@mui/utils': 5.17.1(@types/react@19.2.2)(react@19.1.0) + '@types/react-transition-group': 4.4.12(@types/react@19.2.2) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - react-is: 18.2.0 + react-is: 18.3.1 react-transition-group: 4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: - '@emotion/react': 11.11.4(@types/react@19.1.3)(react@19.1.0) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0) - '@types/react': 19.1.3 + '@emotion/react': 11.11.4(@types/react@19.2.2)(react@19.1.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0) + '@types/react': 19.2.2 - '@mui/private-theming@5.17.1(@types/react@19.1.3)(react@19.1.0)': + '@mui/private-theming@5.17.1(@types/react@19.2.2)(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.0 - '@mui/utils': 5.17.1(@types/react@19.1.3)(react@19.1.0) + '@babel/runtime': 7.28.4 + '@mui/utils': 5.17.1(@types/react@19.2.2)(react@19.1.0) prop-types: 15.8.1 react: 19.1.0 optionalDependencies: - '@types/react': 19.1.3 + '@types/react': 19.2.2 - '@mui/styled-engine@5.16.14(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(react@19.1.0)': + '@mui/styled-engine@5.18.0(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 csstype: 3.1.3 prop-types: 15.8.1 react: 19.1.0 optionalDependencies: - '@emotion/react': 11.11.4(@types/react@19.1.3)(react@19.1.0) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0) + '@emotion/react': 11.11.4(@types/react@19.2.2)(react@19.1.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0) - '@mui/system@5.17.1(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0)': + '@mui/system@5.18.0(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.0 - '@mui/private-theming': 5.17.1(@types/react@19.1.3)(react@19.1.0) - '@mui/styled-engine': 5.16.14(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0))(react@19.1.0) - '@mui/types': 7.2.24(@types/react@19.1.3) - '@mui/utils': 5.17.1(@types/react@19.1.3)(react@19.1.0) + '@babel/runtime': 7.28.4 + '@mui/private-theming': 5.17.1(@types/react@19.2.2)(react@19.1.0) + '@mui/styled-engine': 5.18.0(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0))(react@19.1.0) + '@mui/types': 7.2.24(@types/react@19.2.2) + '@mui/utils': 5.17.1(@types/react@19.2.2)(react@19.1.0) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 react: 19.1.0 optionalDependencies: - '@emotion/react': 11.11.4(@types/react@19.1.3)(react@19.1.0) - '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@19.1.3)(react@19.1.0))(@types/react@19.1.3)(react@19.1.0) - '@types/react': 19.1.3 + '@emotion/react': 11.11.4(@types/react@19.2.2)(react@19.1.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4(@types/react@19.2.2)(react@19.1.0))(@types/react@19.2.2)(react@19.1.0) + '@types/react': 19.2.2 - '@mui/types@7.2.24(@types/react@19.1.3)': + '@mui/types@7.2.24(@types/react@19.2.2)': optionalDependencies: - '@types/react': 19.1.3 + '@types/react': 19.2.2 - '@mui/types@7.4.1(@types/react@19.1.3)': + '@mui/types@7.4.7(@types/react@19.2.2)': dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 optionalDependencies: - '@types/react': 19.1.3 + '@types/react': 19.2.2 - '@mui/utils@5.17.1(@types/react@19.1.3)(react@19.1.0)': + '@mui/utils@5.17.1(@types/react@19.2.2)(react@19.1.0)': dependencies: - '@babel/runtime': 7.27.0 - '@mui/types': 7.2.24(@types/react@19.1.3) - '@types/prop-types': 15.7.12 + '@babel/runtime': 7.28.4 + '@mui/types': 7.2.24(@types/react@19.2.2) + '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 react: 19.1.0 - react-is: 19.1.0 + react-is: 19.2.0 optionalDependencies: - '@types/react': 19.1.3 + '@types/react': 19.2.2 '@nodelib/fs.scandir@2.1.5': dependencies: @@ -5677,21 +5221,24 @@ snapshots: '@oslojs/encoding@1.1.0': {} - '@pagefind/darwin-arm64@1.3.0': + '@pagefind/darwin-arm64@1.4.0': optional: true - '@pagefind/darwin-x64@1.3.0': + '@pagefind/darwin-x64@1.4.0': optional: true - '@pagefind/default-ui@1.3.0': {} + '@pagefind/default-ui@1.4.0': {} + + '@pagefind/freebsd-x64@1.4.0': + optional: true - '@pagefind/linux-arm64@1.3.0': + '@pagefind/linux-arm64@1.4.0': optional: true - '@pagefind/linux-x64@1.3.0': + '@pagefind/linux-x64@1.4.0': optional: true - '@pagefind/windows-x64@1.3.0': + '@pagefind/windows-x64@1.4.0': optional: true '@parcel/bundler-default@2.15.4(@parcel/core@2.15.4(@swc/helpers@0.5.17))': @@ -5785,14 +5332,14 @@ snapshots: '@parcel/utils': 2.15.4 '@parcel/workers': 2.15.4(@parcel/core@2.15.4(@swc/helpers@0.5.17)) base-x: 3.0.11 - browserslist: 4.25.1 + browserslist: 4.26.3 clone: 2.1.2 dotenv: 16.6.1 dotenv-expand: 11.0.7 json5: 2.2.3 - msgpackr: 1.11.4 + msgpackr: 1.11.5 nullthrows: 1.1.1 - semver: 7.7.2 + semver: 7.7.3 transitivePeerDependencies: - '@swc/helpers' - napi-wasm @@ -5851,7 +5398,7 @@ snapshots: '@parcel/rust': 2.15.4 '@parcel/utils': 2.15.4 nullthrows: 1.1.1 - semver: 7.7.2 + semver: 7.7.3 transitivePeerDependencies: - '@parcel/core' - napi-wasm @@ -5862,8 +5409,8 @@ snapshots: '@parcel/plugin': 2.15.4(@parcel/core@2.15.4(@swc/helpers@0.5.17)) '@parcel/source-map': 2.1.1 '@parcel/utils': 2.15.4 - browserslist: 4.25.1 - lightningcss: 1.30.1 + browserslist: 4.26.3 + lightningcss: 1.30.2 nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' @@ -5904,7 +5451,7 @@ snapshots: '@parcel/plugin': 2.15.4(@parcel/core@2.15.4(@swc/helpers@0.5.17)) '@parcel/source-map': 2.1.1 '@parcel/utils': 2.15.4 - '@swc/core': 1.12.9(@swc/helpers@0.5.17) + '@swc/core': 1.13.5(@swc/helpers@0.5.17) nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' @@ -5921,8 +5468,8 @@ snapshots: '@parcel/types': 2.15.4(@parcel/core@2.15.4(@swc/helpers@0.5.17)) '@parcel/utils': 2.15.4 '@parcel/workers': 2.15.4(@parcel/core@2.15.4(@swc/helpers@0.5.17)) - '@swc/core': 1.12.9(@swc/helpers@0.5.17) - semver: 7.7.2 + '@swc/core': 1.13.5(@swc/helpers@0.5.17) + semver: 7.7.3 transitivePeerDependencies: - '@swc/helpers' - napi-wasm @@ -5933,7 +5480,7 @@ snapshots: '@parcel/plugin': 2.15.4(@parcel/core@2.15.4(@swc/helpers@0.5.17)) '@parcel/source-map': 2.1.1 '@parcel/utils': 2.15.4 - lightningcss: 1.30.1 + lightningcss: 1.30.2 nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' @@ -6129,10 +5676,10 @@ snapshots: '@parcel/plugin': 2.15.4(@parcel/core@2.15.4(@swc/helpers@0.5.17)) '@parcel/source-map': 2.1.1 '@parcel/utils': 2.15.4 - browserslist: 4.25.1 + browserslist: 4.26.3 json5: 2.2.3 nullthrows: 1.1.1 - semver: 7.7.2 + semver: 7.7.3 transitivePeerDependencies: - '@parcel/core' - napi-wasm @@ -6143,8 +5690,8 @@ snapshots: '@parcel/plugin': 2.15.4(@parcel/core@2.15.4(@swc/helpers@0.5.17)) '@parcel/source-map': 2.1.1 '@parcel/utils': 2.15.4 - browserslist: 4.25.1 - lightningcss: 1.30.1 + browserslist: 4.26.3 + lightningcss: 1.30.2 nullthrows: 1.1.1 transitivePeerDependencies: - '@parcel/core' @@ -6179,10 +5726,10 @@ snapshots: '@parcel/utils': 2.15.4 '@parcel/workers': 2.15.4(@parcel/core@2.15.4(@swc/helpers@0.5.17)) '@swc/helpers': 0.5.17 - browserslist: 4.25.1 + browserslist: 4.26.3 nullthrows: 1.1.1 regenerator-runtime: 0.14.1 - semver: 7.7.2 + semver: 7.7.3 transitivePeerDependencies: - napi-wasm @@ -6210,7 +5757,7 @@ snapshots: clone: 2.1.2 nullthrows: 1.1.1 postcss-value-parser: 4.2.0 - semver: 7.7.2 + semver: 7.7.3 transitivePeerDependencies: - '@parcel/core' - napi-wasm @@ -6372,163 +5919,111 @@ snapshots: '@popperjs/core@2.11.8': {} - '@rollup/pluginutils@5.1.4(rollup@4.44.1)': + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/pluginutils@5.3.0(rollup@4.52.4)': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.2 + picomatch: 4.0.3 optionalDependencies: - rollup: 4.44.1 - - '@rollup/rollup-android-arm-eabi@4.40.2': - optional: true - - '@rollup/rollup-android-arm-eabi@4.44.1': - optional: true - - '@rollup/rollup-android-arm64@4.40.2': - optional: true - - '@rollup/rollup-android-arm64@4.44.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.40.2': - optional: true - - '@rollup/rollup-darwin-arm64@4.44.1': - optional: true - - '@rollup/rollup-darwin-x64@4.40.2': - optional: true - - '@rollup/rollup-darwin-x64@4.44.1': - optional: true + rollup: 4.52.4 - '@rollup/rollup-freebsd-arm64@4.40.2': + '@rollup/rollup-android-arm-eabi@4.52.4': optional: true - '@rollup/rollup-freebsd-arm64@4.44.1': + '@rollup/rollup-android-arm64@4.52.4': optional: true - '@rollup/rollup-freebsd-x64@4.40.2': + '@rollup/rollup-darwin-arm64@4.52.4': optional: true - '@rollup/rollup-freebsd-x64@4.44.1': + '@rollup/rollup-darwin-x64@4.52.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.40.2': + '@rollup/rollup-freebsd-arm64@4.52.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.44.1': + '@rollup/rollup-freebsd-x64@4.52.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.40.2': + '@rollup/rollup-linux-arm-gnueabihf@4.52.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.44.1': + '@rollup/rollup-linux-arm-musleabihf@4.52.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.40.2': + '@rollup/rollup-linux-arm64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.44.1': + '@rollup/rollup-linux-arm64-musl@4.52.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.40.2': + '@rollup/rollup-linux-loong64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.44.1': + '@rollup/rollup-linux-ppc64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.40.2': + '@rollup/rollup-linux-riscv64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.44.1': + '@rollup/rollup-linux-riscv64-musl@4.52.4': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': + '@rollup/rollup-linux-s390x-gnu@4.52.4': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': + '@rollup/rollup-linux-x64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.40.2': + '@rollup/rollup-linux-x64-musl@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.44.1': + '@rollup/rollup-openharmony-arm64@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.40.2': + '@rollup/rollup-win32-arm64-msvc@4.52.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.44.1': + '@rollup/rollup-win32-ia32-msvc@4.52.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.40.2': + '@rollup/rollup-win32-x64-gnu@4.52.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.44.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.40.2': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.44.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.40.2': - optional: true - - '@rollup/rollup-linux-x64-musl@4.44.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.40.2': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.44.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.40.2': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.44.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.40.2': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.44.1': + '@rollup/rollup-win32-x64-msvc@4.52.4': optional: true '@sec-ant/readable-stream@0.4.1': {} - '@shikijs/core@3.4.0': + '@shikijs/core@3.13.0': dependencies: - '@shikijs/types': 3.4.0 + '@shikijs/types': 3.13.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@3.4.0': + '@shikijs/engine-javascript@3.13.0': dependencies: - '@shikijs/types': 3.4.0 + '@shikijs/types': 3.13.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.3 - '@shikijs/engine-oniguruma@3.4.0': + '@shikijs/engine-oniguruma@3.13.0': dependencies: - '@shikijs/types': 3.4.0 + '@shikijs/types': 3.13.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.4.0': + '@shikijs/langs@3.13.0': dependencies: - '@shikijs/types': 3.4.0 + '@shikijs/types': 3.13.0 - '@shikijs/themes@3.4.0': + '@shikijs/themes@3.13.0': dependencies: - '@shikijs/types': 3.4.0 + '@shikijs/types': 3.13.0 - '@shikijs/types@3.4.0': + '@shikijs/types@3.13.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -6537,51 +6032,51 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@swc/core-darwin-arm64@1.12.9': + '@swc/core-darwin-arm64@1.13.5': optional: true - '@swc/core-darwin-x64@1.12.9': + '@swc/core-darwin-x64@1.13.5': optional: true - '@swc/core-linux-arm-gnueabihf@1.12.9': + '@swc/core-linux-arm-gnueabihf@1.13.5': optional: true - '@swc/core-linux-arm64-gnu@1.12.9': + '@swc/core-linux-arm64-gnu@1.13.5': optional: true - '@swc/core-linux-arm64-musl@1.12.9': + '@swc/core-linux-arm64-musl@1.13.5': optional: true - '@swc/core-linux-x64-gnu@1.12.9': + '@swc/core-linux-x64-gnu@1.13.5': optional: true - '@swc/core-linux-x64-musl@1.12.9': + '@swc/core-linux-x64-musl@1.13.5': optional: true - '@swc/core-win32-arm64-msvc@1.12.9': + '@swc/core-win32-arm64-msvc@1.13.5': optional: true - '@swc/core-win32-ia32-msvc@1.12.9': + '@swc/core-win32-ia32-msvc@1.13.5': optional: true - '@swc/core-win32-x64-msvc@1.12.9': + '@swc/core-win32-x64-msvc@1.13.5': optional: true - '@swc/core@1.12.9(@swc/helpers@0.5.17)': + '@swc/core@1.13.5(@swc/helpers@0.5.17)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.23 + '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.12.9 - '@swc/core-darwin-x64': 1.12.9 - '@swc/core-linux-arm-gnueabihf': 1.12.9 - '@swc/core-linux-arm64-gnu': 1.12.9 - '@swc/core-linux-arm64-musl': 1.12.9 - '@swc/core-linux-x64-gnu': 1.12.9 - '@swc/core-linux-x64-musl': 1.12.9 - '@swc/core-win32-arm64-msvc': 1.12.9 - '@swc/core-win32-ia32-msvc': 1.12.9 - '@swc/core-win32-x64-msvc': 1.12.9 + '@swc/core-darwin-arm64': 1.13.5 + '@swc/core-darwin-x64': 1.13.5 + '@swc/core-linux-arm-gnueabihf': 1.13.5 + '@swc/core-linux-arm64-gnu': 1.13.5 + '@swc/core-linux-arm64-musl': 1.13.5 + '@swc/core-linux-x64-gnu': 1.13.5 + '@swc/core-linux-x64-musl': 1.13.5 + '@swc/core-win32-arm64-msvc': 1.13.5 + '@swc/core-win32-ia32-msvc': 1.13.5 + '@swc/core-win32-x64-msvc': 1.13.5 '@swc/helpers': 0.5.17 '@swc/counter@0.1.3': {} @@ -6590,30 +6085,30 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/types@0.1.23': + '@swc/types@0.1.25': dependencies: '@swc/counter': 0.1.3 '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.27.1 - '@babel/types': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.7 + '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.27.1 + '@babel/types': 7.28.4 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.27.7 - '@babel/types': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 - '@types/babel__traverse@7.20.7': + '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.27.1 + '@babel/types': 7.28.4 '@types/chai@5.2.2': dependencies: @@ -6629,8 +6124,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - '@types/estree@1.0.7': {} - '@types/estree@1.0.8': {} '@types/file-saver@2.0.7': {} @@ -6669,17 +6162,17 @@ snapshots: '@types/parse-json@4.0.2': {} - '@types/prop-types@15.7.12': {} + '@types/prop-types@15.7.15': {} - '@types/react-dom@19.1.3(@types/react@19.1.3)': + '@types/react-dom@19.2.1(@types/react@19.2.2)': dependencies: - '@types/react': 19.1.3 + '@types/react': 19.2.2 - '@types/react-transition-group@4.4.10': + '@types/react-transition-group@4.4.12(@types/react@19.2.2)': dependencies: - '@types/react': 19.1.3 + '@types/react': 19.2.2 - '@types/react@19.1.3': + '@types/react@19.2.2': dependencies: csstype: 3.1.3 @@ -6693,30 +6186,31 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@22.1.0)(lightningcss@1.30.1)(yaml@2.7.1))': + '@vitejs/plugin-react@4.7.0(vite@6.3.6(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1))': dependencies: - '@babel/core': 7.27.1 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1) + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.1.0)(lightningcss@1.30.1)(yaml@2.7.1) + vite: 6.3.6(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(lightningcss@1.30.1)(yaml@2.7.1))': + '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(lightningcss@1.30.2)(yaml@2.8.1))': dependencies: '@istanbuljs/schema': 0.1.3 - debug: 4.4.1 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.1.7 + istanbul-reports: 3.2.0 magicast: 0.3.5 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(lightningcss@1.30.1)(yaml@2.7.1) + vitest: 3.2.4(@types/debug@4.1.12)(lightningcss@1.30.2)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -6725,16 +6219,16 @@ snapshots: '@types/chai': 5.2.2 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 - chai: 5.2.0 + chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.0.0(lightningcss@1.30.1)(yaml@2.7.1))': + '@vitest/mocker@3.2.4(vite@7.1.9(lightningcss@1.30.2)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.17 + magic-string: 0.30.19 optionalDependencies: - vite: 7.0.0(lightningcss@1.30.1)(yaml@2.7.1) + vite: 7.1.9(lightningcss@1.30.2)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -6744,42 +6238,42 @@ snapshots: dependencies: '@vitest/utils': 3.2.4 pathe: 2.0.3 - strip-literal: 3.0.0 + strip-literal: 3.1.0 '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.17 + magic-string: 0.30.19 pathe: 2.0.3 '@vitest/spy@3.2.4': dependencies: - tinyspy: 4.0.3 + tinyspy: 4.0.4 '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - loupe: 3.1.4 + loupe: 3.2.1 tinyrainbow: 2.0.0 - '@volar/kit@2.4.13(typescript@5.8.3)': + '@volar/kit@2.4.23(typescript@5.8.3)': dependencies: - '@volar/language-service': 2.4.13 - '@volar/typescript': 2.4.13 + '@volar/language-service': 2.4.23 + '@volar/typescript': 2.4.23 typesafe-path: 0.2.2 typescript: 5.8.3 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 - '@volar/language-core@2.4.13': + '@volar/language-core@2.4.23': dependencies: - '@volar/source-map': 2.4.13 + '@volar/source-map': 2.4.23 - '@volar/language-server@2.4.13': + '@volar/language-server@2.4.23': dependencies: - '@volar/language-core': 2.4.13 - '@volar/language-service': 2.4.13 - '@volar/typescript': 2.4.13 + '@volar/language-core': 2.4.23 + '@volar/language-service': 2.4.23 + '@volar/typescript': 2.4.23 path-browserify: 1.0.1 request-light: 0.7.0 vscode-languageserver: 9.0.1 @@ -6787,18 +6281,18 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 - '@volar/language-service@2.4.13': + '@volar/language-service@2.4.23': dependencies: - '@volar/language-core': 2.4.13 + '@volar/language-core': 2.4.23 vscode-languageserver-protocol: 3.17.5 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 - '@volar/source-map@2.4.13': {} + '@volar/source-map@2.4.23': {} - '@volar/typescript@2.4.13': + '@volar/typescript@2.4.23': dependencies: - '@volar/language-core': 2.4.13 + '@volar/language-core': 2.4.23 path-browserify: 1.0.1 vscode-uri: 3.1.0 @@ -6812,26 +6306,21 @@ snapshots: '@vscode/l10n@0.0.18': {} - '@zarrita/storage@0.1.1': - dependencies: - reference-spec-reader: 0.2.0 - unzipit: 1.4.3 - '@zarrita/storage@0.1.3': dependencies: reference-spec-reader: 0.2.0 unzipit: 1.4.3 - acorn-jsx@5.3.2(acorn@8.14.1): + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: - acorn: 8.14.1 + acorn: 8.15.0 - acorn@8.14.1: {} + acorn@8.15.0: {} ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.0.6 + fast-uri: 3.1.0 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -6841,13 +6330,13 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} anymatch@3.1.3: dependencies: @@ -6866,73 +6355,73 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.41.2(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1)): + astro-expressive-code@0.41.3(astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1)): dependencies: - astro: 5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1) - rehype-expressive-code: 0.41.2 + astro: 5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1) + rehype-expressive-code: 0.41.3 - astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.7.1): + astro@5.8.0(@types/node@22.1.0)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.8.3)(yaml@2.8.1): dependencies: - '@astrojs/compiler': 2.12.0 + '@astrojs/compiler': 2.13.0 '@astrojs/internal-helpers': 0.6.1 '@astrojs/markdown-remark': 6.3.2 '@astrojs/telemetry': 3.3.0 '@capsizecss/unpack': 2.4.0 '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.1.4(rollup@4.44.1) - acorn: 8.14.1 + '@rollup/pluginutils': 5.3.0(rollup@4.52.4) + acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 boxen: 8.0.1 - ci-info: 4.2.0 + ci-info: 4.3.1 clsx: 2.1.1 common-ancestor-path: 1.0.1 cookie: 1.0.2 cssesc: 3.0.0 - debug: 4.4.0 + debug: 4.4.3 deterministic-object-hash: 2.0.2 - devalue: 5.1.1 + devalue: 5.3.2 diff: 5.2.0 dlv: 1.1.3 dset: 3.1.4 es-module-lexer: 1.7.0 - esbuild: 0.25.4 + esbuild: 0.25.10 estree-walker: 3.0.3 flattie: 1.1.1 - fontace: 0.3.0 + fontace: 0.3.1 github-slugger: 2.0.0 html-escaper: 3.0.3 - http-cache-semantics: 4.1.1 - import-meta-resolve: 4.1.0 + http-cache-semantics: 4.2.0 + import-meta-resolve: 4.2.0 js-yaml: 4.1.0 kleur: 4.1.5 - magic-string: 0.30.17 + magic-string: 0.30.19 magicast: 0.3.5 mrmime: 2.0.1 neotraverse: 0.6.18 p-limit: 6.2.0 - p-queue: 8.1.0 - package-manager-detector: 1.3.0 - picomatch: 4.0.2 + p-queue: 8.1.1 + package-manager-detector: 1.4.0 + picomatch: 4.0.3 prompts: 2.4.2 rehype: 13.0.2 - semver: 7.7.1 - shiki: 3.4.0 + semver: 7.7.3 + shiki: 3.13.0 tinyexec: 0.3.2 - tinyglobby: 0.2.13 - tsconfck: 3.1.5(typescript@5.8.3) + tinyglobby: 0.2.15 + tsconfck: 3.1.6(typescript@5.8.3) ultrahtml: 1.6.0 - unifont: 0.5.0 + unifont: 0.5.2 unist-util-visit: 5.0.0 - unstorage: 1.16.0 + unstorage: 1.17.1 vfile: 6.0.3 - vite: 6.3.5(@types/node@22.1.0)(lightningcss@1.30.1)(yaml@2.7.1) - vitefu: 1.0.6(vite@6.3.5(@types/node@22.1.0)(lightningcss@1.30.1)(yaml@2.7.1)) + vite: 6.3.6(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1) + vitefu: 1.1.1(vite@6.3.6(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1)) xxhash-wasm: 1.1.0 yargs-parser: 21.1.1 - yocto-spinner: 0.2.2 + yocto-spinner: 0.2.3 zod: 3.25.76 - zod-to-json-schema: 3.24.5(zod@3.25.76) + zod-to-json-schema: 3.24.6(zod@3.25.76) zod-to-ts: 1.2.0(typescript@5.8.3)(zod@3.25.76) optionalDependencies: sharp: 0.33.5 @@ -6950,6 +6439,7 @@ snapshots: - '@types/node' - '@upstash/redis' - '@vercel/blob' + - '@vercel/functions' - '@vercel/kv' - aws4fetch - db0 @@ -6975,7 +6465,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 cosmiconfig: 7.1.0 resolve: 1.22.10 @@ -6991,6 +6481,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.8.13: {} + bcp-47-match@2.0.3: {} bcp-47@2.1.0: @@ -7007,12 +6499,12 @@ snapshots: dependencies: ansi-align: 3.0.1 camelcase: 8.0.0 - chalk: 5.4.1 + chalk: 5.6.2 cli-boxes: 3.0.0 string-width: 7.2.0 type-fest: 4.41.0 widest-line: 5.0.0 - wrap-ansi: 9.0.0 + wrap-ansi: 9.0.2 brace-expansion@2.0.2: dependencies: @@ -7026,12 +6518,13 @@ snapshots: dependencies: base64-js: 1.5.1 - browserslist@4.25.1: + browserslist@4.26.3: dependencies: - caniuse-lite: 1.0.30001726 - electron-to-chromium: 1.5.178 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.1) + baseline-browser-mapping: 2.8.13 + caniuse-lite: 1.0.30001748 + electron-to-chromium: 1.5.232 + node-releases: 2.0.23 + update-browserslist-db: 1.1.3(browserslist@4.26.3) buffer@6.0.0: dependencies: @@ -7044,16 +6537,16 @@ snapshots: camelcase@8.0.0: {} - caniuse-lite@1.0.30001726: {} + caniuse-lite@1.0.30001748: {} ccount@2.0.1: {} - chai@5.2.0: + chai@5.3.3: dependencies: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.4 + loupe: 3.2.1 pathval: 2.0.1 chalk@4.1.2: @@ -7061,7 +6554,7 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.4.1: {} + chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -7079,7 +6572,7 @@ snapshots: chrome-trace-event@1.0.4: {} - ci-info@4.2.0: {} + ci-info@4.3.1: {} cli-boxes@3.0.0: {} @@ -7104,7 +6597,7 @@ snapshots: color-string@1.9.1: dependencies: color-name: 1.1.4 - simple-swizzle: 0.2.2 + simple-swizzle: 0.2.4 color@4.2.3: dependencies: @@ -7128,7 +6621,7 @@ snapshots: cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 - import-fresh: 3.3.0 + import-fresh: 3.3.1 parse-json: 5.2.0 path-type: 4.0.0 yaml: 1.10.2 @@ -7149,7 +6642,7 @@ snapshots: dependencies: uncrypto: 0.1.3 - css-selector-parser@3.1.2: {} + css-selector-parser@3.1.3: {} css-tree@3.1.0: dependencies: @@ -7160,15 +6653,11 @@ snapshots: csstype@3.1.3: {} - debug@4.4.0: + debug@4.4.3: dependencies: ms: 2.1.3 - debug@4.4.1: - dependencies: - ms: 2.1.3 - - decode-named-character-reference@1.1.0: + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -7182,13 +6671,13 @@ snapshots: detect-libc@1.0.3: {} - detect-libc@2.0.4: {} + detect-libc@2.1.2: {} deterministic-object-hash@2.0.2: dependencies: base-64: 1.0.0 - devalue@5.1.1: {} + devalue@5.3.2: {} devlop@1.1.0: dependencies: @@ -7204,7 +6693,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 csstype: 3.1.3 dotenv-expand@11.0.7: @@ -7217,22 +6706,22 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.178: {} + electron-to-chromium@1.5.232: {} emmet@2.4.11: dependencies: '@emmetio/abbreviation': 2.3.3 '@emmetio/css-abbreviation': 2.1.8 - emoji-regex@10.4.0: {} + emoji-regex@10.5.0: {} emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} - entities@6.0.0: {} + entities@6.0.1: {} - error-ex@1.3.2: + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -7248,65 +6737,38 @@ snapshots: esast-util-from-js@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - acorn: 8.14.1 + acorn: 8.15.0 esast-util-from-estree: 2.0.0 - vfile-message: 4.0.2 + vfile-message: 4.0.3 - esbuild@0.25.4: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.4 - '@esbuild/android-arm': 0.25.4 - '@esbuild/android-arm64': 0.25.4 - '@esbuild/android-x64': 0.25.4 - '@esbuild/darwin-arm64': 0.25.4 - '@esbuild/darwin-x64': 0.25.4 - '@esbuild/freebsd-arm64': 0.25.4 - '@esbuild/freebsd-x64': 0.25.4 - '@esbuild/linux-arm': 0.25.4 - '@esbuild/linux-arm64': 0.25.4 - '@esbuild/linux-ia32': 0.25.4 - '@esbuild/linux-loong64': 0.25.4 - '@esbuild/linux-mips64el': 0.25.4 - '@esbuild/linux-ppc64': 0.25.4 - '@esbuild/linux-riscv64': 0.25.4 - '@esbuild/linux-s390x': 0.25.4 - '@esbuild/linux-x64': 0.25.4 - '@esbuild/netbsd-arm64': 0.25.4 - '@esbuild/netbsd-x64': 0.25.4 - '@esbuild/openbsd-arm64': 0.25.4 - '@esbuild/openbsd-x64': 0.25.4 - '@esbuild/sunos-x64': 0.25.4 - '@esbuild/win32-arm64': 0.25.4 - '@esbuild/win32-ia32': 0.25.4 - '@esbuild/win32-x64': 0.25.4 - - esbuild@0.25.5: + esbuild@0.25.10: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.5 - '@esbuild/android-arm': 0.25.5 - '@esbuild/android-arm64': 0.25.5 - '@esbuild/android-x64': 0.25.5 - '@esbuild/darwin-arm64': 0.25.5 - '@esbuild/darwin-x64': 0.25.5 - '@esbuild/freebsd-arm64': 0.25.5 - '@esbuild/freebsd-x64': 0.25.5 - '@esbuild/linux-arm': 0.25.5 - '@esbuild/linux-arm64': 0.25.5 - '@esbuild/linux-ia32': 0.25.5 - '@esbuild/linux-loong64': 0.25.5 - '@esbuild/linux-mips64el': 0.25.5 - '@esbuild/linux-ppc64': 0.25.5 - '@esbuild/linux-riscv64': 0.25.5 - '@esbuild/linux-s390x': 0.25.5 - '@esbuild/linux-x64': 0.25.5 - '@esbuild/netbsd-arm64': 0.25.5 - '@esbuild/netbsd-x64': 0.25.5 - '@esbuild/openbsd-arm64': 0.25.5 - '@esbuild/openbsd-x64': 0.25.5 - '@esbuild/sunos-x64': 0.25.5 - '@esbuild/win32-arm64': 0.25.5 - '@esbuild/win32-ia32': 0.25.5 - '@esbuild/win32-x64': 0.25.5 + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 escalade@3.2.0: {} @@ -7336,7 +6798,7 @@ snapshots: dependencies: '@types/estree-jsx': 1.0.5 astring: 1.9.0 - source-map: 0.7.4 + source-map: 0.7.6 estree-util-visit@2.0.0: dependencies: @@ -7364,7 +6826,7 @@ snapshots: pretty-ms: 9.3.0 signal-exit: 4.1.0 strip-final-newline: 4.0.0 - yoctocolors: 2.1.1 + yoctocolors: 2.1.2 expect-type@1.2.2: {} @@ -7387,15 +6849,15 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-uri@3.0.6: {} + fast-uri@3.1.0: {} fastq@1.19.1: dependencies: reusify: 1.1.0 - fdir@6.4.6(picomatch@4.0.2): + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 fflate@0.8.2: {} @@ -7413,7 +6875,7 @@ snapshots: flattie@1.1.1: {} - fontace@0.3.0: + fontace@0.3.1: dependencies: '@types/fontkit': 2.0.8 fontkit: 2.0.4 @@ -7444,7 +6906,7 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.3.0: {} + get-east-asian-width@1.4.0: {} get-port@4.2.0: {} @@ -7497,20 +6959,18 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - globals@11.12.0: {} - globals@13.24.0: dependencies: type-fest: 0.20.2 - h3@1.15.3: + h3@1.15.4: dependencies: cookie-es: 1.2.2 crossws: 0.3.5 defu: 6.1.4 destr: 2.0.5 iron-webcrypto: 1.2.1 - node-mock-http: 1.0.0 + node-mock-http: 1.0.3 radix3: 1.1.2 ufo: 1.6.1 uncrypto: 0.1.3 @@ -7543,7 +7003,7 @@ snapshots: hast-util-from-parse5: 8.0.3 parse5: 7.3.0 vfile: 6.0.3 - vfile-message: 4.0.2 + vfile-message: 4.0.3 hast-util-from-parse5@8.0.3: dependencies: @@ -7610,7 +7070,7 @@ snapshots: '@types/unist': 3.0.3 bcp-47-match: 2.0.3 comma-separated-tokens: 2.0.3 - css-selector-parser: 3.1.2 + css-selector-parser: 3.1.3 devlop: 1.1.0 direction: 2.0.1 hast-util-has-property: 3.0.0 @@ -7673,7 +7133,7 @@ snapshots: space-separated-tokens: 2.0.2 style-to-js: 1.1.17 unist-util-position: 5.0.0 - vfile-message: 4.0.2 + vfile-message: 4.0.3 transitivePeerDependencies: - supports-color @@ -7722,22 +7182,22 @@ snapshots: html-whitespace-sensitive-tag-names@3.0.1: {} - http-cache-semantics@4.1.1: {} + http-cache-semantics@4.2.0: {} human-signals@8.0.1: {} i18next@23.16.8: dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 ieee754@1.2.1: {} - import-fresh@3.3.0: + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - import-meta-resolve@4.1.0: {} + import-meta-resolve@4.2.0: {} inline-style-parser@0.2.4: {} @@ -7752,7 +7212,7 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: {} + is-arrayish@0.3.4: {} is-core-module@2.16.1: dependencies: @@ -7794,11 +7254,11 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.27.7 - '@babel/parser': 7.27.7 + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.2 + semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -7810,13 +7270,13 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: - '@jridgewell/trace-mapping': 0.3.28 - debug: 4.4.1 + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color - istanbul-reports@3.1.7: + istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 @@ -7855,59 +7315,63 @@ snapshots: klona@2.0.6: {} - lightningcss-darwin-arm64@1.30.1: + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: optional: true - lightningcss-darwin-x64@1.30.1: + lightningcss-darwin-x64@1.30.2: optional: true - lightningcss-freebsd-x64@1.30.1: + lightningcss-freebsd-x64@1.30.2: optional: true - lightningcss-linux-arm-gnueabihf@1.30.1: + lightningcss-linux-arm-gnueabihf@1.30.2: optional: true - lightningcss-linux-arm64-gnu@1.30.1: + lightningcss-linux-arm64-gnu@1.30.2: optional: true - lightningcss-linux-arm64-musl@1.30.1: + lightningcss-linux-arm64-musl@1.30.2: optional: true - lightningcss-linux-x64-gnu@1.30.1: + lightningcss-linux-x64-gnu@1.30.2: optional: true - lightningcss-linux-x64-musl@1.30.1: + lightningcss-linux-x64-musl@1.30.2: optional: true - lightningcss-win32-arm64-msvc@1.30.1: + lightningcss-win32-arm64-msvc@1.30.2: optional: true - lightningcss-win32-x64-msvc@1.30.1: + lightningcss-win32-x64-msvc@1.30.2: optional: true - lightningcss@1.30.1: + lightningcss@1.30.2: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 optionalDependencies: - lightningcss-darwin-arm64: 1.30.1 - lightningcss-darwin-x64: 1.30.1 - lightningcss-freebsd-x64: 1.30.1 - lightningcss-linux-arm-gnueabihf: 1.30.1 - lightningcss-linux-arm64-gnu: 1.30.1 - lightningcss-linux-arm64-musl: 1.30.1 - lightningcss-linux-x64-gnu: 1.30.1 - lightningcss-linux-x64-musl: 1.30.1 - lightningcss-win32-arm64-msvc: 1.30.1 - lightningcss-win32-x64-msvc: 1.30.1 + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 lines-and-columns@1.2.4: {} lmdb@2.8.5: dependencies: - msgpackr: 1.11.4 + msgpackr: 1.11.5 node-addon-api: 6.1.0 node-gyp-build-optional-packages: 5.1.1 - ordered-binary: 1.5.3 + ordered-binary: 1.6.0 weak-lru-cache: 1.2.2 optionalDependencies: '@lmdb/lmdb-darwin-arm64': 2.8.5 @@ -7925,7 +7389,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.4: {} + loupe@3.2.1: {} lru-cache@10.4.3: {} @@ -7933,19 +7397,19 @@ snapshots: dependencies: yallist: 3.1.1 - magic-string@0.30.17: + magic-string@0.30.19: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 magicast@0.3.5: dependencies: - '@babel/parser': 7.27.1 - '@babel/types': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 source-map-js: 1.2.1 make-dir@4.0.0: dependencies: - semver: 7.7.2 + semver: 7.7.3 markdown-extensions@2.0.0: {} @@ -7982,7 +7446,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 micromark: 4.0.2 @@ -8076,7 +7540,7 @@ snapshots: parse-entities: 4.0.2 stringify-entities: 4.0.4 unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.2 + vfile-message: 4.0.3 transitivePeerDependencies: - supports-color @@ -8140,7 +7604,7 @@ snapshots: micromark-core-commonmark@2.0.3: dependencies: - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-factory-destination: 2.0.1 micromark-factory-label: 2.0.1 @@ -8247,7 +7711,7 @@ snapshots: micromark-util-events-to-acorn: 2.0.3 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - vfile-message: 4.0.2 + vfile-message: 4.0.3 micromark-extension-mdx-md@2.0.0: dependencies: @@ -8263,12 +7727,12 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 unist-util-position-from-estree: 2.0.0 - vfile-message: 4.0.2 + vfile-message: 4.0.3 micromark-extension-mdxjs@3.0.0: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) micromark-extension-mdx-expression: 3.0.1 micromark-extension-mdx-jsx: 3.0.2 micromark-extension-mdx-md: 2.0.0 @@ -8299,7 +7763,7 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 unist-util-position-from-estree: 2.0.0 - vfile-message: 4.0.2 + vfile-message: 4.0.3 micromark-factory-space@2.0.1: dependencies: @@ -8346,7 +7810,7 @@ snapshots: micromark-util-decode-string@2.0.1: dependencies: - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 micromark-util-character: 2.1.1 micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-symbol: 2.0.1 @@ -8361,7 +7825,7 @@ snapshots: estree-util-visit: 2.0.0 micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - vfile-message: 4.0.2 + vfile-message: 4.0.3 micromark-util-html-tag-name@2.0.1: {} @@ -8393,8 +7857,8 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 - decode-named-character-reference: 1.1.0 + debug: 4.4.3 + decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 micromark-factory-space: 2.0.1 @@ -8439,7 +7903,7 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 optional: true - msgpackr@1.11.4: + msgpackr@1.11.5: optionalDependencies: msgpackr-extract: 3.0.3 @@ -8457,7 +7921,7 @@ snapshots: node-addon-api@7.1.1: {} - node-fetch-native@1.6.6: {} + node-fetch-native@1.6.7: {} node-fetch@2.7.0: dependencies: @@ -8465,16 +7929,16 @@ snapshots: node-gyp-build-optional-packages@5.1.1: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 node-gyp-build-optional-packages@5.2.2: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 optional: true - node-mock-http@1.0.0: {} + node-mock-http@1.0.3: {} - node-releases@2.0.19: {} + node-releases@2.0.23: {} normalize-path@3.0.0: {} @@ -8498,7 +7962,7 @@ snapshots: ofetch@1.4.1: dependencies: destr: 2.0.5 - node-fetch-native: 1.6.6 + node-fetch-native: 1.6.7 ufo: 1.6.1 ohash@2.0.11: {} @@ -8511,13 +7975,13 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 - ordered-binary@1.5.3: {} + ordered-binary@1.6.0: {} p-limit@6.2.0: dependencies: yocto-queue: 1.2.1 - p-queue@8.1.0: + p-queue@8.1.1: dependencies: eventemitter3: 5.0.1 p-timeout: 6.1.4 @@ -8526,15 +7990,16 @@ snapshots: package-json-from-dist@1.0.1: {} - package-manager-detector@1.3.0: {} + package-manager-detector@1.4.0: {} - pagefind@1.3.0: + pagefind@1.4.0: optionalDependencies: - '@pagefind/darwin-arm64': 1.3.0 - '@pagefind/darwin-x64': 1.3.0 - '@pagefind/linux-arm64': 1.3.0 - '@pagefind/linux-x64': 1.3.0 - '@pagefind/windows-x64': 1.3.0 + '@pagefind/darwin-arm64': 1.4.0 + '@pagefind/darwin-x64': 1.4.0 + '@pagefind/freebsd-x64': 1.4.0 + '@pagefind/linux-arm64': 1.4.0 + '@pagefind/linux-x64': 1.4.0 + '@pagefind/windows-x64': 1.4.0 pako@0.2.9: {} @@ -8568,7 +8033,7 @@ snapshots: '@types/unist': 2.0.11 character-entities-legacy: 3.0.0 character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.1.0 + decode-named-character-reference: 1.2.0 is-alphanumerical: 2.0.1 is-decimal: 2.0.1 is-hexadecimal: 2.0.1 @@ -8576,7 +8041,7 @@ snapshots: parse-json@5.2.0: dependencies: '@babel/code-frame': 7.27.1 - error-ex: 1.3.2 + error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -8593,7 +8058,7 @@ snapshots: parse5@7.3.0: dependencies: - entities: 6.0.0 + entities: 6.0.1 path-browserify@1.0.1: {} @@ -8618,7 +8083,7 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.2: {} + picomatch@4.0.3: {} postcss-nested@6.2.0(postcss@8.5.6): dependencies: @@ -8632,12 +8097,6 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.3: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -8681,9 +8140,9 @@ snapshots: react-is@16.13.1: {} - react-is@18.2.0: {} + react-is@18.3.1: {} - react-is@19.1.0: {} + react-is@19.2.0: {} react-refresh@0.16.0: {} @@ -8691,7 +8150,7 @@ snapshots: react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@babel/runtime': 7.27.0 + '@babel/runtime': 7.28.4 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -8708,15 +8167,14 @@ snapshots: estree-util-build-jsx: 3.0.1 vfile: 6.0.3 - recma-jsx@1.0.0(acorn@8.14.1): + recma-jsx@1.0.1(acorn@8.15.0): dependencies: - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) estree-util-to-js: 2.0.0 recma-parse: 1.0.0 recma-stringify: 1.0.0 unified: 11.0.5 - transitivePeerDependencies: - - acorn recma-parse@1.0.0: dependencies: @@ -8750,7 +8208,7 @@ snapshots: regl@2.1.1: {} - rehype-expressive-code@0.41.2: + rehype-expressive-code@0.41.3: dependencies: expressive-code: 0.41.3 @@ -8812,7 +8270,7 @@ snapshots: transitivePeerDependencies: - supports-color - remark-mdx@3.1.0: + remark-mdx@3.1.1: dependencies: mdast-util-mdx: 3.0.0 micromark-extension-mdxjs: 3.0.0 @@ -8894,56 +8352,32 @@ snapshots: reusify@1.1.0: {} - rollup@4.40.2: - dependencies: - '@types/estree': 1.0.7 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.40.2 - '@rollup/rollup-android-arm64': 4.40.2 - '@rollup/rollup-darwin-arm64': 4.40.2 - '@rollup/rollup-darwin-x64': 4.40.2 - '@rollup/rollup-freebsd-arm64': 4.40.2 - '@rollup/rollup-freebsd-x64': 4.40.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.40.2 - '@rollup/rollup-linux-arm-musleabihf': 4.40.2 - '@rollup/rollup-linux-arm64-gnu': 4.40.2 - '@rollup/rollup-linux-arm64-musl': 4.40.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.40.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.40.2 - '@rollup/rollup-linux-riscv64-gnu': 4.40.2 - '@rollup/rollup-linux-riscv64-musl': 4.40.2 - '@rollup/rollup-linux-s390x-gnu': 4.40.2 - '@rollup/rollup-linux-x64-gnu': 4.40.2 - '@rollup/rollup-linux-x64-musl': 4.40.2 - '@rollup/rollup-win32-arm64-msvc': 4.40.2 - '@rollup/rollup-win32-ia32-msvc': 4.40.2 - '@rollup/rollup-win32-x64-msvc': 4.40.2 - fsevents: 2.3.3 - - rollup@4.44.1: + rollup@4.52.4: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.44.1 - '@rollup/rollup-android-arm64': 4.44.1 - '@rollup/rollup-darwin-arm64': 4.44.1 - '@rollup/rollup-darwin-x64': 4.44.1 - '@rollup/rollup-freebsd-arm64': 4.44.1 - '@rollup/rollup-freebsd-x64': 4.44.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.44.1 - '@rollup/rollup-linux-arm-musleabihf': 4.44.1 - '@rollup/rollup-linux-arm64-gnu': 4.44.1 - '@rollup/rollup-linux-arm64-musl': 4.44.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.44.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.44.1 - '@rollup/rollup-linux-riscv64-gnu': 4.44.1 - '@rollup/rollup-linux-riscv64-musl': 4.44.1 - '@rollup/rollup-linux-s390x-gnu': 4.44.1 - '@rollup/rollup-linux-x64-gnu': 4.44.1 - '@rollup/rollup-linux-x64-musl': 4.44.1 - '@rollup/rollup-win32-arm64-msvc': 4.44.1 - '@rollup/rollup-win32-ia32-msvc': 4.44.1 - '@rollup/rollup-win32-x64-msvc': 4.44.1 + '@rollup/rollup-android-arm-eabi': 4.52.4 + '@rollup/rollup-android-arm64': 4.52.4 + '@rollup/rollup-darwin-arm64': 4.52.4 + '@rollup/rollup-darwin-x64': 4.52.4 + '@rollup/rollup-freebsd-arm64': 4.52.4 + '@rollup/rollup-freebsd-x64': 4.52.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.4 + '@rollup/rollup-linux-arm-musleabihf': 4.52.4 + '@rollup/rollup-linux-arm64-gnu': 4.52.4 + '@rollup/rollup-linux-arm64-musl': 4.52.4 + '@rollup/rollup-linux-loong64-gnu': 4.52.4 + '@rollup/rollup-linux-ppc64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-gnu': 4.52.4 + '@rollup/rollup-linux-riscv64-musl': 4.52.4 + '@rollup/rollup-linux-s390x-gnu': 4.52.4 + '@rollup/rollup-linux-x64-gnu': 4.52.4 + '@rollup/rollup-linux-x64-musl': 4.52.4 + '@rollup/rollup-openharmony-arm64': 4.52.4 + '@rollup/rollup-win32-arm64-msvc': 4.52.4 + '@rollup/rollup-win32-ia32-msvc': 4.52.4 + '@rollup/rollup-win32-x64-gnu': 4.52.4 + '@rollup/rollup-win32-x64-msvc': 4.52.4 fsevents: 2.3.3 run-parallel@1.2.0: @@ -8958,15 +8392,13 @@ snapshots: semver@6.3.1: {} - semver@7.7.1: {} - - semver@7.7.2: {} + semver@7.7.3: {} sharp@0.33.5: dependencies: color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.1 + detect-libc: 2.1.2 + semver: 7.7.3 optionalDependencies: '@img/sharp-darwin-arm64': 0.33.5 '@img/sharp-darwin-x64': 0.33.5 @@ -8992,8 +8424,8 @@ snapshots: sharp@0.34.1: dependencies: color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.1 + detect-libc: 2.1.2 + semver: 7.7.3 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.1 '@img/sharp-darwin-x64': 0.34.1 @@ -9022,14 +8454,14 @@ snapshots: shebang-regex@3.0.0: {} - shiki@3.4.0: + shiki@3.13.0: dependencies: - '@shikijs/core': 3.4.0 - '@shikijs/engine-javascript': 3.4.0 - '@shikijs/engine-oniguruma': 3.4.0 - '@shikijs/langs': 3.4.0 - '@shikijs/themes': 3.4.0 - '@shikijs/types': 3.4.0 + '@shikijs/core': 3.13.0 + '@shikijs/engine-javascript': 3.13.0 + '@shikijs/engine-oniguruma': 3.13.0 + '@shikijs/langs': 3.13.0 + '@shikijs/themes': 3.13.0 + '@shikijs/types': 3.13.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -9037,9 +8469,9 @@ snapshots: signal-exit@4.1.0: {} - simple-swizzle@0.2.2: + simple-swizzle@0.2.4: dependencies: - is-arrayish: 0.3.2 + is-arrayish: 0.3.4 sisteransi@1.0.5: {} @@ -9050,13 +8482,13 @@ snapshots: arg: 5.0.2 sax: 1.4.1 - smol-toml@1.3.4: {} + smol-toml@1.4.2: {} source-map-js@1.2.1: {} source-map@0.5.7: {} - source-map@0.7.4: {} + source-map@0.7.6: {} space-separated-tokens@2.0.2: {} @@ -9076,13 +8508,13 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string-width@7.2.0: dependencies: - emoji-regex: 10.4.0 - get-east-asian-width: 1.3.0 - strip-ansi: 7.1.0 + emoji-regex: 10.5.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 stringify-entities@4.0.4: dependencies: @@ -9093,13 +8525,13 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: + strip-ansi@7.1.2: dependencies: - ansi-regex: 6.1.0 + ansi-regex: 6.2.2 strip-final-newline@4.0.0: {} - strip-literal@3.0.0: + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -9133,21 +8565,16 @@ snapshots: tinyexec@0.3.2: {} - tinyglobby@0.2.13: - dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 - - tinyglobby@0.2.14: + tinyglobby@0.2.15: dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 tinypool@1.1.1: {} tinyrainbow@2.0.0: {} - tinyspy@4.0.3: {} + tinyspy@4.0.4: {} to-regex-range@5.0.1: dependencies: @@ -9159,13 +8586,10 @@ snapshots: trough@2.2.0: {} - tsconfck@3.1.5(typescript@5.8.3): + tsconfck@3.1.6(typescript@5.8.3): optionalDependencies: typescript: 5.8.3 - tslib@2.6.2: - optional: true - tslib@2.8.1: {} type-fest@0.20.2: {} @@ -9174,9 +8598,9 @@ snapshots: typesafe-path@0.2.2: {} - typescript-auto-import-cache@0.3.5: + typescript-auto-import-cache@0.3.6: dependencies: - semver: 7.7.2 + semver: 7.7.3 typescript@5.8.3: {} @@ -9212,9 +8636,10 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unifont@0.5.0: + unifont@0.5.2: dependencies: css-tree: 3.1.0 + ofetch: 1.4.1 ohash: 2.0.11 unist-util-find-after@5.0.0: @@ -9263,14 +8688,14 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - unstorage@1.16.0: + unstorage@1.17.1: dependencies: anymatch: 3.1.3 chokidar: 4.0.3 destr: 2.0.5 - h3: 1.15.3 + h3: 1.15.4 lru-cache: 10.4.3 - node-fetch-native: 1.6.6 + node-fetch-native: 1.6.7 ofetch: 1.4.1 ufo: 1.6.1 @@ -9278,9 +8703,9 @@ snapshots: dependencies: uzip-module: 1.0.3 - update-browserslist-db@1.1.3(browserslist@4.25.1): + update-browserslist-db@1.1.3(browserslist@4.26.3): dependencies: - browserslist: 4.25.1 + browserslist: 4.26.3 escalade: 3.2.0 picocolors: 1.1.1 @@ -9297,7 +8722,7 @@ snapshots: '@types/unist': 3.0.3 vfile: 6.0.3 - vfile-message@4.0.2: + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 @@ -9305,15 +8730,15 @@ snapshots: vfile@6.0.3: dependencies: '@types/unist': 3.0.3 - vfile-message: 4.0.2 + vfile-message: 4.0.3 - vite-node@3.2.4(lightningcss@1.30.1)(yaml@2.7.1): + vite-node@3.2.4(lightningcss@1.30.2)(yaml@2.8.1): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.0.0(lightningcss@1.30.1)(yaml@2.7.1) + vite: 7.1.9(lightningcss@1.30.2)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -9328,61 +8753,61 @@ snapshots: - tsx - yaml - vite@6.3.5(@types/node@22.1.0)(lightningcss@1.30.1)(yaml@2.7.1): + vite@6.3.6(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1): dependencies: - esbuild: 0.25.4 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 - postcss: 8.5.3 - rollup: 4.40.2 - tinyglobby: 0.2.13 + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.4 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.1.0 fsevents: 2.3.3 - lightningcss: 1.30.1 - yaml: 2.7.1 + lightningcss: 1.30.2 + yaml: 2.8.1 - vite@7.0.0(lightningcss@1.30.1)(yaml@2.7.1): + vite@7.1.9(lightningcss@1.30.2)(yaml@2.8.1): dependencies: - esbuild: 0.25.5 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.44.1 - tinyglobby: 0.2.14 + rollup: 4.52.4 + tinyglobby: 0.2.15 optionalDependencies: fsevents: 2.3.3 - lightningcss: 1.30.1 - yaml: 2.7.1 + lightningcss: 1.30.2 + yaml: 2.8.1 - vitefu@1.0.6(vite@6.3.5(@types/node@22.1.0)(lightningcss@1.30.1)(yaml@2.7.1)): + vitefu@1.1.1(vite@6.3.6(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1)): optionalDependencies: - vite: 6.3.5(@types/node@22.1.0)(lightningcss@1.30.1)(yaml@2.7.1) + vite: 6.3.6(@types/node@22.1.0)(lightningcss@1.30.2)(yaml@2.8.1) - vitest@3.2.4(@types/debug@4.1.12)(lightningcss@1.30.1)(yaml@2.7.1): + vitest@3.2.4(@types/debug@4.1.12)(lightningcss@1.30.2)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.0.0(lightningcss@1.30.1)(yaml@2.7.1)) + '@vitest/mocker': 3.2.4(vite@7.1.9(lightningcss@1.30.2)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 - chai: 5.2.0 - debug: 4.4.1 + chai: 5.3.3 + debug: 4.4.3 expect-type: 1.2.2 - magic-string: 0.30.17 + magic-string: 0.30.19 pathe: 2.0.3 - picomatch: 4.0.2 + picomatch: 4.0.3 std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.0.0(lightningcss@1.30.1)(yaml@2.7.1) - vite-node: 3.2.4(lightningcss@1.30.1)(yaml@2.7.1) + vite: 7.1.9(lightningcss@1.30.2)(yaml@2.8.1) + vite-node: 3.2.4(lightningcss@1.30.2)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -9400,69 +8825,69 @@ snapshots: - tsx - yaml - volar-service-css@0.0.62(@volar/language-service@2.4.13): + volar-service-css@0.0.62(@volar/language-service@2.4.23): dependencies: - vscode-css-languageservice: 6.3.5 + vscode-css-languageservice: 6.3.8 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.13 + '@volar/language-service': 2.4.23 - volar-service-emmet@0.0.62(@volar/language-service@2.4.13): + volar-service-emmet@0.0.62(@volar/language-service@2.4.23): dependencies: '@emmetio/css-parser': 0.4.0 '@emmetio/html-matcher': 1.3.0 '@vscode/emmet-helper': 2.11.0 vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.13 + '@volar/language-service': 2.4.23 - volar-service-html@0.0.62(@volar/language-service@2.4.13): + volar-service-html@0.0.62(@volar/language-service@2.4.23): dependencies: - vscode-html-languageservice: 5.4.0 + vscode-html-languageservice: 5.5.2 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.13 + '@volar/language-service': 2.4.23 - volar-service-prettier@0.0.62(@volar/language-service@2.4.13): + volar-service-prettier@0.0.62(@volar/language-service@2.4.23): dependencies: vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.13 + '@volar/language-service': 2.4.23 - volar-service-typescript-twoslash-queries@0.0.62(@volar/language-service@2.4.13): + volar-service-typescript-twoslash-queries@0.0.62(@volar/language-service@2.4.23): dependencies: vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.13 + '@volar/language-service': 2.4.23 - volar-service-typescript@0.0.62(@volar/language-service@2.4.13): + volar-service-typescript@0.0.62(@volar/language-service@2.4.23): dependencies: path-browserify: 1.0.1 - semver: 7.7.1 - typescript-auto-import-cache: 0.3.5 + semver: 7.7.3 + typescript-auto-import-cache: 0.3.6 vscode-languageserver-textdocument: 1.0.12 vscode-nls: 5.2.0 vscode-uri: 3.1.0 optionalDependencies: - '@volar/language-service': 2.4.13 + '@volar/language-service': 2.4.23 - volar-service-yaml@0.0.62(@volar/language-service@2.4.13): + volar-service-yaml@0.0.62(@volar/language-service@2.4.23): dependencies: vscode-uri: 3.1.0 yaml-language-server: 1.15.0 optionalDependencies: - '@volar/language-service': 2.4.13 + '@volar/language-service': 2.4.23 - vscode-css-languageservice@6.3.5: + vscode-css-languageservice@6.3.8: dependencies: '@vscode/l10n': 0.0.18 vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 vscode-uri: 3.1.0 - vscode-html-languageservice@5.4.0: + vscode-html-languageservice@5.5.2: dependencies: '@vscode/l10n': 0.0.18 vscode-languageserver-textdocument: 1.0.12 @@ -9543,15 +8968,15 @@ snapshots: wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 - wrap-ansi@9.0.0: + wrap-ansi@9.0.2: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 7.2.0 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 xxhash-wasm@1.1.0: {} @@ -9578,7 +9003,7 @@ snapshots: yaml@2.2.2: {} - yaml@2.7.1: {} + yaml@2.8.1: {} yargs-parser@21.1.1: {} @@ -9594,15 +9019,15 @@ snapshots: yocto-queue@1.2.1: {} - yocto-spinner@0.2.2: + yocto-spinner@0.2.3: dependencies: - yoctocolors: 2.1.1 + yoctocolors: 2.1.2 - yoctocolors@2.1.1: {} + yoctocolors@2.1.2: {} zarrita@0.5.1: dependencies: - '@zarrita/storage': 0.1.1 + '@zarrita/storage': 0.1.3 numcodecs: 0.3.2 zarrita@0.5.3: @@ -9610,7 +9035,7 @@ snapshots: '@zarrita/storage': 0.1.3 numcodecs: 0.3.2 - zod-to-json-schema@3.24.5(zod@3.25.76): + zod-to-json-schema@3.24.6(zod@3.25.76): dependencies: zod: 3.25.76 From 8718c85ca0cc94cf01967241c23781b705fdbf3b Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 11:50:45 -0700 Subject: [PATCH 28/91] Removing "module": "path..." entries --- packages/core/package.json | 1 - packages/dzi/package.json | 1 - packages/geometry/package.json | 1 - packages/omezarr/package.json | 1 - 4 files changed, 4 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 48bdd1b8..0738e65e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,7 +30,6 @@ "license": "BSD-3-Clause", "source": "src/index.ts", "main": "dist/main.js", - "module": "dist/module.js", "types": "dist/types.d.ts", "type": "module", "files": [ diff --git a/packages/dzi/package.json b/packages/dzi/package.json index deca0ab9..4e131871 100644 --- a/packages/dzi/package.json +++ b/packages/dzi/package.json @@ -26,7 +26,6 @@ "license": "BSD-3-Clause", "source": "src/index.ts", "main": "dist/main.js", - "module": "dist/module.js", "types": "dist/types.d.ts", "type": "module", "files": [ diff --git a/packages/geometry/package.json b/packages/geometry/package.json index b5cdaeb2..9a843928 100644 --- a/packages/geometry/package.json +++ b/packages/geometry/package.json @@ -30,7 +30,6 @@ "license": "BSD-3-Clause", "source": "src/index.ts", "main": "dist/main.js", - "module": "dist/module.js", "types": "dist/types.d.ts", "type": "module", "files": [ diff --git a/packages/omezarr/package.json b/packages/omezarr/package.json index 0e8c5241..00c25e88 100644 --- a/packages/omezarr/package.json +++ b/packages/omezarr/package.json @@ -26,7 +26,6 @@ "license": "BSD-3-Clause", "source": "src/index.ts", "main": "dist/main.js", - "module": "dist/module.js", "types": "dist/types.d.ts", "type": "module", "files": [ From 829eecaca9b1d910a3880597fb8f7db8b5323deb Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 12:49:24 -0700 Subject: [PATCH 29/91] Removing zod --- packages/core/package.json | 3 +- packages/core/src/workers/messages.ts | 40 +++++++-------------------- pnpm-lock.yaml | 3 -- 3 files changed, 11 insertions(+), 35 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 627d924d..a9993957 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -57,8 +57,7 @@ "@alleninstitute/vis-geometry": "workspace:*", "lodash": "4.17.21", "regl": "2.1.0", - "uuid": "13.0.0", - "zod": "4.1.11" + "uuid": "13.0.0" }, "packageManager": "pnpm@9.14.2" } diff --git a/packages/core/src/workers/messages.ts b/packages/core/src/workers/messages.ts index bb9b96a7..e2733c7a 100644 --- a/packages/core/src/workers/messages.ts +++ b/packages/core/src/workers/messages.ts @@ -1,6 +1,3 @@ -import { z } from 'zod'; -import { logger } from '../logger'; - export type WorkerMessage = { type: string; }; @@ -9,44 +6,27 @@ export type WorkerMessageWithId = WorkerMessage & { id: string; }; -export const WorkerMessageSchema = z.object({ - type: z.string(), -}); - -export const WorkerMessageWithIdSchema = WorkerMessageSchema.extend({ - id: z.string().nonempty(), -}); - export function isWorkerMessage(val: unknown): val is WorkerMessage { - const { success, error } = WorkerMessageSchema.safeParse(val); - if (error) { - logger.warn('parsing WorkerMessage failed', error); - } - return success; + return ( + val !== undefined && + val !== null && + typeof val === 'object' && + 'type' in val && + typeof val.type === 'string' && + val.type.length > 0 + ); } export function isWorkerMessageWithId(val: unknown): val is WorkerMessageWithId { - const { success, error } = WorkerMessageWithIdSchema.safeParse(val); - if (error) { - logger.warn('parsing WorkerMessageWithId failed', error); - } - return success; + return isWorkerMessage(val) && 'id' in val && typeof val.id === 'string' && val.id.length > 0; } export type HeartbeatMessage = { type: 'heartbeat'; }; -export const HeartbeatMessageSchema = z.object({ - type: z.literal('heartbeat'), -}); - export function isHeartbeatMessage(val: unknown): val is HeartbeatMessage { - const { success, error } = HeartbeatMessageSchema.safeParse(val); - if (error) { - logger.warn('parsing WorkerMessageWithId failed', error); - } - return success; + return isWorkerMessage(val) && val.type === 'heartbeat'; } export const HEARTBEAT_RATE_MS = 500; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3b1bdf1..1f472264 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,9 +53,6 @@ importers: uuid: specifier: 13.0.0 version: 13.0.0 - zod: - specifier: 4.1.11 - version: 4.1.11 devDependencies: '@types/lodash': specifier: 4.17.20 From de3ad63a253ad28d9b40323a37c0516674306866 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 13:14:58 -0700 Subject: [PATCH 30/91] Fixes to Store --- packages/omezarr/src/zarr/cached-loading/store.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index ca71c782..d14a145d 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -199,7 +199,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { const cacheKey = asCacheKey(key, range); this.#priorityByTimestamp.set(cacheKey, Date.now()); - this.#dataCache.reprioritize(); + this.#dataCache.reprioritize(this.#scoreFn); this.#incrementKeyCount(cacheKey); @@ -217,7 +217,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { const count = this.#decrementKeyCount(cacheKey); if (count === 0) { this.#priorityByTimestamp.set(cacheKey, 0); - this.#dataCache.reprioritize(); + this.#dataCache.reprioritize(this.#scoreFn); } }; } @@ -233,6 +233,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { }, isFetchSliceResponseMessage, [], + abort ); request @@ -246,7 +247,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { this.#dataCache.put(cacheKey, new CacheableByteArray(arr)); resolve(arr); }) - .catch((e) => { + .catch((e: unknown) => { reject(e); }) .finally(() => { From 64ec66aea63bf7944a82bd8317f2efd54a3c20ae Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 13:15:16 -0700 Subject: [PATCH 31/91] Fmt fixes --- packages/omezarr/src/zarr/cached-loading/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index d14a145d..a9da5041 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -233,7 +233,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { }, isFetchSliceResponseMessage, [], - abort + abort, ); request From 6b5aa07ff0aa859755ed86d42b35a35eec788e74 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 14:36:56 -0700 Subject: [PATCH 32/91] Silly me --- packages/omezarr/src/zarr/cached-loading/store.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index a9da5041..d4462324 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -229,7 +229,6 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { path: key, range, options, - abort, }, isFetchSliceResponseMessage, [], From ed0509211a51e5bcce1f85ef97462c3d694f35d9 Mon Sep 17 00:00:00 2001 From: noah Date: Tue, 7 Oct 2025 14:52:26 -0700 Subject: [PATCH 33/91] some unit tests, most of which pass - one does not. I also made a slight change so I dont have to mock a pile of stuff. --- .../src/zarr/cached-loading/store.test.ts | 147 ++++++++++++++++++ .../omezarr/src/zarr/cached-loading/store.ts | 31 +++- 2 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 packages/omezarr/src/zarr/cached-loading/store.test.ts diff --git a/packages/omezarr/src/zarr/cached-loading/store.test.ts b/packages/omezarr/src/zarr/cached-loading/store.test.ts new file mode 100644 index 00000000..490d5ec9 --- /dev/null +++ b/packages/omezarr/src/zarr/cached-loading/store.test.ts @@ -0,0 +1,147 @@ + +import { describe, expect, test } from 'vitest'; +import { ICachingMultithreadedFetchStore, RequestHandler } from './store'; +import { FetchSliceMessage, FetchSliceResponseMessage } from './fetch-slice.interface'; +import { PromiseFarm } from '@alleninstitute/vis-core/src/shared-priority-cache/test-utils'; + +type SpyLog = { + request: any, + response: any, + status: 'cancelled' | 'failed' | 'resolved' +} +class Whatever implements RequestHandler { + promises: PromiseFarm; + log: SpyLog[]; + constructor(farm: PromiseFarm) { + this.promises = farm; + this.log = [] + } + submitRequest(message: RequestType, responseValidator: (obj: unknown) => obj is ResponseType, transfers: Transferable[], signal?: AbortSignal | undefined): Promise { + // so the generic parameters here cant work - the compile-time types of the interface to the worker are determined at construction time, not request time. + // you can make the types work out here, but its a foot-gun. if you pass a responseValidator that the worker cant handle, the types will work out, but none of your promises will ever + // resolve. + const p = this.promises.promiseMe(() => { + const resp = { + request: message, + id: message.id, + payload: new Uint8Array(400) + } + this.log.push({ request: message, response: resp, status: 'resolved' }) + return resp + }) + // this if(signal) block simulates the real worker-pool.ts, line 78 + if (signal) { + signal.addEventListener('abort', () => { + console.log('simulate cancel message to worker', message) + this.log.push({ request: message, response: undefined, status: 'cancelled' }) + if (!this.promises.mockReject(p, 'cancel')) { + console.log('fake promise could not be found to reject...') + } + }) + } else { + console.warn('warning - test request had no abort signal!') + } + + return p; + } + +} +describe('basics', () => { + + test('requests seem to work', async () => { + const farm = new PromiseFarm() + const pool = new Whatever(farm) + const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }) + + const a = store.get('/0/0') + const b = store.get('/0/1') + + farm.resolveAll(); + const [A, B] = await Promise.all([a, b]); + console.log(pool.log) + expect(pool.log).toHaveLength(2) + expect(A).toBeDefined() + expect(B).toBeDefined() + + }) + test('duplicate requests get cached', async () => { + const farm = new PromiseFarm() + const pool = new Whatever(farm) + const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }) + + const a = store.getRange('/0/0', { length: 100, offset: 0, suffixLength: 22 }) + const b = store.getRange('/0/1', { length: 100, offset: 0, suffixLength: 22 }) + const c = store.getRange('/0/0', { length: 100, offset: 0, suffixLength: 22 }) // same as a + + farm.resolveAll(); + const [A, B, C] = await Promise.all([a, b, c]); + console.log(pool.log) + expect(pool.log).toHaveLength(2) // c should come from the cache, the pool should not even see it + expect(A).toBeDefined() + expect(B).toBeDefined() + expect(C).toBeDefined() + }) + test('requests can be cancelled', async () => { + const farm = new PromiseFarm() + const pool = new Whatever(farm) + const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }) + const abortBoth = new AbortController() + const a = store.get('/0/0', { signal: abortBoth.signal }) + const b = store.get('/0/1', { signal: abortBoth.signal }) + abortBoth.abort(); + // const [A, B] = await Promise.all([a, b]); + expect(pool.log).toHaveLength(2) + expect(pool.log.every((x) => x.status === 'cancelled')) + // farm.resolveAll(); + try { + const x = await a; + // indicates the test should fail because we expect a rejection! + expect(x).toBe('cancelled') + + } catch (reason) { + expect(reason).toBe('cancel') + } + try { + const x = await b; + // indicates the test should fail because we expect a rejection! + expect(x).toBe('cancelled') + + } catch (reason) { + expect(reason).toBe('cancel') + } + }) + test('request the same thing twice, cancel one of the requests before either can resolve', async () => { + + const farm = new PromiseFarm() + const pool = new Whatever(farm) + const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }) + const abortA = new AbortController(); + const abortB = new AbortController(); + const a = store.get('/0/0', { signal: abortA.signal }) + const b = store.get('/0/0', { signal: abortB.signal }) + try { + console.log('lets abort') + try { + abortA.abort() + const A = await a; + expect(false).toBe(true) + } catch (reasonForA) { + console.log('a cancelled, this is fine') + expect(reasonForA).toBe('cancel') // we aborted it, this is fine + } + const B = await b + console.log('B is ', B) + expect(B instanceof Uint8Array).toBeTruthy() + expect(pool.log).toHaveLength(1) + expect(pool.log[0].status).toEqual('resolved') + } catch (reason) { + console.error('b was aborted - this is a bug! ', reason) + expect(false).toBeTruthy() + } + // the above stuff indicates a bug - in which cancelling A also cancels B, due to how the abort controllers are passed around + // once that is fixed - we can call farm.resolveAll() after we call abortA.abort() and we should expect B to resolve! + // expect the non-cancelled response to resolve + // farm.resolveAll() + + }) +}) \ No newline at end of file diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index d4462324..5b21a7a0 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -32,7 +32,7 @@ class CacheableByteArray implements Cacheable { this.#arr = arr; } - destroy() {} + destroy() { } sizeInBytes(): number { return this.#arr.byteLength; @@ -98,11 +98,21 @@ type PendingRequest = { promise: Promise; }; -export class CachingMultithreadedFetchStore extends zarr.FetchStore { +type Guard = (obj: unknown) => obj is T +export interface RequestHandler { + submitRequest( + message: RequestType, + responseValidator: Guard, + transfers: Transferable[], + signal?: AbortSignal | undefined, + ): Promise; +} + +export class ICachingMultithreadedFetchStore extends zarr.FetchStore { /** * Maintains a pool of available worker threads. */ - #workerPool: WorkerPool; + #workerPool: RequestHandler; /** * Stores the current set of cached data that has been successfully @@ -138,7 +148,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { */ #scoreFn: (h: CacheKey) => number; - constructor(url: string | URL, options?: CachingMultithreadedFetchStoreOptions) { + constructor(url: string | URL, handler: RequestHandler, options?: CachingMultithreadedFetchStoreOptions) { super(url, options?.fetchStoreOptions); this.#scoreFn = (h: CacheKey) => this.score(h); this.#dataCache = new PriorityCache( @@ -147,10 +157,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { options?.maxBytes ?? getDataCacheSizeLimit(), ); this.#priorityByTimestamp = new Map(); - this.#workerPool = new WorkerPool( - options?.numWorkers ?? DEFAULT_NUM_WORKERS, - new URL('./fetch-slice.worker.ts', import.meta.url), - ); + this.#workerPool = handler; this.#pendingRequests = new Map(); this.#pendingRequestKeyCounts = new Map(); } @@ -285,3 +292,11 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { return this.#doFetch(key, range, workerOptions, abort); } } +export class CachingMultithreadedFetchStore extends ICachingMultithreadedFetchStore { + constructor(url: string | URL, options?: CachingMultithreadedFetchStoreOptions) { + super(url, new WorkerPool( + options?.numWorkers ?? DEFAULT_NUM_WORKERS, + new URL('./fetch-slice.worker.ts', import.meta.url), + ), options) + } +} \ No newline at end of file From 0671fe7a080511a7eacaee3d698878d7ace59d3c Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 15:17:19 -0700 Subject: [PATCH 34/91] Updates to Store --- packages/omezarr/src/zarr/cached-loading/store.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 5b21a7a0..74af9df3 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -100,7 +100,7 @@ type PendingRequest = { type Guard = (obj: unknown) => obj is T export interface RequestHandler { - submitRequest( + submitRequest( message: RequestType, responseValidator: Guard, transfers: Transferable[], @@ -108,10 +108,15 @@ export interface RequestHandler { ): Promise; } -export class ICachingMultithreadedFetchStore extends zarr.FetchStore { +export class CachingMultithreadedFetchStore extends zarr.FetchStore { /** * Maintains a pool of available worker threads. + * + * TODO: Enable end-to-end Message-based type constraints for these that + * enable us to restrict what types of messages can be sent to workers + * for a given store instance. */ + // biome-ignore lint/suspicious/noExplicitAny: the type system for these parameters is a future feature #workerPool: RequestHandler; /** @@ -148,6 +153,7 @@ export class ICachingMultithreadedFetchStore extends zarr.FetchStore { */ #scoreFn: (h: CacheKey) => number; + // biome-ignore lint/suspicious/noExplicitAny: the type system for these parameters is a future feature constructor(url: string | URL, handler: RequestHandler, options?: CachingMultithreadedFetchStoreOptions) { super(url, options?.fetchStoreOptions); this.#scoreFn = (h: CacheKey) => this.score(h); @@ -292,7 +298,7 @@ export class ICachingMultithreadedFetchStore extends zarr.FetchStore { return this.#doFetch(key, range, workerOptions, abort); } } -export class CachingMultithreadedFetchStore extends ICachingMultithreadedFetchStore { +export class ZarrSliceFetchStore extends CachingMultithreadedFetchStore { constructor(url: string | URL, options?: CachingMultithreadedFetchStoreOptions) { super(url, new WorkerPool( options?.numWorkers ?? DEFAULT_NUM_WORKERS, From 61f66ea5bd8e65e438473d0133b706516f199198 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 15:19:05 -0700 Subject: [PATCH 35/91] Fmt fixes --- .../src/zarr/cached-loading/store.test.ts | 153 +++++++++--------- .../omezarr/src/zarr/cached-loading/store.ts | 20 ++- 2 files changed, 87 insertions(+), 86 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.test.ts b/packages/omezarr/src/zarr/cached-loading/store.test.ts index 490d5ec9..3081ea45 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.test.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.test.ts @@ -1,22 +1,26 @@ - import { describe, expect, test } from 'vitest'; import { ICachingMultithreadedFetchStore, RequestHandler } from './store'; import { FetchSliceMessage, FetchSliceResponseMessage } from './fetch-slice.interface'; import { PromiseFarm } from '@alleninstitute/vis-core/src/shared-priority-cache/test-utils'; type SpyLog = { - request: any, - response: any, - status: 'cancelled' | 'failed' | 'resolved' -} + request: any; + response: any; + status: 'cancelled' | 'failed' | 'resolved'; +}; class Whatever implements RequestHandler { promises: PromiseFarm; log: SpyLog[]; constructor(farm: PromiseFarm) { this.promises = farm; - this.log = [] + this.log = []; } - submitRequest(message: RequestType, responseValidator: (obj: unknown) => obj is ResponseType, transfers: Transferable[], signal?: AbortSignal | undefined): Promise { + submitRequest( + message: RequestType, + responseValidator: (obj: unknown) => obj is ResponseType, + transfers: Transferable[], + signal?: AbortSignal | undefined, + ): Promise { // so the generic parameters here cant work - the compile-time types of the interface to the worker are determined at construction time, not request time. // you can make the types work out here, but its a foot-gun. if you pass a responseValidator that the worker cant handle, the types will work out, but none of your promises will ever // resolve. @@ -24,124 +28,117 @@ class Whatever implements RequestHandler { - console.log('simulate cancel message to worker', message) - this.log.push({ request: message, response: undefined, status: 'cancelled' }) + console.log('simulate cancel message to worker', message); + this.log.push({ request: message, response: undefined, status: 'cancelled' }); if (!this.promises.mockReject(p, 'cancel')) { - console.log('fake promise could not be found to reject...') + console.log('fake promise could not be found to reject...'); } - }) + }); } else { - console.warn('warning - test request had no abort signal!') + console.warn('warning - test request had no abort signal!'); } return p; } - } describe('basics', () => { - test('requests seem to work', async () => { - const farm = new PromiseFarm() - const pool = new Whatever(farm) - const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }) + const farm = new PromiseFarm(); + const pool = new Whatever(farm); + const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); - const a = store.get('/0/0') - const b = store.get('/0/1') + const a = store.get('/0/0'); + const b = store.get('/0/1'); farm.resolveAll(); const [A, B] = await Promise.all([a, b]); - console.log(pool.log) - expect(pool.log).toHaveLength(2) - expect(A).toBeDefined() - expect(B).toBeDefined() - - }) + console.log(pool.log); + expect(pool.log).toHaveLength(2); + expect(A).toBeDefined(); + expect(B).toBeDefined(); + }); test('duplicate requests get cached', async () => { - const farm = new PromiseFarm() - const pool = new Whatever(farm) - const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }) + const farm = new PromiseFarm(); + const pool = new Whatever(farm); + const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); - const a = store.getRange('/0/0', { length: 100, offset: 0, suffixLength: 22 }) - const b = store.getRange('/0/1', { length: 100, offset: 0, suffixLength: 22 }) - const c = store.getRange('/0/0', { length: 100, offset: 0, suffixLength: 22 }) // same as a + const a = store.getRange('/0/0', { length: 100, offset: 0, suffixLength: 22 }); + const b = store.getRange('/0/1', { length: 100, offset: 0, suffixLength: 22 }); + const c = store.getRange('/0/0', { length: 100, offset: 0, suffixLength: 22 }); // same as a farm.resolveAll(); const [A, B, C] = await Promise.all([a, b, c]); - console.log(pool.log) - expect(pool.log).toHaveLength(2) // c should come from the cache, the pool should not even see it - expect(A).toBeDefined() - expect(B).toBeDefined() - expect(C).toBeDefined() - }) + console.log(pool.log); + expect(pool.log).toHaveLength(2); // c should come from the cache, the pool should not even see it + expect(A).toBeDefined(); + expect(B).toBeDefined(); + expect(C).toBeDefined(); + }); test('requests can be cancelled', async () => { - const farm = new PromiseFarm() - const pool = new Whatever(farm) - const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }) - const abortBoth = new AbortController() - const a = store.get('/0/0', { signal: abortBoth.signal }) - const b = store.get('/0/1', { signal: abortBoth.signal }) + const farm = new PromiseFarm(); + const pool = new Whatever(farm); + const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); + const abortBoth = new AbortController(); + const a = store.get('/0/0', { signal: abortBoth.signal }); + const b = store.get('/0/1', { signal: abortBoth.signal }); abortBoth.abort(); // const [A, B] = await Promise.all([a, b]); - expect(pool.log).toHaveLength(2) - expect(pool.log.every((x) => x.status === 'cancelled')) + expect(pool.log).toHaveLength(2); + expect(pool.log.every((x) => x.status === 'cancelled')); // farm.resolveAll(); try { const x = await a; // indicates the test should fail because we expect a rejection! - expect(x).toBe('cancelled') - + expect(x).toBe('cancelled'); } catch (reason) { - expect(reason).toBe('cancel') + expect(reason).toBe('cancel'); } try { const x = await b; // indicates the test should fail because we expect a rejection! - expect(x).toBe('cancelled') - + expect(x).toBe('cancelled'); } catch (reason) { - expect(reason).toBe('cancel') + expect(reason).toBe('cancel'); } - }) + }); test('request the same thing twice, cancel one of the requests before either can resolve', async () => { - - const farm = new PromiseFarm() - const pool = new Whatever(farm) - const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }) + const farm = new PromiseFarm(); + const pool = new Whatever(farm); + const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); const abortA = new AbortController(); const abortB = new AbortController(); - const a = store.get('/0/0', { signal: abortA.signal }) - const b = store.get('/0/0', { signal: abortB.signal }) + const a = store.get('/0/0', { signal: abortA.signal }); + const b = store.get('/0/0', { signal: abortB.signal }); try { - console.log('lets abort') + console.log('lets abort'); try { - abortA.abort() + abortA.abort(); const A = await a; - expect(false).toBe(true) + expect(false).toBe(true); } catch (reasonForA) { - console.log('a cancelled, this is fine') - expect(reasonForA).toBe('cancel') // we aborted it, this is fine + console.log('a cancelled, this is fine'); + expect(reasonForA).toBe('cancel'); // we aborted it, this is fine } - const B = await b - console.log('B is ', B) - expect(B instanceof Uint8Array).toBeTruthy() - expect(pool.log).toHaveLength(1) - expect(pool.log[0].status).toEqual('resolved') + const B = await b; + console.log('B is ', B); + expect(B instanceof Uint8Array).toBeTruthy(); + expect(pool.log).toHaveLength(1); + expect(pool.log[0].status).toEqual('resolved'); } catch (reason) { - console.error('b was aborted - this is a bug! ', reason) - expect(false).toBeTruthy() + console.error('b was aborted - this is a bug! ', reason); + expect(false).toBeTruthy(); } // the above stuff indicates a bug - in which cancelling A also cancels B, due to how the abort controllers are passed around // once that is fixed - we can call farm.resolveAll() after we call abortA.abort() and we should expect B to resolve! // expect the non-cancelled response to resolve // farm.resolveAll() - - }) -}) \ No newline at end of file + }); +}); diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 74af9df3..c47f1d28 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -32,7 +32,7 @@ class CacheableByteArray implements Cacheable { this.#arr = arr; } - destroy() { } + destroy() {} sizeInBytes(): number { return this.#arr.byteLength; @@ -98,7 +98,7 @@ type PendingRequest = { promise: Promise; }; -type Guard = (obj: unknown) => obj is T +type Guard = (obj: unknown) => obj is T; export interface RequestHandler { submitRequest( message: RequestType, @@ -111,7 +111,7 @@ export interface RequestHandler { export class CachingMultithreadedFetchStore extends zarr.FetchStore { /** * Maintains a pool of available worker threads. - * + * * TODO: Enable end-to-end Message-based type constraints for these that * enable us to restrict what types of messages can be sent to workers * for a given store instance. @@ -300,9 +300,13 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { } export class ZarrSliceFetchStore extends CachingMultithreadedFetchStore { constructor(url: string | URL, options?: CachingMultithreadedFetchStoreOptions) { - super(url, new WorkerPool( - options?.numWorkers ?? DEFAULT_NUM_WORKERS, - new URL('./fetch-slice.worker.ts', import.meta.url), - ), options) + super( + url, + new WorkerPool( + options?.numWorkers ?? DEFAULT_NUM_WORKERS, + new URL('./fetch-slice.worker.ts', import.meta.url), + ), + options, + ); } -} \ No newline at end of file +} From 851483f96ac39e39fe044bf9eb28ef901f963dc9 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 15:21:33 -0700 Subject: [PATCH 36/91] Fixed type issues in test --- .../omezarr/src/zarr/cached-loading/store.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.test.ts b/packages/omezarr/src/zarr/cached-loading/store.test.ts index 3081ea45..29d0bdc9 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.test.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { ICachingMultithreadedFetchStore, RequestHandler } from './store'; -import { FetchSliceMessage, FetchSliceResponseMessage } from './fetch-slice.interface'; +import { CachingMultithreadedFetchStore, type RequestHandler } from './store'; +import type { FetchSliceMessage, FetchSliceResponseMessage } from './fetch-slice.interface'; import { PromiseFarm } from '@alleninstitute/vis-core/src/shared-priority-cache/test-utils'; type SpyLog = { @@ -53,7 +53,7 @@ describe('basics', () => { test('requests seem to work', async () => { const farm = new PromiseFarm(); const pool = new Whatever(farm); - const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); + const store = new CachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); const a = store.get('/0/0'); const b = store.get('/0/1'); @@ -68,7 +68,7 @@ describe('basics', () => { test('duplicate requests get cached', async () => { const farm = new PromiseFarm(); const pool = new Whatever(farm); - const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); + const store = new CachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); const a = store.getRange('/0/0', { length: 100, offset: 0, suffixLength: 22 }); const b = store.getRange('/0/1', { length: 100, offset: 0, suffixLength: 22 }); @@ -85,7 +85,7 @@ describe('basics', () => { test('requests can be cancelled', async () => { const farm = new PromiseFarm(); const pool = new Whatever(farm); - const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); + const store = new CachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); const abortBoth = new AbortController(); const a = store.get('/0/0', { signal: abortBoth.signal }); const b = store.get('/0/1', { signal: abortBoth.signal }); @@ -112,7 +112,7 @@ describe('basics', () => { test('request the same thing twice, cancel one of the requests before either can resolve', async () => { const farm = new PromiseFarm(); const pool = new Whatever(farm); - const store = new ICachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); + const store = new CachingMultithreadedFetchStore('fake.zarr', pool, { maxFetches: 10, numWorkers: 5 }); const abortA = new AbortController(); const abortB = new AbortController(); const a = store.get('/0/0', { signal: abortA.signal }); From 4ecd91419c4210ded3b0d89caf4199ff01da4716 Mon Sep 17 00:00:00 2001 From: noah Date: Tue, 7 Oct 2025 15:23:41 -0700 Subject: [PATCH 37/91] dont use onabort --- packages/core/src/workers/worker-pool.ts | 4 ++-- packages/omezarr/src/zarr/cached-loading/store.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/workers/worker-pool.ts b/packages/core/src/workers/worker-pool.ts index 660131d0..d77081ad 100644 --- a/packages/core/src/workers/worker-pool.ts +++ b/packages/core/src/workers/worker-pool.ts @@ -103,10 +103,10 @@ export class WorkerPool { this.#promises.set(workerIndex, messagePromise as unknown as MessagePromise); if (signal) { - signal.onabort = () => { + signal.addEventListener('abort', () => { this.#sendMessageToWorker(workerIndex, { type: 'cancel', id: reqId }, []); messagePromise.reject('cancelled'); - }; + }) } this.#sendMessageToWorker(workerIndex, messageWithId, transfers); diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index c47f1d28..282c2bc3 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -32,7 +32,7 @@ class CacheableByteArray implements Cacheable { this.#arr = arr; } - destroy() {} + destroy() { } sizeInBytes(): number { return this.#arr.byteLength; @@ -226,13 +226,13 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { this.#pendingRequests.set(cacheKey, { promise, resolve, reject }); if (abort) { - abort.onabort = () => { + abort.addEventListener('abort', () => { const count = this.#decrementKeyCount(cacheKey); if (count === 0) { this.#priorityByTimestamp.set(cacheKey, 0); this.#dataCache.reprioritize(this.#scoreFn); } - }; + }) } const request = this.#workerPool.submitRequest( From 57977754d91f9ef610d2a8004ec6efebe1e818f5 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Tue, 7 Oct 2025 15:26:08 -0700 Subject: [PATCH 38/91] Update store.test to use updated Requesthandler contract --- packages/omezarr/src/zarr/cached-loading/store.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.test.ts b/packages/omezarr/src/zarr/cached-loading/store.test.ts index 29d0bdc9..763afce4 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.test.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.test.ts @@ -15,12 +15,12 @@ class Whatever implements RequestHandler( - message: RequestType, - responseValidator: (obj: unknown) => obj is ResponseType, + submitRequest( + message: FetchSliceMessage, + responseValidator: (obj: unknown) => obj is FetchSliceResponseMessage, transfers: Transferable[], signal?: AbortSignal | undefined, - ): Promise { + ): Promise { // so the generic parameters here cant work - the compile-time types of the interface to the worker are determined at construction time, not request time. // you can make the types work out here, but its a foot-gun. if you pass a responseValidator that the worker cant handle, the types will work out, but none of your promises will ever // resolve. From 5fecf611b3ff6c2fa71988e234a1c9908469a575 Mon Sep 17 00:00:00 2001 From: noah Date: Tue, 7 Oct 2025 16:04:08 -0700 Subject: [PATCH 39/91] only abort the inner request if the count is zero --- .../src/zarr/cached-loading/store.test.ts | 21 ++++++++++++------- .../omezarr/src/zarr/cached-loading/store.ts | 5 +++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/omezarr/src/zarr/cached-loading/store.test.ts b/packages/omezarr/src/zarr/cached-loading/store.test.ts index 29d0bdc9..3c3a95ee 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.test.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.test.ts @@ -119,15 +119,20 @@ describe('basics', () => { const b = store.get('/0/0', { signal: abortB.signal }); try { console.log('lets abort'); - try { - abortA.abort(); - const A = await a; - expect(false).toBe(true); - } catch (reasonForA) { - console.log('a cancelled, this is fine'); - expect(reasonForA).toBe('cancel'); // we aborted it, this is fine - } + abortA.abort() + // try { + // abortA.abort(); + // const A = await a; + // expect(false).toBe(true); + // } catch (reasonForA) { + // console.log('a cancelled, this is fine'); + // expect(reasonForA).toBe('cancel'); // we aborted it, this is fine + // } + console.log('--------- resolve now?') + farm.resolveAll(); const B = await b; + // we know a is toast... do it this way for shortness: + a.then((x) => console.log('a should be cancelled, but instead its', x), (why) => console.log('all is well')) console.log('B is ', B); expect(B instanceof Uint8Array).toBeTruthy(); expect(pool.log).toHaveLength(1); diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 282c2bc3..21185e91 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -224,13 +224,14 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { const { promise, resolve, reject } = Promise.withResolvers(); this.#pendingRequests.set(cacheKey, { promise, resolve, reject }); - + const chain = new AbortController() if (abort) { abort.addEventListener('abort', () => { const count = this.#decrementKeyCount(cacheKey); if (count === 0) { this.#priorityByTimestamp.set(cacheKey, 0); this.#dataCache.reprioritize(this.#scoreFn); + chain.abort() } }) } @@ -245,7 +246,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { }, isFetchSliceResponseMessage, [], - abort, + chain.signal, ); request From 5403640d1b9509b82556db463ef936afb1d2f46b Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Wed, 8 Oct 2025 08:38:01 -0700 Subject: [PATCH 40/91] Linting and formatting cleanup --- packages/core/src/workers/worker-pool.ts | 2 +- .../cached-loading/fetch-slice.interface.ts | 6 +-- .../src/zarr/cached-loading/store.test.ts | 40 +++++++++---------- .../omezarr/src/zarr/cached-loading/store.ts | 8 ++-- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/core/src/workers/worker-pool.ts b/packages/core/src/workers/worker-pool.ts index d77081ad..49fba3b4 100644 --- a/packages/core/src/workers/worker-pool.ts +++ b/packages/core/src/workers/worker-pool.ts @@ -106,7 +106,7 @@ export class WorkerPool { signal.addEventListener('abort', () => { this.#sendMessageToWorker(workerIndex, { type: 'cancel', id: reqId }, []); messagePromise.reject('cancelled'); - }) + }); } this.#sendMessageToWorker(workerIndex, messageWithId, transfers); diff --git a/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts b/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts index 203d4b2f..8774a551 100644 --- a/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts +++ b/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts @@ -14,7 +14,7 @@ export type FetchSliceMessagePayload = { }; export const FETCH_SLICE_MESSAGE_TYPE = 'fetch-slice' as const; -export const FETCH_SLICE_RESPOSNE_MESSAGE_TYPE = 'fetch-slice-response' as const; +export const FETCH_SLICE_RESPONSE_MESSAGE_TYPE = 'fetch-slice-response' as const; export const CANCEL_MESSAGE_TYPE = 'cancel' as const; export type FetchSliceMessage = { @@ -24,7 +24,7 @@ export type FetchSliceMessage = { }; export type FetchSliceResponseMessage = { - type: typeof FETCH_SLICE_RESPOSNE_MESSAGE_TYPE; + type: typeof FETCH_SLICE_RESPONSE_MESSAGE_TYPE; id: string; payload: ArrayBufferLike | undefined; }; @@ -56,7 +56,7 @@ const FetchSliceMessageSchema = z.object({ }); const FetchSliceResponseMessageSchema = z.object({ - type: z.literal(FETCH_SLICE_RESPOSNE_MESSAGE_TYPE), + type: z.literal(FETCH_SLICE_RESPONSE_MESSAGE_TYPE), id: z.string().nonempty(), payload: z.unknown().optional(), // unclear if it's feasible/wise to define a schema for this one }); diff --git a/packages/omezarr/src/zarr/cached-loading/store.test.ts b/packages/omezarr/src/zarr/cached-loading/store.test.ts index 5354e889..5221d5e0 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.test.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.test.ts @@ -1,10 +1,16 @@ import { describe, expect, test } from 'vitest'; import { CachingMultithreadedFetchStore, type RequestHandler } from './store'; -import type { FetchSliceMessage, FetchSliceResponseMessage } from './fetch-slice.interface'; +import { + FETCH_SLICE_RESPONSE_MESSAGE_TYPE, + type FetchSliceMessage, + type FetchSliceResponseMessage, +} from './fetch-slice.interface'; import { PromiseFarm } from '@alleninstitute/vis-core/src/shared-priority-cache/test-utils'; type SpyLog = { + // biome-ignore lint/suspicious/noExplicitAny: Typing improvements for Messages are a future enhancement request: any; + // biome-ignore lint/suspicious/noExplicitAny: Typing improvements for Messages are a future enhancement response: any; status: 'cancelled' | 'failed' | 'resolved'; }; @@ -17,8 +23,8 @@ class Whatever implements RequestHandler obj is FetchSliceResponseMessage, - transfers: Transferable[], + _responseValidator: (obj: unknown) => obj is FetchSliceResponseMessage, + _transfers: Transferable[], signal?: AbortSignal | undefined, ): Promise { // so the generic parameters here cant work - the compile-time types of the interface to the worker are determined at construction time, not request time. @@ -26,9 +32,10 @@ class Whatever implements RequestHandler { const resp = { + type: FETCH_SLICE_RESPONSE_MESSAGE_TYPE, request: message, id: message.id, - payload: new Uint8Array(400), + payload: new Uint8Array(400).buffer, }; this.log.push({ request: message, response: resp, status: 'resolved' }); return resp; @@ -36,13 +43,14 @@ class Whatever implements RequestHandler { - console.log('simulate cancel message to worker', message); this.log.push({ request: message, response: undefined, status: 'cancelled' }); if (!this.promises.mockReject(p, 'cancel')) { + // biome-ignore lint/suspicious/noConsole: Provides test outcome context console.log('fake promise could not be found to reject...'); } }); } else { + // biome-ignore lint/suspicious/noConsole: Provides test outcome context console.warn('warning - test request had no abort signal!'); } @@ -60,7 +68,6 @@ describe('basics', () => { farm.resolveAll(); const [A, B] = await Promise.all([a, b]); - console.log(pool.log); expect(pool.log).toHaveLength(2); expect(A).toBeDefined(); expect(B).toBeDefined(); @@ -76,7 +83,6 @@ describe('basics', () => { farm.resolveAll(); const [A, B, C] = await Promise.all([a, b, c]); - console.log(pool.log); expect(pool.log).toHaveLength(2); // c should come from the cache, the pool should not even see it expect(A).toBeDefined(); expect(B).toBeDefined(); @@ -118,26 +124,20 @@ describe('basics', () => { const a = store.get('/0/0', { signal: abortA.signal }); const b = store.get('/0/0', { signal: abortB.signal }); try { - console.log('lets abort'); - abortA.abort() - // try { - // abortA.abort(); - // const A = await a; - // expect(false).toBe(true); - // } catch (reasonForA) { - // console.log('a cancelled, this is fine'); - // expect(reasonForA).toBe('cancel'); // we aborted it, this is fine - // } - console.log('--------- resolve now?') + abortA.abort(); farm.resolveAll(); const B = await b; // we know a is toast... do it this way for shortness: - a.then((x) => console.log('a should be cancelled, but instead its', x), (why) => console.log('all is well')) - console.log('B is ', B); + a.then( + // biome-ignore lint/suspicious/noConsole: Provides test outcome context + (x) => console.log('a should be cancelled, but instead its', x), + () => {}, + ); expect(B instanceof Uint8Array).toBeTruthy(); expect(pool.log).toHaveLength(1); expect(pool.log[0].status).toEqual('resolved'); } catch (reason) { + // biome-ignore lint/suspicious/noConsole: Provides test outcome context console.error('b was aborted - this is a bug! ', reason); expect(false).toBeTruthy(); } diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 21185e91..50d120d2 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -32,7 +32,7 @@ class CacheableByteArray implements Cacheable { this.#arr = arr; } - destroy() { } + destroy() {} sizeInBytes(): number { return this.#arr.byteLength; @@ -224,16 +224,16 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { const { promise, resolve, reject } = Promise.withResolvers(); this.#pendingRequests.set(cacheKey, { promise, resolve, reject }); - const chain = new AbortController() + const chain = new AbortController(); if (abort) { abort.addEventListener('abort', () => { const count = this.#decrementKeyCount(cacheKey); if (count === 0) { this.#priorityByTimestamp.set(cacheKey, 0); this.#dataCache.reprioritize(this.#scoreFn); - chain.abort() + chain.abort(); } - }) + }); } const request = this.#workerPool.submitRequest( From 4e1f6cd0c9a13d15881cbef8f1d3152f7d4b6f3f Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Wed, 8 Oct 2025 08:47:20 -0700 Subject: [PATCH 41/91] WIP --- packages/omezarr/src/zarr/omezarr-fileset.ts | 65 ++++++++++++++++---- packages/omezarr/src/zarr/types.ts | 58 +++++++++++++++++ 2 files changed, 111 insertions(+), 12 deletions(-) diff --git a/packages/omezarr/src/zarr/omezarr-fileset.ts b/packages/omezarr/src/zarr/omezarr-fileset.ts index 2478569b..73252c8d 100644 --- a/packages/omezarr/src/zarr/omezarr-fileset.ts +++ b/packages/omezarr/src/zarr/omezarr-fileset.ts @@ -1,33 +1,74 @@ +import { logger, type WebResource } from '@alleninstitute/vis-core'; import * as zarr from 'zarrita'; import { z } from 'zod'; -import { OmeZarrArrayMetadata, OmeZarrAttrs, OmeZarrAttrsSchema, OmeZarrMetadata } from './types'; +import { type OmeZarrArray, OmeZarrArrayTransform, type OmeZarrGroup, OmeZarrGroupTransform } from './types'; import { CachingMultithreadedFetchStore } from "./cached-loading/store"; -import { logger } from '@alleninstitute/vis-core'; + +// export type OmeZarrAttrsBundle = { +// root: OmeZarrAttrs, +// arrays: ReadonlyMap +// }; export class OmeZarrFileset { #store: CachingMultithreadedFetchStore; #root: zarr.Location; - #rootAttrs: OmeZarrAttrs | undefined; - #arrayAttrs: Map + #rootGroup: OmeZarrGroup | null; + #arrays: Map; - constructor(url: string | URL) { - this.#store = new CachingMultithreadedFetchStore(url); + constructor(res: WebResource) { + this.#store = new CachingMultithreadedFetchStore(res.url); this.#root = zarr.root(this.#store); + this.#rootGroup = null; + this.#arrays = new Map(); } - async #loadRootAttrs(store: zarr.FetchStore): Promise { - const group = await zarr.open(store, { kind: 'group' }); + async #loadGroup(location: zarr.FetchStore | zarr.Location): Promise { + const group = await zarr.open(location, { kind: 'group' }); try { - return OmeZarrAttrsSchema.parse(group.attrs); + return OmeZarrGroupTransform.parse(group.attrs); } catch (e) { if (e instanceof z.ZodError) { - logger.error('could not load Zarr file: parsing failed'); + logger.error('could not load Zarr group metadata: parsing failed'); } throw e; } } - loadMetadata(): Promise { - + async #loadArray(location: zarr.FetchStore | zarr.Location): Promise { + const array = await zarr.open(location, { kind: 'array' }); + try { + return OmeZarrArrayTransform.parse(array); + } catch (e) { + if (e instanceof z.ZodError) { + logger.error('could not load Zarr array metadata: parsing failed'); + } + throw e; + } + } + + async #loadRootAttrs(): Promise { + return await this.#loadGroup(this.#root); + } + + async loadMetadata() { + if (this.#rootGroup !== null) { + logger.warn('attempted to load the same OME-Zarr fileset after it was already loaded'); + return; + } + this.#rootGroup = await this.#loadRootAttrs(); + + const arrayResults = await Promise.all( + this.#rootGroup.attributes.multiscales + .map((multiscale) => + multiscale.datasets?.map(async (dataset) => { + return (await this.#loadArray(this.#root.resolve(dataset.path))) + }) + ) + .reduce((prev, curr) => prev.concat(curr)) + .filter((arr) => arr !== undefined), + ); + arrayResults.forEach((arr) => { + this.#arrays.set(arr.path, arr); + }); } } \ No newline at end of file diff --git a/packages/omezarr/src/zarr/types.ts b/packages/omezarr/src/zarr/types.ts index 58d26c02..bcca2315 100644 --- a/packages/omezarr/src/zarr/types.ts +++ b/packages/omezarr/src/zarr/types.ts @@ -1,6 +1,7 @@ import type { CartesianPlane, Interval, vec3, vec4 } from '@alleninstitute/vis-geometry'; import { VisZarrDataError, VisZarrIndexError } from '../errors'; import { logger, makeRGBAColorVector } from '@alleninstitute/vis-core'; +import type * as zarr from 'zarrita'; import { z } from 'zod'; export type ZarrDimension = 't' | 'c' | 'z' | 'y' | 'x'; @@ -152,6 +153,33 @@ export type OmeZarrAttrs = { zarrVersion: number; } & BaseOmeZarrAttrs; +// newer types that align a little more closely with how Zarr/OME-Zarr data +// is actually represented +export type OmeZarrNode = { + nodeType: 'group' | 'array'; +} + +export type OmeZarrGroup = OmeZarrNode & { + nodeType: 'group'; + zarrFormat: 2 | 3; + attributes: OmeZarrGroupAttributes; +} + +export type OmeZarrGroupAttributes = { + multiscales: OmeZarrMultiscale[]; + omero?: OmeZarrOmero | undefined; // omero is a transitional field, meaning it is expected to go away in a later version + version?: string | undefined; +}; + +export type OmeZarrArray = OmeZarrNode & { + nodeType: 'array'; + path: string; + chunkShape: number[]; + dataType: string; + shape: number[]; + attributes: Record; +}; + export const OmeZarrAttrsBaseSchema: z.ZodType = z.object({ multiscales: OmeZarrMultiscaleSchema.array().nonempty(), omero: OmeZarrOmeroSchema.optional(), @@ -178,6 +206,36 @@ export const OmeZarrAttrsSchema = z }; }); +export const OmeZarrGroupTransform = z.union([OmeZarrAttrsV2Schema, OmeZarrAttrsV3Schema]) + .transform((v: OmeZarrAttrsV2 | OmeZarrAttrsV3) => { + if ('ome' in v) { + return { + nodeType: 'group', + zarrFormat: 3, + attributes: v.ome, + }; + } + return { + nodeType: 'group', + zarrFormat: 2, + attributes: v, + }; + }); + + +type ZarritaArray = zarr.Array; + +export const OmeZarrArrayTransform = z.transform((v: ZarritaArray) => { + return { + nodeType: 'array', + path: v.path, + chunkShape: v.chunks, + dataType: v.dtype, + shape: v.shape, + attributes: v.attrs, + } as OmeZarrArray; +}); + export type DehydratedOmeZarrArray = { path: string; }; From e66585ddc457b0c88fc968fefe6806710bf02ef2 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Wed, 8 Oct 2025 14:06:57 -0700 Subject: [PATCH 42/91] Fixed Worker Pools to use IDs instead of indices for promises --- packages/core/src/workers/worker-pool.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/src/workers/worker-pool.ts b/packages/core/src/workers/worker-pool.ts index 0aaac3f0..c80ef0a5 100644 --- a/packages/core/src/workers/worker-pool.ts +++ b/packages/core/src/workers/worker-pool.ts @@ -26,7 +26,7 @@ export enum WorkerStatus { export class WorkerPool { #workers: Worker[]; - #promises: Map; + #promises: Map; #timeOfPreviousHeartbeat: Map; #which: number; @@ -44,17 +44,18 @@ export class WorkerPool { #handleMessage(workerIndex: number, msg: MessageEvent) { const { data } = msg; - const messagePromise = this.#promises.get(workerIndex); if (isHeartbeatMessage(data)) { this.#timeOfPreviousHeartbeat.set(workerIndex, Date.now()); return; } - if (messagePromise === undefined) { - logger.warn('unexpected message from worker'); - return; - } if (isWorkerMessageWithId(data)) { - this.#promises.delete(workerIndex); + const { id } = data; + const messagePromise = this.#promises.get(id); + if (messagePromise === undefined) { + logger.warn('unexpected message from worker'); + return; + } + this.#promises.delete(id); if (!messagePromise.validator(data)) { const reason = 'invalid response from worker: message type did not match expected type'; logger.error(reason); @@ -64,8 +65,7 @@ export class WorkerPool { messagePromise.resolve(data); } else { const reason = 'encountered an invalid message; skipping'; - logger.error(reason); - messagePromise.reject(new Error(reason)); + logger.warn(reason); } } @@ -84,7 +84,7 @@ export class WorkerPool { const messageWithId = { ...message, id: reqId }; const messagePromise = this.#createMessagePromise(responseValidator); - this.#promises.set(workerIndex, messagePromise); + this.#promises.set(reqId, messagePromise); if (signal) { signal.addEventListener('abort', () => { From 58f8eb7379d1325835692ba39ff57c004768de22 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Wed, 8 Oct 2025 15:02:47 -0700 Subject: [PATCH 43/91] Moved 'tile-renderer' to its own package and named it 'tile-rendering' --- .../tile-rendering.ts} | 158 +++++++++--------- 1 file changed, 83 insertions(+), 75 deletions(-) rename packages/omezarr/src/{sliceview/tile-renderer.ts => rendering/tile-rendering.ts} (83%) diff --git a/packages/omezarr/src/sliceview/tile-renderer.ts b/packages/omezarr/src/rendering/tile-rendering.ts similarity index 83% rename from packages/omezarr/src/sliceview/tile-renderer.ts rename to packages/omezarr/src/rendering/tile-rendering.ts index e1128f16..4cc357ca 100644 --- a/packages/omezarr/src/sliceview/tile-renderer.ts +++ b/packages/omezarr/src/rendering/tile-rendering.ts @@ -6,7 +6,8 @@ import type { vec2, vec3, vec4 } from '@alleninstitute/vis-geometry'; import type REGL from 'regl'; import type { Framebuffer2D } from 'regl'; -const vert = /*glsl*/ ` +/* ========================= STANDARD VERTEX SHADER ========================= */ +const tileVert = /*glsl*/ ` precision highp float; attribute vec2 pos; @@ -27,6 +28,35 @@ void main(){ gl_Position = vec4(p.x,p.y,depth,1.0); } `; +/* -------------------------------------------------------------------------- */ + +/* ======================== RGB TILE FRAGMENT SHADER ======================== */ +const rgbTileFrag = /*glsl*/ ` +precision highp float; +uniform sampler2D R; +uniform sampler2D G; +uniform sampler2D B; // for reasons which are pretty annoying +// its more direct to do 3 separate channels... +uniform vec2 Rgamut; +uniform vec2 Ggamut; +uniform vec2 Bgamut; + +varying vec2 texCoord; +void main(){ + vec3 mins = vec3(Rgamut.x,Ggamut.x,Bgamut.x); + vec3 maxs = vec3(Rgamut.y,Ggamut.y,Bgamut.y); + vec3 span = maxs-mins; + vec3 color = (vec3( + texture2D(R, texCoord).r, + texture2D(G, texCoord).r, + texture2D(B, texCoord).r + )-mins) /span; + + gl_FragColor = vec4(color, 1.0); +} +`; +/* -------------------------------------------------------------------------- */ + type CommonRenderProps = { target: Framebuffer2D | null; depth: number; // the Z value at which to render the tile, from 0 (the front) to 1 (the back) @@ -53,78 +83,6 @@ type TileRenderProps = CommonRenderProps & { channels: Channel[]; }; -/** - * - * @param regl an active REGL context - * @returns a function (regl command) which renders 3 individual channels as the RGB - * components of an image. Each channel is mapped to the output RGB space via the given Gamut. - * the rendering is done in the given target buffer (or null for the screen). - */ -export function buildRGBTileRenderer(regl: REGL.Regl) { - const cmd = regl< - { - view: vec4; - tile: vec4; - depth: number; - R: REGL.Texture2D; - G: REGL.Texture2D; - B: REGL.Texture2D; - Rgamut: vec2; - Ggamut: vec2; - Bgamut: vec2; - }, - { pos: REGL.BufferData }, - RGBTileRenderProps - >({ - vert, - frag: ` - precision highp float; - uniform sampler2D R; - uniform sampler2D G; - uniform sampler2D B; // for reasons which are pretty annoying - // its more direct to do 3 separate channels... - uniform vec2 Rgamut; - uniform vec2 Ggamut; - uniform vec2 Bgamut; - - varying vec2 texCoord; - void main(){ - vec3 mins = vec3(Rgamut.x,Ggamut.x,Bgamut.x); - vec3 maxs = vec3(Rgamut.y,Ggamut.y,Bgamut.y); - vec3 span = maxs-mins; - vec3 color = (vec3( - texture2D(R, texCoord).r, - texture2D(G, texCoord).r, - texture2D(B, texCoord).r - )-mins) /span; - - gl_FragColor = vec4(color, 1.0); - }`, - framebuffer: regl.prop('target'), - attributes: { - pos: [0, 0, 1, 0, 1, 1, 0, 1], - }, - uniforms: { - tile: regl.prop('tile'), - view: regl.prop('view'), - depth: regl.prop('depth'), - R: regl.prop('R'), - G: regl.prop('G'), - B: regl.prop('B'), - Rgamut: regl.prop('Rgamut'), - Ggamut: regl.prop('Ggamut'), - Bgamut: regl.prop('Bgamut'), - }, - depth: { - enable: true, - }, - count: 4, - primitive: 'triangle fan', - }); - - return (p: RGBTileRenderProps) => cmd(p); -} - // biome-ignore lint/suspicious/noExplicitAny: type of uniforms cannot be given explicitly due to dynamic nature of uniforms in these shaders type ReglUniforms = REGL.MaybeDynamicUniforms; @@ -136,7 +94,7 @@ type ReglUniforms = REGL.MaybeDynamicUniforms({ - vert, + vert: tileVert, frag, framebuffer: regl.prop('target'), attributes: { @@ -197,3 +155,53 @@ export function buildTileRenderer(regl: REGL.Regl, numChannels: number) { return (p: TileRenderProps) => cmd(p); } + +/** + * A simplified Tile Render Command that specifically handles RGB channels. + * @param regl an active REGL context + * @returns a function (regl command) which renders 3 individual channels as the RGB + * components of an image. Each channel is mapped to the output RGB space via the given Gamut. + * the rendering is done in the given target buffer (or null for the screen). + */ +export function buildRGBTileRenderCommand(regl: REGL.Regl) { + const cmd = regl< + { + view: vec4; + tile: vec4; + depth: number; + R: REGL.Texture2D; + G: REGL.Texture2D; + B: REGL.Texture2D; + Rgamut: vec2; + Ggamut: vec2; + Bgamut: vec2; + }, + { pos: REGL.BufferData }, + RGBTileRenderProps + >({ + vert: tileVert, + frag: rgbTileFrag, + framebuffer: regl.prop('target'), + attributes: { + pos: [0, 0, 1, 0, 1, 1, 0, 1], + }, + uniforms: { + tile: regl.prop('tile'), + view: regl.prop('view'), + depth: regl.prop('depth'), + R: regl.prop('R'), + G: regl.prop('G'), + B: regl.prop('B'), + Rgamut: regl.prop('Rgamut'), + Ggamut: regl.prop('Ggamut'), + Bgamut: regl.prop('Bgamut'), + }, + depth: { + enable: true, + }, + count: 4, + primitive: 'triangle fan', + }); + + return (p: RGBTileRenderProps) => cmd(p); +} From 170e6bfcfc4fa07295fe52d1a949ca4f36396bca Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Wed, 8 Oct 2025 15:03:08 -0700 Subject: [PATCH 44/91] Cleaned up outdated comment in loading.ts --- packages/omezarr/src/zarr/loading.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/omezarr/src/zarr/loading.ts b/packages/omezarr/src/zarr/loading.ts index 06627850..9bdf11b7 100644 --- a/packages/omezarr/src/zarr/loading.ts +++ b/packages/omezarr/src/zarr/loading.ts @@ -179,10 +179,11 @@ export function pickBestScale( }, datasets[0]); return choice ?? datasets[datasets.length - 1]; } -// TODO this is a duplicate of indexOfDimension... delete one of them! + function indexFor(dim: ZarrDimension, axes: readonly OmeZarrAxis[]) { return axes.findIndex((axis) => axis.name === dim); } + /** * * @param layer a shaped layer from within the omezarr dataset From 27827fabe226e5249800290a8495e4c6630e3586 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Wed, 8 Oct 2025 15:03:40 -0700 Subject: [PATCH 45/91] Updated slice-renderer to use new tile-rendering nomenclature --- packages/omezarr/src/sliceview/slice-renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/omezarr/src/sliceview/slice-renderer.ts b/packages/omezarr/src/sliceview/slice-renderer.ts index c9e84822..a574776c 100644 --- a/packages/omezarr/src/sliceview/slice-renderer.ts +++ b/packages/omezarr/src/sliceview/slice-renderer.ts @@ -18,7 +18,7 @@ import { import type REGL from 'regl'; import type { ZarrRequest } from '../zarr/loading'; import { type VoxelTile, getVisibleTiles } from './loader'; -import { buildTileRenderer } from './tile-renderer'; +import { buildTileRenderCommand } from '../rendering/tile-rendering'; import type { OmeZarrMetadata, OmeZarrShapedDataset } from '../zarr/types'; export type RenderSettingsChannel = { @@ -130,7 +130,7 @@ export function buildOmeZarrSliceRenderer( type: 'texture', }; } - const cmd = buildTileRenderer(regl, numChannels); + const cmd = buildTileRenderCommand(regl, numChannels); return { cacheKey: (item, requestKey, dataset, settings) => { const channelKeys = Object.keys(settings.channels); From 43197bccf2a6b1b76792e9c70f0be50795574f64 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 9 Oct 2025 09:16:10 -0700 Subject: [PATCH 46/91] First potentially-stable version --- packages/omezarr/src/index.ts | 16 +- .../omezarr/src/planar-view/calculations.ts | 284 ++++++++++++++++++ .../src/planar-view/planar-renderer.ts | 170 +++++++++++ packages/omezarr/src/planar-view/settings.ts | 22 ++ packages/omezarr/src/planar-view/types.ts | 17 ++ packages/omezarr/src/zarr/omezarr-fileset.ts | 262 +++++++++++++++- .../omezarr/src/zarr/omezarr-transforms.ts | 40 +++ packages/omezarr/tsconfig.json | 2 +- 8 files changed, 797 insertions(+), 16 deletions(-) create mode 100644 packages/omezarr/src/planar-view/calculations.ts create mode 100644 packages/omezarr/src/planar-view/planar-renderer.ts create mode 100644 packages/omezarr/src/planar-view/settings.ts create mode 100644 packages/omezarr/src/planar-view/types.ts create mode 100644 packages/omezarr/src/zarr/omezarr-transforms.ts diff --git a/packages/omezarr/src/index.ts b/packages/omezarr/src/index.ts index 733db642..46f1aad2 100644 --- a/packages/omezarr/src/index.ts +++ b/packages/omezarr/src/index.ts @@ -12,7 +12,7 @@ export { defaultDecoder, getVisibleTiles, } from './sliceview/loader'; -export { buildTileRenderer, buildRGBTileRenderer } from './sliceview/tile-renderer'; +export { buildTileRenderCommand, buildRGBTileRenderCommand } from './rendering/tile-rendering'; export { type ZarrDimension, type OmeZarrAxis, @@ -55,3 +55,17 @@ export { } from './zarr/loading'; export { type CancelRequest, type ZarrSliceRequest, makeOmeZarrSliceLoaderWorker } from './sliceview/worker-loader'; + +export { + type OmeZarrDataContext, + type OmeZarrDatasetSpecifier, + OmeZarrFileset, + type ZarrDataRequest, + type ZarrDimensionSelection, + type ZarrSelection, + type ZarrSlice, +} from './zarr/omezarr-fileset'; +export { + OmeZarrArrayTransform, + OmeZarrGroupTransform, +} from './zarr/omezarr-transforms'; diff --git a/packages/omezarr/src/planar-view/calculations.ts b/packages/omezarr/src/planar-view/calculations.ts new file mode 100644 index 00000000..3a1ee838 --- /dev/null +++ b/packages/omezarr/src/planar-view/calculations.ts @@ -0,0 +1,284 @@ +import { logger } from "@alleninstitute/vis-core"; +import { Box2D, type box2D, type CartesianPlane, Vec2, type vec2 } from "@alleninstitute/vis-geometry"; +import { VisZarrDataError } from "../errors"; +import type { OmeZarrFileset, OmeZarrDataContext as OmeZarrLevel } from "../zarr/omezarr-fileset"; +import type { OmeZarrAxis, OmeZarrCoordinateTransform, ZarrDimension } from "../zarr/types"; +import type { VoxelTile } from "./types"; + +function indexFor(dim: ZarrDimension, axes: readonly OmeZarrAxis[]) { + return axes.findIndex((axis) => axis.name === dim); +} + +export const pickBestScale = ( + fileset: OmeZarrFileset, + plane: CartesianPlane, + relativeView: box2D, // a box in data-unit-space + displayResolution: vec2, // in the plane given above + multiscaleName?: string | undefined, +): OmeZarrLevel => { + if (!fileset.ready) { + const message = 'cannot pick best-fitting scale: OME-Zarr metadata not yet loaded'; + logger.error(message); + throw new VisZarrDataError(message); + } + + const dataContext = fileset.getDataContext({ index: 0, multiscale: multiscaleName }); + if (!dataContext) { + const message = 'cannot pick best-fitting scale: no initial dataset context found'; + logger.error(message); + throw new VisZarrDataError(message); + } + + const { multiscale, dataset, array } = dataContext; + + const axes = multiscale.axes; + const transforms = dataset.coordinateTransformations; + const shape = array.shape; + + const realSize = sizeInUnits(plane, axes, transforms, shape); + if (!realSize) { + const message = 'invalid Zarr data: could not determine the size of the plane in the given units'; + logger.error(message); + throw new VisZarrDataError(message); + } + + const vxlPitch = (size: vec2) => Vec2.div(realSize, size); + + // size, in dataspace, of a pixel 1/res + const pxPitch = Vec2.div(Box2D.size(relativeView), displayResolution); + const dstToDesired = (a: vec2, goal: vec2) => { + const diff = Vec2.sub(a, goal); + if (diff[0] * diff[1] > 0) { + // the res (a) is higher than our goal - + // weight this heavily to prefer smaller than the goal + return 1000 * Vec2.length(Vec2.sub(a, goal)); + } + return Vec2.length(Vec2.sub(a, goal)); + }; + + const dataContexts = Array.from(fileset.getDataContexts()); + + // we assume the datasets are ordered... hmmm TODO + const choice = dataContexts.reduce((bestSoFar, cur) => { + const planeSizeBest = planeSizeInVoxels(plane, axes, bestSoFar.array.shape); + const planeSizeCur = planeSizeInVoxels(plane, axes, cur.array.shape); + if (!planeSizeBest || !planeSizeCur) { + return bestSoFar; + } + return dstToDesired(vxlPitch(planeSizeBest), pxPitch) > dstToDesired(vxlPitch(planeSizeCur), pxPitch) + ? cur + : bestSoFar; + }, dataContexts[0]); + return choice ?? dataContexts[dataContexts.length - 1]; +} + +/** + * + * @param shape the dimensional extents of the data space + * @param axes the axes describing this omezarr dataset + * @param parameter a value from [0:1] indicating a parameter of the volume, along the given dimension @param dim, + * @param dim the dimension (axis) along which @param parameter refers + * @returns a valid index (between [0,layer.shape[axis] ]) from the volume, suitable for + */ +export function indexOfRelativeSlice( + shape: readonly number[], + axes: readonly OmeZarrAxis[], + parameter: number, + dim: ZarrDimension, +): number { + const dimIndex = indexFor(dim, axes); + return Math.floor(shape[dimIndex] * Math.max(0, Math.min(1, parameter))); +} + +/** + * @param zarr + * @param plane + * @param relativeView + * @param displayResolution + * @returns + */ +export function nextSliceStep( + fileset: OmeZarrFileset, + plane: CartesianPlane, + relativeView: box2D, // a box in data-unit-space + displayResolution: vec2, // in the plane given above +) { + if (!fileset.ready) { + const message = 'cannot pick best-fitting scale: OME-Zarr metadata not yet loaded'; + logger.error(message); + throw new VisZarrDataError(message); + } + + // figure out what layer we'd be viewing + const layer = pickBestScale(fileset, plane, relativeView, displayResolution); + const axes = layer.multiscale.axes; + const slices = sizeInVoxels(plane.ortho, axes, layer.array.shape); + return slices === undefined ? undefined : 1 / slices; +} + +/** + * determine the size of a slice of the volume, in the units specified by the axes metadata + * as described in the ome-zarr spec (https://ngff.openmicroscopy.org/latest/#axes-md) + * NOTE that only scale transformations (https://ngff.openmicroscopy.org/latest/#trafo-md) are supported at present - other types will be ignored. + * @param plane the plane to measure (eg. CartesianPlane('xy')) + * @param axes the axes metadata from the omezarr file in question + * @param transforms the set of coordinate transforms to use for scaling (https://ngff.openmicroscopy.org/latest/#multiscale-md) + * @param shape the dimensional extents of this coordinate space + * @returns the size, with respect to the coordinateTransformations present on the given dataset, of the requested plane. + * @example imagine a layer that is 29998 voxels wide in the X dimension, and a scale transformation of 0.00035 for that dimension. + * this function would return (29998*0.00035 = 10.4993) for the size of that dimension, which you would interpret to be in whatever unit + * is given by the axes metadata for that dimension (eg. millimeters) + */ +export function sizeInUnits( + plane: CartesianPlane, + axes: readonly OmeZarrAxis[], + transforms: OmeZarrCoordinateTransform[], + shape: readonly number[] +): vec2 | undefined { + const vxls = planeSizeInVoxels(plane, axes, shape); + + if (vxls === undefined) return undefined; + + let size: vec2 = vxls; + + // now, just apply the correct transforms, if they exist... + for (const trn of transforms) { + if (trn.type === 'scale') { + // try to apply it! + const uIndex = indexFor(plane.u, axes); + const vIndex = indexFor(plane.v, axes); + size = Vec2.mul(size, [trn.scale[uIndex], trn.scale[vIndex]]); + } + } + return size; +} + +/** + * get the size in voxels of a layer of an omezarr on a given dimension + * @param dim the dimension to measure + * @param axes the axes metadata for the zarr dataset + * @param shape the dimensional extents of the target dataset + * @returns the size, in voxels, of the given dimension of the given layer + * @example (pseudocode of course) return omezarr.multiscales[0].datasets[LAYER].shape[DIMENSION] + */ +export function sizeInVoxels(dim: ZarrDimension, axes: readonly OmeZarrAxis[], shape: readonly number[]) { + const uI = indexFor(dim, axes); + if (uI === -1) return undefined; + + return shape[uI]; +} + +// TODO move into ZarrMetadata object +/** + * get the size of a plane of a volume (given a specific layer) in voxels + * see @function sizeInVoxels + * @param plane the plane to measure (eg. 'xy') + * @param axes the axes metadata of an omezarr object + * @param shape the dimensional extents of the target dataset + * @returns a vec2 containing the requested sizes, or undefined if the requested plane is malformed, or not present in the dataset + */ +export function planeSizeInVoxels( + plane: CartesianPlane, + axes: readonly OmeZarrAxis[], + shape: readonly number[], +): vec2 | undefined { + // first - u&v must not refer to the same dimension, + // and both should exist in the axes... + if (!plane.isValid()) { + return undefined; + } + const uI = indexFor(plane.u, axes); + const vI = indexFor(plane.v, axes); + if (uI === -1 || vI === -1) { + return undefined; + } + + return [shape[uI], shape[vI]] as const; +} + +/** + * given a image with @param size pixels, break it into tiles, each @param idealTilePx. + * for all such tiles which intersect the given bounds, call the visitor + * @param idealTilePx the size of a tile, in pixels + * @param size the size of the image at this level of detail + * @param bounds visit only the tiles that are within the given bounds (in pixels) + */ +function visitTilesWithin(idealTilePx: vec2, size: vec2, bounds: box2D, visit: (tile: box2D) => void) { + const withinBoth = Box2D.intersection(bounds, Box2D.create([0, 0], size)); + if (!withinBoth) { + return; + } + // convert the image into tile indexes: + const boundsInTiles = Box2D.map(withinBoth, (corner) => Vec2.div(corner, idealTilePx)); + for (let x = Math.floor(boundsInTiles.minCorner[0]); x < Math.ceil(boundsInTiles.maxCorner[0]); x += 1) { + for (let y = Math.floor(boundsInTiles.minCorner[1]); y < Math.ceil(boundsInTiles.maxCorner[1]); y += 1) { + // all tiles visited are always within both the bounds, and the image itself + const lo = Vec2.mul([x, y], idealTilePx); + const hi = Vec2.min(size, Vec2.add(lo, idealTilePx)); + visit(Box2D.create(lo, hi)); + } + } +} + +function getVisibleTilesInLayer( + camera: { + view: box2D; + screenSize: vec2; + }, + plane: CartesianPlane, + orthoVal: number, + axes: OmeZarrAxis[], + tileSize: number, + level: OmeZarrLevel, +) { + const size = planeSizeInVoxels(plane, axes, level.array.shape); + const realSize = sizeInUnits(plane, axes, level.dataset.coordinateTransformations, level.array.shape); + if (!size || !realSize) return []; + const scale = Vec2.div(realSize, size); + const vxlToReal = (vxl: box2D) => Box2D.scale(vxl, scale); + const realToVxl = (real: box2D) => Box2D.scale(real, Vec2.div([1, 1], scale)); + const visibleTiles: VoxelTile[] = []; + visitTilesWithin([tileSize, tileSize], size, realToVxl(camera.view), (uv) => { + visibleTiles.push({ + plane: plane.axes, + realBounds: vxlToReal(uv), + bounds: uv, + orthoVal, + dataContext: level, + }); + }); + return visibleTiles; +} + +/** + * Gets the list of tiles of the given OME-Zarr image which are visible (i.e. they intersect with @param camera.view). + * @param camera an object describing the current view: the region of the omezarr, and the resolution at which it + * will be displayed. + * @param plane the plane (eg. CartesianPlane('xy')) from which to draw tiles + * @param orthoVal the value of the dimension orthogonal to the reference plane, e.g. the Z value relative to an XY plane. This gives + * which XY slice of voxels to display within the overall XYZ space of the 3D image. + * Note that not all OME-Zarr LOD layers can be expected to have the same number of slices! An index which exists at a high LOD may not + * exist at a low LOD. + * @param metadata the OME-Zarr image to pull tiles from + * @param tileSize the size of the tiles, in pixels. It is recommended to use a size that agrees with the chunking used in the dataset; however, + * other utilities in this library will stitch together chunks to satisfy the requested tile size. + * @returns an array of objects representing tiles (bounding information, etc.) which are visible within the given dataset + */ +export function getVisibleTiles( + camera: { + view: box2D; + screenSize: vec2; + }, + plane: CartesianPlane, + planeLocation: number, + fileset: OmeZarrFileset, + tileSize: number, +): VoxelTile[] { + // TODO (someday) open the array, look at its chunks, use that size for the size of the tiles I request! + + const level = pickBestScale(fileset, plane, camera.view, camera.screenSize); + // figure out the index of the slice + + const sliceIndex = indexOfRelativeSlice(level.array.shape, level.multiscale.axes, planeLocation, plane.ortho); + return getVisibleTilesInLayer(camera, plane, sliceIndex, level.multiscale.axes, tileSize, level); +} diff --git a/packages/omezarr/src/planar-view/planar-renderer.ts b/packages/omezarr/src/planar-view/planar-renderer.ts new file mode 100644 index 00000000..b825ffad --- /dev/null +++ b/packages/omezarr/src/planar-view/planar-renderer.ts @@ -0,0 +1,170 @@ +import { + buildAsyncRenderer, + type CachedTexture, + logger, + type QueueOptions, + type ReglCacheEntry, + type Renderer, +} from '@alleninstitute/vis-core'; +import { Box2D, type Interval, intervalToVec2, type OrthogonalCartesianAxes } from '@alleninstitute/vis-geometry'; +import type REGL from 'regl'; +import { buildTileRenderCommand } from '../rendering/tile-rendering'; +import type { OmeZarrDataContext, OmeZarrFileset, ZarrDataRequest, ZarrSlice } from '../zarr/omezarr-fileset'; +import { getVisibleTiles } from './calculations'; +import type { PlanarRenderSettings } from './settings'; +import type { VoxelTile, VoxelTileImage } from './types'; + +type ImageChannels = { + [channelKey: string]: CachedTexture; +}; + +function toZarrSlice( + plane: OrthogonalCartesianAxes, + u: Interval, + v: Interval, + channel: number, + orthoVal: number, +): ZarrSlice { + switch (plane) { + case 'xy': + return { + x: u, + y: v, + t: 0, + c: channel, + z: orthoVal, + }; + case 'xz': + return { + x: u, + z: v, + t: 0, + c: channel, + y: orthoVal, + }; + case 'yz': + return { + y: u, + z: v, + t: 0, + c: channel, + x: orthoVal, + }; + } +} + +function toZarrDataRequest(tile: VoxelTile, channel: number): ZarrDataRequest { + const { plane, orthoVal, bounds } = tile; + const { minCorner: min, maxCorner: max } = bounds; + const u = { min: min[0], max: max[0] }; + const v = { min: min[1], max: max[1] }; + return { + dataset: tile.dataContext.dataset.path, + multiscale: tile.dataContext.multiscale.name, + slice: toZarrSlice(plane, u, v, channel, orthoVal), + }; +} + +function isPrepared(cacheData: Record): cacheData is ImageChannels { + if (!cacheData) { + return false; + } + const keys = Object.keys(cacheData); + if (keys.length < 1) { + return false; + } + return keys.every((key) => cacheData[key]?.type === 'texture'); +} + +type OmeZarrVoxelTileImageDecoder = ( + fileset: OmeZarrFileset, + req: ZarrDataRequest, + dataContext: OmeZarrDataContext, + signal?: AbortSignal, +) => Promise; + +export type OmeZarrSliceRendererOptions = { + numChannels?: number; + queueOptions?: QueueOptions; +}; + +const DEFAULT_NUM_CHANNELS = 3; + +export function buildOmeZarrPlanarRenderer( + regl: REGL.Regl, + decoder: OmeZarrVoxelTileImageDecoder, + options?: OmeZarrSliceRendererOptions | undefined, +): Renderer { + const numChannels = options?.numChannels ?? DEFAULT_NUM_CHANNELS; + function sliceAsTexture(slice: VoxelTileImage): CachedTexture { + const { data, shape } = slice; + return { + bytes: data.byteLength, + texture: regl.texture({ + data: data, + width: shape[1], + height: shape[0], + format: 'luminance', + }), + type: 'texture', + }; + } + const cmd = buildTileRenderCommand(regl, numChannels); + return { + cacheKey: (item, requestKey, dataset, settings) => { + const channelKeys = Object.keys(settings.channels); + if (!channelKeys.includes(requestKey)) { + const message = `cannot retrieve cache key: unrecognized requestKey [${requestKey}]`; + logger.error(message); + throw new Error(message); + } + return `${dataset.url}_${JSON.stringify(item)}_ch=${requestKey}`; + }, + destroy: () => {}, + getVisibleItems: (dataset, settings) => { + const { camera, plane, planeLocation, tileSize } = settings; + return getVisibleTiles(camera, plane, planeLocation, dataset, tileSize); + }, + fetchItemContent: (item, dataset, settings): Record Promise> => { + const contents: Record Promise> = {}; + for (const key in settings.channels) { + contents[key] = (signal) => + decoder( + dataset, + toZarrDataRequest(item, settings.channels[key].index), + item.dataContext, + signal, + ).then(sliceAsTexture); + } + return contents; + }, + isPrepared, + renderItem: (target, item, _fileset, settings, gpuData) => { + const channels = Object.keys(gpuData).map((key) => ({ + tex: gpuData[key].texture, + gamut: intervalToVec2(settings.channels[key].gamut), + rgb: settings.channels[key].rgb, + })); + const numLayers = item.dataContext.multiscale.datasets.length; + // per the spec, the highest resolution layer should be first + // we want that layer most in front, so: + const depth = item.dataContext.datasetIndex / numLayers; + const { camera } = settings; + cmd({ + channels, + target, + depth, + tile: Box2D.toFlatArray(item.realBounds), + view: Box2D.toFlatArray(camera.view), + }); + }, + }; +} + +export function buildAsyncOmeZarrPlanarRenderer( + regl: REGL.Regl, + decoder: OmeZarrVoxelTileImageDecoder, + options?: OmeZarrSliceRendererOptions, +) { + return buildAsyncRenderer(buildOmeZarrPlanarRenderer(regl, decoder, options), options?.queueOptions); +} diff --git a/packages/omezarr/src/planar-view/settings.ts b/packages/omezarr/src/planar-view/settings.ts new file mode 100644 index 00000000..9640d65c --- /dev/null +++ b/packages/omezarr/src/planar-view/settings.ts @@ -0,0 +1,22 @@ +import type { box2D, CartesianPlane, Interval, vec2, vec3 } from '@alleninstitute/vis-geometry'; + +export type PlanarRenderSettingsChannel = { + index: number; + gamut: Interval; + rgb: vec3; +}; + +export type PlanarRenderSettingsChannels = { + [key: string]: PlanarRenderSettingsChannel; +}; + +export type PlanarRenderSettings = { + camera: { + view: box2D; + screenSize: vec2; + }; + planeLocation: number; + tileSize: number; + plane: CartesianPlane; + channels: PlanarRenderSettingsChannels; +}; diff --git a/packages/omezarr/src/planar-view/types.ts b/packages/omezarr/src/planar-view/types.ts new file mode 100644 index 00000000..ab3d6359 --- /dev/null +++ b/packages/omezarr/src/planar-view/types.ts @@ -0,0 +1,17 @@ +import type { box2D, OrthogonalCartesianAxes } from "@alleninstitute/vis-geometry"; +import type { OmeZarrDataContext } from "../zarr/omezarr-fileset"; + +// represent a 2D slice of a volume +export type VoxelTile = { + plane: OrthogonalCartesianAxes; // the plane in which the tile sits + realBounds: box2D; // in the space given by the axis descriptions of the omezarr dataset + bounds: box2D; // in voxels, in the plane + orthoVal: number; // the value along the orthogonal axis to the plane (e.g. the slice index along Z relative to an XY plane) + dataContext: OmeZarrDataContext; // the index in the resolution pyramid of the omezarr dataset +}; + +// a slice of a volume (as voxels suitable for display) +export type VoxelTileImage = { + data: Float32Array; + shape: number[]; +}; diff --git a/packages/omezarr/src/zarr/omezarr-fileset.ts b/packages/omezarr/src/zarr/omezarr-fileset.ts index 73252c8d..cefe6e0f 100644 --- a/packages/omezarr/src/zarr/omezarr-fileset.ts +++ b/packages/omezarr/src/zarr/omezarr-fileset.ts @@ -1,29 +1,97 @@ -import { logger, type WebResource } from '@alleninstitute/vis-core'; +import { logger, type WebResource, WorkerPool } from '@alleninstitute/vis-core'; +import { type Interval, limit } from '@alleninstitute/vis-geometry'; import * as zarr from 'zarrita'; import { z } from 'zod'; -import { type OmeZarrArray, OmeZarrArrayTransform, type OmeZarrGroup, OmeZarrGroupTransform } from './types'; -import { CachingMultithreadedFetchStore } from "./cached-loading/store"; +import { VisZarrDataError } from '../errors'; +import { CachingMultithreadedFetchStore } from './cached-loading/store'; +import { OmeZarrArrayTransform, OmeZarrGroupTransform } from './omezarr-transforms'; +import type { + OmeZarrArray, + OmeZarrAxis, + OmeZarrDataset, + OmeZarrGroup, + OmeZarrGroupAttributes, + OmeZarrMultiscale, + ZarrDimension, +} from './types'; -// export type OmeZarrAttrsBundle = { -// root: OmeZarrAttrs, -// arrays: ReadonlyMap -// }; +const WORKER_MODULE_URL = new URL('./cached-loading/fetch-slice.worker.ts', import.meta.url); +const NUM_WORKERS = 8; + +export type ZarrDimensionSelection = number | Interval | null; + +export type ZarrSelection = (number | zarr.Slice | null)[]; + +export type ZarrSlice = Record; + +export type ZarrDataRequest = { + multiscale?: string | undefined; + dataset: string; + slice: ZarrSlice; +}; + +export const buildSliceQuery = ( + r: Readonly, + axes: readonly OmeZarrAxis[], + shape: readonly number[], +): ZarrSelection => { + const ordered = axes.map((a) => r[a.name as ZarrDimension]); + + if (ordered.some((a) => a === undefined)) { + throw new VisZarrDataError('requested slice does not match specified dimensions of OME-Zarr dataset'); + } + + return ordered.map((d, i) => { + const bounds = { min: 0, max: shape[i] }; + if (d === null) { + return d; + } + if (typeof d === 'number') { + return limit(bounds, d); + } + return zarr.slice(limit(bounds, d.min), limit(bounds, d.max)); + }); +}; + +export type OmeZarrDataContext = { + path: string; + multiscale: OmeZarrMultiscale; + dataset: OmeZarrDataset; + datasetIndex: number; + array: OmeZarrArray; +}; + +export type OmeZarrDatasetSpecifier = { + multiscale?: string; +} & ( + | { + index: number; + } + | { + path: string; + } +); export class OmeZarrFileset { #store: CachingMultithreadedFetchStore; #root: zarr.Location; #rootGroup: OmeZarrGroup | null; #arrays: Map; + #zarritaGroups: Map>; + #zarritaArrays: Map>; constructor(res: WebResource) { - this.#store = new CachingMultithreadedFetchStore(res.url); + this.#store = new CachingMultithreadedFetchStore(res.url, new WorkerPool(NUM_WORKERS, WORKER_MODULE_URL)); this.#root = zarr.root(this.#store); this.#rootGroup = null; this.#arrays = new Map(); + this.#zarritaGroups = new Map(); + this.#zarritaArrays = new Map(); } - async #loadGroup(location: zarr.FetchStore | zarr.Location): Promise { + async #loadGroup(location: zarr.Location): Promise { const group = await zarr.open(location, { kind: 'group' }); + this.#zarritaGroups.set(location.path, group); try { return OmeZarrGroupTransform.parse(group.attrs); } catch (e) { @@ -34,8 +102,9 @@ export class OmeZarrFileset { } } - async #loadArray(location: zarr.FetchStore | zarr.Location): Promise { + async #loadArray(location: zarr.Location): Promise { const array = await zarr.open(location, { kind: 'array' }); + this.#zarritaArrays.set(location.path, array); try { return OmeZarrArrayTransform.parse(array); } catch (e) { @@ -59,16 +128,181 @@ export class OmeZarrFileset { const arrayResults = await Promise.all( this.#rootGroup.attributes.multiscales - .map((multiscale) => + .map((multiscale) => multiscale.datasets?.map(async (dataset) => { - return (await this.#loadArray(this.#root.resolve(dataset.path))) - }) + return await this.#loadArray(this.#root.resolve(dataset.path)); + }), ) .reduce((prev, curr) => prev.concat(curr)) .filter((arr) => arr !== undefined), ); + arrayResults.forEach((arr) => { this.#arrays.set(arr.path, arr); }); } -} \ No newline at end of file + + get ready(): boolean { + return this.#rootGroup !== null; + } + + get url(): string | URL { + return this.#store.url; + } + + get attrs(): OmeZarrGroupAttributes | undefined { + return this.#rootGroup?.attributes; + } + + getAxes(multiscaleName: string | undefined): OmeZarrAxis[] | undefined { + if (this.#rootGroup === null || this.#rootGroup.attributes.multiscales.length < 1) { + const message = + 'cannot request multiscale axes: OME-Zarr fileset has no multiscale data (it may not have been loaded yet)'; + logger.error(message); + throw new VisZarrDataError(message); + } + const multiscales = this.#rootGroup.attributes.multiscales; + const multiscale = + multiscaleName === undefined ? multiscales[0] : multiscales.find((v) => v.name === multiscaleName); + return multiscale?.axes; + } + + getDataContexts(): Iterable { + const multiscales = this.#rootGroup?.attributes.multiscales ?? []; + const arrays = this.#arrays; + + return { + *[Symbol.iterator]() { + for (const multiscale of multiscales) { + for (const dataset of multiscale.datasets) { + const path = dataset.path; + const array = arrays.get(path); + if (array === undefined) { + return; + } + yield { path, multiscale, dataset, array } as OmeZarrDataContext; + } + } + }, + }; + } + + getDataContext(specifier: OmeZarrDatasetSpecifier): OmeZarrDataContext | undefined { + if (this.#rootGroup === undefined) { + return; + } + const multiscales = this.#rootGroup?.attributes.multiscales ?? []; + const selectedMultiscales = multiscales.filter((m) => + specifier.multiscale ? m.name === specifier.multiscale : true, + ); + + let matching: { path: string, multiscale: OmeZarrMultiscale, dataset: OmeZarrDataset, datasetIndex: number }[]; + + if ('index' in specifier) { + const i = specifier.index; + if (selectedMultiscales.length > 1) { + const message = `cannot get matching dataset and array for index [${i}]: multiple multiscales specified`; + logger.error(message); + throw new VisZarrDataError(message); + } + matching = selectedMultiscales.map((m) => { + if (i < 0 || i >= m.datasets.length) { + const message = `cannot get matching dataset and array for index [${i}]: index out of bounds`; + logger.error(message); + throw new VisZarrDataError(message); + } + const dataset = m.datasets[specifier.index]; + if (dataset === undefined) { + const message = `cannot get matching dataset and array for index [${i}]: dataset undefined`; + logger.error(message); + throw new VisZarrDataError(message); + } + return { path: dataset.path, datasetIndex: i, multiscale: m, dataset }; + }); + + if (matching.length > 1) { + const message = `cannot get matching dataset and array for index [${i}]: multiple matching datasets found`; + logger.error(message); + throw new VisZarrDataError(message); + } + + } else { + const path = specifier.path; + matching = selectedMultiscales.map((m) => { + const datasets = m.datasets.filter((d) => d.path === path); + if (datasets.length > 1) { + const message = `cannot get matching dataset and array for path [${path}]: multiple matching datasets found`; + logger.error(message); + throw new VisZarrDataError(message); + } + const dataset = datasets[0]; + if (dataset === undefined) { + const message = `cannot get matching dataset and array for path [${path}]: dataset was undefined`; + logger.error(message); + throw new VisZarrDataError(message); + } + const datasetIndex = m.datasets.findIndex((d) => d.path === dataset.path); + if (datasetIndex === -1) { + const message = `cannot get matching dataset and array for path [${path}]: index of matching dataset was not found`; + logger.error(message); + throw new VisZarrDataError(message); + } + return { path, multiscale: m, dataset, datasetIndex }; + }); + + if (matching.length > 1) { + const message = `cannot get matching dataset and array for path [${path}]: multiple matching datasets found`; + logger.error(message); + throw new VisZarrDataError(message); + } + } + + if (matching.length < 1) { + return; + } + + const { path, multiscale, dataset, datasetIndex } = matching[0]; + const array = this.#arrays.get(path); + if (multiscale === undefined || dataset === undefined || array === undefined) { + const message = `cannot get matching dataset and array for path [${path}]: one or more elements were undefined`; + logger.error(message); + throw new VisZarrDataError(message); + } + return { path, multiscale, dataset, datasetIndex, array }; + } + + /** + * Loads and returns any voxel data from this OME-Zarr that matches the requested segment of the overall fileset, + * as defined by a multiscale, a dataset, and a chunk slice. + * @see https://zarrita.dev/slicing.html for more details on how slicing is handled. + * @param r The data request, specifying the coordinates within the OME-Zarr's data from which to source voxel data + * @param signal An optional abort signal with which to cancel this request if necessary + * @returns .... + */ + async loadSlice(r: ZarrDataRequest, signal?: AbortSignal | undefined) { + const axes = this.getAxes(r.multiscale); + if (axes === undefined) { + const message = 'invalid Zarr data: no axes found for specified multiscale'; + logger.error(message); + throw new VisZarrDataError(message); + } + const arr = this.#zarritaArrays.get(r.dataset); + if (arr === undefined) { + const message = 'invalid Zarr data: no array found for specified dataset'; + logger.error(message); + throw new VisZarrDataError(message); + } + const shape = arr.shape; + const query = buildSliceQuery(r.slice, axes, shape); + const result = await zarr.get(arr, query, { opts: { signal: signal ?? null } }); + if (typeof result === 'number') { + const message = "could not fetch Zarr slice: parsed slice data's shape was undefined"; + logger.error(message); + throw new VisZarrDataError(message); + } + return { + shape: result.shape, + buffer: result, + }; + } +} diff --git a/packages/omezarr/src/zarr/omezarr-transforms.ts b/packages/omezarr/src/zarr/omezarr-transforms.ts new file mode 100644 index 00000000..d28b1e49 --- /dev/null +++ b/packages/omezarr/src/zarr/omezarr-transforms.ts @@ -0,0 +1,40 @@ +import type * as zarr from 'zarrita'; +import z from 'zod'; +import { + type OmeZarrArray, + type OmeZarrAttrsV2, + OmeZarrAttrsV2Schema, + type OmeZarrAttrsV3, + OmeZarrAttrsV3Schema, + type OmeZarrGroup, +} from './types'; + +type ZarritaArray = zarr.Array; + +export const OmeZarrArrayTransform = z.transform((v: ZarritaArray) => { + return { + nodeType: 'array', + path: v.path, + chunkShape: v.chunks, + dataType: v.dtype, + shape: v.shape, + attributes: v.attrs, + } as OmeZarrArray; +}); + +export const OmeZarrGroupTransform = z + .union([OmeZarrAttrsV2Schema, OmeZarrAttrsV3Schema]) + .transform((v: OmeZarrAttrsV2 | OmeZarrAttrsV3) => { + if ('ome' in v) { + return { + nodeType: 'group', + zarrFormat: 3, + attributes: v.ome, + }; + } + return { + nodeType: 'group', + zarrFormat: 2, + attributes: v, + }; + }); diff --git a/packages/omezarr/tsconfig.json b/packages/omezarr/tsconfig.json index d8a6412f..5946286b 100644 --- a/packages/omezarr/tsconfig.json +++ b/packages/omezarr/tsconfig.json @@ -5,7 +5,7 @@ "~/*": ["./*"] }, "moduleResolution": "Bundler", - "module": "es6", + "module": "esnext", "target": "es2024", "lib": ["es2024", "DOM"] }, From 9d93e9061284167d7b3f14065f9baa54f04002b7 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 9 Oct 2025 09:44:36 -0700 Subject: [PATCH 47/91] Fmt and typecheck fixes --- packages/omezarr/src/planar-view/calculations.ts | 16 ++++++++-------- packages/omezarr/src/planar-view/types.ts | 4 ++-- .../omezarr/src/zarr/cached-loading/store.ts | 4 ++-- packages/omezarr/src/zarr/omezarr-fileset.ts | 3 +-- packages/omezarr/src/zarr/types.ts | 8 ++++---- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/omezarr/src/planar-view/calculations.ts b/packages/omezarr/src/planar-view/calculations.ts index 3a1ee838..8404a461 100644 --- a/packages/omezarr/src/planar-view/calculations.ts +++ b/packages/omezarr/src/planar-view/calculations.ts @@ -1,9 +1,9 @@ -import { logger } from "@alleninstitute/vis-core"; -import { Box2D, type box2D, type CartesianPlane, Vec2, type vec2 } from "@alleninstitute/vis-geometry"; -import { VisZarrDataError } from "../errors"; -import type { OmeZarrFileset, OmeZarrDataContext as OmeZarrLevel } from "../zarr/omezarr-fileset"; -import type { OmeZarrAxis, OmeZarrCoordinateTransform, ZarrDimension } from "../zarr/types"; -import type { VoxelTile } from "./types"; +import { logger } from '@alleninstitute/vis-core'; +import { Box2D, type box2D, type CartesianPlane, Vec2, type vec2 } from '@alleninstitute/vis-geometry'; +import { VisZarrDataError } from '../errors'; +import type { OmeZarrFileset, OmeZarrDataContext as OmeZarrLevel } from '../zarr/omezarr-fileset'; +import type { OmeZarrAxis, OmeZarrCoordinateTransform, ZarrDimension } from '../zarr/types'; +import type { VoxelTile } from './types'; function indexFor(dim: ZarrDimension, axes: readonly OmeZarrAxis[]) { return axes.findIndex((axis) => axis.name === dim); @@ -70,7 +70,7 @@ export const pickBestScale = ( : bestSoFar; }, dataContexts[0]); return choice ?? dataContexts[dataContexts.length - 1]; -} +}; /** * @@ -133,7 +133,7 @@ export function sizeInUnits( plane: CartesianPlane, axes: readonly OmeZarrAxis[], transforms: OmeZarrCoordinateTransform[], - shape: readonly number[] + shape: readonly number[], ): vec2 | undefined { const vxls = planeSizeInVoxels(plane, axes, shape); diff --git a/packages/omezarr/src/planar-view/types.ts b/packages/omezarr/src/planar-view/types.ts index ab3d6359..e201532e 100644 --- a/packages/omezarr/src/planar-view/types.ts +++ b/packages/omezarr/src/planar-view/types.ts @@ -1,5 +1,5 @@ -import type { box2D, OrthogonalCartesianAxes } from "@alleninstitute/vis-geometry"; -import type { OmeZarrDataContext } from "../zarr/omezarr-fileset"; +import type { box2D, OrthogonalCartesianAxes } from '@alleninstitute/vis-geometry'; +import type { OmeZarrDataContext } from '../zarr/omezarr-fileset'; // represent a 2D slice of a volume export type VoxelTile = { diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 50d120d2..81e473ea 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -46,8 +46,8 @@ class CacheableByteArray implements Cacheable { type CacheKey = string; type TransferableRequestInit = Omit & { - body?: string; - headers?: Record; + body?: string | undefined; + headers?: Record | undefined; }; const copyToTransferableHeaders = (headers: RequestInit['headers']): Record | undefined => { diff --git a/packages/omezarr/src/zarr/omezarr-fileset.ts b/packages/omezarr/src/zarr/omezarr-fileset.ts index cefe6e0f..53b6fcd0 100644 --- a/packages/omezarr/src/zarr/omezarr-fileset.ts +++ b/packages/omezarr/src/zarr/omezarr-fileset.ts @@ -196,7 +196,7 @@ export class OmeZarrFileset { specifier.multiscale ? m.name === specifier.multiscale : true, ); - let matching: { path: string, multiscale: OmeZarrMultiscale, dataset: OmeZarrDataset, datasetIndex: number }[]; + let matching: { path: string; multiscale: OmeZarrMultiscale; dataset: OmeZarrDataset; datasetIndex: number }[]; if ('index' in specifier) { const i = specifier.index; @@ -225,7 +225,6 @@ export class OmeZarrFileset { logger.error(message); throw new VisZarrDataError(message); } - } else { const path = specifier.path; matching = selectedMultiscales.map((m) => { diff --git a/packages/omezarr/src/zarr/types.ts b/packages/omezarr/src/zarr/types.ts index bcca2315..bd941fd8 100644 --- a/packages/omezarr/src/zarr/types.ts +++ b/packages/omezarr/src/zarr/types.ts @@ -157,13 +157,13 @@ export type OmeZarrAttrs = { // is actually represented export type OmeZarrNode = { nodeType: 'group' | 'array'; -} +}; export type OmeZarrGroup = OmeZarrNode & { nodeType: 'group'; zarrFormat: 2 | 3; attributes: OmeZarrGroupAttributes; -} +}; export type OmeZarrGroupAttributes = { multiscales: OmeZarrMultiscale[]; @@ -206,7 +206,8 @@ export const OmeZarrAttrsSchema = z }; }); -export const OmeZarrGroupTransform = z.union([OmeZarrAttrsV2Schema, OmeZarrAttrsV3Schema]) +export const OmeZarrGroupTransform = z + .union([OmeZarrAttrsV2Schema, OmeZarrAttrsV3Schema]) .transform((v: OmeZarrAttrsV2 | OmeZarrAttrsV3) => { if ('ome' in v) { return { @@ -222,7 +223,6 @@ export const OmeZarrGroupTransform = z.union([OmeZarrAttrsV2Schema, OmeZarrAttrs }; }); - type ZarritaArray = zarr.Array; export const OmeZarrArrayTransform = z.transform((v: ZarritaArray) => { From 253298be1f01187a8c1d09d3716bbe541e810518 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 9 Oct 2025 10:47:08 -0700 Subject: [PATCH 48/91] Fixed up some typing and import issues --- packages/omezarr/src/index.ts | 8 ++++++++ packages/omezarr/src/planar-view/calculations.ts | 6 +++--- packages/omezarr/src/planar-view/planar-renderer.ts | 10 +++++----- packages/omezarr/src/planar-view/types.ts | 6 +++--- packages/omezarr/src/zarr/omezarr-fileset.ts | 2 +- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/omezarr/src/index.ts b/packages/omezarr/src/index.ts index 46f1aad2..2c2c7294 100644 --- a/packages/omezarr/src/index.ts +++ b/packages/omezarr/src/index.ts @@ -69,3 +69,11 @@ export { OmeZarrArrayTransform, OmeZarrGroupTransform, } from './zarr/omezarr-transforms'; +export type { + OmeZarrVoxelTile, + OmeZarrVoxelTileImage, +} from './planar-view/types'; +export { + buildOmeZarrPlanarRenderer, + buildAsyncOmeZarrPlanarRenderer, +} from './planar-view/planar-renderer'; diff --git a/packages/omezarr/src/planar-view/calculations.ts b/packages/omezarr/src/planar-view/calculations.ts index 8404a461..0338a85c 100644 --- a/packages/omezarr/src/planar-view/calculations.ts +++ b/packages/omezarr/src/planar-view/calculations.ts @@ -3,7 +3,7 @@ import { Box2D, type box2D, type CartesianPlane, Vec2, type vec2 } from '@alleni import { VisZarrDataError } from '../errors'; import type { OmeZarrFileset, OmeZarrDataContext as OmeZarrLevel } from '../zarr/omezarr-fileset'; import type { OmeZarrAxis, OmeZarrCoordinateTransform, ZarrDimension } from '../zarr/types'; -import type { VoxelTile } from './types'; +import type { OmeZarrVoxelTile } from './types'; function indexFor(dim: ZarrDimension, axes: readonly OmeZarrAxis[]) { return axes.findIndex((axis) => axis.name === dim); @@ -237,7 +237,7 @@ function getVisibleTilesInLayer( const scale = Vec2.div(realSize, size); const vxlToReal = (vxl: box2D) => Box2D.scale(vxl, scale); const realToVxl = (real: box2D) => Box2D.scale(real, Vec2.div([1, 1], scale)); - const visibleTiles: VoxelTile[] = []; + const visibleTiles: OmeZarrVoxelTile[] = []; visitTilesWithin([tileSize, tileSize], size, realToVxl(camera.view), (uv) => { visibleTiles.push({ plane: plane.axes, @@ -273,7 +273,7 @@ export function getVisibleTiles( planeLocation: number, fileset: OmeZarrFileset, tileSize: number, -): VoxelTile[] { +): OmeZarrVoxelTile[] { // TODO (someday) open the array, look at its chunks, use that size for the size of the tiles I request! const level = pickBestScale(fileset, plane, camera.view, camera.screenSize); diff --git a/packages/omezarr/src/planar-view/planar-renderer.ts b/packages/omezarr/src/planar-view/planar-renderer.ts index b825ffad..bb58706b 100644 --- a/packages/omezarr/src/planar-view/planar-renderer.ts +++ b/packages/omezarr/src/planar-view/planar-renderer.ts @@ -12,7 +12,7 @@ import { buildTileRenderCommand } from '../rendering/tile-rendering'; import type { OmeZarrDataContext, OmeZarrFileset, ZarrDataRequest, ZarrSlice } from '../zarr/omezarr-fileset'; import { getVisibleTiles } from './calculations'; import type { PlanarRenderSettings } from './settings'; -import type { VoxelTile, VoxelTileImage } from './types'; +import type { OmeZarrVoxelTile, OmeZarrVoxelTileImage } from './types'; type ImageChannels = { [channelKey: string]: CachedTexture; @@ -53,7 +53,7 @@ function toZarrSlice( } } -function toZarrDataRequest(tile: VoxelTile, channel: number): ZarrDataRequest { +function toZarrDataRequest(tile: OmeZarrVoxelTile, channel: number): ZarrDataRequest { const { plane, orthoVal, bounds } = tile; const { minCorner: min, maxCorner: max } = bounds; const u = { min: min[0], max: max[0] }; @@ -81,7 +81,7 @@ type OmeZarrVoxelTileImageDecoder = ( req: ZarrDataRequest, dataContext: OmeZarrDataContext, signal?: AbortSignal, -) => Promise; +) => Promise; export type OmeZarrSliceRendererOptions = { numChannels?: number; @@ -94,9 +94,9 @@ export function buildOmeZarrPlanarRenderer( regl: REGL.Regl, decoder: OmeZarrVoxelTileImageDecoder, options?: OmeZarrSliceRendererOptions | undefined, -): Renderer { +): Renderer { const numChannels = options?.numChannels ?? DEFAULT_NUM_CHANNELS; - function sliceAsTexture(slice: VoxelTileImage): CachedTexture { + function sliceAsTexture(slice: OmeZarrVoxelTileImage): CachedTexture { const { data, shape } = slice; return { bytes: data.byteLength, diff --git a/packages/omezarr/src/planar-view/types.ts b/packages/omezarr/src/planar-view/types.ts index e201532e..d4a56043 100644 --- a/packages/omezarr/src/planar-view/types.ts +++ b/packages/omezarr/src/planar-view/types.ts @@ -2,7 +2,7 @@ import type { box2D, OrthogonalCartesianAxes } from '@alleninstitute/vis-geometr import type { OmeZarrDataContext } from '../zarr/omezarr-fileset'; // represent a 2D slice of a volume -export type VoxelTile = { +export type OmeZarrVoxelTile = { plane: OrthogonalCartesianAxes; // the plane in which the tile sits realBounds: box2D; // in the space given by the axis descriptions of the omezarr dataset bounds: box2D; // in voxels, in the plane @@ -11,7 +11,7 @@ export type VoxelTile = { }; // a slice of a volume (as voxels suitable for display) -export type VoxelTileImage = { +export type OmeZarrVoxelTileImage = { data: Float32Array; - shape: number[]; + shape: readonly number[]; }; diff --git a/packages/omezarr/src/zarr/omezarr-fileset.ts b/packages/omezarr/src/zarr/omezarr-fileset.ts index 53b6fcd0..891b7613 100644 --- a/packages/omezarr/src/zarr/omezarr-fileset.ts +++ b/packages/omezarr/src/zarr/omezarr-fileset.ts @@ -62,7 +62,7 @@ export type OmeZarrDataContext = { }; export type OmeZarrDatasetSpecifier = { - multiscale?: string; + multiscale?: string | undefined; } & ( | { index: number; From f6bfb0c9073677a1eb8b87d0438d10d8b2650739 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 9 Oct 2025 12:21:14 -0700 Subject: [PATCH 49/91] Reorganized the planar renderer folder and type names --- packages/omezarr/src/index.ts | 9 +++-- .../omezarr/src/planar-view/calculations.ts | 6 ++-- .../{planar-renderer.ts => renderer.ts} | 23 +++++-------- packages/omezarr/src/planar-view/settings.ts | 22 ------------- packages/omezarr/src/planar-view/types.ts | 33 +++++++++++++++++-- 5 files changed, 47 insertions(+), 46 deletions(-) rename packages/omezarr/src/planar-view/{planar-renderer.ts => renderer.ts} (88%) delete mode 100644 packages/omezarr/src/planar-view/settings.ts diff --git a/packages/omezarr/src/index.ts b/packages/omezarr/src/index.ts index 2c2c7294..a93afdf2 100644 --- a/packages/omezarr/src/index.ts +++ b/packages/omezarr/src/index.ts @@ -70,10 +70,13 @@ export { OmeZarrGroupTransform, } from './zarr/omezarr-transforms'; export type { - OmeZarrVoxelTile, - OmeZarrVoxelTileImage, + PlanarVoxelTile, + PlanarVoxelTileImage, + PlanarRenderSettings, + PlanarRenderSettingsChannel, + PlanarRenderSettingsChannels, } from './planar-view/types'; export { buildOmeZarrPlanarRenderer, buildAsyncOmeZarrPlanarRenderer, -} from './planar-view/planar-renderer'; +} from './planar-view/renderer'; diff --git a/packages/omezarr/src/planar-view/calculations.ts b/packages/omezarr/src/planar-view/calculations.ts index 0338a85c..15a4f9dc 100644 --- a/packages/omezarr/src/planar-view/calculations.ts +++ b/packages/omezarr/src/planar-view/calculations.ts @@ -3,7 +3,7 @@ import { Box2D, type box2D, type CartesianPlane, Vec2, type vec2 } from '@alleni import { VisZarrDataError } from '../errors'; import type { OmeZarrFileset, OmeZarrDataContext as OmeZarrLevel } from '../zarr/omezarr-fileset'; import type { OmeZarrAxis, OmeZarrCoordinateTransform, ZarrDimension } from '../zarr/types'; -import type { OmeZarrVoxelTile } from './types'; +import type { PlanarVoxelTile } from './types'; function indexFor(dim: ZarrDimension, axes: readonly OmeZarrAxis[]) { return axes.findIndex((axis) => axis.name === dim); @@ -237,7 +237,7 @@ function getVisibleTilesInLayer( const scale = Vec2.div(realSize, size); const vxlToReal = (vxl: box2D) => Box2D.scale(vxl, scale); const realToVxl = (real: box2D) => Box2D.scale(real, Vec2.div([1, 1], scale)); - const visibleTiles: OmeZarrVoxelTile[] = []; + const visibleTiles: PlanarVoxelTile[] = []; visitTilesWithin([tileSize, tileSize], size, realToVxl(camera.view), (uv) => { visibleTiles.push({ plane: plane.axes, @@ -273,7 +273,7 @@ export function getVisibleTiles( planeLocation: number, fileset: OmeZarrFileset, tileSize: number, -): OmeZarrVoxelTile[] { +): PlanarVoxelTile[] { // TODO (someday) open the array, look at its chunks, use that size for the size of the tiles I request! const level = pickBestScale(fileset, plane, camera.view, camera.screenSize); diff --git a/packages/omezarr/src/planar-view/planar-renderer.ts b/packages/omezarr/src/planar-view/renderer.ts similarity index 88% rename from packages/omezarr/src/planar-view/planar-renderer.ts rename to packages/omezarr/src/planar-view/renderer.ts index bb58706b..534bda39 100644 --- a/packages/omezarr/src/planar-view/planar-renderer.ts +++ b/packages/omezarr/src/planar-view/renderer.ts @@ -2,7 +2,6 @@ import { buildAsyncRenderer, type CachedTexture, logger, - type QueueOptions, type ReglCacheEntry, type Renderer, } from '@alleninstitute/vis-core'; @@ -11,8 +10,7 @@ import type REGL from 'regl'; import { buildTileRenderCommand } from '../rendering/tile-rendering'; import type { OmeZarrDataContext, OmeZarrFileset, ZarrDataRequest, ZarrSlice } from '../zarr/omezarr-fileset'; import { getVisibleTiles } from './calculations'; -import type { PlanarRenderSettings } from './settings'; -import type { OmeZarrVoxelTile, OmeZarrVoxelTileImage } from './types'; +import type { PlanarRenderSettings, PlanarRendererOptions, PlanarVoxelTile, PlanarVoxelTileImage } from './types'; type ImageChannels = { [channelKey: string]: CachedTexture; @@ -53,7 +51,7 @@ function toZarrSlice( } } -function toZarrDataRequest(tile: OmeZarrVoxelTile, channel: number): ZarrDataRequest { +function toZarrDataRequest(tile: PlanarVoxelTile, channel: number): ZarrDataRequest { const { plane, orthoVal, bounds } = tile; const { minCorner: min, maxCorner: max } = bounds; const u = { min: min[0], max: max[0] }; @@ -76,27 +74,22 @@ function isPrepared(cacheData: Record): cach return keys.every((key) => cacheData[key]?.type === 'texture'); } -type OmeZarrVoxelTileImageDecoder = ( +export type OmeZarrVoxelTileImageDecoder = ( fileset: OmeZarrFileset, req: ZarrDataRequest, dataContext: OmeZarrDataContext, signal?: AbortSignal, -) => Promise; - -export type OmeZarrSliceRendererOptions = { - numChannels?: number; - queueOptions?: QueueOptions; -}; +) => Promise; const DEFAULT_NUM_CHANNELS = 3; export function buildOmeZarrPlanarRenderer( regl: REGL.Regl, decoder: OmeZarrVoxelTileImageDecoder, - options?: OmeZarrSliceRendererOptions | undefined, -): Renderer { + options?: PlanarRendererOptions | undefined, +): Renderer { const numChannels = options?.numChannels ?? DEFAULT_NUM_CHANNELS; - function sliceAsTexture(slice: OmeZarrVoxelTileImage): CachedTexture { + function sliceAsTexture(slice: PlanarVoxelTileImage): CachedTexture { const { data, shape } = slice; return { bytes: data.byteLength, @@ -164,7 +157,7 @@ export function buildOmeZarrPlanarRenderer( export function buildAsyncOmeZarrPlanarRenderer( regl: REGL.Regl, decoder: OmeZarrVoxelTileImageDecoder, - options?: OmeZarrSliceRendererOptions, + options?: PlanarRendererOptions, ) { return buildAsyncRenderer(buildOmeZarrPlanarRenderer(regl, decoder, options), options?.queueOptions); } diff --git a/packages/omezarr/src/planar-view/settings.ts b/packages/omezarr/src/planar-view/settings.ts deleted file mode 100644 index 9640d65c..00000000 --- a/packages/omezarr/src/planar-view/settings.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { box2D, CartesianPlane, Interval, vec2, vec3 } from '@alleninstitute/vis-geometry'; - -export type PlanarRenderSettingsChannel = { - index: number; - gamut: Interval; - rgb: vec3; -}; - -export type PlanarRenderSettingsChannels = { - [key: string]: PlanarRenderSettingsChannel; -}; - -export type PlanarRenderSettings = { - camera: { - view: box2D; - screenSize: vec2; - }; - planeLocation: number; - tileSize: number; - plane: CartesianPlane; - channels: PlanarRenderSettingsChannels; -}; diff --git a/packages/omezarr/src/planar-view/types.ts b/packages/omezarr/src/planar-view/types.ts index d4a56043..b6e55f62 100644 --- a/packages/omezarr/src/planar-view/types.ts +++ b/packages/omezarr/src/planar-view/types.ts @@ -1,8 +1,9 @@ -import type { box2D, OrthogonalCartesianAxes } from '@alleninstitute/vis-geometry'; +import type { QueueOptions } from '@alleninstitute/vis-core'; +import type { box2D, CartesianPlane, Interval, OrthogonalCartesianAxes, vec2, vec3 } from '@alleninstitute/vis-geometry'; import type { OmeZarrDataContext } from '../zarr/omezarr-fileset'; // represent a 2D slice of a volume -export type OmeZarrVoxelTile = { +export type PlanarVoxelTile = { plane: OrthogonalCartesianAxes; // the plane in which the tile sits realBounds: box2D; // in the space given by the axis descriptions of the omezarr dataset bounds: box2D; // in voxels, in the plane @@ -11,7 +12,33 @@ export type OmeZarrVoxelTile = { }; // a slice of a volume (as voxels suitable for display) -export type OmeZarrVoxelTileImage = { +export type PlanarVoxelTileImage = { data: Float32Array; shape: readonly number[]; }; + +export type PlanarRendererOptions = { + numChannels?: number; + queueOptions?: QueueOptions; +}; + +export type PlanarRenderSettingsChannel = { + index: number; + gamut: Interval; + rgb: vec3; +}; + +export type PlanarRenderSettingsChannels = { + [key: string]: PlanarRenderSettingsChannel; +}; + +export type PlanarRenderSettings = { + camera: { + view: box2D; + screenSize: vec2; + }; + planeLocation: number; + tileSize: number; + plane: CartesianPlane; + channels: PlanarRenderSettingsChannels; +}; From ec367b0c4f87ab07b21b75552efa3304f62b1ce2 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 9 Oct 2025 12:22:08 -0700 Subject: [PATCH 50/91] Fmt fixes --- packages/omezarr/src/planar-view/types.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/omezarr/src/planar-view/types.ts b/packages/omezarr/src/planar-view/types.ts index b6e55f62..d5d02f63 100644 --- a/packages/omezarr/src/planar-view/types.ts +++ b/packages/omezarr/src/planar-view/types.ts @@ -1,5 +1,12 @@ import type { QueueOptions } from '@alleninstitute/vis-core'; -import type { box2D, CartesianPlane, Interval, OrthogonalCartesianAxes, vec2, vec3 } from '@alleninstitute/vis-geometry'; +import type { + box2D, + CartesianPlane, + Interval, + OrthogonalCartesianAxes, + vec2, + vec3, +} from '@alleninstitute/vis-geometry'; import type { OmeZarrDataContext } from '../zarr/omezarr-fileset'; // represent a 2D slice of a volume From baf5e878087569086f12d65cc984421957242b98 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 9 Oct 2025 15:15:07 -0700 Subject: [PATCH 51/91] Some refactoring, adding calculations into data classes --- packages/omezarr/src/index.ts | 4 +- .../omezarr/src/planar-view/calculations.ts | 284 ------------------ packages/omezarr/src/planar-view/renderer.ts | 16 +- packages/omezarr/src/planar-view/types.ts | 4 +- .../omezarr/src/planar-view/visibility.ts | 91 ++++++ packages/omezarr/src/zarr/omezarr-fileset.ts | 264 +++++++++++++--- packages/omezarr/src/zarr/omezarr-level.ts | 135 +++++++++ 7 files changed, 470 insertions(+), 328 deletions(-) delete mode 100644 packages/omezarr/src/planar-view/calculations.ts create mode 100644 packages/omezarr/src/planar-view/visibility.ts create mode 100644 packages/omezarr/src/zarr/omezarr-level.ts diff --git a/packages/omezarr/src/index.ts b/packages/omezarr/src/index.ts index a93afdf2..031a6224 100644 --- a/packages/omezarr/src/index.ts +++ b/packages/omezarr/src/index.ts @@ -57,14 +57,14 @@ export { export { type CancelRequest, type ZarrSliceRequest, makeOmeZarrSliceLoaderWorker } from './sliceview/worker-loader'; export { - type OmeZarrDataContext, - type OmeZarrDatasetSpecifier, + type OmeZarrLevelSpecifier as OmeZarrDatasetSpecifier, OmeZarrFileset, type ZarrDataRequest, type ZarrDimensionSelection, type ZarrSelection, type ZarrSlice, } from './zarr/omezarr-fileset'; +export { OmeZarrLevel } from './zarr/omezarr-level'; export { OmeZarrArrayTransform, OmeZarrGroupTransform, diff --git a/packages/omezarr/src/planar-view/calculations.ts b/packages/omezarr/src/planar-view/calculations.ts deleted file mode 100644 index 15a4f9dc..00000000 --- a/packages/omezarr/src/planar-view/calculations.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { logger } from '@alleninstitute/vis-core'; -import { Box2D, type box2D, type CartesianPlane, Vec2, type vec2 } from '@alleninstitute/vis-geometry'; -import { VisZarrDataError } from '../errors'; -import type { OmeZarrFileset, OmeZarrDataContext as OmeZarrLevel } from '../zarr/omezarr-fileset'; -import type { OmeZarrAxis, OmeZarrCoordinateTransform, ZarrDimension } from '../zarr/types'; -import type { PlanarVoxelTile } from './types'; - -function indexFor(dim: ZarrDimension, axes: readonly OmeZarrAxis[]) { - return axes.findIndex((axis) => axis.name === dim); -} - -export const pickBestScale = ( - fileset: OmeZarrFileset, - plane: CartesianPlane, - relativeView: box2D, // a box in data-unit-space - displayResolution: vec2, // in the plane given above - multiscaleName?: string | undefined, -): OmeZarrLevel => { - if (!fileset.ready) { - const message = 'cannot pick best-fitting scale: OME-Zarr metadata not yet loaded'; - logger.error(message); - throw new VisZarrDataError(message); - } - - const dataContext = fileset.getDataContext({ index: 0, multiscale: multiscaleName }); - if (!dataContext) { - const message = 'cannot pick best-fitting scale: no initial dataset context found'; - logger.error(message); - throw new VisZarrDataError(message); - } - - const { multiscale, dataset, array } = dataContext; - - const axes = multiscale.axes; - const transforms = dataset.coordinateTransformations; - const shape = array.shape; - - const realSize = sizeInUnits(plane, axes, transforms, shape); - if (!realSize) { - const message = 'invalid Zarr data: could not determine the size of the plane in the given units'; - logger.error(message); - throw new VisZarrDataError(message); - } - - const vxlPitch = (size: vec2) => Vec2.div(realSize, size); - - // size, in dataspace, of a pixel 1/res - const pxPitch = Vec2.div(Box2D.size(relativeView), displayResolution); - const dstToDesired = (a: vec2, goal: vec2) => { - const diff = Vec2.sub(a, goal); - if (diff[0] * diff[1] > 0) { - // the res (a) is higher than our goal - - // weight this heavily to prefer smaller than the goal - return 1000 * Vec2.length(Vec2.sub(a, goal)); - } - return Vec2.length(Vec2.sub(a, goal)); - }; - - const dataContexts = Array.from(fileset.getDataContexts()); - - // we assume the datasets are ordered... hmmm TODO - const choice = dataContexts.reduce((bestSoFar, cur) => { - const planeSizeBest = planeSizeInVoxels(plane, axes, bestSoFar.array.shape); - const planeSizeCur = planeSizeInVoxels(plane, axes, cur.array.shape); - if (!planeSizeBest || !planeSizeCur) { - return bestSoFar; - } - return dstToDesired(vxlPitch(planeSizeBest), pxPitch) > dstToDesired(vxlPitch(planeSizeCur), pxPitch) - ? cur - : bestSoFar; - }, dataContexts[0]); - return choice ?? dataContexts[dataContexts.length - 1]; -}; - -/** - * - * @param shape the dimensional extents of the data space - * @param axes the axes describing this omezarr dataset - * @param parameter a value from [0:1] indicating a parameter of the volume, along the given dimension @param dim, - * @param dim the dimension (axis) along which @param parameter refers - * @returns a valid index (between [0,layer.shape[axis] ]) from the volume, suitable for - */ -export function indexOfRelativeSlice( - shape: readonly number[], - axes: readonly OmeZarrAxis[], - parameter: number, - dim: ZarrDimension, -): number { - const dimIndex = indexFor(dim, axes); - return Math.floor(shape[dimIndex] * Math.max(0, Math.min(1, parameter))); -} - -/** - * @param zarr - * @param plane - * @param relativeView - * @param displayResolution - * @returns - */ -export function nextSliceStep( - fileset: OmeZarrFileset, - plane: CartesianPlane, - relativeView: box2D, // a box in data-unit-space - displayResolution: vec2, // in the plane given above -) { - if (!fileset.ready) { - const message = 'cannot pick best-fitting scale: OME-Zarr metadata not yet loaded'; - logger.error(message); - throw new VisZarrDataError(message); - } - - // figure out what layer we'd be viewing - const layer = pickBestScale(fileset, plane, relativeView, displayResolution); - const axes = layer.multiscale.axes; - const slices = sizeInVoxels(plane.ortho, axes, layer.array.shape); - return slices === undefined ? undefined : 1 / slices; -} - -/** - * determine the size of a slice of the volume, in the units specified by the axes metadata - * as described in the ome-zarr spec (https://ngff.openmicroscopy.org/latest/#axes-md) - * NOTE that only scale transformations (https://ngff.openmicroscopy.org/latest/#trafo-md) are supported at present - other types will be ignored. - * @param plane the plane to measure (eg. CartesianPlane('xy')) - * @param axes the axes metadata from the omezarr file in question - * @param transforms the set of coordinate transforms to use for scaling (https://ngff.openmicroscopy.org/latest/#multiscale-md) - * @param shape the dimensional extents of this coordinate space - * @returns the size, with respect to the coordinateTransformations present on the given dataset, of the requested plane. - * @example imagine a layer that is 29998 voxels wide in the X dimension, and a scale transformation of 0.00035 for that dimension. - * this function would return (29998*0.00035 = 10.4993) for the size of that dimension, which you would interpret to be in whatever unit - * is given by the axes metadata for that dimension (eg. millimeters) - */ -export function sizeInUnits( - plane: CartesianPlane, - axes: readonly OmeZarrAxis[], - transforms: OmeZarrCoordinateTransform[], - shape: readonly number[], -): vec2 | undefined { - const vxls = planeSizeInVoxels(plane, axes, shape); - - if (vxls === undefined) return undefined; - - let size: vec2 = vxls; - - // now, just apply the correct transforms, if they exist... - for (const trn of transforms) { - if (trn.type === 'scale') { - // try to apply it! - const uIndex = indexFor(plane.u, axes); - const vIndex = indexFor(plane.v, axes); - size = Vec2.mul(size, [trn.scale[uIndex], trn.scale[vIndex]]); - } - } - return size; -} - -/** - * get the size in voxels of a layer of an omezarr on a given dimension - * @param dim the dimension to measure - * @param axes the axes metadata for the zarr dataset - * @param shape the dimensional extents of the target dataset - * @returns the size, in voxels, of the given dimension of the given layer - * @example (pseudocode of course) return omezarr.multiscales[0].datasets[LAYER].shape[DIMENSION] - */ -export function sizeInVoxels(dim: ZarrDimension, axes: readonly OmeZarrAxis[], shape: readonly number[]) { - const uI = indexFor(dim, axes); - if (uI === -1) return undefined; - - return shape[uI]; -} - -// TODO move into ZarrMetadata object -/** - * get the size of a plane of a volume (given a specific layer) in voxels - * see @function sizeInVoxels - * @param plane the plane to measure (eg. 'xy') - * @param axes the axes metadata of an omezarr object - * @param shape the dimensional extents of the target dataset - * @returns a vec2 containing the requested sizes, or undefined if the requested plane is malformed, or not present in the dataset - */ -export function planeSizeInVoxels( - plane: CartesianPlane, - axes: readonly OmeZarrAxis[], - shape: readonly number[], -): vec2 | undefined { - // first - u&v must not refer to the same dimension, - // and both should exist in the axes... - if (!plane.isValid()) { - return undefined; - } - const uI = indexFor(plane.u, axes); - const vI = indexFor(plane.v, axes); - if (uI === -1 || vI === -1) { - return undefined; - } - - return [shape[uI], shape[vI]] as const; -} - -/** - * given a image with @param size pixels, break it into tiles, each @param idealTilePx. - * for all such tiles which intersect the given bounds, call the visitor - * @param idealTilePx the size of a tile, in pixels - * @param size the size of the image at this level of detail - * @param bounds visit only the tiles that are within the given bounds (in pixels) - */ -function visitTilesWithin(idealTilePx: vec2, size: vec2, bounds: box2D, visit: (tile: box2D) => void) { - const withinBoth = Box2D.intersection(bounds, Box2D.create([0, 0], size)); - if (!withinBoth) { - return; - } - // convert the image into tile indexes: - const boundsInTiles = Box2D.map(withinBoth, (corner) => Vec2.div(corner, idealTilePx)); - for (let x = Math.floor(boundsInTiles.minCorner[0]); x < Math.ceil(boundsInTiles.maxCorner[0]); x += 1) { - for (let y = Math.floor(boundsInTiles.minCorner[1]); y < Math.ceil(boundsInTiles.maxCorner[1]); y += 1) { - // all tiles visited are always within both the bounds, and the image itself - const lo = Vec2.mul([x, y], idealTilePx); - const hi = Vec2.min(size, Vec2.add(lo, idealTilePx)); - visit(Box2D.create(lo, hi)); - } - } -} - -function getVisibleTilesInLayer( - camera: { - view: box2D; - screenSize: vec2; - }, - plane: CartesianPlane, - orthoVal: number, - axes: OmeZarrAxis[], - tileSize: number, - level: OmeZarrLevel, -) { - const size = planeSizeInVoxels(plane, axes, level.array.shape); - const realSize = sizeInUnits(plane, axes, level.dataset.coordinateTransformations, level.array.shape); - if (!size || !realSize) return []; - const scale = Vec2.div(realSize, size); - const vxlToReal = (vxl: box2D) => Box2D.scale(vxl, scale); - const realToVxl = (real: box2D) => Box2D.scale(real, Vec2.div([1, 1], scale)); - const visibleTiles: PlanarVoxelTile[] = []; - visitTilesWithin([tileSize, tileSize], size, realToVxl(camera.view), (uv) => { - visibleTiles.push({ - plane: plane.axes, - realBounds: vxlToReal(uv), - bounds: uv, - orthoVal, - dataContext: level, - }); - }); - return visibleTiles; -} - -/** - * Gets the list of tiles of the given OME-Zarr image which are visible (i.e. they intersect with @param camera.view). - * @param camera an object describing the current view: the region of the omezarr, and the resolution at which it - * will be displayed. - * @param plane the plane (eg. CartesianPlane('xy')) from which to draw tiles - * @param orthoVal the value of the dimension orthogonal to the reference plane, e.g. the Z value relative to an XY plane. This gives - * which XY slice of voxels to display within the overall XYZ space of the 3D image. - * Note that not all OME-Zarr LOD layers can be expected to have the same number of slices! An index which exists at a high LOD may not - * exist at a low LOD. - * @param metadata the OME-Zarr image to pull tiles from - * @param tileSize the size of the tiles, in pixels. It is recommended to use a size that agrees with the chunking used in the dataset; however, - * other utilities in this library will stitch together chunks to satisfy the requested tile size. - * @returns an array of objects representing tiles (bounding information, etc.) which are visible within the given dataset - */ -export function getVisibleTiles( - camera: { - view: box2D; - screenSize: vec2; - }, - plane: CartesianPlane, - planeLocation: number, - fileset: OmeZarrFileset, - tileSize: number, -): PlanarVoxelTile[] { - // TODO (someday) open the array, look at its chunks, use that size for the size of the tiles I request! - - const level = pickBestScale(fileset, plane, camera.view, camera.screenSize); - // figure out the index of the slice - - const sliceIndex = indexOfRelativeSlice(level.array.shape, level.multiscale.axes, planeLocation, plane.ortho); - return getVisibleTilesInLayer(camera, plane, sliceIndex, level.multiscale.axes, tileSize, level); -} diff --git a/packages/omezarr/src/planar-view/renderer.ts b/packages/omezarr/src/planar-view/renderer.ts index 534bda39..ea56fdb2 100644 --- a/packages/omezarr/src/planar-view/renderer.ts +++ b/packages/omezarr/src/planar-view/renderer.ts @@ -8,9 +8,10 @@ import { import { Box2D, type Interval, intervalToVec2, type OrthogonalCartesianAxes } from '@alleninstitute/vis-geometry'; import type REGL from 'regl'; import { buildTileRenderCommand } from '../rendering/tile-rendering'; -import type { OmeZarrDataContext, OmeZarrFileset, ZarrDataRequest, ZarrSlice } from '../zarr/omezarr-fileset'; -import { getVisibleTiles } from './calculations'; +import type { OmeZarrFileset, ZarrDataRequest, ZarrSlice } from '../zarr/omezarr-fileset'; import type { PlanarRenderSettings, PlanarRendererOptions, PlanarVoxelTile, PlanarVoxelTileImage } from './types'; +import { getVisibleOmeZarrTiles } from './visibility'; +import type { OmeZarrLevel } from '../zarr/omezarr-level'; type ImageChannels = { [channelKey: string]: CachedTexture; @@ -77,7 +78,7 @@ function isPrepared(cacheData: Record): cach export type OmeZarrVoxelTileImageDecoder = ( fileset: OmeZarrFileset, req: ZarrDataRequest, - dataContext: OmeZarrDataContext, + dataContext: OmeZarrLevel, signal?: AbortSignal, ) => Promise; @@ -102,7 +103,9 @@ export function buildOmeZarrPlanarRenderer( type: 'texture', }; } + const cmd = buildTileRenderCommand(regl, numChannels); + return { cacheKey: (item, requestKey, dataset, settings) => { const channelKeys = Object.keys(settings.channels); @@ -113,11 +116,14 @@ export function buildOmeZarrPlanarRenderer( } return `${dataset.url}_${JSON.stringify(item)}_ch=${requestKey}`; }, + destroy: () => {}, + getVisibleItems: (dataset, settings) => { const { camera, plane, planeLocation, tileSize } = settings; - return getVisibleTiles(camera, plane, planeLocation, dataset, tileSize); + return getVisibleOmeZarrTiles(dataset, camera, plane, planeLocation, tileSize); }, + fetchItemContent: (item, dataset, settings): Record Promise> => { const contents: Record Promise> = {}; for (const key in settings.channels) { @@ -131,7 +137,9 @@ export function buildOmeZarrPlanarRenderer( } return contents; }, + isPrepared, + renderItem: (target, item, _fileset, settings, gpuData) => { const channels = Object.keys(gpuData).map((key) => ({ tex: gpuData[key].texture, diff --git a/packages/omezarr/src/planar-view/types.ts b/packages/omezarr/src/planar-view/types.ts index d5d02f63..f3563398 100644 --- a/packages/omezarr/src/planar-view/types.ts +++ b/packages/omezarr/src/planar-view/types.ts @@ -7,7 +7,7 @@ import type { vec2, vec3, } from '@alleninstitute/vis-geometry'; -import type { OmeZarrDataContext } from '../zarr/omezarr-fileset'; +import type { OmeZarrLevel } from '../zarr/omezarr-level'; // represent a 2D slice of a volume export type PlanarVoxelTile = { @@ -15,7 +15,7 @@ export type PlanarVoxelTile = { realBounds: box2D; // in the space given by the axis descriptions of the omezarr dataset bounds: box2D; // in voxels, in the plane orthoVal: number; // the value along the orthogonal axis to the plane (e.g. the slice index along Z relative to an XY plane) - dataContext: OmeZarrDataContext; // the index in the resolution pyramid of the omezarr dataset + dataContext: OmeZarrLevel; // the index in the resolution pyramid of the omezarr dataset }; // a slice of a volume (as voxels suitable for display) diff --git a/packages/omezarr/src/planar-view/visibility.ts b/packages/omezarr/src/planar-view/visibility.ts new file mode 100644 index 00000000..2c8537da --- /dev/null +++ b/packages/omezarr/src/planar-view/visibility.ts @@ -0,0 +1,91 @@ +import { Box2D, type box2D, type CartesianPlane, Vec2, type vec2 } from '@alleninstitute/vis-geometry'; +import type { OmeZarrFileset } from '../zarr/omezarr-fileset'; +import type { OmeZarrLevel } from '../zarr/omezarr-level'; +import type { PlanarVoxelTile } from './types'; + + +/** + * given a image with @param size pixels, break it into tiles, each @param idealTilePx. + * for all such tiles which intersect the given bounds, call the visitor + * @param idealTilePx the size of a tile, in pixels + * @param size the size of the image at this level of detail + * @param bounds visit only the tiles that are within the given bounds (in pixels) + */ +function visitTilesWithin(idealTilePx: vec2, size: vec2, bounds: box2D, visit: (tile: box2D) => void) { + const withinBoth = Box2D.intersection(bounds, Box2D.create([0, 0], size)); + if (!withinBoth) { + return; + } + // convert the image into tile indexes: + const boundsInTiles = Box2D.map(withinBoth, (corner) => Vec2.div(corner, idealTilePx)); + for (let x = Math.floor(boundsInTiles.minCorner[0]); x < Math.ceil(boundsInTiles.maxCorner[0]); x += 1) { + for (let y = Math.floor(boundsInTiles.minCorner[1]); y < Math.ceil(boundsInTiles.maxCorner[1]); y += 1) { + // all tiles visited are always within both the bounds, and the image itself + const lo = Vec2.mul([x, y], idealTilePx); + const hi = Vec2.min(size, Vec2.add(lo, idealTilePx)); + visit(Box2D.create(lo, hi)); + } + } +} + +function getVisibleTilesInLevel( + camera: { + view: box2D; + screenSize: vec2; + }, + plane: CartesianPlane, + orthoVal: number, + tileSize: number, + level: OmeZarrLevel, +) { + const size = level.planeSizeInVoxels(plane); + const realSize = level.sizeInUnits(plane); + if (!size || !realSize) return []; + const scale = Vec2.div(realSize, size); + const vxlToReal = (vxl: box2D) => Box2D.scale(vxl, scale); + const realToVxl = (real: box2D) => Box2D.scale(real, Vec2.div([1, 1], scale)); + const visibleTiles: PlanarVoxelTile[] = []; + visitTilesWithin([tileSize, tileSize], size, realToVxl(camera.view), (uv) => { + visibleTiles.push({ + plane: plane.axes, + realBounds: vxlToReal(uv), + bounds: uv, + orthoVal, + dataContext: level, + }); + }); + return visibleTiles; +} + +/** + * Gets the list of tiles of the given OME-Zarr image which are visible (i.e. they intersect with @param camera.view). + * @param camera an object describing the current view: the region of the omezarr, and the resolution at which it + * will be displayed. + * @param plane the plane (eg. CartesianPlane('xy')) from which to draw tiles + * @param orthoVal the value of the dimension orthogonal to the reference plane, e.g. the Z value relative to an XY plane. This gives + * which XY slice of voxels to display within the overall XYZ space of the 3D image. + * Note that not all OME-Zarr LOD layers can be expected to have the same number of slices! An index which exists at a high LOD may not + * exist at a low LOD. + * @param metadata the OME-Zarr image to pull tiles from + * @param tileSize the size of the tiles, in pixels. It is recommended to use a size that agrees with the chunking used in the dataset; however, + * other utilities in this library will stitch together chunks to satisfy the requested tile size. + * @returns an array of objects representing tiles (bounding information, etc.) which are visible within the given dataset + */ +export function getVisibleOmeZarrTiles( + fileset: OmeZarrFileset, + camera: { + view: box2D; + screenSize: vec2; + }, + plane: CartesianPlane, + planeLocation: number, + tileSize: number, +): PlanarVoxelTile[] { + // TODO (someday) open the array, look at its chunks, use that size for the size of the tiles I request! + + const level = fileset.pickBestScale(plane, camera.view, camera.screenSize); + // figure out the index of the slice + + const sliceIndex = level.indexOfRelativeSlice(planeLocation, plane.ortho); + return getVisibleTilesInLevel(camera, plane, sliceIndex, tileSize, level); +} diff --git a/packages/omezarr/src/zarr/omezarr-fileset.ts b/packages/omezarr/src/zarr/omezarr-fileset.ts index 891b7613..a56652e9 100644 --- a/packages/omezarr/src/zarr/omezarr-fileset.ts +++ b/packages/omezarr/src/zarr/omezarr-fileset.ts @@ -1,19 +1,30 @@ import { logger, type WebResource, WorkerPool } from '@alleninstitute/vis-core'; -import { type Interval, limit } from '@alleninstitute/vis-geometry'; +import { + Box2D, + type box2D, + type CartesianPlane, + type Interval, + limit, + Vec2, + type vec2, +} from '@alleninstitute/vis-geometry'; import * as zarr from 'zarrita'; import { z } from 'zod'; import { VisZarrDataError } from '../errors'; import { CachingMultithreadedFetchStore } from './cached-loading/store'; import { OmeZarrArrayTransform, OmeZarrGroupTransform } from './omezarr-transforms'; -import type { - OmeZarrArray, - OmeZarrAxis, - OmeZarrDataset, - OmeZarrGroup, - OmeZarrGroupAttributes, - OmeZarrMultiscale, - ZarrDimension, +import { + convertFromOmeroToColorChannels, + type OmeZarrArray, + type OmeZarrAxis, + type OmeZarrColorChannel, + type OmeZarrDataset, + type OmeZarrGroup, + type OmeZarrGroupAttributes, + type OmeZarrMultiscale, + type ZarrDimension, } from './types'; +import { OmeZarrLevel } from './omezarr-level'; const WORKER_MODULE_URL = new URL('./cached-loading/fetch-slice.worker.ts', import.meta.url); const NUM_WORKERS = 8; @@ -24,6 +35,11 @@ export type ZarrSelection = (number | zarr.Slice | null)[]; export type ZarrSlice = Record; +export type OmeZarrFieldsetJsonOptions = { + readable?: boolean | undefined; + spaces?: number | undefined; +}; + export type ZarrDataRequest = { multiscale?: string | undefined; dataset: string; @@ -53,15 +69,15 @@ export const buildSliceQuery = ( }); }; -export type OmeZarrDataContext = { - path: string; - multiscale: OmeZarrMultiscale; - dataset: OmeZarrDataset; - datasetIndex: number; - array: OmeZarrArray; -}; +export type OmeZarrMultiscaleSpecifier = + | { + index: number; + } + | { + name: string; + }; -export type OmeZarrDatasetSpecifier = { +export type OmeZarrLevelSpecifier = { multiscale?: string | undefined; } & ( | { @@ -167,27 +183,24 @@ export class OmeZarrFileset { return multiscale?.axes; } - getDataContexts(): Iterable { - const multiscales = this.#rootGroup?.attributes.multiscales ?? []; - const arrays = this.#arrays; + getMultiscale(specifier: OmeZarrMultiscaleSpecifier): OmeZarrMultiscale | undefined { + if (!this.ready) { + const message = 'cannot get multiscale: OME-Zarr metadata not yet loaded'; + logger.error(message); + throw new VisZarrDataError(message); + } - return { - *[Symbol.iterator]() { - for (const multiscale of multiscales) { - for (const dataset of multiscale.datasets) { - const path = dataset.path; - const array = arrays.get(path); - if (array === undefined) { - return; - } - yield { path, multiscale, dataset, array } as OmeZarrDataContext; - } - } - }, - }; + const multiscales = this.#rootGroup?.attributes.multiscales; + if (multiscales === undefined) { + const message = 'cannot get multiscale: no multiscales found'; + logger.error(message); + throw new VisZarrDataError(message); + } + + return 'index' in specifier ? multiscales[specifier.index] : multiscales.find((m) => m.name === specifier.name); } - getDataContext(specifier: OmeZarrDatasetSpecifier): OmeZarrDataContext | undefined { + getLevel(specifier: OmeZarrLevelSpecifier): OmeZarrLevel | undefined { if (this.#rootGroup === undefined) { return; } @@ -267,7 +280,186 @@ export class OmeZarrFileset { logger.error(message); throw new VisZarrDataError(message); } - return { path, multiscale, dataset, datasetIndex, array }; + return new OmeZarrLevel(path, multiscale, dataset, datasetIndex, array); + } + + getLevels(): Iterable { + const multiscales = this.#rootGroup?.attributes.multiscales ?? []; + const arrays = this.#arrays; + + return { + *[Symbol.iterator]() { + for (const multiscale of multiscales) { + let i = 0; + for (const dataset of multiscale.datasets) { + const path = dataset.path; + const array = arrays.get(path); + if (array === undefined) { + return; + } + yield new OmeZarrLevel(path, multiscale, dataset, i, array); + i += 1; + } + } + }, + }; + } + + getColorChannels(): OmeZarrColorChannel[] { + const omero = this.#rootGroup?.attributes.omero; + return omero ? convertFromOmeroToColorChannels(omero) : []; + } + + toJSON() { + const rootGroup = this.#zarritaGroups.get(this.#root.path); + return rootGroup ? { url: this.#root.path, ready: true, rootGroup } : { url: this.#root.path, ready: false }; + } + + pickBestScale( + plane: CartesianPlane, + relativeView: box2D, // a box in data-unit-space + displayResolution: vec2, // in the plane given above + multiscaleName?: string | undefined, + ): OmeZarrLevel { + if (!this.ready) { + const message = 'cannot pick best-fitting scale: OME-Zarr metadata not yet loaded'; + logger.error(message); + throw new VisZarrDataError(message); + } + + const level = this.getLevel({ index: 0, multiscale: multiscaleName }); + if (!level) { + const message = 'cannot pick best-fitting scale: no initial dataset context found'; + logger.error(message); + throw new VisZarrDataError(message); + } + + const realSize = level.sizeInUnits(plane); + if (!realSize) { + const message = 'invalid Zarr data: could not determine the size of the plane in the given units'; + logger.error(message); + throw new VisZarrDataError(message); + } + + const vxlPitch = (size: vec2) => Vec2.div(realSize, size); + + // size, in dataspace, of a pixel 1/res + const pxPitch = Vec2.div(Box2D.size(relativeView), displayResolution); + const dstToDesired = (a: vec2, goal: vec2) => { + const diff = Vec2.sub(a, goal); + if (diff[0] * diff[1] > 0) { + // the res (a) is higher than our goal - + // weight this heavily to prefer smaller than the goal + return 1000 * Vec2.length(Vec2.sub(a, goal)); + } + return Vec2.length(Vec2.sub(a, goal)); + }; + + const dataContexts = Array.from(this.getLevels()); + + // per the OME-Zarr spec, datasets/levels are ordered by scale + const choice = dataContexts.reduce((bestSoFar, cur) => { + const planeSizeBest = bestSoFar.planeSizeInVoxels(plane); + const planeSizeCur = cur.planeSizeInVoxels(plane); + if (!planeSizeBest || !planeSizeCur) { + return bestSoFar; + } + return dstToDesired(vxlPitch(planeSizeBest), pxPitch) > dstToDesired(vxlPitch(planeSizeCur), pxPitch) + ? cur + : bestSoFar; + }, dataContexts[0]); + return choice ?? dataContexts[dataContexts.length - 1]; + } + + nextSliceStep( + plane: CartesianPlane, + relativeView: box2D, // a box in data-unit-space + displayResolution: vec2, // in the plane given above + ) { + if (!this.ready) { + const message = 'cannot pick best-fitting scale: OME-Zarr metadata not yet loaded'; + logger.error(message); + throw new VisZarrDataError(message); + } + + // figure out what layer we'd be viewing + const level = this.pickBestScale(plane, relativeView, displayResolution); + const slices = level.sizeInVoxels(plane.ortho); + return slices === undefined ? undefined : 1 / slices; + } + + #getDimensionIndex(dim: ZarrDimension, multiscaleSpec: OmeZarrMultiscaleSpecifier): number | undefined { + if (!this.ready) { + return undefined; + } + const multiscale = this.getMultiscale(multiscaleSpec); + if (multiscale === undefined) { + return undefined; + } + const index = multiscale.axes.findIndex((a) => a.name === dim); + return index > -1 ? index : undefined; + } + + #getMaximumForDimension(dim: ZarrDimension, multiscaleSpec: OmeZarrMultiscaleSpecifier): number { + const multiscale = this.getMultiscale(multiscaleSpec); + if (multiscale === undefined) { + const message = `cannot get maximum ${dim}: no matching multiscale found`; + logger.error(message); + throw new VisZarrDataError(message); + } + + const arrays = multiscale.datasets.map((d) => this.#arrays.get(d.path)); + const dimIdx = this.#getDimensionIndex(dim, multiscaleSpec); + if (dimIdx === undefined) { + const message = `cannot get maximum ${dim}: '${dim}' is not a valid dimension for this multiscale`; + logger.error(message); + throw new VisZarrDataError(message); + } + const sortedValues = arrays.map((arr) => arr?.shape[dimIdx] ?? 0).sort(); + return sortedValues.at(sortedValues.length - 1) ?? 0; + } + + /** + * Given a specific @param multiscaleIdent representation of the Zarr data, finds the + * largest X shape component among the shapes of the different dataset arrays. + * @param multiscaleIdent the index or path of a specific multiscale representation (defaults to 0) + * @returns the largest Z scale for the specified multiscale representation + */ + maxX(multiscaleSpec: OmeZarrMultiscaleSpecifier): number { + return this.#getMaximumForDimension('x', multiscaleSpec); + } + + /** + * Given a specific @param multiscale representation of the Zarr data, finds the + * largest Y shape component among the shapes of the different dataset arrays. + * @param multiscale the index or path of a specific multiscale representation (defaults to 0) + * @returns the largest Z scale for the specified multiscale representation + */ + maxY(multiscaleSpec: OmeZarrMultiscaleSpecifier): number { + return this.#getMaximumForDimension('y', multiscaleSpec); + } + + /** + * Given a specific @param multiscale representation of the Zarr data, finds the + * largest Z shape component among the shapes of the different dataset arrays. + * @param multiscale the index or path of a specific multiscale representation (defaults to 0) + * @returns the largest Z scale for the specified multiscale representation + */ + maxZ(multiscaleSpec: OmeZarrMultiscaleSpecifier): number { + return this.#getMaximumForDimension('z', multiscaleSpec); + } + + maxOrthogonal(plane: CartesianPlane, multiscaleSpec: OmeZarrMultiscaleSpecifier): number { + if (plane.ortho === 'x') { + return this.maxX(multiscaleSpec); + } + if (plane.ortho === 'y') { + return this.maxY(multiscaleSpec); + } + if (plane.ortho === 'z') { + return this.maxZ(multiscaleSpec); + } + throw new VisZarrDataError(`invalid plane: ortho set to '${plane.ortho}'`); } /** diff --git a/packages/omezarr/src/zarr/omezarr-level.ts b/packages/omezarr/src/zarr/omezarr-level.ts new file mode 100644 index 00000000..e036a45b --- /dev/null +++ b/packages/omezarr/src/zarr/omezarr-level.ts @@ -0,0 +1,135 @@ +import { + type CartesianPlane, + Vec2, + type vec2, +} from '@alleninstitute/vis-geometry'; +import type { + OmeZarrArray, + OmeZarrAxis, + OmeZarrDataset, + OmeZarrMultiscale, + ZarrDimension, +} from './types'; + +export class OmeZarrLevel { + readonly path: string; + readonly multiscale: OmeZarrMultiscale; + readonly dataset: OmeZarrDataset; + readonly datasetIndex: number; + readonly array: OmeZarrArray; + + constructor( + path: string, + multiscale: OmeZarrMultiscale, + dataset: OmeZarrDataset, + datasetIndex: number, + array: OmeZarrArray, + ) { + this.path = path; + this.multiscale = multiscale; + this.dataset = dataset; + this.datasetIndex = datasetIndex; + this.array = array; + } + + get shape(): readonly number[] { + return this.array.shape; + } + + get axes(): readonly OmeZarrAxis[] { + return this.multiscale.axes; + } + + indexFor(dim: ZarrDimension) { + const axes = this.multiscale.axes; + return axes.findIndex((axis) => axis.name === dim); + } + + /** + * Determine the size of a slice of the volume, in the units specified by the axes metadata + * as described in the ome-zarr spec (https://ngff.openmicroscopy.org/latest/#axes-md). + * NOTE that only scale transformations (https://ngff.openmicroscopy.org/latest/#trafo-md) + * are supported at present - other types will be ignored. + * @param plane the plane to measure (eg. CartesianPlane('xy')) + * @returns the size, with respect to the coordinateTransformations present on the given dataset, of the requested plane. + * @example imagine a layer that is 29998 voxels wide in the X dimension, and a scale transformation of 0.00035 for that dimension. + * this function would return (29998*0.00035 = 10.4993) for the size of that dimension, which you would interpret to be in whatever unit + * is given by the axes metadata for that dimension (eg. millimeters) + */ + sizeInUnits(plane: CartesianPlane): vec2 | undefined { + const vxls = this.planeSizeInVoxels(plane); + if (vxls === undefined) { + return undefined; + } + + let size: vec2 = vxls; + const transforms = this.dataset.coordinateTransformations; + + // now, just apply the correct transforms, if they exist... + for (const trn of transforms) { + if (trn.type === 'scale') { + // try to apply it! + const uIndex = this.indexFor(plane.u); + const vIndex = this.indexFor(plane.v); + size = Vec2.mul(size, [trn.scale[uIndex], trn.scale[vIndex]]); + } + } + return size; + } + + /** + * get the size in voxels of a layer of an omezarr on a given dimension + * @param dim the dimension to measure + * @param axes the axes metadata for the zarr dataset + * @param shape the dimensional extents of the target dataset + * @returns the size, in voxels, of the given dimension of the given layer + * @example (pseudocode of course) return omezarr.multiscales[0].datasets[LAYER].shape[DIMENSION] + */ + sizeInVoxels(dim: ZarrDimension) { + const uI = this.indexFor(dim); + if (uI === -1) return undefined; + + return this.array.shape[uI]; + } + + /** + * Get the size of a plane of within this level's volume, in voxels + * see @function sizeInVoxels + * @param plane the plane to measure (eg. 'xy') + * @returns a vec2 containing the requested sizes, or undefined if the requested plane is malformed, or not present in the dataset + */ + planeSizeInVoxels(plane: CartesianPlane): vec2 | undefined { + // first - u&v must not refer to the same dimension, + // and both should exist in the axes... + if (!plane.isValid()) { + return undefined; + } + const uI = this.indexFor(plane.u); + const vI = this.indexFor(plane.v); + if (uI === -1 || vI === -1) { + return undefined; + } + + return [this.shape[uI], this.shape[vI]] as const; + } + + /** + * + * @param parameter a value from [0:1] indicating a parameter of the volume, along the given dimension @param dim, + * @param dim the dimension (axis) along which @param parameter refers + * @returns a valid index (between [0, level.shape[axis]]) from the volume, suitable for + */ + indexOfRelativeSlice(parameter: number, dim: ZarrDimension): number { + const dimIndex = this.indexFor(dim); + return Math.floor(this.shape[dimIndex] * Math.max(0, Math.min(1, parameter))); + } + + toJSON() { + const path = this.path; + const multiscale = this.multiscale; + const dataset = this.dataset; + const datasetIndex = this.datasetIndex; + const array = this.array; + return { path, multiscale, dataset, datasetIndex, array }; + } +} From b50f683f6ac3f60f17d22803c43178198378ee28 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 9 Oct 2025 15:15:43 -0700 Subject: [PATCH 52/91] Fmt fixes --- packages/omezarr/src/planar-view/visibility.ts | 1 - packages/omezarr/src/zarr/omezarr-level.ts | 14 ++------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/omezarr/src/planar-view/visibility.ts b/packages/omezarr/src/planar-view/visibility.ts index 2c8537da..145f4021 100644 --- a/packages/omezarr/src/planar-view/visibility.ts +++ b/packages/omezarr/src/planar-view/visibility.ts @@ -3,7 +3,6 @@ import type { OmeZarrFileset } from '../zarr/omezarr-fileset'; import type { OmeZarrLevel } from '../zarr/omezarr-level'; import type { PlanarVoxelTile } from './types'; - /** * given a image with @param size pixels, break it into tiles, each @param idealTilePx. * for all such tiles which intersect the given bounds, call the visitor diff --git a/packages/omezarr/src/zarr/omezarr-level.ts b/packages/omezarr/src/zarr/omezarr-level.ts index e036a45b..6ca40c7f 100644 --- a/packages/omezarr/src/zarr/omezarr-level.ts +++ b/packages/omezarr/src/zarr/omezarr-level.ts @@ -1,15 +1,5 @@ -import { - type CartesianPlane, - Vec2, - type vec2, -} from '@alleninstitute/vis-geometry'; -import type { - OmeZarrArray, - OmeZarrAxis, - OmeZarrDataset, - OmeZarrMultiscale, - ZarrDimension, -} from './types'; +import { type CartesianPlane, Vec2, type vec2 } from '@alleninstitute/vis-geometry'; +import type { OmeZarrArray, OmeZarrAxis, OmeZarrDataset, OmeZarrMultiscale, ZarrDimension } from './types'; export class OmeZarrLevel { readonly path: string; From 4c621ba0774749b2bfdf6e08fb3a4b897d34b4c2 Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Thu, 9 Oct 2025 15:37:18 -0700 Subject: [PATCH 53/91] Cleaned up fileset.getLevel --- packages/omezarr/src/zarr/omezarr-fileset.ts | 86 +++++++------------- 1 file changed, 28 insertions(+), 58 deletions(-) diff --git a/packages/omezarr/src/zarr/omezarr-fileset.ts b/packages/omezarr/src/zarr/omezarr-fileset.ts index a56652e9..9a397c2b 100644 --- a/packages/omezarr/src/zarr/omezarr-fileset.ts +++ b/packages/omezarr/src/zarr/omezarr-fileset.ts @@ -78,7 +78,7 @@ export type OmeZarrMultiscaleSpecifier = }; export type OmeZarrLevelSpecifier = { - multiscale?: string | undefined; + multiscale?: OmeZarrMultiscaleSpecifier | undefined; } & ( | { index: number; @@ -204,79 +204,49 @@ export class OmeZarrFileset { if (this.#rootGroup === undefined) { return; } - const multiscales = this.#rootGroup?.attributes.multiscales ?? []; - const selectedMultiscales = multiscales.filter((m) => - specifier.multiscale ? m.name === specifier.multiscale : true, - ); + const targetDesc = 'index' in specifier ? `index [${specifier.index}]` : `path [${specifier.path}]`; + + const multiscale = this.getMultiscale(specifier.multiscale ?? { index: 0 }); + if (multiscale === undefined) { + const message = `cannot get matching dataset and array for ${targetDesc}: multiple multiscales specified`; + logger.error(message); + throw new VisZarrDataError(message); + } - let matching: { path: string; multiscale: OmeZarrMultiscale; dataset: OmeZarrDataset; datasetIndex: number }[]; + let matching: { path: string; dataset: OmeZarrDataset; datasetIndex: number }; if ('index' in specifier) { const i = specifier.index; - if (selectedMultiscales.length > 1) { - const message = `cannot get matching dataset and array for index [${i}]: multiple multiscales specified`; + // matching = selectedMultiscales.map((m) => { + if (i < 0 || i >= multiscale.datasets.length) { + const message = `cannot get matching dataset and array for ${targetDesc}: index out of bounds`; logger.error(message); throw new VisZarrDataError(message); } - matching = selectedMultiscales.map((m) => { - if (i < 0 || i >= m.datasets.length) { - const message = `cannot get matching dataset and array for index [${i}]: index out of bounds`; - logger.error(message); - throw new VisZarrDataError(message); - } - const dataset = m.datasets[specifier.index]; - if (dataset === undefined) { - const message = `cannot get matching dataset and array for index [${i}]: dataset undefined`; - logger.error(message); - throw new VisZarrDataError(message); - } - return { path: dataset.path, datasetIndex: i, multiscale: m, dataset }; - }); - - if (matching.length > 1) { - const message = `cannot get matching dataset and array for index [${i}]: multiple matching datasets found`; + const dataset = multiscale.datasets[specifier.index]; + if (dataset === undefined) { + const message = `cannot get matching dataset and array for index ${targetDesc}: no dataset found at that index`; logger.error(message); throw new VisZarrDataError(message); } + matching = { path: dataset.path, datasetIndex: i, dataset }; + } else { const path = specifier.path; - matching = selectedMultiscales.map((m) => { - const datasets = m.datasets.filter((d) => d.path === path); - if (datasets.length > 1) { - const message = `cannot get matching dataset and array for path [${path}]: multiple matching datasets found`; - logger.error(message); - throw new VisZarrDataError(message); - } - const dataset = datasets[0]; - if (dataset === undefined) { - const message = `cannot get matching dataset and array for path [${path}]: dataset was undefined`; - logger.error(message); - throw new VisZarrDataError(message); - } - const datasetIndex = m.datasets.findIndex((d) => d.path === dataset.path); - if (datasetIndex === -1) { - const message = `cannot get matching dataset and array for path [${path}]: index of matching dataset was not found`; - logger.error(message); - throw new VisZarrDataError(message); - } - return { path, multiscale: m, dataset, datasetIndex }; - }); - - if (matching.length > 1) { - const message = `cannot get matching dataset and array for path [${path}]: multiple matching datasets found`; + const datasetIndex = multiscale.datasets.findIndex((d) => d.path === path); + const dataset = multiscale.datasets[datasetIndex]; + if (datasetIndex === -1 || dataset === undefined) { + const message = `cannot get matching dataset and array for ${targetDesc}: no matching path found`; logger.error(message); throw new VisZarrDataError(message); } + matching = { path, dataset, datasetIndex }; } - if (matching.length < 1) { - return; - } - - const { path, multiscale, dataset, datasetIndex } = matching[0]; + const { path, dataset, datasetIndex } = matching; const array = this.#arrays.get(path); - if (multiscale === undefined || dataset === undefined || array === undefined) { - const message = `cannot get matching dataset and array for path [${path}]: one or more elements were undefined`; + if (array === undefined) { + const message = `cannot get matching dataset and array for ${targetDesc}: no matching array found`; logger.error(message); throw new VisZarrDataError(message); } @@ -319,7 +289,7 @@ export class OmeZarrFileset { plane: CartesianPlane, relativeView: box2D, // a box in data-unit-space displayResolution: vec2, // in the plane given above - multiscaleName?: string | undefined, + multiscaleSpec?: OmeZarrMultiscaleSpecifier | undefined, ): OmeZarrLevel { if (!this.ready) { const message = 'cannot pick best-fitting scale: OME-Zarr metadata not yet loaded'; @@ -327,7 +297,7 @@ export class OmeZarrFileset { throw new VisZarrDataError(message); } - const level = this.getLevel({ index: 0, multiscale: multiscaleName }); + const level = this.getLevel({ index: 0, multiscale: multiscaleSpec }); if (!level) { const message = 'cannot pick best-fitting scale: no initial dataset context found'; logger.error(message); From 55b639f5f3cccddf0be6ef0d9932648d6597804e Mon Sep 17 00:00:00 2001 From: Joel Arbuckle Date: Fri, 10 Oct 2025 10:54:38 -0700 Subject: [PATCH 54/91] Working on demo app + getting workers working --- packages/core/src/workers/worker-pool.ts | 42 ++- packages/omezarr/src/index.ts | 16 ++ packages/omezarr/src/planar-view/renderer.ts | 14 +- ...e.interface.ts => fetch-data.interface.ts} | 40 +-- ....worker.ts => fetch-data.worker-loader.ts} | 59 ++-- .../omezarr/src/zarr/cached-loading/store.ts | 24 +- packages/omezarr/src/zarr/omezarr-fileset.ts | 196 +++++++------- .../src/content/docs/examples/ome-zarr-v3.mdx | 8 + .../common/filesets/omezarr/demo-filesets.ts | 82 ++++++ site/src/examples/omezarr-v3/fetch.worker.ts | 3 + .../examples/omezarr-v3/omezarr-v3-client.tsx | 172 ++++++++++++ .../examples/omezarr-v3/omezarr-v3-demo.tsx | 255 ++++++++++++++++++ .../omezarr-v3-via-priority-cache.tsx | 14 + .../examples/omezarr-v3/omezarr-v3-viewer.tsx | 218 +++++++++++++++ site/src/examples/omezarr-v3/render-utils.ts | 125 +++++++++ 15 files changed, 1111 insertions(+), 157 deletions(-) rename packages/omezarr/src/zarr/cached-loading/{fetch-slice.interface.ts => fetch-data.interface.ts} (62%) rename packages/omezarr/src/zarr/cached-loading/{fetch-slice.worker.ts => fetch-data.worker-loader.ts} (50%) create mode 100644 site/src/content/docs/examples/ome-zarr-v3.mdx create mode 100644 site/src/examples/common/filesets/omezarr/demo-filesets.ts create mode 100644 site/src/examples/omezarr-v3/fetch.worker.ts create mode 100644 site/src/examples/omezarr-v3/omezarr-v3-client.tsx create mode 100644 site/src/examples/omezarr-v3/omezarr-v3-demo.tsx create mode 100644 site/src/examples/omezarr-v3/omezarr-v3-via-priority-cache.tsx create mode 100644 site/src/examples/omezarr-v3/omezarr-v3-viewer.tsx create mode 100644 site/src/examples/omezarr-v3/render-utils.ts diff --git a/packages/core/src/workers/worker-pool.ts b/packages/core/src/workers/worker-pool.ts index c80ef0a5..85ffc3c7 100644 --- a/packages/core/src/workers/worker-pool.ts +++ b/packages/core/src/workers/worker-pool.ts @@ -36,7 +36,7 @@ export class WorkerPool { for (let i = 0; i < size; i++) { this.#workers[i] = new Worker(workerModule, { type: 'module' }); this.#workers[i].onmessage = (msg) => this.#handleMessage(i, msg); - this.#timeOfPreviousHeartbeat.set(i, Date.now()); + this.#timeOfPreviousHeartbeat.set(i, 0); } this.#promises = new Map(); this.#which = 0; @@ -73,17 +73,45 @@ export class WorkerPool { this.#which = (this.#which + 1) % this.#workers.length; } - submitRequest( + async #getNextInitializedWorker(timeout = 5000, waitTime = 200): Promise { + const wait = (millis: number) => new Promise((resolve) => setTimeout(resolve, millis)); + let timeWaited = 0; + while (timeWaited < timeout) { + for (let i = this.#which; i < this.#workers.length; i++) { + const lastHeartbeat = this.#timeOfPreviousHeartbeat.get(i); + if (lastHeartbeat === undefined) { + throw new Error('invalid worker state'); + } + if (lastHeartbeat > 0) { + this.#which = i; + return i; + } + } + const beforeWaiting = Date.now(); + wait(waitTime); + timeWaited += Date.now() - beforeWaiting; + } + // we didn't find an initialized worker before the timeout occurred :( + return -1; + } + + async submitRequest( message: WorkerMessage, responseValidator: MessageValidator, transfers: Transferable[], signal?: AbortSignal | undefined, ): Promise { const reqId = `rq${uuidv4()}`; - const workerIndex = this.#which; + const workerIndex = await this.#getNextInitializedWorker(); + if (workerIndex === -1) { + throw new Error('could not submit request: workers have not initialized'); + } const messageWithId = { ...message, id: reqId }; const messagePromise = this.#createMessagePromise(responseValidator); + console.log('>>> >>> >>> received submission of request'); + console.log('>>> >>> >>> worker health:', this.getStatus(workerIndex)); + this.#promises.set(reqId, messagePromise); if (signal) { @@ -126,7 +154,13 @@ export class WorkerPool { if (!this.#isValidIndex(workerIndex)) { throw new Error('invalid worker index'); } - const delta = Date.now() - (this.#timeOfPreviousHeartbeat.get(workerIndex) ?? 0); + const lastHeartbeat = this.#timeOfPreviousHeartbeat.get(workerIndex) ?? 0; + if (lastHeartbeat === 0) { + return WorkerStatus.Unresponsive; + } + const delta = Date.now() - lastHeartbeat; + console.log('>>> >>> >>> >>> lastHeartbeat:', lastHeartbeat); + console.log('>>> >>> >>> >>> delta:', delta); if (delta && delta > 1500) { return WorkerStatus.Unresponsive; } diff --git a/packages/omezarr/src/index.ts b/packages/omezarr/src/index.ts index 031a6224..2c10dc30 100644 --- a/packages/omezarr/src/index.ts +++ b/packages/omezarr/src/index.ts @@ -63,6 +63,7 @@ export { type ZarrDimensionSelection, type ZarrSelection, type ZarrSlice, + loadOmeZarrFileset, } from './zarr/omezarr-fileset'; export { OmeZarrLevel } from './zarr/omezarr-level'; export { @@ -79,4 +80,19 @@ export type { export { buildOmeZarrPlanarRenderer, buildAsyncOmeZarrPlanarRenderer, + defaultPlanarDecoder, } from './planar-view/renderer'; +export { + setupFetchDataWorker +} from './zarr/cached-loading/fetch-data.worker-loader'; +export { + type TransferrableRequestInit, + type FetchMessagePayload, + type FetchMessage, + type FetchResponseMessage, + type CancelMessage, + isFetchMessage, + isFetchResponseMessage, + isCancelMessage, + isCancellationError, +} from './zarr/cached-loading/fetch-data.interface'; diff --git a/packages/omezarr/src/planar-view/renderer.ts b/packages/omezarr/src/planar-view/renderer.ts index ea56fdb2..029985a6 100644 --- a/packages/omezarr/src/planar-view/renderer.ts +++ b/packages/omezarr/src/planar-view/renderer.ts @@ -11,7 +11,7 @@ import { buildTileRenderCommand } from '../rendering/tile-rendering'; import type { OmeZarrFileset, ZarrDataRequest, ZarrSlice } from '../zarr/omezarr-fileset'; import type { PlanarRenderSettings, PlanarRendererOptions, PlanarVoxelTile, PlanarVoxelTileImage } from './types'; import { getVisibleOmeZarrTiles } from './visibility'; -import type { OmeZarrLevel } from '../zarr/omezarr-level'; +import type { Chunk } from 'zarrita'; type ImageChannels = { [channelKey: string]: CachedTexture; @@ -78,7 +78,6 @@ function isPrepared(cacheData: Record): cach export type OmeZarrVoxelTileImageDecoder = ( fileset: OmeZarrFileset, req: ZarrDataRequest, - dataContext: OmeZarrLevel, signal?: AbortSignal, ) => Promise; @@ -131,7 +130,6 @@ export function buildOmeZarrPlanarRenderer( decoder( dataset, toZarrDataRequest(item, settings.channels[key].index), - item.dataContext, signal, ).then(sliceAsTexture); } @@ -169,3 +167,13 @@ export function buildAsyncOmeZarrPlanarRenderer( ) { return buildAsyncRenderer(buildOmeZarrPlanarRenderer(regl, decoder, options), options?.queueOptions); } + +export const defaultPlanarDecoder: OmeZarrVoxelTileImageDecoder = async ( + fileset: OmeZarrFileset, + req: ZarrDataRequest, + signal?: AbortSignal, +) => { + const result: { shape: number[]; buffer: Chunk<'float32'> } = await fileset.loadSlice(req, signal); + const { shape, buffer } = result; + return { shape, data: new Float32Array(buffer.data) }; +} diff --git a/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts b/packages/omezarr/src/zarr/cached-loading/fetch-data.interface.ts similarity index 62% rename from packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts rename to packages/omezarr/src/zarr/cached-loading/fetch-data.interface.ts index 8774a551..3c0926b5 100644 --- a/packages/omezarr/src/zarr/cached-loading/fetch-slice.interface.ts +++ b/packages/omezarr/src/zarr/cached-loading/fetch-data.interface.ts @@ -6,25 +6,25 @@ export type TransferrableRequestInit = Omit; }; -export type FetchSliceMessagePayload = { +export type FetchMessagePayload = { rootUrl: string; path: AbsolutePath; - range: RangeQuery; + range?: RangeQuery | undefined; options?: TransferrableRequestInit | undefined; }; -export const FETCH_SLICE_MESSAGE_TYPE = 'fetch-slice' as const; -export const FETCH_SLICE_RESPONSE_MESSAGE_TYPE = 'fetch-slice-response' as const; +export const FETCH_MESSAGE_TYPE = 'fetch' as const; +export const FETCH_RESPONSE_MESSAGE_TYPE = 'fetch-response' as const; export const CANCEL_MESSAGE_TYPE = 'cancel' as const; -export type FetchSliceMessage = { - type: typeof FETCH_SLICE_MESSAGE_TYPE; +export type FetchMessage = { + type: typeof FETCH_MESSAGE_TYPE; id: string; - payload: FetchSliceMessagePayload; + payload: FetchMessagePayload; }; -export type FetchSliceResponseMessage = { - type: typeof FETCH_SLICE_RESPONSE_MESSAGE_TYPE; +export type FetchResponseMessage = { + type: typeof FETCH_RESPONSE_MESSAGE_TYPE; id: string; payload: ArrayBufferLike | undefined; }; @@ -34,7 +34,7 @@ export type CancelMessage = { id: string; }; -const FetchSliceMessagePayloadSchema = z.object({ +const FetchMessagePayloadSchema = z.object({ rootUrl: z.string().nonempty(), path: z.string().nonempty().startsWith('/'), range: z.union([ @@ -45,18 +45,18 @@ const FetchSliceMessagePayloadSchema = z.object({ z.object({ suffixLength: z.number(), }), - ]), + ]).optional(), options: z.unknown().optional(), // being "lazy" for now; doing a full schema for this could be complex and fragile }); -const FetchSliceMessageSchema = z.object({ - type: z.literal(FETCH_SLICE_MESSAGE_TYPE), +const FetchMessageSchema = z.object({ + type: z.literal(FETCH_MESSAGE_TYPE), id: z.string().nonempty(), - payload: FetchSliceMessagePayloadSchema, + payload: FetchMessagePayloadSchema, }); -const FetchSliceResponseMessageSchema = z.object({ - type: z.literal(FETCH_SLICE_RESPONSE_MESSAGE_TYPE), +const FetchResponseMessageSchema = z.object({ + type: z.literal(FETCH_RESPONSE_MESSAGE_TYPE), id: z.string().nonempty(), payload: z.unknown().optional(), // unclear if it's feasible/wise to define a schema for this one }); @@ -66,12 +66,12 @@ const CancelMessageSchema = z.object({ id: z.string().nonempty(), }); -export function isFetchSliceMessage(val: unknown): val is FetchSliceMessage { - return FetchSliceMessageSchema.safeParse(val).success; +export function isFetchMessage(val: unknown): val is FetchMessage { + return FetchMessageSchema.safeParse(val).success; } -export function isFetchSliceResponseMessage(val: unknown): val is FetchSliceResponseMessage { - return FetchSliceResponseMessageSchema.safeParse(val).success; +export function isFetchResponseMessage(val: unknown): val is FetchResponseMessage { + return FetchResponseMessageSchema.safeParse(val).success; } export function isCancelMessage(val: unknown): val is CancelMessage { diff --git a/packages/omezarr/src/zarr/cached-loading/fetch-slice.worker.ts b/packages/omezarr/src/zarr/cached-loading/fetch-data.worker-loader.ts similarity index 50% rename from packages/omezarr/src/zarr/cached-loading/fetch-slice.worker.ts rename to packages/omezarr/src/zarr/cached-loading/fetch-data.worker-loader.ts index bd845a25..a30be977 100644 --- a/packages/omezarr/src/zarr/cached-loading/fetch-slice.worker.ts +++ b/packages/omezarr/src/zarr/cached-loading/fetch-data.worker-loader.ts @@ -2,23 +2,31 @@ import { HEARTBEAT_RATE_MS, logger } from '@alleninstitute/vis-core'; import { type AbsolutePath, FetchStore, type RangeQuery } from 'zarrita'; -import type { CancelMessage, FetchSliceMessage, TransferrableRequestInit } from './fetch-slice.interface'; -import { isCancellationError, isCancelMessage, isFetchSliceMessage } from './fetch-slice.interface'; +import type { CancelMessage, FetchMessage, TransferrableRequestInit } from './fetch-data.interface'; +import { FETCH_RESPONSE_MESSAGE_TYPE, isCancellationError, isCancelMessage, isFetchMessage } from './fetch-data.interface'; -async function fetchSlice( +const fetchFile = async ( rootUrl: string, path: AbsolutePath, - range: RangeQuery, options?: TransferrableRequestInit | undefined, abortController?: AbortController | undefined, -): Promise { +): Promise => { const store = new FetchStore(rootUrl); - return store.getRange(path, range, { ...(options || {}), signal: abortController?.signal }); + return store.get(path, { ...(options || {}), signal: abortController?.signal ?? null }); } -const abortControllers: Record = {}; +const fetchSlice = async ( + rootUrl: string, + path: AbsolutePath, + range: RangeQuery, + options?: TransferrableRequestInit | undefined, + abortController?: AbortController | undefined, +): Promise => { + const store = new FetchStore(rootUrl); + return store.getRange(path, range, { ...(options || {}), signal: abortController?.signal ?? null }); +} -const handleFetchSlice = (message: FetchSliceMessage) => { +const handleFetch = (message: FetchMessage, abortControllers: Record) => { const { id, payload } = message; const { rootUrl, path, range, options } = payload; @@ -30,13 +38,15 @@ const handleFetchSlice = (message: FetchSliceMessage) => { const abort = new AbortController(); abortControllers[id] = abort; - fetchSlice(rootUrl, path, range, options, abort) + const fetchFn = range !== undefined ? () => fetchSlice(rootUrl, path, range, options, abort) : () => fetchFile(rootUrl, path, options, abort); + + fetchFn() .then((result: Uint8Array | undefined) => { const buffer = result?.buffer; const options = buffer !== undefined ? { transfer: [buffer] } : {}; self.postMessage( { - type: 'fetch-slice-response', + type: FETCH_RESPONSE_MESSAGE_TYPE, id, payload: result?.buffer, }, @@ -51,7 +61,7 @@ const handleFetchSlice = (message: FetchSliceMessage) => { }); }; -const handleCancel = (message: CancelMessage) => { +const handleCancel = (message: CancelMessage, abortControllers: Record) => { const { id } = message; const abortController = abortControllers[id]; if (!abortController) { @@ -61,16 +71,25 @@ const handleCancel = (message: CancelMessage) => { } }; -self.setInterval(() => { +const startHeartbeat = () => setInterval(() => { self.postMessage({ type: 'heartbeat' }); }, HEARTBEAT_RATE_MS); -self.onmessage = async (e: MessageEvent) => { - const { data: message } = e; +const setupOnMessage = () => { + const abortControllers: Record = {}; + const onmessage = async (e: MessageEvent) => { + const { data: message } = e; - if (isFetchSliceMessage(message)) { - handleFetchSlice(message); - } else if (isCancelMessage(message)) { - handleCancel(message); - } -}; + if (isFetchMessage(message)) { + handleFetch(message, abortControllers); + } else if (isCancelMessage(message)) { + handleCancel(message, abortControllers); + } + }; + return onmessage; +} + +export const setupFetchDataWorker = (ctx: typeof self) => { + ctx.onmessage = setupOnMessage(); + return { startHeartbeat }; +} diff --git a/packages/omezarr/src/zarr/cached-loading/store.ts b/packages/omezarr/src/zarr/cached-loading/store.ts index 81e473ea..e664a671 100644 --- a/packages/omezarr/src/zarr/cached-loading/store.ts +++ b/packages/omezarr/src/zarr/cached-loading/store.ts @@ -1,10 +1,10 @@ import { type Cacheable, logger, PriorityCache, WorkerPool } from '@alleninstitute/vis-core'; import * as zarr from 'zarrita'; import { - FETCH_SLICE_MESSAGE_TYPE, - type FetchSliceResponseMessage, - isFetchSliceResponseMessage, -} from './fetch-slice.interface'; + FETCH_MESSAGE_TYPE, + type FetchResponseMessage, + isFetchResponseMessage, +} from './fetch-data.interface'; const DEFAULT_NUM_WORKERS = 6; const DEFAULT_MAX_DATA_CACHE_BYTES = 256 * 2 ** 10; // 256 MB -- aribtrarily chosen at this point @@ -209,6 +209,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { options: TransferableRequestInit, abort: AbortSignal | undefined, ): Promise { + console.log('>>> >>> starting doFetch'); const cacheKey = asCacheKey(key, range); this.#priorityByTimestamp.set(cacheKey, Date.now()); @@ -236,21 +237,23 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { }); } + console.log('>>> >>> submitting request'); const request = this.#workerPool.submitRequest( { - type: FETCH_SLICE_MESSAGE_TYPE, + type: FETCH_MESSAGE_TYPE, rootUrl: this.url, path: key, range, options, }, - isFetchSliceResponseMessage, + isFetchResponseMessage, [], chain.signal, ); request - .then((response: FetchSliceResponseMessage) => { + .then((response: FetchResponseMessage) => { + console.log('>>> >>> received resolve'); const payload = response.payload; if (payload === undefined) { resolve(undefined); @@ -261,6 +264,7 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { resolve(arr); }) .catch((e: unknown) => { + console.log('>>> >>> received reject'); reject(e); }) .finally(() => { @@ -299,13 +303,13 @@ export class CachingMultithreadedFetchStore extends zarr.FetchStore { return this.#doFetch(key, range, workerOptions, abort); } } -export class ZarrSliceFetchStore extends CachingMultithreadedFetchStore { - constructor(url: string | URL, options?: CachingMultithreadedFetchStoreOptions) { +export class ZarrFetchStore extends CachingMultithreadedFetchStore { + constructor(url: string | URL, workerModule: URL, options?: CachingMultithreadedFetchStoreOptions) { super( url, new WorkerPool( options?.numWorkers ?? DEFAULT_NUM_WORKERS, - new URL('./fetch-slice.worker.ts', import.meta.url), + workerModule, ), options, ); diff --git a/packages/omezarr/src/zarr/omezarr-fileset.ts b/packages/omezarr/src/zarr/omezarr-fileset.ts index 9a397c2b..6c28a309 100644 --- a/packages/omezarr/src/zarr/omezarr-fileset.ts +++ b/packages/omezarr/src/zarr/omezarr-fileset.ts @@ -1,4 +1,4 @@ -import { logger, type WebResource, WorkerPool } from '@alleninstitute/vis-core'; +import { logger, type WebResource } from '@alleninstitute/vis-core'; import { Box2D, type box2D, @@ -11,7 +11,7 @@ import { import * as zarr from 'zarrita'; import { z } from 'zod'; import { VisZarrDataError } from '../errors'; -import { CachingMultithreadedFetchStore } from './cached-loading/store'; +import { ZarrFetchStore } from './cached-loading/store'; import { OmeZarrArrayTransform, OmeZarrGroupTransform } from './omezarr-transforms'; import { convertFromOmeroToColorChannels, @@ -26,9 +26,6 @@ import { } from './types'; import { OmeZarrLevel } from './omezarr-level'; -const WORKER_MODULE_URL = new URL('./cached-loading/fetch-slice.worker.ts', import.meta.url); -const NUM_WORKERS = 8; - export type ZarrDimensionSelection = number | Interval | null; export type ZarrSelection = (number | zarr.Slice | null)[]; @@ -88,78 +85,108 @@ export type OmeZarrLevelSpecifier = { } ); -export class OmeZarrFileset { - #store: CachingMultithreadedFetchStore; - #root: zarr.Location; - #rootGroup: OmeZarrGroup | null; - #arrays: Map; - #zarritaGroups: Map>; - #zarritaArrays: Map>; +export type LoadOmeZarrMetadataOptions = { + numWorkers?: number | undefined; +}; - constructor(res: WebResource) { - this.#store = new CachingMultithreadedFetchStore(res.url, new WorkerPool(NUM_WORKERS, WORKER_MODULE_URL)); - this.#root = zarr.root(this.#store); - this.#rootGroup = null; - this.#arrays = new Map(); - this.#zarritaGroups = new Map(); - this.#zarritaArrays = new Map(); - } +type OmeZarrGroupLoadSet = { + raw: zarr.Group; + transformed: OmeZarrGroup; +}; - async #loadGroup(location: zarr.Location): Promise { - const group = await zarr.open(location, { kind: 'group' }); - this.#zarritaGroups.set(location.path, group); - try { - return OmeZarrGroupTransform.parse(group.attrs); - } catch (e) { - if (e instanceof z.ZodError) { - logger.error('could not load Zarr group metadata: parsing failed'); - } - throw e; +type OmeZarrArrayLoadSet = { + raw: zarr.Array; + transformed: OmeZarrArray; +}; + +const loadGroup = async (location: zarr.Location): Promise> => { + console.log('>>> group loading...'); + const group = await zarr.open(location, { kind: 'group' }); + console.log('>>> group loaded!'); + try { + return { raw: group, transformed: OmeZarrGroupTransform.parse(group.attrs) }; + } catch (e) { + if (e instanceof z.ZodError) { + logger.error('could not load Zarr group metadata: parsing failed'); } + throw e; } +}; - async #loadArray(location: zarr.Location): Promise { - const array = await zarr.open(location, { kind: 'array' }); - this.#zarritaArrays.set(location.path, array); - try { - return OmeZarrArrayTransform.parse(array); - } catch (e) { - if (e instanceof z.ZodError) { - logger.error('could not load Zarr array metadata: parsing failed'); - } - throw e; +const loadArray = async (location: zarr.Location): Promise> => { + const array = await zarr.open(location, { kind: 'array' }); + try { + return { raw: array, transformed: OmeZarrArrayTransform.parse(array) }; + } catch (e) { + if (e instanceof z.ZodError) { + logger.error('could not load Zarr array metadata: parsing failed'); } + throw e; } +}; - async #loadRootAttrs(): Promise { - return await this.#loadGroup(this.#root); - } +export async function loadOmeZarrFileset( + res: WebResource, + workerModule: URL, + options?: LoadOmeZarrMetadataOptions | undefined, +): Promise { + console.log('generating store'); + const store = new ZarrFetchStore(res.url, workerModule, { numWorkers: options?.numWorkers }); + console.log('store generated'); + const root = zarr.root(store); + console.log('zarr root:', root.path); + + const zarritaGroups = new Map>(); + const zarritaArrays = new Map>(); + + console.log('loading root group'); + const { raw: rawRootGroup, transformed: rootGroup } = await loadGroup(root); + console.log('loaded root group'); + zarritaGroups.set('/', rawRootGroup); + + const arrayResults = await Promise.all( + rootGroup.attributes.multiscales + .map((multiscale) => + multiscale.datasets?.map(async (dataset) => { + return await loadArray(root.resolve(dataset.path)); + }), + ) + .reduce((prev, curr) => prev.concat(curr)) + .filter((arr) => arr !== undefined), + ); + + const arrays = new Map(); + + arrayResults.forEach(({ raw, transformed }) => { + arrays.set(transformed.path, transformed); + zarritaArrays.set(raw.path, raw); + }); - async loadMetadata() { - if (this.#rootGroup !== null) { - logger.warn('attempted to load the same OME-Zarr fileset after it was already loaded'); - return; - } - this.#rootGroup = await this.#loadRootAttrs(); - - const arrayResults = await Promise.all( - this.#rootGroup.attributes.multiscales - .map((multiscale) => - multiscale.datasets?.map(async (dataset) => { - return await this.#loadArray(this.#root.resolve(dataset.path)); - }), - ) - .reduce((prev, curr) => prev.concat(curr)) - .filter((arr) => arr !== undefined), - ); - - arrayResults.forEach((arr) => { - this.#arrays.set(arr.path, arr); - }); - } + return new OmeZarrFileset(store, root, rootGroup, arrays, zarritaGroups, zarritaArrays); +} + +export class OmeZarrFileset { + #store: ZarrFetchStore; + #root: zarr.Location; + #rootGroup: OmeZarrGroup; + #arrays: Map; + #zarritaGroups: Map>; + #zarritaArrays: Map>; - get ready(): boolean { - return this.#rootGroup !== null; + constructor( + store: ZarrFetchStore, + root: zarr.Location, + rootGroup: OmeZarrGroup, + arrays: Map, + zarritaGroups: Map>, + zarritaArrays: Map>, + ) { + this.#store = store; + this.#root = root; + this.#rootGroup = rootGroup; + this.#arrays = arrays; + this.#zarritaGroups = zarritaGroups; + this.#zarritaArrays = zarritaArrays; } get url(): string | URL { @@ -171,12 +198,6 @@ export class OmeZarrFileset { } getAxes(multiscaleName: string | undefined): OmeZarrAxis[] | undefined { - if (this.#rootGroup === null || this.#rootGroup.attributes.multiscales.length < 1) { - const message = - 'cannot request multiscale axes: OME-Zarr fileset has no multiscale data (it may not have been loaded yet)'; - logger.error(message); - throw new VisZarrDataError(message); - } const multiscales = this.#rootGroup.attributes.multiscales; const multiscale = multiscaleName === undefined ? multiscales[0] : multiscales.find((v) => v.name === multiscaleName); @@ -184,13 +205,7 @@ export class OmeZarrFileset { } getMultiscale(specifier: OmeZarrMultiscaleSpecifier): OmeZarrMultiscale | undefined { - if (!this.ready) { - const message = 'cannot get multiscale: OME-Zarr metadata not yet loaded'; - logger.error(message); - throw new VisZarrDataError(message); - } - - const multiscales = this.#rootGroup?.attributes.multiscales; + const multiscales = this.#rootGroup.attributes.multiscales; if (multiscales === undefined) { const message = 'cannot get multiscale: no multiscales found'; logger.error(message); @@ -201,10 +216,7 @@ export class OmeZarrFileset { } getLevel(specifier: OmeZarrLevelSpecifier): OmeZarrLevel | undefined { - if (this.#rootGroup === undefined) { - return; - } - const targetDesc = 'index' in specifier ? `index [${specifier.index}]` : `path [${specifier.path}]`; + const targetDesc = 'index' in specifier ? `index [${specifier.index}]` : `path [${specifier.path}]`; const multiscale = this.getMultiscale(specifier.multiscale ?? { index: 0 }); if (multiscale === undefined) { @@ -230,7 +242,6 @@ export class OmeZarrFileset { throw new VisZarrDataError(message); } matching = { path: dataset.path, datasetIndex: i, dataset }; - } else { const path = specifier.path; const datasetIndex = multiscale.datasets.findIndex((d) => d.path === path); @@ -291,12 +302,6 @@ export class OmeZarrFileset { displayResolution: vec2, // in the plane given above multiscaleSpec?: OmeZarrMultiscaleSpecifier | undefined, ): OmeZarrLevel { - if (!this.ready) { - const message = 'cannot pick best-fitting scale: OME-Zarr metadata not yet loaded'; - logger.error(message); - throw new VisZarrDataError(message); - } - const level = this.getLevel({ index: 0, multiscale: multiscaleSpec }); if (!level) { const message = 'cannot pick best-fitting scale: no initial dataset context found'; @@ -346,12 +351,6 @@ export class OmeZarrFileset { relativeView: box2D, // a box in data-unit-space displayResolution: vec2, // in the plane given above ) { - if (!this.ready) { - const message = 'cannot pick best-fitting scale: OME-Zarr metadata not yet loaded'; - logger.error(message); - throw new VisZarrDataError(message); - } - // figure out what layer we'd be viewing const level = this.pickBestScale(plane, relativeView, displayResolution); const slices = level.sizeInVoxels(plane.ortho); @@ -359,9 +358,6 @@ export class OmeZarrFileset { } #getDimensionIndex(dim: ZarrDimension, multiscaleSpec: OmeZarrMultiscaleSpecifier): number | undefined { - if (!this.ready) { - return undefined; - } const multiscale = this.getMultiscale(multiscaleSpec); if (multiscale === undefined) { return undefined; diff --git a/site/src/content/docs/examples/ome-zarr-v3.mdx b/site/src/content/docs/examples/ome-zarr-v3.mdx new file mode 100644 index 00000000..a84a7cec --- /dev/null +++ b/site/src/content/docs/examples/ome-zarr-v3.mdx @@ -0,0 +1,8 @@ +--- +title: OME-Zarr (v3) +tableOfContents: false +--- + +import { OmeZarrV3Demo } from '../../../examples/omezarr-v3/omezarr-v3-via-priority-cache.tsx'; + + diff --git a/site/src/examples/common/filesets/omezarr/demo-filesets.ts b/site/src/examples/common/filesets/omezarr/demo-filesets.ts new file mode 100644 index 00000000..10222b43 --- /dev/null +++ b/site/src/examples/common/filesets/omezarr/demo-filesets.ts @@ -0,0 +1,82 @@ +import type { WebResource } from '@alleninstitute/vis-core'; + +export type OmeZarrFilesetOption = { value: string; label: string; zarrVersion: number; res: WebResource }; + +export const OMEZARR_FILESET_OPTIONS: OmeZarrFilesetOption[] = [ + { + value: 'opt1', + label: 'VERSA OME-Zarr Example (HTTPS) (color channels: [R, G, B])', + zarrVersion: 2, + res: { type: 'https', url: 'https://neuroglancer-vis-prototype.s3.amazonaws.com/VERSA/scratch/0500408166/' }, + }, + { + value: 'opt2', + label: 'VS200 Example Image (S3) (color channels: [CFP, YFP])', + zarrVersion: 2, + res: { + type: 's3', + region: 'us-west-2', + url: 's3://allen-genetic-tools/epifluorescence/1401210938/ome_zarr_conversion/1401210938.zarr/', + }, + }, + { + value: 'opt3', + label: 'EPI Example Image (S3) (color channels: [R, G, B])', + zarrVersion: 2, + res: { + type: 's3', + region: 'us-west-2', + url: 's3://allen-genetic-tools/epifluorescence/1383646325/ome_zarr_conversion/1383646325.zarr/', + }, + }, + { + value: 'opt4', + label: 'STPT Example Image (S3) (color channels: [R, G, B])', + zarrVersion: 2, + res: { + type: 's3', + region: 'us-west-2', + url: 's3://allen-genetic-tools/tissuecyte/823818122/ome_zarr_conversion/823818122.zarr/', + }, + }, + { + value: 'opt5', + label: 'Smart-SPIM (experimental)', + zarrVersion: 2, + res: { + type: 's3', + region: 'us-west-2', + url: 's3://aind-open-data/SmartSPIM_787715_2025-04-08_18-33-36_stitched_2025-04-09_22-42-59/image_tile_fusing/OMEZarr/Ex_445_Em_469.zarr', + }, + }, + { + value: 'opt6', + label: 'VS200 Brightfield #1458501514 (Zarr v3)', + zarrVersion: 3, + res: { + type: 's3', + region: 'us-west-2', + url: 's3://h301-scanning-802451596237-us-west-2/2402091625/ome_zarr_conversion/1458501514.zarr/', + }, + }, + { + value: 'opt7', + label: 'VS200 Epifluorescence #1161134570 (Zarr v3)', + zarrVersion: 3, + res: { + type: 's3', + region: 'us-west-2', + url: 's3://cortex-aav-toolbox-802451596237-us-west-2/epifluorescence/1161134570/ome_zarr_conversion/1161134570.zarr', + }, + }, + { + value: 'opt8', + label: 'VERSA Epifluorescence #1161864579 (Zarr v3)', + zarrVersion: 3, + res: { + type: 's3', + region: 'us-west-2', + url: 's3://cortex-aav-toolbox-802451596237-us-west-2/epifluorescence/1161864579/ome_zarr_conversion/1161864579.zarr', + }, + }, +]; diff --git a/site/src/examples/omezarr-v3/fetch.worker.ts b/site/src/examples/omezarr-v3/fetch.worker.ts new file mode 100644 index 00000000..55f9cc2e --- /dev/null +++ b/site/src/examples/omezarr-v3/fetch.worker.ts @@ -0,0 +1,3 @@ +import { setupFetchDataWorker } from '@alleninstitute/vis-omezarr'; + +setupFetchDataWorker(self).startHeartbeat(); diff --git a/site/src/examples/omezarr-v3/omezarr-v3-client.tsx b/site/src/examples/omezarr-v3/omezarr-v3-client.tsx new file mode 100644 index 00000000..3cd45bb0 --- /dev/null +++ b/site/src/examples/omezarr-v3/omezarr-v3-client.tsx @@ -0,0 +1,172 @@ +/** biome-ignore-all lint/correctness/useExhaustiveDependencies: */ +import { logger, type WebResource } from '@alleninstitute/vis-core'; +import { Box2D, PLANE_XY, type box2D, type Interval, type vec2 } from '@alleninstitute/vis-geometry'; +import { + defaultPlanarDecoder, + loadOmeZarrFileset, + type OmeZarrFileset, + type PlanarRenderSettings, + type PlanarRenderSettingsChannels, +} from '@alleninstitute/vis-omezarr'; +import { useContext, useState, useRef, useCallback, useEffect } from 'react'; +import { zoom, pan } from '../common/camera'; +import { SharedCacheContext } from '../common/react/priority-cache-provider'; +import { buildConnectedRenderer } from './render-utils'; + +const defaultInterval: Interval = { min: 0, max: 80 }; + +function makeZarrSettings(screenSize: vec2, view: box2D, param: number, omezarr: OmeZarrFileset): PlanarRenderSettings { + const omezarrChannels = omezarr.getColorChannels().reduce((acc, val, index) => { + acc[val.label ?? `${index}`] = { + rgb: val.rgb, + gamut: val.range, + index, + }; + return acc; + }, {} as PlanarRenderSettingsChannels); + + const fallbackChannels: PlanarRenderSettingsChannels = { + R: { rgb: [1.0, 0, 0], gamut: defaultInterval, index: 0 }, + G: { rgb: [0, 1.0, 0], gamut: defaultInterval, index: 1 }, + B: { rgb: [0, 0, 1.0], gamut: defaultInterval, index: 2 }, + }; + + return { + camera: { screenSize, view }, + planeLocation: param, + plane: PLANE_XY, + tileSize: 256, + channels: Object.keys(omezarrChannels).length > 0 ? omezarrChannels : fallbackChannels, + }; +} + +type Props = { + res: WebResource; + screenSize: vec2; +}; + +export function OmeZarrView(props: Props) { + const { screenSize } = props; + const server = useContext(SharedCacheContext); + const [omezarr, setOmezarr] = useState(null); + const [view, setView] = useState(Box2D.create([0, 0], [1, 1])); + const [planeParam, setPlaneParam] = useState(0.5); + const [dragging, setDragging] = useState(false); + const [renderer, setRenderer] = useState>(); + const [tick, setTick] = useState(0); + const cnvs = useRef(null); + + const load = async (res: WebResource) => { + const fileset = await loadOmeZarrFileset(res, new URL('./fetch.worker.ts', import.meta.url)); + setOmezarr(fileset); + setPlaneParam(0.5); + const level = fileset.getLevel({ index: 0 }); + if (!level) { + throw new Error('level 0 does not exist!'); + } + + const size = level.sizeInUnits(PLANE_XY); + if (size) { + logger.info('size', size); + setView(Box2D.create([0, 0], size)); + } + }; + + // you could put this on the mouse wheel, but for this demo we'll have buttons + const handleScrollSlice = (next: 1 | -1) => { + if (omezarr) { + const step = omezarr.nextSliceStep(PLANE_XY, view, screenSize); + setPlaneParam((prev) => Math.max(0, Math.min(prev + next * (step ?? 1), 1))); + } + }; + + const handleZoom = useCallback( + (e: WheelEvent) => { + e.preventDefault(); + + const zoomScale = e.deltaY > 0 ? 1.1 : 0.9; + const v = zoom(view, screenSize, zoomScale, [e.offsetX, e.offsetY]); + setView(v); + }, + [view, screenSize], + ); + + const handlePan = (e: React.MouseEvent) => { + if (dragging) { + const v = pan(view, screenSize, [e.movementX, e.movementY]); + setView(v); + } + }; + + const handleMouseDown = () => { + setDragging(true); + }; + + const handleMouseUp = () => { + setDragging(false); + }; + useEffect(() => { + if (cnvs.current && server && !renderer) { + const { regl, cache } = server; + const renderer = buildConnectedRenderer(regl, screenSize, cache, defaultPlanarDecoder, () => { + requestAnimationFrame(() => { + setTick(performance.now()); + }); + }); + setRenderer(renderer); + load(props.res); + } + }, [cnvs.current]); + + useEffect(() => { + if (omezarr && cnvs.current && renderer) { + const settings = makeZarrSettings(screenSize, view, planeParam, omezarr); + const ctx = cnvs.current.getContext('2d'); + if (ctx) { + renderer?.render(omezarr, settings); + requestAnimationFrame(() => { + renderer?.copyPixels(ctx); + }); + } + } + }, [omezarr, planeParam, view, tick]); + + useEffect(() => { + if (cnvs?.current) { + cnvs.current.addEventListener('wheel', handleZoom, { passive: false }); + } + return () => { + if (cnvs?.current) { + cnvs.current.removeEventListener('wheel', handleZoom); + } + }; + }, [handleZoom]); + + return ( +
+ +
+ + +
+
+ ); +} diff --git a/site/src/examples/omezarr-v3/omezarr-v3-demo.tsx b/site/src/examples/omezarr-v3/omezarr-v3-demo.tsx new file mode 100644 index 00000000..12badb90 --- /dev/null +++ b/site/src/examples/omezarr-v3/omezarr-v3-demo.tsx @@ -0,0 +1,255 @@ +/////////////////////////////// +/// IGNORE THIS ONE FOR NOW /// +/////////////////////////////// + +// import { Box2D, type Interval, PLANE_XY, type box2D, type vec2 } from '@alleninstitute/vis-geometry'; +// import { +// OmeZarrFileset, +// type OmeZarrMultiscaleSpecifier, +// type PlanarRenderSettings, +// type PlanarRenderSettingsChannels, +// } from '@alleninstitute/vis-omezarr'; +// import { logger, type WebResource } from '@alleninstitute/vis-core'; +// import type React from 'react'; +// import { useEffect, useId, useMemo, useState } from 'react'; +// import { pan, zoom } from '../common/camera'; +// import { RenderServerProvider } from '../common/react/render-server-provider'; +// import { OmeZarrV3Viewer } from './omezarr-v3-viewer'; +// import { OMEZARR_FILESET_OPTIONS } from '../common/filesets/omezarr/demo-filesets'; + +// const screenSize: vec2 = [800, 800]; + +// const defaultInterval: Interval = { min: 0, max: 80 }; + +// function makeRenderSettings( +// omezarr: OmeZarrFileset, +// screenSize: vec2, +// view: box2D, +// param: number, +// ): PlanarRenderSettings { +// const omezarrChannels = omezarr.getColorChannels().reduce((acc, val, index) => { +// acc[val.label ?? `${index}`] = { +// rgb: val.rgb, +// gamut: val.range, +// index, +// }; +// return acc; +// }, {} as PlanarRenderSettingsChannels); + +// const fallbackChannels: PlanarRenderSettingsChannels = { +// R: { rgb: [1.0, 0, 0], gamut: defaultInterval, index: 0 }, +// G: { rgb: [0, 1.0, 0], gamut: defaultInterval, index: 1 }, +// B: { rgb: [0, 0, 1.0], gamut: defaultInterval, index: 2 }, +// }; + +// return { +// camera: { screenSize, view }, +// planeLocation: param, +// plane: PLANE_XY, +// tileSize: 256, +// channels: Object.keys(omezarrChannels).length > 0 ? omezarrChannels : fallbackChannels, +// }; +// } + +// const multiscaleSpec: OmeZarrMultiscaleSpecifier = { index: 0 }; // NOTE: expecting just one multiscale + +// export function OmeZarrV3Demo() { +// const [customUrl, setCustomUrl] = useState(''); +// const [selectedDemoOptionValue, setSelectedDemoOptionValue] = useState(''); +// const [omezarr, setOmezarr] = useState(null); +// const [omezarrJson, setOmezarrJson] = useState(''); +// const [view, setView] = useState(Box2D.create([0, 0], [1, 1])); +// const [planeIndex, setPlaneParam] = useState(0); +// const [dragging, setDragging] = useState(false); + +// const selectId = useId(); +// const textAreaId = useId(); +// const omezarrId = useId(); + +// const settings: PlanarRenderSettings | undefined = useMemo( +// () => (omezarr ? makeRenderSettings(omezarr, screenSize, view, planeIndex) : undefined), +// [omezarr, view, planeIndex], +// ); + +// useEffect(() => { +// setOmezarrJson(JSON.stringify(omezarr, undefined, 4)); +// }, [omezarr]); + +// const load = async (res: WebResource) => { +// const fileset = new OmeZarrFileset(res); +// await fileset.loadMetadata(); +// setOmezarr(fileset); +// const level = fileset.getLevel({ index: 0 }); +// if (!level) { +// throw new Error('level 0 does not exist!'); +// } + +// const size = level.sizeInUnits(PLANE_XY); +// if (size) { +// logger.info('size', size); +// setView(Box2D.create([0, 0], size)); +// } +// }; + +// const handleOptionSelected = (e: React.ChangeEvent) => { +// const selectedValue = e.target.value; +// setOmezarr(null); +// setSelectedDemoOptionValue(selectedValue); +// if (selectedValue && selectedValue !== 'custom') { +// const option = OMEZARR_FILESET_OPTIONS.find((v) => v.value === selectedValue); +// if (option) { +// load(option.res); +// } +// } +// }; + +// const handleCustomUrlLoad = () => { +// const urlRegex = /^(s3|https):\/\/.*/; +// if (!urlRegex.test(customUrl)) { +// logger.error('cannot load resource: invalid URL'); +// return; +// } +// const isS3 = customUrl.slice(0, 5) === 's3://'; +// const resource: WebResource = isS3 +// ? { type: 's3', url: customUrl, region: 'us-west-2' } +// : { type: 'https', url: customUrl }; +// load(resource); +// }; + +// // you could put this on the mouse wheel, but for this demo we'll have buttons +// const handlePlaneIndex = (next: 1 | -1) => { +// if (omezarr) { +// const step = omezarr.nextSliceStep(PLANE_XY, view, screenSize); +// setPlaneParam((prev) => Math.max(0, Math.min(prev + next * (step ?? 1), 1))); +// } +// }; + +// const handleZoom = (e: WheelEvent) => { +// e.preventDefault(); +// const zoomScale = e.deltaY > 0 ? 1.1 : 0.9; +// const v = zoom(view, screenSize, zoomScale, [e.offsetX, e.offsetY]); +// setView(v); +// }; + +// const handlePan = (e: React.MouseEvent) => { +// if (dragging) { +// const v = pan(view, screenSize, [e.movementX, e.movementY]); +// setView(v); +// } +// }; + +// const handleMouseDown = () => { +// setDragging(true); +// }; + +// const handleMouseUp = () => { +// setDragging(false); +// }; + +// return ( +// +//
+//
+//
+// +// +// {selectedDemoOptionValue === 'custom' && ( +//
+// setCustomUrl(e.target.value)} +// style={{ flexGrow: 1 }} +// /> +// +//
+// )} +//
+//
+// {omezarr && settings && ( +// +// )} +//
+//
+// {(omezarr && ( +// +// Slide {Math.floor(planeIndex * (omezarr?.maxOrthogonal(PLANE_XY, multiscaleSpec) ?? 1))} of{' '} +// {omezarr?.maxOrthogonal(PLANE_XY, multiscaleSpec) ?? 0} +// +// )) || No image loaded} +//
+// +// +//
+//
+//
+//
+//
+//
+// +//