@gj-kit/expo-media
An upload with no size limit, or an iCloud download nobody asked for, is a compile error.
- npm
0.6.2 - 0 runtime dependencies
- 80 @ts-expect-error guards
- 570+ unit tests, no expo mocking
- 17 MediaError codes, one closed union
Keep learning
Section titled “Keep learning”Why this exists
Section titled “Why this exists”Expo media failures rarely look like failures. On iOS 26 FileSystem.uploadAsync ends the process while it is starting an upload, so no promise ever rejects and no retry fires; the localUri MediaLibrary hands back points inside the photo library rather than your app container, so it passes stat and then kills URLSession mid-upload. Android still reports the original fileSize after a quality<1 re-encode, so the presigned size and the bytes storage actually receives disagree.
What it does about it
Section titled “What it does about it”- Unlimited uploads must be spelled out —
createMediaKit({ api })does not compile:limitsis required, andNumber.POSITIVE_INFINITYis rejected, so unlimited is written'server-enforced'. - duplicate cannot be forgotten —
UploadResult.duplicateis required rather than optional, because a missing flag reads as “newly created” and the cancel path then deletes the user’s older photo. - Whoever copies owns the cleanup —
createDeviceLibrarywill not compile withoutstaging, so the factory that materializes cache copies of device photos always carries theStagingCache.cleanupthat deletes them. - iCloud downloads never default on —
adapter.getAssetInfo('id')is a compile error: the second argument is required and carriesdownloadFromNetwork, so the caller decides on every call and no adapter can quietly inherit the legacy default oftruethat started cellular transfers. - Peer isolation is a CI assertion — The
dist-peer-graphguard re-extracts each entry’s external specifiers from the built output across browser/node/native by ESM/CJS;./core,./image/pure,./weband./testingresolve zero peers.
Golden path
Section titled “Golden path”Outcome: An Expo-backed media kit while your application keeps upload authorization.
1. Install
Section titled “1. Install”pnpm add @gj-kit/expo-media2. Keep the app-owned boundary explicit
Section titled “2. Keep the app-owned boundary explicit”Implement uploadApi in your app for upload intent and completion, then declare explicit file limits.
3. Start with the smallest integration
Section titled “3. Start with the smallest integration”Copy this first, then replace only the app-owned values named above.
import { createMediaKit, type MediaUploadApi } from '@gj-kit/expo-media';
type Asset = { readonly id: string };declare const uploadApi: MediaUploadApi<Asset>; // Your app owns auth and upload URLs.
export const media = createMediaKit({ api: uploadApi, limits: { image: { maxBytes: 15 * 1024 * 1024 } },});What that looks like
Section titled “What that looks like”When an upload dies mid-flight the error narrows into URL-free recovery metadata — a stage plus frozen object records — never the presigned URL and never the native error text.
import { createMediaKit, mediaUploadFailureInfo } from '@gj-kit/expo-media';import type { MediaUploadApi, MediaUploadFailureInfo } from '@gj-kit/expo-media';
declare const uploadApi: MediaUploadApi<{ readonly id: string }>; // App owns auth + upload URLs.declare function reconcile(failure: MediaUploadFailureInfo): Promise<void>; // App owns cleanup.
// `limits` is required, and there is no numeric escape hatch:// createMediaKit({ api: uploadApi });// -> error TS2345: Argument of type '{ api: MediaUploadApi<...>; }' is not// assignable to parameter of type 'MediaKitConfig<...>'.export const media = createMediaKit({ api: uploadApi, limits: 'server-enforced' });
export async function recover(error: unknown): Promise<void> { const failure = mediaUploadFailureInfo(error); if (!failure) throw error; // Not an upload failure — never swallow it. // failure.stage : 'intent' | 'put' | 'complete' // failure.orphanedObjects : readonly { objectName; contentType; sizeBytes; // storageState: 'uploaded' | 'possibly-uploaded' }[] // No presigned URL and no native error text ever reach this value. await reconcile(failure); throw error;}Use it when
Section titled “Use it when”Use it for media selection, upload preparation, hashing, device-library access, and durable local files while your app keeps its own API and storage policy.
Do not use it when
Section titled “Do not use it when”Do not put record ownership, presign authorization, or orphan-cleanup policy in this library.
Runtime and peers
Section titled “Runtime and peers”Engines: node >=20
| Peer | Supported range | Required for |
|---|---|---|
expo |
>=56.0.0 <58.0.0 |
documented optional subpaths only |
expo-file-system |
>=56.0.0 |
documented optional subpaths only |
expo-image-manipulator |
>=56.0.0 |
documented optional subpaths only |
expo-image-picker |
>=16.0.0 |
documented optional subpaths only |
expo-media-library |
>=56.0.5 |
documented optional subpaths only |
expo-video-thumbnails |
>=8.0.0 |
documented optional subpaths only |
react-native |
>=0.71.0 |
documented optional subpaths only |
Public subpaths
Section titled “Public subpaths”| Entry | Exported symbols |
|---|---|
Root entry |
167 |
./core |
157 |
./device |
1 |
./image |
12 |
./image/pure |
6 |
./picker |
1 |
./save |
1 |
./storage |
10 |
./testing |
36 |
./video |
1 |
./web |
5 |
Safety boundary
Section titled “Safety boundary”Never expose presigned URLs or native URI details in public errors. Keep cleanup authorization and attachment transactions in the consuming application.
API reference
Section titled “API reference”complete API reference contains every public function, class, type, constant, and error contract generated from release declarations. Each item shows its import path, release signature, and package version.
Package relationships
Section titled “Package relationships”| Relationship | Package | Why combine them |
|---|---|---|
| Standalone | — | This package can be adopted without another GJ Kit package. |
| Optional companion | @gj-kit/expo-auth |
Keep media upload authorization in an application session. |