1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * Handles serialization of the data and persistence into a file.
8 * This modules handles the raw data stored in JavaScript serializable objects,
9 * and contains no special validation or query logic, that is handled entirely
10 * by "storage.js" instead.
12 * The data can be manipulated only after it has been loaded from disk. The
13 * load process can happen asynchronously, through the "load" method, or
14 * synchronously, through "ensureDataReady". After any modification, the
15 * "saveSoon" method must be called to flush the data to disk asynchronously.
17 * The raw data should be manipulated synchronously, without waiting for the
18 * event loop or for promise resolution, so that the saved file is always
19 * consistent. This synchronous approach also simplifies the query and update
20 * logic. For example, it is possible to find an object and modify it
21 * immediately without caring whether other code modifies it in the meantime.
23 * An asynchronous shutdown observer makes sure that data is always saved before
24 * the browser is closed. The data cannot be modified during shutdown.
26 * The file is stored in JSON format, without indentation, using UTF-8 encoding.
31 var EXPORTED_SYMBOLS = ["JSONFile"];
35 const { XPCOMUtils } = ChromeUtils.import(
36 "resource://gre/modules/XPCOMUtils.jsm"
39 ChromeUtils.defineModuleGetter(
42 "resource://gre/modules/AsyncShutdown.jsm"
44 ChromeUtils.defineModuleGetter(
47 "resource://gre/modules/DeferredTask.jsm"
49 ChromeUtils.defineModuleGetter(
52 "resource://gre/modules/FileUtils.jsm"
54 ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
55 ChromeUtils.defineModuleGetter(
58 "resource://gre/modules/NetUtil.jsm"
61 XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function() {
62 return new TextDecoder();
65 XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function() {
66 return new TextEncoder();
69 const FileInputStream = Components.Constructor(
70 "@mozilla.org/network/file-input-stream;1",
76 * Delay between a change to the data and the related save operation.
78 const kSaveDelayMs = 1500;
83 * Handles serialization of the data and persistence into a file.
85 * @param config An object containing following members:
86 * - path: String containing the file path where data should be saved.
87 * - dataPostProcessor: Function triggered when data is just loaded. The
88 * data object will be passed as the first argument
89 * and should be returned no matter it's modified or
90 * not. Its failure leads to the failure of load()
91 * and ensureDataReady().
92 * - saveDelayMs: Number indicating the delay (in milliseconds) between a
93 * change to the data and the related save operation. The
94 * default value will be applied if omitted.
95 * - beforeSave: Promise-returning function triggered just before the
96 * data is written to disk. This can be used to create any
97 * intermediate directories before saving. The file will
98 * not be saved if the promise rejects or the function
99 * throws an exception.
100 * - finalizeAt: An `AsyncShutdown` phase or barrier client that should
101 * automatically finalize the file when triggered. Defaults
102 * to `profileBeforeChange`; exposed as an option for
104 * - compression: A compression algorithm to use when reading and
107 function JSONFile(config) {
108 this.path = config.path;
110 if (typeof config.dataPostProcessor === "function") {
111 this._dataPostProcessor = config.dataPostProcessor;
113 if (typeof config.beforeSave === "function") {
114 this._beforeSave = config.beforeSave;
117 if (config.saveDelayMs === undefined) {
118 config.saveDelayMs = kSaveDelayMs;
120 this._saver = new DeferredTask(() => this._save(), config.saveDelayMs);
123 if (config.compression) {
124 this._options.compression = config.compression;
127 this._finalizeAt = config.finalizeAt || AsyncShutdown.profileBeforeChange;
128 this._finalizeInternalBound = this._finalizeInternal.bind(this);
129 this._finalizeAt.addBlocker(
130 "JSON store: writing data",
131 this._finalizeInternalBound
135 JSONFile.prototype = {
137 * String containing the file path where data should be saved.
142 * True when data has been loaded.
147 * DeferredTask that handles the save operation.
152 * Internal data object.
157 * Internal fields used during finalization.
160 _finalizePromise: null,
161 _finalizeInternalBound: null,
164 * Serializable object containing the data. This is populated directly with
165 * the data loaded from the file, and is saved without modifications.
167 * The raw data should be manipulated synchronously, without waiting for the
168 * event loop or for promise resolution, so that the saved file is always
172 if (!this.dataReady) {
173 throw new Error("Data is not ready.");
179 * Sets the loaded data to a new object. This will overwrite any persisted
180 * data on the next save.
184 this.dataReady = true;
188 * Loads persistent data from the file to memory.
191 * @resolves When the operation finished successfully.
192 * @rejects JavaScript exception when dataPostProcessor fails. It never fails
193 * if there is no dataPostProcessor.
196 if (this.dataReady) {
203 let bytes = await OS.File.read(this.path, this._options);
205 // If synchronous loading happened in the meantime, exit now.
206 if (this.dataReady) {
210 data = JSON.parse(gTextDecoder.decode(bytes));
212 // If an exception occurred because the file did not exist, we should
213 // just start with new data. Other errors may indicate that the file is
214 // corrupt, thus we move it to a backup location before allowing it to
215 // be overwritten by an empty file.
216 if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
219 // Move the original file to a backup location, ignoring errors.
221 let openInfo = await OS.File.openUnique(this.path + ".corrupt", {
224 await openInfo.file.close();
225 await OS.File.move(this.path, openInfo.path);
231 // In some rare cases it's possible for data to have been added to
232 // our database between the call to OS.File.read and when we've been
233 // notified that there was a problem with it. In that case, leave the
234 // synchronously-added data alone.
235 if (this.dataReady) {
240 this._processLoadedData(data);
244 * Loads persistent data from the file to memory, synchronously. An exception
245 * can be thrown only if dataPostProcessor exists and fails.
248 if (this.dataReady) {
255 // This reads the file and automatically detects the UTF-8 encoding.
256 let inputStream = new FileInputStream(
257 new FileUtils.File(this.path),
258 FileUtils.MODE_RDONLY,
259 FileUtils.PERMS_FILE,
263 let bytes = NetUtil.readInputStream(
265 inputStream.available()
267 data = JSON.parse(gTextDecoder.decode(bytes));
272 // If an exception occurred because the file did not exist, we should just
273 // start with new data. Other errors may indicate that the file is
274 // corrupt, thus we move it to a backup location before allowing it to be
275 // overwritten by an empty file.
278 ex instanceof Components.Exception &&
279 ex.result == Cr.NS_ERROR_FILE_NOT_FOUND
283 // Move the original file to a backup location, ignoring errors.
285 let originalFile = new FileUtils.File(this.path);
286 let backupFile = originalFile.clone();
287 backupFile.leafName += ".corrupt";
288 backupFile.createUnique(
289 Ci.nsIFile.NORMAL_FILE_TYPE,
292 backupFile.remove(false);
293 originalFile.moveTo(backupFile.parent, backupFile.leafName);
300 this._processLoadedData(data);
304 * Called when the data changed, this triggers asynchronous serialization.
307 return this._saver.arm();
311 * Saves persistent data from memory to the file.
313 * If an error occurs, the previous file is not deleted.
316 * @resolves When the operation finished successfully.
317 * @rejects JavaScript exception.
322 json = JSON.stringify(this._data);
324 // If serialization fails, try fallback safe JSON converter.
325 if (typeof this._data.toJSONSafe == "function") {
326 json = JSON.stringify(this._data.toJSONSafe());
332 // Create or overwrite the file.
333 let bytes = gTextEncoder.encode(json);
334 if (this._beforeSave) {
335 await Promise.resolve(this._beforeSave());
337 await OS.File.writeAtomic(
340 Object.assign({ tmpPath: this.path + ".tmp" }, this._options)
345 * Synchronously work on the data just loaded into memory.
347 _processLoadedData(data) {
348 if (this._finalizePromise) {
349 // It's possible for `load` to race with `finalize`. In that case, don't
350 // process or set the loaded data.
353 this.data = this._dataPostProcessor ? this._dataPostProcessor(data) : data;
357 * Finishes persisting data to disk and resets all state for this file.
360 * @resolves When the object is finalized.
362 _finalizeInternal() {
363 if (this._finalizePromise) {
364 // Finalization already in progress; return the pending promise. This is
365 // possible if `finalize` is called concurrently with shutdown.
366 return this._finalizePromise;
368 this._finalizePromise = (async () => {
369 await this._saver.finalize();
371 this.dataReady = false;
373 return this._finalizePromise;
377 * Ensures that all data is persisted to disk, and prevents future calls to
378 * `saveSoon`. This is called automatically on shutdown, but can also be
379 * called explicitly when the file is no longer needed.
382 if (this._finalizePromise) {
383 throw new Error(`The file ${this.path} has already been finalized`);
385 // Wait for finalization before removing the shutdown blocker.
386 await this._finalizeInternal();
387 this._finalizeAt.removeBlocker(this._finalizeInternalBound);