Skip to content

Commit cd1e9f0

Browse files
committed
fix(client): ignore stale dashboard details
1 parent 144a04e commit cd1e9f0

5 files changed

Lines changed: 143 additions & 4 deletions

File tree

‎chat2db-community-client/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"build": "yarn run build:web:community",
1313
"build:prod": "yarn run build:web:community",
1414
"build:web": "umi build",
15-
"prebuild:web:community": "yarn test:community-boundary && yarn test:database-capabilities && yarn test:table-editor && yarn test:retired-ai && yarn test:tree-title-highlight && yarn test:tree-search-lifecycle && yarn test:driver-upload && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:tree-node-path && yarn test:data-source-authorization && yarn test:active-transactions && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:shortcut && yarn test:console-tab-name && yarn test:workspace-tab-scroll && yarn test:workspace-split-lifecycle && yarn test:sql-completion-context && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:local-file-save && yarn test:local-file-tree-path && yarn test:saved-console-dirty && yarn test:editor-close && yarn test:result-set-ui && yarn test:result-pagination && yarn test:sql-execution-stream && yarn test:result-table-layout && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:monaco-lifecycle && yarn test:result-set-editor && yarn test:account-grants-request",
15+
"prebuild:web:community": "yarn test:community-boundary && yarn test:database-capabilities && yarn test:table-editor && yarn test:retired-ai && yarn test:tree-title-highlight && yarn test:tree-search-lifecycle && yarn test:driver-upload && yarn test:tree-loading && yarn test:tree-node-lookup && yarn test:tree-node-path && yarn test:data-source-authorization && yarn test:active-transactions && yarn test:data-source-mutation-refresh && yarn test:ai-model-config && yarn test:ai-model-select && yarn test:export-connections && yarn test:main-page-navigation && yarn test:shortcut && yarn test:console-tab-name && yarn test:workspace-tab-scroll && yarn test:workspace-split-lifecycle && yarn test:sql-completion-context && yarn test:file-manager-label && yarn test:local-file-encoding && yarn test:local-file-save && yarn test:local-file-tree-path && yarn test:saved-console-dirty && yarn test:editor-close && yarn test:result-set-ui && yarn test:result-pagination && yarn test:sql-execution-stream && yarn test:result-table-layout && yarn test:result-status && yarn test:data-source-identity && yarn test:data-source-execution-snapshot && yarn test:data-source-watermark && yarn test:hot-update && yarn test:task-center && yarn test:application-exit && yarn test:monaco-lifecycle && yarn test:result-set-editor && yarn test:account-grants-request && yarn test:dashboard-store",
1616
"postbuild:web:community": "node ./scripts/verify-production-bundles.cjs",
1717
"build:web:2java": "yarn run build:web:community && rm -rf ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/* && cp -r dist/index.html ../chat2db-community-server/chat2db-community-start/src/main/resources/thymeleaf/",
1818
"build:web:community": "cross-env UMI_ENV=community cross-env APP_NAME=chat2db-community cross-env APP_VERSION=${npm_config_app_version} cross-env PRINT_LOGS=${npm_config_print_logs} cross-env APP_PORT=${npm_config_app_port} umi build",
@@ -30,6 +30,7 @@
3030
"test:retired-ai": "tsx src/blocks/AI/retiredAiSurface.test.ts && tsx src/blocks/AI/components/AIChatInput/mentionSelection.test.ts && tsx src/blocks/AI/knowledgeSelection.test.ts && tsx src/blocks/AI/messageNavigation.test.ts",
3131
"test:hot-update": "tsx src/store/global/slices/hotUpdate/action.test.ts",
3232
"test:active-transactions": "tsx src/blocks/NewTree/components/ActiveTransactionsContent/activeTransactionUtils.test.ts && tsx src/blocks/NewTree/treeMenuGrouping.test.ts",
33+
"test:dashboard-store": "node scripts/run-dashboard-store-tests.cjs",
3334
"test:result-pagination": "tsx src/blocks/SearchResult/components/ResultSet/pagination.test.ts",
3435
"test:tree-node-path": "tsx src/blocks/NewTree/utils/treeNodePath.test.ts",
3536
"test:ai-model-config": "tsx src/blocks/AI/components/AIModelConfigModal/modelConfigDefaults.test.ts",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const { existsSync } = require('node:fs');
2+
const { spawnSync } = require('node:child_process');
3+
4+
const testFiles = [
5+
'src/store/dashboard/slices/common/dashboardDetailRequest.test.ts',
6+
'src/store/dashboard/slices/common/dashboardMutation.test.ts',
7+
'src/store/dashboard/slices/common/refreshCurrentDashboard.test.ts',
8+
'src/blocks/BI/ChartCardBox/DingChartModal/pinChartToDashboard.test.ts',
9+
];
10+
const tsxCli = require.resolve('tsx/cli');
11+
let executed = 0;
12+
13+
for (const testFile of testFiles) {
14+
if (!existsSync(testFile)) continue;
15+
executed += 1;
16+
const result = spawnSync(process.execPath, [tsxCli, testFile], { stdio: 'inherit' });
17+
if (result.status !== 0) {
18+
process.exit(result.status ?? 1);
19+
}
20+
}
21+
22+
if (!executed) {
23+
console.error('No dashboard store tests were found.');
24+
process.exit(1);
25+
}

