API for downloading study or multiple studies

One of the needed elements of the Sonador Viewer is the ability for users to download one (or more) studies. For this reason a "Download" button was added to the Bulk Actions menu. Unfortunately, the bulk action "Download" button does not appear to currently work.

Screenshot_2023-09-14_at_20.47.45

During the Sonador 0.4 development cycle, a download endpoint was added to the API which allows for a zip archive of a study to be downloaded. If users have a view permission, they are able to access an action from the "Action" menu which allows for download of DICOM files in the study. That functionality needs to be extended to the bulk actions menu. As part of updating the "Download" button to work, a refactor of the Downloads system is desirable.

General notes:

  • Downloads should be queued to a "Download Manager" which is able to handle retrieval, reporting, and status.
  • It should be possible to review current and recent downloads from a "Downloads" dropdown menu which appears to the left of "Offline Storage" toolbar button.
    • The downloads status icon should show a count of active downloads.
    • Clicking on the icon shows a dropdown with the study (and series) downloads in progress, along with a status indicator displaying overall progress. Downloads are zip archives, so the progress bar should be driven by the overall size of the downloaded file.
    • Because there is a delay between when a request is made and when the server will respond with the file stream there should be a couple of states for the download progress.
      • While the request is being processed (but the size of the file archive isn't known), there should be a label which says "Processing" (or something like that, utilizing UI best practices).
      • Once the download beings and the archive size is known, the progress bar should be displayed (and should be blue).
      • As the file streams, the progress should be updated. Once the download is complete, the color should change to green.
    • Each active download should have a "Cancel" button which can be used to terminate the download. Cancelling the download changes the status label to "Cancelled" and grays the progress bar.
    • Cancelled or complete download entries should have a "Clear" button which can be used to remove the download from the menu.
  • When downloads are added to the queue via the "action" menu, an "info" user notification including common patient, study, and (if downloading a single series) series attributes (these should be taken from the studylist DICOM metadata).
  • When a download completes, a "success" notification should be displayed notifying the user that the download has been completed. Common patient, study, and (for download of a single series) series attributes should be displayed.

Specification

1. Overview

Archive export ("download this study to my computer as a .zip") becomes a first-class, observable, cancellable operation instead of a fire-and-forget fetch that either silently succeeds or silently fails.

Three things change from a user's point of view:

  1. The bulk-actions Download button works. Selecting N studies and clicking Download queues N archive exports.
  2. A Downloads control appears in the Studylist toolbar, immediately to the left of the existing Offline Storage control, with a badge counting in-flight exports. Its dropdown lists each export with a state label, a progress bar, Cancel, and Clear.
  3. Queueing and completing an export raise user notifications identifying the patient, study, and (for a series export) the series.

Scoping headline: this is about exporting a zip archive to the user's file system. It is a different operation, with a different destination, from saving a study into browser storage for offline viewing. Those two must remain visibly and structurally distinct (see AR-1, §6).

2. Background and current state

2.1 The client today

platform/viewer/src/api/ext.js holds two archive helpers:

Helper Request Behavior
fetchDownloadStudies(server, studyId) GET {server.wadoRoot}/studies/{studyId}/archive, Authorization: Bearer {token} await response.blob(), object URL, synthetic <a download="{studyId}.zip"> click, revoke
fetchDownloadSeries(server, seriesId) GET {server.wadoRoot}/series/{seriesId}/archive, same headers Same, {seriesId}.zip

Both buffer the entire response through response.blob(), which yields no progress information at all, and both swallow failures into console.error — a failed export is indistinguishable from a slow one.

Call sites:

  • platform/viewer/src/components/studyList/StudyListNG/components/SelectAndSettingsAndExpandCell/SelectAndSettingsAndExpandCell.js — the per-row Actions menu download item calls fetchDownloadStudies(activeServer, StudyInstanceUID). Gated on aclDownload, which is activeServer.perms.view || studyMeta.perms.View, resolved lazily by fetchStudyAclPermissions when the menu is opened. This path works today.
  • platform/viewer/src/components/studyList/StudyListNG/components/StudiesTableActions/StudiesTableActions.js — the bulk Download button renders with an icon and label and no onClick handler at all. That is the whole of the reported defect. It is also the only action in that toolbar that is not permission-gated. The offline-cache work explicitly left this button alone as belonging to the archive-export lineage rather than to the cache.
  • The viewer-side archive controls added under #36 (closed) are the other consumers of these helpers. They are not rewritten here; AR-5 routes them through the queue by construction.

A third, unrelated control also carries the word "Download": the viewer toolbar's More-menu Download item, which is CornerstoneViewportDownloadForm.js, a canvas screenshot/PNG export. It has nothing to do with DICOM archives and must not be touched or collided with.

2.2 The server endpoint

sonador_orthanc/web/download.py (StudyDICOMDownloadView, SeriesDICOMDownloadView) does not stream the archive itself. It verifies the resource exists and the caller is authorized, then answers HTTP 302 with Location set to the Orthanc instance's own archive URL (obj.pacs.orthanc_apiurl_fqdn(obj.filearchive_url, internal_dns=False)). The browser follows the redirect transparently; the bytes come from Orthanc.

Two consequences the implementation must respect:

  • Do not change the request shape. The existing single-study path works against this redirect today (headers, credentials mode, no explicit redirect option). Reproduce it exactly and add streaming on top; do not "improve" it into a request that has never been proven against the deployed gateway.
  • Content-Length may or may not be present. Content-Length is a CORS-safelisted response header, so it is readable from JavaScript even when the final response comes from a different origin than the gateway. Whether Orthanc sends it for an archive response — rather than answering chunked — has not been confirmed against a live server. The design must not depend on it (FR-6).

2.3 What already exists and must not be disturbed

The offline-study cache landed recently and occupies adjacent screen real estate and adjacent vocabulary:

Component File Role
DownloadManagerService platform/core/src/services/LocalCacheService/DownloadManagerService.ts Queue that pulls a study's instances via WADO-RS into IndexedDB for offline viewing
LocalCacheService platform/core/src/services/LocalCacheService/LocalCacheService.ts The IndexedDB persistence layer behind it
DownloadManagerModal .../StudyListNG/components/DownloadManagerModal/DownloadManagerModal.js "Offline Storage" dialog: Active Transfers + Offline Studies tabs
Offline Storage toolbar button .../StudyListNG/components/Filters/Filters.js (styles.downloadManager) Opens that modal; badge shows in-flight cache jobs
useLocalCacheVersion .../StudyListNG/hooks/useLocalCacheVersion.js Throttled re-render hook subscribed to both services

None of these move, change name, or change behavior. DownloadManagerService is a module singleton independent of the React tree, so navigation does not interrupt its jobs — that is the pattern to copy (AR-2).

2.4 Notification and logging surfaces

uiNotificationService.show({...}) (platform/core/src/services/UINotificationService/index.ts) is the single notification pathway. Relevant contract details:

  • Accepted keys: title, message, type (success | error | info | warning | loading), duration, position, autoClose, action, id, promise/promiseMessages, log, source, studyInstanceUID, seriesInstanceUID, details, error.
  • autoClose: false maps to an infinite duration — the notification stays until dismissed.
  • Write-through to the unified log is automatic for error and warning; info and success are recorded only when the caller passes log: true.
  • A message-only call promotes message to title, so pass both explicitly.

notificationLogService backs the viewer's Issues list. details is the structured diagnostics slot and carries { url, status, body } for a failed request.

3. Functional requirements

FR-1 — Bulk download queues one export per selected study. With N studies selected, clicking Download enqueues N independent archive-export jobs and clears the row selection, matching the commit-clears-selection convention already used by the other bulk actions. There is no combined multi-study archive (see §8).

FR-2 — Bulk Download respects the view permission. The button renders only when the active server grants view, matching the gating already applied to View and to Save Offline Copy in the same toolbar. It remains disabled while no rows are selected.

FR-3 — Every archive export is a tracked job. Exports raised from the per-row Actions menu, from the bulk action, and from any other current caller of the archive helpers all become jobs in the same queue and all appear in the Downloads dropdown. No code path may download an archive without a job.

FR-4 — A Downloads control sits in the Studylist toolbar. It is placed in the toolbar's header controls immediately before the Offline Storage control. It carries a badge showing the number of jobs currently queued, processing, or downloading; the badge is absent at zero. It appears on every surface that renders the shared Studylist toolbar (Studies, Worklist, Shared).

FR-5 — The dropdown lists jobs newest first. Each row shows:

  • a primary line identifying the resource — PatientName (PatientID) · Service Episode {id}, falling back to the UID when no descriptor fields are available;
  • the study description on its own line, quoted, when present;
  • for a series job, an additional line Series {SeriesNumber}: {SeriesDescription};
  • a progress bar (FR-6);
  • a status line combining the state label with transferred bytes, and the error message when the job failed.

Empty state reads "No downloads".

FR-6 — Progress has determinate and indeterminate forms. The bar's appearance is driven by job state and by whether the total size is known:

State Bar Label
Queued (waiting for a concurrency slot) indeterminate, neutral "Queued"
Processing (request sent, response headers not yet received) indeterminate, neutral "Processing"
Downloading, total size known determinate, blue, bytesReceived / totalBytes "Downloading — {received} of {total}"
Downloading, total size unknown indeterminate, blue "Downloading — {received}"
Completed full, green "Completed — {total}"
Cancelled frozen at last position, gray "Cancelled"
Error frozen at last position, gray "Failed — {message}"

Byte counts render through OHIF.utils.formatBytes, as the Offline Storage dialog does.

FR-7 — Active jobs can be cancelled. A Cancel control on each queued/processing/downloading row aborts the request, discards every buffered byte without writing a file, and moves the job to Cancelled. Cancelling a job that has not yet started simply removes it from the pending queue.

FR-8 — Terminal jobs can be cleared. A Clear control on each completed/cancelled/failed row removes that row. A "Clear finished" control in the dropdown header removes all terminal rows at once and is disabled when there are none. Clearing never affects an active job and never touches a downloaded file.

FR-9 — Queueing raises an info notification. Enqueueing a single job shows type: 'info' with title "Download queued" and a message carrying the descriptor line from FR-5. Descriptor values come from DicomMetadataStore study metadata at the call site — PatientName, PatientID, StudyDescription, AccessionNumber, ServiceEpisodeID, plus SeriesNumber, SeriesDescription and Modality for a series job.

FR-10 — Bulk queueing raises one notification, not N. When a single user action enqueues more than three jobs, one aggregate info notification is shown ("{n} studies queued for download") in place of the per-job notifications. Three or fewer are announced individually.

FR-11 — Completion raises a success notification. type: 'success', title "Download complete", message carrying the same descriptor line plus the archive size.

FR-12 — Failure is visible and diagnosable. A failed job shows type: 'error' with autoClose: false, carrying studyInstanceUID (and seriesInstanceUID where applicable) and details: { url, status, body }. Errors are recorded in the unified log automatically; the info and success notifications above are transient and are not logged.

FR-13 — Cancellation is silent. A user-initiated cancel raises no notification and writes no log entry. The dropdown row is the feedback.

FR-14 — Duplicate requests are de-duplicated. Requesting an export for a resource that already has a queued/processing/downloading job returns the existing job rather than starting a second one. Re-running a bulk action over a partially-queued selection is therefore harmless.

FR-15 — Jobs survive navigation. Moving between Studies, Worklist, and Shared, opening and closing the dropdown, or unmounting the Studylist entirely does not pause, cancel, or lose a job. Reopening the dropdown shows the same jobs with current progress.

FR-16 — The saved file keeps a sensible name. When the response exposes a Content-Disposition filename, use it. Otherwise fall back to {StudyInstanceUID}.zip / {SeriesInstanceUID}.zip, preserving today's behavior.

4. Architectural requirements

AR-1 — Archive export is a separate service from the offline cache, and separately named. The new service is ArchiveDownloadService. DownloadManagerService continues to mean the offline/IndexedDB cache queue and is not renamed. The two queues, the two toolbar controls, and the two badges are independent: an archive export never appears in the Offline Storage dialog, and a cache job never appears in the Downloads dropdown. The user-facing labels are "Downloads" (archive export, destination: the user's disk) and "Offline Storage" (cache, destination: this browser). This distinction is the one thing most likely to be lost in implementation — every new symbol, style class, and string must make clear which of the two it belongs to.

AR-2 — ArchiveDownloadService is a module singleton outside the React tree. Same shape as DownloadManagerService: a class extending PubSubService, instantiated once at module scope, exported from platform/core/src/services/index.js and platform/core/src/index.js. This is what makes FR-15 true. Do not model jobs in a zustand store or a context provider.

AR-3 — Job state is in memory only; nothing is persisted. DownloadManagerService persists job metadata to IndexedDB because its work product (cached instances) survives a reload. An archive export's work product is a byte stream held in memory; a reload destroys it and it cannot be resumed. Persisting job rows would therefore only ever restore rows that are already dead. Jobs are dropped on reload, deliberately, and this divergence from the sibling service is documented in the service header.

AR-4 — Do not re-export the bare name JOB_STATES, and keep command/element IDs distinct. @ohif/core already exports JOB_STATES for the cache queue. The archive states export as ARCHIVE_JOB_STATES, and the events as ArchiveDownloadServiceEvents, mirroring DownloadManagerServiceEvents. Three UI identifiers already read as "download" in this codebase and none may be reused or shadowed: the study-list Action-menu item id: 'download' (archive export, this feature), the viewer More-menu item id: 'Download' (canvas screenshot export, unrelated), and the offline-cache commands goOffline / removeOffline / cancelStudyDownload. New identifiers introduced here are scoped to archive export — e.g. archiveDownloadStudy, archiveDownloadCancel.

AR-5 — The existing archive helpers keep their signatures and delegate. fetchDownloadStudies(server, studyId) and fetchDownloadSeries(server, seriesId) remain exported from platform/viewer/src/api/ext.js with their current parameter lists, gain an optional third descriptor argument, and have their bodies replaced by a call into ArchiveDownloadService. This routes every current and future caller through the queue without an exhaustive call-site census, and satisfies FR-3 by construction. See §6.

AR-6 — Reactivity follows the useLocalCacheVersion precedent. A dedicated useArchiveDownloadVersion hook subscribes to the archive service's events and bumps a version counter, throttled to at most one re-render per 200 ms so that a fast stream does not re-render the toolbar hundreds of times a second. Do not extend useLocalCacheVersion to cover both services — that would couple the two features and make an archive export re-render the offline badge.

AR-7 — Streaming reads the response body incrementally. Use response.body.getReader() and accumulate chunks, rather than response.blob(). Progress is emitted from the read loop. Cancellation is an AbortController plus reader.cancel(). The final file is assembled from the accumulated chunks only after the stream completes.

AR-8 — Progress events are throttled at the source. The service emits a progress event at most every 200 ms or every 1 MB, whichever comes first, in addition to a final event when the stream ends. A 5 GB archive must not produce tens of thousands of events.

AR-9 — The dropdown does not dismiss on interaction. Radix menu items close their menu on activation, which is wrong for Cancel and Clear. Render the panel body as ordinary elements inside the dropdown content rather than as menu items, so acting on one row leaves the panel open. Follow the toolbar's existing Radix usage for the trigger, tooltip, and content styling.

AR-10 — Concurrency is bounded. At most two archive exports run concurrently; the rest wait in a pending queue. Archive exports and offline-cache jobs have independent limits and do not contend for slots with each other.

5. Implementation specification

5.1 Job model

Field Type Notes
id string archive-{uid}-{timestamp}
kind 'study' | 'series' Selects the endpoint and the descriptor rendering
StudyInstanceUID string Always present
SeriesInstanceUID string? Present for kind === 'series'
state ARCHIVE_JOB_STATES QUEUED, PROCESSING, DOWNLOADING, COMPLETED, CANCELLED, ERROR
bytesReceived number Running total from the read loop
totalBytes number | null From Content-Length; null when not exposed — drives FR-6's determinate/indeterminate split
filename string Resolved per FR-16
error string? Human-readable failure summary
createdAt / startedAt / completedAt number Epoch ms; createdAt drives newest-first ordering
PatientName, PatientID, StudyDescription, AccessionNumber, ServiceEpisodeID string? Descriptor, supplied at enqueue
SeriesNumber, SeriesDescription, Modality string? Series descriptor

PatientName values arriving naturalized (array or { Alphabetic } object) are normalized to a string at enqueue, as DownloadManagerService does.

5.2 State transitions

enqueue ──► QUEUED ──(slot free)──► PROCESSING ──(headers received)──► DOWNLOADING ──► COMPLETED
              │                          │                                 │
              └──────────────────────────┴────────────► CANCELLED ◄────────┘
                                         │                                 │
                                         └──────────► ERROR ◄──────────────┘

COMPLETED, CANCELLED, and ERROR are terminal. Only terminal jobs can be dismissed; only non-terminal jobs can be cancelled.

5.3 Events

Event Payload Emitted when
event::archiveDownloadService:jobQueued { job } A job enters the queue
event::archiveDownloadService:jobProgress { job } Throttled per AR-8, plus once at stream end
event::archiveDownloadService:jobStateChanged { job } Any state transition, and on dismiss

5.4 Public API

enqueueStudy({ server, StudyInstanceUID, descriptor }) -> job
enqueueSeries({ server, StudyInstanceUID, SeriesInstanceUID, descriptor }) -> job
listJobs() -> job[]                    // newest first
listActiveJobs() -> job[]              // QUEUED | PROCESSING | DOWNLOADING
getActiveJobForResource(uid) -> job?   // de-duplication, FR-14
cancel(jobId)
dismiss(jobId)                         // terminal only
clearTerminal()
EVENTS, STATES

5.5 Download routine

  1. Build the URL with utils.urlUtil.urlJoin(server.wadoRoot, kind === 'study' ? 'studies' : 'series', uid, 'archive') — identical to the current helpers.
  2. fetch(url, { headers: { Authorization: 'Bearer ' + sonador.getAuthToken() }, signal }). Transition to PROCESSING before awaiting.
  3. On resolution: if !response.ok, transition to ERROR with { url, status, body } captured for the notification's details.
  4. Read Content-Length into totalBytes (null when absent or unparseable) and resolve filename from Content-Disposition when readable. Transition to DOWNLOADING.
  5. Loop reader.read(), pushing chunks and advancing bytesReceived, emitting throttled progress.
  6. On completion, assemble new Blob(chunks, { type: 'application/zip' }), create an object URL, click a synthetic anchor with download = filename, remove it, and revoke the URL. Transition to COMPLETED with totalBytes set to the final byte count. Release the chunk array.
  7. On AbortError, discard the chunks, write no file, transition to CANCELLED.
  8. On any other throw, transition to ERROR.
  9. In all cases, release the concurrency slot and pump the pending queue.

Memory note for the implementer: the whole archive is held in memory before the file is written, which is what the current implementation already does. This is acceptable for the study sizes in scope and is called out in §8 as a known bound rather than silently accepted.

5.6 Files

platform/core/src/services/ArchiveDownloadService/index.ts (new) Service class per §5.1–§5.5. Header comment states the AR-1 distinction from DownloadManagerService, the AR-3 no-persistence rationale, and the AR-7 streaming rationale, in the style of the sibling service.

platform/core/src/services/index.js Export ArchiveDownloadService, ArchiveDownloadServiceEvents, ARCHIVE_JOB_STATES.

platform/core/src/index.js Re-export the same three symbols, and add ArchiveDownloadService to the OHIF default object alongside DownloadManagerService.

platform/viewer/src/api/ext.js Rewrite fetchDownloadStudies and fetchDownloadSeries per AR-5: same names, same first two parameters, optional third descriptor, bodies reduced to an enqueueStudy / enqueueSeries call returning the job. Retain the existing doc comments, updated to say the call now queues rather than downloads inline.

.../StudyListNG/components/StudiesTableActions/StudiesTableActions.js Add handleDownloadSelectedStudies: for each selected row resolve the StudyInstanceUID via the shared _getStudyInstanceUID helper, read the descriptor from DicomMetadataStore.getStudyMetadata, enqueue, then clearSelection(). Wire it to the existing Download button, add the activeServer?.perms?.view gate (FR-2), and apply the FR-10 aggregate-notification rule for the batch. Mirror the structure of the adjacent handleSaveOfflineSelectedStudies, and comment the distinction between the two.

.../StudyListNG/components/SelectAndSettingsAndExpandCell/SelectAndSettingsAndExpandCell.js The download item passes the study descriptor it already has in studyMeta as the new third argument. No other change; the existing aclDownload gate is correct.

.../StudyListNG/components/DownloadsMenu/DownloadsMenu.js (new) Trigger button (cloud-download icon, badge, tooltip) plus dropdown panel. Uses useArchiveDownloadVersion for reactivity and reads job state directly from the service singleton on each render, as DownloadManagerModal does. Panel body rendered as plain elements per AR-9. Strings go through the StudyList i18n namespace.

.../StudyListNG/components/DownloadsMenu/DownloadsMenu.module.scss (new) Progress track/fill geometry reuses the treatment in DownloadManagerModal.module.scss; state colors and the badge draw from the existing toolbar tokens rather than new literals.

.../StudyListNG/hooks/useArchiveDownloadVersion.js (new) Direct analogue of useLocalCacheVersion, subscribed to the archive service's three events, 200 ms throttle.

.../StudyListNG/components/Filters/Filters.js Render <DownloadsMenu /> inside the header controls, immediately before the existing Offline Storage button (FR-4).

platform/core/src/services/ArchiveDownloadService/ArchiveDownloadService.test.js (new) Unit coverage per §7's testable behaviors: de-duplication, cancel-before-start, cancel-mid-stream discarding bytes, absent Content-Length producing totalBytes === null, progress throttling, terminal-only dismiss, and the concurrency bound. Mock fetch with a ReadableStream body; follow the setup in UINotificationService.test.js.

5.7 Verification task for the implementer

Confirm against a running server whether the archive response carries Content-Length, and record the answer in this issue. If it does, the determinate path in FR-6 is the normal case and the indeterminate path is a fallback; if it does not, the indeterminate path is the normal case and the note in §2.2 should be corrected. Either way the code supports both — this is a documentation-accuracy task, not a design fork.

6. Refactor analysis

The archive helpers in platform/viewer/src/api/ext.js are the only entry points to archive export. Rather than find and rewrite every caller, the helpers themselves become thin queue adapters (AR-5) — the lightest touch that satisfies FR-3.

Consumer Disposition
fetchDownloadStudies Body replaced by ArchiveDownloadService.enqueueStudy. Signature preserved plus an optional descriptor.
fetchDownloadSeries Body replaced by ArchiveDownloadService.enqueueSeries. Signature preserved plus an optional descriptor.
Per-row Actions menu download item (SelectAndSettingsAndExpandCell.js) In scope. Passes a descriptor; otherwise unchanged. Reference call site for the pattern.
Bulk Download button (StudiesTableActions.js) In scope. Gains a handler and a permission gate.
Viewer-side archive controls from #36 (closed) Routed through the queue automatically by the adapter. Locate them during implementation and add descriptors where study or series metadata is already in hand; a caller that passes no descriptor still produces a valid job that renders by UID.
Viewer More-menu Download (canvas screenshot export) Untouched. Not an archive export; shares only the word.
DownloadManagerService / offline cache Untouched.

Normative convention for future work: any code that produces a .zip archive for the user goes through ArchiveDownloadService. A direct fetch of an /archive URL followed by an anchor click is a defect — it produces an export the user cannot see, monitor, or cancel.

7. Acceptance criteria

  • Selecting five studies and clicking Download produces five queued jobs, five saved .zip files, and a Downloads badge that counts down to zero as they finish.
  • The bulk Download button is absent for a user whose active server does not grant view, and disabled whenever no rows are selected.
  • The per-row Actions menu Download continues to work and now produces a job visible in the Downloads dropdown.
  • The Downloads control renders immediately to the left of Offline Storage, on the Studies, Worklist, and Shared surfaces alike.
  • A job shows "Processing" with an indeterminate bar between the click and the arrival of response headers, then switches to a blue bar, then to green on completion.
  • A response with no readable Content-Length still downloads to completion, showing an indeterminate blue bar and a live transferred-bytes count throughout.
  • Cancelling a job mid-stream stops the transfer, saves no file, sets the label to "Cancelled", and grays the bar.
  • Cancelling a job that is still queued removes it from the pending set without ever issuing a request.
  • Clear removes a completed, cancelled, or failed row; it is not offered on an active row; "Clear finished" empties all terminal rows and is disabled when there are none.
  • Queueing a single export raises one info notification naming the patient, study, and — for a series export — the series; a bulk action over more than three studies raises one aggregate notification instead of one per study.
  • Completing an export raises a success notification naming the same attributes plus the archive size.
  • A server error produces a failed job, a sticky error notification, and an entry in the Issues list carrying the request URL, HTTP status, and response body.
  • A user-initiated cancel raises no notification and adds no entry to the Issues list.
  • Requesting an export for a study that already has one in flight returns the existing job; no second request is issued and no second row appears.
  • Navigating from Studies to Worklist and back mid-download leaves the job running with correct progress, and the dropdown reflects it on reopen.
  • Opening the dropdown while a job completes shows the row transition to green live, without closing the panel.
  • Cancelling or clearing a row leaves the dropdown open.
  • Archive exports never appear in the Offline Storage dialog's Active Transfers tab, and offline-cache jobs never appear in the Downloads dropdown; each badge counts only its own queue.
  • Starting an offline-cache save and an archive export for the same study at the same time leaves both running independently to completion.
  • Rapidly clicking Download several times on the same row produces exactly one job.
  • Unmounting the Studylist and remounting it does not duplicate event subscriptions or leak the throttle timer.
  • yarn build completes clean, and the new service's unit tests pass with no regression against the existing suite baseline.

8. Out of scope

  • A combined multi-study archive. N selected studies produce N files. A single archive containing several studies would need a new multi-resource endpoint on the gateway; worth doing, not here.
  • Resuming an export across a page reload. See AR-3.
  • Streaming directly to disk. The archive is buffered in memory before the file is written, as it is today. Writing through the File System Access API would lift the memory bound and support very large archives; it is a follow-up, not part of this change.
  • A series-level download control in the Studylist. The service supports series jobs and the series helper routes through it, so any existing caller is covered. Adding a new UI affordance for series export is separate work.
  • A Download action on the Worklist bulk toolbar. That toolbar shows View and Process and Update Status instead; changing its composition is out of scope.
  • Renaming DownloadManagerService or the Offline Storage control. See AR-1.

9. References

  • Origin notes: the General notes section at the top of this issue.
  • Viewer-side zip archive export (the helpers this refactors): #36 (closed)
  • Offline study cache (sibling queue, reference implementation; its AR-6 sets the naming conventions AR-4 continues): #125 (closed)
  • Unified notifications and logging: #84, #85
  • Reference files:
    • platform/core/src/services/LocalCacheService/DownloadManagerService.ts — job model, cooperative cancellation, bounded pool, module-singleton lifecycle
    • platform/core/src/services/UINotificationService/index.ts — notification contract and log write-through
    • platform/core/src/services/NotificationLogService/index.ts — unified log entry shape, details payload
    • platform/viewer/src/components/studyList/StudyListNG/components/DownloadManagerModal/DownloadManagerModal.js — list rendering, progress treatment, descriptor lines
    • platform/viewer/src/components/studyList/StudyListNG/hooks/useLocalCacheVersion.js — throttled service-to-render bridge
    • platform/viewer/src/components/studyList/StudyListNG/components/Filters/Filters.js — toolbar control, badge, tooltip conventions
    • platform/viewer/src/api/ext.js — the archive helpers being refactored
  • Server endpoint: sonador_orthanc/web/download.py in oak-tree/medical-imaging/orthanc-sonador
  • Platform docs: dev.ohif-frontend, dev.ohif-frontend.notifications, dev.ohif-frontend.service-overview

Project Tracking

OP#142

Edited by Rob Oakes