{"action":"created","issue":{"url":"https://api.github.com/repos/Netflix/falcor/issues/809","repository_url":"https://api.github.com/repos/Netflix/falcor","labels_url":"https://api.github.com/repos/Netflix/falcor/issues/809/labels{/name}","comments_url":"https://api.github.com/repos/Netflix/falcor/issues/809/comments","events_url":"https://api.github.com/repos/Netflix/falcor/issues/809/events","html_url":"https://github.com/Netflix/falcor/issues/809","id":172273766,"number":809,"title":"Abstract cache interface?","user":{"login":"sfrdmn","id":590570,"avatar_url":"https://avatars.githubusercontent.com/u/590570?v=3","gravatar_id":"","url":"https://api.github.com/users/sfrdmn","html_url":"https://github.com/sfrdmn","followers_url":"https://api.github.com/users/sfrdmn/followers","following_url":"https://api.github.com/users/sfrdmn/following{/other_user}","gists_url":"https://api.github.com/users/sfrdmn/gists{/gist_id}","starred_url":"https://api.github.com/users/sfrdmn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sfrdmn/subscriptions","organizations_url":"https://api.github.com/users/sfrdmn/orgs","repos_url":"https://api.github.com/users/sfrdmn/repos","events_url":"https://api.github.com/users/sfrdmn/events{/privacy}","received_events_url":"https://api.github.com/users/sfrdmn/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":1,"created_at":"2016-08-20T14:34:30Z","updated_at":"2016-08-20T23:18:17Z","closed_at":null,"body":"Possibly a feature request, possibly a misunderstanding!\r\n\r\nI'm trying to implement some offline-first functionality in a mobile app and so I'd like data to be retrieved from the cache as much as possible. Let's say for a subset of my data, I can give it an `$expires` value of 1 and only ever retrieve it from the remote once. This works great, until of course my app is closed and the in-memory cache is wiped. When I restart the app, the cache is empty, and I fetch the value once more from the remote.\r\n\r\nI could periodically serialize the cache and persist it, then load it whenever the app boots up. However, this seems a wee bit hacky, like using a CRON job for replication or something. Since Falcor already allows for using Models as DataSources, it smells like there's a cleaner solution in chaining `Model -> Model/DataSource [-> Model/DataSource] -> DataSource` as a means of implementing a cache hierarchy. This already works if your hierarchy is something like `Client -> Kinda close server -> Super far server` since the default cache implementation makes sense there. However, if the intermediate DataSource represents a disk on the same device, I don't need another in-memory cache. Its cache should persist to disk incrementally and transparently, with misses proxying to the next DataSource. But currently, there is no abstract cache interface to implement and no means of supplying a Model with a custom cache implementation.\r\n\r\nIs this something that's already possible? Planned? Or, does it make sense at all?\r\n\r\nI guess this would have pretty hairy repercussions for manual cache invalidation. But `$expires` should work as before?\r\n\r\nEDIT: er, I guess the cache is probably assumed to be a synchronous API, so this idea might not fly. Still, would be interesting to hear the reasoning behind the design decision"},"comment":{"url":"https://api.github.com/repos/Netflix/falcor/issues/comments/241229544","html_url":"https://github.com/Netflix/falcor/issues/809#issuecomment-241229544","issue_url":"https://api.github.com/repos/Netflix/falcor/issues/809","id":241229544,"user":{"login":"trxcllnt","id":178183,"avatar_url":"https://avatars.githubusercontent.com/u/178183?v=3","gravatar_id":"","url":"https://api.github.com/users/trxcllnt","html_url":"https://github.com/trxcllnt","followers_url":"https://api.github.com/users/trxcllnt/followers","following_url":"https://api.github.com/users/trxcllnt/following{/other_user}","gists_url":"https://api.github.com/users/trxcllnt/gists{/gist_id}","starred_url":"https://api.github.com/users/trxcllnt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/trxcllnt/subscriptions","organizations_url":"https://api.github.com/users/trxcllnt/orgs","repos_url":"https://api.github.com/users/trxcllnt/repos","events_url":"https://api.github.com/users/trxcllnt/events{/privacy}","received_events_url":"https://api.github.com/users/trxcllnt/received_events","type":"User","site_admin":false},"created_at":"2016-08-20T23:18:17Z","updated_at":"2016-08-20T23:18:17Z","body":"@sfrdmn the cache was designed to be serialized (to disk, local storage, redis, etc.) to allow hydrating on startup if you want. You can pass an `onChange` handler in the props object to your root Model's constructor that will be called after any writes to the cache. Here's an example:\r\n\r\n```es6\r\nimport { Model } from 'falcor';\r\nconst localStorageKey = 'my-falcor-app-cache';\r\nconst serializeCache = (key, cache) => localStorage.setItem(key, JSON.stringify(cache));\r\nconst deserializeCache =(key) => JSON.parse(localStorage.getItem(key) || {});\r\nconst rootModel = new Model({\r\n  cache: deserializeCache(localStorageKey),\r\n  onChange: function() { // <-- probably want to debounce this function\r\n    if (!rootModel) { return }; // <-- onChange can get called before `rootModel` is bound\r\n    serializeCache(localStorageKey, rootModel.getCache());\r\n  }\r\n});\r\n```\r\n\r\n`onChange` is a little noisy (a get/set/call operation might invoke `onChange` multiple times, depending on whether it has to retry, or if a call returns values/invalidations), so you'll probably want to debounce the handler.\r\n\r\nAlternatively, I implemented an `onChangesCompleted` handler in [my fork](https://github.com/trxcllnt/falcor/tree/onChangesCompleted) that only gets called after all pending cache operations are finished. My fork also has other benefits (stability improvements, critical bugfixes, better performance, friendlier error messages), that haven't or won't make it into the official release, so feel free to experiment and contribute there if you have any ideas:\r\n\r\n```es6\r\n// set \"dependencies\": { \"falcor\": \"trxcllnt/falcor#onChangesCompleted\" } in your package.json\r\nimport { Model } from 'falcor';\r\nconst localStorageKey = 'my-falcor-app-cache';\r\nconst serializeCache = (key, cache) => localStorage.setItem(key, JSON.stringify(cache));\r\nconst deserializeCache = (key) => JSON.parse(localStorage.getItem(key) || {});\r\nconst rootModel = new Model({\r\n  cache: deserializeCache(localStorageKey),\r\n  onChangesCompleted: function() { //    `this` is bound to rootModel\r\n    serializeCache(localStorageKey, this.getCache()); \r\n  }\r\n});\r\n```\r\n\r\nIf you want more granular control over when you serialize the cache, you could implement the DataSource API that acts as client-side caching middleware that serializes the cache state as desired:\r\n\r\n```es6\r\nimport { Observable } from 'rx';\r\nconst localStorageKey = 'my-falcor-app-cache';\r\nconst serializeCache = (key, cache) => localStorage.setItem(key, JSON.stringify(cache));\r\nconst deserializeCache = (key) => JSON.parse(localStorage.getItem(key) || {});\r\n\r\nconst onlineDatasource = new falcor.HTTPDataSource('/model.json');\r\nconst localRootModel = new Model({ cache: deserializeCache(localStorageKey) });\r\nconst remoteRootModel = new Model({\r\n    ...localRootModel, // <-- ensures remoteRootModel will use the same cache as localRootModel\r\n    source: cachingMiddleware(localRootModel, onlineDatasource)\r\n});\r\n\r\nfunction cachingMiddleware(model, datasource) {\r\n    let requestCount = 0, cacheInterval = 10;\r\n    return {\r\n        get(...args) {\r\n            return cacheAfterComplete(datasource.get(...args));\r\n        },\r\n        set(...args) {\r\n            return cacheAfterComplete(datasource.set(...args));\r\n        },\r\n        call(...args) {\r\n            return cacheAfterComplete(datasource.call(...args));\r\n        }\r\n    };\r\n    function cacheAfterComplete(operation) {\r\n        return Observable.create(function subscribe(observer) {\r\n            return operation.subscribe({\r\n                onNext: observer.onNext.bind(observer),\r\n                onError: observer.onError.bind(observer),\r\n                onCompleted() {\r\n                    // Tell the Model the operation is done.\r\n                    // The Model will write the results into the cache synchronously,\r\n                    // so they will be there when we call `getCache` below.\r\n                    observer.onCompleted();\r\n                    // Custom logic -- only serialize after every 10th request\r\n                    if (++requestCount % cacheInterval === 0) {\r\n                        serializeCache(localStorageKey, model.getCache());\r\n                    }\r\n                }\r\n            });\r\n        });\r\n    }\r\n}\r\n```"}}
{"actor":{"display_login":"trxcllnt"}}