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