ZuloOne
English
Sign in
Menu

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.

← Developer wiki

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

Where next#

Was this page helpful?