michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: #ifndef MERGED_COMPARTMENT michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "Measurement", michael@0: "Provider", michael@0: ]; michael@0: michael@0: const {utils: Cu} = Components; michael@0: michael@0: const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; michael@0: michael@0: #endif michael@0: michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://gre/modules/Preferences.jsm"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: Cu.import("resource://services-common/utils.js"); michael@0: michael@0: michael@0: michael@0: /** michael@0: * Represents a collection of related pieces/fields of data. michael@0: * michael@0: * This is an abstract base type. michael@0: * michael@0: * This type provides the primary interface for storing, retrieving, and michael@0: * serializing data. michael@0: * michael@0: * Each measurement consists of a set of named fields. Each field is primarily michael@0: * identified by a string name, which must be unique within the measurement. michael@0: * michael@0: * Each derived type must define the following properties: michael@0: * michael@0: * name -- String name of this measurement. This is the primary way michael@0: * measurements are distinguished within a provider. michael@0: * michael@0: * version -- Integer version of this measurement. This is a secondary michael@0: * identifier for a measurement within a provider. The version denotes michael@0: * the behavior of this measurement and the composition of its fields over michael@0: * time. When a new field is added or the behavior of an existing field michael@0: * changes, the version should be incremented. The initial version of a michael@0: * measurement is typically 1. michael@0: * michael@0: * fields -- Object defining the fields this measurement holds. Keys in the michael@0: * object are string field names. Values are objects describing how the michael@0: * field works. The following properties are recognized: michael@0: * michael@0: * type -- The string type of this field. This is typically one of the michael@0: * FIELD_* constants from the Metrics.Storage type. michael@0: * michael@0: * michael@0: * FUTURE: provide hook points for measurements to supplement with custom michael@0: * storage needs. michael@0: */ michael@0: this.Measurement = function () { michael@0: if (!this.name) { michael@0: throw new Error("Measurement must have a name."); michael@0: } michael@0: michael@0: if (!this.version) { michael@0: throw new Error("Measurement must have a version."); michael@0: } michael@0: michael@0: if (!Number.isInteger(this.version)) { michael@0: throw new Error("Measurement's version must be an integer: " + this.version); michael@0: } michael@0: michael@0: if (!this.fields) { michael@0: throw new Error("Measurement must define fields."); michael@0: } michael@0: michael@0: for (let [name, info] in Iterator(this.fields)) { michael@0: if (!info) { michael@0: throw new Error("Field does not contain metadata: " + name); michael@0: } michael@0: michael@0: if (!info.type) { michael@0: throw new Error("Field is missing required type property: " + name); michael@0: } michael@0: } michael@0: michael@0: this._log = Log.repository.getLogger("Services.Metrics.Measurement." + this.name); michael@0: michael@0: this.id = null; michael@0: this.storage = null; michael@0: this._fields = {}; michael@0: michael@0: this._serializers = {}; michael@0: this._serializers[this.SERIALIZE_JSON] = { michael@0: singular: this._serializeJSONSingular.bind(this), michael@0: daily: this._serializeJSONDay.bind(this), michael@0: }; michael@0: } michael@0: michael@0: Measurement.prototype = Object.freeze({ michael@0: SERIALIZE_JSON: "json", michael@0: michael@0: /** michael@0: * Obtain a serializer for this measurement. michael@0: * michael@0: * Implementations should return an object with the following keys: michael@0: * michael@0: * singular -- Serializer for singular data. michael@0: * daily -- Serializer for daily data. michael@0: * michael@0: * Each item is a function that takes a single argument: the data to michael@0: * serialize. The passed data is a subset of that returned from michael@0: * this.getValues(). For "singular," data.singular is passed. For "daily", michael@0: * data.days.get() is passed. michael@0: * michael@0: * This function receives a single argument: the serialization format we michael@0: * are requesting. This is one of the SERIALIZE_* constants on this base type. michael@0: * michael@0: * For SERIALIZE_JSON, the function should return an object that michael@0: * JSON.stringify() knows how to handle. This could be an anonymous object or michael@0: * array or any object with a property named `toJSON` whose value is a michael@0: * function. The returned object will be added to a larger document michael@0: * containing the results of all `serialize` calls. michael@0: * michael@0: * The default implementation knows how to serialize built-in types using michael@0: * very simple logic. If small encoding size is a goal, the default michael@0: * implementation may not be suitable. If an unknown field type is michael@0: * encountered, the default implementation will error. michael@0: * michael@0: * @param format michael@0: * (string) A SERIALIZE_* constant defining what serialization format michael@0: * to use. michael@0: */ michael@0: serializer: function (format) { michael@0: if (!(format in this._serializers)) { michael@0: throw new Error("Don't know how to serialize format: " + format); michael@0: } michael@0: michael@0: return this._serializers[format]; michael@0: }, michael@0: michael@0: /** michael@0: * Whether this measurement contains the named field. michael@0: * michael@0: * @param name michael@0: * (string) Name of field. michael@0: * michael@0: * @return bool michael@0: */ michael@0: hasField: function (name) { michael@0: return name in this.fields; michael@0: }, michael@0: michael@0: /** michael@0: * The unique identifier for a named field. michael@0: * michael@0: * This will throw if the field is not known. michael@0: * michael@0: * @param name michael@0: * (string) Name of field. michael@0: */ michael@0: fieldID: function (name) { michael@0: let entry = this._fields[name]; michael@0: michael@0: if (!entry) { michael@0: throw new Error("Unknown field: " + name); michael@0: } michael@0: michael@0: return entry[0]; michael@0: }, michael@0: michael@0: fieldType: function (name) { michael@0: let entry = this._fields[name]; michael@0: michael@0: if (!entry) { michael@0: throw new Error("Unknown field: " + name); michael@0: } michael@0: michael@0: return entry[1]; michael@0: }, michael@0: michael@0: _configureStorage: function () { michael@0: let missing = []; michael@0: for (let [name, info] in Iterator(this.fields)) { michael@0: if (this.storage.hasFieldFromMeasurement(this.id, name)) { michael@0: this._fields[name] = michael@0: [this.storage.fieldIDFromMeasurement(this.id, name), info.type]; michael@0: continue; michael@0: } michael@0: michael@0: missing.push([name, info.type]); michael@0: } michael@0: michael@0: if (!missing.length) { michael@0: return CommonUtils.laterTickResolvingPromise(); michael@0: } michael@0: michael@0: // We only perform a transaction if we have work to do (to avoid michael@0: // extra SQLite overhead). michael@0: return this.storage.enqueueTransaction(function registerFields() { michael@0: for (let [name, type] of missing) { michael@0: this._log.debug("Registering field: " + name + " " + type); michael@0: let id = yield this.storage.registerField(this.id, name, type); michael@0: this._fields[name] = [id, type]; michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: //--------------------------------------------------------------------------- michael@0: // Data Recording Functions michael@0: // michael@0: // Functions in this section are used to record new values against this michael@0: // measurement instance. michael@0: // michael@0: // Generally speaking, these functions will throw if the specified field does michael@0: // not exist or if the storage function requested is not appropriate for the michael@0: // type of that field. These functions will also return a promise that will michael@0: // be resolved when the underlying storage operation has completed. michael@0: //--------------------------------------------------------------------------- michael@0: michael@0: /** michael@0: * Increment a daily counter field in this measurement by 1. michael@0: * michael@0: * By default, the counter for the current day will be incremented. michael@0: * michael@0: * If the field is not known or is not a daily counter, this will throw. michael@0: * michael@0: * michael@0: * michael@0: * @param field michael@0: * (string) The name of the field whose value to increment. michael@0: * @param date michael@0: * (Date) Day on which to increment the counter. michael@0: * @param by michael@0: * (integer) How much to increment by. michael@0: * @return Promise<> michael@0: */ michael@0: incrementDailyCounter: function (field, date=new Date(), by=1) { michael@0: return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field), michael@0: date, by); michael@0: }, michael@0: michael@0: /** michael@0: * Record a new numeric value for a daily discrete numeric field. michael@0: * michael@0: * @param field michael@0: * (string) The name of the field to append a value to. michael@0: * @param value michael@0: * (Number) Number to append. michael@0: * @param date michael@0: * (Date) Day on which to append the value. michael@0: * michael@0: * @return Promise<> michael@0: */ michael@0: addDailyDiscreteNumeric: function (field, value, date=new Date()) { michael@0: return this.storage.addDailyDiscreteNumericFromFieldID( michael@0: this.fieldID(field), value, date); michael@0: }, michael@0: michael@0: /** michael@0: * Record a new text value for a daily discrete text field. michael@0: * michael@0: * This is like `addDailyDiscreteNumeric` but for daily discrete text fields. michael@0: */ michael@0: addDailyDiscreteText: function (field, value, date=new Date()) { michael@0: return this.storage.addDailyDiscreteTextFromFieldID( michael@0: this.fieldID(field), value, date); michael@0: }, michael@0: michael@0: /** michael@0: * Record the last seen value for a last numeric field. michael@0: * michael@0: * @param field michael@0: * (string) The name of the field to set the value of. michael@0: * @param value michael@0: * (Number) The value to set. michael@0: * @param date michael@0: * (Date) When this value was recorded. michael@0: * michael@0: * @return Promise<> michael@0: */ michael@0: setLastNumeric: function (field, value, date=new Date()) { michael@0: return this.storage.setLastNumericFromFieldID(this.fieldID(field), value, michael@0: date); michael@0: }, michael@0: michael@0: /** michael@0: * Record the last seen value for a last text field. michael@0: * michael@0: * This is like `setLastNumeric` except for last text fields. michael@0: */ michael@0: setLastText: function (field, value, date=new Date()) { michael@0: return this.storage.setLastTextFromFieldID(this.fieldID(field), value, michael@0: date); michael@0: }, michael@0: michael@0: /** michael@0: * Record the most recent value for a daily last numeric field. michael@0: * michael@0: * @param field michael@0: * (string) The name of a daily last numeric field. michael@0: * @param value michael@0: * (Number) The value to set. michael@0: * @param date michael@0: * (Date) Day on which to record the last value. michael@0: * michael@0: * @return Promise<> michael@0: */ michael@0: setDailyLastNumeric: function (field, value, date=new Date()) { michael@0: return this.storage.setDailyLastNumericFromFieldID(this.fieldID(field), michael@0: value, date); michael@0: }, michael@0: michael@0: /** michael@0: * Record the most recent value for a daily last text field. michael@0: * michael@0: * This is like `setDailyLastNumeric` except for a daily last text field. michael@0: */ michael@0: setDailyLastText: function (field, value, date=new Date()) { michael@0: return this.storage.setDailyLastTextFromFieldID(this.fieldID(field), michael@0: value, date); michael@0: }, michael@0: michael@0: //--------------------------------------------------------------------------- michael@0: // End of data recording APIs. michael@0: //--------------------------------------------------------------------------- michael@0: michael@0: /** michael@0: * Obtain all values stored for this measurement. michael@0: * michael@0: * The default implementation obtains all known types from storage. If the michael@0: * measurement provides custom types or stores values somewhere other than michael@0: * storage, it should define its own implementation. michael@0: * michael@0: * This returns a promise that resolves to a data structure which is michael@0: * understood by the measurement's serialize() function. michael@0: */ michael@0: getValues: function () { michael@0: return this.storage.getMeasurementValues(this.id); michael@0: }, michael@0: michael@0: deleteLastNumeric: function (field) { michael@0: return this.storage.deleteLastNumericFromFieldID(this.fieldID(field)); michael@0: }, michael@0: michael@0: deleteLastText: function (field) { michael@0: return this.storage.deleteLastTextFromFieldID(this.fieldID(field)); michael@0: }, michael@0: michael@0: /** michael@0: * This method is used by the default serializers to control whether a field michael@0: * is included in the output. michael@0: * michael@0: * There could be legacy fields in storage we no longer care about. michael@0: * michael@0: * This method is a hook to allow measurements to change this behavior, e.g., michael@0: * to implement a dynamic fieldset. michael@0: * michael@0: * You will also need to override `fieldType`. michael@0: * michael@0: * @return (boolean) true if the specified field should be included in michael@0: * payload output. michael@0: */ michael@0: shouldIncludeField: function (field) { michael@0: return field in this._fields; michael@0: }, michael@0: michael@0: _serializeJSONSingular: function (data) { michael@0: let result = {"_v": this.version}; michael@0: michael@0: for (let [field, data] of data) { michael@0: // There could be legacy fields in storage we no longer care about. michael@0: if (!this.shouldIncludeField(field)) { michael@0: continue; michael@0: } michael@0: michael@0: let type = this.fieldType(field); michael@0: michael@0: switch (type) { michael@0: case this.storage.FIELD_LAST_NUMERIC: michael@0: case this.storage.FIELD_LAST_TEXT: michael@0: result[field] = data[1]; michael@0: break; michael@0: michael@0: case this.storage.FIELD_DAILY_COUNTER: michael@0: case this.storage.FIELD_DAILY_DISCRETE_NUMERIC: michael@0: case this.storage.FIELD_DAILY_DISCRETE_TEXT: michael@0: case this.storage.FIELD_DAILY_LAST_NUMERIC: michael@0: case this.storage.FIELD_DAILY_LAST_TEXT: michael@0: continue; michael@0: michael@0: default: michael@0: throw new Error("Unknown field type: " + type); michael@0: } michael@0: } michael@0: michael@0: return result; michael@0: }, michael@0: michael@0: _serializeJSONDay: function (data) { michael@0: let result = {"_v": this.version}; michael@0: michael@0: for (let [field, data] of data) { michael@0: if (!this.shouldIncludeField(field)) { michael@0: continue; michael@0: } michael@0: michael@0: let type = this.fieldType(field); michael@0: michael@0: switch (type) { michael@0: case this.storage.FIELD_DAILY_COUNTER: michael@0: case this.storage.FIELD_DAILY_DISCRETE_NUMERIC: michael@0: case this.storage.FIELD_DAILY_DISCRETE_TEXT: michael@0: case this.storage.FIELD_DAILY_LAST_NUMERIC: michael@0: case this.storage.FIELD_DAILY_LAST_TEXT: michael@0: result[field] = data; michael@0: break; michael@0: michael@0: case this.storage.FIELD_LAST_NUMERIC: michael@0: case this.storage.FIELD_LAST_TEXT: michael@0: continue; michael@0: michael@0: default: michael@0: throw new Error("Unknown field type: " + type); michael@0: } michael@0: } michael@0: michael@0: return result; michael@0: }, michael@0: }); michael@0: michael@0: michael@0: /** michael@0: * An entity that emits data. michael@0: * michael@0: * A `Provider` consists of a string name (must be globally unique among all michael@0: * known providers) and a set of `Measurement` instances. michael@0: * michael@0: * The main role of a `Provider` is to produce metrics data and to store said michael@0: * data in the storage backend. michael@0: * michael@0: * Metrics data collection is initiated either by a manager calling a michael@0: * `collect*` function on `Provider` instances or by the `Provider` registering michael@0: * to some external event and then reacting whenever they occur. michael@0: * michael@0: * `Provider` implementations interface directly with a storage backend. For michael@0: * common stored values (daily counters, daily discrete values, etc), michael@0: * implementations should interface with storage via the various helper michael@0: * functions on the `Measurement` instances. For custom stored value types, michael@0: * implementations will interact directly with the low-level storage APIs. michael@0: * michael@0: * Because multiple providers exist and could be responding to separate michael@0: * external events simultaneously and because not all operations performed by michael@0: * storage can safely be performed in parallel, writing directly to storage at michael@0: * event time is dangerous. Therefore, interactions with storage must be michael@0: * deferred until it is safe to perform them. michael@0: * michael@0: * This typically looks something like: michael@0: * michael@0: * // This gets called when an external event worthy of recording metrics michael@0: * // occurs. The function receives a numeric value associated with the event. michael@0: * function onExternalEvent (value) { michael@0: * let now = new Date(); michael@0: * let m = this.getMeasurement("foo", 1); michael@0: * michael@0: * this.enqueueStorageOperation(function storeExternalEvent() { michael@0: * michael@0: * // We interface with storage via the `Measurement` helper functions. michael@0: * // These each return a promise that will be resolved when the michael@0: * // operation finishes. We rely on behavior of storage where operations michael@0: * // are executed single threaded and sequentially. Therefore, we only michael@0: * // need to return the final promise. michael@0: * m.incrementDailyCounter("foo", now); michael@0: * return m.addDailyDiscreteNumericValue("my_value", value, now); michael@0: * }.bind(this)); michael@0: * michael@0: * } michael@0: * michael@0: * michael@0: * `Provider` is an abstract base class. Implementations must define a few michael@0: * properties: michael@0: * michael@0: * name michael@0: * The `name` property should be a string defining the provider's name. The michael@0: * name must be globally unique for the application. The name is used as an michael@0: * identifier to distinguish providers from each other. michael@0: * michael@0: * measurementTypes michael@0: * This must be an array of `Measurement`-derived types. Note that elements michael@0: * in the array are the type functions, not instances. Instances of the michael@0: * `Measurement` are created at run-time by the `Provider` and are bound michael@0: * to the provider and to a specific storage backend. michael@0: */ michael@0: this.Provider = function () { michael@0: if (!this.name) { michael@0: throw new Error("Provider must define a name."); michael@0: } michael@0: michael@0: if (!Array.isArray(this.measurementTypes)) { michael@0: throw new Error("Provider must define measurement types."); michael@0: } michael@0: michael@0: this._log = Log.repository.getLogger("Services.Metrics.Provider." + this.name); michael@0: michael@0: this.measurements = null; michael@0: this.storage = null; michael@0: } michael@0: michael@0: Provider.prototype = Object.freeze({ michael@0: /** michael@0: * Whether the provider only pulls data from other sources. michael@0: * michael@0: * If this is true, the provider pulls data from other sources. By contrast, michael@0: * "push-based" providers subscribe to foreign sources and record/react to michael@0: * external events as they happen. michael@0: * michael@0: * Pull-only providers likely aren't instantiated until a data collection michael@0: * is performed. Thus, implementations cannot rely on a provider instance michael@0: * always being alive. This is an optimization so provider instances aren't michael@0: * dead weight while the application is running. michael@0: * michael@0: * This must be set on the prototype to have an effect. michael@0: */ michael@0: pullOnly: false, michael@0: michael@0: /** michael@0: * Obtain a `Measurement` from its name and version. michael@0: * michael@0: * If the measurement is not found, an Error is thrown. michael@0: */ michael@0: getMeasurement: function (name, version) { michael@0: if (!Number.isInteger(version)) { michael@0: throw new Error("getMeasurement expects an integer version. Got: " + version); michael@0: } michael@0: michael@0: let m = this.measurements.get([name, version].join(":")); michael@0: michael@0: if (!m) { michael@0: throw new Error("Unknown measurement: " + name + " v" + version); michael@0: } michael@0: michael@0: return m; michael@0: }, michael@0: michael@0: init: function (storage) { michael@0: if (this.storage !== null) { michael@0: throw new Error("Provider() not called. Did the sub-type forget to call it?"); michael@0: } michael@0: michael@0: if (this.storage) { michael@0: throw new Error("Provider has already been initialized."); michael@0: } michael@0: michael@0: this.measurements = new Map(); michael@0: this.storage = storage; michael@0: michael@0: let self = this; michael@0: return Task.spawn(function init() { michael@0: let pre = self.preInit(); michael@0: if (!pre || typeof(pre.then) != "function") { michael@0: throw new Error("preInit() does not return a promise."); michael@0: } michael@0: yield pre; michael@0: michael@0: for (let measurementType of self.measurementTypes) { michael@0: let measurement = new measurementType(); michael@0: michael@0: measurement.provider = self; michael@0: measurement.storage = self.storage; michael@0: michael@0: let id = yield storage.registerMeasurement(self.name, measurement.name, michael@0: measurement.version); michael@0: michael@0: measurement.id = id; michael@0: michael@0: yield measurement._configureStorage(); michael@0: michael@0: self.measurements.set([measurement.name, measurement.version].join(":"), michael@0: measurement); michael@0: } michael@0: michael@0: let post = self.postInit(); michael@0: if (!post || typeof(post.then) != "function") { michael@0: throw new Error("postInit() does not return a promise."); michael@0: } michael@0: yield post; michael@0: }); michael@0: }, michael@0: michael@0: shutdown: function () { michael@0: let promise = this.onShutdown(); michael@0: michael@0: if (!promise || typeof(promise.then) != "function") { michael@0: throw new Error("onShutdown implementation does not return a promise."); michael@0: } michael@0: michael@0: return promise; michael@0: }, michael@0: michael@0: /** michael@0: * Hook point for implementations to perform pre-initialization activity. michael@0: * michael@0: * This method will be called before measurement registration. michael@0: * michael@0: * Implementations should return a promise which is resolved when michael@0: * initialization activities have completed. michael@0: */ michael@0: preInit: function () { michael@0: return CommonUtils.laterTickResolvingPromise(); michael@0: }, michael@0: michael@0: /** michael@0: * Hook point for implementations to perform post-initialization activity. michael@0: * michael@0: * This method will be called after `preInit` and measurement registration, michael@0: * but before initialization is finished. michael@0: * michael@0: * If a `Provider` instance needs to register observers, etc, it should michael@0: * implement this function. michael@0: * michael@0: * Implementations should return a promise which is resolved when michael@0: * initialization activities have completed. michael@0: */ michael@0: postInit: function () { michael@0: return CommonUtils.laterTickResolvingPromise(); michael@0: }, michael@0: michael@0: /** michael@0: * Hook point for shutdown of instances. michael@0: * michael@0: * This is the opposite of `onInit`. If a `Provider` needs to unregister michael@0: * observers, etc, this is where it should do it. michael@0: * michael@0: * Implementations should return a promise which is resolved when michael@0: * shutdown activities have completed. michael@0: */ michael@0: onShutdown: function () { michael@0: return CommonUtils.laterTickResolvingPromise(); michael@0: }, michael@0: michael@0: /** michael@0: * Collects data that doesn't change during the application's lifetime. michael@0: * michael@0: * Implementations should return a promise that resolves when all data has michael@0: * been collected and storage operations have been finished. michael@0: * michael@0: * @return Promise<> michael@0: */ michael@0: collectConstantData: function () { michael@0: return CommonUtils.laterTickResolvingPromise(); michael@0: }, michael@0: michael@0: /** michael@0: * Collects data approximately every day. michael@0: * michael@0: * For long-running applications, this is called approximately every day. michael@0: * It may or may not be called every time the application is run. It also may michael@0: * be called more than once per day. michael@0: * michael@0: * Implementations should return a promise that resolves when all data has michael@0: * been collected and storage operations have completed. michael@0: * michael@0: * @return Promise<> michael@0: */ michael@0: collectDailyData: function () { michael@0: return CommonUtils.laterTickResolvingPromise(); michael@0: }, michael@0: michael@0: /** michael@0: * Queue a deferred storage operation. michael@0: * michael@0: * Deferred storage operations are the preferred method for providers to michael@0: * interact with storage. When collected data is to be added to storage, michael@0: * the provider creates a function that performs the necessary storage michael@0: * interactions and then passes that function to this function. Pending michael@0: * storage operations will be executed sequentially by a coordinator. michael@0: * michael@0: * The passed function should return a promise which will be resolved upon michael@0: * completion of storage interaction. michael@0: */ michael@0: enqueueStorageOperation: function (func) { michael@0: return this.storage.enqueueOperation(func); michael@0: }, michael@0: michael@0: /** michael@0: * Obtain persisted provider state. michael@0: * michael@0: * Provider state consists of key-value pairs of string names and values. michael@0: * Providers can stuff whatever they want into state. They are encouraged to michael@0: * store as little as possible for performance reasons. michael@0: * michael@0: * State is backed by storage and is robust. michael@0: * michael@0: * These functions do not enqueue on storage automatically, so they should michael@0: * be guarded by `enqueueStorageOperation` or some other mutex. michael@0: * michael@0: * @param key michael@0: * (string) The property to retrieve. michael@0: * michael@0: * @return Promise String value on success. null if no state michael@0: * is available under this key. michael@0: */ michael@0: getState: function (key) { michael@0: return this.storage.getProviderState(this.name, key); michael@0: }, michael@0: michael@0: /** michael@0: * Set state for this provider. michael@0: * michael@0: * This is the complementary API for `getState` and obeys the same michael@0: * storage restrictions. michael@0: */ michael@0: setState: function (key, value) { michael@0: return this.storage.setProviderState(this.name, key, value); michael@0: }, michael@0: michael@0: _dateToDays: function (date) { michael@0: return Math.floor(date.getTime() / MILLISECONDS_PER_DAY); michael@0: }, michael@0: michael@0: _daysToDate: function (days) { michael@0: return new Date(days * MILLISECONDS_PER_DAY); michael@0: }, michael@0: }); michael@0: