PageSourceSearch

https://jlharchitect.com/lib/pxloader/PxLoader.js?c=1

js jlharchitect.com collected 2026-09-26 14:40:17 UTC 11,933 bytes, 377 lines download raw bytes

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.