Skip to content

Commit 7dd569a

Browse files
authored
Do additional cleanup to make up for plotly's lack of proper cleanup (#169)
1 parent d4bb85d commit 7dd569a

File tree

3 files changed

+57
-21
lines changed

3 files changed

+57
-21
lines changed

js/src/output.ts

+55-19
Original file line numberDiff line numberDiff line change
@@ -180,43 +180,70 @@ Shiny.addCustomMessageHandler("shinywidgets_comm_open", (msg_txt) => {
180180

181181
// Handle any mutation of the model (e.g., add a marker to a map, without a full redraw)
182182
// Basically out version of https://github.com/jupyterlab/jupyterlab/blob/d33de15/packages/services/src/kernel/default.ts#L1200-L1215
183-
Shiny.addCustomMessageHandler("shinywidgets_comm_msg", (msg_txt) => {
183+
Shiny.addCustomMessageHandler("shinywidgets_comm_msg", async (msg_txt) => {
184184
const msg = jsonParse(msg_txt);
185185
const id = msg.content.comm_id;
186186
const model = manager.get_model(id);
187187
if (!model) {
188188
console.error(`Couldn't handle message for model ${id} because it doesn't exist.`);
189189
return;
190190
}
191-
model
192-
.then(m => {
193-
// @ts-ignore for some reason IClassicComm doesn't have this method, but we do
194-
m.comm.handle_msg(msg);
195-
})
196-
.catch(console.error);
191+
try {
192+
const m = await model;
193+
// @ts-ignore for some reason IClassicComm doesn't have this method, but we do
194+
m.comm.handle_msg(msg);
195+
} catch (err) {
196+
console.error("Error handling message:", err);
197+
}
197198
});
198199

199200

200201
// Handle the closing of a widget/comm/model
201-
Shiny.addCustomMessageHandler("shinywidgets_comm_close", (msg_txt) => {
202+
Shiny.addCustomMessageHandler("shinywidgets_comm_close", async (msg_txt) => {
202203
const msg = jsonParse(msg_txt);
203204
const id = msg.content.comm_id;
204205
const model = manager.get_model(id);
205206
if (!model) {
206207
console.error(`Couldn't close model ${id} because it doesn't exist.`);
207208
return;
208209
}
209-
model
210-
.then(m => {
211-
// Closing the model removes the corresponding view from the DOM
212-
m.close();
213-
// .close() isn't enough to remove manager's reference to it,
214-
// and apparently the only way to remove it is through the `comm:close` event
215-
// https://github.com/jupyter-widgets/ipywidgets/blob/303cae4/packages/base-manager/src/manager-base.ts#L330-L337
216-
// https://github.com/jupyter-widgets/ipywidgets/blob/303cae4/packages/base/src/widget.ts#L251-L253
217-
m.trigger("comm:close");
218-
})
219-
.catch(console.error);
210+
211+
try {
212+
const m = await model;
213+
214+
// Before .close()ing the model (which will .remove() each view), do some
215+
// additional cleanup that .remove() might miss
216+
await Promise.all(
217+
Object.values(m.views).map(async (viewPromise) => {
218+
try {
219+
const v = await viewPromise;
220+
221+
// Old versions of plotly need a .destroy() to properly clean up
222+
// https://github.com/plotly/plotly.py/pull/3805/files#diff-259c92d
223+
if (hasMethod<DestroyMethod>(v, 'destroy')) {
224+
v.destroy();
225+
// Also, empirically, when this destroy() is relevant, it also helps to
226+
// delete the view's reference to the model, I think this is the only
227+
// way to drop the resize event listener (see the diff in the link above)
228+
// https://github.com/posit-dev/py-shinywidgets/issues/166
229+
delete v.model;
230+
}
231+
232+
233+
} catch (err) {
234+
console.error("Error cleaning up view:", err);
235+
}
236+
})
237+
);
238+
239+
// Close model after all views are cleaned up
240+
await m.close();
241+
242+
// Trigger comm:close event to remove manager's reference
243+
m.trigger("comm:close");
244+
} catch (err) {
245+
console.error("Error during model cleanup:", err);
246+
}
220247
});
221248

222249
$(document).on("shiny:disconnected", () => {
@@ -230,3 +257,12 @@ function setBaseURL(x: string = '') {
230257
document.querySelector('body').setAttribute('data-base-url', x);
231258
}
232259
}
260+
261+
// TypeGuard to safely check if an object has a method
262+
function hasMethod<T>(obj: any, methodName: keyof T): obj is T {
263+
return typeof obj[methodName] === 'function';
264+
}
265+
266+
interface DestroyMethod {
267+
destroy(): void;
268+
}

shinywidgets/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
__author__ = """Carson Sievert"""
44
__email__ = "[email protected]"
5-
__version__ = "0.3.4.9001"
5+
__version__ = "0.3.4.9002"
66

77
from ._as_widget import as_widget
88
from ._dependencies import bokeh_dependency

0 commit comments

Comments
 (0)