diff --git a/src/lib/olap-engine/EventListener.ts b/src/lib/olap-engine/EventListener.ts new file mode 100644 index 0000000..2ef2dda --- /dev/null +++ b/src/lib/olap-engine/EventListener.ts @@ -0,0 +1,26 @@ +type Callback = (...param: any[]) => void; + +type OptionalRecord = { [P in K]?: T }; + +export interface IListener { + on(event: Events, fn: Callback): () => any; +} + +export abstract class InternalEventEmitter implements IListener { + #listeners: OptionalRecord> = {}; + + on(event: Events, fn: Callback) { + this.#listeners[event] ??= new Set(); + + this.#listeners[event].add(fn); + return () => this.#listeners[event]?.delete(fn); + } + + protected emit(event: Events, ...param: any[]) { + if (!this.#listeners[event]?.size) return; + + queueMicrotask(() => { + if (this.#listeners[event]) for (const fn of this.#listeners[event]) fn(...param); + }); + } +} diff --git a/src/lib/olap-engine/Logger.ts b/src/lib/olap-engine/Logger.ts deleted file mode 100644 index 694bc9a..0000000 --- a/src/lib/olap-engine/Logger.ts +++ /dev/null @@ -1,25 +0,0 @@ -type Callback = (param: any) => void; - -export interface ILogger { - on(level: 'error', fn: Callback): () => void; - log(level: 'error', param: any): void; -} - -export abstract class Logger implements ILogger { - #listeners: { [key: string]: Set } = {}; - - on(logEvent: 'error', fn: Callback) { - this.#listeners[logEvent] ??= new Set(); - - this.#listeners[logEvent].add(fn); - return () => this.#listeners[logEvent].delete(fn); - } - - log(level: 'error', param: any) { - if (!this.#listeners[level]?.size) return; - - queueMicrotask(() => { - for (const fn of this.#listeners[level]) fn(param); - }); - } -} diff --git a/src/lib/olap-engine/engine-chdb.ts b/src/lib/olap-engine/engine-chdb.ts index a246cc0..532260f 100644 --- a/src/lib/olap-engine/engine-chdb.ts +++ b/src/lib/olap-engine/engine-chdb.ts @@ -1,37 +1,41 @@ import { invoke } from '@tauri-apps/api/core'; -import type { OLAPEngine, OLAPResponse, Table } from './index'; -import { Logger } from './Logger'; +import { InternalEventEmitter } from './EventListener'; +import type { Events, OLAPEngine, OLAPResponse, Table } from './index'; import CLICKHOUSE_GET_SCHEMA from './queries/clickhouse_get_schema.sql?raw'; import CLICKHOUSE_GET_UDFS from './queries/clickhouse_get_udfs.sql?raw'; import CLICKHOUSE_INIT_DB from './queries/clickhouse_init_db.sql?raw'; -export class CHDBEngine extends Logger implements OLAPEngine { +export class CHDBEngine extends InternalEventEmitter implements OLAPEngine { async init() { await this.exec(CLICKHOUSE_INIT_DB); } - async exec(query: string) { + async exec(query: string, _emit = true) { try { const r: string = await invoke('query', { query }); - if (!r) return; - return JSON.parse(r) as OLAPResponse; + let data: OLAPResponse | undefined; + if (r) data = JSON.parse(r) as OLAPResponse; + + if (_emit) this.emit('success', query, data); + + return data; } catch (e) { if (typeof e === 'string') e = new Error(e); console.error(e); - this.log('error', e); + if (_emit) this.emit('error', e); } } async getSchema() { - const response = await this.exec(CLICKHOUSE_GET_SCHEMA); + const response = await this.exec(CLICKHOUSE_GET_SCHEMA, false); if (!response) return []; return response.data as Table[]; } async getUDFs() { - const response = await this.exec(CLICKHOUSE_GET_UDFS); + const response = await this.exec(CLICKHOUSE_GET_UDFS, false); if (!response) return []; return response.data.map((row) => row.name as string); diff --git a/src/lib/olap-engine/engine-remote.ts b/src/lib/olap-engine/engine-remote.ts index 77b88dd..60634b1 100644 --- a/src/lib/olap-engine/engine-remote.ts +++ b/src/lib/olap-engine/engine-remote.ts @@ -1,13 +1,13 @@ -import type { OLAPEngine, OLAPResponse, Table } from './index'; -import { Logger } from './Logger'; +import { InternalEventEmitter } from './EventListener'; +import type { Events, OLAPEngine, OLAPResponse, Table } from './index'; import CLICKHOUSE_GET_SCHEMA from './queries/clickhouse_get_schema.sql?raw'; import CLICKHOUSE_GET_UDFS from './queries/clickhouse_get_udfs.sql?raw'; -export class RemoteEngine extends Logger implements OLAPEngine { +export class RemoteEngine extends InternalEventEmitter implements OLAPEngine { async init() {} - async exec(query: string) { + async exec(query: string, _emit = true) { try { const proxy = new URLSearchParams(window.location.search).get('proxy') ?? 'https://proxy.agx.app/query'; @@ -22,21 +22,23 @@ export class RemoteEngine extends Logger implements OLAPEngine { if ('exception' in data) throw new Error(data.exception); + if (_emit) this.emit('success', query, data); + return data; } catch (e) { console.error(e); - this.log('error', e); + if (_emit) this.emit('error', e); } } async getSchema() { - const response = await this.exec(CLICKHOUSE_GET_SCHEMA); + const response = await this.exec(CLICKHOUSE_GET_SCHEMA, false); if (!response) return []; return response.data as Table[]; } async getUDFs() { - const response = await this.exec(CLICKHOUSE_GET_UDFS); + const response = await this.exec(CLICKHOUSE_GET_UDFS, false); if (!response) return []; return response.data.map((row) => row.name as string); diff --git a/src/lib/olap-engine/index.ts b/src/lib/olap-engine/index.ts index fe1f6ac..d9fd7d1 100644 --- a/src/lib/olap-engine/index.ts +++ b/src/lib/olap-engine/index.ts @@ -1,6 +1,6 @@ import { CHDBEngine } from './engine-chdb'; import { RemoteEngine } from './engine-remote'; -import type { ILogger } from './Logger'; +import type { IListener } from './EventListener'; export type OLAPResponse = { meta: Array; @@ -24,7 +24,9 @@ export interface Table { columns: ColumnDescriptor[]; } -export interface OLAPEngine extends ILogger { +export type Events = 'error' | 'success'; + +export interface OLAPEngine extends IListener { init(): Promise; exec(query: string): Promise; getSchema(): Promise; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 67e32b2..53d4806 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,6 +3,9 @@ import { ContextMenuState } from '$lib/components/ContextMenu'; import ContextMenu from '$lib/components/ContextMenu/ContextMenu.svelte'; import Drawer from '$lib/components/Drawer.svelte'; + import { functions, keywords, operators, types } from '$lib/components/Editor/clickhouse'; + import Editor from '$lib/components/Editor/Editor.svelte'; + import { setupLanguage } from '$lib/components/Editor/language'; import { SaveQueryModal } from '$lib/components/Queries'; import Result from '$lib/components/Result.svelte'; import SideBar from '$lib/components/SideBar.svelte'; @@ -23,13 +26,10 @@ import { historyRepository, type HistoryEntry } from '$lib/repositories/history'; import { queryRepository, type Query } from '$lib/repositories/queries'; import { tabRepository, type Tab } from '$lib/repositories/tabs'; - import Editor from '$lib/components/Editor/Editor.svelte'; import { SplitPane } from '@rich_harris/svelte-split-pane'; import debounce from 'p-debounce'; import { format } from 'sql-formatter'; import { tick, type ComponentProps } from 'svelte'; - import { setupLanguage } from '$lib/components/Editor/language'; - import { keywords, functions, operators, types } from '$lib/components/Editor/clickhouse'; let response = $state.raw(); let loading = $state(false); @@ -37,24 +37,15 @@ async function handleExec() { const query = currentTab.content; - if (loading || !query) { - return; - } + if (loading || !query) return; loading = true; counter?.start(); - response = await engine.exec(query).finally(() => { + try { + response = await engine.exec(query); + } finally { loading = false; counter?.stop(); - }); - - const last = await historyRepository.getLast(); - - if (response && last?.content !== query) await addHistoryEntry(query); - - if (response) { - bottomPanel.open = true; - if (bottomPanelTab === 'logs') bottomPanelTab = 'data'; } } @@ -82,6 +73,23 @@ $effect(() => void historyRepository.getAll().then((entries) => (history = entries))); $effect(() => void queryRepository.getAll().then((q) => (queries = q))); + engine.on('success', async (query: string) => { + if (typeof query !== 'string') return; + if (/(CREATE|DROP)/gi.test(query)) tables = await engine.getSchema(); + }); + + engine.on('success', async (query: string, response?: OLAPResponse) => { + const last = await historyRepository.getLast(); + if (response && last?.content !== query) await addHistoryEntry(query); + }); + + engine.on('success', (query: string, response?: OLAPResponse) => { + if (response) { + bottomPanel.open = true; + if (bottomPanelTab === 'logs') bottomPanelTab = 'data'; + } + }); + async function addHistoryEntry(query: string) { try { const entry = await historyRepository.add(query);