PrefetchedImageStore
ImageStore is synchronous. That fits files and bytes; it does not fit a photo library, where resolving a PHAsset means PHImageManager and a callback.
The obvious fix — an async ImageStore, with async encode/decode to match — was rejected: it makes the encode path async for every consumer, including the many with no images at all, to serve the one case that needs it.
This is the other answer. Resolve first, then encode, with the await left in the layer that already had one:
let document = try JSONDecoder().decode(KadrDocument.self, from: data)
let needed = PrefetchedImageStore.tokens(in: document)
let store = PrefetchedImageStore(try await resolve(needed))
let video = try KadrCoding.decode(document, images: store)tokens(in:) is what makes it work: a document is readable on its own, so the tokens are knowable before the composition is built. The chicken-and-egg problem is only apparent.
An image the store was not given is refused rather than issued an invented token — that would encode cleanly and resolve to nothing on the next open, which is exactly the silent loss this package exists to prevent.
StoringImages
A DocC article on where a composition's pictures live: both stores, the relative-token hazard, pruning, and the one rule for writing your own — a token must be stable across saves, or every save rewrites the whole file.
119 tests.