Skip to content

feat: Add WireCellsLocalAssetRepository - WPB-18848 #3400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from

Conversation

samwyndham
Copy link
Contributor

@samwyndham samwyndham commented Jul 28, 2025

TaskWPB-18848 [iOS] Create repository that returns local URL to assets

Issue

This PR introduces a WireCellsLocalAssetRepository. This repository is responsible for downloading and accessing files from wire cells. A WireCellsLocalAsset holds metadata about an asset (id, size, etc) and what its current download state is. The repository provides a way of observing these assets for a given ID. This will allow table view cells in the conversation to have live updates as things change - for example showing a download progress bar or updating if the name of a file is changed remotely.

Currently I'm not super happy about the implementation and may revisit this in a little when I come to using it in the project. However, the public APIs of WireCellsLocalAssetRepository and WireCellsLocalAsset are how I imagine they should be.

Testing

Run automated tests


Checklist

  • Title contains a reference JIRA issue number like [WPB-XXX].
  • Description is filled and free of optional paragraphs.
  • Adds/updates automated tests.

Copy link
Contributor

github-actions bot commented Jul 28, 2025

Test Results

62 tests   62 ✅  12s ⏱️
13 suites   0 💤
 1 files     0 ❌

Results for commit 10ce4b6.

♻️ This comment has been updated with latest results.

@samwyndham samwyndham requested review from a team, netbe, caldrian and KaterinaWire and removed request for a team and netbe July 28, 2025 09:34
Comment on lines +83 to +85
/// The size of the asset in bytes as defined by the backend or `-1` if unknown.

public let size: UInt64?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: if -1 as default, does it need to be optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh the comment is wrong here. When it gets stored in core data it will be -1. In the domain it will be optional. I will fix it. Thanks!

Comment on lines +46 to +48
public var cacheKey: String {
"\(nodeID.uuidString)-\(eTag)"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is it data layer or domain layer property?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that whole object should just be data layer. I will move it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: usually I'm against DTO in the name because it's not clear to which layer it belongs and what it maps form or to?
I'd suggest some other prefix+model, e.g. StoredModel, CachedModel, NetworkModel, etc.
Is it possible to find better name for this model?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This is still left over from the original implementation and I haven't got around to changing it yet. It should be the NetworkModel I believe. I'll fix it. Eventually it will move somewhere else.


final class WireCellsTests: XCTestCase {}
// sourcery: AutoMockable
public protocol FileCache: Sendable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: FileCache is about persistence but lives under WireCellsAPI which is network related code and FileCache is more like related to file system? Probably better put to some other folder?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI WireCellsAPI is not network related code - it's currently a big mix. It used to be a way of the different layers talking to each other. Julian is working on moving existing code from WireCells to WireMessaging after which we will follow our clean architecture approach. I will will move this to WireMessagingData once his PR is done.

Comment on lines +87 to +116
switch downloadStates[nodeID] {
case .downloading:
throw WireCellsLocalAssetRepositoryError.downloadAlreadyInProgress
case .error:
setDownloadState(nodeID: nodeID, state: .none)
case .none:
break
}

do {
var (node, metadata) = try await _refreshMetadata(nodeID: nodeID)
let (downloadURL, eTag) = try node.downloadInfo

let (progress, download) = fileDownloader.download(from: downloadURL)

for await progress in progress {
setDownloadState(nodeID: nodeID, state: .downloading(progress: progress, task: download))
}

let (tempURL, _) = try await download.value
try await fileCache.saveFile(at: tempURL, key: metadata.cacheKey)

if try metadataStore.assetMetadata(nodeID: nodeID)?.eTag == eTag {
// downloaded file is up-to-date so update the metadata
metadata.isDownloaded = true
try metadataStore.upsertAssetMetadata(metadata)
} else {
// remove the out-of-date file
try await fileCache.deleteFile(forKey: metadata.cacheKey)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: To me it looks like more of a use case not a repo, with business logic with states and api, file cache and metadata store as individual repositories, wdyt @samwyndham ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So making the repositories simple right and moving the logic to the use case. I will have a think about it. It might well make more sense.


public let downloadState: DownloadState

public init(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: will a public init be needed?


case failed(error: any Error)

public static func == (lhs: DownloadState, rhs: DownloadState) -> Bool {
Copy link
Contributor

@caldrian caldrian Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Do we really need Equatable conformance?
If we want to have it, I'm not sure that the current implementation, ignoring the actual error type, is behaving like any user would expect.

optional: One alternative I could think of is offering properties, like isPending: Bool etc.
But there are other possibilities, depending on what the goal actually is.

public import Foundation

// sourcery: AutoMockable
@MainActor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is it really needed to restrict every conforming type to be isolated to the @MainActor?
What about moving the attribute to the funcs?

import Foundation
public import WireCellsAPI

extension URLSession: FileDownloading {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal opinion: Declaring protocols and making third party (or SDK) types conform to one's own protocols is an anti-pattern for me. As usual I'm having difficulties phrasing my thoughts why it feels wrong.

But one example what I don't like is that from now on every target which links WireCells suddenly offers a URLSession.download(from:) method.
Depending on whether we want this code to be a general helper for any other part of the codebase, or whether this is something WireCells specific, we should either move this into WireUtilities/WireFoundation or prevent the namespace of URLSession from being spoiled with this API.

I suggest to make a simple structure wrapping an URLSession and conforming to the FileDownloading protocol.

}

func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {
precondition(cancellables.isEmpty, "Delegate must not be reused across multiple tasks.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: does it mean if one wants to download two files the following will have to be done in a strictly sequential order?

  • set URLSession delegate to a new instance of URLSessionTaskProgressDelegate
  • start a task and wait for it to be finished
  • replace the delegate of the URLSession
  • start another task

import Foundation
import WireCellsAPI

/// Repository for accessing & updating `WireCellsLocalAsset`s.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Repository for accessing & updating `WireCellsLocalAsset`s.
/// Repository for accessing & updating `WireCellsLocalAssets.

Since you're the native speaker feel free to ignore this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants