Mobile sync pull
Updated
Why one POST /api/mobile/sync/pull must finish inside about 70 seconds,
what is left out of the body, and what breaks if a later change puts the
photos or a per-SKU loop back on that path.
The phone's first screen after sign-in is not this POST. The descriptor paints the shell; the pull runs behind a Downloading the catalogue… bar. That bar used to stay up for hours on a tenant with a large catalogue: the HTTP client had no timeout, the stand was still walking every SKU, and Cloudflare had already dropped the request at ~100 s. The phone then applied nothing.
What one pull is allowed to cost#
The proxy in front of a production tenant cuts a request at about 100 seconds (Cloudflare 524). A body that is not finished by then is not a partial apply — the phone sees a failed HTTP and keeps the previous SQLite. So the stand aims to return something useful before 70 s, and the phone waits 95 s then retries.
| Budget | What | If it runs out |
|---|---|---|
| 70 s wall clock | the whole PullAsync |
remaining projections are omitted from changes (yesterday's rows stay on the phone). The catalogue is already in the body. |
| 30 s | new CachedPrice cells |
the agent's in-memory book continues on the next pull. Prices run before characteristics, campaigns and ABC so a long sheet walk cannot skip the catalogue prices. |
| 15 s | new CachedCharacteristics sheets (SheetAsync per item) |
the same book pattern |
| 95 s | the phone's HTTP wait | SyncException, Downloading the catalogue… ends, the pump retries |
Do not add a new per-SKU await on this path without a budget of its own.
IRecordCharacteristics.SheetAsync is the trap that already burned a
night: thousands of items, one sheet each, no cap.
Photos are not in the JSON#
QueryAsync(..., includeBinary: false) omits Image/Binary columns from
the SELECT, and MobilePullRows.StripBinaries drops any PNG/JPEG/GIF/WebP
payload that still slipped through (including nested table parts).
Detection is the payload prefix / byte[], not a field named Image.
The Flutter client does not know Item.Image.
Putting the bytes back — even as a “small” thumbnail generated per row —
recreates the hang: PostgreSQL detoasts every bytea, STJ writes
base64, the body is hundreds of megabytes, the proxy cuts it.
Pictures are GET /api/mobile/picture?role={role}&entity={name}&id={metaId}
after the pull. The stand loads the row the role may see and returns the
first PNG/JPEG/GIF/WebP cell (MobilePullRows.FirstBinary), not a field
named Image. 200 + bytes + ETag; 204 when the row has no picture;
404 when the entity is not in the role or the id is missing; If-None-Match
is 304. The phone caches {entity}/{id} in SQLite and only GETs on a miss.
The catalogue paints a letter avatar when the picture cell is empty. That is the honest empty state, not a bug.
The catalogue is read once#
A first pull used to CollectAsync("Item") and then QueryAllAsync("Item")
again so projections (stock, prices) saw the whole set, not the
watermarked delta. Two full scans with photos was two TOAST dumps.
CollectCatalogueAsync reads the whole set without binaries once:
the phone gets that set on a stale mark, or the delta otherwise;
projections always see the whole set.
A watermarked pull still needs the whole catalogue for CachedStock /
CachedPrice. That query is metadata-only now. Do not “optimize” it by
building those projections from the Item delta — the next pull would
replace the phone's stock with the handful of items that changed.
Caps that look like “it was cut”#
QueryAllAsync stops at 50 pages × 2,000 rows = 100,000. Hitting that
logs a warning and adds Item:100000 (or whichever entity) to
summary.truncated. The You tab then says still filling on the next
sync. Raising the cap does not help a hang; omitting binaries does.
A phone does not want 100,000 SKUs with pictures either.
Same topic for other readers: Analyst · User