Implement Couch Search and Request Batching (#3798)

* Implemented search in couch provider

* Promises, not await

* Batch requests

* Only batch if > 1

* Remove legacy Couch adapter

* Cleaned up couch request batching code

* Added test cases

* Code cleanup

* Changes to new and legacy objects API to remove redundant persists due to mutation of modified and persisted timestamps

* Cleaned up couch unit tests

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
This commit is contained in:
Andrew Henry
2021-04-22 06:29:28 -07:00
committed by GitHub
parent 9fa71244ea
commit 564f254652
15 changed files with 273 additions and 871 deletions

View File

@@ -26,6 +26,7 @@ import CouchObjectQueue from "./CouchObjectQueue";
const REV = "_rev";
const ID = "_id";
const HEARTBEAT = 50000;
const ALL_DOCS = "_all_docs?include_docs=true";
export default class CouchObjectProvider {
// options {
@@ -41,6 +42,8 @@ export default class CouchObjectProvider {
this.objectQueue = {};
this.observeEnabled = options.disableObserve !== true;
this.observers = {};
this.batchIds = [];
if (this.observeEnabled) {
this.observeObjectChanges(options.filter);
}
@@ -67,6 +70,9 @@ export default class CouchObjectProvider {
// stringify body if needed
if (fetchOptions.body) {
fetchOptions.body = JSON.stringify(fetchOptions.body);
fetchOptions.headers = {
"Content-Type": "application/json"
};
}
return fetch(this.url + '/' + subPath, fetchOptions)
@@ -78,14 +84,18 @@ export default class CouchObjectProvider {
});
}
// Check the response to a create/update/delete request;
// track the rev if it's valid, otherwise return false to
// indicate that the request failed.
// persist any queued objects
/**
* Check the response to a create/update/delete request;
* track the rev if it's valid, otherwise return false to
* indicate that the request failed.
* persist any queued objects
* @private
*/
checkResponse(response, intermediateResponse) {
let requestSuccess = false;
const id = response ? response.id : undefined;
let rev;
if (response && response.ok) {
rev = response.rev;
requestSuccess = true;
@@ -106,6 +116,9 @@ export default class CouchObjectProvider {
}
}
/**
* @private
*/
getModel(response) {
if (response && response.model) {
let key = response[ID];
@@ -131,10 +144,118 @@ export default class CouchObjectProvider {
}
get(identifier, abortSignal) {
return this.request(identifier.key, "GET", undefined, abortSignal).then(this.getModel.bind(this));
this.batchIds.push(identifier.key);
if (this.bulkPromise === undefined) {
this.bulkPromise = this.deferBatchedGet(abortSignal);
}
return this.bulkPromise
.then((domainObjectMap) => {
return domainObjectMap[identifier.key];
});
}
async getObjectsByFilter(filter) {
/**
* @private
*/
deferBatchedGet(abortSignal) {
// We until the next event loop cycle to "collect" all of the get
// requests triggered in this iteration of the event loop
return this.waitOneEventCycle().then(() => {
let batchIds = this.batchIds;
this.clearBatch();
if (batchIds.length === 1) {
let objectKey = batchIds[0];
//If there's only one request, just do a regular get
return this.request(objectKey, "GET", undefined, abortSignal)
.then(this.returnAsMap(objectKey));
} else {
return this.bulkGet(batchIds, abortSignal);
}
});
}
/**
* @private
*/
returnAsMap(objectKey) {
return (result) => {
let objectMap = {};
objectMap[objectKey] = this.getModel(result);
return objectMap;
};
}
/**
* @private
*/
clearBatch() {
this.batchIds = [];
delete this.bulkPromise;
}
/**
* @private
*/
waitOneEventCycle() {
return new Promise((resolve) => {
setTimeout(resolve);
});
}
/**
* @private
*/
bulkGet(ids, signal) {
ids = this.removeDuplicates(ids);
const query = {
'keys': ids
};
return this.request(ALL_DOCS, 'POST', query, signal).then((response) => {
if (response && response.rows !== undefined) {
return response.rows.reduce((map, row) => {
if (row.doc !== undefined) {
map[row.key] = this.getModel(row.doc);
}
return map;
}, {});
} else {
return {};
}
});
}
/**
* @private
*/
removeDuplicates(array) {
return Array.from(new Set(array));
}
search(query, abortSignal) {
const filter = {
"selector": {
"model": {
"name": {
"$regex": `(?i)${query}`
}
}
}
};
return this.getObjectsByFilter(filter, abortSignal);
}
async getObjectsByFilter(filter, abortSignal) {
let objects = [];
let url = `${this.url}/_find`;
@@ -149,6 +270,7 @@ export default class CouchObjectProvider {
headers: {
"Content-Type": "application/json"
},
signal: abortSignal,
body
});
@@ -203,6 +325,9 @@ export default class CouchObjectProvider {
};
}
/**
* @private
*/
abortGetChanges() {
if (this.controller) {
this.controller.abort();
@@ -212,6 +337,9 @@ export default class CouchObjectProvider {
return true;
}
/**
* @private
*/
async observeObjectChanges(filter) {
let intermediateResponse = this.getIntermediateResponse();
@@ -292,6 +420,9 @@ export default class CouchObjectProvider {
}
/**
* @private
*/
getIntermediateResponse() {
let intermediateResponse = {};
intermediateResponse.promise = new Promise(function (resolve, reject) {
@@ -302,6 +433,9 @@ export default class CouchObjectProvider {
return intermediateResponse;
}
/**
* @private
*/
enqueueObject(key, model, intermediateResponse) {
if (this.objectQueue[key]) {
this.objectQueue[key].enqueue({
@@ -330,6 +464,9 @@ export default class CouchObjectProvider {
return intermediateResponse.promise;
}
/**
* @private
*/
updateQueued(key) {
if (!this.objectQueue[key].pending) {
this.objectQueue[key].pending = true;