diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java index f0c693cbb6..22ad19dfa9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/session/WebSessionManager.java @@ -274,7 +274,7 @@ public void expireIdleSessions() { public Collection getAllActiveSessions() { synchronized (sessionMap) { - return sessionMap.values(); + return new ArrayList<>(sessionMap.values()); } } diff --git a/webapp/packages/core-utils/src/debounce.test.ts b/webapp/packages/core-utils/src/debounce.test.ts index 4dcaa9b79d..d2e8605b8f 100644 --- a/webapp/packages/core-utils/src/debounce.test.ts +++ b/webapp/packages/core-utils/src/debounce.test.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { debounce } from './debounce'; +import { debounce, debounceAsync } from './debounce'; // https://jestjs.io/docs/timer-mocks // Tell Jest to mock all timeout functions @@ -23,6 +23,22 @@ describe('Debounce', () => { // Fast-forward time jest.runAllTimers(); - expect(func).toBeCalledTimes(1); + expect(func).toHaveBeenCalledTimes(1); + }); +}); + +describe('DebounceAsync', () => { + test('function should be executed just once', async () => { + const func = jest.fn(() => Promise.resolve(true)); + const debouncedFunction = debounceAsync(func, 1000); + + debouncedFunction(); + debouncedFunction(); + debouncedFunction(); + + // Fast-forward time + jest.runAllTimers(); + + expect(func).toHaveBeenCalledTimes(1); }); }); diff --git a/webapp/packages/core-utils/src/debounce.ts b/webapp/packages/core-utils/src/debounce.ts index 7778c38326..c273340061 100644 --- a/webapp/packages/core-utils/src/debounce.ts +++ b/webapp/packages/core-utils/src/debounce.ts @@ -19,3 +19,27 @@ export function debounce any>(func: T, delay: numb }, delay); }; } + +export function debounceAsync Promise>(func: T, delay: number): T { + let timeoutId: NodeJS.Timeout | null; + + return function (this: any, ...args: Parameters): Promise> { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const context = this; + + return new Promise((resolve, reject) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + + timeoutId = setTimeout(async () => { + try { + const result = await func.apply(context, args); + resolve(result); + } catch (error) { + reject(error); + } + }, delay); + }); + } as T; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts index 830ce84b91..185938782d 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts @@ -27,7 +27,6 @@ export interface ISQLEditorData { readonly cursorSegment: ISQLScriptSegment | undefined; readonly readonly: boolean; readonly editing: boolean; - readonly isLineScriptEmpty: boolean; readonly isScriptEmpty: boolean; readonly isDisabled: boolean; readonly isIncomingChanges: boolean; @@ -42,7 +41,7 @@ export interface ISQLEditorData { /** displays if last getHintProposals call ended with limit */ readonly hintsLimitIsMet: boolean; - updateParserScriptsThrottle(): Promise; + updateParserScriptsDebounced(): Promise; setScript(query: string): void; init(): void; destruct(): void; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.tsx index 6eefe6ec8e..59ba5fd917 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.tsx @@ -26,7 +26,7 @@ export const SQLEditorActions = observer(function SQLEditorActions({ data const styles = useS(style); const translate = useTranslate(); const isActiveSegmentMode = getComputed(() => data.activeSegmentMode.activeSegmentMode); - const disabled = getComputed(() => data.isLineScriptEmpty || data.isDisabled); + const disabled = getComputed(() => data.isScriptEmpty || data.isDisabled); const isQuery = data.dataSource?.hasFeature(ESqlDataSourceFeatures.query); const isExecutable = data.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index 1755d7dbb5..4370dd7fe3 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -15,7 +15,7 @@ import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dial import { NotificationService } from '@cloudbeaver/core-events'; import { SyncExecutor } from '@cloudbeaver/core-executor'; import type { SqlCompletionProposal, SqlDialectInfo, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk'; -import { createLastPromiseGetter, LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils'; +import { createLastPromiseGetter, debounceAsync, LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils'; import type { ISqlEditorTabState } from '../ISqlEditorTabState'; import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures'; @@ -104,12 +104,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { return this.dataSource?.isEditing() ?? false; }, - get isLineScriptEmpty(): boolean { - return !this.activeSegment?.query; - }, - get isScriptEmpty(): boolean { - return this.value === '' || this.parser.scripts.length === 0; + return this.value === ''; }, get isDisabled(): boolean { @@ -166,7 +162,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { untracked(() => { this.sqlDialectInfoService.loadSqlDialectInfo(key).then(async () => { try { - await this.updateParserScriptsThrottle(); + await this.updateParserScriptsDebounced(); } catch {} }); }); @@ -205,13 +201,14 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { this.hintsLimitIsMet = hints.length >= MAX_HINTS_LIMIT; return hints; - }, 1000 / 3), + }, 300), async formatScript(): Promise { if (this.isDisabled || this.isScriptEmpty || !this.dataSource?.executionContext) { return; } + await this.updateParserScripts(); const query = this.value; const script = this.getExecutingQuery(false); @@ -238,6 +235,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { if (!isQuery || !isExecutable) { return; } + + await this.updateParserScripts(); const query = this.getSubQuery(); try { @@ -269,6 +268,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { if (!isQuery || !isExecutable) { return; } + + await this.updateParserScripts(); const query = this.getSubQuery(); try { @@ -286,6 +287,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { return; } + await this.updateParserScripts(); const query = this.getSubQuery(); try { @@ -362,9 +364,9 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { this.dataSource?.setScript(query); }, - updateParserScriptsThrottle: throttleAsync(async function updateParserScriptsThrottle() { + updateParserScriptsDebounced: debounceAsync(async function updateParserScriptsThrottle() { await data.updateParserScripts(); - }, 1000 / 2), + }, 2000), async updateParserScripts() { if (!this.dataSource?.hasFeature(ESqlDataSourceFeatures.script)) { @@ -402,7 +404,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { passEmpty?: boolean, passDisabled?: boolean, ): Promise { - if (!segment || (this.isDisabled && !passDisabled) || (!passEmpty && this.isLineScriptEmpty)) { + if (!segment || (this.isDisabled && !passDisabled) || (!passEmpty && this.isScriptEmpty)) { return; } @@ -433,6 +435,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { const projectId = this.dataSource?.executionContext?.projectId; const connectionId = this.dataSource?.executionContext?.connectionId; + await data.updateParserScripts(); + if (!projectId || !connectionId || this.cursor.begin !== this.cursor.end) { return this.getSubQuery(); } @@ -443,12 +447,11 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { const result = await this.sqlEditorService.parseSQLQuery(projectId, connectionId, this.value, this.cursor.begin); - const segment = this.parser.getSegment(result.start, result.end); - - if (!segment) { - throw new Error('Failed to get position'); + if (result.end === 0 && result.start === 0) { + return; } + const segment = this.parser.getSegment(result.start, result.end); return segment; }, @@ -469,6 +472,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, }), { + getHintProposals: action.bound, formatScript: action.bound, executeQuery: action.bound, executeQueryNewTab: action.bound, @@ -507,7 +511,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { // ensure that cursor is in script boundaries data.setCursor(data.cursor.begin, data.cursor.end); data.parser.setScript(script); - data.updateParserScriptsThrottle().catch(() => {}); + data.updateParserScriptsDebounced().catch(() => {}); data.onUpdate.execute(); }, ],