Skip to content

@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

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.

  • Unlimited uploads must be spelled out — createMediaKit({ api }) does not compile: limits is required, and Number.POSITIVE_INFINITY is rejected, so unlimited is written 'server-enforced'.
  • duplicate cannot be forgotten — UploadResult.duplicate is 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 — createDeviceLibrary will not compile without staging, so the factory that materializes cache copies of device photos always carries the StagingCache.cleanup that deletes them.
  • iCloud downloads never default on — adapter.getAssetInfo('id') is a compile error: the second argument is required and carries downloadFromNetwork, so the caller decides on every call and no adapter can quietly inherit the legacy default of true that started cellular transfers.
  • Peer isolation is a CI assertion — The dist-peer-graph guard re-extracts each entry’s external specifiers from the built output across browser/node/native by ESM/CJS; ./core, ./image/pure, ./web and ./testing resolve zero peers.

Outcome: An Expo-backed media kit while your application keeps upload authorization.

Terminal window
pnpm add @gj-kit/expo-media

Implement uploadApi in your app for upload intent and completion, then declare explicit file limits.

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 } },
});

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 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 put record ownership, presign authorization, or orphan-cleanup policy in this library.

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
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

Never expose presigned URLs or native URI details in public errors. Keep cleanup authorization and attachment transactions in the consuming application.

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.

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.

npm · GitHub