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, getPanelModule,
onModeExit: ({ servicesManager }: withAppTypes): void => { onModeExit: ({ servicesManager }: withAppTypes): void => {
unsubscriptions.forEach(unsubscribe => unsubscribe());
// Clear the unsubscriptions
unsubscriptions.length = 0;
const { cineService, segmentationService } = servicesManager.services; const { cineService, segmentationService } = servicesManager.services;
// Empty out the image load and retrieval pools to prevent memory leaks // Empty out the image load and retrieval pools to prevent memory leaks
// on the mode exits // on the mode exits
@ -150,10 +154,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
useToggleOneUpViewportGridStore.getState().clearToggleOneUpViewportGridStore(); useToggleOneUpViewportGridStore.getState().clearToggleOneUpViewportGridStore();
useSegmentationPresentationStore.getState().clearSegmentationPresentationStore(); useSegmentationPresentationStore.getState().clearSegmentationPresentationStore();
segmentationService.removeAllSegmentations(); 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._onSegmentationAddedFromSource
); );
this.listeners = {}; this.reset();
}; };
public async addSegmentationRepresentation( public async addSegmentationRepresentation(

View File

@ -9,7 +9,11 @@ export function setupSegmentationDataModifiedHandler({
customizationService, customizationService,
commandsManager, 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, segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
async ({ segmentationId }) => { async ({ segmentationId }) => {
const segmentation = segmentationService.getSegmentation(segmentationId); const segmentation = segmentationService.getSegmentation(segmentationId);
@ -42,7 +46,7 @@ export function setupSegmentationDataModifiedHandler({
readableText, readableText,
}); });
if (updatedSegmentation) { if (!isUnsubscribed && updatedSegmentation) {
segmentationService.addOrUpdateSegmentation({ segmentationService.addOrUpdateSegmentation({
segmentationId, segmentationId,
segments: updatedSegmentation.segments, segments: updatedSegmentation.segments,
@ -52,6 +56,10 @@ export function setupSegmentationDataModifiedHandler({
1000 1000
); );
const unsubscribe = () => {
isUnsubscribed = true;
debouncedUnsubscribe();
};
return { unsubscribe }; return { unsubscribe };
} }

View File

@ -54,7 +54,10 @@ function _unsubscribe(eventName, listenerId) {
const listeners = this.listeners[eventName]; const listeners = this.listeners[eventName];
if (Array.isArray(listeners)) { 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 { } else {
this.listeners[eventName] = undefined; this.listeners[eventName] = undefined;
} }
@ -134,6 +137,13 @@ export class PubSubService {
reset() { reset() {
this.unsubscriptions.forEach(unsub => unsub()); this.unsubscriptions.forEach(unsub => unsub());
this.unsubscriptions = []; 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 // be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the // N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing. // 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) { function debounce(func, wait, immediate) {
var timeout; var timeout;
return function () { const callback = function () {
var context = this, var context = this,
args = arguments; args = arguments;
var later = function () { var later = function () {
@ -20,6 +22,13 @@ function debounce(func, wait, immediate) {
func.apply(context, args); func.apply(context, args);
} }
}; };
callback.clearDebounceTimeout = () => {
clearTimeout(timeout);
timeout = null;
};
return callback;
} }
export default debounce; export default debounce;