Skip to content

[Assets] 1 - Normalize and upload Map Point photos #312

Description

@KacperKozak

Parent

  • vescape-app/vescape-server#29 adds Hosted Asset storage, normalization, public reads, resizing, deletion, and caller-supplied Asset IDs.
  • vescape-app/vescape-server#33 adds the Account-authenticated map-points group and ownership rules.

What to build

Add one reusable native photo pipeline for Map Point photos. It accepts picker and camera files in modern phone formats, writes a normalized app-owned photo, uploads exactly one image through the existing authenticated API boundary, and durably tracks transfer and deletion across process death, offline periods, background work, sign-out, and retry.

Native owns normalized files, the Asset job queue, remote identity, retry policy, and long-lived work. JS receives stable state and sends intents. This slice exposes only the finite map-points group. Future features may extend the pipeline without adding another codec, multipart client, queue, or retry loop.

Use the existing Device Token API boundary, Account generation and reset guards, App Status gating, connectivity policy, background ride window, cold-launch resume, diagnostics, and stale-response protection already present on dev.

Normalization contract

  • Decode JPEG, PNG, WebP, HEIC or HEIF, and AVIF inputs where the platform supplies a decoder.
  • Apply encoded orientation before sizing.
  • Remove EXIF and other metadata, including GPS.
  • Preserve aspect ratio, never upscale, and limit the long edge to 4096px.
  • Prefer WebP where platform encoding is reliable. Otherwise use JPEG. AVIF is never required on the wire.
  • Use one parity-linked quality policy with deterministic MIME type, extension, width, height, byte count, and SHA-256 over normalized bytes.
  • Publish through temp file, final file, then durable row. Failed normalization or publication leaves no visible partial state or orphan temp file.
  • Callers use the normalized result and never retain the full original.

Wire contract

  • Mint and persist a UUIDv4 Asset ID before the first attempt.
  • Upload multipart fields id and file to POST /assets/map-points. file contains exactly one normalized image.
  • Retrying the same ID after an ambiguous response addresses the same immutable Asset. A 200 response means the existing Asset was found, and 201 means it was created.
  • Use the existing VescapeApi Device Token credential during native and background work. Never send Internal API keys or caller-selected group fields.
  • Stream multipart bytes from disk. Do not send image bytes through the JS bridge or build a possible 50 MiB body in one memory buffer.
  • Strictly parse the server response fields id, url, width, height, and bytes.
  • Persist group and Asset ID as durable remote identity. Treat the URL as display data, never as an object-store identity.
  • Treat upload 409 asset-owner-conflict as permanent for that job. It means the generated ID belongs to another Account and must not be retried under a new ID automatically.
  • Treat delete 204 and every delete 404 as converged. The server deliberately hides wrong ownership behind 404.
  • Treat delete 409 asset-in-use as blocked on detachment. Keep the delete job and retry only after the feature has removed the live reference.

Durable state

Use an additive native table or manifest with states equivalent to:

pending -> uploading -> uploaded
   |          |
   +------> retryable-failure -> uploading
              |
              +-> permanent-failure

uploaded -> deleting -> deleted

Keep local owner identity, normalized file identity, group, Asset ID, attempt metadata, last classified failure, remote response data, and the Account generation needed to reject stale responses.

  • Network, timeout, server-unavailable, and future 429 results back off without dropping the normalized file.
  • Authentication and App Status failures use the shared API policy and diagnostics.
  • Cold launch, foreground, connectivity regain, sign-in, and the existing background ride window resume eligible work.
  • Sign-out stops transfer without discarding queued media. Signing back into the same Account resumes it.
  • Account reset invalidates old work before replacing local data. An old response cannot publish into a new Account's database.
  • Keep the normalized file until the owning feature confirms its remote reference is durable. The pipeline may then release it when that feature has no local-storage requirement.

Likely files

  • modules/vescape-core/ios/api/VescapeApi.swift - add streamed multipart support to the shared Device Token boundary.
  • modules/vescape-core/android/src/main/java/expo/modules/vescapecore/api/VescapeApi.kt - Android parity peer.
  • modules/vescape-core/ios/assets/ - new normalization, manifest, transfer engine, and focused tests.
  • modules/vescape-core/android/src/main/java/expo/modules/vescapecore/assets/ - Android parity package.
  • modules/vescape-core/ios/telemetry/TelemetryDatabase.swift - additive durable Asset-job schema.
  • modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDatabase.kt - Room migration peer.
  • Existing native bootstrap and lifecycle seams - resume eligible Asset jobs without coupling the queue to another domain.
  • modules/vescape-core/src/index.ts - parity-linked photo, Asset reference, and job-state contracts exposed to JS.

Acceptance criteria

  • Supported picker and camera inputs normalize with correct orientation and no EXIF or GPS metadata.
  • Aspect ratio is preserved, the long edge is at most 4096px, and smaller images are never upscaled.
  • WebP is preferred where reliable and JPEG fallback produces a supported upload.
  • Normalized MIME type, extension, dimensions, byte count, and SHA-256 describe the final bytes.
  • Original picker or camera bytes are not retained.
  • A durable UUIDv4 is created before upload and sent as multipart field id with one streamed file.
  • Ambiguous upload retry reuses the same Asset ID and accepts either the server's idempotent 200 or creation 201 response.
  • Pending, uploading, uploaded, retryable, permanent, deleting, and deleted states survive process death.
  • Offline, timeout, server-unavailable, authentication, App Status, and Account-reset outcomes follow the durable policy above.
  • Delete 204 and 404 converge, while 409 asset-in-use waits for detachment.
  • Failed normalization, failed database publication, and successful cleanup leave no orphan local files.
  • Tests cover orientation, metadata removal, supported formats, 4096px boundaries, no-upscale, WebP and JPEG output, multipart streaming, response validation, ambiguous retry, process restart, backoff, Account reset, and deletion classification.
  • TS, Swift, and Kotlin contracts and native implementations stay linked by @parity.

Blocked by

  • vescape-app/vescape-server#33.

Related

Explicit exclusions

  • Map Point attachment and UI.
  • Video decoding, transcoding, or upload.
  • Content deduplication or hash-addressed identity.
  • Private reads, bucket URLs, presigned uploads, or arbitrary Asset groups.
  • Scheduled WorkManager or BGTaskScheduler behavior beyond existing lifecycle windows.
  • Quota or storage-management UI.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:assetsHosted media, image normalization, upload state, and remote Asset referencesarea:authClerk sessions, native credentials, and endpoint caller policyarea:dbTouches database / persistent storagearea:nativeTouches native side (modules/vesc-ble, Swift/Kotlin)area:serverVescape backend APIs, relay behavior, server policy, and deployment-facing contractscomplexity:highCritical paths, subtle correctness, native pipelines. Use opus.ready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions