Merge pull request #106 from agnosticeng/feat/cancelable-request
This commit is contained in:
15
src/lib/icons/Stop.svelte
Normal file
15
src/lib/icons/Stop.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
interface Props extends Omit<SvelteHTMLElements['svg'], 'width' | 'height'> {
|
||||
size?: string | number | null;
|
||||
}
|
||||
|
||||
let { size = 24, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg width={size} height={size} fill="currentColor" viewBox="0 0 24 24" {...rest}>
|
||||
<path
|
||||
d="M4.75 3A1.75 1.75 0 0 0 3 4.75v14.5c0 .966.784 1.75 1.75 1.75h14.5A1.75 1.75 0 0 0 21 19.25V4.75A1.75 1.75 0 0 0 19.25 3H4.75Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -1,17 +1,19 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { InternalEventEmitter } from './EventListener';
|
||||
import type { Events, OLAPEngine, OLAPResponse, Table } from './index';
|
||||
import type { Events, ExecOptions, 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 InternalEventEmitter<Events> implements OLAPEngine {
|
||||
readonly isAbortable = false;
|
||||
|
||||
async init() {
|
||||
await this.exec(CLICKHOUSE_INIT_DB);
|
||||
}
|
||||
|
||||
async exec(query: string, _emit = true) {
|
||||
async exec(query: string, options: ExecOptions = {}, _emit = true) {
|
||||
try {
|
||||
const r: string = await invoke('query', { query });
|
||||
|
||||
@@ -29,13 +31,13 @@ export class CHDBEngine extends InternalEventEmitter<Events> implements OLAPEngi
|
||||
}
|
||||
|
||||
async getSchema() {
|
||||
const response = await this.exec(CLICKHOUSE_GET_SCHEMA, false);
|
||||
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, false);
|
||||
const response = await this.exec(CLICKHOUSE_GET_UDFS, {}, false);
|
||||
if (!response) return [];
|
||||
|
||||
return response.data.map((row) => row.name as string);
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { InternalEventEmitter } from './EventListener';
|
||||
import type { Events, OLAPEngine, OLAPResponse, Table } from './index';
|
||||
import type { Events, ExecOptions, 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 InternalEventEmitter<Events> implements OLAPEngine {
|
||||
readonly isAbortable = true;
|
||||
|
||||
async init() {}
|
||||
|
||||
async exec(query: string, _emit = true) {
|
||||
async exec(query: string, options: ExecOptions = {}, _emit = true) {
|
||||
try {
|
||||
const proxy = new URLSearchParams(window.location.search).get('proxy') ?? CLICKHOUSE_URL;
|
||||
const response = await fetch(proxy, { method: 'POST', body: query });
|
||||
const response = await fetch(proxy, { method: 'POST', body: query, signal: options.signal });
|
||||
|
||||
const r = await response.text();
|
||||
if (!r) throw new Error(`Empty Response`);
|
||||
@@ -29,13 +31,13 @@ export class RemoteEngine extends InternalEventEmitter<Events> implements OLAPEn
|
||||
}
|
||||
|
||||
async getSchema() {
|
||||
const response = await this.exec(CLICKHOUSE_GET_SCHEMA, false);
|
||||
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, false);
|
||||
const response = await this.exec(CLICKHOUSE_GET_UDFS, {}, false);
|
||||
if (!response) return [];
|
||||
|
||||
return response.data.map((row) => row.name as string);
|
||||
|
||||
@@ -28,9 +28,12 @@ export interface Table {
|
||||
|
||||
export type Events = 'error' | 'success';
|
||||
|
||||
export type ExecOptions = { signal?: AbortSignal | null };
|
||||
|
||||
export interface OLAPEngine extends IListener<Events> {
|
||||
readonly isAbortable: boolean;
|
||||
init(): Promise<void>;
|
||||
exec(query: string): Promise<OLAPResponse | undefined>;
|
||||
exec(query: string, options?: ExecOptions): Promise<OLAPResponse | undefined>;
|
||||
getSchema(): Promise<Table[]>;
|
||||
getUDFs(): Promise<string[]>;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import Plus from '$lib/icons/Plus.svelte';
|
||||
import Save from '$lib/icons/Save.svelte';
|
||||
import Sparkles from '$lib/icons/Sparkles.svelte';
|
||||
import Stop from '$lib/icons/Stop.svelte';
|
||||
import type { Table } from '$lib/olap-engine';
|
||||
import { engine, type OLAPResponse } from '$lib/olap-engine';
|
||||
import { PanelState } from '$lib/PanelState.svelte';
|
||||
@@ -55,6 +56,7 @@
|
||||
let response = $state.raw<OLAPResponse>();
|
||||
let loading = $state(false);
|
||||
let counter = $state<ReturnType<typeof TimeCounter>>();
|
||||
let abortController: AbortController | undefined;
|
||||
|
||||
const cache = new IndexedDBCache({ dbName: 'query-cache', storeName: 'response-data' });
|
||||
let cached = $state(false);
|
||||
@@ -79,11 +81,13 @@
|
||||
}
|
||||
|
||||
cached = false;
|
||||
response = await engine.exec(query);
|
||||
abortController = new AbortController();
|
||||
response = await engine.exec(query, { signal: abortController.signal });
|
||||
await cache.set(query, response);
|
||||
} finally {
|
||||
loading = false;
|
||||
counter?.stop();
|
||||
abortController = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +295,7 @@
|
||||
let errors = $state.raw<Log[]>([]);
|
||||
engine.on('error', (e) => {
|
||||
if (e instanceof Error) {
|
||||
if (e.message === 'Canceled') return;
|
||||
errors = errors.concat({ level: 'error', timestamp: new Date(), data: e.message });
|
||||
bottomPanel.open = true;
|
||||
bottomPanelTab = 'logs';
|
||||
@@ -479,14 +484,24 @@ LIMIT 100;`;
|
||||
>
|
||||
<Save size="12" />
|
||||
</button>
|
||||
<button
|
||||
class="action"
|
||||
title="Run"
|
||||
onclick={() => handleExec()}
|
||||
disabled={loading}
|
||||
>
|
||||
<Play size="12" />
|
||||
</button>
|
||||
{#if loading && engine.isAbortable}
|
||||
<button
|
||||
class="action"
|
||||
title="Cancel"
|
||||
onclick={() => abortController?.abort(new Error('Canceled'))}
|
||||
>
|
||||
<Stop size="12" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="action"
|
||||
title="Run"
|
||||
onclick={() => handleExec()}
|
||||
disabled={loading}
|
||||
>
|
||||
<Play size="12" />
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="action"
|
||||
title="Force run"
|
||||
|
||||
Reference in New Issue
Block a user