Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

1889
LINES

< > BotCompany Repo | #1018990 // LargeLocalStorage.js

Document

(function(glob) {
	var undefined = {}.a;

	function definition(Q) {
	

/**
@author Matt Crinklaw-Vogt
*/
function PipeContext(handlers, nextMehod, end) {
	this._handlers = handlers;
	this._next = nextMehod;
	this._end = end;

	this._i = 0;
}

PipeContext.prototype = {
	next: function() {
		// var args = Array.prototype.slice.call(arguments, 0);
		// args.unshift(this);
		this.__pipectx = this;
		return this._next.apply(this, arguments);
	},

	_nextHandler: function() {
		if (this._i >= this._handlers.length) return this._end;

		var handler = this._handlers[this._i].handler;
		this._i += 1;
		return handler;
	},

	length: function() {
		return this._handlers.length;
	}
};

function indexOfHandler(handlers, len, target) {
	for (var i = 0; i < len; ++i) {
		var handler = handlers[i];
		if (handler.name === target || handler.handler === target) {
			return i;
		}
	}

	return -1;
}

function forward(ctx) {
	return ctx.next.apply(ctx, Array.prototype.slice.call(arguments, 1));
}

function coerce(methodNames, handler) {
	methodNames.forEach(function(meth) {
		if (!handler[meth])
			handler[meth] = forward;
	});
}

var abstractPipeline = {
	addFirst: function(name, handler) {
		coerce(this._pipedMethodNames, handler);
		this._handlers.unshift({name: name, handler: handler});
	},

	addLast: function(name, handler) {
		coerce(this._pipedMethodNames, handler);
		this._handlers.push({name: name, handler: handler});
	},

 	/**
 	Add the handler with the given name after the 
 	handler specified by target.  Target can be a handler
 	name or a handler instance.
 	*/
	addAfter: function(target, name, handler) {
		coerce(this._pipedMethodNames, handler);
		var handlers = this._handlers;
		var len = handlers.length;
		var i = indexOfHandler(handlers, len, target);

		if (i >= 0) {
			handlers.splice(i+1, 0, {name: name, handler: handler});
		}
	},

	/**
	Add the handler with the given name after the handler
	specified by target.  Target can be a handler name or
	a handler instance.
	*/
	addBefore: function(target, name, handler) {
		coerce(this._pipedMethodNames, handler);
		var handlers = this._handlers;
		var len = handlers.length;
		var i = indexOfHandler(handlers, len, target);

		if (i >= 0) {
			handlers.splice(i, 0, {name: name, handler: handler});
		}
	},

	/**
	Replace the handler specified by target.
	*/
	replace: function(target, newName, handler) {
		coerce(this._pipedMethodNames, handler);
		var handlers = this._handlers;
		var len = handlers.length;
		var i = indexOfHandler(handlers, len, target);

		if (i >= 0) {
			handlers.splice(i, 1, {name: newName, handler: handler});
		}
	},

	removeFirst: function() {
		return this._handlers.shift();
	},

	removeLast: function() {
		return this._handlers.pop();
	},

	remove: function(target) {
		var handlers = this._handlers;
		var len = handlers.length;
		var i = indexOfHandler(handlers, len, target);

		if (i >= 0)
			handlers.splice(i, 1);
	},

	getHandler: function(name) {
		var i = indexOfHandler(this._handlers, this._handlers.length, name);
		if (i >= 0)
			return this._handlers[i].handler;
		return null;
	}
};

function createPipeline(pipedMethodNames) {
	var end = {};
	var endStubFunc = function() { return end; };
	var nextMethods = {};

	function Pipeline(pipedMethodNames) {
		this.pipe = {
			_handlers: [],
			_contextCtor: PipeContext,
			_nextMethods: nextMethods,
			end: end,
			_pipedMethodNames: pipedMethodNames
		};
	}

	var pipeline = new Pipeline(pipedMethodNames);
	for (var k in abstractPipeline) {
		pipeline.pipe[k] = abstractPipeline[k];
	}

	pipedMethodNames.forEach(function(name) {
		end[name] = endStubFunc;

		nextMethods[name] = new Function(
			"var handler = this._nextHandler();" +
			"handler.__pipectx = this.__pipectx;" +
			"return handler." + name + ".apply(handler, arguments);");

		pipeline[name] = new Function(
			"var ctx = new this.pipe._contextCtor(this.pipe._handlers, this.pipe._nextMethods." + name + ", this.pipe.end);"
			+ "return ctx.next.apply(ctx, arguments);");
	});

	return pipeline;
}

createPipeline.isPipeline = function(obj) {
	return obj instanceof Pipeline;
}
var utils = (function() {
	return {
		convertToBase64: function(blob, cb) {
			var fr = new FileReader();
			fr.onload = function(e) {
				cb(e.target.result);
			};
			fr.onerror = function(e) {
			};
			fr.onabort = function(e) {
			};
			fr.readAsDataURL(blob);
		},

		dataURLToBlob: function(dataURL) {
				var BASE64_MARKER = ';base64,';
				if (dataURL.indexOf(BASE64_MARKER) == -1) {
					var parts = dataURL.split(',');
					var contentType = parts[0].split(':')[1];
					var raw = parts[1];

					return new Blob([raw], {type: contentType});
				}

				var parts = dataURL.split(BASE64_MARKER);
				var contentType = parts[0].split(':')[1];
				var raw = window.atob(parts[1]);
				var rawLength = raw.length;

				var uInt8Array = new Uint8Array(rawLength);

				for (var i = 0; i < rawLength; ++i) {
					uInt8Array[i] = raw.charCodeAt(i);
				}

				return new Blob([uInt8Array.buffer], {type: contentType});
		},

		splitAttachmentPath: function(path) {
			var parts = path.split('/');
			if (parts.length == 1) 
				parts.unshift('__nodoc__');
			return parts;
		},

		mapAsync: function(fn, promise) {
			var deferred = Q.defer();
			promise.then(function(data) {
				_mapAsync(fn, data, [], deferred);
			}, function(e) {
				deferred.reject(e);
			});

			return deferred.promise;
		},

		countdown: function(n, cb) {
		    var args = [];
		    return function() {
		      for (var i = 0; i < arguments.length; ++i)
		        args.push(arguments[i]);
		      n -= 1;
		      if (n == 0)
		        cb.apply(this, args);
		    }
		}
	};

	function _mapAsync(fn, data, result, deferred) {
		fn(data[result.length], function(v) {
			result.push(v);
			if (result.length == data.length)
				deferred.resolve(result);
			else
				_mapAsync(fn, data, result, deferred);
		}, function(err) {
			deferred.reject(err);
		})
	}
})();
var requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
var persistentStorage = navigator.persistentStorage || navigator.webkitPersistentStorage;
var FilesystemAPIProvider = (function(Q) {
	function makeErrorHandler(deferred, finalDeferred) {
		// TODO: normalize the error so
		// we can handle it upstream
		return function(e) {
			if (e.code == 1) {
				deferred.resolve(undefined);
			} else {
				if (finalDeferred)
					finalDeferred.reject(e);
				else
					deferred.reject(e);
			}
		}
	}

	function getAttachmentPath(docKey, attachKey) {
		docKey = docKey.replace(/\//g, '--');
		var attachmentsDir = docKey + "-attachments";
		return {
			dir: attachmentsDir,
			path: attachmentsDir + "/" + attachKey
		};
	}

	function readDirEntries(reader, result) {
		var deferred = Q.defer();

		_readDirEntries(reader, result, deferred);

		return deferred.promise;
	}

	function _readDirEntries(reader, result, deferred) {
		reader.readEntries(function(entries) {
			if (entries.length == 0) {
				deferred.resolve(result);
			} else {
				result = result.concat(entries);
				_readDirEntries(reader, result, deferred);
			}
		}, function(err) {
			deferred.reject(err);
		});
	}

	function entryToFile(entry, cb, eb) {
		entry.file(cb, eb);
	}

	function entryToURL(entry) {
		return entry.toURL();
	}

	function FSAPI(fs, numBytes, prefix) {
		this._fs = fs;
		this._capacity = numBytes;
		this._prefix = prefix;
		this.type = "FileSystemAPI";
	}

	FSAPI.prototype = {
		getContents: function(path, options) {
			var deferred = Q.defer();
			path = this._prefix + path;
			this._fs.root.getFile(path, {}, function(fileEntry) {
				fileEntry.file(function(file) {
					var reader = new FileReader();

					reader.onloadend = function(e) {
						var data = e.target.result;
						var err;
						if (options && options.json) {
							try {
								data = JSON.parse(data);
							} catch(e) {
								err = new Error('unable to parse JSON for ' + path);
							}
						}

						if (err) {
							deferred.reject(err);
						} else {
							deferred.resolve(data);
						}
					};

					reader.readAsText(file);
				}, makeErrorHandler(deferred));
			}, makeErrorHandler(deferred));

			return deferred.promise;
		},

		// create a file at path
		// and write `data` to it
		setContents: function(path, data, options) {
			var deferred = Q.defer();

			if (options && options.json)
				data = JSON.stringify(data);

			path = this._prefix + path;
			this._fs.root.getFile(path, {create:true}, function(fileEntry) {
				fileEntry.createWriter(function(fileWriter) {
					var blob;
					fileWriter.onwriteend = function(e) {
						fileWriter.onwriteend = function() {
							deferred.resolve();
						};
						fileWriter.truncate(blob.size);
					}

					fileWriter.onerror = makeErrorHandler(deferred);

					if (data instanceof Blob) {
						blob = data;
					} else {
						blob = new Blob([data], {type: 'text/plain'});
					}

					fileWriter.write(blob);
				}, makeErrorHandler(deferred));
			}, makeErrorHandler(deferred));

			return deferred.promise;
		},

		ls: function(docKey) {
			var isRoot = false;
			if (!docKey) {docKey = this._prefix; isRoot = true;}
			else docKey = this._prefix + docKey + "-attachments";

			var deferred = Q.defer();

			this._fs.root.getDirectory(docKey, {create:false},
			function(entry) {
				var reader = entry.createReader();
				readDirEntries(reader, []).then(function(entries) {
					var listing = [];
					entries.forEach(function(entry) {
						if (!entry.isDirectory) {
							listing.push(entry.name);
						}
					});
					deferred.resolve(listing);
				});
			}, function(error) {
				deferred.reject(error);
			});

			return deferred.promise;
		},

		clear: function() {
			var deferred = Q.defer();
			var failed = false;
			var ecb = function(err) {
				failed = true;
				deferred.reject(err);
			}

			this._fs.root.getDirectory(this._prefix, {},
			function(entry) {
				var reader = entry.createReader();
				reader.readEntries(function(entries) {
					var latch = 
					utils.countdown(entries.length, function() {
						if (!failed)
							deferred.resolve();
					});

					entries.forEach(function(entry) {
						if (entry.isDirectory) {
							entry.removeRecursively(latch, ecb);
						} else {
							entry.remove(latch, ecb);
						}
					});

					if (entries.length == 0)
						deferred.resolve();
				}, ecb);
			}, ecb);

			return deferred.promise;
		},

		rm: function(path) {
			var deferred = Q.defer();
			var finalDeferred = Q.defer();

			// remove attachments that go along with the path
			path = this._prefix + path;
			var attachmentsDir = path + "-attachments";

			this._fs.root.getFile(path, {create:false},
				function(entry) {
					entry.remove(function() {
						deferred.promise.then(finalDeferred.resolve);
					}, function(err) {
						finalDeferred.reject(err);
					});
				},
				makeErrorHandler(finalDeferred));

			this._fs.root.getDirectory(attachmentsDir, {},
				function(entry) {
					entry.removeRecursively(function() {
						deferred.resolve();
					}, function(err) {
						finalDeferred.reject(err);
					});
				},
				makeErrorHandler(deferred, finalDeferred));

			return finalDeferred.promise;
		},

		getAttachment: function(docKey, attachKey) {
			var attachmentPath = this._prefix + getAttachmentPath(docKey, attachKey).path;

			var deferred = Q.defer();
			this._fs.root.getFile(attachmentPath, {}, function(fileEntry) {
				fileEntry.file(function(file) {
					if (file.size == 0)
						deferred.resolve(undefined);
					else
						deferred.resolve(file);
				}, makeErrorHandler(deferred));
			}, function(err) {
				if (err.code == 1) {
					deferred.resolve(undefined);
				} else {
					deferred.reject(err);
				}
			});

			return deferred.promise;
		},

		getAttachmentURL: function(docKey, attachKey) {
			var attachmentPath = this._prefix + getAttachmentPath(docKey, attachKey).path;

			var deferred = Q.defer();
			var url = 'filesystem:' + window.location.protocol + '//' + window.location.host + '/persistent/' + attachmentPath;
			deferred.resolve(url);
			// this._fs.root.getFile(attachmentPath, {}, function(fileEntry) {
			// 	deferred.resolve(fileEntry.toURL());
			// }, makeErrorHandler(deferred, "getting attachment file entry"));

			return deferred.promise;
		},

		getAllAttachments: function(docKey) {
			var deferred = Q.defer();
			var attachmentsDir = this._prefix + docKey + "-attachments";

			this._fs.root.getDirectory(attachmentsDir, {},
			function(entry) {
				var reader = entry.createReader();
				deferred.resolve(
					utils.mapAsync(function(entry, cb, eb) {
						entry.file(function(file) {
							cb({
								data: file,
								docKey: docKey,
								attachKey: entry.name
							});
						}, eb);
					}, readDirEntries(reader, [])));
			}, function(err) {
				deferred.resolve([]);
			});

			return deferred.promise;
		},

		getAllAttachmentURLs: function(docKey) {
			var deferred = Q.defer();
			var attachmentsDir = this._prefix + docKey + "-attachments";

			this._fs.root.getDirectory(attachmentsDir, {},
			function(entry) {
				var reader = entry.createReader();
				readDirEntries(reader, []).then(function(entries) {
					deferred.resolve(entries.map(
					function(entry) {
						return {
							url: entry.toURL(),
							docKey: docKey,
							attachKey: entry.name
						};
					}));
				});
			}, function(err) {
				deferred.reject(err);
			});

			return deferred.promise;
		},

		revokeAttachmentURL: function(url) {
			// we return FS urls so this is a no-op
			// unless someone is being silly and doing
			// createObjectURL(getAttachment()) ......
		},

		// Create a folder at dirname(path)+"-attachments"
		// add attachment under that folder as basename(path)
		setAttachment: function(docKey, attachKey, data) {
			var attachInfo = getAttachmentPath(docKey, attachKey);

			var deferred = Q.defer();

			var self = this;
			this._fs.root.getDirectory(this._prefix + attachInfo.dir,
			{create:true}, function(dirEntry) {
				deferred.resolve(self.setContents(attachInfo.path, data));
			}, makeErrorHandler(deferred));

			return deferred.promise;
		},

		// rm the thing at dirname(path)+"-attachments/"+basename(path)
		rmAttachment: function(docKey, attachKey) {
			var attachmentPath = getAttachmentPath(docKey, attachKey).path;

			var deferred = Q.defer();
			this._fs.root.getFile(this._prefix + attachmentPath, {create:false},
				function(entry) {
					entry.remove(function() {
						deferred.resolve();
					}, makeErrorHandler(deferred));
			}, makeErrorHandler(deferred));

			return deferred.promise;
		},

		getCapacity: function() {
			return this._capacity;
		}
	};

	return {
		init: function(config) {
			var deferred = Q.defer();

			if (!requestFileSystem) {
				deferred.reject("No FS API");
				return deferred.promise;
			}

			var prefix = config.name + '/';

			persistentStorage.requestQuota(config.size,
			function(numBytes) {
				requestFileSystem(window.PERSISTENT, numBytes,
				function(fs) {
					fs.root.getDirectory(config.name, {create: true},
					function() {
						deferred.resolve(new FSAPI(fs, numBytes, prefix));
					}, function(err) {
						console.error(err);
						deferred.reject(err);
					});
				}, function(err) {
					// TODO: implement various error messages.
					console.error(err);
					deferred.reject(err);
				});
			}, function(err) {
				// TODO: implement various error messages.
				console.error(err);
				deferred.reject(err);
			});

			return deferred.promise;
		},

		isAvailable: function() {
			return requestFileSystem != null;
		}
	}
})(Q);
var indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.OIDBTransaction || window.msIDBTransaction;
var IndexedDBProvider = (function(Q) {
	var URL = window.URL || window.webkitURL;

	var convertToBase64 = utils.convertToBase64;
	var dataURLToBlob = utils.dataURLToBlob;

	function IDB(db) {
		this._db = db;
		this.type = 'IndexedDB';

		var transaction = this._db.transaction(['attachments'], 'readwrite');
		this._supportsBlobs = true;
		try {
			transaction.objectStore('attachments')
			.put(Blob(["sdf"], {type: "text/plain"}), "featurecheck");
		} catch (e) {
			this._supportsBlobs = false;
		}
	}

	// TODO: normalize returns and errors.
	IDB.prototype = {
		getContents: function(docKey) {
			var deferred = Q.defer();
			var transaction = this._db.transaction(['files'], 'readonly');

			var get = transaction.objectStore('files').get(docKey);
			get.onsuccess = function(e) {
				deferred.resolve(e.target.result);
			};

			get.onerror = function(e) {
				deferred.reject(e);
			};

			return deferred.promise;
		},

		setContents: function(docKey, data) {
			var deferred = Q.defer();
			var transaction = this._db.transaction(['files'], 'readwrite');

			var put = transaction.objectStore('files').put(data, docKey);
			put.onsuccess = function(e) {
				deferred.resolve(e);
			};

			put.onerror = function(e) {
				deferred.reject(e);
			};

			return deferred.promise;
		},

		rm: function(docKey) {
			var deferred = Q.defer();
			var finalDeferred = Q.defer();

			var transaction = this._db.transaction(['files', 'attachments'], 'readwrite');
			
			var del = transaction.objectStore('files').delete(docKey);

			del.onsuccess = function(e) {
				deferred.promise.then(function() {
					finalDeferred.resolve();
				});
			};

			del.onerror = function(e) {
				deferred.promise.catch(function() {
					finalDeferred.reject(e);
				});
			};

			var attachmentsStore = transaction.objectStore('attachments');
			var index = attachmentsStore.index('fname');
			var cursor = index.openCursor(IDBKeyRange.only(docKey));
			cursor.onsuccess = function(e) {
				var cursor = e.target.result;
				if (cursor) {
					cursor.delete();
					cursor.continue();
				} else {
					deferred.resolve();
				}
			};

			cursor.onerror = function(e) {
				deferred.reject(e);
			}

			return finalDeferred.promise;
		},

		getAttachment: function(docKey, attachKey) {
			var deferred = Q.defer();

			var transaction = this._db.transaction(['attachments'], 'readonly');
			var get = transaction.objectStore('attachments').get(docKey + '/' + attachKey);

			var self = this;
			get.onsuccess = function(e) {
				if (!e.target.result) {
					deferred.resolve(undefined);
					return;
				}

				var data = e.target.result.data;
				if (!self._supportsBlobs) {
					data = dataURLToBlob(data);
				}
				deferred.resolve(data);
			};

			get.onerror = function(e) {
				deferred.reject(e);
			};

			return deferred.promise;
		},

		ls: function(docKey) {
			var deferred = Q.defer();

			if (!docKey) {
				// list docs
				var store = 'files';
			} else {
				// list attachments
				var store = 'attachments';
			}

			var transaction = this._db.transaction([store], 'readonly');
			var cursor = transaction.objectStore(store).openCursor();
			var listing = [];

			cursor.onsuccess = function(e) {
				var cursor = e.target.result;
				if (cursor) {
					listing.push(!docKey ? cursor.key : cursor.key.split('/')[1]);
					cursor.continue();
				} else {
					deferred.resolve(listing);
				}
			};

			cursor.onerror = function(e) {
				deferred.reject(e);
			};

			return deferred.promise;
		},

		clear: function() {
			var deferred = Q.defer();
			var finalDeferred = Q.defer();

			var t = this._db.transaction(['attachments', 'files'], 'readwrite');


			var req1 = t.objectStore('attachments').clear();
			var req2 = t.objectStore('files').clear();

			req1.onsuccess = function() {
				deferred.promise.then(finalDeferred.resolve);
			};

			req2.onsuccess = function() {
				deferred.resolve();
			};

			req1.onerror = function(err) {
				finalDeferred.reject(err);
			};

			req2.onerror = function(err) {
				finalDeferred.reject(err);
			};

			return finalDeferred.promise;
		},

		getAllAttachments: function(docKey) {
			var deferred = Q.defer();
			var self = this;

			var transaction = this._db.transaction(['attachments'], 'readonly');
			var index = transaction.objectStore('attachments').index('fname');

			var cursor = index.openCursor(IDBKeyRange.only(docKey));
			var values = [];
			cursor.onsuccess = function(e) {
				var cursor = e.target.result;
				if (cursor) {
					var data;
					if (!self._supportsBlobs) {
						data = dataURLToBlob(cursor.value.data)
					} else {
						data = cursor.value.data;
					}
					values.push({
						data: data,
						docKey: docKey,
						attachKey: cursor.primaryKey.split('/')[1] // TODO
					});
					cursor.continue();
				} else {
					deferred.resolve(values);
				}
			};

			cursor.onerror = function(e) {
				deferred.reject(e);
			};

			return deferred.promise;
		},

		getAllAttachmentURLs: function(docKey) {
			var deferred = Q.defer();
			this.getAllAttachments(docKey).then(function(attachments) {
				var urls = attachments.map(function(a) {
					a.url = URL.createObjectURL(a.data);
					delete a.data;
					return a;
				});

				deferred.resolve(urls);
			}, function(e) {
				deferred.reject(e);
			});

			return deferred.promise;
		},

		getAttachmentURL: function(docKey, attachKey) {
			var deferred = Q.defer();
			this.getAttachment(docKey, attachKey).then(function(attachment) {
				deferred.resolve(URL.createObjectURL(attachment));
			}, function(e) {
				deferred.reject(e);
			});

			return deferred.promise;
		},

		revokeAttachmentURL: function(url) {
			URL.revokeObjectURL(url);
		},

		setAttachment: function(docKey, attachKey, data) {
			var deferred = Q.defer();

			if (data instanceof Blob && !this._supportsBlobs) {
				var self = this;
				convertToBase64(data, function(data) {
					continuation.call(self, data);
				});
			} else {
				continuation.call(this, data);
			}

			function continuation(data) {
				var obj = {
					path: docKey + '/' + attachKey,
					fname: docKey,
					data: data
				};
				var transaction = this._db.transaction(['attachments'], 'readwrite');
				var put = transaction.objectStore('attachments').put(obj);

				put.onsuccess = function(e) {
					deferred.resolve(e);
				};

				put.onerror = function(e) {
					deferred.reject(e);
				};
			}

			return deferred.promise;
		},

		rmAttachment: function(docKey, attachKey) {
			var deferred = Q.defer();
			var transaction = this._db.transaction(['attachments'], 'readwrite');
			var del = transaction.objectStore('attachments').delete(docKey + '/' + attachKey);

			del.onsuccess = function(e) {
				deferred.resolve(e);
			};

			del.onerror = function(e) {
				deferred.reject(e);
			};

			return deferred.promise;
		}
	};

	return {
		init: function(config) {
			var deferred = Q.defer();
			var dbVersion = 2;

			if (!indexedDB || !IDBTransaction) {
				deferred.reject("No IndexedDB");
				return deferred.promise;
			}

			var request = indexedDB.open(config.name, dbVersion);

			function createObjectStore(db) {
				db.createObjectStore("files");
				var attachStore = db.createObjectStore("attachments", {keyPath: 'path'});
				attachStore.createIndex('fname', 'fname', {unique: false})
			}

			// TODO: normalize errors
			request.onerror = function (event) {
				deferred.reject(event);
			};
		 
			request.onsuccess = function (event) {
				var db = request.result;
		 
				db.onerror = function (event) {
					console.log(event);
				};
				
				// Chrome workaround
				if (db.setVersion) {
					if (db.version != dbVersion) {
						var setVersion = db.setVersion(dbVersion);
						setVersion.onsuccess = function () {
							createObjectStore(db);
							deferred.resolve();
						};
					}
					else {
						deferred.resolve(new IDB(db));
					}
				} else {
					deferred.resolve(new IDB(db));
				}
			}
			
			request.onupgradeneeded = function (event) {
				createObjectStore(event.target.result);
			};

			return deferred.promise;
		},

		isAvailable: function() {
			return indexedDB != null && IDBTransaction != null;
		}
	}
})(Q);
var LocalStorageProvider = (function(Q) {
	return {
		init: function() {
			return Q({type: 'LocalStorage'});
		}
	}
})(Q);
var openDb = window.openDatabase;
var WebSQLProvider = (function(Q) {
	var URL = window.URL || window.webkitURL;
	var convertToBase64 = utils.convertToBase64;
	var dataURLToBlob = utils.dataURLToBlob;

	function WSQL(db) {
		this._db = db;
		this.type = 'WebSQL';
	}

	WSQL.prototype = {
		getContents: function(docKey, options) {
			var deferred = Q.defer();
			this._db.transaction(function(tx) {
				tx.executeSql('SELECT value FROM files WHERE fname = ?', [docKey],
				function(tx, res) {
					if (res.rows.length == 0) {
						deferred.resolve(undefined);
					} else {
						var data = res.rows.item(0).value;
						if (options && options.json)
							data = JSON.parse(data);
						deferred.resolve(data);
					}
				});
			}, function(err) {
				consol.log(err);
				deferred.reject(err);
			});

			return deferred.promise;
		},

		setContents: function(docKey, data, options) {
			var deferred = Q.defer();
			if (options && options.json)
				data = JSON.stringify(data);

			this._db.transaction(function(tx) {
				tx.executeSql(
				'INSERT OR REPLACE INTO files (fname, value) VALUES(?, ?)', [docKey, data]);
			}, function(err) {
				console.log(err);
				deferred.reject(err);
			}, function() {
				deferred.resolve();
			});

			return deferred.promise;
		},

		rm: function(docKey) {
			var deferred = Q.defer();

			this._db.transaction(function(tx) {
				tx.executeSql('DELETE FROM files WHERE fname = ?', [docKey]);
				tx.executeSql('DELETE FROM attachments WHERE fname = ?', [docKey]);
			}, function(err) {
				console.log(err);
				deferred.reject(err);
			}, function() {
				deferred.resolve();
			});

			return deferred.promise;
		},

		getAttachment: function(fname, akey) {
			var deferred = Q.defer();

			this._db.transaction(function(tx){ 
				tx.executeSql('SELECT value FROM attachments WHERE fname = ? AND akey = ?',
				[fname, akey],
				function(tx, res) {
					if (res.rows.length == 0) {
						deferred.resolve(undefined);
					} else {
						deferred.resolve(dataURLToBlob(res.rows.item(0).value));
					}
				});
			}, function(err) {
				deferred.reject(err);
			});

			return deferred.promise;
		},

		getAttachmentURL: function(docKey, attachKey) {
			var deferred = Q.defer();
			this.getAttachment(docKey, attachKey).then(function(blob) {
				deferred.resolve(URL.createObjectURL(blob));
			}, function() {
				deferred.reject();
			});

			return deferred.promise;
		},

		ls: function(docKey) {
			var deferred = Q.defer();

			var select;
			var field;
			if (!docKey) {
				select = 'SELECT fname FROM files';
				field = 'fname';
			} else {
				select = 'SELECT akey FROM attachments WHERE fname = ?';
				field = 'akey';
			}

			this._db.transaction(function(tx) {
				tx.executeSql(select, docKey ? [docKey] : [],
				function(tx, res) {
					var listing = [];
					for (var i = 0; i < res.rows.length; ++i) {
						listing.push(res.rows.item(i)[field]);
					}

					deferred.resolve(listing);
				}, function(err) {
					deferred.reject(err);
				});
			});

			return deferred.promise;
		},

		clear: function() {
			var deffered1 = Q.defer();
			var deffered2 = Q.defer();

			this._db.transaction(function(tx) {
				tx.executeSql('DELETE FROM files', function() {
					deffered1.resolve();
				});
				tx.executeSql('DELETE FROM attachments', function() {
					deffered2.resolve();
				});
			}, function(err) {
				deffered1.reject(err);
				deffered2.reject(err);
			});

			return Q.all([deffered1, deffered2]);
		},

		getAllAttachments: function(fname) {
			var deferred = Q.defer();

			this._db.transaction(function(tx) {
				tx.executeSql('SELECT value, akey FROM attachments WHERE fname = ?',
				[fname],
				function(tx, res) {
					// TODO: ship this work off to a webworker
					// since there could be many of these conversions?
					var result = [];
					for (var i = 0; i < res.rows.length; ++i) {
						var item = res.rows.item(i);
						result.push({
							docKey: fname,
							attachKey: item.akey,
							data: dataURLToBlob(item.value)
						});
					}

					deferred.resolve(result);
				});
			}, function(err) {
				deferred.reject(err);
			});

			return deferred.promise;
		},

		getAllAttachmentURLs: function(fname) {
			var deferred = Q.defer();
			this.getAllAttachments(fname).then(function(attachments) {
				var urls = attachments.map(function(a) {
					a.url = URL.createObjectURL(a.data);
					delete a.data;
					return a;
				});

				deferred.resolve(urls);
			}, function(e) {
				deferred.reject(e);
			});

			return deferred.promise;
		},

		revokeAttachmentURL: function(url) {
			URL.revokeObjectURL(url);
		},

		setAttachment: function(fname, akey, data) {
			var deferred = Q.defer();

			var self = this;
			convertToBase64(data, function(data) {
				self._db.transaction(function(tx) {
					tx.executeSql(
					'INSERT OR REPLACE INTO attachments (fname, akey, value) VALUES(?, ?, ?)',
					[fname, akey, data]);
				}, function(err) {
					deferred.reject(err);
				}, function() {
					deferred.resolve();
				});
			});

			return deferred.promise;
		},

		rmAttachment: function(fname, akey) {
			var deferred = Q.defer();
			this._db.transaction(function(tx) {
				tx.executeSql('DELETE FROM attachments WHERE fname = ? AND akey = ?',
				[fname, akey]);
			}, function(err) {
				deferred.reject(err);
			}, function() {
				deferred.resolve();
			});

			return deferred.promise;
		}
	};

	return {
		init: function(config) {
			var deferred = Q.defer();
			if (!openDb) {
				deferred.reject("No WebSQL");
				return deferred.promise;
			}

			var db = openDb(config.name, '1.0', 'large local storage', config.size);

			db.transaction(function(tx) {
				tx.executeSql('CREATE TABLE IF NOT EXISTS files (fname unique, value)');
				tx.executeSql('CREATE TABLE IF NOT EXISTS attachments (fname, akey, value)');
				tx.executeSql('CREATE INDEX IF NOT EXISTS fname_index ON attachments (fname)');
				tx.executeSql('CREATE INDEX IF NOT EXISTS akey_index ON attachments (akey)');
				tx.executeSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_attach ON attachments (fname, akey)')
			}, function(err) {
				deferred.reject(err);
			}, function() {
				deferred.resolve(new WSQL(db));
			});

			return deferred.promise;
		},

		isAvailable: function() {
			return openDb != null;
		}
	}
})(Q);
var LargeLocalStorage = (function(Q) {
	var sessionMeta = localStorage.getItem('LargeLocalStorage-meta');
	if (sessionMeta)
		sessionMeta = JSON.parse(sessionMeta);
	else
		sessionMeta = {};

	window.addEventListener('beforeunload', function() {
		localStorage.setItem('LargeLocalStorage-meta', JSON.stringify(sessionMeta));
	});

	function defaults(options, defaultOptions) {
		for (var k in defaultOptions) {
			if (options[k] === undefined)
				options[k] = defaultOptions[k];
		}

		return options;
	}

	var providers = {
		FileSystemAPI: FilesystemAPIProvider,
		IndexedDB: IndexedDBProvider,
		WebSQL: WebSQLProvider
		// LocalStorage: LocalStorageProvider
	}

	var defaultConfig = {
		size: 10 * 1024 * 1024,
		name: 'lls'
	};

	function selectImplementation(config) {
		if (!config) config = {};
		config = defaults(config, defaultConfig);

		if (config.forceProvider) {
			return providers[config.forceProvider].init(config);
		}

		return FilesystemAPIProvider.init(config).then(function(impl) {
			return Q(impl);
		}, function() {
			return IndexedDBProvider.init(config);
		}).then(function(impl) {
			return Q(impl);
		}, function() {
			return WebSQLProvider.init(config);
		}).then(function(impl) {
			return Q(impl);
		}, function() {
			console.error('Unable to create any storage implementations.  Using LocalStorage');
			return LocalStorageProvider.init(config);
		});
	}

	function copy(obj) {
		var result = {};
		Object.keys(obj).forEach(function(key) {
			result[key] = obj[key];
		});

		return result;
	}

	function handleDataMigration(storageInstance, config, previousProviderType, currentProivderType) {
		var previousProviderType = 
			sessionMeta[config.name] && sessionMeta[config.name].lastStorageImpl;
		if (config.migrate) {
			if (previousProviderType != currentProivderType
				&& previousProviderType in providers) {
				config = copy(config);
				config.forceProvider = previousProviderType;
				selectImplementation(config).then(function(prevImpl) {
					config.migrate(null, prevImpl, storageInstance, config);
				}, function(e) {
					config.migrate(e);
				});
			} else {
				if (config.migrationComplete)
					config.migrationComplete();
			}
		}
	}

	/**
	 * 
	 * LargeLocalStorage (or LLS) gives you a large capacity 
	 * (up to several gig with permission from the user)
	 * key-value store in the browser.
	 *
	 * For storage, LLS uses the [FilesystemAPI](https://developer.mozilla.org/en-US/docs/WebGuide/API/File_System)
	 * when running in Chrome and Opera, 
	 * [IndexedDB](https://developer.mozilla.org/en-US/docs/IndexedDB) in Firefox and IE
	 * and [WebSQL](http://www.w3.org/TR/webdatabase/) in Safari.
	 *
	 * When IndexedDB becomes available in Safari, LLS will
	 * update to take advantage of that storage implementation.
	 *
	 *
	 * Upon construction a LargeLocalStorage (LLS) object will be 
	 * immediately returned but not necessarily immediately ready for use.
	 *
	 * A LLS object has an `initialized` property which is a promise
	 * that is resolved when the LLS object is ready for us.
	 *
	 * Usage of LLS would typically be:
	 * ```
	 * var storage = new LargeLocalStorage({size: 75*1024*1024});
	 * storage.initialized.then(function(grantedCapacity) {
	 *   // storage ready to be used.
	 * });
	 * ```
	 *
	 * The reason that LLS may not be immediately ready for
	 * use is that some browsers require confirmation from the
	 * user before a storage area may be created.  Also,
	 * the browser's native storage APIs are asynchronous.
	 *
	 * If an LLS instance is used before the storage
	 * area is ready then any
	 * calls to it will throw an exception with code: "NO_IMPLEMENTATION"
	 *
	 * This behavior is useful when you want the application
	 * to continue to function--regardless of whether or
	 * not the user has allowed it to store data--and would
	 * like to know when your storage calls fail at the point
	 * of those calls.
	 *
	 * LLS-contrib has utilities to queue storage calls until
	 * the implementation is ready.  If an implementation
	 * is never ready this could obviously lead to memory issues
	 * which is why it is not the default behavior.
	 *
	 * @example
	 *	var desiredCapacity = 50 * 1024 * 1024; // 50MB
	 *	var storage = new LargeLocalStorage({
	 *		// desired capacity, in bytes.
	 *		size: desiredCapacity,
	 *
	 * 		// optional name for your LLS database. Defaults to lls.
	 *		// This is the name given to the underlying
	 *		// IndexedDB or WebSQL DB or FSAPI Folder.
	 *		// LLS's with different names are independent.
	 *		name: 'myStorage'
	 *
	 *		// the following is an optional param 
	 *		// that is useful for debugging.
	 *		// force LLS to use a specific storage implementation
	 *		// forceProvider: 'IndexedDB' or 'WebSQL' or 'FilesystemAPI'
	 *		
	 *		// These parameters can be used to migrate data from one
	 *		// storage implementation to another
	 *		// migrate: LargeLocalStorage.copyOldData,
	 *		// migrationComplete: function(err) {
	 *		//   db is initialized and old data has been copied.
	 *		// }
	 *	});
	 *	storage.initialized.then(function(capacity) {
	 *		if (capacity != -1 && capacity != desiredCapacity) {
	 *			// the user didn't authorize your storage request
	 *			// so instead you have some limitation on your storage
	 *		}
	 *	})
	 *
	 * @class LargeLocalStorage
	 * @constructor
	 * @param {object} config {size: sizeInByes, [forceProvider: force a specific implementation]}
	 * @return {LargeLocalStorage}
	 */
	function LargeLocalStorage(config) {
		var deferred = Q.defer();
		/**
		* @property {promise} initialized
		*/
		this.initialized = deferred.promise;

		var piped = createPipeline([
			'ready',
			'ls',
			'rm',
			'clear',
			'getContents',
			'setContents',
			'getAttachment',
			'setAttachment',
			'getAttachmentURL',
			'getAllAttachments',
			'getAllAttachmentURLs',
			'revokeAttachmentURL',
			'rmAttachment',
			'getCapacity',
			'initialized']);

		piped.pipe.addLast('lls', this);
		piped.initialized = this.initialized;

		var self = this;
		selectImplementation(config).then(function(impl) {
			self._impl = impl;
			handleDataMigration(piped, config, self._impl.type);
			sessionMeta[config.name] = sessionMeta[config.name] || {};
			sessionMeta[config.name].lastStorageImpl = impl.type;
			deferred.resolve(piped);
		}).catch(function(e) {
			// This should be impossible
			console.log(e);
			deferred.reject('No storage provider found');
		});

		return piped;
	}

	LargeLocalStorage.prototype = {
		/**
		* Whether or not LLS is ready to store data.
		* The `initialized` property can be used to
		* await initialization.
		* @example
		*	// may or may not be true
		*	storage.ready();
		*	
		*	storage.initialized.then(function() {
		*		// always true
		*		storage.ready();
		*	})
		* @method ready
		*/
		ready: function() {
			return this._impl != null;
		},

		/**
		* List all attachments under a given key.
		*
		* List all documents if no key is provided.
		*
		* Returns a promise that is fulfilled with
		* the listing.
		*
		* @example
		*	storage.ls().then(function(docKeys) {
		*		console.log(docKeys);
		*	})
		*
		* @method ls
		* @param {string} [docKey]
		* @returns {promise} resolved with the listing, rejected if the listing fails.
		*/
		ls: function(docKey) {
			this._checkAvailability();
			return this._impl.ls(docKey);
		},

		/**
		* Remove the specified document and all
		* of its attachments.
		*
		* Returns a promise that is fulfilled when the
		* removal completes.
		*
		* If no docKey is specified, this throws an error.
		*
		* To remove all files in LargeLocalStorage call
		* `lls.clear();`
		*
		* To remove all attachments that were written without
		* a docKey, call `lls.rm('__emptydoc__');`
		*
		* rm works this way to ensure you don't lose
		* data due to an accidently undefined variable.
		*
		* @example
		* 	stoarge.rm('exampleDoc').then(function() {
		*		alert('doc and all attachments were removed');
		* 	})
		*
		* @method rm
		* @param {string} docKey
		* @returns {promise} resolved when removal completes, rejected if the removal fails.
		*/
		rm: function(docKey) {
			this._checkAvailability();
			return this._impl.rm(docKey);
		},

		/**
		* An explicit way to remove all documents and
		* attachments from LargeLocalStorage.
		*
		* @example
		*	storage.clear().then(function() {
		*		alert('all data has been removed');
		*	});
		* 
		* @returns {promise} resolve when clear completes, rejected if clear fails.
		*/
		clear: function() {
			this._checkAvailability();
			return this._impl.clear();
		},

		/**
		* Get the contents of a document identified by `docKey`
		* TODO: normalize all implementations to allow storage
		* and retrieval of JS objects?
		*
		* @example
		* 	storage.getContents('exampleDoc').then(function(contents) {
		* 		alert(contents);
		* 	});
		*
		* @method getContents
		* @param {string} docKey
		* @returns {promise} resolved with the contents when the get completes
		*/
		getContents: function(docKey, options) {
			this._checkAvailability();
			return this._impl.getContents(docKey, options);
		},

		/**
		* Set the contents identified by `docKey` to `data`.
		* The document will be created if it does not exist.
		*
		* @example
		* 	storage.setContents('exampleDoc', 'some data...').then(function() {
		*		alert('doc written');
		* 	});
		*
		* @method setContents
		* @param {string} docKey
		* @param {any} data
		* @returns {promise} fulfilled when set completes
		*/
		setContents: function(docKey, data, options) {
			this._checkAvailability();
			return this._impl.setContents(docKey, data, options);
		},

		/**
		* Get the attachment identified by `docKey` and `attachKey`
		*
		* @example
		* 	storage.getAttachment('exampleDoc', 'examplePic').then(function(attachment) {
		*    	var url = URL.createObjectURL(attachment);
		*    	var image = new Image(url);
		*    	document.body.appendChild(image);
		*    	URL.revokeObjectURL(url);
		* 	})
		*
		* @method getAttachment
		* @param {string} [docKey] Defaults to `__emptydoc__`
		* @param {string} attachKey key of the attachment
		* @returns {promise} fulfilled with the attachment or
		* rejected if it could not be found.  code: 1
		*/
		getAttachment: function(docKey, attachKey) {
			if (!docKey) docKey = '__emptydoc__';
			this._checkAvailability();
			return this._impl.getAttachment(docKey, attachKey);
		},

		/**
		* Set an attachment for a given document.  Identified
		* by `docKey` and `attachKey`.
		*
		* @example
		* 	storage.setAttachment('myDoc', 'myPic', blob).then(function() {
		*    	alert('Attachment written');
		* 	})
		*
		* @method setAttachment
		* @param {string} [docKey] Defaults to `__emptydoc__`
		* @param {string} attachKey key for the attachment
		* @param {any} attachment data
		* @returns {promise} resolved when the write completes.  Rejected
		* if an error occurs.
		*/
		setAttachment: function(docKey, attachKey, data) {
			if (!docKey) docKey = '__emptydoc__';
			this._checkAvailability();
			return this._impl.setAttachment(docKey, attachKey, data);
		},

		/**
		* Get the URL for a given attachment.
		*
		* @example
		* 	storage.getAttachmentURL('myDoc', 'myPic').then(function(url) {
	 	*   	var image = new Image();
	 	*   	image.src = url;
	 	*   	document.body.appendChild(image);
	 	*   	storage.revokeAttachmentURL(url);
		* 	})
		*
		* This is preferrable to getting the attachment and then getting the
		* URL via `createObjectURL` (on some systems) as LLS can take advantage of 
		* lower level details to improve performance.
		*
		* @method getAttachmentURL
		* @param {string} [docKey] Identifies the document.  Defaults to `__emptydoc__`
		* @param {string} attachKey Identifies the attachment.
		* @returns {promose} promise that is resolved with the attachment url.
		*/
		getAttachmentURL: function(docKey, attachKey) {
			if (!docKey) docKey = '__emptydoc__';
			this._checkAvailability();
			return this._impl.getAttachmentURL(docKey, attachKey);
		},

		/**
		* Gets all of the attachments for a document.
		*
		* @example
		* 	storage.getAllAttachments('exampleDoc').then(function(attachEntries) {
		* 		attachEntries.map(function(entry) {
		*			var a = entry.data;
		*			// do something with it...
		* 			if (a.type.indexOf('image') == 0) {
		*				// show image...
		*			} else if (a.type.indexOf('audio') == 0) {
		*				// play audio...
		*			} else ...
		*		})
		* 	})
		*
		* @method getAllAttachments
		* @param {string} [docKey] Identifies the document.  Defaults to `__emptydoc__`
		* @returns {promise} Promise that is resolved with all of the attachments for
		* the given document.
		*/
		getAllAttachments: function(docKey) {
			if (!docKey) docKey = '__emptydoc__';
			this._checkAvailability();
			return this._impl.getAllAttachments(docKey);
		},

		/**
		* Gets all attachments URLs for a document.
		*
		* @example
		* 	storage.getAllAttachmentURLs('exampleDoc').then(function(urlEntries) {
		*		urlEntries.map(function(entry) {
		*			var url = entry.url;
		* 			// do something with the url...
		* 		})
		* 	})
		*
		* @method getAllAttachmentURLs
		* @param {string} [docKey] Identifies the document.  Defaults to the `__emptydoc__` document.
		* @returns {promise} Promise that is resolved with all of the attachment
		* urls for the given doc.
		*/
		getAllAttachmentURLs: function(docKey) {
			if (!docKey) docKey = '__emptydoc__';
			this._checkAvailability();
			return this._impl.getAllAttachmentURLs(docKey);
		},

		/**
		* Revoke the attachment URL as required by the underlying
		* storage system.
		*
		* This is akin to `URL.revokeObjectURL(url)`
		* URLs that come from `getAttachmentURL` or `getAllAttachmentURLs` 
		* should be revoked by LLS and not `URL.revokeObjectURL`
		*
		* @example
		* 	storage.getAttachmentURL('doc', 'attach').then(function(url) {
		*		// do something with the URL
		*		storage.revokeAttachmentURL(url);
		* 	})
		*
		* @method revokeAttachmentURL
		* @param {string} url The URL as returned by `getAttachmentURL` or `getAttachmentURLs`
		* @returns {void}
		*/
		revokeAttachmentURL: function(url) {
			this._checkAvailability();
			return this._impl.revokeAttachmentURL(url);
		},

		/**
		* Remove an attachment from a document.
		*
		* @example
		* 	storage.rmAttachment('exampleDoc', 'someAttachment').then(function() {
		* 		alert('exampleDoc/someAttachment removed');
		* 	}).catch(function(e) {
		*		alert('Attachment removal failed: ' + e);
		* 	});
		*
		* @method rmAttachment
		* @param {string} docKey
		* @param {string} attachKey
		* @returns {promise} Promise that is resolved once the remove completes
		*/
		rmAttachment: function(docKey, attachKey) {
			if (!docKey) docKey = '__emptydoc__';
			this._checkAvailability();
			return this._impl.rmAttachment(docKey, attachKey);
		},

		/**
		* Returns the actual capacity of the storage or -1
		* if it is unknown.  If the user denies your request for
		* storage you'll get back some smaller amount of storage than what you
		* actually requested.
		*
		* TODO: return an estimated capacity if actual capacity is unknown?
		* -Firefox is 50MB until authorized to go above,
		* -Chrome is some % of available disk space,
		* -Safari unlimited as long as the user keeps authorizing size increases
		* -Opera same as safari?
		*
		* @example
		*	// the initialized property will call you back with the capacity
		* 	storage.initialized.then(function(capacity) {
		*		console.log('Authorized to store: ' + capacity + ' bytes');
		* 	});
		*	// or if you know your storage is already available
		*	// you can call getCapacity directly
		*	storage.getCapacity()
		*
		* @method getCapacity
		* @returns {number} Capacity, in bytes, of the storage.  -1 if unknown.
		*/
		getCapacity: function() {
			this._checkAvailability();
			if (this._impl.getCapacity)
				return this._impl.getCapacity();
			else
				return -1;
		},

		_checkAvailability: function() {
			if (!this._impl) {
				throw {
					msg: "No storage implementation is available yet.  The user most likely has not granted you app access to FileSystemAPI or IndexedDB",
					code: "NO_IMPLEMENTATION"
				};
			}
		}
	};

	LargeLocalStorage.contrib = {};

	function writeAttachments(docKey, attachments, storage) {
		var promises = [];
		attachments.forEach(function(attachment) {
			promises.push(storage.setAttachment(docKey, attachment.attachKey, attachment.data));
		});

		return Q.all(promises);
	}

	function copyDocs(docKeys, oldStorage, newStorage) {
		var promises = [];
		docKeys.forEach(function(key) {
			promises.push(oldStorage.getContents(key).then(function(contents) {
				return newStorage.setContents(key, contents);
			}));
		});

		docKeys.forEach(function(key) {
			promises.push(oldStorage.getAllAttachments(key).then(function(attachments) {
				return writeAttachments(key, attachments, newStorage);
			}));
		});

		return Q.all(promises);
	}

	LargeLocalStorage.copyOldData = function(err, oldStorage, newStorage, config) {
		if (err) {
			throw err;
		}

		oldStorage.ls().then(function(docKeys) {
			return copyDocs(docKeys, oldStorage, newStorage)
		}).then(function() {
			if (config.migrationComplete)
				config.migrationComplete();
		}, function(e) {
			config.migrationComplete(e);
		});
	};

	LargeLocalStorage._sessionMeta = sessionMeta;
	
	var availableProviders = [];
	Object.keys(providers).forEach(function(potentialProvider) {
		if (providers[potentialProvider].isAvailable())
			availableProviders.push(potentialProvider);
	});

	LargeLocalStorage.availableProviders = availableProviders;

	return LargeLocalStorage;
})(Q);

	return LargeLocalStorage;
}

if (typeof define === 'function' && define.amd) {
	define(['Q'], definition);
} else {
	glob.LargeLocalStorage = definition.call(glob, Q);
}

}).call(this, this);

Author comment

From http://tantaman.github.io/LargeLocalStorage/dist/LargeLocalStorage.js

download  show line numbers   

Travelled to 12 computer(s): aoiabmzegqzx, bhatertpkbcr, cbybwowwnfue, gwrvuhgaqvyk, irmadwmeruwu, ishqpsrjomds, lpdgvwnxivlt, mqqgnosmbjvj, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, vouqrxazstgt

No comments. add comment

Snippet ID: #1018990
Snippet name: LargeLocalStorage.js
Eternal ID of this version: #1018990/2
Text MD5: 9e71c922ae158003f28ddc3f96b577d3
Author: stefan
Category: javax / web
Type: Document
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2018-10-19 16:43:38
Source code size: 50103 bytes / 1889 lines
Pitched / IR pitched: No / No
Views / Downloads: 288 / 97
Version history: 1 change(s)
Referenced in: [show references]