Images and durable media
Persist image bytes with canonical references for restoration, undo and DOCX conversion.
Document JSON stores image references; media services own their bytes. Downloading JSON alone does not guarantee images in another browser. Default memory media suits previews. Production products need durable identities, storage and restoration.
1. Prepare media before creating the instance
media is a creation-time service shared by mounted backends and file services:
| Interface | Responsibility |
|---|---|
sink.put(asset) | Synchronously register complete bytes and return { kind: 'asset', assetId } |
resolver.resolve(reference) | Resolve bytes synchronously or asynchronously |
registrar.register(asset) | Return an accept() / rollback() lease for fallible insertion |
createAssetId(kind) | Allocate an identity unique within durable storage |
An asynchronous upload function cannot implement synchronous sink.put(). Restore assets into a synchronous working set first, or upload and then register/insert inside a guarded commit.
2. Build a saveable working set
The recipe wraps @quirekit/media memory storage and tracks accepted assets. The host supplies restored bytes and server-issued unique IDs through allocateId. For client-generated UUIDs, use v4 from uuid.
import { createQuireMemoryMediaRepository, type QuireMediaAssetInput, type QuireAssetMediaReference } from "@quirekit/media";
import type { ClassicEditorMediaServices } from "@quirekit/classic-editor";
/**
* Builds the synchronous working set from bytes restored by the host before mounting.
* @param restored - Assets fetched with the document's asset manifest.
* @param allocateId - Host allocator of globally unique persistent IDs, such as server-issued IDs.
* @returns Classic services and an accepted-asset snapshot for durable storage.
*/
export function createDocumentMedia(restored: readonly QuireMediaAssetInput[], allocateId: () => string) {
const repository = createQuireMemoryMediaRepository();
const references = new Map<string, QuireAssetMediaReference>();
for (const asset of restored) { const reference = repository.put(asset); references.set(reference.assetId, reference); }
const media: ClassicEditorMediaServices = {
resolver: repository,
createAssetId: allocateId,
sink: {
/** @param asset - Complete in-memory bytes. @returns Registered reference. */
put(asset) { const reference = repository.put(asset); references.set(reference.assetId, reference); return reference; },
},
registrar: {
/** @param asset - Provisional authoring bytes. @returns Lease accepting only committed assets into the save manifest. */
register(asset) {
const lease = repository.register(asset);
return {
reference: lease.reference,
/** @returns Whether this operation accepted the asset. */
accept() {
const accepted = lease.accept();
if (accepted) references.set(lease.reference.assetId, lease.reference);
return accepted;
},
/** @returns Whether unaccepted new bytes were removed. */
rollback: () => lease.rollback(),
};
},
},
};
return {
media,
/** @returns Owned byte copies, including assets retained for undo; not JSON strings or object URLs. */
snapshotAssets(): readonly QuireMediaAssetInput[] {
return [...references.values()].map(reference => {
const asset = repository.resolve(reference)!;
return { assetId: reference.assetId, kind: asset.kind, bytes: asset.bytes,
mimeType: asset.mimeType, fileName: asset.fileName, intrinsicSize: asset.intrinsicSize };
});
},
};
}
Pass media: session.media at creation. snapshotAssets() returns independent byte copies for upload, rather than a binary encoding suitable for ordinary JSON. Store bytes in object storage or IndexedDB with a manifest of asset IDs, content types and storage locations.
Undo history can still reference accepted assets that are no longer visible. The recipe retains them; garbage collection follows the document/history storage policy. Never assign different bytes to an existing asset ID.
3. Save and restore in order
- Capture
instance.getJSON()andsession.snapshotAssets()together. - Upload and confirm required assets. On failure, retain dirty state and do not publish references to missing bytes.
- Commit the document version and manifest. The host storage protocol owns conflicts and atomic publication.
- On reopening, restore bytes under their original IDs, then create Classic with the restored content and media services.
JSON.stringify(Uint8Array) does not produce a directly restorable media bundle. Browser object URLs are temporary resources, not durable document identities.
4. DOCX and failed operations
Official DOCX import stages new images while parsing. It registers them only when the operation can still write into the current document, rolling back new leases if the document commit fails. Providing a registrar establishes cleanup ownership. Custom sink-only implementations handle partial registration failures themselves.
Export requires the resolver to provide real bytes. Successful screen rendering does not prove export support. Hosts explicitly control external-reference fetching; verify cross-origin, expired-link and offline cases. See saving and files for conversion diagnostics and errors.
Verify the complete lifecycle
Insert an image, save, destroy the product, restore through a fresh working set and export DOCX. The image must resolve under the same asset ID. Also test upload failure, cancelled imports, duplicate resources, deletion and undo. Inspect bytes and canonical structure as well as screenshots.