Skip to content

Commit ad1dc1c

Browse files
sunnylqmclaude
andcommitted
feat: add diffWindow (window/fast-match single-format generation)
create_single_compressed_diff_window binding: big covers from streaming block matching + suffix-string refinement inside a 2MB window over old data. Output is standard HDIFFSF20 (verified applying via legacy v3.1.1 client core), so all existing apply sides work unchanged. Benchmarks vs diffSingleStream(block=64): 218KB hermes pair 4.9KB vs 19KB, 100MB pair 455B vs 2258B, same ~27MB RSS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b6178fd commit ad1dc1c

8 files changed

Lines changed: 178 additions & 2 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ in-memory `diff()` for the same inputs; prefer `diff()` when memory allows.
4646
In sync mode returns `outDiffPath`; async callback signature is
4747
`(err, outDiffPath)`.
4848

49+
### diffWindow(oldPath, newPath, outDiffPath[, cb])
50+
51+
Create a **single-format** (same wire format as `diff()`) patch using window
52+
mode: big covers come from streaming block matching, then residuals are
53+
refined with suffix-string matching inside a sliding window (2MB) over the old
54+
data. Match quality is close to the in-memory `diff()` while generation memory
55+
stays at the streaming tier — usually a much smaller patch than
56+
`diffSingleStream()` for the same inputs. The output applies with `patch()`,
57+
`patchSingleStream()`, and any existing single-format apply side. In sync mode
58+
returns `outDiffPath`; async callback signature is `(err, outDiffPath)`.
59+
4960
### patchSingleStream(oldPath, diffPath, outNewPath[, cb])
5061

5162
Apply a single-compressed hpatch payload created by `diff` or

index.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ export interface NativeAddon {
3838
outNewPath: string,
3939
cb: StreamCallback
4040
): void;
41+
diffWindow(oldPath: string, newPath: string, outDiffPath: string): string;
42+
diffWindow(
43+
oldPath: string,
44+
newPath: string,
45+
outDiffPath: string,
46+
cb: StreamCallback
47+
): void;
4148
}
4249

4350
export const native: NativeAddon;
@@ -102,6 +109,20 @@ export function patchSingleStream(
102109
cb: StreamCallback
103110
): void;
104111

