I found it useful to recreate the part of the dataloader that I use to see one of the possible ways to implement it. (in my case, I only use the .load() function)
So, creating a new instance of the DataLoader constructor gives you two things:
- List of identifiers (empty for starters)
- A function that uses this list of identifiers to query the database (you provide this).
The constructor might look something like this:
function DataLoader (_batchLoadingFn) { this._keys = [] this._batchLoadingFn = _batchLoadingFn }
And instances of the DataLoader constructor have access to the .load() function, which should be able to access the _keys property. So, it is defined for the DataLoad.prototype object:
DataLoader.prototype.load = function(key) {
When creating a new object using the DataLoader constructor ( new DataLoader(fn) ), the fn you pass in must retrieve data from somewhere, taking an array of keys as arguments and returning a promise that resolves to the array of values โโthat correspond to the original key array.
For example, here is a dummy function that takes an array of keys and passes the same array back, but with doubled values:
const batchLoadingFn = keys => new Promise( resolve => resolve(keys.map(k => k * 2)) )
keys: [1,2,3] vals: [2,4,6] keys[0] corresponds to vals[0] keys[1] corresponds to vals[1] keys[2] corresponds to vals[2]
Then, each time you call the .load(indentifier) function, you add the key to the _keys array, and at some point batchLoadingFn is batchLoadingFn and the _keys array is _keys as an argument.
The trick is ... How do I call .load(id) many times, but batchLoadingFn execute only once? This is cool, and the reason I learned how this library works.
I found that this can be done by indicating that batchLoadingFn is executed after the timeout, but if .load() is called again before the timeout interval, then the timeout is canceled, a new key is added and batchLoadingFn is batchLoadingFn transferred. Achieving this in code looks like this:
DataLoader.prototype.load = function(key) { clearTimeout(this._timer) this.timer = setTimeout(() => this.batchLoadingFn(), 0) }
Essentially, calling .load() removes pending calls to batchLoadingFn , and then schedules a new call to batchLoadingFn at the end of the event loop. This ensures that for a short period of time, if .load() is called many times, batchLoadingFn will be called only once. Actually it is very similar to disassembly . Or at least it's useful when building websites, and you want to do something for the mousemove event, but you get a lot more events than you want to deal with. I think this is called exposure.
But to call .load(key) also need to press a key in the _keys array, which we can do in the body of the .load function by passing the key argument to _keys (just this._keys.push(key) ). However, the contract for the .load function is that it returns a single value related to what the key argument refers to. At some point, batchLoadingFn will be called and the result will be obtained (it should return a result corresponding to _keys ). In addition, batchLoadingFn is required to actually return a promise of this value.
The next bit that I thought was especially smart (and was worth it to look at the source code)!
The dataloader library dataloader instead of storing the list of keys in _keys actually contains a list of keys associated with a reference to the resolve function, which, when called, .load() value as a result of .load() . .load() returns a promise, the promise is resolved when the resolve function is called.
Thus, the _keys array actually stores a list of tuples [key, resolve] . And when your batchLoadingFn returns, the resolve function is called with a value (which, we hope, matches the element in the _keys array through the sequence number).
Thus, the .load function looks as follows (in terms of placing the [key, resolve] tuple in the _keys array):
DataLoader.prototype.load = function(key) { const promisedValue = new Promise ( resolve => this._keys.push({key, resolve}) ) ... return promisedValue }
And it remains only to execute batchLoadingFn with the batchLoadingFn keys as an argument and call the correct resolve function for it, return
this._batchLoadingFn(this._keys.map(k => k.key)) .then(values => { this._keys.forEach(({resolve}, i) => { resolve(values[i]) }) this._keys = []
And collectively, all the code for implementing the above is here:
function DataLoader (_batchLoadingFn) { this._keys = [] this._batchLoadingFn = _batchLoadingFn } DataLoader.prototype.load = function(key) { clearTimeout(this._timer) const promisedValue = new Promise ( resolve => this._keys.push({key, resolve}) ) this._timer = setTimeout(() => { console.log('You should only see me printed once!') this._batchLoadingFn(this._keys.map(k => k.key)) .then(values => { this._keys.forEach(({resolve}, i) => { resolve(values[i]) }) this._keys = [] }) }, 0) return promisedValue }
If I remember correctly, I don't think the dataloader library uses setTimeout , but uses process.nextTick instead. But I could not get this to work.