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

This commit is contained in:
Joe Boccanfuso authored and GitHub committed 2025-06-02 12:12:31 -04:00
1 parent a1fb7217bd
commit 38ed7ba744
5 files changed
+36 -9

No files matched your search

+4 -4
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;
},
/**
@@ -255,7 +255,7 @@ class SegmentationService extends PubSubService {
this._onSegmentationAddedFromSource
);
this.listeners = {};
this.reset();
};
public async addSegmentationRepresentation(
@@ -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 };
}
@@ -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 = {};
}
/**
+10 -1
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;