blob: b3a3a01948528232371acb879b2313d417013600 [file] [log] [blame]
/// <reference path="odata-utils.js" />
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
// modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// cache.js
(function (window, undefined) {
var datajs = window.datajs || {};
var assigned = datajs.assigned;
var delay = datajs.delay;
var extend = datajs.extend;
var djsassert = datajs.djsassert;
var isArray = datajs.isArray;
var normalizeURI = datajs.normalizeURI;
var parseInt10 = datajs.parseInt10;
var undefinedDefault = datajs.undefinedDefault;
var createDeferred = datajs.createDeferred;
var DjsDeferred = datajs.DjsDeferred;
var ODataCacheSource = datajs.ODataCacheSource;
// CONTENT START
var appendPage = function (operation, page) {
/// <summary>Appends a page's data to the operation data.</summary>
/// <param name="operation" type="Object">Operation with (i)ndex, (c)ount and (d)ata.</param>
/// <param name="page" type="Object">Page with (i)ndex, (c)ount and (d)ata.</param>
var intersection = intersectRanges(operation, page);
if (intersection) {
var start = intersection.i - page.i;
var end = start + (operation.c - operation.d.length);
operation.d = operation.d.concat(page.d.slice(start, end));
}
};
var intersectRanges = function (x, y) {
/// <summary>Returns the {(i)ndex, (c)ount} range for the intersection of x and y.</summary>
/// <param name="x" type="Object">Range with (i)ndex and (c)ount members.</param>
/// <param name="y" type="Object">Range with (i)ndex and (c)ount members.</param>
/// <returns type="Object">The intersection (i)ndex and (c)ount; undefined if there is no intersection.</returns>
var xLast = x.i + x.c;
var yLast = y.i + y.c;
var resultIndex = (x.i > y.i) ? x.i : y.i;
var resultLast = (xLast < yLast) ? xLast : yLast;
var result;
if (resultLast >= resultIndex) {
result = { i: resultIndex, c: resultLast - resultIndex };
}
return result;
};
var checkZeroGreater = function (val, name) {
/// <summary>Checks whether val is a defined number with value zero or greater.</summary>
/// <param name="val" type="Number">Value to check.</param>
/// <param name="name" type="String">Parameter name to use in exception.</param>
if (val === undefined || typeof val !== "number") {
throw { message: "'" + name + "' must be a number." };
}
if (isNaN(val) || val < 0 || !isFinite(val)) {
throw { message: "'" + name + "' must be greater than or equal to zero." };
}
};
var checkUndefinedGreaterThanZero = function (val, name) {
/// <summary>Checks whether val is undefined or a number with value greater than zero.</summary>
/// <param name="val" type="Number">Value to check.</param>
/// <param name="name" type="String">Parameter name to use in exception.</param>
if (val !== undefined) {
if (typeof val !== "number") {
throw { message: "'" + name + "' must be a number." };
}
if (isNaN(val) || val <= 0 || !isFinite(val)) {
throw { message: "'" + name + "' must be greater than zero." };
}
}
};
var checkUndefinedOrNumber = function (val, name) {
/// <summary>Checks whether val is undefined or a number</summary>
/// <param name="val" type="Number">Value to check.</param>
/// <param name="name" type="String">Parameter name to use in exception.</param>
if (val !== undefined && (typeof val !== "number" || isNaN(val) || !isFinite(val))) {
throw { message: "'" + name + "' must be a number." };
}
};
var removeFromArray = function (arr, item) {
/// <summary>Performs a linear search on the specified array and removes the first instance of 'item'.</summary>
/// <param name="arr" type="Array">Array to search.</param>
/// <param name="item">Item being sought.</param>
/// <returns type="Boolean">Whether the item was removed.</returns>
var i, len;
for (i = 0, len = arr.length; i < len; i++) {
if (arr[i] === item) {
arr.splice(i, 1);
return true;
}
}
return false;
};
var estimateSize = function (obj) {
/// <summary>Estimates the size of an object in bytes.</summary>
/// <param name="obj" type="Object">Object to determine the size of.</param>
/// <returns type="Integer">Estimated size of the object in bytes.</returns>
var size = 0;
var type = typeof obj;
if (type === "object" && obj) {
for (var name in obj) {
size += name.length * 2 + estimateSize(obj[name]);
}
} else if (type === "string") {
size = obj.length * 2;
} else {
size = 8;
}
return size;
};
var snapToPageBoundaries = function (lowIndex, highIndex, pageSize) {
/// <summary>Snaps low and high indices into page sizes and returns a range.</summary>
/// <param name="lowIndex" type="Number">Low index to snap to a lower value.</param>
/// <param name="highIndex" type="Number">High index to snap to a higher value.</param>
/// <param name="pageSize" type="Number">Page size to snap to.</param>
/// <returns type="Object">A range with (i)ndex and (c)ount of elements.</returns>
lowIndex = Math.floor(lowIndex / pageSize) * pageSize;
highIndex = Math.ceil((highIndex + 1) / pageSize) * pageSize;
return { i: lowIndex, c: highIndex - lowIndex };
};
// The DataCache is implemented using state machines. The following constants are used to properly
// identify and label the states that these machines transition to.
// DataCache state constants
var CACHE_STATE_DESTROY = "destroy";
var CACHE_STATE_IDLE = "idle";
var CACHE_STATE_INIT = "init";
var CACHE_STATE_READ = "read";
var CACHE_STATE_PREFETCH = "prefetch";
var CACHE_STATE_WRITE = "write";
// DataCacheOperation state machine states.
// Transitions on operations also depend on the cache current of the cache.
var OPERATION_STATE_CANCEL = "cancel";
var OPERATION_STATE_END = "end";
var OPERATION_STATE_ERROR = "error";
var OPERATION_STATE_START = "start";
var OPERATION_STATE_WAIT = "wait";
// Destroy state machine states
var DESTROY_STATE_CLEAR = "clear";
// Read / Prefetch state machine states
var READ_STATE_DONE = "done";
var READ_STATE_LOCAL = "local";
var READ_STATE_SAVE = "save";
var READ_STATE_SOURCE = "source";
var DataCacheOperation = function (stateMachine, promise, isCancelable, index, count, data, pending) {
/// <summary>Creates a new operation object.</summary>
/// <param name="stateMachine" type="Function">State machine that describes the specific behavior of the operation.</param>
/// <param name="promise" type ="DjsDeferred">Promise for requested values.</param>
/// <param name="isCancelable" type ="Boolean">Whether this operation can be canceled or not.</param>
/// <param name="index" type="Number">Index of first item requested.</param>
/// <param name="count" type="Number">Count of items requested.</param>
/// <param name="data" type="Array">Array with the items requested by the operation.</param>
/// <param name="pending" type="Number">Total number of pending prefetch records.</param>
/// <returns type="DataCacheOperation">A new data cache operation instance.</returns>
/// <field name="p" type="DjsDeferred">Promise for requested values.</field>
/// <field name="i" type="Number">Index of first item requested.</field>
/// <field name="c" type="Number">Count of items requested.</field>
/// <field name="d" type="Array">Array with the items requested by the operation.</field>
/// <field name="s" type="Array">Current state of the operation.</field>
/// <field name="canceled" type="Boolean">Whether the operation has been canceled.</field>
/// <field name="pending" type="Number">Total number of pending prefetch records.</field>
/// <field name="oncomplete" type="Function">Callback executed when the operation reaches the end state.</field>
var stateData;
var cacheState;
var that = this;
that.p = promise;
that.i = index;
that.c = count;
that.d = data;
that.s = OPERATION_STATE_START;
that.canceled = false;
that.pending = pending;
that.oncomplete = null;
that.cancel = function () {
/// <summary>Transitions this operation to the cancel state and sets the canceled flag to true.</summary>
/// <remarks>The function is a no-op if the operation is non-cancelable.</summary>
if (!isCancelable) {
return;
}
var state = that.s;
if (state !== OPERATION_STATE_ERROR && state !== OPERATION_STATE_END && state !== OPERATION_STATE_CANCEL) {
that.canceled = true;
transition(OPERATION_STATE_CANCEL, stateData);
}
};
that.complete = function () {
/// <summary>Transitions this operation to the end state.</summary>
djsassert(that.s !== OPERATION_STATE_END, "DataCacheOperation.complete() - operation is in the end state", that);
transition(OPERATION_STATE_END, stateData);
};
that.error = function (err) {
/// <summary>Transitions this operation to the error state.</summary>
if (!that.canceled) {
djsassert(that.s !== OPERATION_STATE_END, "DataCacheOperation.error() - operation is in the end state", that);
djsassert(that.s !== OPERATION_STATE_ERROR, "DataCacheOperation.error() - operation is in the error state", that);
transition(OPERATION_STATE_ERROR, err);
}
};
that.run = function (state) {
/// <summary>Executes the operation's current state in the context of a new cache state.</summary>
/// <param name="state" type="Object">New cache state.</param>
cacheState = state;
that.transition(that.s, stateData);
};
that.wait = function (data) {
/// <summary>Transitions this operation to the wait state.</summary>
djsassert(that.s !== OPERATION_STATE_END, "DataCacheOperation.wait() - operation is in the end state", that);
transition(OPERATION_STATE_WAIT, data);
};
var operationStateMachine = function (opTargetState, cacheState, data) {
/// <summary>State machine that describes all operations common behavior.</summary>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
switch (opTargetState) {
case OPERATION_STATE_START:
// Initial state of the operation. The operation will remain in this state until the cache has been fully initialized.
if (cacheState !== CACHE_STATE_INIT) {
stateMachine(that, opTargetState, cacheState, data);
}
break;
case OPERATION_STATE_WAIT:
// Wait state indicating that the operation is active but waiting for an asynchronous operation to complete.
stateMachine(that, opTargetState, cacheState, data);
break;
case OPERATION_STATE_CANCEL:
// Cancel state.
stateMachine(that, opTargetState, cacheState, data);
that.fireCanceled();
transition(OPERATION_STATE_END);
break;
case OPERATION_STATE_ERROR:
// Error state. Data is expected to be an object detailing the error condition.
stateMachine(that, opTargetState, cacheState, data);
that.canceled = true;
that.fireRejected(data);
transition(OPERATION_STATE_END);
break;
case OPERATION_STATE_END:
// Final state of the operation.
if (that.oncomplete) {
that.oncomplete(that);
}
if (!that.canceled) {
that.fireResolved();
}
stateMachine(that, opTargetState, cacheState, data);
break;
default:
// Any other state is passed down to the state machine describing the operation's specific behavior.
// DATAJS INTERNAL START
if (true) {
// Check that the state machine actually handled the sate.
var handled = stateMachine(that, opTargetState, cacheState, data);
djsassert(handled, "Bad operation state: " + opTargetState + " cacheState: " + cacheState, this);
} else {
// DATAJS INTERNAL END
stateMachine(that, opTargetState, cacheState, data);
// DATAJS INTERNAL START
}
// DATAJS INTERNAL END
break;
}
};
var transition = function (state, data) {
/// <summary>Transitions this operation to a new state.</summary>
/// <param name="state" type="Object">State to transition the operation to.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
that.s = state;
stateData = data;
operationStateMachine(state, cacheState, data);
};
that.transition = transition;
return that;
};
DataCacheOperation.prototype.fireResolved = function () {
/// <summary>Fires a resolved notification as necessary.</summary>
// Fire the resolve just once.
var p = this.p;
if (p) {
this.p = null;
p.resolve(this.d);
}
};
DataCacheOperation.prototype.fireRejected = function (reason) {
/// <summary>Fires a rejected notification as necessary.</summary>
// Fire the rejection just once.
var p = this.p;
if (p) {
this.p = null;
p.reject(reason);
}
};
DataCacheOperation.prototype.fireCanceled = function () {
/// <summary>Fires a canceled notification as necessary.</summary>
this.fireRejected({ canceled: true, message: "Operation canceled" });
};
var DataCache = function (options) {
/// <summary>Creates a data cache for a collection that is efficiently loaded on-demand.</summary>
/// <param name="options">
/// Options for the data cache, including name, source, pageSize,
/// prefetchSize, cacheSize, storage mechanism, and initial prefetch and local-data handler.
/// </param>
/// <returns type="DataCache">A new data cache instance.</returns>
var state = CACHE_STATE_INIT;
var stats = { counts: 0, netReads: 0, prefetches: 0, cacheReads: 0 };
var clearOperations = [];
var readOperations = [];
var prefetchOperations = [];
var actualCacheSize = 0; // Actual cache size in bytes.
var allDataLocal = false; // Whether all data is local.
var cacheSize = undefinedDefault(options.cacheSize, 1048576); // Requested cache size in bytes, default 1 MB.
var collectionCount = 0; // Number of elements in the server collection.
var highestSavedPage = 0; // Highest index of all the saved pages.
var highestSavedPageSize = 0; // Item count of the saved page with the highest index.
var overflowed = cacheSize === 0; // If the cache has overflowed (actualCacheSize > cacheSize or cacheSize == 0);
var pageSize = undefinedDefault(options.pageSize, 50); // Number of elements to store per page.
var prefetchSize = undefinedDefault(options.prefetchSize, pageSize); // Number of elements to prefetch from the source when the cache is idling.
var version = "1.0";
var cacheFailure;
var pendingOperations = 0;
var source = options.source;
if (typeof source === "string") {
// Create a new cache source.
source = new ODataCacheSource(options);
}
source.options = options;
// Create a cache local store.
var store = datajs.createStore(options.name, options.mechanism);
var that = this;
that.onidle = options.idle;
that.stats = stats;
that.count = function () {
/// <summary>Counts the number of items in the collection.</summary>
/// <returns type="Object">A promise with the number of items.</returns>
if (cacheFailure) {
throw cacheFailure;
}
var deferred = createDeferred();
var canceled = false;
if (allDataLocal) {
delay(function () {
deferred.resolve(collectionCount);
});
return deferred.promise();
}
// TODO: Consider returning the local data count instead once allDataLocal flag is set to true.
var request = source.count(function (count) {
request = null;
stats.counts++;
deferred.resolve(count);
}, function (err) {
request = null;
deferred.reject(extend(err, { canceled: canceled }));
});
return extend(deferred.promise(), {
cancel: function () {
/// <summary>Aborts the count operation.</summary>
if (request) {
canceled = true;
request.abort();
request = null;
}
}
});
};
that.clear = function () {
/// <summary>Cancels all running operations and clears all local data associated with this cache.</summary>
/// <remarks>
/// New read requests made while a clear operation is in progress will not be canceled.
/// Instead they will be queued for execution once the operation is completed.
/// </remarks>
/// <returns type="Object">A promise that has no value and can't be canceled.</returns>
if (cacheFailure) {
throw cacheFailure;
}
if (clearOperations.length === 0) {
var deferred = createDeferred();
var op = new DataCacheOperation(destroyStateMachine, deferred, false);
queueAndStart(op, clearOperations);
return deferred.promise();
}
return clearOperations[0].p;
};
that.filterForward = function (index, count, predicate) {
/// <summary>Filters the cache data based a predicate.</summary>
/// <param name="index" type="Number">The index of the item to start filtering forward from.</param>
/// <param name="count" type="Number">Maximum number of items to include in the result.</param>
/// <param name="predicate" type="Function">Callback function returning a boolean that determines whether an item should be included in the result or not.</param>
/// <remarks>
/// Specifying a negative count value will yield all the items in the cache that satisfy the predicate.
/// </remarks>
/// <returns type="DjsDeferred">A promise for an array of results.</returns>
return filter(index, count, predicate, false);
};
that.filterBack = function (index, count, predicate) {
/// <summary>Filters the cache data based a predicate.</summary>
/// <param name="index" type="Number">The index of the item to start filtering backward from.</param>
/// <param name="count" type="Number">Maximum number of items to include in the result.</param>
/// <param name="predicate" type="Function">Callback function returning a boolean that determines whether an item should be included in the result or not.</param>
/// <remarks>
/// Specifying a negative count value will yield all the items in the cache that satisfy the predicate.
/// </remarks>
/// <returns type="DjsDeferred">A promise for an array of results.</returns>
return filter(index, count, predicate, true);
};
that.readRange = function (index, count) {
/// <summary>Reads a range of adjacent records.</summary>
/// <param name="index" type="Number">Zero-based index of record range to read.</param>
/// <param name="count" type="Number">Number of records in the range.</param>
/// <remarks>
/// New read requests made while a clear operation is in progress will not be canceled.
/// Instead they will be queued for execution once the operation is completed.
/// </remarks>
/// <returns type="DjsDeferred">
/// A promise for an array of records; less records may be returned if the
/// end of the collection is found.
/// </returns>
checkZeroGreater(index, "index");
checkZeroGreater(count, "count");
if (cacheFailure) {
throw cacheFailure;
}
var deferred = createDeferred();
// Merging read operations would be a nice optimization here.
var op = new DataCacheOperation(readStateMachine, deferred, true, index, count, [], 0);
queueAndStart(op, readOperations);
return extend(deferred.promise(), {
cancel: function () {
/// <summary>Aborts the readRange operation.</summary>
op.cancel();
}
});
};
that.ToObservable = that.toObservable = function () {
/// <summary>Creates an Observable object that enumerates all the cache contents.</summary>
/// <returns>A new Observable object that enumerates all the cache contents.</returns>
if (!window.Rx || !window.Rx.Observable) {
throw { message: "Rx library not available - include rx.js" };
}
if (cacheFailure) {
throw cacheFailure;
}
return window.Rx.Observable.CreateWithDisposable(function (obs) {
var disposed = false;
var index = 0;
var errorCallback = function (error) {
if (!disposed) {
obs.OnError(error);
}
};
var successCallback = function (data) {
if (!disposed) {
var i, len;
for (i = 0, len = data.length; i < len; i++) {
// The wrapper automatically checks for Dispose
// on the observer, so we don't need to check it here.
obs.OnNext(data[i]);
}
if (data.length < pageSize) {
obs.OnCompleted();
} else {
index += pageSize;
that.readRange(index, pageSize).then(successCallback, errorCallback);
}
}
};
that.readRange(index, pageSize).then(successCallback, errorCallback);
return { Dispose: function () { disposed = true; } };
});
};
var cacheFailureCallback = function (message) {
/// <summary>Creates a function that handles a callback by setting the cache into failure mode.</summary>
/// <param name="message" type="String">Message text.</param>
/// <returns type="Function">Function to use as error callback.</returns>
/// <remarks>
/// This function will specifically handle problems with critical store resources
/// during cache initialization.
/// </remarks>
return function (error) {
cacheFailure = { message: message, error: error };
// Destroy any pending clear or read operations.
// At this point there should be no prefetch operations.
// Count operations will go through but are benign because they
// won't interact with the store.
djsassert(prefetchOperations.length === 0, "prefetchOperations.length === 0");
var i, len;
for (i = 0, len = readOperations.length; i < len; i++) {
readOperations[i].fireRejected(cacheFailure);
}
for (i = 0, len = clearOperations.length; i < len; i++) {
clearOperations[i].fireRejected(cacheFailure);
}
// Null out the operation arrays.
readOperations = clearOperations = null;
};
};
var changeState = function (newState) {
/// <summary>Updates the cache's state and signals all pending operations of the change.</summary>
/// <param name="newState" type="Object">New cache state.</param>
/// <remarks>This method is a no-op if the cache's current state and the new state are the same.</remarks>
if (newState !== state) {
state = newState;
var operations = clearOperations.concat(readOperations, prefetchOperations);
var i, len;
for (i = 0, len = operations.length; i < len; i++) {
operations[i].run(state);
}
}
};
var clearStore = function () {
/// <summary>Removes all the data stored in the cache.</summary>
/// <returns type="DjsDeferred">A promise with no value.</returns>
djsassert(state === CACHE_STATE_DESTROY || state === CACHE_STATE_INIT, "DataCache.clearStore() - cache is not on the destroy or initialize state, current sate = " + state);
var deferred = new DjsDeferred();
store.clear(function () {
// Reset the cache settings.
actualCacheSize = 0;
allDataLocal = false;
collectionCount = 0;
highestSavedPage = 0;
highestSavedPageSize = 0;
overflowed = cacheSize === 0;
// version is not reset, in case there is other state in eg V1.1 that is still around.
// Reset the cache stats.
stats = { counts: 0, netReads: 0, prefetches: 0, cacheReads: 0 };
that.stats = stats;
store.close();
deferred.resolve();
}, function (err) {
deferred.reject(err);
});
return deferred;
};
var dequeueOperation = function (operation) {
/// <summary>Removes an operation from the caches queues and changes the cache state to idle.</summary>
/// <param name="operation" type="DataCacheOperation">Operation to dequeue.</param>
/// <remarks>This method is used as a handler for the operation's oncomplete event.</remarks>
var removed = removeFromArray(clearOperations, operation);
if (!removed) {
removed = removeFromArray(readOperations, operation);
if (!removed) {
removeFromArray(prefetchOperations, operation);
}
}
pendingOperations--;
changeState(CACHE_STATE_IDLE);
};
var fetchPage = function (start) {
/// <summary>Requests data from the cache source.</summary>
/// <param name="start" type="Number">Zero-based index of items to request.</param>
/// <returns type="DjsDeferred">A promise for a page object with (i)ndex, (c)ount, (d)ata.</returns>
djsassert(state !== CACHE_STATE_DESTROY, "DataCache.fetchPage() - cache is on the destroy state");
djsassert(state !== CACHE_STATE_IDLE, "DataCache.fetchPage() - cache is on the idle state");
var deferred = new DjsDeferred();
var canceled = false;
var request = source.read(start, pageSize, function (data) {
var page = { i: start, c: data.length, d: data };
deferred.resolve(page);
}, function (err) {
deferred.reject(err);
});
return extend(deferred, {
cancel: function () {
if (request) {
request.abort();
canceled = true;
request = null;
}
}
});
};
var filter = function (index, count, predicate, backwards) {
/// <summary>Filters the cache data based a predicate.</summary>
/// <param name="index" type="Number">The index of the item to start filtering from.</param>
/// <param name="count" type="Number">Maximum number of items to include in the result.</param>
/// <param name="predicate" type="Function">Callback function returning a boolean that determines whether an item should be included in the result or not.</param>
/// <param name="backwards" type="Boolean">True if the filtering should move backward from the specified index, falsey otherwise.</param>
/// <remarks>
/// Specifying a negative count value will yield all the items in the cache that satisfy the predicate.
/// </remarks>
/// <returns type="DjsDeferred">A promise for an array of results.</returns>
index = parseInt10(index);
count = parseInt10(count);
if (isNaN(index)) {
throw { message: "'index' must be a valid number.", index: index };
}
if (isNaN(count)) {
throw { message: "'count' must be a valid number.", count: count };
}
if (cacheFailure) {
throw cacheFailure;
}
index = Math.max(index, 0);
var deferred = createDeferred();
var arr = [];
var canceled = false;
var pendingReadRange = null;
var readMore = function (readIndex, readCount) {
if (!canceled) {
if (count >= 0 && arr.length >= count) {
deferred.resolve(arr);
} else {
pendingReadRange = that.readRange(readIndex, readCount).then(function (data) {
for (var i = 0, length = data.length; i < length && (count < 0 || arr.length < count); i++) {
var dataIndex = backwards ? length - i - 1 : i;
var item = data[dataIndex];
if (predicate(item)) {
var element = {
index: readIndex + dataIndex,
item: item
};
backwards ? arr.unshift(element) : arr.push(element);
}
}
// Have we reached the end of the collection?
if ((!backwards && data.length < readCount) || (backwards && readIndex <= 0)) {
deferred.resolve(arr);
} else {
var nextIndex = backwards ? Math.max(readIndex - pageSize, 0) : readIndex + readCount;
readMore(nextIndex, pageSize);
}
}, function (err) {
deferred.reject(err);
});
}
}
};
// Initially, we read from the given starting index to the next/previous page boundary
var initialPage = snapToPageBoundaries(index, index, pageSize);
var initialIndex = backwards ? initialPage.i : index;
var initialCount = backwards ? index - initialPage.i + 1 : initialPage.i + initialPage.c - index;
readMore(initialIndex, initialCount);
return extend(deferred.promise(), {
cancel: function () {
/// <summary>Aborts the filter operation</summary>
if (pendingReadRange) {
pendingReadRange.cancel();
}
canceled = true;
}
});
};
var fireOnIdle = function () {
/// <summary>Fires an onidle event if any functions are assigned.</summary>
if (that.onidle && pendingOperations === 0) {
that.onidle();
}
};
var prefetch = function (start) {
/// <summary>Creates and starts a new prefetch operation.</summary>
/// <param name="start" type="Number">Zero-based index of the items to prefetch.</param>
/// <remarks>
/// This method is a no-op if any of the following conditions is true:
/// 1.- prefetchSize is 0
/// 2.- All data has been read and stored locally in the cache.
/// 3.- There is already an all data prefetch operation queued.
/// 4.- The cache has run out of available space (overflowed).
/// <remarks>
if (allDataLocal || prefetchSize === 0 || overflowed) {
return;
}
djsassert(state === CACHE_STATE_READ, "DataCache.prefetch() - cache is not on the read state, current state: " + state);
if (prefetchOperations.length === 0 || (prefetchOperations[0] && prefetchOperations[0].c !== -1)) {
// Merging prefetch operations would be a nice optimization here.
var op = new DataCacheOperation(prefetchStateMachine, null, true, start, prefetchSize, null, prefetchSize);
queueAndStart(op, prefetchOperations);
}
};
var queueAndStart = function (op, queue) {
/// <summary>Queues an operation and runs it.</summary>
/// <param name="op" type="DataCacheOperation">Operation to queue.</param>
/// <param name="queue" type="Array">Array that will store the operation.</param>
op.oncomplete = dequeueOperation;
queue.push(op);
pendingOperations++;
op.run(state);
};
var readPage = function (key) {
/// <summary>Requests a page from the cache local store.</summary>
/// <param name="key" type="Number">Zero-based index of the reuqested page.</param>
/// <returns type="DjsDeferred">A promise for a found flag and page object with (i)ndex, (c)ount, (d)ata, and (t)icks.</returns>
djsassert(state !== CACHE_STATE_DESTROY, "DataCache.readPage() - cache is on the destroy state");
var canceled = false;
var deferred = extend(new DjsDeferred(), {
cancel: function () {
/// <summary>Aborts the readPage operation.</summary>
canceled = true;
}
});
var error = storeFailureCallback(deferred, "Read page from store failure");
store.contains(key, function (contained) {
if (canceled) {
return;
}
if (contained) {
store.read(key, function (_, data) {
if (!canceled) {
deferred.resolve(data !== undefined, data);
}
}, error);
return;
}
deferred.resolve(false);
}, error);
return deferred;
};
var savePage = function (key, page) {
/// <summary>Saves a page to the cache local store.</summary>
/// <param name="key" type="Number">Zero-based index of the requested page.</param>
/// <param name="page" type="Object">Object with (i)ndex, (c)ount, (d)ata, and (t)icks.</param>
/// <returns type="DjsDeferred">A promise with no value.</returns>
djsassert(state !== CACHE_STATE_DESTROY, "DataCache.savePage() - cache is on the destroy state");
djsassert(state !== CACHE_STATE_IDLE, "DataCache.savePage() - cache is on the idle state");
var canceled = false;
var deferred = extend(new DjsDeferred(), {
cancel: function () {
/// <summary>Aborts the readPage operation.</summary>
canceled = true;
}
});
var error = storeFailureCallback(deferred, "Save page to store failure");
var resolve = function () {
deferred.resolve(true);
};
if (page.c > 0) {
var pageBytes = estimateSize(page);
overflowed = cacheSize >= 0 && cacheSize < actualCacheSize + pageBytes;
if (!overflowed) {
store.addOrUpdate(key, page, function () {
updateSettings(page, pageBytes);
saveSettings(resolve, error);
}, error);
} else {
resolve();
}
} else {
updateSettings(page, 0);
saveSettings(resolve, error);
}
return deferred;
};
var saveSettings = function (success, error) {
/// <summary>Saves the cache's current settings to the local store.</summary>
/// <param name="success" type="Function">Success callback.</param>
/// <param name="error" type="Function">Errror callback.</param>
var settings = {
actualCacheSize: actualCacheSize,
allDataLocal: allDataLocal,
cacheSize: cacheSize,
collectionCount: collectionCount,
highestSavedPage: highestSavedPage,
highestSavedPageSize: highestSavedPageSize,
pageSize: pageSize,
sourceId: source.identifier,
version: version
};
store.addOrUpdate("__settings", settings, success, error);
};
var storeFailureCallback = function (deferred/*, message*/) {
/// <summary>Creates a function that handles a store error.</summary>
/// <param name="deferred" type="DjsDeferred">Deferred object to resolve.</param>
/// <param name="message" type="String">Message text.</param>
/// <returns type="Function">Function to use as error callback.</returns>
/// <remarks>
/// This function will specifically handle problems when interacting with the store.
/// </remarks>
return function (/*error*/) {
// var console = window.console;
// if (console && console.log) {
// console.log(message);
// console.dir(error);
// }
deferred.resolve(false);
};
};
var updateSettings = function (page, pageBytes) {
/// <summary>Updates the cache's settings based on a page object.</summary>
/// <param name="page" type="Object">Object with (i)ndex, (c)ount, (d)ata.</param>
/// <param name="pageBytes" type="Number">Size of the page in bytes.</param>
var pageCount = page.c;
var pageIndex = page.i;
// Detect the collection size.
if (pageCount === 0) {
if (highestSavedPage === pageIndex - pageSize) {
collectionCount = highestSavedPage + highestSavedPageSize;
}
} else {
highestSavedPage = Math.max(highestSavedPage, pageIndex);
if (highestSavedPage === pageIndex) {
highestSavedPageSize = pageCount;
}
actualCacheSize += pageBytes;
if (pageCount < pageSize && !collectionCount) {
collectionCount = pageIndex + pageCount;
}
}
// Detect the end of the collection.
if (!allDataLocal && collectionCount === highestSavedPage + highestSavedPageSize) {
allDataLocal = true;
}
};
var cancelStateMachine = function (operation, opTargetState, cacheState, data) {
/// <summary>State machine describing the behavior for cancelling a read or prefetch operation.</summary>
/// <param name="operation" type="DataCacheOperation">Operation being run.</param>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
/// <remarks>
/// This state machine contains behavior common to read and prefetch operations.
/// </remarks>
var canceled = operation.canceled && opTargetState !== OPERATION_STATE_END;
if (canceled) {
if (opTargetState === OPERATION_STATE_CANCEL) {
// Cancel state.
// Data is expected to be any pending request made to the cache.
if (data && data.cancel) {
data.cancel();
}
}
}
return canceled;
};
var destroyStateMachine = function (operation, opTargetState, cacheState) {
/// <summary>State machine describing the behavior of a clear operation.</summary>
/// <param name="operation" type="DataCacheOperation">Operation being run.</param>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <remarks>
/// Clear operations have the highest priority and can't be interrupted by other operations; however,
/// they will preempt any other operation currently executing.
/// </remarks>
var transition = operation.transition;
// Signal the cache that a clear operation is running.
if (cacheState !== CACHE_STATE_DESTROY) {
changeState(CACHE_STATE_DESTROY);
return true;
}
switch (opTargetState) {
case OPERATION_STATE_START:
// Initial state of the operation.
transition(DESTROY_STATE_CLEAR);
break;
case OPERATION_STATE_END:
// State that signals the operation is done.
fireOnIdle();
break;
case DESTROY_STATE_CLEAR:
// State that clears all the local data of the cache.
clearStore().then(function () {
// Terminate the operation once the local store has been cleared.
operation.complete();
});
// Wait until the clear request completes.
operation.wait();
break;
default:
return false;
}
return true;
};
var prefetchStateMachine = function (operation, opTargetState, cacheState, data) {
/// <summary>State machine describing the behavior of a prefetch operation.</summary>
/// <param name="operation" type="DataCacheOperation">Operation being run.</param>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
/// <remarks>
/// Prefetch operations have the lowest priority and will be interrupted by operations of
/// other kinds. A preempted prefetch operation will resume its execution only when the state
/// of the cache returns to idle.
///
/// If a clear operation starts executing then all the prefetch operations are canceled,
/// even if they haven't started executing yet.
/// </remarks>
// Handle cancelation
if (!cancelStateMachine(operation, opTargetState, cacheState, data)) {
var transition = operation.transition;
// Handle preemption
if (cacheState !== CACHE_STATE_PREFETCH) {
if (cacheState === CACHE_STATE_DESTROY) {
if (opTargetState !== OPERATION_STATE_CANCEL) {
operation.cancel();
}
} else if (cacheState === CACHE_STATE_IDLE) {
// Signal the cache that a prefetch operation is running.
changeState(CACHE_STATE_PREFETCH);
}
return true;
}
switch (opTargetState) {
case OPERATION_STATE_START:
// Initial state of the operation.
if (prefetchOperations[0] === operation) {
transition(READ_STATE_LOCAL, operation.i);
}
break;
case READ_STATE_DONE:
// State that determines if the operation can be resolved or has to
// continue processing.
// Data is expected to be the read page.
var pending = operation.pending;
if (pending > 0) {
pending -= Math.min(pending, data.c);
}
// Are we done, or has all the data been stored?
if (allDataLocal || pending === 0 || data.c < pageSize || overflowed) {
operation.complete();
} else {
// Continue processing the operation.
operation.pending = pending;
transition(READ_STATE_LOCAL, data.i + pageSize);
}
break;
default:
return readSaveStateMachine(operation, opTargetState, cacheState, data, true);
}
}
return true;
};
var readStateMachine = function (operation, opTargetState, cacheState, data) {
/// <summary>State machine describing the behavior of a read operation.</summary>
/// <param name="operation" type="DataCacheOperation">Operation being run.</param>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
/// <remarks>
/// Read operations have a higher priority than prefetch operations, but lower than
/// clear operations. They will preempt any prefetch operation currently running
/// but will be interrupted by a clear operation.
///
/// If a clear operation starts executing then all the currently running
/// read operations are canceled. Read operations that haven't started yet will
/// wait in the start state until the destory operation finishes.
/// </remarks>
// Handle cancelation
if (!cancelStateMachine(operation, opTargetState, cacheState, data)) {
var transition = operation.transition;
// Handle preemption
if (cacheState !== CACHE_STATE_READ && opTargetState !== OPERATION_STATE_START) {
if (cacheState === CACHE_STATE_DESTROY) {
if (opTargetState !== OPERATION_STATE_START) {
operation.cancel();
}
} else if (cacheState !== CACHE_STATE_WRITE) {
// Signal the cache that a read operation is running.
djsassert(state == CACHE_STATE_IDLE || state === CACHE_STATE_PREFETCH, "DataCache.readStateMachine() - cache is not on the read or idle state.");
changeState(CACHE_STATE_READ);
}
return true;
}
switch (opTargetState) {
case OPERATION_STATE_START:
// Initial state of the operation.
// Wait until the cache is idle or prefetching.
if (cacheState === CACHE_STATE_IDLE || cacheState === CACHE_STATE_PREFETCH) {
// Signal the cache that a read operation is running.
changeState(CACHE_STATE_READ);
if (operation.c > 0) {
// Snap the requested range to a page boundary.
var range = snapToPageBoundaries(operation.i, operation.c, pageSize);
transition(READ_STATE_LOCAL, range.i);
} else {
transition(READ_STATE_DONE, operation);
}
}
break;
case READ_STATE_DONE:
// State that determines if the operation can be resolved or has to
// continue processing.
// Data is expected to be the read page.
appendPage(operation, data);
var len = operation.d.length;
// Are we done?
if (operation.c === len || data.c < pageSize) {
// Update the stats, request for a prefetch operation.
stats.cacheReads++;
prefetch(data.i + data.c);
// Terminate the operation.
operation.complete();
} else {
// Continue processing the operation.
transition(READ_STATE_LOCAL, data.i + pageSize);
}
break;
default:
return readSaveStateMachine(operation, opTargetState, cacheState, data, false);
}
}
return true;
};
var readSaveStateMachine = function (operation, opTargetState, cacheState, data, isPrefetch) {
/// <summary>State machine describing the behavior for reading and saving data into the cache.</summary>
/// <param name="operation" type="DataCacheOperation">Operation being run.</param>
/// <param name="opTargetState" type="Object">Operation state to transition to.</param>
/// <param name="cacheState" type="Object">Current cache state.</param>
/// <param name="data" type="Object" optional="true">Additional data passed to the state.</param>
/// <param name="isPrefetch" type="Boolean">Flag indicating whether a read (false) or prefetch (true) operation is running.
/// <remarks>
/// This state machine contains behavior common to read and prefetch operations.
/// </remarks>
var error = operation.error;
var transition = operation.transition;
var wait = operation.wait;
var request;
switch (opTargetState) {
case OPERATION_STATE_END:
// State that signals the operation is done.
fireOnIdle();
break;
case READ_STATE_LOCAL:
// State that requests for a page from the local store.
// Data is expected to be the index of the page to request.
request = readPage(data).then(function (found, page) {
// Signal the cache that a read operation is running.
if (!operation.canceled) {
if (found) {
// The page is in the local store, check if the operation can be resolved.
transition(READ_STATE_DONE, page);
} else {
// The page is not in the local store, request it from the source.
transition(READ_STATE_SOURCE, data);
}
}
});
break;
case READ_STATE_SOURCE:
// State that requests for a page from the cache source.
// Data is expected to be the index of the page to request.
request = fetchPage(data).then(function (page) {
// Signal the cache that a read operation is running.
if (!operation.canceled) {
// Update the stats and save the page to the local store.
if (isPrefetch) {
stats.prefetches++;
} else {
stats.netReads++;
}
transition(READ_STATE_SAVE, page);
}
}, error);
break;
case READ_STATE_SAVE:
// State that saves a page to the local store.
// Data is expected to be the page to save.
// Write access to the store is exclusive.
if (cacheState !== CACHE_STATE_WRITE) {
changeState(CACHE_STATE_WRITE);
request = savePage(data.i, data).then(function (saved) {
if (!operation.canceled) {
if (!saved && isPrefetch) {
operation.pending = 0;
}
// Check if the operation can be resolved.
transition(READ_STATE_DONE, data);
}
changeState(CACHE_STATE_IDLE);
});
}
break;
default:
// Unknown state that can't be handled by this state machine.
return false;
}
if (request) {
// The operation might have been canceled between stack frames do to the async calls.
if (operation.canceled) {
request.cancel();
} else if (operation.s === opTargetState) {
// Wait for the request to complete.
wait(request);
}
}
return true;
};
// Initialize the cache.
store.read("__settings", function (_, settings) {
if (assigned(settings)) {
var settingsVersion = settings.version;
if (!settingsVersion || settingsVersion.indexOf("1.") !== 0) {
cacheFailureCallback("Unsupported cache store version " + settingsVersion)();
return;
}
if (pageSize !== settings.pageSize || source.identifier !== settings.sourceId) {
// The shape or the source of the data was changed so invalidate the store.
clearStore().then(function () {
// Signal the cache is fully initialized.
changeState(CACHE_STATE_IDLE);
}, cacheFailureCallback("Unable to clear store during initialization"));
} else {
// Restore the saved settings.
actualCacheSize = settings.actualCacheSize;
allDataLocal = settings.allDataLocal;
cacheSize = settings.cacheSize;
collectionCount = settings.collectionCount;
highestSavedPage = settings.highestSavedPage;
highestSavedPageSize = settings.highestSavedPageSize;
version = settingsVersion;
// Signal the cache is fully initialized.
changeState(CACHE_STATE_IDLE);
}
} else {
// This is a brand new cache.
saveSettings(function () {
// Signal the cache is fully initialized.
changeState(CACHE_STATE_IDLE);
}, cacheFailureCallback("Unable to write settings during initialization."));
}
}, cacheFailureCallback("Unable to read settings from store."));
return that;
};
datajs.createDataCache = function (options) {
/// <summary>Creates a data cache for a collection that is efficiently loaded on-demand.</summary>
/// <param name="options">
/// Options for the data cache, including name, source, pageSize,
/// prefetchSize, cacheSize, storage mechanism, and initial prefetch and local-data handler.
/// </param>
/// <returns type="DataCache">A new data cache instance.</returns>
checkUndefinedGreaterThanZero(options.pageSize, "pageSize");
checkUndefinedOrNumber(options.cacheSize, "cacheSize");
checkUndefinedOrNumber(options.prefetchSize, "prefetchSize");
if (!assigned(options.name)) {
throw { message: "Undefined or null name", options: options };
}
if (!assigned(options.source)) {
throw { message: "Undefined source", options: options };
}
return new DataCache(options);
};
// DATAJS INTERNAL START
window.datajs.estimateSize = estimateSize;
// DATAJS INTERNAL END
// CONTENT END
})(this);