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