UNPKG

phaser4-rex-plugins

Version:
1,438 lines (1,228 loc) 370 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.rexcharactercacheplugin = factory()); })(this, (function () { 'use strict'; var EventEmitterMethods = { setEventEmitter(eventEmitter, EventEmitterClass) { if (EventEmitterClass === undefined) { EventEmitterClass = Phaser.Events.EventEmitter; // Use built-in EventEmitter class by default } this._privateEE = (eventEmitter === true) || (eventEmitter === undefined); this._eventEmitter = (this._privateEE) ? (new EventEmitterClass()) : eventEmitter; return this; }, destroyEventEmitter() { if (this._eventEmitter && this._privateEE) { this._eventEmitter.shutdown(); } return this; }, getEventEmitter() { return this._eventEmitter; }, on() { if (this._eventEmitter) { this._eventEmitter.on.apply(this._eventEmitter, arguments); } return this; }, once() { if (this._eventEmitter) { this._eventEmitter.once.apply(this._eventEmitter, arguments); } return this; }, off() { if (this._eventEmitter) { this._eventEmitter.off.apply(this._eventEmitter, arguments); } return this; }, emit(event) { if (this._eventEmitter && event) { this._eventEmitter.emit.apply(this._eventEmitter, arguments); } return this; }, addListener() { if (this._eventEmitter) { this._eventEmitter.addListener.apply(this._eventEmitter, arguments); } return this; }, removeListener() { if (this._eventEmitter) { this._eventEmitter.removeListener.apply(this._eventEmitter, arguments); } return this; }, removeAllListeners() { if (this._eventEmitter) { this._eventEmitter.removeAllListeners.apply(this._eventEmitter, arguments); } return this; }, listenerCount() { if (this._eventEmitter) { return this._eventEmitter.listenerCount.apply(this._eventEmitter, arguments); } return 0; }, listeners() { if (this._eventEmitter) { return this._eventEmitter.listeners.apply(this._eventEmitter, arguments); } return []; }, eventNames() { if (this._eventEmitter) { return this._eventEmitter.eventNames.apply(this._eventEmitter, arguments); } return []; }, }; const GameClass = Phaser.Game; var IsGame = function (object) { return (object instanceof GameClass); }; const SceneClass = Phaser.Scene; var IsSceneObject = function (object) { return (object instanceof SceneClass); }; var GetGame = function (object) { if ((object == null) || (typeof (object) !== 'object')) { return null; } else if (IsGame(object)) { return object; } else if (IsGame(object.game)) { return object.game; } else if (IsSceneObject(object)) { // object = scene object return object.sys.game; } else if (IsSceneObject(object.scene)) { // object = game object return object.scene.sys.game; } }; var GetWhiteFrame = function (game) { return GetGame(game).textures.getFrame('__WHITE'); }; var DynamicTextureClearRectangle = function (texture, x, y, width, height) { if (WhiteFrameWidth === undefined) { var whiteFrame = GetWhiteFrame(texture.manager.game); WhiteFrameWidth = whiteFrame.cutWidth; WhiteFrameHeight = whiteFrame.cutHeight; } texture.stamp('__WHITE', undefined, x, y, { scaleX: width / WhiteFrameWidth, scaleY: height / WhiteFrameHeight, originX: 0, originY: 0, erase: true, }); return texture; }; var WhiteFrameWidth; var WhiteFrameHeight; var Draw = function (frameName, callback, scope) { var index = this.getFrameIndex(frameName); if (index === -1) { index = this.getFrameIndex(undefined); } if (index === -1) { console.warn('Does not have free space.'); return this; } var tl = this.getTopLeftPosition(index), outerX = tl.x, outerY = tl.y, cellPadding = this.cellPadding, innerX = outerX + cellPadding, innerY = outerY + cellPadding; ClearFrame.call(this, outerX, outerY, this.outerCellWidth, this.outerCellHeight); var frameSize = { width: this.cellWidth, height: this.cellHeight }; var drawCallback = (this.useDynamicTexture) ? DrawDynamicTexture : DrawCanvasTexture; drawCallback.call(this, innerX, innerY, frameSize, callback, scope); // frameSize might be changed this.texture.add(frameName, 0, innerX, innerY, frameSize.width, frameSize.height); this.addFrameName(index, frameName); this.dirty = true; return this; }; var ClearFrame = function (x, y, width, height) { if (this.useDynamicTexture) { DynamicTextureClearRectangle(this.texture, x, y, width, height); } else { this.context.clearRect(x, y, width, height); } }; var DrawCanvasTexture = function (x, y, frameSize, callback, scope) { var context = this.context; context.save(); context.translate(x, y); // Draw cell if (scope) { callback.call(scope, this.canvas, context, frameSize); } else { callback(this.canvas, context, frameSize); } // frameSize might be changed context.restore(); }; var DrawDynamicTexture = function (x, y, frameSize, callback, scope) { var texture = this.texture; // Draw cell texture.camera.setScroll(-x, -y); if (scope) { callback.call(scope, texture, frameSize); } else { callback(texture, frameSize); } texture.camera.setScroll(0, 0); // frameSize might be changed }; var GetDisplayWidth = function (gameObject) { if (gameObject.displayWidth !== undefined) { return gameObject.displayWidth; } else { return gameObject.width; } }; var GetDisplayHeight = function (gameObject) { if (gameObject.displayHeight !== undefined) { return gameObject.displayHeight; } else { return gameObject.height; } }; var Paste = function (frameName, gameObject) { var drawCallback; if (this.useDynamicTexture) { var srcWidth = GetDisplayWidth(gameObject), srcHeight = GetDisplayHeight(gameObject); var scale; if ((srcWidth <= this.cellWidth) && (srcHeight <= this.cellHeight)) { scale = 1; } else { // Scale down and keep ratio scale = Math.max((srcWidth / this.cellWidth), (srcHeight / this.cellHeight)); } drawCallback = function (texture, frameSize) { var originXSave = gameObject.originX, originYSave = gameObject.originY; var scaleXSave = gameObject.scaleX, scaleYSave = gameObject.scaleY; gameObject .setOrigin(0, 0) .setScale(scale, scale); texture.draw(gameObject); gameObject .setOrigin(originXSave, originYSave) .setScale(scaleXSave, scaleYSave); frameSize.width = srcWidth / scale; frameSize.height = srcHeight / scale; }; } else { var srcCanvas = gameObject.canvas; if (!srcCanvas) { console.warn(`Can't get canvas of game object.`); return this; } var srcWidth = srcCanvas.width, srcHeight = srcCanvas.height; var dWidth, dHeight; if ((srcWidth <= this.cellWidth) && (srcHeight <= this.cellHeight)) { dWidth = srcWidth; dHeight = srcHeight; } else { // Scale down and keep ratio var scale = Math.max((srcWidth / this.cellWidth), (srcHeight / this.cellHeight)); dWidth = srcWidth / scale; dHeight = srcHeight / scale; } drawCallback = function (canvas, context, frameSize) { context.drawImage(srcCanvas, 0, 0, dWidth, dHeight); frameSize.width = dWidth; frameSize.height = dHeight; }; } this.draw(frameName, drawCallback); return this; }; var AddEmptyFrame = function (frameName, width, height) { if (width === undefined) { width = this.cellWidth; } if (height === undefined) { height = this.cellHeight; } var drawCallback; if (this.useDynamicTexture) { drawCallback = function (texture, frameSize) { frameSize.width = width; frameSize.height = height; }; } else { drawCallback = function (canvas, context, frameSize) { frameSize.width = width; frameSize.height = height; }; } this.draw(frameName, drawCallback); return this; }; var RemoveMethods = { // Remove a frame remove(frameName) { var index = this.getFrameIndex(frameName); if (index === -1) { return this; } this.addFrameName(index, undefined); this.texture.remove(frameName); // Don't clear canvas return this; }, // Remove all frames clear() { for (var i, cnt = this.frameNames.length; i < cnt; i++) { var frameName = this.frameNames[i]; if (frameName !== undefined) { this.addFrameName(index, undefined); this.texture.remove(frameName); } } return this; } }; var AddToBitmapFont = function () { var textureKey = this.texture.key; // Don't add a new font data, reuse current font data var cacheData = this.bitmapFontCache.get(textureKey); if (!cacheData) { cacheData = { data: { retroFont: true, font: textureKey, size: this.cellWidth, lineHeight: this.cellHeight, chars: {} }, texture: textureKey, frame: null, }; this.bitmapFontCache.add(textureKey, cacheData); } var charData = cacheData.data.chars; var letters = this.frameNames; for (var i = 0, cnt = letters.length; i < cnt; i++) { var char = letters[i]; if (char === undefined) { continue; } var frame = this.texture.get(char); var x = frame.cutX, y = frame.cutY, width = frame.cutWidth, height = frame.cutHeight; charData[char.charCodeAt(0)] = { x: x, y: y, width: width, height: height, centerX: x + (width / 2), centerY: y + (height / 2), xOffset: 0, yOffset: 0, xAdvance: width, data: {}, kerning: {}, u0: frame.u0, v0: frame.v0, u1: frame.u1, v1: frame.v1 }; } return this; }; var methods = { draw: Draw, paste: Paste, addEmptyFrame: AddEmptyFrame, addToBitmapFont: AddToBitmapFont, }; Object.assign( methods, RemoveMethods ); var CreateTexture = function (game, key, width, height, useDynamicTexture) { game = GetGame(game); if (useDynamicTexture === undefined) { useDynamicTexture = false; } var textureManager = game.textures; if (textureManager.exists(key)) { textureManager.remove(key); } var methodName = (useDynamicTexture) ? 'addDynamicTexture' : 'createCanvas'; return textureManager[methodName](key, width, height); }; const IsPlainObject$1 = Phaser.Utils.Objects.IsPlainObject; const GetValue$3 = Phaser.Utils.Objects.GetValue; class FrameManager { constructor(scene, key, width, height, cellWidth, cellHeight, fillColor, useDynamicTexture) { var columns, rows, cellPadding; if (IsPlainObject$1(key)) { var config = key; key = GetValue$3(config, 'key'); width = GetValue$3(config, 'width'); height = GetValue$3(config, 'height'); cellWidth = GetValue$3(config, 'cellWidth'); cellHeight = GetValue$3(config, 'cellHeight'); cellPadding = GetValue$3(config, 'cellPadding', 0); columns = GetValue$3(config, 'columns'); rows = GetValue$3(config, 'rows'); fillColor = GetValue$3(config, 'fillColor'); useDynamicTexture = GetValue$3(config, 'useDynamicTexture'); } else { if (typeof (fillColor) === 'boolean') { useDynamicTexture = fillColor; fillColor = undefined; } } if (cellWidth === undefined) { cellWidth = 64; } if (cellHeight === undefined) { cellHeight = 64; } if (cellPadding === undefined) { cellPadding = 0; } this.scene = scene; this.cellWidth = cellWidth; this.cellHeight = cellHeight; this.cellPadding = cellPadding; this.outerCellWidth = cellWidth + (cellPadding * 2); this.outerCellHeight = cellHeight + (cellPadding * 2); if (columns) { width = this.outerCellWidth * columns; } else { if (width === undefined) { width = 4096; } columns = Math.floor(width / this.outerCellWidth); } if (rows) { height = this.outerCellHeight * rows; } else { if (height === undefined) { height = 4096; } rows = Math.floor(height / this.outerCellHeight); } if (useDynamicTexture === undefined) { useDynamicTexture = false; } var game = GetGame(scene); this.useDynamicTexture = useDynamicTexture; this.texture = CreateTexture(game, key, width, height, useDynamicTexture); this.canvas = (useDynamicTexture) ? undefined : this.texture.getCanvas(); this.context = (useDynamicTexture) ? undefined : this.texture.getContext(); this.bitmapFontCache = game.cache.bitmapFont; if (fillColor !== undefined) { if (useDynamicTexture) { this.texture.fill(fillColor); } else { var context = this.context; context.fillStyle = fillColor; context.fillRect(0, 0, this.canvas.width, this.canvas.height); } } this.key = key; this.width = width; this.height = height; this.columns = columns; this.rows = rows; this.totalCount = this.columns * this.rows; this.fillColor = fillColor; this.frameNames = Array(this.totalCount); for (var i = 0, cnt = this.frameNames.length; i < cnt; i++) { this.frameNames[i] = undefined; } this.dirty = false; } destroy() { this.scene = undefined; this.texture = undefined; this.canvas = undefined; this.context = undefined; this.frameNames = undefined; this.bitmapFontCache = undefined; } getFrameIndex(frameName) { return this.frameNames.indexOf(frameName); } contains(frameName) { return this.getFrameIndex(frameName) !== -1; } addFrameName(index, frameName) { this.frameNames[index] = frameName; return this; } get isFull() { return this.getFrameIndex(undefined) === -1; } getTopLeftPosition(frameIndex, out) { if (out === undefined) { out = {}; } var columnIndex = frameIndex % this.columns; var rowIndex = Math.floor(frameIndex / this.columns); out.x = columnIndex * (this.cellWidth + (this.cellPadding * 2)); out.y = rowIndex * (this.cellHeight + (this.cellPadding * 2)); return out; } updateTexture() { if (this.useDynamicTexture) ; else { this.texture.refresh(); } this.dirty = false; return this; } } Object.assign( FrameManager.prototype, methods ); const GetValue$2 = Phaser.Utils.Objects.GetValue; var CreateFrameManager = function (scene, config) { var key = GetValue$2(config, 'key'); var cellWidth = GetValue$2(config, 'cellWidth', 32); var cellHeight = GetValue$2(config, 'cellHeight', 32); var maxCharacterCount = GetValue$2(config, 'maxCharacterCount', 4096); var colCount = Math.ceil(Math.sqrt(maxCharacterCount)); var rowCount = colCount; var width = cellWidth * colCount; var height = cellHeight * rowCount; var frameManager = new FrameManager(scene, key, width, height, cellWidth, cellHeight); return frameManager; }; var global$1 = (typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}); // shim for using process in browser // based off https://github.com/defunctzombie/node-process/blob/master/browser.js function defaultSetTimout() { throw new Error('setTimeout has not been defined'); } function defaultClearTimeout () { throw new Error('clearTimeout has not been defined'); } var cachedSetTimeout = defaultSetTimout; var cachedClearTimeout = defaultClearTimeout; if (typeof global$1.setTimeout === 'function') { cachedSetTimeout = setTimeout; } if (typeof global$1.clearTimeout === 'function') { cachedClearTimeout = clearTimeout; } function runTimeout(fun) { if (cachedSetTimeout === setTimeout) { //normal enviroments in sane situations return setTimeout(fun, 0); } // if setTimeout wasn't available but was latter defined if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { cachedSetTimeout = setTimeout; return setTimeout(fun, 0); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedSetTimeout(fun, 0); } catch(e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedSetTimeout.call(null, fun, 0); } catch(e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error return cachedSetTimeout.call(this, fun, 0); } } } function runClearTimeout(marker) { if (cachedClearTimeout === clearTimeout) { //normal enviroments in sane situations return clearTimeout(marker); } // if clearTimeout wasn't available but was latter defined if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { cachedClearTimeout = clearTimeout; return clearTimeout(marker); } try { // when when somebody has screwed with setTimeout but no I.E. maddness return cachedClearTimeout(marker); } catch (e){ try { // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally return cachedClearTimeout.call(null, marker); } catch (e){ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. // Some versions of I.E. have different rules for clearTimeout vs setTimeout return cachedClearTimeout.call(this, marker); } } } var queue = []; var draining = false; var currentQueue; var queueIndex = -1; function cleanUpNextTick() { if (!draining || !currentQueue) { return; } draining = false; if (currentQueue.length) { queue = currentQueue.concat(queue); } else { queueIndex = -1; } if (queue.length) { drainQueue(); } } function drainQueue() { if (draining) { return; } var timeout = runTimeout(cleanUpNextTick); draining = true; var len = queue.length; while(len) { currentQueue = queue; queue = []; while (++queueIndex < len) { if (currentQueue) { currentQueue[queueIndex].run(); } } queueIndex = -1; len = queue.length; } currentQueue = null; draining = false; runClearTimeout(timeout); } function nextTick(fun) { var args = new Array(arguments.length - 1); if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i]; } } queue.push(new Item(fun, args)); if (queue.length === 1 && !draining) { runTimeout(drainQueue); } } // v8 likes predictible objects function Item(fun, array) { this.fun = fun; this.array = array; } Item.prototype.run = function () { this.fun.apply(null, this.array); }; var title = 'browser'; var platform = 'browser'; var browser = true; var env = {}; var argv = []; var version = ''; // empty string to avoid regexp issues var versions = {}; var release = {}; var config = {}; function noop() {} var on = noop; var addListener = noop; var once = noop; var off = noop; var removeListener = noop; var removeAllListeners = noop; var emit = noop; function binding(name) { throw new Error('process.binding is not supported'); } function cwd () { return '/' } function chdir (dir) { throw new Error('process.chdir is not supported'); }function umask() { return 0; } // from https://github.com/kumavis/browser-process-hrtime/blob/master/index.js var performance = global$1.performance || {}; var performanceNow = performance.now || performance.mozNow || performance.msNow || performance.oNow || performance.webkitNow || function(){ return (new Date()).getTime() }; // generate timestamp or delta // see http://nodejs.org/api/process.html#process_process_hrtime function hrtime(previousTimestamp){ var clocktime = performanceNow.call(performance)*1e-3; var seconds = Math.floor(clocktime); var nanoseconds = Math.floor((clocktime%1)*1e9); if (previousTimestamp) { seconds = seconds - previousTimestamp[0]; nanoseconds = nanoseconds - previousTimestamp[1]; if (nanoseconds<0) { seconds--; nanoseconds += 1e9; } } return [seconds,nanoseconds] } var startTime = new Date(); function uptime() { var currentTime = new Date(); var dif = currentTime - startTime; return dif / 1000; } var process = { nextTick: nextTick, title: title, browser: browser, env: env, argv: argv, version: version, versions: versions, on: on, addListener: addListener, once: once, off: off, removeListener: removeListener, removeAllListeners: removeAllListeners, emit: emit, binding: binding, cwd: cwd, chdir: chdir, umask: umask, hrtime: hrtime, platform: platform, release: release, config: config, uptime: uptime }; var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } function getAugmentedNamespace(n) { if (n.__esModule) return n; var f = n.default; if (typeof f == "function") { var a = function a () { if (this instanceof a) { return Reflect.construct(f, arguments, this.constructor); } return f.apply(this, arguments); }; a.prototype = f.prototype; } else a = {}; Object.defineProperty(a, '__esModule', {value: true}); Object.keys(n).forEach(function (k) { var d = Object.getOwnPropertyDescriptor(n, k); Object.defineProperty(a, k, d.get ? d : { enumerable: true, get: function () { return n[k]; } }); }); return a; } function commonjsRequire(path) { throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.'); } var lokijs = {exports: {}}; var lokiIndexedAdapter = {exports: {}}; /* Loki IndexedDb Adapter (need to include this script to use it) Console Usage can be used for management/diagnostic, here are a few examples : adapter.getDatabaseList(); // with no callback passed, this method will log results to console adapter.saveDatabase('UserDatabase', JSON.stringify(myDb)); adapter.loadDatabase('UserDatabase'); // will log the serialized db to console adapter.deleteDatabase('UserDatabase'); */ var hasRequiredLokiIndexedAdapter; function requireLokiIndexedAdapter () { if (hasRequiredLokiIndexedAdapter) return lokiIndexedAdapter.exports; hasRequiredLokiIndexedAdapter = 1; (function (module, exports) { (function (root, factory) { { // Node, CommonJS-like module.exports = factory(); } }(commonjsGlobal, function () { return (function() { /** * Loki persistence adapter class for indexedDb. * This class fulfills abstract adapter interface which can be applied to other storage methods. * Utilizes the included LokiCatalog app/key/value database for actual database persistence. * Indexeddb is highly async, but this adapter has been made 'console-friendly' as well. * Anywhere a callback is omitted, it should return results (if applicable) to console. * IndexedDb storage is provided per-domain, so we implement app/key/value database to * allow separate contexts for separate apps within a domain. * * @example * var idbAdapter = new LokiIndexedAdapter('finance'); * * @constructor LokiIndexedAdapter * * @param {string} appname - (Optional) Application name context can be used to distinguish subdomains, 'loki' by default * @param {object=} options Configuration options for the adapter * @param {boolean} options.closeAfterSave Whether the indexedDB database should be closed after saving. */ function LokiIndexedAdapter(appname, options) { this.app = 'loki'; this.options = options || {}; if (typeof (appname) !== 'undefined') { this.app = appname; } // keep reference to catalog class for base AKV operations this.catalog = null; if (!this.checkAvailability()) { throw new Error('indexedDB does not seem to be supported for your environment'); } } /** * Used for closing the indexeddb database. */ LokiIndexedAdapter.prototype.closeDatabase = function () { if (this.catalog && this.catalog.db) { this.catalog.db.close(); this.catalog.db = null; } }; /** * Used to check if adapter is available * * @returns {boolean} true if indexeddb is available, false if not. * @memberof LokiIndexedAdapter */ LokiIndexedAdapter.prototype.checkAvailability = function() { if (typeof indexedDB !== 'undefined' && indexedDB) return true; return false; }; /** * Retrieves a serialized db string from the catalog. * * @example * // LOAD * var idbAdapter = new LokiIndexedAdapter('finance'); * var db = new loki('test', { adapter: idbAdapter }); * db.loadDatabase(function(result) { * console.log('done'); * }); * * @param {string} dbname - the name of the database to retrieve. * @param {function} callback - callback should accept string param containing serialized db string. * @memberof LokiIndexedAdapter */ LokiIndexedAdapter.prototype.loadDatabase = function(dbname, callback) { var appName = this.app; var adapter = this; // lazy open/create db reference so dont -need- callback in constructor if (this.catalog === null || this.catalog.db === null) { this.catalog = new LokiCatalog(function(cat) { adapter.catalog = cat; adapter.loadDatabase(dbname, callback); }); return; } // lookup up db string in AKV db this.catalog.getAppKey(appName, dbname, function(result) { if (typeof (callback) === 'function') { if (result.id === 0) { callback(null); return; } callback(result.val); } else { // support console use of api console.log(result.val); } }); }; // alias LokiIndexedAdapter.prototype.loadKey = LokiIndexedAdapter.prototype.loadDatabase; /** * Saves a serialized db to the catalog. * * @example * // SAVE : will save App/Key/Val as 'finance'/'test'/{serializedDb} * var idbAdapter = new LokiIndexedAdapter('finance'); * var db = new loki('test', { adapter: idbAdapter }); * var coll = db.addCollection('testColl'); * coll.insert({test: 'val'}); * db.saveDatabase(); // could pass callback if needed for async complete * * @param {string} dbname - the name to give the serialized database within the catalog. * @param {string} dbstring - the serialized db string to save. * @param {function} callback - (Optional) callback passed obj.success with true or false * @memberof LokiIndexedAdapter */ LokiIndexedAdapter.prototype.saveDatabase = function(dbname, dbstring, callback) { var appName = this.app; var adapter = this; function saveCallback(result) { if (result && result.success === true) { callback(null); } else { callback(new Error("Error saving database")); } if (adapter.options.closeAfterSave) { adapter.closeDatabase(); } } // lazy open/create db reference so dont -need- callback in constructor if (this.catalog === null || this.catalog.db === null) { this.catalog = new LokiCatalog(function(cat) { adapter.saveDatabase(dbname, dbstring, saveCallback); }); return; } // set (add/update) entry to AKV database this.catalog.setAppKey(appName, dbname, dbstring, saveCallback); }; // alias LokiIndexedAdapter.prototype.saveKey = LokiIndexedAdapter.prototype.saveDatabase; /** * Deletes a serialized db from the catalog. * * @example * // DELETE DATABASE * // delete 'finance'/'test' value from catalog * idbAdapter.deleteDatabase('test', function { * // database deleted * }); * * @param {string} dbname - the name of the database to delete from the catalog. * @param {function=} callback - (Optional) executed on database delete * @memberof LokiIndexedAdapter */ LokiIndexedAdapter.prototype.deleteDatabase = function(dbname, callback) { var appName = this.app; var adapter = this; // lazy open/create db reference and pass callback ahead if (this.catalog === null || this.catalog.db === null) { this.catalog = new LokiCatalog(function(cat) { adapter.catalog = cat; adapter.deleteDatabase(dbname, callback); }); return; } // catalog was already initialized, so just lookup object and delete by id this.catalog.getAppKey(appName, dbname, function(result) { var id = result.id; if (id !== 0) { adapter.catalog.deleteAppKey(id, callback); } else if (typeof (callback) === 'function') { callback({ success: true }); } }); }; // alias LokiIndexedAdapter.prototype.deleteKey = LokiIndexedAdapter.prototype.deleteDatabase; /** * Removes all database partitions and pages with the base filename passed in. * This utility method does not (yet) guarantee async deletions will be completed before returning * * @param {string} dbname - the base filename which container, partitions, or pages are derived * @memberof LokiIndexedAdapter */ LokiIndexedAdapter.prototype.deleteDatabasePartitions = function(dbname) { var self=this; this.getDatabaseList(function(result) { result.forEach(function(str) { if (str.startsWith(dbname)) { self.deleteDatabase(str); } }); }); }; /** * Retrieves object array of catalog entries for current app. * * @example * idbAdapter.getDatabaseList(function(result) { * // result is array of string names for that appcontext ('finance') * result.forEach(function(str) { * console.log(str); * }); * }); * * @param {function} callback - should accept array of database names in the catalog for current app. * @memberof LokiIndexedAdapter */ LokiIndexedAdapter.prototype.getDatabaseList = function(callback) { var appName = this.app; var adapter = this; // lazy open/create db reference so dont -need- callback in constructor if (this.catalog === null || this.catalog.db === null) { this.catalog = new LokiCatalog(function(cat) { adapter.catalog = cat; adapter.getDatabaseList(callback); }); return; } // catalog already initialized // get all keys for current appName, and transpose results so just string array this.catalog.getAppKeys(appName, function(results) { var names = []; for(var idx = 0; idx < results.length; idx++) { names.push(results[idx].key); } if (typeof (callback) === 'function') { callback(names); } else { names.forEach(function(obj) { console.log(obj); }); } }); }; // alias LokiIndexedAdapter.prototype.getKeyList = LokiIndexedAdapter.prototype.getDatabaseList; /** * Allows retrieval of list of all keys in catalog along with size * * @param {function} callback - (Optional) callback to accept result array. * @memberof LokiIndexedAdapter */ LokiIndexedAdapter.prototype.getCatalogSummary = function(callback) { this.app; var adapter = this; // lazy open/create db reference if (this.catalog === null || this.catalog.db === null) { this.catalog = new LokiCatalog(function(cat) { adapter.catalog = cat; adapter.getCatalogSummary(callback); }); return; } // catalog already initialized // get all keys for current appName, and transpose results so just string array this.catalog.getAllKeys(function(results) { var entries = []; var obj, size, oapp, okey, oval; for(var idx = 0; idx < results.length; idx++) { obj = results[idx]; oapp = obj.app || ''; okey = obj.key || ''; oval = obj.val || ''; // app and key are composited into an appkey column so we will mult by 2 size = oapp.length * 2 + okey.length * 2 + oval.length + 1; entries.push({ "app": obj.app, "key": obj.key, "size": size }); } if (typeof (callback) === 'function') { callback(entries); } else { entries.forEach(function(obj) { console.log(obj); }); } }); }; /** * LokiCatalog - underlying App/Key/Value catalog persistence * This non-interface class implements the actual persistence. * Used by the IndexedAdapter class. */ function LokiCatalog(callback) { this.db = null; this.initializeLokiCatalog(callback); } LokiCatalog.prototype.initializeLokiCatalog = function(callback) { var openRequest = indexedDB.open('LokiCatalog', 1); var cat = this; // If database doesn't exist yet or its version is lower than our version specified above (2nd param in line above) openRequest.onupgradeneeded = function(e) { var thisDB = e.target.result; if (thisDB.objectStoreNames.contains('LokiAKV')) { thisDB.deleteObjectStore('LokiAKV'); } if(!thisDB.objectStoreNames.contains('LokiAKV')) { var objectStore = thisDB.createObjectStore('LokiAKV', { keyPath: 'id', autoIncrement:true }); objectStore.createIndex('app', 'app', {unique:false}); objectStore.createIndex('key', 'key', {unique:false}); // hack to simulate composite key since overhead is low (main size should be in val field) // user (me) required to duplicate the app and key into comma delimited appkey field off object // This will allow retrieving single record with that composite key as well as // still supporting opening cursors on app or key alone objectStore.createIndex('appkey', 'appkey', {unique:true}); } }; openRequest.onsuccess = function(e) { cat.db = e.target.result; if (typeof (callback) === 'function') callback(cat); }; openRequest.onerror = function(e) { throw e; }; }; LokiCatalog.prototype.getAppKey = function(app, key, callback) { var transaction = this.db.transaction(['LokiAKV'], 'readonly'); var store = transaction.objectStore('LokiAKV'); var index = store.index('appkey'); var appkey = app + "," + key; var request = index.get(appkey); request.onsuccess = (function(usercallback) { return function(e) { var lres = e.target.result; if (lres === null || typeof(lres) === 'undefined') { lres = { id: 0, success: false }; } if (typeof(usercallback) === 'function') { usercallback(lres); } else { console.log(lres); } }; })(callback); request.onerror = (function(usercallback) { return function(e) { if (typeof(usercallback) === 'function') { usercallback({ id: 0, success: false }); } else { throw e; } }; })(callback); }; LokiCatalog.prototype.getAppKeyById = function (id, callback, data) { var transaction = this.db.transaction(['LokiAKV'], 'readonly'); var store = transaction.objectStore('LokiAKV'); var request = store.get(id); request.onsuccess = (function(data, usercallback){ return function(e) { if (typeof(usercallback) === 'function') { usercallback(e.target.result, data); } else { console.log(e.target.result); } }; })(data, callback); }; LokiCatalog.prototype.setAppKey = function (app, key, val, callback) { var transaction = this.db.transaction(['LokiAKV'], 'readwrite'); var store = transaction.objectStore('LokiAKV'); var index = store.index('appkey'); var appkey = app + "," + key; var request = index.get(appkey); // first try to retrieve an existing object by that key // need to do this because to update an object you need to have id in object, otherwise it will append id with new autocounter and clash the unique index appkey request.onsuccess = function(e) { var res = e.target.result; if (res === null || res === undefined) { res = { app:app, key:key, appkey: app + ',' + key, val:val }; } else { res.val = val; } var requestPut = store.put(res); requestPut.onerror = (function(usercallback) { return function(e) { if (typeof(usercallback) === 'function') { usercallback({ success: false }); } else { console.error('LokiCatalog.setAppKey (set) onerror'); console.error(request.error); } }; })(callback); requestPut.onsuccess = (function(usercallback) { return function(e) { if (typeof(usercallback) === 'function') { usercallback({ success: true }); } }; })(callback); }; request.onerror = (function(usercallback) { return function(e) { if (typeof(usercallback) === 'function') { usercallback({ success: false }); } else { console.error('LokiCatalog.setAppKey (get) onerror'); console.error(request.error); } }; })(callback); }; LokiCatalog.prototype.deleteAppKey = function (id, callback) { var transaction = this.db.transaction(['LokiAKV'], 'readwrite'); var store = transaction.objectStore('LokiAKV'); var request = store.delete(id); request.onsuccess = (function(usercallback) { return function(evt) { if (typeof(usercallback) === 'function') usercallback({ success: true }); }; })(callback); request.onerror = (function(usercallback) { return function(evt) { if (typeof(usercallback) === 'function') { usercallback({ success: false }); } else { console.error('LokiCatalog.deleteAppKey raised onerror'); console.error(request.error); } }; })(callback); }; LokiCatalog.prototype.getAppKeys = function(app, callback) { var transaction = this.db.transaction(['LokiAKV'], 'readonly'); var store = transaction.objectStore('LokiAKV'); var index = store.index('app'); // We want cursor to all values matching our (single) app param var singleKeyRange = IDBKeyRange.only(app); // To use one of the key ranges, pass it in as the first argument of openCursor()/openKeyCursor() var cursor = index.openCursor(singleKeyRange); // cursor internally, pushing results into this.data[] and return // this.data[] when done (similar to service) var localdata = []; cursor.onsuccess =