Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion chat2db-community-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
"test:tree-loading": "tsx src/store/tree/loadNamespaceTree.test.ts && tsx src/store/tree/treeDataUpdate.test.ts && tsx src/store/tree/hiddenTreeNodeState.test.ts && tsx src/blocks/NewTree/components/TitleRender/switcherAction.test.ts && tsx src/blocks/NewTree/treeScrollWidth.test.ts && tsx src/blocks/NewTree/monitorTree.test.ts && tsx src/pages/main/workspace/components/WorkspaceLeft/loadDatabaseTreePath.test.ts && tsx src/store/tree/savedConsoleTreeRefresh.test.ts",
"test:tree-node-lookup": "tsx src/utils/treeNodeLookup.test.ts",
"test:data-source-watermark": "tsx src/utils/dataSourceWatermark.test.ts",
"test:driver-upload": "tsx src/components/ConnectionEdit/components/Driver/driverUpload.test.ts",
"test:driver-upload": "tsx src/components/ConnectionEdit/components/Driver/driverUpload.test.ts && tsx src/components/ConnectionEdit/components/Driver/driverListRequest.test.ts",
"test:terminal": "tsx src/constants/terminal.test.ts && tsx src/pages/main/workspace/components/WorkspaceExtend/WorkspaceExtendNav/quickTerminal.test.ts && tsx src/pages/main/workspace/components/WorkspaceTabs/terminalTabPlacement.test.ts && tsx src/pages/main/workspace/components/WorkspaceTabs/terminalAttachmentLifecycle.test.ts && tsx src/pages/main/workspace/components/WorkspaceTabs/workspaceTabSelection.test.ts",
"test:task-center": "tsx src/store/importExport/taskCenterUtils.test.ts && tsx src/store/importExport/taskCenterStore.test.ts && tsx src/blocks/ImportAndExport/components/Log/eventMessage.test.ts",
"test:import-preview": "tsx src/components/SingleCharacterSelect/index.test.ts && tsx src/blocks/ImportAndExport/components/ImportMappingContent/mapping.test.ts && tsx src/blocks/ImportAndExport/components/ImportMappingContent/fileStaging.test.ts && tsx src/blocks/ImportAndExport/components/ImportExportFile/selection.test.ts && tsx src/blocks/ImportAndExport/components/ImportFileModal/submission.test.ts && tsx src/blocks/ImportAndExport/utils/csvOptions.test.ts && tsx src/blocks/ImportAndExport/utils/importOptions.test.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import assert from 'node:assert/strict';
import { DriverListRequestOwner, type DriverListRequestScope } from './driverListRequest';

interface Deferred<T> {
promise: Promise<T>;
resolve: (value: T) => void;
reject: (error: unknown) => void;
}

function deferred<T>(): Deferred<T> {
let resolvePromise: ((value: T) => void) | undefined;
let rejectPromise: ((error: unknown) => void) | undefined;
const promise = new Promise<T>((resolve, reject) => {
resolvePromise = resolve;
rejectPromise = reject;
});
return {
promise,
resolve: (value) => resolvePromise?.(value),
reject: (error) => rejectPromise?.(error),
};
}

function activate(owner: DriverListRequestOwner): DriverListRequestScope {
const scope = owner.createScope();
owner.activate(scope);
return scope;
}

async function testLatestSuccessOwnsDriverStateAndOnChange() {
const owner = new DriverListRequestOwner();
const scope = activate(owner);
const firstRequest = deferred<string>();
const latestRequest = deferred<string>();
let currentDriver: string | null = null;
const changes: string[] = [];
const commit = (driver: string) => {
currentDriver = driver;
changes.push(driver);
};

const firstLoad = owner.run(scope, () => firstRequest.promise, { onSuccess: commit });
const latestLoad = owner.run(scope, () => latestRequest.promise, { onSuccess: commit });
latestRequest.resolve('driver-b');
await latestLoad;
firstRequest.resolve('driver-a');
await firstLoad;

assert.equal(currentDriver, 'driver-b');
assert.deepEqual(changes, ['driver-b'], 'a stale success must not invoke the form onChange path');
}

async function testStaleFailureDoesNotReachCurrentErrorHandler() {
const owner = new DriverListRequestOwner();
const scope = activate(owner);
const firstRequest = deferred<string>();
const latestRequest = deferred<string>();
const errors: unknown[] = [];
const firstLoad = owner.run(scope, () => firstRequest.promise, {
onSuccess: () => undefined,
onError: (error) => errors.push(error),
});
const latestLoad = owner.run(scope, () => latestRequest.promise, { onSuccess: () => undefined });

firstRequest.reject(new Error('stale failure'));
await firstLoad;
assert.deepEqual(errors, []);
latestRequest.resolve('driver-b');
await latestLoad;
}

