From 533893d4e7d7431395cf3fb7962c19ccefba95f0 Mon Sep 17 00:00:00 2001 From: Mark Amery Date: Fri, 26 Jan 2024 14:45:06 +0000 Subject: [PATCH] Add `timeout` option (#478) * Add timeout option * Add docs * Add test of timeout feature * Add release notes --- README.md | 2 ++ release-notes.md | 1 + src/diff/base.js | 6 ++++-- test/diff/array.js | 22 ++++++++++++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c02b95d6f..82f077701 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,8 @@ Certain options can be provided in the `options` object of *any* method that cal (Note that if the ONLY option you want to provide is a callback, you can pass the callback function directly as the `options` parameter instead of passing an object with a `callback` property.) * `maxEditLength`: a number specifying the maximum edit distance to consider between the old and new texts. If the edit distance is higher than this, jsdiff will return `undefined` instead of a diff. You can use this to limit the computational cost of diffing large, very different texts by giving up early if the cost will be huge. Works for functions that return change objects and also for `structuredPatch`, but not other patch-generation functions. +* `timeout`: a number of milliseconds after which the diffing algorithm will abort and return `undefined`. Supported by the same functions as `maxEditLength`. + ### Defining custom diffing behaviors If you need behavior a little different to what any of the text diffing functions above offer, you can roll your own by customizing both the tokenization behavior used and the notion of equality used to determine if two tokens are equal. diff --git a/release-notes.md b/release-notes.md index 3919c7405..b7c36f484 100644 --- a/release-notes.md +++ b/release-notes.md @@ -10,6 +10,7 @@ - [#344](https://github.com/kpdecker/jsdiff/issues/344) `diffLines`, `createTwoFilesPatch`, and other patch-creation methods now take an optional `stripTrailingCr: true` option which causes Windows-style `\r\n` line endings to be replaced with Unix-style `\n` line endings before calculating the diff, just like GNU `diff`'s `--strip-trailing-cr` flag. - [#451](https://github.com/kpdecker/jsdiff/pull/451) Added `diff.formatPatch`. - [#450](https://github.com/kpdecker/jsdiff/pull/450) Added `diff.reversePatch`. +- [#478](https://github.com/kpdecker/jsdiff/pull/478) Added `timeout` option. ## v5.1.0 diff --git a/src/diff/base.js b/src/diff/base.js index 99f93d7a4..9e5f0aead 100644 --- a/src/diff/base.js +++ b/src/diff/base.js @@ -33,6 +33,8 @@ Diff.prototype = { if(options.maxEditLength) { maxEditLength = Math.min(maxEditLength, options.maxEditLength); } + const maxExecutionTime = options.timeout ?? Infinity; + const abortAfterTimestamp = Date.now() + maxExecutionTime; let bestPath = [{ oldPos: -1, lastComponent: undefined }]; @@ -128,7 +130,7 @@ Diff.prototype = { if (callback) { (function exec() { setTimeout(function() { - if (editLength > maxEditLength) { + if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) { return callback(); } @@ -138,7 +140,7 @@ Diff.prototype = { }, 0); }()); } else { - while (editLength <= maxEditLength) { + while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) { let ret = execEditLength(); if (ret) { return ret; diff --git a/test/diff/array.js b/test/diff/array.js index 33dcf61a6..455ac118d 100644 --- a/test/diff/array.js +++ b/test/diff/array.js @@ -75,5 +75,27 @@ describe('diff/array', function() { {count: 1, value: [d], removed: undefined, added: true} ]); }); + it('Should terminate early if execution time exceeds `timeout` ms', function() { + // To test this, we also pass a comparator that hot sleeps as a way to + // artificially slow down execution so we reach the timeout. + function comparator(left, right) { + const start = Date.now(); + // Hot-sleep for 10ms + while (Date.now() < start + 10) { + // Do nothing + } + return left === right; + } + + // It will require 14 comparisons (140ms) to diff these arrays: + const arr1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; + const arr2 = ['a', 'b', 'c', 'd', 'x', 'y', 'z']; + + // So with a timeout of 50ms, we are guaranteed failure: + expect(diffArrays(arr1, arr2, {comparator, timeout: 50})).to.be.undefined; + + // But with a longer timeout, we expect success: + expect(diffArrays(arr1, arr2, {comparator, timeout: 1000})).not.to.be.undefined; + }); }); });