These patterns cover the application responsibilities around the patch engine: unique paths, cleanup, integrity, and platform boundaries.
Error messages are diagnostic text. Use code for recovery decisions.
type PatchError = Error & { code?: string };
export function isPatchError(error: unknown): error is PatchError {
return error instanceof Error;
}
try {
await patch(oldPath, outputPath, patchPath);
} catch (error) {
if (isPatchError(error) && error.code === 'EEXIST') {
// Remove a known temporary output or retry with a new unique path.
} else if (isPatchError(error) && error.code === 'ENOENT') {
// Re-download or re-resolve the required old file or patch.
} else {
throw error;
}
}Do not remove a user-owned destination merely because EEXIST was returned.
Only clean paths your application created as temporary outputs.
All platforms use ENDSLEY/BSDIFF43, so a valid workflow can cross runtime
boundaries:
- Generate a patch with
diffon Android or iOS, ordiffByteson Web. - Store or transfer the patch as opaque binary data without text conversion.
- Deliver the exact baseline file expected by that patch.
- Apply it with the API family for the destination runtime.
- Verify the restored bytes against a trusted target hash.
The baseline identity matters as much as the patch. A valid patch applied to the wrong baseline is not a supported update workflow.
Transport security alone does not establish that a patch belongs to the expected release. Distribute a signed manifest containing at least:
- baseline version or baseline digest;
- patch digest and byte length;
- target digest and byte length;
- patch format identifier;
- release identifier and signature metadata.
Verify the manifest and downloaded patch before applying it. Verify the restored file before replacing application data. Cryptographic signing and hashing stay outside this library so applications can use their existing trust model.
Write the reconstructed file to a unique path in the same storage area as the
final destination. After integrity verification, use the filesystem layer to
atomically rename or replace the destination when the platform supports it.
Never ask patch to overwrite the active file directly; output paths are
required to be unused.
The algorithm operates on complete buffers and peak memory can be several times the input or output size. Before starting an operation:
- reject input larger than the product's tested limit;
- confirm sufficient local storage for native temporary outputs;
- prevent unbounded simultaneous calls from user actions;
- use
startDiff/startPatchto enforce native byte limits and expose Cancel; - pass an
AbortSignalto Web operations when cancellation is appropriate; - move very large update generation to controlled backend infrastructure.
Native calls share a library-owned serial queue. Web calls without a signal reuse a shared Worker and WebAssembly instance. Signalled calls use dedicated Workers so they can be terminated independently. The application should still limit aggregate Web concurrency and memory explicitly.
const controller = new AbortController();
const options = {
signal: controller.signal,
maxInputBytes: 64 * 1024 * 1024,
maxOutputBytes: 64 * 1024 * 1024,
};
try {
const patchData = await diffBytes(oldData, newData, options);
const restoredData = await patchBytes(oldData, patchData, options);
} catch (error) {
if (isPatchError(error) && error.code === 'EABORTED') return;
if (isPatchError(error) && error.code === 'ERESOURCE') {
// Show the product's size-limit guidance.
return;
}
throw error;
}The equivalent native control flow uses a job:
const job = startPatch(oldPath, outputPath, patchPath, {
maxInputBytes: 64 * 1024 * 1024,
maxOutputBytes: 128 * 1024 * 1024,
});
const unsubscribe = job.onProgress(renderProgress);
try {
await job.result;
} catch (error) {
if (isPatchError(error) && error.code === 'ECANCELLED') return;
if (
isPatchError(error) &&
['EINPUT_TOO_LARGE', 'EOUTPUT_TOO_LARGE'].includes(error.code || '')
) {
// Show the native size-limit guidance.
return;
}
throw error;
} finally {
unsubscribe();
}Create an object URL, trigger the download, and revoke the URL after use:
const patchData = await diffBytes(oldFile, newFile);
const url = URL.createObjectURL(new Blob([patchData]));
const link = Object.assign(document.createElement('a'), {
href: url,
download: 'release.patch',
});
link.click();
URL.revokeObjectURL(url);Keep the bytes binary when uploading or storing them. Converting arbitrary patch bytes through UTF-8 strings corrupts the data.