UNPKG

pixl-server-storage

Version:

A key/value/list storage component for the pixl-server framework.

1,164 lines (1,023 loc) 33.4 kB
// PixlServer Storage System - List Mixin // Copyright (c) 2015 Joseph Huckaby // Released under the MIT License var util = require("util"); var async = require('async'); var Class = require("pixl-class"); var Tools = require("pixl-tools"); var ListSplice = require("./list-splice.js"); // support for older node versions var isArray = Array.isArray || util.isArray; module.exports = Class.create({ __mixins: [ ListSplice ], listCreate: function(key, opts, callback) { // Create new list var self = this; if (!opts) opts = {}; if (!opts.page_size) opts.page_size = this.listItemsPerPage; opts.first_page = 0; opts.last_page = 0; opts.length = 0; opts.type = 'list'; this.logDebug(9, "Creating new list: " + key, opts); this.get(key, function(err, list) { if (list) { // list already exists return callback(null, list); } self.put( key, opts, function(err) { if (err) return callback(err); // create first page self.put( key + '/0', { type: 'list_page', items: [] }, function(err) { if (err) return callback(err); else callback(null, opts); } ); } ); // header created } ); // get check }, _listLoad: function(key, create_opts, callback) { // Internal method, load list root, create if doesn't exist var self = this; if (create_opts && (typeof(create_opts) != 'object')) create_opts = {}; this.logDebug(9, "Loading list: " + key); this.get(key, function(err, data) { if (data) { // list already exists callback(null, data); } else if (create_opts && err && (err.code == "NoSuchKey")) { // create new list, ONLY if record was not found (and not some other error) self.logDebug(9, "List not found, creating it: " + key, create_opts); self.listCreate(key, create_opts, function(err, data) { if (err) callback(err, null); else callback( null, data ); } ); } else { // no exist and no create, or some other error self.logDebug(9, "List could not be loaded: " + key + ": " + err); callback(err, null); } } ); // get }, _listLoadPage: function(key, idx, create, callback) { // Internal method, load page from list, create if doesn't exist var self = this; var page_key = key + '/' + idx; this.logDebug(9, "Loading list page: " + page_key); this.get(page_key, function(err, data) { if (data) { // list page already exists callback(null, data); } else if (create && err && (err.code == "NoSuchKey")) { // create new list page, ONLY if record was not found (and not some other error) self.logDebug(9, "List page not found, creating it: " + page_key); callback( null, { type: 'list_page', items: [] } ); } else { // no exist and no create self.logDebug(9, "List page could not be loaded: " + page_key + ": " + err); callback(err, null); } } ); // get }, _listLock: function(key, wait, callback) { // internal list lock wrapper // uses unique key prefix so won't deadlock with user locks this.lock( '|'+key, wait, callback ); }, _listUnlock: function(key) { // internal list unlock wrapper this.unlock( '|'+key ); }, _listShareLock: function(key, wait, callback) { // internal list shared lock wrapper // uses unique key prefix so won't deadlock with user locks this.shareLock( 'C|'+key, wait, callback ); }, _listShareUnlock: function(key) { // internal list shared unlock wrapper this.shareUnlock( 'C|'+key ); }, listPush: function(key, items, create_opts, callback) { // Push new items onto end of list var self = this; if (!callback && (typeof(create_opts) == 'function')) { callback = create_opts; create_opts = {}; } var list = null; var page = null; if (!isArray(items)) items = [items]; this.logDebug(9, "Pushing " + items.length + " items onto end of list: " + key, this.debugLevel(10) ? items : null); this._listLock(key, true, function() { async.series([ function(callback) { // first load list header self._listLoad(key, create_opts, function(err, data) { list = data; callback(err, data); } ); }, function(callback) { // now load last page in list self._listLoadPage(key, list.last_page, 'create', function(err, data) { page = data; callback(err, data); } ); } ], function(err, results) { // list and page loaded, proceed with push if (err) { self._listUnlock(key); return callback(err, null); } // populate tasks array with records to save var tasks = []; // split items into pages var item = null; var count = 0; while (item = items.shift()) { // make sure item is an object if (typeof(item) != 'object') continue; // if last page is full, we need to create a new one if (page.items.length >= list.page_size) { // complete current page, queue for save if (count) tasks.push({ key: key + '/' + list.last_page, data: page }); // add new page list.last_page++; page = { type: 'list_page', items: [] }; } // push item onto list page.items.push( item ); list.length++; count++; } // foreach item if (!count) { self._listUnlock(key); return callback(new Error("No valid objects found to add."), null); } // add current page, and main list record tasks.push({ key: key + '/' + list.last_page, data: page }); tasks.push({ key: key, data: list }); // save all pages and main list var lastErr = null; var q = async.queue(function (task, callback) { self.put( task.key, task.data, callback ); }, self.concurrency ); q.drain = function() { // all pages saved, complete self._listUnlock(key); callback(lastErr, list); }; q.push( tasks, function(err) { lastErr = err; } ); } ); // loaded } ); // locked }, listUnshift: function(key, items, create_opts, callback) { // Unshift new items onto beginning of list var self = this; if (!callback && (typeof(create_opts) == 'function')) { callback = create_opts; create_opts = {}; } var list = null; var page = null; if (!isArray(items)) items = [items]; this.logDebug(9, "Unshifting " + items.length + " items onto beginning of list: " + key, this.debugLevel(10) ? items : null); this._listLock( key, true, function() { async.series([ function(callback) { // first load list header self._listLoad(key, create_opts, function(err, data) { list = data; callback(err, data); } ); }, function(callback) { // now load first page in list self._listLoadPage(key, list.first_page, 'create', function(err, data) { page = data; callback(err, data); } ); } ], function(err, results) { // list and page loaded, proceed with unshift if (err) { self._listUnlock(key); return callback(err, null); } // populate tasks array with records to save var tasks = []; // split items into pages var item = null; var count = 0; while (item = items.pop()) { // make sure item is an object if (typeof(item) != 'object') continue; // if last page is full, we need to create a new one if (page.items.length >= list.page_size) { // complete current page, queue for save if (count) tasks.push({ key: key + '/' + list.first_page, data: page }); // add new page list.first_page--; page = { type: 'list_page', items: [] }; } // push item onto list page.items.unshift( item ); list.length++; count++; } // foreach item if (!count) { self._listUnlock(key); return callback(new Error("No valid objects found to add."), null); } // add current page, and main list record tasks.push({ key: key + '/' + list.first_page, data: page }); tasks.push({ key: key, data: list }); // save all pages and main list var lastErr = null; var q = async.queue(function (task, callback) { self.put( task.key, task.data, callback ); }, self.concurrency ); q.drain = function() { // all pages saved, complete self._listUnlock(key); callback(lastErr, list); }; q.push( tasks, function(err) { lastErr = err; } ); } ); // loaded } ); // locked }, listPop: function(key, callback) { // Pop last item off end of list, shrink as necessary, return item var self = this; var list = null; var page = null; this.logDebug(9, "Popping item off end of list: " + key); this._listLock( key, true, function() { async.series([ function(callback) { // first load list header self._listLoad(key, false, function(err, data) { list = data; callback(err, data); } ); }, function(callback) { // now load last page in list self._listLoadPage(key, list.last_page, false, function(err, data) { page = data; callback(err, data); } ); } ], function(err, results) { // list and page loaded, proceed with pop if (err) { self._listUnlock(key); return callback(err, null); } if (!page.items.length) { self._listUnlock(key); return callback( null, null ); } var actions = []; var item = page.items.pop(); var old_last_page = list.last_page; if (!page.items.length) { // out of items in this page, delete page, adjust list if (list.last_page > list.first_page) { list.last_page--; actions.push( function(callback) { self.delete( key + '/' + old_last_page, callback ); } ); } else { // list is empty, create new first page actions.push( function(callback) { self.put( key + '/' + old_last_page, { type: 'list_page', items: [] }, callback ); } ); } } else { // still have items left, save page actions.push( function(callback) { self.put( key + '/' + list.last_page, page, callback ); } ); } // shrink list list.length--; actions.push( function(callback) { self.put( key, list, callback ); } ); // save everything in parallel async.parallel( actions, function(err, results) { // success, fire user callback self._listUnlock(key); callback(err, err ? null : item); } ); // save complete } ); // loaded } ); // locked }, listShift: function(key, callback) { // Shift first item off beginning of list, shrink as necessary, return item var self = this; var list = null; var page = null; this.logDebug(9, "Shifting item off beginning of list: " + key); this._listLock( key, true, function() { async.series([ function(callback) { // first load list header self._listLoad(key, false, function(err, data) { list = data; callback(err, data); } ); }, function(callback) { // now load first page in list self._listLoadPage(key, list.first_page, false, function(err, data) { page = data; callback(err, data); } ); } ], function(err, results) { // list and page loaded, proceed with shift if (err) { self._listUnlock(key); return callback(err, null); } if (!page.items.length) { self._listUnlock(key); return callback( null, null ); } var actions = []; var item = page.items.shift(); var old_first_page = list.first_page; if (!page.items.length) { // out of items in this page, delete page, adjust list if (list.first_page < list.last_page) { list.first_page++; actions.push( function(callback) { self.delete( key + '/' + old_first_page, callback ); } ); } else { // list is empty, create new first page actions.push( function(callback) { self.put( key + '/' + old_first_page, { type: 'list_page', items: [] }, callback ); } ); } } else { // still have items left, save page actions.push( function(callback) { self.put( key + '/' + list.first_page, page, callback ); } ); } // shrink list list.length--; actions.push( function(callback) { self.put( key, list, callback ); } ); // save everything in parallel async.parallel( actions, function(err, results) { // success, fire user callback self._listUnlock(key); callback(err, err ? null : item); } ); // save complete } ); // loaded } ); // locked }, listGet: function(key, idx, len, callback) { // Fetch chunk from list of any size, in any location // Use negative idx to fetch from end of list var self = this; var list = null; var page = null; var items = []; if (!this.started) return callback( new Error("Storage has not completed startup.") ); idx = parseInt( idx || 0 ); if (isNaN(idx)) return callback( new Error("Position must be an integer.") ); len = parseInt( len || 0 ); if (isNaN(len)) return callback( new Error("Length must be an integer.") ); this.logDebug(9, "Fetching " + len + " items at position " + idx + " from list: " + key); async.series([ function(callback) { // first we share lock self._listShareLock(key, true, callback); }, function(callback) { // next load list header self._listLoad(key, false, function(err, data) { list = data; callback(err, data); } ); }, function(callback) { // now load first page in list self._listLoadPage(key, list.first_page, false, function(err, data) { page = data; callback(err, data); } ); } ], function(err, results) { // list and page loaded, proceed with get if (err) { self._listShareUnlock(key); return callback(err, null, list); } // apply defaults if applicable if (!idx) idx = 0; if (!len) len = list.length; // range check if (list.length && (idx >= list.length)) { self._listShareUnlock(key); return callback( new Error("Index out of range"), null, list ); } // Allow user to get items from end of list if (idx < 0) { idx += list.length; } if (idx < 0) { idx = 0; } if (idx + len > list.length) { len = list.length - idx; } // First page is special, as it is variably sized // and shifts the paging algorithm while (idx < page.items.length) { items.push( page.items[idx++] ); len--; if (!len) break; } if (!len || (idx >= list.length)) { // all items were on first page, return now self._listShareUnlock(key); return callback( null, items, list ); } // we need items from other pages var num_fp_items = page.items.length; var chunk_size = list.page_size; var first_page_needed = list.first_page + 1 + Math.floor((idx - num_fp_items) / chunk_size); var last_page_needed = list.first_page + 1 + Math.floor(((idx - num_fp_items) + len - 1) / chunk_size); var page_idx = first_page_needed; async.whilst( function() { return page_idx <= last_page_needed; }, function(callback) { self._listLoadPage(key, page_idx, false, function(err, data) { if (err) return callback(err); var page = data; var page_start_idx = num_fp_items + ((page_idx - list.first_page - 1) * chunk_size); var local_idx = idx - page_start_idx; while ((local_idx >= 0) && (local_idx < page.items.length)) { items.push( page.items[local_idx++] ); idx++; len--; if (!len) break; } if (!len) page_idx = last_page_needed; page_idx++; callback(); } ); }, function(err) { // all pages loaded self._listShareUnlock(key); if (err) return callback(err, null); callback( null, items, list ); } ); // pages loaded } ); // list loaded }, listFind: function(key, criteria, callback) { // Find single item in list given criteria -- WARNING: this can be slow with long lists var self = this; var num_crit = Tools.numKeys(criteria); this.logDebug(9, "Locating item in list: " + key, criteria); this._listShareLock(key, true, function() { // share locked self._listLoad(key, false, function(err, list) { // list loaded, proceed if (err) { self._listShareUnlock(key); return callback(err, null); } var item = null; var item_idx = 0; var page_idx = list.first_page; if (!list.length) { self._listShareUnlock(key); return callback(null, null); } async.whilst( function() { return page_idx <= list.last_page; }, function(callback) { self._listLoadPage(key, page_idx, false, function(err, page) { if (err) return callback(err, null); // now scan page's items for (var idx = 0, len = page.items.length; idx < len; idx++) { var matches = 0; for (var k in criteria) { if (criteria[k].test) { if (criteria[k].test(page.items[idx][k])) { matches++; } } else if (criteria[k] == page.items[idx][k]) { matches++; } } if (matches == num_crit) { // we found our item! item = page.items[idx]; idx = len; page_idx = list.last_page; } else item_idx++; } // foreach item page_idx++; callback(); } ); // page loaded }, function(err) { // all pages loaded self._listShareUnlock(key); if (err) return callback(err, null); if (!item) item_idx = -1; callback( null, item, item_idx ); } ); // whilst } ); // loaded } ); // _listShareLock }, listFindCut: function(key, criteria, callback) { // Find single object by criteria, and if found, delete it -- WARNING: this can be slow with long lists var self = this; // This is a two-part macro function, which performs a find followed by a splice, // so we need an outer lock that lasts the entire duration of both ops, but we can't collide // with the natural lock that splice invokes, so we must add an additional '|' lock prefix. this._listLock( '|'+key, true, function() { self.listFind(key, criteria, function(err, item, idx) { if (err) { self._listUnlock( '|'+key ); return callback(err, null); } if (!item) { self._listUnlock( '|'+key ); return callback(new Error("Item not found"), null); } self.listSplice(key, idx, 1, null, function(err, items) { self._listUnlock( '|'+key ); callback(err, items ? items[0] : null); }); // splice } ); // find } ); // locked }, listFindDelete: function(key, criteria, callback) { // alias for listFindCut return this.listFindCut(key, criteria, callback); }, listFindReplace: function(key, criteria, new_item, callback) { // Find single object by criteria, and if found, replace it -- WARNING: this can be slow with long lists var self = this; // This is a two-part macro function, which performs a find followed by a splice, // so we need an outer lock that lasts the entire duration of both ops, but we can't collide // with the natural lock that splice invokes, so we must add an additional '|' lock prefix. this._listLock( '|'+key, true, function() { self.listFind(key, criteria, function(err, item, idx) { if (err) { self._listUnlock( '|'+key ); return callback(err, null); } if (!item) { self._listUnlock( '|'+key ); return callback(new Error("Item not found"), null); } self.listSplice(key, idx, 1, [new_item], function(err, items) { self._listUnlock( '|'+key ); callback(err); }); // splice } ); // find } ); // locked }, listFindUpdate: function(key, criteria, updates, callback) { // Find single object by criteria, and if found, update it -- WARNING: this can be slow with long lists // Updates are merged into original item, with numerical increments starting with "+" or "-" var self = this; // This is a two-part macro function, which performs a find followed by a splice, // so we need an outer lock that lasts the entire duration of both ops, but we can't collide // with the natural lock that splice invokes, so we must add an additional '|' lock prefix. this._listLock( '|'+key, true, function() { self.listFind(key, criteria, function(err, item, idx) { if (err) { self._listUnlock( '|'+key ); return callback(err, null); } if (!item) { self._listUnlock( '|'+key ); return callback(new Error("Item not found"), null); } // apply updates for (var ukey in updates) { var uvalue = updates[ukey]; if ((typeof(uvalue) == 'string') && (typeof(item[ukey]) == 'number') && uvalue.match(/^(\+|\-)([\d\.]+)$/)) { var op = RegExp.$1; var amt = parseFloat(RegExp.$2); if (op == '+') item[ukey] += amt; else item[ukey] -= amt; } else item[ukey] = uvalue; } self.listSplice(key, idx, 1, [item], function(err, items) { self._listUnlock( '|'+key ); callback(err, item); }); // splice } ); // find } ); // locked }, listFindEach: function(key, criteria, iterator, callback) { // fire iterator for every matching element in list, only load one page at a time var self = this; var num_crit = Tools.numKeys(criteria); this.logDebug(9, "Locating items in list: " + key, criteria); this._listShareLock(key, true, function() { // share locked self._listLoad(key, false, function(err, list) { // list loaded, proceed if (err) { self._listShareUnlock(key); callback(err); return; } var page_idx = list.first_page; var item_idx = 0; async.whilst( function() { return page_idx <= list.last_page; }, function(callback) { // load each page self._listLoadPage(key, page_idx++, false, function(err, page) { if (err) return callback(err); // iterate over page items if (page && page.items && page.items.length) { async.eachSeries( page.items, function(item, callback) { // for each item, check against criteria var matches = 0; for (var k in criteria) { if (criteria[k].test) { if (criteria[k].test(item[k])) { matches++; } } else if (criteria[k] == item[k]) { matches++; } } if (matches == num_crit) { iterator(item, item_idx++, callback); } else { item_idx++; callback(); } }, callback ); } else callback(); } ); // page loaded }, function(err) { // all pages iterated self._listShareUnlock(key); if (err) return callback(err); else callback(null); } // pages complete ); // whilst } ); // loaded } ); // _listShareLock }, listDelete: function(key, entire, callback) { // Delete entire list and all pages var self = this; this.logDebug(9, "Deleting list: " + key); this._listLock( key, true, function() { // locked self._listLoad(key, false, function(err, list) { // list loaded, proceed if (err) { self._listUnlock(key); return callback(err, null); } var page_idx = list.first_page; if (!entire) page_idx++; // skip first page, will be rewritten async.whilst( function() { return page_idx <= list.last_page; }, function(callback) { // delete each page self.delete( key + '/' + page_idx, function(err, data) { page_idx++; return callback(err); } ); // delete }, function(err) { // all pages deleted if (err) { self._listUnlock(key); return callback(err, null); } // delete list itself, or just clear it? if (entire) { // delete entire list self.delete(key, function(err, data) { // final delete complete self._listUnlock(key); callback(err); } ); // deleted } // entire else { // zero list for reuse list.length = 0; list.first_page = 0; list.last_page = 0; self.put( key, list, function(err, data) { // finished saving list header if (err) { self._listUnlock(key); return callback(err); } // now save a blank first page self.put( key + '/0', { type: 'list_page', items: [] }, function(err, data) { // save complete self._listUnlock(key); callback(err); } ); // saved } ); // saved header } // reuse } // pages deleted ); // whilst } ); // loaded } ); // locked }, listGetInfo: function(key, callback) { // Return info about list (number of items, etc.) this._listLoad( key, false, callback ); }, listCopy: function(old_key, new_key, callback) { // Copy list to new path (and all pages) var self = this; this.logDebug(9, "Copying list: " + old_key + " to " + new_key); this._listLoad(old_key, false, function(err, list) { // list loaded, proceed if (err) { callback(err); return; } var page_idx = list.first_page; async.whilst( function() { return page_idx <= list.last_page; }, function(callback) { // load each page self._listLoadPage(old_key, page_idx, false, function(err, page) { if (err) return callback(err); // and copy it self.copy( old_key + '/' + page_idx, new_key + '/' + page_idx, function(err, data) { page_idx++; return callback(err); } ); // copy } ); // page loaded }, function(err) { // all pages copied if (err) return callback(err); // now copy list header self.copy(old_key, new_key, function(err, data) { // final copy complete callback(err); } ); // deleted } // pages copied ); // whilst } ); // loaded }, listRename: function(old_key, new_key, callback) { // Copy, then delete list (and all pages) var self = this; this.logDebug(9, "Renaming list: " + old_key + " to " + new_key); this.listCopy( old_key, new_key, function(err) { // copy complete, now delete old list if (err) return callback(err); self.listDelete( old_key, true, callback ); } ); // copied }, listEach: function(key, iterator, callback) { // fire iterator for every element in list, only load one page at a time var self = this; this._listShareLock(key, true, function() { // share locked self._listLoad(key, false, function(err, list) { // list loaded, proceed if (err) { self._listShareUnlock(key); callback(err); return; } var page_idx = list.first_page; var item_idx = 0; async.whilst( function() { return page_idx <= list.last_page; }, function(callback) { // load each page self._listLoadPage(key, page_idx++, false, function(err, page) { if (err) return callback(err); // iterate over page items if (page && page.items && page.items.length) { async.eachSeries( page.items, function(item, callback) { iterator(item, item_idx++, callback); }, callback ); } else callback(); } ); // page loaded }, function(err) { // all pages iterated self._listShareUnlock(key); if (err) return callback(err); else callback(null); } // pages complete ); // whilst } ); // loaded } ); // _listShareLock }, listEachPage: function(key, iterator, callback) { // fire iterator for every page in list var self = this; this._listShareLock(key, true, function() { // share locked self._listLoad(key, false, function(err, list) { // list loaded, proceed if (err) { self._listShareUnlock(key); callback(err); return; } var page_idx = list.first_page; var item_idx = 0; async.whilst( function() { return page_idx <= list.last_page; }, function(callback) { // load each page self._listLoadPage(key, page_idx++, false, function(err, page) { if (err) return callback(err); // call iterator for page items if (page && page.items && page.items.length) { iterator(page.items, callback); } else callback(); } ); // page loaded }, function(err) { // all pages iterated self._listShareUnlock(key); callback( err || null ); } // pages complete ); // whilst } ); // loaded } ); // _listShareLock }, listEachUpdate: function(key, iterator, callback) { // fire iterator for every element in list, only load one page at a time // iterator can signal that a change was made to any items, triggering an update var self = this; this._listLock(key, true, function() { // exclusively locked self._listLoad(key, false, function(err, list) { // list loaded, proceed if (err) { self._listUnlock(key); callback(err); return; } var page_idx = list.first_page; var item_idx = 0; async.whilst( function() { return page_idx <= list.last_page; }, function(callback) { // load each page var page_key = key + '/' + page_idx; self._listLoadPage(key, page_idx++, false, function(err, page) { if (err) return callback(err); // iterate over page items if (page && page.items && page.items.length) { var num_updated = 0; async.eachSeries( page.items, function(item, callback) { iterator(item, item_idx++, function(err, updated) { if (updated) num_updated++; callback(err); }); }, function(err) { if (err) return callback(err); if (num_updated) self.put( page_key, page, callback ); else callback(); } ); // async.eachSeries } else callback(); } ); // page loaded }, function(err) { // all pages iterated self._listUnlock(key); callback( err || null ); } // pages complete ); // whilst } ); // loaded } ); // _listLock }, listEachPageUpdate: function(key, iterator, callback) { // fire iterator for every page in list // iterator can signal that a change was made to any page, triggering an update var self = this; this._listLock(key, true, function() { // exclusively locked self._listLoad(key, false, function(err, list) { // list loaded, proceed if (err) { self._listUnlock(key); callback(err); return; } var page_idx = list.first_page; var item_idx = 0; async.whilst( function() { return page_idx <= list.last_page; }, function(callback) { // load each page var page_key = key + '/' + page_idx; self._listLoadPage(key, page_idx++, false, function(err, page) { if (err) return callback(err); // call iterator for page items if (page && page.items && page.items.length) { iterator(page.items, function(err, updated) { if (!err && updated) self.put( page_key, page, callback ); else callback(err); }); } else callback(); } ); // page loaded }, function(err) { // all pages iterated self._listUnlock(key); callback( err || null ); } // pages complete ); // whilst } ); // loaded } ); // _listLock }, listInsertSorted: function(key, insert_item, comparator, callback) { // insert item into list while keeping it sorted var self = this; var loc = false; if (isArray(comparator)) { // convert to closure var sort_key = comparator[0]; var sort_dir = comparator[1] || 1; comparator = function(a, b) { return( ((a[sort_key] < b[sort_key]) ? -1 : 1) * sort_dir ); }; } // This is a two-part macro function, which performs a find followed by a splice, // so we need an outer lock that lasts the entire duration of both ops, but we can't collide // with the natural lock that splice invokes, so we must add an additional '|' lock prefix. this._listLock( '|'+key, true, function() { // list is locked self.listEach( key, function(item, idx, callback) { // listEach iterator var result = comparator(insert_item, item); if (result < 0) { // our item should come before compared item, so splice here! loc = idx; callback("break"); } else callback(); }, // listEach iterator function(err) { // listEach complete // Ignoring error here, as we'll just create a new list if (loc !== false) { // found location, so perform non-removal splice self.listSplice( key, loc, 0, [insert_item], function(err) { self._listUnlock( '|'+key ); callback(err); } ); } else { // no suitable location found, so add to end of list self.listPush( key, insert_item, function(err) { self._listUnlock( '|'+key ); callback(err); } ); } } // listEach complete ); // listEach } ); // list locked } });