New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

persisto

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

persisto - npm Package Compare versions

Comparing version 1.1.0 to 1.2.0

.eslintignore

2

bower.json
{
"name": "persisto",
"description": "Persist Javascript objects to localStorage and remote servers.",
"version": "1.1.0",
"version": "1.2.0",
"main": [

@@ -6,0 +6,0 @@ "dist/persisto.js"

@@ -1,14 +0,16 @@

# 1.1.1-0 / Unreleased
*
# 1.2.1-0 / Unreleased
# 1.2.0 / 2019-09-07
* [ADD] AMD wrapper and module support (`persisto = require('persisto')`).
* [CHANGE] Apply and enforce 'prettier' codestyle
# 1.1.0 / 2016-08-14
* [CHANGE] rename `.init` option to `.defaults`
* [CHANGE] rename `.debug` option to `.debugLevel`
* [CHANGE] Rename `.init` option to `.defaults`.
* [CHANGE] Rename `.debug` option to `.debugLevel`.
# 1.0.0 / 2016-08-11
* [ADD] .set() creates intermediate objects if missing
* [ADD] .writeToForm() and .readFromForm() accept inputs with nested names
* [FIX] .remove() supports dot notation
* [ADD] .set() creates intermediate objects if missing.
* [ADD] .writeToForm() and .readFromForm() accept inputs with nested names.
* [FIX] .remove() supports dot notation.
# 0.0.1 / 2016-04-10
* Initial release
* Initial release.

@@ -1,2 +0,2 @@

Copyright 2016 Martin Wendt,
Copyright 2016-2017 Martin Wendt,
http://wwWendt.de/

@@ -3,0 +3,0 @@

/*!
* persisto.js
*
* Persistent Javascript objects and web forms using Web Storage.
* Persistent JavaScript objects and web forms using Web Storage.
*
* Copyright (c) 2016, Martin Wendt (http://wwWendt.de)
* Copyright (c) 2016-2017, Martin Wendt (http://wwWendt.de)
* Released under the MIT license
*
* @version 1.1.0
* @date 2016-08-14T10:00
* @version 1.2.0
* @date 2019-09-07T13:47:35Z
*/
;(function($, window, document, undefined) {
(function(factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["jquery"], factory);
} else if (typeof module === "object" && module.exports) {
// Node/CommonJS
module.exports = factory(require("jquery"));
} else {
// Browser globals
factory(jQuery);
}
})(function($) {
"use strict";
/*globals console */
/*******************************************************************************
* Private functions and variables
*/
"use strict";
var MAX_INT = 9007199254740991,
// Allow mangling of some global names:
console = window.console,
error = $.error;
/*******************************************************************************
* Private functions and variables
*/
/**
* A persistent plain object or array.
*/
window.PersistentObject = function(namespace, opts) {
var prevValue,
self = this,
dfd = $.Deferred(),
stamp = Date.now(); // disable warning 'PersistentObject is not defined'
var MAX_INT = 9007199254740991;
/* Return current time stamp. */
function _getNow() {
return new Date().getTime();
}
/**
* A persistent plain object or array.
*/
window.PersistentObject = function(namespace, opts) {
var prevValue,
self = this,
dfd = $.Deferred(),
stamp = _getNow();
/* jshint ignore:start */ // disable warning 'PersistentObject is not defined'
if ( !(this instanceof PersistentObject) ) { $.error("Must use 'new' keyword"); }
/* jshint ignore:start */ if (!(this instanceof PersistentObject)) {
error("Must use 'new' keyword");
}
/* jshint ignore:end */
if ( typeof namespace !== "string" ) {
$.error(this + ": Missing required argument: namespace");
}
if (typeof namespace !== "string") {
error(this + ": Missing required argument: namespace");
}
this.opts = $.extend({
remote: null, // URL for GET/PUT, ajax options, or callback
defaults: {}, // default value if no data is found in localStorage
commitDelay: 500, // commit changes after 0.5 seconds of inactivity
createParents: true, // set() creates missing intermediate parent objects for children
maxCommitDelay: 3000, // commit changes max. 3 seconds after first change
pushDelay: 5000, // push commits after 5 seconds of inactivity
maxPushDelay: 30000, // push commits max. 30 seconds after first change
storage: window.localStorage,
// Default debugLevel is set to 1 by `grunt build`:
debugLevel: 1, // 0:quiet, 1:normal, 2:verbose
// Events
change: $.noop,
commit: $.noop,
conflict: $.noop,
error: $.noop,
pull: $.noop,
push: $.noop,
update: $.noop
}, opts);
this._checkTimer = null;
this.namespace = namespace;
this.storage = this.opts.storage;
this._data = this.opts.defaults;
this.opts = $.extend(
{
remote: null, // URL for GET/PUT, ajax options, or callback
defaults: {}, // default value if no data is found in localStorage
commitDelay: 500, // commit changes after 0.5 seconds of inactivity
createParents: true, // set() creates missing intermediate parent objects for children
maxCommitDelay: 3000, // commit changes max. 3 seconds after first change
pushDelay: 5000, // push commits after 5 seconds of inactivity
maxPushDelay: 30000, // push commits max. 30 seconds after first change
storage: window.localStorage,
// Default debugLevel is set to 1 by `grunt build`:
debugLevel: 1, // 0:quiet, 1:normal, 2:verbose
// Events
change: $.noop,
commit: $.noop,
conflict: $.noop,
error: $.noop,
pull: $.noop,
push: $.noop,
update: $.noop,
},
opts
);
this._checkTimer = null;
this.namespace = namespace;
this.storage = this.opts.storage;
this._data = this.opts.defaults;
this.offline = undefined;
this.phase = null;
this.uncommittedSince = null;
this.unpushedSince = null;
this.lastUpdate = 0;
this.lastPull = 0;
this.commitCount = 0;
this.pushCount = 0;
this.lastModified = stamp;
this.offline = undefined;
this.phase = null;
this.uncommittedSince = null;
this.unpushedSince = null;
this.lastUpdate = 0;
this.lastPull = 0;
this.commitCount = 0;
this.pushCount = 0;
this.lastModified = stamp;
this.ready = dfd.promise;
this.ready = dfd.promise;
// _data contains the default value. Now load from persistent storage if any
prevValue = this.storage ? this.storage.getItem(this.namespace) : null;
// _data contains the default value. Now load from persistent storage if any
prevValue = this.storage ? this.storage.getItem(this.namespace) : null;
if ( this.opts.remote ) {
// Try to pull, then resolve
this.pull().done(function() {
// self.debug("init from remote", this._data);
self.offline = false;
dfd.resolve();
}).fail(function() {
self.offline = true;
if ( prevValue != null ) {
console.warn(self + ": could not init from remote; falling back to storage.");
// self._data = JSON.parse(prevValue);
self._data = $.extend({}, self.opts.defaults, JSON.parse(prevValue));
} else {
console.warn(self + ": could not init from remote; falling back default.");
}
dfd.resolve();
});
} else if ( prevValue != null ) {
this.update();
// We still extend from opts.defaults, in case some fields where missing
this._data = $.extend({}, this.opts.defaults, this._data);
// this.debug("init from storage", this._data);
dfd.resolve();
// this.lastUpdate = stamp;
// this.setDirty();
} else {
// this.debug("init to default", this._data);
dfd.resolve();
}
};
if (this.opts.remote) {
// Try to pull, then resolve
this.pull()
.done(function() {
// self.debug("init from remote", this._data);
self.offline = false;
dfd.resolve();
})
.fail(function() {
self.offline = true;
if (prevValue != null) {
console.warn(
self + ": could not init from remote; falling back to storage."
);
// self._data = JSON.parse(prevValue);
self._data = $.extend(
{},
self.opts.defaults,
JSON.parse(prevValue)
);
} else {
console.warn(
self + ": could not init from remote; falling back default."
);
}
dfd.resolve();
});
} else if (prevValue != null) {
this.update();
// We still extend from opts.defaults, in case some fields where missing
this._data = $.extend({}, this.opts.defaults, this._data);
// this.debug("init from storage", this._data);
dfd.resolve();
// this.lastUpdate = stamp;
// this.setDirty();
} else {
// this.debug("init to default", this._data);
dfd.resolve();
}
};
window.PersistentObject.prototype = {
/** @type {string} */
version: "1.1.0", // Set to semver by 'grunt release'
window.PersistentObject.prototype = {
/** @type {string} */
version: "1.2.0", // Set to semver by 'grunt release'
/* Trigger commit/push according to current settings. */
_invalidate: function(hint, deferredCall) {
var self = this,
prevChange = this.lastModified,
now = _getNow(),
nextCommit = 0,
nextPush = 0,
nextCheck = 0;
/* Trigger commit/push according to current settings. */
_invalidate: function(hint, deferredCall) {
var self = this,
prevChange = this.lastModified,
now = Date.now(),
nextCommit = 0,
nextPush = 0,
nextCheck = 0;
if ( this._checkTimer ) {
clearTimeout(this._checkTimer);
this._checkTimer = null;
}
if (this._checkTimer) {
clearTimeout(this._checkTimer);
this._checkTimer = null;
}
if ( !deferredCall ) {
// this.debug("_invalidate(" + hint + ")");
this.lastModified = now;
if ( !this.uncommittedSince ) { this.uncommittedSince = now; }
if ( !this.unpushedSince ) { this.unpushedSince = now; }
this.opts.change(hint);
} else {
this.debug("_invalidate() recursive");
}
if (!deferredCall) {
// this.debug("_invalidate(" + hint + ")");
this.lastModified = now;
if (!this.uncommittedSince) {
this.uncommittedSince = now;
}
if (!this.unpushedSince) {
this.unpushedSince = now;
}
this.opts.change(hint);
} else {
this.debug("_invalidate() recursive");
}
if ( this.storage ) {
// If we came here by a deferred timer (or delay is 0), commit
// immedialtely
if ( ((now - prevChange) >= this.opts.commitDelay) ||
((now - this.uncommittedSince) >= this.opts.maxCommitDelay) ) {
if (this.storage) {
// If we came here by a deferred timer (or delay is 0), commit
// immedialtely
if (
now - prevChange >= this.opts.commitDelay ||
now - this.uncommittedSince >= this.opts.maxCommitDelay
) {
this.debug(
"_invalidate(): force commit",
now - prevChange >= this.opts.commitDelay,
now - this.uncommittedSince >= this.opts.maxCommitDelay
);
this.commit();
} else {
// otherwise schedule next check
nextCommit = Math.min(
now + this.opts.commitDelay + 1,
this.uncommittedSince + this.opts.maxCommitDelay + 1
);
}
}
this.debug("_invalidate(): force commit",
((now - prevChange) >= this.opts.commitDelay),
((now - this.uncommittedSince) >= this.opts.maxCommitDelay));
this.commit();
} else {
// otherwise schedule next check
nextCommit = Math.min(
now + this.opts.commitDelay + 1,
this.uncommittedSince + this.opts.maxCommitDelay + 1 );
}
}
if (this.opts.remote) {
if (
now - prevChange >= this.opts.pushDelay ||
now - this.unpushedSince >= this.opts.maxPushDelay
) {
this.debug(
"_invalidate(): force push",
now - prevChange >= this.opts.pushDelay,
now - this.unpushedSince >= this.opts.maxPushDelay
);
this.push();
} else {
nextPush = Math.min(
now + this.opts.pushDelay + 1,
this.unpushedSince + this.opts.maxPushDelay + 1
);
}
}
if (nextCommit || nextPush) {
nextCheck = Math.min(nextCommit || MAX_INT, nextPush || MAX_INT);
// this.debug("Defer update:", nextCheck - now)
this.debug(
"_invalidate(" + hint + ") defer by " + (nextCheck - now) + "ms"
);
this._checkTimer = setTimeout(function() {
self._checkTimer = null; // no need to call clearTimeout in the handler...
self._invalidate.call(self, null, true);
}, nextCheck - now);
}
},
/* Load data from localStorage. */
_update: function(objData) {
if (this.uncommittedSince) {
console.warn("Updating an uncommitted object.");
if (this.conflict(objData, this._data) === false) {
return;
}
}
this._data = objData;
// this.dirty = false;
this.lastUpdate = Date.now();
},
/* Return readable string representation for this instance. */
toString: function() {
return "PersistentObject('" + this.namespace + "')";
},
/* Log to console if opts.debugLevel >= 2 */
debug: function() {
if (this.opts.debugLevel >= 2) {
Array.prototype.unshift.call(arguments, this.toString());
console.log.apply(console, arguments);
}
},
/* Log to console if opts.debugLevel >= 1 */
log: function() {
if (this.opts.debugLevel >= 1) {
Array.prototype.unshift.call(arguments, this.toString());
console.log.apply(console, arguments);
}
},
/** Return true if there are uncommited or unpushed modifications. */
isDirty: function() {
return !!(
(this.storage && this.uncommittedSince) ||
(this.opts.remote && this.unpushedSince)
);
},
/** Return true if initial pull has completed. */
isReady: function() {
return this.ready.state !== "pending";
},
/** Access object property (`key` supports dot notation). */
get: function(key) {
var i,
cur = this._data,
parts = ("" + key) // convert to string
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties
.replace(/^\./, "") // strip a leading dot
.split(".");
if ( this.opts.remote ) {
if ( ((now - prevChange) >= this.opts.pushDelay) ||
((now - this.unpushedSince) >= this.opts.maxPushDelay) ) {
// NOTE: this is slower (tested on Safari):
// return key.split(".").reduce(function(prev, curr) {
// return prev[curr];
// }, this._data);
this.debug("_invalidate(): force push", ((now - prevChange) >= this.opts.pushDelay),
((now - this.unpushedSince) >= this.opts.maxPushDelay));
this.push();
} else {
nextPush = Math.min(
now + this.opts.pushDelay + 1,
this.unpushedSince + this.opts.maxPushDelay + 1 );
}
}
if ( nextCommit || nextPush ) {
nextCheck = Math.min(
nextCommit || MAX_INT,
nextPush || MAX_INT);
// this.debug("Defer update:", nextCheck - now)
this.debug("_invalidate(" + hint + ") defer by " + (nextCheck - now) + "ms" );
this._checkTimer = setTimeout(function() {
self._checkTimer = null; // no need to call clearTimeout in the handler...
self._invalidate.call(self, null, true);
}, nextCheck - now);
}
},
/* Load data from localStorage. */
_update: function(objData) {
if ( this.uncommittedSince ) {
console.warn("Updating an uncommitted object.");
if ( this.conflict(objData, this._data) === false ) {
return;
}
}
this._data = objData;
// this.dirty = false;
this.lastUpdate = _getNow();
},
/* Return readable string representation for this instance. */
toString: function() {
return "PersistentObject('" + this.namespace + "')";
},
/* Log to console if opts.debugLevel >= 2 */
debug: function() {
if ( this.opts.debugLevel >= 2 ) {
Array.prototype.unshift.call(arguments, this.toString());
console.log.apply(window.console, arguments);
}
},
/* Log to console if opts.debugLevel >= 1 */
log: function() {
if ( this.opts.debugLevel >= 1 ) {
Array.prototype.unshift.call(arguments, this.toString());
console.log.apply(window.console, arguments);
}
},
/** Return true if there are uncommited or unpushed modifications. */
isDirty: function() {
return !!(
(this.storage && this.uncommittedSince) ||
(this.opts.remote && this.unpushedSince));
},
/** Return true if initial pull has completed. */
isReady: function() {
return this.ready.state !== "pending";
},
/** Access object property (`key` supports dot notation). */
get: function(key) {
var i,
cur = this._data,
parts = ("" + key) // convert to string
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties
.replace(/^\./, "") // strip a leading dot
.split(".");
for (i = 0; i < parts.length; i++) {
cur = cur[parts[i]];
if (cur === undefined && i < parts.length - 1) {
error(
this +
": Property '" +
key +
"' could not be accessed because parent '" +
parts.slice(0, i + 1).join(".") +
"' does not exist"
);
}
}
return cur;
},
/* Modify object property and set the `dirty` flag (`key` supports dot notation). */
_setOrRemove: function(key, value, remove) {
var i,
parent,
cur = this._data,
parts = ("" + key) // convert to string
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties
.replace(/^\./, "") // strip a leading dot
.split("."),
lastPart = parts.pop();
// NOTE: this is slower (tested on Safari):
// return key.split(".").reduce(function(prev, curr) {
// return prev[curr];
// }, this._data);
for (i = 0; i < parts.length; i++) {
parent = cur;
cur = parent[parts[i]];
// Create intermediate parent objects properties if required
if (cur === undefined) {
if (this.opts.createParents) {
this.debug("Creating intermediate parent '" + parts[i] + "'");
cur = parent[parts[i]] = {};
} else {
error(
this +
": Property '" +
key +
"' could not be set because parent '" +
parts.slice(0, i + 1).join(".") +
"' does not exist"
);
}
}
}
if (cur[lastPart] !== value) {
if (remove === true) {
delete cur[lastPart];
this._invalidate("remove");
} else {
cur[lastPart] = value;
this._invalidate("set");
}
}
},
/** Modify object property and set the `dirty` flag (`key` supports dot notation). */
set: function(key, value) {
return this._setOrRemove(key, value, false);
},
/** Delete object property and set the `dirty` flag (`key` supports dot notation). */
remove: function(key) {
return this._setOrRemove(key, undefined, true);
},
/** Replace data object with a new instance. */
reset: function(obj) {
this._data = obj || {};
this._invalidate("reset");
},
/** Flag object as modified, so that commit / push will be scheduled. */
setDirty: function(flag) {
if (flag === false) {
} else {
this._invalidate("explicit");
}
},
/** Load data from localStorage. */
update: function() {
if (this.phase) {
error(
this + ": Trying to update while '" + this.phase + "' is pending."
);
}
if (this.opts.debugLevel >= 2 && console.time) {
console.time(this + ".update");
}
var data = this.storage.getItem(this.namespace);
data = JSON.parse(data);
this._update(data);
if (this.opts.debugLevel >= 2 && console.time) {
console.timeEnd(this + ".update");
}
},
/** Write data to localStorage. */
commit: function() {
var data;
if (this.phase) {
error(
this + ": Trying to commit while '" + this.phase + "' is pending."
);
}
if (this.opts.debugLevel >= 2 && console.time) {
console.time(this + ".commit");
}
// try { data = JSON.stringify(this._data); } catch(e) { }
data = JSON.stringify(this._data);
this.storage.setItem(this.namespace, data);
// this.dirty = false;
this.uncommittedSince = null;
this.commitCount += 1;
// this.lastCommit = Date.now();
if (this.opts.debugLevel >= 2 && console.time) {
console.timeEnd(this + ".commit");
}
return data;
},
/** Download, then update data from the cloud. */
pull: function() {
var self = this;
for (i = 0; i < parts.length; i++) {
cur = cur[parts[i]];
if ( cur === undefined && i < (parts.length - 1) ) {
$.error(this + ": Property '" + key + "' could not be accessed because parent '" +
parts.slice(0, i + 1).join(".") + "' does not exist");
}
}
return cur;
},
/* Modify object property and set the `dirty` flag (`key` supports dot notation). */
_setOrRemove: function(key, value, remove) {
var i, parent,
cur = this._data,
parts = ("" + key) // convert to string
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties
.replace(/^\./, "") // strip a leading dot
.split("."),
lastPart = parts.pop();
if (this.phase) {
error(this + ": Trying to pull while '" + this.phase + "' is pending.");
}
if (this.opts.debugLevel >= 2 && console.time) {
console.time(this + ".pull");
}
this.phase = "pull";
// return $.get(this.opts.remote, function(objData) {
return $.ajax({
type: "GET",
url: this.opts.remote,
})
.done(function(objData) {
var strData = objData;
if ($.isArray(objData) || $.isPlainObject(objData)) {
strData = JSON.stringify(objData);
} else {
objData = JSON.parse(objData);
}
self.storage.setItem(self.namespace, strData);
self._update(objData);
self.lastPull = Date.now();
})
.fail(function() {
self.opts.error(arguments);
})
.always(function() {
self.phase = null;
if (self.opts.debugLevel >= 2 && console.time) {
console.timeEnd(self + ".pull");
}
});
},
/** Commit, then upload data to the cloud. */
push: function() {
var self = this,
data = this.commit();
for (i = 0; i < parts.length; i++) {
parent = cur;
cur = parent[parts[i]];
// Create intermediate parent objects properties if required
if ( cur === undefined ) {
if ( this.opts.createParents ) {
this.debug("Creating intermediate parent '" + parts[i] + "'");
cur = parent[parts[i]] = {};
} else {
$.error(this + ": Property '" + key + "' could not be set because parent '" +
parts.slice(0, i + 1).join(".") + "' does not exist");
}
}
}
if ( cur[lastPart] !== value ) {
if ( remove === true ) {
delete cur[lastPart];
this._invalidate("remove");
} else {
cur[lastPart] = value;
this._invalidate("set");
}
}
},
/** Modify object property and set the `dirty` flag (`key` supports dot notation). */
set: function(key, value) {
return this._setOrRemove(key, value, false);
},
/** Delete object property and set the `dirty` flag (`key` supports dot notation). */
remove: function(key) {
return this._setOrRemove(key, undefined, true);
},
/** Replace data object with a new instance. */
reset: function(obj) {
this._data = obj || {};
this._invalidate("reset");
},
/** Flag object as modified, so that commit / push will be scheduled. */
setDirty: function(flag) {
if ( flag === false ) {
} else {
this._invalidate("explicit");
}
},
/** Load data from localStorage. */
update: function() {
if ( this.phase ) {
$.error(this + ": Trying to update while '" + this.phase + "' is pending.");
}
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".update"); }
var data = this.storage.getItem(this.namespace);
data = JSON.parse(data);
this._update(data);
if ( this.opts.debugLevel >= 2 && console.time ) { console.timeEnd(this + ".update"); }
},
/** Write data to localStorage. */
commit: function() {
var data;
if ( this.phase ) {
$.error(this + ": Trying to commit while '" + this.phase + "' is pending.");
}
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".commit"); }
// try { data = JSON.stringify(this._data); } catch(e) { }
data = JSON.stringify(this._data);
this.storage.setItem(this.namespace, data);
// this.dirty = false;
this.uncommittedSince = null;
this.commitCount += 1;
// this.lastCommit = _getNow();
if ( this.opts.debugLevel >= 2 && console.time ) { console.timeEnd(this + ".commit"); }
return data;
},
/** Download, then update data from the cloud. */
pull: function() {
var self = this;
if (this.phase) {
error(this + ": Trying to push while '" + this.phase + "' is pending.");
}
if (this.opts.debugLevel >= 2 && console.time) {
console.time(self + ".push");
}
this.phase = "push";
if (!this.opts.remote) {
error(this + ": Missing remote option");
}
return $.ajax({
type: "PUT",
url: this.opts.remote,
data: data,
})
.done(function() {
// console.log("PUT", arguments);
// self.lastPush = Date.now();
self.unpushedSince = null;
self.pushCount += 1;
})
.fail(function() {
self.opts.error(arguments);
})
.always(function() {
self.phase = null;
if (self.opts.debugLevel >= 2 && console.time) {
console.timeEnd(self + ".push");
}
});
},
/** Read data properties from form input elements with the same name.
* Supports elements of input (type: text, radio, checkbox), textarea,
* and select.
*/
readFromForm: function(form, options) {
var self = this,
$form = $(form),
opts = $.extend(
{
addNew: false,
coerce: true, // convert single checkboxes to bool (instead value)
trim: true,
},
options
);
if ( this.phase ) {
$.error(this + ": Trying to pull while '" + this.phase + "' is pending.");
}
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".pull"); }
this.phase = "pull";
// return $.get(this.opts.remote, function(objData) {
return $.ajax({
type: "GET",
url: this.opts.remote
}).done(function(objData) {
var strData = objData;
if ( $.isArray(objData) || $.isPlainObject(objData) ) {
strData = JSON.stringify(objData);
} else {
objData = JSON.parse(objData);
}
self.storage.setItem(self.namespace, strData);
self._update(objData);
self.lastPull = _getNow();
}).fail(function() {
self.opts.error(arguments);
}).always(function() {
self.phase = null;
if ( self.opts.debugLevel >= 2 && console.time ) { console.timeEnd(self + ".pull"); }
});
},
/** Commit, then upload data to the cloud. */
push: function() {
var self = this,
data = this.commit();
if (opts.addNew) {
$form.find("[name]").each(function() {
var name = $(this).attr("name");
if (self._data[name] === undefined) {
self.debug("readFromForm: add field '" + name + "'");
self._data[name] = null;
}
});
}
$.each(this._data, function(k, v) {
var val,
$input = $form.find("[name='" + k + "']"),
type = $input.attr("type");
if ( this.phase ) {
$.error(this + ": Trying to push while '" + this.phase + "' is pending.");
}
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(self + ".push"); }
this.phase = "push";
if ( !this.opts.remote ) { $.error(this + ": Missing remote option"); }
return $.ajax({
type: "PUT",
url: this.opts.remote,
data: data
}).done(function() {
// console.log("PUT", arguments);
// self.lastPush = _getNow();
self.unpushedSince = null;
self.pushCount += 1;
}).fail(function() {
self.opts.error(arguments);
}).always(function() {
self.phase = null;
if ( self.opts.debugLevel >= 2 && console.time ) { console.timeEnd(self + ".push"); }
});
},
/** Read data properties from form input elements with the same name.
* Supports elements of input (type: text, radio, checkbox), textarea,
* and select.
*/
readFromForm: function(form, options) {
var self = this,
$form = $(form),
opts = $.extend({
addNew: false,
coerce: true, // convert single checkboxes to bool (instead value)
trim: true
}, options);
if (!$input.length) {
self.debug("readFromForm: field not found: '" + k + "'");
return;
}
if (type === "radio") {
val = $input.filter(":checked").val();
} else if (type === "checkbox" && $input.length === 1) {
val = !!$input.filter(":checked").length;
} else if (type === "checkbox" && $input.length > 1) {
val = [];
$input.filter(":checked").each(function() {
val.push($(this).val());
});
} else {
val = $input.val();
if (opts.trim && typeof val === "string") {
val = $.trim(val);
}
}
// console.log("readFromForm: val(" + k + "): '" + val + "'");
self.set(k, val);
});
// console.log("readFromForm: '" + this + "'", this._data);
},
/** Write data to form elements with the same name.
*/
writeToForm: function(form, options) {
var $form = $(form);
if ( opts.addNew ) {
$form.find("[name]").each(function() {
var name = $(this).attr("name");
if ( self._data[name] === undefined ) {
self.debug("readFromForm: add field '" + name + "'");
self._data[name] = null;
}
});
}
$.each(this._data, function(k, v) {
var val,
$input = $form.find("[name='" + k + "']"),
type = $input.attr("type");
$.each(this._data, function(k, v) {
var $input = $form.find("[name='" + k + "']"),
type = $input.attr("type");
if ( !$input.length ) {
self.debug("readFromForm: field not found: '" + k + "'");
return;
}
if ( type === "radio") {
val = $input.filter(":checked").val();
} else if ( type === "checkbox" && $input.length === 1 ) {
val = !!$input.filter(":checked").length;
} else if ( type === "checkbox" && $input.length > 1 ) {
val = [];
$input.filter(":checked").each(function() {
val.push($(this).val());
});
} else {
val = $input.val();
if ( opts.trim && typeof val === "string" ) { val = $.trim(val); }
}
// console.log("readFromForm: val(" + k + "): '" + val + "'");
self.set(k, val);
});
// console.log("readFromForm: '" + this + "'", this._data);
},
/** Write data to form elements with the same name.
*/
writeToForm: function(form, options) {
var $form = $(form);
$.each(this._data, function(k, v) {
var $input = $form.find("[name='" + k + "']"),
type = $input.attr("type");
if ( $input.length ) {
if ( type === "radio" ) {
$input.filter("[value='" + v + "']").prop("checked", true);
} else if ( type === "checkbox" ) {
if ( $input.length === 1 ) {
$input.prop("checked", !!v);
} else {
// multi-value checkbox
$input.each(function() {
$(this).prop("checked", $.isArray(v) ?
$.inArray(this.value, v) >= 0 : this.value === v);
});
}
} else if ( $input.is("select") ) {
// listbox
$input.find("option").each(function() {
$(this).prop("selected", $.isArray(v) ?
$.inArray(this.value, v) >= 0 : this.value === v);
});
} else {
$input.val(v);
}
}
});
}
};
// -----------------------------------------------------------------------------
}(jQuery, window, document));
if ($input.length) {
if (type === "radio") {
$input.filter("[value='" + v + "']").prop("checked", true);
} else if (type === "checkbox") {
if ($input.length === 1) {
$input.prop("checked", !!v);
} else {
// multi-value checkbox
$input.each(function() {
$(this).prop(
"checked",
$.isArray(v)
? $.inArray(this.value, v) >= 0
: this.value === v
);
});
}
} else if ($input.is("select")) {
// listbox
$input.find("option").each(function() {
$(this).prop(
"selected",
$.isArray(v) ? $.inArray(this.value, v) >= 0 : this.value === v
);
});
} else {
$input.val(v);
}
}
});
},
};
// -----------------------------------------------------------------------------
// Value returned by `require('persisto')`
return window.PersistentObject;
}); // End of closure

