Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 110 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,81 +1,101 @@
# react-native-bs-diff-patch

Create a compact binary patch from two versions of a file, then reconstruct the
new version from the old file and that patch. Android, iOS, and React Native Web
all use the compatible `ENDSLEY/BSDIFF43` wire format.

[Documentation](https://bs-dff-patch.corerobin.com/docs/) ·
[Playground](https://bs-dff-patch.corerobin.com/#playground) ·
[中文说明](./README.zh-CN.md) · [npm](https://www.npmjs.com/package/react-native-bs-diff-patch)

## Why use it?

- **One patch format:** generate on one supported runtime and apply on another.
- **Both React Native architectures:** legacy bridge and TurboModule/New Architecture.
- **Responsive by default:** native work uses dedicated serial queues; Web work
reuses a module Worker and cached WebAssembly instance off the page thread.
- **Control expensive work:** native jobs expose progress, cooperative
cancellation, input/output limits, and atomic output; Web uses
`AbortSignal` and binary limits.
- **No Web service required:** the browser implementation is the same bundled C
core compiled to WebAssembly.

| Runtime | Input model | Create a patch | Apply a patch |
| ------------ | ------------------ | -------------- | ------------- |
| Android, iOS | Absolute paths | `diff` | `patch` |
| Web | In-memory binaries | `diffBytes` | `patchBytes` |

## Installation
<p align="center">
<a href="https://bs-dff-patch.corerobin.com/">
<img src="https://bs-dff-patch.corerobin.com/assets/social-preview.png" alt="Binary patches everywhere React Native runs: Android, iOS, and Web" width="100%" />
</a>
</p>

<p align="center">
<strong>Turn two versions of a file into a compact binary patch, then reconstruct the new file from the old file plus that patch.</strong><br />
One compatible format across React Native Android, iOS, and Web.
</p>

<p align="center">
<a href="https://www.npmjs.com/package/react-native-bs-diff-patch"><img src="https://img.shields.io/npm/v/react-native-bs-diff-patch?color=b8ff3d&label=npm" alt="npm version" /></a>
<a href="https://www.npmjs.com/package/react-native-bs-diff-patch"><img src="https://img.shields.io/npm/dm/react-native-bs-diff-patch?color=39e6ff" alt="npm downloads" /></a>
<a href="https://github.com/JimmyDaddy/react-native-bs-diff-patch/actions/workflows/ci.yml"><img src="https://github.com/JimmyDaddy/react-native-bs-diff-patch/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
<a href="./LICENSE"><img src="https://img.shields.io/npm/l/react-native-bs-diff-patch?color=f6bf6f" alt="MIT license" /></a>
</p>

<p align="center">
<a href="https://bs-dff-patch.corerobin.com/docs/">Documentation</a> ·
<a href="https://bs-dff-patch.corerobin.com/#playground">Live Playground</a> ·
<a href="./README.zh-CN.md">中文说明</a> ·
<a href="https://www.npmjs.com/package/react-native-bs-diff-patch">npm</a>
</p>

## What does it do?

Use it when your app already has an old version of a file and you want to move
to a new version without transporting the complete replacement file.

| 1. Create the delta | 2. Deliver it your way | 3. Reconstruct the file |
| ------------------------------------------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Compare `old.bin` with `new.bin` and produce `update.patch`. | Send or store the patch with your existing CDN, API, or offline workflow. | Apply `update.patch` to `old.bin` and write the restored `new.bin`. |

The library handles binary diffing and patching. Your application remains in
control of transport, authentication, integrity checks, and when an output
replaces live data.

## Why this package?

- **One wire format:** Android, iOS, and Web produce compatible
`ENDSLEY/BSDIFF43` patches.
- **Every current RN runtime:** legacy bridge and TurboModule/New Architecture
are both supported.
- **Native performance, browser reach:** JNI/ObjC++ use the bundled C core;
React Native Web runs that core as WebAssembly in a reusable module Worker.
- **Control expensive native work:** observe progress, cancel cooperatively,
cap input/output sizes, and avoid exposing partial output files.
- **No patch service required:** Web diffing and patching happen locally in the
browser.

## Platform overview

| | Android / iOS | React Native Web |
| -------------- | ------------------------------ | -------------------------------------------------- |
| Input | Absolute file paths | `ArrayBuffer`, typed arrays, `DataView`, or `Blob` |
| Basic API | `diff()` / `patch()` | `diffBytes()` / `patchBytes()` |
| Controlled API | `startDiff()` / `startPatch()` | `AbortSignal` and binary limits |
| Engine | Native C via JNI / ObjC++ | Same C core via WASM Worker |

## Install

```sh
npm install react-native-bs-diff-patch
```

After adding the package, install iOS pods and rebuild the native app:
For iOS, install Pods and rebuild the native application:

```sh
npx pod-install
```

React Native autolinking handles native registration. A Metro reload alone is
not enough after adding a native dependency.
React Native autolinking handles native registration. Adding a native module
requires a native rebuild; a Metro reload is not enough.

## Native quick start
## Native: first round trip

The native API works with absolute file paths. Use the filesystem library
already present in your app to select a writable cache directory.
Native APIs use absolute paths. Pick unique output paths in a writable cache or
documents directory through the filesystem library already used by your app.

```ts
import { diff, patch } from 'react-native-bs-diff-patch';

type NativeRoundTripOptions = {
oldFilePath: string;
newFilePath: string;
cacheDirectory: string;
};

export async function nativeRoundTrip({
oldFilePath,
newFilePath,
cacheDirectory,
}: NativeRoundTripOptions) {
const runId = Date.now();
const patchPath = `${cacheDirectory}/update-${runId}.patch`;
const restoredPath = `${cacheDirectory}/restored-${runId}.bin`;

await diff(oldFilePath, newFilePath, patchPath);
await patch(oldFilePath, restoredPath, patchPath);

return { patchPath, restoredPath };
}
const patchPath = `${cacheDirectory}/content-v2.patch`;
const restoredPath = `${cacheDirectory}/content-v2.restored`;

await diff(oldFilePath, newFilePath, patchPath);
await patch(oldFilePath, restoredPath, patchPath);
```

Output paths must not exist, all paths in one call must be different, and the
required input files must already exist. Both functions resolve to `0` on
success.
Input files must already exist. Output paths must not exist, and all paths in a
single call must be different. Both functions resolve to `0` on success.

### Progress, cancellation, and limits

Use the job API when the operation needs progress, cancellation, or resource
bounds:
Use the job API for work that needs lifecycle control:

```ts
import { startPatch } from 'react-native-bs-diff-patch';
Expand All @@ -84,47 +104,41 @@ const job = startPatch(oldPath, outputPath, patchPath, {
maxInputBytes: 64 * 1024 * 1024,
maxOutputBytes: 128 * 1024 * 1024,
});

const unsubscribe = job.onProgress(({ phase, progress }) => {
renderProgress(phase, progress);
});

try {
await job.result;
// await job.cancel(); // cancel from your UI when needed
} finally {
unsubscribe();
}
```

## React Native Web quick start
## Web: first round trip

```ts
import { diffBytes, patchBytes } from 'react-native-bs-diff-patch';

export async function webRoundTrip(
oldFile: File,
newFile: File,
signal?: AbortSignal
) {
const oldData = await oldFile.arrayBuffer();
const newData = await newFile.arrayBuffer();
const options = {
signal,
maxInputBytes: 64 * 1024 * 1024,
maxOutputBytes: 64 * 1024 * 1024,
};
const patchData = await diffBytes(oldData, newData, options);
const restoredData = await patchBytes(oldData, patchData, options);

return { patchData, restoredData };
}
const oldBytes = await oldFile.arrayBuffer();
const newBytes = await newFile.arrayBuffer();

const patchBytesValue = await diffBytes(oldBytes, newBytes, {
signal: abortController.signal,
maxInputBytes: 64 * 1024 * 1024,
});
const restoredBytes = await patchBytes(oldBytes, patchBytesValue, {
maxOutputBytes: 64 * 1024 * 1024,
});
```

`diffBytes` and `patchBytes` accept `ArrayBuffer`, any `ArrayBufferView`
(including typed arrays and `DataView`), or `Blob`. They resolve to a new
`Uint8Array` and leave the caller's buffers usable. Aborted operations reject
with `EABORTED`; configured size limits reject with `ERESOURCE`.
Web calls return a new `Uint8Array` and leave caller-owned buffers usable.
Aborted operations reject with `EABORTED`; configured binary limits reject with
`ERESOURCE`.

## Platform API matrix
## API matrix

| API | Android | iOS | Web |
| ------------------------------------------ | ------- | --- | --- |
Expand All @@ -133,29 +147,31 @@ with `EABORTED`; configured size limits reject with `ERESOURCE`.
| `startDiff(...)` / `startPatch(...)` | Yes | Yes | No |
| `diffBytes(oldData, newData, options?)` | No | No | Yes |
| `patchBytes(oldData, patchData, options?)` | No | No | Yes |
| Legacy architecture (when provided by RN) | Yes | Yes | N/A |
| Legacy architecture, while supplied by RN | Yes | Yes | N/A |
| New Architecture / TurboModule | Yes | Yes | N/A |

Calling an API family that is unavailable on the current platform rejects with
`EUNSUPPORTED` instead of silently choosing different behavior.
Unavailable platform APIs reject with `EUNSUPPORTED`; the package never
silently switches to a different input model.

## Production checklist
## Production safety

- Verify the restored output before replacing application data.
- Authenticate patches from remote or otherwise untrusted sources.
- Use unique native output paths and clean temporary files after success or failure.
- Set product-specific input-size and time limits; binary diffing can use
several times the input size in peak memory.
- Verify restored output before replacing application data.
- Use unique native output paths and remove outputs you no longer need.
- Set product-specific resource limits. Binary diffing can use several times
the input size in peak memory.
- Generate and apply patches with this library. Generic `BSDIFF40` patches are
not interchangeable with `ENDSLEY/BSDIFF43` patches.

See [Production recipes](./docs/recipes.md) for error handling, downloads,
cross-runtime patch exchange, and integrity checks.
See [Production recipes](./docs/recipes.md) for integrity checks, downloads,
cross-runtime exchange, error handling, and cleanup patterns.

## Verified compatibility

CI directly compiles the Android New Architecture sources against React Native
0.73.11, 0.74.7, and 0.86.0. Packed-consumer tests also verify that browser,
ESM, CommonJS, and TypeScript resolution work without installing optional React
Native peers for Web-only consumers.
CI compiles the Android and iOS APIs against React Native 0.73.11, 0.74.7, and
0.86.0, and runs device-level New Architecture assertions on Android and iOS.
Packed-consumer tests verify browser, ESM, CommonJS, Metro, and TypeScript
resolution from the real npm package shape.

## Documentation

Expand Down
Loading