Scope sorting
Chalarangelo opened this issue · 2 comments
Sorting scopes based on a sorter function would be pretty nice. In fact, it could be a a major feature, especially if we can couple it with scope caching to optimize performance for data retrieval.
Update
Scope caching is in no way feasible at this time. However, scope sorting is pretty reasonable to implement and it will net a major performance optimization, as follows:
Current implementation
Supposing that one would query a scope and then sort it, the following steps must be taken:
- Call
RecordSet.prototype.where()
. - Call
RecordSet.prototype.filter()
. - Call
RecordSet.prototype.entries()
. - Spread the result to an array.
- Call
Array.prototype.reduce()
. - Call
RecordSet()
constructor to create a newRecordSet
. - Call
callbackFn
for eachRecord
andRecordSet.prototype.set()
if the result of the function is truthy. - Call
RecordSet.prototype.freeze()
. - Call
RecordSet.prototype.sort()
. - Call
RecordSet.prototype.entries()
. - Spread the result to an array.
- Call
comparatorFn
for each pair ofRecord
objects until the result is sorted. - Call
RecordSet()
constructor to create a newRecordSet
. - Call
RecordSet.prototype.freeze()
.
For obvious reason, this is not extremely performant.
Proposed implementation
Supposing that a scope can receive a third argument for the comparatorFn
function either via an optional argument or as part of an object defining the scope, the code for calling the scope could be refactored to an internal $scopedWhere
function as follows:
const $scopedWhere = Symbol.for('scopedWhere');
class RecordSet {
[$scopedWhere](matcherFn, comparatorFn) {
let matches = [];
for (const entry of this.entries())
if (matcherFn(value, key, this)) matches.push(entry);
if (comparatorFn)
matches.sort(([key1, value1], [key2, value2]) => comparatorFn(value1, value2, key1, key2));
return new RecordSet({ iterable: matches, copyScopesFrom: this }).freeze();
}
}
The steps are as follows:
- Call
RecordSet.prototype[$scopedWhere]()
. - Create an empty array.
- Call
RecordSet.prototype.entries()
. - Use a
for...of
loop over the iterator. - Call
matcherFn
for eachRecord
and Array.prototype.push()` if the result of the function is truthy. - If
comparatorFn
is truthy, callArray.prototype.sort()
. - Call
comparatorFn
for each pair ofRecord
objects until the result is sorted. - Call
RecordSet()
constructor to create a newRecordSet
. - Call
RecordSet.prototype.freeze()
.
As shown above, this reduces the steps nearly in half and ensures that no unnecessary conversions are performed. It also optimizes memory usage by avoiding to spread the result of RecordSet.prototype.entries()
to an array and instead uses a loop to iterate over the data, which can also boost performance.
Potential improvements
Generally speaking, Array.prototype.push()
is considered pretty slow. Alternatively, spreading and filtering can be applied, if more performant.
Array.prototype.push()
seems to perform better than Array.prototype.filter()
(link):