Merge remote-tracking branch 'origin/open1329' into open1343

This commit is contained in:
larkin 2015-06-26 15:13:21 -07:00
commit d729cfcd3e
11 changed files with 377 additions and 42 deletions

View File

@ -183,7 +183,7 @@
{
"key": "mutation",
"implementation": "capabilities/MutationCapability.js",
"depends": [ "now" ]
"depends": [ "topic", "now" ]
},
{
"key": "delegation",
@ -200,6 +200,10 @@
"key": "throttle",
"implementation": "services/Throttle.js",
"depends": [ "$timeout" ]
},
{
"key": "topic",
"implementation": "services/Topic.js"
}
],
"roots": [

View File

@ -29,6 +29,8 @@ define(
function () {
"use strict";
var TOPIC_PREFIX = "mutation:";
// Utility function to overwrite a destination object
// with the contents of a source object.
function copyValues(destination, source) {
@ -71,7 +73,8 @@ define(
* which will expose this capability
* @constructor
*/
function MutationCapability(now, domainObject) {
function MutationCapability(topic, now, domainObject) {
var t = topic(TOPIC_PREFIX + domainObject.getId());
function mutate(mutator, timestamp) {
// Get the object's model and clone it, so the
@ -96,6 +99,7 @@ define(
copyValues(model, result);
}
model.modified = useTimestamp ? timestamp : now();
t.notify(model);
}
// Report the result of the mutation
@ -107,6 +111,10 @@ define(
return fastPromise(mutator(clone)).then(handleMutation);
}
function listen(listener) {
return t.listen(listener);
}
return {
/**
* Alias of `mutate`, used to support useCapability.
@ -139,10 +147,19 @@ define(
* @returns {Promise.<boolean>} a promise for the result
* of the mutation; true if changes were made.
*/
mutate: mutate
mutate: mutate,
/**
* Listen for mutations of this domain object's model.
* The provided listener will be invoked with the domain
* object's new model after any changes. To stop listening,
* invoke the function returned by this method.
* @param {Function} listener function to call on mutation
* @returns {Function} a function to stop listening
*/
listen: listen
};
}
return MutationCapability;
}
);
);

View File

@ -1,3 +1,24 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define(

View File

@ -0,0 +1,87 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define(
[],
function () {
"use strict";
/**
* The `topic` service provides a way to create both named,
* shared listeners and anonymous, private listeners.
*
* Usage:
*
* ```
* var t = topic('foo'); // Use/create a named topic
* t.listen(function () { ... });
* t.notify({ some: "message" });
* ```
*
* Named topics are shared; multiple calls to `topic`
* with the same argument will return a single object instance.
* Anonymous topics (where `topic`has been called with no
* arguments) are private; each call returns a new instance.
*
* @returns {Function}
*/
function Topic() {
var topics = {};
function createTopic() {
var listeners = [];
return {
listen: function (listener) {
listeners.push(listener);
return function unlisten() {
listeners = listeners.filter(function (l) {
return l !== listener;
});
};
},
notify: function (message) {
listeners.forEach(function (listener) {
listener(message);
});
}
};
}
/**
* Use and (if necessary) create a new topic.
* @param {string} [key] name of the topic to use
*/
return function (key) {
if (arguments.length < 1) {
return createTopic();
} else {
topics[key] = topics[key] || createTopic();
return topics[key];
}
};
}
return Topic;
}
);

View File

@ -25,21 +25,33 @@
* MutationCapabilitySpec. Created by vwoeltje on 11/6/14.
*/
define(
["../../src/capabilities/MutationCapability"],
function (MutationCapability) {
[
"../../src/capabilities/MutationCapability",
"../../src/services/Topic"
],
function (MutationCapability, Topic) {
"use strict";
describe("The mutation capability", function () {
var testModel,
topic,
mockNow,
domainObject = { getModel: function () { return testModel; } },
domainObject = {
getId: function () { return "test-id"; },
getModel: function () { return testModel; }
},
mutation;
beforeEach(function () {
testModel = { number: 6 };
topic = new Topic();
mockNow = jasmine.createSpy('now');
mockNow.andReturn(12321);
mutation = new MutationCapability(mockNow, domainObject);
mutation = new MutationCapability(
topic,
mockNow,
domainObject
);
});
it("allows mutation of a model", function () {
@ -83,6 +95,42 @@ define(
// Should have gotten a timestamp from 'now'
expect(testModel.modified).toEqual(42);
});
it("notifies listeners of mutation", function () {
var mockCallback = jasmine.createSpy('callback');
mutation.listen(mockCallback);
mutation.invoke(function (m) {
m.number = 8;
});
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback.mostRecentCall.args[0].number)
.toEqual(8);
});
it("allows listeners to stop listening", function () {
var mockCallback = jasmine.createSpy('callback');
mutation.listen(mockCallback)(); // Unlisten immediately
mutation.invoke(function (m) {
m.number = 8;
});
expect(mockCallback).not.toHaveBeenCalled();
});
it("shares listeners across instances", function () {
var mockCallback = jasmine.createSpy('callback'),
otherMutation = new MutationCapability(
topic,
mockNow,
domainObject
);
mutation.listen(mockCallback);
otherMutation.invoke(function (m) {
m.number = 8;
});
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback.mostRecentCall.args[0].number)
.toEqual(8);
});
});
}
);
);

View File

@ -1,3 +1,24 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(

View File

@ -0,0 +1,70 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/services/Topic"],
function (Topic) {
"use strict";
describe("The 'topic' service", function () {
var topic,
testMessage,
mockCallback;
beforeEach(function () {
testMessage = { someKey: "some value"};
mockCallback = jasmine.createSpy('callback');
topic = new Topic();
});
it("notifies listeners on a topic", function () {
topic("abc").listen(mockCallback);
topic("abc").notify(testMessage);
expect(mockCallback).toHaveBeenCalledWith(testMessage);
});
it("does not notify listeners across topics", function () {
topic("abc").listen(mockCallback);
topic("xyz").notify(testMessage);
expect(mockCallback).not.toHaveBeenCalledWith(testMessage);
});
it("does not notify listeners after unlistening", function () {
topic("abc").listen(mockCallback)(); // Unlisten immediately
topic("abc").notify(testMessage);
expect(mockCallback).not.toHaveBeenCalledWith(testMessage);
});
it("provides anonymous private topics", function () {
var t1 = topic(), t2 = topic();
t1.listen(mockCallback);
t2.notify(testMessage);
expect(mockCallback).not.toHaveBeenCalledWith(testMessage);
t1.notify(testMessage);
expect(mockCallback).toHaveBeenCalledWith(testMessage);
});
});
}
);

View File

@ -25,6 +25,7 @@
"services/Now",
"services/Throttle",
"services/Topic",
"types/MergeModels",
"types/TypeCapability",
@ -35,4 +36,4 @@
"views/ViewCapability",
"views/ViewProvider"
]
]

View File

@ -52,6 +52,7 @@ define(
origin = [0, 0],
domainExtrema,
rangeExtrema,
buffers = {},
bufferArray = [],
domainOffset;
@ -63,11 +64,10 @@ define(
// Check if this set of ids matches the current set of ids
// (used to detect if line preparation can be skipped)
function idsMatch(nextIds) {
return nextIds.map(function (id, index) {
return ids[index] === id;
}).reduce(function (a, b) {
return a && b;
}, true);
return ids.length === nextIds.length &&
nextIds.every(function (id, index) {
return ids[index] === id;
});
}
// Prepare plot lines for this group of telemetry objects
@ -76,7 +76,7 @@ define(
next = {};
// Detect if we already have everything we need prepared
if (ids.length === nextIds.length && idsMatch(nextIds)) {
if (idsMatch(nextIds)) {
// Nothing to prepare, move on
return;
}
@ -90,13 +90,13 @@ define(
// Create buffers for these objects
bufferArray = ids.map(function (id) {
var buffer = new PlotLineBuffer(
domainOffset,
INITIAL_SIZE,
maxPoints
);
next[id] = lines[id] || new PlotLine(buffer);
return buffer;
buffers[id] = buffers[id] || new PlotLineBuffer(
domainOffset,
INITIAL_SIZE,
maxPoints
);
next[id] = lines[id] || new PlotLine(buffers[id]);
return buffers[id];
});
}

View File

@ -59,6 +59,7 @@ define(
telemetryObjects = [],
pool = lossless ? new TelemetryQueue() : new TelemetryTable(),
metadatas,
unlistenToMutation,
updatePending;
// Look up domain objects which have telemetry capabilities.
@ -146,23 +147,59 @@ define(
telemetryObjects = objects;
metadatas = objects.map(lookupMetadata);
// Fire callback, as this will be the first time that
// telemetry objects are available
// telemetry objects are available, or these objects
// will have changed.
if (callback) {
callback();
}
return objects;
}
// Get a reference to relevant objects (those with telemetry
// capabilities) and subscribe to their telemetry updates.
// Keep a reference to their promised return values, as these
// will be unsubscribe functions. (This must be a promise
// because delegation is supported, and retrieving delegate
// telemetry-capable objects may be an asynchronous operation.)
telemetryObjectPromise = promiseRelevantObjects(domainObject);
unsubscribePromise = telemetryObjectPromise
.then(cacheObjectReferences)
.then(subscribeAll);
function unsubscribeAll() {
return unsubscribePromise.then(function (unsubscribes) {
return $q.all(unsubscribes.map(function (unsubscribe) {
return unsubscribe();
}));
});
}
function initialize() {
// Get a reference to relevant objects (those with telemetry
// capabilities) and subscribe to their telemetry updates.
// Keep a reference to their promised return values, as these
// will be unsubscribe functions. (This must be a promise
// because delegation is supported, and retrieving delegate
// telemetry-capable objects may be an asynchronous operation.)
telemetryObjectPromise = promiseRelevantObjects(domainObject);
unsubscribePromise = telemetryObjectPromise
.then(cacheObjectReferences)
.then(subscribeAll);
}
function idsMatch(ids) {
return ids.length === telemetryObjects.length &&
ids.every(function (id, index) {
return telemetryObjects[index].getId() === id;
});
}
function modelChange(model) {
if (!idsMatch((model || {}).composition || [])) {
// Reinitialize if composition has changed
unsubscribeAll().then(initialize);
}
}
function addMutationListener() {
var mutation = domainObject &&
domainObject.getCapability('mutation');
if (mutation) {
return mutation.listen(modelChange);
}
}
initialize();
unlistenToMutation = addMutationListener();
return {
/**
@ -172,11 +209,10 @@ define(
* @memberof TelemetrySubscription
*/
unsubscribe: function () {
return unsubscribePromise.then(function (unsubscribes) {
return $q.all(unsubscribes.map(function (unsubscribe) {
return unsubscribe();
}));
});
if (unlistenToMutation) {
unlistenToMutation();
}
return unsubscribeAll();
},
/**
* Get the most recent domain value that has been observed
@ -264,4 +300,4 @@ define(
return TelemetrySubscription;
}
);
);

View File

@ -32,7 +32,9 @@ define(
mockDomainObject,
mockCallback,
mockTelemetry,
mockMutation,
mockUnsubscribe,
mockUnlisten,
mockSeries,
testMetadata,
subscription;
@ -59,7 +61,12 @@ define(
"telemetry",
["subscribe", "getMetadata"]
);
mockMutation = jasmine.createSpyObj(
"mutation",
["mutate", "listen"]
);
mockUnsubscribe = jasmine.createSpy("unsubscribe");
mockUnlisten = jasmine.createSpy("unlisten");
mockSeries = jasmine.createSpyObj(
"series",
[ "getPointCount", "getDomainValue", "getRangeValue" ]
@ -68,12 +75,19 @@ define(
mockQ.when.andCallFake(mockPromise);
mockDomainObject.hasCapability.andReturn(true);
mockDomainObject.getCapability.andReturn(mockTelemetry);
mockDomainObject.getCapability.andCallFake(function (c) {
return {
telemetry: mockTelemetry,
mutation: mockMutation
}[c];
});
mockDomainObject.getId.andReturn('test-id');
mockTelemetry.subscribe.andReturn(mockUnsubscribe);
mockTelemetry.getMetadata.andReturn(testMetadata);
mockMutation.listen.andReturn(mockUnlisten);
mockSeries.getPointCount.andReturn(42);
mockSeries.getDomainValue.andReturn(123456);
mockSeries.getRangeValue.andReturn(789);
@ -213,6 +227,22 @@ define(
expect(mockCallback2)
.toHaveBeenCalledWith([ mockDomainObject ]);
});
it("reinitializes on mutation", function () {
expect(mockTelemetry.subscribe.calls.length).toEqual(1);
// Notify of a mutation which appears to change composition
mockMutation.listen.mostRecentCall.args[0]({
composition: ['Z']
});
// Use subscribe call as an indication of reinitialization
expect(mockTelemetry.subscribe.calls.length).toEqual(2);
});
it("stops listening for mutation on unsubscribe", function () {
expect(mockUnlisten).not.toHaveBeenCalled();
subscription.unsubscribe();
expect(mockUnlisten).toHaveBeenCalled();
});
});
}
);
);