From 0aabc67774fdda57d0703b958b29c46cd2006566 Mon Sep 17 00:00:00 2001 From: dragonwocky Date: Sun, 7 Nov 2021 16:08:22 +1100 Subject: [PATCH] port to cjs --- api/_.cjs | 22 ++++ api/_.mjs | 2 +- api/components/panel.mjs | 19 +-- api/env.cjs | 47 ++++++++ api/fmt.cjs | 139 ++++++++++++++++++++++ api/fs.cjs | 49 ++++++++ api/fs.mjs | 2 +- api/registry-validation.cjs | 224 ++++++++++++++++++++++++++++++++++++ api/registry.cjs | 159 +++++++++++++++++++++++++ api/storage.cjs | 68 +++++++++++ 10 files changed, 722 insertions(+), 9 deletions(-) create mode 100644 api/_.cjs create mode 100644 api/env.cjs create mode 100644 api/fmt.cjs create mode 100644 api/fs.cjs create mode 100644 api/registry-validation.cjs create mode 100644 api/registry.cjs create mode 100644 api/storage.cjs diff --git a/api/_.cjs b/api/_.cjs new file mode 100644 index 0000000..c1424d7 --- /dev/null +++ b/api/_.cjs @@ -0,0 +1,22 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +/** @module notion-enhancer/api */ + +module.exports = { + /** environment-specific methods and constants */ + env: require('notion-enhancer/api/env.cjs'), + /** environment-specific file reading */ + fs: require('notion-enhancer/api/fs.cjs'), + /** environment-specific data persistence */ + storage: require('notion-enhancer/api/storage.cjs'), + /** helpers for formatting, validating and parsing values */ + fmt: require('notion-enhancer/api/fmt.cjs'), + /** interactions with the enhancer's repository of mods */ + registry: require('notion-enhancer/api/registry.cjs'), +}; diff --git a/api/_.mjs b/api/_.mjs index d970444..a81a82a 100644 --- a/api/_.mjs +++ b/api/_.mjs @@ -10,7 +10,7 @@ /** environment-specific methods and constants */ export * as env from './env.mjs'; -/** environment-specific filesystem reading */ +/** environment-specific file reading */ export * as fs from './fs.mjs'; /** environment-specific data persistence */ export * as storage from './storage.mjs'; diff --git a/api/components/panel.mjs b/api/components/panel.mjs index 5d44fc8..82ce3e3 100644 --- a/api/components/panel.mjs +++ b/api/components/panel.mjs @@ -13,7 +13,6 @@ */ import { fmt, web, components, registry } from '../_.mjs'; -const db = await registry.db('36a2ffc9-27ff-480e-84a7-c7700a7d232d'); web.loadStylesheet('api/components/panel.css'); @@ -27,8 +26,9 @@ const _views = [], 5.43056L 3.01191 8.43056L 3.98809 9.56944Z"> `; -// open + close -let $notionFrame, +let db, + // open + close + $notionFrame, $notionRightSidebar, // resize dragStartX, @@ -36,13 +36,11 @@ let $notionFrame, dragEventsFired, panelWidth, // render content - $notionApp; + $notionApp, + $pinnedToggle; // open + close const $panel = web.html`
`, - $pinnedToggle = web.html`
- ${await components.feather('chevrons-right')} -
`, $hoverTrigger = web.html`
`, panelPinnedAttr = 'data-enhancer-panel-pinned', isPinned = () => $panel.hasAttribute(panelPinnedAttr), @@ -258,6 +256,13 @@ export const addPanelView = async ({ onFocus = () => {}, onBlur = () => {}, }) => { + if (!db) db = await registry.db('36a2ffc9-27ff-480e-84a7-c7700a7d232d'); + if (!$pinnedToggle) { + $pinnedToggle = web.html`
+ ${await components.feather('chevrons-right')} +
`; + } + const view = { id, $icon: web.render( diff --git a/api/env.cjs b/api/env.cjs new file mode 100644 index 0000000..c328f19 --- /dev/null +++ b/api/env.cjs @@ -0,0 +1,47 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +/** + * environment-specific methods and constants + * @module notion-enhancer/api/env + */ + +const env = require('../env/env.cjs'); +module.exports = {}; + +/** + * the environment/platform name code is currently being executed in + * @constant + * @type {string} + */ +module.exports.name = env.name; + +/** + * the current version of the enhancer + * @constant + * @type {string} + */ +module.exports.version = env.version; + +/** + * open the enhancer's menu + * @type {function} + */ +module.exports.focusMenu = env.focusMenu; + +/** + * focus an active notion tab + * @type {function} + */ +module.exports.focusNotion = env.focusNotion; + +/** + * reload all notion and enhancer menu tabs to apply changes + * @type {function} + */ +module.exports.reload = env.reload; diff --git a/api/fmt.cjs b/api/fmt.cjs new file mode 100644 index 0000000..72b125d --- /dev/null +++ b/api/fmt.cjs @@ -0,0 +1,139 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +/** + * helpers for formatting or parsing text + * @module notion-enhancer/api/fmt + */ + +const fs = require('notion-enhancer/api/fs.cjs'); +module.exports = {}; + +/** + * transform a heading into a slug (a lowercase alphanumeric string separated by dashes), + * e.g. for use as an anchor id + * @param {string} heading - the original heading to be slugified + * @param {Set} [slugs] - a list of pre-generated slugs to avoid duplicates + * @returns {string} the generated slug + */ +module.exports.slugger = (heading, slugs = new Set()) => { + heading = heading + .replace(/\s/g, '-') + .replace(/[^A-Za-z0-9-_]/g, '') + .toLowerCase(); + let i = 0, + slug = heading; + while (slugs.has(slug)) { + i++; + slug = `${heading}-${i}`; + } + return slug; +}; + +/** + * generate a reasonably random uuidv4 string. uses crypto implementation if available + * (from https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid) + * @returns {string} a uuidv4 + */ +module.exports.uuidv4 = () => { + if (crypto?.randomUUID) return crypto.randomUUID(); + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => + (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) + ); +}; + +/** + * log-based shading of an rgb color, from + * https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors + * @param {number} p - a decimal amount to shade the color. + * 1 = white, 0 = the original color, -1 = black + * @param {string} c - the rgb color + * @returns {string} the shaded color + */ +module.exports.rgbLogShade = (p, c) => { + var i = parseInt, + r = Math.round, + [a, b, c, d] = c.split(','), + P = p < 0, + t = P ? 0 : p * 255 ** 2, + P = P ? 1 + p : 1 - p; + return ( + 'rgb' + + (d ? 'a(' : '(') + + r((P * i(a[3] == 'a' ? a.slice(5) : a.slice(4)) ** 2 + t) ** 0.5) + + ',' + + r((P * i(b) ** 2 + t) ** 0.5) + + ',' + + r((P * i(c) ** 2 + t) ** 0.5) + + (d ? ',' + d : ')') + ); +}; + +/** + * pick a contrasting color e.g. for text on a variable color background + * using the hsp (perceived brightness) constants from http://alienryderflex.com/hsp.html + * @param {number} r - red (0-255) + * @param {number} g - green (0-255) + * @param {number} b - blue (0-255) + * @returns {string} the contrasting rgb color, white or black + */ +module.exports.rgbContrast = (r, g, b) => { + return Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)) > 165.75 + ? 'rgb(0,0,0)' + : 'rgb(255,255,255)'; +}; + +const patterns = { + alphanumeric: /^[\w\.-]+$/, + uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, + semver: + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/i, + email: + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i, + url: /^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,64}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i, + color: /^(?:#|0x)(?:[a-f0-9]{3}|[a-f0-9]{6})\b|(?:rgb|hsl)a?\([^\)]*\)$/i, +}; +function test(str, pattern) { + const match = str.match(pattern); + return !!(match && match.length); +} + +/** + * test the type of a value. unifies builtin, regex, and environment/api checks + * @param {*} value - the value to check + * @param {string|array} type - the type the value should be or a list of allowed values + * @returns {boolean} whether or not the value matches the type + */ +module.exports.is = async (value, type, { extension = '' } = {}) => { + extension = !value || !value.endsWith || value.endsWith(extension); + if (Array.isArray(type)) { + return type.includes(value); + } + switch (type) { + case 'array': + return Array.isArray(value); + case 'object': + return value && typeof value === 'object' && !Array.isArray(value); + case 'undefined': + case 'boolean': + case 'number': + return typeof value === type && extension; + case 'string': + return typeof value === type && extension; + case 'alphanumeric': + case 'uuid': + case 'semver': + case 'email': + case 'url': + case 'color': + return typeof value === 'string' && test(value, patterns[type]) && extension; + case 'file': + return typeof value === 'string' && value && (await fs.isFile(value)) && extension; + } + return false; +}; diff --git a/api/fs.cjs b/api/fs.cjs new file mode 100644 index 0000000..376d6a4 --- /dev/null +++ b/api/fs.cjs @@ -0,0 +1,49 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +/** + * environment-specific file reading + * @module notion-enhancer/api/fs + */ + +const fs = require('../env/fs.cjs'); +module.exports = {}; + +/** + * transform a path relative to the enhancer root directory into an absolute path + * @type {function} + * @param {string} path - a url or within-the-enhancer filepath + * @returns {string} an absolute filepath + */ +module.exports.localPath = fs.localPath; + +/** + * fetch and parse a json file's contents + * @type {function} + * @param {string} path - a url or within-the-enhancer filepath + * @param {object} [opts] - the second argument of a fetch() request + * @returns {object} the json value of the requested file as a js object + */ +module.exports.getJSON = fs.getJSON; + +/** + * fetch a text file's contents + * @type {function} + * @param {string} path - a url or within-the-enhancer filepath + * @param {object} [opts] - the second argument of a fetch() request + * @returns {string} the text content of the requested file + */ +module.exports.getText = fs.getText; + +/** + * check if a file exists + * @type {function} + * @param {string} path - a url or within-the-enhancer filepath + * @returns {boolean} whether or not the file exists + */ +module.exports.isFile = fs.isFile; diff --git a/api/fs.mjs b/api/fs.mjs index eeb02df..e821d87 100644 --- a/api/fs.mjs +++ b/api/fs.mjs @@ -7,7 +7,7 @@ 'use strict'; /** - * environment-specific filesystem reading + * environment-specific file reading * @module notion-enhancer/api/fs */ diff --git a/api/registry-validation.cjs b/api/registry-validation.cjs new file mode 100644 index 0000000..6c522e9 --- /dev/null +++ b/api/registry-validation.cjs @@ -0,0 +1,224 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +const { fmt, registry } = require('notion-enhancer/api/_.cjs'); + +const check = async ( + mod, + key, + value, + types, + { + extension = '', + error = `invalid ${key} (${extension ? `${extension} ` : ''}${types}): ${JSON.stringify( + value + )}`, + optional = false, + } = {} +) => { + let test; + for (const type of Array.isArray(types) ? [types] : types.split('|')) { + if (type === 'file') { + test = + value && !value.startsWith('http') + ? await fmt.is(`repo/${mod._dir}/${value}`, type, { extension }) + : false; + } else test = await fmt.is(value, type, { extension }); + if (test) break; + } + if (!test) { + if (optional && (await fmt.is(value, 'undefined'))) return true; + if (error) mod._err(error); + return false; + } + return true; +}; + +const validateEnvironments = async (mod) => { + mod.environments = mod.environments ?? registry.supportedEnvs; + const isArray = await check(mod, 'environments', mod.environments, 'array'); + if (!isArray) return false; + return mod.environments.map((tag) => + check(mod, 'environments.env', tag, registry.supportedEnvs) + ); + }, + validateTags = async (mod) => { + const isArray = await check(mod, 'tags', mod.tags, 'array'); + if (!isArray) return false; + const categoryTags = ['core', 'extension', 'theme', 'integration'], + containsCategory = mod.tags.filter((tag) => categoryTags.includes(tag)).length; + if (!containsCategory) { + mod._err( + `invalid tags (must contain at least one of 'core', 'extension', 'theme' or 'integration'): + ${JSON.stringify(mod.tags)}` + ); + return false; + } + const isTheme = mod.tags.includes('theme'), + hasThemeMode = mod.tags.includes('light') || mod.tags.includes('dark'), + isBothThemeModes = mod.tags.includes('light') && mod.tags.includes('dark'); + if (isTheme && (!hasThemeMode || isBothThemeModes)) { + mod._err( + `invalid tags (themes must be either 'light' or 'dark', not neither or both): + ${JSON.stringify(mod.tags)}` + ); + return false; + } + return mod.tags.map((tag) => check(mod, 'tags.tag', tag, 'string')); + }, + validateAuthors = async (mod) => { + const isArray = await check(mod, 'authors', mod.authors, 'array'); + if (!isArray) return false; + return mod.authors.map((author) => [ + check(mod, 'authors.author.name', author.name, 'string'), + check(mod, 'authors.author.email', author.email, 'email', { optional: true }), + check(mod, 'authors.author.homepage', author.homepage, 'url'), + check(mod, 'authors.author.avatar', author.avatar, 'url'), + ]); + }, + validateCSS = async (mod) => { + const isArray = await check(mod, 'css', mod.css, 'object'); + if (!isArray) return false; + const tests = []; + for (let dest of ['frame', 'client', 'menu']) { + if (!mod.css[dest]) continue; + let test = await check(mod, `css.${dest}`, mod.css[dest], 'array'); + if (test) { + test = mod.css[dest].map((file) => + check(mod, `css.${dest}.file`, file, 'file', { extension: '.css' }) + ); + } + tests.push(test); + } + return tests; + }, + validateJS = async (mod) => { + const isArray = await check(mod, 'js', mod.js, 'object'); + if (!isArray) return false; + const tests = []; + for (let dest of ['frame', 'client', 'menu']) { + if (!mod.js[dest]) continue; + let test = await check(mod, `js.${dest}`, mod.js[dest], 'array'); + if (test) { + test = mod.js[dest].map((file) => + check(mod, `js.${dest}.file`, file, 'file', { extension: '.mjs' }) + ); + } + tests.push(test); + } + if (mod.js.electron) { + const isArray = await check(mod, 'js.electron', mod.js.electron, 'array'); + if (isArray) { + for (const file of mod.js.electron) { + const isObject = await check(mod, 'js.electron.file', file, 'object'); + if (!isObject) { + tests.push(false); + continue; + } + tests.push([ + check(mod, 'js.electron.file.source', file.source, 'file', { + extension: '.mjs', + }), + // referencing the file within the electron app + // existence can't be validated, so only format is + check(mod, 'js.electron.file.target', file.target, 'string', { + extension: '.js', + }), + ]); + } + } else tests.push(false); + } + return tests; + }, + validateOptions = async (mod) => { + const isArray = await check(mod, 'options', mod.options, 'array'); + if (!isArray) return false; + const tests = []; + for (const option of mod.options) { + const key = 'options.option', + optTypeValid = await check(mod, `${key}.type`, option.type, registry.optionTypes); + if (!optTypeValid) { + tests.push(false); + continue; + } + option.environments = option.environments ?? registry.supportedEnvs; + tests.push([ + check(mod, `${key}.key`, option.key, 'alphanumeric'), + check(mod, `${key}.label`, option.label, 'string'), + check(mod, `${key}.tooltip`, option.tooltip, 'string', { + optional: true, + }), + check(mod, `${key}.environments`, option.environments, 'array').then((isArray) => { + if (!isArray) return false; + return option.environments.map((environment) => + check(mod, `${key}.environments.env`, environment, registry.supportedEnvs) + ); + }), + ]); + switch (option.type) { + case 'toggle': + tests.push(check(mod, `${key}.value`, option.value, 'boolean')); + break; + case 'select': { + let test = await check(mod, `${key}.values`, option.values, 'array'); + if (test) { + test = option.values.map((value) => + check(mod, `${key}.values.value`, value, 'string') + ); + } + tests.push(test); + break; + } + case 'text': + case 'hotkey': + tests.push(check(mod, `${key}.value`, option.value, 'string')); + break; + case 'number': + case 'color': + tests.push(check(mod, `${key}.value`, option.value, option.type)); + break; + case 'file': { + let test = await check(mod, `${key}.extensions`, option.extensions, 'array'); + if (test) { + test = option.extensions.map((ext) => + check(mod, `${key}.extensions.extension`, ext, 'string') + ); + } + tests.push(test); + break; + } + } + } + return tests; + }; + +/** + * internally used to validate mod.json files and provide helpful errors + * @private + * @param {object} mod - a mod's mod.json in object form + * @returns {boolean} whether or not the mod has passed validation + */ +module.exports.validate = async function (mod) { + let conditions = [ + check(mod, 'name', mod.name, 'string'), + check(mod, 'id', mod.id, 'uuid'), + check(mod, 'version', mod.version, 'semver'), + validateEnvironments(mod), + check(mod, 'description', mod.description, 'string'), + check(mod, 'preview', mod.preview, 'file|url', { optional: true }), + validateTags(mod), + validateAuthors(mod), + validateCSS(mod), + validateJS(mod), + validateOptions(mod), + ]; + do { + conditions = await Promise.all(conditions.flat(Infinity)); + } while (conditions.some((condition) => Array.isArray(condition))); + return conditions.every((passed) => passed); +}; diff --git a/api/registry.cjs b/api/registry.cjs new file mode 100644 index 0000000..47359c8 --- /dev/null +++ b/api/registry.cjs @@ -0,0 +1,159 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +/** + * interactions with the enhancer's repository of mods + * @module notion-enhancer/api/registry + */ + +const { env, fs, storage } = require('notion-enhancer/api/_.cjs'), + { validate } = require('notion-enhancer/api/registry-validation.cjs'); + +/** + * mod ids whitelisted as part of the enhancer's core, permanently enabled + * @constant + * @type {array} + */ +module.exports.core = [ + 'a6621988-551d-495a-97d8-3c568bca2e9e', + '0f0bf8b6-eae6-4273-b307-8fc43f2ee082', + '36a2ffc9-27ff-480e-84a7-c7700a7d232d', +]; + +/** + * all environments/platforms currently supported by the enhancer + * @constant + * @type {array} + */ +module.exports.supportedEnvs = ['linux', 'win32', 'darwin', 'extension']; + +/** + * all available configuration types + * @constant + * @type {array} + */ +module.exports.optionTypes = ['toggle', 'select', 'text', 'number', 'color', 'file', 'hotkey']; + +/** + * the name of the active configuration profile + * @returns {string} + */ +module.exports.profileName = async () => storage.get(['currentprofile'], 'default'); + +/** + * the root database for the current profile + * @returns {object} the get/set functions for the profile's storage + */ +module.exports.profileDB = async () => storage.db(['profiles', await profileName()]); + +let _list, + _errors = []; +/** + * list all available mods in the repo + * @param {function} filter - a function to filter out mods + * @returns {array} a validated list of mod.json objects + */ +module.exports.list = async (filter = (mod) => true) => { + if (!_list) { + _list = new Promise(async (res, rej) => { + const passed = []; + for (const dir of await fs.getJSON('repo/registry.json')) { + try { + const mod = { + ...(await fs.getJSON(`repo/${dir}/mod.json`)), + _dir: dir, + _err: (message) => _errors.push({ source: dir, message }), + }; + if (await validate(mod)) passed.push(mod); + } catch { + _errors.push({ source: dir, message: 'invalid mod.json' }); + } + } + res(passed); + }); + } + const filtered = []; + for (const mod of await _list) if (await filter(mod)) filtered.push(mod); + return filtered; +}; + +/** + * list validation errors encountered when loading the repo + * @returns {array} error objects with an error message and a source directory + */ +module.exports.errors = async () => { + await list(); + return _errors; +}; + +/** + * get a single mod from the repo + * @param {string} id - the uuid of the mod + * @returns {object} the mod's mod.json + */ +module.exports.get = async (id) => { + return (await list((mod) => mod.id === id))[0]; +}; + +/** + * checks if a mod is enabled: affected by the core whitelist, + * environment and menu configuration + * @param {string} id - the uuid of the mod + * @returns {boolean} whether or not the mod is enabled + */ +module.exports.enabled = async (id) => { + const mod = await get(id); + if (!mod.environments.includes(env.name)) return false; + if (core.includes(id)) return true; + return (await profileDB()).get(['_mods', id], false); +}; + +/** + * get a default value of a mod's option according to its mod.json + * @param {string} id - the uuid of the mod + * @param {string} key - the key of the option + * @returns {string|number|boolean|undefined} the option's default value + */ +module.exports.optionDefault = async (id, key) => { + const mod = await get(id), + opt = mod.options.find((opt) => opt.key === key); + if (!opt) return undefined; + switch (opt.type) { + case 'toggle': + case 'text': + case 'number': + case 'color': + case 'hotkey': + return opt.value; + case 'select': + return opt.values[0]; + case 'file': + return undefined; + } +}; + +/** + * access the storage partition of a mod in the current profile + * @param {string} id - the uuid of the mod + * @returns {object} an object with the wrapped get/set functions + */ +module.exports.db = async (id) => { + const db = await profileDB(); + return storage.db( + [id], + async (path, fallback = undefined) => { + if (typeof path === 'string') path = [path]; + if (path.length === 2) { + // profiles -> profile -> mod -> option + fallback = (await optionDefault(id, path[1])) ?? fallback; + } + return db.get(path, fallback); + }, + db.set + ); +}; diff --git a/api/storage.cjs b/api/storage.cjs new file mode 100644 index 0000000..4556584 --- /dev/null +++ b/api/storage.cjs @@ -0,0 +1,68 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +/** + * environment-specific data persistence + * @module notion-enhancer/api/storage + */ + +const storage = require('../env/storage.cjs'); +module.exports = {}; + +/** + * get persisted data + * @type {function} + * @param {array} path - the path of keys to the value being fetched + * @param {*} [fallback] - a default value if the path is not matched + * @returns {Promise} value ?? fallback + */ +module.exports.get = storage.get; + +/** + * persist data + * @type {function} + * @param {array} path - the path of keys to the value being set + * @param {*} value - the data to save + * @returns {Promise} resolves when data has been saved + */ +module.exports.set = storage.set; + +/** + * create a wrapper for accessing a partition of the storage + * @type {function} + * @param {array} namespace - the path of keys to prefix all storage requests with + * @param {function} [get] - the storage get function to be wrapped + * @param {function} [set] - the storage set function to be wrapped + * @returns {object} an object with the wrapped get/set functions + */ +module.exports.db = storage.db; + +/** + * add an event listener for changes in storage + * @type {function} + * @param {onStorageChangeCallback} callback - called whenever a change in + * storage is initiated from the current process + */ +module.exports.addChangeListener = storage.addChangeListener; + +/** + * remove a listener added with storage.addChangeListener + * @type {function} + * @param {onStorageChangeCallback} callback + */ +module.exports.removeChangeListener = storage.removeChangeListener; + +/** + * @callback onStorageChangeCallback + * @param {object} event + * @param {string} event.type - 'set' or 'reset' + * @param {string} event.namespace- the name of the store, e.g. a mod id + * @param {string} [event.key] - the key associated with the changed value + * @param {string} [event.new] - the new value being persisted to the store + * @param {string} [event.old] - the previous value associated with the key + */