Resumable, deletion-aware collection and file enumeration on Client #38

Closed
opened 2026-09-22 09:10:57 +02:00 by clawbot · 2 comments
Collaborator

Foundation unit for the cache/API design (#36).
Depends on the decrypt-fields unit (needs the isDeleted field on EnteFile).

Goal

Give Client enumeration variants that the cache refresh needs: they take a
starting cursor, return the final cursor, and surface deletion tombstones. The
existing whole-account methods stay for callers that still want them.

Current state (grounded, src/client.ts)

  • listCollections() always calls /collections/v2 with sinceTime: 0 and
    drops every isDeleted collection. It cannot report a cursor or deletions.
  • listFiles(collectionID, collectionKey) paginates /collections/v2/diff but
    always restarts at sinceTime: 0, drops isDeleted rows, and returns no
    cursor. Its loop advances sinceTime only from rows it sees; if the server
    returns hasMore: true with a page whose max updationTime does not advance,
    it loops forever (this is #7).

Scope (extend existing)

Add cursor-taking, tombstone-surfacing variants (names are the implementer's to
choose, plain and descriptive). Suggested shape:

  • collectionsSince(args: { sinceTime: number }){ collections: Collection[]; deleted: number[]; cursor: number }, where collections are live decrypted records, deleted are the ids of tombstoned collections, and cursor is the max updationTime seen (fall back to the input when the response is empty).
  • filesSince(args: { collectionID; collectionKey; sinceTime }){ files: EnteFile[]; deleted: number[]; cursor: number }, paginating from the given cursor, decrypting live rows, collecting isDeleted file ids, returning the final cursor.
  • Guard against a non-advancing server (#7):
    if hasMore is true but the page's max updationTime is <= the cursor we
    entered the page with, stop and throw a clear error rather than loop. Cover
    this with a test.
  • Keep listCollections()/listFiles() as thin wrappers (call the new variant
    with sinceTime: 0, drop deletions) so existing callers and tests are
    unaffected — or leave them exactly as-is and add the new methods alongside.
    Do not change the low-level ApiClient; it already accepts an arbitrary
    sinceTime.

Definition of done

  • New enumeration methods return live records, deleted ids, and a usable cursor.
  • Passing the returned cursor into a second call fetches only newer rows (tested
    with a fixture/mock that pages then returns an empty diff).
  • The non-advancing-server case is bounded and tested (closes
    #7).
  • Existing listCollections/listFiles behaviour and tests unchanged.
  • make check green.

Grounding

Files: src/client.ts, test/api/client.test.ts (and/or a new test file).
Subsumes #7 — reference it in the landing
commit and close it there too.

Dispatch notes: TDD; no scripted edits; no interactive questions; decide from
this brief. 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). Depends on the decrypt-fields unit (needs the `isDeleted` field on `EnteFile`). ## Goal Give `Client` enumeration variants that the cache refresh needs: they take a starting cursor, return the final cursor, and surface deletion tombstones. The existing whole-account methods stay for callers that still want them. ## Current state (grounded, `src/client.ts`) - `listCollections()` always calls `/collections/v2` with `sinceTime: 0` and drops every `isDeleted` collection. It cannot report a cursor or deletions. - `listFiles(collectionID, collectionKey)` paginates `/collections/v2/diff` but always restarts at `sinceTime: 0`, drops `isDeleted` rows, and returns no cursor. Its loop advances `sinceTime` only from rows it sees; if the server returns `hasMore: true` with a page whose max `updationTime` does not advance, it loops forever (this is https://git.eeqj.de/sneak/quak/issues/7). ## Scope (extend existing) Add cursor-taking, tombstone-surfacing variants (names are the implementer's to choose, plain and descriptive). Suggested shape: - `collectionsSince(args: { sinceTime: number })` → `{ collections: Collection[]; deleted: number[]; cursor: number }`, where `collections` are live decrypted records, `deleted` are the ids of tombstoned collections, and `cursor` is the max `updationTime` seen (fall back to the input when the response is empty). - `filesSince(args: { collectionID; collectionKey; sinceTime })` → `{ files: EnteFile[]; deleted: number[]; cursor: number }`, paginating from the given cursor, decrypting live rows, collecting `isDeleted` file ids, returning the final cursor. - Guard against a non-advancing server (https://git.eeqj.de/sneak/quak/issues/7): if `hasMore` is true but the page's max `updationTime` is `<=` the cursor we entered the page with, stop and throw a clear error rather than loop. Cover this with a test. - Keep `listCollections()`/`listFiles()` as thin wrappers (call the new variant with `sinceTime: 0`, drop deletions) so existing callers and tests are unaffected — or leave them exactly as-is and add the new methods alongside. Do not change the low-level `ApiClient`; it already accepts an arbitrary `sinceTime`. ## Definition of done - New enumeration methods return live records, deleted ids, and a usable cursor. - Passing the returned cursor into a second call fetches only newer rows (tested with a fixture/mock that pages then returns an empty diff). - The non-advancing-server case is bounded and tested (closes https://git.eeqj.de/sneak/quak/issues/7). - Existing `listCollections`/`listFiles` behaviour and tests unchanged. - `make check` green. ## Grounding Files: `src/client.ts`, `test/api/client.test.ts` (and/or a new test file). Subsumes https://git.eeqj.de/sneak/quak/issues/7 — reference it in the landing commit and close it there too. Dispatch notes: TDD; no scripted edits; no interactive questions; decide from this brief. Squash subject ends ` (closes #<this issue>)`. End every message with `Model: opus-4-8`. Model: opus-4-8
Author
Collaborator

Scope note: surfacing deletion tombstones through decryption now belongs here. #37 was narrowed to only carry fileSize/thumbSize/isDeleted onto live EnteFile records (it leaves listFiles filtering tombstones before decrypt). This unit owns the tombstone path: the resumable enumerators pass isDeleted rows through and decide the return shape for a tombstone (a genuinely minimal EnteFile is not type-clean under strict tsc, so design it here — e.g. a separate tombstone type or deleted ids alongside live records, as this issue already sketches).

Model: opus-4-8

Scope note: surfacing deletion tombstones through decryption now belongs here. https://git.eeqj.de/sneak/quak/issues/37 was narrowed to only carry `fileSize`/`thumbSize`/`isDeleted` onto live `EnteFile` records (it leaves `listFiles` filtering tombstones before decrypt). This unit owns the tombstone path: the resumable enumerators pass `isDeleted` rows through and decide the return shape for a tombstone (a genuinely minimal `EnteFile` is not type-clean under strict tsc, so design it here — e.g. a separate tombstone type or `deleted` ids alongside live records, as this issue already sketches). Model: opus-4-8
Author
Collaborator

Implemented in #57 (branch issue-38-resumable-enumeration, base next).

Added collectionsSince/filesSince on Client: they take a starting cursor, decrypt live records, surface tombstoned ids in a separate deleted list, and return the max updationTime seen as the cursor to resume from. Per the scope note, tombstones are surfaced as bare ids rather than hollow records, which keeps the return type clean under strict tsc.

filesSince throws instead of looping when the diff reports more rows without advancing the cursor, which also resolves #7 (the PR closes it). listCollections/listFiles are now thin wrappers from sinceTime 0 that drop deletions, so existing callers are unaffected.

Model: opus-4-8

Implemented in https://git.eeqj.de/sneak/quak/pulls/57 (branch `issue-38-resumable-enumeration`, base `next`). Added `collectionsSince`/`filesSince` on `Client`: they take a starting cursor, decrypt live records, surface tombstoned ids in a separate `deleted` list, and return the max `updationTime` seen as the cursor to resume from. Per the scope note, tombstones are surfaced as bare ids rather than hollow records, which keeps the return type clean under strict tsc. `filesSince` throws instead of looping when the diff reports more rows without advancing the cursor, which also resolves https://git.eeqj.de/sneak/quak/issues/7 (the PR closes it). `listCollections`/`listFiles` are now thin wrappers from `sinceTime` 0 that drop deletions, so existing callers are unaffected. Model: opus-4-8
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/quak#38