1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2012 7x7 Software, Inc.
  4 // License:   Licensed under MIT license
  5 // ==========================================================================
  6 sc_require("tasks/task");
  7 
  8 
  9 /** @private */
 10 SC.AppCacheTask = SC.Task.extend({
 11   run: function () {
 12     window.applicationCache.update();
 13     SC.appCache._appCacheStatusDidChange();
 14   }
 15 });
 16 
 17 
 18 /** @class
 19 
 20   This is a very simple object that makes it easier to use the
 21   window.applicationCache in a SproutCore application.
 22 
 23   The reason this object exists is that SproutCore applications
 24   are excellent candidates for offline access but, it takes more than a little
 25   effort to understand the application cache's various possible states and
 26   events in order to use it effectively.
 27 
 28   You will likely find a use for SC.appCache in two scenarios.  The first is
 29   you want to ensure that your users are notified each time a new version of
 30   your application is deployed in order to ensure they get the latest version.
 31   Because of the manner in which the application cache works, a user may launch
 32   a previous version from the cache and not see the new version.  In this
 33   scenario, you would simply check the value of SC.appCache.get('hasNewVersion')
 34   at some point in your app's initialization cycle.  If hasNewVersion is true,
 35   you can then check SC.appCache.get('isNewVersionValid') to determine whether
 36   to show a message to the user informing them of the new version and reload
 37   the app or to log that the new version failed to upload so you can fix it.
 38 
 39   Note, because the application cache takes some time to determine if a new
 40   version exists, hasNewVersion may initially be `undefined`.  Therefore, you
 41   will likely want to add an observer to the property and continue on with
 42   your app.  By using an observer, you can also tap into another feature of
 43   SC.appCache, which is to lazily check for updates after the app is loaded.
 44   Typically, the browser only checks for updates on the initial load of a page,
 45   but by setting SC.appCache.set('shouldPoll', true), you can have SC.appCache
 46   check for updates in the background at a set interval.  Your observer will
 47   then fire if hasNewVersion ever changes while the app is in use.
 48 
 49   For example,
 50 
 51       // A sample application.
 52       MyApp = SC.Application.create({
 53 
 54         // Called when the value of SC.appCache.hasNewVersion changes.
 55         appCacheHasNewVersionDidChange: function () {
 56           var hasNewVersion = SC.appCache.get('hasNewVersion'),
 57             isNewVersionValid = SC.appCache.get('isNewVersionValid');
 58 
 59           if (hasNewVersion) {
 60             if (isNewVersionValid) {
 61               // Show a message to the user.
 62               MyApp.mainPage.get('newVersionAvailablePanel').append();
 63             } else {
 64               // There is a new version available, but it failed to load.
 65               SC.error('Failed to update application cache.');
 66             }
 67 
 68             // Clean up.
 69             SC.appCache.removeObserver('hasNewVersion', this, 'appCacheHasNewVersionDidChange');
 70           } else {
 71             // Start polling for new versions.  Because the observer is still attached,
 72             // appCacheHasNewVersionDidChange will be called if a new version ever becomes available.
 73             SC.appCache.set('shouldPoll', true);
 74           }
 75 
 76         }
 77 
 78       });
 79 
 80       // The loading state in our sample application.
 81       MyApp.LoadingState = SC.State.extend({
 82 
 83         enterState: function () {
 84           var hasNewVersion = SC.appCache.get('hasNewVersion');
 85 
 86           if (SC.none(hasNewVersion)) {
 87             // The application cache is either caching for the first time , updating or idle.
 88             // In either case we will observe it.
 89             SC.appCache.addObserver('hasNewVersion', MyApp, 'appCacheHasNewVersionDidChange');
 90           } else if (hasNewVersion) {
 91             // There is already a new version available, use our existing code to handle it.
 92             MyApp.appCacheHasNewVersionDidChange();
 93           } else {
 94             // There is no new version, but it's possible that one will appear.
 95             // User our existing code to start polling for new versions.
 96             MyApp.appCacheHasNewVersionDidChange();
 97             SC.appCache.addObserver('hasNewVersion', MyApp, 'appCacheHasNewVersionDidChange');
 98           }
 99         }
100 
101       });
102 
103   The second scenario is if you want to present a UI indicating when the app is
104   ready for offline use.  Remember that it takes some time for the browser
105   to retrieve all the resources in the manifest, so an app may be running for
106   a while before it is ready for offline use.  In this scenario, you would
107   observe the isReadyForOffline property of SC.appCache.
108 
109   Like hasNewVersion, isReadyForOffline has three possible values: undefined, true or false,
110   where the property is undefined while the value is still undetermined.
111 
112   For example,
113 
114       // A sample view.
115       MyApp.MyView = SC.View.extend({
116 
117         childView: ['offlineIndicatorCV'],
118 
119         // An image we use to indicate when the app is safe to use offline.
120         offlineIndicatorCV: SC.ImageView.extend({
121           valueBinding: SC.Binding.oneWay('SC.appCache.isReadyForOffline').
122             transform(function (isReadyForOffline) {
123               if (isReadyForOffline) {
124                 return 'offline-ready';
125               } else if (SC.none(isReadyForOffline)) {
126                 return 'offline-unknown';
127               } else {
128                 return 'offline-not-ready';
129               }
130             })
131         })
132 
133       });
134 
135   The following are some excellent resources on the application cache that were
136   used to develop this class:
137 
138   - [Using the application cache - HTML | MDN](https://developer.mozilla.org/en-US/docs/HTML/Using_the_application_cache)
139   - [Appcache Facts](http://appcachefacts.info)
140   - [Offline Web Applications - Dive Into HTML5](http://diveintohtml5.info/offline.html)
141 
142   @extends SC.Object
143   @since Version 1.10
144 */
145 SC.appCache = SC.Object.create(
146 /** @scope SC.appCache.prototype */{
147 
148   // ------------------------------------------------------------------------
149   // Properties
150   //
151 
152   /**
153     Whether the new version is valid or not.
154 
155     This property is undefined until it can be determined that a new version exists
156     or doesn't exist and if it does exist, whether it is valid or not.
157 
158     @field
159     @type Boolean
160     @default undefined
161     @readonly
162     */
163   isNewVersionValid: function () {
164     var hasNewVersion = this.get('hasNewVersion'),
165       ret,
166       status = this.get('status');
167 
168     if (SC.platform.supportsApplicationCache) {
169       if (hasNewVersion) {
170         if (status === window.applicationCache.UPDATEREADY) {
171           ret = true;
172         } else {
173           ret = false;
174         }
175       } // Else we don't know yet.
176     } else {
177       // The platform doesn't support it, so it must always be false.
178       ret = false;
179     }
180 
181     return ret;
182   }.property('hasNewVersion').cacheable(),
183 
184   /**
185     Whether the application may be run offline or not.
186 
187     This property is undefined until it can be determined that the application
188     has been cached or not cached.
189 
190     @field
191     @type Boolean
192     @default undefined
193     @readonly
194     */
195   isReadyForOffline: function () {
196     var ret,
197       status = this.get('status');
198 
199     if (SC.platform.supportsApplicationCache) {
200       if (status === window.applicationCache.IDLE ||
201           status === window.applicationCache.UPDATEREADY) {
202         ret = true;
203       } else if (status === window.applicationCache.UNCACHED ||
204           status === window.applicationCache.OBSOLETE) {
205         ret = false;
206       } // Else we don't know yet.
207     } else {
208       // The platform doesn't support it, so it must always be false.
209       ret = false;
210     }
211 
212     return ret;
213   }.property('status').cacheable(),
214 
215   /**
216     Whether there is a new version of the application's cache or not.
217 
218     This property is undefined until it can be determined that a new version exists
219     or not.  Note that the new version may not necessarily be valid.  You
220     should check isNewVersionValid after determining that hasNewVersion is
221     true.
222 
223     @field
224     @type Boolean
225     @default undefined
226     @readonly
227     */
228   hasNewVersion: function () {
229     var ret,
230       status = this.get('status');
231 
232     if (SC.platform.supportsApplicationCache) {
233       if (status === window.applicationCache.UPDATEREADY ||
234           status === window.applicationCache.OBSOLETE) {
235         // It is only true if there is an update (which may have failed).
236         ret = true;
237       } else if (status === window.applicationCache.IDLE) {
238         // It is only false if there was no update.
239         ret = false;
240       } // Else we don't know yet.
241     } else {
242       // The platform doesn't support it, so it must always be false.
243       ret = false;
244     }
245 
246     return ret;
247   }.property('status').cacheable(),
248 
249   /**
250     The interval in milliseconds to poll for updates when shouldPoll is true.
251 
252     @type Number
253     @default 1800000 (30 minutes)
254     */
255   interval: 1800000,
256 
257   /**
258     The progress of the application cache between 0.0 (0%) and 1.0 (100%).
259 
260     @type Number
261     @default 0.0
262     */
263   progress: 0,
264 
265   /**
266     Whether or not to regularly check for updates to the application cache
267     while the application is running.
268 
269     This is useful for applications that are left open for several hours at a
270     time, such as a SproutCore application being used in business.  In order to
271     ensure that the users have the latest version, you can set this property
272     to true to have the application regularly check for updates.
273 
274     Updates are run using the background task queue, so as to pose the smallest
275     detriment possible to performance.
276 
277     @field
278     @type Boolean
279     @default false
280     */
281   shouldPoll: function (key, value) {
282     if (SC.none(value)) {
283       // Default value.
284       value = false;
285     } else if (value) {
286       var status = this.get('status');
287       if (status === window.applicationCache.IDLE) {
288         // Start regularly polling for updates.
289         this._timer = SC.Timer.schedule({
290           target: this,
291           action: '_checkForUpdates',
292           interval: this.get('interval'),
293           repeats: YES
294         });
295       } else {
296         // @if(debug)
297         SC.warn('Developer Warning: Attempting to update the application cache should only be done when it is in an IDLE state.  Otherwise, the browser will throw an exception.  The current status is %@.'.fmt(status));
298         // @endif
299       }
300     } else {
301       // Stop any previous polling.
302       if (this._timer) {
303         this._timer.invalidate();
304         this._timer = null;
305         this._task = null;
306       }
307     }
308 
309     return value;
310   }.property().cacheable(),
311 
312   /**
313     The current window.applicationCache status.
314 
315     This is a KVO mapping of window.applicationCache.status.  Possible values
316     are:
317 
318     * window.applicationCache.UNCACHED
319     * window.applicationCache.IDLE
320     * window.applicationCache.CHECKING
321     * window.applicationCache.DOWNLOADING
322     * window.applicationCache.UPDATEREADY
323     * window.applicationCache.OBSOLETE
324 
325     Because of the various interpretations these statuses can mean, you will
326     likely find it easier to use the helper properties on SC.appCache instead.
327 
328     @type Number
329     @default 0
330     */
331   status: 0,
332 
333   // ------------------------------------------------------------------------
334   // Methods
335   //
336 
337   /** @private */
338   _appCacheDidProgress: function (evt) {
339     evt = evt.originalEvent;
340     if (evt.lengthComputable) {
341       this.set('progress', evt.loaded / evt.total);
342     } else {
343       this.set('progress', null);
344     }
345   },
346 
347   /** @private */
348   _appCacheStatusDidChange: function () {
349     var appCache = window.applicationCache,
350       status;
351 
352     status = appCache.status;
353 
354     // Clear all previous event listeners.
355     SC.Event.remove(appCache);
356     switch (status) {
357     case appCache.UNCACHED: // UNCACHED == 0
358       break;
359     case appCache.IDLE: // IDLE == 1
360       this.set('progress', 1);
361       break;
362     case appCache.CHECKING: // CHECKING == 2
363       SC.Event.add(appCache, 'downloading', this, '_appCacheStatusDidChange');
364       SC.Event.add(appCache, 'noupdate', this, '_appCacheStatusDidChange');
365       SC.Event.add(appCache, 'error', this, '_appCacheStatusDidChange');
366       break;
367     case appCache.DOWNLOADING: // DOWNLOADING == 3
368       SC.Event.add(appCache, 'progress', this, '_appCacheDidProgress');
369       SC.Event.add(appCache, 'cached', this, '_appCacheStatusDidChange');
370       SC.Event.add(appCache, 'updateready', this, '_appCacheStatusDidChange');
371       SC.Event.add(appCache, 'error', this, '_appCacheStatusDidChange');
372       break;
373     case appCache.UPDATEREADY:  // UPDATEREADY == 4
374       break;
375     case appCache.OBSOLETE: // OBSOLETE == 5
376       break;
377     default:
378       SC.error('Unknown application cache status: %@'.fmt(appCache.status));
379       break;
380     }
381 
382     // Update our status.
383     this.set('status', status);
384   },
385 
386   /** @private Adds a task to check for application updates to the background task queue. */
387   _checkForUpdates: function () {
388     var task = this._task;
389 
390     if (this.get('status') === window.applicationCache.IDLE) {
391       if (!task) { task = this._task = SC.AppCacheTask.create(); }
392       SC.backgroundTaskQueue.push(task);
393     } else {
394       // Stop polling if the status isn't IDLE.
395       this.set('shouldPoll', false);
396     }
397   },
398 
399   /** @private */
400   init: function () {
401     sc_super();
402 
403     if (SC.platform.supportsApplicationCache) {
404       // By the time that this object is created, we may have already passed
405       // out of the CHECKING state, but _appCacheStatusDidChange() will take care of it.
406       this._appCacheStatusDidChange();
407     } else {
408       SC.warn('Unable to use SC.appCache, the browser does not support the application cache.');
409     }
410   }
411 
412 });
413