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