diff --git a/packages/core/components/FileRow/Cell.tsx b/packages/core/components/FileRow/Cell.tsx index 3fec1b6b6..963422813 100644 --- a/packages/core/components/FileRow/Cell.tsx +++ b/packages/core/components/FileRow/Cell.tsx @@ -5,6 +5,7 @@ import interact from "interactjs"; import * as React from "react"; import Tooltip from "../Tooltip"; +import { MINIMUM_COLUMN_WIDTH } from "../../entity/SearchParams"; import styles from "./Cell.module.css"; @@ -38,8 +39,6 @@ enum ResizeDirection { * This component uses pixel-based widths for columns. */ export default class Cell extends React.Component, CellState> { - public static MINIMUM_WIDTH = 50; // px; somewhat arbitrary - public state: CellState = { resizeTargetClassName: styles.cursorResizeEitherDirection, }; @@ -101,7 +100,7 @@ export default class Cell extends React.Component
@@ -125,7 +124,7 @@ export default class Cell extends React.Component @@ -176,7 +175,7 @@ export default class Cell extends React.Component result.length > 0).map((result) => result[0].column_name); } + /** + * Fetch the length of the longest value for each annotation, which can be used to compute optimal column widths in the UI. + * This is a bit of a hack, but it allows us to avoid fetching all values for an annotation just to compute column widths. + */ + public async fetchOptimalWidthForAnnotations( + annotationNames: string[], + ignoreWidthLimit = false + ): Promise> { + // Try to fetch values for new annotations to compute optimal column widths + const widthByAnnotation: Record = {}; + try { + const fetchQuery = this.fetchLengthiestValues(annotationNames); + // Set a timeout on this query in case it takes too long to return, + // since it could potentially be slow for annotations with very long values + // which is fine and we will just cancel it and fall back to default column widths in that case + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => { + fetchQuery.cancel?.(); + reject(new Error("Timeout fetching annotation values")); + }, 1000) + ); + const annotationToLength = await Promise.race([fetchQuery.promise, timeoutPromise]); + for (const [annotation, length] of Object.entries(annotationToLength)) { + // Grab whichever is longer, the longest value or the header + // to compute the column width needed to fit this column without truncation + const maxLengthOfColumn = Math.max(length, annotation.length); + // Convert this length to a pixel width using our sample character width + // + some extra pixels for padding + const maxLengthOfColumnInPx = + Math.ceil(maxLengthOfColumn * this.sampleCharWidthInPx) + 8; + // Avoid letting width get too large for extremely long values by capping it + const minOptimalWidth = ignoreWidthLimit + ? maxLengthOfColumnInPx + : Math.min(maxLengthOfColumnInPx, DEFAULT_COLUMN_WIDTH * 3); + // Avoid letting width get too small by setting a minimum width + // like in the case of canvas measurement failing + const width = Math.max(minOptimalWidth, MINIMUM_COLUMN_WIDTH); + widthByAnnotation[annotation] = width; + } + } catch { + // If fetching values fails entirely, fall through to default widths + } + for (const annotationName of annotationNames) { + if (!widthByAnnotation.hasOwnProperty(annotationName)) { + widthByAnnotation[annotationName] = DEFAULT_COLUMN_WIDTH; + } + } + for (const annotation of TOP_LEVEL_FILE_ANNOTATIONS) { + if (!widthByAnnotation.hasOwnProperty(annotation.name)) { + widthByAnnotation[annotation.name] = DEFAULT_COLUMN_WIDTH; + } + } + return widthByAnnotation; + } + + /** + * Fetch the length of the longest value for each annotation, which can be used to compute optimal column widths in the UI. + * This is a bit of a hack, but it allows us to avoid fetching all values for an annotation just to compute column widths. + */ + private fetchLengthiestValues( + annotationNames: string[] + ): CancellablePromise<{ [annotation: string]: number }> { + if (!this.dataSourceNames.length || annotationNames.length === 0) { + return { promise: Promise.resolve({}) }; + } + + const aggregateDataSourceName = this.dataSourceNames.sort().join(", "); + const sql = new SQLBuilder() + .select( + annotationNames + .map( + (annotation) => + `MAX(LENGTH(CAST("${annotation}" AS VARCHAR))) AS "${annotation}"` + ) + .join(", ") + ) + .from(aggregateDataSourceName) + .toSQL(); + + const query = this.databaseService.query<{ [annotation: string]: number }>(sql); + return { + promise: query.promise.then((results): { [annotation: string]: number } => { + const annotationToLength: { [annotation: string]: number } = {}; + for (const row of results) { + for (const [annotation, length] of Object.entries(row)) { + annotationToLength[annotation] = length; + } + } + return annotationToLength; + }), + cancel: query.cancel, + }; + } + /** * Validate annotation values according the type the annotation they belong to. */ diff --git a/packages/core/services/AnnotationService/DatabaseAnnotationService/test/DatabaseAnnotationService.test.ts b/packages/core/services/AnnotationService/DatabaseAnnotationService/test/DatabaseAnnotationService.test.ts index b5bce73bc..7678157fe 100644 --- a/packages/core/services/AnnotationService/DatabaseAnnotationService/test/DatabaseAnnotationService.test.ts +++ b/packages/core/services/AnnotationService/DatabaseAnnotationService/test/DatabaseAnnotationService.test.ts @@ -1,9 +1,11 @@ import { expect } from "chai"; import sinon from "sinon"; -import FileFilter, { FilterType } from "../../../../entity/FileFilter"; import DatabaseService from "../../../DatabaseService"; import DatabaseServiceNoop from "../../../DatabaseService/DatabaseServiceNoop"; +import { TOP_LEVEL_FILE_ANNOTATIONS } from "../../../../constants"; +import FileFilter, { FilterType } from "../../../../entity/FileFilter"; +import { DEFAULT_COLUMN_WIDTH, MINIMUM_COLUMN_WIDTH } from "../../../../entity/SearchParams"; import SQLBuilder from "../../../../entity/SQLBuilder"; import DatabaseAnnotationService from ".."; @@ -14,7 +16,7 @@ describe("DatabaseAnnotationService", () => { select_key: name.toLowerCase() + index, })); class MockDatabaseService extends DatabaseServiceNoop { - public query(): { promise: Promise<{ [key: string]: string }[]> } { + public query(): { promise: Promise } { return { promise: Promise.resolve(annotations) }; } } @@ -38,7 +40,7 @@ describe("DatabaseAnnotationService", () => { column_type: "VARCHAR", })); class MockDatabaseService extends DatabaseServiceNoop { - public query(): { promise: Promise<{ [key: string]: string }[]> } { + public query(): { promise: Promise } { return { promise: Promise.resolve(annotations) }; } } @@ -95,7 +97,7 @@ describe("DatabaseAnnotationService", () => { bar: name + index, })); class MockDatabaseService extends DatabaseServiceNoop { - public query(): { promise: Promise<{ [key: string]: string }[]> } { + public query(): { promise: Promise } { return { promise: Promise.resolve(annotations) }; } } @@ -154,7 +156,7 @@ describe("DatabaseAnnotationService", () => { }); class MockDatabaseService extends DatabaseService { - public query(sql: string): { promise: Promise<{ [key: string]: string }[]> } { + public query(sql: string): { promise: Promise } { querySpy(sql); // pass SQL to the spy func return { promise: Promise.resolve([]) }; } @@ -246,7 +248,7 @@ describe("DatabaseAnnotationService", () => { const annotationNames = ["Cell Line", "Is Split Scene", "Whatever"]; const sampleRow = Object.fromEntries(annotationNames.map((name) => [name, "dummy value"])); class MockDatabaseService extends DatabaseService { - public query(sql: string): { promise: Promise<{ [key: string]: string }[]> } { + public query(sql: string): { promise: Promise } { if (sql.includes("SELECT *") && sql.includes("LIMIT 1")) { // First query for fetchAvailableAnnotationsForHierarchy gets the available // column names with a SELECT * FROM ... LIMIT 1 @@ -278,4 +280,173 @@ describe("DatabaseAnnotationService", () => { expect(values).to.deep.equal(annotationNames); }); }); + + describe("fetchOptimalWidthForAnnotations", () => { + it("returns widths based on longest value lengths from the database", async () => { + // Mock query returning max lengths for each annotation + class MockDatabaseService extends DatabaseServiceNoop { + public query(): { promise: Promise } { + return { + promise: Promise.resolve([{ "Cell Line": 200, Gene: 100 }]), + }; + } + } + const annotationService = new DatabaseAnnotationService({ + dataSourceNames: ["source1"], + databaseService: new MockDatabaseService(), + }); + + const result = await annotationService.fetchOptimalWidthForAnnotations([ + "Cell Line", + "Gene", + ]); + + // Both annotations should have computed widths (not just DEFAULT_COLUMN_WIDTH) + expect(result).to.have.property("Cell Line"); + expect(result).to.have.property("Gene"); + // The width for "Cell Line" should reflect max(20, "Cell Line".length=9) = 20 + // and for "Gene" should reflect max(10, "Gene".length=4) = 10 + // Both should be > MINIMUM_COLUMN_WIDTH + expect(result["Cell Line"]).to.be.greaterThan(MINIMUM_COLUMN_WIDTH); + expect(result["Gene"]).to.be.greaterThan(MINIMUM_COLUMN_WIDTH); + }); + + it("caps width at DEFAULT_COLUMN_WIDTH * 3 when ignoreWidthLimit is false", async () => { + // Return a very long value length that would exceed the cap + class MockDatabaseService extends DatabaseServiceNoop { + public query(): { promise: Promise } { + return { + promise: Promise.resolve([{ LongAnnotation: 500 }]), + }; + } + } + const annotationService = new DatabaseAnnotationService({ + dataSourceNames: ["source1"], + databaseService: new MockDatabaseService(), + }); + + const result = await annotationService.fetchOptimalWidthForAnnotations([ + "LongAnnotation", + ]); + + expect(result["LongAnnotation"]).to.equal(DEFAULT_COLUMN_WIDTH * 3); + }); + + it("does not cap width when ignoreWidthLimit is true", async () => { + // Return a very long value length that would exceed the cap + class MockDatabaseService extends DatabaseServiceNoop { + public query(): { promise: Promise } { + return { + promise: Promise.resolve([{ LongAnnotation: 500 }]), + }; + } + } + const annotationService = new DatabaseAnnotationService({ + dataSourceNames: ["source1"], + databaseService: new MockDatabaseService(), + }); + + const result = await annotationService.fetchOptimalWidthForAnnotations( + ["LongAnnotation"], + true + ); + + // With ignoreWidthLimit=true, width should not be capped + expect(result["LongAnnotation"]).to.be.greaterThan(DEFAULT_COLUMN_WIDTH * 3); + }); + + it("falls back to DEFAULT_COLUMN_WIDTH when query fails", async () => { + class MockDatabaseService extends DatabaseServiceNoop { + public query(): { promise: Promise } { + return { + promise: Promise.reject(new Error("query error")), + }; + } + } + const annotationService = new DatabaseAnnotationService({ + dataSourceNames: ["source1"], + databaseService: new MockDatabaseService(), + }); + + const result = await annotationService.fetchOptimalWidthForAnnotations(["SomeColumn"]); + + expect(result["SomeColumn"]).to.equal(DEFAULT_COLUMN_WIDTH); + }); + + it("includes TOP_LEVEL_FILE_ANNOTATIONS with default widths when not in query results", async () => { + class MockDatabaseService extends DatabaseServiceNoop { + public query(): { promise: Promise } { + return { + promise: Promise.resolve([{ CustomAnnotation: 15 }]), + }; + } + } + const annotationService = new DatabaseAnnotationService({ + dataSourceNames: ["source1"], + databaseService: new MockDatabaseService(), + }); + + const result = await annotationService.fetchOptimalWidthForAnnotations([ + "CustomAnnotation", + ]); + + // All top-level file annotations should be present with default widths + for (const annotation of TOP_LEVEL_FILE_ANNOTATIONS) { + expect(result).to.have.property(annotation.name); + expect(result[annotation.name]).to.equal(DEFAULT_COLUMN_WIDTH); + } + }); + + it("returns default widths when dataSourceNames is empty", async () => { + const annotationService = new DatabaseAnnotationService({ + dataSourceNames: [], + databaseService: new DatabaseServiceNoop(), + }); + + const result = await annotationService.fetchOptimalWidthForAnnotations(["SomeColumn"]); + + // fetchLengthiestValues returns {} for empty data sources, so all should be default + expect(result["SomeColumn"]).to.equal(DEFAULT_COLUMN_WIDTH); + }); + + it("uses annotation name length when it is longer than the longest value", async () => { + const veryLongAnnotationName = "VeryLongAnnotationName".repeat(100); // 2400 chars + // Annotation name "VeryLongAnnotationName" (2400 chars) > longest value length (5) + class MockDatabaseService extends DatabaseServiceNoop { + public query(): { promise: Promise } { + return { + promise: Promise.resolve([{ [veryLongAnnotationName]: 51 }]), + }; + } + } + const annotationService = new DatabaseAnnotationService({ + dataSourceNames: ["source1"], + databaseService: new MockDatabaseService(), + }); + + const resultShortValue = await annotationService.fetchOptimalWidthForAnnotations([ + veryLongAnnotationName, + ]); + + // Now test with a short annotation name but long value + class MockDatabaseService2 extends DatabaseServiceNoop { + public query(): { promise: Promise } { + return { + promise: Promise.resolve([{ X: 51 }]), + }; + } + } + const annotationService2 = new DatabaseAnnotationService({ + dataSourceNames: ["source1"], + databaseService: new MockDatabaseService2(), + }); + const resultShortName = await annotationService2.fetchOptimalWidthForAnnotations(["X"]); + + // The width for "VeryLongAnnotationName" should be wider because + // the annotation name is longer than the value + expect(resultShortValue[veryLongAnnotationName]).to.be.greaterThan( + resultShortName["X"] + ); + }); + }); }); diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/index.ts b/packages/core/services/AnnotationService/HttpAnnotationService/index.ts index b2570a47f..5a2d5eb18 100644 --- a/packages/core/services/AnnotationService/HttpAnnotationService/index.ts +++ b/packages/core/services/AnnotationService/HttpAnnotationService/index.ts @@ -7,6 +7,7 @@ import Annotation, { AnnotationResponse, AnnotationResponseMms } from "../../../ import { AnnotationType, AnnotationTypeIdMap } from "../../../entity/AnnotationFormatter"; import FileFilter from "../../../entity/FileFilter"; import { TOP_LEVEL_FILE_ANNOTATIONS, TOP_LEVEL_FILE_ANNOTATION_NAMES } from "../../../constants"; +import { DEFAULT_COLUMN_WIDTH } from "../../../entity/SearchParams"; enum QueryParam { EXCLUDE = "exclude", @@ -154,6 +155,25 @@ export default class HttpAnnotationService extends HttpServiceBase implements An return [...TOP_LEVEL_FILE_ANNOTATION_NAMES, ...response.data, ...annotations]; } + /** + * Fetch the lengthiest values for the specified annotations. + * + * **Not actually implemented** for this feature due to backend + * not having a way to fetch lengthiest values for annotations. + * Instead, we'll just return default widths for all annotations. + */ + public async fetchOptimalWidthForAnnotations( + annotationNames: string[] + ): Promise> { + const widthByAnnotation: Record = {}; + for (const annotationName of annotationNames) { + if (!widthByAnnotation.hasOwnProperty(annotationName)) { + widthByAnnotation[annotationName] = DEFAULT_COLUMN_WIDTH; + } + } + return widthByAnnotation; + } + /** * Validate annotation values according the type the annotation they belong to. */ diff --git a/packages/core/services/AnnotationService/index.ts b/packages/core/services/AnnotationService/index.ts index 217228e04..0a7afee4d 100644 --- a/packages/core/services/AnnotationService/index.ts +++ b/packages/core/services/AnnotationService/index.ts @@ -25,5 +25,9 @@ export default interface AnnotationService { filters: FileFilter[] ): Promise; fetchAvailableAnnotationsForHierarchy(annotations: string[]): Promise; + fetchOptimalWidthForAnnotations( + annotationNames: string[], + ignoreWidthLimit?: boolean + ): Promise>; validateAnnotationValues(name: string, values: AnnotationValue[]): Promise; } diff --git a/packages/core/services/DatabaseService/DatabaseServiceNoop.ts b/packages/core/services/DatabaseService/DatabaseServiceNoop.ts index c65906697..371be9d39 100644 --- a/packages/core/services/DatabaseService/DatabaseServiceNoop.ts +++ b/packages/core/services/DatabaseService/DatabaseServiceNoop.ts @@ -1,5 +1,5 @@ import { noop } from "lodash"; -import DatabaseService from "."; +import DatabaseService, { CancellablePromise } from "."; export default class DatabaseServiceNoop extends DatabaseService { public deleteSourceMetadata(): Promise { @@ -22,10 +22,7 @@ export default class DatabaseServiceNoop extends DatabaseService { return Promise.reject("DatabaseServiceNoop:saveQuery"); } - public query(): { - promise: Promise<{ [key: string]: any }[]>; - cancel?: (reason?: string) => void; - } { + public query(): CancellablePromise { return { promise: Promise.reject("DatabaseServiceNoop:query"), cancel: noop }; } diff --git a/packages/core/services/DatabaseService/index.ts b/packages/core/services/DatabaseService/index.ts index 98b2b496e..bc04768d6 100644 --- a/packages/core/services/DatabaseService/index.ts +++ b/packages/core/services/DatabaseService/index.ts @@ -10,6 +10,11 @@ import { Source } from "../../entity/SearchParams"; import SQLBuilder from "../../entity/SQLBuilder"; import DataSourcePreparationError from "../../errors/DataSourcePreparationError"; +export interface CancellablePromise { + promise: Promise; + cancel?: (reason?: string) => void; +} + enum PreDefinedColumn { FILE_ID = "File ID", FILE_PATH = "File Path", @@ -151,13 +156,11 @@ export default abstract class DatabaseService { } } - public query( - sql: string - ): { promise: Promise<{ [key: string]: any }[]>; cancel?: (reason?: string) => void } { - return { promise: this.runQuery(sql) }; + public query(sql: string): CancellablePromise { + return { promise: this.runQuery(sql) }; } - private async runQuery(sql: string): Promise<{ [key: string]: any }[]> { + private async runQuery(sql: string): Promise { if (!this.database) { throw new Error("Database failed to initialize"); } diff --git a/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts b/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts index 0f827624c..d98f1148f 100644 --- a/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts +++ b/packages/core/services/FileService/DatabaseFileService/test/DatabaseFileService.test.ts @@ -25,7 +25,7 @@ describe("DatabaseFileService", () => { class MockDatabaseService extends DatabaseServiceNoop { protected readonly existingDataSources = new Set(["MockDataSource"]); - public query(): { promise: Promise<{ [key: string]: string }[]> } { + public query(): { promise: Promise } { return { promise: Promise.resolve(files) }; } } @@ -83,7 +83,7 @@ describe("DatabaseFileService", () => { ]; class MockParquetDatabaseService extends DatabaseServiceNoop { protected readonly existingDataSources = new Set(["parquet_source"]); - public query(_sql?: string): { promise: Promise<{ [key: string]: string }[]> } { + public query(_sql?: string): { promise: Promise } { return { promise: Promise.resolve(parquetFiles) }; } } @@ -255,7 +255,7 @@ describe("DatabaseFileService", () => { return Promise.reject("MockDatabaseEditService:saveQuery"); } - public query(): { promise: Promise<{ [key: string]: string }[]> } { + public query(): { promise: Promise } { return { promise: Promise.reject("MockDatabaseEditService:query") }; } diff --git a/packages/core/state/metadata/logics.ts b/packages/core/state/metadata/logics.ts index 19a44a8d3..73d85d74b 100644 --- a/packages/core/state/metadata/logics.ts +++ b/packages/core/state/metadata/logics.ts @@ -26,7 +26,6 @@ import AnnotationName from "../../entity/Annotation/AnnotationName"; import { AnnotationType, AnnotationTypeIdMap } from "../../entity/AnnotationFormatter"; import FileFilter from "../../entity/FileFilter"; import FileSort, { SortOrder } from "../../entity/FileSort"; -import { DEFAULT_COLUMN_WIDTH } from "../../entity/SearchParams"; import HttpAnnotationService from "../../services/AnnotationService/HttpAnnotationService"; /** @@ -65,6 +64,7 @@ const requestAnnotations = createLogic({ const receiveAnnotationsLogic = createLogic({ async process(deps: ReduxLogicDeps, dispatch, done) { const { payload: annotations } = deps.action as ReceiveAnnotationAction; + const annotationService = interaction.selectors.getAnnotationService(deps.getState()); const currentSortColumn = selection.selectors.getSortColumn(deps.getState()); const currentColumns = selection.selectors.getColumns(deps.getState()); const isQueryingAicsFms = selection.selectors.isQueryingAicsFms(deps.getState()); @@ -85,20 +85,16 @@ const receiveAnnotationsLogic = createLogic({ (annotation) => !columnNamesThatStillExist.includes(annotation.name) ); - // TODO: To come in follow-up PR: calculate optimal column widths for new annotations based on content - // (currently defaulting to an arbitrary width for all new columns) - const widthByAnnotation: Record = {}; // Try to fetch values for new annotations to compute optimal column widths - // const widthByAnnotation = await annotationService.fetchOptimalWidthForAnnotations( - // newAnnotations.map((annotation) => annotation.name) - // ); + const widthByAnnotation = await annotationService.fetchOptimalWidthForAnnotations( + newAnnotations.map((annotation) => annotation.name) + ); let columns = [ ...columnsThatStillExist, ...newAnnotations.map((annotation) => ({ name: annotation.name, - // TODO: Remove default when above optimal width fetching is implemented - width: widthByAnnotation[annotation.name] ?? DEFAULT_COLUMN_WIDTH, + width: widthByAnnotation[annotation.name], })), ]; @@ -111,8 +107,7 @@ const receiveAnnotationsLogic = createLogic({ // Add "File Name" back to the front of the columns array columns.unshift({ name: AnnotationName.FILE_NAME, - // TODO: Remove default when above optimal width fetching is implemented - width: widthByAnnotation[AnnotationName.FILE_NAME] ?? DEFAULT_COLUMN_WIDTH, + width: widthByAnnotation[AnnotationName.FILE_NAME], }); } diff --git a/packages/core/state/selection/logics.ts b/packages/core/state/selection/logics.ts index 18538e94a..430899237 100644 --- a/packages/core/state/selection/logics.ts +++ b/packages/core/state/selection/logics.ts @@ -64,7 +64,7 @@ import * as selectionSelectors from "./selectors"; import { findChildNodes } from "../../components/DirectoryTree/findChildNodes"; import { NO_VALUE_NODE, ROOT_NODE } from "../../components/DirectoryTree/directory-hierarchy-state"; import Annotation from "../../entity/Annotation"; -import SearchParams, { DEFAULT_COLUMN_WIDTH } from "../../entity/SearchParams"; +import SearchParams from "../../entity/SearchParams"; import FileFilter, { FilterType } from "../../entity/FileFilter"; import FileFolder from "../../entity/FileFolder"; import FileSelection from "../../entity/FileSelection"; @@ -426,17 +426,16 @@ const expandAllFileFolders = createLogic({ const resizeColumnLogic = createLogic({ async process(deps: ReduxLogicDeps, dispatch, done) { const { payload: column } = deps.action as ResizeColumnAction; + const annotationService = interaction.selectors.getAnnotationService(deps.getState()); const columns = selectionSelectors.getColumns(deps.getState()); let width = column.width; if (!width) { - // TODO: To come in follow-up - // const autoSizedWidth = await annotationService.fetchOptimalWidthForAnnotations( - // [column.name], - // true - // ); - // width = autoSizedWidth[column.name] as number; - width = DEFAULT_COLUMN_WIDTH; + const autoSizedWidth = await annotationService.fetchOptimalWidthForAnnotations( + [column.name], + true + ); + width = autoSizedWidth[column.name] as number; } dispatch( diff --git a/packages/core/state/selection/test/logics.test.ts b/packages/core/state/selection/test/logics.test.ts index 3d0520451..bb8961ba6 100644 --- a/packages/core/state/selection/test/logics.test.ts +++ b/packages/core/state/selection/test/logics.test.ts @@ -702,7 +702,7 @@ describe("Selection logics", () => { return Promise.reject("MockDatabaseService:saveQuery"); } - public query(): { promise: Promise<{ [key: string]: string }[]> } { + public query(): { promise: Promise } { return { promise: Promise.reject("MockDatabaseService:query") }; } } diff --git a/packages/web/src/services/DatabaseServiceWeb/duckdb-worker.worker.ts b/packages/web/src/services/DatabaseServiceWeb/duckdb-worker.worker.ts index bfdb6164a..4dc44d624 100644 --- a/packages/web/src/services/DatabaseServiceWeb/duckdb-worker.worker.ts +++ b/packages/web/src/services/DatabaseServiceWeb/duckdb-worker.worker.ts @@ -9,7 +9,7 @@ import SQLBuilder from "../../../../core/entity/SQLBuilder"; import { HIDDEN_UID_ANNOTATION } from "../../../../core/constants"; import DataSourcePreparationError from "../../../../core/errors/DataSourcePreparationError"; import { DatabaseService } from "../../../../core/services"; -import { initializeDuckDB } from "../../../../core/services/DatabaseService"; +import { CancellablePromise, initializeDuckDB } from "../../../../core/services/DatabaseService"; declare const self: DedicatedWorkerGlobalScope & typeof globalThis; let databaseService: DatabaseServiceWebWorker | null = null; @@ -239,13 +239,11 @@ export default class DatabaseServiceWebWorker extends DatabaseService { await this.prepareDataSource(dataSource, skipNormalization); } - public query( - sql: string - ): { promise: Promise; cancel?: (reason?: string) => void } { - return { promise: this.queryWorker(sql) }; + public query(sql: string): CancellablePromise { + return { promise: this.queryWorker(sql) }; } - public async queryWorker(sql: string, queryId?: string): Promise { + public async queryWorker(sql: string, queryId?: string): Promise { if (!this.database) { throw new Error("DuckDB not initialized"); } diff --git a/packages/web/src/services/DatabaseServiceWeb/index.ts b/packages/web/src/services/DatabaseServiceWeb/index.ts index 0411a33f4..30f7922e3 100644 --- a/packages/web/src/services/DatabaseServiceWeb/index.ts +++ b/packages/web/src/services/DatabaseServiceWeb/index.ts @@ -6,12 +6,14 @@ import { Source } from "../../../../core/entity/SearchParams"; import { CanceledError, Pending, + QueryRow, WorkerMsgType, WorkerResPayload, WorkerResponse, WorkerResType, } from "./types"; import { DatabaseService } from "../../../../core/services"; +import { CancellablePromise } from "../../../../core/services/DatabaseService"; export default class DatabaseServiceWeb extends DatabaseService { // Initialize with AICS FMS data source name to pretend it always exists @@ -77,15 +79,13 @@ export default class DatabaseServiceWeb extends DatabaseService { return promise; } - public query( - sql: string - ): { promise: Promise<{ [key: string]: any }[]>; cancel?: (reason?: string) => void } { + public query(sql: string): CancellablePromise { if (!this.ready) { throw new Error(`Database failed to initialize in query with ${sql}`); } const queryId = `q-${Date.now()}-${uniqueId()}`; let settled = false; - const promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { this.pending.set(queryId, { resolve, reject,