Task: downloading list of files. Architectural solution?

pavel.fljot's Avatar

pavel.fljot

01 Aug, 2011 09:17 AM

I'm developing set of kiosk applications (AIR, for museum) which, of course, loading some assets in runtime (xmls, pictures, videos, ...). To make it kinda faster and network-problems-bulletproof I'm pre-caching all the assets at the runtime (downloading them in other words). The server supplies me with a list of files that have been updated/added since last caching attempt (timestamp saved in app "cookies"), then I download them to local folder and then load the into app when needed instead of loading from server.

So the question is how to do that nicely in architectural way? I guess:
1. CachingListService — gets list of files to download (Vector., where DownloadableFileVO has remote path (where to download from) and local path(where locally save to) ).
2. DownloadingService — accepts list of files to download. Downloading all the files, uses Promise to notify complete/fail.
Am I right at this point?

Next what is unclear: I have a view which is, let's say, minimalcomps List where items should display files and their's downloading process (progress). Also button to skip file (so there is DownloadingService.skipFile(file:DownloadableFileVO);) How is it better to update progress? I used to have progress field and Signals in my DownloadFileVO and so populating list with that vector of DownloadFileVOs... as we discussed somewhere it's not that much VO anymore, but actually some mini model.. What bothers me — I don't need those progress field and signals as soon as it's downloaded, so it's just a waste of RAM. I've heard about some proxies concept?

And general thing — is it better to keep those Services, Commands and stuff as usual in app context or make a separate context (sort of module)?

