1/** 2 * PixelLab Resource Loader 3 * Loads resources while providing progress updates. 4 */ 5 6function PxLoader(settings) { 7 8 // merge settings with defaults 9 settings = settings || {}; 10 11 // how frequently we poll resources for progress 12 if (settings.statusInterval == null) { 13 settings.statusInterval = 5000; // every 5 seconds by default 14 } 15 16 // delay before logging since last progress change 17 if (settings.loggingDelay == null) { 18 settings.loggingDelay = 20 * 1000; // log stragglers after 20 secs 19 } 20 21 // stop waiting if no progress has been made in the moving time window 22 if (settings.noProgressTimeout == null) { 23 settings.noProgressTimeout = Infinity; // do not stop waiting by default 24 } 25 26 var entries = [], 27 // holds resources to be loaded with their status 28 progressListeners = [], 29 timeStarted, progressChanged = +new Date; 30 31 /** 32 * The status of a resource 33 * @enum {number} 34 */ 35 var ResourceState = { 36 QUEUED: 0, 37 WAITING: 1, 38 LOADED: 2, 39 ERROR: 3, 40 TIMEOUT: 4 41 }; 42 43 // places non-array values into an array. 44 var ensureArray = function(val) { 45 if (val == null) { 46 return []; 47 } 48 49 if (Array.isArray(val)) { 50 return val; 51 } 52 53 return [val]; 54 }; 55 56 // add an entry to the list of resources to be loaded 57 this.add = function(resource) { 58 59 // ensure tags are in an object 60 resource.tags = new PxLoaderTags(resource.tags); 61 62 // ensure priority is set 63 if (resource.priority == null) { 64 resource.priority = Infinity; 65 } 66 67 entries.push({ 68 resource: resource, 69 status: ResourceState.QUEUED 70 }); 71 }; 72 73 this.addProgressListener = function(callback, tags) { 74 progressListeners.push({ 75 callback: callback, 76 tags: new PxLoaderTags(tags) 77 }); 78 }; 79 80 this.addCompletionListener = function(callback, tags) { 81 progressListeners.push({ 82 tags: new PxLoaderTags(tags), 83 callback: function(e) { 84 if (e.completedCount === e.totalCount) { 85 callback(); 86 } 87 } 88 }); 89 }; 90 91 // creates a comparison function for resources 92 var getResourceSort = function(orderedTags) { 93 94 // helper to get the top tag's order for a resource 95 orderedTags = ensureArray(orderedTags); 96 var getTagOrder = function(entry) { 97 var resource = entry.resource, 98 bestIndex = Infinity; 99 for (var i = 0; i < resource.tags.length; i++) { 100 for (var j = 0; j < Math.min(orderedTags.length, bestIndex); j++) { 101 if (resource.tags[i] == orderedTags[j] && j < bestIndex) { 102 bestIndex = j; 103 if (bestIndex === 0) break; 104 } 105 if (bestIndex === 0) break; 106 } 107 } 108 return bestIndex; 109 }; 110 return function(a, b) { 111 // check tag order first 112 var aOrder = getTagOrder(a), 113 bOrder = getTagOrder(b); 114 if (aOrder < bOrder) return -1; 115 if (aOrder > bOrder) return 1; 116 117 // now check priority 118 if (a.priority < b.priority) return -1; 119 if (a.priority > b.priority) return 1; 120 return 0; 121 } 122 }; 123 124 this.start = function(orderedTags) { 125 timeStarted = +new Date; 126 127 // first order the resources 128 var compareResources = getResourceSort(orderedTags); 129 entries.sort(compareResources); 130 131 // trigger requests for each resource 132 for (var i = 0, len = entries.length; i < len; i++) { 133 var entry = entries[i]; 134 entry.status = ResourceState.WAITING; 135 entry.resource.start(this); 136 } 137 138 // do an initial status check soon since items may be loaded from the cache 139 setTimeout(statusCheck, 100); 140 }; 141 142 var statusCheck = function() { 143 var checkAgain = false, 144 noProgressTime = (+new Date) - progressChanged, 145 timedOut = (noProgressTime >= settings.noProgressTimeout), 146 shouldLog = (noProgressTime >= settings.loggingDelay); 147 148 for (var i = 0, len = entries.length; i < len; i++) { 149 var entry = entries[i]; 150 if (entry.status !== ResourceState.WAITING) { 151 continue; 152 } 153 154 // see if the resource has loaded 155 if (entry.resource.checkStatus) { 156 entry.resource.checkStatus(); 157 } 158 159 // if still waiting, mark as timed out or make sure we check again 160 if (entry.status === ResourceState.WAITING) { 161 if (timedOut) { 162 entry.resource.onTimeout(); 163 } else { 164 checkAgain = true; 165 } 166 } 167 } 168 169 // log any resources that are still pending 170 if (shouldLog && checkAgain) { 171 log(); 172 } 173 174 if (checkAgain) { 175 setTimeout(statusCheck, settings.statusInterval); 176 } 177 }; 178 179 this.isBusy = function() { 180 for (var i = 0, len = entries.length; i < len; i++) { 181 if (entries[i].status === ResourceState.QUEUED || entr
181ies[i].status === ResourceState.WAITING) { 182 return true; 183 } 184 } 185 return false; 186 }; 187 188 var onProgress = function(resource, statusType) { 189 // find the entry for the resource 190 var entry = null; 191 for (var i = 0, len = entries.length; i < len; i++) { 192 if (entries[i].resource === resource) { 193 entry = entries[i]; 194 break; 195 } 196 } 197 198 // we have already updated the status of the resource 199 if (entry == null || entry.status !== ResourceState.WAITING) { 200 return; 201 } 202 entry.status = statusType; 203 progressChanged = +new Date; 204 205 var numResourceTags = resource.tags.length; 206 207 // fire callbacks for interested listeners 208 for (var i = 0, numListeners = progressListeners.length; i < numListeners; i++) { 209 var listener = progressListeners[i], 210 shouldCall; 211 212 if (listener.tags.length === 0) { 213 // no tags specified so always tell the listener 214 shouldCall = true; 215 } else { 216 // listener only wants to hear about certain tags 217 shouldCall = resource.tags.contains(listener.tags); 218 } 219 220 if (shouldCall) { 221 sendProgress(entry, listener); 222 } 223 } 224 }; 225 226 this.onLoad = function(resource) { 227 onProgress(resource, ResourceState.LOADED); 228 }; 229 this.onError = function(resource) { 230 onProgress(resource, ResourceState.ERROR); 231 }; 232 this.onTimeout = function(resource) { 233 onProgress(resource, ResourceState.TIMEOUT); 234 }; 235 236 // sends a progress report to a listener 237 var sendProgress = function(updatedEntry, listener) { 238 // find stats for all the resources the caller is interested in 239 var completed = 0, 240 total = 0; 241 for (var i = 0, len = entries.length; i < len; i++) { 242 var entry = entries[i], 243 includeResource = false;
244 245 if (listener.tags.length === 0) { 246 // no tags specified so always tell the listener 247 includeResource = true; 248 } else { 249 includeResource = entry.resource.tags.contains(listener.tags); 250 } 251 252 if (includeResource) { 253 total++; 254 if (entry.status === ResourceState.LOADED || entry.status === ResourceState.ERROR || entry.status === ResourceState.TIMEOUT) { 255 completed++; 256 } 257 } 258 } 259 260 listener.callback({ 261 // info about the resource that changed 262 resource: updatedEntry.resource, 263 264 // should we expose StatusType instead? 265 loaded: (updatedEntry.status === ResourceState.LOADED), 266 error: (updatedEntry.status === ResourceState.ERROR), 267 timeout: (updatedEntry.status === ResourceState.TIMEOUT), 268 269 // updated stats for all resources 270 completedCount: completed, 271 totalCount: total 272 }); 273 }; 274 275 // prints the status of each resource to the console 276 var log = this.log = function(showAll) { 277 if (!window.console) { 278 return; 279 } 280 281 var elapsedSeconds = Math.round((+new Date - timeStarted) / 1000); 282 window.console.log('PxLoader elapsed: ' + elapsedSeconds + ' sec'); 283 284 for (var i = 0, len = entries.length; i < len; i++) { 285 var entry = entries[i]; 286 if (!showAll && entry.status !== ResourceState.WAITING) { 287 continue; 288 } 289 290 var message = 'PxLoader: #' + i + ' ' + entry.resource.getName(); 291 switch(entry.status) { 292 case ResourceState.QUEUED: 293 message += ' (Not Started)'; 294 break; 295 case ResourceState.WAITING: 296 message += ' (Waiting)'; 297 break; 298 case ResourceState.LOADED: 299 message += ' (Loaded)'; 300 break; 301 case ResourceState.ERROR: 302 message += ' (Error)'; 303 break; 304 case ResourceState.TIMEOUT: 305 message += ' (Timeout)'; 306 break; 307 } 308 309 if (entry.resource.tags.length > 0) { 310 message += ' Tags: [' + entry.resource.tags.join(',') + ']'; 311 } 312 313 window.console.log(message); 314 } 315 }; 316} 317 318// Tag object to handle tag intersection; once created not meant to be changed 319// Performance rationale: http://jsperf.com/lists-indexof-vs-in-operator/3 320 321function PxLoaderTags(values) { 322 323 this.array = []; 324 this.object = {}; 325 this.value = null; // single value 326 this.length = 0; 327 328 if (values !== null && values !== undefined) { 329 if (Array.isArray(values)) { 330 this.array = values; 331 } else if (typeof values === 'object') { 332 for (var key in values) { 333 this.array.push(key); 334 } 335 } else { 336 this.array.push(values); 337 this.value = values; 338 } 339 340 this.length = this.array.length; 341 342 // convert array values to object with truthy values, used by contains function below 343 for (var i = 0; i < this.length; i++) { 344 this.object[this.array[i]] = true; 345 } 346 } 347 348 // compare this object with another; return true if they share at least one value 349 this.contains = function(other) { 350 if (this.length === 0 || other.length === 0) { 351 return false; 352 } else if (this.length === 1 && this.value !== null) { 353 if (other.length === 1) { 354 return this.value === other.value; 355 } else { 356 return other.object.hasOwnProperty(this.value); 357 } 358 } else if (other.length < this.length) { 359 return other.contains(this); // better to loop through the smaller object 360 } else { 361 for (var key in this.object) { 362 if (other.object[key]) { 363 return true; 364 } 365 } 366 return false;
367 } 368 } 369} 370 371// shims to ensure we have newer Array utility methods 372// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray 373if (!Array.isArray) { 374 Array.isArray = function(arg) { 375 return Object.prototype.toString.call(arg) == '[object Array]'; 376 }; 377}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.