async function testContextSwitchSuppressesLateCallbacks() {
const owner = new DriverListRequestOwner();
const firstScope = activate(owner);
const request = deferred<string>();
const calls: string[] = [];
const load = owner.run(firstScope, () => request.promise, {
onSuccess: () => calls.push('success'),
onError: () => calls.push('error'),
});
owner.dispose(firstScope);
activate(owner);
request.resolve('driver-a');
await load;
assert.deepEqual(calls, []);
}

async function testLatestFailureReachesItsErrorHandlerOnce() {
const owner = new DriverListRequestOwner();
const scope = activate(owner);
const failure = new Error('latest failure');
const errors: unknown[] = [];
await owner.run(scope, () => Promise.reject(failure), {
onSuccess: () => undefined,
onError: (error) => errors.push(error),
});
assert.deepEqual(errors, [failure]);
}

async function testOldMutationCannotStartARefreshAfterContextSwitch() {
const owner = new DriverListRequestOwner();
const firstScope = activate(owner);
const mutation = deferred<void>();
let currentDriver: string | null = null;
let oldRefreshCalls = 0;
const oldMutationRefresh = mutation.promise.then(() =>
owner.run(
firstScope,
() => {
oldRefreshCalls += 1;
return Promise.resolve('driver-a');
},
{
onSuccess: (driver) => {
currentDriver = driver;
},
},
),
);

owner.dispose(firstScope);
const latestScope = activate(owner);
await owner.run(latestScope, () => Promise.resolve('driver-b'), {
onSuccess: (driver) => {
currentDriver = driver;
},
});
mutation.resolve();
await oldMutationRefresh;

assert.equal(oldRefreshCalls, 0, 'a mutation from the old datasource must not start another list request');
assert.equal(currentDriver, 'driver-b');
}

async function testDisposedOwnerRejectsLateStarts() {
const owner = new DriverListRequestOwner();
const scope = activate(owner);
let requestCalls = 0;
const callbacks: string[] = [];
owner.dispose(scope);
await owner.run(
scope,
() => {
requestCalls += 1;
return Promise.resolve('driver-a');
},
{ onSuccess: () => callbacks.push('success'), onError: () => callbacks.push('error') },
);

assert.equal(requestCalls, 0, 'an unmounted owner must not start a request from an old closure');
assert.deepEqual(callbacks, []);
}

async function testOldMutationCannotReviveAcrossAnAbaContextSwitch() {
const owner = new DriverListRequestOwner();
const firstMysqlScope = activate(owner);
const mutation = deferred<void>();
let currentDriver: string | null = null;
let oldRefreshCalls = 0;
const oldMutationRefresh = mutation.promise.then(() =>
owner.run(
firstMysqlScope,
() => {
oldRefreshCalls += 1;
return Promise.resolve('driver-a');
},
{
onSuccess: (driver) => {
currentDriver = driver;
},
},
),
);

owner.dispose(firstMysqlScope);
const postgresScope = activate(owner);
await owner.run(postgresScope, () => Promise.resolve('driver-b'), {
onSuccess: (driver) => {
currentDriver = driver;
},
});
owner.dispose(postgresScope);
const secondMysqlScope = activate(owner);
assert.notEqual(firstMysqlScope, secondMysqlScope, 'separate mysql activations must use unique scopes');
await owner.run(secondMysqlScope, () => Promise.resolve('driver-c'), {
onSuccess: (driver) => {
currentDriver = driver;
},
});

mutation.resolve();
await oldMutationRefresh;
assert.equal(oldRefreshCalls, 0, 'an old mysql scope must not match a later mysql scope');
assert.equal(currentDriver, 'driver-c');
}

async function testSameTypeCustomDriverChangeInvalidatesOldList() {
const owner = new DriverListRequestOwner();
const emptyDriverScope = activate(owner);
const oldList = deferred<string>();
let selectedDriver = '';
const load = owner.run(emptyDriverScope, () => oldList.promise, {
onSuccess: (driver) => {
selectedDriver = driver;
},
});

owner.dispose(emptyDriverScope);
const customDriverScope = activate(owner);
assert.notEqual(emptyDriverScope, customDriverScope, 'a custom-driver render must receive a new scope');
selectedDriver = 'custom-driver';
oldList.resolve('default-driver');
await load;
assert.equal(selectedDriver, 'custom-driver', 'a list from the pre-custom scope must not overwrite the selection');
}

