fix(segmentation): Allow for debounced pub/sub events to be reliably cancelled when unsubscribed to. (#5092)

This commit is contained in:
Joe Boccanfuso 2025-06-02 12:12:31 -04:00 committed by GitHub
parent a1fb7217bd
commit 38ed7ba744
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 36 additions and 9 deletions

View File

@ -132,6 +132,10 @@ const cornerstoneExtension: Types.Extensions.Extension = {
},
getPanelModule,
onModeExit: ({ servicesManager }: withAppTypes): void => {
unsubscriptions.forEach(unsubscribe => unsubscribe());
// Clear the unsubscriptions
unsubscriptions.length = 0;
const { cineService, segmentationService } = servicesManager.services;
// Empty out the image load and retrieval pools to prevent memory leaks
// on the mode exits
@ -150,10 +154,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
useToggleOneUpViewportGridStore.getState().clearToggleOneUpViewportGridStore();
useSegmentationPresentationStore.getState().clearSegmentationPresentationStore();
segmentationService.removeAllSegmentations();
unsubscriptions.forEach(unsubscribe => unsubscribe());
// Clear the unsubscriptions
unsubscriptions.length = 0;
},
/**

View File

@ -255,7 +255,7 @@ class SegmentationService extends PubSubService {
this._onSegmentationAddedFromSource
);
this.listeners = {};
this.reset();
};
public async addSegmentationRepresentation(

View File

@ -9,7 +9,11 @@ export function setupSegmentationDataModifiedHandler({
customizationService,
commandsManager,
}) {
const { unsubscribe } = segmentationService.subscribeDebounced(
// A flag to indicate if the event is unsubscribed to. This is important because
// the debounced callback does an await and in that period of time the event may have
// been unsubscribed.
let isUnsubscribed = false;
const { unsubscribe: debouncedUnsubscribe } = segmentationService.subscribeDebounced(
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
async ({ segmentationId }) => {
const segmentation = segmentationService.getSegmentation(segmentationId);
@ -42,7 +46,7 @@ export function setupSegmentationDataModifiedHandler({
readableText,
});
if (updatedSegmentation) {
if (!isUnsubscribed && updatedSegmentation) {
segmentationService.addOrUpdateSegmentation({
segmentationId,
segments: updatedSegmentation.segments,
@ -52,6 +56,10 @@ export function setupSegmentationDataModifiedHandler({
1000
);
const unsubscribe = () => {
isUnsubscribed = true;
debouncedUnsubscribe();
};
return { unsubscribe };
}

View File

@ -54,7 +54,10 @@ function _unsubscribe(eventName, listenerId) {
const listeners = this.listeners[eventName];
if (Array.isArray(listeners)) {
this.listeners[eventName] = listeners.filter(({ id }) => id !== listenerId);
this.listeners[eventName] = listeners.filter(({ id, callback }) => {
callback?.clearDebounceTimeout?.();
return id !== listenerId;
});
} else {
this.listeners[eventName] = undefined;
}
@ -134,6 +137,13 @@ export class PubSubService {
reset() {
this.unsubscriptions.forEach(unsub => unsub());
this.unsubscriptions = [];
Object.keys(this.listeners).forEach(eventName =>
this.listeners[eventName].forEach(({ callback }) => {
callback?.clearDebounceTimeout?.();
})
);
this.listeners = {};
}
/**

View File

@ -2,9 +2,11 @@
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// The callback function returned is assigned a clearDebounceTimeout function
// that provides for clearing the timeout/debounced function so that it is not called.
function debounce(func, wait, immediate) {
var timeout;
return function () {
const callback = function () {
var context = this,
args = arguments;
var later = function () {
@ -20,6 +22,13 @@ function debounce(func, wait, immediate) {
func.apply(context, args);
}
};
callback.clearDebounceTimeout = () => {
clearTimeout(timeout);
timeout = null;
};
return callback;
}
export default debounce;