‎chat2db-community-client/src/store/dashboard/slices/common/action.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import {
1212
import i18n from '@/i18n';
1313
import { staticMessage } from '@chat2db/ui';
1414
import { filterSchemaByChartIds } from '@/utils/dashboard';
15+
import { DashboardDetailRequestOwner } from './dashboardDetailRequest';
16+
17+
const dashboardDetailRequestOwner = new DashboardDetailRequestOwner();
1518

1619
export interface CommonAction {
1720
/** Set up Dashboard list */
@@ -74,6 +77,7 @@ export const createCommonAction: StateCreator<DashboardStore, [['zustand/devtool
7477
}
7578
},
7679
setCurrentDashboard: async (dashboard) => {
80+
dashboardDetailRequestOwner.invalidate();
7781
set({ currentDashboard: dashboard });
7882
},
7983
updateDashboard: (dashboard) => {
@@ -127,9 +131,10 @@ export const createCommonAction: StateCreator<DashboardStore, [['zustand/devtool
127131
// history.pushState(null, '', `/dashboard/${id}`);
128132
// }
129133
// set({ currentDashboard: null });
130-
return getDashboardById({ id }).then((res) => {
131-
set({ currentDashboard: res });
132-
});
134+
return dashboardDetailRequestOwner.run(
135+
() => getDashboardById({ id }),
136+
(dashboard) => set({ currentDashboard: dashboard }),
137+
);
133138
},
134139
refreshCurrentDashboard: () => {
135140
const currentDashboardId = get().currentDashboard?.id;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import assert from 'node:assert/strict';
2+
import { DashboardDetailRequestOwner } from './dashboardDetailRequest';
3+
4+
interface Deferred<T> {
5+
promise: Promise<T>;
6+
resolve: (value: T) => void;
7+
}
8+
9+
function deferred<T>(): Deferred<T> {
10+
let resolvePromise: ((value: T) => void) | undefined;
11+
const promise = new Promise<T>((resolve) => {
12+
resolvePromise = resolve;
13+
});
14+
return {
15+
promise,
16+
resolve: (value) => resolvePromise?.(value),
17+
};
18+
}
19+
20+
async function testLatestDashboardDetailOwnsCurrentState() {
21+
const owner = new DashboardDetailRequestOwner();
22+
const firstRequest = deferred<{ id: number }>();
23+
const latestRequest = deferred<{ id: number }>();
24+
let currentDashboard: { id: number } | null = null;
25+
const commit = (dashboard: { id: number }) => {
26+
currentDashboard = dashboard;
27+
};
28+
29+
const firstLoad = owner.run(() => firstRequest.promise, commit);
30+
const latestLoad = owner.run(() => latestRequest.promise, commit);
31+
latestRequest.resolve({ id: 2 });
32+
await latestLoad;
33+
assert.equal(currentDashboard?.id, 2);
34+
35+
firstRequest.resolve({ id: 1 });
36+
await firstLoad;
37+
assert.equal(currentDashboard?.id, 2, 'an older response must not replace the latest dashboard');
38+
}
39+
40+
async function testDirectSelectionInvalidatesPendingDetail() {
41+
const owner = new DashboardDetailRequestOwner();
42+
const request = deferred<{ id: number }>();
43+
let currentDashboard: { id: number } | null = null;
44+
const load = owner.run(
45+
() => request.promise,
46+
(dashboard) => {
47+
currentDashboard = dashboard;
48+
},
49+
);
50+
51+
owner.invalidate();
52+
currentDashboard = { id: 2 };
53+
request.resolve({ id: 1 });
54+
await load;
55+
56+
assert.equal(currentDashboard.id, 2, 'a direct selection must not be overwritten by an older request');
57+
}
58+
59+
async function testLatestFailureStillInvalidatesOlderSuccess() {
60+
const owner = new DashboardDetailRequestOwner();
61+
const olderRequest = deferred<{ id: number }>();
62+
let currentDashboard: { id: number } | null = null;
63+
const olderLoad = owner.run(
64+
() => olderRequest.promise,
65+
(dashboard) => {
66+
currentDashboard = dashboard;
67+
},
68+
);
69+
const latestError = new Error('latest request failed');
70+
await assert.rejects(
71+
owner.run(
72+
() => Promise.reject(latestError),
73+
() => undefined,
74+
),
75+
latestError,
76+
);
77+
78+
olderRequest.resolve({ id: 1 });
79+
await olderLoad;
80+
assert.equal(currentDashboard, null, 'an older success must stay stale after the latest request fails');
81+
}
82+
83+
void Promise.all([
84+
testLatestDashboardDetailOwnsCurrentState(),
85+
testDirectSelectionInvalidatesPendingDetail(),
86+
testLatestFailureStillInvalidatesOlderSuccess(),
87+
])
88+
.then(() => console.log('Dashboard detail request ownership tests passed'))
89+
.catch((error) => {
90+
console.error(error);
91+
process.exitCode = 1;
92+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export class DashboardDetailRequestOwner {
2+
private generation = 0;
3+
4+
invalidate() {
5+
this.generation += 1;
6+
}
7+
8+
async run<T>(request: () => Promise<T>, commit: (result: T) => void): Promise<void> {
9+
const generation = this.generation + 1;
10+
this.generation = generation;
11+
const result = await request();
12+
if (this.generation === generation) {
13+
commit(result);
14+
}
15+
}
16+
}

0 commit comments

Comments
 (0)