]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/cache.jsm
Import r6948 from upstream hg supporting Firefox up to 24.*
[dactyl.git] / common / modules / cache.jsm
1 // Copyright (c) 2011-2012 Kris Maglione <maglione.k@gmail.com>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
7 defineModule("cache", {
8     exports: ["Cache", "cache"],
9     require: ["config", "services", "util"]
10 });
11
12 lazyRequire("overlay", ["overlay"]);
13 lazyRequire("storage", ["File"]);
14
15 var Cache = Module("Cache", XPCOM(Ci.nsIRequestObserver), {
16     init: function init() {
17         this.queue = [];
18         this.cache = {};
19         this.providers = {};
20         this.globalProviders = this.providers;
21         this.providing = {};
22         this.localProviders = {};
23
24         if (JSMLoader.cacheFlush)
25             this.flush();
26
27         update(services["dactyl:"].providers, {
28             "cache": function (uri, path) {
29                 let contentType = "text/plain";
30                 try {
31                     contentType = services.mime.getTypeFromURI(uri);
32                 }
33                 catch (e) {}
34
35                 if (!cache.cacheReader || !cache.cacheReader.hasEntry(path))
36                     return [contentType, cache.force(path)];
37
38                 let channel = services.StreamChannel(uri);
39                 channel.contentStream = cache.cacheReader.getInputStream(path);
40                 channel.contentType = contentType;
41                 channel.contentCharset = "UTF-8";
42                 return channel;
43             }
44         });
45     },
46
47     Local: function Local(dactyl, modules, window) ({
48         init: function init() {
49             delete this.instance;
50             this.providers = {};
51         },
52
53         isLocal: true
54     }),
55
56     parse: function parse(str) {
57         if (~'{['.indexOf(str[0]))
58             return JSON.parse(str);
59         return str;
60     },
61
62     stringify: function stringify(obj) {
63         if (isString(obj))
64             return obj;
65         return JSON.stringify(obj);
66     },
67
68     compression: 9,
69
70     cacheFile: Class.Memoize(function () {
71         let dir = File(services.directory.get("ProfD", Ci.nsIFile))
72                     .child("dactyl");
73         if (!dir.exists())
74             dir.create(dir.DIRECTORY_TYPE, octal(777));
75         return dir.child("cache.zip");
76     }),
77
78     get cacheReader() {
79         if (!this._cacheReader && this.cacheFile.exists()
80                 && !this.inQueue)
81             try {
82                 this._cacheReader = services.ZipReader(this.cacheFile.file);
83             }
84             catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
85                 util.reportError(e);
86                 this.closeWriter();
87                 this.cacheFile.remove(false);
88             }
89
90         return this._cacheReader;
91     },
92
93     get inQueue() this._cacheWriter && this._cacheWriter.inQueue,
94
95     getCacheWriter: function () {
96         if (!this._cacheWriter)
97             try {
98                 let mode = File.MODE_RDWR;
99                 if (!this.cacheFile.exists())
100                     mode |= File.MODE_CREATE;
101
102                 cache._cacheWriter = services.ZipWriter(this.cacheFile.file, mode);
103             }
104             catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
105                 util.reportError(e);
106                 this.cacheFile.remove(false);
107
108                 mode |= File.MODE_CREATE;
109                 cache._cacheWriter = services.ZipWriter(this.cacheFile.file, mode);
110             }
111         return this._cacheWriter;
112     },
113
114     closeReader: function closeReader() {
115         if (cache._cacheReader) {
116             this.cacheReader.close();
117             delete cache._cacheReader;
118         }
119     },
120
121     closeWriter: util.wrapCallback(function closeWriter() {
122         this.closeReader();
123
124         if (this._cacheWriter) {
125             this._cacheWriter.close();
126             delete cache._cacheWriter;
127
128             // ZipWriter bug.
129             if (this.cacheFile.fileSize <= 22)
130                 this.cacheFile.remove(false);
131         }
132     }),
133
134     flush: function flush() {
135         cache.cache = {};
136         if (this.cacheFile.exists()) {
137             this.closeReader();
138
139             this.flushJAR(this.cacheFile);
140             this.cacheFile.remove(false);
141         }
142     },
143
144     flushAll: function flushAll(file) {
145         this.flushStartup();
146         this.flush();
147     },
148
149     flushEntry: function flushEntry(name, time) {
150         if (this.cacheReader && this.cacheReader.hasEntry(name)) {
151             if (time && this.cacheReader.getEntry(name).lastModifiedTime / 1000 >= time)
152                 return;
153
154             this.queue.push([null, name]);
155             cache.processQueue();
156         }
157
158         delete this.cache[name];
159     },
160
161     flushJAR: function flushJAR(file) {
162         services.observer.notifyObservers(File(file).file, "flush-cache-entry", "");
163     },
164
165     flushStartup: function flushStartup() {
166         services.observer.notifyObservers(null, "startupcache-invalidate", "");
167     },
168
169     force: function force(name, localOnly) {
170         util.waitFor(function () !this.inQueue, this);
171
172         if (this.cacheReader && this.cacheReader.hasEntry(name)) {
173             return this.parse(File.readStream(
174                 this.cacheReader.getInputStream(name)));
175         }
176
177         if (Set.has(this.localProviders, name) && !this.isLocal) {
178             for each (let { cache } in overlay.modules)
179                 if (cache._has(name))
180                     return cache.force(name, true);
181         }
182
183         if (Set.has(this.providers, name)) {
184             util.assert(!Set.add(this.providing, name),
185                         "Already generating cache for " + name,
186                         false);
187             try {
188                 let [func, self] = this.providers[name];
189                 this.cache[name] = func.call(self || this, name);
190             }
191             finally {
192                 delete this.providing[name];
193             }
194
195             cache.queue.push([Date.now(), name]);
196             cache.processQueue();
197
198             return this.cache[name];
199         }
200
201         if (this.isLocal && !localOnly)
202             return cache.force(name);
203     },
204
205     get: function get(name, callback, self) {
206         if (!Set.has(this.cache, name)) {
207             if (callback && !(Set.has(this.providers, name) ||
208                               Set.has(this.localProviders, name)))
209                 this.register(name, callback, self);
210
211             this.cache[name] = this.force(name);
212             util.assert(this.cache[name] !== undefined,
213                         "No such cache key", false);
214         }
215
216         return this.cache[name];
217     },
218
219     _has: function _has(name) Set.has(this.providers, name) || set.has(this.cache, name),
220
221     has: function has(name) [this.globalProviders, this.cache, this.localProviders]
222             .some(function (obj) Set.has(obj, name)),
223
224     register: function register(name, callback, self) {
225         if (this.isLocal)
226             Set.add(this.localProviders, name);
227
228         this.providers[name] = [callback, self];
229     },
230
231     processQueue: function processQueue() {
232         this.closeReader();
233         this.closeWriter();
234
235         if (this.queue.length && !this.inQueue) {
236             // removeEntry does not work properly with queues.
237             let removed = 0;
238             for each (let [, entry] in this.queue)
239                 if (this.getCacheWriter().hasEntry(entry)) {
240                     this.getCacheWriter().removeEntry(entry, false);
241                     removed++;
242                 }
243             if (removed)
244                 this.closeWriter();
245
246             this.queue.splice(0).forEach(function ([time, entry]) {
247                 if (time && Set.has(this.cache, entry)) {
248                     let stream = services.CharsetConv("UTF-8")
249                                          .convertToInputStream(this.stringify(this.cache[entry]));
250
251                     this.getCacheWriter().addEntryStream(entry, time * 1000,
252                                                          this.compression, stream,
253                                                          true);
254                 }
255             }, this);
256
257             if (this._cacheWriter)
258                 this.getCacheWriter().processQueue(this, null);
259         }
260     },
261
262     onStopRequest: function onStopRequest() {
263         this.processQueue();
264     }
265 });
266
267 endModule();
268
269 // catch(e){ if (typeof e === "string") e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
270
271 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: