diff --git a/@here/olp-sdk-dataservice-read/lib/client/VersionedLayerClient.ts b/@here/olp-sdk-dataservice-read/lib/client/VersionedLayerClient.ts index 433973ba..a60df299 100644 --- a/@here/olp-sdk-dataservice-read/lib/client/VersionedLayerClient.ts +++ b/@here/olp-sdk-dataservice-read/lib/client/VersionedLayerClient.ts @@ -29,7 +29,7 @@ import { } from "@here/olp-sdk-core"; import { BlobApi, MetadataApi, QueryApi } from "@here/olp-sdk-dataservice-api"; import { MetadataCacheRepository } from "../cache/MetadataCacheRepository"; -import { getTile } from "../utils/getTile"; +import { getTile, TileResponse } from "../utils/getTile"; import { DataRequest } from "./DataRequest"; import { PartitionsRequest } from "./PartitionsRequest"; import { QuadKeyPartitionsRequest } from "./QuadKeyPartitionsRequest"; @@ -102,7 +102,65 @@ export class VersionedLayerClient { * * @return Tile data (if it exists) or the nearest parent tile data */ - async getAggregatedData(request: TileRequest, abortSignal?: AbortSignal) { + getAggregatedData( + request: TileRequest, + abortSignal?: AbortSignal + ): Promise; + + /** + * @brief Fetches data of a tile or its closest ancestor. + * Use this API for tile-tree structures where children tile data is aggregated and stored in parent tiles. + * + * @param request The `TileRequest` instance that contains a complete set + * of request parameters. + * @param abortSignal A signal object that allows you to communicate with a request (such as the `fetch` request) + * and, if required, abort it using the `AbortController` object. + * @param options An optional object that can include the `includeTileKey` flag to indicate whether the parent tile key should be included in the response. + * + * For more information, see the [`AbortController` documentation](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). + * + * @return Tile data (if it exists) or the nearest parent tile data including the parent tile key + */ + getAggregatedData( + request: TileRequest, + abortSignal: AbortSignal | undefined, + options: { includeTileKey: true } + ): Promise; + + /** + * @brief Fetches data of a tile or its closest ancestor. + * + * Same as the two-argument `getAggregatedData`, for callers that pass an + * options object with `includeTileKey` disabled or omitted. + * + * @return Tile data (if it exists) or the nearest parent tile data + */ + getAggregatedData( + request: TileRequest, + abortSignal: AbortSignal | undefined, + options: { includeTileKey?: false } + ): Promise; + + /** + * @brief Fetches data of a tile or its closest ancestor. + * + * Same as the two-argument `getAggregatedData`, for an `includeTileKey` + * flag whose value is only known at run time. Narrow the result before + * using it. + * + * @return A `TileResponse` if the flag was `true`, otherwise the tile data + */ + getAggregatedData( + request: TileRequest, + abortSignal: AbortSignal | undefined, + options: { includeTileKey?: boolean } + ): Promise; + + async getAggregatedData( + request: TileRequest, + abortSignal?: AbortSignal, + options?: { includeTileKey?: boolean } + ): Promise { let catalogVersion = this.version; if (catalogVersion === undefined) { @@ -119,7 +177,10 @@ export class VersionedLayerClient { catalogVersion }; - return getTile(request, params, abortSignal); + const tileResponse = await getTile(request, params, abortSignal, { + includeTileKey: true + }); + return options?.includeTileKey ? tileResponse : tileResponse.response; } /** diff --git a/@here/olp-sdk-dataservice-read/lib/client/VolatileLayerClient.ts b/@here/olp-sdk-dataservice-read/lib/client/VolatileLayerClient.ts index 79bbad2e..bc8a6093 100644 --- a/@here/olp-sdk-dataservice-read/lib/client/VolatileLayerClient.ts +++ b/@here/olp-sdk-dataservice-read/lib/client/VolatileLayerClient.ts @@ -98,7 +98,10 @@ export class VolatileLayerClient { * * @return Tile data (if it exists) or the nearest parent tile data */ - async getAggregatedData(request: TileRequest, abortSignal?: AbortSignal) { + async getAggregatedData( + request: TileRequest, + abortSignal?: AbortSignal + ): Promise { const params: TileRequestParams = { catalogHrn: HRN.fromString(this.hrn), layerId: this.layerId, diff --git a/@here/olp-sdk-dataservice-read/lib/utils/getTile.ts b/@here/olp-sdk-dataservice-read/lib/utils/getTile.ts index 4a2ec48c..8950bab5 100644 --- a/@here/olp-sdk-dataservice-read/lib/utils/getTile.ts +++ b/@here/olp-sdk-dataservice-read/lib/utils/getTile.ts @@ -26,7 +26,10 @@ import { ApiName } from "@here/olp-sdk-core"; import { BlobApi } from "@here/olp-sdk-dataservice-api"; -import { Index as QuadTreeIndex } from "@here/olp-sdk-dataservice-api/lib/query-api"; +import { + ParentQuad, + Index as QuadTreeIndex +} from "@here/olp-sdk-dataservice-api/lib/query-api"; import { QuadTreeIndexCacheRepository } from "../cache/QuadTreeIndexCacheRepository"; import { QuadTreeIndexDepth, @@ -44,6 +47,11 @@ export interface GetTileParams { layerType: "versioned" | "volatile"; } +export interface TileResponse { + response: Response; + parentTileKey: TileKey | undefined; +} + /** * Fetches asynchronously data from a tile or from its nearest ancestor if not found. * @@ -78,11 +86,73 @@ export interface GetTileParams { * * @returns The blob of the requested tile or the blob of the closest parent Tile. */ +export function getTile( + request: TileRequest, + params: TileRequestParams, + abortSignal?: AbortSignal +): Promise; + +/** + * Same as the three-argument [[getTile]], but also resolves the tile key of + * the parent whose data was actually returned. + * + * @param options Pass `{ includeTileKey: true }` to receive a [[TileResponse]] + * instead of a plain `Response`. + * + * @returns The blob of the requested tile or the blob of the closest parent + * tile, together with that tile's key. + */ +export function getTile( + request: TileRequest, + params: TileRequestParams, + abortSignal: AbortSignal | undefined, + options: { includeTileKey: true } +): Promise; + +/** + * Same as the three-argument [[getTile]], for callers that pass an options + * object with `includeTileKey` disabled or omitted. + * + * @returns The blob of the requested tile or the blob of the closest parent Tile. + */ +export function getTile( + request: TileRequest, + params: TileRequestParams, + abortSignal: AbortSignal | undefined, + options: { includeTileKey?: false } +): Promise; + +/** + * Same as the three-argument [[getTile]], for an `includeTileKey` flag whose + * value is only known at run time. Narrow the result before using it. + * + * @returns A [[TileResponse]] if the flag was `true`, otherwise the plain blob. + */ +export function getTile( + request: TileRequest, + params: TileRequestParams, + abortSignal: AbortSignal | undefined, + options: { includeTileKey?: boolean } +): Promise; + export async function getTile( + request: TileRequest, + params: TileRequestParams, + abortSignal?: AbortSignal, + options?: { includeTileKey?: boolean } +): Promise { + const tileResponse = await fetchTile(request, params, abortSignal); + return options?.includeTileKey ? tileResponse : tileResponse.response; +} + +/** + * @hidden + */ +async function fetchTile( request: TileRequest, params: TileRequestParams, abortSignal?: AbortSignal -): Promise { +): Promise { let catalogVersion: number | undefined; const quadKey = request.getTileKey(); @@ -105,91 +175,119 @@ export async function getTile( abortSignal ); - const delta = 4; + const depth = 4; const requestedTileKey = TileKey.fromRowColumnLevel( quadKey.row, quadKey.column, quadKey.level ); + const quadTreeIndexRoot = requestedTileKey.changedLevelBy(-depth); - let quadTreeIndex = null; + let quadTreeIndex: QuadTreeIndex | undefined; if (request.getFetchOption() !== FetchOptions.OnlineOnly) { const cache = new QuadTreeIndexCacheRepository(params.settings.cache); - - for (let i = 1; i <= delta; i++) { - const parentInCache = requestedTileKey.changedLevelBy(-i); - const cachedTree = cache.get({ - hrn: params.catalogHrn.toString(), - layerId: params.layerId, - depth: delta, - root: parentInCache, - version: catalogVersion - }); - - if (cachedTree) { - quadTreeIndex = cachedTree; - } - } + quadTreeIndex = cache.get({ + hrn: params.catalogHrn.toString(), + layerId: params.layerId, + depth, + root: quadTreeIndexRoot, + version: catalogVersion + }); } - const parentTileKey = requestedTileKey.changedLevelBy(-delta); - if (!quadTreeIndex) { quadTreeIndex = await fetchQuadTreeIndex({ ...params, catalogVersion, - depth: delta, + depth, fetchOptions: request.getFetchOption(), - tileKey: parentTileKey, + tileKey: quadTreeIndexRoot, abortSignal, billingTag: request.getBillingTag() }); } - if (!quadTreeIndex.subQuads || !quadTreeIndex.subQuads.length) { - return Promise.resolve( - new Response(null, { + const subQuads = quadTreeIndex.subQuads ?? []; + const parentQuads = quadTreeIndex.parentQuads ?? []; + + if (!subQuads.length && !parentQuads.length) { + return Promise.resolve({ + response: new Response(null, { status: 204, statusText: "No Content" - }) + }), + parentTileKey: undefined + }); + } + + const closestParent = findClosestParent( + requestedTileKey, + quadTreeIndex, + quadTreeIndexRoot + ); + if (!closestParent) { + return Promise.reject( + new Error( + `Error getting blob for Tile: ${requestedTileKey.toHereTile()}` + ) ); } - // Return the data for the requested QuadKey or for the closest parent - const subQuads = quadTreeIndex.subQuads; + return { + response: await BlobApi.getBlob(blobRequestBuilder, { + dataHandle: closestParent.parentQuad.dataHandle, + layerId: params.layerId, + billingTag: request.getBillingTag() + }), + parentTileKey: closestParent.tileKey + }; +} - let currentTileKey = requestedTileKey; - let currentDelta = delta; - for ( - let level = currentTileKey.level; - level >= parentTileKey.level; - --level - ) { - const metadata = subQuads.find( - (item) => - item.subQuadKey === currentTileKey.getSubHereTile(currentDelta) +/** + * Helper function to get the closest parent quad for a given tile key. + * @hidden + */ +function findClosestParent( + tileKey: TileKey, + quadTreeIndex: QuadTreeIndex, + quadTreeIndexRoot: TileKey +) { + const subQuads = quadTreeIndex.subQuads ?? []; + const parentQuads = quadTreeIndex.parentQuads ?? []; + + // First, iterate through the sub quad trees, starting from lowest level up to the quad tree index root + let currentTileKey = tileKey; + while (currentTileKey.level >= quadTreeIndexRoot.level) { + const currentDelta = currentTileKey.level - quadTreeIndexRoot.level; + const subHereTile = currentTileKey.getSubHereTile(currentDelta); + const subQuad = subQuads.find( + (item) => item.subQuadKey === subHereTile ); - if (metadata) { - return BlobApi.getBlob(blobRequestBuilder, { - dataHandle: metadata.dataHandle, - layerId: params.layerId, - billingTag: request.getBillingTag() - }); + if (subQuad) { + return { parentQuad: subQuad, tileKey: currentTileKey }; } - try { - currentTileKey = currentTileKey.parent(); - currentDelta = parentTileKey.level - currentTileKey.level; - } catch (error) { - continue; + // Stop before requesting the parent of the quad tree index root - + // that root can be the global root tile (level 0), whose parent() + // throws rather than returning another tile. + if (currentTileKey.level === quadTreeIndexRoot.level) { + break; } + + currentTileKey = currentTileKey.parent(); } - return Promise.reject( - new Error(`Error getting blob for Tile: ${JSON.stringify(quadKey)}`) - ); + // not found? Find the closest parent quad + return parentQuads.reduce< + { parentQuad: ParentQuad; tileKey: TileKey } | undefined + >((closest, parentQuad) => { + const tileKey = TileKey.fromHereTile(parentQuad.partition); + return !closest || tileKey.level > closest.tileKey.level + ? { parentQuad, tileKey } + : closest; + }, undefined); } /** @@ -229,7 +327,8 @@ export async function fetchQuadTreeIndex( if ( params.fetchOptions !== FetchOptions.OnlineOnly && - quadTreeIndex.subQuads + (quadTreeIndex.subQuads !== undefined || + quadTreeIndex.parentQuads !== undefined) ) { cache.put({ hrn: params.catalogHrn.toString(), diff --git a/@here/olp-sdk-dataservice-read/package.json b/@here/olp-sdk-dataservice-read/package.json index b1849373..abd6b9d8 100644 --- a/@here/olp-sdk-dataservice-read/package.json +++ b/@here/olp-sdk-dataservice-read/package.json @@ -1,6 +1,6 @@ { "name": "@here/olp-sdk-dataservice-read", - "version": "3.0.0", + "version": "3.1.0", "description": "Wrapper around a subset of the HERE Open Location Platform Data REST API related to reading data from OLP catalogs", "main": "index.js", "browser": "index.web.js", diff --git a/@here/olp-sdk-dataservice-read/test/unit/VersionedLayerClient.test.ts b/@here/olp-sdk-dataservice-read/test/unit/VersionedLayerClient.test.ts index 8f6dcea5..d7a40e3d 100644 --- a/@here/olp-sdk-dataservice-read/test/unit/VersionedLayerClient.test.ts +++ b/@here/olp-sdk-dataservice-read/test/unit/VersionedLayerClient.test.ts @@ -875,6 +875,90 @@ describe("VersionedLayerClient", function () { await versionedLayerClient.getPartitions(partitionsRequest); expect(partitions).equals(mockedPartitions); }); + + it("Method getAggregatedData should return a plain Response by default", async function () { + // The sub quad key of row 818, column 2021, level 11 within the quad + // tree that is rooted four levels above it. + const requestedSubQuadKey = "281"; + const mockedQuadKeyTreeData: Index = { + subQuads: [ + { + subQuadKey: requestedSubQuadKey, + version: 5, + dataHandle: "mocked-aggregated-data-handle" + } + ], + parentQuads: [] + }; + const mockedBlobData = new Response("mocked-aggregated-blob"); + + getQuadTreeIndexStub.mockImplementation( + (builder: any, params: any): Promise => { + return Promise.resolve(mockedQuadKeyTreeData); + } + ); + getBlobStub.mockImplementation( + (builder: any, params: any): Promise => { + return Promise.resolve(mockedBlobData); + } + ); + + const tileRequest = new dataServiceRead.TileRequest() + .withTileKey({ row: 818, column: 2021, level: 11 }) + .withFetchOption(core.FetchOptions.OnlineOnly); + + const response = + await versionedLayerClient.getAggregatedData(tileRequest); + + assert.instanceOf(response, Response); + expect(response).eqls(mockedBlobData); + assert.isUndefined((response as any).parentTileKey); + }); + + it("Method getAggregatedData should return the parent tile key when includeTileKey is true", async function () { + // The sub quad key of row 818, column 2021, level 11 within the quad + // tree that is rooted four levels above it. + const requestedSubQuadKey = "281"; + const mockedQuadKeyTreeData: Index = { + subQuads: [ + { + subQuadKey: requestedSubQuadKey, + version: 5, + dataHandle: "mocked-aggregated-data-handle" + } + ], + parentQuads: [] + }; + const mockedBlobData = new Response("mocked-aggregated-blob"); + + getQuadTreeIndexStub.mockImplementation( + (builder: any, params: any): Promise => { + return Promise.resolve(mockedQuadKeyTreeData); + } + ); + getBlobStub.mockImplementation( + (builder: any, params: any): Promise => { + return Promise.resolve(mockedBlobData); + } + ); + + const requestedTileKey = core.TileKey.fromRowColumnLevel(818, 2021, 11); + const tileRequest = new dataServiceRead.TileRequest() + .withTileKey(requestedTileKey) + .withFetchOption(core.FetchOptions.OnlineOnly); + + const response = await versionedLayerClient.getAggregatedData( + tileRequest, + undefined, + { includeTileKey: true } + ); + + expect(response.response).eqls(mockedBlobData); + expect(response.parentTileKey?.equals(requestedTileKey)).toBe(true); + expect(getBlobStub.mock.calls[0][1].dataHandle).eqls( + "mocked-aggregated-data-handle" + ); + }); }); describe("VersionedLayerClient with locked version 0 in constructor", function () { diff --git a/@here/olp-sdk-dataservice-read/test/unit/getTile.test.ts b/@here/olp-sdk-dataservice-read/test/unit/getTile.test.ts index c1582c24..14d71191 100644 --- a/@here/olp-sdk-dataservice-read/test/unit/getTile.test.ts +++ b/@here/olp-sdk-dataservice-read/test/unit/getTile.test.ts @@ -30,6 +30,7 @@ import { } from "vitest"; import { createStubInstance } from "./stub-instance"; import { TileRequest, getTile } from "@here/olp-sdk-dataservice-read/lib"; +import { QuadTreeIndexCacheRepository } from "../../lib/cache/QuadTreeIndexCacheRepository"; import * as core from "@here/olp-sdk-core"; import { BlobApi, QueryApi } from "@here/olp-sdk-dataservice-api"; import { FetchOptions } from "@here/olp-sdk-core"; @@ -93,6 +94,38 @@ describe("getTile", function () { expect(response.statusText).eqls("No Content"); }); + it("Should return a 204 TileResponse when includeTileKey is true", async function () { + const mockedQuadKeyTreeData = { + subQuads: [], + parentQuads: [] + }; + + quadTreeIndexStub.mockImplementation( + (builder: any, params: any): Promise => { + return Promise.resolve(mockedQuadKeyTreeData); + } + ); + + const response = await getTile( + new TileRequest() + .withTileKey({ row: 0, column: 0, level: 0 }) + .withFetchOption(FetchOptions.OnlineOnly), + { + settings: olpClientSettingsStub as any, + catalogHrn: core.HRN.fromString("hrn:here:data:::mocked-hrn"), + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }, + undefined, + { includeTileKey: true } + ); + + expect(response.response.status).eqls(204); + expect(response.response.statusText).eqls("No Content"); + assert.isUndefined(response.parentTileKey); + }); + it("Should return the blob of the requested tile", async function () { // The sub quad key of row 818, column 2021, level 11 within the quad // tree that is rooted four levels above it. @@ -135,6 +168,211 @@ describe("getTile", function () { ); }); + it("Should return a plain Response when includeTileKey is false or a run-time flag", async function () { + const mockedQuadKeyTreeData = { + subQuads: [ + { + subQuadKey: "281", + version: 309, + dataHandle: "mocked-data-handle" + } + ], + parentQuads: [] + }; + const mockedBlob = new Response("mocked-blob"); + + quadTreeIndexStub.mockImplementation((): Promise => + Promise.resolve(mockedQuadKeyTreeData) + ); + vi.spyOn(BlobApi, "getBlob").mockReturnValue( + Promise.resolve(mockedBlob) + ); + + const params = { + settings: olpClientSettingsStub as any, + catalogHrn: core.HRN.fromString("hrn:here:data:::mocked-hrn"), + layerId: "mocked-layer-id", + layerType: "versioned" as const, + catalogVersion: 123 + }; + const buildRequest = () => + new TileRequest() + .withTileKey({ row: 818, column: 2021, level: 11 }) + .withFetchOption(FetchOptions.OnlineOnly); + + const explicitlyFalse = await getTile( + buildRequest(), + params, + undefined, + { + includeTileKey: false + } + ); + expect(explicitlyFalse).eqls(mockedBlob); + + const omitted = await getTile(buildRequest(), params, undefined, {}); + expect(omitted).eqls(mockedBlob); + + // A flag only known at run time resolves to the union overload, so the + // caller has to narrow it. + const runtimeFlag: boolean = false; + const dynamic = await getTile(buildRequest(), params, undefined, { + includeTileKey: runtimeFlag + }); + assert.instanceOf(dynamic, Response); + }); + + it("Should resolve the requested tile's own key when includeTileKey is true", async function () { + // The sub quad key of row 818, column 2021, level 11 within the quad + // tree that is rooted four levels above it. + const requestedSubQuadKey = "281"; + const mockedQuadKeyTreeData = { + subQuads: [ + { + subQuadKey: requestedSubQuadKey, + version: 309, + dataHandle: "mocked-data-handle" + } + ], + parentQuads: [] + }; + const mockedBlob = new Response("mocked-blob"); + + quadTreeIndexStub.mockImplementation((): Promise => + Promise.resolve(mockedQuadKeyTreeData) + ); + const getBlobStub = vi + .spyOn(BlobApi, "getBlob") + .mockReturnValue(Promise.resolve(mockedBlob)); + + const response = await getTile( + new TileRequest() + .withTileKey({ row: 818, column: 2021, level: 11 }) + .withFetchOption(FetchOptions.OnlineOnly), + { + settings: olpClientSettingsStub as any, + catalogHrn: core.HRN.fromString("hrn:here:data:::mocked-hrn"), + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }, + undefined, + { includeTileKey: true } + ); + + expect(response.response).eqls(mockedBlob); + expect( + response.parentTileKey?.equals( + core.TileKey.fromRowColumnLevel(818, 2021, 11) + ) + ).toBe(true); + expect(getBlobStub.mock.calls[0][1].dataHandle).eqls( + "mocked-data-handle" + ); + }); + + it("Should return the blob of the closest parent tile from sub quads", async function () { + const mockedQuadKeyTreeData = { + subQuads: [ + { + subQuadKey: "17", + version: 309, + dataHandle: "farther-parent-data-handle" + }, + { + subQuadKey: "70", + version: 309, + dataHandle: "closest-parent-data-handle" + } + ], + parentQuads: [] + }; + const mockedBlob = new Response("mocked-parent-blob"); + + quadTreeIndexStub.mockImplementation((): Promise => + Promise.resolve(mockedQuadKeyTreeData) + ); + const getBlobStub = vi + .spyOn(BlobApi, "getBlob") + .mockReturnValue(Promise.resolve(mockedBlob)); + + const response = await getTile( + new TileRequest() + .withTileKey({ row: 818, column: 2021, level: 11 }) + .withFetchOption(FetchOptions.OnlineOnly), + { + settings: olpClientSettingsStub as any, + catalogHrn: core.HRN.fromString("hrn:here:data:::mocked-hrn"), + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }, + undefined, + { includeTileKey: true } + ); + + expect(response.response).eqls(mockedBlob); + expect( + response.parentTileKey?.equals( + core.TileKey.fromRowColumnLevel(409, 1010, 10) + ) + ).toBe(true); + expect(getBlobStub.mock.calls[0][1].dataHandle).eqls( + "closest-parent-data-handle" + ); + }); + + it("Should return the blob of the closest parent tile from parent quads", async function () { + const mockedQuadKeyTreeData = { + subQuads: [], + parentQuads: [ + { + partition: "1525", + version: 309, + dataHandle: "farther-parent-data-handle" + }, + { + partition: "6103", + version: 309, + dataHandle: "closest-parent-data-handle" + } + ] + }; + const mockedBlob = new Response("mocked-parent-blob"); + + quadTreeIndexStub.mockImplementation((): Promise => + Promise.resolve(mockedQuadKeyTreeData) + ); + const getBlobStub = vi + .spyOn(BlobApi, "getBlob") + .mockReturnValue(Promise.resolve(mockedBlob)); + + const response = await getTile( + new TileRequest() + .withTileKey({ row: 818, column: 2021, level: 11 }) + .withFetchOption(FetchOptions.OnlineOnly), + { + settings: olpClientSettingsStub as any, + catalogHrn: core.HRN.fromString("hrn:here:data:::mocked-hrn"), + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }, + undefined, + { includeTileKey: true } + ); + + expect(response.response).eqls(mockedBlob); + expect( + response.parentTileKey?.equals( + core.TileKey.fromRowColumnLevel(25, 63, 6) + ) + ).toBe(true); + expect(getBlobStub.mock.calls[0][1].dataHandle).eqls( + "closest-parent-data-handle" + ); + }); + it("Should reject if no quad of the tree covers the tile", async function () { const mockedQuadKeyTreeData = { subQuads: [ @@ -168,12 +406,286 @@ describe("getTile", function () { catalogVersion: 123 } ) - ).rejects.toThrow( - `Error getting blob for Tile: ${JSON.stringify({ - row: 818, - column: 2021, - level: 11 - })}` + ).rejects.toThrow("Error getting blob for Tile: 6250009"); + }); + + it("Should return the blob of the closest parent tile when the requested tile is at or below the quad tree index depth", async function () { + // The requested tile is at level 2, which is not deeper than the + // quad tree index depth (4). This clamps the root of the quad tree + // index to the global root tile (level 0), so walking up from the + // requested tile reaches level 0 without ever leaving the tree. + const mockedQuadKeyTreeData = { + subQuads: [], + parentQuads: [ + { + partition: "1", + version: 309, + dataHandle: "root-data-handle" + } + ] + }; + const mockedBlob = new Response("mocked-parent-blob"); + + quadTreeIndexStub.mockImplementation((): Promise => + Promise.resolve(mockedQuadKeyTreeData) + ); + const getBlobStub = vi + .spyOn(BlobApi, "getBlob") + .mockReturnValue(Promise.resolve(mockedBlob)); + + const response = await getTile( + new TileRequest() + .withTileKey({ row: 1, column: 1, level: 2 }) + .withFetchOption(FetchOptions.OnlineOnly), + { + settings: olpClientSettingsStub as any, + catalogHrn: core.HRN.fromString("hrn:here:data:::mocked-hrn"), + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }, + undefined, + { includeTileKey: true } + ); + + expect(response.response).eqls(mockedBlob); + expect( + response.parentTileKey?.equals( + core.TileKey.fromRowColumnLevel(0, 0, 0) + ) + ).toBe(true); + expect(getBlobStub.mock.calls[0][1].dataHandle).eqls( + "root-data-handle" + ); + }); + + it("Should keep the already-found deepest parent quad", async function () { + const mockedQuadKeyTreeData = { + subQuads: [], + parentQuads: [ + { + partition: "6103", + version: 309, + dataHandle: "closest-parent-data-handle" + }, + { + partition: "1525", + version: 309, + dataHandle: "should-be-ignored-data-handle" + } + ] + }; + const mockedBlob = new Response("mocked-parent-blob"); + + quadTreeIndexStub.mockImplementation((): Promise => + Promise.resolve(mockedQuadKeyTreeData) + ); + const getBlobStub = vi + .spyOn(BlobApi, "getBlob") + .mockReturnValue(Promise.resolve(mockedBlob)); + + const response = await getTile( + new TileRequest() + .withTileKey({ row: 818, column: 2021, level: 11 }) + .withFetchOption(FetchOptions.OnlineOnly), + { + settings: olpClientSettingsStub as any, + catalogHrn: core.HRN.fromString("hrn:here:data:::mocked-hrn"), + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }, + undefined, + { includeTileKey: true } + ); + + expect(response.response).eqls(mockedBlob); + expect( + response.parentTileKey?.equals( + core.TileKey.fromRowColumnLevel(25, 63, 6) + ) + ).toBe(true); + expect(getBlobStub.mock.calls[0][1].dataHandle).eqls( + "closest-parent-data-handle" + ); + }); + + it("Should use the cached quad tree index instead of fetching it", async function () { + const settings = new core.OlpClientSettings({ + environment: "here", + getToken: () => Promise.resolve("mocked-token") + }); + const requestedTileKey = core.TileKey.fromRowColumnLevel(818, 2021, 11); + + new QuadTreeIndexCacheRepository(settings.cache).put({ + hrn: "hrn:here:data:::mocked-hrn", + layerId: "mocked-layer-id", + depth: 4, + root: requestedTileKey.changedLevelBy(-4), + version: 123, + tree: { + subQuads: [ + { + subQuadKey: "281", + version: 309, + dataHandle: "cached-data-handle" + } + ], + parentQuads: [] + } + }); + + const mockedBlob = new Response("cached-blob"); + const getBlobStub = vi + .spyOn(BlobApi, "getBlob") + .mockReturnValue(Promise.resolve(mockedBlob)); + + const response = await getTile( + new TileRequest().withTileKey(requestedTileKey), + { + settings, + catalogHrn: core.HRN.fromString("hrn:here:data:::mocked-hrn"), + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }, + undefined, + { includeTileKey: true } + ); + + expect(quadTreeIndexStub).not.toHaveBeenCalled(); + expect(response.response).eqls(mockedBlob); + expect(response.parentTileKey?.equals(requestedTileKey)).toBe(true); + expect(getBlobStub.mock.calls[0][1].dataHandle).eqls( + "cached-data-handle" + ); + }); + + it("Should cache a quad tree index that has only parent quads", async function () { + const settings = new core.OlpClientSettings({ + environment: "here", + getToken: () => Promise.resolve("mocked-token") + }); + const mockedQuadKeyTreeData = { + parentQuads: [ + { + partition: "6103", + version: 309, + dataHandle: "closest-parent-data-handle" + } + ] + }; + + quadTreeIndexStub.mockImplementation((): Promise => + Promise.resolve(mockedQuadKeyTreeData) + ); + vi.spyOn(BlobApi, "getBlob").mockReturnValue( + Promise.resolve(new Response("mocked-parent-blob")) + ); + + const params = { + settings, + catalogHrn: core.HRN.fromString("hrn:here:data:::mocked-hrn"), + layerId: "mocked-layer-id", + layerType: "versioned" as const, + catalogVersion: 123 + }; + const tileKey = { row: 818, column: 2021, level: 11 }; + + await getTile(new TileRequest().withTileKey(tileKey), params); + await getTile(new TileRequest().withTileKey(tileKey), params); + + expect(quadTreeIndexStub).toHaveBeenCalledTimes(1); + }); + + it("Should treat a missing subQuads field as an empty list", async function () { + const mockedQuadKeyTreeData = { + parentQuads: [ + { + partition: "6103", + version: 309, + dataHandle: "closest-parent-data-handle" + } + ] + }; + const mockedBlob = new Response("mocked-parent-blob"); + + quadTreeIndexStub.mockImplementation((): Promise => + Promise.resolve(mockedQuadKeyTreeData) + ); + const getBlobStub = vi + .spyOn(BlobApi, "getBlob") + .mockReturnValue(Promise.resolve(mockedBlob)); + + const response = await getTile( + new TileRequest() + .withTileKey({ row: 818, column: 2021, level: 11 }) + .withFetchOption(FetchOptions.OnlineOnly), + { + settings: olpClientSettingsStub as any, + catalogHrn: core.HRN.fromString("hrn:here:data:::mocked-hrn"), + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }, + undefined, + { includeTileKey: true } + ); + + expect(response.response).eqls(mockedBlob); + expect( + response.parentTileKey?.equals( + core.TileKey.fromRowColumnLevel(25, 63, 6) + ) + ).toBe(true); + expect(getBlobStub.mock.calls[0][1].dataHandle).eqls( + "closest-parent-data-handle" + ); + }); + + it("Should treat a missing parentQuads field as an empty list", async function () { + const requestedSubQuadKey = "281"; + const mockedQuadKeyTreeData = { + subQuads: [ + { + subQuadKey: requestedSubQuadKey, + version: 309, + dataHandle: "mocked-data-handle" + } + ] + }; + const mockedBlob = new Response("mocked-blob"); + + quadTreeIndexStub.mockImplementation((): Promise => + Promise.resolve(mockedQuadKeyTreeData) + ); + const getBlobStub = vi + .spyOn(BlobApi, "getBlob") + .mockReturnValue(Promise.resolve(mockedBlob)); + + const response = await getTile( + new TileRequest() + .withTileKey({ row: 818, column: 2021, level: 11 }) + .withFetchOption(FetchOptions.OnlineOnly), + { + settings: olpClientSettingsStub as any, + catalogHrn: core.HRN.fromString("hrn:here:data:::mocked-hrn"), + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }, + undefined, + { includeTileKey: true } + ); + + expect(response.response).eqls(mockedBlob); + expect( + response.parentTileKey?.equals( + core.TileKey.fromRowColumnLevel(818, 2021, 11) + ) + ).toBe(true); + expect(getBlobStub.mock.calls[0][1].dataHandle).eqls( + "mocked-data-handle" ); }); diff --git a/CHANGELOG.md b/CHANGELOG.md index b321b113..e61754cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## v3.1.0 (22/09/2026) + +**olp-sdk-dataservice-read** + +- Added the `includeTileKey` option to `getTile()` and `VersionedLayerClient::getAggregatedData()`. Pass `{ includeTileKey: true }` to receive a `TileResponse` that carries the key of the tile whose data was actually returned, instead of a plain `Response`. +- **Behavior change:** `getTile()` now falls back to the `parentQuads` of the quad tree index when the requested tile has no data in its own sub tree. Previously such a tile resolved a `204 No Content` response; it now resolves the data of the closest ancestor. This affects both `VersionedLayerClient::getAggregatedData()` and `VolatileLayerClient::getAggregatedData()`. Callers that branch on `response.status === 204` to detect missing data should be reviewed. +- **Behavior change:** the rejection message of `getTile()` now identifies the tile by its HERE tile key (`Error getting blob for Tile: 6250009`) instead of a serialized quad key (`Error getting blob for Tile: {"row":818,"column":2021,"level":11}`). +- Fixed a crash in the closest-parent lookup when the quad tree index root is the global root tile (level 0). + ## v3.0.0 (2026) - Discontinued support of Node.js versions less than 24.x. diff --git a/package-lock.json b/package-lock.json index f6607ef9..cf68fb87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@types/node": "^24.0.0", - "@vitest/coverage-v8": "^4.1.10", + "@vitest/coverage-v8": "^4.1.11", "eslint": "^10.8.0", "eslint-config-prettier": "^10.1.8", "fetch-mock": "^12.6.0", @@ -27,7 +27,7 @@ "typedoc": "^0.28.20", "typescript": "~5.8.3", "typescript-eslint": "^8.65.0", - "vitest": "^4.1.10", + "vitest": "^4.1.11", "webpack": "^5.109.1", "webpack-cli": "^7.2.1" }, @@ -68,7 +68,7 @@ } }, "@here/olp-sdk-dataservice-read": { - "version": "3.0.0", + "version": "3.1.0", "license": "Apache-2.0", "dependencies": { "@here/olp-sdk-core": "^3.0.0", @@ -168,40 +168,6 @@ "node": ">=14.17.0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -484,33 +450,14 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", + "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", "dev": true, "license": "MIT", "funding": { - "url": "https://github.com/sponsors/Boshen" + "url": "https://github.com/sponsors/oxc-project" } }, "node_modules/@puppeteer/browsers": { @@ -542,10 +489,27 @@ } } }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", + "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz", + "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==", "cpu": [ "arm64" ], @@ -560,9 +524,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz", + "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==", "cpu": [ "arm64" ], @@ -577,9 +541,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz", + "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==", "cpu": [ "x64" ], @@ -594,9 +558,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz", + "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==", "cpu": [ "x64" ], @@ -611,9 +575,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz", + "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==", "cpu": [ "arm" ], @@ -628,9 +592,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz", + "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==", "cpu": [ "arm64" ], @@ -648,9 +612,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz", + "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==", "cpu": [ "arm64" ], @@ -668,9 +632,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz", + "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==", "cpu": [ "ppc64" ], @@ -688,9 +652,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz", + "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==", "cpu": [ "s390x" ], @@ -708,9 +672,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz", + "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==", "cpu": [ "x64" ], @@ -728,9 +692,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz", + "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==", "cpu": [ "x64" ], @@ -748,9 +712,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz", + "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==", "cpu": [ "arm64" ], @@ -764,29 +728,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz", + "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==", "cpu": [ "arm64" ], @@ -801,9 +746,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz", + "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==", "cpu": [ "x64" ], @@ -880,17 +825,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1208,14 +1142,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", - "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.10", + "@vitest/utils": "4.1.11", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -1229,8 +1163,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.10", - "vitest": "4.1.10" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1239,16 +1173,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -1257,13 +1191,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.10", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1284,9 +1218,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -1297,13 +1231,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.10", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -1311,14 +1245,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1327,9 +1261,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -1337,13 +1271,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1691,9 +1625,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2376,9 +2310,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", - "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -3491,9 +3425,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -3553,9 +3487,9 @@ } }, "node_modules/obug": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", - "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -3685,9 +3619,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -3774,9 +3708,9 @@ "license": "MIT" }, "node_modules/postcss": { - "version": "8.5.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", - "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -3794,7 +3728,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3980,13 +3914,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", + "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.139.0", + "@oxc-project/types": "=0.150.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3996,21 +3930,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" + "@rolldown/binding-android-arm-eabi": "1.2.9", + "@rolldown/binding-android-arm64": "1.2.9", + "@rolldown/binding-darwin-arm64": "1.2.9", + "@rolldown/binding-darwin-x64": "1.2.9", + "@rolldown/binding-freebsd-x64": "1.2.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", + "@rolldown/binding-linux-arm64-gnu": "1.2.9", + "@rolldown/binding-linux-arm64-musl": "1.2.9", + "@rolldown/binding-linux-ppc64-gnu": "1.2.9", + "@rolldown/binding-linux-s390x-gnu": "1.2.9", + "@rolldown/binding-linux-x64-gnu": "1.2.9", + "@rolldown/binding-linux-x64-musl": "1.2.9", + "@rolldown/binding-openharmony-arm64": "1.2.9", + "@rolldown/binding-win32-arm64-msvc": "1.2.9", + "@rolldown/binding-win32-x64-msvc": "1.2.9" } }, "node_modules/schema-utils": { @@ -4320,9 +4254,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -4342,14 +4276,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", @@ -4519,16 +4445,16 @@ } }, "node_modules/vite": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "bin": { @@ -4545,7 +4471,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", + "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -4597,19 +4523,19 @@ } }, "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -4637,12 +4563,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index fa143e1e..851c910d 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "http-server-testing-bundles": "node scripts/static-server.js tests/integration/bundles/umd/static 8080", "test-published-bundles": "vitest run --config ./vitest.bundles-published.config.ts", "test-generated-bundles": "vitest run --config ./vitest.bundles-generated.config.ts", - "api-breaks-test": "vitest run --config ./vitest.api-breaks.config.ts", + "api-breaks-test": "tsc --noEmit -p tsconfig.json && vitest run --config ./vitest.api-breaks.config.ts", "integration-test": "vitest run --config ./vitest.integration.config.ts", "functional-test": "vitest run --config ./vitest.functional.config.ts", "coverage": "npm run coverage --workspaces --if-present", @@ -52,7 +52,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@types/node": "^24.0.0", - "@vitest/coverage-v8": "^4.1.10", + "@vitest/coverage-v8": "^4.1.11", "eslint": "^10.8.0", "eslint-config-prettier": "^10.1.8", "fetch-mock": "^12.6.0", @@ -65,7 +65,7 @@ "typedoc": "^0.28.20", "typescript": "~5.8.3", "typescript-eslint": "^8.65.0", - "vitest": "^4.1.10", + "vitest": "^4.1.11", "webpack": "^5.109.1", "webpack-cli": "^7.2.1" }, diff --git a/scripts/linux/psv/build_test_psv.sh b/scripts/linux/psv/build_test_psv.sh index e00cea3a..5d49e969 100755 --- a/scripts/linux/psv/build_test_psv.sh +++ b/scripts/linux/psv/build_test_psv.sh @@ -40,6 +40,9 @@ npm run test # Integration tests npm run integration-test +# Public API surface guards (type-checks tests/ before running) +npm run api-breaks-test + # Functional tests npm run functional-test diff --git a/tests/integration/api-breaks/VersionedLayerClient.test.ts b/tests/integration/api-breaks/VersionedLayerClient.test.ts index d7d487f0..3a19edef 100644 --- a/tests/integration/api-breaks/VersionedLayerClient.test.ts +++ b/tests/integration/api-breaks/VersionedLayerClient.test.ts @@ -33,7 +33,9 @@ import { VersionedLayerClientParams, QuadKeyPartitionsRequest, DataRequest, - PartitionsRequest + PartitionsRequest, + TileRequest, + TileResponse } from "@here/olp-sdk-dataservice-read"; import { QueryApi, MetadataApi } from "@here/olp-sdk-dataservice-api"; import { HRN, OlpClientSettings } from "@here/olp-sdk-core"; @@ -97,6 +99,34 @@ describe("VersionedLayerClient", function () { > { return Promise.resolve({}); } + + getAggregatedData( + request: TileRequest, + abortSignal?: AbortSignal + ): Promise; + getAggregatedData( + request: TileRequest, + abortSignal: AbortSignal | undefined, + options: { includeTileKey: true } + ): Promise; + getAggregatedData( + request: TileRequest, + abortSignal: AbortSignal | undefined, + options: { includeTileKey?: false } + ): Promise; + getAggregatedData( + request: TileRequest, + abortSignal: AbortSignal | undefined, + options: { includeTileKey?: boolean } + ): Promise; + + async getAggregatedData( + request: TileRequest, + abortSignal?: AbortSignal, + options?: { includeTileKey?: boolean } + ): Promise { + return Promise.resolve(new Response()); + } } beforeEach(function () { @@ -118,6 +148,7 @@ describe("VersionedLayerClient", function () { assert.isFunction(layerClient.getData); assert.isFunction(layerClient.getPartitions); + assert.isFunction(layerClient.getAggregatedData); }); it("Shoud be initialized with VersionedLayerClientParams", async function () { @@ -134,6 +165,7 @@ describe("VersionedLayerClient", function () { assert.isFunction(layerClient.getData); assert.isFunction(layerClient.getPartitions); + assert.isFunction(layerClient.getAggregatedData); }); it("getPartitions method with QuadKeyPartitionsRequest", async function () { diff --git a/tests/integration/api-breaks/getTile.test.ts b/tests/integration/api-breaks/getTile.test.ts new file mode 100644 index 00000000..39a7ab05 --- /dev/null +++ b/tests/integration/api-breaks/getTile.test.ts @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import { + afterEach, + assert, + beforeEach, + describe, + expect, + it, + vi +} from "vitest"; +import { + getTile, + TileRequest, + TileResponse +} from "@here/olp-sdk-dataservice-read"; +import { BlobApi, QueryApi } from "@here/olp-sdk-dataservice-api"; +import { + FetchOptions, + HRN, + OlpClientSettings, + RequestFactory, + TileKey +} from "@here/olp-sdk-core"; + +// getTile() is re-exported from the package root, so it is public API even +// though it is mostly used internally by VersionedLayerClient and +// VolatileLayerClient. These tests guard its call signature and return +// shape against an accidental breaking change, such as the three-argument +// call silently starting to resolve something other than a plain Response. +describe("getTile", function () { + const settings = new OlpClientSettings({ + environment: "here", + getToken: () => Promise.resolve("mocked-token") + }); + const catalogHrn = HRN.fromString("hrn:here:data:::mocked-hrn"); + // The sub quad key of row 818, column 2021, level 11 within the quad + // tree that is rooted four levels above it. + const requestedSubQuadKey = "281"; + + beforeEach(function () { + vi.spyOn(RequestFactory, "getBaseUrl").mockImplementation(() => + Promise.resolve("http://fake-base.url") + ); + vi.spyOn(QueryApi, "quadTreeIndex").mockImplementation( + (): Promise => + Promise.resolve({ + subQuads: [ + { + subQuadKey: requestedSubQuadKey, + version: 1, + dataHandle: "mocked-data-handle" + } + ], + parentQuads: [] + }) + ); + vi.spyOn(BlobApi, "getBlob").mockReturnValue( + Promise.resolve(new Response("mocked-blob")) + ); + }); + + afterEach(function () { + vi.restoreAllMocks(); + }); + + function tileRequest(): TileRequest { + return new TileRequest() + .withTileKey({ row: 818, column: 2021, level: 11 }) + .withFetchOption(FetchOptions.OnlineOnly); + } + + it("Should resolve a plain Response with the three-argument call", async function () { + const response = await getTile(tileRequest(), { + settings, + catalogHrn, + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }); + + assert.instanceOf(response, Response); + assert.isUndefined((response as any).parentTileKey); + expect((await response.blob()).size).to.be.greaterThan(0); + }); + + it("Should still accept an abortSignal as the third argument", async function () { + const abortController = new AbortController(); + + const response = await getTile( + tileRequest(), + { + settings, + catalogHrn, + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }, + abortController.signal + ); + + assert.instanceOf(response, Response); + }); + + it("Should resolve a TileResponse when includeTileKey is true", async function () { + const response: TileResponse = await getTile( + tileRequest(), + { + settings, + catalogHrn, + layerId: "mocked-layer-id", + layerType: "versioned", + catalogVersion: 123 + }, + undefined, + { includeTileKey: true } + ); + + assert.instanceOf(response.response, Response); + expect( + response.parentTileKey?.equals( + TileKey.fromRowColumnLevel(818, 2021, 11) + ) + ).to.be.equal(true); + }); +});