-
Notifications
You must be signed in to change notification settings - Fork 4
Ignore updates to source files while live-reloading is in progress #155 #31
Conversation
"Every one second" is logically different from "debounce", which is closer to "after one second".
A lock does not ensure that only one rebuild per file is called; it ensures that one is run after another. |
| const clear = require('clear'); | ||
| const fs = require('fs-extra-promise'); | ||
| const path = require('path'); | ||
| const Promise = require('bluebird'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why remove this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
es-lint told me it was redundant 🙈
|
How about handling |
Yes. |
lib/Site.js
Outdated
| if (waitingPromise === null) { | ||
| waitingPromise = new Promise((resolve, reject) => { | ||
| Promise.all([Promise.delay(wait), previousPromise]).then(() => { | ||
| previousPromise = waitingPromise || Promise.resolve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is in a block that checks if (waitingPromise === null).
Why don't we just return a chained Promise? Something like:
return waitingPromise.then(() => func.apply(context, pendingArgs));There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using rebuildSourceFiles as an example,
waitingPromisehas not started executing anything inrebuildSourceFilespendingPromiseis still executingrebuildSourceFilesand its promises and is not fulfilled/rejected yet.
Since the args are accumulated, we only need 1 waitingPromise (hence the if (waitingPromise === null) check)
Before we can do func.apply(context, pendingArgs) e.g. rebuild multiple source files, we will have to
- wait for the previous rebuild to complete (or live reloading will crash)
- This was previously done with
async-lock - Now,
waitingPromisemust wait forpendingPromiseto be fulfilled or rejected.
- This was previously done with
- maybe wait for some time so we can batch process a few files together
- waitingPromise wait for
Promise.delay(wait) - This isn't strictly debouncing as it ensures at least k seconds after the first call is made (not the last as in debouncing)
- waitingPromise wait for
Some examples with wait = 1 sec / further clarification
- Changes are first made to File A. We do not rebuild the file immediately. We wait for 1 second and if File B and C change within this second, they will be rebuilt together.
- If the ongoing rebuild is really slow (e.g. 2 sec), we will just collect all the subsequent file paths that need to be rebuilt and rebuild them together after the ongoing one is complete (the 1 sec wait was already complete). This is unlike the debounce + lock approach where they will still be done in batches decided by the debounced call.
- Since the handlers in
index.jsexpect to get a promise back and chain it, we return the samewaitingPromiseto all calls. If one file is missing which will cause ENOENT, multiplecatchmight be executed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation!
I propose to remove the explicit Promise construction:
// Current
waitingPromise = new Promise((resolve, reject) => {
Promise.all([Promise.delay(wait), previousPromise]).then(() => {
previousPromise = waitingPromise || Promise.resolve();
func.apply(context, pendingArgs).then(resolve, reject);
waitingPromise = null;
pendingArgs = [];
});
});
// Proposed
waitingPromise = Promise.all([Promise.delay(wait), previousPromise])
.then(() => {
previousPromise = waitingPromise || Promise.resolve();
waitingPromise = null;
let funcPromise = func.apply(context, pendingArgs);
pendingArgs = [];
return funcPromise;
});In the "Current" case, if previousPromise rejects, then waitingPromise will never resolve (nor reject).
That said, should it be finally instead of then? Otherwise, all of the calls after a reject will always fail.
Can we also rename previousPromise to runningPromise to better reflect the running/waiting state?
Otherwise, previousPromise = waitingPromise looks really odd.
index.js
Outdated
| .then(() => { | ||
| const watcher = chokidar.watch(rootFolder, { | ||
| ignored: [outputFolder, /(^|[/\\])\../], | ||
| ignored: [outputFolder, /(^|[/\\])\../, '/**/*.md__jb_tmp__'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nicholaschuayunzhi Can test this? I don't get this files when I use intellij or webstorm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rachx, I'm still getting changes recorded.

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this worked for me:
ignored: [outputFolder, /(^|[/\\])\../, '*.md__jb_tmp__'],
lib/Site.js
Outdated
| if (waitingPromise === null) { | ||
| waitingPromise = new Promise((resolve, reject) => { | ||
| Promise.all([Promise.delay(wait), previousPromise]).then(() => { | ||
| previousPromise = waitingPromise || Promise.resolve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation!
I propose to remove the explicit Promise construction:
// Current
waitingPromise = new Promise((resolve, reject) => {
Promise.all([Promise.delay(wait), previousPromise]).then(() => {
previousPromise = waitingPromise || Promise.resolve();
func.apply(context, pendingArgs).then(resolve, reject);
waitingPromise = null;
pendingArgs = [];
});
});
// Proposed
waitingPromise = Promise.all([Promise.delay(wait), previousPromise])
.then(() => {
previousPromise = waitingPromise || Promise.resolve();
waitingPromise = null;
let funcPromise = func.apply(context, pendingArgs);
pendingArgs = [];
return funcPromise;
});In the "Current" case, if previousPromise rejects, then waitingPromise will never resolve (nor reject).
That said, should it be finally instead of then? Otherwise, all of the calls after a reject will always fail.
Can we also rename previousPromise to runningPromise to better reflect the running/waiting state?
Otherwise, previousPromise = waitingPromise looks really odd.
lib/Site.js
Outdated
|
|
||
| /** | ||
| * Creates a function that delays invoking func until after wait milliseconds have elapsed | ||
| * and the previous function have resolved/rejected. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-* Creates a function that delays invoking func until after wait milliseconds have elapsed
-* and the previous function have resolved/rejected.
+* Creates a function that delays invoking `func` until after `wait` milliseconds have elapsed
+* and the running `func` has resolved/rejected.
lib/Site.js
Outdated
|
|
||
| /** | ||
| * Creates a function that delays invoking `func` until after `wait` milliseconds have elapsed | ||
| * and the running `func` have resolved/rejected. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"has resolved/rejected"
lib/Site.js
Outdated
| Site.prototype.buildAsset = function (filePath) { | ||
| /** | ||
| * Rebuild pages that are affected by change in filePath | ||
| * @param filePath path corresponding to file that have changed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- This should use the same plural jsdoc, right?
- Can we remove the jsdoc on
_-prefixed versions, and add to non-prefixed versions?
| Site.prototype.rebuildSourceFiles | ||
| = delay(Site.prototype._rebuildSourceFiles, 1000); | ||
|
|
||
| Site.prototype._buildMultipleAssets = function (filePaths) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can buildAssets() call _buildMultipleAssets(filePaths)?
lib/Site.js
Outdated
| const uniquePaths = _.uniq(filePaths); | ||
| const ignoreConfig = this.siteConfig.ignore || []; | ||
| const fileIgnore = ignore().add(ignoreConfig); | ||
| const relativeFiles = uniquePaths.map(filePath => path.relative(this.rootPath, filePath)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fileRelativePaths?
lib/Site.js
Outdated
| const fileRelative = path.relative(this.rootPath, filePath); | ||
| const fileToRemove = path.join(this.outputPath, fileRelative); | ||
| fs.removeAsync(fileToRemove) | ||
| Promise.all(removeFiles) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Remove explicit promise construction when using
Promise.all. - Do the same in
Site.prototype.buildAllAssets.
lib/Site.js
Outdated
| assets.map(asset => fs.copyAsync(path.join(this.rootPath, asset), path.join(this.outputPath, asset))), | ||
| ) | ||
| .then(copyAssets => Promise.all(copyAssets)) | ||
| .then(assetPaths => this._buildMultipleAssets(assetPaths)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, let's revert this if there's almost no benefit.
lib/Site.js
Outdated
| /** | ||
| * Creates a function that delays invoking `func` until after `wait` milliseconds have elapsed | ||
| * and the running `func` has resolved/rejected. | ||
| * @param func the promise-returning function to delay |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func's parameter must be an array, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope but we can change it that way
- Currently, it is using
func.apply(context, pendingArgs);andpendingArgsis supposed to be the argument list. So all the delayed functions take in a variable number of arguments...filePaths - Will you prefer it to be
func.apply(context, [pendingArgs]);instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Function.prototype.apply() calls a function with arguments provided as an array.
The way before 55b874a is preferable, and already the case where func's parameter was an array.
I meant the jsdoc should specify that.
I don't understand why func.apply(context, [pendingArgs]); was suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example
function sum(a, b) { // does not take in an array
return a + b;
}
function wrapsum(a, b) {
// invalid without []
// this passes it as sum(a,b) instead of sum([a, b])
return sum.apply(this, [a, b]);
}If the delayed functions do not take in a variable number of arguments ...filePaths, if there is only one path, it will call rebuildSourceFile("filename") which won't work as an array is expected.
I forgot to use rest parameters for some (building assets) before 55b874a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't seem like a suitable example.
In our case, the wrapped function takes in a single argument — either a filename, or an array.
var _pendingArgs = [];
var _sum = 0;
function sum(a) { // does not take in an array
_sum += a;
return _sum;
}
function wrapsum(...args) {
_pendingArgs.push(...args);
// Fake delay
if (_pendingArgs < 2) return -1;
const pendingArgs = _pendingArgs;
_pendingArgs = [];
return sum.apply(this, [pendingArgs]);
}I mean, if the "original" function only expects only 1 number as above, this won't work:
wrapsum(1); // -1
wrapsum(2); // "01,2"If its parameter is an array:
var _sum = 0;
function sum(arr) { // takes in an array
arr.forEach(a => { _sum += a });
return _sum;
}Then this will work:
wrapsum(1); // -1
wrapsum(2); // 3Now I understand why func.apply(context, [pendingArgs]); was suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I understand why
func.apply(context, [pendingArgs]);was suggested.
I see that it's not necessary if rest parameters are used.
index.js
Outdated
| .then(() => { | ||
| const watcher = chokidar.watch(rootFolder, { | ||
| ignored: [outputFolder, /(^|[/\\])\../, '/**/*.md__jb_tmp__'], | ||
| ignored: [outputFolder, /(^|[/\\])\../, '*.md__jb_tmp__', '*.html__jb_tmp__'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm really sorry, the previous suggestion that i mentioned didn't work - first time i tested i did not have any temp files built..

How about this instead:
ignored: [outputFolder, /(^|[/\\])\../, path => path.endsWith('___jb_tmp___')],I've tested this multiple times and had no issue. Would this be better?
|
I've found another temp file. |
lib/Site.js
Outdated
|
|
||
| return function (arg) { | ||
| context = this; | ||
| pendingArgs.push(arg); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, for the non-prefixed method e.g. rebuildSourceFiles,
it should only take in one filePath parameter.
Currently, that is how it is used in index.js too.
If it is supposed to be multiple parameters
then the function should take in ...args, we should concat instead of push args or flatten the array in the delayed functions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I commented above: The way before 55b874a is preferable.
The spread syntax rest parameters allowed the "wrapped" function to take in one or more parameters.
We should use concat since the "original" function accepted an array (seems clean).
|
@nicholaschuayunzhi @acjh updated to address all comments. |
lib/Site.js
Outdated
| }); | ||
| /** | ||
| * Rebuild pages that are affected by change in filePath | ||
| * @param filePath path corresponding to file that have changed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plural
lib/Site.js
Outdated
| const uniquePaths = _.uniq(filePaths); | ||
| const ignoreConfig = this.siteConfig.ignore || []; | ||
| const fileIgnore = ignore().add(ignoreConfig); | ||
| const relativeFiles = uniquePaths.map(filePath => path.relative(this.rootPath, filePath)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fileRelativePaths?
lib/Site.js
Outdated
| * Creates a function that delays invoking `func` until after `wait` milliseconds have elapsed | ||
| * and the running `func` has resolved/rejected. | ||
| * @param func the promise-returning function to delay, | ||
| * func should be able to take in an indefinite amount of arguments |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Continuing from #31 (comment):
aaron:
func's parameter must be an array, right?
rachx:
Nope but we can change it that way
- Currently, it is using
func.apply(context, pendingArgs);andpendingArgsis supposed to be the argument list. So all the delayed functions take in a variable number of arguments...filePaths- Will you prefer it to be
func.apply(context, [pendingArgs]);instead?
- Can we require that the original
functake in an array, and limitargof the wrapped function to a single parameter? This doesn't seem like a good use case for spread syntax and rest parameters. For flexibility, we can add a check for whetherargis already an array or a single value. - Let's rename
rebuildSourceFilestorebuildAffectedSourceFilesto better reflect what it does (similar to the existingregenerateAffectedPages).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify,
Can we require that the original func take in an array
Does this mean removing the rest parameters in rebuildSourceFiles etc and ensuring an array is always passed in, e.g. by using [pendingArgs] and updating the jsdocs accordingly?
limit arg of the wrapped function to a single parameter
Does this mean changing function (...args) [code] in delay to function (arg).
So arg in rebuildSourceFiles (the non-prefixed method) can be a single value or array
This doesn't seem like a good use case for spread syntax and rest parameters.
Which lines are you referring to?
return function (...args)pendingArgs.push(...args);to extend an arraySite.prototype._buildMultipleAssets = function (...filePaths)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean removing the rest parameters in
rebuildSourceFilesetc and ensuring an array is always passed in, e.g. by using[pendingArgs]and updating the jsdocs accordingly?
Yes.
Does this mean changing
function (...args)[code] indelaytofunction (arg).So
arginrebuildSourceFiles(the non-prefixed method) can be a single value or array
Yes.
Which lines are you referring to?
This as in our use case of passing in a single filePath for "original" function accepting an array.
- is not useful because we really won't be calling it as
buildAsset(filePath1, filePath2)even if we have multiple files. It makes more sense to pass in an array. - seems reasonable at first, but flattening defeats the purpose of rest parameters.
e.g. If we have an "original" function that accepts (param1,param2),pendingArgsbecomes[param1_first, param2_first, param1_second, param2_second]. So we're already expecting the "original" function to collect it into an array via rest parameters. - is not useful for the same reason as 1.
|
I've tested abit and it works well with md files especially if its only content. The delay works well to queue up all other actions. However when i delete files, i think the rebuild process does not work (or it takes too long due to all the logging) and when i readd the files, not all the files are rebuilt. I think this can be handled in a different PR. |
Can you elaborate? I have tested deleting files before - the page that includes those files did rebuild, fresh and show an error that the referenced page was missing. However, the error was logged in the console multiple times Also when I tested in the past, sometimes it seems to "hang" and will work when I type enter or something in the command line |
|
When i delete |
acjh
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work!
lib/Site.js
Outdated
|
|
||
| /** | ||
| * Remove assets that are specified in filePaths | ||
| * @param filePaths a single path or an array of patsh corresponding to the assets to remove |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: patshpaths
…s and until the previous promise has resolved
lib/Site.js
Outdated
| * @param wait the number of milliseconds to delay | ||
| * @returns delayedFunc that takes in a single argument (either an array or a single value) | ||
| */ | ||
| function delay(func, wait) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this function should live under a utility file in ./util/ since it could be used in other places than Site.js.
|
@rachx You can merge this :) |

Resolves MarkBind/markbind#155
Needs/ follows MarkBind/markbind#156
Attempt to address the problem that live reloading crash when there are
Attempted fix -
delayfunction:Using
rebuildSourceFilesas an example,waitingPromisehas not started executing anything inrebuildSourceFilesrunningPromiseis still executingrebuildSourceFilesand its promises and is not fulfilled/rejected yet.Since the args are accumulated, we only need 1
waitingPromise(hence theif (waitingPromise === null)check)Before we can do
func.apply(context, [pendingArgs])e.g. rebuild multiple source files, we will have towaitingPromisemust wait forrunningPromiseto be fulfilled or rejected.Promise.delay(wait)Some examples with wait = 1 sec / further clarification
index.jsexpect to get a promise back and chain it, we return the samewaitingPromiseto all calls. If one file is missing which will cause ENOENT, multiplecatchmight be executed.