Skip to content

Commit 37e8a11

Browse files
authored
fix: DH-20908: Handle table disconnects in Plotly Express (#1267)
fix: DH-20908: Handle table disconnect in widgets
1 parent c626cd0 commit 37e8a11

File tree

2 files changed

+90
-13
lines changed

2 files changed

+90
-13
lines changed

plugins/plotly-express/src/js/src/PlotlyExpressChartModel.test.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ import {
1212
} from './PlotlyExpressChartUtils';
1313

1414
const SMALL_TABLE = TestUtils.createMockProxy<DhType.Table>({
15+
addEventListener: jest.fn(() => jest.fn()),
1516
columns: [{ name: 'x' }, { name: 'y' }] as DhType.Column[],
1617
size: 500,
17-
subscribe: () => TestUtils.createMockProxy(),
18+
subscribe: () =>
19+
TestUtils.createMockProxy<DhType.TableSubscription>({
20+
addEventListener: jest.fn(() => jest.fn()),
21+
}),
1822
});
1923

2024
const LARGE_TABLE = TestUtils.createMockProxy<DhType.Table>({
@@ -113,6 +117,7 @@ function createMockWidget(
113117
close: jest.fn(),
114118
})),
115119
addEventListener: jest.fn(),
120+
close: jest.fn(),
116121
sendMessage: jest.fn(),
117122
} satisfies Partial<DhType.Widget> as unknown as DhType.Widget;
118123
}
@@ -187,7 +192,7 @@ const checkEventTypes = (mockSubscribe: jest.Mock, eventTypes: string[]) => {
187192
};
188193

189194
beforeEach(() => {
190-
jest.resetAllMocks();
195+
jest.clearAllMocks();
191196
});
192197

193198
describe('PlotlyExpressChartModel', () => {
@@ -614,4 +619,50 @@ describe('PlotlyExpressChartModel', () => {
614619
new CustomEvent(ChartModel.EVENT_LAYOUT_UPDATED)
615620
);
616621
});
622+
623+
it('should emit disconnect event when table disconnects', async () => {
624+
const mockWidget = createMockWidget([SMALL_TABLE], 'scatter');
625+
const chartModel = new PlotlyExpressChartModel(
626+
mockDh,
627+
mockWidget,
628+
jest.fn()
629+
);
630+
631+
const mockSubscribe = jest.fn();
632+
await chartModel.subscribe(mockSubscribe);
633+
await new Promise(process.nextTick);
634+
635+
const table0: DhType.Table = await mockWidget.exportedObjects[0].fetch();
636+
637+
const handler = TestUtils.findLastCall(
638+
TestUtils.asMock(table0.addEventListener),
639+
() => true
640+
);
641+
642+
handler?.[1](new CustomEvent(mockDh.Table.EVENT_DISCONNECT));
643+
644+
expect(mockSubscribe).toHaveBeenCalledTimes(1);
645+
expect(mockSubscribe).toHaveBeenLastCalledWith(
646+
new CustomEvent(ChartModel.EVENT_DISCONNECT)
647+
);
648+
});
649+
650+
it('should cleanup subscriptions', async () => {
651+
const mockWidget = createMockWidget([SMALL_TABLE], 'scatter');
652+
const chartModel = new PlotlyExpressChartModel(
653+
mockDh,
654+
mockWidget,
655+
jest.fn()
656+
);
657+
658+
const mockSubscribe = jest.fn();
659+
await chartModel.subscribe(mockSubscribe);
660+
await new Promise(process.nextTick);
661+
662+
expect(chartModel.subscriptionCleanupMap.size).toBe(1);
663+
664+
chartModel.unsubscribe(jest.fn());
665+
666+
expect(chartModel.subscriptionCleanupMap.size).toBe(0);
667+
});
617668
});

plugins/plotly-express/src/js/src/PlotlyExpressChartModel.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export class PlotlyExpressChartModel extends ChartModel {
112112
/**
113113
* Map of table index to cleanup function for the subscription.
114114
*/
115-
subscriptionCleanupMap: Map<number, () => void> = new Map();
115+
subscriptionCleanupMap: Map<number, Set<() => void>> = new Map();
116116

117117
/**
118118
* Map of table index to map of column names to array of paths where the data should be replaced.
@@ -189,6 +189,21 @@ export class PlotlyExpressChartModel extends ChartModel {
189189
*/
190190
requiredColumns: Set<string> = new Set();
191191

192+
cleanupSubscriptions(id: number): void {
193+
this.subscriptionCleanupMap.get(id)?.forEach(cleanup => {
194+
cleanup();
195+
});
196+
197+
try {
198+
this.tableSubscriptionMap.get(id)?.close();
199+
} catch {
200+
// ignore
201+
}
202+
203+
this.subscriptionCleanupMap.delete(id);
204+
this.tableSubscriptionMap.delete(id);
205+
}
206+
192207
override getData(): Partial<Data>[] {
193208
const hydratedData = [...this.plotlyData];
194209

@@ -688,11 +703,11 @@ export class PlotlyExpressChartModel extends ChartModel {
688703
log.debug('Updating downsampled table', downsampleInfo);
689704

690705
this.fireDownsampleStart(null);
691-
this.subscriptionCleanupMap.get(id)?.();
692-
this.tableSubscriptionMap.get(id)?.close();
693-
this.subscriptionCleanupMap.delete(id);
694-
this.tableSubscriptionMap.delete(id);
706+
707+
this.cleanupSubscriptions(id);
708+
695709
this.tableReferenceMap.delete(id);
710+
696711
this.addTable(id, oldDownsampleInfo.originalTable);
697712
}
698713

@@ -807,24 +822,35 @@ export class PlotlyExpressChartModel extends ChartModel {
807822
const columns = table.columns.filter(({ name }) => columnNames.has(name));
808823
const subscription = table.subscribe(columns);
809824
this.tableSubscriptionMap.set(id, subscription);
810-
this.subscriptionCleanupMap.set(
811-
id,
825+
826+
if (!this.subscriptionCleanupMap.has(id)) {
827+
this.subscriptionCleanupMap.set(id, new Set());
828+
}
829+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
830+
const cleanupSet = this.subscriptionCleanupMap.get(id)!;
831+
832+
cleanupSet.add(
812833
subscription.addEventListener<DhType.SubscriptionTableData>(
813834
this.dh.Table.EVENT_UPDATED,
814835
e => this.handleFigureUpdated(e, id)
815836
)
816837
);
838+
839+
cleanupSet.add(
840+
table.addEventListener<DhType.Table>(
841+
this.dh.Table.EVENT_DISCONNECT,
842+
e => this.fireDisconnect()
843+
)
844+
);
817845
}
818846
}
819847

820848
removeTable(id: number): void {
821-
this.subscriptionCleanupMap.get(id)?.();
822-
this.tableSubscriptionMap.get(id)?.close();
849+
this.cleanupSubscriptions(id);
823850

824851
this.tableReferenceMap.delete(id);
852+
825853
this.downsampleMap.delete(id);
826-
this.subscriptionCleanupMap.delete(id);
827-
this.tableSubscriptionMap.delete(id);
828854
this.chartDataMap.delete(id);
829855
this.tableDataMap.delete(id);
830856
this.tableColumnReplacementMap.delete(id);

0 commit comments

Comments
 (0)