@@ -1,4 +0,4 @@

/*! Persistent Javascript objects and web forms using Web Storage. - v1.1.0 - 2016-08-14 | https://github.com/mar10/persisto | Copyright (c) 2016 Martin Wendt; Licensed MIT */
/*! Persistent JavaScript objects and web forms using Web Storage. - v1.2.0 - 2019-09-07 | https://github.com/mar10/persisto | Copyright (c) 2019 Martin Wendt; Licensed MIT */
!function(a,b,c,d){"use strict";function e(){return(new Date).getTime()}var f=9007199254740991;b.PersistentObject=function(c,f){var g,h=this,i=a.Deferred(),j=e();this instanceof PersistentObject||a.error("Must use 'new' keyword"),"string"!=typeof c&&a.error(this+": Missing required argument: namespace"),this.opts=a.extend({remote:null,defaults:{},commitDelay:500,createParents:!0,maxCommitDelay:3e3,pushDelay:5e3,maxPushDelay:3e4,storage:b.localStorage,debugLevel: 1,change:a.noop,commit:a.noop,conflict:a.noop,error:a.noop,pull:a.noop,push:a.noop,update:a.noop},f),this._checkTimer=null,this.namespace=c,this.storage=this.opts.storage,this._data=this.opts.defaults,this.offline=d,this.phase=null,this.uncommittedSince=null,this.unpushedSince=null,this.lastUpdate=0,this.lastPull=0,this.commitCount=0,this.pushCount=0,this.lastModified=j,this.ready=i.promise,g=this.storage?this.storage.getItem(this.namespace):null,this.opts.remote?this.pull().done(function(){h.offline=!1,i.resolve()}).fail(function(){h.offline=!0,null!=g?(console.warn(h+": could not init from remote; falling back to storage."),h._data=a.extend({},h.opts.defaults,JSON.parse(g))):console.warn(h+": could not init from remote; falling back default."),i.resolve()}):null!=g?(this.update(),this._data=a.extend({},this.opts.defaults,this._data),i.resolve()):i.resolve()},b.PersistentObject.prototype={version:"1.1.0",_invalidate:function(a,b){var c=this,d=this.lastModified,g=e(),h=0,i=0,j=0;this._checkTimer&&(clearTimeout(this._checkTimer),this._checkTimer=null),b?this.debug("_invalidate() recursive"):(this.lastModified=g,this.uncommittedSince||(this.uncommittedSince=g),this.unpushedSince||(this.unpushedSince=g),this.opts.change(a)),this.storage&&(g-d>=this.opts.commitDelay||g-this.uncommittedSince>=this.opts.maxCommitDelay?(this.debug("_invalidate(): force commit",g-d>=this.opts.commitDelay,g-this.uncommittedSince>=this.opts.maxCommitDelay),this.commit()):h=Math.min(g+this.opts.commitDelay+1,this.uncommittedSince+this.opts.maxCommitDelay+1)),this.opts.remote&&(g-d>=this.opts.pushDelay||g-this.unpushedSince>=this.opts.maxPushDelay?(this.debug("_invalidate(): force push",g-d>=this.opts.pushDelay,g-this.unpushedSince>=this.opts.maxPushDelay),this.push()):i=Math.min(g+this.opts.pushDelay+1,this.unpushedSince+this.opts.maxPushDelay+1)),(h||i)&&(j=Math.min(h||f,i||f),this.debug("_invalidate("+a+") defer by "+(j-g)+"ms"),this._checkTimer=setTimeout(function(){c._checkTimer=null,c._invalidate.call(c,null,!0)},j-g))},_update:function(a){this.uncommittedSince&&(console.warn("Updating an uncommitted object."),this.conflict(a,this._data)===!1)||(this._data=a,this.lastUpdate=e())},toString:function(){return"PersistentObject('"+this.namespace+"')"},debug:function(){this.opts.debugLevel>=2&&(Array.prototype.unshift.call(arguments,this.toString()),console.log.apply(b.console,arguments))},log:function(){this.opts.debugLevel>=1&&(Array.prototype.unshift.call(arguments,this.toString()),console.log.apply(b.console,arguments))},isDirty:function(){return!!(this.storage&&this.uncommittedSince||this.opts.remote&&this.unpushedSince)},isReady:function(){return"pending"!==this.ready.state},get:function(b){var c,e=this._data,f=(""+b).replace(/\[(\w+)\]/g,".$1").replace(/^\./,"").split(".");for(c=0;c<f.length;c++)e=e[f[c]],e===d&&c<f.length-1&&a.error(this+": Property '"+b+"' could not be accessed because parent '"+f.slice(0,c+1).join(".")+"' does not exist");return e},_setOrRemove:function(b,c,e){var f,g,h=this._data,i=(""+b).replace(/\[(\w+)\]/g,".$1").replace(/^\./,"").split("."),j=i.pop();for(f=0;f<i.length;f++)g=h,h=g[i[f]],h===d&&(this.opts.createParents?(this.debug("Creating intermediate parent '"+i[f]+"'"),h=g[i[f]]={}):a.error(this+": Property '"+b+"' could not be set because parent '"+i.slice(0,f+1).join(".")+"' does not exist"));h[j]!==c&&(e===!0?(delete h[j],this._invalidate("remove")):(h[j]=c,this._invalidate("set")))},set:function(a,b){return this._setOrRemove(a,b,!1)},remove:function(a){return this._setOrRemove(a,d,!0)},reset:function(a){this._data=a||{},this._invalidate("reset")},setDirty:function(a){a===!1||this._invalidate("explicit")},update:function(){this.phase&&a.error(this+": Trying to update while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&console.time&&console.time(this+".update");var b=this.storage.getItem(this.namespace);b=JSON.parse(b),this._update(b),this.opts.debugLevel>=2&&console.time&&console.timeEnd(this+".update")},commit:function(){var b;return this.phase&&a.error(this+": Trying to commit while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&console.time&&console.time(this+".commit"),b=JSON.stringify(this._data),this.storage.setItem(this.namespace,b),this.uncommittedSince=null,this.commitCount+=1,this.opts.debugLevel>=2&&console.time&&console.timeEnd(this+".commit"),b},pull:function(){var b=this;return this.phase&&a.error(this+": Trying to pull while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&console.time&&console.time(this+".pull"),this.phase="pull",a.ajax({type:"GET",url:this.opts.remote}).done(function(c){var d=c;a.isArray(c)||a.isPlainObject(c)?d=JSON.stringify(c):c=JSON.parse(c),b.storage.setItem(b.namespace,d),b._update(c),b.lastPull=e()}).fail(function(){b.opts.error(arguments)}).always(function(){b.phase=null,b.opts.debugLevel>=2&&console.time&&console.timeEnd(b+".pull")})},push:function(){var b=this,c=this.commit();return this.phase&&a.error(this+": Trying to push while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&console.time&&console.time(b+".push"),this.phase="push",this.opts.remote||a.error(this+": Missing remote option"),a.ajax({type:"PUT",url:this.opts.remote,data:c}).done(function(){b.unpushedSince=null,b.pushCount+=1}).fail(function(){b.opts.error(arguments)}).always(function(){b.phase=null,b.opts.debugLevel>=2&&console.time&&console.timeEnd(b+".push")})},readFromForm:function(b,c){var e=this,f=a(b),g=a.extend({addNew:!1,coerce:!0,trim:!0},c);g.addNew&&f.find("[name]").each(function(){var b=a(this).attr("name");e._data[b]===d&&(e.debug("readFromForm: add field '"+b+"'"),e._data[b]=null)}),a.each(this._data,function(b,c){var d,h=f.find("[name='"+b+"']"),i=h.attr("type");return h.length?("radio"===i?d=h.filter(":checked").val():"checkbox"===i&&1===h.length?d=!!h.filter(":checked").length:"checkbox"===i&&h.length>1?(d=[],h.filter(":checked").each(function(){d.push(a(this).val())})):(d=h.val(),g.trim&&"string"==typeof d&&(d=a.trim(d))),void e.set(b,d)):void e.debug("readFromForm: field not found: '"+b+"'")})},writeToForm:function(b,c){var d=a(b);a.each(this._data,function(b,c){var e=d.find("[name='"+b+"']"),f=e.attr("type");e.length&&("radio"===f?e.filter("[value='"+c+"']").prop("checked",!0):"checkbox"===f?1===e.length?e.prop("checked",!!c):e.each(function(){a(this).prop("checked",a.isArray(c)?a.inArray(this.value,c)>=0:this.value===c)}):e.is("select")?e.find("option").each(function(){a(this).prop("selected",a.isArray(c)?a.inArray(this.value,c)>=0:this.value===c)}):e.val(c))})}}}(jQuery,window,document);
!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){"use strict";var b=9007199254740991,c=window.console,d=a.error;return window.PersistentObject=function(b,e){var f,g=this,h=a.Deferred(),i=Date.now();this instanceof PersistentObject||d("Must use 'new' keyword"),"string"!=typeof b&&d(this+": Missing required argument: namespace"),this.opts=a.extend({remote:null,defaults:{},commitDelay:500,createParents:!0,maxCommitDelay:3e3,pushDelay:5e3,maxPushDelay:3e4,storage:window.localStorage,debugLevel: 1,change:a.noop,commit:a.noop,conflict:a.noop,error:a.noop,pull:a.noop,push:a.noop,update:a.noop},e),this._checkTimer=null,this.namespace=b,this.storage=this.opts.storage,this._data=this.opts.defaults,this.offline=void 0,this.phase=null,this.uncommittedSince=null,this.unpushedSince=null,this.lastUpdate=0,this.lastPull=0,this.commitCount=0,this.pushCount=0,this.lastModified=i,this.ready=h.promise,f=this.storage?this.storage.getItem(this.namespace):null,this.opts.remote?this.pull().done(function(){g.offline=!1,h.resolve()}).fail(function(){g.offline=!0,null!=f?(c.warn(g+": could not init from remote; falling back to storage."),g._data=a.extend({},g.opts.defaults,JSON.parse(f))):c.warn(g+": could not init from remote; falling back default."),h.resolve()}):null!=f?(this.update(),this._data=a.extend({},this.opts.defaults,this._data),h.resolve()):h.resolve()},window.PersistentObject.prototype={version:"1.2.0",_invalidate:function(a,c){var d=this,e=this.lastModified,f=Date.now(),g=0,h=0,i=0;this._checkTimer&&(clearTimeout(this._checkTimer),this._checkTimer=null),c?this.debug("_invalidate() recursive"):(this.lastModified=f,this.uncommittedSince||(this.uncommittedSince=f),this.unpushedSince||(this.unpushedSince=f),this.opts.change(a)),this.storage&&(f-e>=this.opts.commitDelay||f-this.uncommittedSince>=this.opts.maxCommitDelay?(this.debug("_invalidate(): force commit",f-e>=this.opts.commitDelay,f-this.uncommittedSince>=this.opts.maxCommitDelay),this.commit()):g=Math.min(f+this.opts.commitDelay+1,this.uncommittedSince+this.opts.maxCommitDelay+1)),this.opts.remote&&(f-e>=this.opts.pushDelay||f-this.unpushedSince>=this.opts.maxPushDelay?(this.debug("_invalidate(): force push",f-e>=this.opts.pushDelay,f-this.unpushedSince>=this.opts.maxPushDelay),this.push()):h=Math.min(f+this.opts.pushDelay+1,this.unpushedSince+this.opts.maxPushDelay+1)),(g||h)&&(i=Math.min(g||b,h||b),this.debug("_invalidate("+a+") defer by "+(i-f)+"ms"),this._checkTimer=setTimeout(function(){d._checkTimer=null,d._invalidate.call(d,null,!0)},i-f))},_update:function(a){this.uncommittedSince&&(c.warn("Updating an uncommitted object."),this.conflict(a,this._data)===!1)||(this._data=a,this.lastUpdate=Date.now())},toString:function(){return"PersistentObject('"+this.namespace+"')"},debug:function(){this.opts.debugLevel>=2&&(Array.prototype.unshift.call(arguments,this.toString()),c.log.apply(c,arguments))},log:function(){this.opts.debugLevel>=1&&(Array.prototype.unshift.call(arguments,this.toString()),c.log.apply(c,arguments))},isDirty:function(){return!!(this.storage&&this.uncommittedSince||this.opts.remote&&this.unpushedSince)},isReady:function(){return"pending"!==this.ready.state},get:function(a){var b,c=this._data,e=(""+a).replace(/\[(\w+)\]/g,".$1").replace(/^\./,"").split(".");for(b=0;b<e.length;b++)c=c[e[b]],void 0===c&&b<e.length-1&&d(this+": Property '"+a+"' could not be accessed because parent '"+e.slice(0,b+1).join(".")+"' does not exist");return c},_setOrRemove:function(a,b,c){var e,f,g=this._data,h=(""+a).replace(/\[(\w+)\]/g,".$1").replace(/^\./,"").split("."),i=h.pop();for(e=0;e<h.length;e++)f=g,g=f[h[e]],void 0===g&&(this.opts.createParents?(this.debug("Creating intermediate parent '"+h[e]+"'"),g=f[h[e]]={}):d(this+": Property '"+a+"' could not be set because parent '"+h.slice(0,e+1).join(".")+"' does not exist"));g[i]!==b&&(c===!0?(delete g[i],this._invalidate("remove")):(g[i]=b,this._invalidate("set")))},set:function(a,b){return this._setOrRemove(a,b,!1)},remove:function(a){return this._setOrRemove(a,void 0,!0)},reset:function(a){this._data=a||{},this._invalidate("reset")},setDirty:function(a){a===!1||this._invalidate("explicit")},update:function(){this.phase&&d(this+": Trying to update while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&c.time&&c.time(this+".update");var a=this.storage.getItem(this.namespace);a=JSON.parse(a),this._update(a),this.opts.debugLevel>=2&&c.time&&c.timeEnd(this+".update")},commit:function(){var a;return this.phase&&d(this+": Trying to commit while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&c.time&&c.time(this+".commit"),a=JSON.stringify(this._data),this.storage.setItem(this.namespace,a),this.uncommittedSince=null,this.commitCount+=1,this.opts.debugLevel>=2&&c.time&&c.timeEnd(this+".commit"),a},pull:function(){var b=this;return this.phase&&d(this+": Trying to pull while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&c.time&&c.time(this+".pull"),this.phase="pull",a.ajax({type:"GET",url:this.opts.remote}).done(function(c){var d=c;a.isArray(c)||a.isPlainObject(c)?d=JSON.stringify(c):c=JSON.parse(c),b.storage.setItem(b.namespace,d),b._update(c),b.lastPull=Date.now()}).fail(function(){b.opts.error(arguments)}).always(function(){b.phase=null,b.opts.debugLevel>=2&&c.time&&c.timeEnd(b+".pull")})},push:function(){var b=this,e=this.commit();return this.phase&&d(this+": Trying to push while '"+this.phase+"' is pending."),this.opts.debugLevel>=2&&c.time&&c.time(b+".push"),this.phase="push",this.opts.remote||d(this+": Missing remote option"),a.ajax({type:"PUT",url:this.opts.remote,data:e}).done(function(){b.unpushedSince=null,b.pushCount+=1}).fail(function(){b.opts.error(arguments)}).always(function(){b.phase=null,b.opts.debugLevel>=2&&c.time&&c.timeEnd(b+".push")})},readFromForm:function(b,c){var d=this,e=a(b),f=a.extend({addNew:!1,coerce:!0,trim:!0},c);f.addNew&&e.find("[name]").each(function(){var b=a(this).attr("name");void 0===d._data[b]&&(d.debug("readFromForm: add field '"+b+"'"),d._data[b]=null)}),a.each(this._data,function(b,c){var g,h=e.find("[name='"+b+"']"),i=h.attr("type");return h.length?("radio"===i?g=h.filter(":checked").val():"checkbox"===i&&1===h.length?g=!!h.filter(":checked").length:"checkbox"===i&&h.length>1?(g=[],h.filter(":checked").each(function(){g.push(a(this).val())})):(g=h.val(),f.trim&&"string"==typeof g&&(g=a.trim(g))),void d.set(b,g)):void d.debug("readFromForm: field not found: '"+b+"'")})},writeToForm:function(b,c){var d=a(b);a.each(this._data,function(b,c){var e=d.find("[name='"+b+"']"),f=e.attr("type");e.length&&("radio"===f?e.filter("[value='"+c+"']").prop("checked",!0):"checkbox"===f?1===e.length?e.prop("checked",!!c):e.each(function(){a(this).prop("checked",a.isArray(c)?a.inArray(this.value,c)>=0:this.value===c)}):e.is("select")?e.find("option").each(function(){a(this).prop("selected",a.isArray(c)?a.inArray(this.value,c)>=0:this.value===c)}):e.val(c))})}},window.PersistentObject});
//# sourceMappingURL=persisto.min.js.map

@@ -1,2 +0,2 @@

Copyright 2016 Martin Wendt,
Copyright 2016-2017 Martin Wendt,
http://wwWendt.de/

@@ -3,0 +3,0 @@

{
"name": "persisto",
"title": "Persistent Javascript objects and web forms using Web Storage.",
"description": "Persist Javascript objects to localStorage and remote servers.",
"title": "Persistent JavaScript objects and web forms using Web Storage.",
"description": "Persist JavaScript objects to localStorage and remote servers.",
"filename": "dist/persisto.min.js",
"main": "dist/persisto.js",
"version": "1.1.0",
"version": "1.2.0",
"homepage": "https://github.com/mar10/persisto",

@@ -37,16 +37,20 @@ "author": {

"devDependencies": {
"grunt": "~0.4.5",
"eslint": "^5.12.0",
"eslint-config-prettier": "^3.4.0",
"eslint-plugin-prettier": "^3.0.1",
"grunt": "^1.0.4",
"grunt-contrib-clean": "^1.0.0",
"grunt-contrib-compress": "^1.0.0",
"grunt-contrib-concat": "^0.5.1",
"grunt-contrib-connect": "^0.11.2",
"grunt-contrib-copy": "^0.8.2",
"grunt-contrib-concat": "^1.0.0",
"grunt-contrib-connect": "^1.0.0",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-jshint": "^1.0.0",
"grunt-contrib-qunit": "^1.0.1",
"grunt-contrib-uglify": "^0.11.1",
"grunt-contrib-watch": "^0.6.1",
"grunt-jscs": "^2.7.0",
"grunt-saucelabs": "^8.1.1",
"grunt-text-replace": "~0.3.7",
"grunt-yabs": "^0.5.0"
"grunt-contrib-uglify": "^1.0.0",
"grunt-contrib-watch": "^1.1.0",
"grunt-eslint": "^21.0.0",
"grunt-saucelabs": "^9.0.1",
"grunt-text-replace": "^0.4.0",
"grunt-yabs": "^1.1.4",
"prettier": "^1.15.3"
},

@@ -53,0 +57,0 @@ "scripts": {

@@ -1,8 +0,13 @@

# persisto [![GitHub version](https://badge.fury.io/gh/mar10%2Fpersisto.svg)](https://github.com/mar10/persisto/releases/latest) [![Build Status](https://travis-ci.org/mar10/persisto.svg?branch=master)](https://travis-ci.org/mar10/persisto)
# persisto
[![GitHub version](https://img.shields.io/github/release/mar10/persisto.svg)](https://github.com/mar10/persisto/releases/latest)
[![Build Status](https://travis-ci.org/mar10/persisto.svg?branch=master)](https://travis-ci.org/mar10/persisto)
[![npm](https://img.shields.io/npm/dm/persisto.svg)](https://www.npmjs.com/package/persisto)
[![](https://data.jsdelivr.com/v1/package/npm/persisto/badge)](https://www.jsdelivr.com/package/npm/persisto)
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
> Persistent Javascript objects and web forms using Web Storage.
> Persistent JavaScript objects and web forms using Web Storage.
Features
- Persist Javascript objects (`{...}`) to
- Persist JavaScript objects (`{...}`) to
[`localStorage` / `sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API).<br>

@@ -14,3 +19,3 @@ Use the `get()`/`set()` API for direct (even nested) access, hiding the need

(deferred writing appears to be 10-15 times faster) and remote backends.
- Make Javascript objects editable in HTML forms.<br>
- Make JavaScript objects editable in HTML forms.<br>
Common use case: maintain persistent client settings and let users edit them.

@@ -33,7 +38,8 @@ - Optionally synchronize the data with a remote endpoint

[Download the latest persisto.js](https://github.com/mar10/persisto/releases)
or include directly [from CDN](https://www.jsdelivr.com/projects/persisto):
[Download the latest persisto.js](https://github.com/mar10/persisto/releases)
or include directly from CDN: [![](https://data.jsdelivr.com/v1/package/npm/persisto/badge)](https://www.jsdelivr.com/package/npm/persisto) or
[UNPKG](https://unpkg.com/persisto@latest/dist/persisto.min.js):
```html
<script src="//cdn.jsdelivr.net/persisto/1/persisto.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/persisto@1/dist/persisto.min.js"></script>
```

@@ -51,3 +57,3 @@

`store` now contains the data that was stored in `localStorage.mySettings` if
`store` now contains the data that was stored in `localStorage.mySettings` if
present. Otherwise, `store` is initialized to the default values that we

@@ -109,3 +115,3 @@ passed with the `.defaults` option.

<label>Nickname:<input name="nickname" type="text" value="" /></label><br>
<label>Theme:
<label>Theme:
<fieldset>

@@ -122,3 +128,3 @@ <label> <input name="theme" type="radio" value="default" /> Standard </label><br>

Note also that only fields are synchronized, that already existed in the storage
data. Use the `addNew` option if *all* form fields should be evaluated and create
data. Use the `addNew` option if *all* form fields should be evaluated and create
new properties in the store object:

@@ -136,6 +142,6 @@

- Any `PersistentObject` instance is stored as one monolythic JSON string.<br>
*Persisto* deferres and collates these updates, but modifying a single
*Persisto* deferres and collates these updates, but modifying a single
property of a large data object still comes with some overhead.<br>
Splitting data into several `PersistentObject`s may remedy the problem.<br>
But if your data model is more like a table with hundredth's of rows, a
But if your data model is more like a table with hundredth's of rows, a
responsive database backend may be a better choice.

@@ -151,3 +157,3 @@

Arrays are only a special form of plain Javascript objects, so we can store and
Arrays are only a special form of plain JavaScript objects, so we can store and
access them as top level type like this:

@@ -179,6 +185,6 @@

### Performance and Direct Access
### Performance and Direct Access
In general, performance costs of `set()` and `get()` calls should be
neglectable, compared to the resulting synchronization times, but in some cases
In general, performance costs of `set()` and `get()` calls should be
neglectable, compared to the resulting synchronization times, but in some cases
direct access of the internal data object may be preferred.<br>

@@ -206,5 +212,5 @@ In this case modifications must be signalled by a call to `setDirty()`.

// Page reload would prevent the delayed commit from happen, so we force
// Page reload would prevent the delayed commit from happen, so we force
// synchronization:
commit();
store.commit();

@@ -232,3 +238,3 @@ location.reload();

store.ready
).done(function(){

@@ -253,8 +259,9 @@ // Page was loaded and and store has pulled the data from the remote endpoint...

<dd>
Type: <code>int</code>,
Type: <code>int</code>,
default: <code>500</code> milliseconds<br>
Commit changes after 0.5 seconds of inactivity.<br>
This means, after each change, we wait 0.5 more seconds for additional changes
to come in, before the actual commit is executed.<br>
The total delay (first change until actual commit) is limited `maxCommitDelay`.<br>
Commit cached changes to localStorage after 0.5 seconds of inactivity.<br>
After each change, we wait 0.5 more seconds for additional changes to come
in, before the actual commit is executed.<br>
The maximum delay (first change until actual commit) is limited by
`maxCommitDelay`.<br>
Set to <code>0</code> to force synchronous mode.

@@ -264,3 +271,3 @@ </dd>

<dd>
Type: <code>int</code>,
Type: <code>int</code>,
default: <code>1</code><br>

@@ -271,3 +278,3 @@ Verbosity level: 0:quiet, 1:normal, 2:verbose.

<dd>
Type: <code>object</code>,
Type: <code>object</code>,
default: <code>{}</code><br>

@@ -278,17 +285,20 @@ Default value if no data is found in localStorage.

<dd>
Type: <code>int</code>,
Type: <code>int</code>,
default: <code>3000</code> milliseconds<br>
Commit changes max. 3 seconds after first change.
Commit every change max. 3 seconds after it occurred.
</dd>
<dt>maxPushDelay</dt>
<dd>
Type: <code>int</code>,
Type: <code>int</code>,
default: <code>30000</code> milliseconds<br>
Push commits to remote max. 30 seconds after first change.
Push every commit to remote max. 30 seconds after it occurred.
</dd>
<dt>pushDelay</dt>
<dd>
Type: <code>int</code>,
Type: <code>int</code>,
default: <code>5000</code> milliseconds<br>
Push commits to remote after 5 seconds of inactivity.
Push commits to remote after 5 seconds of inactivity.<br>
After each change, we wait 5 more seconds for additional changes to come
in, before the actual push is executed.<br>
The maximum delay (first change until actual push) is limited by `maxPushDelay`.<br>
Set to <code>0</code> to force synchronous mode.

@@ -298,3 +308,3 @@ </dd>

<dd>
Type: <code>string</code>,
Type: <code>string</code>,
default: <code>null</code><br>

@@ -305,3 +315,3 @@ URL for GET/PUT request. Pass `null` to disable remote synchronization.

<dd>
Type: <code>object</code>,
Type: <code>object</code>,
default: <code>window.localStorage</code><br>

@@ -430,2 +440,2 @@ Instance of [Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API).<br>

</dl>
-->
-->
/*!
* persisto.js
*
* Persistent Javascript objects and web forms using Web Storage.
* Persistent JavaScript objects and web forms using Web Storage.
*
* Copyright (c) 2016, Martin Wendt (http://wwWendt.de)
* Copyright (c) 2016-2017, Martin Wendt (http://wwWendt.de)
* Released under the MIT license

@@ -13,468 +13,553 @@ *

;(function($, window, document, undefined) {
(function(factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["jquery"], factory);
} else if (typeof module === "object" && module.exports) {
// Node/CommonJS
module.exports = factory(require("jquery"));
} else {
// Browser globals
factory(jQuery);
}
})(function($) {
"use strict";
/*globals console */
/*******************************************************************************
* Private functions and variables
*/
"use strict";
var MAX_INT = 9007199254740991,
// Allow mangling of some global names:
console = window.console,
error = $.error;
/*******************************************************************************
* Private functions and variables
*/
/**
* A persistent plain object or array.
*/
window.PersistentObject = function(namespace, opts) {
var prevValue,
self = this,
dfd = $.Deferred(),
stamp = Date.now(); // disable warning 'PersistentObject is not defined'
var MAX_INT = 9007199254740991;
/* Return current time stamp. */
function _getNow() {
return new Date().getTime();
}
/**
* A persistent plain object or array.
*/
window.PersistentObject = function(namespace, opts) {
var prevValue,
self = this,
dfd = $.Deferred(),
stamp = _getNow();
/* jshint ignore:start */ // disable warning 'PersistentObject is not defined'
if ( !(this instanceof PersistentObject) ) { $.error("Must use 'new' keyword"); }
/* jshint ignore:start */ if (!(this instanceof PersistentObject)) {
error("Must use 'new' keyword");
}
/* jshint ignore:end */
if ( typeof namespace !== "string" ) {
$.error(this + ": Missing required argument: namespace");
}
if (typeof namespace !== "string") {
error(this + ": Missing required argument: namespace");
}
this.opts = $.extend({
remote: null, // URL for GET/PUT, ajax options, or callback
defaults: {}, // default value if no data is found in localStorage
commitDelay: 500, // commit changes after 0.5 seconds of inactivity
createParents: true, // set() creates missing intermediate parent objects for children
maxCommitDelay: 3000, // commit changes max. 3 seconds after first change
pushDelay: 5000, // push commits after 5 seconds of inactivity
maxPushDelay: 30000, // push commits max. 30 seconds after first change
storage: window.localStorage,
// Default debugLevel is set to 1 by `grunt build`:
debugLevel: 2, // 0:quiet, 1:normal, 2:verbose
// Events
change: $.noop,
commit: $.noop,
conflict: $.noop,
error: $.noop,
pull: $.noop,
push: $.noop,
update: $.noop
}, opts);
this._checkTimer = null;
this.namespace = namespace;
this.storage = this.opts.storage;
this._data = this.opts.defaults;
this.opts = $.extend(
{
remote: null, // URL for GET/PUT, ajax options, or callback
defaults: {}, // default value if no data is found in localStorage
commitDelay: 500, // commit changes after 0.5 seconds of inactivity
createParents: true, // set() creates missing intermediate parent objects for children
maxCommitDelay: 3000, // commit changes max. 3 seconds after first change
pushDelay: 5000, // push commits after 5 seconds of inactivity
maxPushDelay: 30000, // push commits max. 30 seconds after first change
storage: window.localStorage,
// Default debugLevel is set to 1 by `grunt build`:
debugLevel: 2, // 0:quiet, 1:normal, 2:verbose
// Events
change: $.noop,
commit: $.noop,
conflict: $.noop,
error: $.noop,
pull: $.noop,
push: $.noop,
update: $.noop,
},
opts
);
this._checkTimer = null;
this.namespace = namespace;
this.storage = this.opts.storage;
this._data = this.opts.defaults;
this.offline = undefined;
this.phase = null;
this.uncommittedSince = null;
this.unpushedSince = null;
this.lastUpdate = 0;
this.lastPull = 0;
this.commitCount = 0;
this.pushCount = 0;
this.lastModified = stamp;
this.offline = undefined;
this.phase = null;
this.uncommittedSince = null;
this.unpushedSince = null;
this.lastUpdate = 0;
this.lastPull = 0;
this.commitCount = 0;
this.pushCount = 0;
this.lastModified = stamp;
this.ready = dfd.promise;
this.ready = dfd.promise;
// _data contains the default value. Now load from persistent storage if any
prevValue = this.storage ? this.storage.getItem(this.namespace) : null;
// _data contains the default value. Now load from persistent storage if any
prevValue = this.storage ? this.storage.getItem(this.namespace) : null;
if ( this.opts.remote ) {
// Try to pull, then resolve
this.pull().done(function() {
// self.debug("init from remote", this._data);
self.offline = false;
dfd.resolve();
}).fail(function() {
self.offline = true;
if ( prevValue != null ) {
console.warn(self + ": could not init from remote; falling back to storage.");
// self._data = JSON.parse(prevValue);
self._data = $.extend({}, self.opts.defaults, JSON.parse(prevValue));
} else {
console.warn(self + ": could not init from remote; falling back default.");
}
dfd.resolve();
});
} else if ( prevValue != null ) {
this.update();
// We still extend from opts.defaults, in case some fields where missing
this._data = $.extend({}, this.opts.defaults, this._data);
// this.debug("init from storage", this._data);
dfd.resolve();
// this.lastUpdate = stamp;
// this.setDirty();
} else {
// this.debug("init to default", this._data);
dfd.resolve();
}
};
if (this.opts.remote) {
// Try to pull, then resolve
this.pull()
.done(function() {
// self.debug("init from remote", this._data);
self.offline = false;
dfd.resolve();
})
.fail(function() {
self.offline = true;
if (prevValue != null) {
console.warn(
self + ": could not init from remote; falling back to storage."
);
// self._data = JSON.parse(prevValue);
self._data = $.extend(
{},
self.opts.defaults,
JSON.parse(prevValue)
);
} else {
console.warn(
self + ": could not init from remote; falling back default."
);
}
dfd.resolve();
});
} else if (prevValue != null) {
this.update();
// We still extend from opts.defaults, in case some fields where missing
this._data = $.extend({}, this.opts.defaults, this._data);
// this.debug("init from storage", this._data);
dfd.resolve();
// this.lastUpdate = stamp;
// this.setDirty();
} else {
// this.debug("init to default", this._data);
dfd.resolve();
}
};
window.PersistentObject.prototype = {
/** @type {string} */
version: "@VERSION", // Set to semver by 'grunt release'
window.PersistentObject.prototype = {
/** @type {string} */
version: "@VERSION", // Set to semver by 'grunt release'
/* Trigger commit/push according to current settings. */
_invalidate: function(hint, deferredCall) {
var self = this,
prevChange = this.lastModified,
now = _getNow(),
nextCommit = 0,
nextPush = 0,
nextCheck = 0;
/* Trigger commit/push according to current settings. */
_invalidate: function(hint, deferredCall) {
var self = this,
prevChange = this.lastModified,
now = Date.now(),
nextCommit = 0,
nextPush = 0,
nextCheck = 0;
if ( this._checkTimer ) {
clearTimeout(this._checkTimer);
this._checkTimer = null;
}
if (this._checkTimer) {
clearTimeout(this._checkTimer);
this._checkTimer = null;
}
if ( !deferredCall ) {
// this.debug("_invalidate(" + hint + ")");
this.lastModified = now;
if ( !this.uncommittedSince ) { this.uncommittedSince = now; }
if ( !this.unpushedSince ) { this.unpushedSince = now; }
this.opts.change(hint);
} else {
this.debug("_invalidate() recursive");
}
if (!deferredCall) {
// this.debug("_invalidate(" + hint + ")");
this.lastModified = now;
if (!this.uncommittedSince) {
this.uncommittedSince = now;
}
if (!this.unpushedSince) {
this.unpushedSince = now;
}
this.opts.change(hint);
} else {
this.debug("_invalidate() recursive");
}
if ( this.storage ) {
// If we came here by a deferred timer (or delay is 0), commit
// immedialtely
if ( ((now - prevChange) >= this.opts.commitDelay) ||
((now - this.uncommittedSince) >= this.opts.maxCommitDelay) ) {
if (this.storage) {
// If we came here by a deferred timer (or delay is 0), commit
// immedialtely
if (
now - prevChange >= this.opts.commitDelay ||
now - this.uncommittedSince >= this.opts.maxCommitDelay
) {
this.debug(
"_invalidate(): force commit",
now - prevChange >= this.opts.commitDelay,
now - this.uncommittedSince >= this.opts.maxCommitDelay
);
this.commit();
} else {
// otherwise schedule next check
nextCommit = Math.min(
now + this.opts.commitDelay + 1,
this.uncommittedSince + this.opts.maxCommitDelay + 1
);
}
}
this.debug("_invalidate(): force commit",
((now - prevChange) >= this.opts.commitDelay),
((now - this.uncommittedSince) >= this.opts.maxCommitDelay));
this.commit();
} else {
// otherwise schedule next check
nextCommit = Math.min(
now + this.opts.commitDelay + 1,
this.uncommittedSince + this.opts.maxCommitDelay + 1 );
}
}
if (this.opts.remote) {
if (
now - prevChange >= this.opts.pushDelay ||
now - this.unpushedSince >= this.opts.maxPushDelay
) {
this.debug(
"_invalidate(): force push",
now - prevChange >= this.opts.pushDelay,
now - this.unpushedSince >= this.opts.maxPushDelay
);
this.push();
} else {
nextPush = Math.min(
now + this.opts.pushDelay + 1,
this.unpushedSince + this.opts.maxPushDelay + 1
);
}
}
if (nextCommit || nextPush) {
nextCheck = Math.min(nextCommit || MAX_INT, nextPush || MAX_INT);
// this.debug("Defer update:", nextCheck - now)
this.debug(
"_invalidate(" + hint + ") defer by " + (nextCheck - now) + "ms"
);
this._checkTimer = setTimeout(function() {
self._checkTimer = null; // no need to call clearTimeout in the handler...
self._invalidate.call(self, null, true);
}, nextCheck - now);
}
},
/* Load data from localStorage. */
_update: function(objData) {
if (this.uncommittedSince) {
console.warn("Updating an uncommitted object.");
if (this.conflict(objData, this._data) === false) {
return;
}
}
this._data = objData;
// this.dirty = false;
this.lastUpdate = Date.now();
},
/* Return readable string representation for this instance. */
toString: function() {
return "PersistentObject('" + this.namespace + "')";
},
/* Log to console if opts.debugLevel >= 2 */
debug: function() {
if (this.opts.debugLevel >= 2) {
Array.prototype.unshift.call(arguments, this.toString());
console.log.apply(console, arguments);
}
},
/* Log to console if opts.debugLevel >= 1 */
log: function() {
if (this.opts.debugLevel >= 1) {
Array.prototype.unshift.call(arguments, this.toString());
console.log.apply(console, arguments);
}
},
/** Return true if there are uncommited or unpushed modifications. */
isDirty: function() {
return !!(
(this.storage && this.uncommittedSince) ||
(this.opts.remote && this.unpushedSince)
);
},
/** Return true if initial pull has completed. */
isReady: function() {
return this.ready.state !== "pending";
},
/** Access object property (`key` supports dot notation). */
get: function(key) {
var i,
cur = this._data,
parts = ("" + key) // convert to string
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties
.replace(/^\./, "") // strip a leading dot
.split(".");
if ( this.opts.remote ) {
if ( ((now - prevChange) >= this.opts.pushDelay) ||
((now - this.unpushedSince) >= this.opts.maxPushDelay) ) {
// NOTE: this is slower (tested on Safari):
// return key.split(".").reduce(function(prev, curr) {
// return prev[curr];
// }, this._data);
this.debug("_invalidate(): force push", ((now - prevChange) >= this.opts.pushDelay),
((now - this.unpushedSince) >= this.opts.maxPushDelay));
this.push();
} else {
nextPush = Math.min(
now + this.opts.pushDelay + 1,
this.unpushedSince + this.opts.maxPushDelay + 1 );
}
}
if ( nextCommit || nextPush ) {
nextCheck = Math.min(
nextCommit || MAX_INT,
nextPush || MAX_INT);
// this.debug("Defer update:", nextCheck - now)
this.debug("_invalidate(" + hint + ") defer by " + (nextCheck - now) + "ms" );
this._checkTimer = setTimeout(function() {
self._checkTimer = null; // no need to call clearTimeout in the handler...
self._invalidate.call(self, null, true);
}, nextCheck - now);
}
},
/* Load data from localStorage. */
_update: function(objData) {
if ( this.uncommittedSince ) {
console.warn("Updating an uncommitted object.");
if ( this.conflict(objData, this._data) === false ) {
return;
}
}
this._data = objData;
// this.dirty = false;
this.lastUpdate = _getNow();
},
/* Return readable string representation for this instance. */
toString: function() {
return "PersistentObject('" + this.namespace + "')";
},
/* Log to console if opts.debugLevel >= 2 */
debug: function() {
if ( this.opts.debugLevel >= 2 ) {
Array.prototype.unshift.call(arguments, this.toString());
console.log.apply(window.console, arguments);
}
},
/* Log to console if opts.debugLevel >= 1 */
log: function() {
if ( this.opts.debugLevel >= 1 ) {
Array.prototype.unshift.call(arguments, this.toString());
console.log.apply(window.console, arguments);
}
},
/** Return true if there are uncommited or unpushed modifications. */
isDirty: function() {
return !!(
(this.storage && this.uncommittedSince) ||
(this.opts.remote && this.unpushedSince));
},
/** Return true if initial pull has completed. */
isReady: function() {
return this.ready.state !== "pending";
},
/** Access object property (`key` supports dot notation). */
get: function(key) {
var i,
cur = this._data,
parts = ("" + key) // convert to string
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties
.replace(/^\./, "") // strip a leading dot
.split(".");
for (i = 0; i < parts.length; i++) {
cur = cur[parts[i]];
if (cur === undefined && i < parts.length - 1) {
error(
this +
": Property '" +
key +
"' could not be accessed because parent '" +
parts.slice(0, i + 1).join(".") +
"' does not exist"
);
}
}
return cur;
},
/* Modify object property and set the `dirty` flag (`key` supports dot notation). */
_setOrRemove: function(key, value, remove) {
var i,
parent,
cur = this._data,
parts = ("" + key) // convert to string
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties
.replace(/^\./, "") // strip a leading dot
.split("."),
lastPart = parts.pop();
// NOTE: this is slower (tested on Safari):
// return key.split(".").reduce(function(prev, curr) {
// return prev[curr];
// }, this._data);
for (i = 0; i < parts.length; i++) {
parent = cur;
cur = parent[parts[i]];
// Create intermediate parent objects properties if required
if (cur === undefined) {
if (this.opts.createParents) {
this.debug("Creating intermediate parent '" + parts[i] + "'");
cur = parent[parts[i]] = {};
} else {
error(
this +
": Property '" +
key +
"' could not be set because parent '" +
parts.slice(0, i + 1).join(".") +
"' does not exist"
);
}
}
}
if (cur[lastPart] !== value) {
if (remove === true) {
delete cur[lastPart];
this._invalidate("remove");
} else {
cur[lastPart] = value;
this._invalidate("set");
}
}
},
/** Modify object property and set the `dirty` flag (`key` supports dot notation). */
set: function(key, value) {
return this._setOrRemove(key, value, false);
},
/** Delete object property and set the `dirty` flag (`key` supports dot notation). */
remove: function(key) {
return this._setOrRemove(key, undefined, true);
},
/** Replace data object with a new instance. */
reset: function(obj) {
this._data = obj || {};
this._invalidate("reset");
},
/** Flag object as modified, so that commit / push will be scheduled. */
setDirty: function(flag) {
if (flag === false) {
} else {
this._invalidate("explicit");
}
},
/** Load data from localStorage. */
update: function() {
if (this.phase) {
error(
this + ": Trying to update while '" + this.phase + "' is pending."
);
}
if (this.opts.debugLevel >= 2 && console.time) {
console.time(this + ".update");
}
var data = this.storage.getItem(this.namespace);
data = JSON.parse(data);
this._update(data);
if (this.opts.debugLevel >= 2 && console.time) {
console.timeEnd(this + ".update");
}
},
/** Write data to localStorage. */
commit: function() {
var data;
if (this.phase) {
error(
this + ": Trying to commit while '" + this.phase + "' is pending."
);
}
if (this.opts.debugLevel >= 2 && console.time) {
console.time(this + ".commit");
}
// try { data = JSON.stringify(this._data); } catch(e) { }
data = JSON.stringify(this._data);
this.storage.setItem(this.namespace, data);
// this.dirty = false;
this.uncommittedSince = null;
this.commitCount += 1;
// this.lastCommit = Date.now();
if (this.opts.debugLevel >= 2 && console.time) {
console.timeEnd(this + ".commit");
}
return data;
},
/** Download, then update data from the cloud. */
pull: function() {
var self = this;
for (i = 0; i < parts.length; i++) {
cur = cur[parts[i]];
if ( cur === undefined && i < (parts.length - 1) ) {
$.error(this + ": Property '" + key + "' could not be accessed because parent '" +
parts.slice(0, i + 1).join(".") + "' does not exist");
}
}
return cur;
},
/* Modify object property and set the `dirty` flag (`key` supports dot notation). */
_setOrRemove: function(key, value, remove) {
var i, parent,
cur = this._data,
parts = ("" + key) // convert to string
.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties
.replace(/^\./, "") // strip a leading dot
.split("."),
lastPart = parts.pop();
if (this.phase) {
error(this + ": Trying to pull while '" + this.phase + "' is pending.");
}
if (this.opts.debugLevel >= 2 && console.time) {
console.time(this + ".pull");
}
this.phase = "pull";
// return $.get(this.opts.remote, function(objData) {
return $.ajax({
type: "GET",
url: this.opts.remote,
})
.done(function(objData) {
var strData = objData;
if ($.isArray(objData) || $.isPlainObject(objData)) {
strData = JSON.stringify(objData);
} else {
objData = JSON.parse(objData);
}
self.storage.setItem(self.namespace, strData);
self._update(objData);
self.lastPull = Date.now();
})
.fail(function() {
self.opts.error(arguments);
})
.always(function() {
self.phase = null;
if (self.opts.debugLevel >= 2 && console.time) {
console.timeEnd(self + ".pull");
}
});
},
/** Commit, then upload data to the cloud. */
push: function() {
var self = this,
data = this.commit();
for (i = 0; i < parts.length; i++) {
parent = cur;
cur = parent[parts[i]];
// Create intermediate parent objects properties if required
if ( cur === undefined ) {
if ( this.opts.createParents ) {
this.debug("Creating intermediate parent '" + parts[i] + "'");
cur = parent[parts[i]] = {};
} else {
$.error(this + ": Property '" + key + "' could not be set because parent '" +
parts.slice(0, i + 1).join(".") + "' does not exist");
}
}
}
if ( cur[lastPart] !== value ) {
if ( remove === true ) {
delete cur[lastPart];
this._invalidate("remove");
} else {
cur[lastPart] = value;
this._invalidate("set");
}
}
},
/** Modify object property and set the `dirty` flag (`key` supports dot notation). */
set: function(key, value) {
return this._setOrRemove(key, value, false);
},
/** Delete object property and set the `dirty` flag (`key` supports dot notation). */
remove: function(key) {
return this._setOrRemove(key, undefined, true);
},
/** Replace data object with a new instance. */
reset: function(obj) {
this._data = obj || {};
this._invalidate("reset");
},
/** Flag object as modified, so that commit / push will be scheduled. */
setDirty: function(flag) {
if ( flag === false ) {
} else {
this._invalidate("explicit");
}
},
/** Load data from localStorage. */
update: function() {
if ( this.phase ) {
$.error(this + ": Trying to update while '" + this.phase + "' is pending.");
}
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".update"); }
var data = this.storage.getItem(this.namespace);
data = JSON.parse(data);
this._update(data);
if ( this.opts.debugLevel >= 2 && console.time ) { console.timeEnd(this + ".update"); }
},
/** Write data to localStorage. */
commit: function() {
var data;
if ( this.phase ) {
$.error(this + ": Trying to commit while '" + this.phase + "' is pending.");
}
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".commit"); }
// try { data = JSON.stringify(this._data); } catch(e) { }
data = JSON.stringify(this._data);
this.storage.setItem(this.namespace, data);
// this.dirty = false;
this.uncommittedSince = null;
this.commitCount += 1;
// this.lastCommit = _getNow();
if ( this.opts.debugLevel >= 2 && console.time ) { console.timeEnd(this + ".commit"); }
return data;
},
/** Download, then update data from the cloud. */
pull: function() {
var self = this;
if (this.phase) {
error(this + ": Trying to push while '" + this.phase + "' is pending.");
}
if (this.opts.debugLevel >= 2 && console.time) {
console.time(self + ".push");
}
this.phase = "push";
if (!this.opts.remote) {
error(this + ": Missing remote option");
}
return $.ajax({
type: "PUT",
url: this.opts.remote,
data: data,
})
.done(function() {
// console.log("PUT", arguments);
// self.lastPush = Date.now();
self.unpushedSince = null;
self.pushCount += 1;
})
.fail(function() {
self.opts.error(arguments);
})
.always(function() {
self.phase = null;
if (self.opts.debugLevel >= 2 && console.time) {
console.timeEnd(self + ".push");
}
});
},
/** Read data properties from form input elements with the same name.
* Supports elements of input (type: text, radio, checkbox), textarea,
* and select.
*/
readFromForm: function(form, options) {
var self = this,
$form = $(form),
opts = $.extend(
{
addNew: false,
coerce: true, // convert single checkboxes to bool (instead value)
trim: true,
},
options
);
if ( this.phase ) {
$.error(this + ": Trying to pull while '" + this.phase + "' is pending.");
}
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(this + ".pull"); }
this.phase = "pull";
// return $.get(this.opts.remote, function(objData) {
return $.ajax({
type: "GET",
url: this.opts.remote
}).done(function(objData) {
var strData = objData;
if ( $.isArray(objData) || $.isPlainObject(objData) ) {
strData = JSON.stringify(objData);
} else {
objData = JSON.parse(objData);
}
self.storage.setItem(self.namespace, strData);
self._update(objData);
self.lastPull = _getNow();
}).fail(function() {
self.opts.error(arguments);
}).always(function() {
self.phase = null;
if ( self.opts.debugLevel >= 2 && console.time ) { console.timeEnd(self + ".pull"); }
});
},
/** Commit, then upload data to the cloud. */
push: function() {
var self = this,
data = this.commit();
if (opts.addNew) {
$form.find("[name]").each(function() {
var name = $(this).attr("name");
if (self._data[name] === undefined) {
self.debug("readFromForm: add field '" + name + "'");
self._data[name] = null;
}
});
}
$.each(this._data, function(k, v) {
var val,
$input = $form.find("[name='" + k + "']"),
type = $input.attr("type");
if ( this.phase ) {
$.error(this + ": Trying to push while '" + this.phase + "' is pending.");
}
if ( this.opts.debugLevel >= 2 && console.time ) { console.time(self + ".push"); }
this.phase = "push";
if ( !this.opts.remote ) { $.error(this + ": Missing remote option"); }
return $.ajax({
type: "PUT",
url: this.opts.remote,
data: data
}).done(function() {
// console.log("PUT", arguments);
// self.lastPush = _getNow();
self.unpushedSince = null;
self.pushCount += 1;
}).fail(function() {
self.opts.error(arguments);
}).always(function() {
self.phase = null;
if ( self.opts.debugLevel >= 2 && console.time ) { console.timeEnd(self + ".push"); }
});
},
/** Read data properties from form input elements with the same name.
* Supports elements of input (type: text, radio, checkbox), textarea,
* and select.
*/
readFromForm: function(form, options) {
var self = this,
$form = $(form),
opts = $.extend({
addNew: false,
coerce: true, // convert single checkboxes to bool (instead value)
trim: true
}, options);
if (!$input.length) {
self.debug("readFromForm: field not found: '" + k + "'");
return;
}
if (type === "radio") {
val = $input.filter(":checked").val();
} else if (type === "checkbox" && $input.length === 1) {
val = !!$input.filter(":checked").length;
} else if (type === "checkbox" && $input.length > 1) {
val = [];
$input.filter(":checked").each(function() {
val.push($(this).val());
});
} else {
val = $input.val();
if (opts.trim && typeof val === "string") {
val = $.trim(val);
}
}
// console.log("readFromForm: val(" + k + "): '" + val + "'");
self.set(k, val);
});
// console.log("readFromForm: '" + this + "'", this._data);
},
/** Write data to form elements with the same name.
*/
writeToForm: function(form, options) {
var $form = $(form);
if ( opts.addNew ) {
$form.find("[name]").each(function() {
var name = $(this).attr("name");
if ( self._data[name] === undefined ) {
self.debug("readFromForm: add field '" + name + "'");
self._data[name] = null;
}
});
}
$.each(this._data, function(k, v) {
var val,
$input = $form.find("[name='" + k + "']"),
type = $input.attr("type");
$.each(this._data, function(k, v) {
var $input = $form.find("[name='" + k + "']"),
type = $input.attr("type");
if ( !$input.length ) {
self.debug("readFromForm: field not found: '" + k + "'");
return;
}
if ( type === "radio") {
val = $input.filter(":checked").val();
} else if ( type === "checkbox" && $input.length === 1 ) {
val = !!$input.filter(":checked").length;
} else if ( type === "checkbox" && $input.length > 1 ) {
val = [];
$input.filter(":checked").each(function() {
val.push($(this).val());
});
} else {
val = $input.val();
if ( opts.trim && typeof val === "string" ) { val = $.trim(val); }
}
// console.log("readFromForm: val(" + k + "): '" + val + "'");
self.set(k, val);
});
// console.log("readFromForm: '" + this + "'", this._data);
},
/** Write data to form elements with the same name.
*/
writeToForm: function(form, options) {
var $form = $(form);
$.each(this._data, function(k, v) {
var $input = $form.find("[name='" + k + "']"),
type = $input.attr("type");
if ( $input.length ) {
if ( type === "radio" ) {
$input.filter("[value='" + v + "']").prop("checked", true);
} else if ( type === "checkbox" ) {
if ( $input.length === 1 ) {
$input.prop("checked", !!v);
} else {
// multi-value checkbox
$input.each(function() {
$(this).prop("checked", $.isArray(v) ?
$.inArray(this.value, v) >= 0 : this.value === v);
});
}
} else if ( $input.is("select") ) {
// listbox
$input.find("option").each(function() {
$(this).prop("selected", $.isArray(v) ?
$.inArray(this.value, v) >= 0 : this.value === v);
});
} else {
$input.val(v);
}
}
});
}
};
// -----------------------------------------------------------------------------
}(jQuery, window, document));
if ($input.length) {
if (type === "radio") {
$input.filter("[value='" + v + "']").prop("checked", true);
} else if (type === "checkbox") {
if ($input.length === 1) {
$input.prop("checked", !!v);
} else {
// multi-value checkbox
$input.each(function() {
$(this).prop(
"checked",
$.isArray(v)
? $.inArray(this.value, v) >= 0
: this.value === v
);
});
}
} else if ($input.is("select")) {
// listbox
$input.find("option").each(function() {
$(this).prop(
"selected",
$.isArray(v) ? $.inArray(this.value, v) >= 0 : this.value === v
);
});
} else {
$input.val(v);
}
}
});
},
};
// -----------------------------------------------------------------------------
// Value returned by `require('persisto')`
return window.PersistentObject;
}); // End of closure

@@ -72,5 +72,5 @@ ;(function($, window, document, undefined) {

store.remove("bar.qux");
assert.strictEqual( store.get("bar.qux"), undefined, "reset('bar.qux')" );
assert.strictEqual( store.get("bar.qux"), undefined, "remove('bar.qux')" );
store.remove("bar");
assert.strictEqual( store.get("bar"), undefined, "reset('bar')" );
assert.strictEqual( store.get("bar"), undefined, "remove('bar')" );

@@ -77,0 +77,0 @@ store.set("bar.undefined1.test", "testval");

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc