diff --git a/src/framework/core/js/FluidRequests.js b/src/framework/core/js/FluidRequests.js index c4ddb38902..d00721233b 100644 --- a/src/framework/core/js/FluidRequests.js +++ b/src/framework/core/js/FluidRequests.js @@ -14,9 +14,9 @@ var fluid_2_0 = fluid_2_0 || {}; (function ($, fluid) { "use strict"; - + /** NOTE: All contents of this file are DEPRECATED and no entry point should be considered a supported API **/ - + fluid.explodeLocalisedName = function (fileName, locale, defaultLocale) { var lastDot = fileName.lastIndexOf("."); if (lastDot === -1 || lastDot === 0) { @@ -24,9 +24,9 @@ var fluid_2_0 = fluid_2_0 || {}; } var baseName = fileName.substring(0, lastDot); var extension = fileName.substring(lastDot); - + var segs = locale.split("_"); - + var exploded = fluid.transform(segs, function (seg, index) { var shortSegs = segs.slice(0, index + 1); return baseName + "_" + shortSegs.join("_") + extension; @@ -77,7 +77,7 @@ var fluid_2_0 = fluid_2_0 || {}; that.operate(); return that; }; - + fluid.fetchResources.explodeForLocales = function (resourceSpecs) { fluid.each(resourceSpecs, function (resourceSpec, key) { if (resourceSpec.locale) { @@ -95,7 +95,7 @@ var fluid_2_0 = fluid_2_0 || {}; }); return resourceSpecs; }; - + fluid.fetchResources.condenseOneResource = function (resourceSpecs, resourceSpec, key, localeCount) { var localeSpecs = [resourceSpec]; for (var i = 0; i < localeCount; ++ i) { @@ -110,7 +110,7 @@ var fluid_2_0 = fluid_2_0 || {}; resourceSpecs[key] = lastNonError; } }; - + fluid.fetchResources.condenseForLocales = function (resourceSpecs) { fluid.each(resourceSpecs, function (resourceSpec, key) { if (typeof(resourceSpec.localeExploded) === "number") { @@ -118,7 +118,7 @@ var fluid_2_0 = fluid_2_0 || {}; } }); }; - + fluid.fetchResources.notifyResources = function (that, resourceSpecs, callback) { fluid.fetchResources.condenseForLocales(resourceSpecs); callback(resourceSpecs); @@ -431,5 +431,229 @@ var fluid_2_0 = fluid_2_0 || {}; return fluid.NO_VALUE; }; + /** Start dataSource **/ + + /* + * A grade defining a DataSource. + * This grade illustrates the expected structure a dataSource, as well as + * providing a means for identifying dataSources in a component tree by type. + * + * The purpose of the "dataSource" abstraction is to express indexed access + * to state. A REST/CRUD implementation is an example of a DataSource; however, + * it is not limited to this method of interaction. + */ + fluid.defaults("fluid.dataSource", { + gradeNames: ["fluid.eventedComponent", "autoInit"] + // Invokers should be defined for the typical CRUD functions and return + // a promise. + // + // The "get" and "delete" methods require the signature (directModel). + // The "set" method requires the signature (directModel, model) + // + // directModel: An object expressing an "index" into some set of + // state which can be read or written. + // + // model: The payload sent to the storage. + // + // options: An object expressing implementation specific details + // regarding the handling of a request. Note: this does not + // include details for identifying the resource. Those should be + // placed in the directModel. + // + // invokers: { + // "get": {}, // handles the Read function + // "set": {}, // handles the Create and Update functions + // "delete": {} // handles the Delete function + // } + }); + + /* + * Converts an object or array to string for use as a key. + * The objects are sorted alphabetically to insure that they + * result in the same string across executions. + */ + fluid.objectToHashKey = function (obj) { + var str = fluid.isArrayable(obj) ? "array " : "object "; + var keys = fluid.keys(obj).sort(); + + fluid.each(keys, function (key) { + var val = obj[key]; + str += key + ":" + fluid.toHashKey(val); + }); + + return str; + }; + + /* + * Generates a string for use as a key. + * They typeof of the value passed in will be prepended + * to ensure that (strings vs numbers) and (arrays vs objects) + * are distinguishable. + */ + fluid.toHashKey = function (val) { + var str; + if(fluid.isPlainObject(val)){ + str = "<" + fluid.objectToHashKey(val) + ">"; + } else { + str = "|" + JSON.stringify(val) + "|"; + } + return str; + }; + + /* + * A dataSource wrapper providing a queuing mechanism for requests. + * Requests are queued based on type (read/write) and resource (directModel). + * + * A fully implemented dataSource, following the structure outlined by fluid.dataSource, + * must be provided in the wrappedDataSource subcomponent. The get, set, and delete methods + * found on the queuedDataSource will call their counterparts in the wrappedDataSource, after + * filtering through the appropriate queue. + * + * TODO: A fully realized implementation should provide a mechanism for working with a local + * cache. For example pending write requests could be used to service get requests directly. + */ + fluid.defaults("fluid.queuedDataSource", { + gradeNames: ["fluid.eventedComponent", "autoInit"], + members: { + requests: { + read: {}, + write: {} + } + }, + components: { + wrappedDataSource: { + // requires a dataSource that implements the standard set, get, and delete methods. + type: "fluid.dataSource" + } + }, + events: { + enqueued: null, + afterRequestComplete: null + }, + listeners: { + "enqueued.start": { + listener: "{that}.start", + args: ["{arguments}.1"] + }, + "afterRequestComplete.start": { + listener: "{that}.start", + args: ["{arguments}.1"] + } + }, + invokers: { + // The add to queue method needs to be provided by the integrator + // addToQueue: "", + // addToQueue: {funcName: "fluid.identity"}, + set: { + funcName: "fluid.queuedDataSource.enqueueWithModel", + args: ["{that}", "{that}.requests.write", "{wrappedDataSource}.set", "{arguments}.0", "{arguments}.1", "{arguments}.2"] + }, + get: { + funcName: "fluid.queuedDataSource.enqueue", + args: ["{that}", "{that}.requests.read", "{wrappedDataSource}.get", "{arguments}.0", "{arguments}.1"] + }, + "delete": { + funcName: "fluid.queuedDataSource.enqueue", + args: ["{that}", "{that}.requests.write", "{wrappedDataSource}.delete", "{arguments}.0", "{arguments}.1"] + }, + start: { + funcName: "fluid.queuedDataSource.start", + args: ["{that}", "{arguments}.0"] + } + } + }); + + fluid.queuedDataSource.start = function (that, queue) { + if (!queue.isActive && queue.requests.length) { + var request = queue.requests.shift(); + + var requestComplete = function () { + queue.isActive = false; + that.events.afterRequestComplete.fire(request, queue); + }; + + queue.isActive = true; + var args = "model" in request ? [request.directModel, request.model, request.options] : [request.directModel, request.options]; + var response = request.method.apply(null, args); + + response.then(requestComplete, requestComplete); + fluid.promise.follow(response, request.promise); + } + }; + + /* + * Adds only one item to the queue at a time, new requests replace older ones + * + * The request object contains the request function and arguments. + * In the form {method: requestFn, directModel: {}, model: {}, callback: callbackFn} + */ + fluid.queuedDataSource.enqueueImpl = function (that, requestsQueue, request) { + var promise = fluid.promise(); + var key = fluid.toHashKey(request.directModel); + var queue = requestsQueue[key]; + + // add promise to the request object + // to be resolved in the start method + // when the wrapped dataSource's request returns. + request.promise = promise; + + // create a queue if one doesn't already exist + if (!queue) { + queue = { + isActive: false, + requests: [] + }; + requestsQueue[key] = queue; + } + + that.addToQueue(queue, request); + that.events.enqueued.fire(request, queue); + + return promise; + }; + + fluid.queuedDataSource.enqueue = function (that, requestsQueue, method, directModel, options) { + var request = { + method: method, + directModel: directModel, + options: options + }; + + return fluid.queuedDataSource.enqueueImpl(that, requestsQueue, request); + }; + + fluid.queuedDataSource.enqueueWithModel = function (that, requestsQueue, method, directModel, model, options) { + var request = { + method: method, + directModel: directModel, + model: model, + options: options + }; + + return fluid.queuedDataSource.enqueueImpl(that, requestsQueue, request); + }; + + fluid.queuedDataSource.replaceRequest = function (queue, request) { + queue.requests[0] = request; + }; + + /* + * A dataSource wrapper providing a debounce queuing mechanism for requests. + * Requests are queued based on type (read/write) and resource (directModel). + * Only 1 requested is queued at a time. New requests replace older ones. + * + * A fully implemented dataSource, following the structure outlined by fluid.dataSource, + * must be provided in the wrappedDataSource subcomponent. The get, set, and delete methods + * found on the queuedDataSource will call their counterparts in the wrappedDataSource, after + * filtering through the appropriate queue. + */ + fluid.defaults("fluid.debouncedDataSource", { + gradeNames: ["fluid.queuedDataSource", "autoInit"], + invokers: { + addToQueue: "fluid.queuedDataSource.replaceRequest" + } + }); + + /** End dataSource **/ })(jQuery, fluid_2_0); diff --git a/tests/all-tests.html b/tests/all-tests.html index 04e945864a..77b2267a5c 100644 --- a/tests/all-tests.html +++ b/tests/all-tests.html @@ -29,6 +29,7 @@ "./framework-tests/core/html/FluidIoC-test.html", "./framework-tests/core/html/FluidIoCStandalone-test.html", "./framework-tests/core/html/FluidIoCView-test.html", + "./framework-tests/core/html/DataSource-test.html", "./framework-tests/enhancement/html/ProgressiveEnhancement-test.html", "./framework-tests/renderer/html/RendererUtilities-test.html", "./framework-tests/preferences/html/AuxBuilder-test.html", diff --git a/tests/framework-tests/core/html/DataSource-test.html b/tests/framework-tests/core/html/DataSource-test.html new file mode 100644 index 0000000000..bbe9fb08e0 --- /dev/null +++ b/tests/framework-tests/core/html/DataSource-test.html @@ -0,0 +1,33 @@ + + + + + DataSource Tests + + + + + + + + + + + + + + + + + + + + +

DataSource Test Suite

+

+
+

+
    + + + diff --git a/tests/framework-tests/core/js/DataSourceTests.js b/tests/framework-tests/core/js/DataSourceTests.js new file mode 100644 index 0000000000..1bbdd7b2d0 --- /dev/null +++ b/tests/framework-tests/core/js/DataSourceTests.js @@ -0,0 +1,198 @@ +/* +Copyright 2014 OCAD University + +Licensed under the Educational Community License (ECL), Version 2.0 or the New +BSD license. You may not use this file except in compliance with one these +Licenses. + +You may obtain a copy of the ECL 2.0 License and BSD License at +https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt +*/ + +// Declare dependencies +/* global fluid, jqUnit */ + +(function () { + "use strict"; + + fluid.registerNamespace("fluid.tests"); + + jqUnit.test("fluid.toHashKey", function () { + var singleLevel = { + a: "A", + b: "B", + g: 5, + c: ["D", "E", "F"] + }; + + var nested = { + a: "A", + h: 6, + f: ["F", "G"], + b: { + c: "C", + d: ["D", "E"] + } + }; + + var expected = { + singleLevel: "g:|5|>", + nested: ">f:h:|6|>" + }; + + jqUnit.assertEquals("The single level object should be converted properly", expected.singleLevel, fluid.toHashKey(singleLevel)); + jqUnit.assertEquals("The multi-level object should be converted properly", expected.nested, fluid.toHashKey(nested)); + }); + + jqUnit.test("fluid.toHashKey - order consistency", function () { + var order1 = {}; + order1.a = "1"; + order1.b = "2"; + + var order2 = {}; + order2.b = "2"; + order2.a = "1"; + + jqUnit.assertEquals("Identical objects created in different orders should produce a consistent output", fluid.toHashKey(order1), fluid.toHashKey(order2)); + }); + + fluid.defaults("fluid.tests.dataSource", { + gradeNames: ["fluid.dataSource", "autoInit"], + invokers: { + "get": { + funcName: "fluid.tests.dataSource.request", + args: ["get", "{arguments}.0", "{arguments}.1"] + }, + "set": { + funcName: "fluid.tests.dataSource.request", + args: ["set", "{arguments}.0", "{arguments}.2", "{arguments}.1"] + }, + "delete": { + funcName: "fluid.tests.dataSource.request", + args: ["delete", "{arguments}.0", "{arguments}.1"] + } + } + }); + + fluid.tests.dataSource.request = function (type, directModel, options, model) { + var promise = fluid.promise(); + var ID = fluid.get(model, "ID") || options.ID; + setTimeout(function () { + promise.resolve({ + requestType: type, + ID: ID + }); + }, options.duration); + return promise; + }; + + fluid.defaults("fluid.tests.debouncedDataSource", { + gradeNames: ["fluid.debouncedDataSource", "autoInit"], + members: { + // Each record should contain a list of the directModels from the requests + // that were sent to the respective events. + fireRecord: { + "get": [], + "set": [], + "delete": [] + } + }, + components: { + wrappedDataSource: { + type: "fluid.tests.dataSource" + } + } + }); + + fluid.tests.invokeRequestWithDelay = function (that, type, delays, duration, directModel) { + var count = 0; + fluid.each(delays, function (delay) { + setTimeout(function () { + var response; + if (type === "set") { + response = that[type](directModel, {ID: ++count}, {duration: duration}); + } else { + response = that[type](directModel, { + duration: duration, + ID: ++count + }); + } + response.then(function (val) { + that.fireRecord[val.requestType].push(val.ID); + }); + }, delay); + }); + }; + + // s = short delay + // l = long delay + fluid.tests.debouncedDataSource.requestRuns = { + ss: [0, 50, 55], + sl: [0, 50, 200], + ls: [0, 150, 200], + ll: [0, 150, 300], + sss: [0, 50, 55, 75], + lss: [0, 150, 200, 220], + ssl: [0, 50, 75, 200], + ssss: [0, 50, 55, 75, 80], + lsss: [0, 150, 200, 220, 250], + sssl: [0, 50, 60, 75, 200] + }; + + fluid.tests.debouncedDataSource.expected100ms = { + ss: [1, 3], + sl: [1, 2, 3], + ls: [1, 2, 3], + ll: [1, 2, 3], + sss: [1, 4], + lss: [1, 2, 4], + ssl: [1, 3, 4], + ssss: [1, 5], + lsss: [1, 2, 5], + sssl: [1, 4, 5] + }; + + fluid.tests.debouncedDataSource.testRequests = [{ + requestType: "get", + source: fluid.tests.debouncedDataSource.expected100ms + }, { + requestType: "set", + source: fluid.tests.debouncedDataSource.expected100ms + }, { + requestType: "delete", + source: fluid.tests.debouncedDataSource.expected100ms + }]; + + fluid.tests.debouncedDataSource.assertRequest = function (requestType, setName, delays, expected, delayBuffer) { + // Time in milliseconds to add to the assertion delay, to buffer against setTimeout impressions. + // Because multiple instances run simultaneously this number may need to be increased to take into + // account extra delays related to the thread blocking. + delayBuffer = delayBuffer || 300; + var promise = fluid.promise(); + var that = fluid.tests.debouncedDataSource(); + var assertionDelay = delays[delays.length - 1] + delayBuffer; // time to wait to assert the requests. + + fluid.tests.invokeRequestWithDelay(that, requestType, delays, 100, {path: "value"}); + + setTimeout(function () { + jqUnit.assertDeepEq("The " + requestType + " requests from set '" + setName + "' should have been queued correctly.", expected, that.fireRecord[requestType]); + promise.resolve(that); + }, assertionDelay); + return promise; + }; + + // Note: Due to the timeouts used to simulate actual asynchronous operations, this test + // will take a while to execute. + jqUnit.asyncTest("Debounce DataSource", function () { + var sources = []; + fluid.each(fluid.tests.debouncedDataSource.testRequests, function (testRequest) { + fluid.each(fluid.tests.debouncedDataSource.requestRuns, function (delays, setName) { + sources.push(function () { + return fluid.tests.debouncedDataSource.assertRequest(testRequest.requestType, setName, delays, testRequest.source[setName]); + }); + }); + }); + var testSequence = fluid.promise.sequence(sources); + testSequence.then(jqUnit.start); + }); +})();