/**
* Copyright (c) 2017, Oracle and/or its affiliates.
* All rights reserved.
*/
define(['./persistenceManager', './persistenceUtils', './fetchStrategies',
'./cacheStrategies', './persistenceStoreManager', './impl/defaultCacheHandler', './impl/logger'],
function (persistenceManager, persistenceUtils, fetchStrategies,
cacheStrategies, persistenceStoreManager, cacheHandler, logger) {
'use strict';
/**
* Default Response Proxy
* @export
* @class DefaultResponseProxy
* @classdesc Provides a fetch event listener which uses the default Fetch and Cache strategies.
* @constructor
* @param {{jsonProcessor: Object, fetchStrategy: Function, cacheStrategy: Function}=} options Options
*/
function DefaultResponseProxy(options) {
options = options || {};
if (options['fetchStrategy'] == null) {
options['fetchStrategy'] = fetchStrategies.getCacheIfOfflineStrategy();
}
if (options['cacheStrategy'] == null) {
options['cacheStrategy'] = cacheStrategies.getHttpCacheHeaderStrategy();
}
options.requestHandlerOverride = options.requestHandlerOverride || {};
if (options['requestHandlerOverride']['handleGet'] == null) {
options['requestHandlerOverride']['handleGet'] = this.handleGet;
}
if (options['requestHandlerOverride']['handlePost'] == null) {
options['requestHandlerOverride']['handlePost'] = this.handlePost;
}
if (options['requestHandlerOverride']['handlePut'] == null) {
options['requestHandlerOverride']['handlePut'] = this.handlePut;
}
if (options['requestHandlerOverride']['handlePatch'] == null) {
options['requestHandlerOverride']['handlePatch'] = this.handlePatch;
}
if (options['requestHandlerOverride']['handleDelete'] == null) {
options['requestHandlerOverride']['handleDelete'] = this.handleDelete;
}
if (options['requestHandlerOverride']['handleHead'] == null) {
options['requestHandlerOverride']['handleHead'] = this.handleHead;
}
if (options['requestHandlerOverride']['handleOptions'] == null) {
options['requestHandlerOverride']['handleOptions'] = this.handleOptions;
}
Object.defineProperty(this, '_options', {
value: options
});
};
/**
* Return an instance of the default response proxy
* @method
* @name getResponseProxy
* @param {{jsonProcessor: Object, fetchStrategy: Function, cacheStrategy: Function}=} options Options
* <ul>
* <li>options.jsonProcessor An object containing the JSON shredder, unshredder, and queryHandler for the responses.</li>
* <li>options.jsonProcessor.shredder JSON shredder for the responses</li>
* <li>options.jsonProcessor.unshredder JSON unshredder for the responses</li>
* <li>options.queryHandler query parameter handler. Should be a function object which takes a
* Request and returns a Promise which resolves with a Response
* when the query parameters have been processed. If the Request
* was not handled then resolve to null. The queryHandler object
* also contains an optional function named normalizeQueryParameter
* that takes a request URL and returns the normalized query parameters
* as defined in {@link NormalizedQuery}</li>
* <li>options.fetchStrategy Should be a function which takes a
* Request and returns a Promise which resolves to a Response
* If unspecified then uses the default.</li>
* <li>options.cacheStrategy Should be a function which returns a Promise which
* resolves with a response when the cache expiration behavior has been processed.
* If unspecified then uses the default which
* uses the HTTP cache headers to determine cache expiry.</li>
* <li>options.requestHandlerOverride An object containing request handler overrides.</li>
* <li>options.requestHandlerOverride.handleGet Override the default GET request handler with the supplied function.
* The function should take a Request object as parameter and return a Promise which resolves to a Response object.</li>
* <li>options.requestHandlerOverride.handlePost Override the default POST request handler with the supplied function.
* The function should take a Request object as parameter and return a Promise which resolves to a Response object.</li>
* <li>options.requestHandlerOverride.handlePut Override the default PUT request handler with the supplied function.
* The function should take a Request object as parameter and return a Promise which resolves to a Response object.</li>
* <li>options.requestHandlerOverride.handlePatch Override the default PATCH request handler with the supplied function.
* The function should take a Request object as parameter and return a Promise which resolves to a Response object.</li>
* <li>options.requestHandlerOverride.handleDelete Override the default DELETE request handler with the supplied function.
* The function should take a Request object as parameter and return a Promise which resolves to a Response object.</li>
* <li>options.requestHandlerOverride.handleHead Override the default HEAD request handler with the supplied function.
* The function should take a Request object as parameter and return a Promise which resolves to a Response object.</li>
* <li>options.requestHandlerOverride.handleOptions Override the default OPTIONS request handler with the supplied function.
* The function should take a Request object as parameter and return a Promise which resolves to a Response object.</li>
* </ul>
* @export
* @static
* @memberof DefaultResponseProxy
*/
function getResponseProxy(options) {
return new DefaultResponseProxy(options);
};
/**
* Returns the Fetch Event listener
* @method
* @name getFetchEventListener
* @return {Function} Returns the fetch event listener
* @export
* @instance
* @memberof! DefaultResponseProxy
*/
DefaultResponseProxy.prototype.getFetchEventListener = function () {
var self = this;
return function (event) {
event.respondWith(
self.processRequest(event.request)
)
};
};
/**
* Process the Request. Use this function if you want to chain request
* processing within a fetch event listener.
* @method
* @name processRequest
* @param {Request} request Request object
* @return {Function} Promise
* @export
* @instance
* @memberof! DefaultResponseProxy
*/
DefaultResponseProxy.prototype.processRequest = function (request) {
var self = this;
var endpointKey = persistenceUtils.buildEndpointKey(request);
return new Promise(function (resolve, reject) {
// set the shredder/unshredder information
cacheHandler.registerEndpointOptions(endpointKey, self._options);
var requestHandler = _getRequestHandler(self, request);
var localVars = {};
localVars.isReplayRequest = persistenceUtils.isReplayRequest(request);
var requestClone = request.clone();
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Calling requestHandler for request with enpointKey: " + endpointKey);
requestHandler.call(self, request).then(function (response) {
if (persistenceUtils.isCachedResponse(response)) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Response is cached for request with enpointKey: " + endpointKey);
localVars.isCachedResponse = true;
}
if (response.ok) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Response is ok for request with enpointKey: " + endpointKey);
return _applyCacheStrategy(self, request, response);
} else {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Response is not ok for request with enpointKey: " + endpointKey);
return response;
}
}).then(function (response) {
localVars.response = response;
if (response.ok) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Response is ok after cacheStrategy for request with enpointKey: " + endpointKey);
// cache the shredded data
return _cacheShreddedData(request, response);
} else {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Response is not ok after cacheStrategy for request with enpointKey: " + endpointKey);
return null;
}
}).then(function (undoRedoDataArray) {
if (!localVars.isReplayRequest) {
return _insertSyncManagerRequest(request, undoRedoDataArray, localVars.isCachedResponse && !persistenceManager.isOnline());
}
}).then(function () {
cacheHandler.unregisterEndpointOptions(endpointKey);
resolve(localVars.response);
}).catch(function (err) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Insert Response in syncManager after error for request with enpointKey: " + endpointKey);
if (!localVars.isReplayRequest) {
_insertSyncManagerRequest(requestClone, null, true).then(function() {
cacheHandler.unregisterEndpointOptions(endpointKey);
reject(err);
}, function() {
cacheHandler.unregisterEndpointOptions(endpointKey);
reject(err);
});
} else {
cacheHandler.unregisterEndpointOptions(endpointKey);
reject(err);
}
});
});
};
function _getRequestHandler(defaultResponseProxy, request) {
var self = defaultResponseProxy;
var options = self._options;
var requestHandler = null;
if (persistenceUtils.isReplayRequest(request)) {
requestHandler = self.handleSyncReplay;
} else if (request.method === 'POST') {
requestHandler = options['requestHandlerOverride']['handlePost'];
} else if (request.method === 'GET') {
requestHandler = options['requestHandlerOverride']['handleGet'];
} else if (request.method === 'PUT') {
requestHandler = options['requestHandlerOverride']['handlePut'];
} else if (request.method === 'PATCH') {
requestHandler = options['requestHandlerOverride']['handlePatch'];
} else if (request.method === 'DELETE') {
requestHandler = options['requestHandlerOverride']['handleDelete'];
} else if (request.method === 'HEAD') {
requestHandler = options['requestHandlerOverride']['handleHead'];
} else if (request.method === 'OPTIONS') {
requestHandler = options['requestHandlerOverride']['handleOptions'];
}
return requestHandler;
};
/**
* The default POST request handler.
* The default implementation when offline will return a Response with
* '503 Service Unavailable' error code.
* @method
* @name handlePost
* @param {Request} request Request object
* @return {Promise} Returns a Promise which resolves to a Response object
* @export
* @instance
* @memberof! DefaultResponseProxy
*/
DefaultResponseProxy.prototype.handlePost = function (request) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Processing Request with default POST Handler");
return _handleRequestWithErrorIfOffline(request);
};
function _handleRequestWithErrorIfOffline(request) {
if (!persistenceManager.isOnline()) {
var init = {'status': 503, 'statusText': 'Must provide handlePost override for offline'};
return Promise.resolve(new Response(null, init));
} else {
return persistenceManager.browserFetch(request);
}
};
/**
* The request handler to handle request initiated from sync operation.
* It directs the request handling to browser fetch.
* @method
* @name handleSyncReplay
* @param {Request} request Request object
* @return {Promise} Returns a Promise which resolves to a Response object
* @private
* @instance
* @memberof! DefaultResponseProxy
*/
DefaultResponseProxy.prototype.handleSyncReplay = function (request) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Processing Request from Sync Replay");
// remove the custom header before sending the request out.
persistenceUtils.markReplayRequest(request, false);
return persistenceManager.browserFetch(request);
};
/**
* The default GET request handler.
* Processes the GET Request using the default logic. Can be overrided to provide
* custom processing logic.
* @method
* @name handleGet
* @param {Request} request Request object
* @return {Promise} Returns a Promise which resolves to a Response object
* @export
* @instance
* @memberof! DefaultResponseProxy
*/
DefaultResponseProxy.prototype.handleGet = function (request) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Processing Request with default GET Handler");
return _handleGetWithFetchStrategy(this, request);
};
function _handleGetWithFetchStrategy(defaultResponseProxy, request) {
var self = defaultResponseProxy;
var fetchStrategy = self._options['fetchStrategy'];
return fetchStrategy(request, self._options);
};
/**
* The default HEAD request handler.
* Processes the HEAD Request using the default logic. Can be overrided to provide
* custom processing logic.
* @method
* @name handleHead
* @param {Request} request Request object
* @return {Promise} Returns a Promise which resolves to a Response object
* @export
* @instance
* @memberof! DefaultResponseProxy
*/
DefaultResponseProxy.prototype.handleHead = function (request) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Processing Request with default HEAD Handler");
return _handleGetWithFetchStrategy(this, request);
};
/**
* The default OPTIONS request handler.
* The default implementation when offline will return a Response with
* '503 Service Unavailable' error code.
* @method
* @name handleOptions
* @param {Request} request Request object
* @return {Promise} Returns a Promise which resolves to a Response object
* @export
* @instance
* @memberof! DefaultResponseProxy
*/
DefaultResponseProxy.prototype.handleOptions = function (request) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Processing Request with default OPTIONS Handler");
return _handleRequestWithErrorIfOffline(request);
};
/**
* The default PUT request handler.
* Processes the PUT Request using the default logic. Can be overrided to provide
* custom processing logic.
* @method
* @name handlePut
* @param {Request} request Request object
* @return {Promise} Returns a Promise which resolves to a Response object
* @export
* @instance
* @memberof! DefaultResponseProxy
*/
DefaultResponseProxy.prototype.handlePut = function (request) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Processing Request with default PUT Handler");
return _handlePutRequest(this, request);
};
function _handlePutRequest(defaultResponseProxy, request) {
var self = defaultResponseProxy;
if (persistenceManager.isOnline()) {
return persistenceManager.browserFetch(request.clone()).then(function (response) {
// check for response.ok. That indicates HTTP status in the 200-299 range
if (response.ok) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Response is ok for default PUT Handler");
return response;
} else {
return _handleResponseNotOk(self, request, response, _handleOfflinePutRequest);
}
}, function (err) {
return _handleOfflinePutRequest(self, request);
});
} else {
return _handleOfflinePutRequest(self, request);
}
};
function _handleOfflinePutRequest(defaultResponseProxy, request) {
// first we convert the Request obj to JSON and then we create a
// a Response obj from that JSON. Request/Response objs have similar
// properties so that is equivalent to creating a Response obj by
// copying over Request obj values.
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Processing offline logic for default PUT Handler");
return persistenceUtils.requestToJSON(request).then(function (requestData) {
requestData.status = 200;
requestData.statusText = 'OK';
requestData.headers['content-type'] = 'application/json';
requestData.headers['x-oracle-jscpt-cache-expiration-date'] = '';
// if the request contains an ETag then we have to generate a new one
var ifMatch = requestData.headers['if-match'];
var ifNoneMatch = requestData.headers['if-none-match'];
if (ifMatch || ifNoneMatch) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Generating ETag for offline Response for default PUT Handler");
var randomInt = Math.floor(Math.random() * 1000000); // @RandomNumberOK - Only used to generate ETag while offline
requestData.headers['etag'] = (Date.now() + randomInt).toString();
requestData.headers['x-oracle-jscpt-etag-generated'] = requestData.headers['etag'];
delete requestData.headers['if-match'];
delete requestData.headers['if-none-match'];
}
return persistenceUtils.responseFromJSON(requestData);
});
};
/**
* The default PATCH request handler.
* The default implementation when offline will return a Response with
* '503 Service Unavailable' error code.
* @method
* @name handlePatch
* @param {Request} request Request object
* @return {Promise} Returns a Promise which resolves to a Response object
* @export
* @instance
* @memberof! DefaultResponseProxy
*/
DefaultResponseProxy.prototype.handlePatch = function (request) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Processing Request with default PATCH Handler");
return _handleRequestWithErrorIfOffline(request);
};
/**
* The default DELETE request handler.
* Processes the DELETE Request using the default logic. Can be overridden to provide
* custom processing logic.
* @method
* @name handleDelete
* @param {Request} request Request object
* @return {Promise} Returns a Promise which resolves to a Response object
* @export
* @instance
* @memberof! DefaultResponseProxy
*/
DefaultResponseProxy.prototype.handleDelete = function (request) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Processing Request with default DELETE Handler");
return _handleDeleteRequest(this, request);
};
function _handleDeleteRequest(defaultResponseProxy, request) {
var self = defaultResponseProxy;
if (persistenceManager.isOnline()) {
return persistenceManager.browserFetch(request.clone()).then(function (response) {
// check for response.ok. That indicates HTTP status in the 200-299 range
if (response.ok) {
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Response is ok for default DELETE Handler");
return response;
} else {
return _handleResponseNotOk(self, request, response, _handleOfflineDeleteRequest);
}
}, function (err) {
return _handleOfflineDeleteRequest(self, request);
});
} else {
return _handleOfflineDeleteRequest(self, request);
}
};
function _handleOfflineDeleteRequest(defaultResponseProxy, request) {
var self = defaultResponseProxy;
// first we convert the Request obj to JSON and then we create a
// a Response obj from that JSON. Request/Response objs have similar
// properties so that is equivalent to creating a Response obj by
// copying over Request obj values.
logger.log("Offline Persistence Toolkit DefaultResponseProxy: Processing offline logic for default DELETE Handler");
return persistenceUtils.requestToJSON(request).then(function (requestData) {
requestData.status = 200;
requestData.statusText = 'OK';
requestData.headers['content-type'] = 'application/json';
requestData.headers['x-oracle-jscpt-cache-expiration-date'] = '';
return persistenceUtils.responseFromJSON(requestData).then(function (response) {
// for DELETE requests, we don't have data in the payload but
// the response does so we have to get the data from the shredded
// store to construct a response.
// the DELETE key is in the URL
var key = _getRequestUrlId(request);
// query for the data
var jsonShredder = null;
if (self._options && self._options.jsonProcessor &&
self._options.jsonProcessor.shredder) {
jsonShredder = self._options.jsonProcessor.shredder;
}
if (jsonShredder) {
return jsonShredder(response).then(function (shreddedObjArray) {
if (shreddedObjArray) {
// only look at the first one
var storeName = shreddedObjArray[0]['name'];
return persistenceStoreManager.openStore(storeName).then(function (store) {
return store.findByKey(key).then(function (row) {
// set the payload with the data we got from the shredded store
if (row) {
return persistenceUtils.responseFromJSON(requestData).then(function (response) {
return persistenceUtils.setResponsePayload(response, row).then(function (response) {
return response;
});
});
} else {
return response;
}
});
});
} else {
return response;
}
});
} else {
// if we don't have shredded data then just resolve. The Response obj payload
// will be empty but that's the best we can do.
return response;
}
});
});
};
function _handleResponseNotOk(defaultResponseProxy, request, response, offlineHandler) {
var self = defaultResponseProxy;
// for 300-499 range, we should not fetch from cache.
// 300-399 are redirect errors
// 400-499 are client errors which should be handled by the client
if (response.status < 500) {
return Promise.resolve(response);
} else {
// 500-599 are server errors so we can fetch from cache
return offlineHandler(self, request);
}
};
function _getRequestUrlId(request) {
var urlTokens = request.url.split('/');
return urlTokens[urlTokens.length - 1];
};
function _applyCacheStrategy(defaultResponseProxy, request, response) {
var self = defaultResponseProxy;
if (request.method === 'GET' ||
request.method === 'HEAD') {
var cacheStrategy = self._options['cacheStrategy'];
return cacheStrategy(request, response, self._options);
} else {
return Promise.resolve(response);
}
};
function _insertSyncManagerRequest(request, undoRedoDataArray, force) {
if (!persistenceManager.isOnline() || force) {
// put the request in the sync manager if offline or if force is true
return persistenceManager.getSyncManager().insertRequest(request, {'undoRedoDataArray': undoRedoDataArray});
}
return Promise.resolve();
};
function _cacheShreddedData(request, response) {
if (request.method == 'GET' ||
request.method == 'HEAD') {
// the cache strategy would have cached the response unless
// response is not to be stored, e.g. no-store. In that case we don't want
// to shred. Either way, we do not need to shred again here
// since the cache strategy should have shredded it unless it should not
// be stored.
return Promise.resolve();
} else {
return _processShreddedData(request, response);
}
};
function _processShreddedData(request, response) {
return cacheHandler.constructShreddedData(request, response).then(function (shreddedData) {
if (shreddedData) {
// if we have shredded data then update the local store with it
return _updateShreddedDataStore(request, shreddedData);
} else {
return Promise.resolve();
}
});
};
function _updateShreddedDataStore(request, shreddedData) {
var promises = [];
shreddedData.forEach(function (shreddedDataItem) {
var storename = Object.keys(shreddedDataItem)[0];
promises.push(_updateShreddedDataStoreForItem(request, storename, shreddedDataItem[storename]));
});
return Promise.all(promises);
};
function _updateShreddedDataStoreForItem(request, storename, shreddedDataItem) {
return _getUndoRedoDataForShreddedDataItem(request, storename, shreddedDataItem).then(function (undoRedoArray) {
if (request.method === 'DELETE') {
if (!shreddedDataItem || shreddedDataItem.length === 0) {
var deletedItemId = _getRequestUrlId(request);
shreddedDataItem = [{key: deletedItemId}];
}
return _updateShreddedDataStoreForDeleteRequest(storename, shreddedDataItem, undoRedoArray);
} else {
return _updateShreddedDataStoreForNonDeleteRequest(storename, shreddedDataItem, undoRedoArray);
}
});
};
function _getRequestUrlId(request) {
var urlTokens = request.url.split('/');
if (urlTokens.length > 1) {
return urlTokens[urlTokens.length - 1].split('?')[0];
}
return null;
};
function _getUndoRedoDataForShreddedDataItem(request, storename, shreddedDataItem) {
var undoRedoArray = [];
var key;
var value;
var undoRedoData = function (i, dataArray) {
// we should not have any undoRedo data for GET requests
if (i < dataArray.length &&
request.method !== 'GET' &&
request.method !== 'HEAD') {
// when deleting a row offline then coming online to sync
// it obtains a 'document deleted' doc which does not contain a key
if (!dataArray[i]['key']){
return undoRedoData(++i, dataArray);
}
key = dataArray[i]['key'].toString();
if (request.method !== 'DELETE') {
value = dataArray[i]['value'];
} else {
// redo data is null for DELETE
value = null;
}
// find the existing data so we can get the undo data
return persistenceStoreManager.openStore(storename).then(function (store) {
return store.findByKey(key).then(function (undoRow) {
undoRedoArray.push({'key': key, 'undo': undoRow, 'redo': value});
return undoRedoData(++i, dataArray);
}, function (error) {
// if there is no existing data then undo is null
undoRedoArray.push({'key': key, 'undo': null, 'redo': value});
return undoRedoData(++i, dataArray);
});
});
} else {
return Promise.resolve(undoRedoArray);
}
};
return undoRedoData(0, shreddedDataItem);
};
function _updateShreddedDataStoreForNonDeleteRequest(storename, shreddedDataItem, undoRedoArray) {
// for other requests, upsert the shredded data
return persistenceStoreManager.openStore(storename).then(function (store) {
return store.upsertAll(shreddedDataItem);
}).then(function () {
if (undoRedoArray.length > 0) {
return {'storeName': storename, 'operation': 'upsert', 'undoRedoData': undoRedoArray};
} else {
return null;
}
});
};
function _updateShreddedDataStoreForDeleteRequest(storename, shreddedDataItem, undoRedoArray) {
// for DELETE requests, simple remove the existing shredded data
return persistenceStoreManager.openStore(storename).then(function (store) {
return store.removeByKey(shreddedDataItem[0]['key']);
}).then(function () {
if (undoRedoArray.length > 0) {
return {'storeName': storename, 'operation': 'remove', 'undoRedoData': undoRedoArray};
} else {
return null;
}
});
};
return {'getResponseProxy': getResponseProxy};
});
Source: defaultResponseProxy.js