dicom-curate 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -48613,14 +48613,14 @@ var require_dist_cjs70 = __commonJS({
48613
48613
  const exitConditions = [runPolling(params, input, acceptorChecks)];
48614
48614
  const finalize = [];
48615
48615
  if (options.abortSignal) {
48616
- const { aborted, clearListener } = abortTimeout(options.abortSignal);
48616
+ const { aborted: aborted2, clearListener } = abortTimeout(options.abortSignal);
48617
48617
  finalize.push(clearListener);
48618
- exitConditions.push(aborted);
48618
+ exitConditions.push(aborted2);
48619
48619
  }
48620
48620
  if (options.abortController?.signal) {
48621
- const { aborted, clearListener } = abortTimeout(options.abortController.signal);
48621
+ const { aborted: aborted2, clearListener } = abortTimeout(options.abortController.signal);
48622
48622
  finalize.push(clearListener);
48623
- exitConditions.push(aborted);
48623
+ exitConditions.push(aborted2);
48624
48624
  }
48625
48625
  return Promise.race(exitConditions).then((result) => {
48626
48626
  for (const fn of finalize) {
@@ -87483,6 +87483,7 @@ var filesMapped = 0;
87483
87483
  var workerCurrentFile = /* @__PURE__ */ new Map();
87484
87484
  var lastWorkerProgressTime = 0;
87485
87485
  var pendingReplacements = 0;
87486
+ var aborted = false;
87486
87487
  var currentFileInfoIndex;
87487
87488
  var filesToProcess = [];
87488
87489
  var directoryScanFinished = false;
@@ -87505,12 +87506,32 @@ function setScanResumeCallback(cb) {
87505
87506
  function markScanPaused() {
87506
87507
  scanPaused = true;
87507
87508
  }
87509
+ function terminateAllWorkers() {
87510
+ aborted = true;
87511
+ while (availableMappingWorkers.length) {
87512
+ availableMappingWorkers.pop().terminate();
87513
+ }
87514
+ for (const [worker] of workerCurrentFile) {
87515
+ try {
87516
+ worker.terminate();
87517
+ } catch {
87518
+ }
87519
+ }
87520
+ workerCurrentFile.clear();
87521
+ filesToProcess.length = 0;
87522
+ workersActive = 0;
87523
+ pendingReplacements = 0;
87524
+ directoryScanFinished = false;
87525
+ scanPaused = false;
87526
+ scanResumeCallback = null;
87527
+ }
87508
87528
  async function initializeMappingWorkers(skipCollectingMappings, fileInfoIndex, progressCb, workerCount) {
87509
87529
  mappingWorkerOptions = {};
87510
87530
  workersActive = 0;
87511
87531
  mapResultsList = skipCollectingMappings ? void 0 : [];
87512
87532
  filesMapped = 0;
87513
87533
  pendingReplacements = 0;
87534
+ aborted = false;
87514
87535
  workerCurrentFile.clear();
87515
87536
  lastWorkerProgressTime = Date.now();
87516
87537
  currentFileInfoIndex = fileInfoIndex;
@@ -87526,6 +87547,8 @@ async function initializeMappingWorkers(skipCollectingMappings, fileInfoIndex, p
87526
87547
  availableMappingWorkers.push(...workers);
87527
87548
  }
87528
87549
  async function dispatchMappingJobs() {
87550
+ if (aborted)
87551
+ return;
87529
87552
  while (filesToProcess.length > 0 && availableMappingWorkers.length > 0) {
87530
87553
  const { fileInfo, previousFileInfo } = filesToProcess.pop();
87531
87554
  const mappingWorker = availableMappingWorkers.pop();
@@ -87588,6 +87611,8 @@ async function getHardwareConcurrency() {
87588
87611
  return cpus().length;
87589
87612
  }
87590
87613
  function recoverCrashedWorker(mappingWorker, errorMessage) {
87614
+ if (aborted)
87615
+ return;
87591
87616
  if (!workerCurrentFile.has(mappingWorker)) {
87592
87617
  return;
87593
87618
  }
@@ -87623,6 +87648,10 @@ function recoverCrashedWorker(mappingWorker, errorMessage) {
87623
87648
  pendingReplacements += 1;
87624
87649
  void createMappingWorker().then((worker) => {
87625
87650
  pendingReplacements -= 1;
87651
+ if (aborted) {
87652
+ worker.terminate();
87653
+ return;
87654
+ }
87626
87655
  availableMappingWorkers.push(worker);
87627
87656
  dispatchMappingJobs();
87628
87657
  }).catch((error2) => {
@@ -87652,6 +87681,8 @@ async function createMappingWorker() {
87652
87681
  });
87653
87682
  }
87654
87683
  mappingWorker.addEventListener("message", (event) => {
87684
+ if (aborted)
87685
+ return;
87655
87686
  if (event.data.response === "lookup") {
87656
87687
  const outputPath = event.data.outputPath;
87657
87688
  const entry = currentFileInfoIndex?.[OUTPUT_FILE_PREFIX + outputPath];
@@ -87870,7 +87901,14 @@ function queueUrlsForMapping(organizeOptions) {
87870
87901
  setDirectoryScanFinished(true);
87871
87902
  }
87872
87903
  async function curateMany(organizeOptions, onProgress) {
87904
+ if (organizeOptions.signal?.aborted) {
87905
+ return Promise.reject(
87906
+ new DOMException("The operation was aborted.", "AbortError")
87907
+ );
87908
+ }
87873
87909
  return new Promise(async (resolve, reject) => {
87910
+ let settled = false;
87911
+ const signal = organizeOptions.signal;
87874
87912
  const STALL_TIMEOUT_MS = 10 * 60 * 1e3;
87875
87913
  const stallWatchdog = setInterval(() => {
87876
87914
  if (getWorkersActive() > 0 && Date.now() - getLastWorkerProgressTime() > STALL_TIMEOUT_MS) {
@@ -87889,15 +87927,42 @@ async function curateMany(organizeOptions, onProgress) {
87889
87927
  }, 6e4);
87890
87928
  const progressCallback2 = (msg) => {
87891
87929
  onProgress?.(msg);
87892
- if (msg.response === "done") {
87930
+ if (msg.response === "done" && !settled) {
87931
+ settled = true;
87893
87932
  clearInterval(stallWatchdog);
87933
+ signal?.removeEventListener("abort", onAbort);
87894
87934
  resolve(msg);
87895
87935
  }
87896
87936
  };
87897
87937
  const rejectCallback = (reason) => {
87938
+ if (settled)
87939
+ return;
87940
+ settled = true;
87898
87941
  clearInterval(stallWatchdog);
87942
+ signal?.removeEventListener("abort", onAbort);
87899
87943
  reject(reason);
87900
87944
  };
87945
+ let fileListWorker;
87946
+ const onAbort = () => {
87947
+ try {
87948
+ fileListWorker?.terminate();
87949
+ } catch {
87950
+ }
87951
+ terminateAllWorkers();
87952
+ rejectCallback(
87953
+ new DOMException("The operation was aborted.", "AbortError")
87954
+ );
87955
+ };
87956
+ if (signal) {
87957
+ if (signal.aborted) {
87958
+ clearInterval(stallWatchdog);
87959
+ rejectCallback(
87960
+ new DOMException("The operation was aborted.", "AbortError")
87961
+ );
87962
+ return;
87963
+ }
87964
+ signal.addEventListener("abort", onAbort, { once: true });
87965
+ }
87901
87966
  try {
87902
87967
  await initializeMappingWorkers(
87903
87968
  organizeOptions.skipCollectingMappings,
@@ -87909,7 +87974,7 @@ async function curateMany(organizeOptions, onProgress) {
87909
87974
  await collectMappingOptions(organizeOptions)
87910
87975
  );
87911
87976
  if (organizeOptions.inputType === "directory" || organizeOptions.inputType === "path" || organizeOptions.inputType === "s3") {
87912
- const fileListWorker = await initializeFileListWorker(rejectCallback);
87977
+ fileListWorker = await initializeFileListWorker(rejectCallback);
87913
87978
  setScanResumeCallback(() => {
87914
87979
  fileListWorker.postMessage({ request: "resume" });
87915
87980
  });
@@ -87966,7 +88031,7 @@ async function curateMany(organizeOptions, onProgress) {
87966
88031
  }
87967
88032
  dispatchMappingJobs();
87968
88033
  } catch (error2) {
87969
- reject(error2);
88034
+ rejectCallback(error2);
87970
88035
  }
87971
88036
  });
87972
88037
  }
@@ -12055,6 +12055,7 @@ var filesMapped = 0;
12055
12055
  var workerCurrentFile = /* @__PURE__ */ new Map();
12056
12056
  var lastWorkerProgressTime = 0;
12057
12057
  var pendingReplacements = 0;
12058
+ var aborted = false;
12058
12059
  var currentFileInfoIndex;
12059
12060
  var filesToProcess = [];
12060
12061
  var directoryScanFinished = false;
@@ -12077,12 +12078,35 @@ function setScanResumeCallback(cb) {
12077
12078
  function markScanPaused() {
12078
12079
  scanPaused = true;
12079
12080
  }
12081
+ function terminateAllWorkers() {
12082
+ aborted = true;
12083
+ while (availableMappingWorkers.length) {
12084
+ availableMappingWorkers.pop().terminate();
12085
+ }
12086
+ for (const [worker] of workerCurrentFile) {
12087
+ try {
12088
+ worker.terminate();
12089
+ } catch {
12090
+ }
12091
+ }
12092
+ workerCurrentFile.clear();
12093
+ filesToProcess.length = 0;
12094
+ workersActive = 0;
12095
+ pendingReplacements = 0;
12096
+ directoryScanFinished = false;
12097
+ scanPaused = false;
12098
+ scanResumeCallback = null;
12099
+ }
12100
+ function isAborted() {
12101
+ return aborted;
12102
+ }
12080
12103
  async function initializeMappingWorkers(skipCollectingMappings, fileInfoIndex, progressCb, workerCount) {
12081
12104
  mappingWorkerOptions = {};
12082
12105
  workersActive = 0;
12083
12106
  mapResultsList = skipCollectingMappings ? void 0 : [];
12084
12107
  filesMapped = 0;
12085
12108
  pendingReplacements = 0;
12109
+ aborted = false;
12086
12110
  workerCurrentFile.clear();
12087
12111
  lastWorkerProgressTime = Date.now();
12088
12112
  currentFileInfoIndex = fileInfoIndex;
@@ -12098,6 +12122,8 @@ async function initializeMappingWorkers(skipCollectingMappings, fileInfoIndex, p
12098
12122
  availableMappingWorkers.push(...workers);
12099
12123
  }
12100
12124
  async function dispatchMappingJobs() {
12125
+ if (aborted)
12126
+ return;
12101
12127
  while (filesToProcess.length > 0 && availableMappingWorkers.length > 0) {
12102
12128
  const { fileInfo, previousFileInfo } = filesToProcess.pop();
12103
12129
  const mappingWorker = availableMappingWorkers.pop();
@@ -12160,6 +12186,8 @@ async function getHardwareConcurrency() {
12160
12186
  return cpus().length;
12161
12187
  }
12162
12188
  function recoverCrashedWorker(mappingWorker, errorMessage) {
12189
+ if (aborted)
12190
+ return;
12163
12191
  if (!workerCurrentFile.has(mappingWorker)) {
12164
12192
  return;
12165
12193
  }
@@ -12195,6 +12223,10 @@ function recoverCrashedWorker(mappingWorker, errorMessage) {
12195
12223
  pendingReplacements += 1;
12196
12224
  void createMappingWorker().then((worker) => {
12197
12225
  pendingReplacements -= 1;
12226
+ if (aborted) {
12227
+ worker.terminate();
12228
+ return;
12229
+ }
12198
12230
  availableMappingWorkers.push(worker);
12199
12231
  dispatchMappingJobs();
12200
12232
  }).catch((error) => {
@@ -12224,6 +12256,8 @@ async function createMappingWorker() {
12224
12256
  });
12225
12257
  }
12226
12258
  mappingWorker.addEventListener("message", (event) => {
12259
+ if (aborted)
12260
+ return;
12227
12261
  if (event.data.response === "lookup") {
12228
12262
  const outputPath = event.data.outputPath;
12229
12263
  const entry = currentFileInfoIndex?.[OUTPUT_FILE_PREFIX + outputPath];
@@ -12299,9 +12333,11 @@ export {
12299
12333
  getWorkerCurrentFile,
12300
12334
  getWorkersActive,
12301
12335
  initializeMappingWorkers,
12336
+ isAborted,
12302
12337
  markScanPaused,
12303
12338
  scanAnomalies,
12304
12339
  setDirectoryScanFinished,
12305
12340
  setMappingWorkerOptions,
12306
- setScanResumeCallback
12341
+ setScanResumeCallback,
12342
+ terminateAllWorkers
12307
12343
  };
@@ -39,6 +39,18 @@ export declare function setScanResumeCallback(cb: (() => void) | null): void;
39
39
  * index.ts when the queue exceeds the high-water mark.
40
40
  */
41
41
  export declare function markScanPaused(): void;
42
+ /**
43
+ * Hard-terminate all workers (idle and active) and reset pool state.
44
+ * Called when curateMany is aborted via AbortSignal. Equivalent to a
45
+ * tab reload — partially written files are handled by hash checks on
46
+ * the next run.
47
+ */
48
+ export declare function terminateAllWorkers(): void;
49
+ /**
50
+ * Whether the current run has been aborted. Used by worker message handlers
51
+ * to bail out on messages arriving after teardown.
52
+ */
53
+ export declare function isAborted(): boolean;
42
54
  /**
43
55
  * Initialize the mapping worker pool. Call once per curateMany invocation.
44
56
  */
@@ -33,6 +33,7 @@ export type OrganizeOptions = {
33
33
  fileInfoIndex?: TFileInfoIndex;
34
34
  excludedPathGlobs?: string[];
35
35
  workerCount?: number;
36
+ signal?: AbortSignal;
36
37
  } & ({
37
38
  inputType: 'directory';
38
39
  inputDirectory: FileSystemDirectoryHandle;
@@ -126940,6 +126940,10 @@
126940
126940
  // The termination condition in dispatchMappingJobs() waits for this to reach 0
126941
126941
  // before finishing, to avoid orphaning in-flight replacements.
126942
126942
  let pendingReplacements = 0;
126943
+ // Set to true when curateMany is aborted via AbortSignal. Guards dispatch,
126944
+ // crash recovery, and worker message handlers against acting on stale state
126945
+ // after teardown.
126946
+ let aborted = false;
126943
126947
  // Stored fileInfoIndex from initializeMappingWorkers, used for lookup
126944
126948
  // responses when workers query for previousMappedFileInfo.
126945
126949
  let currentFileInfoIndex;
@@ -126984,6 +126988,36 @@
126984
126988
  function markScanPaused() {
126985
126989
  scanPaused = true;
126986
126990
  }
126991
+ /**
126992
+ * Hard-terminate all workers (idle and active) and reset pool state.
126993
+ * Called when curateMany is aborted via AbortSignal. Equivalent to a
126994
+ * tab reload — partially written files are handled by hash checks on
126995
+ * the next run.
126996
+ */
126997
+ function terminateAllWorkers() {
126998
+ aborted = true;
126999
+ // Terminate idle workers
127000
+ while (availableMappingWorkers.length) {
127001
+ availableMappingWorkers.pop().terminate();
127002
+ }
127003
+ // Terminate active workers (those with an in-flight file)
127004
+ for (const [worker] of workerCurrentFile) {
127005
+ try {
127006
+ worker.terminate();
127007
+ }
127008
+ catch {
127009
+ /* already terminated */
127010
+ }
127011
+ }
127012
+ workerCurrentFile.clear();
127013
+ // Clear the queue and reset counters
127014
+ filesToProcess.length = 0;
127015
+ workersActive = 0;
127016
+ pendingReplacements = 0;
127017
+ directoryScanFinished = false;
127018
+ scanPaused = false;
127019
+ scanResumeCallback = null;
127020
+ }
126987
127021
  /**
126988
127022
  * Initialize the mapping worker pool. Call once per curateMany invocation.
126989
127023
  */
@@ -126993,6 +127027,7 @@
126993
127027
  mapResultsList = skipCollectingMappings ? undefined : [];
126994
127028
  filesMapped = 0;
126995
127029
  pendingReplacements = 0;
127030
+ aborted = false;
126996
127031
  workerCurrentFile.clear();
126997
127032
  lastWorkerProgressTime = Date.now();
126998
127033
  currentFileInfoIndex = fileInfoIndex;
@@ -127011,6 +127046,8 @@
127011
127046
  * replacements, scan finished) and emits the 'done' progress message.
127012
127047
  */
127013
127048
  async function dispatchMappingJobs() {
127049
+ if (aborted)
127050
+ return;
127014
127051
  while (filesToProcess.length > 0 && availableMappingWorkers.length > 0) {
127015
127052
  const { fileInfo, previousFileInfo } = filesToProcess.pop();
127016
127053
  const mappingWorker = availableMappingWorkers.pop();
@@ -127097,6 +127134,9 @@
127097
127134
  * on('exit'), and the stall watchdog.
127098
127135
  */
127099
127136
  function recoverCrashedWorker(mappingWorker, errorMessage) {
127137
+ // Bail out if processing has been aborted — no recovery needed.
127138
+ if (aborted)
127139
+ return;
127100
127140
  // Guard against double-recovery (e.g., both onerror and on('exit') firing
127101
127141
  // for the same crash). Without this, workersActive could go negative.
127102
127142
  if (!workerCurrentFile.has(mappingWorker)) {
@@ -127137,6 +127177,12 @@
127137
127177
  void createMappingWorker()
127138
127178
  .then((worker) => {
127139
127179
  pendingReplacements -= 1;
127180
+ // If processing was aborted while the replacement was being created,
127181
+ // terminate it immediately instead of adding it to the pool.
127182
+ if (aborted) {
127183
+ worker.terminate();
127184
+ return;
127185
+ }
127140
127186
  availableMappingWorkers.push(worker);
127141
127187
  dispatchMappingJobs();
127142
127188
  })
@@ -127174,6 +127220,9 @@
127174
127220
  });
127175
127221
  }
127176
127222
  mappingWorker.addEventListener('message', (event) => {
127223
+ // Ignore messages from workers after abort — the pool is torn down.
127224
+ if (aborted)
127225
+ return;
127177
127226
  // Handle lookup requests from the worker. The worker sends these when
127178
127227
  // curateOne needs to check if a mapped file was already uploaded
127179
127228
  // (previousMappedFileInfo). The index is kept on the main thread to
@@ -127423,7 +127472,14 @@
127423
127472
  setDirectoryScanFinished(true);
127424
127473
  }
127425
127474
  async function curateMany(organizeOptions, onProgress) {
127475
+ // Early rejection for pre-aborted signal — don't create any workers.
127476
+ if (organizeOptions.signal?.aborted) {
127477
+ return Promise.reject(new DOMException('The operation was aborted.', 'AbortError'));
127478
+ }
127426
127479
  return new Promise(async (resolve, reject) => {
127480
+ // Prevents double-settle when abort races with natural completion.
127481
+ let settled = false;
127482
+ const signal = organizeOptions.signal;
127427
127483
  // Stall watchdog: if no mapping worker at all has reported back for 10
127428
127484
  // minutes (i.e., all active workers are stuck), terminate them and count
127429
127485
  // their in-flight files as mapping errors. This guards against undetectable
@@ -127453,15 +127509,46 @@
127453
127509
  // Progress callback wraps the user's callback and handles lifecycle
127454
127510
  const progressCallback = (msg) => {
127455
127511
  onProgress?.(msg);
127456
- if (msg.response === 'done') {
127512
+ if (msg.response === 'done' && !settled) {
127513
+ settled = true;
127457
127514
  clearInterval(stallWatchdog);
127515
+ signal?.removeEventListener('abort', onAbort);
127458
127516
  resolve(msg);
127459
127517
  }
127460
127518
  };
127461
127519
  const rejectCallback = (reason) => {
127520
+ if (settled)
127521
+ return;
127522
+ settled = true;
127462
127523
  clearInterval(stallWatchdog);
127524
+ signal?.removeEventListener('abort', onAbort);
127463
127525
  reject(reason);
127464
127526
  };
127527
+ // Reference to the scan worker, hoisted so the abort handler can
127528
+ // terminate it. Assigned later when a directory/path/s3 input is used.
127529
+ let fileListWorker;
127530
+ const onAbort = () => {
127531
+ // Terminate the scan worker if it exists
127532
+ try {
127533
+ fileListWorker?.terminate();
127534
+ }
127535
+ catch {
127536
+ /* already terminated */
127537
+ }
127538
+ // Hard-terminate all mapping workers and reset pool state
127539
+ terminateAllWorkers();
127540
+ // Reject with standard AbortError
127541
+ rejectCallback(new DOMException('The operation was aborted.', 'AbortError'));
127542
+ };
127543
+ if (signal) {
127544
+ // Re-check in case abort happened between the early check and here
127545
+ if (signal.aborted) {
127546
+ clearInterval(stallWatchdog);
127547
+ rejectCallback(new DOMException('The operation was aborted.', 'AbortError'));
127548
+ return;
127549
+ }
127550
+ signal.addEventListener('abort', onAbort, { once: true });
127551
+ }
127465
127552
  try {
127466
127553
  // create the mapping workers
127467
127554
  await initializeMappingWorkers(organizeOptions.skipCollectingMappings, organizeOptions.fileInfoIndex, progressCallback, organizeOptions.workerCount);
@@ -127476,7 +127563,7 @@
127476
127563
  if (organizeOptions.inputType === 'directory' ||
127477
127564
  organizeOptions.inputType === 'path' ||
127478
127565
  organizeOptions.inputType === 's3') {
127479
- const fileListWorker = await initializeFileListWorker(rejectCallback);
127566
+ fileListWorker = await initializeFileListWorker(rejectCallback);
127480
127567
  // Wire up backpressure resume: when the dispatch loop drains the
127481
127568
  // queue below the low-water mark, it calls this to resume scanning.
127482
127569
  setScanResumeCallback(() => {
@@ -127543,7 +127630,7 @@
127543
127630
  dispatchMappingJobs();
127544
127631
  }
127545
127632
  catch (error) {
127546
- reject(error);
127633
+ rejectCallback(error);
127547
127634
  }
127548
127635
  });
127549
127636
  }