Thank you. And hope to see (in general) more examples with using services except for basic text-based services (Twitter, RSS — that's trivial).

  1. Support Staff 1 Posted by Ondina D.F. on 04 Aug, 2011 06:23 PM

    Ondina D.F.'s Avatar

    Hi Pavel
    I’m sorry that you didn’t get an answer yet.

    It seems that everyone is busy, me included :)

    Your are looking for an architectural solution that I can’t answer in 5 minutes or so and anyway it would be just my opinion.
    Usually Stray is the one who likes to answer such questions:)

  2. 2 Posted by pavel.fljot on 17 Aug, 2011 11:57 AM

    pavel.fljot's Avatar

    OK, probably I shall try to separate it into smaller questions. So let's start from the beginning:

    First we need to create(retrieve and create) list of files to download. So I created Interface for Service:

    public interface ICacheAssetsListRetreiverService
    {
        function set parser(value:ICacheAssetsListParser):void;
    
        function getList(lastUpdateTimestamp:uint = 0):Promise;
    }
    

    where parser interface is

    public interface ICacheAssetsListParser
    {
        function parseData(data:String):Vector.<CacheAssetVO>;
    }
    

    and CacheAssetVO is describing file to be downloaded

    public class CacheAssetVO
    {
        public var source:String;// http://domain.com/project/path/to/file.jpg
        public var relativeURL:String;// path/to/file.jpg
        public var tempFile:File;// pointer to temporary file location, e.g. tempDir.resolvePath(relativeURL)
        public var localFile:File;// pointer to final file location, e.g. storageDirectory.resolvePath(relativeURL)
        public var filename:String;// file.jpg
    }
    

    Question 1: how to tell my service where to download xml from?
    Is this approach good enough?

    public class CacheAssetsListRetreieverService implements ICacheAssetsListRetreiverService
    {
        [Inject]
        public var config:CacheAssetsListRetreieverServiceConfig;
    
        private var promiseMap:Dictionary = new Dictionary();
    
    
        private var _parser:ICacheAssetsListParser;
        [Inject]
        public function set parser(value:ICacheAssetsListParser):void
        {
            _parser = value;
        }
    
    
        public function getList(lastUpdateTimestamp:uint = 0):Promise
        {
            var promise:Promise = new Promise();
            var urlLoader:URLLoader = new URLLoader();
            urlLoader.addEventListener(Event.COMPLETE, urlLoader_completeHandler);
            urlLoader.addEventListener(IOErrorEvent.IO_ERROR, urlLoader_errorHandler);
            urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, urlLoader_errorHandler);
            urlLoader.load(new URLRequest(config.assetsURL.replace("%timestamp%", lastUpdateTimestamp)));
    
            return promise;
        }
    
    
        private function removeListeners(urlLoader:URLLoader):void
        {
            urlLoader.removeEventListener(Event.COMPLETE, urlLoader_completeHandler);
            urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, urlLoader_errorHandler);
            urlLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, urlLoader_errorHandler);
        }
    
    
        private function urlLoader_completeHandler(event:Event):void
        {
            var urlLoader:URLLoader = event.currentTarget as URLLoader;
            removeListeners(urlLoader);
            var promise:Promise = promiseMap[urlLoader] as Promise;
            delete promiseMap[urlLoader];
    
            promise.handleResult(_parser.parseData(String(urlLoader.data)));
        }
    
    
        private function urlLoader_errorHandler(event:Event):void
        {
            var urlLoader:URLLoader = event.currentTarget as URLLoader;
            removeListeners(urlLoader);
            var promise:Promise = promiseMap[urlLoader] as Promise;
            delete promiseMap[urlLoader];
    
            promise.handleError(event);
        }
    }
    
    
    public class CacheAssetsListRetreieverServiceConfig
    {
        public var assetsURL:String = "http://domain/foo/bar/cms/Virtualtour_assets.php?timestamp=%timestamp%";
    }
    
    
    public class CacheAssetsModel
    {
        private static const cookies:SharedObject = SharedObject.getLocal("CacheAssetsModel");
    
    
        public function get lastUpdateTimestamp():uint
        {
            if (cookies.data.lastUpdateTime == undefined)
            {
                cookies.data.lastUpdateTime = 0;
            }
            return cookies.data.lastUpdateTime;
        }
        public function set lastUpdateTimestamp(value:uint):void
        {
            cookies.data.lastUpdateTime = value;
            cookies.flush();
        }
    }
    

    Question 2: how to decide wether Service should be mapped as Singleton or not?
    Question 3: how to deal with concurrent requests (if it's mapped as Singleton)?

    Thanks in advance.

  3. Support Staff 3 Posted by Ondina D.F. on 22 Aug, 2011 08:20 AM

    Ondina D.F.'s Avatar
    OK, probably I shall try to separate it into smaller questions. So let's start from the beginning:

    hehe, yes let’s try again to get your questions answered.

    Question 1: how to tell my service where to download xml from?
    Is this approach good enough?
    I use a similar approach, so I’d say it is ok. (my opinion)

    Let’s hope others will chime in too this time :)

    Ondina

  4. 4 Posted by pavel.fljot on 22 Aug, 2011 09:17 PM

    pavel.fljot's Avatar

    But what are the exact benefits in doing so?

  5. Support Staff 5 Posted by creynders on 23 Aug, 2011 04:41 PM

    creynders's Avatar

    Hey Pavel,

    Not much to say, it looks fine!

    But what are the exact benefits in doing so?

    What exactly do you mean? Injecting the config data? Or?

  6. 6 Posted by pavel.fljot on 23 Aug, 2011 04:51 PM

    pavel.fljot's Avatar

    Yes, regarding injecting config data. How is it better than storing that url in service itself (const or public property).
    And btw I've changed that to

    public class CacheAssetsListRetreieverServiceConfig
    {
        public var url:String = "http://black.velvet.ee/EESTI_PANK/cms/VirtualTour_assets.php?timestamp=%timestamp%";
    
        public function getURL(timestamp:uint):String
        {
            return url.replace("%timestamp%", timestamp);
        }
    }
    

    which looks a bit cleaner IMHO.

  7. Support Staff 7 Posted by creynders on 24 Aug, 2011 07:14 AM

    creynders's Avatar

    Well, I would implement it a little differently TBH, but I think that's a matter of preference.

    I'd have the url stored as a const in a separate constants class. And create a separate helper class with the getURL method. Then either I'd let the calling command pass the url to CacheAssetsListRetreieverService#getList or if the url stays the same for all subsequent service calls I'd map it as a named injection in my mapping/configuring command and that way inject it into CacheAssetsListRetreieverService.
    Another option, if there are many url's that will need to be constructed using getURL I'd have a separate URLListModel (with getURL method) that caches the urls.

  8. 8 Posted by pavel.fljot on 24 Aug, 2011 12:30 PM

    pavel.fljot's Avatar

    creynders,

    well I'm not sure what I'm for, but definitely against named injections since they give a huge chance to break something (wrong string or conflicts). As to store such things in another constants class — that's how I used to do things before (god object AppConfig), but now trying to find a better (flexible and loose-coupled) way. And passing url to getList() is also looks like a wrong way for me, because that abstraction (service and service interface) is about getting the list according to a timestamp, not necessarily via http(url). So that's why I got that feeling to have only timestamp in interface and leave all configuration for certain implementations. Sound right?

    So I got this kinda feeling how to do it right, but not sure/don't have exact points why so.

  9. Support Staff 9 Posted by creynders on 24 Aug, 2011 01:57 PM

    creynders's Avatar

    Ah don't get me wrong, I didn't say to reference the constant directly from the service.

    Actually what I meant was I'd avoid hardcoding the url in the CacheAssetsListRetreieverServiceConfig class. You can use it as a VO to pass the url in order to avoid using a named injection.

    But as I said, I don't think there's anything wrong with the way you're doing it now. I personally would do it a little bit differently, that's all.

  10. Support Staff 10 Posted by Ondina D.F. on 02 Nov, 2011 03:20 PM

    Ondina D.F.'s Avatar

    Hi Pavel,
    Closing the thread.
    If you need more help with this, feel free to re-open this discussion. Please open new threads for new issues.
    Thanks!
    Ondina

  11. Ondina D.F. closed this discussion on 02 Nov, 2011 03:20 PM.

Comments are currently closed for this discussion. You can start a new one.

Keyboard shortcuts

Generic

? Show this help
ESC Blurs the current field

Comment Form

r Focus the comment reply box
^ + ↩ Submit the comment

You can use Command ⌘ instead of Control ^ on Mac