You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Early draft — posted here mainly so it's tracked against 4.5.0, not a finished design. The contract below is still being argued about internally; expect it to change before implementation starts. Feedback and alternative shapes welcome.
Overview
Goal: let a caller pause and resume a streaming download (DownloadTask's TaskResult<AsyncBytes>) using HTTP Range/If-Range, without adding a new concrete task type (BackgroundDownloadTask already covers OS-level background transfers on Darwin — this is a same-process, any-platform complement, not a replacement).
The mechanism piggybacks on infrastructure that already exists:
RawTask._result(environment:) (Sources/RequestDL/Tasks/Sources/Raw Task/Raw/RawTask.swift) is the one place that resolves a Property tree into a RequestConfiguration and calls client.execute(...). It already mutates the resolved configuration in place for cross-cutting concerns (tracer header injection) and already runs environment-queued hooks (taskDescriptorContextHooks, backing .description(_:enabled:onDescribe:)) at that exact point. A resume hint (offset + validator) would be read the same way, right before client.execute.
Internals.CacheControl.getUpdatedHeadersForCache (Sources/RequestDL/Tasks/Sources/Raw Task/Cache Control/Internals.CacheControl.swift) already does the closest analogue to what this needs: reissue a request with conditional headers (If-None-Match/If-Modified-Since), inspect the status code that comes back (304 vs. not), and branch. The resumable case is the same shape with Range/If-Range and 206/200/416 instead of 304.
So the proposal is: an extension on RequestTask where Element == TaskResult<AsyncBytes> returning a ResumableTask wrapper, not a new task type users declare their request under.
Proposed contract (rough)
extensionRequestTaskwhere Element ==TaskResult<AsyncBytes>{func resumable()->ResumableTask<Self>}extension ResumableTask {enumResumption:Sendable{
/// 206 — server honored Range. `payload` is only the missing tail; append it after
/// whatever the caller already persisted at `offset`.
case resumed(TaskResult<AsyncBytes>)
/// 200 — server ignored Range/If-Range (no range support, or the validator no longer
/// matched). `payload` is the entire resource from byte 0; caller discards whatever it
/// already wrote.
case restarted(TaskResult<AsyncBytes>)
/// 416 — `offset` doesn't exist on the resource anymore. No payload; `head` usually
/// carries `Content-Range: bytes */<total>` to compare against `offset`.
case unsatisfiable(head:ResponseHead)}func resume(from offset:Int64, validator:Validator?)asyncthrows->Resumption}struct Validator: Sendable {
static func etag(_ value:String)->Validator
static func lastModified(_ value:String)->Validator}
offset/validator are supplied by the caller on every resume(from:validator:) call, not tracked internally — only the caller knows how many bytes it actually persisted (wrote to disk, etc.), which can lag behind how many bytes the stream delivered.
No explicit pause(): pausing is the caller stopping/cancelling its own for try await consumption of AsyncBytes. Whether that actually tears down the underlying connection cleanly on both transports (NIO and URLSession) rather than leaving it dangling until a timeout is unverified — see open questions.
Open questions
Should Resumption hide the byte bookkeeping and hand back one byte-continuous stream (merging old + new bytes internally), instead of leaving concatenation entirely to the caller? Raised and pushed back on, not settled.
Does cancelling the caller's consuming Task actually release the connection on both the NIO and URLSession backends, or does ResumableTask need its own explicit teardown path?
Worth adding typed RangeHeader/IfRangeHeader (alongside AcceptEncodingHeader, AcceptLanguageHeader, etc.) as public headers independent of resumable(), for callers who want to build the request by hand?
ResumableTask only covers same-process resume. Surviving an app relaunch is BackgroundDownloadTask's territory today — should stay that way, or should this converge with it eventually?
Naming (ResumableTask vs. alternatives), and whether it belongs as a method on RequestTask at all vs. a dedicated initializer.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Status
Early draft — posted here mainly so it's tracked against 4.5.0, not a finished design. The contract below is still being argued about internally; expect it to change before implementation starts. Feedback and alternative shapes welcome.
Overview
Goal: let a caller pause and resume a streaming download (
DownloadTask'sTaskResult<AsyncBytes>) using HTTPRange/If-Range, without adding a new concrete task type (BackgroundDownloadTaskalready covers OS-level background transfers on Darwin — this is a same-process, any-platform complement, not a replacement).The mechanism piggybacks on infrastructure that already exists:
RawTask._result(environment:)(Sources/RequestDL/Tasks/Sources/Raw Task/Raw/RawTask.swift) is the one place that resolves aPropertytree into aRequestConfigurationand callsclient.execute(...). It already mutates the resolved configuration in place for cross-cutting concerns (tracer header injection) and already runs environment-queued hooks (taskDescriptorContextHooks, backing.description(_:enabled:onDescribe:)) at that exact point. A resume hint (offset + validator) would be read the same way, right beforeclient.execute.Internals.CacheControl.getUpdatedHeadersForCache(Sources/RequestDL/Tasks/Sources/Raw Task/Cache Control/Internals.CacheControl.swift) already does the closest analogue to what this needs: reissue a request with conditional headers (If-None-Match/If-Modified-Since), inspect the status code that comes back (304 vs. not), and branch. The resumable case is the same shape withRange/If-Rangeand 206/200/416 instead of 304.So the proposal is: an extension on
RequestTask where Element == TaskResult<AsyncBytes>returning aResumableTaskwrapper, not a new task type users declare their request under.Proposed contract (rough)
offset/validatorare supplied by the caller on everyresume(from:validator:)call, not tracked internally — only the caller knows how many bytes it actually persisted (wrote to disk, etc.), which can lag behind how many bytes the stream delivered.No explicit
pause(): pausing is the caller stopping/cancelling its ownfor try awaitconsumption ofAsyncBytes. Whether that actually tears down the underlying connection cleanly on both transports (NIO and URLSession) rather than leaving it dangling until a timeout is unverified — see open questions.Open questions
Resumptionhide the byte bookkeeping and hand back one byte-continuous stream (merging old + new bytes internally), instead of leaving concatenation entirely to the caller? Raised and pushed back on, not settled.Taskactually release the connection on both the NIO and URLSession backends, or doesResumableTaskneed its own explicit teardown path?RangeHeader/IfRangeHeader(alongsideAcceptEncodingHeader,AcceptLanguageHeader, etc.) as public headers independent ofresumable(), for callers who want to build the request by hand?ResumableTaskonly covers same-process resume. Surviving an app relaunch isBackgroundDownloadTask's territory today — should stay that way, or should this converge with it eventually?ResumableTaskvs. alternatives), and whether it belongs as a method onRequestTaskat all vs. a dedicated initializer.Out of scope (v1)
offset/validator across app restarts — caller-owned state,ResumableTaskdoesn't serialize anything itself.BackgroundDownloadTask.Milestone: 4.5.0
All reactions