Set contact cards in batches the server will take #218

Closed
opened 2026-09-02 13:54:35 +00:00 by jcoffey-dev · 0 comments
Owner

The same bug #215 fixed in the calendar import, in the three places contacts write more than one card at once. Promised on #173; nobody has hit it yet.

ContactCard/set is refused whole over maxObjectsInSetrequestTooLarge, nothing created — so:

  • a large enough vCard import filed nothing,
  • a large enough LDIF import filed nothing (#174's format),
  • and select-all → delete over a large address book deleted nothing, reporting it in JMAP's words.

What changed

Both imports now go through one createCards, which splits by the ceiling the session advertises and falls back to 500. That is what the LDIF import's own comment — "from ContactCard/set down they are the same" — was already claiming, and is now true of.

destroyCards splits the same way, and takes off the list the ids the server said it destroyed rather than everything that was asked for. It removed all of them before, which was harmless while there was one call and wrong the moment a later batch can fail: deleted contacts must not stay on screen, and live ones must not disappear from it.

One behaviour change beyond the batching: a vCard import the server accepted no card of returned 0, and the view reported importing no contacts — which reads as an empty file rather than as a refusal. It now says why, which is what the LDIF import has always done. A genuinely empty file still says so, earlier and separately.

Tests

Eleven new ones in contact-set-batching.test.ts, against a server that refuses an oversized call the way Stalwart does rather than quietly taking the first 500. Ten of the eleven fail on main (the eleventh is a regression guard on partial-success counting, which already worked).

Covers all three call sites: the 500/500/200 split for each, splitting by the advertised value rather than a constant (40 → 40/40/20), entries staying distinct across the split, the partial-failure message, and — for delete — that a third-batch failure leaves exactly the 200 undeleted cards on the list.

npm run typecheck, npm test (889 web + 122 server), npm run build pass.

Not in this PR

Duplicate checking on re-import, which is #174's first item and still open on #173 for calendars. Worth one decision for both rather than two.

Merged 2026-09-02 as coffey-labs/ihasmail@6e1ac42d5a

Rebuilt from: git history, session transcript.

The same bug #215 fixed in the calendar import, in the three places contacts write more than one card at once. Promised on #173; nobody has hit it yet. `ContactCard/set` is refused whole over `maxObjectsInSet` — `requestTooLarge`, nothing created — so: - a large enough **vCard** import filed nothing, - a large enough **LDIF** import filed nothing (#174's format), - and **select-all → delete** over a large address book deleted nothing, reporting it in JMAP's words. ## What changed Both imports now go through one `createCards`, which splits by the ceiling the session advertises and falls back to 500. That is what the LDIF import's own comment — *"from `ContactCard/set` down they are the same"* — was already claiming, and is now true of. `destroyCards` splits the same way, and takes off the list **the ids the server said it destroyed** rather than everything that was asked for. It removed all of them before, which was harmless while there was one call and wrong the moment a later batch can fail: deleted contacts must not stay on screen, and live ones must not disappear from it. One behaviour change beyond the batching: a vCard import the server accepted no card of returned `0`, and the view reported importing no contacts — which reads as an empty file rather than as a refusal. It now says why, which is what the LDIF import has always done. A genuinely empty file still says so, earlier and separately. ## Tests Eleven new ones in `contact-set-batching.test.ts`, against a server that refuses an oversized call the way Stalwart does rather than quietly taking the first 500. Ten of the eleven fail on `main` (the eleventh is a regression guard on partial-success counting, which already worked). Covers all three call sites: the 500/500/200 split for each, splitting by the advertised value rather than a constant (40 → 40/40/20), entries staying distinct across the split, the partial-failure message, and — for delete — that a third-batch failure leaves exactly the 200 undeleted cards on the list. `npm run typecheck`, `npm test` (889 web + 122 server), `npm run build` pass. ## Not in this PR Duplicate checking on re-import, which is #174's first item and still open on #173 for calendars. Worth one decision for both rather than two. **Merged** 2026-09-02 as coffey-labs/ihasmail@6e1ac42d5a49 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.