void Promise.all([
testLatestSuccessOwnsDriverStateAndOnChange(),
testStaleFailureDoesNotReachCurrentErrorHandler(),
testContextSwitchSuppressesLateCallbacks(),
testLatestFailureReachesItsErrorHandlerOnce(),
testOldMutationCannotStartARefreshAfterContextSwitch(),
testDisposedOwnerRejectsLateStarts(),
testOldMutationCannotReviveAcrossAnAbaContextSwitch(),
testSameTypeCustomDriverChangeInvalidatesOldList(),
])
.then(() => console.log('Driver list request ownership tests passed'))
.catch((error) => {
console.error(error);
process.exitCode = 1;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export interface DriverListRequestCallbacks<T> {
onSuccess: (result: T) => void;
onError?: (error: unknown) => void;
}

export type DriverListRequestScope = symbol;

export class DriverListRequestOwner {
private generation = 0;

private scope?: DriverListRequestScope;

private disposed = true;

createScope(): DriverListRequestScope {
return Symbol('driver-list-request-scope');
}

activate(scope: DriverListRequestScope) {
this.generation += 1;
this.scope = scope;
this.disposed = false;
}

dispose(scope: DriverListRequestScope) {
if (this.scope !== scope) {
return;
}
this.generation += 1;
this.disposed = true;
}

async run<T>(
expectedScope: DriverListRequestScope,
request: () => Promise<T>,
callbacks: DriverListRequestCallbacks<T>,
): Promise<void> {
if (this.disposed || expectedScope !== this.scope) {
return;
}
const generation = this.generation + 1;
this.generation = generation;
try {
const result = await request();
if (this.owns(expectedScope, generation)) {
callbacks.onSuccess(result);
}
} catch (error) {
if (this.owns(expectedScope, generation)) {
callbacks.onError?.(error);
}
}
}

private owns(expectedScope: DriverListRequestScope, generation: number) {
return !this.disposed && this.scope === expectedScope && this.generation === generation;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo, useState, useEffect } from 'react';
import React, { memo, useState, useEffect, useMemo, useRef } from 'react';
import styles from './index.less';
import classnames from 'classnames';
import { i18n } from '@/i18n';
Expand All @@ -10,6 +10,7 @@ import LoadingGracile from '@/components/Loading/LoadingGracile';
import { isCommunityEnv, isDesktop } from '@/utils/env';
import feedback from '@/utils/feedback';
import { canSaveDriverDraft, resolveDriverSavePayload, type IDriverSaveDraft } from './driverUpload';
import { DriverListRequestOwner } from './driverListRequest';
const { Option } = Select;

interface IProps {
Expand All @@ -34,12 +35,29 @@ export default memo<IProps>((props) => {
const [uploadDriverModal, setUploadDriverModal] = useState(false);
const [driverSaved, setDriverSaved] = useState<IDriverSaveDraft>({ dbType: backfillData?.type });
const [desktopLoading, setDesktopLoading] = useState(false);
const driverListRequestOwnerRef = useRef<DriverListRequestOwner>();
if (!driverListRequestOwnerRef.current) {
driverListRequestOwnerRef.current = new DriverListRequestOwner();
}
const driverListRequestOwner = driverListRequestOwnerRef.current;
const driverListScope = useMemo(
() => driverListRequestOwner.createScope(),
[
backfillData?.id,
backfillData?.type,
backfillData?.driverConfig?.jdbcDriver,
backfillData?.driverConfig?.jdbcDriverClass,
],
);

useEffect(() => {
const dbType = backfillData?.type;
driverListRequestOwner.activate(driverListScope);
if (backfillData) {
getDriverList();
void getDriverList(driverListScope, dbType);
}
}, [backfillData?.type]);
return () => driverListRequestOwner.dispose(driverListScope);
}, [driverListScope]);

useEffect(() => {
if (backfillData) {
Expand All @@ -52,24 +70,31 @@ export default memo<IProps>((props) => {
}
}, [backfillData?.driverConfig, backfillData?.id]);

function getDriverList() {
connectionService.getDriverList({ dbType: backfillData.type }).then((res) => {
if (!res) {
return;
}
setDriverObj({
...res,
driverConfigList: res.driverConfigList || [],
});
if (res.driverConfigList?.length && !backfillData?.driverConfig?.jdbcDriver) {
const data = {
jdbcDriverClass: res.driverConfigList[0]?.jdbcDriverClass,
jdbcDriver: res.driverConfigList[0]?.jdbcDriver,
};
driverForm.setFieldsValue(data);
onChange(data);
}
});
function getDriverList(expectedScope = driverListScope, expectedDbType = backfillData.type) {
return driverListRequestOwner.run(
expectedScope,
() => connectionService.getDriverList({ dbType: expectedDbType }),
{
onSuccess: (res) => {
if (!res) {
return;
}
setDriverObj({
...res,
driverConfigList: res.driverConfigList || [],
});
if (res.driverConfigList?.length && !backfillData?.driverConfig?.jdbcDriver) {
const data = {
jdbcDriverClass: res.driverConfigList[0]?.jdbcDriverClass,
jdbcDriver: res.driverConfigList[0]?.jdbcDriver,
};
driverForm.setFieldsValue(data);
onChange(data);
}
},
onError: (error) => console.error('get driver list error', error),
},
);
}

function formChange(data: any) {
Expand Down
Loading