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