Foundation unit for the cache/API design (#36).
Independent of the decrypt-fields and enumeration units (touches only the
download module), so it can run in parallel with them.
Goal
Two small download-module changes the cache and the GUI need:
A shared atomic file writer that survives a power cut (fsync the file before
the rename, fsync the directory after), exported for the cache to use.
A per-chunk progress hook on downloadFile/downloadThumbnail so callers
can report bytes-written as the body streams (feeds the design's onProgressbytesDone).
Current state (grounded, src/download/index.ts)
writeAtomic(destination, plaintext) stages a temp sibling and renames, but
is module-private and does NOT fsync — after a crash the renamed file can be
empty (tracked as #22).
downloadFile/downloadThumbnail report bytesWritten only in the final DownloadResult; there is no signal while the body streams.
Scope
Add fsync to the atomic writer: open the temp file, write, fsync it, close, rename, then fsync the containing directory. Keep the existing best-effort
temp cleanup on error. Export it (its own small module or a named export from
the download module) so the metadata store can reuse one writer.
Add an optional onProgress?: (bytesDone: number) => void (or a richer typed
event if that is cleaner — align with the design's ProgressEvent) to downloadFile/downloadThumbnail, called as decrypted plaintext accumulates.
For now, whole-file buffering stays (streaming-to-disk is the next unit); the
hook can fire per pulled chunk inside streamDecrypt, threaded out through the
download functions. Keep the callback optional and side-effect-free on the
happy path when absent.
Do not change retry semantics or the TAG_FINAL truncation checks.
Definition of done
Atomic writer fsyncs file and directory; exported and covered by a test
(a test may assert the temp-then-rename path and that no partial file is left
on a simulated write failure).
Progress callback fires with monotonically increasing byte counts and a final
value equal to bytesWritten; tested.
Existing download tests still pass; make check green.
Overlaps #22 (fsync durability). Note it
in the landing commit; close#22 only if this unit fully covers its scope,
otherwise cross-reference and leave orphan-reaping to the content-store unit.
Grounding
Files: src/download/index.ts (+ possibly a new small atomic-write module), test/download/download.test.ts.
Dispatch notes: TDD; no scripted edits; no interactive questions. Squash subject
ends (closes #<this issue>). End every message with Model: opus-4-8.
Model: opus-4-8
Foundation unit for the cache/API design (https://git.eeqj.de/sneak/quak/issues/36).
Independent of the decrypt-fields and enumeration units (touches only the
download module), so it can run in parallel with them.
## Goal
Two small download-module changes the cache and the GUI need:
1. A shared atomic file writer that survives a power cut (fsync the file before
the rename, fsync the directory after), exported for the cache to use.
2. A per-chunk progress hook on `downloadFile`/`downloadThumbnail` so callers
can report bytes-written as the body streams (feeds the design's
`onProgress` `bytesDone`).
## Current state (grounded, `src/download/index.ts`)
- `writeAtomic(destination, plaintext)` stages a temp sibling and renames, but
is module-private and does NOT fsync — after a crash the renamed file can be
empty (tracked as https://git.eeqj.de/sneak/quak/issues/22).
- `downloadFile`/`downloadThumbnail` report `bytesWritten` only in the final
`DownloadResult`; there is no signal while the body streams.
## Scope
- Add fsync to the atomic writer: open the temp file, write, `fsync` it, close,
`rename`, then `fsync` the containing directory. Keep the existing best-effort
temp cleanup on error. Export it (its own small module or a named export from
the download module) so the metadata store can reuse one writer.
- Add an optional `onProgress?: (bytesDone: number) => void` (or a richer typed
event if that is cleaner — align with the design's `ProgressEvent`) to
`downloadFile`/`downloadThumbnail`, called as decrypted plaintext accumulates.
For now, whole-file buffering stays (streaming-to-disk is the next unit); the
hook can fire per pulled chunk inside `streamDecrypt`, threaded out through the
download functions. Keep the callback optional and side-effect-free on the
happy path when absent.
- Do not change retry semantics or the `TAG_FINAL` truncation checks.
## Definition of done
- Atomic writer fsyncs file and directory; exported and covered by a test
(a test may assert the temp-then-rename path and that no partial file is left
on a simulated write failure).
- Progress callback fires with monotonically increasing byte counts and a final
value equal to `bytesWritten`; tested.
- Existing download tests still pass; `make check` green.
- Overlaps https://git.eeqj.de/sneak/quak/issues/22 (fsync durability). Note it
in the landing commit; close #22 only if this unit fully covers its scope,
otherwise cross-reference and leave orphan-reaping to the content-store unit.
## Grounding
Files: `src/download/index.ts` (+ possibly a new small atomic-write module),
`test/download/download.test.ts`.
Dispatch notes: TDD; no scripted edits; no interactive questions. Squash subject
ends ` (closes #<this issue>)`. End every message with `Model: opus-4-8`.
Model: opus-4-8
Implemented in #56 (branch issue-39-atomic-fsync-progress, base next).
writeAtomic fsyncs the staged temp file before the rename and the containing
directory after, and is now exported for the metadata store to reuse.
downloadFile/downloadThumbnail take an optional onProgress(bytesDone)
hook fired within streamDecrypt; values are non-decreasing and the last
equals bytesWritten, and it is a no-op when omitted.
Whole-file buffering, retry semantics, and the TAG_FINAL truncation checks are
unchanged.
Tests added for the two fsyncs and their order, the temp-file cleanup on a failed
rename, and monotonic progress ending at bytesWritten; existing download tests
pass and make check is green.
This covers only the durability gap (area 1) of #22; orphan reaping and the rest there
remain for a later unit, so it is referenced, not closed.
Model: opus-4-8
Implemented in https://git.eeqj.de/sneak/quak/pulls/56 (branch `issue-39-atomic-fsync-progress`, base `next`).
- `writeAtomic` fsyncs the staged temp file before the rename and the containing
directory after, and is now exported for the metadata store to reuse.
- `downloadFile`/`downloadThumbnail` take an optional `onProgress(bytesDone)`
hook fired within `streamDecrypt`; values are non-decreasing and the last
equals `bytesWritten`, and it is a no-op when omitted.
- Whole-file buffering, retry semantics, and the TAG_FINAL truncation checks are
unchanged.
Tests added for the two fsyncs and their order, the temp-file cleanup on a failed
rename, and monotonic progress ending at `bytesWritten`; existing download tests
pass and `make check` is green.
This covers only the durability gap (area 1) of
https://git.eeqj.de/sneak/quak/issues/22; orphan reaping and the rest there
remain for a later unit, so it is referenced, not closed.
Model: opus-4-8
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Foundation unit for the cache/API design (#36).
Independent of the decrypt-fields and enumeration units (touches only the
download module), so it can run in parallel with them.
Goal
Two small download-module changes the cache and the GUI need:
the rename, fsync the directory after), exported for the cache to use.
downloadFile/downloadThumbnailso callerscan report bytes-written as the body streams (feeds the design's
onProgressbytesDone).Current state (grounded,
src/download/index.ts)writeAtomic(destination, plaintext)stages a temp sibling and renames, butis module-private and does NOT fsync — after a crash the renamed file can be
empty (tracked as #22).
downloadFile/downloadThumbnailreportbytesWrittenonly in the finalDownloadResult; there is no signal while the body streams.Scope
fsyncit, close,rename, thenfsyncthe containing directory. Keep the existing best-efforttemp cleanup on error. Export it (its own small module or a named export from
the download module) so the metadata store can reuse one writer.
onProgress?: (bytesDone: number) => void(or a richer typedevent if that is cleaner — align with the design's
ProgressEvent) todownloadFile/downloadThumbnail, called as decrypted plaintext accumulates.For now, whole-file buffering stays (streaming-to-disk is the next unit); the
hook can fire per pulled chunk inside
streamDecrypt, threaded out through thedownload functions. Keep the callback optional and side-effect-free on the
happy path when absent.
TAG_FINALtruncation checks.Definition of done
(a test may assert the temp-then-rename path and that no partial file is left
on a simulated write failure).
value equal to
bytesWritten; tested.make checkgreen.in the landing commit; close #22 only if this unit fully covers its scope,
otherwise cross-reference and leave orphan-reaping to the content-store unit.
Grounding
Files:
src/download/index.ts(+ possibly a new small atomic-write module),test/download/download.test.ts.Dispatch notes: TDD; no scripted edits; no interactive questions. Squash subject
ends
(closes #<this issue>). End every message withModel: opus-4-8.Model: opus-4-8
Implemented in #56 (branch
issue-39-atomic-fsync-progress, basenext).writeAtomicfsyncs the staged temp file before the rename and the containingdirectory after, and is now exported for the metadata store to reuse.
downloadFile/downloadThumbnailtake an optionalonProgress(bytesDone)hook fired within
streamDecrypt; values are non-decreasing and the lastequals
bytesWritten, and it is a no-op when omitted.unchanged.
Tests added for the two fsyncs and their order, the temp-file cleanup on a failed
rename, and monotonic progress ending at
bytesWritten; existing download testspass and
make checkis green.This covers only the durability gap (area 1) of
#22; orphan reaping and the rest there
remain for a later unit, so it is referenced, not closed.
Model: opus-4-8