using System; using System.Collections.Generic; using TradeIdeas.TIProData.Interfaces; namespace TradeIdeas.TIProData { public class SymbolDetailsCacheManager : ISymbolDetailsCacheManager { private readonly ISendManager _sendManager; public SymbolDetailsCacheManager(ISendManager sendManager) { _sendManager = sendManager; } public interface Info { /// /// The original request. /// string Symbol { get; } /// /// Might be null. /// string CompanyName { get; } /// /// Might be null. /// string Exchange { get; } } /// /// Look for the given data. Immediately return something, whether we have data or /// not. /// /// If we have not already requested data for this symbol, we will request it now, /// so it will be in the cache soon. You can use this to preload the cache with /// symbols that we expect people to request soon. This object will not send more /// than one request to the server even if this object receives multiple requests. /// /// /// /// An info object. This might or might not have any data. This might or might not /// change from a state of no data to having data based on another thread. (This will /// never change from having data back to no data.) /// public Info Request(String symbol) { return RequestInternal(symbol); } private Item RequestInternal(String symbol) { Item result; lock (_items) { if (!_items.TryGetValue(symbol, out result)) { result = new Item(symbol, _items, _sendManager); _items.Add(symbol, result); } } return result; } /// /// Request info. We will call the callback whenever the info is avaialble. The /// callback can come in any thread. In particular, if we already have the data /// in the cache, the callback might happen in the current thread, before this function /// returns. /// /// This will automatically combine multiple requests, if appropriate. I.e. if three /// different parts of the code all want the same data, they can each make a seperate /// request to this object. This object will make sure only one request makes it to /// the server. /// /// /// public void Request(String symbol, Action callback) { RequestInternal(symbol).addListener(callback); } private class Item : Info, TopListRequest.Listener { public string Symbol { get; private set; } public string CompanyName { get; set; } public string Exchange { get; set; } /// /// Null means we've already made the callback, so we shouldn't add anything /// to the list. /// private List> _listeners = new List>(); /// /// Notify this listener when we get data. /// /// If we already have data, notify him immediately, in the current thread, /// before we return. /// /// If we call the listener immediately, any exceptions will be reported /// like normal. If we call the callback at a later time, any exceptions /// might be reported to the debugger but are otherwise ignored. /// /// public void addListener(Action listener) { System.Diagnostics.Debug.Assert(null != listener); lock (_mutex) { if (null != _listeners) { // Do the callback later. _listeners.Add(listener); listener = null; } } if (null != listener) // Do the callback now. listener(this); } private readonly Object _mutex; private readonly ISendManager _sendManager; public Item(String symbol, Object mutex, ISendManager sendManager) { Symbol = symbol; _mutex = mutex; _sendManager = sendManager; RequestFromServer(); } private void RequestFromServer() { // Single symbol requests are much faster than general reqests. If someone // accidentally sets Symbol to null, that will be a slow request and it won't // give you what you expect. System.Diagnostics.Debug.Assert(null != Symbol); TopListRequest r = new TopListRequest(); r.SingleSymbol = Symbol; r.Streaming = false; r.UseDatabase = false; // We have a lot of flexibility in the keys. By using the standard values that // the alerts and top lists normally use, then we can use // StreamingAlertsManager.GetCompanyName() and friends to parse the result. // TODO there should be a common place for constants for the internal codes, like // "D_Name". We have constants for the wire names like "c_D_Name" but that's // not appropriate here. r.AddExtraColumn("D_Name"); r.AddExtraColumn("D_Exch"); r.Send(_sendManager, this); } void TopListRequest.Listener.OnDisconnect(TopListRequest.Token requestToken) { // The request failed. Presumably a network error. Try it again. requestToken.GetRequest().Send(_sendManager, this); } void TopListRequest.Listener.OnRowData(List rows, DateTime? start, DateTime? end, TopListRequest.Token requestToken) { System.Diagnostics.Debug.Assert(null != rows); // That was possible in a previous version of TopListRequest. if (rows.Count == 0) // Could be a lot of things. Maybe this symbol doesn't exist. Maybe there was something // wrong with our request. System.Diagnostics.Debug.Print("Could not find data for " + Symbol); else if (rows.Count > 1) // Seems unlikely. Some sort of mistake. System.Diagnostics.Debug.Print("Found " + rows.Count + " rows for " + Symbol + ", expecting 1."); else { var row = rows[0]; CompanyName = row.GetCompanyName(null); Exchange = row.GetExchange(null); } List> toNotify = null; lock (_mutex) { toNotify = _listeners; _listeners = null; } if (null == toNotify) { // We've already notified the listeners. We should only receive one callback from the // top list, so we should never get here. But if the client is slow enough, this might // happen. If it happens a lot, it's probably a programming error. System.Diagnostics.Debug.Print("Multiple calls in SymbolDetailsCache!"); } else { // This might be overkill. Normally you'd just have a variable of a delegate type // and use += to add more listeners. I don't like the way that handles exceptions. // The first listener to throw an exception will prevent any of the other listeners // from receiving an exception. That seems especially bad because in this case // we will throw away any exceptions! Iterating over my own list allows me to // insulate the callbacks from one another. foreach (var callback in toNotify) try { callback(this); } catch (Exception ex) { System.Diagnostics.Debug.Print("Error in callback in SymbolDetailsCache: " + ex); } } } void TopListRequest.Listener.OnMetaData(TopListRequest.Token requestToken) { // We don't expect to see anything here. However, the client API doesn't enforce that. // And the API is in flux in general. Better just to ignore this if we see it. } } /// /// This item is used as a mutex. Don't modify _items without locking it first. /// Also, don't do a modify Item.loaded or Item.listeners without holding this mutex. /// private readonly Dictionary _items = new Dictionary(); } }