# Hand a large file between pages without a server

Store the picked file's bytes in IndexedDB on the upload page, navigate, and open them with `PDFDocument.fromBytes()` on the editor page. Nothing is uploaded until the person saves.

- Level: Loading
- Uses: `IndexedDB`, `PDFDocument.fromBytes()`, `createPasswordPrompt()`, `doc.save()`, `viewer.markSaved()`

A blob URL dies with the page that made it, and a large file does not fit in session storage. IndexedDB survives a full navigation and holds binary data, so the upload page can hand the bytes to the editor page directly. The store below is host code; the SDK only sees a `Uint8Array`.

## 1. A small store

`pdfStore.ts`

```ts
const DB = 'my-app-edit', STORE = 'pending-pdf', KEY = 'current';

function open(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB, 1);
    req.onupgradeneeded = () => req.result.createObjectStore(STORE);
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

async function run<T>(mode: IDBTransactionMode, op: (s: IDBObjectStore) => IDBRequest<T>): Promise<T> {
  const db = await open();
  return new Promise((resolve, reject) => {
    const req = op(db.transaction(STORE, mode).objectStore(STORE));
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

export const putPending = (bytes: Uint8Array, name: string) =>
  run('readwrite', (s) => s.put({ bytes, name }, KEY));
export const takePending = () =>
  run<{ bytes: Uint8Array; name: string } | undefined>('readonly', (s) => s.get(KEY));
export const clearPending = () => run('readwrite', (s) => s.delete(KEY));
```

## 2. Upload page: store and navigate

`upload.ts`

```ts
input.addEventListener('change', async () => {
  const file = input.files?.[0];
  if (!file) return;
  await putPending(new Uint8Array(await file.arrayBuffer()), file.name);
  location.href = '/edit';
});
```

## 3. Editor page: open from bytes

`PDFDocument.fromBytes(bytes, options?)` loads a PDF from a `Uint8Array`; `options` is `{ password? }`. `Kernstra.init()` must complete first. A protected file rejects with `PASSWORD_ERROR`: `createPasswordPrompt` asks in place and reopens with the password; resolve `false` from `onUnlock` for a wrong one, which shakes the field and says so.

`edit.ts`

```ts
import { Kernstra, PDFDocument, PDFViewer, SDKError, createPasswordPrompt } from '@kernstra/pdf-web-sdk';

await Kernstra.init({ license: import.meta.env.KERNSTRA_LICENSE });
const pending = await takePending();
if (!pending) { location.href = '/'; throw new Error('nothing to open'); }

let doc: PDFDocument;
try {
  doc = await PDFDocument.fromBytes(pending.bytes);
} catch (err) {
  if (err instanceof SDKError && err.code === 'PASSWORD_ERROR') {
    const prompt = createPasswordPrompt(container, {
      name: pending.name,
      onUnlock: async (password) => {
        try {
          doc = await PDFDocument.fromBytes(pending.bytes, { password });
          prompt.destroy();
          return true;
        } catch {
          return false; // shakes the field
        }
      },
    });
  }
  throw err;
}

const viewer = new PDFViewer(container, doc, { export: { defaultFilename: pending.name } });
```

## 4. Save, then clear the store

`doc.save()` resolves to the document's bytes, a `Uint8Array`: every change made through the API and, with a viewer open, the values its form fields hold. A signature drawn on screen is **not** in it; use `viewer.createExporter()` for everything the user sees. After saving your own way, call `viewer.markSaved()` so `hasUnsavedChanges` is `false` until the next change, then clear the handoff.

`edit.ts`

```ts
saveButton.addEventListener('click', async () => {
  const bytes = await doc.save();
  await fetch('/api/documents', { method: 'POST', body: bytes.slice() });
  viewer.markSaved();
  await clearPending();
});
```

## Gotchas

- `doc.save()` throws `FEATURE_NOT_LICENSED` without `export`, `INVALID_STATE` while a progressive document is still downloading, and `EXPORT_FAILED` when a changed PDF/A file's claim cannot be removed.
- A document opened with a digital signature is saved as an incremental update: the bytes it was signed over, untouched, then one update with what changed.
- `documentLoaded` on `Kernstra.events` fires for `fromBytes` like the other factories, with `{ document, pageCount, hasPassword }`.
- How long the bytes stay in IndexedDB, and clearing them when the person leaves without saving, is your page's decision; the SDK stores nothing of the document itself.

## Related

- [PDFDocument.fromBytes](https://kernstra.com/docs/web-sdk/api/pdfdocument/frombytes)
- [doc.save()](https://kernstra.com/docs/web-sdk/api/pdfdocument/save)
- [Document state helpers](https://kernstra.com/docs/web-sdk/api/helpers/createpasswordprompt)
- [Own the download](https://kernstra.com/docs/web-sdk/recipes/custom-download-s3)

Web page: https://kernstra.com/docs/web-sdk/recipes/handoff-indexeddb
