src/menu/v1beta1/storage.js
/*
* Copyright 2019, Momentum Ideas, Co. All rights reserved.
*
* Source and object computer code contained herein is the private intellectual
* property of Bloombox, a California Limited Liability Corporation. Use of this
* code in source form requires permission in writing before use or the
* assembly, distribution, or publishing of derivative works, for commercial
* purposes or any other purpose, from a duly authorized officer of Momentum
* Ideas Co.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Bloombox: Menu Storage
*
* @fileoverview Provides local support for caching, storage, indexing, and
* other rich stuff with regard to menu data.
*/
/*global goog */
goog.require('bloombox.DEBUG');
goog.require('bloombox.db.MENU_STORE');
goog.require('bloombox.db.acquire');
goog.require('goog.db');
goog.require('goog.db.IndexedDb');
goog.require('goog.db.ObjectStore');
goog.require('goog.pubsub.TopicId');
goog.require('goog.pubsub.TypedPubSub');
goog.require('proto.opencannabis.products.menu.MenuProduct');
goog.require('proto.opencannabis.products.menu.section.Section');
goog.provide('bloombox.menu.LocalMenuIndex');
goog.provide('bloombox.menu.LocalMenuProperty');
goog.provide('bloombox.menu.processMenu');
goog.provide('bloombox.menu.setupMenuDb');
/**
* Maps shortened property IDs to the properties they represent.
*
* @enum {!string}
*/
bloombox.menu.LocalMenuProperty = {
PAYLOAD: 'p',
MODIFIED: 'm',
KIND: 'k',
ID: 'i'
};
/**
* Index names enumerated to their shortened IDs.
*
* @enum {!string}
*/
bloombox.menu.LocalMenuIndex = {
ID: 'pid',
KIND: 'pkind'
};
goog.scope(function() {
/**
* Menu publish/subscribe feed. Receives emitted events for each product and
* section that changes, as they change.
*
* @type {goog.pubsub.TypedPubSub}
* @package
*/
bloombox.menu.feed = new goog.pubsub.TypedPubSub(true);
/**
* Enumerates menu pub/sub feed topics that other library code can subscribe
* to. This includes a topic for menu product changes, section changes, and
* featured products.
*
* @enum {string}
*/
bloombox.menu.FeedTopic = {
PRODUCTS: 'bb.products',
SECTIONS: 'bb.sections'
};
if (bloombox.DEBUG === true) {
// subscribe to the menu pub/sub feed
const rootTopic = /**
@type {!goog.pubsub.TopicId<*>} */ (
new goog.pubsub.TopicId(bloombox.menu.FeedTopic.SECTIONS));
bloombox.menu.feed.subscribe(rootTopic, function(event) {
// there was some menu event
bloombox.logging.log('Menu section event emitted over pubsub.',
{'event': event, 'topic': rootTopic});
});
}
/**
* Bag of types, which tracks each product type witnessed by this frontend. If
* it has not seen constituent products from a given section, it won't be
* here.
*
* @type {Set<number>}
* @package
*/
bloombox.menu._types = new Set();
/**
* Main products topic.
*
* @type {!goog.pubsub.TopicId<!proto.opencannabis.products.menu.MenuProduct>}
*/
const productsTopic = /**
@type {!goog.pubsub.TopicId<!proto.opencannabis.products.menu.MenuProduct>} */ (
new goog.pubsub.TopicId(bloombox.menu.FeedTopic.PRODUCTS));
/**
* Main sections topic.
*
* @type {!goog.pubsub.TopicId<!proto.opencannabis.products.menu.section.Section>}
*/
const sectionsTopic = /**
@type {!goog.pubsub.TopicId<!proto.opencannabis.products.menu.section.Section>} */ (
new goog.pubsub.TopicId(bloombox.menu.FeedTopic.SECTIONS));
/**
* Process a product that was retrieved via the API. We store it in any local
* caching mechanisms and also note the time it was last modified, at least
* according to our local data state. Additionally, perform any object-level
* indexing we wish to do.
*
* @param {proto.opencannabis.products.menu.MenuProduct} product Product that
* was fetched from the server, which we should process.
* @param {!goog.db.ObjectStore} store Local store to write to.
* @param {number} ts Timestamp to use for product writes.
* @param {boolean=} opt_keysOnly Flag to indicate the request was operating
* in keys only mode, so a payload is not expected.
*/
function processProduct(product, store, ts, opt_keysOnly) {
const key = product.getKey();
const keyId = key.getId();
const keyKind = key.getType();
// add to types index
bloombox.menu._types.add(keyKind);
// generate encoded key in b64
const encodedKey = bloombox.util.b64.encode(
keyKind.toString() + '::' + keyId);
// store in local DB first (write it down)
const data = product.serializeBinary();
const obj = {};
obj[bloombox.menu.LocalMenuProperty.ID] = keyId;
obj[bloombox.menu.LocalMenuProperty.MODIFIED] = ts;
obj[bloombox.menu.LocalMenuProperty.KIND] = keyKind;
if (!opt_keysOnly)
obj[bloombox.menu.LocalMenuProperty.PAYLOAD] = data;
store.put(obj, encodedKey);
const productSpecificTopic = /**
@type {!goog.pubsub.TopicId<!proto.opencannabis.products.menu.MenuProduct>} */ (
new goog.pubsub.TopicId(
[bloombox.menu.FeedTopic.PRODUCTS,
'sections',
keyKind.toString(),
'products',
keyId].join('/')));
bloombox.menu.feed.publish(productSpecificTopic, product);
bloombox.menu.feed.publish(productsTopic, product);
}
/**
* Process a collection of products fetched for a given section. This method
* does not expect the complete set of products for the section. For each
* constituent product, process it locally by adding it to caching and doing any
* indexing we need to do.
*
* @param {proto.opencannabis.products.menu.section.Section} section Catalog
* section we are processing in this invocation.
* @param {Array<proto.opencannabis.products.menu.MenuProduct>} products Items
* to process as constituent products in `section`.
* @param {!goog.db.ObjectStore} store Local store to write to.
* @param {number} ts Timestamp to use for writes.
* @param {boolean=} opt_keysOnly Flag to indicate that we are operating in
* keys-only mode, and so, we should not expect payloads back.
*/
function processSection(section, products, store, ts, opt_keysOnly) {
products.map((item) => {
processProduct(item, store, ts, opt_keysOnly);
});
bloombox.menu.feed.publish(sectionsTopic, section);
}
/**
* Process an entire menu catalog payload, which may contain one or more stanzas
* of data, by menu section/product type. Other metadata should be attached to
* the menu as well, like the current fingerprint value, last modified time, and
* so on.
*
* @param {proto.opencannabis.products.menu.Menu} menu Menu payload to process
* for local indexing and storage.
* @param {boolean=} opt_keysOnly Flag to indicate keys-only mode.
* @return {?goog.async.Deferred} Asynchronous operation to store menu.
*/
bloombox.menu.processMenu = function(menu, opt_keysOnly) {
if (!menu.hasPayload()) return null;
const sectioned = menu.getPayload();
bloombox.logging.log('Processing/indexing menu catalog...',
{'catalog': sectioned, 'count': sectioned.getCount()});
if (sectioned.getCount() < 1) return null;
const sections = sectioned.getPayloadList();
return bloombox.db.acquire((db) => {
if (db === null) return null;
const txn = db.createTransaction(
[bloombox.db.MENU_STORE, bloombox.db.DEFAULT_STORE],
goog.db.Transaction.TransactionMode.READ_WRITE);
const store = txn.objectStore(bloombox.db.MENU_STORE);
const root = txn.objectStore(bloombox.db.DEFAULT_STORE);
const ts = +(new Date());
const menuFingerprint = menu
.getMetadata()
.getSettings()
.getFingerprint()
.getHex();
const menuVersion = menu
.getMetadata()
.getVersion();
if (bloombox.menu.lastSeenFingerprint === menuFingerprint) {
// fingerprints match.
return txn.wait();
} else {
// fingerprints don't match. update it.
sections.map((payload) => {
let section = /**
@type {proto.opencannabis.products.menu.SectionData} */ (payload);
if (section.getCount() > 0) {
const productList = section.getProductList();
const sectionSpec = section.getSection();
// @TODO(sgammon) support for custom sections
if (sectionSpec.hasSection()) {
processSection(
sectionSpec.getSection(), productList, store, ts, opt_keysOnly);
}
}
});
// update fingerprint in local storage and in memory
root.put(menuFingerprint, 'catalog.fingerprint');
root.put(menuVersion, 'catalog.version');
root.put(ts, 'catalog.lastModified');
bloombox.menu.lastSeenFingerprint = menuFingerprint;
}
return txn.wait();
});
};
/**
* Setup the local menu object database, with any indexes it needs or other
* early configuration settings.
*
* @param {!goog.db.IndexedDb} db IndexedDB instance we are setting up.
* @param {!goog.db.ObjectStore} objectStore Object store we are setting up
* for use as local menu storage.
*/
bloombox.menu.setupMenuDb = function(db, objectStore) {
// create ID index
objectStore.createIndex(
bloombox.menu.LocalMenuIndex.ID,
bloombox.menu.LocalMenuProperty.ID,
{unique: true});
// create kind index
objectStore.createIndex(
bloombox.menu.LocalMenuIndex.KIND,
bloombox.menu.LocalMenuProperty.KIND,
{});
};
});