112+
// window 模式生成 HDIFFSF20 single 格式 patch:匹配质量接近内存版
113+
// diff(),内存占用保持流式档;产物用 patch()/patchSingleStream() 应用
114+
export function diffWindow(
115+
oldPath: string,
116+
newPath: string,
117+
outDiffPath: string
118+
): string;
119+
export function diffWindow(
120+
oldPath: string,
121+
newPath: string,
122+
outDiffPath: string,
123+
cb: StreamCallback
124+
): void;
125+
105126
declare const hdiffpatch: {
106127
native: NativeAddon;
107128
diff: typeof diff;
@@ -110,6 +131,7 @@ declare const hdiffpatch: {
110131
patchStream: typeof patchStream;
111132
diffSingleStream: typeof diffSingleStream;
112133
patchSingleStream: typeof patchSingleStream;
134+
diffWindow: typeof diffWindow;
113135
};
114136

115137
export default hdiffpatch;

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ exports.diffStream = native.diffStream;
3737
exports.patchStream = native.patchStream;
3838
exports.diffSingleStream = native.diffSingleStream;
3939
exports.patchSingleStream = native.patchSingleStream;
40+
exports.diffWindow = native.diffWindow;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-hdiffpatch",
3-
"version": "2.0.0",
3+
"version": "2.1.0",
44
"description": "hdiffpatch port to node.js",
55
"main": "index.js",
66
"types": "index.d.ts",

src/hdiff.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,39 @@ void hdiff_stream(const char* oldPath,const char* newPath,const char* outDiffPat
145145
streams.closeAllOrThrow();
146146
}
147147

148+
void hdiff_window(const char* oldPath,const char* newPath,const char* outDiffPath){
149+
if (!oldPath || !newPath || !outDiffPath) {
150+
throw std::runtime_error("Invalid file path.");
151+
}
152+
153+
hpatch_TDecompress* decompressPlugin = &lzma2DecompressPlugin;
154+
TCompressPlugin_lzma2 compressPlugin;
155+
configure_lzma2(compressPlugin);
156+
157+
FileStreamGuard streams;
158+
streams.openInputs(oldPath, newPath);
159+
streams.openDiffOut(outDiffPath);
160+
161+
// window 模式:大块流式匹配拿大 cover,再在 old 数据的滑动窗口
162+
// (默认 2MB)内做后缀串精修。除 patchStepMemSize/匹配分沿用本库
163+
// 固定值外,其余参数取 v5 默认。
164+
create_single_compressed_diff_window(&streams.newStream.base, &streams.oldStream.base,
165+
&streams.diffOutStream.base,
166+
&compressPlugin.base, kPatchStepMemSize,
167+
kDefaultWindowOldSize, 0,
168+
kDefaultBigCoverSize, kMatchWindowsBlockSize_default,
169+
kDefaultFastMatchBlockSize,
170+
kSingleMatchScore);
171+
172+
streams.closeDiffOut();
173+
streams.openDiffIn(outDiffPath);
174+
if (!check_single_compressed_diff(&streams.newStream.base, &streams.oldStream.base,
175+
&streams.diffInStream.base, decompressPlugin)) {
176+
throw std::runtime_error("check_single_compressed_diff() failed, diff code error!");
177+
}
178+
streams.closeAllOrThrow();
179+
}
180+
148181
void hdiff_single_stream(const char* oldPath,const char* newPath,const char* outDiffPath){
149182
if (!oldPath || !newPath || !outDiffPath) {
150183
throw std::runtime_error("Invalid file path.");

src/hdiff.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@ void hdiff_stream(const char* oldPath,const char* newPath,const char* outDiffPat
1515
// HDIFFSF20 single 格式的流式生成(生成端低内存,产物与 diff() 同格式,
1616
// 任何既有 single 应用端可直接使用)
1717
void hdiff_single_stream(const char* oldPath,const char* newPath,const char* outDiffPath);
18+
// HDIFFSF20 single 格式的 window 模式生成:大块流式匹配 + 窗口内后缀串
19+
// 精修,匹配质量接近内存版而内存占用保持流式档;产物与 diff() 同格式
20+
void hdiff_window(const char* oldPath,const char* newPath,const char* outDiffPath);
1821

1922
#endif

src/main.cc

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,45 @@ namespace hdiffpatchNode
466466
return Napi::String::New(env, outNewPath);
467467
}
468468

469+
// ============ 异步 Window Diff Worker ============
470+
class DiffWindowAsyncWorker : public Napi::AsyncWorker {
471+
public:
472+
DiffWindowAsyncWorker(Napi::Function& callback,
473+
std::string oldPath,
474+
std::string newPath,
475+
std::string outDiffPath)
476+
: Napi::AsyncWorker(callback),
477+
oldPath_(std::move(oldPath)),
478+
newPath_(std::move(newPath)),
479+
outDiffPath_(std::move(outDiffPath)) {
480+
}
481+
482+
void Execute() override {
483+
try {
484+
hdiff_window(oldPath_.c_str(), newPath_.c_str(), outDiffPath_.c_str());
485+
} catch (const std::exception& e) {
486+
SetError(e.what());
487+
}
488+
}
489+
490+
void OnOK() override {
491+
Napi::Env env = Env();
492+
Napi::HandleScope scope(env);
493+
Callback().Call({env.Null(), Napi::String::New(env, outDiffPath_)});
494+
}
495+
496+
void OnError(const Napi::Error& e) override {
497+
Napi::Env env = Env();
498+
Napi::HandleScope scope(env);
499+
Callback().Call({e.Value()});
500+
}
501+
502+
private:
503+
std::string oldPath_;
504+
std::string newPath_;
505+
std::string outDiffPath_;
506+
};
507+
469508
// ============ 同步/异步 diffSingleStream ============
470509
// single 格式(HDIFFSF20)的流式生成:低内存(块匹配),产物与 diff()
471510
// 同格式,所有既有 single 应用端(含历史客户端)可直接应用。
@@ -503,6 +542,44 @@ namespace hdiffpatchNode
503542
return Napi::String::New(env, outDiffPath);
504543
}
505544

545+
// ============ 同步/异步 diffWindow ============
546+
// single 格式(HDIFFSF20)的 window 模式生成:大块流式匹配 + 窗口内
547+
// 后缀串精修,匹配质量接近内存版 diff() 而内存占用保持流式档。
548+
// 产物与 diff()/diffSingleStream() 同格式,既有应用端可直接应用。
549+
Napi::Value diffWindow(const Napi::CallbackInfo& info) {
550+
Napi::Env env = info.Env();
551+
552+
std::string oldPath;
553+
std::string newPath;
554+
std::string outDiffPath;
555+
if (info.Length() < 3 ||
556+
!getStringUtf8(info[0], oldPath) ||
557+
!getStringUtf8(info[1], newPath) ||
558+
!getStringUtf8(info[2], outDiffPath)) {
559+
Napi::TypeError::New(env, "Invalid arguments: expected (oldPath, newPath, outDiffPath).")
560+
.ThrowAsJavaScriptException();
561+
return env.Undefined();
562+
}
563+
564+
if (info.Length() > 3 && info[3].IsFunction()) {
565+
Napi::Function callback = info[3].As<Napi::Function>();
566+
DiffWindowAsyncWorker* worker = new DiffWindowAsyncWorker(
567+
callback, oldPath, newPath, outDiffPath
568+
);
569+
worker->Queue();
570+
return env.Undefined();
571+
}
572+
573+
try {
574+
hdiff_window(oldPath.c_str(), newPath.c_str(), outDiffPath.c_str());
575+
} catch (const std::exception& e) {
576+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
577+
return env.Undefined();
578+
}
579+
580+
return Napi::String::New(env, outDiffPath);
581+
}
582+
506583
// ============ 同步/异步 patchSingleStream ============
507584
Napi::Value patchSingleStream(const Napi::CallbackInfo& info) {
508585
Napi::Env env = info.Env();
@@ -544,6 +621,7 @@ namespace hdiffpatchNode
544621
exports.Set(Napi::String::New(env, "diffStream"), Napi::Function::New(env, diffStream));
545622
exports.Set(Napi::String::New(env, "patchStream"), Napi::Function::New(env, patchStream));
546623
exports.Set(Napi::String::New(env, "diffSingleStream"), Napi::Function::New(env, diffSingleStream));
624+
exports.Set(Napi::String::New(env, "diffWindow"), Napi::Function::New(env, diffWindow));
547625
exports.Set(Napi::String::New(env, "patchSingleStream"), Napi::Function::New(env, patchSingleStream));
548626
return exports;
549627
}

test/test.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,27 @@ hdiffpatch.patchSingleStream(ssOldPath, ssDiffPath, ssOutPath);
7070
assert.deepStrictEqual(fs.readFileSync(ssOutPath), newData);
7171
console.log(" ✓ diffSingleStream output applies via patch() and patchSingleStream()");
7272

73+
console.log("\nTest 5b: diffWindow (single format, window/fast-match generation)...");
74+
var winDiffPath = path.join(ssDir, "win.diff");
75+
var winOutPath = path.join(ssDir, "win-out.bin");
76+
var winDiffOut = hdiffpatch.diffWindow(ssOldPath, ssNewPath, winDiffPath);
77+
assert.strictEqual(winDiffOut, winDiffPath);
78+
var winDiffData = fs.readFileSync(winDiffPath);
79+
// 产物是标准 single 格式(HDIFFSF20 开头),与 diff() 产物同规范
80+
assert.strictEqual(winDiffData.slice(0, 9).toString("latin1"), "HDIFFSF20");
81+
assert.deepStrictEqual(hdiffpatch.patch(oldData, winDiffData), newData);
82+
hdiffpatch.patchSingleStream(ssOldPath, winDiffPath, winOutPath);
83+
assert.deepStrictEqual(fs.readFileSync(winOutPath), newData);
84+
// 匹配质量应接近内存版:不应显著差于 diff() 产物(经验上限 1.5x)
85+
assert(
86+
winDiffData.length <= Math.max(diffResult.length * 1.5, diffResult.length + 256),
87+
"diffWindow size " + winDiffData.length + " vs diff " + diffResult.length
88+
);
89+
console.log(
90+
" ✓ diffWindow output (" + winDiffData.length + "B vs diff " + diffResult.length +
91+
"B) applies via patch() and patchSingleStream()"
92+
);
93+
7394
console.log("\nTest 6: Single-compressed patchSingleStream (file paths)...");
7495
var tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hdiffpatch-"));
7596
var oldPath = path.join(tempDir, "old.bin");
@@ -109,6 +130,7 @@ var patchAsync = util.promisify(hdiffpatch.patch);
109130
var diffStreamAsync = util.promisify(hdiffpatch.diffStream);
110131
var patchStreamAsync = util.promisify(hdiffpatch.patchStream);
111132
var patchSingleStreamAsync = util.promisify(hdiffpatch.patchSingleStream);
133+
var diffWindowAsync = util.promisify(hdiffpatch.diffWindow);
112134

113135
async function runAsyncTests() {
114136
console.log("\nTest 8: Async diff/patch callbacks...");
@@ -140,7 +162,13 @@ async function runAsyncTests() {
140162
await assert.rejects(
141163
() => patchStreamAsync(oldPath, path.join(tempDir, "no-such.diff"), asyncOutPath)
142164
);
143-
console.log(" ✓ Async stream diff/patch works");
165+
var asyncWinDiffPath = path.join(tempDir, "async-win.diff");
166+
assert.strictEqual(await diffWindowAsync(oldPath, newPath, asyncWinDiffPath), asyncWinDiffPath);
167+
assert.deepStrictEqual(hdiffpatch.patch(oldData, fs.readFileSync(asyncWinDiffPath)), newData);
168+
await assert.rejects(
169+
() => diffWindowAsync(path.join(tempDir, "no-such.bin"), newPath, asyncWinDiffPath)
170+
);
171+
console.log(" ✓ Async stream diff/patch works (incl. diffWindow)");
144172

145173
console.log("\nTest 11: CLI auto-detects both diff formats...");
146174
var cliBin = path.join(__dirname, "..", "bin", "hdiffpatch.js");

0 commit comments

Comments
 (0)